PPO2代码 pytorch框架

Posted 方土成亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PPO2代码 pytorch框架相关的知识,希望对你有一定的参考价值。

PPO2代码玩gym库的Pendulum环境

2022-8-02更新

我发现这篇文章浏览量惨淡啊。

咋滴,是不相信的我代码能用是吗?

所以,我给出reward的收敛曲线图:

开玩笑,出来混,我能卖你生瓜码子吗?

——————————————————这里分割线————————————————

xdm,时隔一年,今天终于走到了莫烦视频的最后一章——PPO,不得不说,我自己个人亲历,感觉PPO比其他基础算法(PG、DQN、A2C、DDPG)都要难点儿。我之前的关于A2C等等博客直接都是给出代码就不管事了,但是,今天,这篇针对PPO2的博客,我们既谈算法~~~~~也抠代码!!!!!

- PPO与PPO2的区别:

首先说明一下,近端策略优化(Proximal Policy Optimization,PPO)这里,其实是分为:

1.DeepMind公司发明的算法PPO;
2.OpenAI公司发明的算法PPO2;

PPO、PPO2都是母体置信域策略优化(Trust Region Policy Optimization,TRPO)的变种(即改进版),它们两个的区别,只差在更新公式上。

如果你神经网络的参数θ是按照下式更新,你就是PPO:

KL是指计算KL散度,β是个系数。

而如果你神经网络的参数θ是按照下式更新,你就是PPO2:

clip是python里内置的限幅函数,clip(r,1-ε,1+ε):r大于1+ε,r就取到1+ε;r小于1-ε,r就取到1-ε。

我们的这一篇博客的讲解和代码都只针对于PPO2。为什么我不讲PPO,只讲PPO2?因为经过亲测PPO2的训练效果要优于PPO,所以我们就没有必要讲一个差的算法。毕竟,码字是真滴累!

算法区

在开始算法讲解之前,请让我先对你们发出灵魂一问 (●˙ε˙●) :emmmm,你觉得PPO2是在线的还是离线的?

答案后面公布。

现在开始算法讲解:

  • PPO2是什么?

你说它仅是一条公式,没错!你说它是一整份代码,也没错!

如果PPO2是放在论文中,那PPO2就是一个公式,公式长这样:

那么我只需要讲清楚这个公式即可。

但如果PPO2是放在代码里,那PPO2就是一个涵盖从创建神经网络→choose_action函数→step函数→learn函数等等的一系列代码。

所以,今天我们讲的PPO2,不只讲公式,我们会讲整个使用PPO2玩Pendulum环境的所有环节,从创建网络开始,到test测试结束。

PPO2是一种基于Actor-Critic框架的算法,所以,它的代码组成与A2C类似(不是很清楚A2C?这篇文章给你整得明明白白)。PPO2的critic部分和A2C的critic部分是完全一样的,两者唯一的不同之处在于actor部分。我们先给出A2C算法的计算流程图,如下图所示:

1.A2C的critic部分V现实减去V估计就是td-e,对td_e进行误差反向传递,使其越小越好;
2.actor部分:由得到的td-e进行学习,编写公式l

接下来,我们从A2C的算法流程图类比画出PPO2的算法流程图,如下图所示:

从上面的PPO2流程图它完美的讲述了PPO2算法干了什么,也完美地体现出了它与A2C算法的区别与联系,建议大家多花点时间看看这两幅流程图。我们可以看出来,PPO2算法一共需要定义三个神经网络,其中的actor部分网络有两个,一个是老策略pi_old,另一个是新策略pi_new;critic部分网络就只有一个,并且这一部分与传统的A2C一模一样,这就意味着这里的代码编写也是一样的。对上图整理一下:

1.PPO2算法的第一步:

那肯定是先定义三个神经网络了,它们分别是A_old、A_new、Critic_net

2.第二步:

把状态s作为输入,输给A_old网络,网络会生成正态分布的均值和标准差,此策略用来选择动作,只需要对此正太分布采样即可得到动作值,这一步也就是在编写我们的choose_action函数。

3.第三步:

把未来状态s_作为输入,输给Critic_net网络,此网络会生成动作的价值(Q值),把这个Q值配合上奖励reward值就可以计算累积期望回报,为了避免在无限时界下累积期望回报趋于无穷大,我们就引入衰减因子γ,把其改写为一个标准的贝尔曼方程的形式,即可编写出代码:

v_observation_ = reward + GAMMA * v_observation_

