Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

DL——深度学习并行技术总结


整体说明

  • 并行化技术一般在训练大型深度学习模型时使用
  • 并行化技术氛围三种:
    • 数据并行 (Data Parallelism)
    • 模型并行 (Model Parallelism)
    • 流水线并行 (Pipeline Parallelism),有的地方也翻译为管道并行

各种并行方法之间的关系总结

  • 整体可分为 模型并行 (Model Parallelism) 和 数据并行
    • 数据并行 :每个 GPU 都拥有一个完整的模型副本,但处理不同的数据批次
    • 模型并行 :每个 GPU 只负责模型的一部分,所有 GPU 共同处理一个完整的数据批次,包括 张量并行 和 流水线并行 两种具体实现
  • 模型并行的进一步介绍:当模型太大无法放入单个 GPU 时,就需要使用模型并行
    • 将模型的不同部分分配给不同的 GPU
    • 优点是可以训练显存无法容纳的巨大模型
    • 缺点是实现相对复杂,且由于不同 GPU 间的通信和等待,可能会导致 GPU 利用率不高
  • 实际应用中,为了充分利用资源并训练超大模型,通常会结合多种并行化技术,形成 混合并行 策略,例如同时使用数据并行、流水线并行和张量并行

数据并行 (Data Parallelism)

  • 最常见、也最容易理解的并行化方法
  • 数据并行的工作方式 :
    • 训练数据集被分成多个子集(例如,一个 128 张图片的批次被分成 4 个 32 张图片的子批次)
    • 每个 GPU 拥有一个完整的模型,并独立处理一个子批次的数据
  • 数据并行的训练过程 :
    • 1)每个 GPU 计算其子批次的前向传播和反向传播,得到各自的梯度
    • 2)通过 All-Reduce 这样的通信操作,将所有 GPU 的梯度进行汇总和平均
    • 3)每个 GPU 用这个平均后的梯度来更新自己的模型参数,从而确保所有模型副本保持同步
  • 优点是实现简单,对模型结构无特殊要求 ,比如使用 PyTorch 的 DP 类就可以实现
  • 缺点是每个 GPU 都需要存储完整的模型 ,当模型参数量非常大时,会超出单个 GPU 的显存限制 ,此时数据并行就无法使用

流水线并行 (Pipeline Parallelism)

  • 流水线并行将模型的不同“层”(或一组层)分配给不同的 GPU,形成一个“流水线”
    • 例如,GPU 1 负责模型的第 1-4 层,GPU 2 负责第 5-8 层,以此类推
  • 数据并行的训练过程 :
    • 一个数据批次被分解成更小的“微批次”(micro-batches)
    • GPU 1 处理第一个微批次,完成后将输出传给 GPU 2
    • 当 GPU 1 开始处理第二个微批次时,GPU 2 就可以同时处理第一个微批次
  • 优点是部分解决了模型过大的问题(单层过大仍然无法解决)
  • 缺点是存在 “流水线气泡”(pipeline bubble) 问题,即流水线开始和结束时,部分 GPU 会处于空闲等待状态,导致 GPU 利用率并非 100%
    • 注:通过流水线的方式(即错位并行处理不同微批次的方式),可以一定程度上重叠不同 GPU 的计算,提高整体效率

张量并行 (Tensor Parallelism)

  • 张量并行 不按层切分模型,而是将模型中某个操作内部的 张量(例如一个大型矩阵)切分到不同的 GPU 上
  • 举例来说:一个 \(A \times B\) 的矩阵乘法,可以把矩阵 B 按列切分,每个 GPU 分别计算,最后再通过通信操作将结果合并
  • 优点是:
    • 可以进一步解决单个层或单个操作的参数过大的问题,从根本上解决了模型过大的问题
    • 因为所有 GPU 都在同一时间处理同一个微批次,所以不会有流水线气泡问题 ,GPU 利用率通常更高
  • 缺点是
    • 对模型结构有要求,通常只能在某些特定操作(如矩阵乘法、线性层)中应用
    • 需要频繁的 GPU 间通信来同步切分后的张量,这要求非常高速的 GPU 互联(例如 NVLink)

NLP——LLM模型存储形式


整体说明

  • 当前主流的大模型存储格式可以按 “训练框架原生格式 -> 通用交换格式 -> 高效推理格式” 这条演进路线来理解
  • TLDR:“训练阶段用 .pth/.ckpt/.bin,跨框架交换用 ONNX,线上部署优先 Safetensors,如果对体积和 CPU 推理速度极端敏感就转 GGUF”

模型格式归纳

  • 训练框架原生格式
    • .pth / .pt:PyTorch 的 pickle 序列化结果,既可以是 state_dict,也可以是完整模型(含结构+权重)通用、易用,但体积大、加载慢,且存在反序列化安全风险
    • .ckpt:PyTorch Lightning 在 .pth 基础上扩展出的 Checkpoint 格式,额外保存优化器状态、epoch、超参等,用于断点续训
    • .bin:TensorFlow 早期常用的纯权重二进制文件,没有统一元数据,需配合 config.json 使用;在 Hugging Face 生态中仍大量出现
  • 通用交换格式
    • ONNX(.onnx):微软+Facebook 推出的开放标准,旨在跨框架(PyTorch/TF/ Paddle 等)部署;支持图优化、量化,但大模型时文件体积依旧可观
    • HDF5 / .h5:Keras/TensorFlow 传统格式,层次化存储网络结构和权重;对超大规模模型支持有限,已逐渐被 TF Checkpoint 或 SavedModel 取代
  • 高效推理格式
    • Safetensors(.safetensors):Hugging Face 推出的安全张量格式,只存权重、无代码、支持 zero-copy 与懒加载,加载速度 >pickle,且杜绝反序列化漏洞,已成为 HF Hub 的默认推荐
    • GGUF(GPT-Generated Unified Format):由 llama.cpp 作者 Georgi Gerganov 设计,用于取代旧版 GGML二进制紧凑、自带量化方案(Q2_K/Q4_0 等)、内存映射快速加载、元数据自包含,无需额外文件即可部署;Gemma、Qwen、Llama-3 等均官方提供 GGUF 版本
    • GGML(已弃用):早期 llama.cpp 使用的二进制格式,无版本控制、扩展困难,已全部迁移到 GGUF
  • 训练/数据级格式(辅助)
    • TFRecord / RecordIO:TensorFlow 训练数据管道常用,顺序、可压缩、高吞吐
    • Parquet / Arrow / LMDB:离线特征或中间结果列式存储,便于大规模并行读取

大模型常用框架相关的格式整体说明

  • 本文描述大模型的存储的形式和 转到 Hugging Face 的方式
  • Megatron / DeepSpeed / FSDP 都把 “一张完整的权重图” 切成很多片,文件名、目录结构、张量 key 名均与 HF 不一致;
  • 想进 HF 生态,必须 “合并分片 + 重命名 key + 生成 config.json”;
  • 合并脚本一般都已有各框架的官方提供,一般按照官方提供脚本转换即可

Hugging Face 原生格式

  • Hugging Face 上的开源模型通常以 “模型仓库(model repository)” 的形式托管,下载到本地后是一个目录,里面包含若干标准文件
  • 使用 Hugging Face 的接口加载模型时,该接口会大致进行以下流程:
    • 加载配置:读取 config.json 文件,用于构建模型的基本结构
    • 加载权重:读取模型权重文件(如 model.safetensors)中的参数值会被加载到定义好的模型结构中
    • 分词器初始化(处理输入):分词器文件(如 tokenizer.json, vocab.json)负责将原始文本转换为模型能够理解的 token ID 序列
    • 其他步骤:如果是文本生成任务,generation_config.json 会提供默认的生成参数
  • 推理最少三件套 :config.json + 权重文件 + 分词器文件
  • 微调再补 :tokenizer_config.json、special_tokens_map.json、generation_config.json 及优化器 checkpoint
  • 大模型 :使用 .safetensors 分片和 *.index.json 索引,断点续传更方便

必存在文件(推理/微调都少不了)

  • config.json
    • 主要包含模型超参与架构描述:隐藏层大小(hidden_size)、注意力头数(num_attention_heads)、层数(num_hidden_layers)、激活函数(hidden_act)等
    • 不同模型的内容不完全相同(是各家模型厂商自己自定义的),这个文件是会被当做超参数传递到模型的初始化文件中的
    • 还包含模型的 参数类型 (比如 "torch_dtype": "bfloat16") 作为加载时的统一转换类型
      • 注:同一个模型的不同参数可以存储为不同类型,这里的 "torch_dtype" 字段仅仅指定加载时的参数
  • 权重主文件(可以是 .bin, .h5, safetensors 等类型的文件,也可能是 gguf 等量化格式的文件)
    • pytorch_model.bin(PyTorch)
      • 分片(shard)文件 比如pytorch_model-00001-of-00008.bin 到 pytorch_model-00008-of-00008.bin,会伴随一个 pytorch_model.bin.index.json 索引文件来记录这些分片信息
    • tf_model.h5(TensorFlow)
    • flax_model.msgpack(Flax/JAX,不常见)
    • model.safetensors(新版统一二进制格式,零拷贝、更安全)
      • Hugging Face 推荐的安全格式 ,不包含可执行代码 ,避免了传统 PyTorch 格式因使用 pickle 序列化而可能存在的安全风险(如恶意代码执行)
      • 通常加载更快且更节省内存
      • 分片(shard)文件 比如model-00001-of-00008.safetensors 到 model-00008-of-00008.safetensors,会伴随一个 model.safetensors.index.json 索引文件来记录这些分片信息
    • gguf(量化后的 GGUF 格式)
      • CPU 或个人设备上进行本地推理 ,首选能提供更好的体验
      • 一般会按照不同的量化格式提供多个 gguf 文件,如 q4_k_m.gguf 等来说明量化形式
    • 注:加载过程中,根据不同的模型权重类型,Hugging Face 框架会使用不同的加载函数加载
  • tokenizer.json
    • 分词器核心配置:预处理器状态、编解码规则、特殊 token 映射
    • 词表内容一般也会在这格文件中,以 vocab 为 Key 存在,所以 tokenizer.json 一般会有几十 MB 大小
  • 补充:model.safetensors.index.json 索引文件示例,包含模型的每一层权重到权重分片的映射
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "metadata": {
    "total_size": 144575840256
    },
    "weight_map": {
    "lm_head.weight": "model-00082-of-00082.safetensors",
    "transformer.h.0.attn.c_attn.bias": "model-00002-of-00082.safetensors",
    "transformer.h.0.attn.c_attn.weight": "model-00002-of-00082.safetensors",
    "transformer.h.0.attn.c_proj.weight": "model-00002-of-00082.safetensors",
    "..."
    }
    }

常见文件(大部分仓库可见)

  • 分词器相关文件:
    • tokenizer.json:分词器的完整定义,包括编码规则和词汇表映射(前面已经介绍过)
    • tokenizer_config.json:分词器的附加配置,如特殊标记(如[CLS]、[SEP]、[PAD])、填充方式、截断策略等
      • 部分模型会将聊天模版也放到这个文件中的 chat_template 字段(Qwen,Deepseek 等),部分模型则将聊天模板放到外面的 chat_template.jinja 文件(这样虽然不便于管理,但可读性会更高)
    • vocab.txt, vocab.json:模型的词汇表,存储 token 到 ID 的映射关
      • 注:目前许多模型已经不需要这个文件,因为该文件会以 "vocab" 字段的形式放到 tokenizer.json 中
    • merges.txt:适用于 BPE 等分词算法,定义了 token 的合并规则
      • 注:目前许多模型已经不需要这个文件,因为该文件会以 "merges" 字段的形式放到 tokenizer.json 中
    • special_tokens_map.json:统一声明 [PAD]、[CLS]、[SEP]、<|im_start|> 等特殊 token 的 ID 与字符串映射
      • 注:目前许多模型已经不需要这个文件,因为该文件会以 "additional_special_tokens" 字段的形式放到 tokenizer_config.json
    • added_tokens.json :用户或微调阶段追加的新 token
      • 注:目前许多模型已经不需要这个文件,因为该文件会以 "added_tokens" 字段的形式放到 tokenizer.json 中
  • generation_config.json : 文本生成默认策略:max_new_tokens、do_sample、temperature、top_p 等
  • .gitattributes : 用于配合 Git-LFS 把大文件托管到 LFS
    • gitattributes 是 Git 中用于定义特定文件(或文件类型)在 Git 操作中的处理规则的配置文件
    • 核心作用是 “为不同文件定制 Git 行为” ,告诉 Git:对于不同类型的文件,应该如何执行换行符转换、合并策略、文件属性标记、diff 对比方式等操作,从而在团队协作或跨平台开发中保持文件处理的一致性

附录:关于 generation_config.json 文件的使用

  • 在使用 Hugging Face 的 transformers 库加载模型时,会自动读取模型文件路径下的 generation_config.json 文件(如果存在的话)

  • generation_config.json 是用于存储模型生成相关配置的文件,包含了如最大生成长度(max_length)、采样温度(temperature)、top-k 采样等与文本生成任务相关的参数

  • 当使用 from_pretrained() 方法加载模型时,库会自动检查并加载该文件中的配置,这些配置会被存储在模型的 generation_config 属性中。例如:

    1
    2
    3
    4
    5
    6
    7
    from transformers import AutoModelForCausalLM, AutoTokenizer

    model = AutoModelForCausalLM.from_pretrained("model_path")
    tokenizer = AutoTokenizer.from_pretrained("model_path")

    # 查看加载的生成配置
    print(model.generation_config)
  • 如果模型路径中存在 generation_config.json,上述代码会自动加载其中的配置;如果该文件不存在,transformers 会使用默认的生成配置

  • 也可以通过 GenerationConfig 类手动加载或修改这些配置,并在生成文本时传入:

    1
    2
    3
    4
    5
    6
    7
    8
    from transformers import GenerationConfig

    # 手动加载生成配置
    gen_config = GenerationConfig.from_pretrained("model_path")
    # 修改配置
    gen_config.max_length = 100
    # 生成文本时使用
    outputs = model.generate(**inputs, generation_config=gen_config)

其他可选/场景文件

  • training_args.bin : 由 transformers.Trainer 自动保存,包含学习率、warmup step、batch_size 等训练超参
  • optimizer.bin / scheduler.bin : 断点续训时保存的优化器状态和 LR scheduler 状态
  • quantization/ 目录 : 低比特量化权重,如 F8_E4M3、INT4、GGML 等
  • README.md / LICENSE / *.md : 模型卡片、许可证、使用示例、局限性与伦理声明
  • preprocessor_config.json : 多模态模型(如 LLaVA、BLIP-2)中,图像预处理超参
  • adapter_config.json / adapter_model.bin : PEFT/LoRA 微调产生的轻量 adapter,仅含可训练增量参数
  • tokenizer.model 文件是 SentencePiece 分词器的核心文件,通常以二进制格式存储,包含分词规则、词汇表和预处理逻

关于 .bin 格式 和 .safetensors 格式的说明

  • .bin 是 Hugging Face 最早、最通用的格式(PyTorch 的格式),任何支持 from_pretrained() 的库都能直接加载

  • 如果同时包含一个同名的 .safetensors 和 .bin,HF 会优先用 .safetensors(更快、更安全)

  • 将 .bin 格式升级为 .safetensors 格式的接口如下:

    1
    2
    import safetensors
    safetensors.torch.save_file(state_dict, "model.safetensors")
  • 特别说明:.bin 和 .safetensors 中会包含权重文件的参数类型(fp16, fp32, bf16 等)

    • 而且,.bin 和 .safetensors 文件会为每个不同的参数张量存储各自的参数,所以理论上这些参数类型可以不用
    • 在加载模型时,会先按照权重文件中的真实类型读取,并转换成 config.json 中指定的文件格式(比如 "torch_dtype": "bfloat16" 指定 bf16 格式),存放到内存中

Megatron-LM 框架文件格式

  • 分片数量与分片参数(TP=N1、PP=N2、DP=N3)有关,下面是磁盘目录示例:

    1
    2
    3
    4
    5
    6
    7
    8
    iter_0001000/
    ├── model_optim_rng.pt # 传统同步格式(老版本)
    ├── __0_0.distcp # 新异步格式(v0.7+),每个文件只含本 rank 的分片
    ├── ...
    ├── __1_0.distcp
    ├── common.pt # 公共张量(embedding、lm_head 等)
    ├── metadata.json # 并行拓扑
    └── latest_checkpointed_iteration.txt
  • 注:部分 Megatron-LM 存储形式中, iter_0001000 下存储的是多个类似 mp_rank_xx_xx_cp_xx_dp_xx 目录的结构,每个结构存储部分模型参数

    • 这是 Megatron-LM 原生 checkpoint 的分布式存储结构

    • 命名规则:

      1
      mp_rank_{tensor_parallel_rank}_{checkpoint_partition}_{data_parallel_rank}
    • 含义:

      • mp_rank_xx_xx :Tensor Model Parallel rank(张量并行+流水线并行的分片编号)
      • cp_xx :Context parallel rank(上下文并行分片编号)
      • dp_xx :Data parallel rank(数据并行的副本编号)
    • 每个目录里可能包含:

      • distrib_optim.pt
        • 分布式优化器(比如 ZeRO)的状态分片,包含梯度累积缓冲、参数分片等,用于 resume 训练使用,若确定不再需要继续训练,则可以删除该文件
      • model_optim_rng.pt
        • 保存随机数生成器状态(Python random、NumPy RNG、PyTorch CPU/CUDA RNG、Megatron并行RNG),用于恢复训练时保证随机性一致
        • 注:部分架构中,模型权重也存储在这个文件里面
  • Megatron 格式与 HF 不兼容;需要合并+重命名,下面是官方给出的转换脚本(同步格式)

    1
    2
    3
    4
    5
    6
    python tools/checkpoint_converter.py \
    --model-type GPT \
    --load-dir iter_0001000 \
    --save-dir hf_format \
    --target-tensor-parallel-size 1 \
    --target-pipeline-parallel-size 1
  • HF 的一般格式现在是类似下面的形式

    1
    2
    3
    4
    5
    6
    7
    hf_format/
    ├── model_00001-of-00010.safetensors # 文件权重
    ├── model_xxx...
    ├── model.safetensors.index.json # 分片成多个文件时用于索引
    ├── tokenizer.json
    ├── tokenizer_config.json
    └── config.json # 由脚本自动生成

