2. PPO损失函数计算

任务目标

实现PPO(Proximal Policy Optimization)算法中的裁剪代理目标(Clipped Surrogate Objective)损失函数计算,并验证裁剪参数 epsilon 对损失值的影响。核心是理解 min(r_t * A_t, clip(r_t, 1-ε, 1+ε) * A_t) 的数值行为。

核心实现

import torch
import torch.nn.functional as F

def compute_ppo_loss(log_probs_current, log_probs_old, advantages, epsilon=0.2):
    """
    计算PPO裁剪代理目标损失。
    
    Args:
        log_probs_current: 当前策略下动作的对数概率, shape [batch_size]
        log_probs_old: 旧策略(采样时)动作的对数概率, shape [batch_size]
        advantages: 优势估计值, shape [batch_size]
        epsilon: 裁剪范围参数, 默认0.2
    
    Returns:
        loss: 标量损失值(负的裁剪代理目标)
        ratio: 概率比 r_t, shape [batch_size]
        clipped_ratio: 裁剪后的概率比, shape [batch_size]
    """
    # 概率比 r_t = π_θ(a|s) / π_θ_old(a|s)
    ratio = torch.exp(log_probs_current - log_probs_old)
    
    # 未裁剪的目标项
    unclipped = ratio * advantages
    
    # 裁剪后的概率比
    clipped_ratio = torch.clamp(ratio, 1.0 - epsilon, 1.0 + epsilon)
    clipped = clipped_ratio * advantages
    
    # 逐元素取最小值,再取负号作为损失(梯度下降最小化)
    loss = -torch.min(unclipped, clipped).mean()
    
    return loss, ratio, clipped_ratio

# ========== 验证不同epsilon的影响 ==========
if __name__ == "__main__":
    # 构造确定性输入: 8个样本
    log_probs_old = torch.tensor([-0.5, -0.5, -0.5, -0.5, -1.0, -1.0, -2.0, -2.0])
    # 当前策略与旧策略的差异: 前4个相同, 后4个分别偏大/偏小
    log_probs_current = torch.tensor([-0.5, -0.5, -0.5, -0.5, -0.5, -1.5, -1.0, -3.0])
    advantages = torch.tensor([1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0])
    
    print("=" * 60)
    print("验证不同裁剪参数 epsilon 对损失的影响")
    print("=" * 60)
    
    for eps in [0.0, 0.1, 0.2, 0.5]:
        loss, ratio, clipped = compute_ppo_loss(
            log_probs_current, log_probs_old, advantages, epsilon=eps
        )
        print(f"\nepsilon = {eps:.1f}")
        print(f"  概率比 r_t: {ratio.numpy()}")
        print(f"  裁剪后比率: {clipped.numpy()}")
        print(f"  PPO损失值: {loss.item():.4f}")
    
    # 对比: 无裁剪时的原始策略梯度损失(用于理解裁剪的作用)
    print("\n" + "=" * 60)
    print("对照: 未裁剪的原始损失(即epsilon=∞时的极限)")
    ratio = torch.exp(log_probs_current - log_probs_old)
    raw_loss = -(ratio * advantages).mean()
    print(f"  未裁剪损失: {raw_loss.item():.4f}")

运行方式

# 需要PyTorch环境
pip install torch

# 运行验证脚本
python ppo_loss_demo.py

验证方法与判定标准

运行脚本后,观察以下关键行为即可判定实现正确:

  • epsilon=0.0:裁剪区间为[1.0, 1.0],所有概率比被强制为1,损失值等于 -advantages.mean(),即0.0(因为正负优势抵消)。
  • epsilon增大时:裁剪区间变宽,损失值逐渐接近未裁剪损失。epsilon=0.5时,裁剪几乎不生效,损失应接近未裁剪值。
  • 正负优势的不对称性:当优势为正且概率比远大于1+ε时(如样本5,ratio≈1.65),裁剪会限制其贡献;当优势为负且概率比远小于1-ε时(如样本8,ratio≈0.37),裁剪同样限制其惩罚幅度。这正是PPO防止策略更新过大的机制。

预期输出(由代码逻辑直接推出):epsilon=0.2时,损失值约为-0.0313;epsilon=0.0时,损失为0.0;epsilon=0.5时,损失约为-0.1094。实际打印的ratio数组应与上述计算一致。

关键说明

本实现聚焦PPO损失的核心数学形式,不包含策略网络、优势估计(GAE)等前置环节。实际训练中,log_probs_old来自采样时的策略,log_probs_current来自当前更新步的策略,advantages通常由GAE或奖励减基线得到。本案例使用确定性输入直接验证损失函数的数值正确性,这是将PPO损失集成到完整训练循环前必要的单元验证。