文章目录
- 前言
- 遇到的困难
- 参考
- 最后实现情况
- 一、popart 是什么?(论文解读)
- 具体的理解
- 关于mappo原代码中debiasing_term
- 二、复刻popart
- 主要进行了什么操作?
- 1.art:
- 2.pop:
- 3.算法理解
- 4.上述未考虑的部分(关键)
- 三、代码实现
- popart技巧关键代码
- 疑问:
前言
本来是想根据论文和原代码复刻一遍MAPPO的,看完论文和代码后发现其运用到了很多收敛小技巧,这里在复刻第一个技巧popart时,遇到了很多困难,花了近两周时间才算是复刻成功。这里以RLz中的DDPG算法为基算法,加上popart技巧,来展示这个技巧如何添加到自己的其他RL算法中。
遇到的困难
1.参考1, 原论文并没提供原代码,参考2,3复刻的代码只告诉了对单个网络(Q网络或Critic网络)如何使用给出了代码。(并没有给出actor该如何更改)
2.参考4 mappo的实现巧妙利用ppo的actor的更新为近似的函数,不为-v。(后来复刻完后,发现在计算GAE时利用到v的计算)
mappo中r_mappo的179行
复刻关键点:denormalize
3.参考5,虽然readme上写是说在DDPG上实现了popart,可是下载后,进行跑了一遍Pendulum-v1的环境,一点没收敛,说明并未完成。
4.参考6 虽然是实现了,但是是tensorflow版本,不是pytorch版本。
参考
1.popart原论文
2.popart原论文的翻译和解读
3.popart原论文的实现
4.mappo原代码中关于popart的实现
5.github上未完成的DDPG_popart的实现
6.openai baselines中关于ddpg加入popart的tensorflow实现
最后实现情况
修改多次后(其他眼色),最后达到蓝色的收敛效果,其中黑色为原ddpg不加popart的效果。后对DDPG进行了修改,在 代码实现 中展示
环境为Pendulum-v1。
一、popart 是什么?(论文解读)
原论文写道:我们提出了一种方法来自适应地归一化学习更新中使用的目标。如果保证这些目标被规范化,则找到合适的超参数要容易得多。所提出的技术并不特定于 DQN,更普遍地适用于监督学习和强化学习。
即:Preserving Outputs Precisely, while Adaptively Rescaling Targets,自适应归一化目标的同时,保证精确的输出。
此算法是为了解决奖励跨度大的问题:例:吃豆人游戏中吃一个鬼魂有1600的奖励,而其他的奖励则很小。(之前有提出的办法是将奖励裁剪到(-1,1))
实验结果:DoubleDQN_popart 在Atari 57 games 中 使用DDQN_popart 在32个游戏中表现比DDQN的效果要好,(有一个是相等成绩),结果论文提及如下:
具体的理解
在参考2,3中已经解释的很详细了,可参考。
关于mappo原代码中debiasing_term
mappo代码:popart.py#L27
在原论文popart中的此处有提及,这里debiasing_term 的定义和下文的z_t的定义基本一致。(疑问:但是以下论文中并未提及该如何在popart中消除这个Adam初始化的影响,可能这里的mappo代码这里是解决了这个问题)
二、复刻popart
主要参考2,3的解释和代码实现,讲的十分好。
主要理解几个关键点:
论文目的:为了解决现有reward_clip解决方法并不能有效解决reward跨度大的问题
所以这里的技巧主要是与reward相关的操作。
主要进行了什么操作?
解决reward跨度大:使用归一化目标函数。
1.art:
动态归一化目标函数,在这里即Q_target,这个操作有点类似于在PPO中加入对reward进行nomalization技巧的操作。(但是两者的操作的地方不一致,前者是在更新中直接对target操作,后者是在对环境中step后的reward进行操作)两者trick的目标有些类似。(这样说比较好理解算法。)
有点类似于滑动平均的取mean和std
由于上述的归一化target,有可能出现上一批的taget范围为[-1,1]归一化,而后一批taget的范围为[-10,10]归一化,那么原来给出的归一化范围可能会受到影响,为了解决这个问题,就干脆在原有Q网络的模型上后面再加一层要训练的网络,使这一层的weight和bias也一起更新,使得这个模型输出的范围和target的范围在差不多的范围内。(如下:见参考2)
2.pop:
保证精确的输出,即为了保证原函数拟合的结果,在原有的模型上再加一层线性层,然后使用合适的更新方法,就可以保证和原来一样的拟合结果(即加入此线性层更新后,原函数(无此线性层的函数)更新后的weight和bias不变)。
合适的更新方式为如下:
在参考3中证明了这点。(好像不是,是证明了提议3,这里是提议1)
3.算法理解
popart实现: Preserving Outputs Precisely, while Adaptively Rescaling Targets
1.初始化W(权重) = I , b(偏差) = 0, sigma(标准差) = 1 ,u(均值,代码写作mu) = 0
2.Use Y to compute new scale sigma_new and new shift mu_new
3.W = W * sigma / sigma_new b = (sigma * b + mu - mu_new) / sigma_new ; sigma = sigma_new mu = mu_new
4.得到拟合函数Q(s)的值
5.loss = W(Q(s))+b - (target_Q-mean)/std
5.用梯度下降更新Q的参数,再更新W和b的参数
根据上述1,2 两点,不难看出这里的计算归一化损失两者都是标准化过后的,W,b是标准化线性层,减均值除标准差也是标准化过程。
至于这句话,意思应该就是允许h函数可更新。
4.上述未考虑的部分(关键)
考虑到的部分:
1.用art来滑动平均取mean和std
2.原来的critic的函数不变,再新加入一层新的popart线性层当作输出层,此输出层的目的为标准化Q值输出。
3.更新时,先更新critic函数参数,再更新最后一层的线性层。
4.若是ppo,则在计算advantange时,若是以popart线性层当作输出,则应该反归一化其值->原始值来计算。
未考虑到的部分:
1.使用actor时,原本不加popart时,loss为-Q(以ddpg为例),加入后,若是以popart线性层当作输出,则应该反归一化其值->原始值,来计算。而不是单单用popart线性层上一层的Q函数来拟合。
(单单用popart线性层上一层的Q函数来当loss的-Q中的Q,效果如下)
原因:在上述计算loss时是用的标准化后的值,并且之后更新,其原始Q函数的规模(或范围)已经发生改变,若此时输出Q的值,就不正确了,而正确的Q(加入popart前的q)的计算方式应该为反归一化其现在线性层输出值。
2.ddpg使用了target函数来减小Q的过高估计,这里参考3,参考4,并没有此值,需要加入一个target的critic函数输出加入到popart线性层的类。
三、代码实现
为了展示popart对ddpg的核心部分修改了哪些,这里使用简单版本的DDPG_simple.py未基础,在此算法上加入popart技术来展示。
加入popart算法的代码在:DDPG_simple_with_tricks.py(欢迎star)
popart技巧关键代码
class UpperLayer(nn.Module):
def __init__(self, H, n_out):
super(UpperLayer, self).__init__()
self.output_linear = nn.Linear(H, n_out)
'''1.初始化W(权重) = I , b(偏差) = 0, sigma(标准差) = 1 ,u(均值,代码写作mu) = 0'''
nn.init.ones_(self.output_linear.weight) # W = I
nn.init.zeros_(self.output_linear.bias) # b = 0
def forward(self, x):
return self.output_linear(x)
class PopArt:
def __init__(self, mode, LowerLayers, LowerLayers_target,H, n_out, critic_lr):
super(PopArt, self).__init__()
self.mode = mode.upper() # 大写
assert self.mode in ['ART', 'POPART'], "Please select mode from 'Art' or 'PopArt'."
self.lower_layers = LowerLayers
self.lower_layers_target = LowerLayers_target
self.upper_layer = UpperLayer(H, n_out).to(device)
self.sigma = torch.tensor(1., dtype=torch.float) # consider scalar first
self.sigma_new = None
self.mu = torch.tensor(0., dtype=torch.float)
self.mu_new = None
self.nu = self.sigma**2 + self.mu**2 # second-order moment
self.beta = 10. ** (-0.5)
self.lr = 1e-3
self.loss_func = torch.nn.MSELoss()
self.loss = None
self.opt_upper = torch.optim.Adam(self.upper_layer.parameters(), lr = self.lr)
self.opt_lower = torch.optim.Adam(self.lower_layers.parameters(), lr = critic_lr)
def art(self, y):
'''2.Use Y to compute new scale sigma_new and new shift mu_new 相当于滑动式更新均值和标准差'''
self.mu_new = (1. - self.beta) * self.mu + self.beta * y.mean()
self.nu = (1. - self.beta) * self.nu + self.beta * (y**2).mean()
self.sigma_new = torch.sqrt(self.nu - self.mu_new**2)
def pop(self):
'''3.W = W * sigma_new / sigma b = (sigma * b + mu - mu_new) / sigma_new ; sigma = sigma_new , mu = mu_new '''
relative_sigma = (self.sigma / self.sigma_new)
self.upper_layer.output_linear.weight.data.mul_(relative_sigma)
self.upper_layer.output_linear.bias.data.mul_(relative_sigma).add_((self.mu-self.mu_new)/self.sigma_new)
def update_stats(self):
# update statistics
if self.sigma_new is not None:
self.sigma = self.sigma_new
if self.mu_new is not None:
self.mu = self.mu_new
def normalize(self, y):
return (y - self.mu) / self.sigma
def denormalize(self, y):
return self.sigma * y + self.mu
def backward(self):
'''4.更新拟合函数theta
5.用梯度下降更新W,b的参数
'''
self.opt_lower.zero_grad()
self.opt_upper.zero_grad()
self.loss.backward()
def step(self):
torch.nn.utils.clip_grad_norm_(self.lower_layers.parameters(), 0.5)
self.opt_lower.step()
torch.nn.utils.clip_grad_norm_(self.upper_layer.parameters(), 0.5)
self.opt_upper.step()
def forward(self, o,a, y):
if self.mode in ['POPART', 'ART']:
self.art(y)
if self.mode in ['POPART']:
self.pop()
self.update_stats()
y_pred = self.upper_layer(self.lower_layers(o,a))
self.loss = self.loss_func(y_pred, self.normalize(y))
self.backward()
self.step()
return self.loss , self.lower_layers
def output(self, x, u):
return self.upper_layer(self.lower_layers(x, u))
def output_target(self, x, u):
return self.upper_layer(self.lower_layers_target(x, u))
其中self.nu 论文中未明确(论文中的符号为vt) 但是说明 vt - µ^2 is positive
于是这里选择了参考3中的代码self.sigma**2 + self.mu**2
,而mappo中则初始化这个值为0。
在Pendulum-v1环境下效果如下:两者参数一致(其中 sigma=1 batch_size=265)
黑色为原DDPG_simple,而蓝色为初始nu为0,黄色初始化为self.sigma2 + self.mu2,可见两者差别并不大。
换成MountainCarContinuous-v0下测试:黄色为原ddpg_simple,紫色为加入popart后。
(参数均一致,其中sigma=1,batch_size=64)
效果如下:发现效果竟不如原来的算法。
self.nu = self.sigma**2 + self.mu**2 # second-order moment 二阶矩 用于计算方差
self.beta = 10. ** (-0.5)
self.lr = 1e-3 #10. ** (-2.5)
将上述的self.opt_upper = torch.optim.Adam(self.upper_layer.parameters(), lr = self.lr)的最后一层学习率改成和论文中一样的10**(-2.5)
self.nu = self.sigma**2 + self.mu**2 # second-order moment 二阶矩 用于计算方差
self.beta = 10. ** (-0.5)
self.lr = 10. ** (-2.5)
效果如下:为下图的橙色
将lr改回1e-3,beta改成0.99999
self.nu = self.sigma**2 + self.mu**2 # second-order moment 二阶矩 用于计算方差
self.beta = 0.99999#10. ** (-0.5)
self.lr = 1e-3 #10. ** (-2.5)
效果如下,为下图的的粉色:
self.nu = 0#self.sigma**2 + self.mu**2 # second-order moment 二阶矩 用于计算方差
self.beta = 0.99999#10. ** (-0.5)
self.lr = 1e-3 #10. ** (-2.5)
效果为如下的蓝色
疑问:
这里的MountainCarContinuous-v0环境的奖励如下:
到达目的地后得到一个大的奖励值,和前文论文中说要解决的问题是同种问题(奖励的差值大),可是效果最终测试还是不如原版。
论文中加入此算法的展示效果为在一半的环境下有增益效果。
原因可能是这里使用了Adam,并没有使用SGD?
(使用Adam可能需要使用和参考4一样的初始化方法。)
希望能在评论区看到解答。