1 简介

本章将介绍两个非常著名的算法:Double DQN 和 Dueling DQN,这两个算法的实现非常简单,只需要在 DQN 的基础上稍加修改,就能在一定程度上改善 DQN 的效果。

2 Double DQN 算法

普通的 DQN 算法会导致对 Q 值的过高估计(overestimate),回顾上一章中我们的 TD 目标为

\[r+γ\underset{a'}{\max}q_ω(s',a')

\]

其中,\(\underset{a'}{\max}q_ω(s',a')\) 由目标网络计算得出,可以将其写成如下形式:

\[q_ω(s',\underset{a'}{argmax}q_ω(s',a'))

\]

也就是说 \(max\) 操作可以分为两部分:先选取状态 \(s'\) 下的最优动作 \(a'=argmax_{a'}q_ω(s',a')\),再计算该动作的价值 \(q_ω(s',a')\)。当这两部分采用同一套 Q 网络进行计算时,每次得到的都是神经网络当前估算的所有动作价值中的最大值,虑到通过神经网络估算的值本身在某些时候会产生正向或负向的误差,这样的正向误差会逐步累积,对于状态动作空间较大的任务,会造成 DQN 无法有效工作。

顾名思义,Double DQN 就是用两套独立训练的神经网络来估算 \(max_{a'}q_*(s,a)\)。具体做法是将原有的 \(max_{a'}q_ω(s',a')\) 更改为 \(q(s',argmax_{a'}q_T(s',a'))\),即用一套神经网络的输出选取 action value 最大的动作,用另一套神经网络计算该动作的 action value(两个value的值是不一样的)。这样即使其中一套神经网络存在比较严重的过高估计问题,由于另一套网络的存在,这个动作最终使用的 q 值也不会存在很大的过高估计问题。

在上一章的 DQN 中我们已知传统的 DQN 本就存在两套神经网络——训练网络和目标网络,因此在这里我们就可以将训练网络作为Double DQN 算法的第一套网络来选取 action,将目标网络作为第二套网络来计算 q 值。因此,Double DQN 的优化目标为:

\[r+γq_{ω_T}(s',\underset{a'}{argmax}q_ω(s',a'))

\]

2.1 代码

显然,DQN 与 Double DQN 的差别只是在于计算状态 \(s'\) 下如何选取动作:

DQN 的优化目标为 \(r+γmaxq_{ω_T}(s',argmax_{a'}q_{ω_T}(s',a')\),动作的选取依靠目标网络

Double DQN 的优化目标为 \(r+γmaxq_{ω_T}(s',argmax_{a'}q_ω(s',a')\),动作的选取依靠训练网络

Double DQN 的代码实现可以直接在 DQN 的基础上进行,无须做过多修改。

本节采用的环境是倒立摆(Inverted Pendulum),该环境下有一个处于随机位置的倒立摆,如图所示。环境的状态包括倒立摆角度的正弦值 \(sinθ\),余弦值 \(cosθ\),角速度 \(ω\);动作为对倒立摆施加的力矩,详情参见下表。每一步都会根据当前倒立摆的状态的好坏给予智能体不同的奖励,该环境的奖励函数为 \(-(θ^2+0.1ω^2+0.001a^2)\),倒立摆向上保持直立不动时奖励为 0,倒立摆在其他位置时奖励为负数。环境本身没有终止状态,运行 200 步后游戏自动结束。

状态空间

标号

名称

最小值

最大值

0

\(cosθ\)

-1.0

1.0

1

\(sinθ\)

-1.0

1.0

2

ω

-8.0

8.0

动作空间

标号

动作

最小值

最大值

0

力矩

-2.0

2.0

力矩大小是在[-2,2]范围内的连续值。由于 DQN 只能处理离散动作环境,因此我们无法直接用 DQN 来处理倒立摆环境,但倒立摆环境可以比较方便地验证 DQN 对值的过高估计:倒立摆环境下 Q 值的最大估计应为 0(倒立摆向上保持直立时能选取的最大 Q 值),Q 值出现大于 0 的情况则说明出现了过高估计。为了能够应用 DQN,我们采用离散化动作的技巧。例如,下面的代码将连续的动作空间离散为 11 个动作。动作[0,1,2,...,9,10]分别代表力矩为[-2,-1.6,-1.2,...,1.2,1.6,2]。

import random

import gym

import numpy as np

import torch

import torch.nn.functional as F

import matplotlib.pyplot as plt

import rl_utils

from tqdm import tqdm

class Qnet(torch.nn.Module):

''' 只有一层隐藏层的Q网络 '''

def __init__(self, state_dim, hidden_dim, action_dim):

super(Qnet, self).__init__()

# 定义两个全连接层fc1和fc2

self.fc1 = torch.nn.Linear(state_dim, hidden_dim)

self.fc2 = torch.nn.Linear(hidden_dim, action_dim)

def forward(self, x):

x = F.relu(self.fc1(x))

return self.fc2(x)

class DQN:

''' DQN算法,包括Double DQN '''

def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,

epsilon, target_update, device, dqn_type='VanillaDQN'):

self.action_dim = action_dim

self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)

self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)

