强化学习 7. 深度Q网络进阶技巧(实验部分)
前言
从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
《蘑菇书》原文(课本):https://datawhalechina.github.io/easy-rl/#/
(由于有时候公式太多,可能会直接贴图片)
蘑菇书的文章结构不会跟之前《动手学深度学习》按照原文章节进行,个人会适当调节。
0. 介绍
首先,我们的原式DQN采用上一章节实验的网络。如果不清楚的可以自行去上一章节查看。
我们同样是作用在CartPole-v1中,仅仅只是对DQN网络进行相对应的修改。
原DQN:
1 | import gym |
注意:每一种网络都是在单独的DQN代码上修改,并不是重复在同一个DQN代码中。
1. 双深度Q网络(double DQN,DDQN)
作用:避免Q 值过高估计(overestimation)
原始 DQN 的目标公式: \[ y =r+\gamma\max_{a'}Q_{target}(s',a') \] 在DDQN中,我们变成: \[ y =r+\gamma\max_{a'}Q_{target}(s',\arg \max_{a'}Q(s',a')) \] 这样选动作的Q函数与计算值的Q函数 不是同一个。可以减少 Q 值的过高估计。
代码中仅修改一行:

查看执行结果:

2.竞争深度Q网络(dueling DQN)
作用:让模型学“状态价值”和“动作优势”,提高效率。
有些状态根本不需要区分动作,却被迫学习,比如CartPole 中:杆子已经倒了 → 左右都一样烂
原网络是输出Q(s,a),而竞争DQN输出的是 A(s,a) 和 V(s):

这里我们变化的是原来的网络结构。

同时,我们的前向传播计算也要进行对应的修改。Dueling DQN 的定义(带零均值)是:


由于我们这里使用了
A.mean(dim=1),因此我们输入的维度必须也是符合A的形状的:A.shape=(batch,action_dim)。因此测试的时候,我们的state必须要添加前面一维batch维
查看修改后的效果

训练好了后,测试代码报错了,因此图片没连在一起。

3. 优先级经验回放(prioritized experience replay,PER)
根据 TD error 的绝对值来给每条经验池中的数据赋予优先级,误差越大的优先级越高,我们越优先使用。
作用:避免重要经验学不到(采样不均)
3.1 初始化
首先初始化添加新的等级队列。以及后续需要的参数:

3.2 push 添加新数据
经验池的push函数修改如下。这里 新样本默认是“最重要的”,因此新样本的等级是直接等于队列中最高那个。

3.3 sample 取出数据
如下图所示。

首先计算概率,我们利用如下公式将等级转换成我们的概率 \[
P(i)=\frac{p^\alpha_i}{\sum_jp^\alpha_j}
\] 然后按照我们计算出的概率,对 self.buffer
进行采样。这里我们。
其中
indices = np.random.choice(len(self.buffer), batch_size, p=probs)
指的是:从 [0, 1, 2, ..., len(self.buffer)-1]
这些索引中,按照概率 probs,随机采样
batch_size 个索引。
最后我们计算纠正偏差用的重要性采样权重。公式如下 \[ w_i=[(\frac{1}{N*P(i)})^\beta]/w_{max} \] β 初期比较小是为了稳定训练,随着训练慢慢变成1,主要是为了后期的无偏估计收敛。
然后就是训练中我们利用权重修改loss

我们算出来的权重是针对每条数据的,因此无法单独乘在最外面。因此针对每条数据,loss其实是td_error.pow(2),使用对应的权重w进行矫正后,将所有结果再进行平均得到最终loss
3.4 更新等级
我们在上一张图中的loss计算完后就需要执行他的等级更新函数。

我们用以下公式,根据TD误差重新分配等级 \[ p_i=|\delta_i|+\epsilon \]
3.5 查看效果
首先是我们固定了 β=0.4 的结果(一开始没加,后来更新代码才加上β趋于1的)

接下来就是 β趋于1的代码

4. n-step DQN
作用:解决奖励传播太慢的问题
我们这里的修改很简单,从原来的一步时序差分,改成多步时序差分。我这里选的是3步。
首先增加 n-step buffer

然后是修改 push 函数。只有存够了 n 条数据,才会开始计算累计奖励。(上面初始化的时候队列大小为3,因此下面append多出来的时候,旧的就会挤出去)

最后修改target的计算。(因为是3步,因此根据公式,
self.GAMMA 的指数为3)


查看结果