v_observation_ 是现实Q值,再把状态s作为输入,输给Critic_net网络得到Q估计值,Q现实值减去Q估计值就是td_e。td_e既要进行critic部分的梯度下降,还要把td_e送给actor网络去计算actor网络的误差。

4.第四步:

把状态s作为输入,输给A_new网络,生成新的正态分布, pi_old/pi_newa可以得到ratio,根据PPO2的公式编写代码:

ratio=torch.exp(action_new_logprob - bap.detach())
surr1 = ratio * td_e
surr2 = torch.clamp(ratio, 1 - 0.2,1 + 0.2) * td-e
a_loss = -torch.min(surr1, surr2)

把a_loss作为actor部分的loss反向传递即可。

5.第五步:

把pi_new网络的参数赋给pi_old网络。


PPO2的choose_action策略pi_old与learn策略pi_new两者互不关联,pi_old用于与环境交互生成数据供pi_new训练。所以,PPO2不必向policy gradient那样,在choose_action进行一次训练后,整个策略的参数全变了,必须从头开始再次choose_action收集新一批数据去训练,收集的数据仅能训练一次,这样效率就很低。当然,这也情有可原,因为毕竟PG是在线的,而PPO2它可是离线的,正因为PPO2的离线,它的choose_action策略与learn策略是两个不同的策略,由老策略pi_old执行一次choose_action函数收集的数据可以供learn函数训练n多次。

其实,只要你learn训练一次,你的策略参数必会改变,但由于在线算法全程就一个策略(choose_action策略μ就是learn策略π,π=μ),learn一次,choose_action策略与learn策略同时更新(因为在线算法中这两本来就是同一个策略),所以,全程只牵扯到一个概率分布p,分布p里采样的数据指导分布p做更新,这就很对!我们不需要做任何处理。

离线算法,它的choose_action策略μ与learn策略π是两个不同的策略,相当于你用一批从另一个与你不同的策略下收集的数据来训练你的当前策略。在训练的过程中,我们必须进行梯度反向传递,但这里梯度的计算是用learn策略π来算法,而离线算法的数据是来自choose_action策略μ的,两个策略就会牵扯到两个不同的分布,μ是p,π是q,分布q的梯度反向传递凭什么能与分布p采样的数据联系一起,并且还能产生效果?——————因为用到了重要性采样!!!正是因为PPO2里的重要性采样对策略π和策略μ之间有修正处理,所以PPO2就可以用离线的方式————choose_action采样一次数据,learn训练好几次。

那么谈及重要性采样,我们就顺便讲讲什么是重要性采样!!!!

在解释什么是重要性采样之前,我们先搞清楚一点——采样的目的是什么?————目的是评估一个函数在某个分布上的期望值,也就是:期望E=[f(x)],x服从分布p。

  • 重要性采样

    一句话概括:重要性采样可以让我们从本来没办法进行采样的分布p,把它转化为从另一个分布q进行采样(转化方法就是乘以重要性权值ρ)。

  • 重要性采样的缺陷
    重要性采样虽然可以把一个p分布采样的问题,转换为一个从q分布采样,且可以保证这两个分布下的期望值都一样,说明重要性采样是无偏的。但是虽然期望一样,可方差却不一样。p与q分布相差越大,方差也就越大,这对算法的收敛性会引起不好的效果,所以我们必须限制一下p与q的差异,不能让其差异过大。限制q与p,这就是PPO/PPO2算法做的事情。

  • 在/离线算法与重要性采样的关系
    在线算法要改为离线算法,怎么改?
    在线算法中,与环境交互的策略(choose_action)是π,进行采样可生成数据τ,用来训练学习的策略(learn)也是π。当π策略的参数θ学习更新一次,我们必须重新采样数据。
    改为离线,应该是,现有另一个策略μ与环境交互采样数据得到τ,收集的这一次数据可以供learn策略π去训练学习多次。策略π对应的分布为p,而策略μ对应的分布为q,在线算法中采样数据是对分布p进行采样,而在离散算法中,本来我们还是需要从分布p进行采样,但是由于离线算法和环境交互的策略是μ,所以我们采样数据就要从μ对应的分布q里面采。这要怎么操作?还记得上面提及的重要性采样吗?没错,你永远都可以使用重要性采样,把一个不能从分布p采样的问题,转化为从q采样。

    重要性权值为:ρ=p(x)/q(x)。明显的,在线算法里重要性权值ρ=p(x)/p(x)=1。

注意:PPO2并不是引入了重要性采样,PPO2是解决了重要性采样自带的缺陷问题。引入了重要性采样的算法称不上PPO2,但是解决了重要性采样缺陷问题的算法就称得上PPO2了。

