PPO强化学习算法详解

一、什么是PPO?

PPO是一种策略梯度方法,由OpenAI在2017年提出。它的核心思想是:在更新策略时,不要让新策略偏离旧策略太远,这样训练更稳定。

为什么需要PPO?

传统的策略梯度方法(如REINFORCE)更新步长难以控制,容易导致:

  • 更新太大 → 策略崩溃
  • 更新太小 → 训练缓慢

二、PPO的核心公式

1. 重要性采样比率

rt(θ)=πθ(at∣st)πθold(at∣st)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}rt​(θ)=πθold​​(at​∣st​)πθ​(at​∣st​)​

  • πθ\pi_\thetaπθ​:新策略
  • πθold\pi_{\theta_{old}}πθold​​:旧策略
  • 表示新旧策略选择同一动作的概率比

2. PPO-Clip目标函数(最常用)

LCLIP(θ)=Et[min⁡(rt(θ)⋅At,clip(rt(θ),1−ϵ,1+ϵ)⋅At)]L^{CLIP}(\theta) = \mathbb{E}_t[\min(r_t(\theta) \cdot A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \cdot A_t)]LCLIP(θ)=Et​[min(rt​(θ)⋅At​,clip(rt​(θ),1−ϵ,1+ϵ)⋅At​)]

其中:

  • AtA_tAt​:优势函数(Advantage),表示动作好坏
  • ϵ\epsilonϵ:裁剪参数(通常取0.2)
  • clip(x,a,b)\text{clip}(x, a, b)clip(x,a,b):将x限制在[a, b]范围内

三、数值例子

假设一个简单场景:智能体在某状态下有3个动作可选。

输入数据

状态 s_t: [1.0, 0.5]
动作 a_t: 动作1
奖励 r_t: 10
优势值 A_t: 2.5  (计算得出,表示这个动作比平均好)
裁剪参数 ε: 0.2

策略概率

旧策略 π_old(动作1|s_t) = 0.3
新策略 π_new(动作1|s_t) = 0.5

计算过程

步骤1:计算重要性采样比率
rt=πnew(at∣st)πold(at∣st)=0.50.3=1.667r_t = \frac{\pi_{new}(a_t|s_t)}{\pi_{old}(a_t|s_t)} = \frac{0.5}{0.3} = 1.667rt​=πold​(at​∣st​)πnew​(at​∣st​)​=0.30.5​=1.667

步骤2:计算未裁剪项
未裁剪项=rt⋅At=1.667×2.5=4.167\text{未裁剪项} = r_t \cdot A_t = 1.667 \times 2.5 = 4.167未裁剪项=rt​⋅At​=1.667×2.5=4.167

步骤3:计算裁剪项

clip(r_t, 1-ε, 1+ε) = clip(1.667, 0.8, 1.2) = 1.2  (因为1.667 > 1.2)
裁剪项 = 1.2 × 2.5 = 3.0

步骤4:取最小值
LCLIP=min⁡(4.167,3.0)=3.0L^{CLIP} = \min(4.167, 3.0) = 3.0LCLIP=min(4.167,3.0)=3.0

输出结果

最终损失值:3.0(这个会用来更新神经网络)

四、关键理解

为什么要裁剪?

看上面的例子:

  • 比率 r_t = 1.667,说明新策略比旧策略更偏爱这个动作
  • 但PPO将其限制在1.2(1+ε),防止更新过激
  • 如果优势A_t是负的(坏动作),裁剪会限制在0.8(1-ε)

直观解释

如果 A_t > 0(好动作):
  - r_t 太大(>1.2)→ 被限制,不让新策略过度自信
  
如果 A_t < 0(坏动作):
  - r_t 太小(<0.8)→ 被限制,不让新策略过度惩罚

五、完整例子对比

情况r_tA_t未裁剪项裁剪后r_t裁剪项最终Loss
例11.6672.54.1671.23.03.0 ✓
例20.52.51.250.82.01.25 ✓
例31.12.52.751.12.752.75 ✓

注意例3:r_t=1.1在[0.8, 1.2]范围内,所以不需要裁剪。


总结:PPO通过裁剪机制,让策略更新"稳扎稳打",既保证学习效率,又避免崩溃,是目前最流行的强化学习算法之一!

你理解了吗?有任何疑问欢迎继续提问!

问题:
这里的新策略和旧策略是哪来的?是通过正在训练的模型生成的吗?

新旧策略的来源

