【深度强化学习】DDPG+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一样的初始化方法。)

希望能在评论区看到解答。

### Graphcore公司简介 Graphcore是一家成立于2016年的英国初创企业,专注于设计和开发专门用于加速机器学习工作负载的人工智能(AI)芯片——IPU(Intelligent Processing Unit)[^2]。这家公司在短时间内迅速崛起,在全球范围内获得了广泛关注和支持。 ### 技术产品:IPU智能处理器 #### 架构特点 IPU是一种专为高效执行复杂神经网络运算而优化的新一代AI处理器架构。它不仅能够支持诸如BERT-Large这样的大型预训练语言模型,而且成为继NVIDIA GPU和Google TPU之后第三个可以胜任此类任务的技术平台之一[^1]。 #### 性能优势 相比于传统GPU或CPU, IPU通过独特的片上存储系统以及大规模并行计算能力实现了更高的吞吐量与更低延迟特性;此外还具备强大的可扩展性和灵活性来适应不同规模的应用场景需求[^4]。 ```python # Python伪代码展示如何利用IPU进行深度学习推理 import popart # PopART库提供了Python接口访问Graphcore的Poplar SDK builder = popart.Builder() model_proto = builder.getModelProto() session_options = {"deviceType": "ipu_model"} with popart.Session(fnModel=model_proto, deviceInfo=session_options) as session: outputs = session.run(inputs) ``` #### 应用案例 在中国市场上,Graphcore与中国领先的开源深度学习框架PaddlePaddle合作推出了针对IPU优化过的解决方案,使得开发者可以在该平台上更便捷地部署自己的AI应用项目。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值