第一次跑通策略梯度的时候,我盯着终端里的reward曲线发呆——它跳得比股票分时图还猛。损失函数明明在下降,reward却一路乱颤;换个随机种子,出来的又是另一条曲线。这不是我的bug,而是策略梯度(Policy Gradient)这一类方法骨子里的特性。
策略梯度不是某一个算法,而是一整类方法的统称:REINFORCE、Actor-Critic、TRPO、PPO、A3C,全都挂在它名下。它们的共同点是:直接参数化一个策略,然后通过采样的回报信号去更新参数。换句话说,只要你的训练循环里出现了"动作的对数概率乘以回报"这种东西,你就跑不出策略梯度的框架。
这篇文章我尽量按"从原理到代码再到调参"的顺序写,包含完整推导、最小实现、方差分析和现代算法演进脉络。无论你是刚入门想搞懂REINFORCE为什么有效,还是已经在用PPO炼丹却说不清它内部在做什么,都可以收藏起来当参考资料。
1. 为什么学强化学习绕不开策略梯度
1.1 价值学习的天花板:连续动作和随机策略
先聊点背景。强化学习很长一段时间里被DQN这类价值方法刷屏,价值学习的思想非常优雅:先学会估计"在状态s下采取动作a有多好",即Q(s,a),然后策略就是简单地取argmax Q(s,a)。
这套路在动作空间是离散且维度不高的时候很好用,比如Atari游戏。但你一旦往更真实的任务上靠,两个问题就会跳出来。
第一个问题是动作空间连续。机械臂的关节角度、自动驾驶的方向盘转角、连续控制里的力度,都是连续量。DQN要取的是argmax Q(s,a),那意味着要对a做最优化——在连续空间里这通常是个费劲的非凸优化问题,每一步决策都现算一个最优化,成本和延迟都受不了。
第二个问题更深,也常被忽略:价值学习只能表达确定性策略,而很多任务的最优策略本身就是随机的。最经典的是猜拳和扑克里的诈唬,你如果总是出同样的动作,对手很容易适应秒杀。自博弈场景里,随机策略往往是纳什均衡的一部分。价值方法对这种需求无能为力。
1.2 策略梯度的思路:直接parameterize策略
策略梯度的思路是换个角度:干脆把策略本身定义成一个带参数的分布。
离散动作空间,策略可以是一个softmax网络,输出动作的离散概率分布;连续动作空间,策略可以输出高斯分布的均值和方差,从里面采样得到动作。
这样做的直接好处是:
- 不需要对动作空间做最优化,采样就行,连续动作和离散动作天然统一。
- 策略天然是随机策略,熵可控,探索和利用可以直接通过熵来调节。
- 策略的参数化可以非常简单,网络输入状态,输出动作分布,"下一步做什么"由这个分布决定。
1.3 一个直观的类比:策略是如何被"推"着走的
想象你闭着眼站在一座山坡上,目标是找到一个海拔更高的位置。你看不见全貌,只能试着朝某个方向迈一步,然后感受脚底是上坡还是下坡。
策略梯度的更新逻辑很接近这个画面。先让当前策略在环境里跑几轮,收集轨迹和回报;然后根据这些轨迹,把"带来高回报的动作"对应的策略概率调大,把"带来低回报的动作"对应的概率调小。参数θ往前走一步,策略就微调一步。反复这个过程。
但这句"高回报的动作概率调大"说起来简单,实际做起来有个很深的数学问题:你怎么知道某个动作的功劳有多大?一个轨迹里的回报是所有动作共同作用的结果,而且环境本身还有随机性。策略梯度定理就是用来解决这个分配问题的,下面把它彻底推导一遍。
2. 策略梯度定理:完整推导与那只"看不见的归一化"
2.1 目标函数:把"策略好坏"写成可求导的形式
我们的优化目标是一个期望总回报。定义策略πθ(a|s),轨迹τ是一连串的状态和动作:
τ = (s0, a0, r0, s1, a1, r1, ..., sT, aT, rT)轨迹发生的概率取决于策略参数θ和环境转移概率:
pθ(τ) = p(s0) · Π πθ(at|st) · p(st+1|st, at)注意这里面的环境转移p(s'|s,a)和初始状态分布p(s0)我们都不会去管它,它们不是θ的函数。
目标函数定义为期望回报:
J(θ) = Eτ~pθ [ R(τ) ] = ∫ pθ(τ) R(τ) dτ其中R(τ)可以是一个轨迹的总回报,也可以是折扣回报。
2.2 梯度重写:log-ratio魔术
现在要求J(θ)关于θ的梯度,严格推导如下:
∇θ J(θ) = ∇θ ∫ pθ(τ) R(τ) dτ = ∫ ∇θ pθ(τ) R(τ) dτ = ∫ pθ(τ) ∇θ log pθ(τ) R(τ) dτ = Eτ~pθ [ R(τ) ∇θ log pθ(τ) ]这里用的是对数导数技巧:
∇θ log pθ(τ) = ∇θ pθ(τ) / pθ(τ)所以pθ(τ)乘上∇θ log pθ(τ)就等于∇θ pθ(τ)。这个技巧的意义在于:我们没法知道环境的概率分布,也就没法直接算∇θ pθ(τ),但在给定一条采样的轨迹后,∇θ log pθ(τ)是可求的。期望依然在,但我们可以通过采样去近似它。
接下来把pθ(τ)展开,梯度的对数只剩下策略部分:
∇θ log pθ(τ) = ∇θ [ log p(s0) + Σ log πθ(at|st) + Σ log p(st+1|st, at) ] = Σ ∇θ log πθ(at|st)环境项全部消掉了。这是策略梯度最迷人的地方:你完全不需要知道环境的动力学模型,只需要按当前策略采样,然后计算动作概率的对数梯度就行。
2.3 为什么要用"对数概率×回报"作为更新方向
把上面的结论合起来,梯度可以写成:
∇θ J(θ) = E [ (Σ ∇θ log πθ(at|st)) · R(τ) ]也就是说,一条轨迹的贡献是:把每一步动作的梯度方向加起来,再乘以整条轨迹的回报。
从直觉上理解,这个式子干了这样一件事:如果一条轨迹最终得到正回报R>0,那么沿∇θ log πθ(at|st)方向调整参数,可以提高该轨迹中所有动作的概率;如果R<0,就降低所有动作的概率。这些动作里当然有好的也有坏的,R>0就把好坏动作的概率一起抬高了,R<0就一起压低了。这正是方差问题的雏形,后面专门分析。
2.4 因果性小技巧:从整条回报换成"当前时刻之后的回报"
另一个重要细节是:轨迹中某个动作at其实只影响它之后的累积回报,而不影响它之前的回报。但在上述推导中,我们用的是整条轨迹的R(τ),理论上是无偏的,方差却不必要地大。
于是实际算法里几乎都会做一步"因果化"替换:把R(τ)换成从当前时刻开始的折扣回报:
Gt = rt + γ rt+1 + γ² rt+2 + ...为什么这样做期望不变?因为动作at对过往时刻的回报没有因果影响,那些过往回报乘以∇log πθ(at|st)的期望为0。既然不影响期望方向,却能显著减少单个样本带来的随机波动,这就是一个免费的方差改良手段。
2.5 基线的出现:公式里天然留了一个口子
还有一个关键点:上述梯度期望中,如果我们在回报上减去一个常数b,期望会怎么变?
∇θ J(θ) = E [ ∇θ log πθ(τ) · (R(τ) - b) ]因为:
E [ ∇θ log πθ(τ) ] = Σ ∇θ πθ(τ) = ∇θ 1 = 0所以减b不会改变梯度的期望,梯度依然是无偏的。但方差却可以大幅降低。这个"减一个数不影响期望但降低方差"的操作就是baseline,也可以叫基线。
很多第一次接触的人会问:既然不减也正确,为什么要减?因为R本身就是随机变量,不同轨迹的回报差异极大,从几百到几千都有;一个巨大正回报乘以梯度,会造成参数一步冲得太远。减去一个基线b,让被乘的数值围绕0波动,方差立刻小很多。这是后面所有策略梯度加速方法——从REINFORCE with baseline到Actor-Critic到GAE——的数学起点。
3. REINFORCE最小实现:第一份能跑的代码与它的致命方差
3.1 网络、采样器和轨迹收集
理解推导后,写一个最简的策略梯度算法(REINFORCE)其实只需要很少的代码。核心组件只有三个:策略网络、采样循环、回报计算。
我用PyTorch写一个CartPole上的最小版本,离散动作环境。策略网络就是一个输入状态、输出动作logits的MLP:
import torch import torch.nn as nn import gymnasium as gym class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim, hidden=64): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, act_dim) ) def forward(self, obs): return self.net(obs)这里输出的是logits,不是softmax后的概率。因为PyTorch的Categorical分布会自动做softmax,我们只需要logits。这里有个实现细节:保存概率的对数要直接调用dist.log_prob(action),不要先算概率再手动log,数值稳定性差很多。
单个回合的采样循环,把每一步的log_prob和reward存下来,注意action要从分布中sample,而不是argmax,否则就退化成确定策略了:
def collect_episode(env, policy): obs, _ = env.reset() log_probs = [] rewards = [] done = False while not done: obs_t = torch.as_tensor(obs, dtype=torch.float32) logits = policy(obs_t) dist = torch.distributions.Categorical(logits=logits) action = dist.sample() log_probs.append(dist.log_prob(action)) obs, reward, terminated, truncated, _ = env.step(action.item()) done = terminated or truncated rewards.append(reward) return log_probs, rewards3.2 从后往前算折扣回报
折扣回报的计算有一个经典技巧:从回合末尾向前累加,这样一次遍历就可以得到每一步的Gt,不用每次重新求和。
def compute_returns(rewards, gamma=0.99): g = 0 returns = [] for r in reversed(rewards): g = r + gamma * g returns.insert(0, g) return torch.tensor(returns, dtype=torch.float32)为什么要折扣?除了经济直觉上"未来的不确定性和机会成本",折扣因子还能有效压缩方差。折扣越大越考虑长远,折扣越小越短视,但这个参数其实也承担了"让远端回报对梯度的贡献不爆炸"的作用。
3.3 主循环:梯度上升变梯度下降
最后是更新过程。注意我们的目标是最大化回报的期望,所以理论上是梯度上升。在优化器里统一做梯度下降,那就给loss加个负号:
policy = PolicyNet(obs_dim=4, act_dim=2) optimizer = torch.optim.Adam(policy.parameters(), lr=1e-3) for episode in range(500): log_probs, rewards = collect_episode(env, policy) returns = compute_returns(rewards) # 减去回合平均回报,等价于一个最简单的baseline returns = (returns - returns.mean()) / (returns.std() + 1e-8) loss = -torch.stack(log_probs) * returns loss = loss.mean() optimizer.zero_grad() loss.backward() optimizer.step()这里的returns归一化其实就是批内baseline加上缩放控制,能让梯度没那么狂野。这个实现的完整逻辑闭环是:采样一条轨迹 -> 算每个时刻的回报 -> 让高回报动作的概率增大 -> 更新参数 -> 继续采。
3.4 第一次训练:曲线为什么像心电图
如果你亲手跑这个版本,大概率会看到reward曲线在0到200之间疯狂跳动,完全不像某些教程里那些平滑上升的图。这不是环境问题,而是REINFORCE的天然属性。
原因很简单:单条蒙特卡洛轨迹的回报方差极大。一个回合里,随便哪个动作走偏一点,拐杖倒了、小车翻了,回报就从200直接变0;而这个随机波动会被直接乘进梯度里,再叠加到参数更新上。所以REINFORCE有个广为人知的评价:理论上正确,实际慢且抖。
不过这个代码的意义和价值在于:它是策略梯度家族的"最小信息基准"。后面所有高级方法,都是在同一个骨架上打补丁——要么减小方差,要么提升样本效率,要么限制更新步幅。
4. 与方差的漫长斗争:baseline、优势函数和actor-critic
4.1 方差从哪来:一个轨迹和一个梯度的差距
要理解为什么策略梯度训练这么不稳定,得拆开方差的构成:
方差的第一层来自蒙特卡洛回报本身。一条轨迹的回报是所有随机转移和随机动作叠加出来的结果,长度越长,涉及的不确定性越多,单次回报的波动范围就越大。数学上等价于用一次样本来估计一个高方差分布的期望。
第二层来自"整条轨迹乘以整条轨迹的梯度"。一个不好的动作在低回报轨迹里,它的log_prob梯度会被一个大负数放大;一个碰巧好的动作也会被高分进一步放大。这导致每个单个动作的更新方向里都混了大量环境噪声。
第三层来自相邻参数更新之间的不独立性。策略梯度是on-policy的,参数一更新,下一批采样数据的分布就变了,我们相当于时刻在移动的靶子上射箭。
4.2 先减一个常数baseline:免费但不彻底
上一章代码里我顺手做了returns均值减法,这就是一个常数baseline。它把所有回报中心化一下,「这个回合比其他回合好还是差」作为信号。
这能解决一部分问题,但不彻底。原因是:不同状态下的回报尺度完全不同。某些状态天然action都是高分,某些状态下天然低分。用一个统一的常数去减,减不到点子上。你需要的是一个与状态相关的baseline。
4.3 状态价值baseline:actor-critic正式登场
最自然的baseline就是状态价值Vπ(s):给定当前状态,按当前策略继续走,期望能得到多少回报。用V(s)当baseline:
A(s,a) = Q(s,a) - V(s)这个A就是优势函数(advantage),它回答的问题是:在这个状态下,我采取这个动作,比平均情况好多少?A>0说明这是个好动作,A<0说明是个坏动作。梯度变成:
∇θ J(θ) = E [ ∇θ log πθ(a|s) · A(s,a) ]这个变换的意义不仅仅是降方差。它还改变了学习信号的语义:从"这条轨迹好不好"变成"这个动作在这一状态下好不好"。信号更细粒度了,策略更新更有针对性。
为了得到A(s,a),我们通常用一个神经网络Vφ(s)去逼近真实价值函数。于是就有了两个网络:
- actor:策略网络πθ,负责输出动作分布。
- critic:价值网络Vφ,负责估计状态价值,给actor提供优势信号。
critic的训练就是一个普通的回归问题:让Vφ(s)逼近蒙特卡洛回报Gt或者TD目标。actor继续按策略梯度更新。这种结构就是actor-critic(AC)算法。它名字起得很形象:「评论家」告诉「演员」每个动作相对于平均水平好多少,「演员」根据评价调整自己的表演。
4.4 为什么优势函数这么重要
我自己的体会是,从REINFORCE到actor-critic,算法最大的进步不是跑得更快,而是信号的语义变了。REINFORCE学到的是全局统计规律:高分轨迹里的动作整体概率上调;actor-critic学到的是局部因果关系:这个状态下这个动作比平均水平好多少。
为了更直观,可以类比成考试分析。如果你想找出「哪些题型值得重点练习」,只看总分是完全不够的,因为总分受太多因素影响。最有效的方式是:每个题目得分对比全班平均分。某道题你比平均分高很多,说明这块是你的强项;低很多,说明是弱项。优势函数干的就是这件事:它不是关心这个动作本身得分多少,而是关心它相对该状态下平均水平高多少。这个差比绝对分可靠得多。
4.5 用TD误差近似优势:进一步降低方差
实践中我们往往不会完全用蒙特卡洛来估计A,因为MC方差依然很大。更常见的是用TD风格的误差。举一个最简单的GAE前身:单步优势
A(s,a) ≈ r + γ V(s') - V(s)这个式子的好处是:它不需要跑完整个回合就能估计优势,方差比MC小。坏处是:如果Vφ估计不准,偏差会大。偏差和方差之间的权衡,正是后面GAE要做文章的地方。
5. 从REINFORCE到PPO:现代策略梯度算法到底在改什么
5.1 为什么能用旧数据再更新:重要性采样
REINFORCE每采集一批数据,更新一次参数,然后这批数据就扔掉。原因是策略变了以后,旧数据不再符合新的采样分布,直接复用在数学上不对。数据效率很低。
TRPO和PPO都在解决同一个问题:如何让旧数据可以被多次更新。关键工具是重要性采样。
设旧策略为π_old,新策略为π_new,对于旧数据中的(s,a),它的概率在两种策略下是不同的。如果我们想用旧策略采样下的期望来估计新策略下的期望,需要乘以一个比率:
ρ = π_new(a|s) / π_old(a|s)这个ρ就叫做重要性权重。如果新旧策略很接近,ρ接近1,旧数据仍然有效;如果差别很大,ρ偏离1,说明旧数据已经不足以代表新策略,这时权重就要被控制住。
TRPO和PPO都围绕这个ρ做文章,区别在于如何控制"新旧策略不能差太远"。
5.2 TRPO的硬约束和PPO的软裁剪
TRPO的做法是给更新加一个KL散度约束,保证新旧策略的距离不超过某个δ。理论很漂亮,但实现很重:需要用共轭梯度法求解约束优化,还要算KL散度的二阶近似,工程上非常吃力,日常调参更是噩梦。
PPO的思路则是把约束软化:与其硬性限制KL,不如在目标函数里直接加一道保险。PPO的经典目标是:
L_clip(θ) = E [ min( ρ · A, clip(ρ, 1-ε, 1+ε) · A ) ]这里的clip含义是:把ρ限制在[1-ε, 1+ε]区间内,常见ε=0.2。min操作保证:当A>0时,我们最多让概率提高到1+ε倍;当A<0时,最多让概率降低到1-ε倍。超出这个范围的更新,梯度为0,不起作用。
打个比方:TRPO是"你最多能走10米";PPO是"你可以随便走,但每走出一步都检查一下自己是不是离起点太远了,超过一个范围就罚停"。PPO用极小的工程代价,获得了接近TRPO的效果,而且超参数不那么敏感,这就是它成为事实标准的原因。
5.3 GAE:偏差和方差的滑杆
现代PPO实现里,优势估计使用的是GAE(Generalized Advantage Estimation),它其实是用一个参数λ来平滑地控制"到底用多长的回报窗口":
A_t = Σ (γλ)^l δ_{t+l} 其中 δ_t = r_t + γ V(s_{t+1}) - V(s_t)- λ=0时,A就是单步TD误差,方差最小但偏差大(依赖价值函数估计准)。
- λ=1时,A退化为蒙特卡洛回报的优势,偏差小但方差大。
GAE把方差和偏差的权衡变成一个可以调的旋钮。实践中λ=0.95是相当稳的起点,如果你发现训练过程中critic loss很难降下来,可以适当调低。
这里值得记住的原则是:价值函数估计越准,越可以用偏MC的优势;价值函数噪声大,就更得靠TD类的平滑。
5.4 熵正则:防止策略过早锁死
训练策略梯度模型时常会遇到一种尴尬:训练还没见起色,策略的熵先塌了。熵塌缩意味着策略几乎变成了确定性策略,只敢输出一个动作,不再探索。
解决方法很简单:在目标函数里加一项熵正则。
L = L_policy + 熵正则系数 β · H(π)熵的定义是:
H(π) = -Σ π(a|s) log π(a|s)熵大意味着分布均匀,探索充分;熵小意味着分布尖锐,确定性高。加熵正则就是给探索装个地保险,不让策略把所有概率压到一个动作上。常见β取0.001到0.01,稀疏奖励环境需要更大。
5.5 PPO一次训练的标准流程
把上面所有组件串起来,一次标准PPO训练循环长这样:
- 用当前策略π_old在环境中采集一批数据,记录每个step的s,a,r,done以及π_old(a|s)。
- 用GAE和当前价值网络计算优势估计A和回报目标G。
- 从这批数据里切minibatch,重复多个epoch更新:
- 计算新策略πθ(a|s),得到重要性权重ρ。
- 计算clip目标。
- actor损失 = -期望min(ρA, clip(ρ)A) - β熵。
- critic损失 = 均方误差(V(s), G)。
- 更新完θ和φ后,清空数据,回到第1步。
PPO与REINFORCE最大的差别除了clip之外,就是它能让一组数据小步地更新多个epoch,样本效率高很多。现代库里的PPO会配合向量化环境并行采样,一批几千条transition,效率和稳定性都是REINFORCE不能比的。
6. 实现策略梯度的常见坑与调试清单
强调一下:策略梯度是少数"看着end-to-end很顺、实际一跑就崩"的算法家族。这里列几个我自己在实战中踩过的坑,每条都对应一个可以检查的方向。
6.1 坑一:奖励尺度不对,梯度像一个醉汉
策略梯度的梯度大小与回报尺度成正比。如果reward量级动辄上千,梯度值也会巨大,参数一步就飞出物理边界;如果reward本身就接近0,梯度信号又太小,模型一动不动。
我调试的第一个策略梯度项目是机械臂仿真环境,奖励在几十到几百之间波动,直接把REINFORCE跑起来,loss直接NaN。
对策有两类:
- 环境层:检查reward的工程定义,必要时压缩尺度。比如对奖励做clip或者除以一个常数。
- 算法层:对所有优势或回报做标准化,例如减均值除以标准差,或者使用running normalization。
推荐优先从算法层处理,因为环境reward的语义往往和你对算法的修改纠缠在一起,改环境容易伤及任务本身的定义。
6.2 坑二:熵塌缩,探索提前死亡
训练到中后期,你可能会发现策略几乎不更新了,表现也不涨。这时候打印一下策略的熵:如果离散动作的概率分布几乎one-hot,连续动作的std已经降到极小,那就是熵塌缩。
一个小技巧是连续控制里关注log_std参数。有些框架直接输出log_std,如果训练中log_std一路降到-5以下,说明探索能力几乎归零。
对策:
- 在目标函数里加熵正则。
- 检查是否更新幅度过大。PPO的clip参数ε可以适当调小,比如0.1。
- 连续动作策略里给log_std设一个下限,避免它无限缩小。
我见过不止一次,去掉熵正则条件后算法跑着跑着突然崩溃,加上正则后虽然奖励略低但能稳定收敛。探索这个东西,在策略梯度里不是态度问题,是数学上必须有。
6.3 坑三:只看一次实验的曲线,被随机性骗了
策略梯度实验的随机性大得惊人。同一个代码、同一个超参数,换个随机种子,收敛速度可能差三倍,甚至有的种子成功、有的种子失败。
所以我的习惯是:
- 每个实验至少跑5个种子。
- 汇报结果时画中位数曲线,同时画分位数区间。
- 判断一个改动有没有效,看的是几个种子上的分布,而不是单条reward曲线。
很多论文里的对比图,大家默认也是有这个统计意识的。收藏这篇文章后如果你只记一条经验,那就是"多跑几个种子"。
6.4 坑四:复现性细节:gym版本、线程、种子初始化
复现实验是策略梯度里面最生气的事:同一个seed,在别人机器上跑就是另一个结果。常见的原因不完全在算法,而在工程环境:
- gymnasium/gym版本差异会影响环境动力学,比如termination和truncation的处理不同。
- 转置seed时没有同时固定numpy、torch、环境各自的随机源。
- 多进程采样时,线程并发导致采样顺序不稳定,进而影响批内数据的分布。
我的做法是写一个setup函数,统一处理seed并固定到进程级别:
def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False6.5 坑五:使用了旧策略状态继续训练,分布不匹配
这个问题常发生在加载一个训练到一半的模型继续训练时。策略梯度的采样分布依赖当前策略,加载模型前后如果熵值、reward scale完全不同,新一批数据的分布和旧数据分布差异巨大,梯度更新就会变得极不稳定。
遇到过的情况是:用A模型训练了100万步,保存;某天改了一行代码或者换了环境版本,重新加载A模型,继续在旧代码上训练,reward从原本的300直接跌到50,之后再也回不来。排查下来是reward normalization的统计量没有重置,旧均值旧方差把新信号的尺度压坏了。
对策:加载模型继续训练前,务必清空running statistics、重置熵系数、甚至考虑让初始KL不再惩罚。
调试策略梯度有点像调一艘很敏感的船:方向大致正确,但任何一节参数变化都可能带来不可忽视的震荡。建议一次只改一个变量,日志里至少打印五项:reward均值、策略熵、clip比例、critic loss、优势均值。"clip比例"特别有用——如果clip比例超过20%,说明新旧策略差异过大,学习率大概率太高。
写在最后的一点个人建议
如果你现在正准备从零开始学策略梯度,我的建议是不要一上来就调PPO库,先手工把REINFORCE在CartPole上完整写完、跑通、观察一遍。这个过程花的不是冤枉时间,你会亲眼看见方差长什么样,知道"减baseline"到底减掉了什么,也知道"打印熵"这种习惯为什么重要。有了这个底子,后面再上PPO、接分布式框架,都不会只是调参机器人。
策略梯度这块内容,理论上环环相扣,实践上处处有坑。希望这份整理能帮你把从第一个REINFORCE到现代PPO之间的路都走明白,也欢迎你把自己实际调试中遇到的反常现象拿出来一起讨论——很多我自己的经验,恰恰就是被各种"反直觉"的曲线教会的。