PPO强化学习算法详解
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_t | A_t | 未裁剪项 | 裁剪后r_t | 裁剪项 | 最终Loss |
|---|---|---|---|---|---|---|
| 例1 | 1.667 | 2.5 | 4.167 | 1.2 | 3.0 | 3.0 ✓ |
| 例2 | 0.5 | 2.5 | 1.25 | 0.8 | 2.0 | 1.25 ✓ |
| 例3 | 1.1 | 2.5 | 2.75 | 1.1 | 2.75 | 2.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πoldilogπ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-Clip | PPO-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更流行?
优点
- 不需要调参:ε=0.2通常就够了
- 计算简单:不需要完整分布
- 效果稳定:硬约束更可靠
PPO-Penalty的问题
- β需要精心调整
- 计算开销大
- 不同环境需要不同的β
实际应用
# 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-Clip | min(r·A, clip®·A) | 硬约束 | 实际应用(推荐) |
| PPO-Penalty | r·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 | 新logP | Ratio | Clipped | Loss |
|---|---|---|---|---|---|---|---|
| 很 | 0.35 | -1.05 | 0.42 | -0.87 | 1.197 | 1.200 | -0.838 |
| 好 | 0.68 | -0.39 | 0.72 | -0.33 | 1.062 | 1.062 | -0.743 |
| ! | 0.35 | -1.05 | 0.40 | -0.92 | 1.139 | 1.139 | -0.797 |
平均Policy Loss: (-0.838 - 0.743 - 0.797) / 3 = -0.793
PPO算法中的优势函数计算
使用的模型
PPO算法使用2个神经网络:
- 策略网络(Policy Network / Actor):输出动作概率分布
- 价值网络(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=0 | s0s_0s0 | a0a_0a0 | 1.0 | 3.0 |
| t=1t=1t=1 | s1s_1s1 | a1a_1a1 | 0.0 | 2.5 |
| t=2t=2t=2 | s2s_2s2 | a2a_2a2 | 2.0 | 4.0 |
| t=3t=3t=3 | s3s_3s3 | a3a_3a3 | 1.0 | 3.5 |
| t=4t=4t=4 | s4s_4s4 | a4a_4a4 | 5.0 | 5.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=0 | 5.181 | 非常好的动作 |
| t=1t=1t=1 | 5.004 | 非常好的动作 |
| t=2t=2t=2 | 3.769 | 好的动作 |
| t=3t=3t=3 | 2.45 | 较好的动作 |
| t=4t=4t=4 | 0.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负责评估。
更多推荐
所有评论(0)