self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=learning_rate)

self.gamma = gamma

self.epsilon = epsilon

self.target_update = target_update

self.count = 0

self.dqn_type = dqn_type

self.device = device

def take_action(self, state):

if np.random.random() < self.epsilon:

action = np.random.randint(self.action_dim)

else:

state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)

action = self.q_net(state).argmax().item()

return action

def max_q_value(self, state):

state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)

return self.q_net(state).max().item()

def update(self, transition_dict):

states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)

actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(self.device)

rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device)

next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)

dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device)

q_values = self.q_net(states).gather(1, actions) # Q值

# 下个状态的最大Q值

if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别

# 返回下个状态(next_states)的所有可能动作中取得最大q值的动作索引

# .max(1)表示找出某一维度(这里1指动作维度)的最大值(包含最大q值及其动作索引)

# ps:为什么1指动作维度? Q网络输出一个形状为(batch_size,action_space)的张量,显然动作空间索引为1

# [1]表示取动作索引;.view(-1,1)表示将动作索引转成列向量形式

max_action = self.q_net(next_states).max(1)[1].view(-1, 1)

# 向Q网络输入next_states,继续返回形状为(batch_size,action_space)的张量,

# .gather(1,max_action)表示从索引为1的动作空间维度选取动作为max_action的q值

max_next_q_values = self.target_q_net(next_states).gather(1, max_action)

else: # DQN的情况

max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)

q_targets = rewards + self.gamma * max_next_q_values * (1 - dones) # TD误差目标

dqn_loss = torch.mean(F.mse_loss(q_values, q_targets)) # 均方误差损失函数

self.optimizer.zero_grad() # PyTorch中默认梯度会累积,这里需要显式将梯度置为0

dqn_loss.backward() # 反向传播更新参数

self.optimizer.step()

if self.count % self.target_update == 0:

self.target_q_net.load_state_dict(

self.q_net.state_dict()) # 更新目标网络

self.count += 1

def dis_to_con(discrete_action, env, action_dim): # 离散动作转回连续的函数

action_lowbound = env.action_space.low[0] # 连续动作的最小值

action_upbound = env.action_space.high[0] # 连续动作的最大值

return action_lowbound + (discrete_action /

(action_dim - 1)) * (action_upbound -

action_lowbound)

lr = 1e-2

num_episodes = 500

hidden_dim = 128

gamma = 0.98

epsilon = 0.01

target_update = 50

buffer_size = 5000

minimal_size = 1000

batch_size = 64

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

env_name = 'Pendulum-v1'

env = gym.make(env_name, new_step_api=True)

random.seed(0)

np.random.seed(0)

env.reset(seed=0)

torch.manual_seed(0)