5. 噪声网络(noisy net)
作用:更“聪明”的探索
我们不再使用 ε-贪心 算法来随机采样动作。而是往网络中添加噪音来达到随机采样动作的目的。
5.1 噪音附加功能函数
首先添加新函数,给输入的网络层添加噪音:
1 | def add_noise_to_linear(self, layer, sigma_init=0.5): |
我们给输入的网络层添加了 layer.weight_sigma 、
layer.bias_sigma 和
layer.weight_epsilon、layer.bias_epsilon
共4个参数。其中前两个是决定噪音强度/噪音大小的参数,而后面两个则是与网络层参数同样大小的噪音Tensor,这里初始化为0只是为了占位,在计算前会修改值。
torch.full((out_f, in_f), sigma_init / math.sqrt(in_f))
的意思是制作一个大小为 (out_f, in_f)
的tensor,其中每个参数值为:sigma_init / math.sqrt(in_f)
- 为什么值的内容长这样呢?
我们的目的是让噪声强度随着输入规模缩小,从而保证训练的稳定性,因此我们让 sigma_init 除以 math.sqrt(in_f) 。
因为随着输入的变多,每个输入叠加在一起的噪声也越多,导致最后的输出数值非常不稳定。所以我们对此进行限制。
- 为什么tensor的形状是 (out_f, in_f) 而不是(in_f, out_f) 呢?
因为 PyTorch 的计算式这样的: \[ y=x*w^T+b \] 我们可以查看自定义的网络验证这点
- 为什么要通过
layer.register_buffer添加参数,而不能像前面layer.weight_sigma =这样直接赋值吗?
nn.Parameter是 PyTorch 自动识别并注册的模型参数,会自动加入参数列表、参与训练、跟随 device,并被保存。 普通 Tensor 直接赋值不会被 PyTorch 管理:不会跟随 device、不会被保存、也不会出现在模型结构中。 因此,对于“不需要训练但需要参与计算和保存”的变量(如 ε 噪声),必须使用register_buffer手动注册,使其能够:我们这里注意到,强度参数
- 跟随
.to(device)- 被
state_dict()保存- 在模型中被正确管理(但不参与梯度更新)
layer.weight_sigma、layer.bias_sigma都是可学习的参数。前期由于TD error很大,根据以下公式 $$ \[\begin{aligned} Q&=f(u+\sigma·\epsilon)\\ L&=(Q-target)^2\\ \end{aligned}\]$$ Loss对原网络中的参数求导后,原网络参数进行相应调整,朝着梯度为0的方向逼近,即让loss变小的方向接近。
那么对于噪音参数 \(\sigma\) 来说,要想Loss减小,则 \(\sigma\) 的值减小,因为它意味着噪音的强度,噪音越强,loss越大。
训练初期,TD error 大,梯度幅度大,σ 更新幅度大,探索强。随着训练进行,TD error 变小,梯度减小,σ 自然收敛,探索减弱,策略逐渐稳定。
σ 的动态变化是 梯度驱动的自适应探索
然后,我们在初始化中,对网络层添加噪音功能:

5.2 噪音前向传播计算
1 | def noisy_forward(self,layer, x): |
我们在5.1中对目标层添加了4个新参数后,我们前向传播就能通过这几个变量,给参数加上噪音。
假设
fc1 = nn.Linear(3, 4), 那么在计算fc1(x)的时候实际等价于F.linear(x, fc1.weight, fc1.bias)。因此这里我们修改完
weight和bias之后,就直接利用这两者进行计算F.linear(x, weight, bias)。在反向传播的时候,因为weight = layer.weight + layer.weight_sigma * layer.weight_epsilon与原来层的layer.weight,所以是能够获得原网络参数的梯度的。
5.3 前向传播

5.4 重置噪音
对存在噪音参数的层,重置他们的噪音值。

5.5 DQN流程
首先我们去除原来的 ε-贪婪算法。直接选取网络最优动作

然后是训练流程中

课本中我们说是每个episode都需要固定噪声,然后在新一轮episde的时候再更新。这是正确的,但是我们DQN里面是off-policy的,因此我们可以每一步都更新噪声,毕竟我们是经验池中获取旧的数据进行训练,本来也不是同一轮episode的数据。如果是同策略的话,必须遵守每个episode都需要固定噪声这点。
5.6 查看效果
中间层噪声可以理解为特征层探索,类似:探索“策略内部特征”
而全部层加噪音则是有着极强的探索效果,虽然这样意味着不太稳定。
- 全部层添加噪音,每个episode固定噪音

- 中间层添加噪音,每个episode固定噪音

- 中间层添加噪音,每步变噪音

- 全部层添加噪音,每步变噪音

我们能看出,2种配置的结果比较符合我们的要求
- 每个episode固定噪音,仅中间层添加噪音
- 每步都重置噪音,全层添加噪音。
6. 分布式Q函数(distributional Q-function)
课本只是浅显的介绍了输出层从标量变成了分布:
原来DQN: Q(s)-> [action_dim]
分布式DQN: Q(s)->[action_dim, N_atoms]
但实际实现上有许多其他困难的知识点,难以理解实现。暂且没空继续弄(感觉时效比低)。但是根据课本中说的,分布式DQN的提升是我们这几种网络修改里面最大的,因此放这里占个位,以后有需要的时候再继续补充。