DeepSpeed 框架文件格式

  • ZeRO-3 会对参数进行分片,分片数量参数有关,磁盘目录(16 GPU)

    1
    2
    3
    4
    5
    6
    7
    global_step1000/
    ├── bf16_zero_pp_rank_00_mp_rank_00_optim_states.pt # 优化器状态
    ├── bf16_zero_pp_rank_01_mp_rank_00_optim_states.pt
    ├── ...
    ├── zero_pp_rank_00_mp_rank_00_model_states.pt # 权重分片
    ├── zero_pp_rank_01_mp_rank_00_model_states.pt
    └── ...
  • DeepSpeed 与 HF 不兼容;需要合并(DeepSpeed 自带工具)

    1
    python zero_to_fp32.py global_step1000 ds_model.pth
  • 进一步精简权重文件(仅保留权重)并转 HF 的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import torch
    from transformers import AutoConfig, AutoModelForCausalLM

    state_dict = torch.load('ds_model.pth', map_location='cpu')
    torch.save(state_dict, 'pytorch_model.bin') # 仅权重

    config = AutoConfig.from_pretrained('meta-llama/Llama-2-7b-hf')
    model = AutoModelForCausalLM.from_config(config)
    model.load_state_dict(state_dict)
    model.save_pretrained('hf_from_ds')

PyTorch FSDP 框架文件格式

  • 磁盘目录(8 GPU)

    1
    2
    3
    4
    5
    checkpoint-1000/
    ├── __0_0.distcp # 每个 rank 的分片
    ├── ...
    ├── __7_0.distcp # 每个 rank 的分片
    └── .metadata # FSDP 元数据
  • PyTorch FSDP 与 HF 不兼容;需要合并,官方合并脚本(PyTorch 大于 2.2)

    1
    2
    3
    python -m torch.distributed.checkpoint.format_utils dcp_to_torch_save \
    checkpoint-1000 \
    fsdp_model.pth
  • 再转成 HF Safetensors(更快、安全)

    1
    2
    3
    4
    5
    from safetensors.torch import save_file
    import torch

    state_dict = torch.load('fsdp_model.pth')
    save_file(state_dict, 'model.safetensors')

附录:在不加载模型的情况下查看 safetensors 文件参数类型

  • 使用 transformers 库加载模型后查看参数,参数可能会被自动转换(依据不同模型实现有所不同,部分模型参数加载后是 float32)
    • 注意:即使 config.json 中显示是 "torch_dtype": "bfloat16",在 from_pretrain 函数不显示指定参数类型的情况下,也会出现自动转换为 float32 的情况
  • 显示指定参数类型加载后,输出与指定类型一致,但是看不到原始的参数类型了
  • 下面介绍两种方法,可以直接查看某个 safetensors 文件的参数类型

方式一:命令行查看

  • 使用 hexdump 命令可以抽取部分文件查看其 dtype 信息

    1
    hexdump -C -n 4096 model_00001-of-00010.safetensors | grep -A 20 '"dtype"'
  • 这条命令的作用是查看 safetensors 模型文件的十六进制内容,并筛选出包含 “dtype” 的行及其后 20 行,以便分析模型数据类型相关信息。下面是详细解释:

    • hexdump:用于以十六进制和 ASCII 形式显示文件内容的工具
    • -C:以规范的十六进制+ASCII 格式显示,左侧为十六进制值,右侧为对应的可打印字符
    • -n 4096:仅显示文件的前 4096 个字节(4KB)
    • grep -A 20:在文本中搜索匹配模式,除了显示匹配的行外,还显示该行之后的 20 行内容(A 即 After 的缩写)
  • 执行命令后会看到类似的输出:

    1
    ..."dtype":"BF16"...

方式二:python 查看

  • 安装 safetensors 包

  • 执行下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from safetensors.torch import load_file

    # 加载 .safetensors 文件
    weight_file = "~/model/Qwen2.5-7B-Instruct/model-00001-of-00004.safetensors"

    state_dict = load_file(weight_file, device="cpu")

    # 查看存储类型
    for name, param in list(state_dict.items())[:5]:
    print(f"参数 {name} 在硬盘上的存储类型: {param.dtype}")
  • 输出如下:

    1
    2
    3
    4
    5
    参数 model.embed_tokens.weight 在硬盘上的存储类型: torch.bfloat16
    参数 model.layers.0.input_layernorm.weight 在硬盘上的存储类型: torch.bfloat16
    参数 model.layers.0.mlp.down_proj.weight 在硬盘上的存储类型: torch.bfloat16
    参数 model.layers.0.mlp.gate_proj.weight 在硬盘上的存储类型: torch.bfloat16
    参数 model.layers.0.mlp.up_proj.weight 在硬盘上的存储类型: torch.bfloat16
  • bfloat16 与 Qwen2.5-7B-Instruct 的 config.json 类型能对齐


附录:ckpt 中添加自定义模型类

  • 在模型 ckpt 目录(hf 文件目录)下,可以存放 *.py 文件,用于定义自定义的模型结构
  • 这些 *.py 文件会被 transformers 库加载,故而可以在 config.json 中指定使用
  • 注意:megatron 训练一般不会使用 config.json 中指定的类,而是根据各种超参加载的
  • transformers 库 AutoModelForCausalLM.from_pretrained -> AutoConfig.from_pretrained 加载模型的方式有两种:
    • 第一种:config.json 包含 model_type 参数的
      • 此时要求模型类提前备注册过
    • 第二种:config.json 不包含 model_type 参数的
      • 此时可以按照自定义的类进行初始化(定义在 *.py 中,放到 ckpt 路径下即可)
      • 执行 AutoModelForCausalLM.from_pretrained 函数时添加 trust_remote_code=True 参数,否则无法加载模型文件

RL——PPO及其训练技巧

  • 参考链接:
    • 原始论文:Proximal Policy Optimization Algorithms, 2017, OpenAI
    • 相关博客:影响PPO算法性能的10个关键技巧(附PPO算法简洁Pytorch实现)

PPO 方法介绍

PPO 的目标

  • PPO目标定义
    $$
    \begin{aligned}
    \max_{\theta_\text{new}} \quad &\mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left[\frac{\pi_{\theta_\text{new}}(a|s)}{\pi_{\theta_\text{old}}(a|s)} A_{\pi_{\theta_\text{old}}}(s,a)\right] \\
    &\text{s.t. } \quad \quad \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}} \left[D_{\text{KL}}(\pi_{\theta_\text{old}}, \pi_{\theta_\text{new}})\right] \le \delta
    \end{aligned}
    $$
  • PPO目标详细推导见RL——TRPO-PPO-目标函数基础推导

PPO-Penalty

  • 又名PPO-惩罚
    $$
    \begin{aligned}
    \max_{\theta}&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left[\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a) - \beta D_{KL}(\pi_{\theta_{\text{old}}}(\cdot|s), \pi_\theta(\cdot|s))\right]
    \end{aligned}
    $$

PPO-Clip

  • 又名PPO截断
    $$
    \begin{aligned}
    \max_\theta&\ \ \mathbb{E}_{s\sim \rho_{\theta_{\text{old}}},a\sim q(a|s)}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{q(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{q(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
    \end{aligned}
    $$
  • 理论上,以上采样分布可以是任意分布 ,实际上使用Old策略效果更好,样本利用率也更高,所以常用的PPO目标一般会如下定义:
    $$
    \begin{aligned}
    \max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
    \end{aligned}
    $$
    • 以上目标是 \(\max_\theta\),实际实现时会在令损失函数等于负的目标函数
  • 令 \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} \),则有:
    $$
    \begin{aligned}
    \max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
    \end{aligned}
    $$

PPO-Clip 进阶讨论

  • 副标题:PPO-Clip的损失函数究竟在做什么?为什么需要使用 \(\min\) 操作?
  • 参考链接:如何理解 PPO-CLIP 目标函数中的 clip 和 min 操作?过犹不及论 - Finch的文章 - 知乎
  • PPO的目标定义如下:
    $$
    \begin{aligned}
    \max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
    \end{aligned}
    $$
    • 注意: 发生截断时, \((1+\epsilon)A_{\theta_{\text{old}}}(a,s)\) 或 \((1-\epsilon)A_{\theta_{\text{old}}}(a,s)\) 对策略参数 \(\theta\) 的梯度为 0 :
      • 常数 \((1+\epsilon)\) 和 \((1+\epsilon)\) 对策略参数的梯度为 0
      • \(A_{\theta_{\text{old}}}(a,s)\) 本身与旧策略有关(但本质上也只是按照旧策略与环境交互得到 Reward 而已,也不会回传梯度到旧策略),与当前策略参数 \(\theta\) 无关,对策略参数 \(\theta\) 的梯度为 0;
      • \(A_{\theta_{\text{old}}}(a,s)\) 计算优势函数时用到了价值网络,这也与当前策略参数 \(\theta\) 无关,对策略参数 \(\theta\) 的梯度为 0(特别地,\(A_{\theta_{\text{old}}}(a,s)\) 是经过 stop_gradient 得到的,也不会影响价值网络的参数)
  • \(clip + \min\) 操作讨论 ,关于 \(\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)\),由于 \(A_{\theta_{\text{old}}}(a,s) > 0\) 是有正有负的(\(A=Q-V\) 的加权平均),对于某个样本来说:
    • 当 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时,要提升目标动作概率 \(\pi_\theta(a|s)\) :
      • 若 \(r(\theta) > 1+\epsilon\),则说明相对原始策略 \(\pi_{\theta_\text{old}(a|s)}\),\(\pi_\theta(a|s)\) 已经提升够多了 ,不希望再继续提升(偏离原始策略太多容易不稳定),\(clip + \min\) 操作可以将这个样本的目标值截断为 \((1+\epsilon)A_{\theta_{\text{old}}}(a,s)\),这与策略参数 \(\theta\) 无关 ,不会有梯度回传,即该样本相当于被废弃了(不考虑求均值会用到样本数量)
      • 若 \(r(\theta) \leq 1+\epsilon\),则目标动作概率 \(\pi_\theta(a|s)\) 还小 ,可以正常更新以提升该动作的概率(问题:这里其实是无法控制目标概率更新的幅度的,更新后的真实值可能超过 \(1+\epsilon\) 这个阈值)
    • 当 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时,要降低目标动作概率 \(\pi_\theta(a|s)\) :
      • 若 \(r(\theta) < 1-\epsilon\),则说明相对原始策略 \(\pi_{\theta_\text{old}(a|s)}\),\(\pi_\theta(a|s)\) 已经降低的够多了 ,不希望再继续降低(偏离原始策略太多容易不稳定),\(clip + \min\) 操作可以将这个样本的目标值截断为 \((1-\epsilon)A_{\theta_{\text{old}}}(a,s)\),这与策略参数 \(\theta\) 无关,不会有梯度回传,即该样本相当于被废弃了(不考虑求均值会用到样本数量)
      • 若 \(r(\theta) \geq 1+\epsilon\),则目标动作概率 \(\pi_\theta(a|s)\) 较大 ,可以正常更新以降低该动作的概率(问题:这里其实是无法控制目标概率更新的幅度的,更新后的真实值可能小于 \(1-\epsilon\) 这个阈值)
    • 思考:由于 \(r(\theta) > 1+\epsilon\) 的样本动作概率不许继续提升(但可以被降低),\(r(\theta) < 1-\epsilon\) 的样本动作概率不许继续降低(但可以提升),所以整体来说,所有动作的概率都倾向于维持 \(r(\theta) \in [1-\epsilon, 1+\epsilon]\) 之间(只是倾向于,不能完全保证)
    • 严格One-Step更新下,损失函数可做如下简化:此时旧策略采样的样本仅更新一次模型即丢弃(即epoch=1,且一次更新完所有参数,batch_size足够大),且立刻会将新策略的更新同步到旧策略上,保证每次更新模型前新旧策略完全一致 ,则无需使用Clip操作和min操作(因为 \(r(\theta)=1\)),PPO目标函数将可以简化为如下形式(注意,虽然此时 \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}=1\),但是必须保留 \(r(\theta)\),因为梯度传导需要分子 \(\pi_\theta(a|s)\)):
      $$
      \begin{aligned}
      \max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}\right)
      \end{aligned}
      $$
      • 建议epoch大于1的理由1 :One-Step更新样本效率低,如果设置的学习率过大又会引发不稳定,所以建议还是多次epoch更新,增加样本利用率
      • 建议epoch大于1的理由2 :epoch大于1的多次更新还有补救回调的作用,当策略已经更新偏离旧策略太多时,PPO损失函数保证可以有机会被拉回来
      • 补充讨论:实际上,严格One-Step更新下,PPO降级为普通PG ,更详细的讨论见附录
    • 补充:Simplified PPO-Clip Objective中有PPO简化的推导,能更加清晰的显示PPO的 \(clip + \min\) 操作
  • 一些博客(比如:Visualize the Clipped Surrogate Objective Function)有关于Clip仅限制了单边的讨论(有效性有待商榷):
    • 当 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时, \(r(\theta)\) 在Clip后的生效范围在 \(r(\theta) \in [0,1+\epsilon]\),也就是 \(1-\epsilon\) 边界会失效(问题 :此时失效是正常的吧,因为此时 \(A_{\theta_{\text{old}}}(a,s) > 0\),我们要提升目标动作概率,当前的策略目标动作概率越小,我们越应该提升,这反而是PPO的设计:为了使得整体策略不偏离旧策略太远?)
    • 当 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时, \(r(\theta)\) 在Clip后的生效范围在 \(r(\theta) \in [1-\epsilon,+\infty]\),也就是 \(1+\epsilon\) 边界会失效(同上问题 :此时失效是正常的吧,因为此时 \(A_{\theta_{\text{old}}}(a,s) < 0\),我们要降低目标动作概率,当前的策略目标动作概率越大,我们越应该降低,这反而是PPO的设计:为了使得整体策略不偏离旧策略太远?)
    • 改进方案(有效性有待商榷) :在on-policy的设定下,我们认为策略新旧策略的比值 \(r(\theta)\) 不会太大,一般不会出现问题,但是off-policy设定下,可能会出现问题,所以需要再加一层Clip(参考自:Visualize the Clipped Surrogate Objective Function)
      $$
      \begin{aligned}
      \max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\max\left( \color{red}{\eta A_{\theta_{\text{old}}}(a,s)}, \min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)\right)
      \end{aligned}
      $$
      • 其中 \(\color{red}{\eta}\) 是超参数(理解:一般会比较大,比如 5 或 10 等?)
      • 注:综合上面的问题,这里的结论不一定合理 ,这种做法的有效性还有待商榷,因为 Clip 不是在控制每一个epoch更新后的 \(r(\theta)\),而是根据更新前的 \(r(\theta)\) 判断是否要继续更新对应的状态动作对
      • 如果从比值过大可能是对应异常值导致来看(比如模型推理错误导致异常值),这里确实可以做一下截断
        • 否则使用保留梯度的截断更合适(至少保留部分梯度),详情见附录
      • 特别说明:理论上来说,上述的 \(\max\left( \color{red}{\eta A_{\theta_{\text{old}}}(a,s)}, \cdot\right)\) 只会在 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时生效,因为 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时会被更小的上界 \(1+\epsilon\) 提前 Clip 掉(相当于 \(\color{red}{\eta}\) 没有生效)

PPO 网络更新

  • Critic 网络更新(原始论文中未明确给出 Critic 网络更新的公式,实际上 Critic 网络的更新有直接使用真实折扣奖励作为目标值、TD-Error 作为损失函数和使用GAE 作为目标值等版本,这里给出TD-Error 作为损失函数的形式,更多详情见后面的章节单独讨论)
    $$
    Loss_{\text{critic}} = \sum (r_t + \gamma V^{\bar{w}}(s_{t+1}) - V^{w}(s_{t})) ^ 2
    $$
    • 这里虽然使用 Target V 网络表达,但实际上PPO一般不需要使用 Target V 网络
    • 这里V值拟合的目标是策略 \(\pi_\theta\) 对应的V值 \(V^{\pi_\theta}\)
    • \(r_t = r(s_t, a_t)\vert_{a_t \sim \pi_\theta(\cdot|s_t)}\),训练用的整个轨迹链路都是从策略 \(\pi_\theta\) 采样得到的
  • Actor网络更新
    $$
    Loss_{\text{actor}} = - \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
    $$
    • 其中: \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} \)