是的!它们都是正在训练的模型,但处于不同的训练阶段:

旧策略 π_old (θ_old)

  • 就是当前已训练好的神经网络的参数
  • 在一轮训练开始前,先冻结这个版本
  • 用它来收集经验数据(与环境交互)

新策略 π_new (θ)

  • 是正在更新中的同一个神经网络
  • 参数在训练过程中不断调整
  • 目标是变得更好,但不能离旧策略太远

PPO训练流程(完整版)

第k轮训练:

1️⃣ 保存当前模型参数
   θ_old ← θ(拷贝一份,作为旧策略)

2️⃣ 用旧策略收集数据
   for 每个episode:
       用 π_θ_old 与环境交互
       收集:(状态s, 动作a, 奖励r, π_old(a|s))
   
3️⃣ 计算优势函数 A_t
   
4️⃣ 更新策略(多次迭代)
   for epoch in range(K):  # 通常K=3~10
       for 每个batch:
           计算 π_θ(a|s)  # 新策略概率
           计算 r_t = π_θ(a|s) / π_old(a|s)
           计算 PPO loss
           梯度下降更新 θ
   
5️⃣ 回到步骤1,开始新一轮

数值例子:训练过程

假设我们训练一个智能体玩简化版游戏:

初始状态

模型:一个神经网络(随机初始化)
输入:游戏状态 [位置, 速度]
输出:3个动作的概率 [左, 右, 跳]

第1轮训练

阶段1:保存旧策略

θ_old = copy(θ)  # 拷贝当前参数

# 此时的输出(例如状态[1.0, 0.5])
π_old([左, 右, 跳]) = [0.3, 0.5, 0.2]

阶段2:收集数据

# 用旧策略玩100局游戏
for episode in range(100):
    state = env.reset()
    while not done:
        action = sample_from(π_old(state))  # 用旧策略选动作
        next_state, reward, done = env.step(action)
        存储:(state, action, reward, π_old(action|state))

# 收集到的一条数据示例:
数据点1: 
  状态: [1.0, 0.5]
  动作: "右" 
  π_old(右|状态) = 0.5  # 记录下来!
  奖励: +10

阶段3:更新模型

# 现在开始更新θ,产生新策略
for epoch in range(5):  # 用同一批数据更新5次
    for batch in data:
        # 用当前模型(新策略)重新计算概率
        π_new = model(state)  # 新策略输出
        
        # 对于上面的数据点1:
        π_new([左, 右, 跳]) = [0.25, 0.6, 0.15]  # 参数更新后
        π_new(右|状态) = 0.6
        
        # 计算比率
        r_t = π_new(右) / π_old(右) = 0.6 / 0.5 = 1.2
        
        # 计算PPO loss
        loss = compute_ppo_loss(r_t, A_t)
        
        # 梯度下降
        θ = θ - lr * ∇loss

关键点对比

时间点旧策略 π_old新策略 π_new说明
收集数据时π_old = model(θ_old)不存在旧策略固定,用于采样
第1次更新后π_old = model(θ_old)π_new = model(θ_1)θ_1 ≠ θ_old
第5次更新后π_old = model(θ_old)π_new = model(θ_5)θ_5 离 θ_old 更远

为什么这样设计?

问题:直接用新数据不行吗?

# ❌ 错误做法(普通策略梯度)
while True:
    收集数据(用当前策略)
    更新一次
    丢弃数据  # 浪费!

PPO的优势

# ✅ PPO做法
while True:
    收集数据(用θ_old)
    用同一批数据更新多次(3-10次)  # 数据利用率高!
    但用重要性采样修正偏差

直观理解

把模型想象成一个学生:

旧策略(θ_old):
  - 上周的你,已经考完试了
  - 试卷已经答完(数据已收集)
  
新策略(θ):
  - 现在的你,正在复习
  - 根据试卷错题学习
  - 但不能学得"面目全非"(裁剪限制)
  
下一轮:
  - 这周的你变成"旧的你"
  - 继续循环

完整代码框架

class PPO:
    def __init__(self):
        self.policy_net = NeuralNetwork()  # 策略网络
        
    def train(self):
        for iteration in range(1000):
            # 1. 保存旧策略
            old_params = copy(self.policy_net.parameters())
            
            # 2. 收集数据
            trajectories = []
            for episode in range(100):
                states, actions, rewards, old_probs = self.collect_episode()
                trajectories.append((states, actions, rewards, old_probs))
            
            # 3. 计算优势
            advantages = self.compute_advantages(trajectories)
            
            # 4. 更新策略(多个epoch)
            for epoch in range(5):
                for batch in trajectories:
                    # 新策略概率
                    new_probs = self.policy_net(states)
                    
                    # 计算比率
                    ratio = new_probs / old_probs  # 这里!
                    
                    # PPO loss
                    loss = self.ppo_loss(ratio, advantages)
                    
                    # 更新
                    loss.backward()
                    optimizer.step()

