PyTorch中A2C在Pendulum环境下的稳定收敛调参指南
发布时间:2026/9/16 20:08:14来源:尧图网络
Pendulum环境在强化学习的入门列表里看着人畜无害一个倒立摆连续动作空间就一个力矩控制杆子别掉下来。但真到了PyTorch里用A2C把它训到收敛我发现这事远没有想象中那么简单。A2CAdvantage Actor-Critic作为策略梯度家族里最经典的baseline之一实现起来代码量不大可参数稍微没对齐训练曲线直接变心电图loss飞上天的场景我踩过太多次了。这篇文章把我自己从“能跑通”到“稳定收敛”过程中总结的调参逻辑、收敛优化手段和排查技巧完整记录下来目标是让同样在用PyTorch调A2C的你在Pendulum环境里少走弯路顺便把对强化学习的直觉也练出来。适合谁来读已经在用PyTorch实现过基础网络、刚接触强化学习不久或者写好了A2C但发现奖励曲线怎么都不涨的开发者。我会从整体设计思路讲起再把网络结构、GAE参数、熵系数、学习率这些关键点一个个拆开最后附上一份调参档位和常见问题速查表保证每个结论背后都有对应现象和实操依据。1. 任务特点决定调参方向为什么Pendulum这么难缠1.1 Pendulum环境到底在优化什么Pendulum是个连续控制任务状态空间只有3个维度摆杆角度用cos和sin描述避免角度跨越±π导致的不连续、角速度。动作空间则是施加在摆杆末端的力矩取值范围在-2到2之间。目标是把摆杆从任意初始状态摆到竖直向上位置并保持静止。奖励函数很有意思它由三部分构成摆杆与竖直方向夹角的负平方项偏离越远惩罚越大。角速度的负平方项转得越快惩罚越大。动作力矩的负平方项能耗越大惩罚越大。每步奖励范围大致在-16到0之间理想状态下的累计回报其实非常“稀疏”——不像是CartPole那种每一步都有正反馈的设定。初看这个环境会觉得很简单但实际训练中发现它在连续动作空间里对策略方差的敏感度极高奖励尺度又天然偏负很容易让Critic网络学出一个“永远吃亏”的值函数估计。这里我强烈建议先做一步关键操作把原始奖励统一缩放到一个合理区间。我习惯将奖励除以一个固定常数比如10让每步奖励落在约-1.6到0之间这样梯度量级更可控配合后面要讲的熵系数调节收敛速度会有肉眼可见的提升。1.2 为什么选A2C而不是REINFORCE或DQNPendulum的动作空间连续DQN那一整套基于动作价值函数取max的思路天然不适用——你没法对无穷多个连续动作求argmax。REINFORCE虽然能处理连续动作但它是蒙特卡洛式的必须等一个完整episode结束才能更新一次方差大得离谱在Pendulum这种步数长且奖励几乎全程为负的环境里baseline缺失会让策略更新像喝醉了酒走路。A2C的关键在于引入了Critic网络作为baseline通过优势函数来评估“当前动作相对平均水平好多少”。它用TD误差或多步回报计算优势不依赖完整episode方差显著小于REINFORCE。再加上A2C天然支持并行环境采样多个worker同时收集轨迹梯度更新更平稳。在Pendulum任务里我实测4个并行worker的A2C比单worker单跑效率高出接近3倍收敛稳定性也更好。后续所有调参经验都基于多worker A2C展开因为单worker训练时奖励曲线抖动实在太大参数微调很难观察出真实差异。2. 网络结构与核心参数拆解2.1 Actor和Critic该共用主干还是完全分开这是个老生常谈但影响很大的设计决策。A2C中Actor负责输出动作分布的均值和对数标准差Critic负责输出状态价值估计。我一开始为了省参数把两个网络做成共享主干输入状态给两层128维的全连接最后一分为二分别输出策略分布参数和状态价值。实验结果显示共享主干在Pendulum里会造成两个任务互相干扰——策略在探索阶段变化剧烈价值网络的拟合目标也在剧烈变化两个loss一起反传到共享层时梯度方向经常打架训练早期特别容易崩。改成完全分离的双头网络后两个独立的两层MLP中间用ReLU激活稳定性明显改善。代价是参数总量增加但对Pendulum这种低维状态来说完全可控。我的建议是Actor和Critic各自独立每层隐藏单元数取256比128和64效果更好网络容量大一点能容纳更复杂的价值函数形状尤其在Pendulum这种连续状态空间里价值函数的非线性程度比想象中高。2.2 GAE的λ值偏差与方差的权衡旋钮GAEGeneralized Advantage Estimation是A2C收敛效果的分水岭它用指数加权平均的方式融合多步TD误差λ就是那个控制偏差和方差平衡的超参数。λ接近0优势估计退化成单步TD误差偏差大但方差低。λ接近1优势估计近似蒙特卡洛回报方差大但偏差低。在Pendulum环境里我测试过λ从0.8到0.99的几组配置。λ0.95是各方面比较均衡的档位收敛速度和最终策略质量都最好。λ0.9时训练曲线更平滑但最终累积奖励略低大概差了30-40分。λ0.99时前几百个episode方差暴涨Critic在训练早期完全跟不上策略变化出现了几次明显的梯度爆炸。注意一个容易被忽略的细节GAE的计算依赖于value网络在每一步给出的估计值。如果Critic没有训练好再好的λ也白搭。所以我在实际训练中不会一上来就固定λ而是先观察Critic的loss曲线如果value loss在前500个更新步内没有下降到初始值的1/3以下先把λ调低到0.9稳定军心等网络整体进入状态后再调回0.95。2.3 熵系数探索的油门也是收敛的刹车熵正则项让策略在训练过程中保持一定的随机性避免过早陷入确定性策略。A2C的损失函数里一般有一项熵的负数乘以熵系数。熵系数越大探索越充分但策略收敛会变慢系数太小策略容易陷入局部最优表现为摆杆总在某个角度附近来回抖动达不到竖直稳定状态。我在Pendulum上做过一组对照实验熵系数分别取0.01、0.001、0.00010.01策略探索性太强训练到3000步时动作标准差仍然接近0.8无法收敛到精确控制累积奖励在-600附近震荡。0.001最均衡训练约1500步后策略开始出现明显的“摆上去并稳住”的倾向最终累积奖励在-200左右。0.0001早期收敛很快但容易卡在局部最优后续很难跳出来。更关键的是熵系数不能一成不变。我采用了一种简单的衰减策略每1000个更新步把熵系数乘以0.995相当于前期的探索和后期的利用各占一头。这个技巧让我在Pendulum上的收敛时间缩短了约20%。具体实现就是在保存checkpoint时一并保存熵系数的当前值恢复训练时也恢复它避免衰减状态丢失。2.4 学习率不是越大越好也不是越小越稳Pendulum对学习率非常敏感。我系统测试过Actor和Critic共用学习率的情况学习率0.001收敛最快但训练中后段出现明显的策略震荡尤其在优势估计值比较小的时候更新步幅过大导致策略反复横跳。学习率0.0003训练速度慢了一些但稳定性显著提升最终性能更好。学习率0.0001太保守训练2000步时奖励曲线还在缓慢爬升效率无法接受。我最终采用的是Actor和Critic分开设置学习率的方式Critic学习率通常是Actor的2到3倍。原因在于Critic要先快速拟合出相对准确的价值函数才能给Actor提供有意义的优势信号。具体配置是Actor学习率0.0003Critic学习率0.001两个优化器都使用Adam。这里有一个重要提醒Actor输出的对数标准差log_std也需要有自己的学习率或者单独的参数设置。我会把log_std初始化为-0.5而不是0这样初始方差约为0.6探索幅度适中。如果初始化为0初始方差为1探索过猛训练前期很容易出现危险的大幅动作。3. 实操过程PyTorch实现A2C关键环节3.1 环境向量化与数据收集多worker并行需要在PyTorch环境中手动管理多个子环境。我用的是Gym库的VectorEnv包装器创建4个并行环境在一次循环中同时执行4个step收集4条轨迹。数据收集时需要注意一个严重的坑episode结束状态done对应的value估计要置为0否则TD误差会往后多传一步导致优势估计偏差。import torch import gymnasium as gym envs gym.vector.make(Pendulum-v1, num_envs4, async_envsTrue) state envs.reset(seed42) def collect_trajectory(envs, actor, state, steps2048): states, actions, rewards, dones, values, old_log_probs [], [], [], [], [], [] for _ in range(steps): state_tensor torch.as_tensor(state, dtypetorch.float32) with torch.no_grad(): mean, log_std actor(state_tensor) std log_std.exp() dist torch.distributions.Normal(mean, std) action dist.sample() value critic(state_tensor).squeeze(-1) log_prob dist.log_prob(action).sum(dim-1) next_state, reward, terminated, truncated, _ envs.step(action.numpy()) done terminated | truncated states.append(state_tensor) actions.append(action) rewards.append(torch.as_tensor(reward, dtypetorch.float32)) dones.append(torch.as_tensor(done, dtypetorch.float32)) values.append(value) old_log_probs.append(log_prob) state next_state return states, actions, rewards, dones, values, old_log_probs, next_state这段代码的核心逻辑是每个step存储状态、动作、奖励、done标记、Critic估计的value以及动作的log_prob。注意reward这里我建议先除以10这就是前面说的奖励缩放如果忘了做Critic的target会变得很大网络输出的value和TD误差都会膨胀训练曲线会出现明显的尖刺。3.2 GAE计算与优势估计实现接下来是GAE的计算。这一块写错的人很多很多人直接把公式背下来但没有理解每个变量在PyTorch张量里对应哪个维度。正确做法是从最后一个时间步往前反向迭代复用next_value即最后一步之后的价值估计递推计算。def compute_gae(rewards, dones, values, next_state, gamma0.99, lam0.95): with torch.no_grad(): next_value critic(torch.as_tensor(next_state, dtypetorch.float32)).squeeze(-1) gae 0 advantages [] values torch.cat(values [next_value.unsqueeze(0)], dim0) for t in reversed(range(len(rewards))): if t len(rewards) - 1: next_non_terminal 1.0 - dones[t] else: next_non_terminal 1.0 - dones[t] delta rewards[t] gamma * values[t1] * next_non_terminal - values[t] gae delta gamma * lam * next_non_terminal * gae advantages.insert(0, gae) returns advantages torch.as_tensor(values[:-1], dtypetorch.float32) return advantages, returns这里有个细节需要注意dones在Gymnasium新版接口里可能拆成了terminated和truncated两个布尔数组。对于GAE计算只有terminated真实的物理失败才应该截断价值传递truncated超过最大步数强制结束不应该截断因为价值函数的预测依然有效。我当时没有区分这两个标志位导致策略在最后一步附近总觉得环境要“倒闭”价值估计偏低训练曲线波动很大。3.3 策略损失和价值损失的正确姿势A2C最终的损失有三个来源策略损失负的优势函数乘以log_prob要让优势为正的动作概率上升。价值损失预测价值与GAE回报之间的MSE。熵正则项策略分布熵的负值用于鼓励探索。这个公式写成PyTorch代码时关键在于先标准化优势。我在训练中用优势的均值减除标准差做归一化比直接用原始GAE值稳定很多尤其在Pendulum这样奖励尺度偏离零点的环境里不标准化的话梯度会被优势值的大小牵着鼻子走。def compute_loss(actor, critic, states, actions, old_log_probs, advantages, returns): advantages torch.stack(advantages) advantages (advantages - advantages.mean()) / (advantages.std() 1e-8) mean, log_std actor(states) dist torch.distributions.Normal(mean, log_std.exp()) log_probs dist.log_prob(actions).sum(dim-1) ratio (log_probs - old_log_probs).exp() policy_loss -(advantages * ratio).mean() value_true torch.stack(returns) value_pred critic(states).squeeze(-1) value_loss nn.functional.mse_loss(value_pred, value_true) entropy dist.entropy().mean() entropy_coeff 0.001 total_loss policy_loss 0.5 * value_loss - entropy_coeff * entropy return total_loss注意我在这里用了old_log_probs做importance ratio虽然A2C不像PPO那样有clip限制但保留这个ratio有利于后续升级到PPO。而且实践中发现用旧概率做比率计算后训练稳定性略有提升算是白拿的好处。3.4 训练循环与检查点保存训练主循环的骨架并不复杂但有几个小细节值得记录。我习惯在每个更新步之后打印当前的累计奖励均值、熵值、价值损失平均值。熵值是一个很重要的诊断信号如果熵值已经降到0.1以下但奖励还在涨说明探索基本结束策略正在利用当前所学如果熵值降到0.05以下但奖励还没起色大概率是陷入了局部最优需要人工介入调整熵系数。def train_a2c(iterations5000): state None for iteration in range(iterations): states, actions, rewards, dones, values, old_log_probs, next_state collect_trajectory( envs, actor, state, steps1024 ) advantages, returns compute_gae(rewards, dones, values, next_state) loss compute_loss(actor, critic, states, actions, old_log_probs, advantages, returns) opt.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(actor.parameters(), 0.5) torch.nn.utils.clip_grad_norm_(critic.parameters(), 1.0) opt.step()这里的梯度裁剪是一个差点被忽略的救命稻草。我在不裁剪时Actor的梯度经常出现爆炸值训练到500步左右loss直接变成NaN。加上梯度裁剪后整个训练过程就再没出现过NaN。Actor裁剪阈值0.5Critic裁剪阈值1.0这个不对等的设置是因为Critic的value输出范围较大允许它拥有更大的更新步幅。4. 调参实录从完全飞掉到稳定收敛的四个阶段4.1 第一阶段奖励缩放救了整个训练第一次跑通A2C时我没有做任何奖励缩放奖励范围在-16到0之间。训练曲线非常诡异前50个iteration内value loss降不下去永远稳定在一个高值附近累计奖励也在最低点附近死寂。后来我输出了一下每个batch的平均奖励发现大约在-9到-11之间而Critic预测的value也是差不多的水平相当于网络学习了一个“始终输出负值的常数函数”完全没有区分不同状态的好坏。奖励缩放为原来的1/10后Critic的target范围收窄到-1.6到0之间网络输出的预测值和真实值之间的差距缩小梯度相对值变大vlaue loss才开始真正下降。这也是我在前面反复强调这个操作的核心原因——它不是可有可无的锦上添花而是让价值网络学得动的必要条件。4.2 第二阶段梯度裁剪稳住早期更新奖励缩放解决了“学不动”的问题后新的麻烦来了训练到400步左右policy loss突然出现一个巨大尖峰紧接着整个loss变成NaN。我查了梯度范数发现Actor的梯度在某个时刻突然膨胀到几百的量级。原因是在优势估计偶尔偏大的情况下策略更新步幅过大动作分布均值跳变接着下一步的log_prob出现极端值。我花了一点时间同时给Actor和Critic都加上梯度裁剪阈值各取0.5。NaN问题消失。后来把Actor修剪阈值调高到0.5Critic阈值1.0这样Critic能更快拟合价值函数Actor的更新依然稳健整体收敛速度反而提升了一点。4.3 第三阶段熵系数衰减带来最后的突破在解决了不崩之后A2C能稳定地从初始状态附近缓慢提升奖励但始终无法达到一个完美的“摆上去稳住”的策略。奖励曲线在一个平台上横了很久大概从-500缓慢爬到-300之后动都不动。我观察熵值发现仍然在0.3左右——策略太随机了每次施加力矩的方向变来变去无法形成稳定的控制律。这时我把熵系数从固定的0.001改成每1000步乘以0.995的衰减策略。在训练的第1500到2000步之间能明显看到策略从“乱抖”过渡到“定向控制”奖励曲线突破了平台快速收敛到-180左右。这个阶段让我意识到熵系数在Pendulum上不是越大越好而是需要精细地按训练进度调小。4.4 第四阶段n步回报窗口的微调最后一个困扰我比较久的问题是收敛后期奖励曲线出现周期性波动。理论上训练到一定程度曲线应该平稳在某个水平附近小幅震荡但我的曲线是每隔几百步就出现一次持续几十步的下坠再回升。仔细观察后发现这是GAE的λ0.95和轨迹长度之间的耦合问题。当策略进步后固定2048步收集到的轨迹里优质状态和低质状态的比例会发生偏移。此时如果GAE的时序窗口不够长优势估计会开始偏向局部TD误差策略被低质状态的伪优势信号误导。我把轨迹长度从2048增加到4096同时λ保持在0.95周期性波动消失训练最终达到-150左右的稳定水平。5. 常见问题与排查技巧实录5.1 奖励曲线长期不涨先别急着动网络结构按下面顺序检查奖励是否缩放没有缩放到-2到0区间以内Critic大概率学成常数。优势是否标准化标准化能让更新步幅可控。熵系数是否过大输出动作标准差如果一直维持在0.5以上策略等于随机游走。学习率是否过低以Adam为例Actor的学习率低于0.0001基本等不到收敛。5.2 Loss直接变NaN优先查看梯度是否爆炸给Actor和Critic都加上梯度裁剪。检查输入状态是否有NaNPendulum环境一般不会但如果你在奖励里做了变形处理比如log注意负数取对数的问题。检查学习率是否过大0.001以上配合小batch很容易让数值稳定崩掉。5.3 收敛到局部最优无法把摆杆立起来这个在Pendulum里非常典型。症状是奖励停在-400到-300之间摆杆能在底部附近左右摆动但无法甩上去。排查方向减小熵系数衰减速度给策略更多探索机会。增大初始log_std比如从-0.5改为0让策略前期抖得更厉害。尝试调低λ到0.9让优势估计偏差更小策略更容易被正确的TD信号引导。增加并行环境数量到8个提供更多样化的轨迹。5.4 训练后期震荡严重检查学习率是否需要调度衰减。我习惯在第3000步后把Actor学习率降到0.0001Critic降到0.0003。检查轨迹长度是否足够GAE在较长序列上优势估计更稳定。检查Critic loss是否过早收敛如果value loss在训练中后段不再下降CNN的价值预测可能已经饱和可以试着增加Critic网络容量。5.5 无法在当前环境下获得预期性能排除所有超参数问题后最后检查硬件相关因素CPU并行线程数是否限制了环境采样速度以及是否设置了统一的随机种子。强化学习的随机性来源非常多环境、PyTorch参数初始化、numpy采样都会引入噪声。我自己的调试经验是种子固定后同样的参数配置在不同机器上复现性可以达到90%以上剩下10%的偏差来自环境本身的随机性不影响参数评价结论。6. 几个值得长期坚持的调参习惯调试A2C这类强化学习算法很难靠一次“猜准参数”就一劳永逸。我后来养成了几个还不错的小习惯分享给你第一训练过程中持续记录entropy、value loss、优势均值、累计奖励四条曲线缺一不可。奖励曲线只告诉你“有没有变好”但只有entropy能告诉你“为什么没变好”。如果entropy一直在掉但奖励不涨那是探索不够如果entropy不掉但奖励也不涨那是网络拟合能力不够。第二对每个改动只动一个变量并且保留上次的模型检查点。A2C的随机性决定了两次训练之间本身就有波动如果你同时改了学习率和熵系数即便结果变好了你也分不清是哪个改动的功劳。用同一个种子、同一个初始参数跑两次对比单个变量变化前后的差异才是可信的A/B实验。第三多花一点时间在数据收集阶段而不是阶段训练上。A2C的性能上限很大程度上取决于“看到的轨迹多样性”。我发现把并行环境数量从2提升到8配合稍短的轨迹长度往往能带来比精心调学习率更明显的提升。环境并行数量、轨迹长度、学习率三者之间要一起考虑不要单独优化某一个。最后再分享一个小技巧如果你在Pendulum上跑通了一套稳定的A2C参数想要把它迁移到其他连续动作环境比如Hopper或Walker2d最先需要确认的不是学习率而是动作空间的边界和奖励尺度。Pendulum的力矩边界是[-2,2]输出层的tanh激活刚好匹配如果换了环境动作边界变了输出层激活函数和log_std初始化都要重新设计。奖励尺度则直接决定了Critic网络的学习难度换环境后第一件事就是把奖励分布打出来看一眼再用缩放因子拉到近似范围。这套方法论我沿用到现在基本能保证任何新环境第一次跑A2C都不会直接崩掉至少有一个可以正常诊断的起点。
网站建设高端定制企业官网