改进DQN算法 Double DQN算法 DuelingDQN算法

第 8 章 DQN 改进算法

8.1 简介

DQN算法敲开了深度强化学习的大门,但是作为先驱性的工作,其本身存在着一些问题以及一些可以改进的地方。于是,在DQN之后,学术界涌现了非常多的改进算法。本章将介绍其中两个非常著名的算法:Double DQN和Dueling DQN,这两个算法的实现非常简单,只需要在DQN的基础上稍加修改,它们能在一定程度上改善DQN的效果。

8.2 Double DQN

普通的DQN算法通常会导致对Q值的过高估计(overestimate)。传统DQN优化的TD误差目标为

r + γ m a x a ′ Q ω − ( s ′ , a ′ ) r+\gamma \underset {a'} {max}Q_{\omega -}\left( s',a' \right) r+γamaxQω(s,a)

其中 m a x a ′ Q ω − ( s ′ , a ′ ) max_{a'} Q_{\omega ^-}(s',a') maxaQω(s,a)由目标网络(参数为 ω − \omega^- ω)计算得出,我们还可以将其写成如下形式:

Q ω − ( s ′ , a r g m a x a ′ Q ω − ( s ′ , a ′ ) ) Q_{\omega ^-}(s',arg \underset {a'}{max}Q_{\omega^-}(s',a')) Qω(s,argamaxQω(s,a))

换句话说,max操作实际可以被拆解为两部分:首先选取状态s‘下的最优动作 a ∗ = a r g m a x a ′ Q ω − ( s ′ , a ′ ) a^*=arg max_{a'}Q_{\omega^-}(s',a') a=argmaxaQω(s,a),接着计算该做动作对应的价值 Q ω − ( s ′ , a ∗ ) Q_{\omega^-}(s',a^*) Qω(s,a)。当这两部分采用同一套Q网络进行计算时,每次得到的都是神经网络当前估算的所有动作价值中的最大值。考虑到通过神经网络估算Q值本身在某些时候会产生正向或负向的误差,在DQN的更新方式下神经网络会将正向误差累积。例如,我们考虑一个特殊情形:在状态s‘下所有动作的Q值均为0,即 Q ( s ′ , a i ) = 0 , ∀ i Q(s',a_i)=0,\forall i Q(s,ai)=0,i,此时正确的更新目标应该是 r + 0 = r r+0=r r+0=r,但是由于神经网络拟合的误差通常会出现某些动作的估算有正误差的情况,即存在某个动作a’有 Q ( s ′ , a ′ ) > 0 Q(s',a')>0 Q(s,a)>0,此时我们的更新目标出现了过高估计, r + γ m a x Q > r + 0 r+\gamma max Q>r+0 r+γmaxQ>r+0。因此,当我们用DQN的更新公式进行更新时, Q ( s , a ) Q(s,a) Q(s,a)也就会被过高估计了。同理,我们拿这个 Q ( s , a ) Q(s,a) Q(s,a)来作为更新目标来更新上一步的Q值时,同样会被过高估计,这样的误差将会累积。对于动作空间较大的任务,DQN中的过高估计问题非常严重,造成DQN无法有效工作的后果。

为了解决这一问题,Doube DQN算法提出利用两个独立训练的神经网络估算 m a x a ′ Q ∗ ( s ′ , a ′ ) max_{a'}Q_*(s',a') maxaQ(s,a)。具体做法是将原有的 m a x a ′ Q ω − ( s ′ , a ′ ) max_{a'}Q_{\omega^-}(s',a') maxaQω(s,a)更改为 Q ω − ( s ′ , a r g m a x a ′ Q ω ( s ′ , a ′ ) ) Q_{\omega^-}(s',arg max _{a'}Q_\omega(s',a')) Qω(s,argmaxaQω(s,a)),即利用一套神经网络 Q ω Q_\omega Qω的输出选取价值最大的动作,但在使用该动作的价值时,用另一套神经网络 Q ω − Q^-_{\omega} Qω计算该动作的价值。这样,即使其中一套神经网络的某个动作存在比较严重的过高估计问题,由于另一套神经网络的存在,这个动作最终使用的Q值不会存在很大的过高估计的问题。

在传统的DQN算法中,本来就存在两套Q函数的神经网络——目标网络和训练网络,只不过 m a x a ′ Q ω − ( s ′ , a ′ ) max_{a'}Q_{\omega^-}(s',a') maxaQω(s,a)的计算只用到了其中的目标网络,那么我们恰好可以直接将训练网络作为Double DQN算法中的第一套神经网络来选取动作,将目标网络作为第二套神经网络计算Q值,这便是Doube DQN的主要思想。由于在DQN算法中将训练网络的参数记为 ω \omega ω,将目标网络的参数记为 ω − \omega^- ω,这与本节中Double DQN的两套神经网络的参数是统一的,因此,我们可以直接写出如下Double DQN 的优化目标:

r + γ Q ω − ( s ′ , a r g m a x a ′ Q ω ( s ′ , a ′ ) ) r+\gamma Q_{\omega ^-}(s',\underset{a'}{arg max}Q_\omega(s',a')) r+γQω(s,aargmaxQω(s,a))

8.3 Double DQN 代码实战

显然,DQN与Double DQN的差别只是在于计算状态s‘下Q值时如何选取动作:

  • DQN的优化目标可以写为 r + γ Q ω − ( s ′ , a r g m a x a ′ Q ω − ( s ′ , a ′ ) ) r+\gamma Q_{\omega^-}(s',arg max_{a'}Q_{\omega^-}(s',a')) r+γQω(s,argmaxaQω(s,a)),动作的选取依靠目标网络 Q ω − Q_{\omega^-} Qω
  • Double DQN 的优化目标为 r + γ Q ω − ( s ′ , a r g m a x a ′ Q ω ( s ′ , a ′ ) ) r+\gamma Q_{\omega^-}(s',arg max_{a'}Q_{\omega}(s',a')) r+γQω(s,argmaxaQω(s,a)),动作的选取依靠训练网络 Q ω Q_\omega Qω

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

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