总结:

  • 旧策略 = 本轮开始时的模型快照
  • 新策略 = 正在训练中的模型
  • 它们是同一个网络的不同版本
  • PPO通过比较两者,控制更新幅度

PPO的两个版本

1️⃣ PPO-Clip(我之前讲的)

LCLIP(θ)=Et[min⁡(rt(θ)⋅At,clip(rt(θ),1−ϵ,1+ϵ)⋅At)]L^{CLIP}(\theta) = \mathbb{E}_t[\min(r_t(\theta) \cdot A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \cdot A_t)]LCLIP(θ)=Et​[min(rt​(θ)⋅At​,clip(rt​(θ),1−ϵ,1+ϵ)⋅At​)]

2️⃣ PPO-Penalty(带KL散度约束)

LKLPEN(θ)=Et[rt(θ)⋅At−β⋅KL[πθold∣∣πθ]]L^{KLPEN}(\theta) = \mathbb{E}_t[r_t(\theta) \cdot A_t - \beta \cdot KL[\pi_{\theta_{old}}||\pi_\theta]]LKLPEN(θ)=Et​[rt​(θ)⋅At​−β⋅KL[πθold​​∣∣πθ​]]

什么是KL散度?

KL散度(Kullback-Leibler Divergence)衡量两个概率分布的差异:

KL[πold∣∣πnew]=∑aπold(a∣s)log⁡πold(a∣s)πnew(a∣s)KL[\pi_{old}||\pi_{new}] = \sum_a \pi_{old}(a|s) \log\frac{\pi_{old}(a|s)}{\pi_{new}(a|s)}KL[πold​∣∣πnew​]=a∑​πold​(a∣s)logπnew​(a∣s)πold​(a∣s)​

直观理解:

  • KL散度越大 → 新旧策略差异越大
  • KL散度 = 0 → 两个策略完全相同
  • KL散度 > 0 → 策略有差异

数值例子:KL散度计算

场景设置

状态 s: [1.0, 0.5]
可选动作: [左, 右, 跳]

旧策略概率分布:
π_old = [0.2, 0.5, 0.3]

新策略概率分布:
π_new = [0.3, 0.4, 0.3]

计算KL散度

KL[πold∣∣πnew]=∑i=13πoldilog⁡πoldiπnewiKL[\pi_{old}||\pi_{new}] = \sum_{i=1}^3 \pi_{old}^i \log\frac{\pi_{old}^i}{\pi_{new}^i}KL[πold​∣∣πnew​]=i=1∑3​πoldi​logπnewi​πoldi​​

逐项计算:

动作1(左):
  0.2 × log(0.2/0.3) = 0.2 × log(0.667) = 0.2 × (-0.405) = -0.081

动作2(右):
  0.5 × log(0.5/0.4) = 0.5 × log(1.25) = 0.5 × 0.223 = 0.112

动作3(跳):
  0.3 × log(0.3/0.3) = 0.3 × log(1.0) = 0.3 × 0 = 0

总和:
  KL = -0.081 + 0.112 + 0 = 0.031

结果:KL散度 = 0.031(差异较小)

PPO-Penalty 完整例子

输入数据

状态: [1.0, 0.5]
选择的动作: "右"
优势值 A_t: 2.5
β(KL惩罚系数): 0.5

旧策略: π_old(右|s) = 0.5, π_old = [0.2, 0.5, 0.3]
新策略: π_new(右|s) = 0.6, π_new = [0.3, 0.4, 0.3]

计算过程

步骤1:计算比率项
rt=0.60.5=1.2r_t = \frac{0.6}{0.5} = 1.2rt​=0.50.6​=1.2
比率项=rt⋅At=1.2×2.5=3.0\text{比率项} = r_t \cdot A_t = 1.2 \times 2.5 = 3.0比率项=rt​⋅At​=1.2×2.5=3.0

步骤2:计算KL散度(如上)
KL=0.031KL = 0.031KL=0.031

