ARTICLE DETAIL

资讯详情

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

深度强化学习核心:Actor-Critic框架原理与工程实现精讲

深度强化学习核心:Actor-Critic框架原理与工程实现精讲 做强化学习有一段时间的人最后基本都会落到Actor-Critic这个框架上。不管是刚入门时照着教程敲的A2C还是后来在机械臂、游戏AI、推荐系统里用得最多的PPO、SAC、TD3底层全是Actor-Critic的影子。可以说搞懂AC就等于拿到了深度强化学习的主线钥匙。这篇内容不是给你念教材而是从“为什么要AC”、“AC到底在干什么”、“怎么把它写到能跑”三个层面拆开讲。我会先梳理AC从策略梯度与价值方法里各自继承了什么再贴核心公式和PyTorch风格的实现思路最后把训练里常见的坑和排查经验一并交代。适合刚看完DQN、想往连续控制或策略优化方向走的人也适合项目里已经上了AC、但训练老不稳、想回头找原因的同学。1. 为什么绕了一大圈最后还是回到Actor-Critic1.1 策略梯度的“高方差”痛点先说最朴素的Policy Gradient方法也就是REINFORCE。它的思路很直接用一个策略网络直接输出动作概率分布然后用整个回合的累计回报来评价这次动作好不好好的动作加大概率差的动作减小概率。问题出在“用整个回合的累计回报来评价”这一步。想象一下一个回合持续100步第10步的动作其实是对的但后面90步全是随机抖动最后累计回报很低。REINFORCE会把这个“差评”平均分配到这100步里第10步明明表现不错也跟着挨打。这就是所谓的高方差每次更新的梯度方向噪声很大同一个状态和动作换个随机种子可能梯度方向完全不一样。高方差带来的直接后果就是训练效率极低往往要几百万步才能看到起色。我当时跑CartPole这种玩具环境还不觉得一旦换到稍微复杂点的环境比如HalfCheetah或机械臂仿真你会发现loss曲线跟心电图似的根本没法收敛。1.2 价值方法的“连续动作”困境另一条路线是Value-Based方法最典型的就是DQN。DQN学习的是Q函数也就是“在状态s下采取动作a之后一直按照最优策略走能拿到多少期望回报”。决策的时候贪心一下选Q值最大的动作就行。这个思路在离散动作空间里很优雅但一碰到连续动作就麻烦了。机械臂的关节角度是连续值无人车的方向盘转角也是连续值你不可能把每一个可能的动作都枚举出来算Q值。就算用优化方法在线求解argmax计算开销也大得离谱根本满足不了实时控制的要求。另外Value-Based方法天然是确定性策略对需要探索、需要随机性的场景也不够灵活。很多时候最优策略本身是带随机性的——比如扑克游戏里你永远用同一个动作反而会被对手拿捏。1.3 AC的切入点让Critic来帮Actor“打分”Actor-Critic的精髓就是把这两条路线缝在一起Actor负责输出策略解决连续动作和随机策略的问题Critic负责评价当前状态或动作的好坏解决策略梯度高方差的问题。怎么评价核心是用一个Critic网络来估计价值函数然后拿它计算“优势”——也就是某个动作相比平均水平好多少。这个优势值替代了REINFORCE里那种粗糙的回合总回报方差一下子小了很多。打个比方REINFORCE像是让一个只看期末成绩的学生去复盘每一道题该不该错而AC是让老师每节课都告诉你“这道题答得比班级平均分高继续保持”——反馈粒度细了学习自然快。我在实际项目里的感受是AC架构的收敛速度和稳定性相对朴素策略梯度是肉眼可见的提升。早年机器人控制领域还在纠结怎么给策略梯度降方差AC框架出来之后大家终于有了一套标准打法后面PPO、SAC这些工程上极其好用的算法全都是在这个框架上长出来的。2. Actor-Critic到底在更新什么网络结构、TD误差与优势函数2.1 Actor和Critic各自的职责分工先明确两件事Critic网络不看动作只看状态输出的是状态价值V(s)的估计Actor网络只看状态输出的是动作的策略分布π(a|s)在连续控制里通常是高斯分布的均值和方差。为什么Critic不直接输出Q(s,a)而是输出V(s)因为Actor的动作是采样出来的如果你用Q函数做评价每次更新Actor的时候还得再算一遍动作对应的Q值多一层计算不说在连续动作空间里还不好处理。用V函数配合TD误差可以直接推导出优势估计计算链路更顺畅。在实际代码里两个网络通常是分开定义的两套MLP但也可以让它们共享前面的特征提取层比如图像输入时的CNN部分后面再分头输出策略参数和状态值。共享特征的好处是参数少、训练快坏处是两个任务的梯度会互相干扰尤其当环境复杂度上去之后我更推荐两个网络完全独立调参的时候也省心。2.2 TD误差就是Critic的“学习信号”Critic的更新方式用的是时序差分也就是让V(s_t)去逼近r_t γV(s_{t1})。这里γ是折扣因子表示未来的回报折算到当前值多少钱。两个目标之间的差δ_t r_t γV(s_{t1}) - V(s_t)就是TD误差。值得多讲两句的是TD误差这个东西的意义被很多人低估了。表面上看它只是Critic网络的损失函数来源但实际上它同时是Actor更新的“打分凭证”。在标准的AC实现里Actor的梯度方向是“增大当前动作概率”乘上的系数就是这个TD误差。为什么能用同一个δ来驱动两个网络因为δ_t的期望值恰好等于优势函数A(s_t, a_t)的无偏估计。它表示“这一步走完拿到的实际回报比原先预期的好还是差”比单纯用回报R_t作为信号要精准得多。Critic不断优化自己对这个值的预测Actor则拿着这个反馈不断调整自己的动作分布形成一套闭环。2.3 优势函数与GAE从单步到多步的平滑单步TD误差的问题在于信息量太少某些状态下奖励稀疏单步看不出好坏。多步回报信息量大但方差高单步回报方差低但有偏怎么平衡是个经典问题。实践里最常用的是Generalized Advantage Estimation也就是广义优势估计。GAE的做法是把1步、2步、3步……一直到整个回合的TD误差按指数衰减加权求和。权重系数λ控制着这个衰减速度λ接近0等价于只看单步TD方差低但偏差大λ接近1等价于看完整回合偏差小但方差大。A_t δ_t (γλ)δ_{t1} (γλ)²δ_{t2} ... (γλ)^{T-t1}δ_{T-1}调参的时候λ在0.95到0.99之间是一个常见区间。控制类任务我一般从0.95起步如果发现训练不稳定再往0.99方向调。GAE是整个AC体系里性价比最高的一行代码几乎所有后续算法都用到了它建议一定吃透。3. 主流Actor-Critic变体与选型思路A2C、PPO、DDPG、TD3、SAC3.1 A2C/A3C入门必写多环境并行稳定训练A2CAdvantage Actor-Critic是最标准的AC实现PyTorch官方强化学习教程里那个例子就是它。它同时开多个环境每个环境各自跑一段轨迹然后统一汇总数据更新网络。A3C是异步版本每个环境配一个独立线程各自更新参数但异步操作在工程实现上容易出bug实际用A2C这种同步版本的人反而更多。A2C适合做什么适合验证一个环境的可行性也适合作为后续一切算法的“洗碗工版本”。我带的实习生来项目组第一周任务就是手写A2C不调库不抄完整开源实现目的就是让TA把“采样、算优势、更新Actor、更新Critic”这条流水线彻底刻在脑子里。3.2 PPO工程上最省心的信赖域策略优化PPO的全称是Proximal Policy Optimization核心解决的是“策略更新太快导致崩溃”的问题。它给Actor更新加了一个裁剪项如果新策略和旧策略的概率比超出[1-ε, 1ε]的范围就把梯度裁掉保证一次更新不会让策略跑太远。这个思想来自更早的TRPOTrust Region Policy Optimization但TRPO要用共轭梯度解约束优化实现复杂计算开销大。PPO用一行min操作就把同样的目的达到了所以成了工业界的默认选择。我现在做机械臂操作任务首选几乎都是PPO仓储机器人仿真环境里做订单分拣策略用的也是PPO。PPO里那个裁剪系数ε典型值是0.2。太大更新激进容易崩太小更新太慢。如果你发现策略在某一步之后突然性能崩盘多半是ε偏大或者优势估计没做好后面我会专门讲排查方法。3.3 DDPG/TD3确定性策略的连续控制利器DDPGDeep Deterministic Policy Gradient走的是另一条路Actor直接输出一个确定性的动作值而不是动作分布。它结合了DQN的经验回放和目标网络让Critic学Q(s,a)再用梯度上升直接调整Actor输出。DDPG当年在连续控制上效果惊艳但工程上有两个痛点一是Q值容易被高估导致策略飘到虚假的高奖励区域二是超参数敏感稍微没调好就发散。TD3在此基础上做了三项改进用两个Critic取较小值来抑制过估计、延迟更新Actor网络、给目标动作加噪声实现平滑正则。就这三点改动稳定性提升非常明显。确定性策略的变体适合动作维度高、需要精准输出的任务比如四足机器人腿部控制。但它对超参数和随机种子依然比较敏感如果你项目周期紧建议直接上TD3而不是DDPG。3.4 SAC最大熵与随机策略的优雅结合SACSoft Actor-Critic在AC框架里引入了“熵”这个正则项。熵代表策略的随机程度SAC在优化累计回报的同时还希望策略尽量随机。这么做的好处是探索更充分不容易陷入局部最优训练也更稳定。SAC在输出动作分布时通过重参数化技巧来保留梯度流所以既支持连续动作的随机策略又能在训练过程中自动调节熵的权重系数。在绝大多数连续控制benchmark上SAC的样本效率和最终性能都优于DDPG和TD3。代价是实现复杂度高一些需要维护两个Critic、一个Actor、一个温度系数以及各自的目标网络。如果你做的是仿真环境且计算资源足够我建议直接上SAC。它属于那种“你愿意多写几百行代码就能换回大量调参时间”的算法。3.5 变体之外的扩展方向离线IQL与基于模型强化学习除了在线交互式的AC算法现在有两个方向在实际项目中越来越常见。一个是离线强化学习典型代表是IQLImplicit Q-Learning。它的核心思路是只用已有数据集训练不与环境交互通过expectile回归让Q函数只学到数据里“比较好的行为”的价值从而避免分布外动作的过估计。做推荐系统、医疗策略这类没法在线试错的场景IQL是AC框架在离线环境下的一个漂亮变种。另一个是基于模型的强化学习。这类方法先用一个神经网络学习环境的状态转移模型然后在这个“虚拟环境”里用AC算法训练策略。优势是样本效率可以提升一到两个数量级非常适合真实机器人这类采样成本极高的场景。但世界模型学得不准的时候策略会在误差累积中跑偏实际应用需要配合模型不确定性估计来兜底。这些扩展方向说明AC框架的生命力很强换掉其中某个组件就能适配完全不同的场景约束。你在选型的时候核心对比维度就三件事动作空间是离散还是连续、交互成本是便宜还是昂贵、策略是否允许较大的探索幅度。4. 实操实现从伪代码到可运行的A2C核心逻辑4.1 主循环与数据流采样、算优势、更新前面讲了一堆理论现在落到代码层面。下面是一个最简A2C的训练主循环伪代码所有AC类算法基本都长这样# 伪代码展示AC主循环数据流 for epoch in range(total_epochs): # 1. 并行环境采样 states, actions, rewards, next_states, dones collect_rollout(envs, actor) # 2. 计算TD误差与GAE优势 values critic(states) next_values critic(next_states) td_errors rewards gamma * next_values * (1 - dones) - values advantages compute_gae(td_errors, gamma, lam) returns advantages values # Critic的回归目标 # 3. 更新Critic让V(s)逼近returns critic_loss mse_loss(critic(states), returns) update(critic, critic_loss) # 4. 更新Actor让好动作的概率变大 log_probs actor.log_prob(states, actions) actor_loss -(log_probs * advantages.detach()).mean() update(actor, actor_loss)这个结构里有一个关键细节Actor更新用的优势必须做detach也就是不让梯度通过优势值反向传播到Critic或价值估计上。原因很直接——我们希望Actor只调整策略不想让它莫名其妙去修改Critic网络的表征。很多新手写代码忽略了这一步梯度流一起反传训练必崩。4.2 GAE的计算实现GAE看起来有一串累加实际实现用一个从后往前扫描的累积变量就能搞定def compute_gae(rewards, values, dones, gamma0.99, lam0.95): 从后往前扫描计算GAE优势。 values: 每个时刻的V(s_t) dones: 回合结束标记1表示终止 T len(rewards) advantages [0] * T gae 0 next_value 0 for t in reversed(range(T)): # 如果t是回合终止则下一时刻价值视为0 delta rewards[t] gamma * next_value * (1 - dones[t]) - values[t] gae delta gamma * lam * (1 - dones[t]) * gae advantages[t] gae next_value values[t] return advantages这里有个细节dones的处理非常重要。回合终止时目标值应该是r_t后面没有未来回报了所以next_value必须归零否则价值估计会把不存在的未来回报算进去产生严重的偏置。我看到过不少开源代码在这个地方偷懒导致算法在稀疏奖励环境里怎么调都学不会。4.3 连续动作场景下Actor的输出层设计如果动作空间是连续的Actor输出层的设计很讲究。以机械臂关节控制为例假设有6个关节动作维度是6。通常做法是让Actor网络输出每个关节动作的均值和log标准差import torch import torch.nn as nn import torch.distributions as D class ContinuousActor(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim256): super().__init__() self.trunk nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), ) self.mean_head nn.Linear(hidden_dim, action_dim) self.log_std nn.Parameter(torch.zeros(action_dim)) def forward(self, obs): feat self.trunk(obs) mean self.mean_head(feat) std torch.exp(self.log_std.clamp(-20, 2)) return D.Normal(mean, std) def sample_action(self, obs): dist self.forward(obs) action dist.sample() return action, dist.log_prob(action).sum(dim-1)为什么要把log_std做成一个独立的可学习参数而不是从网络里直接输出因为均值的量级和方差量级差别很大直接让网络同时输出两套参数梯度容易失衡。把std设成可学习参数让网络只去学均值相当于告诉优化器“方差这种东西我自己慢慢调”整个网络练起来会稳很多。采样动作之后梯度不能通过随机采样节点反向传播这是另一处新手高频踩坑点。连续控制任务中通常需要加tanh来限制动作范围不过这里为了保持逻辑清晰先不给代码加料。4.4 经验回放、目标网络与并行环境的取舍A2C这类on-policy算法不开经验回放因为旧策略采出来的数据对当前策略没意义。而DDPG、TD3、SAC这类off-policy算法必须开经验回放因为它们复用一个固定大小的buffer每次更新随机抽一批样本。目标网络的作用是让价值更新的目标值相对稳定。如果不加目标网络Critic一边更新自己、一边用更新后的自己作为目标相当于“自己给自己出题”容易数值震荡。DQN里这个思想已经成熟DDPG/TD3/SAC全继承了这一设计。并行环境方面A2C一般开4到16个并行环境每个环境算出来的梯度汇总后再更新能显著降低训练方差。如果你机器资源充足我建议并行数量往大里顶CarPole这种轻量环境开8个都没问题物理仿真器环境则要看CPU负载一般4个比较稳妥。5. 训练中那些让你怀疑人生的坑与排查技巧5.1 奖励尺度过大导致梯度爆炸AC算法对奖励的尺度非常敏感。如果奖励动辄上千TD误差的量级就会非常大梯度更新一步就可能把网络参数甩到天边去训练曲线直接变野马。我接过一个项目环境里给了一个“完成任务1000”的稀疏大奖励训练前500轮完全看不到任何有效学习后来把奖励除以100放到一个相对小的量级策略才开始正常上升。处理思路主要有三种一是对奖励做缩放/归一化二是对优势值做标准归一化三是在设计环境时就把奖励的尺度控制在合理范围。如果算法里已经用了GAE优先对GAE输出做标准化操作简单作用明显。5.2 熵坍缩策略过早变“一根筋”Actor输出的动作分布方差会随着训练逐渐缩小如果缩得太快策略就过早变成确定性策略探索能力几乎归零性能会卡在一个次优水平而不自知。排查方法很粗暴把每轮动作分布的熵打印出来如果熵值断崖式下降说明策略很快就锁死在某条路上了。解决手段一是调大熵系数直接给Actor损失里加正则项二是换用SAC这种自带自适应熵权重的算法三是审查奖励设计看是不是存在特别容易被利用的捷径让策略早早扎进去。机械臂抓取里很常见的一个现象是一开始策略老往某个固定方向偏熵值掉到接近零这个时候大多数情况是奖励信号给得太偏了。5.3 Critic学偏了价值估计不收敛如果Actor学到的东西不错但Critic的损失一直居高不下常见原因是当前策略下价值函数的输入分布一直在变Critic追不上。另一个原因是某些状态的价值在不同回合里差异太大方差高的地方Casualty很难拟合。遇到这种情况我会先做一步“价值校准检查”在训练早期打印Critic对随机初始状态的输出看量级是否合理。如果V(s)的输出动不动就上亿那多半是TD误差累积导致的发散需要减小学习率、增加归一化层或者检查奖励是否被错误地重复累加。5.4 不同随机种子下的方差问题AC算法的运气成分比想象中大得多同一个超参数、同一个环境随机种子不同可能训练结果天差地别。做实验对比的时候务必跑多个种子取平均只跑一遍得出“我的算法比基线好”的结论在AC领域没有任何说服力。种子之外PyTorch的浮点运算在GPU上有不确定性同一个种子在不同硬件上也可能不完全一致。如果复现性对你的项目很重要建议固定CPU线程数并关闭cudnn的自动tuning。但我也要说句实话在真实项目里追求绝对复现意义不大环境本身的随机性早把这点差异淹没掉了把精力放在提升平均性能和稳定性上更划算。5.5 超参调优优先级先学习率再GAE λ最后熵系数给还在调参泥潭里挣扎的朋友一份我自己的调参顺序这是一份优先级清单照着顺序试会少走很多弯路。第一优先看学习率。AC里Actor和Critic的学习率可以分开设Critic通常略微慢一点经验范围在3e-4到1e-3之间。如果训练直接NaN先把学习率降到1e-4以下排查代码里有没有除零或log(0)问题。第二优先调GAE的λ它直接决定优势估计的平滑程度。第三才轮到熵系数而且最好只在确定性策略导致探索不足时再动它。调试的时候别只看episode reward这一条曲线至少要把actor_loss、critic_loss、entropy、advantage的均值这几条曲线一起打出来。有一次我调了半天reward上不去翻曲线才发现critic_loss早就在起飞问题根本不在策略更新而在价值网络本身。6. 写在最后的一点个人体会做深度强化学习这几年一个很深的感受是算法框架的迭代替换永远在发生但Actor-Critic的核心结构始终没变。从最开始的A2C到现在的PPO、SAC、TD3本质上都是T接不同组件价值函数、优势估计、置信域约束、熵正则项但“Actor出策略、Critic做裁判”这套交互机制经久不衰。对于刚入门的同学我建议从手写一个极简A2C开始在CartPole或Pendulum上跑通整套采样-更新流程然后一步步往上叠加GAE、并行环境、裁剪目标。这个过程比直接调库学到的细节多得多因为强化学习的坑几乎全藏在工程细节里dones有没有正确处理、detach有没有加、梯度有没有被错误地截断这些地方全靠手写踩过一遍才有体感。如果你已经在项目里用AC算法跑了很久但效果不顺不妨回头检查一下我在常见问题里列的那几条。很多时候困扰你三天的问题就是某个细节没处理到位跟算法本身的能力没关系。调策略优化算法拼的就是谁更细心。
返回列表