PPO Critic 网络更新的其他方式

  • 损失函数形式的选择 :在PPO算法中,Critic网络(即价值函数网络)的更新通常采用均方误差(MSE)损失或Huber Loss(也称为 Smooth_l1_loss),通过梯度下降来最小化价值函数的预测误差
  • PPO中Critic的核心更新公式为 MSE损失等,但目标值 \( V_{\text{target} } \) 的计算方式可能因具体实现而异(单步TD、GAE等)。实际代码中通常结合以下步骤:
    • 从经验缓冲区采样数据 \((s_t, r_t, s_{t+1})\)
    • 计算目标值 \( V_{\text{target} } \)(如单步TD或GAE)
    • 最小化 \( L_{\text{critic} } \) 更新Critic参数 \( \theta \)
  • Critic 损失函数(MSE) :
    $$
    L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( V_{\theta}(s_t) - V_{\text{target} }(s_t) \right)^2 \right]
    $$
    • \( V_\theta(s_t) \) 是当前Critic网络的输出,\( \theta \) 为网络参数
    • \( V_{\text{target} }(s_t) \) 是目标值,可以有多种实现,具体实现见下文
可选目标1:基于 TD 误差的更新(Temporal Difference Learning)
  • Critic的目标是拟合状态值函数 \( V^\pi(s) \),通过TD误差计算当前值函数的预测与目标值的差异

  • TD目标值(单步) :
    $$
    V_{\text{target} }(s_t) = r_t + \gamma V_{\text{old} }(s_{t+1})
    $$

    • \( \gamma \) 是折扣因子
    • \( V_{\text{old} } \) 是旧Critic网络的输出(稳定训练),需要注意这里不是Target网络,而是PPO每个episode可能会更新多个epoch ,需要保证更新过程中Critic网络的学习目标是不变的,即每个epoch中 ,目标值\( V_{\text{target} }(s_t) \) 始终是不变的 ,实现时,确保目标值是提前计算得到的即可,具体实现可以如下(参考自动手学强化学习实现):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      # ...
      td_target = rewards + self.gamma * self.critic(next_states) * (1 - dones)
      # ...
      for _ in range(self.epochs):
      critic_loss = torch.mean(F.mse_loss(self.critic(states), td_target.detach()))
      self.critic_optimizer.zero_grad()
      critic_loss.backward()
      self.critic_optimizer.step()
      # ...
  • 此时的Critic 损失函数相当于:
    $$
    L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{r_t + \gamma V_{\text{old} }(s_{t+1})} - V_{\theta}(s_t)\right)^2 \right]
    $$

可选目标2:广义优势估计(GAE)结合多步TD
  • 当使用GAE(Generalized Advantage Estimation)时,Critic的目标值会引入多步TD误差的加权平均,进一步减少方差
  • GAE 优势函数 :
    $$
    A_t^{\text{GAE}(\gamma, \lambda)} = \sum_{k=0}^{T-t} (\gamma \lambda)^k \delta_{t+k}
    $$
    • 其中 \( \delta_t = r_t + \gamma V_{\text{old} }(s_{t+1}) - V_{\text{old} }(s_t) \) 是单步TD误差,\( \lambda \) 是GAE超参数
  • Critic 目标值(GAE) :
    $$
    V_{\text{target} }(s_t) = A_t^{\text{GAE} } + V_{\text{old} }(s_t)
    $$
  • 此时的Critic 损失函数相当于:
    $$
    L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{A_t^{\text{GAE} } + V_{\text{old} }(s_t)} - V_{\theta}(s_t)\right)^2 \right]
    $$
可选目标3:折扣真实奖励
  • 对于状态 \( s_t \),其目标值 \( V_{\text{target}}(s_t) \) 是从 \( t \) 时刻开始到回合结束的累计折扣奖励:
    $$
    V_{\text{target}}(s_t) = \sum_{k=0}^{T-t} \gamma^k r_{t+k}
    $$
    • \( T \) 是回合终止时间步,
    • \( \gamma \) 是折扣因子(如0.99),
    • \( r_{t+k} \) 是 \( t+k \) 时刻的即时奖励
  • 此时的Critic损失函数相当于:
    $$
    L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{\sum_{k=0}^{T-t} \gamma^k r_{t+k}} - V_{\theta}(s_t)\right)^2 \right]
    $$
其他优化:目标网络(Target Network,一般PPO不需要)
  • 为稳定训练,Critic的目标值 \( V_{\text{target} } \) 可能通过慢更新的目标网络计算(类似DQN):
    $$
    V_{\text{target} }(s_t) = r_t + \gamma V_{\bar{\theta}}(s_{t+1})
    $$
    • 其中 \( \bar{\theta} \) 是目标网络参数,通过Polyak平均更新(一种加权平均方法,即RL中常说的软更新):
      $$
      \theta^- \leftarrow \tau \theta + (1 - \tau) \bar{\theta} \quad (\tau \ll 1)
      $$

PPO 的一些实践说明

  • 常用的形式是 PPO-Clip 形式,实践中效果更好
  • 一般来说 PPO 需要使用 Target V 网络,使用 Target V 网络会导致收敛较慢
  • Critic 网络的损失函数可以归回方法中常用的 smooth_l1_loss(即huber_loss),以减少异常值带来的影响
  • PPO-Clip 中一般设置 \(\epsilon=0.2\)
  • PPO原始论文中,每次采样到的数据会作 K 次 epochs,且不同游戏使用的次数不同,在 Mujoco 中使用 \(epochs=10\),Roboschool 中使用 \(epochs=15\),Atari 中使用 \(epochs=3\)

PPO 连续动作实现离散动作的实现主要区别

模型建模
  • 策略网络 :连续动作需要使用 \(\mu_\theta,\sigma_\theta\) 表示均值和方差,连续分布下,每个动作的概率理论上都是0,但借助概率密度函数的含义,可以通过计算

  • 采样方式 :采样时需要创建分布来采样,由于不需要梯度回传,所以不需要使用重参数法

  • Critic网络 :由于离散连续场景都用V网络,仅仅评估状态下的价值即可,与动作无关,连续动作处理不需要特殊修改

    新旧策略比值计算方式不同
  • 离线动作按照推导中的实现即可

    1
    2
    3
    4
    def compute_surrogate_obj(self, states, actions, advantage, old_log_probs, actor):  # 计算策略目标
    log_probs = torch.log(actor(states).gather(1, actions))
    ratio = torch.exp(log_probs - old_log_probs)
    return torch.mean(ratio * advantage)
  • 连续动作需要使用概率密度函数来实现

    1
    2
    3
    4
    5
    6
    def compute_surrogate_obj(self, states, actions, advantage, old_log_probs, actor):
    mu, std = actor(states)
    action_dists = torch.distributions.Normal(mu, std)
    log_probs = action_dists.log_prob(actions) # 返回\log(f(actions)),f为概率密度函数
    ratio = torch.exp(log_probs - old_log_probs) # 这里可以直接用于算概率之间的比值理论是概率密度函数的含义
    return torch.mean(ratio * advantage) # 注意torch内部实现这里的梯度可以回传到actor网络上(基于参数mu,std可以运算得到log_prob,所以梯度可以回传)

PPO 的训练技巧

  • 参考:影响PPO算法性能的10个关键技巧(附PPO算法简洁Pytorch实现)
  • 亲自测试实践总结(主要以 ‘CartPole-v0’ 环境测试):
    • Advantage Normalization 能让 Critic Loss 和 Policy Loss 都更加平滑(比较稳定),但是对整体回报不一定有收益,某些情况下还出现了波动(待确定原因)
    • State Normalization 会严重拖慢训练速度,实际测试时发现一个有趣的现象:使用 State Normalization 技巧后,会导致策略先逐步收敛到最优策略再突然下降,降幅很大且不再恢复,状态和策略都陷入了崩溃状态(结论是 State Normalization 技巧要慎用)
    • Orthogonal Initialization 一般都会有正向的效果,虽然某些场景下不一定提升很大
    • Reward Scaling 会导致策略不稳定(甚至无法收敛)
    • 增加 Policy Entropy 会导致收敛不稳定,且超参数很敏感
  • 附上述实验的代码:
    >>>点击展开折叠内容...
    1
     

Advantage Normalization

  • 最早出自The Mirage of Action-Dependent Baselines in Reinforcement Learning,对 Advantage Function 进行归一化,用于提升 PG 方法的性能

  • 具体方法:减去均值除以方差

  • 理解:

    • 归一化 将 Advantage 的均值强制设为 0:
      • 这意味着在当前的 Batch 中,大约有一半的动作会被认为是“好动作”(\(A>0\),增加概率),另一半是“坏动作”(\(A<0\),减少概率)
      • 这能有效防止 Policy 总是往一个方向跑(例如 Reward 全是正数时),显著加快收敛
    • 归一化 将方差设为 1
      • 这使得 Loss 的量级不会因为 Reward 的绝对数值大小而剧烈波动,使得超参数(如 Learning Rate)更容易调节
  • 实现方案:

    • 方案一:Batch Advantage Normalization(BAN),对当前 Batch 的所有 Advantage 求均值和方差
    • 方案二:Mini-Batch Advantage Normalization(MBAN),仅对当前 Mini-Batch Advantage Normalization
  • 实践中,BAN 效果最好,MBAN 效果次之,不使用任何 AN 效果最差;

    • 理解,方案一和方案二并未限定具体采样的轨迹是多少个,但是主要思路是尽量在更多的样本上统计均值和方差,减少波动,这样效果更好些
  • 关于 MBAN,ppo-implementation-details博客中有详细实现

  • 问题:GAE 中还需要做 Advantage Normalization 吗?是否是在计算 GAE 之前做归一化?

    • 回答,可以做,是在 GAE 之后做
  • 在一个带 GAE 和 Advantage Normalization 的 PPO 的一个训练迭代中,流程通常如下:

    • 第一步:采集数据(Rollout) :Agent 与环境交互,采集一定步数的数据。假设采集了 \(N\) 个步骤(例如 2048 步)
    • 计算 GAE :利用这 \(N\) 个数据,计算出每一个时间步的 Advantage值,得到一个向量 \(\mathbf{A} = [A_1, A_2, …, A_N]\)
    • 计算统计量(非滑动) :直接计算这 \(N\) 个数据的均值和标准差:
      $$ \mu_{batch} = \frac{1}{N} \sum_{i=1}^{N} A_i $$
      $$ \sigma_{batch} = \sqrt{\frac{1}{N} \sum_{i=1}^{N} (A_i - \mu_{batch})^2} $$
    • 执行归一化 :
      $$ A_{norm}^{(i)} = \frac{A_i - \mu_{batch}}{\sigma_{batch} + \epsilon} $$
    • Mini-batch 训练 :将归一化后的数据打乱(Shuffle),切分成多个小批次(Mini-batch)进行 SGD 更新
为什么不用滑动平均?
  • Advantage 的定义是相对的 :Advantage \(A(s,a) = Q(s,a) - V(s)\) 衡量的是动作 \(a\) 比“平均表现”好多少
  • 分布漂移(Non-stationarity) :随着 Policy 的更新,Agent 的能力在变,Value Function 也在变
    • 上一次迭代算出的 Advantage 分布与当前迭代的分布可能完全不同
    • 如果使用历史数据的滑动平均,会引入过时的统计信息,导致对当前策略评估的偏差
  • Advantage Normalization 的核心目的是为了降低方差(Variance Reduction) 并确保 Policy Gradient 的更新幅度在不同 Batch 间保持稳定,而不是为了将数据缩放到某个固定的物理尺度
代码示例
  • 类似 开源框架 Stable Baseline3 的伪代码逻辑如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 采集数据并计算 GAE (在 rollout_buffer 中完成)
    rollout_buffer.compute_returns_and_advantage(...)

    # 在准备训练数据时进行归一化
    # 注意:这里是对整个 buffer 的 advantage 进行操作
    advantages = rollout_buffer.advantages
    if self.normalize_advantage:
    # 直接计算当前 buffer 的均值和标准差
    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    # 之后再进行 mini-batch 循环更新
    for epoch in range(n_epochs):
    for rollout_data in rollout_buffer.get(batch_size):
    # 使用已经归一化好的 advantages 计算 loss
    pass
讨论:PPO 训练过程中,advantages 均值应该在 0 附近波动,为什么还要 Advantage Normalization?
  • 结论:即使没有 Advantage Normalization,advantages 均值也应该在 0 附近波动才对
    • 下面我们可以证明:即使在不增加 Advantage Normalization(优势归一化)的情况下,Advantage(优势函数)的数学期望(均值)确实应该在 0 左右
  • 但在实际的 PPO 训练工程中,由于 Value Network 的估计误差以及有限样本采样的随机性,一个 Batch 内计算出的 Advantage 均值通常只是围绕 0 波动 ,有时甚至会出现明显的偏差
理论推导:为什么期望为 0?
  • 优势函数 \(A^\pi(s, a)\) 的物理意义是:在状态 \(s\) 下,采取动作 \(a\) 比当前策略 \(\pi\) 的平均表现好多少,标准定义为动作价值函数与状态价值函数之差:
    $$ A^\pi(s, a) = Q^\pi(s, a) - V^\pi(s) $$
  • 根据状态价值函数的定义,\(V^\pi(s)\) 本质上是在策略 \(\pi\) 下对 \(Q^\pi(s, a)\) 的数学期望:
    $$ V^\pi(s) = \mathbb{E}_{a \sim \pi(\cdot|s)} \left[ Q^\pi(s, a) \right] $$
  • 如果作者给定状态 \(s\),并严格按照当前策略 \(\pi\) 采样动作 \(a\),优势函数的期望严格为 0:
    $$ \mathbb{E}_{a \sim \pi(\cdot|s)} \left[ A^\pi(s, a) \right] = \mathbb{E}_{a \sim \pi(\cdot|s)} \left[ Q^\pi(s, a) - V^\pi(s) \right] = V^\pi(s) - V^\pi(s) = 0 $$
实际计算:PPO 中的 GAE
  • 在 PPO 的实际实现中,作者通常使用 GAE 来计算 Advantage,GAE 是基于 TD Error 构建的
  • 单步的 TD Error 定义为:
    $$ \delta_t = r_t + \gamma V_\phi(s_{t+1}) - V_\phi(s_t) $$
    • 其中 \(V_\phi\) 是由神经网络拟合的价值函数(Critic)
    • 注:多步 的 TD Error 为多个 步骤真实奖励 \(r_t + r_{t+1}\) 的情况
  • GAE 的计算公式为:
    $$ \hat{A}_t = \sum_{l=0}^{\infty} (\gamma \lambda)^l \delta_{t+l} $$
  • 如果作者的价值网络 \(V_\phi\) 训练得非常完美,即
    $$V_\phi(s) = V^\pi(s)$$
  • 那么 TD 误差的期望
    $$ \mathbb{E}_[\delta_t] = 0$$
  • 进而可以推导出 GAE 的期望
    $$ \mathbb{E}_[\hat{A}_t] = 0 $$
为什么实际训练中均值不绝对等于 0?
  • 如果在训练时打印未归一化的 Advantage 均值,会发现它并不总是 0,主要原因有以下三点
  • 价值网络的拟合误差 :
    • 在训练初期,或者环境奖励发生剧烈变化时,神经网络 \(V_\phi(s)\) 无法完美拟合真实的 \(V^\pi(s)\),一般默认神经网络的值初始化为 0 左右(不一定严格为 0,但初始化策略决定了这个初始值的绝对值一般不会太大(观察到的也是这个结论))
      • 如果环境主要给予正奖励,且 Critic 还未收敛 ,即 \(V_\phi\) 初始化较小或更新滞后,会导致整体低估了状态价值,此时 \(\delta_t\) 偏正,Advantage 的均值会 > 0
      • 如果环境主要给予负奖励,且 Critic 还未收敛 ,即网络未收敛时会高估了状态价值,Advantage 的均值会 < 0
      • 注:以上这个结论既可以通过数学推导严格得到,也可以从 PPO 训练的初期开始观察得到(事实:当 环境主要给予正奖励时,初期 Critic 还未收敛时,Advantage 是从正值逐步下降的)
    • 注:理论上,在只有最后一步有奖励的场景中,Critic 的收敛是从后向前的,此时虽然前面的非终止 Step \(r_t=0\),但 \(V(s_{t+1})\) 本身 收敛比 \(V(s_{t})\) 快,也会导致与上述结论相同的结果
      • 如果环境主要给予正奖励,且 Critic 还未收敛
        $$ V(s_{t+1}) - V(s_{t}) > 0$$
      • 如果环境主要给予负奖励,且 Critic 还未收敛
        $$ V(s_{t+1}) - V(s_{t}) < 0$$
  • 有限样本的采样方差 :
    • PPO 是基于一个固定大小的 Batch 来计算经验均值的,有限样本的经验均值必然会偏离其理论期望 0
  • Actor-Critic 的非平稳性(Critic 的滞后性) :
    • Actor 在不断更新,导致真实的状态价值 \(V^\pi(s)\) 也在不断变化
    • 而 Critic 的拟合永远是在“追赶”策略的变化,这种滞后性打破了 \(\mathbb{E}_[\delta_t] = 0\) 的理想条件