在这里插入图片描述

​ (其实这是一个动图)

​ 表8-1 Pendulum环境的状态空间

标号名称最小值最大值
0 c o s θ cos\theta cosθ-1.01.0
1 s i n θ sin\theta sinθ-1.01.0
2 θ ˙ \dot\theta θ˙-8.08.0

​ 表8-2Pendulum环境的动作空间

标号动作最小值最大值
0力矩-2.02.0

力矩大小是在[-2,2]范围内的连续值。由于DQN只能处理离散动作环境,因此我们无法直接用DQN来处理倒立摆环境,但是倒立摆环境可以比较方便地验证DQN对Q值的过高估计:**倒立摆环境下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__()
        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([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([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的区别
            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: # 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


lr = 1e-2
num_episodes = 200
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-v0'
env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = 11  # 将连续动作分成11个离散动作


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)


def train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size):
    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, done, _ = env.step([action_continuous])
                    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)
    return return_list, max_q_value_list

random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device)
return_list, max_q_value_list = train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size)

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()


random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
            target_update, device, 'DoubleDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
                                          replay_buffer, minimal_size,
                                          batch_size)

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('Double 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('Double DQN on {}'.format(env_name))
plt.show()

区别:

Double DQN和DQN算法的区别就是以下代码 在max_next_q_values的取得方面的不同

 if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别
            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: # DQN的情况
            max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)

与上一章节的不同在于环境的不同,上一章节动作是离散的,本章动作是连续的,为了区分DQN算法和Double DQN算法的区别,本章使用的是同一个环境,但是DQN算法又只能使用离散的动作,所以将动作离散化,之后再变连续。

还有一个不同是将训练函数进行了一个函数的封装。

DQN算法的运行结果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

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

Double DQN 算法的运行结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

8.4 Dueling DQN

Dueling DQN算法是DQN另一种改进的算法,它在DQN的基础上只进行了微小的改动,但却能大幅提升DQN的表现。在强化学习中,我们将状态价值函数Q减去状态价值函数V的结果定义为优势函数A,即 A ( s , a ) = Q ( s , a ) − V ( s ) A(s,a)=Q(s,a)-V(s) A(s,a)=Q(s,a)V(s)。在同一个状态下,所有动作的优势值之和为0,因为所有动作的动作价值的期望就是这个状态的状态价值。据此,在Dueling DQN中,Q网络被建模为:

Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) Q_{\eta ,\alpha ,\beta}\left( s,a \right) =V_{\eta ,\alpha}\left( s \right) +A_{\eta ,\beta}\left( s,a \right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)

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

在这里插入图片描述

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

在这里插入图片描述

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

Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − m a x a ′ A η , β ( s , a ′ ) Q_{\eta ,\alpha ,\beta}\left( s,a \right) =V_{\eta ,\alpha}\left( s \right) +A_{\eta ,\beta}\left( s,a \right)-\underset {a'}{max}A_{\eta,\beta}(s,a') Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)amaxAη,β(s,a)

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

Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − 1 ∣ A ∣ Σ a ′ A η , β ( s , a ′ ) Q_{\eta ,\alpha ,\beta}\left( s,a \right) =V_{\eta ,\alpha}\left( s \right) +A_{\eta ,\beta}\left( s,a \right)- \frac{1}{|A|}\underset{a'}{\varSigma}A_{\eta ,\beta}\left( s,a'\right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)A1aΣAη,β(s,a)

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

为什么Dueling DQN会比DQN好?

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

8.5 Dueling DQN 代码实践

Dueling DQN 与DQN相比的差异只是在网络结构上,大部分代码依然可以继续沿用。我们定义状态价值函数和优势函数的复合神经网络VAnet。

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 VAnet(torch.nn.Module):
    ''' 只有一层隐藏层的A网络和V网络 '''
    def __init__(self, state_dim, hidden_dim, action_dim):
        super(VAnet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)  # 共享网络部分
        self.fc_A = torch.nn.Linear(hidden_dim, action_dim)
        self.fc_V = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x):
        A = self.fc_A(F.relu(self.fc1(x)))
        V = self.fc_V(F.relu(self.fc1(x)))
        Q = V + A - A.mean(1).view(-1, 1)  # Q值由V值和A值计算得到
        return Q

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 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([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([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


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)

def train_DQN(agent, env, num_episodes, replay_buffer, minimal_size,
              batch_size):
    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, done, _ = env.step([action_continuous])
                    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)
    return return_list, max_q_value_list



lr = 1e-2
num_episodes = 200
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-v0'
env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = 11  # 将连续动作分成11个离散动作


random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
            target_update, device, 'DuelingDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
                                          replay_buffer, minimal_size,
                                          batch_size)

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()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

8.6 总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值