replay_buffer = rl_utils.ReplayBuffer(buffer_size)

state_dim = env.observation_space.shape[0]

action_dim = 11 # 将连续动作分成11个离散动作

agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device)

return_list = []

max_q_value_list = []

max_q_value = 0

for i in range(10):

with tqdm(total=int(num_episodes / 10),

desc='Iteration %d' % i) as pbar:

for i_episode in range(int(num_episodes / 10)):

episode_return = 0

state = env.reset()

done = False

while not done:

action = agent.take_action(state)

max_q_value = agent.max_q_value(state) * 0.005 + max_q_value * 0.995 # 平滑处理

max_q_value_list.append(max_q_value) # 保存每个状态的最大Q值

action_continuous = dis_to_con(action, env, agent.action_dim)

next_state, reward, terminated, truncated, _ = env.step([action_continuous])

if terminated or truncated:

done = True

replay_buffer.add(state, action, reward, next_state, done)

state = next_state

episode_return += reward

if replay_buffer.size() > minimal_size:

b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)

transition_dict = {

'states': b_s,

'actions': b_a,

'next_states': b_ns,

'rewards': b_r,

'dones': b_d

}

agent.update(transition_dict)

return_list.append(episode_return)

if (i_episode + 1) % 10 == 0:

pbar.set_postfix({

'episode':

'%d' % (num_episodes / 10 * i + i_episode + 1),

'return':

'%.3f' % np.mean(return_list[-10:])

})

pbar.update(1)

episodes_list = list(range(len(return_list)))

mv_return = rl_utils.moving_average(return_list, 5)

plt.plot(episodes_list, mv_return)

plt.xlabel('Episodes')

plt.ylabel('Returns')

plt.title('DQN on {}'.format(env_name))

plt.show()

frames_list = list(range(len(max_q_value_list)))

plt.plot(frames_list, max_q_value_list)

plt.axhline(0, c='orange', ls='--')

plt.axhline(10, c='red', ls='--')

plt.xlabel('Frames')

plt.ylabel('Q value')

plt.title('DQN on {}'.format(env_name))

plt.show()

Iteration 0: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.51it/s, episode=50, return=-454.388]

Iteration 1: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.36it/s, episode=100, return=-196.513]

Iteration 2: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.37it/s, episode=150, return=-224.498]

Iteration 3: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.34it/s, episode=200, return=-188.761]

Iteration 4: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.35it/s, episode=250, return=-101.969]

Iteration 5: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.39it/s, episode=300, return=-256.004]

Iteration 6: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.34it/s, episode=350, return=-203.617]

Iteration 7: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:15<00:00, 3.33it/s, episode=400, return=-366.128]

Iteration 8: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:15<00:00, 3.31it/s, episode=450, return=-125.057]

Iteration 9: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:14<00:00, 3.34it/s, episode=500, return=-169.396]

根据代码运行结果我们可以发现,DQN 算法在倒立摆环境中能取得不错的回报,最后的期望回报在-200 左右,但是不少 Q 值超过了 0,该现象便是 DQN 算法中的 Q 值过高估计。我们现在来看一下 Double DQN 是否能对此问题进行改善。

非常简单,只需将网络类型修改为"DoubleDQN",即108行的代码替换为:

agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device, 'DoubleDQN')

Iteration 0: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:20<00:00, 2.39it/s, episode=50, return=-393.735]

Iteration 1: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:21<00:00, 2.33it/s, episode=100, return=-250.551]

Iteration 2: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:20<00:00, 2.42it/s, episode=150, return=-323.155]

Iteration 3: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:21<00:00, 2.29it/s, episode=200, return=-191.253]

Iteration 4: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:19<00:00, 2.52it/s, episode=250, return=-113.679]

Iteration 5: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.22it/s, episode=300, return=-389.760]

Iteration 6: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:18<00:00, 2.74it/s, episode=350, return=-215.249]

