从“瞎推车”到“平衡大师”:一文读懂强化学习里的策略梯度法(小白也能懂)
没有复杂的数学公式只有代码和故事带你轻松看懂强化学习的核心算法。你有没有想过一个什么都不会的小车怎么就能自己学会让一根杆子立着不倒它一开始只会乱动但玩着玩着居然变成了高手。这背后全靠一套叫策略梯度法的算法。今天我就用最通俗的话把这个算法从头到尾讲清楚。你不需要懂微积分也不需要会推导公式只要有一点 Python 基础就能看懂。一、先认识一下倒立摆游戏长啥样想象一个画面一辆小车车顶上立着一根杆子。你只能控制小车向左推或向右推。杆子每坚持 1 秒不倒下你就得 1 分。杆子一旦倒下游戏结束。你的任务写一个程序让小车学会自己保持平衡。你能控制的 vs 你不能控制的你唯一能改的就是你的策略。策略长什么样现在的做法是用一个神经网络来做决策。输入小车的位置、速度杆子的角度、角速度一共 4 个数。输出两个概率——向左推的概率向右推的概率。比如神经网络可能输出左推 0.7右推 0.3。然后像掷骰子一样70% 的概率左推30% 的概率右推。这样既有规律又带一点随机性。这就是策略从状态到动作概率的映射。二、核心思想好动作多鼓励坏动作少鼓励我们怎么让这个策略越来越好一个很自然的想法玩完一局之后看看总得分。如果得分高就说明这一局里做的动作大部分都是好的那就提高这些动作的概率。如果得分低就降低这些动作的概率。比如你玩了一局总分 100 分其中在第 3 秒的时候你选择了“左推”。那么我们就认为“在那种状态下左推”是个好动作于是让神经网络以后在那个状态下更倾向于左推。这就是策略梯度法的灵魂用整局的总分或者某个得分作为“权重”去调整每个动作的概率。三、REINFORCE最简单直接的版本REINFORCE 是策略梯度法里最朴素的一个版本也最容易理解。它的做法用当前的策略玩完整的一局记录下每一步状态、动作、奖励。计算出这一局的总回报所有奖励加起来可以打折越往后越不重要。对每一步用总回报 × log(动作概率)来更新神经网络。用人话说如果这局总分很高那么所有执行过的动作它们的概率都会被拉高如果总分很低就拉低。代码实现你一定能看懂import gym import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.distributions import Categorical # 策略网络输入状态输出动作概率 class PolicyNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(4, 128) # 4个状态值 - 128个神经元 self.fc2 nn.Linear(128, 2) # 2个动作左/右 def forward(self, x): x F.relu(self.fc1(x)) probs F.softmax(self.fc2(x), dim-1) return probs # REINFORCE 智能体 class REINFORCE: def __init__(self): self.net PolicyNet() self.optim optim.Adam(self.net.parameters(), lr0.001) self.gamma 0.99 # 折扣因子 def get_action(self, state): # 把状态变成 tensor喂给网络 state_t torch.FloatTensor(state).unsqueeze(0) probs self.net(state_t)[0] # 得到 [左概率, 右概率] m Categorical(probs) # 建造一个概率分布 action m.sample() # 按概率随机采样 return action.item(), probs[action] # 返回动作和它的概率 def update(self, trajectory): # trajectory 包含一局的所有状态、动作、奖励 states, actions, rewards trajectory # 计算每一步的“折扣总回报” returns [] G 0 for r in reversed(rewards): # 从最后一步往前累加 G r self.gamma * G returns.insert(0, G) loss 0 for s, a, G_t in zip(states, actions, returns): s_t torch.FloatTensor(s).unsqueeze(0) probs self.net(s_t)[0] log_prob torch.log(probs[a]) # log(概率) loss -log_prob * G_t # 损失 -总回报 * log(概率) self.optim.zero_grad() loss.backward() self.optim.step() return loss.item() # ---------- 训练 ---------- env gym.make(CartPole-v1) agent REINFORCE() for episode in range(2000): state env.reset() states, actions, rewards [], [], [] done False while not done: action, _ agent.get_action(state) next_state, reward, done, _ env.step(action) states.append(state) actions.append(action) rewards.append(reward) state next_state total sum(rewards) loss agent.update((states, actions, rewards)) if episode % 200 0: print(f第{episode}局总分{total}损失{loss:.4f}) print(训练完成)你是不是想问为什么计算损失要用 -log_prob × G_t 因为我们的目标是让总分变大而神经网络默认是做“最小化损失”。所以我们把“最大化总分”转化成“最小化 负的总分 × log概率”。简单记好动作G_t 为正会让 -log_prob 的梯度变小从而提升它的概率。REINFORCE 的问题太“冲动”REINFORCE 有一个缺点它用整局的总分来衡量每个动作的好坏。但有时候某个动作明明很好可是前面已经犯了大错导致总分很低这个好动作也跟着被批评了。反过来一个很差的动作因为前面有人兜底总分还挺高它也被表扬了。这就好比你考了全班第一但你的同桌只是帮你递了块橡皮就被当成功臣。不合理吧解决方法不要用总分而是用“比平均好多少”。四、Actor-Critic请一个“评论家”来帮忙既然用整局总分太粗糙我们就请一个评论家来打分。演员 (Actor)还是那个策略网络负责出动作。评论家 (Critic)另一个神经网络负责估计“当前状态大概值多少分”。每走一步评论家马上给出一个反馈这一步带来多少“惊喜”。这个“惊喜”叫作TD 误差它的计算方式是惊喜 这一步得到的奖励 下一步状态的估计价值 - 当前状态的估计价值如果惊喜是正的说明这一步做得比预期好演员就增加这个动作的概率如果是负的就减少。为什么这样更好不需要等整局结束每走一步就能学习效率高。惊喜值排除了“历史包袱”只看这一步的贡献方差更低训练更稳。核心代码只改 update 部分class ActorCritic: def __init__(self): self.actor PolicyNet() # 演员 self.critic ValueNet() # 评论家输出一个数字表示状态价值 self.optim_a optim.Adam(self.actor.parameters(), lr0.0003) self.optim_c optim.Adam(self.critic.parameters(), lr0.001) self.gamma 0.99 def update(self, s, a, r, s_next, done): s_t torch.FloatTensor(s).unsqueeze(0) s_next_t torch.FloatTensor(s_next).unsqueeze(0) v self.critic(s_t) # V(s_t) with torch.no_grad(): target r self.gamma * self.critic(s_next_t) * (1 - done) # TD目标 td_error target - v # 惊喜 # 演员更新 -惊喜 * log π(a|s) probs self.actor(s_t)[0] log_prob torch.log(probs[a]) actor_loss -log_prob * td_error.detach() # 评论家更新 让 V(s_t) 尽量接近 TD目标 critic_loss torch.nn.functional.mse_loss(v, target) self.optim_a.zero_grad() actor_loss.backward() self.optim_a.step() self.optim_c.zero_grad() critic_loss.backward() self.optim_c.step()注意td_error.detach() 的意思是演员的更新只影响演员网络不要让评论家的梯度串过来。五、GAE让评论家看得更远上面的 Actor-Critic 只看了1 步的未来。但有时候一个动作的影响要好几步之后才体现出来。只看一步会有点“近视”。GAE广义优势估计就是一个更聪明的评论家它会把多步的未来都考虑进去然后给你一个加权平均的“惊喜”。你可以这样理解只看 1 步反应快但可能看不清长远影响。看完整局看得准但容易受噪声干扰。GAE既看一步又看两步又看三步……然后取一个折中。GAE 的代码实现也很简单逆向递推def compute_gae(rewards, values, gamma0.99, lam0.95): advantages [] gae 0 for t in reversed(range(len(rewards))): if t len(rewards)-1: delta rewards[t] - values[t] else: delta rewards[t] gamma * values[t1] - values[t] gae delta gamma * lam * gae advantages.insert(0, gae) return advantages现在很多先进算法比如 PPO都默认使用 GAE。它几乎成了策略梯度方法的“标配”组件。六、GRPO大模型时代的策略梯度新玩法最近大语言模型比如 ChatGPT也用上了强化学习其中有一个叫GRPO的新算法很有意思。它发现在大模型训练中训练一个“评论家网络”太贵了要占一半显存。于是 GRPO 想了个办法不要评论家改用组内比较。GRPO 的做法极其简单对同一个问题让模型生成4 个回答。用一个奖励模型给这 4 个回答分别打分90分, 80分, 70分, 60分。算出这组分数的平均值和标准差。把每个分数标准化 (自己的分 - 平均分) / 标准差。这样好的回答高于平均得到正的权重差的回答低于平均得到负的权重。完全不需要训练价值网络省了一半的算力。这就是 GRPO 的核心思想用组内相对好坏代替绝对价值估计。GRPO 已经在一些大模型对齐任务中表现出比 PPO 更高效、更稳定的效果是策略梯度方法在 LLM 时代的重要进化。七、看懂所有方法的演进策略梯度思想 │ ├─ REINFORCE │ └─ 用整局总分简单但方差大 │ └─ Actor-Critic ├─ 用 1 步 TD 误差方差小但有偏 ├─ GAE → 多步折中更稳PPO 就用它 └─ GRPO → 去掉评论家用组内比较适合大模型虽然它们看起来越来越复杂但核心只有一个好动作 × 正权重 → 提高概率坏动作 × 负权重 → 降低概率不同的算法只是在想办法得到一个更靠谱的“权重”。八、总结与建议今天我们从一个倒立摆游戏开始一步步走过了为什么需要策略梯度因为环境是黑箱没法直接求导。REINFORCE用整局总分当权重简单但波动大。Actor-Critic引入评论家用“惊喜”来更新方差更低。GAE多看几步平衡偏差与方差。GRPO大模型时代的新思路不用评论家改用组内比较。如果你是个刚接触强化学习的新手我建议先把REINFORCE的代码跑一遍亲眼看着小车从乱晃到站稳。再改成Actor-Critic感受训练曲线变得更平滑。如果感兴趣可以进一步研究PPO它就是在 GAE 的基础上加了“截断”防止步子迈太大。记住一句话策略梯度法本质上就是用奖励信号去调动作的概率。奖励大就多调奖励小就少调奖励负数就反向调。希望这篇文章能帮你看懂策略梯度法的全貌。有任何问题欢迎留言交流。我们一起做出更聪明的智能体。