为什么需要 Advantage Normalization
  • 一方面,正因为未处理的 Advantage 均值只是在 0 左右波动,Advantage Normalization 在工程上强行满足了“均值为 0” 的理论性质
  • 另一方面(最重要的方面),Advantage 的方差可能很大,所以 PPO 的标准实现(如 Baselines 项目)中通常会强制进行 Advantage Normalization :
    $$ \hat{A}_{norm} = \frac{\hat{A} - \mu_{\hat{A} } }{\sigma_{\hat{A} } + \epsilon} $$
    • 这样做能统一梯度的尺度,极大地稳定了策略网络的更新过程
    • 方差太大不容易调参,影响参数的迁移性(比如学习率等)

State Normalization

  • 对状态做归一化

  • State Normalization 的核心在于,与环境交互的过程中,维护一个动态的关于所有经历过的所有 State 的 Mean 和 Std, 然后对当前的获得的 State 做normalization

  • 经过 Normalization 后的 State 符合 Mean=0,Std=1 的正态分布,用这样的状态作为神经网络的输入,更有利于神经网络的训练

  • 采用滑动增量更新的方式(详细证明见附录):

    • 均值:\(\mu_{\text{new}} = \mu_{\text{old}} + \frac{1}{n}(x-\mu_{\text{old}})\)
    • 方差中间变量:\(S_{\text{new}} = S_{\text{old}} + (x-\mu_{\text{old}})\cdot(x-\mu_{\text{new}})\)
      • 注意这个值除以 \(n\) 才是方差
  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class RunningMeanStd:
    # Dynamically calculate mean and std
    def __init__(self, shape): # shape:the dimension of input data
    self.n = 0
    self.mean = np.zeros(shape)
    self.S = np.zeros(shape)
    self.std = np.sqrt(self.S)

    def update(self, x):
    x = np.array(x)
    self.n += 1
    if self.n == 1:
    self.mean = x
    self.std = x
    else:
    old_mean = self.mean.copy()
    self.mean = old_mean + (x - old_mean) / self.n
    self.S = self.S + (x - old_mean) * (x - self.mean)
    self.std = np.sqrt(self.S / self.n )

    class Normalization:
    def __init__(self, shape):
    self.running_ms = RunningMeanStd(shape=shape)

    def __call__(self, x, update=True):
    # Whether to update the mean and std,during the evaluating,update=Flase
    if update:
    self.running_ms.update(x)
    x = (x - self.running_ms.mean) / (self.running_ms.std + 1e-8)

    return x
  • 在模型实现时,状态归一化这个函数是添加到策略网络和状态网络层的输入端的,实现 Demo 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def forward(self, x):
    if config.use_state_norm:
    norm_x = []
    for i in range(x.size(0)): # 逐个动作归一化
    norm_x.append(torch.tensor(self.state_norm(x[i]), dtype=torch.float32))
    x = torch.stack(norm_x, dim=0)
    x = F.relu(self.fc1(x))
    x = F.relu(self.fc2(x))
    return self.fc3(x)

Reward Normalization

  • 与 State Normalization 的方案,动态维护所有获得过的 Reward 的 Mean 和 Std,然后再对当前的 Reward 做 Normalization
  • 问题:是对单次 Reward 做了 Normalization 吗?如果 Reward 已经做了 Normalization,GAE 中实际上就已经使用了标准化后的 Reward 了,Advantage 是否还需要做呢?
  • 常常被替换为 Reward Scaling
  • 虽然 Advantage 不用滑动平均,但 Reward Normalization 通常是使用滑动平均的
    • Reward Normalization :
      • 目的是让环境反馈的 Reward 尺度统一(比如不管是股票涨跌的金额,还是游戏得分,都缩放到 1 左右)
      • 因为环境的物理属性是不变的,所以用滑动平均(Running Mean/Std)来估计环境 Reward 的全局统计特性是合理的
    • Advantage Normalization :是对“优势”的归一化,是策略更新内部的一个数值稳定技巧,只关注当前批次

Reward Scaling

  • 相关论文:PPO-Implementation matters in deep policy gradients A case study on PPO and TRPO
  • Reward Scaling 与 Reward Normalization 的区别在于,Reward Scaling 是动态计算一个 standard deviation of a rolling discounted sum of the rewards,然后只对当前的 reward 除以这个 std(不减去均值?)
  • Reward Normalization 和 Reward Scaling 二选一即可,建议使用 Reward Scaling 而不是 Reward Normalization 即可
  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class RewardScaling:
    def __init__(self, shape, gamma):
    self.shape = shape # reward shape=1
    self.gamma = gamma # discount factor
    self.running_ms = RunningMeanStd(shape=self.shape)
    self.R = np.zeros(self.shape)

    def __call__(self, x):
    self.R = self.gamma * self.R + x
    self.running_ms.update(self.R)
    x = x / (self.running_ms.std + 1e-8) # Only divided std
    return x

    def reset(self): # When an episode is done,we should reset 'self.R'
    self.R = np.zeros(self.shape)

Policy Entropy(Entropy Bonus)

  • 在 Actor 的 Loss 中增加一项策略熵,在最大化收益的同时,最大化策略熵,增加探索性(理解:同时有正则的作用)
  • 但是增加以后会新增加一个新的超参数,且模型对该参数很敏感

Learning Rate Decay

  • 学习率逐步衰减
  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    def lr_decay(self, total_steps):
    lr_a_now = self.lr_a * (1 - total_steps / self.max_train_steps)
    lr_c_now = self.lr_c * (1 - total_steps / self.max_train_steps)
    for p in self.optimizer_actor.param_groups:
    p['lr'] = lr_a_now
    for p in self.optimizer_critic.param_groups:
    p['lr'] = lr_c_now

Gradient Clip

  • 梯度裁剪
  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Update actor
    self.optimizer_actor.zero_grad()
    actor_loss.mean().backward()
    if self.use_grad_clip: # Trick 7: Gradient clip
    torch.nn.utils.clip_grad_norm_(self.actor.parameters(), 0.5)
    self.optimizer_actor.step()

    # Update critic
    self.optimizer_critic.zero_grad()
    critic_loss.backward()
    if self.use_grad_clip: # Trick 7: Gradient clip
    torch.nn.utils.clip_grad_norm_(self.critic.parameters(), 0.5)
    self.optimizer_critic.step()

Orthogonal Initialization

  • 正交初始化(Orthogonal Initialization)是为了防止在训练开始时出现梯度消失、梯度爆炸等问题所提出的一种神经网络初始化方式。具体的方法分为两步:
    • 用均值为 0,标准差为1的高斯分布初始化权重矩阵
    • 对这个权重矩阵进行奇异值分解,得到两个正交矩阵,取其中之一作为该层神经网络的权重矩阵
  • 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    # orthogonal init
    def orthogonal_init(layer, gain=1.0):
    nn.init.orthogonal_(layer.weight, gain=gain)
    nn.init.constant_(layer.bias, 0)

    class Actor_Gaussian(nn.Module):
    def __init__(self, args):
    super(Actor_Gaussian, self).__init__()
    self.max_action = args.max_action
    self.fc1 = nn.Linear(args.state_dim, args.hidden_width)
    self.fc2 = nn.Linear(args.hidden_width, args.hidden_width)
    self.mean_layer = nn.Linear(args.hidden_width, args.action_dim)
    self.log_std = nn.Parameter(torch.zeros(1, args.action_dim)) # We use 'nn.Paremeter' to train log_std automatically
    if args.use_orthogonal_init:
    print("------use_orthogonal_init------")
    orthogonal_init(self.fc1)
    orthogonal_init(self.fc2)
    orthogonal_init(self.mean_layer, gain=0.01)

    def forward(self, s):
    s = torch.tanh(self.fc1(s))
    s = torch.tanh(self.fc2(s))
    mean = self.max_action * torch.tanh(self.mean_layer(s)) # [-1,1]->[-max_action,max_action]
    return mean

    def get_dist(self, s):
    mean = self.forward(s)
    log_std = self.log_std.expand_as(mean) # To make 'log_std' have the same dimension as 'mean'
    std = torch.exp(log_std) # The reason we train the 'log_std' is to ensure std=exp(log_std)>0
    dist = Normal(mean, std) # Get the Gaussian distribution
    return dist

    class Critic(nn.Module):
    def __init__(self, args):
    super(Critic, self).__init__()
    self.fc1 = nn.Linear(args.state_dim, args.hidden_width)
    self.fc2 = nn.Linear(args.hidden_width, args.hidden_width)
    self.fc3 = nn.Linear(args.hidden_width, 1)
    if args.use_orthogonal_init:
    print("------use_orthogonal_init------")
    orthogonal_init(self.fc1)
    orthogonal_init(self.fc2)
    orthogonal_init(self.fc3)

    def forward(self, s):
    s = torch.tanh(self.fc1(s))
    s = torch.tanh(self.fc2(s))
    v_s = self.fc3(s)
    return v_s

Adam Optimizer Epsilon Parameter

  • 实践中,从官方默认值 1e-8 改成 1e-5
  • 原因?

Tanh Activation Function

  • 将 ReLU 换成 tanh 激活函数
  • 建议 PPO 算法默认使用激活函数
  • 原因?

Value Clipping

  • 核心目标:为了训练的稳定性,对价值进行裁剪

  • 实现方式1:Stable Baselines3 的 PPO 实现中有如下代码 /ppo/ppo.py#L234-L244

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if self.clip_range_vf is None:
    # No clipping
    values_pred = values
    else:
    # Clip the difference between old and new value
    # NOTE: this depends on the reward scaling
    values_pred = rollout_data.old_values + th.clamp(
    values - rollout_data.old_values, -clip_range_vf, clip_range_vf
    )
    # Value loss using the TD(gae_lambda) target
    value_loss = F.mse_loss(rollout_data.returns, values_pred)
    • 从代码可以看出,Stable Baselines3 认为只要是当前价值网络的预估值超过旧网络预估值的情况,都是异常情况,则当前样本不置信,将当前样本的损失设置为固定值(通过Clip实现,此时该样本对价值网络参数的梯度为0,相当于丢弃样本了)
  • 实现方式2:一些地方也看到过类似表达(最早是在RLHF中看到的)

    1
    2
    3
    4
    5
    6
    def critic_loss_fn(self, values, old_values, returns, mask):
    values_clipped = torch.clamp(values, old_values - clip_range_vf, old_values + clip_range_vf)
    vf_loss1 = (values - returns) ** 2
    vf_loss2 = (values_clipped - returns) ** 2
    vf_loss = torch.mean(torch.max(vf_loss1, vf_loss2) * mask) / mask.sum()
    return vf_loss
    • 忽略上面的 mask 部分,是语言模型训来特有的
    • 这种实现的基本思路是:如果 Clip 后的价值与真实值的误差(MSE)大于当前策略,则使用 Clip 后的,此时该样本的 Loss 对价值网络参数的梯度为0,相当于丢弃样本了
    • 理解 max 操作:当策略误差已经比 Clip 后的值对应的误差还要小了,就不要使用该误差了(也就是说当前样本的误差相对上一轮已经小了一定大小了,不需要继续更新了,这种做法称为保守更新),只有 MSE 误差相对原始价值网络对应的误差变小时,才丢弃样本

附录-均值方差滑动更新公式证明

均值更新推导

  • 均值更新推导详情:
    $$
    \begin{align}
    \mu_n &= \frac{1}{n}\sum_{i=1}^n x_i \\
    &= \frac{1}{n}(\sum_{i=1}^{n-1} x_i + x_n) \\
    &= \frac{1}{n}(\sum_{i=1}^{n-1} x_i + x_n) \\
    &= \frac{1}{n}((n-1)\cdot\frac{1}{n-1}\sum_{i=1}^{n-1} x_i + x_n) \\
    &= \frac{1}{n}((n-1)\mu_{n-1} + x_n) \\
    &= \frac{1}{n}(n\mu_{n-1} + x_n - \mu_{n-1}) \\
    &= \mu_{n-1} + \frac{1}{n}(x_n - \mu_{n-1})
    \end{align}
    $$

方差更新推导

  • 将方差中间变量 \(S_n\) 展开(这里 \(S_n\) 除以n才是方差)有:
    $$
    \begin{align}
    S_n &= \sum_{i=1}^{n} (x_i - \mu_n)^2 \\
    &= \sum_{i=1}^{n-1} (x_i - \mu_n)^2 + (x_n - \mu_n)^2 \\
    &= \sum_{i=1}^{n-1} (x_i - \mu_{n-1} + \mu_{n-1} - \mu_n)^2 + (x_n - \mu_n)^2 \\
    &= \sum_{i=1}^{n-1} (x_i - \mu_{n-1})^2 + 2(\mu_{n-1} - \mu_{n})\sum_{i=1}^{n-1}(x_i - \mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
    &= S_{n-1} + 2(\mu_{n-1} - \mu_{n})(\sum_{i=1}^{n-1} x_i - (n-1)\mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
    &= S_{n-1} + 2(\mu_{n-1} - \mu_{n})((n-1)\mu_{n-1} - (n-1)\mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
    &= S_{n-1} + 2(\mu_{n-1} - \mu_{n})\cdot 0 + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (n-1)(\mu_{n-1} -\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
    &= S_{n-1} + ((n-1)\mu_{n-1} -(n-1)\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (n\mu_{n} - x_n -(n-1)\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (\mu_{n} - x_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (x_n - \mu_{n})(\mu_n - \mu_{n-1}) + (x_n - \mu_n)^2 \\
    &= S_{n-1} + (x_n - \mu_{n})(\mu_n - \mu_{n-1} + x_n - \mu_n)\\
    &= S_{n-1} + (x_n - \mu_{n})(x_n - \mu_{n-1})\\
    \end{align}
    $$
  • 最终有: \(S_n = S_{n+1} + (x_n - \mu_{n})(x_n - \mu_{n-1})\)

DPPO(Distributed PPO)

  • DPPO是PPO的分布式版本,引入了分布式计算的概念,允许多个计算节点(或智能体)并行地与环境交互,收集数据,并将这些数据用于更新全局模型
  • 分布式架构不仅加快了数据收集的速度,还提高了算法处理大规模并行任务的能力,使得学习过程更加高效

附录:一次采样仅更新一次的 PPO 讨论

  • 问题 :一次采样仅更新一次的 PPO 下,此时是 on-policy 的场景,为什么损失函数看起来和普通的 PG 不相等?(注:可以回顾一下,普通 PG 的更新公式是严格按照 on-policy 更新推导的,二者理应相等)
  • 回答 :其实此时两者的更新公式是一致的
  • 补充说明 :一次采样仅更新一次的 PPO 场景下,此时旧策略采样的样本仅更新一次模型即丢弃(即 epoch=1,且一次更新完所有参数,batch_size 足够大),且立刻会将新策略的更新同步到旧策略上,保证每次更新模型前新旧策略完全一致

普通策略梯度更新公式

  • 普通策略梯度的更新为:
    $$\theta \leftarrow \theta + \alpha \nabla_\theta \log \pi_\theta(a_t|s_t) G_t^n$$
  • 以上梯度更新对应的损失函数为:
    $$ Loss(\theta) = - \log \pi_\theta(a_t|s_t) G_t^n $$
  • 进一步求导有:
    $$\nabla_\theta Loss(\theta) = - \frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_\theta(a_t|s_t)} G_t^n$$
  • 注意,以上更新使用的 \(s_t,a_t,G_t^n\) 等均来源于当前策略,由于此时有 \(\pi_\theta = \pi_{\theta_\text{old}}\),所以上面的更新公式也可以写成
    $$\nabla_\theta Loss(\theta) = - \color{blue}{\frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_{\theta_\text{old}}(a_t|s_t)}} \color{red}{G_t^n}$$

PPO 简化后的更新公式

  • 一次采样仅更新一次的PPO,其简化后的损失函数为:
    $$ Loss(\theta) = - \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)\right)$$
  • 将期望形式转换为采样后有:
    $$ Loss(\theta) = - \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)$$
  • 进一步求导有:
    $$ \nabla_\theta Loss(\theta) = - \color{blue}{\frac{\nabla_\theta \pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}}\color{red}{A_{\theta_{\text{old}}}(s,a)}$$
    • 注:此时有 \(\pi_\theta = \pi_{\theta_\text{old}}\)

总体对比

  • 总结来看,针对一次采样一次更新的 PPO,简化后的更新公式与普通 PG 完全相同,更新使用的样本 \((s,a)\) 也都是从当前策略采样的(即 on-policy 场景),两者的唯一区别是梯度权重选择:
    • 简化 PPO:基于当前策略采样得到的 GAE 估计 \(A_{\theta_{\text{old}}}(s,a)\)
    • 普通 PG:基于当前策略采样的蒙特卡罗收益 \(G_t^n\),除了 REINFORCE 外,其他的方法实际也常用Q函数或者优势函数 A 等估计值来替代,故简化后的 PPO 实际上可以看做是梯度权重是 GAE 的普通 PG 算法

附录:Dual-Clip PPO

Dual-Clip PPO 核心方法总结

  • 原始论文链接:(Dual-Clip PPO)Mastering Complex Control in MOBA Games with Deep Reinforcement Learning, 2020 AAAI, Tencent
  • Dual-Clip PPO 是腾讯 AI Lab 为解决 MOBA 1v1 游戏(如《王者荣耀》)中深度强化学习训练难题而提出的 PPO(Proximal Policy Optimization)改进算法 ,核心目标是在大规模离线训练场景下,解决传统 PPO 因策略偏差过大导致的收敛不稳定问题,适配 MOBA 游戏庞大的状态空间(约 \(10^{600}\))与动作空间(约 \(10^{18000}\))
  • Dual-Clip PPO 跟前面章节中提到的 Visualize the Clipped Surrogate Objective Function 本质是一个事情,这里借助腾讯的文章,进行一些更详细的说明和探讨
    • 注:该博客引用了另一篇文章 Towards Delivering a Coherent Self-Contained Explanation of Proximal Policy Optimization, 20210815 的最早时间晚于 腾讯的论文之后

设计背景:传统 PPO 的局限性

  • 在 MOBA 1v1 游戏的大规模分布式训练中,传统 PPO 面临两大核心问题:
    • 策略偏差过大 :训练数据来自多源历史策略(如不同训练阶段的 AI 自对弈轨迹),这些轨迹与当前训练的目标策略 \(\pi_{\theta}\) 差异显著,导致概率比 \(r_t(\theta) = \frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old} }(a_t|s_t)}\) 可能异常巨大
    • 方差失控 :当优势函数估计值 \(\hat{A}_t < 0\)(即动作 \(a_t\) 为“劣势动作”)时,巨大的 \(r_t(\theta)\) 会使 \(r_t(\theta) \cdot \hat{A}_t\) 产生无界负向偏差,导致策略更新方向混乱,训练难以收敛

