论文下载地址
论文介绍
该论文提出了一个基于卷积神经网络的深度强化学习模型,该模型是Q-learning算法的变体,该模型的输入数据是Atari游戏的原始原素,输出是预测未来的动作价值奖励(Q-value),其神经网络结构可以简单表示如图1所示:
[batch_size, width, height, channel]
(不同深度学习框架中的channel的位置可能不一样,在tf中输入的数据是将channel设置 在最后一个维度,我们暂以此描述)形式的多维数据,输入数据经过卷积神经网络得到特征向量 [batch_size, hidden_dim]
,将特征向量作为全连接神经网络的输入,得到输出的动作价值函数 Q ( s , a ) Q(s,a) Q(s,a) [batch_size, action_dim]
,使用完全贪婪算法或者epsilon贪婪算法基于 Q ( s , a ) Q(s,a) Q(s,a)的值选择对应的动作。 前人相关工作
训练流程
基于gym的DQN2013代码实现:
requirement:
code(代码主要参考书籍【动手学强化学习】(张伟楠,沈键,俞勇),京东购买链接 | 当当购买链接)
import gym
import random
import collections
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tqdm import tqdm
class ReplayBuffer:
"""经验回放池"""
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity) # 队列,先进先出
def add(self, state, action, reward, next_state, done): # 将数据加入buffer
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size): # 从buffer中采样数据,数量为batch_size
transitions = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
def size(self): # 目前buffer中数据的数量
return len(self.buffer)
class Q_net(tf.keras.Model):
"""只有一层隐藏层的Q网络"""
def __init__(self, hidden_dim, action_dim):
super(Q_net, self).__init__()
self.fc1 = tf.keras.layers.Dense(hidden_dim, activation=tf.keras.activations.relu)
self.fc2 = tf.keras.layers.Dense(action_dim)
def call(self, x):
return self.fc2(self.fc1(x))
class DQN:
def __init__(self, hidden_dim, action_dim, learning_rate, gamma, epsilon):
self.action_dim = action_dim
self.q_net = Q_net(hidden_dim, self.action_dim) # Q网络
# 使用Adam优化器
self.optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
# 折扣因子
self.gamma = gamma
# epsilon-贪婪策略,这里没有对epsilon进行退火处理
self.epsilon = epsilon
def take_action(self, state):
# epsilon-贪婪策略采取动作
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = tf.cast(tf.expand_dims(state, axis=0), dtype=tf.float32)
action = tf.argmax(self.q_net.call(state), axis=1).numpy()[0]
return action
def update(self, transition_dict):
states = tf.cast(transition_dict['states'], dtype=tf.float32)
actions = tf.reshape(transition_dict['actions'], shape=(-1))
rewards = tf.reshape(tf.cast(transition_dict['rewards'], dtype=tf.float32), shape=(-1, 1))
next_states = tf.cast(transition_dict['next_states'], dtype=tf.float32)
dones = tf.reshape(tf.cast(transition_dict['dones'], dtype=tf.float32), shape=(-1, 1))
# 梯度优化
with tf.GradientTape() as tape:
# 获取当前状态动作对应的Q值
q_values = tf.gather(self.q_net.call(states), actions, batch_dims=1)
# 下个状态的最大Q值,下个状态不进行梯度优化,使用tf.stop_gradient
max_next_q_values = tf.stop_gradient(tf.reduce_max(self.q_net.call(next_states), axis=1, keepdims=True))
# TD误差目标
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)
# 均方差损失函数
loss_func = tf.keras.losses.MeanSquaredError()
# 计算均方误差损失函数
dqn_loss = tf.reduce_mean(loss_func(q_values, q_targets)) # 计算均方误差损失函数
grads = tape.gradient(dqn_loss, self.q_net.trainable_variables)
self.optimizer.apply_gradients(zip(grads, self.q_net.trainable_variables))
lr = 2e-3
num_episodes = 500
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
buffer_size = 10000
minimal_size = 500
batch_size = 64
env_name = 'CartPole-v0'
env = gym.make(env_name)
random.seed(0)
np.random.seed(0)
env.seed(0)
tf.random.set_seed(0)
replay_buffer = ReplayBuffer(buffer_size)
state_dim = env.observation_space.shape[0] # 输入状态维数
action_dim = env.action_space.n # 动作维数
agent = DQN(hidden_dim, action_dim, lr, gamma, epsilon)
return_list = []
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)
next_state, reward, done, _ = env.step(action)
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
episode_return += reward
# 当buffer数据的数量超过一定值后,才进行Q网络训练
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)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN on {}'.format(env_name))
plt.show()
DQN2013的高估问题
Q-learning算法有一个缺陷,用Q-learning训练的DQN会高估真实的价值,而且高估通常是非均匀的,这个缺陷导致DQN的表现很差。高估问题不是DQN模型的问题,是Q-learning的问题。
Q-learning产生高估的原因有两个:
(1)自举导致偏差的传播;
(2)动作价值函数最大化导致目标动作价值高估真实价值;
下面简单分析一下导致高估的两个原因(具体内容大部分使用了王树森 老师的 【深度强化学习】书籍(京东购买链接|当当购买链接),无意冒犯,侵权请联系我删除,抱歉):
自举导致偏差的传播
在强化学习中,自举意思是“用一个估算去更新同类的估算”,类似于“自己把自己给举起来” 。我们回顾一下训练DQN使用的Q-learning算法,研究其中存在的自举。
算法每次从经验回放池中抽取四元组 ( s j , a j , r j , s j + 1 ) (s_j, a_j, r_j, s_{j+1}) (sj,aj,rj,sj+1),然后执行下面步骤更新DQN的参数:
(1)计算目标动作值函数,如公式(11)所示: y ^ j = r j + γ ⋅ max a j + 1 ∈ A Q ( s j + 1 , a j + 1 ; w now ) ⏟ D Q N 自己做出的估计 (11) \widehat{y}_j=r_j+\gamma \cdot \underbrace{\max _{a_{j+1} \in \mathcal{A}} Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}_{\text {now }}\right)}_{\mathrm{DQN} \text { 自己做出的估计 }} \tag{11} y j=rj+γ⋅DQN 自己做出的估计 aj+1∈AmaxQ(sj+1,aj+1;wnow )(11)(2)计算损失函数,如公式(12)所示: L ( w ) = 1 2 [ Q ( s j , a j ; w ) − y ^ j ⏟ 让 DQN 拟合 y ^ j ] 2 (12) L(\boldsymbol{w})=\frac{1}{2}[\underbrace{Q\left(s_j, a_j ; \boldsymbol{w}\right)-\widehat{y}_j}_{\text {让 DQN 拟合 } \widehat{y}_j}]^2 \tag{12} L(w)=21[让 DQN 拟合 y j Q(sj,aj;w)−y j]2(12)(3)进行梯度下降更新DQN参数,如公式(13)所示: w new ← w now − α ⋅ ∇ w L ( w now ) (13) \boldsymbol{w}_{\text {new }} \leftarrow \boldsymbol{w}_{\text {now }}-\alpha \cdot \nabla_{\boldsymbol{w}} L\left(\boldsymbol{w}_{\text {now }}\right) \tag{13} wnew ←wnow −α⋅∇wL(wnow )(13)第一步计算目标动作值 y ^ j \hat{y}_j y^j是基于DQN自己做出的估计,第二步让DQN去拟合 y ^ j \hat{y}_j y^j,这意味着用DQN自己做出的估计去更新DQN自身,这属于自举。
DQN中 Q ( s , a , w ) Q(s,a,\boldsymbol{w}) Q(s,a,w)是对于最优动作价值函数 Q ⋆ ( s , a ) Q_{\star}(s, a) Q⋆(s,a)的近似,最理想的情况下 Q ( s , a ; w ) = Q ⋆ ( s , a ) , ∀ s , a Q(s, a ; \boldsymbol{w})=Q_{\star}(s, a), \forall s, a Q(s,a;w)=Q⋆(s,a),∀s,a,假如碰巧 Q ( s j + 1 , a j + 1 ; w ) Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}\right) Q(sj+1,aj+1;w)低估(或高估)真实价值 Q ⋆ ( s j + 1 , a j + 1 ) Q_{\star}\left(s_{j+1}, a_{j+1}\right) Q⋆(sj+1,aj+1),则会发生下面的情况: Q ( s j + 1 , a j + 1 ; w ) 低估(或高估) Q ⋆ ( s j + 1 , a j + 1 ) ⟹ y ^ j 低估(或高估) Q ⋆ ( s j , a j ) ⟹ Q ( s j , a j ; w ) 低估(或高估) Q ⋆ ( s j , a j ) . \begin{aligned} & \space \space \space \space \space \space \space \space \space Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}\right) \text { 低估(或高估) } \space \space \space \space Q_{\star}\left(s_{j+1}, a_{j+1}\right) \\ & \Longrightarrow \quad \space \space \space \space \space \space \space \space \space \widehat{y}_j \space \space \space \space \space \space \space \space \space \quad \text { 低估(或高估) } \space \space \space \space Q_{\star}\left(s_j, a_j\right) \\ & \Longrightarrow \space \space \space \space Q\left(s_j, a_j ; \boldsymbol{w}\right) \quad \text { 低估(或高估) } \quad Q_{\star}\left(s_j, a_j\right) \text {. } \\ & \end{aligned} Q(sj+1,aj+1;w) 低估(或高估) Q⋆(sj+1,aj+1)⟹ y j 低估(或高估) Q⋆(sj,aj)⟹ Q(sj,aj;w) 低估(或高估) Q⋆(sj,aj).
因为 y ^ j = r j + γ ⋅ max a j + 1 ∈ A Q ( s j + 1 , a j + 1 ; w now ) \widehat{y}_j=r_j+\gamma \cdot \max _{a_{j+1} \in \mathcal{A}} Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}_{\text {now }}\right) y j=rj+γ⋅maxaj+1∈AQ(sj+1,aj+1;wnow ),所以如果 Q ( s j + 1 , a j + 1 ; w ) Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}\right) Q(sj+1,aj+1;w)是对真实价值的高估或者低估会传递给 y ^ j \hat{y}_j y^j;因为 Q ( s j , a j ; w ) Q\left(s_j, a_j ; \boldsymbol{w}\right) Q(sj,aj;w)会通过均方差损失函数进梯度求解拟合 y ^ j \hat{y}_j y^j,所以 y ^ j \hat{y}_j y^j对真实价值的高估或者低估会传递给 Q ( s j , a j ; w ) Q\left(s_j, a_j ; \boldsymbol{w}\right) Q(sj,aj;w)。
如果 Q ( s j + 1 , a j + 1 ; w ) Q\left(s_{j+1}, a_{j+1} ; \boldsymbol{w}\right) Q(sj+1,aj+1;w)是对真实价值 Q ⋆ ( s j + 1 , a j + 1 ) Q_{\star}\left(s_{j+1}, a_{j+1}\right) Q⋆(sj+1,aj+1)的低估(或高估),就会导致 Q ( s j , a j ; w ) Q\left(s_{j}, a_{j} ; \boldsymbol{w}\right) Q(sj,aj;w)低估或高估价值 Q ⋆ ( s j , a j ) Q_{\star}\left(s_{j}, a_{j}\right) Q⋆(sj,aj)。也就是说低估(或高估)从 ( s j + 1 , a j + 1 ) (s_{j+1},a_{j+1}) (sj+1,aj+1)传播到 ( s j , a j ) (s_j,a_j) (sj,aj),让更多的价值被低估(或高估)。
最大化导致高估
首先用数学解释为什么最大化会导致高估。设 x 1 , ⋯ , x d x_1, \cdots, x_d x1,⋯,xd为任意 d d d个实数,往 x 1 , ⋯ , x d x_1, \cdots, x_d x1,⋯,xd中加入任意均值为零的随机噪声后得到随机变量 Z 1 , ⋯ , Z d Z_1, \cdots, Z_d Z1,⋯,Zd,随机性来源于随机噪声。
我们可以证明,均值为零的随机噪声不会影响均值,如公式(14)所示: E [ mean ( Z 1 , ⋯ , Z d ) ] = mean ( x 1 , ⋯ , x d ) (14) \mathbb{E}\left[\operatorname{mean}\left(Z_1, \cdots, Z_d\right)\right]=\operatorname{mean}\left(x_1, \cdots, x_d\right) \tag{14} E[mean(Z1,⋯,Zd)]=mean(x1,⋯,xd)(14)证明如下:
下面证明,均值为零的随机噪声会影响最大值,如公式(17)所示: E [ max ( Z 1 , ⋯ , Z d ) ] ≥ max ( x 1 , ⋯ , x d ) (17) \mathbb{E}\left[\max \left(Z_1, \cdots, Z_d\right)\right] \geq \max \left(x_1, \cdots, x_d\right) \tag{17} E[max(Z1,⋯,Zd)]≥max(x1,⋯,xd)(17)证明如下:
我们使用jensen不等式来证明,jensen不等式是一个关于凸函数的定理,它表明对于一个凸函数 f f f和随机变量 X X X,如公式(18)所示: E [ f ( X ) ] ≥ f ( E [ X ] ) (18) \mathbb{E}\left[ f(X) \right] \ge f(\mathbb{E}\left[ X \right]) \tag{18} E[f(X)]≥f(E[X])(18)
对于 E [ max ( Z 1 , ⋯ , Z d ) ] ≥ max ( x 1 , ⋯ , x d ) \mathbb{E}\left[\max \left(Z_1, \cdots, Z_d\right)\right] \geq \max \left(x_1, \cdots, x_d\right) E[max(Z1,⋯,Zd)]≥max(x1,⋯,xd),我们只要证明 max ( ) \max() max()是凸函数,就能通过公式(19)证明: E [ max ( Z 1 , ⋯ , Z d ) ] ≥ max ( E [ Z 1 , ⋯ , Z d ] ) = max ( E [ x 1 + ϵ 1 ] , ⋯ , E [ x d + ϵ d ] ) = max ( x 1 , ⋯ , x d ) \begin{align} \mathbb{E}\left[\max \left(Z_1, \cdots, Z_d\right)\right]\ \geq \max (\mathbb{E} \left[Z_1, \cdots, Z_d\right]) \tag{19} &=\max (\mathbb{E} \left[x_1 + \epsilon_1\right], \cdots, \mathbb{E} \left[x_d+\epsilon_d\right]) \\\notag &= \max \left(x_1, \cdots, x_d\right) \end{align} E[max(Z1,⋯,Zd)] ≥max(E[Z1,⋯,Zd])=max(E[x1+ϵ1],⋯,E[xd+ϵd])=max(x1,⋯,xd)(19)
为了证明 max ( ) \max() max()是凸函数,需要证明对于任意两个向量 X X X和 Y Y Y以及任意标量 λ ∈ [ 0 , 1 ] \lambda\in[0,1] λ∈[0,1],都满足以下不等式(20): max ( λ X + ( 1 − λ ) Y ) ≤ λ max ( X ) + ( 1 − λ ) max ( Y ) (20) \max(\lambda X+(1- \lambda)Y) \leq \lambda\max(X)+(1-\lambda)\max(Y) \tag{20} max(λX+(1−λ)Y)≤λmax(X)+(1−λ)max(Y)(20)
上面不等式中左侧是向量 λ X + ( 1 − λ ) X \lambda X+(1-\lambda)X λX+(1−λ)X中的最大值,右边是 λ \lambda λ倍的向量 X X X中的最大值加上 ( 1 − λ ) (1-\lambda) (1−λ)倍的向量 Y Y Y中的最大值,我们通过以下步骤来证明这个不等式:
对于向量 λ X + ( 1 − λ ) Y \lambda X + (1-\lambda )Y λX+(1−λ)Y中的任意元素,有公式(21): λ x i + ( 1 − λ ) y i ≤ λ max ( X ) + ( 1 − λ ) max ( Y ) (21) \lambda x_i + (1-\lambda)y_i\leq\lambda \max(X)+(1-\lambda)\max(Y) \tag{21} λxi+(1−λ)yi≤λmax(X)+(1−λ)max(Y)(21)这是因为 x i ≤ max ( X ) x_i\leq \max(X) xi≤max(X)和 y i ≤ max ( Y ) y_i\leq \max(Y) yi≤max(Y);
因此,向量 λ X + ( 1 − λ ) Y \lambda X + (1-\lambda )Y λX+(1−λ)Y中的最大值也满足公式(22): max ( λ X + ( 1 − λ ) Y ) ≤ λ max ( X ) + ( 1 − λ ) max ( Y ) (22) \max(\lambda X + (1-\lambda )Y) \leq \lambda \max(X)+(1-\lambda)\max(Y) \tag{22} max(λX+(1−λ)Y)≤λmax(X)+(1−λ)max(Y)(22)由此我们证明了 max ( ) \max() max()是凸函数,从而可以知道均值为零的随机噪声会影响最大值(公式(17))。
公式(17)意味着先加入均值为零的噪声,然后求最大值,会产生高估。在DQN中,假设对于所有动作 a ∈ A a \in \mathcal{A} a∈A和状态 s ∈ S s \in \mathcal{S} s∈S,DQN的输出是真实价值 Q ⋆ ( s , a ) Q_{\star}(s,a) Q⋆(s,a)加上均值为零的随机噪声 ϵ \epsilon ϵ,如公式(23)所示: Q ( s , a ; w ) = Q ⋆ ( s , a ) + ϵ (23) Q(s, a ; \boldsymbol{w})=Q_{\star}(s, a)+\epsilon \tag{23} Q(s,a;w)=Q⋆(s,a)+ϵ(23)显然 Q ( s , a ; w ) Q(s, a ; \boldsymbol{w}) Q(s,a;w)是对真实价值 Q ⋆ ( s , a ) Q_{\star}(s, a) Q⋆(s,a)的无偏估计,然而根据公式(17),我们可以知道以下不等式(24): E ϵ [ max a ∈ A Q ( s , a ; w ) ] ≥ max a ∈ A Q ⋆ ( s , a ) (24) \mathbb{E}_\epsilon\left[\max _{a \in \mathcal{A}} Q(s, a ; \boldsymbol{w})\right] \geq \max _{a \in \mathcal{A}} Q_{\star}(s, a) \tag{24} Eϵ[a∈AmaxQ(s,a;w)]≥a∈AmaxQ⋆(s,a)(24)公式说明哪怕DQN是对真实价值的无偏估计,但如果求最大化,DQN就会高估真实价值。复习一下,DQN中希望 Q ( s , a ; w ) Q(s, a ; \boldsymbol{w}) Q(s,a;w)拟合的值 y ^ j \hat{y}_j y^j是通过下面公式(25)计算出来的: y ^ j = r j + γ ⋅ max a ∈ A Q ( s j + 1 , a ; w ) ⏟ 高估 max a ∈ A Q ⋆ ( s j + 1 , a ) (25) \widehat{y}_j=r_j+\gamma \cdot \underbrace{\max _{a \in \mathcal{A}} Q\left(s_{j+1}, a ; \boldsymbol{w}\right)}_{\text {高估 } \max _{a \in \mathcal{A}} Q_{\star}\left(s_{j+1}, a\right)} \tag{25} y j=rj+γ⋅高估 maxa∈AQ⋆(sj+1,a) a∈AmaxQ(sj+1,a;w)(25)这说明DQN在训练过程中通常是对真实价值的高估。
高估的危害
高估是有害的吗?如果高估是均匀的,则高估没有危害;如果高估是非均匀的就会有危害。举个例子,假如agent的动作空间为 A = { 左 , 右 , 上 } \mathcal{A}=\{左,右,上\} A={左,右,上},给定当前状态 s s s,每个动作有一个真实价值: Q ⋆ ( s , 左 ) = 200 Q_{\star}(s \text {, 左 })=200 Q⋆(s, 左 )=200 Q ⋆ ( s , 右 ) = 100 Q_{\star}(s \text {, 右 })=100 Q⋆(s, 右 )=100 Q ⋆ ( s , 上 ) = 230 Q_{\star}(s \text {, 上 })=230 Q⋆(s, 上 )=230根据贪婪策略,智能体应当选择动作"上",因为"上"的价值最大。如果高估是均匀的,所有动作价值都被高估了100,则会有: Q ⋆ ( s , 左 ) = 300 Q_{\star}(s \text {, 左 })=300 Q⋆(s, 左 )=300 Q ⋆ ( s , 右 ) = 200 Q_{\star}(s \text {, 右 })=200 Q⋆(s, 右 )=200 Q ⋆ ( s , 上 ) = 330 Q_{\star}(s \text {, 上 })=330 Q⋆(s, 上 )=330根据贪婪策略,智能体还是会选择"上"。这个例子说明高估本身不是问题,只要所有动作价值被同等高估。
但是在实际训练中,所有动作价值都会被同等高估吗?在DQN训练过程中,每当从经验回放池取出一个四元组 ( s , a , r , s ′ ) (s,a,r,s') (s,a,r,s′) 用来更新DQN参数,就很有可能加重DQN对 Q ⋆ ( s , a ) Q_{\star}(s,a) Q⋆(s,a)的高估。对于同一个状态 s s s,三种组合 ( s , 左 ) , ( s , 右 ) , ( s , 上 ) (s,左),(s,右),(s,上) (s,左),(s,右),(s,上)出现在经验回放池中的频率是不同的,所以三种动作被高估的程度是不同的,假如动作价值被高估的程度不同,比如: Q ⋆ ( s , 左 ) = 280 Q_{\star}(s \text {, 左 })=280 Q⋆(s, 左 )=280 Q ⋆ ( s , 右 ) = 300 Q_{\star}(s \text {, 右 })=300 Q⋆(s, 右 )=300 Q ⋆ ( s , 上 ) = 260 Q_{\star}(s \text {, 上 })=260 Q⋆(s, 上 )=260根据贪婪策略,智能体选择"右"动作,因为其动作价值最高,但实际上"右"是最差的动作,它的实际价值低于其余两个动作。
综上所属,用Q算法训练DQN总会导致DQN高估真实价值,对于大多数的 s ∈ S s\in \mathcal{S} s∈S和 a ∈ A a\in \mathcal{A} a∈A,有不等式(26): Q ( s , a ; w ) > Q ⋆ ( s , a ) (26) Q(s, a ; \boldsymbol{w})>Q_{\star}(s, a) \tag{26} Q(s,a;w)>Q⋆(s,a)(26)高估本身不是问题,真正的麻烦在于DQN的高估往往是非均匀的,如果DQN有非均匀的高估,那么用DQN做出的决策往往是不可靠的。
我们找到了导致DQN高估的两个问题,想要避免DQN的高估,要么切断自举,哟啊么避免最大化造成的高估。注意高估不是DQN本身的属性,高估是Q-learning算法造成的。想要避免高估,就要用更好的算法替代原来的Q-learning。
基于DQN2013存在的高估问题,DQN2015通过目标网络缓解DQN2013中因为自举导致的高估问题,但是DQN2015无法缓解最大化造成的高估,Double DQN在目标网络的基础上做改进,缓解最大化造成的高估问题,我们下面进行分析。
论文下载地址
该论文提出一种基于Q-learning的变体模型来解决这些不稳定性,它使用了两种关键方法,第一,使用经验回放机制,对训练数据进行随机化,消除观测序列中数据的相关性并平滑数据分布的变化;第二,使用目标网络预测下一状态的动作值函数用于更新当前网络的参数,并周期性地更新目标网络的参数(目标网络复制当前网络的参数),以此减少动作值函数Q和目标动作值函数 r + γ m a x a ′ Q ( s ′ , a ′ ) r+\gamma \space max_{a'}Q(s',a') r+γ maxa′Q(s′,a′)的相关性,通过目标网络缓解了DQN2013因为自举导致的高估危害问题。(可以简单理解为比DQN2013多了一个目标网络)
训练流程
基于gym的DQN2015代码实现
requirement:
code(代码主要参考书籍【动手学强化学习】(张伟楠,沈键,俞勇),京东购买链接 | 当当购买链接)
import gym
import random
import collections
import numpy as np
from tqdm import tqdm
import tensorflow as tf
import matplotlib.pyplot as plt
class ReplayBuffer:
"""
经验回放池
"""
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity) # 队列,先进先出
def add(self, state, action, reward, next_state, done): # 将数据加入buffer
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size): # 从buffer中采样数据,数量为batch_size
transitions = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
def size(self): # 目前buffer中数据的数量
return len(self.buffer)
class Q_net(tf.keras.Model):
"""只有一层隐藏层的Q网络"""
def __init__(self, state_dim, hidden_dim, action_dim):
super(Q_net, self).__init__()
self.fc1 = tf.keras.layers.Dense(hidden_dim, activation=tf.keras.activations.relu)
self.fc2 = tf.keras.layers.Dense(action_dim)
def call(self, x):
return self.fc2(self.fc1(x))
class DQN:
def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
epsilon, target_update):
self.action_dim = action_dim
self.q_net = Q_net(state_dim, hidden_dim, self.action_dim) # Q网络
# 目标网络
self.target_q_net = Q_net(state_dim, hidden_dim, self.action_dim)
# 使用Adam优化器
self.optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 计数器,记录更新次数
def take_action(self, state): # epsilon-贪婪策略采取动作
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = tf.cast(tf.expand_dims(state, axis=0), dtype=tf.float32)
action = tf.argmax(self.q_net.call(state), axis=1).numpy()[0]
return action
def update(self, transition_dict):
states = tf.cast(transition_dict['states'], dtype=tf.float32)
actions = tf.reshape(transition_dict['actions'], shape=(-1))
rewards = tf.reshape(tf.cast(transition_dict['rewards'], dtype=tf.float32), shape=(-1, 1))
next_states = tf.cast(transition_dict['next_states'], dtype=tf.float32)
dones = tf.reshape(tf.cast(transition_dict['dones'], dtype=tf.float32), shape=(-1, 1))
with tf.GradientTape() as tape:
q_values = tf.gather(self.q_net.call(states), actions, batch_dims=1) # Q值
# 下个状态的最大Q值
max_next_q_values = tf.reduce_max(self.target_q_net.call(next_states), axis=1, keepdims=True)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones) # TD误差目标
loss_func = tf.keras.losses.MeanSquaredError()
dqn_loss = tf.reduce_mean(loss_func(q_values, q_targets)) # 均方误差损失函数
grads = tape.gradient(dqn_loss, self.q_net.trainable_variables)
self.optimizer.apply_gradients(zip(grads, self.q_net.trainable_variables))
if self.count % self.target_update == 0:
self.target_q_net.set_weights(self.q_net.get_weights()) # 更新目标网络
self.count += 1
lr = 2e-3
num_episodes = 500
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
target_update = 10
buffer_size = 10000
minimal_size = 500
batch_size = 64
env_name = 'CartPole-v0'
env = gym.make(env_name)
random.seed(0)
np.random.seed(0)
env.seed(0)
tf.random.set_seed(0)
replay_buffer = ReplayBuffer(buffer_size)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update)
return_list = []
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)
next_state, reward, done, _ = env.step(action)
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
episode_return += reward
# 当buffer数据的数量超过一定值后,才进行Q网络训练
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)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN on {}'.format(env_name))
plt.show()
论文下载地址
论文介绍
造成DQN高估的原因不是DQN模型本身的问题,而是DQN使用的Q-learning算法有问题:第一,自举造成高估/低估的传播;第二,最大化造成目标动作价值的高估。在DQN2015中使用目标网络缓解自举造成的高估,但是这种方法不能缓解最大化导致的高估。本论文使用双Q学习算法(Double Q-learning),它在目标网络的基础上做改进,缓解最大化导致的高估问题。
回顾一下DQN2013中计算目标动作价值的公式,如公式(35)所示: y ^ j = r j + γ ⋅ max a ∈ A Q ( s j + 1 , a ; w ) (35) \widehat{y}_j=r_j+\gamma \cdot \max _{a \in \mathcal{A}} Q\left(s_{j+1}, a ; \boldsymbol{w}\right) \tag{35} y j=rj+γ⋅a∈AmaxQ(sj+1,a;w)(35)我们把最大化部分拆分成两步:
(1)选择基于状态 s j + 1 s_{j+1} sj+1的最优动作 a ⋆ a^{\star} a⋆,选择方式如下公式(36)所示: a ⋆ = argmax a ∈ A Q ( s j + 1 , a ; w ) (36) a^{\star}=\underset{a \in \mathcal{A}}{\operatorname{argmax}} Q\left(s_{j+1}, a ; \boldsymbol{w}\right) \tag{36} a⋆=a∈AargmaxQ(sj+1,a;w)(36)(2)基于状态 s j + 1 s_{j+1} sj+1和动作 a ⋆ a^{\star} a⋆计算目标动作价值,如公式(37)所示: y ^ j = r j + Q ( s j + 1 , a ⋆ ; w ) (37) \widehat{y}_j=r_j+Q\left(s_{j+1}, a^{\star} ; \boldsymbol{w}\right) \tag{37} y j=rj+Q(sj+1,a⋆;w)(37)上面是DQN2013使用的算法原理,选择基于状态 s j + 1 s_{j+1} sj+1的动作 a ⋆ a^{\star} a⋆和计算基于 s j + 1 s_{j+1} sj+1与 a ⋆ a^{\star} a⋆的动作价值函数 Q ( s j + 1 , a ⋆ ) Q(s_{j+1},a^{\star}) Q(sj+1,a⋆)都是使用同一个神经网络DQN。
在DQN2015中改进了Q-learning学习,选择基于状态 s j + 1 s_{j+1} sj+1的动作 a ⋆ a^{\star} a⋆和计算基于 s j + 1 s_{j+1} sj+1与 a ⋆ a^{\star} a⋆的动作价值函数 Q ( s j + 1 , a ⋆ ) Q(s_{j+1},a^{\star}) Q(sj+1,a⋆)都是使用目标DQN,如下所示:
(1)选择基于状态 s j + 1 s_{j+1} sj+1的最优动作 a ⋆ a^{\star} a⋆,选择方式如下公式(38)所示: a ⋆ = argmax a ∈ A Q ( s j + 1 , a ; w − ) (38) a^{\star}=\underset{a \in \mathcal{A}}{\operatorname{argmax}} Q\left(s_{j+1}, a ; \boldsymbol{w^{-}}\right) \tag{38} a⋆=a∈AargmaxQ(sj+1,a;w−)(38)(2)基于状态 s j + 1 s_{j+1} sj+1和动作 a ⋆ a^{\star} a⋆计算目标动作价值,如公式(39)所示: y ^ j = r j + Q ( s j + 1 , a ⋆ ; w − ) (39) \widehat{y}_j=r_j+Q\left(s_{j+1}, a^{\star} ; \boldsymbol{w^{-}}\right) \tag{39} y j=rj+Q(sj+1,a⋆;w−)(39)
在该论文中,第一步选择最优动作时使用DQN,第二步计算目标动作价值时使用目标网络:
(1)选择基于状态 s j + 1 s_{j+1} sj+1的最优动作 a ⋆ a^{\star} a⋆,选择方式如下公式(40)所示: a ⋆ = argmax a ∈ A Q ( s j + 1 , a ; w ) (40) a^{\star}=\underset{a \in \mathcal{A}}{\operatorname{argmax}} Q\left(s_{j+1}, a ; \boldsymbol{w}\right) \tag{40} a⋆=a∈AargmaxQ(sj+1,a;w)(40)(2)基于状态 s j + 1 s_{j+1} sj+1和动作 a ⋆ a^{\star} a⋆计算目标动作价值,如公式(41)所示: y ^ j = r j + Q ( s j + 1 , a ⋆ ; w − ) (41) \widehat{y}_j=r_j+Q\left(s_{j+1}, a^{\star} ; \boldsymbol{w^{-}}\right) \tag{41} y j=rj+Q(sj+1,a⋆;w−)(41)
双Q网络可以缓解最大化导致的高估,是因为在使用目标网络计算动作价值时所选择的动作 a ⋆ a^{\star} a⋆不一定是目标网络中动作价值最大的动作,通过公式(42)进行理解: Q ( s j + 1 , a ⋆ ; w − ) ⏟ 双 Q 学习 ≤ max a ∈ A Q ( s j + 1 , a ; w − ) ⏟ 用目标网络的 Q 学习 (42) \underbrace{Q\left(s_{j+1}, a^{\star} ; \boldsymbol{w}^{-}\right)}_{\text {双 } \mathrm{Q} \text { 学习 }} \leq \underbrace{\max _{a \in \mathcal{A}} Q\left(s_{j+1}, a ; \boldsymbol{w}^{-}\right)}_{\text {用目标网络的 } \mathrm{Q} \text { 学习 }} \tag{42} 双 Q 学习 Q(sj+1,a⋆;w−)≤用目标网络的 Q 学习 a∈AmaxQ(sj+1,a;w−)(42)假定动作空间为 A = [ a 1 , a 2 , a 3 ] \mathcal{A}=[a_1,a_2,a_3] A=[a1,a2,a3],在第一步计算时基于DQN得到的最优动作为 a 1 a_1 a1,第二步使用目标网络计算 a 1 a_1 a1对应的动作价值函数,由于目标网络中 a 3 a_3 a3对应的动作价值函数最大,这时候可以知道有公式(43): Q ( s j + 1 , a 1 ; w − ) ⏟ 双 Q 学习 ≤ Q ( s j + 1 , a 3 ; w − ) ⏟ 用目标网络的 Q 学习 (43) \underbrace{Q\left(s_{j+1}, a_1 ; \boldsymbol{w}^{-}\right)}_{\text {双 } \mathrm{Q} \text { 学习 }} \leq \underbrace{ Q\left(s_{j+1}, a_3 ; \boldsymbol{w}^{-}\right)}_{\text {用目标网络的 } \mathrm{Q} \text { 学习 }} \tag{43} 双 Q 学习 Q(sj+1,a1;w−)≤用目标网络的 Q 学习 Q(sj+1,a3;w−)(43)因此,可以知道通过双Q学习的到的目标动作价值函数比使用目标网络的Q-leanring的到的动作价值函数更小,所以说双Q学习缓解了最大化导致的高估,如公式(44)所示: y ~ t ⏟ 双 Q 学习 ≤ y ^ t − ⏟ 用目标网络的 Q 学习 (44) \underbrace{\widetilde{y}_t}_{\text {双 } \mathrm{Q} \text { 学习 }} \leq \underbrace{\widehat{y}_t^{-}}_{\text {用目标网络的 } \mathrm{Q} \text { 学习 }} \tag{44} 双 Q 学习 y t≤用目标网络的 Q 学习 y t−(44)
训练流程
DQN2013 | DQN2015 | Double DQN 对比
算法 | 选择下一状态 s ′ s' s′的网络 | 获取目标价值函数 Q ( s ′ , a ) Q(s',a) Q(s′,a) 的网络 | 自举造成高估/低估 | 最大化造成高估/低估 |
---|---|---|---|---|
DQN2013 | DQN | DQN | 严重 | 严重 |
DQN2015 | 目标网络 | 目标网络 | 不严重 | 严重 |
Double DQN | DQN | 目标网络 | 不严重 | 不严重 |
requirement:
code(代码主要参考书籍【动手学强化学习】(张伟楠,沈键,俞勇),京东购买链接 | 当当购买链接)
DQN只能处理离散动作问题,因此对于动作为连续值的倒立摆,我们无法直接使用DQN。由于倒立摆可以比较方便验证DQN对Q值的高估问题(倒立摆环境下的Q值最大估计应该为0),预测得到的Q值大于0说明模型出现了高估问题。为了能够使用DQN,我们采用离散化动作技巧,将倒立摆的连续动作修改为11个离散动作。
import gym
import random
import collections
import numpy as np
from tqdm import tqdm
import tensorflow as tf
import matplotlib.pyplot as plt
class ReplayBuffer:
"""
经验回放池
"""
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity) # 队列,先进先出
def add(self, state, action, reward, next_state, done): # 将数据加入buffer
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size): # 从buffer中采样数据,数量为batch_size
transitions = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
def size(self): # 目前buffer中数据的数量
return len(self.buffer)
class Q_net(tf.keras.Model):
"""只有一层隐藏层的Q网络"""
def __init__(self, hidden_dim, action_dim):
super(Q_net, self).__init__()
self.fc1 = tf.keras.layers.Dense(hidden_dim, activation=tf.keras.activations.relu)
self.fc2 = tf.keras.layers.Dense(action_dim)
def call(self, x):
return self.fc2(self.fc1(x))
class DQN:
def __init__(self, hidden_dim, action_dim, learning_rate, gamma, epsilon, target_update, dqn_type='VanillaDQN'):
self.action_dim = action_dim
self.q_net = Q_net(hidden_dim, self.action_dim) # Q网络
# 目标网络
self.target_q_net = Q_net(hidden_dim, self.action_dim)
# 使用Adam优化器
self.optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 计数器,记录更新次数
self.dqn_type = dqn_type
def take_action(self, state): # epsilon-贪婪策略采取动作
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = tf.cast(tf.expand_dims(state, axis=0), dtype=tf.float32)
action = tf.argmax(self.q_net.call(state), axis=1).numpy()[0]
return action
def max_q_value(self, state):
state = tf.cast(tf.expand_dims(state, axis=0), dtype=tf.float32)
return tf.reduce_max(self.q_net.call(state)).numpy()
def update(self, transition_dict):
states = tf.cast(transition_dict['states'], dtype=tf.float32)
actions = tf.reshape(transition_dict['actions'], shape=(-1))
rewards = tf.reshape(tf.cast(transition_dict['rewards'], dtype=tf.float32), shape=(-1, 1))
next_states = tf.cast(transition_dict['next_states'], dtype=tf.float32)
dones = tf.reshape(tf.cast(transition_dict['dones'], dtype=tf.float32), shape=(-1, 1))
with tf.GradientTape() as tape:
q_values = tf.gather(self.q_net.call(states), actions, batch_dims=1) # Q值
# 下个状态的最大Q值
if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别
max_action = tf.argmax(self.q_net.call(next_states), axis=1, output_type=tf.int32)
max_next_q_values = tf.reshape(tf.gather(self.target_q_net.call(next_states), max_action, batch_dims=1), shape=(-1,1)) # Q值
else:
max_next_q_values = tf.reduce_max(self.target_q_net.call(next_states), axis=1, keepdims=True)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones) # TD误差目标
loss_func = tf.keras.losses.MeanSquaredError()
dqn_loss = tf.reduce_mean(loss_func(q_values, q_targets)) # 均方误差损失函数
grads = tape.gradient(dqn_loss, self.q_net.trainable_variables)
self.optimizer.apply_gradients(zip(grads, self.q_net.trainable_variables))
if self.count % self.target_update == 0:
self.target_q_net.set_weights(self.q_net.get_weights()) # 更新目标网络
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
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)
random.seed(0)
np.random.seed(0)
env.seed(0)
tf.random.set_seed(0)
replay_buffer = ReplayBuffer(buffer_size)
agent = DQN(hidden_dim, action_dim, lr, gamma, epsilon, target_update, "DoubleDQN")
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)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
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()
Gerald Tesauro. Temporal difference learning and td-gammon. Communications of the ACM,38(3):58–68, 1995. ↩︎
Jordan B. Pollack and Alan D. Blair. Why did td-gammon work. In Advances in Neural Information Processing Systems 9, pages 10–16, 1996. ↩︎
John N Tsitsiklis and Benjamin Van Roy. An analysis of temporal-difference learning with function approximation. Automatic Control, IEEE Transactions on, 42(5):674–690, 1997. ↩︎ ↩︎
Leemon Baird. Residual algorithms: Reinforcement learning with function approximation. In Proceedings of the 12th International Conference on Machine Learning (ICML 1995), pages 30–37. Morgan Kaufmann, 1995. ↩︎
Brian Sallans and Geoffrey E. Hinton. Reinforcement learning with factored states and actions. Journal of Machine Learning Research, 5:1063–1088, 2004. ↩︎
Nicolas Heess, David Silver, and Yee Whye Teh. Actor-critic reinforcement learning with energy-based policies. In European Workshop on Reinforcement Learning, page 43, 2012. ↩︎
Hamid Maei, Csaba Szepesvari, Shalabh Bhatnagar, Doina Precup, David Silver, and Rich Sutton. Convergent Temporal-Difference Learning with Arbitrary Smooth Function Approximation. In Advances in Neural Information Processing Systems 22, pages 1204–1212, 2009. ↩︎
Hamid Maei, Csaba Szepesv´ari, Shalabh Bhatnagar, and Richard S. Sutton. Toward off-policy learning control with function approximation. In Proceedings of the 27th International Conference on Machine Learning (ICML 2010), pages 719–726, 2010. ↩︎
Martin Riedmiller. Neural fitted q iteration–first experiences with a data efficient neural reinforcement learning method. In Machine Learning: ECML 2005, pages 317–328. Springer, 2005. ↩︎
Sascha Lange and Martin Riedmiller. Deep auto-encoder neural networks in reinforcement learning. In Neural Networks (IJCNN), The 2010 International Joint Conference on, pages 1–8. IEEE, 2010. ↩︎
Long-Ji Lin. Reinforcement learning for robots using neural networks. Technical report, DTIC Document, 1993. ↩︎