Iteration 7: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:15<00:00, 3.13it/s, episode=400, return=-254.478]

Iteration 8: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:16<00:00, 3.12it/s, episode=450, return=-139.349]

Iteration 9: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:15<00:00, 3.14it/s, episode=500, return=-289.759]

我们可以发现,与普通的 DQN 相比,Double DQN 比较少出现 Q 值大于 0 的情况,说明值过高估计的问题得到了很大缓解。

3 Dueling DQN 算法

我们将 action value 减去 state value 的结果定义为优势函数 \(A\),即 \(A(s,a)=Q(s,a)-V(s)\)。在同一状态下,所有动作的优势值之和为0,因为所有动作的 action value 的期望就是这个状态的 state value。据此,在 Dueling DQN 中,Q 网络被建模为:

\[Q_{η,α,β}(s,a)=V_{η,α}(s)+A_{η,β}(s,a)

\]

其中,\(A_{η,β}(s,a)\) 即为状态 s 下采取动作 a 的优势函数,表示采取不同动作的差异性;\(η\) 是各函数共享的网络参数,一般在神经网络中,用来提取特征的前几层;而 \(α\) 和 \(β\) 分别为 action value function 和 state value function 的参数。在这样的模型下,我们不再让神经网络直接输出 Q 值,而是训练神经网络的最后几层的两个分支,分别输出状态价值函数和优势函数,再求和得到 Q 值。Dueling DQN 的网络结构如图所示。

将状态价值函数和优势函数分别建模的好处在于:某些情境下智能体只会关注状态的价值,而并不关心不同动作导致的差异,此时将二者分开建模能够使智能体更好地处理与动作关联较小的状态。在图所示的驾驶车辆游戏中,智能体注意力集中的部位被显示为橙色,当智能体前面没有车时,车辆自身动作并没有太大差异,此时智能体更关注状态价值,而当智能体前面有车时(智能体需要超车),智能体开始关注不同动作优势值的差异。

对于 Dueling DQN 中的公式 \(Q_{η,α,β}(s,a)=V_{η,α}(s)+A_{η,β}(s,a)\),它存在对于 V 值和 A 值建模不唯一性的问题。例如,对于同样的 Q 值,如果将值加上任意大小的常数 C,再将所有 A 值减去 C,则得到的 Q 值依然不变,这就导致了训练的不稳定性。为了解决这一问题,Dueling DQN 强制最优动作的优势函数的实际输出为 0,即:

\[Q_{η,α,β}(s,a)=V_{η,α}(s)+A_{η,β}(s,a)-\underset{a'}{\max}A_{η,β}(s,a')

\]

此时,\(V(s)=\underset{a}{\max}Q(s,a)\) 可以确保 V 值建模的唯一性。在实现过程中,我们还可以用平均代替最大化操作,即:

\[Q_{η,α,β}(s,a)=V_{η,α}(s)+A_{η,β}(s,a)-\frac{1}{|\mathcal{A}|}\sum_{a'}A_{η,β}(s,a')

\]

此时 \(V(s)=\frac{1}{|\mathcal{A}|}\underset{a'}{\sum}Q(s,a')\)。在下面的代码实现中,我们将采取此种方式,虽然它不再满足贝尔曼最优方程,但实际应用时更加稳定。

有的读者可能会问:“为什么 Dueling DQN 会比 DQN 好?”部分原因在于 Dueling DQN 能更高效学习状态价值函数。每一次更新时,函数 V 都会被更新,这也会影响到其他动作的 Q 值。而传统的 DQN 只会更新某个动作的 Q 值,其他动作的 Q 值就不会更新。因此,Dueling DQN 能够更加频繁、准确地学习状态价值函数。

3.1 代码

import random

import gym

import numpy as np

import torch

import torch.nn.functional as F

import matplotlib.pyplot as plt

import rl_utils

from tqdm import tqdm

class Qnet(torch.nn.Module):

''' 只有一层隐藏层的Q网络 '''