还记得我上面说到的重要性采样的自身缺陷吗?“虽然期望一样,可方差却不一样。”方差不一样就意味着,你只有采样足够多次,才会出现期望一样的情况,采样次数增大意味着训练时间增长。实际中,我们受自己性格的影响,我们等不了那么久。训练20min我等的起,但你训练一次只要超过一个小时~~~我只能说,给爷爬吧你!跑个简单环境还要训练一个小时?

ok,为了解决这个问题:分布p与分布q不应该相差过大的问题。我们自然而然就想到了,如果能有一个数可以衡量出p、q对应的策略之间的距离就好了。那么有这么一个数吗?答案是:有!它就是KL散度(又名“相对熵”),p与q相差越大,KL散度就越大,所以我们只需要把KL散度当个惩罚项减去即可。而这,就是PPO算法!那PPO2是怎么做的?PPO2并不计算KL散度,它只对p与q之间的差异进行限幅处理,大于max取到max,小于min取到min,使得p与q的差异不是那么的大。

remark:

路人:很多论文上都说PPO2是在线算法,就你小子搁这说PPO2是离线算法,与大家格格不入,指’在’为’离’是吧????误人子弟是吧???

我:兄弟,听我解释。我也不是什么大人物,一个小小的硕士,我更是我们导师开辟的新领域(没什么人带,也并非师出名门),人微言轻的,对于强化学习领域的见解肯定不会都对,我之所以把PPO2当做离线算法,是因为我一直不解————为什么PPO2都用到重要性权值了,还要把它理解为在线的?我呢,人比较轴,只相信自己看到的,代码总是不会骗人的。代码:ratio=torch.exp(action_new_logprob - bap.detach()),这个重要性权值的表示它就摆在这,重要性采样与在线/离线的关系也摆在这。PPO2在线与离线,你们可以自己判断。

小黑子:搁这误人子弟是吧?你说我直接开喷呢,还是走流程喷?

我:小黑子你先别叫!!我就算PPO2的算法讲解全是错的,但是我的代码总没问题吧。其他的你可以不看,代码你可以直接拿去。

代码区

这篇博客存在意义:

1.完全是对莫烦PPO2代码TensorFlow框架的类比,只是把它转为pytorch框架,玩得是gym的Pendulum环境。

2.这个PPO2的代码编写,很符合我们传统对离线算法的定义。可以说这份PPO2代码,经典且标准!

至此,代码如下,拿走不谢,复制即用,不行砍我!

#开发者:Bright Fang
#开发时间:2022/6/28 23:02
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import gym
from matplotlib import pyplot as plt
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
env = gym.make('Pendulum-v0').unwrapped
'''Pendulum环境状态特征是三个,杆子的sin(角度)、cos(角度)、角速度,(状态是无限多个,因为连续),动作值是力矩,限定在[-2,2]之间的任意的小数,所以是连续的(动作也是无限个)'''
state_number=env.observation_space.shape[0]
action_number=env.action_space.shape[0]
max_action = env.action_space.high[0]
min_action = env.action_space.low[0]
torch.manual_seed(0)#如果你觉得定了随机种子不能表达代码的泛化能力,你可以把这两行注释掉
env.seed(0)
RENDER=False
EP_MAX = 1000
EP_LEN = 200
GAMMA = 0.9
A_LR = 0.0001
C_LR = 0.0003
BATCH = 128
A_UPDATE_STEPS = 10
C_UPDATE_STEPS = 10
METHOD = [
    dict(name='kl_pen', kl_target=0.01, lam=0.5),   # KL penalty
    dict(name='clip', epsilon=0.2),                 # Clipped surrogate objective, find this is better
][1]        # choose the method for optimization
Switch=0
'''由于PPO也是基于A-C框架,所以我把PPO的编写分为两部分,PPO的第一部分 Actor'''
'''PPO的第一步  编写A-C框架的网络,先编写actor部分的actor网络,actor的网络有新与老两个网络'''
class ActorNet(nn.Module):
    def __init__(self,inp,outp):
        super(ActorNet, self).__init__()
        self.in_to_y1=nn.Linear(inp,100)
        self.in_to_y1.weight.data.normal_(0,0.1)
        self.out=nn.Linear(100,outp)
        self.out.weight.data.normal_(0,0.1)
        self.std_out = nn.Linear(100, outp)
        self.std_out.weight.data.normal_(0, 0.1)
    '''生成均值与标准差,PPO必须这样做,一定要生成分布(所以需要mean与std),不然后续学习策略里的公式写不了,DDPG是可以不用生成概率分布的'''
    def forward(self,inputstate):
        inputstate=self.in_to_y1(inputstate)
        inputstate=F.relu(inputstate)
        mean=max_action*torch.tanh(self.out(inputstate))#输出概率分布的均值mean
        std=F.softplus(self.std_out(inputstate))#softplus激活函数的值域>0
        return mean,std