步骤3:计算最终损失
LKLPEN=3.0−0.5×0.031=3.0−0.0155=2.9845L^{KLPEN} = 3.0 - 0.5 \times 0.031 = 3.0 - 0.0155 = 2.9845LKLPEN=3.0−0.5×0.031=3.0−0.0155=2.9845

输出:损失值 = 2.9845

两个版本对比

PPO-Clip vs PPO-Penalty

特性PPO-ClipPPO-Penalty
约束方式硬裁剪 [1-ε, 1+ε]软惩罚(减去KL项)
超参数ε = 0.2(固定)β(需要自适应调整)
计算复杂度低(只计算比率)高(需计算完整分布)
实践效果更稳定,更常用 ✓理论优美,但调参难
实现难度简单复杂

图形化理解

假设横轴是策略更新幅度,纵轴是Loss:

PPO-Clip (裁剪方式):
     Loss
      |     平台期
      |    _____
      |   /
      |  /
      |_/_____________ 更新幅度
          ↑
        裁剪点(1+ε)
        
PPO-Penalty (惩罚方式):
     Loss
      |      
      |    /---越来越平缓
      |   /
      |  /
      | /
      |/______________ 更新幅度
      ↑
   KL惩罚逐渐增大

自适应KL(改进版)

实际中,PPO-Penalty会动态调整β:

if KL < KL_target / 1.5:
    β = β / 2  # KL太小,减小惩罚
elif KL > KL_target × 1.5:
    β = β × 2  # KL太大,增加惩罚

例子:β调整过程

第1轮: β=0.5, KL=0.02 → KL太小 → β=0.25
第2轮: β=0.25, KL=0.06 → 合适 → β不变
第3轮: β=0.25, KL=0.15 → KL太大 → β=0.5

完整对比例子

同样的场景,两种方法的结果:

场景

r_t = 1.8(新策略偏离较大)
A_t = 2.0
ε = 0.2
β = 0.5
KL = 0.08

PPO-Clip

未裁剪: 1.8 × 2.0 = 3.6
裁剪后: clip(1.8, 0.8, 1.2) = 1.2
裁剪项: 1.2 × 2.0 = 2.4
Loss = min(3.6, 2.4) = 2.4  # 被裁剪了!

PPO-Penalty

比率项: 1.8 × 2.0 = 3.6
惩罚项: 0.5 × 0.08 = 0.04
Loss = 3.6 - 0.04 = 3.56  # 只被轻微惩罚

结论:Clip方法限制更强!

为什么PPO-Clip更流行?

优点

  1. 不需要调参:ε=0.2通常就够了
  2. 计算简单:不需要完整分布
  3. 效果稳定:硬约束更可靠

PPO-Penalty的问题

  1. β需要精心调整
  2. 计算开销大
  3. 不同环境需要不同的β

实际应用

# OpenAI Gym中的PPO大多用Clip版本
class PPOClip:
    def loss(self, ratio, advantage):
        clipped = torch.clamp(ratio, 1-0.2, 1+0.2)
        return -torch.min(ratio * advantage, 
                          clipped * advantage).mean()

# 研究中有时用Penalty版本
class PPOPenalty:
    def loss(self, ratio, advantage, old_dist, new_dist):
        kl = compute_kl(old_dist, new_dist)
        return -(ratio * advantage - self.beta * kl).mean()

总结表

方法公式核心约束强度使用场景
PPO-Clipmin(r·A, clip®·A)硬约束实际应用(推荐)
PPO-Penaltyr·A - β·KL软约束研究/理论分析
混合版本同时使用两者双重保险极端稳定性要求

有关新旧策略的进一步演示

(clip那里需要再核验一下)

PPO中新旧策略Log概率计算详解

📊 完整数值示例

场景设定

假设我们有一个小型词汇表(实际有30000+词),模型正在生成回复:

词汇表(简化版):
Token ID | 词
---------|-----
0        | <PAD>
1        | <EOS>
100      | 很
101      | 好
102      | 不
103      | 错
104      | !
...

用户Query: “你好” (已编码)
模型Response: “很好!”


🔍 Step 1: 旧策略生成阶段 (采样时)

1.1 第一个token:“很” (ID=100)

模型在生成时的状态:

# 输入: Query = "你好"
input_ids = [50, 51]  # "你好"的token IDs

# 模型前向传播
logits = model(input_ids)
# logits shape: [1, 2, vocab_size] 其中vocab_size=30000

