7 DQN 算法
7.1 简介
在第 5 章讲解的 Q-learning 算法中,我们以矩阵的方式建立了一张存储每个状态下所有动作值的表格。表格中的每一个动作价值Q ( s , a ) Q(s,a) Q ( s , a ) 表示在状态s s s 下选择动作a a a 然后继续遵循某一策略预期能够得到的期望回报。然而,这种用表格存储动作价值的做法只在环境的状态和动作都是离散的,并且空间都比较小的情况下适用,我们之前进行代码实战的几个环境都是如此(如悬崖漫步)。当状态或者动作数量非常大的时候,这种做法就不适用了。例如,当状态是一张 RGB 图像时,假设图像大小是210 × 160 × 3 210\times 160\times 3 210 × 160 × 3 ,此时一共有25 6 ( 210 × 160 × 3 ) 256^{(210\times 160\times 3)} 25 6 ( 210 × 160 × 3 ) 种状态,在计算机中存储这个数量级的Q Q Q 值表格是不现实的。更甚者,当状态或者动作连续的时候,就有无限个状态动作对,我们更加无法使用这种表格形式来记录各个状态动作对的Q Q Q 值。
对于这种情况,我们需要用函数拟合的方法来估计Q Q Q 值,即将这个复杂的Q Q Q 值表格视作数据,使用一个参数化的函数Q θ Q_\theta Q θ 来拟合这些数据。很显然,这种函数拟合的方法存在一定的精度损失,因此被称为近似方法。我们今天要介绍的 DQN 算法便可以用来解决连续状态下离散动作的问题。
7.2 CartPole 环境
以图 7-1 中所示的所示的车杆(CartPole )环境为例,它的状态值就是连续的,动作值是离散的。
图 7-1 CartPole环境示意图
在车杆环境中,有一辆小车,智能体的任务是通过左右移动保持车上的杆竖直,若杆的倾斜度数过大,或者车子离初始位置左右的偏离程度过大,或者坚持时间到达 200 帧,则游戏结束。智能体的状态是一个维数为 4 的向量,每一维都是连续的,其动作是离散的,动作空间大小为 2,详情参见表 7-1 和表 7-2。在游戏中每坚持一帧,智能体能获得分数为 1 的奖励,坚持时间越长,则最后的分数越高,坚持 200 帧即可获得最高的分数。
表 7-1 CartPole环境的状态空间
维度
意义
最小值
最大值
0
车的位置
-2.4
2.4
1
车的速度
-Inf
Inf
2
杆的角度
~ -41.8°
~ 41.8°
3
杆尖端的速度
-Inf
Inf
表7-2 CartPole环境的动作空间
7.3 DQN
现在我们想在类似车杆的环境中得到动作价值函数Q ( s , a ) Q(s,a) Q ( s , a ) ,由于状态每一维度的值都是连续的,无法使用表格记录,因此一个常见的解决方法便是使用函数拟合 (function approximation)的思想。由于神经网络具有强大的表达能力,因此我们可以用一个神经网络来表示函数Q Q Q 。若动作是连续(无限)的,神经网络的输入是状态s s s 和动作a a a ,然后输出一个标量,表示在状态s s s 下采取动作a a a 能获得的价值。若动作是离散(有限)的,除了可以采取动作连续情况下的做法,我们还可以只将状态s s s 输入到神经网络中,使其同时输出每一个动作的Q Q Q 值。通常 DQN(以及 Q-learning)只能处理动作离散的情况,因为在函数Q Q Q 的更新过程中有max a \max_a max a 这一操作。假设神经网络用来拟合函数的参数是ω \omega ω ,即每一个状态s s s 下所有可能动作a a a 的值我们都能表示为Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 。我们将用于拟合函数Q Q Q 函数的神经网络称为Q 网络 ,如图 7-2 所示。
图7-2 工作在CartPole环境中的Q网络示意图
那么 Q 网络的损失函数是什么呢?我们先来回顾一下 Q-learning 的更新规则(参见 5.5 节):
Q ( s , a ) ← Q ( s , a ) + α [ r + γ max a ′ ∈ A Q ( s ′ , a ′ ) − Q ( s , a ) ] Q(s,a) \leftarrow Q(s,a) + \alpha \Big[
r + \gamma \max_{a'\in\mathcal{A}} Q(s',a') - Q(s,a)
\Big]
Q ( s , a ) ← Q ( s , a ) + α [ r + γ a ′ ∈ A max Q ( s ′ , a ′ ) − Q ( s , a ) ]
上述公式用时序差分 (temporal difference,TD)学习目标r + γ max a ′ ∈ A Q ( s ′ , a ′ ) r + \gamma \max_{a'\in\mathcal{A}} Q(s',a') r + γ max a ′ ∈ A Q ( s ′ , a ′ ) 来增量式更新Q ( s , a ) Q(s,a) Q ( s , a ) ,也就是说要使Q ( s , a ) Q(s,a) Q ( s , a ) 和TD目标r + γ max a ′ ∈ A Q ( s ′ , a ′ ) r + \gamma \max_{a'\in\mathcal{A}} Q(s',a') r + γ max a ′ ∈ A Q ( s ′ , a ′ ) 靠近。于是,对于一组数据{ ( s i , a i , r i , s i ′ ) } \Big\{(s_i,a_i,r_i,s_i')\Big\} { ( s i , a i , r i , s i ′ ) } ,我们可以很自然地将 Q 网络的损失函数构造为均方误差的形式:
ω ∗ = arg min ω 1 2 N ∑ i = 1 N [ Q ω ( s i , a i ) − ( r i + γ max a ′ Q ω ( s i ′ , a ′ ) ) ] 2 \omega^* = \underset{\omega}{\argmin} \dfrac{1}{2N} \sum_{i=1}^N \bigg[
Q_\omega(s_i,a_i) - \Big(
r_i + \gamma \max_{a'}Q_\omega (s_i', a')
\Big)
\bigg]^2
ω ∗ = ω arg min 2 N 1 i = 1 ∑ N [ Q ω ( s i , a i ) − ( r i + γ a ′ max Q ω ( s i ′ , a ′ ) ) ] 2
至此,我们就可以将 Q-learning 扩展到神经网络形式——深度 Q 网络 (deep Q network,DQN)算法。由于 DQN 是离线策略算法,因此我们在收集数据的时候可以使用一个ε-贪婪策略来平衡探索与利用,将收集到的数据存储起来,在后续的训练中使用。DQN 中还有两个非常重要的模块——经验回放 和目标网络 ,它们能够帮助 DQN 取得稳定、出色的性能。
7.3.1 经验回放
在一般的有监督学习中,假设训练数据是独立同分布的,我们每次训练神经网络的时候从训练数据中随机采样一个或若干个数据来进行梯度下降,随着学习的不断进行,每一个训练数据会被使用多次。在原来的 Q-learning 算法中,每一个数据只会用来更新一次Q Q Q 值。为了更好地将 Q-learning 和深度神经网络结合,DQN 算法采用了经验回放 (experience replay)方法,具体做法为维护一个回放缓冲区 ,将每次从环境中采样得到的四元组数据(状态、动作、奖励、下一状态)存储到回放缓冲区中,训练 Q 网络的时候再从回放缓冲区中随机采样若干数据来进行训练。这么做可以起到以下两个作用。
使样本满足独立假设。在 MDP 中交互采样得到的数据本身不满足独立假设,因为这一时刻的状态和上一时刻的状态有关。非独立同分布的数据对训练神经网络有很大的影响,会使神经网络拟合到最近训练的数据上。采用经验回放可以打破样本之间的相关性,让其满足独立假设。
如果直接使用连续的样本进行训练,会导致样本之间的相关性较强,这可能会影响训练效果,使得Q值函数收敛较慢甚至不收敛。为了避免这种情况,DQN使用经验回放机制,将智能体的经验存储在回放缓冲区中,并从中随机抽取样本进行训练。在回放缓冲区中,每个样本都是从智能体在环境中的不同时间步采集的,因此它们之间的相关性很低。通过随机抽取样本进行训练,可以保证每个样本都有相同的机会被选中,从而使得样本之间的相关性更加随机化。此外,经验回放还可以减少训练数据的相关性,从而避免了过拟合的风险。这是因为经验回放可以从回放缓冲区中删除旧的样本,同时添加新的样本,从而确保样本之间的相关性始终保持在一个合理的范围内。
提高样本效率。每一个样本可以被使用多次,十分适合深度神经网络的梯度学习。
7.3.2 目标网络
DQN 算法最终更新的目标是让Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 逼近r + γ max a ′ Q ω ( s ′ , a ′ ) r + \gamma \max_{a'}Q_\omega(s',a') r + γ max a ′ Q ω ( s ′ , a ′ ) ,由于 TD 误差目标本身就包含神经网络的输出,因此在更新网络参数的同时目标也在不断地改变,这非常容易造成神经网络训练的不稳定性(在监督学习里,标签是固定不动的,但这里的估计Q值和目标Q值会一起变动,我们希望至少有一方变动不那么大,来保证算法稳定)。为了解决这一问题,DQN 便使用了目标网络 (target network)的思想:既然训练过程中 Q 网络的不断更新会导致目标不断发生改变,不如暂时先将 TD 目标中的 Q 网络固定住。为了实现这一思想,我们需要利用两套 Q 网络。
原来的训练网络Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) ,用于计算原来的损失函数1 2 [ Q ω ( s , a ) − ( r + γ max a ′ Q ω − ( s ′ , a ′ ) ) ] 2 \dfrac{1}{2}\bigg[Q_\omega(s,a) - \Big(r + \gamma\underset{a'}{\max} Q_{\omega^{-}}(s',a')\Big)\bigg]^2 2 1 [ Q ω ( s , a ) − ( r + γ a ′ max Q ω − ( s ′ , a ′ ) ) ] 2 中的Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 项,并且使用正常梯度下降方法来进行更新。
目标网络Q ω − ( s , a ) Q_{\omega^{-}}(s,a) Q ω − ( s , a ) ,用于计算原先损失函数1 2 [ Q ω ( s , a ) − ( r + γ max a ′ Q ω − ( s ′ , a ′ ) ) ] 2 \dfrac{1}{2}\bigg[Q_\omega(s,a) - \Big(r + \gamma\underset{a'}{\max} Q_{\omega^{-}}(s',a')\Big)\bigg]^2 2 1 [ Q ω ( s , a ) − ( r + γ a ′ max Q ω − ( s ′ , a ′ ) ) ] 2 中的( r + γ max a ′ Q ω − ( s ′ , a ′ ) ) \Big(r + \gamma\underset{a'}{\max} Q_{\omega^{-}}(s',a')\Big) ( r + γ a ′ max Q ω − ( s ′ , a ′ ) ) 项,其中ω − \omega^{-} ω − 表示目标网络中的参数。如果两套网络的参数随时保持一致,则仍为原先不够稳定的算法。为了让更新目标更稳定,目标网络并不会每一步都更新。具体而言,目标网络使用训练网络的一套较旧的参数,训练网络Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 在训练中的每一步都会更新,而目标网络的参数每隔C C C 步才会与训练网络同步一次,即ω − ← ω \omega^{-} \leftarrow \omega ω − ← ω 。这样做使得目标网络相对于训练网络更加稳定。
综上所述,DQN 算法的具体流程如下:
用随机的网络参数ω \omega ω 初始化网络Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a )
复制相同的参数ω − ← ω \omega^{-}\leftarrow \omega ω − ← ω 来初始化目标网络Q ω − Q_{\omega^{-}} Q ω −
初始化经验回放池R R R
for 序列e = 1 → E e=1\rightarrow E e = 1 → E do
获取环境初始状态s 1 s_1 s 1
for 时间步t = 1 → T t=1\rightarrow T t = 1 → T do
根据当前网络Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 以ε-贪婪策略选择动作a t a_t a t
执行动作a t a_t a t ,获得回报r t r_t r t ,环境状态变为s t + 1 s_{t+1} s t + 1
将( s t , a t , r t , s t + 1 ) (s_t,a_t,r_t,s_{t+1}) ( s t , a t , r t , s t + 1 ) 存储进回放池R R R 中
若R R R 中数据足够,从R R R 中采样N N N 个数据{ ( s 1 , a i , r i , s i + 1 ) } i = 1 , … , N \Big\{(s_1,a_i,r_i,s_{i+1})\Big\}_{i=1,\dots,N} { ( s 1 , a i , r i , s i + 1 ) } i = 1 , … , N
对每个数据,用目标网络计算y i = r i + γ max a Q ω − ( s i + 1 , a ) y_i=r_i+\gamma\max_aQ_{\omega^{-}}(s_{i+1},a) y i = r i + γ max a Q ω − ( s i + 1 , a )
最小化目标损失L = 1 N ∑ i 1 2 ( y i − Q ω ( s i , a i ) ) 2 L=\dfrac{1}{N}\underset{i}{\sum}\dfrac{1}{2}\Big(y_i - Q_\omega(s_i,a_i)\Big)^2 L = N 1 i ∑ 2 1 ( y i − Q ω ( s i , a i ) ) 2 ,以此更新当前网络Q ω Q_\omega Q ω
更新目标网络
end for
end for
7.4 DQN 代码实践
接下来,我们就正式进入 DQN 算法的代码实践环节。我们采用的测试环境是 CartPole-v0,其状态空间相对简单,只有 4 个变量,因此网络结构的设计也相对简单:采用一层 128 个神经元的全连接并以 ReLU 作为激活函数。当遇到更复杂的诸如以图像作为输入的环境时,我们可以考虑采用深度卷积神经网络。
从 DQN 算法开始,我们将会用到rl_utils
库,它包含一些专门为本书准备的函数,如绘制移动平均曲线、计算优势函数等,不同的算法可以一起使用这些函数。为了能够调用rl_utils
库,请从本书的GitHub 仓库 下载rl_utils.py
文件。
1 2 3 4 5 6 7 8 9 import randomimport gymimport numpy as npimport collectionsfrom tqdm import tqdmimport torchimport torch.nn.functional as Fimport matplotlib.pyplot as pltimport rl_utils
首先定义经验回放池的类,主要包括加入数据、采样数据两大函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ReplayBuffer : ''' 经验回放池 ''' def __init__ (self, capacity ): self.buffer = collections.deque(maxlen=capacity) def add (self, state, action, reward, next_state, done ): self.buffer.append((state, action, reward, next_state, done)) def sample (self, 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 ): return len (self.buffer)
然后定义一个只有一层隐藏层的 Q 网络。
1 2 3 4 5 6 7 8 9 10 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)
有了这些基本组件之后,接来下开始实现 DQN 算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class DQN : ''' DQN算法 ''' def __init__ (self, state_dim, hidden_dim, action_dim, learning_rate, gamma, epsilon, target_update, device ): 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.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 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) 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
一切准备就绪,开始训练并查看结果。我们之后会将这一训练过程包装进rl_utils
库中,方便之后要学习的算法的代码实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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 device = torch.device("cuda" ) if torch.cuda.is_available() else torch.device("cpu" ) env_name = 'CartPole-v0' env = gym.make(env_name) random.seed(0 ) np.random.seed(0 ) env.seed(0 ) torch.manual_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, device) 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 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 )
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|████████████████████████████████████████| 50/50 [00:00<00:00, 589.55it/s, episode=50, return=9.300] Iteration 1: 100%|████████████████████████████████████████| 50/50 [00:00<00:00, 60.01it/s, episode=100, return=12.300] Iteration 2: 100%|████████████████████████████████████████| 50/50 [00:04<00:00, 11.94it/s, episode=150, return=123.000] Iteration 3: 100%|████████████████████████████████████████| 50/50 [00:15<00:00, 3.27it/s, episode=200, return=159.300] Iteration 4: 100%|████████████████████████████████████████| 50/50 [00:16<00:00, 3.04it/s, episode=250, return=192.200] Iteration 5: 100%|████████████████████████████████████████| 50/50 [00:15<00:00, 3.23it/s, episode=300, return=199.900] Iteration 6: 100%|████████████████████████████████████████| 50/50 [00:15<00:00, 3.28it/s, episode=350, return=193.400] Iteration 7: 100%|████████████████████████████████████████| 50/50 [00:16<00:00, 3.10it/s, episode=400, return=200.000] Iteration 8: 100%|████████████████████████████████████████| 50/50 [00:15<00:00, 3.28it/s, episode=450, return=172.300] Iteration 9: 100%|████████████████████████████████████████| 50/50 [00:15<00:00, 3.32it/s, episode=500, return=185.000]
1 2 3 4 5 6 7 8 9 10 11 12 13 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() mv_return = rl_utils.moving_average(return_list, 9 ) plt.plot(episodes_list, mv_return) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('DQN on {}' .format (env_name)) plt.show()
可以看到,DQN 的性能在 100 个序列后很快得到提升,最终收敛到策略的最优回报值 200。我们也可以看到,在 DQN 的性能得到提升后,它会持续出现一定程度的震荡,这主要是神经网络过拟合到一些局部经验数据后由arg max \argmax arg max 运算带来的影响。
7.5 以图像为输入的 DQN 算法
在本书前面章节所述的强化学习环境中,我们都使用非图像的状态作为输入(例如车杆环境中车的坐标、速度),但是在一些视频游戏中,智能体并不能直接获取这些状态信息,而只能直接获取屏幕中的图像。要让智能体和人一样玩游戏,我们需要让智能体学会以图像作为状态时的决策。我们可以利用 7.4 节的 DQN 算法,将卷积层加入其网络结构以提取图像特征,最终实现以图像为输入的强化学习。以图像为输入的 DQN 算法的代码与 7.4 节的代码的不同之处主要在于 Q 网络的结构和数据输入。DQN 网络通常会将最近的几帧图像一起作为输入,从而感知环境的动态性。接下来我们实现以图像为输入的 DQN 算法,但由于代码需要运行较长的时间,我们在此便不展示训练结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ConvolutionalQnet (torch.nn.Module): ''' 加入卷积层的Q网络 ''' def __init__ (self, action_dim, in_channels=4 ): super (ConvolutionalQnet, self).__init__() self.conv1 = torch.nn.Conv2d(in_channels, 32 , kernel_size=8 , stride=4 ) self.conv2 = torch.nn.Conv2d(32 , 64 , kernel_size=4 , stride=2 ) self.conv3 = torch.nn.Conv2d(64 , 64 , kernel_size=3 , stride=1 ) self.fc4 = torch.nn.Linear(7 * 7 * 64 , 512 ) self.head = torch.nn.Linear(512 , action_dim) def forward (self, x ): x = x / 255 x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = F.relu(self.fc4(x)) return self.head(x)
7.6 小结
本章讲解了 DQN 算法,其主要思想是用一个神经网络来表示最优策略的函数Q Q Q ,然后利用 Q-learning 的思想进行参数更新。为了保证训练的稳定性和高效性,DQN 算法引入了经验回放和目标网络两大模块,使得算法在实际应用时能够取得更好的效果。在 2013 年的 NIPS 深度学习研讨会上,DeepMind 公司的研究团队发表了 DQN 论文,首次展示了这一直接通过卷积神经网络接受像素输入来玩转各种雅达利(Atari)游戏的强化学习算法,由此拉开了深度强化学习的序幕。DQN 是深度强化学习的基础,掌握了该算法才算是真正进入了深度强化学习领域,本书中还有更多的深度强化学习算法等待读者探索。
7.7 参考文献
[1] VOLODYMYR M, KAVUKCUOGLU K, SILVER D, et al. Human-level control through deep reinforcement learning [J]. Nature, 2015, 518(7540): 529-533.
[2] VOLODYMYR M, KAVUKCUOGLU K, SILVER D, et al. Playing atari with deep reinforcement learning [C]//NIPS Deep Learning Workshop, 2013.
8 DQN 改进算法
8.1 简介
DQN 算法敲开了深度强化学习的大门,但是作为先驱性的工作,其本身存在着一些问题以及一些可以改进的地方。于是,在 DQN 之后,学术界涌现出了非常多的改进算法。本章将介绍其中两个非常著名的算法:Double DQN 和 Dueling DQN,这两个算法的实现非常简单,只需要在 DQN 的基础上稍加修改,它们能在一定程度上改善 DQN 的效果。如果读者想要了解更多、更详细的 DQN 改进方法,可以阅读 Rainbow 模型的论文及其引用文献。
8.2 Double DQN
普通的 DQN 算法通常会导致对Q Q Q 值的过高估计(overestimation)。传统 DQN 优化的 TD 误差目标为
r + γ max a ′ Q ω − ( s ′ , a ′ ) r + \gamma \max_{a'} Q_{\omega^{-}}(s',a')
r + γ a ′ max Q ω − ( s ′ , a ′ )
其中max a ′ Q ω − ( s ′ , a ′ ) \max_a' Q_{\omega^{-}}(s',a') max a ′ Q ω − ( s ′ , a ′ ) 由目标网络(参数为ω − \omega^{-} ω − )计算得出,我们还可以将其写成如下形式:
Q ω − ( s ′ , arg max a ′ Q ω − ( s ′ , a ′ ) ) Q_{\omega^{-}} \Big(
s', \underset{a'}{\argmax} Q_{\omega^{-}}(s',a')
\Big)
Q ω − ( s ′ , a ′ arg max Q ω − ( s ′ , a ′ ) )
换句话说,max \max max 操作实际可以被拆解为两部分:首先选取状态s ′ s' s ′ 下的最优动作a ∗ = arg max a ′ Q ω − ( s ′ , a ′ ) a^*=\underset{a'}{\argmax} Q_{\omega^{-}}(s',a') a ∗ = a ′ arg max Q ω − ( s ′ , a ′ ) ,接着计算该动作对应的价值Q ω − ( s ′ , a ∗ ) Q_{\omega^{-}}(s',a^{*}) Q ω − ( s ′ , a ∗ ) 。 当这两部分采用同一套 Q 网络进行计算时,每次得到的都是神经网络当前估算的所有动作价值中的最大值。考虑到通过神经网络估算的Q Q Q 值本身在某些时候会产生正向或负向的误差,在 DQN 的更新方式下神经网络会将正向误差累积。例如,我们考虑一个特殊情形:在状态s ′ s' s ′ 下所有动作Q Q Q 的值均为 0 0 0 ,即Q ( s ′ , a i ) = 0 , ∀ i Q(s',a_i)=0,\forall i Q ( s ′ , a i ) = 0 , ∀ i ,此时正确的更新目标应为r + 0 = r r+0=r r + 0 = r ,但是由于神经网络拟合的误差通常会出现某些动作的估算有正误差的情况,即存在某个动作a ′ a' a ′ 有Q ( s ′ , a ′ ) > 0 Q(s',a')>0 Q ( s ′ , a ′ ) > 0 ,此时我们的更新目标出现了过高估计,r + γ max Q > r + 0 r+\gamma\max Q > r + 0 r + γ max Q > r + 0 。因此,当我们用 DQN 的更新公式进行更新时,Q ( s , a ) Q(s,a) Q ( s , a ) 也就会被过高估计了。同理,我们拿这个Q ( s , a ) Q(s,a) Q ( s , a ) 来作为更新目标来更新上一步的Q Q Q 值时,同样会过高估计,这样的误差将会逐步累积。对于动作空间较大的任务,DQN 中的过高估计问题会非常严重,造成 DQN 无法有效工作的后果。
为了解决这一问题,Double DQN 算法提出利用两个独立训练的神经网络估算max a ′ Q ∗ ( s ′ , a ′ ) \max\limits_{a'}Q_{*}(s',a') a ′ max Q ∗ ( s ′ , a ′ ) 。具体做法是将原有的max a ′ Q ω − ( s ′ , a ′ ) \max\limits_{a'}Q_{\omega^{-}}(s',a') a ′ max Q ω − ( s ′ , a ′ ) 更改为Q ω − ( s ′ , arg max a ′ Q ω ( s ′ , a ′ ) ) Q_{\omega^{-}}(s',\underset{a'}{\argmax}Q_{\omega}(s',a')) Q ω − ( s ′ , a ′ arg max Q ω ( s ′ , a ′ )) ,即利用一套神经网络Q ω Q_\omega Q ω 的输出选取价值最大的动作,但在使用该动作的价值时,用另一套神经网络Q ω − Q_\omega^- Q ω − 计算该动作的价值。这样,即使其中一套神经网络的某个动作存在比较严重的过高估计问题,由于另一套神经网络的存在,这个动作最终使用的Q Q Q 值不会存在很大的过高估计问题。
在传统的 DQN 算法中,本来就存在两套Q Q Q 函数的神经网络——目标网络和训练网络(参见 7.3.2 节),只不过max a ′ Q ω − ( s ′ , a ′ ) \underset{a'}{\max}Q_{\omega^{-}}(s',a') a ′ max Q ω − ( s ′ , a ′ ) 的计算只用到了其中的目标网络,那么我们恰好可以直接将训练网络作为 Double DQN 算法中的第一套神经网络来选取动作,将目标网络作为第二套神经网络计算Q Q Q 值,这便是 Double DQN 的主要思想。由于在 DQN 算法中将训练网络的参数记为ω \omega ω ,将目标网络的参数记为ω − \omega^{-} ω − ,这与本节中 Double DQN 的两套神经网络的参数是统一的,因此,我们可以直接写出如下 Double DQN 的优化目标:
r + γ Q ω − ( s ′ , arg max a ′ Q ω ( s ′ , a ′ ) ) r + \gamma Q_{\omega^{-}} \Big(
s', \underset{a'}{\argmax}Q_{\omega}(s',a')
\Big)
r + γ Q ω − ( s ′ , a ′ arg max Q ω ( s ′ , a ′ ) )
8.3 Double DQN 代码实践
显然,DQN 与 Double DQN 的差别只是在于计算状态s ′ s' s ′ 下Q Q Q 值时如何选取动作:
DQN 的优化目标可以写为r + γ Q ω − ( s ′ , arg max a ′ Q ω − ( s ′ , a ′ ) ) r + \gamma Q_{\omega^{-}}(s',\underset{a'}{\argmax} Q_{\omega^{-}}(s', a')) r + γ Q ω − ( s ′ , a ′ arg max Q ω − ( s ′ , a ′ )) ,动作的选取依靠目标网络Q ω − Q_{\omega^{-}} Q ω − ;
Double DQN 的优化目标为r + γ Q ω − ( s ′ , arg max a ′ Q ω ( s ′ , a ′ ) ) r + \gamma Q_{\omega^{-}}(s',\underset{a'}{\argmax} Q_{\omega}(s', a')) r + γ Q ω − ( s ′ , a ′ arg max Q ω ( s ′ , a ′ )) ,动作的选取依靠训练网络Q ω Q_{\omega} Q ω 。
所以 Double DQN 的代码实现可以直接在 DQN 的基础上进行,无须做过多修改。
本节采用的环境是倒立摆 (Inverted Pendulum),该环境下有一个处于随机位置的倒立摆,如图 8-1 所示。环境的状态包括倒立摆角度的正弦值sin θ \sin\theta sin θ ,余弦值cos θ \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.001 a 2 ) ,倒立摆向上保持直立不动时奖励为 0 0 0 ,倒立摆在其他位置时奖励为负数。环境本身没有终止状态,运行 200 200 200 步后游戏自动结束。
图8-1 Pendulum环境示意图
标号
名称
最小值
最大值
0
cos θ \cos\theta cos θ
-1.0
1.0
1
sin θ \sin\theta sin θ
-1.0
1.0
2
θ ˙ \dot\theta θ ˙
-8.0
8.0
表8-1 Pendulum环境的状态空间
标号
动作
最小值
最大值
0
力矩
-2.0
2.0
表8-2 Pendulum环境的动作空间
力矩大小是在[ − 2 , 2 ] [-2, 2] [ − 2 , 2 ] 范围内的连续值。由于 DQN 只能处理离散动作环境,因此我们无法直接用 DQN 来处理倒立摆环境,但倒立摆环境可以比较方便地验证 DQN 对Q Q Q 值的过高估计:倒立摆环境下Q Q Q 值的最大估计应为 0 0 0 (倒立摆向上保持直立时能选取的最大Q Q Q 值),值出现大于 0 0 0 的情况则说明出现了过高估计。为了能够应用 DQN,我们采用离散化动作的技巧。例如,下面的代码将连续的动作空间离散为 11 11 11 个动作。动作[ 0 , 1 , 2 , ⋯ , 9 , 10 ] [0,1,2,\cdots,9,10] [ 0 , 1 , 2 , ⋯ , 9 , 10 ] 分别代表力矩为[ − 2 , − 1.6 , − 1.2 , ⋯ , 1.2 , 1.6 , 2 ] [-2, -1.6,-1.2,\cdots,1.2, 1.6, 2] [ − 2 , − 1.6 , − 1.2 , ⋯ , 1.2 , 1.6 , 2 ] 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import randomimport gymimport numpy as npimport torchimport torch.nn.functional as Fimport matplotlib.pyplot as pltimport rl_utilsfrom tqdm import tqdmclass 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)
接下来我们在 DQN 代码的基础上稍做修改以实现 Double DQN。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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) 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
接下来我们设置相应的超参数,并实现将倒立摆环境中的连续动作转化为离散动作的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 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)
接下来要对比 DQN 和 Double DQN 的训练情况,为了便于后续多次调用,我们进一步将 DQN 算法的训练过程定义成一个函数。训练过程会记录下每个状态的最大值,在训练完成后我们可以将结果可视化,观测这些Q Q Q 值存在的过高估计的情况,以此来对比 DQN 和 Double DQN 的不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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) 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
一切就绪!我们首先训练 DQN 并打印出其学习过程中最大Q Q Q 值的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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()
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|██████████| 20/20 [00:02<00:00, 7.14it/s, episode=20, return=-1018.764] Iteration 1: 100%|██████████| 20/20 [00:03<00:00, 5.73it/s, episode=40, return=-463.311] Iteration 2: 100%|██████████| 20/20 [00:03<00:00, 5.53it/s, episode=60, return=-184.817] Iteration 3: 100%|██████████| 20/20 [00:03<00:00, 5.55it/s, episode=80, return=-317.366] Iteration 4: 100%|██████████| 20/20 [00:03<00:00, 5.67it/s, episode=100, return=-208.929] Iteration 5: 100%|██████████| 20/20 [00:03<00:00, 5.59it/s, episode=120, return=-182.659] Iteration 6: 100%|██████████| 20/20 [00:03<00:00, 5.25it/s, episode=140, return=-275.938] Iteration 7: 100%|██████████| 20/20 [00:03<00:00, 5.65it/s, episode=160, return=-209.702] Iteration 8: 100%|██████████| 20/20 [00:03<00:00, 5.73it/s, episode=180, return=-246.861] Iteration 9: 100%|██████████| 20/20 [00:03<00:00, 5.77it/s, episode=200, return=-293.374]
根据代码运行结果我们可以发现,DQN 算法在倒立摆环境中能取得不错的回报,最后的期望回报在− 200 -200 − 200 左右,但是不少Q Q Q 值超过了 0 0 0 ,有一些还超过了 10 10 10 ,该现象便是 DQN 算法中的Q Q Q 值过高估计。我们现在来看一下 Double DQN 是否能对此问题进行改善。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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()
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|██████████| 20/20 [00:03<00:00, 6.60it/s, episode=20, return=-818.719] Iteration 1: 100%|██████████| 20/20 [00:03<00:00, 5.43it/s, episode=40, return=-391.392] Iteration 2: 100%|██████████| 20/20 [00:03<00:00, 5.29it/s, episode=60, return=-216.078] Iteration 3: 100%|██████████| 20/20 [00:03<00:00, 5.52it/s, episode=80, return=-438.220] Iteration 4: 100%|██████████| 20/20 [00:03<00:00, 5.42it/s, episode=100, return=-162.128] Iteration 5: 100%|██████████| 20/20 [00:03<00:00, 5.50it/s, episode=120, return=-389.088] Iteration 6: 100%|██████████| 20/20 [00:03<00:00, 5.44it/s, episode=140, return=-273.700] Iteration 7: 100%|██████████| 20/20 [00:03<00:00, 5.23it/s, episode=160, return=-221.605] Iteration 8: 100%|██████████| 20/20 [00:04<00:00, 4.91it/s, episode=180, return=-262.134] Iteration 9: 100%|██████████| 20/20 [00:03<00:00, 5.34it/s, episode=200, return=-278.752]
我们可以发现,与普通的 DQN 相比,Double DQN 比较少出现Q Q Q 值大于 0 0 0 的情况,说明Q Q Q 值过高估计的问题得到了很大缓解。
8.4 Dueling DQN
Dueling DQN 是 DQN 另一种的改进算法,它在传统 DQN 的基础上只进行了微小的改动,但却能大幅提升 DQN 的表现。在强化学习中,我们将状态动作价值函数Q Q Q 减去状态价值函数V V V 的结果定义为优势函数A A 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 0 0 ,因为所有动作的动作价值的期望就是这个状态的状态价值。据此,在 Dueling DQN 中,Q 网络被建模为:
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) Q_{\eta,\alpha,\beta}(s,a) = V_{\eta,\alpha}(s) + A_{\eta,\beta}(s,a)
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 Q 值,而是训练神经网络的最后几层的两个分支,分别输出状态价值函数和优势函数,再求和得到Q Q Q 值。Dueling DQN 的网络结构如图 8-2 所示。
图8-2 Dueling DQN的网络结构图
将状态价值函数和优势函数分别建模的好处在于:某些情境下智能体只会关注状态的价值,而并不关心不同动作导致的差异,此时将二者分开建模能够使智能体更好地处理与动作关联较小的状态。在图 8-3 所示的驾驶车辆游戏中,智能体注意力集中的部位被显示为橙色(另见彩插图 4),当智能体前面没有车时,车辆自身动作并没有太大差异,此时智能体更关注状态价值,而当智能体前面有车时(智能体需要超车),智能体开始关注不同动作优势值的差异。
图8-3 状态价值和优势值的简单例子
对于 Dueling DQN 中的公式Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) Q_{\eta,\alpha,\beta}(s,a) = V_{\eta,\alpha}(s) + A_{\eta,\beta}(s,a) Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) ,它存在对于V V V 值和A A A 值建模不唯一性的问题。例如,对于同样的Q Q Q 值,如果将V V V 值加上任意大小的常数C C C ,再将所有A A A 值减去C C C ,则得到的Q Q Q 值依然不变,这就导致了训练的不稳定性。为了解决这一问题,Dueling DQN 强制最优动作的优势函数的实际输出为 0 0 0 ,即:
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − max a ′ A η , β ( s , a ′ ) Q_{\eta,\alpha,\beta}(s,a) = V_{\eta,\alpha}(s) + A_{\eta,\beta}(s,a) - \max_{a'} A_{\eta,\beta}(s,a')
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − a ′ max A η , β ( s , a ′ )
此时V ( s ) = max a Q ( s , a ) V(s)=\max\limits_aQ(s,a) V ( s ) = a max Q ( s , a ) ,可以确保V V V 值建模的唯一性。在实现过程中,我们还可以用平均代替最大化操作,即:
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − 1 A ∑ a ′ A η , β ( s , a ′ ) Q_{\eta,\alpha,\beta}(s,a) = V_{\eta,\alpha}(s) + A_{\eta,\beta}(s,a) - \dfrac{1}{\mathcal{A}} \sum_{a'} A_{\eta,\beta}(s,a')
Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − A 1 a ′ ∑ A η , β ( s , a ′ )
此时V ( s ) = 1 A ∑ a ′ Q ( s , a ′ ) V(s)=\dfrac{1}{\mathcal{A}} \sum\limits_{a'} Q(s,a') V ( s ) = A 1 a ′ ∑ Q ( s , a ′ ) 。在下面的代码实现中,我们将采取此种方式,虽然它不再满足贝尔曼最优方程,但实际应用时更加稳定。
有的读者可能会问:“为什么 Dueling DQN 会比 DQN 好?”部分原因在于 Dueling DQN 能更高效学习状态价值函数。每一次更新时,函数V V V 都会被更新,这也会影响到其他动作的Q Q Q 值。而传统的 DQN 只会更新某个动作的Q Q Q 值,其他动作的Q Q Q 值就不会更新。因此,Dueling DQN 能够更加频繁、准确地学习状态价值函数。
8.5 Dueling DQN 代码实践
Dueling DQN 与 DQN 相比的差异只是在网络结构上,大部分代码依然可以继续沿用。我们定义状态价值函数和优势函数的复合神经网络VAnet
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 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 ) return Qclass 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' : 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 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()
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|███████████████████████████████████████| 20/20 [00:10<00:00, 1.87it/s, episode=20, return=-708.652] Iteration 1: 100%|███████████████████████████████████████| 20/20 [00:15<00:00, 1.28it/s, episode=40, return=-229.557] Iteration 2: 100%|███████████████████████████████████████| 20/20 [00:15<00:00, 1.32it/s, episode=60, return=-184.607] Iteration 3: 100%|███████████████████████████████████████| 20/20 [00:13<00:00, 1.50it/s, episode=80, return=-200.323] Iteration 4: 100%|███████████████████████████████████████| 20/20 [00:13<00:00, 1.51it/s, episode=100, return=-213.811] Iteration 5: 100%|███████████████████████████████████████| 20/20 [00:13<00:00, 1.53it/s, episode=120, return=-181.165] Iteration 6: 100%|███████████████████████████████████████| 20/20 [00:14<00:00, 1.35it/s, episode=140, return=-222.040] Iteration 7: 100%|███████████████████████████████████████| 20/20 [00:14<00:00, 1.35it/s, episode=160, return=-173.313] Iteration 8: 100%|███████████████████████████████████████| 20/20 [00:12<00:00, 1.62it/s, episode=180, return=-236.372] Iteration 9: 100%|███████████████████████████████████████| 20/20 [00:12<00:00, 1.57it/s, episode=200, return=-230.058]
根据代码运行结果我们可以发现,相比于传统的 DQN,Dueling DQN 在多个动作选择下的学习更加稳定,得到的回报最大值也更大。由 Dueling DQN 的原理可知,随着动作空间的增大,Dueling DQN 相比于 DQN 的优势更为明显。之前我们在环境中设置的离散动作数为 11,我们可以增加离散动作数(例如 15、25 等),继续进行对比实验。
8.6 总结
在传统的 DQN 基础上,有两种非常容易实现的变式——Double DQN 和 Dueling DQN,Double DQN 解决了 DQN 中对Q Q Q 值的过高估计,而 Dueling DQN 能够很好地学习到不同动作的差异性,在动作空间较大的环境下非常有效。从 Double DQN 和 Dueling DQN 的方法原理中,我们也能感受到深度强化学习的研究是在关注深度学习和强化学习有效结合:一是在深度学习的模块的基础上,强化学习方法如何更加有效地工作,并避免深度模型学习行为带来的一些问题,例如使用 Double DQN 解决Q Q Q 值过高估计的问题;二是在强化学习的场景下,深度学习模型如何有效学习到有用的模式,例如设计 Dueling DQN 网络架构来高效地学习状态价值函数以及动作优势函数。
8.7 扩展阅读: 对 Q 值过高估计的定量分析
我们可以对Q Q Q 值的过高估计做简化的定量分析。假设在状态s s s 下所有动作的期望回报均无差异,即Q ∗ ( s , a ) = V ∗ ( s ) Q^{*}(s,a)=V^{*}(s) Q ∗ ( s , a ) = V ∗ ( s ) (此设置是为了定量分析所简化的情形,实际上不同动作的期望回报通常会存在差异);假设神经网络估算误差Q ω − ( s , a ) − V ∗ Q_{\omega^{-}}(s,a) - V^* Q ω − ( s , a ) − V ∗ 服从[ − 1 , 1 ] [-1,1] [ − 1 , 1 ] 之间的均匀独立同分布;假设动作空间大小为m m m 。那么,对于任意状态s s s ,有:
E [ max a Q ω − ( s , a ) − max a ′ Q ∗ ( s , a ′ ) ] = m − 1 m + 1 \mathbb{E} \Big[
\max_a Q_{\omega^{-}}(s,a) - \max_{a'}Q_{*}(s,a')
\Big] = \dfrac{m-1}{m+1}
E [ a max Q ω − ( s , a ) − a ′ max Q ∗ ( s , a ′ ) ] = m + 1 m − 1
即动作空间m m m 越大时,Q Q Q 值过高,估计越严重。
证明 :将估算误差记为ϵ a = Q ω − ( s , a ) − max a ′ Q ∗ ( s , a ′ ) \epsilon_a = Q_{\omega^{-}}(s,a) - \max\limits_{a'}Q^{*}(s,a') ϵ a = Q ω − ( s , a ) − a ′ max Q ∗ ( s , a ′ ) ,由于估算误差对于不同的动作是独立的,因此有:
P ( max a ϵ a ≤ x ) = ∏ a = 1 m P ( ϵ a ≤ x ) P(\max_{a} \epsilon_a \le x) = \prod_{a=1}^m P(\epsilon_a \le x)
P ( a max ϵ a ≤ x ) = a = 1 ∏ m P ( ϵ a ≤ x )
P ( ϵ a ≤ x ) P(\epsilon_a \le x) P ( ϵ a ≤ x ) 是的累积分布函数 (cumulative distribution function,即 CDF),它可以具体被写为:
P ( ϵ a ≤ x ) = { 0 if x ≤ − 1 x + 1 2 if x ∈ ( − 1 , 1 ) 1 if x ≥ 1 P(\epsilon_a \le x) = \begin{cases}
0 & \text{if } x \le -1 \\
\dfrac{x+1}{2} & \text{if } x \in (-1,1)\\
1 & \text{if } x \ge 1
\end{cases}
P ( ϵ a ≤ x ) = ⎩ ⎨ ⎧ 0 2 x + 1 1 if x ≤ − 1 if x ∈ ( − 1 , 1 ) if x ≥ 1
因此,我们得到关于max a ϵ a \max\limits_{a} \epsilon_a a max ϵ a 的累积分布函数:
P ( max a ϵ a ≤ x ) = ∏ a = 1 m P ( ϵ a ≤ x ) = { 0 if x ≤ − 1 ( x + 1 2 ) m if x ∈ ( − 1 , 1 ) 1 if x ≥ 1 \begin{aligned}
P(\max_{a} \epsilon_a \le x) &= \prod_{a=1}^m P(\epsilon_a \le x)\\
&= \begin{cases}
0 & \text{if } x \le -1 \\
(\dfrac{x+1}{2})^m & \text{if } x \in (-1,1)\\
1 & \text{if } x \ge 1
\end{cases}
\end{aligned}
P ( a max ϵ a ≤ x ) = a = 1 ∏ m P ( ϵ a ≤ x ) = ⎩ ⎨ ⎧ 0 ( 2 x + 1 ) m 1 if x ≤ − 1 if x ∈ ( − 1 , 1 ) if x ≥ 1
最后我们可以得到:
E [ max a ϵ a ] = ∫ − 1 1 x d d x P ( max a ϵ a ≤ x ) d x = [ ( x + 1 2 ) m m x − 1 m + 1 ] ∣ − 1 1 = m − 1 m + 1 \begin{aligned}
\mathbb{E} \Big[\max_a\epsilon_a\Big] &= \int_{-1}^1 x \dfrac{\mathbf {d}}{\mathbf{d}x} P(\max_{a} \epsilon_a \le x) \mathbf{d}x\\
&= \Big[
\Big(\dfrac{x+1}{2}\Big)^m \dfrac{mx - 1}{m+1}
\Big]\bigg|_{-1}^1\\
&= \dfrac{m-1}{m+1}
\end{aligned}
E [ a max ϵ a ] = ∫ − 1 1 x d x d P ( a max ϵ a ≤ x ) d x = [ ( 2 x + 1 ) m m + 1 m x − 1 ] − 1 1 = m + 1 m − 1
虽然这一分析简化了实际环境,但它仍然正确刻画了Q Q Q 值过高估计的一些性质,比如Q Q Q 值的过高估计随动作空间大小m m m 的增加而增加,换言之,在动作选择数更多的环境中,Q Q Q 值的过高估计会更严重。
8.8 参考文献
[1] HASSELT V H, GUEZ A, SILVER D. Deep reinforcement learning with double q-learning [C]// Proceedings of the AAAI conference on artificial intelligence. 2016, 30(1).
[2] WANG Z, SCHAUL T, HESSEL M, et al. Dueling network architectures for deep reinforcement learning [C]// International conference on machine learning, PMLR, 2016: 1995-2003.
[3] HESSEL M, MODAYIL J, HASSELT V H, et al. Rainbow: Combining improvements in deep reinforcement learning [C]// Thirty-second AAAI conference on artificial intelligence, 2018.
9 策略梯度算法
9.1 简介
本书之前介绍的 Q-learning、DQN 及 DQN 改进算法都是基于价值 (value-based)的方法,其中 Q-learning 是处理有限状态的算法,而 DQN 可以用来解决连续状态的问题。在强化学习中,除了基于值函数的方法,还有一支非常经典的方法,那就是基于策略 (policy-based)的方法。对比两者,基于值函数的方法主要是学习值函数,然后根据值函数导出一个策略,学习过程中并不存在一个显式的策略;而基于策略的方法则是直接显式地学习一个目标策略。策略梯度是基于策略的方法的基础,本章从策略梯度算法说起。
9.2 策略梯度
基于策略的方法首先需要将策略参数化。假设目标策略π θ \pi_\theta π θ 是一个随机性策略,并且处处可微,其中θ \theta θ 是对应的参数。我们可以用一个线性模型或者神经网络模型来为这样一个策略函数建模,输入某个状态,然后输出一个动作的概率分布。我们的目标是要寻找一个最优策略并最大化这个策略在环境中的期望回报。我们将策略学习的目标函数定义为
J ( θ ) = E s 0 [ V π θ ( s 0 ) ] J(\theta) = \mathbb{E}_{s_0}\Big[{V^{\pi_\theta}(s_0)}\Big]
J ( θ ) = E s 0 [ V π θ ( s 0 ) ]
其中,s 0 s_0 s 0 表示初始状态。现在有了目标函数J ( θ ) J(\theta) J ( θ ) ,我们将目标函数对策略θ \theta θ 求导,得到导数后,就可以用梯度上升方法来最大化这个目标函数,从而得到最优策略。
我第 3 章讲解过策略π \pi π 下的状态访问分布 ,在此用ν π \nu^{\pi} ν π 表示。然后我们对目标函数J ( θ ) J(\theta) J ( θ ) 求梯度,可以得到如下式子,更详细的推导过程将在 9.6 节给出。
∇ θ J ( θ ) ∝ ∑ s ∈ S ν π θ ( s ) ∑ a ∈ A Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) = ∑ s ∈ S ν π θ ( s ) ∑ a ∈ A Q π θ ( s , a ) π θ ( a ∣ s ) ⋅ ∇ θ π θ ( a ∣ s ) π θ ( a ∣ s ) = ∑ s ∈ S ν π θ ( s ) ( ∑ a ∈ A π θ ( a ∣ s ) Q π θ ( s , a ) ⋅ ∇ θ π θ ( a ∣ s ) π θ ( a ∣ s ) ) = ∑ s ∈ S ν π θ ( s ) ( ∑ a ∈ A π θ ( a ∣ s ) Q π θ ( s , a ) ⋅ ∇ θ log π θ ( a ∣ s ) ) = E π θ [ Q π θ ( s , a ) ∇ θ log π θ ( a ∣ s ) ] \begin{aligned}
\nabla_\theta J(\theta) &\propto \sum_{s\in\mathcal{S}} \nu^{\pi_{\theta}}(s)
\sum_{a\in\mathcal{A}}Q^{\pi_{\theta}}(s,a)\nabla_{\theta}\pi_{\theta}(a|s) \\
&= \sum_{s\in\mathcal{S}} \nu^{\pi_{\theta}}(s)
\sum_{a\in\mathcal{A}}Q^{\pi_{\theta}}(s,a) \pi_{\theta}(a|s) \cdot \dfrac{\nabla_{\theta}\pi_{\theta}(a|s)}{\pi_{\theta}(a|s)} \\
&= \sum_{s\in\mathcal{S}} \nu^{\pi_{\theta}}(s)
\Big(
\sum_{a\in\mathcal{A}} \pi_{\theta}(a|s) Q^{\pi_{\theta}}(s,a) \cdot \dfrac{\nabla_{\theta}\pi_{\theta}(a|s)}{\pi_{\theta}(a|s)}
\Big) \\
&= \sum_{s\in\mathcal{S}} \nu^{\pi_{\theta}}(s)
\Big(
\sum_{a\in\mathcal{A}} \pi_{\theta}(a|s) Q^{\pi_{\theta}}(s,a) \cdot
\nabla_{\theta}\log \pi_{\theta}(a|s)
\Big) \\
&= \mathbb{E}_{\pi_{\theta}} \Big[Q^{\pi_{\theta}}(s,a) \nabla_{\theta}\log \pi_{\theta}(a|s)\Big] \\
\end{aligned}
∇ θ J ( θ ) ∝ s ∈ S ∑ ν π θ ( s ) a ∈ A ∑ Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) = s ∈ S ∑ ν π θ ( s ) a ∈ A ∑ Q π θ ( s , a ) π θ ( a ∣ s ) ⋅ π θ ( a ∣ s ) ∇ θ π θ ( a ∣ s ) = s ∈ S ∑ ν π θ ( s ) ( a ∈ A ∑ π θ ( a ∣ s ) Q π θ ( s , a ) ⋅ π θ ( a ∣ s ) ∇ θ π θ ( a ∣ s ) ) = s ∈ S ∑ ν π θ ( s ) ( a ∈ A ∑ π θ ( a ∣ s ) Q π θ ( s , a ) ⋅ ∇ θ log π θ ( a ∣ s ) ) = E π θ [ Q π θ ( s , a ) ∇ θ log π θ ( a ∣ s ) ]
这个梯度可以用来更新策略。需要注意的是,因为上式中期望E \mathbb{E} E 的下标是π θ \pi_\theta π θ ,所以策略梯度算法为在线策略(on-policy)算法,即必须使用当前策略π θ \pi_\theta π θ 采样得到的数据来计算梯度。直观理解一下策略梯度这个公式,可以发现在每一个状态下,梯度的修改是让策略更多地去采样到带来较高Q Q Q 值的动作,更少地去采样到带来较低Q Q Q 值的动作,如图 9-1 所示。
图9-1 策略梯度示意图
在计算策略梯度的公式中,我们需要用到Q π θ ( s , a ) Q^{\pi_{\theta}}(s,a) Q π θ ( s , a ) ,可以用多种方式对它进行估计。接下来要介绍的 REINFORCE 算法便是采用了蒙特卡洛方法来估计Q π θ ( s , a ) Q^{\pi_{\theta}}(s,a) Q π θ ( s , a ) ,对于一个有限步数的环境来说,REINFORCE 算法中的策略梯度为:
∇ θ J ( θ ) = E π θ [ ∑ t = 0 T ( ∑ t ′ = t T γ t ′ − t r t ′ ) ∇ θ log π θ ( a t ∣ s t ) ] \nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta}
\bigg[
\sum_{t=0}^T \Big(
\sum_{t'=t}^T \gamma^{t'-t}r_{t'}
\Big) \nabla_{\theta} \log \pi_{\theta}(a_t|s_t)
\bigg]
∇ θ J ( θ ) = E π θ [ t = 0 ∑ T ( t ′ = t ∑ T γ t ′ − t r t ′ ) ∇ θ log π θ ( a t ∣ s t ) ]
其中,T T T 是和环境交互的最大步数。例如,在车杆环境中,T = 200 T=200 T = 200 。
9.3 REINFORCE
REINFORCE 算法的具体算法流程如下:
这便是 REINFORCE 算法的全部流程了。接下来让我们来用代码来实现它,看看效果如何吧!
9.4 REINFORCE 代码实践
我们在车杆环境中进行 REINFORCE 算法的实验。
1 2 3 4 5 6 7 import gymimport torchimport torch.nn.functional as Fimport numpy as npimport matplotlib.pyplot as pltfrom tqdm import tqdmimport rl_utils
首先定义策略网络PolicyNet
,其输入是某个状态,输出则是该状态下的动作概率分布,这里采用在离散动作空间上的softmax()
函数来实现一个可学习的多项分布 (multinomial distribution)。
1 2 3 4 5 6 7 8 9 class PolicyNet (torch.nn.Module): def __init__ (self, state_dim, hidden_dim, action_dim ): super (PolicyNet, 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 F.softmax(self.fc2(x), dim=1 )
再定义我们的 REINFORCE 算法。在函数take_action()
函数中,我们通过动作概率分布对离散的动作进行采样。在更新过程中,我们按照算法将损失函数写为策略回报的负数,即− ∑ t ψ t ∇ θ log π θ ( a t ∣ s t ) \displaystyle-\sum\limits_t\psi_t\nabla_\theta \log\pi_\theta(a_t|s_t) − t ∑ ψ t ∇ θ log π θ ( a t ∣ s t ) ,对求导后就可以通过梯度下降来更新策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class REINFORCE : def __init__ ( self, state_dim, hidden_dim, action_dim, learning_rate, gamma, device ): self.policy_net = PolicyNet(state_dim, hidden_dim, action_dim).to(device) self.optimizer = torch.optim.Adam(self.policy_net.parameters(), lr=learning_rate) self.gamma = gamma self.device = device def take_action (self, state ): state = torch.tensor([state], dtype=torch.float ).to(self.device) probs = self.policy_net(state) action_dist = torch.distributions.Categorical(probs) action = action_dist.sample() return action.item() def update (self, transition_dict ): reward_list = transition_dict['rewards' ] state_list = transition_dict['states' ] action_list = transition_dict['actions' ] G = 0 self.optimizer.zero_grad() for i in reversed (range (len (reward_list))): reward = reward_list[i] state = torch.tensor([state_list[i]], dtype=torch.float ).to(self.device) action = torch.tensor([action_list[i]]).view(-1 , 1 ).to(self.device) log_prob = torch.log(self.policy_net(state).gather(1 , action)) G = self.gamma * G + reward loss = -log_prob * G loss.backward() self.optimizer.step()
定义好策略,我们就可以开始实验了,看看 REINFORCE 算法在车杆环境上表现如何吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 learning_rate = 1e-3 num_episodes = 1000 hidden_dim = 128 gamma = 0.98 device = torch.device("cuda" ) if torch.cuda.is_available() else torch.device("cpu" ) env_name = "CartPole-v0" env = gym.make(env_name) env.seed(0 ) torch.manual_seed(0 ) state_dim = env.observation_space.shape[0 ] action_dim = env.action_space.n agent = REINFORCE( state_dim, hidden_dim, action_dim, learning_rate, gamma, device ) 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 transition_dict = { 'states' : [], 'actions' : [], 'next_states' : [], 'rewards' : [], 'dones' : [] } state = env.reset() done = False while not done: action = agent.take_action(state) next_state, reward, done, _ = env.step(action) transition_dict['states' ].append(state) transition_dict['actions' ].append(action) transition_dict['next_states' ].append(next_state) transition_dict['rewards' ].append(reward) transition_dict['dones' ].append(done) state = next_state episode_return += reward return_list.append(episode_return) agent.update(transition_dict) 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 )
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|██████████████████████████████████████| 100/100 [00:02<00:00, 47.36it/s, episode=100, return=55.500] Iteration 1: 100%|██████████████████████████████████████| 100/100 [00:04<00:00, 21.26it/s, episode=200, return=75.300] Iteration 2: 100%|██████████████████████████████████████| 100/100 [00:09<00:00, 10.55it/s, episode=300, return=178.800] Iteration 3: 100%|██████████████████████████████████████| 100/100 [00:11<00:00, 8.74it/s, episode=400, return=164.600] Iteration 4: 100%|██████████████████████████████████████| 100/100 [00:11<00:00, 8.74it/s, episode=500, return=156.500] Iteration 5: 100%|██████████████████████████████████████| 100/100 [00:11<00:00, 8.54it/s, episode=600, return=187.400] Iteration 6: 100%|██████████████████████████████████████| 100/100 [00:11<00:00, 8.52it/s, episode=700, return=194.500] Iteration 7: 100%|██████████████████████████████████████| 100/100 [00:13<00:00, 7.57it/s, episode=800, return=200.000] Iteration 8: 100%|██████████████████████████████████████| 100/100 [00:12<00:00, 7.84it/s, episode=900, return=200.000] Iteration 9: 100%|██████████████████████████████████████| 100/100 [00:12<00:00, 7.89it/s, episode=1000, return=186.100]
在 CartPole-v0 环境中,满分就是 200 分,我们发现 REINFORCE 算法效果很好,可以达到 200 分。接下来我们绘制训练过程中每一条轨迹的回报变化图。由于回报抖动比较大,往往会进行平滑处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 episodes_list = list (range (len (return_list))) plt.plot(episodes_list, return_list) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('REINFORCE on {}' .format (env_name)) plt.show() mv_return = rl_utils.moving_average(return_list, 9 ) plt.plot(episodes_list, mv_return) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('REINFORCE on {}' .format (env_name)) plt.show()
可以看到,随着收集到的轨迹越来越多,REINFORCE 算法有效地学习到了最优策略。不过,相比于前面的 DQN 算法,REINFORCE 算法使用了更多的序列,这是因为 REINFORCE 算法是一个在线策略算法,之前收集到的轨迹数据不会被再次利用。此外,REINFORCE 算法的性能也有一定程度的波动,这主要是因为每条采样轨迹的回报值波动比较大,这也是 REINFORCE 算法主要的不足。
9.5 小结
REINFORCE 算法是策略梯度乃至强化学习的典型代表,智能体根据当前策略直接和环境交互,通过采样得到的轨迹数据直接计算出策略参数的梯度,进而更新当前策略,使其向最大化策略期望回报的目标靠近。这种学习方式是典型的从交互中学习,并且其优化的目标(即策略期望回报)正是最终所使用策略的性能,这比基于价值的强化学习算法的优化目标(一般是时序差分误差的最小化)要更加直接。 REINFORCE 算法理论上是能保证局部最优的,它实际上是借助蒙特卡洛方法采样轨迹来估计动作价值,这种做法的一大优点是可以得到无偏的梯度。但是,正是因为使用了蒙特卡洛方法,REINFORCE 算法的梯度估计的方差很大,可能会造成一定程度上的不稳定,这也是第 10 章将介绍的 Actor-Critic 算法要解决的问题。
9.6 扩展阅读:策略梯度证明
策略梯度定理是强化学习中的重要理论。本节我们来证明
∇ θ J ( θ ) ∝ ∑ s ∈ S ν π θ ( s ) ∑ a ∈ A Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) \nabla_\theta J(\theta) \propto
\sum_{s\in\mathcal{S}} \nu^{\pi_\theta}(s) \sum_{a\in\mathcal{A}} Q^{\pi_\theta}(s,a)\nabla_\theta \pi_\theta(a|s)
∇ θ J ( θ ) ∝ s ∈ S ∑ ν π θ ( s ) a ∈ A ∑ Q π θ ( s , a ) ∇ θ π θ ( a ∣ s )
先从状态价值函数的推导开始:
∇ θ V π θ ( s ) = ∇ θ ( ∑ a ∈ A π θ ( a ∣ s ) Q π θ ( s , a ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) + π θ ( a ∣ s ) ∇ θ Q π θ ( s , a ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + ∑ a ∈ A ( π θ ( a ∣ s ) ∇ θ Q π θ ( s , a ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + ∑ a ∈ A π θ ( a ∣ s ) ∇ θ ( ∑ s ′ , r p ( s ′ , r ∣ s , a ) ( r + γ V π θ ( s ′ ) ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + ∑ a ∈ A π θ ( a ∣ s ) ( ∑ s ′ , r p ( s ′ , r ∣ s , a ) ∇ θ ( r + γ V π θ ( s ′ ) ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + ∑ a ∈ A π θ ( a ∣ s ) ( ∑ s ′ , r p ( s ′ , r ∣ s , a ) ∇ θ ( γ V π θ ( s ′ ) ) ) = ∑ a ∈ A ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + γ ∑ a ∈ A π θ ( a ∣ s ) ( ∑ s ′ p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) ) \begin{aligned}
\nabla_\theta V^{\pi_\theta}(s)
&= \nabla_\theta \Big(
\sum_{a\in\mathcal{A}}\pi_\theta (a|s) Q^{\pi_\theta}(s,a)
\Big)\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s) + \pi_\theta (a|s) \nabla_\theta Q^{\pi_\theta}(s,a)
\Big)\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\Big) + \sum_{a\in\mathcal{A}} \Big( \pi_\theta (a|s) \nabla_\theta Q^{\pi_\theta}(s,a)\Big)
\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\Big) + \sum_{a\in\mathcal{A}} \pi_\theta (a|s) \nabla_\theta \bigg(
\sum_{s',r} p(s',r|s,a)\Big(r+\gamma V^{\pi_\theta}(s')\Big)
\bigg)
\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\Big) + \sum_{a\in\mathcal{A}} \pi_\theta (a|s) \bigg(
\sum_{s',r} p(s',r|s,a) \nabla_\theta \Big(r+\gamma V^{\pi_\theta}(s')\Big)
\bigg)
\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\Big) + \sum_{a\in\mathcal{A}} \pi_\theta (a|s) \bigg(
\sum_{s',r} p(s',r|s,a) \nabla_\theta \Big(\gamma V^{\pi_\theta}(s')\Big)
\bigg)
\\
&= \sum_{a\in\mathcal{A}} \Big(
Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\Big) + \gamma \sum_{a\in\mathcal{A}} \pi_\theta (a|s) \Big(
\sum_{s'} p(s'|s,a) \nabla_\theta V^{\pi_\theta}(s')
\Big)
\end{aligned}
∇ θ V π θ ( s ) = ∇ θ ( a ∈ A ∑ π θ ( a ∣ s ) Q π θ ( s , a ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) + π θ ( a ∣ s ) ∇ θ Q π θ ( s , a ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + a ∈ A ∑ ( π θ ( a ∣ s ) ∇ θ Q π θ ( s , a ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + a ∈ A ∑ π θ ( a ∣ s ) ∇ θ ( s ′ , r ∑ p ( s ′ , r ∣ s , a ) ( r + γ V π θ ( s ′ ) ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + a ∈ A ∑ π θ ( a ∣ s ) ( s ′ , r ∑ p ( s ′ , r ∣ s , a ) ∇ θ ( r + γ V π θ ( s ′ ) ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + a ∈ A ∑ π θ ( a ∣ s ) ( s ′ , r ∑ p ( s ′ , r ∣ s , a ) ∇ θ ( γ V π θ ( s ′ ) ) ) = a ∈ A ∑ ( Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) + γ a ∈ A ∑ π θ ( a ∣ s ) ( s ′ ∑ p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) )
为了简化表示,我们让ϕ ( s ) = ∑ a ∈ A Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) \displaystyle \phi(s) = \sum\limits_{a\in\mathcal{A}}Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s) ϕ ( s ) = a ∈ A ∑ Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) , 定义d π θ ( s → x , k ) d^{\pi_\theta}(s\rightarrow x,k) d π θ ( s → x , k ) 为策略π \pi π 从状态s s s 出发k k k 步后到达状态x x x 的概率。我们继续推导:
∇ θ V π θ ( s ) = ϕ ( s ) + γ ∑ a ∈ A π θ ( a ∣ s ) ( ∑ s ′ p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) ) = ϕ ( s ) + γ ∑ a ∈ A ∑ s ′ π θ ( a ∣ s ) p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) = ϕ ( s ) + γ ∑ s ′ d π θ ( s → s ′ , 1 ) ∇ θ V π θ ( s ′ ) ( 利用该递推式 ) = ϕ ( s ) + γ ∑ s ′ d π θ ( s → s ′ , 1 ) [ ϕ ( s ′ ) + γ ∑ s ′ ′ d π θ ( s ′ → s ′ ′ , 1 ) ∇ θ V π θ ( s ′ ′ ) ] = ϕ ( s ) + γ ∑ s ′ d π θ ( s → s ′ , 1 ) ϕ ( s ′ ) + γ 2 ∑ s ′ ′ d π θ ( s → s ′ ′ , 2 ) ∇ θ V π θ ( s ′ ′ ) = ϕ ( s ) + γ ∑ s ′ d π θ ( s → s ′ , 1 ) ϕ ( s ′ ) + γ 2 ∑ s ′ ′ d π θ ( s → s ′ ′ , 2 ) ϕ ( s ′ ′ ) + γ 3 ∑ s ′ ′ ′ d π θ ( s → s ′ ′ ′ , 3 ) ∇ θ V π θ ( s ′ ′ ′ ) = ⋯ = ∑ s ∈ S ∑ k = 0 ∞ γ k d π θ ( s 0 → s , k ) ϕ ( x ) \begin{aligned}
\nabla_\theta V^{\pi_\theta}(s)
&= \phi(s) + \gamma \sum_{a\in\mathcal{A}} \pi_\theta (a|s) \Big(
\sum_{s'} p(s'|s,a) \nabla_\theta V^{\pi_\theta}(s')
\Big)\\
&= \phi(s) + \gamma \sum_{a\in\mathcal{A}} \sum_{s'} \pi_\theta (a|s) p(s'|s,a) \nabla_\theta V^{\pi_\theta}(s')\\
&= \phi(s) + \gamma \sum_{s'} d^{\pi_\theta}(s\rightarrow s',1) \nabla_\theta V^{\pi_\theta}(s') \quad (\text{利用该递推式})\\
&= \phi(s) + \gamma \sum_{s'} d^{\pi_\theta}(s\rightarrow s',1) \Big[
\phi(s') + \gamma \sum_{s''} d^{\pi_\theta}(s'\rightarrow s'',1) \nabla_\theta V^{\pi_\theta}(s'')
\Big]\\
&= \phi(s) + \gamma \sum_{s'} d^{\pi_\theta}(s\rightarrow s',1) \phi(s') +
\gamma^2 \sum_{s''} d^{\pi_\theta}(s\rightarrow s'',2) \nabla_\theta V^{\pi_\theta}(s'')\\
&= \phi(s) + \gamma \sum_{s'} d^{\pi_\theta}(s\rightarrow s',1) \phi(s') +
\gamma^2 \sum_{s''} d^{\pi_\theta}(s\rightarrow s'',2) \phi(s'') +
\gamma^3 \sum_{s'''} d^{\pi_\theta}(s\rightarrow s''',3) \nabla_\theta V^{\pi_\theta}(s''')\\
&= \cdots\\
&= \sum_{s\in\mathcal{S}}\sum_{k=0}^\infin \gamma^kd^{\pi_\theta}(s_0\rightarrow s,k)\phi(x)
\end{aligned}
∇ θ V π θ ( s ) = ϕ ( s ) + γ a ∈ A ∑ π θ ( a ∣ s ) ( s ′ ∑ p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) ) = ϕ ( s ) + γ a ∈ A ∑ s ′ ∑ π θ ( a ∣ s ) p ( s ′ ∣ s , a ) ∇ θ V π θ ( s ′ ) = ϕ ( s ) + γ s ′ ∑ d π θ ( s → s ′ , 1 ) ∇ θ V π θ ( s ′ ) ( 利用该递推式 ) = ϕ ( s ) + γ s ′ ∑ d π θ ( s → s ′ , 1 ) [ ϕ ( s ′ ) + γ s ′′ ∑ d π θ ( s ′ → s ′′ , 1 ) ∇ θ V π θ ( s ′′ ) ] = ϕ ( s ) + γ s ′ ∑ d π θ ( s → s ′ , 1 ) ϕ ( s ′ ) + γ 2 s ′′ ∑ d π θ ( s → s ′′ , 2 ) ∇ θ V π θ ( s ′′ ) = ϕ ( s ) + γ s ′ ∑ d π θ ( s → s ′ , 1 ) ϕ ( s ′ ) + γ 2 s ′′ ∑ d π θ ( s → s ′′ , 2 ) ϕ ( s ′′ ) + γ 3 s ′′′ ∑ d π θ ( s → s ′′′ , 3 ) ∇ θ V π θ ( s ′′′ ) = ⋯ = s ∈ S ∑ k = 0 ∑ ∞ γ k d π θ ( s 0 → s , k ) ϕ ( x )
定义η ( s ) = E s 0 [ ∑ k = 0 ∞ γ k d π θ ( s 0 → s , k ) ] \displaystyle \eta(s) = \mathbb{E}_{s_0}\Big[\sum_{k=0}^\infin \gamma^kd^{\pi_\theta}(s_0\rightarrow s,k)\Big] η ( s ) = E s 0 [ k = 0 ∑ ∞ γ k d π θ ( s 0 → s , k ) ] 。至此,回到目标函数:
∇ θ J ( θ ) = ∇ θ E s 0 [ V π θ ( s 0 ) ] = ∑ s ∈ S E s 0 [ ∑ k = 0 ∞ γ k d π θ ( s 0 → s , k ) ] ϕ ( x ) = ∑ s ∈ S η ( s ) ϕ ( x ) = ∑ s ∈ S η ( s ) ⋅ 1 ⋅ ϕ ( x ) = ∑ s ∈ S η ( s ) ⋅ ( ∑ s ∈ S η ( s ) ∑ s ∈ S η ( s ) ) ⋅ ϕ ( x ) ∝ ( ∑ s ∈ S η ( s ) ∑ s ∈ S η ( s ) ) ⋅ ϕ ( x ) = ( ∑ s ∈ S η ( s ) ∑ s ∈ S η ( s ) ) ⋅ ( ∑ a ∈ A Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) = ∑ s ∈ S ν π θ ( s ) ∑ a ∈ A Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) \begin{aligned}
\nabla_\theta J(\theta)
&= \nabla_\theta \mathbb{E}_{s_0} \Big[ V^{\pi_\theta}(s_0) \Big]
\\
&= \sum_{s\in\mathcal{S}} \mathbb{E}_{s_0} \bigg[\sum_{k=0}^\infin \gamma^kd^{\pi_\theta}(s_0\rightarrow s,k)\bigg]\phi(x)
\\
&= \sum_{s\in\mathcal{S}} \eta(s)\phi(x)
\\
&= \sum_{s\in\mathcal{S}} \eta(s) \cdot 1 \cdot \phi(x)
\\
&= \sum_{s\in\mathcal{S}} \eta(s) \cdot \bigg(\sum_{s\in\mathcal{S}} \dfrac{\eta(s)}{\displaystyle \sum_{s\in\mathcal{S}} \eta(s)}\bigg) \cdot \phi(x)
\\
&\propto \bigg(\sum_{s\in\mathcal{S}} \dfrac{\eta(s)}{\displaystyle \sum_{s\in\mathcal{S}} \eta(s)}\bigg) \cdot \phi(x)
\\
&= \bigg(\sum_{s\in\mathcal{S}} \dfrac{\eta(s)}{\displaystyle \sum_{s\in\mathcal{S}} \eta(s)} \bigg) \cdot
\bigg( \sum\limits_{a\in\mathcal{A}}Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)\bigg)
\\
&= \sum_{s\in\mathcal{S}} \nu^{\pi_\theta}(s)
\sum\limits_{a\in\mathcal{A}}Q^{\pi_\theta}(s,a) \nabla_\theta\pi_\theta (a|s)
\\
\end{aligned}
∇ θ J ( θ ) = ∇ θ E s 0 [ V π θ ( s 0 ) ] = s ∈ S ∑ E s 0 [ k = 0 ∑ ∞ γ k d π θ ( s 0 → s , k ) ] ϕ ( x ) = s ∈ S ∑ η ( s ) ϕ ( x ) = s ∈ S ∑ η ( s ) ⋅ 1 ⋅ ϕ ( x ) = s ∈ S ∑ η ( s ) ⋅ ( s ∈ S ∑ s ∈ S ∑ η ( s ) η ( s ) ) ⋅ ϕ ( x ) ∝ ( s ∈ S ∑ s ∈ S ∑ η ( s ) η ( s ) ) ⋅ ϕ ( x ) = ( s ∈ S ∑ s ∈ S ∑ η ( s ) η ( s ) ) ⋅ ( a ∈ A ∑ Q π θ ( s , a ) ∇ θ π θ ( a ∣ s ) ) = s ∈ S ∑ ν π θ ( s ) a ∈ A ∑ Q π θ ( s , a ) ∇ θ π θ ( a ∣ s )
证明完毕!
9.7 参考文献
[1] SUTTON R S, MCALLESTER D A, SINGH S P, et al. Policy gradient methods for reinforcement learning with function approximation [C] // Advances in neural information processing systems, 2000: 1057-1063.
10 Actor-Critic 算法
10.1 简介
本书之前的章节讲解了基于值函数的方法(DQN)和基于策略的方法(REINFORCE),其中基于值函数的方法只学习一个价值函数,而基于策略的方法只学习一个策略函数。那么,一个很自然的问题是,有没有什么方法既学习价值函数,又学习策略函数呢?答案就是 Actor-Critic。Actor-Critic 是囊括一系列算法的整体架构,目前很多高效的前沿算法都属于 Actor-Critic 算法,本章接下来将会介绍一种最简单的 Actor-Critic 算法。需要明确的是,Actor-Critic 算法本质上是基于策略的算法,因为这一系列算法的目标都是优化一个带参数的策略,只是会额外学习价值函数,从而帮助策略函数更好地学习。
10.2 Actor-Critic
回顾一下,在 REINFORCE 算法中,目标函数的梯度中有一项轨迹回报,用于指导策略的更新。REINFOCE 算法用蒙特卡洛方法来估计Q ( s , a ) Q(s,a) Q ( s , a ) ,能不能考虑拟合一个值函数来指导策略进行学习呢?这正是 Actor-Critic 算法所做的。在策略梯度中,可以把梯度写成下面这个更加一般的形式:
g = E [ ∑ t = 0 T ψ t ∇ θ log π θ ( a t ∣ s t ) ] g = \mathbb{E} \Big[
\sum_{t=0}^T \psi_t \nabla_\theta \log \pi_\theta (a_t|s_t)
\Big]
g = E [ t = 0 ∑ T ψ t ∇ θ log π θ ( a t ∣ s t ) ]
其中,ψ t \psi_t ψ t 可以有很多种形式:
(1) ∑ t ′ = 0 T γ t ′ r t ′ \displaystyle \sum_{t'=0}^T \gamma^{t'}r_{t'} t ′ = 0 ∑ T γ t ′ r t ′ :轨迹的总回报;
(2) ∑ t ′ = t T γ t ′ − t r t ′ \displaystyle \sum_{t'=t}^T \gamma^{t' - t}r_{t'} t ′ = t ∑ T γ t ′ − t r t ′ :动作a t a_t a t 之后的回报;
(3) ∑ t ′ = t T γ t ′ − t r t ′ − b ( s t ) \displaystyle \sum_{t'=t}^T \gamma^{t'-t}r_{t'} - b(s_t) t ′ = t ∑ T γ t ′ − t r t ′ − b ( s t ) :基准线版本的改进;
(4) Q π θ ( s t , a t ) Q^{\pi_\theta}(s_t,a_t) Q π θ ( s t , a t ) :动作价值函数
(5) A π θ ( s t , a t ) A^{\pi_\theta}(s_t,a_t) A π θ ( s t , a t ) :优势函数
(6) r t + γ V π θ ( s t + 1 ) − V π θ ( s t ) r_t + \gamma V^{\pi_{\theta}}(s_{t+1}) - V^{\pi_{\theta}}(s_t) r t + γ V π θ ( s t + 1 ) − V π θ ( s t ) :时序差分残差
9.5 节提到 REINFORCE 通过蒙特卡洛采样的方法对策略梯度的估计是无偏的,但是方差非常大。我们可以用形式(3)引入基线函数 (baseline function)b ( s t ) b(s_t) b ( s t ) 来减小方差。此外,我们也可以采用 Actor-Critic 算法估计一个动作价值函数Q Q Q ,代替蒙特卡洛采样得到的回报,这便是形式(4)。这个时候,我们可以把状态价值函数V V V 作为基线,从Q Q Q 函数减去这个V V V 函数则得到了A A A 函数,我们称之为优势函数 (advantage function),这便是形式(5)。更进一步,我们可以利用Q = r + γ V Q=r+\gamma V Q = r + γV 等式得到形式(6)。
本章将着重介绍形式(6),即通过时序差分残差ψ t = r t + γ V π ( s t + 1 ) − V π ( s t ) \psi_t = r_t + \gamma V^{\pi}(s_{t+1}) - V^{\pi}(s_t) ψ t = r t + γ V π ( s t + 1 ) − V π ( s t ) 来指导策略梯度进行学习。事实上,用Q Q Q 值或者V V V 值本质上也是用奖励来进行指导,但是用神经网络进行估计的方法可以减小方差、提高鲁棒性。除此之外,REINFORCE 算法基于蒙特卡洛采样,只能在序列结束后进行更新,这同时也要求任务具有有限的步数,而 Actor-Critic 算法则可以在每一步之后都进行更新,并且不对任务的步数做限制。
我们将 Actor-Critic 分为两个部分:Actor(策略网络)和 Critic(价值网络),如图 10-1 所示。
图10-1 Actor 和 Critic 的关系
Actor 的更新采用策略梯度的原则,那 Critic 如何更新呢?我们将 Critic 价值网络表示为V ω V_\omega V ω ,参数为ω \omega ω 。于是,我们可以采取时序差分残差的学习方式,对于单个数据定义如下价值函数的损失函数:
L ( ω ) = 1 2 ( r + γ V ω ( s t + 1 ) − V ω ( s t ) ) 2 \mathcal{L}(\omega) = \dfrac{1}{2} \Big(r + \gamma V_{\omega}(s_{t+1}) - V_{\omega}(s_t)\Big)^2
L ( ω ) = 2 1 ( r + γ V ω ( s t + 1 ) − V ω ( s t ) ) 2
与 DQN 中一样,我们采取类似于目标网络的方法,将上式中r + γ V ω ( s t + 1 ) r + \gamma V_{\omega}(s_{t+1}) r + γ V ω ( s t + 1 ) 作为时序差分目标,不会产生梯度来更新价值函数。因此,价值函数的梯度为:
∇ ω L ( ω ) = − ( r + γ V ω ( s t + 1 ) − V ω ( s t ) ) ∇ ω V ω ( s t ) \nabla_{\omega}\mathcal{L}(\omega) = - \Big(
r + \gamma V_{\omega}(s_{t+1}) - V_{\omega}(s_t)
\Big) \nabla_{\omega} V_{\omega}(s_t)
∇ ω L ( ω ) = − ( r + γ V ω ( s t + 1 ) − V ω ( s t ) ) ∇ ω V ω ( s t )
然后使用梯度下降方法来更新 Critic 价值网络参数即可。
Actor-Critic 算法的具体流程如下:
以上就是 Actor-Critic 算法的流程,接下来让我们来用代码实现它,看看效果如何吧!
10.3 Actor-Critic 代码实践
我们仍然在车杆环境上进行 Actor-Critic 算法的实验。
1 2 3 4 5 6 import gymimport torchimport torch.nn.functional as Fimport numpy as npimport matplotlib.pyplot as pltimport rl_utils
首先定义策略网络PolicyNet
(与 REINFORCE 算法一样)。
1 2 3 4 5 6 7 8 9 class PolicyNet (torch.nn.Module): def __init__ (self, state_dim, hidden_dim, action_dim ): super (PolicyNet, 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 F.softmax(self.fc2(x), dim=1 )
Actor-Critic 算法中额外引入一个价值网络,接下来的代码定义价值网络ValueNet
,其输入是某个状态,输出则是状态的价值。
1 2 3 4 5 6 7 8 9 class ValueNet (torch.nn.Module): def __init__ (self, state_dim, hidden_dim ): super (ValueNet, self).__init__() self.fc1 = torch.nn.Linear(state_dim, hidden_dim) self.fc2 = torch.nn.Linear(hidden_dim, 1 ) def forward (self, x ): x = F.relu(self.fc1(x)) return self.fc2(x)
现在定义ActorCritic
算法,主要包含采取动作(take_action()
)和更新网络参数(update()
)两个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class ActorCritic : def __init__ ( self, state_dim, hidden_dim, action_dim, actor_lr, critic_lr, gamma, device ): self.actor = PolicyNet(state_dim, hidden_dim, action_dim).to(device) self.critic = ValueNet(state_dim, hidden_dim).to(device) self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=actor_lr) self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=critic_lr) self.gamma = gamma self.device = device def take_action (self, state ): state = torch.tensor([state], dtype=torch.float ).to(self.device) probs = self.actor(state) action_dist = torch.distributions.Categorical(probs) action = action_dist.sample() return action.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) td_target = rewards + self.gamma * self.critic(next_states) * (1 - dones) td_delta = td_target - self.critic(states) log_probs = torch.log(self.actor(states).gather(1 , actions)) actor_loss = torch.mean(-log_probs * td_delta.detach()) critic_loss = torch.mean(F.mse_loss(self.critic(states), td_target.detach())) self.actor_optimizer.zero_grad() self.critic_optimizer.zero_grad() actor_loss.backward() critic_loss.backward() self.actor_optimizer.step() self.critic_optimizer.step()
定义好 Actor 和 Critic,我们就可以开始实验了,看看 Actor-Critic 在车杆环境上表现如何吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 actor_lr = 1e-3 critic_lr = 1e-2 num_episodes = 1000 hidden_dim = 128 gamma = 0.98 device = torch.device("cuda" ) if torch.cuda.is_available() else torch.device("cpu" ) env_name = 'CartPole-v0' env = gym.make(env_name) env.seed(0 ) torch.manual_seed(0 ) state_dim = env.observation_space.shape[0 ] action_dim = env.action_space.n agent = ActorCritic( state_dim, hidden_dim, action_dim, actor_lr, critic_lr, gamma, device ) return_list = rl_utils.train_on_policy_agent(env, agent, num_episodes)
1 2 3 4 5 6 7 8 9 10 Iteration 0: 100%|██████████| 100/100 [00:00<00:00, 101.75it/s, episode=100, return=21.100] Iteration 1: 100%|██████████| 100/100 [00:01<00:00, 58.71it/s, episode=200, return=72.800] Iteration 2: 100%|██████████| 100/100 [00:05<00:00, 19.73it/s, episode=300, return=109.300] Iteration 3: 100%|██████████| 100/100 [00:05<00:00, 17.30it/s, episode=400, return=163.000] Iteration 4: 100%|██████████| 100/100 [00:06<00:00, 16.27it/s, episode=500, return=193.600] Iteration 5: 100%|██████████| 100/100 [00:06<00:00, 15.90it/s, episode=600, return=195.900] Iteration 6: 100%|██████████| 100/100 [00:06<00:00, 15.80it/s, episode=700, return=199.100] Iteration 7: 100%|██████████| 100/100 [00:06<00:00, 15.72it/s, episode=800, return=186.900] Iteration 8: 100%|██████████| 100/100 [00:06<00:00, 15.94it/s, episode=900, return=200.000] Iteration 9: 100%|██████████| 100/100 [00:06<00:00, 15.45it/s, episode=1000, return=200.000]
在 CartPole-v0 环境中,满分就是 200 分。和 REINFORCE 相似,接下来我们绘制训练过程中每一条轨迹的回报变化图以及其经过平滑处理的版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 episodes_list = list (range (len (return_list))) plt.plot(episodes_list, return_list) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('Actor-Critic on {}' .format (env_name)) plt.show() mv_return = rl_utils.moving_average(return_list, 9 ) plt.plot(episodes_list, mv_return) plt.xlabel('Episodes' ) plt.ylabel('Returns' ) plt.title('Actor-Critic on {}' .format (env_name)) plt.show()
根据实验结果我们可以发现,Actor-Critic 算法很快便能收敛到最优策略,并且训练过程非常稳定,抖动情况相比 REINFORCE 算法有了明显的改进,这说明价值函数的引入减小了方差。
10.4 总结
本章讲解了 Actor-Critic 算法,它是基于值函数的方法和基于策略的方法的叠加。价值模块 Critic 在策略模块 Actor 采样的数据中学习分辨什么是好的动作,什么不是好的动作,进而指导 Actor 进行策略更新。随着 Actor 的训练的进行,其与环境交互所产生的数据分布也发生改变,这需要 Critic 尽快适应新的数据分布并给出好的判别。
Actor-Critic 算法非常实用,后续章节中的 TRPO、PPO、DDPG、SAC 等深度强化学习算法都是在 Actor-Critic 框架下进行发展的。深入了解 Actor-Critic 算法对读懂目前深度强化学习的研究热点大有裨益。
10.5 参考文献
[1] KONDA, V R, TSITSIKLIS J N. Actor-critic algorithms [C]// Advances in neural information processing systems, 2000.
11 TRPO 算法
11.1 简介
本书之前介绍的基于策略的方法包括策略梯度算法和 Actor-Critic 算法。这些方法虽然简单、直观,但在实际应用过程中会遇到训练不稳定的情况。回顾一下基于策略的方法:参数化智能体的策略,并设计衡量策略好坏的目标函数,通过梯度上升的方法来最大化这个目标函数,使得策略最优。具体来说,假设 θ \theta θ 表示策略 π θ \pi_\theta π θ 的参数,定义J ( θ ) = E π θ [ V π θ ( s 0 ) ] = E π θ [ ∑ t = 0 ∞ γ t r ( s t , a t ) ] \displaystyle J(\theta)=\mathbb{E}_{\pi_{\theta}}[V^{\pi_\theta}(s_0)]=\mathbb{E}_{\pi_{\theta}}[\sum_{t=0}^\infin \gamma^t r(s_t,a_t)] J ( θ ) = E π θ [ V π θ ( s 0 )] = E π θ [ t = 0 ∑ ∞ γ t r ( s t , a t )] ,基于策略的方法的目标是找到θ ∗ = arg max θ J ( θ ) \theta^*=\underset{\theta}{\argmax}J(\theta) θ ∗ = θ arg max J ( θ ) ,策略梯度算法主要沿着 ∇ θ J ( θ ) \nabla_\theta J(\theta) ∇ θ J ( θ ) 方向迭代更新策略参数θ \theta θ 。但是这种算法有一个明显的缺点:当策略网络是深度模型时,沿着策略梯度更新参数,很有可能由于步长太长,策略突然显著变差,进而影响训练效果。
针对以上问题,我们考虑在更新时找到一块信任区域 (trust region),在这个区域上更新策略时能够得到某种策略性能的安全性保证,这就是信任区域策略优化 (trust region policy optimization,TRPO)算法的主要思想。TRPO 算法在 2015 年被提出,它在理论上能够保证策略学习的性能单调性,并在实际应用中取得了比策略梯度算法更好的效果。
11.2 策略目标
策略梯度优化目标有两种表达形式:J ( θ ) = E π θ [ V π θ ( s 0 ) ] J(\theta) = \mathbb{E}_{\pi_{\theta}}[V^{\pi_\theta}(s_0)] J ( θ ) = E π θ [ V π θ ( s 0 )]
第一种是根据cumulative discounted reward设计的,形式为:J ( θ ) = E τ ∼ p θ ( τ ) [ ∑ t ∞ γ t r ( s t , a t ) ] \displaystyle J(\theta) = \mathbb{E}_{\tau \sim p_{\theta}(\tau)} [\sum_t^{\infin} \gamma^t r(s_t, a_t)] J ( θ ) = E τ ∼ p θ ( τ ) [ t ∑ ∞ γ t r ( s t , a t )]
因为 V π θ ( s ) = E a ∼ π θ ( s ) [ Q π θ ( s , a ) ] = E a ∼ π θ ( s ) [ E τ ∼ p θ ( τ ) [ ∑ s k = s , a k = a ∑ t = k ∞ γ t − k r ( s t , a t ) ] ] \displaystyle V^{\pi_\theta}(s) = \mathbb{E}_{a\sim \pi_\theta(s)}[Q^{\pi_\theta}(s,a)] = \mathbb{E}_{a\sim \pi_\theta(s)}\Big[\mathbb{E}_{\tau \sim p_{\theta}(\tau)} [\sum_{s_k=s, a_k=a} \sum_{t=k}^{\infin} \gamma^{t-k} r(s_t,a_t)]\Big] V π θ ( s ) = E a ∼ π θ ( s ) [ Q π θ ( s , a )] = E a ∼ π θ ( s ) [ E τ ∼ p θ ( τ ) [ s k = s , a k = a ∑ t = k ∑ ∞ γ t − k r ( s t , a t )] ]
第二种是根据s 0 s_0 s 0 的distribution来设计的,形式为:J ( θ ) = E s 0 ∼ p θ ( s 0 ) [ V π θ ( s 0 ) ] J(\theta) = \mathbb{E}_{s_0 \sim p_{\theta}(s_0)}[V^{\pi_\theta}(s_0)] J ( θ ) = E s 0 ∼ p θ ( s 0 ) [ V π θ ( s 0 )]
假设当前策略为π θ \pi_\theta π θ ,参数为θ \theta θ 。我们考虑如何借助当前的θ \theta θ 找到一个更优的参数θ ′ \theta' θ ′ ,使得J ( θ ′ ) ≥ J ( θ ) J(\theta')\ge J(\theta) J ( θ ′ ) ≥ J ( θ ) 。具体来说,由于初始状态s 0 s_0 s 0 的分布和策略无关,因此上述策略π θ \pi_\theta π θ 下的优化目标J ( θ ) J(\theta) J ( θ ) 可以写成在新策略π θ ′ \pi_{\theta'} π θ ′ 的期望形式:
s 0 s_0 s 0 只与环境有关,所以s 0 s_0 s 0 的概率与根据策略π θ \pi_{\theta} π θ 采样得到的轨迹τ \tau τ 中的第一个状态s 0 s_0 s 0 的概率是一样的。
这样就可以对期望的因子进行换元,由 s 0 s_0 s 0 变成 τ \tau τ ,然后采 τ \tau τ 里的 s 0 s_0 s 0 。
J ( θ ) = E s 0 ∼ p ( s 0 ) [ V π θ ( s 0 ) ] = E τ ∼ p θ ′ ( τ ) [ V π θ ( s 0 ) ] = E τ ∼ p θ ′ ( τ ) [ V π θ ( s 0 ) + ∑ t = 1 ∞ γ t V π θ ( s t ) − ∑ t = 1 ∞ γ t V π θ ( s t ) ] = E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t V π θ ( s t ) − ∑ t = 1 ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ ∑ t = 1 ∞ γ t V π θ ( s t ) − ∑ t = 0 ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t + 1 V π θ ( s t + 1 ) − ∑ t = 0 ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t ( γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ] \begin{aligned}
J(\theta) &= \mathbb{E}_{s_0\sim p(s_0)} [V^{\pi_\theta}(s_0)]\\
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} [V^{\pi_\theta}(s_0)]\\
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[V^{\pi_\theta}(s_0) + \sum_{t=1}^\infin \gamma^t V^{\pi_{\theta}}(s_t) - \sum_{t=1}^\infin \gamma^t V^{\pi_{\theta}}(s_t)\bigg]\\
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^tV^{\pi_\theta}(s_t) - \sum_{t=1}^\infin \gamma^t V^{\pi_{\theta}}(s_t)\bigg]\\
&= -\mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=1}^\infin \gamma^t V^{\pi_{\theta}}(s_t) - \sum_{t=0}^\infin \gamma^tV^{\pi_\theta}(s_t)\bigg]\\
&= -\mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^{t+1} V^{\pi_{\theta}}(s_{t+1}) - \sum_{t=0}^\infin \gamma^tV^{\pi_\theta}(s_t)\bigg]\\
&= -\mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^t \Big(\gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_{\theta}}(s_t)\Big)\bigg]\\
\end{aligned}
J ( θ ) = E s 0 ∼ p ( s 0 ) [ V π θ ( s 0 )] = E τ ∼ p θ ′ ( τ ) [ V π θ ( s 0 )] = E τ ∼ p θ ′ ( τ ) [ V π θ ( s 0 ) + t = 1 ∑ ∞ γ t V π θ ( s t ) − t = 1 ∑ ∞ γ t V π θ ( s t ) ] = E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t V π θ ( s t ) − t = 1 ∑ ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ t = 1 ∑ ∞ γ t V π θ ( s t ) − t = 0 ∑ ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t + 1 V π θ ( s t + 1 ) − t = 0 ∑ ∞ γ t V π θ ( s t ) ] = − E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t ( γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ]
基于以上等式,我们可以推导新旧策略的目标函数之间的差距:
前面用第一种形式(cumulative discounted reward)展开,后面一个根据上面推导的形式展开
J ( θ ′ ) − J ( θ ) = E s 0 [ V π θ ′ ( s 0 ) ] − E s 0 [ V π θ ( s 0 ) ] = E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t r ( s t , a t ) ] + E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t ( γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ] = E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t ( r ( s t , a t ) + γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ] \begin{aligned}
J(\theta') - J(\theta)
&= \mathbb{E}_{s_0}[V^{\pi_{\theta'}}(s_0)] - \mathbb{E}_{s_0}[V^{\pi_{\theta}}(s_0)]
\\
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)}\Big[ \sum_{t=0}^\infin \gamma^tr(s_t,a_t) \Big] + \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^t \Big(\gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_{\theta}}(s_t)\Big)\bigg]
\\
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^t \Big(r(s_t,a_t) + \gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_{\theta}}(s_t)\Big)\bigg]
\\
\end{aligned}
J ( θ ′ ) − J ( θ ) = E s 0 [ V π θ ′ ( s 0 )] − E s 0 [ V π θ ( s 0 )] = E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t r ( s t , a t ) ] + E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t ( γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ] = E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t ( r ( s t , a t ) + γ V π θ ( s t + 1 ) − V π θ ( s t ) ) ]
将时序差分残差定义为优势函数A A A :
= E τ ∼ p θ ′ ( τ ) [ ∑ t = 0 ∞ γ t A π θ ( s t , a t ) ] = ∑ t = 0 ∞ γ t E s t ∼ P t π θ ′ E a t ∼ π θ ′ ( ⋅ ∣ s t ) [ A π θ ( s t , a t ) ] = 1 1 − γ E s ∼ ν π θ ′ E a ∼ π θ ′ ( ⋅ ∣ s ) [ A π θ ( s , a ) ] \begin{aligned}
&= \mathbb{E}_{\tau \sim p_{\theta'}(\tau)} \bigg[\sum_{t=0}^\infin \gamma^t A^{\pi_\theta}(s_t,a_t)\bigg]
\\
&= \sum_{t=0}^\infin \gamma^t \mathbb{E}_{s_t \sim P_t^{\pi_{\theta'}}} \mathbb{E}_{a_t \sim \pi_{\theta'}(\cdot|s_t)} [A^{\pi_\theta}(s_t,a_t)]
\\
&= \dfrac{1}{1-\gamma} \mathbb{E}_{s \sim \nu^{\pi_{\theta'}}} \mathbb{E}_{a \sim \pi_{\theta'}(\cdot|s)} [A^{\pi_\theta}(s,a)]
\\
\end{aligned}
= E τ ∼ p θ ′ ( τ ) [ t = 0 ∑ ∞ γ t A π θ ( s t , a t