ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch实现PPO训练LunarLander:从原理到踩坑全记录

PyTorch实现PPO训练LunarLander:从原理到踩坑全记录 简介PyTorch-LunarLander是一份基于PyTorch框架的PPO算法实现代码包定位为深度强化学习入门项目面向希望理解策略梯度方法并动手实践的开发者解决在经典LunarLander-v2环境中让智能体学会着陆的控制问题。包内仅5KB包含4个Python脚本分别对应Actor-Critic模型定义、PPO核心更新逻辑、并行环境采样与结果绘图结构精简便于逐行阅读和二次开发。该项目已有1607人学习下载。通过这些代码读者可以直观地看到策略网络和价值网络如何协同工作、优势函数如何计算、新旧策略的信任区域约束如何限制更新幅度从而系统掌握在PyTorch中实现PPO算法的完整思路并将经验迁移到其他连续动作空间任务中。在月球着陆器中手撕PPOPyTorch实现与踩坑记录LunarLander月球着陆器是我见过最适合入门强化学习的OpenAI Gym环境——状态空间只有8维、动作空间只有4个离散动作、奖励信号明确且环境自带渲染界面。再加上PPOProximal Policy Optimization近端策略优化算法目前仍是强化学习界兼顾稳定性与样本效率的标杆两者搭配起来几乎就是RL入门的最好路径。这篇文章我会完整整理一遍用PyTorch实现PPO并在LunarLander中训练到“能稳稳降落”的整个过程包括环境细节、算法原理、代码结构、超参数调优和我在实操中踩过的一系列坑希望能帮你少走弯路。1. 项目整体设计与思路拆解1.1 LunarLander环境到底在模拟什么OpenAI Gym提供的LunarLander环境模拟的是一艘小型登月飞船在二维平面内的着陆过程。飞船初始时悬停在画面顶部带有随机的初始位置和角速度你需要通过控制主推进器和左右侧推进器让飞船平稳降落在两个黄色旗子之间的着陆平台上。具体来看环境的状态空间由8个连续变量构成飞船中心的x坐标、y坐标、水平速度、垂直速度、当前角度、角速度以及左右两个着陆腿是否接触地面的布尔值分别用0或1表示。动作空间是4个离散动作什么都不做、开启左侧推进器、开启主推进器向下喷射、开启右侧推进器。每一步的奖励设计非常讲究每开启一次主推进器会获得-0.3的惩罚开启侧推进器获得-0.03的惩罚这是为了鼓励节能也是为了让算法学会”轻推而不是乱喷”飞船每存活一帧会获得少量正奖励新版gymnasium中为每帧100/更新的奖励如果着陆腿接触到地面会额外10的奖励成功在着陆区软着陆获得100坠毁则获得-100。从这些奖励设定就能看出这个环境的核心目标不只是“飞过去”而是“用最少的燃料、以安全的速度落地”这也让训练过程非常有挑战性。LunarLander还有一个非常友好的地方——它自带reward shaping奖励塑形每一步都能获得及时的反馈信号不会像很多稀疏奖励环境那样让算法无从学习。这一点对于初学者掌握PPO的完整训练流程至关重要。1.2 为什么选择PPO而不是DQN、DDPG不少初学者会纠结第一个RL项目到底用哪个算法。我的建议是在LunarLander上直接上PPO不要从DQN开始。原因很简单DQN虽然也能解决这个环境但它属于value-based方法需要维护经验回放池对奖励尺度敏感而且动作选择依赖epsilon贪心策略探索效率并不高DDPG和TD3这类DDPG系算法虽然也能做但它们是基于连续动作空间的确定性策略方法用在离散动作环境上反而要额外处理而且超参特别多调起来头大。PPO则天生适合这个任务。首先它是一个on-policy算法每一步收集的样本会直接用当前策略更新逻辑上更“自然”——你是在学自己刚刚做过的事而不是反复回放很久以前的经验其次PPO通过clip机制约束策略更新的幅度从机制设计上就避免了策略一步更新过大导致崩溃的问题这对初学者来说简直是救命稻草因为你不需要费劲心思调学习率来保证稳定性最后PPO是actor-critic架构策略网络与价值网络可以共享底层特征在像LunarLander这样状态空间不大的环境中收敛速度很快。我个人的经验是LunarLander配上PPO如果实现正确、超参合理大概在15万到30万步之间就能看到明显的技能提升训练过程稳定且不会出现断崖式崩溃。反观DQN可能在同样的步数下也能学会但波动性会明显更大。2. 环境准备与PyTorch基础配置2.1 搭建虚拟环境并安装依赖工欲善其事必先利其器。我推荐用Anaconda创建独立的虚拟环境来跑这个项目避免把基础环境搞得一团糟。如果你机器上有NVIDIA显卡并且CUDA驱动正常直接装GPU版PyTorch如果没有独立显卡装CPU版本也完全够训练这个小型环境。LunarLander的状态空间很小MLP网络规模不大即使只在CPU上训练完成整个流程也通常只需要几分钟到十几分钟所以这里真的不用纠结硬件。# 创建python 3.10环境 conda create -n lunarlander python3.10 -y conda activate lunarlander # 安装PyTorch CPU版 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 如果是GPU环境去PyTorch官网选择对应CUDA版本安装即可 pip install torch --index-url https://download.pytorch.org/whl/cu121 # 安装强化学习环境和常用库 pip install gymnasium matplotlib numpy装完之后用几行代码验证环境是否正常这一步千万别跳我见过太多同学后面写了一大堆代码结果发现是环境没装对导致疯狂报错。import gymnasium as gym import torch print(torch.__version__) # 确认PyTorch版本 env gym.make(LunarLander-v3, render_modeNone) # 新版gymnasium环境名 print(env.observation_space.shape) # 期望输出 (8,) print(env.action_space) # 期望输出 Discrete(4)这里特别提醒一点早期教程里常用的LunarLander-v2是旧版OpenAI gym的环境名如果你是用gymnasium这个维护更活跃的新库环境名是LunarLander-v3逻辑完全一致但API细节上有差异比如新版step返回5个值多了一个terminated和truncated分开的标志。千万不要照抄老教程然后对着报错发呆。2.2 gymnasium新版API与老版gym的区别很多老教程用的都是env.reset()不带参数新版gymnasium中reset会返回一个(observation, info)元组老版本step返回4个值(obs, reward, done, info)新版返回5个值(obs, reward, terminated, truncated, info)。我最初迁移代码时就被这个细节坑过如果你写的是旧版风格运行时会直接报“too many values to unpack解包的值太多”的错误。另外新版环境还将“是否撞毁/任务是否结束”和“是否因为步数超限而截断”这两个概念拆开了在PPO中我们通常只关心terminated表示真的着陆或坠毁而truncated比如飞行时间太长其实会正常返回一个reward是否终止训练需要根据你的需求灵活处理。3. PPO算法核心原理解读3.1 Actor-Critic双网络结构设计PPO属于actor-critic算法族这意味着我们同时维护两个神经网络。Actor网络策略网络负责输出动作的概率分布在LunarLander这种离散动作环境中输出层使用Softmax产生4个动作各自的概率Critic网络价值网络负责预测当前状态的价值V(s)即“从当前状态出发未来累计奖励的期望”。为什么要两个网络而不是一个因为策略梯度的计算需要知道“某个动作到底比平均水平好多少”而“平均水平”正是Critic网络提供的。拿生活类比的话Actor是选手Critic是教练。选手负责做动作教练负责评估这个动作做得好不好选手根据教练的反馈调整自己。如果只有一个网络你就没法把“动作的好坏”和“状态本身的好坏”分开因为即便是同一个动作在好的状态下做和差的状态下做效果可能天差地别。在实现中两个网络通常共享前几层特征提取层再各自接独立的输出头这样既能节省参数量又能让底层特征提取得更充分。import torch.nn as nn class ActorCritic(nn.Module): def __init__(self, state_dim8, action_dim4, hidden_dim64): super().__init__() # 共享的特征提取层 self.feature nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), ) # Actor输出头输出每个动作的logits self.policy nn.Linear(hidden_dim, action_dim) # Critic输出头输出状态价值 self.value nn.Linear(hidden_dim, 1) def forward(self, x): feat self.feature(x) logits self.policy(feat) value self.value(feat) return logits, value.squeeze(-1) def get_action(self, x, deterministicFalse): logits, _ self.forward(x) # 从分类分布中采样动作 dist torch.distributions.Categorical(logitslogits) if deterministic: return logits.argmax(dim-1) return dist.sample()还需要为网络加入熵正则entropy bonus吗要加而且非常重要。熵正则的作用是鼓励策略保持一定的随机性避免过早收敛到某个确定的动作序列而丧失了探索能力。实战中我会在损失函数中减去一个系数乘以策略分布的熵让模型在“自信”和“好奇”之间做平衡。3.2 重要性采样与clip截断机制PPO最核心的贡献是clip机制。为什么需要它这需要先理解策略梯度中的一个关键工具——重要性采样。当我们用旧策略π_old收集了一批样本后如果用这批样本来更新新策略π_new的参数那么梯度计算中每个样本的权重需要乘以一个比率r_t(θ) π_new(a|s) / π_old(a|s)这就是重要性采样——它把“旧策略下的期望”转换成“新策略下的期望”的无偏估计。问题来了如果单步更新幅度太大r_t会变得非常大或非常小导致更新方向被个别极端样本主导甚至造成策略一夜回到解放前。PPO的做法非常巧妙对r_t进行截断限制在[1-ε, 1ε]区间内ε通常取0.2。具体来说PPO的目标函数用min(r_t * A_t, clip(r_t, 1-ε, 1ε) * A_t)来替代普通的策略梯度目标。这样做的好处是当优势A_t为正时我们鼓励增大r_t但最多到1ε当优势A_t为负时我们鼓励减小r_t但最多减到1-ε。这样无论梯度方向如何单次更新的步子都被限制在一个可控范围内训练不会因为一次冒险的更新而崩掉。其实理解PPO不需要背公式只需要记住一句话PPO在每次更新时都通过截断机制告诉优化器——你可以改进策略但不能改得太离谱否则我截住你。正是这种“戴着镣铐跳舞”的做法让PPO成了RL领域少数能开箱即用的训练稳定的算法。3.3 GAE优势估计与λ的作用优势函数A(s,a)表示在状态s下选择动作a相比平均值好多少。LunarLander环境每一步都会返回奖励这看似省事但如果只使用单步TD误差作为优势估计信息的传递效率太低了——飞船刚在第一步做了个正确动作要等很多步之后才能看到收益这中间的延迟会让梯度信号变得非常噪声大。GAEGeneralized Advantage Estimation广义优势估计解决的就是这个问题。它并不是只用一步TD误差也不是把每一步的实际奖励都算进回报里而是在k步TD误差的加权平均和真实累计奖励之间找一个平衡点——由一个λ参数控制衰减。当λ0时GAE退化为一步TD估计方差小但偏差大当λ1时GAE退化为蒙特卡洛估计无偏但方差极大。实操中λ取0.95是常见选择兼顾了偏差和方差。GANE的计算公式可以迭代实现效率很高。核心思路是从轨迹的末尾往前反向遍历维护一个累计的优势估计值gae 0每一步计算TD误差delta r gamma * V(s) - V(s)然后用gae delta gamma * lambda * gae更新。这里的gamma是折扣因子决定模型对未来奖励的看重程度。LunarLander的训练中我习惯设gamma0.99λ0.95。4. 核心代码实现与训练流程剖析4.1 数据采样与经验存储PPO是on-policy算法每次迭代先用当前策略跑若干个完整回合收集一批样本然后在这批样本上进行多轮梯度更新更新完之后丢弃这批数据、重新采样。这个“采样-更新-丢弃”的循环是PPO的基本节律。我习惯用一个简单的rollout buffer来存储采样数据每批样本记录状态、动作、动作概率对数log_prob要存下采样时的概率值供重要性采样使用、奖励、是否终止、状态价值估计。采样时跑N个完整回合用当前Actor网络输出动作分布采样动作然后逐步执行环境。一个容易忽略的细节是存储的log_prob必须是采样时旧策略的输出概率因为我们在更新时计算重要性采样比率需要知道“旧策略下这个动作出现的概率”。很多人第一次写PPO时会在更新阶段重新用当前网络计算概率这就完全错了算出来的r_t恒等于1PPO就退化成普通的策略梯度了。4.2 GAE计算与mini-batch更新采集完batch数据后首先计算每个时间步的优势估计使用前面提到的GAE公式。注意在GAE实现中最后一个时刻的优势计算用V(s_{T1})如果最后一步已经终止则V(s_{T1})应该为0否则用Critic网络预测。然后进入PPOepochs循环通常对同一批数据训练4~20个epoch每个epoch将数据随机打乱切成mini-batch比如64或128的大小对每个mini-batch计算策略损失、价值损失和熵奖励反向传播更新网络参数。多epoch更新的目的就是提高样本利用效率但也不能太多否则会过拟合到这批样本上。我用的损失函数构成如下# 每个mini-batch计算 # ratio new_prob / old_prob通过log_prob相减再exp得到 ratio (log_probs - old_log_probs).exp() # 策略损失带clip policy_loss -torch.min( ratio * advantages, torch.clamp(ratio, 1 - clip_epsilon, 1 clip_epsilon) * advantages ).mean() # 价值损失Clipped Value Loss可选简单场景用MSE也行 value_loss 0.5 * (returns - values).pow(2).mean() # 熵奖励鼓励探索 entropy dist.entropy().mean() loss policy_loss value_coef * value_loss - entropy_coef * entropy4.3 训练主循环与完整代码结构下面给出一个极简但能跑通的结构框架完整的训练主循环就四条核心步骤采集轨迹、计算GAE、更新网络、重置环境。为了便于理解我保留了最核心的逻辑去掉了参数保存和日志记录等外围代码。import gymnasium as gym import torch import torch.optim as optim from torch.distributions import Categorical def compute_gae(rewards, values, dones, gamma0.99, lam0.95): advantages [] gae 0 values values [0] # 最后补一个0 for t in reversed(range(len(rewards))): delta rewards[t] gamma * values[t 1] * (1 - dones[t]) - values[t] gae delta gamma * lam * (1 - dones[t]) * gae advantages.insert(0, gae) returns [adv v for adv, v in zip(advantages, values[:-1])] return advantages, returns # 训练主循环 for iteration in range(total_iterations): # 1. 用当前策略采样 obs, actions, old_log_probs, rewards, dones, values [], [], [], [], [], [] state, _ env.reset() # ... 循环执行直到回合结束把每一步数据加入列表 ... # 2. 计算GAE优势 advantages, returns compute_gae(rewards, values, dones) # 3. 转换为Tensor并做若干轮mini-batch更新 # ... 将数据打乱切mini-batch计算loss并更新 ... # 4. 定期渲染或保存模型5. 训练效果与避坑经验分享5.1 训练曲线长什么样才算成功一个成功的PPO在LunarLander上的奖励曲线通常呈阶梯状刚开始训练时飞船基本是在乱飞累计奖励通常在-100到-50之间波动毕竟每一步都有燃料惩罚坠毁还要扣100分训练到某一阶段会出现第一个质的飞跃飞船开始会向画面中央移动并用主推进器减速但着陆方式还很粗暴奖励可能上升到50~150再训练一段时间飞船会掌握软着陆的诀窍能够在划定区域稳稳落地平均奖励超过200分。Gym官网给出的标准是平均奖励在100回合内超过200就算问题解决。如果你画出来的曲线是平的、一直不涨那我敢肯定代码里有bug而不是算法问题。最常见的bug是重要性采样比例算错、GAE的dones处理不当、或者价值网络没有收敛导致优势估计全是噪声。5.2 常见问题与排查技巧一览我在复现PPO时踩过不少坑下面这些几乎是新手必踩的高频问题列成表格方便查阅问题现象大概率原因我的解决办法奖励一直不涨log_prob存的是更新后的概率确认采样时立即保存old_log_prob训练崩溃、奖励急剧下降clip_epsilon设置过大或学习率过高用默认的lr3e-4配clip_eps0.2起步NaN损失学习率太高尤其是Adam降低学习率到1e-4或检查网络输入未归一化过早收敛到乱飞状态熵系数太小检查entropy_coef是否在0.01附近不要降为0最后一步总是炸毁没有正确处理dones截断在GAE中终止状态后清零V(s_{t1})且不再累积GAE训练慢但能跑单批采样太少每轮至少采样2048~4096步或者减少更新epoch数让每批样本更独立5.3 超参数选择与我的调参心得我给出一组默认就能跑得不错的超参组合学习率3e-4clip_epsilon0.2gamma0.99lambda0.95每轮采样2048步更新epoch数10mini-batch大小64隐藏层64或128激活函数Tanh训练总步数30万。这套配置适配我的代码结构如果你用的框架不同可能需要微调。调参方面我最大的心得是不要同时改两个参数。我在一次实验里同时调大了学习率和熵系数结果训练曲线出现剧烈震荡奖励在100分和-20分之间反复横跳花了一晚上才定位到是多个参数叠加导致的过冲。如果你也碰到类似情况先把所有参数恢复到参考值只改动一个变量逐个排查。还有一点是关于归一化的——LunarLander的状态空间本身数值范围还算合理但我在实践中发现对观测做简单的标准化减去均值除以标准差用running mean和running std通常能让训练收敛得更快更稳。特别是当后续你从LunarLander迁移到更复杂的环境时这一步几乎就是标配了。不过为了入门简单可以先不加跑通了再加。6. 后续可以怎么扩展如果LunarLander这个项目你已经能稳定跑到200分以上我强烈建议你做两件进阶练习。第一把环境从LunarLander-v3换成LunarLanderContinuous-v3这是一个连续动作空间版本两个连续推进器力度需要你把Actor的输出从Softmax分类分布换成高斯分布均值方差或固定方差整个实现会更有深度第二在PPO代码中加入GAE中λ的消融实验分别跑λ0、0.5、0.95、1.0四组观察训练曲线的差异你会真正理解为什么λ被叫作“偏差方差的调节旋钮”。从我个人经验来说这个项目最大的价值不在于你多会调参或者代码写得多炫而在于你完整地走通了一遍“环境理解 → 算法设计 → 工程实现 → 调试排错”的流程。这套方法论迁移到任何其他RL问题上都是通用的。我在跑这个项目时好几次因为一个细节没对导致训练完全失效那种反复看代码找bug的经历虽然痛苦但回过头来恰恰是最加深理解的过程。希望这篇记录能帮你用更短的时间跨过这些坎尽早享受到第一次看到飞船稳稳着陆时的成就感。本文还有配套的精品资源点击获取
返回列表