# 取最后一个位置的logits(用于预测下一个token)
next_token_logits = logits[0, -1, :]  # shape: [30000]

# 具体数值(只展示相关token):
next_token_logits = {
    100: 8.2,   # "很"
    101: 7.8,   # "好"
    102: 5.3,   # "不"
    103: 6.1,   # "错"
    104: 4.2,   # "!"
    ...
}
计算概率分布
# 应用Softmax得到概率
import torch.nn.functional as F

probs = F.softmax(next_token_logits, dim=-1)

# 相关token的概率:
probs = {
    100: 0.35,  # "很" ← 最高概率
    101: 0.28,  # "好"
    102: 0.02,  # "不"
    103: 0.05,  # "错"
    104: 0.01,  # "!"
    其他: 0.29  # 剩余29995个token
}

# Log概率
log_probs = torch.log(probs)
log_probs = {
    100: -1.05,  # log(0.35) ≈ -1.05
    101: -1.27,  # log(0.28) ≈ -1.27
    102: -3.91,  # log(0.02) ≈ -3.91
    103: -3.00,  # log(0.05) ≈ -3.00
    104: -4.61,  # log(0.01) ≈ -4.61
    ...
}
采样动作
# 从概率分布中采样
sampled_token = torch.multinomial(probs, 1)
# 结果: token_id = 100 ("很")

# 记录这个token的log概率(旧策略)
old_logprob_token1 = log_probs[100] = -1.05

1.2 第二个token:“好” (ID=101)

# 现在输入变成: "你好很"
input_ids = [50, 51, 100]

# 再次前向传播
logits = model(input_ids)
next_token_logits = logits[0, -1, :]  # [30000]

# 新的logits分布(条件概率改变了):
next_token_logits = {
    100: 3.1,   # "很" (已经说过,概率下降)
    101: 9.5,   # "好" ← 现在这个最合理
    102: 2.8,   # "不"
    103: 7.2,   # "错"
    104: 6.8,   # "!"
    ...
}

# Softmax后的概率
probs = {
    100: 0.02,  # "很"
    101: 0.68,  # "好" ← 最高
    102: 0.01,  # "不"
    103: 0.12,  # "错"
    104: 0.09,  # "!"
    其他: 0.08
}

# Log概率
log_probs = {
    101: -0.39,  # log(0.68) ≈ -0.39
    ...
}

# 采样
sampled_token = 101  # "好"
old_logprob_token2 = -0.39

1.3 第三个token:“!” (ID=104)

# 输入: "你好很好"
input_ids = [50, 51, 100, 101]

logits = model(input_ids)
next_token_logits = logits[0, -1, :]

# Logits分布:
next_token_logits = {
    1: 8.9,     # <EOS> 
    100: 2.1,   # "很"
    101: 3.2,   # "好"
    104: 8.5,   # "!" ← 接近EOS
    ...
}

probs = {
    1: 0.48,    # <EOS>
    104: 0.35,  # "!"
    ...
}

log_probs = {
    104: -1.05,  # log(0.35)
    ...
}

# 采样
sampled_token = 104  # "!"
old_logprob_token3 = -1.05

1.4 旧策略的完整记录

# 存储在memory中
old_response = [100, 101, 104]  # "很好!"
old_logprobs = [-1.05, -0.39, -1.05]  # 每个token的log概率

# 这些数据被保存到PPOMemory
memory.actions.append([100, 101, 104])
memory.logprobs.append([-1.05, -0.39, -1.05])

关键代码位置: trainer.py Line 126-129

def select_action(self, state, memory):
    with torch.no_grad():
        action, action_logprob = self.policy_old.act(state)
    memory.logprobs.append(action_logprob)  # 存储旧策略log概率

🔄 Step 2: 新策略评估阶段 (训练时)

现在模型参数已经更新了(经过几轮梯度下降),我们需要用新策略重新评估相同的token序列。

2.1 新策略的前向传播

重要: 这次不是采样,而是计算已经生成的token的概率!

# 输入完整序列: "你好很好!"
full_input_ids = [50, 51, 100, 101, 104]  # Query + Response

# 新模型(参数已更新)一次性前向传播
new_logits = new_model(full_input_ids)
# shape: [1, 5, 30000]

# 对每个位置应用softmax
new_probs = F.softmax(new_logits, dim=-1)
# shape: [1, 5, 30000]

2.2 提取每个token的新log概率

Position 2 (预测token 100 “很”)
# 在位置2,模型看到"你好",预测下一个token
logits_pos2 = new_logits[0, 2, :]  # [30000]

