TRPO 深度解析:从数学原理到代码实现
一、问题背景:为什么需要 TRPO?
强化学习的目标是不断更新策略参数 ,使累计奖励最大化。最朴素的做法是直接做梯度上升:
但这有一个致命问题:步子太大会摔跤。一次更新过大,新策略可能远比旧策略差,而且强化学习没有”后悔药”——坏策略采集的数据质量更差,训练可能一路崩塌。
TRPO 的解决思路是:在每次更新时,显式限制新旧策略的差距不超过 :
注意约束中有 :KL 散度对所有可能出现的状态 取期望,保证在整个状态分布上策略变化都不过大。
二、目标函数拆解
策略参数
是神经网络(策略网络)的全部权重和偏置。策略网络输入状态 ,输出动作的概率分布 。
在语言模型场景中, = “原始问题 + 已生成的前 个 token”, = “第 个 token 的选择”。
重要性采样比率
我们用旧策略 采集数据,但希望估计新策略 的表现。重要性采样的核心等式:
- :新策略比旧策略更倾向选这个动作
- :新策略比旧策略更不倾向选这个动作
- :两个策略对此动作概率相同
优势函数
| 符号 | 含义 |
|---|---|
| 在状态 执行动作 后的期望累计奖励 | |
| 在状态 下,按当前策略执行的期望累计奖励(基线) | |
| 这个动作比平均水平好,应该强化 | |
| 这个动作比平均水平差,应该抑制 |
减去基线 不改变梯度的期望值(因为 与动作选择无关),但能大幅降低方差,让训练更稳定。
对时间步 取期望
等价于对一条(或一批)轨迹中所有时间步求平均,用来估计期望值。
三、KL 散度约束与二阶泰勒展开
KL 散度的定义
对于离散分布:
对于连续分布:
性质:
- (自己与自己的散度为零)
- 恒成立(由 Jensen 不等式保证)
- 不对称:(一般情况下)
对 KL 散度做二阶泰勒展开
设 ,在 处展开 :
零阶项 = 0
一阶项 = 0(完整推导)
对 关于 求梯度:
在 处,,代入:
由于概率归一化 对任意 成立,其关于 的梯度恒为零向量。
二阶项:从一维到多维
一维情形:
多维推广:
其中 Hessian 矩阵的元素为 ,描述各参数方向上的曲率。
对 KL 散度而言,这个 Hessian 恰好等于 Fisher 信息矩阵 (见下节证明)。
三阶项的形状
阶项的一般规律:系数 乘以 阶张量与 的 次缩并。TRPO 忽略三阶及以上项。
四、Fisher 信息矩阵
定义
其中 称为 score 函数(得分函数)。 是 score 函数外积的期望,是一个 的半正定矩阵。
对数导数技巧
这是策略梯度定理的核心变换:将”对概率求梯度”转化为”对对数概率求梯度”,数值上更稳定,且便于利用 进行期望计算。
Fisher 矩阵 = KL 散度的 Hessian(证明)
对 关于 求二阶导,在 处:
同时有等价形式(利用 score 函数期望为零推导):
自然梯度
在 KL 约束 下最大化线性目标 ( 为策略梯度),用拉格朗日乘子法:
步长由约束 确定,解得 ,最终:
这称为自然梯度方向,考虑了参数空间的黎曼曲率,比普通梯度方向更高效。
为什么 Fisher 矩阵很贵
| 网络规模 | 存储 | 求逆 | |
|---|---|---|---|
| 小型 MLP | 400 MB | 可行但慢 | |
| 中型策略网络 | ≈ 4 TB | 不可行 | |
| 大语言模型 | PB | 完全不可行 |
这正是 TRPO 需要共轭梯度法的根本原因。
五、共轭梯度法求解
关键洞察
我们要的不是整个逆矩阵,只是一个向量 ,等价于求解线性方程组:
共轭梯度法(Conjugate Gradient, CG)专门解 这类对称正定线性方程组(Fisher 矩阵恰好满足这一条件),且有超强性质:
只需要能计算” 与某向量 的乘积 “,不需要显式存储
CG 迭代 步即可(),总计算量 。
Hessian-Vector Product(HVP)
利用自动微分, 可以通过两次反向传播计算:
步骤:
- 前向传播,计算
- 第一次反向传播:得到 (需要
create_graph=True保留计算图) - 计算标量
- 第二次反向传播:
全程只需 内存,完全可行。
| 方法 | 存储 | 计算复杂度 |
|---|---|---|
| 显式存储并求逆 | ||
| 共轭梯度 + HVP | , |
六、TRPO 伪代码逐行解析
1 | import torch |
nn.Module 是 PyTorch 所有神经网络的基类,提供参数管理、保存/加载等功能。super().__init__() 必须调用,否则参数追踪机制不会生效。
1 | def get_kl(self, old_dist, new_dist): |
计算一批状态上的平均 KL 散度:
.mean() 对 batch 内所有状态取平均,对应约束中的 。
1 | def trpo_step(self, states, actions, advantages, old_log_probs, max_kl=0.01): |
| 参数 | 数学符号 | 形状 |
|---|---|---|
states |
[batch, state_dim] |
|
actions |
[batch] |
|
advantages |
[batch] |
|
old_log_probs |
${\log\pi_{\theta_\text{old}}(a_t | s_t)}$ |
max_kl |
标量,默认 0.01 |
Step 1:计算当前策略分布
1 | logits = self.policy(states) |
logits:网络输出原始得分,形状[batch, action_dim]Categorical:内部执行 softmax,将 logits 转成合法概率分布:
log_prob(actions):取出实际执行动作的 ,形状[batch]
用 logits 而非直接输出概率,是因为 softmax 内部做了数值稳定处理(log-sum-exp trick),避免概率连乘下溢到 0。
Step 2:计算重要性采样比率
1 | ratio = torch.exp(new_log_probs - old_log_probs) |
对应公式 ,利用对数性质:
先减后 exp,比直接相除数值更稳定(避免两个极小概率相除产生数值爆炸)。
Step 3:计算代理目标函数
1 | loss = -(ratio * advantages).mean() |
对应目标函数:
为什么取负号?
PyTorch 的优化器只做梯度下降(最小化),而我们想最大化 :
类比:你想找山顶,但手里只有”下坡工具”——把地图翻转,让山顶变谷底,下到谷底就等于爬上了山顶。强化学习里凡是要最大化的目标,在代码中均加负号。
Step 4:计算梯度并展平
1 | grads = torch.autograd.grad(loss, self.policy.parameters(), create_graph=True) |
torch.autograd.grad:手动触发反向传播,返回create_graph=True:保留计算图,使后续可对梯度再次求导(HVP 必需)
各层参数形状不同,展平操作将所有梯度拼成一个长向量:
例如一个小网络:
1 | Linear(64→32): weight [32,64] → 2048 个数,bias [32] → 32 个数 |
Step 5(省略):共轭梯度求解更新方向
1 | # step_direction = conjugate_gradient(HVP_function, flat_grads) |
这里要解:
其中 通过 HVP 隐式使用(见第五节)。
得到更新方向 后,TRPO 还需做线性搜索(Line Search):沿方向 找到满足以下两个条件的最大步长 :
- KL 约束:
- 目标提升:
最终更新:
七、完整数据流
1 | states ──→ policy_net ──→ logits ──→ Categorical |
八、总结与对比
| 特性 | 普通策略梯度 | TRPO |
|---|---|---|
| 更新规则 | ||
| 步长控制 | 手动设置学习率 | KL 约束自动确定 |
| 稳定性 | 容易崩塌 | 有理论保证单调提升 |
| 计算代价 | 低 | 高 ,需 CG + HVP |
| 实现难度 | 简单 | 复杂 |
TRPO 的核心贡献是将”策略更新不能太激进”这一直觉严格数学化,并给出可行的计算方案。其后继者 PPO 用更简单的裁剪目标函数近似实现了类似效果,成为目前最主流的策略优化算法:
参考
- Schulman et al., Trust Region Policy Optimization, ICML 2015. arXiv:1502.05477
- Kakade & Langford, Approximately Optimal Approximate Reinforcement Learning, ICML 2002.
- OpenAI Spinning Up: TRPO
- 配套阅读:MiniMind 源码阅读与架构分析(同属「源码解析」合集)