核心改进逻辑:双重裁剪机制

  • 传统 PPO :仅通过单一裁剪(Clip)限制概率比 \(r_t(\theta)\) 的范围(\(1-\epsilon \leq r_t(\theta) \leq 1+\epsilon\)),以避免策略更新幅度过大;
  • Dual-Clip PPO :在传统 PPO 的基础上,针对 \(\hat{A}_t < 0\) 的场景增加第二重裁剪 ,通过下界限制 \(r_t(\theta) \cdot \hat{A}_t\) 的负向偏差,具体逻辑如下:
    • 第一重裁剪(继承传统 PPO) :对概率比 \(r_t(\theta)\) 进行范围约束,确保策略更新“贴近”历史策略,避免突变:
      \(clip(r_t(\theta), 1-\epsilon, 1+\epsilon)\),其中 \(\epsilon\) 为超参数(实验中设为 0.2)
    • 第二重裁剪(新增) :当 \(\hat{A}_t < 0\) 时,对 \(r_t(\theta) \cdot \hat{A}_t\) 增加下界 \(c \cdot \hat{A}_t\)(\(c > 1\) 为超参数,实验中设为 3),限制其负向偏差的最大值
  • 这一改进的核心逻辑是:即使 \(r_t(\theta)\) 异常大,“劣势动作”的损失也不会无限制减小,避免策略被极端样本误导
    • 但这也限制了超过 reference 组过高的动作概率朝下更新,使用软性的梯度缩放可能会更好(即降低这个比例,同时保持梯度继续更新)

数学表达:目标函数定义

  • Dual-Clip PPO 的核心是优化以下目标函数,通过“先 min 后 max”的双重裁剪实现稳定更新:
  • 当 \(\hat{A}_t < 0\) 时,目标函数为:
    $$
    \hat{\mathbb{E} }_t\left[ \max\left( \min\left( r_t(\theta) \cdot \hat{A}_t,\ clip(r_t(\theta), 1-\epsilon, 1+\epsilon) \cdot \hat{A}_t \right),\ c \cdot \hat{A}_t \right) \right]
    $$
    • \(\hat{\mathbb{E} }_t\):对批量样本的经验期望;
    • \(r_t(\theta)\):当前策略与历史策略的概率比;
    • \(\hat{A}_t\):优势函数估计值(衡量动作 \(a_t\) 相对平均水平的优劣);
    • \(\epsilon = 0.2\)、\(c = 3\):实验验证的最优超参数,平衡探索与收敛

关键优势:适配 MOBA 训练需求

  • 注:Dual-Clip PPO 针对 MOBA 1v1 游戏的训练特性,解决了传统算法的核心痛点,论文中提到的以下具体优势主要针对这个场景
  • 保证收敛稳定性 :通过第二重裁剪限制负向偏差,即使在多源离线数据(如百万级 CPU 生成的自对弈轨迹)场景下,也能避免策略更新“失控”,实验中 AI 训练 80 小时后 Elo 评分趋于稳定(达到职业选手水平)
  • 适配大规模分布式训练 :支持“离线数据生成-在线模型训练”解耦的系统架构(如 60 万 CPU 生成样本、1064 块 GPU 训练),无需依赖在线实时采样,大幅提升训练效率(单 GPU 每秒处理 8 万样本)
  • 兼容多标签动作 decoupling :MOBA 游戏的动作需拆解为“按钮类型(如技能/移动)+ 目标单位(如敌方英雄/小兵)”等独立标签,Dual-Clip PPO 可与“控制依赖解耦”策略结合,对每个动作标签独立优化,同时保证整体策略收敛

实际效果展示

  • 在《王者荣耀》1v1 模式的实验中,Dual-Clip PPO 是 AI 击败职业选手的关键组件之一:
  • 与传统 PPO 相比,Dual-Clip PPO 使训练收敛时间缩短约 20% ,且 AI 在与职业选手的 BO5 对战中胜率达 100%(如法师英雄貂蝉 3:0 击败联赛顶尖法师选手);
  • 结合“动作掩码(Action Mask)”“目标注意力(Target Attention)”等策略后,AI 在 2100 场公开对战中胜率达 99.81%,验证了其在复杂控制场景下的有效性

思考

  • 适用于 off-policy 场景,且需要较为严重的 off-policy 才需要
  • 改成保留梯度的截断方式理论会更合适,既可以避免异常点,又可以保证已经过高的动作概率得以被惩罚
    • 注:截止到 2025 年,快手发布的 OneRec 也提出了一种方法 Early Clipped GRPO, 其中用到的方法就是本文提到的保留梯度的截断方式

RL——PPO论文精读

本文是 PPO 的论文精读,经典值的多次回味

  • 参考链接:
    • 原始论文:Proximal Policy Optimization Algorithms, OpenAI, 2017
    • 博主的其他解读:RL——PPO

Paper Summary

  • 近端策略优化(PPO)是一类新的 Policy Gradient 方法(Actor 与环境中交互采样数据,并使用随机梯度上升优化 surrogate 目标函数,交替进行)
  • PPO 兼具 TRPO 的优势,但相对 TRPO,实现更简单、更通用,且样本复杂度更低
  • 标准的 Policy Gradient 方法对每个数据样本执行一次梯度更新,而论文提出了一种新的目标函数 ,支持对 minibatch 数据进行多轮更新
    • 注:论文的核心创新点在于这个新的目标函数可以对数据进行多轮更新(使用重要性采样实现类似 off-policy 的更新)
  • 在多个基准任务上测试了 PPO,包括模拟机器人运动和 Atari 游戏,结果表明 PPO 优于其他 online Policy Gradient 方法;
    • 在样本复杂度、实现简易性和训练时间之间取得了良好的平衡
  • 评价:
    • 在大部分场景下,PPO 都是值得最初尝试的 Online RL 方法,简洁但非常有效的方法,OpenAI 确实有实力
    • 新增补充:LLM 时代来临以后,PPO 已经成为了 RLHF 的默认方法,OpenAI 的含金量还在上升

Policy Optimization

  • 目前针对神经网络函数逼近的 RL 主要的方法包括:
    • Deep Q-Learning
    • “vanilla” Policy Gradient 方法
    • 信任域/自然策略梯度(trust region / natural policy gradient)方法
      • TRPO与自然策略梯度的关系简单讨论见:RL——自然策略梯度法
  • 现有方法在可扩展性(适用于大型模型和并行实现)、数据效率和鲁棒性(即无需调参即可适用于多种问题)方面仍有改进空间
    • Q-Learning(带函数逼近)在许多简单问题上表现不佳且理论理解不足
    • vanilla Policy Gradient 方法的数据效率和鲁棒性较差,而 TRPO 实现复杂,且不兼容包含噪声(如dropout)或参数共享(如策略与值函数共享参数或辅助任务)的架构
  • 论文旨在通过提出一种新算法来改进现状,该算法在保持 TRPO 数据效率和可靠性能的同时,仅需一阶优化(注意:TRPO 是包含二阶求导的,所以性能好但很慢)
  • 论文提出了一种基于裁剪概率比(clipped probability ratios) 的新目标函数,该函数对策略性能形成悲观估计(即下界)
  • 策略优化:交替执行 1)从策略中采样数据;2)在同一批采样数据上进行多轮优化
  • 实验内容:
    • 比较不同代理目标函数的性能 :发现基于 Clip 版本表现最佳
    • 比较 PPO 与文献中的其他算法:在连续控制任务中,PPO 优于其他对比算法;在 Atari 游戏中,其样本复杂度显著优于 A2C,与 ACER 相当,但实现更简单
      • 问题:如何理解这里的样本复杂度?
      • 回答:需要采样的样本数,从文章附录图 6 可以看到,收敛速度远远快与 A2C

Policy Optimization

Policy Gradient Methods

  • Policy Gradient 方法通过计算 Policy Gradient 的估计值,并将其代入随机梯度上升算法中实现优化。最常用的梯度估计器形式为:
    $$
    \hat{g} = \hat{\mathbb{E} }_{t}\left[\nabla_{\theta}\log\pi_{\theta}(a_{t} \mid s_{t})\hat{A}_{t}\right]
    $$
    • \(\pi_{\theta}\) 是随机策略
    • \(\hat{A}_{t}\) 是时间步 \(t\) 的优势函数估计值
    • 这里的期望 \(\hat{\mathbb{E} }_{t}[\dots]\) 表示在有限样本批次上的经验平均,算法交替进行采样和优化
    • 使用自动微分软件的实现通过构造目标函数(其梯度为 Policy Gradient 估计值)实现优化;
    • 梯度估计值 \(\hat{g}\) 通过对以下目标函数求导得到:
      $$
      L^{PG}(\theta) = \hat{\mathbb{E} }_{t}\left[\log\pi_{\theta}(a_{t} \mid s_{t})\hat{A}_{t}\right].
      $$
  • 虽然可以基于同一轨迹对损失 \(L^{PG}\) 进行多步优化,但这样做缺乏理论依据,且经验上常导致策略更新过大(见原始论文第6.1节;结果未展示,但与“无裁剪或惩罚”设置相似或更差)
    • 理解:这里第一次更新是 on-policy,第二次更新则是 off-policy,若要继续更新应该是需要重要性采样实现

信任域方法(Trust Region Methods)

  • 在 TRPO 中,目标函数(surrogate 目标)在策略更新大小的约束下最大化,具体形式为:
    $$
    \text{maximize} \quad \hat{\mathbb{E} }_{t}\left[\frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\theta_{\text{old} } }(a_{t} \mid s_{t})}\hat{A}_{t}\right] \\
    \hat{\mathbb{E} }_{t}\left[\text{KL}\left[\pi_{\theta_{\text{old} } }(\cdot \mid s_{t}), \pi_{\theta}(\cdot \mid s_{t})\right]\right] \leq \delta. \tag{3 & 4}
    $$
    • 其中, \(\theta_{\text{old} }\) 是更新前的策略参数向量
    • 通过对目标函数进行线性近似、对约束进行二次近似,可以高效地使用共轭梯度算法近似求解该问题
  • TRPO的理论支持使用惩罚项而非约束,即求解以下无约束优化问题:
    $$
    \underset{\theta}{\text{maximize} } \quad \hat{\mathbb{E} }_{t}\left[\frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\theta_{\text{old} } }(a_{t} \mid s_{t})}\hat{A}_{t} - \beta \text{KL}\left[\pi_{\theta_{\text{old} } }(\cdot \mid s_{t}), \pi_{\theta}(\cdot \mid s_{t})\right]\right] \tag{5}
    $$
    • 其中, \(\beta\) 为系数
    • 这是因为某些代理目标(计算状态上的最大KL而非均值)形成了策略 \(\pi\) 性能的下界(即悲观界)
    • TRPO 使用硬约束而非惩罚项,因为很难选择一个适用于不同问题(甚至同一问题中学习过程中特性变化)的 \(\beta\) 值
    • 因此,为了实现一阶算法模拟 TRPO 的单调改进目标,实验表明仅选择固定惩罚系数 \(\beta\) 并用 SGD 优化惩罚目标(公式5)是不够的,还需额外修改