# 新策略下的概率分布(参数更新后)
new_probs_pos2 = {
    100: 0.42,  # "很" (从0.35提升到0.42!)
    101: 0.25,  # "好" (从0.28下降到0.25)
    102: 0.03,  # "不"
    ...
}

# 新的log概率
new_log_probs_pos2 = {
    100: -0.87,  # log(0.42) ≈ -0.87
    ...
}

# 我们生成的token是100,所以:
new_logprob_token1 = new_log_probs_pos2[100] = -0.87
Position 3 (预测token 101 “好”)
logits_pos3 = new_logits[0, 3, :]

new_probs_pos3 = {
    101: 0.72,  # "好" (从0.68提升到0.72)
    ...
}

new_logprob_token2 = log(0.72) = -0.33
Position 4 (预测token 104 “!”)
logits_pos4 = new_logits[0, 4, :]

new_probs_pos4 = {
    104: 0.40,  # "!" (从0.35提升到0.40)
    ...
}

new_logprob_token3 = log(0.40) = -0.92

2.3 新策略的完整记录

# 新策略下,相同token序列的log概率
new_response = [100, 101, 104]  # 还是"很好!"
new_logprobs = [-0.87, -0.33, -0.92]

关键代码位置: trainer.py Line 496-498

def evaluate(self, states, actions):
    action_probs, state_values = self.forward(states)
    dist = Categorical(action_probs)
    action_logprobs = dist.log_prob(actions)  # 新策略log概率
    return action_logprobs, state_values

📐 Step 3: 计算比率 (Ratio)

现在我们有新旧两个log概率,可以计算ratio了!

# 旧策略 (π_old)
old_logprobs = [-1.05, -0.39, -1.05]

# 新策略 (π_new)
新_logprobs = [-0.87, -0.33, -0.92]

# 计算ratio = π_new / π_old = exp(log π_new - log π_old)
ratios = torch.exp(new_logprobs - old_logprobs)

Token级别的计算:

Token 1: “很”
old_logprob = -1.05  # 旧策略: P=0.35
new_logprob = -0.87  # 新策略: P=0.42

ratio = exp(-0.87 - (-1.05))
      = exp(0.18)
      = 1.197

解释: 新策略生成"很"的概率是旧策略的1.197倍
Token 2: “好”
old_logprob = -0.39  # 旧策略: P=0.68
new_logprob = -0.33  # 新策略: P=0.72

ratio = exp(-0.33 - (-0.39))
      = exp(0.06)
      = 1.062

解释: 新策略稍微增强了"好"的概率
Token 3: “!”
old_logprob = -1.05  # 旧策略: P=0.35
new_logprob = -0.92  # 新策略: P=0.40

ratio = exp(-0.92 - (-1.05))
      = exp(0.13)
      = 1.139

解释: 新策略也增强了"!"的概率

🎯 Step 4: PPO裁剪

假设我们有奖励 R = 2.5,价值估计 V = 1.8

advantage = R - V = 2.5 - 1.8 = 0.7

eps = 0.2  # 裁剪参数

# 对每个token
for token_idx in range(3):
    ratio = ratios[token_idx]
    
    # Token 1: "很"
    if token_idx == 0:
        ratio = 1.197
        
        # 计算两个目标
        surr1 = ratio * advantage = 1.197 * 0.7 = 0.838
        
        # 裁剪ratio到 [0.8, 1.2]
        clipped_ratio = clip(1.197, 0.8, 1.2) = 1.2
        surr2 = clipped_ratio * advantage = 1.2 * 0.7 = 0.840
        
        # 取最小值(保守更新)
        policy_loss = -min(0.838, 0.840) = -0.838
    
    # Token 2: "好"
    if token_idx == 1:
        ratio = 1.062
        
        surr1 = 1.062 * 0.7 = 0.743
        clipped_ratio = clip(1.062, 0.8, 1.2) = 1.062  # 在范围内
        surr2 = 1.062 * 0.7 = 0.743
        
        policy_loss = -min(0.743, 0.743) = -0.743
    
    # Token 3: "!"
    if token_idx == 2:
        ratio = 1.139
        
        surr1 = 1.139 * 0.7 = 0.797
        clipped_ratio = 1.139  # 在范围内
        surr2 = 0.797
        
        policy_loss = -0.797

🔬 完整数据流对比表