'''再编写critic部分的critic网络,PPO的critic部分与AC算法的critic部分是一样,PPO不一样的地方只在actor部分'''
class CriticNet(nn.Module):
    def __init__(self,input,output):
        super(CriticNet, self).__init__()
        self.in_to_y1=nn.Linear(input,100)
        self.in_to_y1.weight.data.normal_(0,0.1)
        self.out=nn.Linear(100,output)
        self.out.weight.data.normal_(0,0.1)
    def forward(self,inputstate):
        inputstate=self.in_to_y1(inputstate)
        inputstate=F.relu(inputstate)
        Q=self.out(inputstate)
        return Q
class Actor():
    def __init__(self):
        self.old_pi,self.new_pi=ActorNet(state_number,action_number),ActorNet(state_number,action_number)#这只是均值mean
        self.optimizer=torch.optim.Adam(self.new_pi.parameters(),lr=A_LR,eps=1e-5)
    '''第二步 编写根据状态选择动作的函数'''
    def choose_action(self,s):
        inputstate = torch.FloatTensor(s)
        mean,std=self.old_pi(inputstate)
        dist = torch.distributions.Normal(mean, std)
        action=dist.sample()
        action=torch.clamp(action,min_action,max_action)
        action_logprob=dist.log_prob(action)
        return action.detach().numpy(),action_logprob.detach().numpy()
    '''第四步  actor网络有两个策略(更新old策略)————————把new策略的参数赋给old策略'''
    def update_oldpi(self):
        self.old_pi.load_state_dict(self.new_pi.state_dict())
    '''第六步 编写actor网络的学习函数,采用PPO2,即OpenAI推出的clip形式公式'''
    def learn(self,bs,ba,adv,bap):
        bs = torch.FloatTensor(bs)
        ba = torch.FloatTensor(ba)
        adv = torch.FloatTensor(adv)
        bap = torch.FloatTensor(bap)
        for _ in range(A_UPDATE_STEPS):
            mean, std = self.new_pi(bs)
            dist_new=torch.distributions.Normal(mean, std)
            action_new_logprob=dist_new.log_prob(ba)
            ratio=torch.exp(action_new_logprob - bap.detach())
            surr1 = ratio * adv
            surr2 = torch.clamp(ratio, 1 - METHOD['epsilon'], 1 + METHOD['epsilon']) * adv
            loss = -torch.min(surr1, surr2)
            loss=loss.mean()
            self.optimizer.zero_grad()
            loss.backward()
            nn.utils.clip_grad_norm_(self.new_pi.parameters(), 0.5)
            self.optimizer.step()
class Critic():
    def __init__(self):
        self.critic_v=CriticNet(state_number,1)#改网络输入状态,生成一个Q值
        self.optimizer = torch.optim.Adam(self.critic_v.parameters(), lr=C_LR,eps=1e-5)
        self.lossfunc = nn.MSELoss()
    '''第三步  编写评定动作价值的函数'''
    def get_v(self,s):
        inputstate = torch.FloatTensor(s)
        return self.critic_v(inputstate)
    '''第五步  计算优势——————advantage,后面发现第五步计算出来的adv可以与第七步合为一体,所以这里的代码注释了,但是,计算优势依然算是可以单独拎出来的一个步骤'''
    # def get_adv(self,bs,br):
    #     reality_v=torch.FloatTensor(br)
    #     v=self.get_v(bs)
    #     adv=(reality_v-v).detach()
    #     return adv
    '''第七步  编写actor-critic的critic部分的learn函数,td-error的计算代码(V现实减去V估计就是td-error)'''
    def learn(self,bs,br):
        bs = torch.FloatTensor(bs)
        reality_v = torch.FloatTensor(br)
        for _ in range(C_UPDATE_STEPS):
            v=self.get_v(bs)
            td_e = self.lossfunc(reality_v, v)
            self.optimizer.zero_grad()
            td_e.backward()
            nn.utils.clip_grad_norm_(self.critic_v.parameters(), 0.5)
            self.optimizer.step()
        return (reality_v-v).detach()