裁剪代理目标(Clipped Surrogate Objective)

  • 设 \(r_{t}(\theta)\) 表示概率比:
    $$r_{t}(\theta) = \frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\theta_{\text{old} } }(a_{t} \mid s_{t})}$$
    • 特别地:
      $$r(\theta_{\text{old} }) = 1$$
  • TRPO 最大化以下 surrogate 目标:
    $$
    L^{CPI}(\theta) = \hat{\mathbb{E} }_{t}\left[\frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\theta_{\text{old} } }(a_{t} \mid s_{t})}\hat{A}_{t}\right] = \hat{\mathbb{E} }_{t}\left[r_{t}(\theta)\hat{A}_{t}\right].
    $$
    • 上标 \(CPI\) 指代保守策略迭代,该目标函数首次在此提出
      • 理解:其实就是包含重要性采样的 Policy Gradient 损失函数版本
    • 若无约束,最大化 \(L^{CPI}\) 会导致策略更新过大,因此,作者考虑如何修改目标函数,以惩罚使 \(r_{t}(\theta)\) 偏离 1 的策略变化
  • 作者提出的主要目标函数如下:
    $$
    L^{CLIP}(\theta) = \hat{\mathbb{E} }_{t}\left[\min\left(r_{t}(\theta)\hat{A}_{t}, \color{red}{\text{clip}(r_{t}(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_{t}}\right)\right]
    $$
    • \(\epsilon\) 为超参数(例如 \(\epsilon = 0.2\) )
    • 该目标函数的动机如下:
      • min 内的第一项为 \(L^{CPI}\) ;第二项通过裁剪概率比修改代理目标,消除了 \(r_{t}\) 超出区间 \([1-\epsilon, 1+\epsilon]\) 的动机
      • 最后,论文对裁剪和未裁剪目标取最小值,因此最终目标是未裁剪目标的下界(即悲观界)
      • 通过此方案,论文仅在概率比变化使目标改善时忽略该变化,而在其使目标恶化时保留它
      • 注意,在 \(\theta_{\text{old} }\) 附近(即 \(r=1\) 时), \(L^{CLIP}(\theta)\) 与 \(L^{CPI}(\theta)\) 一阶等价,但随着 \(\theta\) 远离 \(\theta_{\text{old} }\) ,二者差异增大
  • 图1 绘制了 \(L^{CLIP}\) 中的单一项(即单个时间步 \(t\) )随概率比 \(r\) 的变化曲线
  • 图2 展示了代理目标 \(L^{CLIP}\) 的另一种直观解释,显示了在连续控制问题上沿策略更新方向插值时多个目标的变化情况。可以看出, \(L^{CLIP}\) 是 \(L^{CPI}\) 的下界,并对过大的策略更新施加惩罚
  • 吐槽:原始论文对该目标函数的解释不够清楚

Adaptive KL Penalty Coefficient

  • 另一种方法(可作为裁剪代理目标的替代或补充)是使用KL散度惩罚,并通过调整惩罚系数使得每次策略更新达到目标KL散度值 \(d_{\text{target} }\)
    • 注:实验发现 KL 惩罚的性能不如裁剪代理目标,但因其为重要基线,仍在此介绍
  • 该算法最简单的实现步骤如下:
    • 第一步:使用多轮 minibatch SGD 优化以下 KL 惩罚目标:
      $$
      L^{KLPEN}(\theta) = \hat{\mathbb{E} }_{t}\left[\frac{\pi_{\theta}(a_{t} \mid s_{t})}{\pi_{\theta_{\text{old} } }(a_{t} \mid s_{t})}\hat{A}_{t} - \beta \text{KL}\left[\pi_{\theta_{\text{old} } }(\cdot \mid s_{t}), \pi_{\theta}(\cdot \mid s_{t})\right]\right]
      $$
    • 第二步:计算 \(d = \hat{\mathbb{E} }_{t}\left[\text{KL}\left[\pi_{\theta_{\text{old} } }(\cdot \mid s_{t}), \pi_{\theta}(\cdot \mid s_{t})\right]\right]\) :
      • 若 \(d < d_{\text{target} }/1.5\) ,则 \(\beta \leftarrow \beta/2\)
      • 若 \(d > d_{\text{target} } \times 1.5\) ,则 \(\beta \leftarrow \beta \times 2\)
      • 更新后的 \(\beta\) 用于下一次策略更新
  • 实验效果:此方案偶尔会出现KL散度与 \(d_{\text{target} }\) 显著偏离的情况,但这种情况较少,且 \(\beta\) 会快速调整
  • 超参数设定和实验结论:
    • 参数 1.5 和 2 为启发式选择,但算法对其不敏感
    • 初始 \(\beta\) 值为另一超参数,但因算法快速调整,实际影响不大
  • 来自多年后的补充:
    • 注意 PPO 的 adaptive KL Penalty Coef 和 Clip 方法 都是 PPO 方法,都是 Trust Region 的思路,即都是约束当前策略到上一步的策略 \(\pi_\text{old}\) 上
    • 这跟当前 RLHF 中的方法约束当前策略到 Reference 策略 \(\pi_\text{ref}\) 上完全不同

Algorithm

  • 前几节介绍的代理损失(surrogate losses)可以通过对典型的 Policy Gradient 实现进行微小改动来计算和微分。对于使用自动微分的实现,只需构建损失函数 \( L^{CLIP} \) 或 \( L^{KLPEN} \) 来代替 \( L^{PG} \) ,并对此目标函数执行多步随机梯度上升
  • 大多数计算方差缩减的优势函数估计方法会利用学习到的状态价值函数 \( V(s) \) ;例如,广义优势估计(GAE),或有限时域估计器(finite-horizon estimators)
  • 如果使用在策略和价值函数之间共享参数的神经网络架构,则必须使用一个结合了策略替代项和价值函数误差项的损失函数。根据以往工作的建议,可以通过添加熵奖励来进一步增强此目标 ,以确保充分的探索。结合这些项,论文得到以下目标函数,每次迭代时(近似)最大化:
    $$
    L_{t}^{CLIP+VF+S}(\theta) = \hat{\mathbb{E} }_{t}\big[L_{t}^{CLIP}(\theta) - c_{1}L_{t}^{VF}(\theta) + c_{2}S[\pi_{\theta}] (s_t)\big],
    $$
    • \( c_{1}, c_{2} \) 是系数
    • \( S \) 表示熵奖励
    • \( L_{t}^{VF} \) 是平方误差损失 \( (V_{\theta}(s_{t}) - V_{t}^{\text{targ} })^{2} \)
  • 一种流行的 Policy Gradient 实现风格(特别适合与循环神经网络一起使用)让策略运行 \( T \) 个时间步(其中 \( T \) 远小于回合长度),并使用收集到的样本进行更新。这种风格需要一个不超出时间步 \( T \) 的优势估计器。Asynchronous methods for deep reinforcement learning中使用的估计器为:
    $$
    \hat{A}_{t} = -V(s_{t}) + r_{t} + \gamma r_{t+1} + \cdots + \gamma^{T-t+1}r_{T-1} + \gamma^{T-t}V(s_{T}),
    $$
    • 其中 \( t \) 指定了给定长度为 \( T \) 的轨迹段中的时间索引
    • 推广这一选择,我们可以使用截断的广义优势估计,当 \( \lambda=1 \) 时退化为式(10):
      $$
      \hat{A}_{t} = \delta_{t} + (\gamma\lambda)\delta_{t+1} + \cdots + (\gamma\lambda)^{T-t+1}\delta_{T-1},\\
      \text{where} \quad \delta_{t} = r_{t} + \gamma V(s_{t+1}) - V(s_{t}).
      $$
  • 使用固定长度轨迹段的近端策略优化(PPO)算法如下所示
    • 每次迭代中,\( N \) 个(并行)执行者各收集 \( T \) 个时间步的数据;然后基于这些 \( NT \) 个时间步的数据构建代理损失,并使用 minibatch SGD (或通常为了更好的性能使用Adam)优化它,进行 \( K \) 轮

Experiments

代理目标的比较

  • 论文在不同超参数下比较了几种代理目标

  • 论文将代理目标 \( L^{CLIP} \) 与几种自然变体和消融版本进行比较:

    • 无裁剪或惩罚(No clipping or penalty) :\( L_{t}(\theta) = r_{t}(\theta)\hat{A}_{t} \)
    • 裁剪(Clipping) :\( L_{t}(\theta) = \min(r_{t}(\theta)\hat{A}_{t}, \text{clip}(r_{t}(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_{t}) \)
    • KL penalty(fixed or adaptive) :\( L_{t}(\theta) = r_{t}(\theta)\hat{A}_{t} - \beta \text{KL}[\pi_{\theta_{\text{old} } }, \pi_{\theta}] \)
  • 对于KL惩罚,可以使用固定惩罚系数 \( \beta \) 或自适应系数(如第4节所述,目标KL值为 \( d_{\text{target} } \) ),论文还尝试了对数空间裁剪,但发现性能没有提升

    • 问题:这里是指将指数换成对数?
  • 由于论文需要为每个算法变体搜索超参数,因此选择了一个计算成本较低的基准测试:

    • 使用了 OpenAI Gym 中实现的 7 个模拟机器人任务,这些任务基于 MuJoCo 物理引擎;
    • 每个任务训练一百万个时间步
  • 除了裁剪参数 \( \epsilon \) 和KL惩罚参数 \( \beta, d_{\text{target} } \) 需要搜索外,其他超参数见表3(原文在附录中)

  • 论文使用了一个具有两个隐藏层(每层64个单元)的全连接 MLP 来表示策略,输出高斯分布的均值,具有可变标准差

  • 论文没有在策略和价值函数之间共享参数(因此系数 \( c_{1} \) 无关),也没有使用熵奖励

  • 每个算法在 7 个环境中各运行 3 次随机种子,每次运行的得分通过计算最后 100 回合的平均总奖励来确定

  • 论文对每个环境的得分进行了平移和缩放,使得随机策略得分为 0,最佳结果为 1,并在 21 次(\(7 \times 3\))运行中取平均,为每个算法设置生成一个标量得分

  • 结果如表1所示(注意,在没有裁剪或惩罚的设置中,得分为负,因为在某个环境(HalfCheetah)中得分非常低,甚至低于初始随机策略)

连续控制领域与其他算法的比较

  • 论文将PPO(使用 第3节 的“clipped”代理目标)与文献中几种其他方法进行比较(这些方法在连续控制问题上表现良好)
  • 论文比较了以下算法的调优实现:
    • 信任域策略优化(TRPO)
    • 交叉熵方法(CEM)
    • 自适应步长的普通 Policy Gradient
    • A2C:A2C代表 Advantage Actor Critic,是 A3C 的同步版本,论文发现其性能与异步版本相同或更好
    • 带信任域的 A2C
  • 对于PPO,论文使用上一节的超参数, \( \epsilon=0.2 \),结果如图3显示,PPO在几乎所有连续控制环境中都优于之前的方法

连续控制领域的展示:人形机器人跑步与转向(Humanoid Running and Steering)

  • 为了展示 PPO 在高维连续控制问题上的性能,论文在涉及 3D 人形机器人的任务上进行训练,机器人需要跑步、转向,甚至在被方块击中时从地面爬起
  • 论文测试的三个任务是:
    • (1) RoboschoolHumanoid:仅向前运动;
    • (2) RoboschoolHumanoidFlagrun:目标位置每200个时间步或达到目标时随机变化;
    • (3) RoboschoolHumanoidFlagrunHarder:机器人被方块击中并需要从地面爬起
  • 图5展示了学习策略的静态帧
  • 图4展示了三个任务的学习曲线
  • 超参数见表4,在并行工作中,Heess等人[]使用PPO的自适应KL变体(第4节)学习了3D机器人的运动策略

Atari领域与其他算法的比较

  • 论文还在 Arcade Learning Environment 基准测试上运行了 PPO,并与调优良好的 A2C 和 ACER 实现进行了比较
  • 对于所有三种算法,论文使用了与 Asynchronous methods for deep reinforcement learning 相同的策略网络架构
  • PPO的超参数见表5,对于其他两种算法,论文使用了针对此基准测试优化的超参数
  • 附录B提供了所有 49 款游戏的结果表和学习曲线
  • 论文考虑以下两个评分指标:
    • (1) 整个训练期间每回合的平均奖励(偏向快速学习);
    • (2) 训练最后100回合的平均奖励(偏向最终性能)
  • 表2显示了每种算法“获胜”的游戏数量,其中评分指标是三次试验的平均值

附录B:更多Atari游戏上的性能表现

  • 此处论文展示了 PPO 与 A2C 在 49 款 Atari 游戏上的对比结果
  • 图6:PPO与A2C在OpenAI Gym中所有49款Atari游戏上的对比(截至发表时),图6显示了三种随机种子的学习曲线
  • 表6:PPO与A2C在Atari游戏上的平均最终得分(最后100回合,40M游戏帧后),表6列出了平均性能

RL——离线强化学习整体介绍

  • 参考文献:
    • 综述1:A Survey on Offline Reinforcement Learning: Taxonomy, Review, and Open Problems(2022,最新更新2024年8月,持续更新)
    • 综述2:Offline Reinforcement Learning: Tutorial, Review, and Perspectives on Open Problems(2020)
    • 离线强化学习(A Survey on Offline Reinforcement Learning)

离线强化学习是什么?

  • 一句话描述:不与环境进行交互,只在固定数据集上进行策略学习的强化学习,也称为 Offline RL 或 Batch RL,自2020年以后基本都叫做Offline RL
  • 直接将off-policy方法使用到Offline RL场景会面临的核心问题是外推误差 ,外推误差(Extrapolation Error)的定义:off-policy值学习中,当前策略真实状态动作访问分布和数据集中的状态动作分布不匹配导致的一种误差,具体来说,包括Absent Data(状态动作对缺失),Training Mismatch(训练预测分布不一致),Model Bias(随机MDP的状态转移概率有偏差)等问题

离线强化学习的优缺点

  • 优点:
    • 应用安全:不需要与真实环境交互,能防止未知风险;
    • 训练高效:样本利用率高;
  • 缺点:
    • 外推误差:容易面临外推误差问题,导致策略学不好;
    • 样本限制:样本量多和行为策略探索性强的数据集较好,对于基于模仿学习的离线强化学习方法,一般需要行为策略是专家策略(本质也于外推误差相关,如果样本量太少,或者行为策略过于局限,都可能只收集到固定的某个局部状态或动作,加重外推误差问题)

离线强化学习数据集D4RL

  • 数据集原始论文:D4RL: Datasets for Deep Data-Driven Reinforcement Learning
  • D4RL的一些介绍:
    • D4RL: DATASETS FOR DEEP DATA-DRIVEN REINFORCEMENT LEARNING,知乎
    • offline RL | D4RL:最常用的 offline 数据集之一
  • 数据集安装:离线强化学习(Offline RL)系列2: (环境篇)D4RL数据集简介、安装及错误解决 - Jensen Wang的文章 - 知乎,直接安装D4RL会遇到一些问题,需要逐步解决
  • 数据集一般都会包含一些特性:
    • Narrow and Biased Data Distributions (NB) :少量且有偏数据集,即OOD问题可能很严重的数据
    • Undirected and Multitask Data (UM) :无方向和多任务数据,即当前收集到的数据不是为了解决问题而收集的。(理解:比如要出找到出口,但是行为策略不找出口而是随机开门关门打开柜子等操作)
    • Sparse Rewards (SR) :系数奖励,即奖励稀疏的场景
    • Suboptimal Data (SD) :次优数据,即使用次优策略收集的数据
    • Nonrepresentable Behavior Policies (NR) :不可表示的行为策略。(理解:当函数逼近器(function approximator)很难完全捕捉基础行为的复杂性时,策略难以被神经网络或其他函数逼近器表示出来)
    • Non-Markovian Behavior Policies (NM) :非马尔可夫行为策略,一般出现在人类Agent或者手动工程控制中?不遵循马尔可夫性?【TODO:待补充】
    • Realistic Domains (RD) :现实领域。即现实数据,不是模拟数据,可能会出现噪声等信息
    • Nonstationarity (NS) :非平稳性,即不稳定的MDP过程。在这个数据集中,Agent可能会遇到传感器故障、执行器退化或奖励函数更新的情况,导致随时间变化的MDP中的扰动(例如,泵的效率随时间下降)
  • D4RL和RL Unplugged环境

离线强化学习的分类

说明:目前没有非常官方的离线强化学习分类方法,本文的分类方法来源于论文A Survey on Offline Reinforcement Learning: Taxonomy, Review, and Open Problems(2022,最新更新2024年8月,持续更新)

  • 本文按照修改类型分类(Modification Types),即按照离线强化学习方法在标准多步AC(Multi-step Actor Critic)的基础上做了哪些修改(注:修改类型的对照基线模型是标准Multi-step AC方法),可以将修改类型分成以下几类
  • 各种修改类型的一些图示
  • 不同算法的修改类型归属(同一个算法可能同时使用多个修改类型)

Policy Constraints

  • 策略约束分为直接策略约束(Direct Policy Constriants)和隐式策略约束(Implicit Policy Constraints)两种
  • 直接策略约束方法 :学习策略 \(\pi_\theta\) 的同时估计行为策略 \(\pi_\beta\),同时限制策略 \(\pi_\theta\) 贴近行为策略 \(\pi_\beta\)
    $$
    \begin{align}
    J(\theta) &= \mathbb{E}_{s\sim d^{\pi_\theta}, a\sim\pi_\theta(\cdot\vert s)}[Q^{\pi}(s,a)] \\
    \text{s.t.} &\ D(\pi_\theta(\cdot\vert s), \hat{\pi}_\beta(\cdot\vert s)) \le \epsilon
    \end{align}
    $$
    • 其中D表示某个距离评估指标,一般常用KL散度
    • 需要直接评估行为策略,如果行为策略评估不准确,这种方式效果会比较差
  • 隐式策略约束方法 :不显示的估计行为策略 \(\pi_\beta\),只依赖于样本(行为策略 \(\pi_\beta\) 收集来的样本),通过修改目标函数来隐式的约束策略 \(\pi_\theta\),实现不评估行为策略 \(\pi_\beta\) 的同时对策略 \(\pi_\theta\) 施加约束
    • 首先通过策略提升推导得到如下目标函数:
      $$
      \begin{align}
      \mathcal{L}(\pi,\lambda) &= \mathbb{E}_{s\sim d^{\pi_\beta}(\cdot)}[\mathbb{E}_{ a\sim\pi(\cdot\vert s)}[\hat{A}^{\pi}(s,a)] + \lambda (\epsilon - D_{\text{KL}}(\pi(\cdot\vert s), \hat{\pi}_\beta(\cdot\vert s)))]
      \end{align}
      $$
    • 然后进一步求解优化问题的解如下:
      $$ \pi^*(a|s) = \pi_\beta(a|s)exp(\frac{1}{\lambda}\hat{A}^{\pi}(s,a)) $$
    • 进一步地,最小化 \(\pi_\theta\) 和 \(\pi^*\) 的KL散度,可以最终推导得到如下的目标函数:
      $$ J(\theta) = \mathbb{E}_{(s,a) \sim \mathcal{D}} \log \pi_\theta(a|s)exp(\frac{1}{\lambda}\hat{A}^{\pi}(s,a)) $$
    • 以上方式相当于一种加权极大似然估计(Weighted Maximum Likelihood),根据以上公式,我们不再需要显示的学习行为策略 \(\pi_\beta\),可以直接从 \((s,a)\) 中学习到最优策略
  • 策略约束有两种形式,一种是Distribution Matching Constraints(也称为Distribution Constraints),一种是Support Matching Constraints(也称为Support Constraints)
    • Distribution Constraints:指限制 \(\pi_\theta\) 和 \(\pi_\beta\) 这两个策略的分布足够接近
    • Support Constraints:不要求 \(\pi_\theta\) 和 \(\pi_\beta\) 这两个策略的分布足够接近,只需要限制从 \(\pi_\theta\) 采样的动作在 \(\pi_\beta\) 采样动作的支持集里面即可(支持集即 \(\pi_\beta(a|s)\) 分配了正概率的所有动作 \(a\) 的集合)

Importance Sampling

  • 通过重要性采样实现
    $$
    \mathbb{E}_{\tau\sim \pi_\beta}\Big[ w_{0:H}\sum_{t=0}^H\nabla_\theta\gamma^t \log \pi_\theta(a_t|s_t)\hat{Q}^{\pi}(s_t,a_t) \Big]
    $$
    • 其中 \(w_{0:H}\) 是重要性权重 \(w_t = \frac{\pi(a_t|s_t)}{\hat{\pi}_\beta(a_t|s_t)}\) 的乘积

Regularization

  • 基本思路是在优化目标上增加一个正则项,分为策略正则(Policy Regularization)和值正则(Values Regularization)
  • 策略正则(Policy Regularization)
    $$ J(\theta) = \mathbb{E}_{(s,a) \sim \mathcal{D}} [\hat{Q}^{\pi_\theta}(s_t,a_t)] + \mathcal{R}(\theta) $$
  • 值正则(Values Regularization)
    $$ J(\phi) = \mathbb{E}_{(s,a,s’) \sim \mathcal{D}}\Big[(r(s,a) + \gamma \mathbb{E}_{a’\sim\pi_{\text{off}}(\cdot|s)}[Q_\phi^\pi(s’,a’)] - Q_\phi^\pi(s,a))^2\Big] + \mathcal{R}(\phi) $$

Uncertainty Estimation

  • 基本思路是在保守(Conservative)RL和原始(Native)RL上做一些Trade off,基于我们对泛化能力的信任程度不同,可以选择不同程度的保守策略,对策略做不同程度的放松;这里的不确定性评估可以是对策略、值和模型的评估
    $$ J(\theta) = \mathbb{E}_{(s,a) \sim \mathcal{D}}\Big[ \mathbb{E}_{Q^\pi\sim\mathcal{P}_{\mathcal{D}}(\cdot)} [Q^\pi(s,a)] - \alpha U_{\mathcal{P}_{\mathcal{D}}} (\mathcal{P}_{\mathcal{D}}(\cdot)) \Big] $$
    • \(\mathcal{P}_{\mathcal{D}}(Q^\pi)\) 表示在数据集 \(\mathcal{D}\) 中,Q函数的分布
    • 其中 \(U_{\mathcal{P}_{\mathcal{D}}}(\cdot)\) 是对 \(\mathcal{P}_{\mathcal{D}}\) 的不确定性评估(这里的 \(\mathcal{P}_{\mathcal{D}}\) 是下角标)
      • 理解:在对数据集 \(\mathcal{D}\) 上对某个变量进行不确定性评估,这个变量也是与数据集相关的,所以 \(U_{\mathcal{P}_{\mathcal{D}}} (\mathcal{P}_{\mathcal{D}}(\cdot))\) 中数据集出现了两次

Model-based

  • 首先通过标准的监督学习回归方法构建模型预测状态转移概率 \(T_{\psi_T}(s_{t+1}|s_t,a_t)\) 和奖励函数 \(r_{\psi_r}(s_t,a_t)\)
  • 使用学习到的状态转移概率和奖励函数模型作为真实环境的一个代理,与策略交互
  • 在数据覆盖面广(探索足够充分)时,Model-based方法效果比较容易学习
  • Model-based方法也可以用到Online RL场景,在Offline RL场景中,由于无法与环境交互,所以无法修正一些错误的预估值,一种解决这个问题的方法是采用保守策略,类似于Uncertainty Estimation思想对奖励函数进行修正
    $$ \tilde{r}_{\psi_r}(s,a) = r_{\psi_r}(s,a) + \lambda U_r(s,a) $$
    • \(U_r(s,a)\) 表示不确定度,在数据集中出现过的 \((s,a)\),对应的不确定性较小,没有出现过的不确定性较大

One-step

  • 多步策略评估和策略提升出现OOD问题的原因是需要对策略 \(\pi_{\text{off}}\) 做策略评估,在选择动作评估目标Q值时,不可避免的容易出现 \(\pi_\beta\) 没有见过的动作
  • One-step方法的思想:先对策略 \(\pi_\beta\) 做策略评估得到准确的 \(Q^{\pi_\beta}(s,a)\),此时不会出现OOD,因为 \(a’\) 都是来自 \(\pi_\beta\) 的;接着从 \(Q^{\pi_\beta}(s,a)\) 中进行一步策略提取,找到最优策略
  • 在One-step的方法下,不需要担心OOD问题,因为我们从不需要访问OOD的状态动作对

Imitation Learning

  • 模仿学习是一类学习方法,这类方法的思路是通过模仿行为策略的行为来实现策略学习
  • 离线强化学习中的模仿学习常见的有行为克隆(Behavior Clone,BC)及其改进版本
  • BC方法要求行为策略是专家策略,BC的基本目标如下:
    $$ J(\theta) D(\pi_\theta(\cdot\vert s), \hat{\pi}_\beta(\cdot\vert s)) $$
    • \(D(\cdot,\cdot)\) 是f-divergence,可以是交叉熵等
    • 其他说明:行为策略本身比较优质时BC能学到较好的效果
  • 改进版本不要求行为策略是专家策略,常常通过丢弃劣质动作或者对高收益动作进行加权实现
    • 动作挑选 :通过Q值或者一些启发式方法识别劣质和优质的动作
    • 动作加权 :设计特殊的目标函数,让策略决策到高收益动作的概率更高,比如AWR的优化目标
    • 条件策略(Conditional Policy) :学习一个条件生成网络和条件策略网络,条件生成网络可在给定轨迹 \(\tau\) 下生成条件,条件策略网络会根据给定条件和状态来生成策略,该策略的目标就是使得决策能够最终生成给定轨迹 \(\tau\) ?【TODO:待补充】

Trajectory optimization

  • 直接建模状态动作的联合分布,即轨迹分布,然后在这些分布里面规划出优质的轨迹,轨迹里面就包含了动作
  • 给定任意的 \(s_0\),先试探性规划多步,找到最优轨迹,然后选择最优轨迹上的动作决策一步即可(注意,为了避免误差累计,一次规划仅进行一次决策)

Off-policy Evaluation (OPE)

  • 目标:给定待评估策略 \(\pi\) 和评估数据集 \(\mathcal{D}_e\),希望定义一个可以评估效果的OPE目标 \(\hat{J}(\pi)\)
  • 方法包含Model-based,Importance Sampling和Fit Q Evaluation
  • 为什么需要OPE?
    • 现实世界中,许多领域里面,直接使用真实环境预测去交互以评估策略效果是危险且成本高昂(包括时间成本和资源成本)的,而OPE提供了一个不需要与环境交互就可以评估策略效果的方案(虽然有时候不是很准确)
    • OPE可以用作超参数的选择

Model-based Approach

  • 类似Model-based强化学习方法,通过标准的监督学习回归方法构建模型预测状态转移概率 \(T_{\psi_T}(s_{t+1}|s_t,a_t)\) 和奖励函数 \(r_{\psi_r}(s_t,a_t)\)
  • 评估结果为:
    $$\hat{J}(\pi) = \mathbb{E}_{\tau\sim p_{\phi_T}(\cdot)}\Big[ \sum_{t=0}^H\gamma^t r_{\psi_r}(s_t,a_t) \Big]$$
    • 其中 \(p_{\phi_T}(\cdot)\) 表示按照策略 \(\pi\) 在环境 \(T_{\psi_T}(s_{t+1}|s_t,a_t)\) 中采样得到的trajectory分布

Importance Sampling (IS)

  • 利用重要性采样的性质完成评估
    $$\hat{J}(\pi) = \mathbb{E}_{\tau\sim p_{\hat{\pi}_\beta(\cdot)}}\Big[ w_{0:H}\sum_{t=0}^H\gamma^t r(s_t,a_t) \Big]$$
    • 其中 \( w_{i:j}\) 是重要性采样的权重乘积
      $$ w_{i:j} = \frac{\prod_{t=i}^j\pi(a_t|s_t)}{\prod_{t=i}^j\hat{\pi}_\beta(a_t|s_t)} $$
    • 其中每一项 \(w_t = \frac{\pi(a_t|s_t)}{\hat{\pi}_\beta(a_t|s_t)}\),实际上 \( w_{i:j}\) 也可以写为:
      $$ w_{i:j} = \prod_{t=i}^j w_t = \prod_{t=i}^j\frac{\pi(a_t|s_t)}{\hat{\pi}_\beta(a_t|s_t)} $$

Fit Q Evaluation (FQE)

  • 先使用策略评估方法学习一个Q值 \(Q_\phi^\pi\) (最小化贝尔曼误差即可),然后在数据集上评估该Q值的累计值
    $$ \hat{J}(\pi) = \mathbb{E}_{(s,a)\sim \mathcal{D}_e}[Q_\phi^\pi(s,a)] $$

离线强化学习方法补充

AWR(Advantage-Weighted Regression)

  • 参考链接:ADVANTAGE-WEIGHTED REGRESSION: SIMPLE AND SCALABLE OFF-POLICY REINFORCEMENT LEARNING, UC Berkeley, arXiv 2019
  • AWR 训练流程

BCQ

CQL

IQL

BRAC(Behavior-Regularized Actor-Critic)

  • 参考链接:(BRAC)Behavior Regularized Offline Reinforcement Learning, arXiv 2019, CMU & Google
  • BRAC 训练流程

XQL(Extreme Q-Learning)

  • 参考链接:Extreme Q-Learning: MaxEnt RL without entropy, ICLR 2023, Google
  • 也称为 \(\mathcal{X}\)-QL
  • XQL 训练流程

SQL(Sparse Q-Learning)

  • 参考链接:[待确认]
  • 参考链接:Sparse Q-learning with Mirror Descent, University of Massachusetts, arXiv 2012
  • 参考链接:SPARSE Q-LEARNING: OFFLINE REINFORCEMENT LEARNING WITH IMPLICIT VALUE REGULARIZATION, Offline RL Workshop 2023

RWR(Reward-Weighted Regression)

  • 参考链接:待确认

EDAC(Ensemble Diversity Actor-Critic)

  • 参考链接:待确认

AWAC(Advantage-Weighted Actor-Critic)

  • 参考链接:AWAC: Accelerating Online Reinforcement Learning with Offline Datasets, UC Berkeley, arXiv 2020
  • AWAC 整体方案介绍(Offline + Online 的训练方式)
  • AWAC 训练流程

Cal-QL(Calibrated Q-Learning)

  • 参考链接:Cal-QL: Calibrated Offline RL Pre-Training for Efficient Online Fine-Tuning, UC Berkely & Stanford University, NeurIPS 2023
  • Cal-QL 训练流程

RS——SENet

  • 参考链接
    • 原始论文:Squeeze-and-Excitation Networks

整体说明

  • SENet(Squeeze-and-Excitation Network)是一种用于图像识别等领域的神经网络架构,通过显式地建模通道之间的相互依赖关系,自适应地调整特征通道的重要性 ,从而提高模型的性能
  • 在CV领域爆火后,近年来,SENet也被广泛应用于推荐系统中
  • 一句话说明:SENet可以给与不同通道不同的权重,从而实现图片的重构(不同通道被乘以不同权重)

SENet整体结构

  • 整体结构图如下:
  • Squeeze层(\(\mathbf{F}_{sq}\)):
    • 输入 :特征图\(U \in \mathbb{R}^{H \times W \times C}\),其中\(H\)、\(W\)、\(C\)分别表示特征图的高度、宽度和通道数
    • 输出 :一个长度为\(C\)的向量\(z \in \mathbb{R}^{C}\)
    • 公式 :\(z_c = \mathbf{F}_{sq}(U_c)=\frac{1}{H\times W}\sum_{i=1}^{H}\sum_{j=1}^{W}U_c(i,j)\),即对每个通道的特征图进行全局平均池化,将二维的特征图压缩成一个实数,得到通道的全局统计信息
  • Excitation层((\(\mathbf{F}_{ex}\))):
    • 输入 :Squeeze层输出的向量\(z\)
    • 输出 :与输入特征图通道数相同的权重向量\(s \in \mathbb{R}^{C}\),用于表示每个通道的重要性
    • 公式 :\(s = \mathbf{F}_{ex}(z, W)= \sigma(g(z, W))=\sigma(W_2\delta(W_1z))\),其中\(\sigma\)是sigmoid函数,\(\delta\)是ReLU函数,\(W_1 \in \mathbb{R}^{\frac{C}{r} \times C}\)和\(W_2 \in \mathbb{R}^{C \times \frac{C}{r} }\)是两个全连接层的权重矩阵,\(r\)是一个缩减比例超参数,用于控制中间神经元的数量,减少模型复杂度
  • Scale层((\(\mathbf{F}_{scale}\))):也称为Reweight
    • 输入 :原始特征图\(U\)和Excitation层输出的权重向量\(s\)
    • 输出 :经过通道加权后的特征图\(\tilde{U} \in \mathbb{R}^{H \times W \times C}\)
    • 公式 :\(\tilde{U}_c = \mathbf{F}_{scale}(U_c, s_c)=s_c \cdot U_c\),即将权重向量\(s\)与原始特征图\(U\)的每个通道对应相乘,实现对特征图的自适应加权
  • 注:\(\mathbf{F}_{tr}\) 是一个转换操作,跟SENet没有直接关系,在 CV 里面就是一个普通的卷积神经网络

在CV领域的使用

  • 图像分类 :SENet可以嵌入到各种经典的图像分类网络中,如ResNet、Inception等,通过对特征通道的自适应加权 ,能够更好地捕捉图像中的重要特征 ,抑制图片中的无关特征 ,从而提高分类准确率

  • 目标检测 :在目标检测任务中,SENet有助于模型更准确地定位和识别目标物体。它可以增强与目标相关的特征通道 ,使模型对目标的细节和特征更加敏感,提高检测的精度和召回率

  • 语义分割 :对于语义分割任务,SENet能够帮助模型更好地理解图像中的语义信息,通过调整通道权重 ,突出不同语义类别对应的特征 ,从而更精确地分割出不同的物体和区域

  • 在 Inception 模块中的嵌入方式

  • 在 Residual 模块中的嵌入方式


在推荐系统中的使用

  • 特征加权 :在推荐系统中,将用户和物品的特征类比为图像中的特征通道。SENet可以学习不同特征的重要性权重,对用户行为特征、物品属性特征等进行自适应加权,强调对推荐结果有重要影响的特征,提高推荐的准确性
    • 理解:可以将SENet应用于用户的嵌入向量、物品的嵌入向量或者深度神经网络中的隐藏层输出,SENet可以对不同特征给与不同的权重(这里的特征和CV中的通道类似),相当于是一种特征重要性抽取器
    • 举例:假设输入 \(N \times d\) 维特征矩阵 ,有 \(N\) 个特征,每个特征是 \(d\) 维的 Embedding,则:
      • Squeeze层 :将每个特征 Embedding 从 \(d\) 维度降低到 1 维标量,输出 \(N\) 维向量
      • Excitation层 :用一个MLP将 \(N\) 维向量先压缩到 \(\frac{N}{r}\) 维再扩展为 \(N\) 维
      • Scale层(Re-weight) :将 \(N\) 维向量作为权重对原始 \(N \times d\) 维的特征矩阵进行加权,输出 \(N \times d\) 维特征矩阵 ,此时每个特征都有自己的个性化权重
    • SENet的本质是对输入 Embedding 做 field-wise 加权(这里认为每个特征就是不同的 field)
  • 注意力机制 :类似于在CV领域中捕捉图像中的重要信息,SENet在推荐系统中可以作为一种注意力机制,聚焦于用户和物品的关键特征,从而更好地建模用户与物品之间的交互关系,为用户提供更个性化的推荐

ML——参数化模型与非参数化模型

本文介绍非参数化模型(Non-parametric models)和参数化模型(Parametric models)


参数化模型

定义

参数化模型指的是那些依赖于固定数量参数的模型,这些参数可以通过训练数据学习得到。参数化模型的特点是一旦参数确定,模型的复杂度也就固定了

举例

参数化模型的例子包括线性回归、逻辑回归、支持向量机(SVM)和神经网络等

非参数化模型

定义

非参数化模型则不对模型形式做出严格的假设,也不依赖于固定数量的参数。这类模型通常具有更大的灵活性,可以根据数据的复杂性来适应模型的复杂度

举例

非参数化模型的例子包括高斯过程回归、决策树、k-最近邻(k-NN)算法、核方法以及各种基于模型的集成方法如随机森林和提升树(Boosting Trees)等

其他说明

  • 尽管神经网络可以拥有大量的参数,使得它们具有高度的灵活性和强大的表达能力,但这些特点并不使其成为非参数化模型。非参数化模型指的是不对模型形式做出严格假设的模型,其参数的数量和模型的复杂度可以随着数据量的增加而增加。因此,即使神经网络在实际应用中可以非常复杂,它们仍然是基于固定参数数量的参数化模型

  • 神经网络越来越复杂,现在参数量已经非常大,实际上可以看做是非参数模型?

DL——迁移学习-元学习-联邦学习

迁移学习-元学习-联邦学习对比

  • 迁移学习侧重于知识从源任务到目标任务的迁移
  • 元学习侧重于快速适应新任务的能力
  • 联邦学习则侧重于在数据隐私保护的前提下进行分布式学习

迁移学习

  • 迁移学习(Transfer Learning)允许模型在一个任务上学习得到的知识应用到另一个不同但相关的任务上。这种方法特别适用于目标任务的数据量不足时。在迁移学习中,通常有一个源域(source domain)和一个目标域(target domain),模型首先在源域上进行训练,然后将学到的特征或参数迁移到目标域以提高学习效率和性能
  • 参考博客: https://blog.csdn.net/dakenz/article/details/85954548

元学习

  • 元学习(Meta-Learning),又称为“学会学习”,是指模型不仅学习如何处理具体的任务,而且学习如何从经验中快速适应和学习新任务的过程。元学习特别关注于当面对新任务时,如何利用已有的知识来加速学习过程。元学习的一个典型应用是通过少量的样本(例如,少样本学习)快速适应新任务
  • 参考链接:【李宏毅-元学习】少样本&元学习Meta Learning_MAML最新机器学习课程!!!
  • 以常见的元学习方法 MAML 的流程为例:
    • 任务采样 :从任务集合中随机选择一批任务,每个任务都包含训练集和验证集
      • 训练集用于更新模型的参数,验证集用于评估模型的泛化性能
    • 任务内优化(内循环) :对于每个采样到的任务,首先使用当前的初始化参数进行梯度更新
      • 通过这些更新,让模型在任务训练集上的表现逐步改善,快速适应当前任务的特定要求
    • 元优化(外循环) :在完成任务内优化后,使用更新后的模型参数在其他任务的验证集上计算损失
      • 然后,基于多个任务的验证集损失,对初始化参数进行更新
      • 外循环的梯度更新是通过任务验证集上的损失进行的,目的是优化整个模型的初始参数 ,使其能在多个任务上快速学习
  • 总结:MAML 的目标是在元训练阶段,通过在多个任务上进行训练和优化 ,找到一个良好的初始参数 ,使得当模型遇到新任务时 ,只需利用新任务的少量数据进行几次快速梯度更新,就能迅速适应新任务,达到较好的性能

联邦学习

  • 联邦学习(Federated Learning)是一种分布式机器学习范式,它允许多个参与者在保持数据隐私和数据本地化的前提下共同构建机器学习模型。在联邦学习中,数据不需要集中存储或处理,而是在各个参与者的本地进行训练,只有模型的更新(如参数)在参与者之间共享。这种方式可以解决数据孤岛问题,同时保护用户隐私。

DL——重参数化技巧

  • 参考链接:
    • 漫谈重参数:从正态分布到Gumbel Softmax
    • 重参数化技巧(Gumbel-Softmax)
    • 通俗易懂地理解Gumbel Softmax

重参数化解决的问题

  • 问题 :假设用NN建模一个分布,比如正太分布可以表达为 \(\mathcal{N}(\mu_\theta,\sigma_\theta)\),此时如果直接从NN建模的分布中采样,由于采样动作是离散的,那么这个采样结果不包含NN分布的梯度信息的,NN反向传播时无法传播回去,也无法实现对参数 \(\theta\) 的更新
  • 重参数化技巧 :通过一些技巧设计采样方式,使得采样过程可导,让采样结果包含NN分布的梯度信息(即实现既可按照NN分布采样 ,又可回传梯度信息)

重参数化的基本思想

  • 不能梯度回传的本质原因是因为采样过程是一种选择动作,这种选择动作本身没有梯度信息,把采样过程挪到计算图之外
  • 用形式来表示,将 \(z \sim f(\theta)\) 构建为形如 \(z = g(\theta, \epsilon), \epsilon \sim p\) 的形式(其中p是与参数无关的某个分布,比如高斯分布)

连续变量分布采样的重参数化

  • 以正太分布为例,原始NN分布采样形式:
    $$ z \sim \mathcal{N}(\mu_\theta,\sigma_\theta) $$
  • 重参数技巧采样:
    $$
    \begin{align}
    \epsilon \sim \mathcal{N}(0,1) \\
    z = \mu_\theta + \sigma_\theta \cdot \epsilon
    \end{align}
    $$

离散变量分布采样的重参数化

以下内容主要参考自重参数化技巧(Gumbel-Softmax)以及其中的回复讨论

原版 softmax(原始问题):

1
2
3
logits = model(x)
probs = softmax(logits)
r = torch.multinomial(probs, num_samples)
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table。缺点是采样这一步把计算图弄断了

Gumbel-Max Trick:

1
2
3
4
5
6
7
8
def sample_gumbel(shape, eps=1e-20, tens_type=torch.FloatTensor):
"""Sample from Gumbel(0, 1)"""
U = Variable(tens_type(*shape).uniform_(), requires_grad=False)
return -torch.log(-torch.log(U + eps) + eps)

logits = model(x)
g = sample_gumbel(logits.size())
r = torch.argmax(logits + g)
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table,计算图连起来了,但 argmax 仍不可导
  • 为什么一定要用sample_gumbel分布而不是其他分布?
    • 因为只有使用gumbel分布采样才能保证与原始softmax后的多项式分布采样完全等价,即 argmax(logits + Gumbel随机变量)与多项式分布采样严格等价 ,相关证明见:漫谈重参数:从正态分布到Gumbel Softmax
  • Gumbel分布的具体定义是什么?
    • 一般Gumbel分布的PDF和CDF:
      $$
      \begin{align}
      \text{PDF}: \quad f(x;\mu,\beta) = e^{-(z+e^{-z})},\quad z=\frac{x-\mu}{\beta} \\
      \text{CDF}: \quad F(x;\mu,\beta) = e^{-e^{-z}}, \quad z=\frac{x-\mu}{\beta}
      \end{align}
      $$
      • \(\mu\) 是位置参数(location parameter)
      • \(\beta\) 是尺度参数(scale parameter)
    • 标准Gumbel分布中, \(\mu=0,\ \beta=1\),此时有 \(z=x\)
      $$
      \begin{align}
      \text{PDF}: \quad f(x;\mu,\beta) = e^{-(x+e^{-x})} \\
      \text{CDF}: \quad F(x;\mu,\beta) = e^{-e^{-x}}
      \end{align}
      $$
  • 在这个场景中,我们使用标准Gumbel分布即可
  • 采样标准Gumbel分布时,可以直接使用逆变换采样(Inverse Transform Sampling) :
    • 先按照均匀分布采样: \(u = \mathcal{U}(0,1)\)
    • 对Gumbel分布原始CDF取逆Gumbel分布采样结果: \(z = -ln(-ln(u))\)

Gumbel-Softmax Trick:

1
2
3
logits = model(x)
g = sample_gumbel(logits.size())
r = F.softmax(logits + g)
  • 采到的 r 都是概率分布,后面可以用 r 把 embedding table 里的各个条目加权平均混合起来,假装是一个单词拿去用。虽然计算图可导了,但是训练和推断不一致!训练时模型见到的都是各个 word embedding 的混合,而非独立的 word embedding!推断时则使用的是独立的 word embedding!

Gumbel-Softmax Trick + Straight-Though Estimator:

1
2
3
4
5
logits = model(x)
g = sample_gumbel(logits.size())
r = F.softmax(logits + g)
r_hard = torch.argmax(r)
r = (r_hard - r).detach() + r
  • 采到的 r 都是整数 ID,后面可以用 r 去查 embedding table
  • 前向传播使用 r_hard 获得独立的单词,反向传播使用 r(即 softmax 的结果)的梯度。一切都很完美
  • Straight-Through Estimator 的意思是说,如果你遇到某一层不可导,你就当它的梯度是 identity,直接把梯度漏下去,即假定当前层的梯度为1
  • 实际上此时正向传播和反向传播面对的公式也不一样
    • 正向传播时得到的是r_hard
    • 反向传播时,由于(r_hard - r).detach()使得梯度为0,所以回传的实际是r的反向梯度

argmax动作的梯度回传

  • argmax操作的形式:
    $$
    \begin{align}
    i^* &= \mathop{\arg\max}_i (\vec{x}) \\
    \text{where} \quad \vec{x}=&(x_1, x_2, \cdots, x_n), \quad x_i = f(\theta)_i
    \end{align}
    $$
    • 注:以上argmax的写法不严谨,严谨的是 \(i^* = \mathop{argmax}_i x_i, \ x_i \in \vec{x}\)
  • 近似形式:
    $$
    \begin{equation}
    \mathop{\arg\max}_i (\vec{x}) \approx \sum_{i=1}^n i\times \text{softmax}(\vec{x})_i
    \end{equation}
    $$
  • argmax本质也可以看做一种离散采样,只是没有随机性,该采样选择使得目标值最大的离散变量
  • 详情见:函数光滑化杂谈:不可导函数的可导逼近

PyTorch——CrossEntopy-Loss和NLL-Loss的区别


整体说明

  • 交叉熵损失(Cross-Entropy Loss, CE Loss)和负对数似然损失(Negative Log-Likelihood Loss, NLL Loss)的核心区别在于 是否包含 Softmax 激活 以及 适用场景的精细化
  • PyTorch 框架中的交叉熵损失 = Softmax(或 Sigmoid)+ 负对数似然损失
  • 使用原则
    • 分类任务直接用 CE Loss(输入 logits),简单且数值稳定
    • 若已手动添加 Softmax/LogSoftmax 层,或需要自定义概率输出,再用 NLL Loss
  • NLL Loss 是“对概率的负对数”,CE Loss 是“对分布差异的衡量”,但在分类任务中(预测分布由 Softmax 生成)二者结果等价

分类任务场景符号定义

  • 模型原始输出(logits):\(z = [z_1, z_2, …, z_C]\)(\(C\) 为类别数)
  • 真实标签:\(y\)(one-hot 编码为 \(y_{one-hot} = [0, 0, …, 1, …, 0]\),或类别索引 \(y \in {0,1,…,C-1}\))
  • Softmax 激活:将 logits 映射为概率分布 \(p_i = \text{Softmax}(z)i = \frac{e^{z_i}}{\sum{j=1}^C e^{z_j}}\)(满足 \(\sum p_i = 1\))
  • 对数概率:\(\log(p_i)\)(概率越大,对数概率越接近 0;概率越小,对数概率越负)

负对数似然损失(Negative Log-Likelihood Loss, NLL Loss)

  • 负对数似然损失(NLL Loss)是 对 模型输出的概率分布 \(p\) 取对数后,取负,再根据真实标签\(y\)选择对应类别的项
    • 若标签为 one-hot 编码:
      $$ L_{\text{NLL}} = -\sum_{i=1}^C y_i \cdot \log(p_i)$$
      • 此时仅真实类别 \(y_k=1\),所以可进一步简化为
        $$ L_{\text{NLL}} = -\log(p_k) $$
        • 这也是 负对数似然损失(NLL Loss)名字的由来
    • 若标签为类别索引(本质是一个 index,one-hot 转换成向量以后与 one-hot 编码一致):直接取真实类别 \(y\) 对应的概率 \(p_y\) 的对数再取负
      $$ L_{\text{NLL}} = -\log(p_y)$$
  • 特别说明: NLL Loss 本身不包含 Softmax 步骤
    • 输入给 NLL Los 的必须是 已经经过 Softmax 激活的概率分布 \(p\) 或对数概率 \(\log(p)\))

交叉熵损失(Cross-Entropy Loss, CE Loss)

  • 交叉熵损失(CE Loss) 用于衡量 模型预测概率分布 \(p\) 与 真实标签分布 \(q\) (one-hot编码)之间的差异,公式与 NLL Loss形式完全一致 ,但隐含了“先将 logits 转为概率”的逻辑
    • 数学公式:
      $$ L_{\text{CE}} = -\sum_{i=1}^C q_i \cdot \log(p_i)$$
      • 其中: \(q\) 是真实分布
      • one-hot 时简化为
        $$ L_{\text{CE}} = -\log(p_y) $$
    • 关键区别:在深度学习框架中(如 PyTorch、TensorFlow),CE Loss 的输入是 原始 logits ,而非概率
      • 框架会内部先对 logits 做 Softmax,再计算负对数似然

CE Loss 和 NLL Loss 对比

  • 本质上,交叉熵(CE)的数学定义是
    $$ H(q,p) = -\sum q_i \log(p_i)$$
  • 而 负对数似然(NLL)是
    $$ -\sum q_i \log(p_i)$$
    • 仅当 \(p\) 是模型预测的概率分布时,因此当 \(p\) 是 Softmax 输出时,CE Loss = NLL Loss ,但二者的 计算链路 不同:
      $$ \text{CE Loss(logits)} = \text{NLL Loss(Softmax(logits))}$$

附录:为什么框架中的 CE Loss 更常用?

  • 在 PyTorch 中:
    • nn.NLLLoss():输入必须是 对数概率 (通常需先过nn.LogSoftmax(dim=1))
    • nn.CrossEntropyLoss():输入是 logits,内部等价于 nn.LogSoftmax(dim=1) + nn.NLLLoss()
  • 选择 CE Loss 的核心原因是数值稳定性 :
    • 直接计算 \(\log(\text{Softmax}(z_i))\) 时,若 \(z_i\) 很大(如 1000),\(e^{z_i}\) 会溢出(变成无穷大);而框架会用数学变换优化:
      $$ \log\left(\frac{e^{z_i}}{\sum_j e^{z_j}}\right) = z_i - \log\left(\sum_j e^{z_j}\right) $$
    • 通过减去 \(\log(\sum_j e^{z_j})\)(即 LogSumExp),避免了指数爆炸,数值更稳定
  • 补充:一种常用的等价推导(可以优化计算的方法)是
    $$
    \begin{align}
    \text{log_sofmax}(x) &= \log \frac{e^{x_{i}}}{\sum_{j=1}x_{j}} \\
    &= \log e^{x_i} - \log \sum_{j=1}x_{j} \\
    &= x_i - \log \sum_{j=1}x_{j}
    \end{align}
    $$
    • 上面的式子中,只需要计算一次 \(\log \sum_{j=1}x_{j}\) 即可(且不同维度可重用该值), 其他的都是加减法运算

使用代码示例

  • 用 CrossEntropyLoss(直接输入 logits)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torch
    import torch.nn as nn

    # 模型输出logits(C=3类,batch_size=2)
    logits = torch.tensor([[2.0, 1.0, 0.1], [1.0, 3.0, 0.5]])
    # 真实标签(类别索引)
    labels = torch.tensor([0, 1])

    # 交叉熵损失(内部含Softmax)
    ce_loss = nn.CrossEntropyLoss()
    print(ce_loss(logits, labels)) # 输出:0.4170(数值稳定)
  • 用 NLLLoss(需先过LogSoftmax)

    1
    2
    3
    4
    5
    6
    7
    # 先将logits转为对数概率
    log_softmax = nn.LogSoftmax(dim=1)
    log_probs = log_softmax(logits) # 等价于框架内部优化后的计算

    # 负对数似然损失(输入是对数概率)
    nll_loss = nn.NLLLoss()
    print(nll_loss(log_probs, labels)) # 输出:0.4170(与CE Loss结果一致)

附录:错误示例(NLLLoss 直接输入 logits)

  • 常常犯的错:
    1
    2
    # 直接用logits输入NLLLoss(会报错或结果错误)
    print(nll_loss(logits, labels)) # 错误:logits不是对数概率,数值会异常

附录:BCE Loss

  • 二分类任务时,CE Loss 常被称为 二元交叉熵(BCE Loss) ,与 NLL Loss 的关系同样成立:

    • nn.BCEWithLogitsLoss():输入是 logits(单输出,如 \(s\)),内部先做 Sigmoid(\(\sigma(s) = 1/(1+e^{-s})\)),再计算BCE Loss:
      $$ L = -[y\log(\sigma(s)) + (1-y)\log(1-\sigma(s))]$$
    • 若手动用 Sigmoid + NLLLoss:需将标签转为 float(如 \(y \in {0.0, 1.0}\)),且输出为单通道概率,此时
      $$ L = -[y\log(p) + (1-y)\log(1-p)]$$
      • 与 BCEWithLogitsLoss 结果一致
  • BCELoss 还可以直接使用

    1
    torch.nn.BCELoss()
    • 具体操作就是实现了书上定义的二分类交叉熵定义
    • 普通样本计算公式:
      $$ loss(o,t)=-\frac{1}{n}\sum_i(t_i\log(o_i)+(1-t_i)\log(1-o_i)) $$
    • 带有权重的单个样本计算公式:
      $$ loss(o,t)=-\frac{1}{n}\sum_i w_i (t_i \log(o_i)+(1-t_i)\log(1-o_i)) $$

BCELoss vs CrossEntropyLoss 比较

  • BCELoss 对应的网络只有一个输出值
  • CrossEntropyLoss 对应的网络有两个输出值
  • 可以证明, 二分类时 BCELoss 与 CrossEntropyLoss等价
    • 证明时,将每个 CrossEntropyLoss 的计算公式中的 softmax 函数分子分母同时除以 shift(\(\text{shift} = max (x_i)\)), 即可得到为下面的定义,进一步可得到 BCELoss 的计算公式
      $$f_i(x) = \frac{e^{(x_i - \text{shift})}} { \sum^j e^{(x_j - \text{shift})}} $$

附录:相关损失函数 MultiLabelMarginLoss

  • 使用

    1
    torch.nn.MultiLabelMarginLoss()
  • 用于多标签分类的损失函数


总结

  • 一般来说直接使用 CrossEntropyLoss 即可
    • 二分类时还可以使用 nn.BCELoss
    • 二分类时使用 nn.BCELoss 的话,输入的 input和target维度都为 (n, 1) 的维度
    • 二分类时使用 CrossEntropyLoss 则输入的 input 为 (n x 2) 的维度
  • 如果使用 NLLLoss 则一定记得在输出层最后加一层 log_softmax 层
  • 注意,log 指的是以 e 为底的对数函数,而不是以10为底的
    • Mac自带的计算器中 log 是以 10 为底的,ln 才是以 e 为底的
1…202122…63
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

630 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4