Token旧P旧logP新P新logPRatioClippedLoss
很0.35-1.050.42-0.871.1971.200-0.838
好0.68-0.390.72-0.331.0621.062-0.743
!0.35-1.050.40-0.921.1391.139-0.797

平均Policy Loss: (-0.838 - 0.743 - 0.797) / 3 = -0.793

PPO算法中的优势函数计算

使用的模型

PPO算法使用2个神经网络:

  1. 策略网络(Policy Network / Actor):输出动作概率分布
  2. 价值网络(Value Network / Critic):输出状态价值 V(s)V(s)V(s)

优势函数的定义

优势函数表示某个动作相比于平均水平的好坏程度:

A(s,a)=Q(s,a)−V(s)A(s,a) = Q(s,a) - V(s)A(s,a)=Q(s,a)−V(s)

其中:

  • Q(s,a)Q(s,a)Q(s,a):状态-动作价值(采取动作a后的期望回报)
  • V(s)V(s)V(s):状态价值(该状态的平均期望回报)

实际计算方法

PPO中通常使用**GAE(广义优势估计)**方法。

首先计算TD误差(时序差分误差):

δt=rt+γV(st+1)−V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)δt​=rt​+γV(st+1​)−V(st​)

然后计算优势函数(从后往前递推):

At=δt+(γλ)δt+1+(γλ)2δt+2+...A_t = \delta_t + (\gamma \lambda) \delta_{t+1} + (\gamma \lambda)^2 \delta_{t+2} + ...At​=δt​+(γλ)δt+1​+(γλ)2δt+2​+...

或者递归形式:

At=δt+(γλ)At+1A_t = \delta_t + (\gamma \lambda) A_{t+1}At​=δt​+(γλ)At+1​

其中:

  • γ\gammaγ:折扣因子(通常0.99)
  • λ\lambdaλ:GAE参数(通常0.95)

具体数值例子

假设智能体走了5步,设定参数:

  • γ=0.99\gamma = 0.99γ=0.99(折扣因子)
  • λ=0.95\lambda = 0.95λ=0.95(GAE参数)

输入数据

时间步状态动作奖励 rtr_trt​价值 V(st)V(s_t)V(st​)
t=0t=0t=0s0s_0s0​a0a_0a0​1.03.0
t=1t=1t=1s1s_1s1​a1a_1a1​0.02.5
t=2t=2t=2s2s_2s2​a2a_2a2​2.04.0
t=3t=3t=3s3s_3s3​a3a_3a3​1.03.5
t=4t=4t=4s4s_4s4​a4a_4a4​5.05.0
t=5t=5t=5终止--0.0

计算步骤

第一步:计算每个时间步的TD误差 δt\delta_tδt​

δ0=r0+γV(s1)−V(s0)=1.0+0.99×2.5−3.0=0.475\delta_0 = r_0 + \gamma V(s_1) - V(s_0) = 1.0 + 0.99 \times 2.5 - 3.0 = 0.475δ0​=r0​+γV(s1​)−V(s0​)=1.0+0.99×2.5−3.0=0.475

δ1=r1+γV(s2)−V(s1)=0.0+0.99×4.0−2.5=1.46\delta_1 = r_1 + \gamma V(s_2) - V(s_1) = 0.0 + 0.99 \times 4.0 - 2.5 = 1.46δ1​=r1​+γV(s2​)−V(s1​)=0.0+0.99×4.0−2.5=1.46

δ2=r2+γV(s3)−V(s2)=2.0+0.99×3.5−4.0=1.465\delta_2 = r_2 + \gamma V(s_3) - V(s_2) = 2.0 + 0.99 \times 3.5 - 4.0 = 1.465δ2​=r2​+γV(s3​)−V(s2​)=2.0+0.99×3.5−4.0=1.465

δ3=r3+γV(s4)−V(s3)=1.0+0.99×5.0−3.5=2.45\delta_3 = r_3 + \gamma V(s_4) - V(s_3) = 1.0 + 0.99 \times 5.0 - 3.5 = 2.45δ3​=r3​+γV(s4​)−V(s3​)=1.0+0.99×5.0−3.5=2.45

δ4=r4+γV(s5)−V(s4)=5.0+0.99×0.0−5.0=0.0\delta_4 = r_4 + \gamma V(s_5) - V(s_4) = 5.0 + 0.99 \times 0.0 - 5.0 = 0.0δ4​=r4​+γV(s5​)−V(s4​)=5.0+0.99×0.0−5.0=0.0