if Switch==0:
    print('PPO2训练中...')
    actor=Actor()
    critic=Critic()
    all_ep_r = []
    for episode in range(EP_MAX):
        observation = env.reset() #环境重置
        buffer_s, buffer_a, buffer_r,buffer_a_logp = [], [], [],[]
        reward_totle=0
        for timestep in range(EP_LEN):
            if RENDER:
                env.render()
            action,action_logprob=actor.choose_action(observation)
            observation_, reward, done, info = env.step(action)
            buffer_s.append(observation)
            buffer_a.append(action)
            buffer_r.append((reward+8)/8)    # normalize reward, find to be useful
            buffer_a_logp.append(action_logprob)
            observation=observation_
            reward_totle+=reward
            reward = (reward - reward.mean()) / (reward.std() + 1e-5)
         #PPO 更新
            if (timestep+1) % BATCH == 0 or timestep == EP_LEN-1:
                v_observation_ = critic.get_v(observation_)
                discounted_r = []
                for reward in buffer_r[::-1]:
                    v_observation_ = reward + GAMMA * v_observation_
                    discounted_r.append(v_observation_.detach().numpy())
                discounted_r.reverse()
                bs, ba, br,bap = np.vstack(buffer_s), np.vstack(buffer_a), np.array(discounted_r),np.vstack(buffer_a_logp)
                buffer_s, buffer_a, buffer_r,buffer_a_logp = [], [], [],[]
                advantage=critic.learn(bs,br)#critic部分更新
                actor.learn(bs,ba,advantage,bap)#actor部分更新
                actor.update_oldpi()  # pi-new的参数赋给pi-old
                # critic.learn(bs,br)
        if episode == 0:
            all_ep_r.append(reward_totle)
        else:
            all_ep_r.append(all_ep_r[-1] * 0.9 + reward_totle * 0.1)
        print("\\rEp:  |rewards: ".format(episode, reward_totle), end="")
        #保存神经网络参数
        if episode % 50 == 0 and episode > 100:#保存神经网络参数
            save_data = 'net': actor.old_pi.state_dict(), 'opt': actor.optimizer.state_dict(), 'i': episode
            torch.save(save_data, "E:\\PPO2_model_actor.pth")
            save_data = 'net': critic.critic_v.state_dict(), 'opt': critic.optimizer.state_dict(), 'i': episode
            torch.save(save_data, "E:\\PPO2_model_critic.pth")
    env.close()
    plt.plot(np.arange(len(all_ep_r)), all_ep_r)
    plt.xlabel('Episode')
    plt.ylabel('Moving averaged episode reward')
    plt.show()
else:
    print('PPO2测试中...')
    aa=Actor()
    cc=Critic()
    checkpoint_aa = torch.load("E:\\PPO2_model_actor.pth")
    aa.old_pi.load_state_dict(checkpoint_aa['net'])
    checkpoint_cc = torch.load("E:\\PPO2_model_critic.pth")
    cc.critic_v.load_state_dict(checkpoint_cc['net'])
    for j in range(10):
        state = env.reset()
        total_rewards = 0
        for timestep in range(EP_LEN):
            env.render()
            action, action_logprob = aa.choose_action(state)
            new_state, reward, done, info = env.step(action)  # 执行动作
            total_rewards += reward
            state = new_state
        print("Score:", total_rewards)
    env.close()

代码用法:

先把Switch标志位赋为0,先训练,等待训练结束后画出图形。然后,把Switch标志为赋为1进行测试,就可以看到训练的效果了。神经网络的参数被我们保存在了路径:E盘里。所以,别告诉我,你的电脑没有E盘,没有就自己改路径。

remark:

记得有小伙伴儿问过这么一个问题:PPO/PPO2是不是没有经验池机制?

我的答案是:有!!!

PPO2凭什么会用到记忆库!??不信是不是?下面的PPO2代码是我从github上淘来的,人家还真就用到了记忆库,并且,最离谱儿的是,人家的PPO2算法还能收敛!!!

import argparse
from collections 强化学习基于tensorflow2.x 的 PPO2(离散动作情况) 训练 CartPole-v1

深度强化学习 PPO 模型解析,附Pytorch完整代码

强化学习⚠️手把手带你走进强化学习 2⚠️ OPP 算法实现月球登陆器 (PyTorch 版)

强化学习PPO算法

堆叠抓取+深度学习基于深度学习+PPO深度强化学习的堆叠物体抓取算法的MATLAB仿真

ChatGPT强化学习大杀器——近端策略优化(PPO)