def __init__(self, state_dim, hidden_dim, action_dim):

super(Qnet, self).__init__()

self.fc1 = torch.nn.Linear(state_dim, hidden_dim)

self.fc2 = torch.nn.Linear(hidden_dim, action_dim)

def forward(self, x):

x = F.relu(self.fc1(x))

return self.fc2(x)

class VAnet(torch.nn.Module):

''' 只有一层隐藏层的A网络和V网络 '''

def __init__(self, state_dim, hidden_dim, action_dim):

super(VAnet, self).__init__()

# 定义一个全连接层,将输入状态映射到隐藏层。该层是共享的,用于后续A网络和V网络的计算

self.fc1 = torch.nn.Linear(state_dim, hidden_dim) # 共享网络部分

# 将隐藏层映射到动作空间,输出为优势函数Advantage

self.fc_A = torch.nn.Linear(hidden_dim, action_dim)

# 从隐藏层映射到一个单独的输出,输出为状态价值函数V

self.fc_V = torch.nn.Linear(hidden_dim, 1)

# 前向传播,x为输入状态,分别输出优势函数(A)和状态价值函数(V)

def forward(self, x):

A = self.fc_A(F.relu(self.fc1(x)))

V = self.fc_V(F.relu(self.fc1(x)))

# A.mean(1)表示从维度1(动作维度)计算A的均值

Q = V + A - A.mean(1).view(-1, 1) # Q值由V值和A值计算得到

return Q

class DQN:

''' DQN算法,包括Double DQN和Dueling DQN '''

def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,

epsilon, target_update, device, dqn_type='VanillaDQN'):

self.action_dim = action_dim

if dqn_type == 'DuelingDQN': # Dueling DQN采取不一样的网络框架

self.q_net = VAnet(state_dim, hidden_dim, self.action_dim).to(device)

self.target_q_net = VAnet(state_dim, hidden_dim, self.action_dim).to(device)

else:

self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)

self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)

self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=learning_rate)

self.gamma = gamma

self.epsilon = epsilon

self.target_update = target_update

self.count = 0

self.dqn_type = dqn_type

self.device = device

def take_action(self, state):

if np.random.random() < self.epsilon:

action = np.random.randint(self.action_dim)

else:

state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)

action = self.q_net(state).argmax().item()

return action

def max_q_value(self, state):

state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)

return self.q_net(state).max().item()

def update(self, transition_dict):

states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)

actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(self.device)

rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device)

next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)

dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device)

q_values = self.q_net(states).gather(1, actions)

if self.dqn_type == 'DoubleDQN':

max_action = self.q_net(next_states).max(1)[1].view(-1, 1)

max_next_q_values = self.target_q_net(next_states).gather(1, max_action)

else:

max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)

q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)

dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))

self.optimizer.zero_grad()

dqn_loss.backward()

self.optimizer.step()

if self.count % self.target_update == 0:

self.target_q_net.load_state_dict(self.q_net.state_dict())

self.count += 1

lr = 1e-2

num_episodes = 500

hidden_dim = 128

gamma = 0.98

epsilon = 0.01

target_update = 50

buffer_size = 5000

minimal_size = 1000

batch_size = 64

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

env_name = 'Pendulum-v1'

env = gym.make(env_name, new_step_api=True)

random.seed(0)

np.random.seed(0)

env.reset(seed=0)

torch.manual_seed(0)

replay_buffer = rl_utils.ReplayBuffer(buffer_size)

state_dim = env.observation_space.shape[0]

action_dim = 11 # 将连续动作分成11个离散动作

agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device, 'DuelingDQN')

def dis_to_con(discrete_action, env, action_dim): # 离散动作转回连续的函数

action_lowbound = env.action_space.low[0] # 连续动作的最小值

action_upbound = env.action_space.high[0] # 连续动作的最大值

return action_lowbound + (discrete_action /

(action_dim - 1)) * (action_upbound -

action_lowbound)