第二步:从后往前计算优势函数

计算系数:γλ=0.99×0.95=0.9405\gamma \lambda = 0.99 \times 0.95 = 0.9405γλ=0.99×0.95=0.9405

A4=δ4=0.0A_4 = \delta_4 = 0.0A4​=δ4​=0.0

A3=δ3+(γλ)A4=2.45+0.9405×0.0=2.45A_3 = \delta_3 + (\gamma \lambda) A_4 = 2.45 + 0.9405 \times 0.0 = 2.45A3​=δ3​+(γλ)A4​=2.45+0.9405×0.0=2.45

A2=δ2+(γλ)A3=1.465+0.9405×2.45=3.769A_2 = \delta_2 + (\gamma \lambda) A_3 = 1.465 + 0.9405 \times 2.45 = 3.769A2​=δ2​+(γλ)A3​=1.465+0.9405×2.45=3.769

A1=δ1+(γλ)A2=1.46+0.9405×3.769=5.004A_1 = \delta_1 + (\gamma \lambda) A_2 = 1.46 + 0.9405 \times 3.769 = 5.004A1​=δ1​+(γλ)A2​=1.46+0.9405×3.769=5.004

A0=δ0+(γλ)A1=0.475+0.9405×5.004=5.181A_0 = \delta_0 + (\gamma \lambda) A_1 = 0.475 + 0.9405 \times 5.004 = 5.181A0​=δ0​+(γλ)A1​=0.475+0.9405×5.004=5.181

输出结果

时间步优势函数 AtA_tAt​含义
t=0t=0t=05.181非常好的动作
t=1t=1t=15.004非常好的动作
t=2t=2t=23.769好的动作
t=3t=3t=32.45较好的动作
t=4t=4t=40.0符合预期的动作

优势函数的含义

  • At>0A_t > 0At​>0:这个动作比平均水平好,应该增加其被选择的概率
  • At<0A_t < 0At​<0:这个动作比平均水平差,应该降低其被选择的概率
  • At=0A_t = 0At​=0:这个动作符合预期

在PPO中的使用

这些优势值会用于更新策略网络的目标函数:

rt(θ)=πθ(at∣st)πθold(at∣st)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}rt​(θ)=πθold​​(at​∣st​)πθ​(at​∣st​)​

LCLIP(θ)=Et[min⁡(rt(θ)At,clip(rt(θ),1−ϵ,1+ϵ)At)]L^{CLIP}(\theta) = \mathbb{E}_t[\min(r_t(\theta) A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) A_t)]LCLIP(θ)=Et​[min(rt​(θ)At​,clip(rt​(θ),1−ϵ,1+ϵ)At​)]

其中:

  • πθ(at∣st)\pi_\theta(a_t|s_t)πθ​(at​∣st​):新策略选择动作 ata_tat​ 的概率
  • πθold(at∣st)\pi_{\theta_{old}}(a_t|s_t)πθold​​(at​∣st​):旧策略选择动作 ata_tat​ 的概率
  • ϵ\epsilonϵ:裁剪参数(通常0.2)

在PPO算法中,通常使用2个神经网络模型:

1. Policy Network (策略网络) = Actor (执行者)

  • 作用:决定在给定状态下应该采取什么动作
  • 输入:当前状态 s
  • 输出:动作的概率分布 π(a|s)
    • 离散动作:输出各个动作的概率
    • 连续动作:输出动作分布的均值和方差
  • 训练目标:最大化累积奖励

2. Value Network (价值网络) = Critic (评论家)

  • 作用:评估当前状态的好坏
  • 输入:当前状态 s
  • 输出:状态价值 V(s),预测从该状态开始能获得的期望回报
  • 训练目标:准确预测状态价值,用于计算优势函数(Advantage)

两者的关系

Actor (Policy):  "我应该做什么动作?"
Critic (Value):  "这个状态有多好?"

Critic帮助Actor学习:

  • Critic计算优势函数 A(s,a) = Q(s,a) - V(s)
  • 优势函数告诉Actor:“这个动作比平均水平好多少”
  • Actor根据这个反馈调整策略

实现方式

独立网络(常见):

policy_net = PolicyNetwork()   # Actor
value_net = ValueNetwork()     # Critic

共享网络(共享特征提取层):

shared_layers = ...
policy_head = ...  # Actor输出头
value_head = ...   # Critic输出头

所以答案是:PPO核心使用2个模型,Policy/Actor负责决策,Value/Critic负责评估。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