前言
从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
《蘑菇书》原文(课本):https://datawhalechina.github.io/easy-rl/#/
(由于有时候公式太多,可能会直接贴图片)
蘑菇书的文章结构不会跟之前《动手学深度学习》按照原文章节进行,个人会适当调节。
1. 近端策略优化裁剪(PPO-clip)
1.1 环境介绍
我们和之前策略梯度一样,使用的环境是CartPole-v1。以方便我们了解从策略梯度到PPO我们做了什么。
状态空间:[ 小车位置,小车速度,杆子的角度,杆子的角速度
],4个连续值
动作空间: [左,右],2个离散值
接下来我们创建环境并设置需要的超参数:
1 2 3 4 5 6 env = gym.make("CartPole-v1" ) state_dim = env.observation_space.shape[0 ] action_dim = env.action_space.n policy = PolicyNet(state_dim, action_dim).to(device) num_episodes = 1000
接下来是我们的策略网络(其中的网络更新部分我们后面讲)
能看到其实跟上一节一样,也是隐藏层为128个神经元的3层感知机。
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 class PolicyNet (nn.Module): def __init__ (self, state_dim, action_dim ): super ().__init__() self .net = nn.Sequential( nn.Linear(state_dim, 128 ), nn.ReLU(), nn.Linear(128 , action_dim) ) self .optimizer = optim.Adam(self .parameters(), lr=3e-4 , weight_decay=1e-4 ) self .ppo_epochs = 10 self .gamma = 0.99 self .clip_eps = 0.2 def forward (self, x ): logits = self .net(x) return torch.softmax(logits, dim=-1 ) def update (self,rewards,states,actions,log_probs_old ): def compute_returns (rewards, gamma ): G = 0 returns = [] for r in reversed (rewards): G = r + gamma * G returns.insert(0 , G) return torch.tensor(returns, dtype=torch.float32) returns = compute_returns(rewards, self .gamma).to(device) advantages = (returns - returns.mean()) / (returns.std() + 1e-8 ) states = torch.stack(states) actions = torch.stack(actions) log_probs_old = torch.stack(log_probs_old) for _ in range (self .ppo_epochs): probs = self .forward(states) dist = torch.distributions.Categorical(probs) log_probs = dist.log_prob(actions) ratios = torch.exp(log_probs - log_probs_old) surr1 = ratios * advantages surr2 = torch.clamp(ratios, 1 - self .clip_eps, 1 + self .clip_eps) * advantages loss = -torch.min (surr1, surr2).mean() self .optimizer.zero_grad() loss.backward() self .optimizer.step()
1.2 算法流程
跟上一节一样,我们第一步收集轨迹链,第二步利用收集的轨迹链进行更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def train (): for episode in range (num_episodes): states = [] actions = [] rewards = [] log_probs_old = [] state, _ = env.reset() collect_datas(env,state, states, actions, rewards, log_probs_old) policy.update( rewards, states, actions, log_probs_old) if episode % 50 == 0 : print (f"Episode {episode} , Reward: {sum (rewards)} " ) env.close() torch.save(policy.state_dict(), "ppo_pure_cartpole.pth" ) print ("Saved to ppo_pure_cartpole.pth" ) return policy
1.3 轨迹链收集
我们在之前1.2中能看到,我们轨迹中主要就是收集这几个列表
1 2 3 4 states = [] actions = [] rewards = [] log_probs_old = []
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def collect_datas (env,init_state,states,actions,rewards,log_probs_old ): state=init_state done = False while not done: state_tensor = torch.tensor(state, dtype=torch.float32).to(device) probs = policy(state_tensor) dist = torch.distributions.Categorical(probs) action = dist.sample() log_prob = dist.log_prob(action) next_state, reward, terminated, truncated, _ = env.step(action.item()) done = terminated or truncated states.append(state_tensor) actions.append(action) rewards.append(reward) log_probs_old.append(log_prob.detach()) state = next_state
1.4 网络更新
其实也就是之前我们网络定义里面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 def update (self,rewards,states,actions,log_probs_old ): def compute_returns (rewards, gamma ): G = 0 returns = [] for r in reversed (rewards): G = r + gamma * G returns.insert(0 , G) return torch.tensor(returns, dtype=torch.float32) returns = compute_returns(rewards, self .gamma).to(device) advantages = (returns - returns.mean()) / (returns.std() + 1e-8 ) states = torch.stack(states) actions = torch.stack(actions) log_probs_old = torch.stack(log_probs_old) for _ in range (self .ppo_epochs): probs = self .forward(states) dist = torch.distributions.Categorical(probs) log_probs = dist.log_prob(actions) ratios = torch.exp(log_probs - log_probs_old) surr1 = ratios * advantages surr2 = torch.clamp(ratios, 1 - self .clip_eps, 1 + self .clip_eps) * advantages loss = -torch.min (surr1, surr2).mean() self .optimizer.zero_grad() loss.backward() self .optimizer.step()
第一步,回报的计算以及将回报转换成优势
1 2 returns = compute_returns(rewards, self .gamma).to(device) advantages = (returns - returns.mean()) / (returns.std() + 1e-8 )
第二步,我们要将原来的列表转换成
nx1的矩阵形式(后面才能放入到网络中计算)
1 2 3 states = torch.stack(states) actions = torch.stack(actions) log_probs_old = torch.stack(log_probs_old)
第三步,利用这些数据进行更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for _ in range (self .ppo_epochs): probs = self .forward(states) dist = torch.distributions.Categorical(probs) log_probs = dist.log_prob(actions) ratios = torch.exp(log_probs - log_probs_old) surr1 = ratios * advantages surr2 = torch.clamp(ratios, 1 - self .clip_eps, 1 + self .clip_eps) * advantages loss = -torch.min (surr1, surr2).mean() self .optimizer.zero_grad() loss.backward() self .optimizer.step()
首先根据相同的state,算出action的分布,然后根据旧策略action中的值,获取对应的概率 ,转换成log_probs。
1 2 3 probs = self .forward(states) dist = torch.distributions.Categorical(probs) log_probs = dist.log_prob(actions)
第四步,是算出我们的重要性
1 ratios = torch.exp(log_probs - log_probs_old)
即 \[
\frac{p_\theta(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}
\]
通常我们会将该除法利用以下公式进行转换(变成我们代码中的式子)
我们做的不单单是公式的转换,我们还有其他许多好处,其中最重要的就是用更稳定的方式计算“新旧策略的概率比值(ratio)”,避免数值问题。因为强化学习里概率往往
非常小(接近 0)。直接相除容易数值不稳定、下溢(变成 0)、梯度爆炸。
第五步,将重要性和优势相乘,然后根据最大值最小值进行裁剪,获取所有结果的平均值。
1 2 3 surr1 = ratios * advantages surr2 = torch.clamp(ratios, 1 - self .clip_eps, 1 + self .clip_eps) * advantages loss = -torch.min (surr1, surr2).mean()
最后一步就是进行求导,然后更新
1 2 3 self .optimizer.zero_grad()loss.backward() self .optimizer.step()
我们反复进行此过程一共self.ppo_epochs次(这里我用的10次)。用同一组数据更新完10次后,我们再从环境中抓取新的数据。
1.5 查看效果
main 函数和展示函数
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 def render (policy, episodes=5 ): env = gym.make("CartPole-v1" , render_mode="human" ) for ep in range (episodes): state, _ = env.reset() done = False total_reward = 0 while not done: state_tensor = torch.tensor(state, dtype=torch.float32).to(device) with torch.no_grad(): probs = policy(state_tensor) action = torch.argmax(probs).item() state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated total_reward += reward print (f"Episode {ep} , Reward: {total_reward} " ) env.close() def load_policy (model_path ): state_dim = 4 action_dim = 2 policy = PolicyNet(state_dim, action_dim) policy.load_state_dict(torch.load(model_path)) policy.to(device) policy.eval () return policy if __name__ == "__main__" : policy = train() policy = load_policy("ppo_pure_cartpole.pth" ) render(policy)
我们能发现再150的时候就出现了500分的一次。后续大概600
episode的时候才稳点了点。最终测试也是5次都满分,其实效果还行。