return_list = []

max_q_value_list = []

max_q_value = 0

for i in range(10):

with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:

for i_episode in range(int(num_episodes / 10)):

episode_return = 0

state = env.reset()

done = False

while not done:

action = agent.take_action(state)

max_q_value = agent.max_q_value(state) * 0.005 + max_q_value * 0.995 # 平滑处理

max_q_value_list.append(max_q_value) # 保存每个状态的最大Q值

action_continuous = dis_to_con(action, env, agent.action_dim)

next_state, reward, terminated, truncated, _ = env.step([action_continuous])

if terminated or truncated:

done = True

replay_buffer.add(state, action, reward, next_state, done)

state = next_state

episode_return += reward

if replay_buffer.size() > minimal_size:

b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)

transition_dict = {

'states': b_s,

'actions': b_a,

'next_states': b_ns,

'rewards': b_r,

'dones': b_d

}

agent.update(transition_dict)

return_list.append(episode_return)

if (i_episode + 1) % 10 == 0:

pbar.set_postfix({

'episode':

'%d' % (num_episodes / 10 * i + i_episode + 1),

'return':

'%.3f' % np.mean(return_list[-10:])

})

pbar.update(1)

episodes_list = list(range(len(return_list)))

mv_return = rl_utils.moving_average(return_list, 5)

plt.plot(episodes_list, mv_return)

plt.xlabel('Episodes')

plt.ylabel('Returns')

plt.title('Dueling DQN on {}'.format(env_name))

plt.show()

frames_list = list(range(len(max_q_value_list)))

plt.plot(frames_list, max_q_value_list)

plt.axhline(0, c='orange', ls='--')

plt.axhline(10, c='red', ls='--')

plt.xlabel('Frames')

plt.ylabel('Q value')

plt.title('Dueling DQN on {}'.format(env_name))

plt.show()

Iteration 0: 100%|███████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:21<00:00, 2.34it/s, episode=50, return=-352.655]

Iteration 1: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.22it/s, episode=100, return=-380.827]

Iteration 2: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.24it/s, episode=150, return=-225.570]

Iteration 3: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.23it/s, episode=200, return=-256.513]

Iteration 4: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.24it/s, episode=250, return=-120.772]

Iteration 5: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.23it/s, episode=300, return=-362.788]

Iteration 6: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.22it/s, episode=350, return=-167.957]

Iteration 7: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.22it/s, episode=400, return=-197.828]

Iteration 8: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.21it/s, episode=450, return=-173.704]

Iteration 9: 100%|██████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:22<00:00, 2.21it/s, episode=500, return=-236.537]

根据代码运行结果我们可以发现,相比于传统的 DQN,Dueling DQN 在多个动作选择下的学习更加稳定,得到的回报最大值也更大。由 Dueling DQN 的原理可知,随着动作空间的增大,Dueling DQN 相比于 DQN 的优势更为明显。之前我们在环境中设置的离散动作数为 11,我们可以增加离散动作数(例如 15、25 等),继续进行对比实验。

4 总结

在传统的 DQN 基础上,有两种非常容易实现的变式——Double DQN 和 Dueling DQN,Double DQN 解决了 DQN 中对 Q 值的过高估计,而 Dueling DQN 能够很好地学习到不同动作的差异性,在动作空间较大的环境下非常有效。从 Double DQN 和 Dueling DQN 的方法原理中,我们也能感受到深度强化学习的研究是在关注深度学习和强化学习有效结合:一是在深度学习的模块的基础上,强化学习方法如何更加有效地工作,并避免深度模型学习行为带来的一些问题,例如使用 Double DQN 解决 Q 值过高估计的问题;二是在强化学习的场景下,深度学习模型如何有效学习到有用的模式,例如设计 Dueling DQN 网络架构来高效地学习状态价值函数以及动作优势函数。

参考资料

https://hrl.boyuai.com/chapter/2/dqn改进算法/