欢迎来到图解强化学习的世界博客主页卿云阁欢迎关注点赞收藏⭐️留言首发时间2026年4月9日✉️希望可以和大家一起完成进阶之路作者水平很有限如果发现错误请留言轰炸哦万分感谢目录主要思想核心点策略比率Probability Ratio优势函数Advantage Function广义优势估计GAE代码实现策略模型和价值模型​编辑定义PPO类模型更新update主要思想传统的 Policy Gradient在更新策略时非常脆弱如果 step size 过大, 学出来的 Policy 会一直乱动, 不会收敛, 但如果 Step Size 太小, 对于完成训练, 我们会等到绝望.PPO 利用 New Policy 和Old Policy 的比例, 限制了 New Policy 的更新幅度, 让 Policy Gradient 对稍微大点的 Step size不那么敏感.核心点策略比率Probability Ratio衡量“新旧”策略的差异优势函数Advantage Function评估行为的“好坏”优势函数Advantage Function是强化学习中一个非常重要的概念。它告诉我们在某个特定状态下采取某个特定动作到底比“平均水平”好多少。广义优势估计GAE更精准的“惊喜”评估GAE是对“动作”的评价。GAE 值 (At​)物理意义神经网络的动作 (Update)正数 ()这个动作比预期更好是“妙手”。增加该动作在当前状态下的选择概率。接近 0这个动作的表现符合预期。保持当前的概率分布基本不变。负数 (-)这个动作导致局面恶化是“俗手/昏招”。降低该动作在当前状态下的选择概率。策略损失剪裁的直观理解为什么min函数是关键现在关键来了为什么我们要取这两个项的最小值min代码实现策略模型和价值模型# 策略模型给定状态生成各个动作的概率 class PolicyModel(nn.Module): def __init__(self, input_dim, output_dim): super(PolicyModel, self).__init__() # 使用全连接层构建一个简单的神经网络ReLU作为激活函数 # 最后加一个Softmax层使得输出可以看作是概率分布 self.fc nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, output_dim), nn.Softmax(dim 1) ) # 定义前向传播输出动作概率分布 def forward(self, x): action_prob self.fc(x) return action_prob # 价值模型给定状态估计价值 class ValueModel(nn.Module): def __init__(self, input_dim): super(ValueModel, self).__init__() # 网络结构和策略模型类似但是输出层只有一个节点 self.fc nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 1) ) # 定义前向传播输出价值估计 def forward(self, x): value self.fc(x) return value假设当前物理引擎传来的状态 Tensor 依然是X [1.0, 0.0, 1.0, 0.0, 0.5, -0.2]形状为 (1* 6)PolicyModel的输出action_prob [0.78, 0.18, 0.04] 状态策略ValueModelvalue -85.5 状态价值定义PPO类使用策略模型生成动作概率分布并采样state [1.0, 0.0, 1.0, 0.0, 0.5, -0.2]action_prob [0.70, 0.20, 0.10]action 1# 使用策略模型生成动作概率分布并采样 def choose_action(self, state): # 将状态转换为tensor输入模型 state torch.FloatTensor(np.array([state])).to(self.device) with torch.no_grad(): action_prob self.policy_model(state) # 生成分布后采样返回动作 c torch.distributions.Categorical(action_prob) action c.sample() return action.item()广义优势估计# 广义优势估计 def calc_advantage(self, td_delta): # 将TD误差转换为numpy数组 td_delta td_delta.cpu().detach().numpy() # 初始化优势函数值及存储优势值的列表 advantage 0 advantage_list [] # 反向遍历从最后一步开始倒推 for r in td_delta[::-1]: # 将当前步的TD误差及上一步优势加权值作为当前步的优势 advantage r self.gamma * self.lamda * advantage # 将优势值加到列表开头最终得到顺序序列 advantage_list.insert(0, advantage) # 转换为tensor后返回 return torch.FloatTensor(np.array(advantage_list)).to(self.device)假设在一个回合的最后 3 步里教练Critic计算出的单步 TD 误差td_delta如下第 0 步 (t0)10.0第 1 步 (t1)5.0第 2 步 (t2)-10.0 (游戏结束)td_delta【10.0 5.0 -10.0】gamma (折扣因子) 0.9lamda (GAE 平滑因子) 0.9decay gamma * lamda 0.81for r in td_delta[::-1]: # 将当前步的TD误差及上一步优势加权值作为当前步的优势 advantage r self.gamma * self.lamda * advantage # 将优势值加到列表开头最终得到顺序序列 advantage_list.insert(0, advantage) # 转换为tensor后返回 return torch.FloatTensor(np.array(advantage_list)).to(self.device)处理最后一步 (t2)r -10.0advantage -10.0 0.81 * 0 -10.0advantage_list [-10.0]物理含义 这是最后一步没有未来了所以它的 GAE 优势就等于它自己的 TD 误差。处理倒数第二步 (t1) r 5.0advantage 5.0 0.81 * (-10.0) 5.0 - 8.1 -3.1advantage_list [-3.1, -10.0]处理倒数第三步 (t0) r 10.0advantage 10.0 0.81 * (-3.1) 10.0 - 2.511 7.489advantage_list [7.489, -3.1, -10.0]TD 误差 就像是一个短视的裁判只看杆子这一瞬间有没有向上抬。GAE 就像是一个懂牛顿力学的物理学大师它看的是能量的注入与传递。模型更新updatebuffer [ # 第 1 步 (未通关) ([1.0, 0.0, 1.0, 0.0, 0.0, 0.0], 0, -1.0, [0.9, 0.1, 0.8, 0.2, 0.5, 0.1], False), # 第 2 步 (通关了) ([0.9, 0.1, 0.8, 0.2, 0.5, 0.1], 2, 0.0, [0.0, 1.0, -1.0, 0.0, 0.0, 0.0], True) ]statestensor([[ 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000],[ 0.9000, 0.1000, 0.8000, 0.2000, 0.5000, 0.1000]], devicecuda:0)rewardstensor([[-1.0000], [ 0.0000]], devicecuda:0)actionstensor([[0],[2]], devicecuda:0)donestensor([[0.], [1.]], devicecuda:0) # 1. 代表 Trueold_action_prob torch.log(self.policy_model(states).gather(1, actions))td_target rewards (1 - dones) * self.gamma * self.value_model(next_states) td_delta td_target - self.value_model(states)advantage self.calc_advantage(td_delta)advantage_list[-0.82, -2.0]假设在第 2 轮action_prob torch.log(self.policy_model(states).gather(1, actions))ratio torch.exp(action_prob - old_action_prob)ratio 矩阵的物理意义非常直观它是“新旧自我的偏差度”part1 ratio * advantage part2 torch.clamp(ratio, 1 - self.clip_eps, 1 self.clip_eps) * advantageAdvantage 0.80part1ratio* advantage 1.30* 0.80 1.04part2torch.clamp(1.30, 0.8, 1.2)1.2 1.20 *0.80 0.96“既然要截断直接用 part2 不就行了为什么要算 part1 还要取最小值”这是因为 PPO 遵循“悲观原则”对于好动作如果涨得太猛了它选 part2更小的那个防止过度膨胀。对于坏动作如果 ratio 往上涨了比如烂动作的概率反而增加了part1 会是一个非常大的负数系统会选 part1 狠狠地惩罚它。policy_loss -torch.min(part1, part2).mean()value_loss F.mse_loss(self.value_model(states), td_target).mean()【1】参考文章(18 封私信) 图文并茂彻底讲明白PPOProximal Policy Optimization算法。全文16000字。 - 知乎