Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Math——线性规划求解方法和理解

本文包含对线性规划的直观理解,不严谨,后续有新的问题/理解持续更新

  • 参考链接:
    • 运筹学中应该如何理解互补松弛性。这条性质又该如何运用?
    • 第4章 对偶理论和敏感度分析
    • 线性规划对偶问题的定义,有什么直觉上的解释吗?:原始问题到对偶问题最好的一种很简洁的解释
    • 互联网广告算法漫谈——浅谈广告中的出价技术。注意:该参考链接中没有把预算约束相关的互补松弛定理写出来,且2.3中存在一些较为明显的小bug,但整体求解思路和结论没问题

原始问题

  • 问题描述:
    • 假设你是一个木匠有200单位的木头和90单位的时间
    • 木匠可以制作桌子或者椅子
      • 桌子成本为5单位木头+2单位时间,售价10元
      • 椅子成本为2单位木头+1单位时间,售价3元
  • 目标:在已有资源情况下,最大化收入,应该生产多少桌子和椅子?
  • 问题形式化描述:
    • 假设应该生产 \(x_1\) 把桌子和 \(x_1\) 把椅子
      $$
      \begin{align}
      \max \ \ 10x_1 &+ 3x_2 \\
      5x_1 + 2x_2 &<= 200 \\
      3x_1 + \ \ x_2 &<= 90 \\
      x_1,x_2 &>= 0 \\
      \end{align}
      $$
  • 作图法可求得最优解为 \(x_1^* = 30, x_2^*=0\),此时最大收益为300
    • 在二维坐标轴上先画出可行域,然后按照目标直线斜率找到最优点

对偶问题

  • 对偶问题描述:
    • 上述原始问题可以换一个视角看
    • 假设现在你是一个原材料收购商(想要以最低价格收购木匠的原材料)
    • 目标:对单位木头和单位时间进行出价,以最低的价格买完木匠的资源(假设木匠愿意卖出的前提是收购上出价的最小值不小于木匠原始问题中收益的最大值)
      • 实际上最好是刚好等于木匠原始问题的最大收益
  • 对偶问题形式化描述
    $$
    \begin{align}
    \min \ \ 200p_1 &+ 90p_2 \quad – 总付款 \\
    5p_1 + 3p_2 &>= 10 \quad – 一张桌子的资源售价不低于一张桌子的收益 \\
    2p_1 + \ \ p_2 &>= 3 \quad – 一张椅子的资源售价不低于一张椅子的收益 \\
    p_1,p_2 &>= 0 \quad – 售价不为负数 \\
    \end{align}
    $$
  • 其中 \(p_1, p_2\) 分别称为单位木头和单位时间的影子价格
  • 作图法可求得最优解为 \(p_1^* = 0, p_2^* = 3.3\),此时最小支付金额为300

互补松弛定理的理解

从原始问题的约束视角出发

等价于从对偶问题的解出发

  • 对偶问题中,最优解是 \(p_1^* = 0, p_2^* = 3.3\)
    • \(p_1^* = 0\) 意味着我们的木材过量了,其实不需要这么多木材,原始问题中,最优解对应的木材约束是松的( \(5x_1^* + 2x_2^*=150 < 200\) )
    • \(p_2^* = 3.3\) 说明时间资源非常紧俏,原始问题中,最优解对应的时间约束是紧的( \(3x_1^* + \ \ x_2^* = 90\) )
  • 对应互补松弛的含义:
    • 如果在最优条件下一个约束不等式是松的(木材),那么这个约束对应的影子价格为0
    • 反过来说,如果某个约束对应的影子价格严格大于0,那么这个约束不等式一定是紧的
    • 总的来说,原始问题的约束和对偶问题变量(影子价格)总有一个要为0

从对偶问题的约束视角出发

等价于从原始问题的解出发

  • 原始问题中,最优解是 \(x_1^* = 30, x_2^*=0\)
    • \(x_1^* = 30\) 意味着桌子非常合算,应该多生产桌子,对偶问题中,桌子约束是紧的( \(5p_1^* + 3p_2^* = 10\) )
    • \(x_2^*=0\) 以为这椅子不合算,不应该生产椅子,对偶问题中,椅子的约束是松的( \(2p_1^* + \ \ p_2^* = 3.3 > 3\) )
  • 补充互补松弛的含义:
    • 如果在对偶最优条件下一个约束不等式是松的(椅子),那么这个约束对应的原始问题变量最优解( \(x_2^*\) )为0
    • 反过来说,如果某个原始问题变量(桌子)对应的解( \(x_1^*\) )严格大于0,那么对偶问题中这个约束不等式一定是紧的
    • 总的来说,对偶问题的约束和对应原始问题变量总有一个要为0

互补松弛定理的公式化

$$
(5p_1^* + 3p_2^* - 10)x_1^* = 0 \\
(2p_1^* + p_2^* - 3)x_2^* = 0 \\
(5x_1^* + 2x_2^* - 200)p_1^* = 0 \\
(3x_1^* + x_2^* - 90)p_2^* = 0 \\
$$


附录:USCB推导

  • 《A Unified Solution to Constrained Bidding in Online Display Advertising》——论文阅读
    • 这篇文章的约束很多,每个商家都有自己的约束
    • 推导时用到的对偶变换和互补松弛定理均可由论文推导得出【有时间再详细推导】

附录:BCB推导(单约束)

  • 《Budget Constrained Bidding by Model-free Reinforcement Learning in Display Advertising》——论文原文
    • 这篇文章中的问题定义比较简单,整体只有一个预算约束
    • 上述结果详细的推导可以参考:
      • 智能出价——BCB求解
      • 互联网广告算法漫谈——浅谈广告中的出价技术。注意:该参考链接中没有把预算约束相关的互补松弛定理写出来,且2.3中存在一些较为明显的小bug,但整体求解思路和结论没问题
    • 推导结果 \(bid = \frac{v_i}{\lambda}\) 与常用的方法(RL-MPCA)结果不一致,但可以证明本质是等价的

附录:CPC约束推导(单约束)

  • 问题描述:单位置、二价拍卖,且CPM计费场景,CPC约束下最大化商家点击量
  • 推导过程可参考论文Bid Optimization by Multivariable Control in Display Advertising
  • 基本推导思路:先通过拉格朗日乘子法得到最优解的形式(这里先忽略边际条件 \(0\le x_i \le 1\) ),再将原始问题转换成对偶问题,进一步分情况讨论得到最终解
  • 问题定义
    $$
    \begin{align}
    &\max \sum_i x_i \cdot ctr_i \\
    \text{s.t.} &\quad \frac{\sum_i x_i \cdot wp_i}{\sum_i x_i \cdot ctr_i} \le cpc \\
    &\quad 0 \le x_i \le 1, \forall i
    \end{align}
    $$
  • 第一步:推导最优出价形式:
    • 写出拉格朗日函数并求导:
      $$\mathcal{L}(x, \lambda, \mu) = - \sum_i x_i \cdot ctr_i + \lambda \left(\sum_i x_i \cdot wp_i - \sum_i x_i \cdot ctr_i \cdot cpc\right) + \sum_i \mu_i (x_i - 1)$$
    • 对任意的 \(x_i\) 求导有:
      $$ \frac{\partial \mathcal{L}(x, \lambda, \mu)}{\partial x_i} = - \sum_i ctr_i + \lambda \sum_i wp_i - \lambda \sum_i ctr_i \cdot cpc + \sum_i \mu_i $$
    • 令上述导数为0有(\(\mu_i\) 来自边界条件 \(0\le x_i \le 1\),为了得到最优解形式,接下来先忽略边界条件,最后会证明在满足边界条件下,该形式也是最优的):
      $$
      \begin{align}
      wp_i &= \frac{ctr_i + \lambda \cdot cpc \cdot ctr_i}{\lambda} \\
      &= \frac{1 + \lambda \cdot cpc}{\lambda} \cdot ctr_i
      \end{align}
      $$
      • 所以我们令出价等于下面的形式:
        $$bid_i = \frac{1 + \lambda \cdot cpc}{\lambda} \cdot ctr_i$$
  • 第二步:验证最优出价形式:
    • 原始问题对应的对偶问题为:
      $$
      \begin{align}
      &\mathop{\min}_{\lambda, r_i} \sum_i r_i \\
      \text{s.t.} &\quad \lambda(wp_i - cpc\cdot ctr_i) + r_i \ge ctr_i \quad \text(1)\\
      &\quad \lambda \ge 0 \\
      &\quad r_i \ge 0, \forall i
      \end{align}
      $$
    • 互补松弛条件:
      $$
      \begin{align}
      x_i(\lambda(wp_i - cpc\cdot ctr_i) + r_i - ctr_i) = 0 \quad &\text{(2)} \\
      r_i(x_i - 1) = 0, \forall i \quad &\text{(3)}
      \end{align}
      $$
    • 将最优出价公式 \(bid_i = \frac{ctr_i + \lambda \cdot cpc \cdot ctr_i}{\lambda}\) 带入公式(2)可得:
      $$ x_i(\lambda(wp_i - bid_i) + r_i) = 0$$
      • 当 \(x_i \gt 0\) 时,有 \(wp_i - bid_i = -\frac{r_i}{\lambda} \lt 0\),进一步推得 \(bid_i \ge wp_i\)
      • 当 \(x_i = 0\) 时,由公式(3)有 \(r_i = 0\);将最优出价公式 \(wp_i = \frac{ctr_i + \lambda \cdot cpc \cdot ctr_i}{\lambda}\) 带入公式(1)可得 \(\lambda(wp_i - bid_i) + r_i \ge 0\),进一步推得 \(wp_i - bid_i \ge 0\),即\(bid_i \le wp_i\)
    • 证毕
  • 如何理解最优出价形式?
    $$
    \begin{align}
    bid_i = \frac{1 + \lambda \cdot cpc}{\lambda} \cdot ctr_i = \color{red}{(\frac{1}{\lambda \cdot cpc} + 1)} \cdot cpc \cdot ctr_i
    \end{align}
    $$
    • 二价计费场景中 ,计费比未知,所以引入了一个大于 1 的出价系数:
      $$ k = \color{red}{\frac{1}{\lambda \cdot cpc} + 1} $$
      • 用来提升出价以做到目标CPC达成(可以证明,在整个周期内流量足够多的情况下,如果实际CPC小于目标CPC,则此时一定不是点击最大化的出价策略)
      • 实际使用中,由于 \(1\) 是全局固定值,商家的 \(cpc\) 是商家粒度的固定值,\(\lambda\) 是商家粒度的变量,可以合并成一个变量即可(\(\lambda\) 和 \(k\) 是一一对应的),最终可以忽略 \(k\) 值的具体形式,只需要直接调节 \(k\) 即可,此时最优公式为:
        $$ bid_i = \color{red}{k} \cdot cpc \cdot ctr_i $$
    • 如果竞争环境非常激烈,计费比趋近于1(同时考虑预估值准确),此时每次出价都按照 \(\color{red}{bid_i = cpc \cdot ctr_i} \),可保证投放周期内实际CPC的期望刚好等于目标CPC,\(\color{red}{k=1}\) 就是最优的出价策略
    • 调控系数的其他功能 :从推导来看,系数 \(k\) 可以用于补足二价计费的Gap;在实际应用中,这个 k 值还可以解决 CTR 均值预估值不准确的问题,比如CTR预估过高 ,\(k\) 会小于1 ,从而保证不超成本
      • 可以注意到:在这个假设下有矛盾点,\(k < 1\) 时对应的 \(\lambda < 0\),并不满足对拉格朗日乘子的要求,但不用担心,这里实际上 \(k = k_1 \cdot k_2\),其中,由 \(\lambda\) 导出的 \(k_1\) 依然是大于1的,用来调平CTR预估值 \(k_2\) 是小于1的,实际上,\(\lambda \geq 0\) 始终成立

附录:oCPC场景约束推导(单约束)

  • 问题描述1:单位置、二价拍卖,且CPC计费场景,CPS约束下最大化商家订单量
  • 实际上,本问题中与上文单位置拍卖的CPM计费场景,CPC约束下最大化商家点击量非常相似,仅需把对应的参数替换一下即可(\(ctr_i \rightarrow cvr_i\),\(cpc \rightarrow cps\)),于是有最优出价形式是:
    $$
    \begin{align}
    wp_i = \frac{1 + \lambda \cdot cps}{\lambda} \cdot cvr_i = \color{red}{(\frac{1}{\lambda \cdot cps} + 1)} \cdot cps \cdot cvr_i
    \end{align}
    $$
    • 出价系数:
      $$ k = \color{red}{\frac{1}{\lambda \cdot cps} + 1} $$
    • 注:实际使用中,同上描述,最终可以忽略 \(k\) 值的具体形式,只需要直接调节 \(k\) 即可,此时最优公式为:
      $$ bid_i = \color{red}{k} \cdot cps \cdot cvr_i $$
  • 问题描述2:单位置、二价拍卖,且CPC计费场景,ROI约束下最大化商家Revenue
  • 此时可以进一步表达为如下形式(\(cvr_i \rightarrow cvr_i\cdot rev_i\),\(cps \rightarrow rate = \frac{1}{ROI}\), ):
    $$
    \begin{align}
    wp_i &= \frac{1 + \lambda \cdot 1/ROI}{\lambda} \cdot cvr_i \cdot rev_i \\
    &= \frac{ROI + \lambda}{\lambda \cdot ROI} \cdot rev_i \cdot cvr_i \\
    &= \frac{ROI + \lambda}{\lambda} \cdot \frac{rev_i \cdot cvr_i}{ROI} \\
    &= \color{red}{(\frac{ROI}{\lambda} + 1)} \cdot \frac{rev_i \cdot cvr_i}{ROI} \\
    \end{align}
    $$
    • 出价系数:
      $$ k = \color{red}{\frac{ROI}{\lambda} + 1}$$
    • 注:实际使用中,同上描述,最终可以忽略 \(k\) 值的具体形式,只需要直接调节 \(k\) 即可,此时最优公式为:
      $$ bid_i = \color{red}{k} \cdot \frac{rev_i \cdot cvr_i}{ROI} $$

附录:紧约束和松约束

  • 紧约束(Tight Constraint)和松约束(Slack Constraint)是描述约束条件对可行解集影响的两个概念
  • 紧约束指的是那些在其边界上限制了最优解的约束条件。换句话说,如果改变某个约束条件会直接影响到最优解的位置或值,那么这个约束条件就是紧的。例如,在线性规划问题中,如果一个不等式约束以“=”的形式满足于最优解处,那么这个约束就是紧约束。紧约束对于确定最优解至关重要,因为它们直接定义了最优解所在的位置
  • 松约束则指的是那些在最优解处并没有起到实际限制作用的约束条件。也就是说,即使这些约束不存在,也不会改变问题的最优解。这类约束条件提供了额外的空间,但在这个空间内的点并不会比边界上的点更优。因此,松约束的存在不会影响最终的优化结果,但在某些情况下,它们可能为寻找最优解提供便利或增加灵活性。

Math——运筹优化开源求解器-GLPK的使用

本文介绍各种运筹优化开源求解器-GLPK的使用

  • GLPK是一款完全开源免费的运筹优化求解器,可以任意商用

Ubuntu安装GLPK

  • 据说Ubuntu安装较为方便,所以建议首选Ubuntu

  • 在网站下载文件:https://ftp.gnu.org/gnu/glpk/

    • 可以下载任意版本,建议选最新
  • 安装命令

    1
    2
    3
    4
    tar -xzvf glpk-xxx.tar.gz
    ./configure
    make
    sudo make install
  • 安装后直接执行可能出现错误

    1
    error while loading shared libraries: libglpk.so.36:...
  • 解决方案(原始解决方案地址):

    1
    https://github.com/rstudio/renv/issues/1881

Ubuntu下GLPK的使用

  • 下列式子参考了:线性规划工具 GLPK 的安装及基本使用

  • 创建问题描述文件glpkDemo.mod

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /* Variables */
    var x1 >= 0;
    var x2 >= 0;
    var x3 >= 0;

    /* Object function */
    maximize z: 3*x1 + x2 +2*x3;

    /* Constrains */
    s.t. con1: x1 + x2 + 3*x3 <= 30;
    s.t. con2: 2*x1 +2*x2 + 5*x3 <= 24;
    s.t. con3: 4*x1 + x2 + 2*x3 <= 36;

    end;
  • 执行命令解决问题

    1
    glpsol -m glpkDemo.mod -o ./output/glpkDemo.sol
  • 输出文件

    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
    Problem:    glpkDemo
    Rows: 4
    Columns: 3
    Non-zeros: 12
    Status: OPTIMAL
    Objective: z = 28 (MAXimum)

    No. Row name St Activity Lower bound Upper bound Marginal
    ------ ------------ -- ------------- ------------- ------------- -------------
    1 z B 28
    2 a B 12 30
    3 b NU 24 24 0.166667
    4 c NU 36 36 0.666667

    No. Column name St Activity Lower bound Upper bound Marginal
    ------ ------------ -- ------------- ------------- ------------- -------------
    1 x1 B 8 0
    2 x2 B 4 0
    3 x3 NL 0 0 -0.166667

    Karush-Kuhn-Tucker optimality conditions:

    KKT.PE: max.abs.err = 0.00e+00 on row 0
    max.rel.err = 0.00e+00 on row 0
    High quality

    KKT.PB: max.abs.err = 0.00e+00 on row 0
    max.rel.err = 0.00e+00 on row 0
    High quality

    KKT.DE: max.abs.err = 2.22e-16 on column 1
    max.rel.err = 3.17e-17 on column 1
    High quality

    KKT.DB: max.abs.err = 0.00e+00 on row 0
    max.rel.err = 0.00e+00 on row 0
    High quality

    End of output
  • Activity这一列就是想要的解

  • 其他输出项如何理解?

自动化生成问题

  • 使用shell或者Python自动生成.mod文件,然后自然解析.sol文件,实现自动化测试参数

CV——ViT

  • 参考链接:
    • 原始论文:
      • (ViT)An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale, Google Brain Team, ICLR 2021

Paper Summary

  • 整体总结:
    • ViT(Vision Transformer)已经成为计算机视觉必不可少的组件了,目前围绕 ViT 已经有了许多变体,本文是 ViT 相关的第一篇文章
    • ViT 核心:将 Transformer 用在图像领域
      • 与之前在计算机视觉中使用自注意力的工作不同
        • 除了初始的 patch 提取步骤外,ViT 没有向架构引入特定于图像的归纳偏置 (image-specific inductive biases)
          • 理解:这里所谓的归纳偏置就是类似 平移等变性和局部性 等 CNN 对图像领域的核心假设
        • ViT 将图像建模为一系列 patches,并使用与 NLP 中相同的标准 Transformer 编码器进行处理
          • 这种简单但可扩展的策略,在与大规模数据集上的预训练相结合时,效果出人意料地好
    • 注:本文之前的状况:
      • 在视觉领域,注意力机制要么与卷积网络结合使用,要么用于替换卷积网络的某些组件,同时保持其整体结构不变
      • 本文表明这种对 CNN 的依赖并非必要,直接应用于图像块序列的纯 Transformer 可以在图像分类任务上表现良好
      • Vision Transformer (ViT) 相比卷积网络更好,同时训练所需的计算资源要少得多

Introduction and Discussion

  • 基于自注意力的架构(特别是 Transformer),已成为自然语言处理中的首选模型
    • 主导方法是在大型文本语料库上进行预训练,然后在较小的特定任务数据集上进行微调 (2019)
    • 得益于 Transformer 的计算效率和可扩展性,训练具有前所未有规模的模型(超过 100B 参数)已成为可能 (2020;2020)
    • 且随着模型和数据集的增长,性能仍未出现饱和迹象
  • 在计算机视觉中,卷积架构仍然占主导地位 (1989;2012;2016)
    • 受 NLP 成功经验的启发,多项工作尝试将类似 CNN 的架构与自注意力相结合 (2018;2020),有些则完全替换了卷积 (2019;2020a)
      • 完全替换了卷积的这一类模型理论上是高效的,但由于使用了专门的注意力模式,尚未在现代硬件加速器上有效扩展
    • 结论:在当前大规模图像识别中,经典的 ResNet 类架构仍然是 SOTA (2018;2020;2020)
  • 本文尝试将标准的 Transformer 直接应用于图像,并进行尽可能少的修改
    • 本文将图像分割成块 (patches),并将这些块的线性嵌入序列作为 Transformer 的输入
    • 图像块的处理方式与 NLP 应用中的 Token(词)相同
    • 本文以监督方式训练模型进行图像分类
  • ViT 效果与数据量级有关:
    • 数据量不足时:
      • 当在中型数据集(如未使用强正则化的 ImageNet)上训练时,这些模型的准确率适中,比同等大小的 ResNet 低几个百分点
      • 这一看似不及预期的结果可能是意料之中的:Transformer 缺乏 CNN 固有的一些归纳偏置 (inductive biases),例如平移等变性和局部性,因此在数据量不足的情况下无法很好地泛化
        • 理解:这里所谓的归纳偏置就是类似 平移等变性和局部性 等 CNN 对图像领域的核心假设
    • 数据量充足时:
      • 如果模型在更大的数据集(14M-300M 张图像)上训练,情况就会发生变化:大规模训练胜过归纳偏置
        • 当在足够规模的数据上进行预训练并迁移到数据点较少的下游任务时, ViT 获得了出色的结果
      • 在公共 ImageNet-21k 数据集或内部的 JFT-300M 数据集上预训练时,ViT 在多个图像识别基准上接近或超越了 state of the art
        • 最佳模型在 ImageNet 上达到了 \(88.55\%\) 的准确率,在 ImageNet-Real 上达到了 \(90.72\%\),在 CIFAR-100 上达到了 \(94.55\%\),在包含 19 个任务的 VTAB 套件上达到了 \(77.63\%\)

Related Work

  • 详情见原文
  • Transformer 已成为许多 NLP 任务中的 SOTA 方法
    • 基于 Transformer 的大型模型通常先在大型语料库上进行预训练,然后针对手头任务进行微调:
      • BERT (2019) 使用去噪自监督预训练任务
      • GPT 系列工作使用语言建模作为其预训练任务 (2018;2019;2020)
  • 像素粒度的 Transformer:
    • 将自注意力直接应用于图像需要每个像素关注其他每个像素(注意:这里是每个像素,而像素是很多很多的)
    • 由于计算成本与像素数量呈二次关系,这无法扩展到实际的输入尺寸
      • 为了在图像处理中应用 Transformer,过去已经尝试了几种近似方法
    • Parmar 等人 (2018) 仅对每个查询像素的局部邻域应用自注意力,而不是全局
      • 这种局部多头点积自注意力块可以完全替代卷积 (2019;2019;2020)
    • Sparse Transformers (2019) 采用可扩展的近似方法来处理全局自注意力 ,以便应用于图像
    • 另一种扩展注意力的方法是将其应用于不同大小的块中 (2019),极端情况下仅沿单个轴应用 (2019;2020a)
      • 理解:这里
    • 许多这些专门的注意力架构在计算机视觉任务上展示了有希望的结果,但需要复杂的工程才能在硬件加速器上高效实现
  • 块粒度的 Transformer
    • (2020) 的模型从输入图像中提取大小为 \(2 \times 2\) 的块,并在其上应用完全的自注意力
    • 该模型与 ViT 非常相似,但 ViT 更进一步,证明了大规模预训练使 vanilla Transformer 能够与最先进的 CNN 竞争(甚至更好)
    • (2020) 使用了 \(2 \times 2\) 像素的小块大小,这使得该模型仅适用于小分辨率图像,而也处理中等分辨率的图像
  • 将卷积神经网络与各种形式的自注意力结合的方法:
    • 通过增强特征图用于图像分类 (2019),或使用自注意力进一步处理 CNN 的输出
    • 用于目标检测 (2018;2020)、视频处理 (2018;2019)、图像分类 (2020)、无监督目标发现 (2020) 或统一文本-视觉任务 (2020c;2019;2019)
  • image GPT (iGPT) (2020a) 在降低图像分辨率和颜色空间后将 Transformer 应用于图像像素
    • iGPT 模型以无监督方式作为生成模型进行训练,然后可以对生成的表示进行微调或线性探查以用于分类性能,在 ImageNet 上达到了 \(72\%\) 的最大准确率
  • 工作为越来越多探索超出标准 ImageNet 数据集的更大规模图像识别的研究增添了新的内容
    • 使用额外的数据源可以在标准基准上达到 SOTA 的结果 (2018;2019;2020)
    • Sun 等人 (2017) 研究了 CNN 性能如何随数据集大小扩展
    • Djolonga 等人 (2020) 对从大规模数据集(如 ImageNet-21k 和 JFT-300M)进行的 CNN 迁移学习进行了实证探索
  • 也关注后两个数据集,但训练的是 Transformer,而不是先前工作中使用的基于 ResNet 的模型

Method

  • 在模型设计上,尽可能严格地遵循原始 Transformer (2017)
  • 这种刻意简化的设置的一个优点是,可扩展的 NLP Transformer 架构及其高效实现几乎可以开箱即用

Vision Transformer

  • 模型概述如图 1 所示
  • 标准 Transformer 接收的输入是一个 1D 的 Token 嵌入序列
  • 定义 2D 图像:
    $$ \mathbf{x} \in \mathbb{R}^{H \times W \times C} $$
  • 将上述图像重塑为一系列展平的 2D 图像块
    $$ \mathbf{x}_p \in \mathbb{R}^{N \times (P^2 \cdot C)} $$
    • \((H, W)\) 是原始图像的分辨率
    • \(C\) 是通道数
    • \((P, P)\) 是每个图像块的分辨率
    • \(N = HW / P^2\) 是产生的图像块数量,也是 Transformer 的有效输入序列长度
  • Transformer 在其所有层中使用恒定的隐向量尺寸 \(D\),因此本文将图像块展平,并使用一个可训练的线性投影将其映射到 \(D\) 维(公式 1)
    • 本文将此投影的输出称为图像块嵌入 (patch embeddings)
  • 类似于 BERT 的 [class] Token,本文在向嵌入后的图像块序列 \((\mathbf{z}_0^0 = \mathbf{x}_{\text{class} })\) 前添加一个可学习的嵌入,其在 Transformer 编码器输出端的状态 \((\mathbf{z}_L^0)\) 用作图像表示 \(\mathbf{y}\)(公式 4)
    • 在预训练和微调期间,一个分类头 (classification head) 被附加到 \(\mathbf{z}_L^0\) 上
    • 分类头在预训练时通过一个带有一个隐藏层的 MLP 实现,在微调时通过一个单线性层实现
  • 位置嵌入 (position embeddings) 被添加到图像块嵌入中以保留位置信息
    • 本文使用标准的可学习 1D 位置嵌入
      • 注:因为本文作者没有观察到使用更高级的 2D 感知位置嵌入能带来显著的性能提升(附录 D.4)
    • 得到的嵌入向量序列作为编码器的输入
  • Transformer 编码器 (2017) 由多头自注意力 (MSA, multiheaded self-attention, 见附录 A) 和 MLP 块的交替层组成(公式 2, 3)
    • 在每个块之前应用层归一化 (LN, Layernorm),在每个块之后应用残差连接 (2019; 2019)
      $$
      \begin{array}{rlr}\mathbf{z}_0 &= [\mathbf{x}_{\text{class} };\mathbf{x}_p^1\mathbf{E};\mathbf{x}_p^2\mathbf{E};\dots ;\mathbf{x}_p^N\mathbf{E}] + \mathbf{E}_{pos}, \quad \quad & \mathbf{E}\in \mathbb{R}^{(P^2\cdot C)\times D},\mathbf{E}_{pos}\in \mathbb{R}^{(N + 1)\times D}\\
      \mathbf{z}’_\ell &= \text{MSA}(\text{LN}(\mathbf{z}_{\ell - 1})) + \mathbf{z}_{\ell - 1}, & \ell = 1\dots L\\
      \mathbf{z}_\ell &= \text{MLP}(\text{LN}(\mathbf{z}’_\ell)) + \mathbf{z}’_\ell, & \ell = 1\dots L\\
      \mathbf{y} &= \text{LN}(\mathbf{z}_L^0)
      \end{array} \tag {4}
      $$
      • 注:这里 MSA 本质就是 Transformer 中的 MHA
Inductive bias,归纳偏置
  • Vision Transformer 的图像特定归纳偏置比 CNN 少得多
    • 在 CNN 中,局部性、二维邻域结构和平移等变性被融入到整个模型的每一层中
    • 在 ViT 中,只有 MLP 层是局部的和平移等变的,而自注意力层是全局的
  • 二维邻域结构的使用非常少:
    • 在模型开始时通过将图像切割成图像块,以及在微调时为不同分辨率的图像调整位置嵌入(如下所述)
  • 除此之外,初始化时的位置嵌入不携带任何关于图像块二维位置的信息,所有图像块之间的空间关系都必须从头学习
Hybrid Architecture
  • 作为原始图像块的替代方案,输入序列可以从 CNN 的特征图中形成 (1989)
  • 在这种混合模型中,图像块嵌入投影 \(\mathbf{E}\)(公式 1)被应用于从 CNN 特征图中提取的图像块
  • 作为一种特殊情况,图像块的空间尺寸可以是 1x1,这意味着输入序列是通过简单地展平特征图的空间维度并投影到 Transformer 维度而获得的
  • 分类输入嵌入和位置嵌入如上所述被添加

Fine-Tuning And Higher Resolution

  • 在大型数据集上预训练 ViT,然后微调到(较小的)下游任务
  • 移除预训练的预测头,并附加一个零初始化的 \(D \times K\) 前馈层
    • \(K\) 是下游类别的数量
    • 理解:此时相当于用前馈层作为分类头,实现多分类任务
  • 以比预训练更高的分辨率进行微调通常是有益的 (2019; 2020)
    • 当输入更高分辨率的图像时,保持图像块大小不变,这会导致更长的有效序列长度
  • 理论上 Vision Transformer 可以处理任意的序列长度(直到内存限制)
    • 但预训练的位置嵌入可能不再有意义
  • 可以考虑根据预训练位置嵌入在原始图像中的位置,对其进行 2D 插值
    • 注:这种分辨率调整和图像块提取是 Vision Transformer 中手动注入关于图像 2D 结构的归纳偏置的唯一两个点

Experiments

  • 本文评估了 ResNet、Vision Transformer (ViT) 以及混合模型的表示学习能力
  • 为了解每个模型的数据需求,本文在不同规模的数据集上进行了预训练,并评估了许多基准任务
  • 在考虑模型预训练的计算成本时,ViT 表现得非常出色,以更低的预训练成本在大多数识别基准上达到了最先进的水平
  • 最后使用自监督进行了一个小型实验,并表明自监督 ViT 为未来带来了希望

Setup

Datasets
  • 为探索模型的可扩展性,使用了
    • 1)包含 1k 个类别和 1.3M 张图像的 ILSVRC-2012 ImageNet 数据集(下文中简称为 ImageNet)
    • 2)1)的超集 ImageNet-21k 包含 21k 个类别和 14M 张图像 (2009)
    • 3)JFT (2017):包含 18k 个类别和 303M 张高分辨率图像
  • 遵循 Kolesnikov 等 (2020) 的做法,针对下游任务的测试集对预训练数据集进行了去重
  • 本文将这些数据集上训练的模型迁移到几个基准任务上:
    • 使用原始验证标签和清理后的 ReaL 标签的 ImageNet (2020),CIFAR-10/100 (2009),Oxford-IIIT Pets (2012),以及 Oxford Flowers-102 (2008)。对于这些数据集,预处理步骤遵循 Kolesnikov 等 (2020)
Model Variants
  • 基于 BERT (2019) 使用的配置来设置 ViT 配置,如表 1 总结
  • “Base”和“Large”模型直接采用自 BERT,本文增加了更大的“Huge”模型
    • 在下文中,本文使用简短的符号来表示模型大小和输入图像块大小:
      • 例如,ViT-L/16 表示“Large”变体,输入图像块大小为 \(16\times 16\)
    • 注:Transformer 的序列长度与图像块大小的平方成反比 ,因此具有更小图像块大小的模型计算成本更高
  • 对于基线 CNN,使用 ResNet (2016),但将批量归一化层 (2015) 替换为组归一化 (2018),并使用了标准化卷积 (2019)
    • 这些修改改善了迁移性能 (2020),将修改后的模型记为“ResNet (BiT)”
  • 对于混合模型,将中间特征图馈送到 ViT,图像块大小为一个“像素”
  • 为了试验不同的序列长度
    • (i) 采用标准 ResNet50 第 4 阶段的输出
    • (ii) 移除第 4 阶段,将相同数量的层放在第 3 阶段(保持总层数不变),并采用这个扩展后的第 3 阶段的输出
      • 注: (ii) 会产生 4 倍长的序列长度,以及一个计算成本更高的 ViT 模型
Training & Fine-tuning
  • 使用 Adam (2015) 训练所有模型,包括 ResNets
    • 设置 \(\beta_{1} = 0.9\),\(\beta_{2} = 0.999\),批大小为 4096,并应用 0.1 的高权重衰减
    • 注:作者实验中发现这对所有模型的迁移都有用(附录 D.1 表明,与常见做法相反,在本文设置中,Adam 对 ResNets 的效果略优于 SGD)
  • 本文使用线性学习率预热和衰减,详见附录 B.1
  • 对于微调,本文对所有模型使用带动量的 SGD,批大小为 512,见附录 B.1.1
  • 对于表 2 中的 ImageNet 结果,本文以更高分辨率进行了微调:
    • ViT-L/16 为 512,ViT-H/14 为 518,并且还使用了 Polyak & Juditsky (1992) 平均,因子为 0.9999 (2019; 2020b)
Metrics
  • 通过 few-shot 或微调准确率报告下游数据集上的结果
    • 微调准确率反映了模型在相应数据集上微调后的性能
    • few-shot 准确率通过求解一个正则化最小二乘回归问题获得,该问题将(冻结的)训练图像子集的表示映射到 \(\{- 1,1\}^{K}\) 目标向量
  • 这种公式化允许作者以封闭形式获得精确解
  • 虽然本文主要关注微调性能,但有时本文会使用线性 Few-shot 准确率进行快速的即时评估,因为微调成本太高
Comparison To State of the art
  • 首先将最大的模型(ViT-H/14 和 ViT-L/16)与文献中最先进的 CNN 进行比较
    • 第一个比较点是 Big Transfer (BiT) (2020)
      • Big Transfer 使用大型 ResNet 进行有监督迁移学习
    • 第二个是 Noisy Student (2020)
      • Noisy Student 是一个大型 EfficientNet,使用半监督学习在 ImageNet 和移除标签的 JFT-300M 上训练
      • 目前,Noisy Student 是 ImageNet 上的 SOTA 模型,BiT-L 是本文报告的其他数据集上的最先进模型
    • 注:所有模型均在 TPUv3 硬件上训练,本文报告了预训练每个模型所需的 TPUv3 核心天数 (TPUv3-core-days),即用于训练的核心数乘以训练天数
  • 表 2 显示了结果
    • 在 JFT-300M 上预训练的较小的 ViT-L/16 模型在所有任务上均优于 BiT-L(在同一数据集上预训练),同时所需的训练计算资源大大减少
    • 更大的模型 ViT-H/14 进一步提升了性能,尤其是在更具挑战性的数据集上(ImageNet、CIFAR-100 和 VTAB 套件)
    • 注:该模型预训练所需计算量仍然远少于先前的先进模型
    • 注:预训练效率不仅可能受到架构选择的影响,还可能受到其他参数的影响,例如训练计划、优化器、权重衰减等
      • 本文在第 4.4 节中对不同架构的性能与计算量进行了受控研究
    • 最后,在公共 ImageNet-21k 数据集上预训练的 ViT-L/16 模型在大多数数据集上也表现良好,同时预训练所需的资源更少:使用一个标准 8 核的云 TPUv3 训练大约需要 30 天
  • 图 2 将 VTAB 任务分解为各自的任务组,并与该基准上之前的 SOTA 方法进行了比较:
    • BiT、VIVI(一个在 ImageNet 和 Youtube 上共同训练的 ResNet (2020))和 S4L(在 ImageNet 上进行有监督加半监督学习 (2019a))
    • ViT-H/14 在自然 (Natural) 和结构化 (Structured) 任务上优于 BiT-R152x4 和其他方法
    • 在专业化 (Specialized) 任务上,前两个模型的性能相似

Pre-training Data Requirements,预训练的数据需求

  • Vision Transformer 在大型 JFT-300M 数据集上预训练时表现良好
    • 与 ResNets 相比,ViT 对视觉的归纳偏置较少,那么数据集大小有多关键?进行了两个系列的实验
第一个实验:在规模递增的数据集上预训练 ViT 模型
  • ImageNet、ImageNet-21k 和 JFT300M
  • 为了提升在较小数据集上的性能,本文优化了三个基本的正则化参数:权重衰减、Dropout 和标签平滑
  • 图 3 显示了微调到 ImageNet 后的结果(其他数据集上的结果见表 5)
    • 当在最小的数据集 ImageNet 上预训练时,尽管进行了(适度的)正则化,ViT-Large 模型的性能仍不如 ViT-Base 模型
      • 使用 ImageNet-21k 预训练时,它们的性能相似
    • 只有在使用 JFT-300M 时,才看到更大模型带来的全部好处
    • 图 3 还展示了不同大小的 BiT 模型所跨越的性能区域
    • BiT CNN 在 ImageNet 上优于 ViT,但随着数据集变大,ViT 实现了反超
第二个实验:在 JFT-300M 数据集的随机子集(9M、30M 和 90M,以及完整数据集)上训练 ViT 模型
  • 本文没有对较小的子集进行额外的正则化,而是在所有设置中使用相同的超参数
    • 注:本文评估的是模型的内在属性,而不是正则化的效果
  • 但本文使用了 early-stopping,并报告训练期间达到的最佳验证准确率
  • 图 4 包含了结果,为了节省计算量,本文报告 Few-shot 线性准确率而非完整的微调准确率
    • 在较小的数据集上,Vision Transformer 比计算成本相当的 ResNet 更容易过拟合
      • 例如,ViT-B/32 比 ResNet50 稍快,且 ViT-B/32 在 9M 子集上的表现要差得多
    • 在 90M+ 子集上表现更好
    • 注:ResNet152x2 和 ViT-L/16 也是如此
    • 这个结果强化了直觉:卷积归纳偏置对较小的数据集有用,但对于较大的数据集,直接从数据中学习相关模式就足够了,甚至是有益的
  • ImageNet 上的 Few-shot 结果(图 4)以及 VTAB 上的低数据量结果(表 2)对于极低数据量的迁移来说似乎很有希望
    • 对 ViT Few-shot 特性的进一步分析是未来工作的一个令人兴奋的方向

Scaling Study

  • 本文通过评估从 JFT-300M 迁移的性能,对不同模型进行了受控的扩展性研究
    • 在这种设置下,数据大小不会成为模型性能的瓶颈,本文评估了每个模型的性能与预训练成本的关系
  • 模型集合包括:
    • 7 个 ResNets,R50x1,R50x2,R101x1,R152x1,R152x2,预训练 7 个 epoch
    • R152x2 和 R200x3 预训练 14 个 epoch
    • 6 个 Vision Transformers,ViT-B/32,B/16,L/32,L/16,预训练 7 个 epoch
    • L/16 和 H/14 预训练 14 个 epoch
    • 5 个混合模型,R50+ViT-B/32,B/16,L/32,L/16 预训练 7 个 epoch
    • R50+ViT-L/16 预训练 14 个 epoch(对于混合模型,模型名称末尾的数字代表的不是图像块大小,而是 ResNet 主干网络中的总降采样率)
  • 图 5 包含了迁移性能与总预训练计算量的对比(关于计算成本的详细信息,请参见附录 D.5)
    • 每个模型的详细结果在附录的表 6 中提供
    • 可以观察到几种模式
      • 第一,在性能/计算量的权衡上,Vision Transformer 主导了 ResNet
        • 为了达到相同的性能(在 5 个数据集上平均),ViT 使用的计算量大约减少 \(2 - 4\) 倍
      • 第二,在小的计算预算下,混合模型的性能略优于 ViT,但对于更大的模型,这种差异消失了
        • 这一结果有点出乎意料,因为人们可能期望卷积局部特征处理能在任何规模下帮助 ViT
      • 第三,在尝试的范围内,Vision Transformer 似乎没有出现饱和(未来前景无限)

Inspecting Vision Transformer,审视 ViT

  • 为理解 Vision Transformer 如何处理图像数据,本文分析了其内部表示
  • Vision Transformer 的第一层将展平的图像块线性投影到一个低维空间(公式 1)
    • 图 7(左)显示了学习到的嵌入滤波器的前几个主成分
      • 这些成分类似于每个图像块内部精细结构的低维表示的合理基函数
  • 在投影之后,一个学习到的位置嵌入被添加到图像块表示中
    • 图 7(中)显示,模型学习了在位置嵌入的相似性中编码图像内的距离,即更近的图像块倾向于具有更相似的位置嵌入
    • 此外:行-列结构出现了
      • 同一行/列的图像块具有相似的嵌入
      • 理解:Position Embedding 看起来已经被隐含的学到了
    • 最后,对于更大的网格,有时会显现出一种正弦结构(附录 D)
      • 位置嵌入学习表示二维图像拓扑结构
        • 这一事实解释了为什么手工制作的二维感知嵌入变体没有带来改进(附录 D.4)
  • 自注意力使 ViT 即使在最底层也能整合整个图像的信息
    • 本文研究了网络在多大程度上利用了这种能力
    • 基于注意力权重计算了整合信息的图像空间中的平均距离(图 7,右)
      • 这个“注意力距离”类似于 CNN 中的感受野大小
  • 发现:
    • 一些头在最底层就已经关注到图像的大部分区域,这表明模型确实使用了全局整合信息的能力
      • 其他注意力头在低层始终具有很小的注意力距离
      • 这种高度局部化的注意力在混合模型(在 Transformer 之前应用了 ResNet)中不太明显(图 7,右),表明它可能起到了与 CNN 中早期卷积层类似的功能
    • 注意力距离随着网络深度增加而增加
    • 从全局来看,模型关注的图像区域与分类任务语义相关(图 6)

Self-Supervision,自监督

  • Transformer 在 NLP 任务中的成功很大程度上不仅源于其出色的可扩展性,还源于大规模的自监督预训练 (2019; 2018)
    • 本文还对用于自监督的掩码图像块预测 (masked patch prediction) 任务进行了初步探索,模仿了 BERT 中使用的掩码语言建模任务
    • 通过自监督预训练,较小的 ViT-B/16 模型在 ImageNet 上达到了 \(79.9\%\) 的准确率,比从头开始训练显著提高了 \(2\%\),但仍比有监督预训练低 \(4\%\)
    • 附录 B.1.2 包含了更多细节
  • 注:本文将对对比预训练 (2020b; 2020; 2019; Hé2020) 的探索留给未来的工作

附录 A:MultiHead Self-Attention,多头自注意力

  • 标准的 qkv 自注意力 (SA, (2017)) 中,对于输入序列 \(\mathbf{z}\in \mathbb{R}^{N\times D}\) 中的每个元素,计算序列中所有值 \(\mathbf{v}\) 的加权和
  • 注意力权重 \(A_{ij}\) 基于序列中两个元素之间的成对相似性及其各自的查询 \(\mathbf{q}^i\) 和键 \(\mathbf{k}^j\) 表示
    $$\begin{array}{rlr}\left[\mathbf{q},\mathbf{k},\mathbf{v}\right] = \mathbf{z}\mathbf{U}_{qkv} & \mathbf{U}_{qkv}\in \mathbb{R}^{D\times 3D_h}, & (5)\ A = \text{softmax}\left(\mathbf{q}\mathbf{k}^\top /\sqrt{D_h}\right) & A\in \mathbb{R}^{N\times N}, & (6)\ \text{SA}(\mathbf{z}) = Av. & & \end{array} \tag {7}$$
  • 多头自注意力 (MSA) 是 SA 的扩展,在其中并行运行 \(k\) 个自注意力操作(称为“头”),并投影它们的拼接输出
    • 为了在改变 \(k\) 时保持计算量和参数数量恒定,\(D_h\) (公式 5) 通常设置为 \(D / k\)
      $$\text{MSA}(\mathbf{z}) = [\text{SA}_1(z);\text{SA}_2(z);\dots ;\text{SA}_k(z)]\mathbf{U}_{msa}\qquad \mathbf{U}_{msa}\in \mathbb{R}^{k\cdot D_h\times D} \tag {8}$$

附录 B:Experiment Details

B.1 Training

  • 表 3 总结了作者针对不同模型的训练设置
    • 发现:在 ImageNet 上从头开始训练模型时,强正则化是关键
    • Dropout(如果使用)在每个密集层之后应用,除了 qkv 投影层以及在将位置嵌入添加到 patch 嵌入之后直接应用
    • 混合模型使用与其对应的 ViT 模型完全相同的设置进行训练
    • 最后,所有训练均在 224 分辨率下进行
  • 表 3:训练的超参数
    • 所有模型均以 4096 的批量大小和 10k 步的学习率预热进行训练
    • 对于 ImageNet,本文发现额外应用全局范数为 1 的梯度裁剪是有益的
    • 训练分辨率为 224
B.1.1 Fine-Tuning
  • 使用带动量为 0.9 的 SGD 微调所有 ViT 模型
  • 本文对学习率进行小范围网格搜索,学习率范围见表 4
  • 使用训练集中的小子集(Pets 和 Flowers 为 10%,CIFAR 为 2%,ImageNet 为 1%)作为开发集,并在剩余数据上进行训练
  • 为了获得最终结果,在整个训练集上进行训练,并在相应的测试数据上进行评估
  • 对于微调 ResNet 和混合模型,使用完全相同的设置,唯一的例外是 ImageNet,在学习率扫描中增加了另一个值 0.06
  • 对于 ResNet,也运行 Kolesnikov 等人 (2020) 的设置,并在此次运行和作者的扫描中选择最佳结果
    • 除非另有说明,否则所有微调实验均在 384 分辨率下运行(以不同于训练的分辨率进行微调是常见做法 (2020))
  • 将 ViT 模型迁移到另一个数据集时,本文会移除整个头(两个线性层),并将其替换为一个零初始化的、输出目标数据集所需类别数的线性层
    • 本文发现这比简单地重新初始化最后一层更稳健一些
  • 对于 VTAB,遵循 Kolesnikov 等人 (2020) 的协议,并对所有任务使用相同的超参数设置
    • 使用 0.01 的学习率并训练 2500 步 (表 4)
    • 通过对两个学习率和两个调度进行小范围扫描,并选择在 200 个示例的验证集上具有最高 VTAB 分数的设置来选定此设置
    • 遵循 Kolesnikov 等人 (2020) 中使用的预处理,除了本文不使用特定于任务的输入分辨率
    • 相反,本文发现 Vision Transformer 从对所有任务采用高分辨率 \((384 \times 384)\) 中获益最多
  • 表 4:微调的超参数
    • 所有模型均使用余弦学习率衰减、批量大小为 512、无权重衰减以及全局范数为 1 的梯度裁剪进行微调
    • 除非另有说明,否则微调分辨率为 384
B.1.2 Self-Supervision
  • 采用掩码 patch 预测目标进行初步的自监督实验
    • 破坏 \(50\%\) 的 patch 嵌入,通过将其嵌入替换为可学习的 [mask] 嵌入 \((80\%)\)、随机的其他 patch 嵌入 \((10\%)\) 或保持不变 \((10\%)\)
      • 注:此设置与 Devlin 等人 (2019) 用于语言的设置非常相似
    • 使用每个被破坏 patch 的相应 patch 表示来预测其 3 位平均颜色(即总共 512 种颜色)
  • 在 JFT 上以 4096 的批量大小训练了自监督模型 100 万步(约 14 个 epoch)
    • 使用 Adam,基础学习率为 \(2 \cdot 10^{-4}\),预热 10k 步并采用余弦学习率衰减
    • 作为预训练的预测目标,本文尝试了以下设置:
      • 1)仅预测平均 3 位颜色(即 512 种颜色的 1 个预测)
      • 2)并行预测 \(16 \times 16\) 个 patch 的 \(4 \times 4\) 下采样版本及 3 位颜色(即 16 个 512 种颜色的预测)
      • 3)使用 L2 对整个 patch 进行回归(即对 3 个 RGB 通道进行 256 次回归)
      • 令人惊讶的是,所有方法都运行良好(L2 稍差一些)
    • 注:本文仅报告选项 1)的最终结果,因为它显示出最佳的 few-shot 性能
      • 本文还尝试了 Devlin 等人 (2019) 使用的 \(15\%\) 破坏率,但根据本文的 few-shot 指标,结果也稍差
  • 最后,作者指出,本文掩码 patch 预测实例化不需要大量的预训练也不需要像 JFT 这样的大型数据集就能在 ImageNet 分类上带来类似的性能提升
    • 也就是说,在 10 万预训练步骤后下游性能的提升出现递减,并且在 ImageNet 上进行预训练时也看到了类似的提升

附录 C:Additional Results

  • 本文报告了与论文中图表相对应的详细结果
  • 表 5 对应论文中的图 3,显示了在不同规模的数据集(ImageNet、ImageNet-21k 和 JFT-300M)上预训练的不同 ViT 模型的迁移性能
  • 表 6 对应论文中的图 5,显示了不同规模的 ViT、ResNet 和混合模型的迁移性能,以及它们预训练的预估计算成本

附录 D:Additional Analysis

D.1 SGD vs. Adam for ResNets

  • ResNet 通常使用 SGD 进行训练,而本文使用 Adam 优化器则非常规
  • 本文比较了在 JFT 上用 SGD 和 Adam 预训练的两个 ResNet(50x1 和 152x2)的微调性能
    • 对于 SGD,本文使用 Kolesnikov 等人 (2020) 推荐的超参数
  • 结果呈现在表 7 中
    • Adam 预训练在大多数数据集上和平均表现上均优于 SGD 预训练
    • 这证明了选择 Adam 作为在 JFT 上预训练 ResNet 的优化器的合理性
    • 注:绝对数值低于 Kolesnikov 等人 (2020) 报告的值,因为本文仅预训练了 7 个 epoch,而不是 30 个

D.2 Transformer Shape

  • 本文对 Scaling Transformer 架构的不同维度进行了消融实验,以找出哪些维度最适合扩展到非常大的模型
  • 图 8 显示了不同配置下 ImageNet 上的 5-shot 性能
    • 所有配置均基于一个具有 8 层、\(D = 1024\)、\(D_{MLP} = 2048\) 和 patch 大小为 32 的 ViT 模型,即所有线的交点
    • 结论:
      • Scaling 深度带来的改进最大,直到 64 层都非常明显
        • 但在 16 层之后已经可以看到收益递减,开始明显,后续逐步变成对数趋势
      • 缩放网络的宽度似乎带来的变化最小
      • 减小 patch 大小从而增加有效序列长度,显示出惊人地稳健的改进,且未引入参数
        • 注:图中 Patch Size 应该是表达计算成本为 X 轴的,不是其绝对值
    • 这些发现表明,计算量可能比参数数量更能预测性能,并且如果可能的话,缩放应侧重于深度而非宽度
      • 总的来说,按比例缩放所有维度会带来稳健的改进

D.3 Head Type and Class Token

  • 为尽可能接近原始的 Transformer 模型
    • 本文使用了额外的 [class] token,并将其作为图像表示
    • 然后通过一个小型 MLP 将该 token 的输出转换为类别预测,该 MLP 在单个隐藏层中使用 tanh 作为非线性激活函数
  • 此设计继承自用于文本的 Transformer 模型,本文在整篇论文中都使用它
    • 最初尝试仅使用图像 patch 嵌入,对其进行全局平均池化 (GAP),然后接一个线性分类器(就像 ResNet 的最终特征图一样),效果非常差
    • 本文发现:
      • 这既不是由于额外的 token,也不是由于 GAP 操作
      • 相反,性能差异完全可以通过对不同学习率的需求来解释,详见图 9

D.4 Position Embedding

  • 本文对使用位置嵌入编码空间信息的不同方式进行了消融实验
  • 本文尝试了以下情况:
    • 不提供位置信息 (Providing no positional information) :将输入视为一个无序的 patch 集合 (a bag of patches)
    • 一维位置嵌入 (1-dimensional positional embedding) :将输入视为按光栅扫描顺序 (raster order) 排列的 patch 序列(本文所有其他实验的默认设置)
    • 二维位置嵌入 (2-dimensional positional embedding) :将输入视为二维平面上的 patch 网格
      • 在这种情况下,学习两组嵌入,每组对应一个坐标轴,即 X 轴嵌入 (X-embedding) 和 Y 轴嵌入 (Y-embedding),每个的维度大小为 \(D/2\)。然后,根据 patch 在输入中的坐标,作者拼接 X 和 Y 的嵌入来得到该 patch 的最终位置嵌入
    • 相对位置嵌入 (Relative positional embeddings) :考虑 patch 之间的相对距离而不是它们的绝对位置来编码空间信息
      • 使用一维相对注意力 (1-dimensional Relative Attention),其中定义了所有可能的 patch 对之间的相对距离
      • 对于每一对给定的 patch(一个作为 query,另一个作为注意力机制中的 key/value),有一个偏移量 \(p_q - p_k\),每个偏移量关联一个嵌入
      • 然后简单地运行一个额外的注意力,使用原始的 query(query 的内容),但使用相对位置嵌入作为 keys
      • 然后将来自相对注意力的 logits 作为一个偏置项 (bias term),并在应用 softmax 之前将其加到主注意力(基于内容的注意力)的 logits 上
  • 除了不同的空间信息编码方式,本文还尝试了将这些信息整合到模型中的不同方法
  • 对于一维和二维位置嵌入,本文尝试了三种不同的情况:
    • (1) 在模型的主干(Stem)之后,将输入馈送到 Transformer 编码器之前(本文中所有其他实验的默认设置)
    • (2) 在每层开始时学习并添加位置嵌入到输入中
    • (3) 在每层开始时添加学习到的位置嵌入到输入中(层之间共享)
  • 表 8 总结了在 ViT-B/16 模型上进行的此消融研究的结果
    • 没有位置嵌入的模型与有位置嵌入的模型性能之间存在巨大差距,但不同编码位置信息的方式之间几乎没有区别
    • 推测:ViT 的 Transformer 编码器在 patch 级别的输入上操作,而不是像素级别,因此如何编码空间信息的差异不那么重要
    • 更准确地说,在 patch 级别的输入中,空间维度远小于原始像素级别的输入,例如 \(14 \times 14\) 对比 \(224 \times 224\),并且在这种分辨率下学习表示空间关系对于这些不同的位置编码策略来说同样容易
      • 即便如此,网络学习到的位置嵌入相似性的具体模式也取决于训练超参数(图 10)

D.5 Empirical Computation Costs,计算成本

  • 计算成本详情见原始论文

D.6 Axial Attention

  • 轴向注意力 (Axial Attention) (2020;2019) 是一种简单而有效的技术,用于在组织为多维张量的大型输入上运行自注意力
    • 轴向注意力的一般思想是执行多个注意力操作,每个操作都沿着输入张量的单个轴进行,而不是将一维注意力应用于输入的扁平化版本
    • 在轴向注意力中,每个注意力沿着特定轴混合信息,同时保持其他轴上的信息独立
  • 沿着这个思路,Wang 等人 (2020b) 提出了 AxialResNet 模型,其中 ResNet50 中所有 \(3 \times 3\) 卷积核大小的卷积都被轴向自注意力(即行和列注意力)取代,并辅以相对位置编码
    • 本文已经实现了 AxialResNet 作为基线模型
  • 此外,本文修改了 ViT 以处理二维形状的输入,而不是一维的 patch 序列,并合并了轴向 Transformer 块 (Axial Transformer blocks)
    • 其中,本文不是使用一个自注意力后接一个 MLP,而是使用一个行自注意力加一个 MLP,后接一个列自注意力加一个 MLP
  • 图 13 展示了在 JFT 数据集上预训练的 Axial ResNet、Axial-ViT-B/32 和 Axial-ViT-B/16 在 ImageNet 5-shot 线性评估上的性能,与预训练计算量的关系,计算量以 FLOPs 数量(左)和推理时间(每秒样本数,右)两种形式表示
    • Axial-ViT-B/32 和 Axial-ViT-B/16 在性能方面均优于其对应的 ViT-B 模型,但这需要更多的计算量
      • 这是因为在 Axial-ViT 模型中,每个具有全局自注意力的 Transformer 块被两个轴向 Transformer 块(一个用于行自注意力,一个用于列自注意力)取代,并且尽管轴向情况下自注意力操作的序列长度较小,但每个 Axial-ViT 块中多了一个 MLP
    • 对于 AxialResNet,尽管在准确性/计算量权衡方面看起来合理(图 13,左),但在 TPU 上,简单的实现极其缓慢(图 13,右)

D.7 Attention Distance

  • 为了理解 ViT 如何使用自注意力跨图像整合信息,本文分析了不同层注意力权重所跨越的平均距离(图 11)
    • 此“注意力距离”类似于 CNN 中的感受野大小
  • 在较低层中,不同头之间的平均注意力距离差异很大,一些头关注图像的大部分区域,而另一些头则关注查询位置附近或周围的小区域
    • 随着深度增加,所有头的注意力距离都增加
      • 注:图 11 的纵轴是每个 Head 的 Attention 距离的平均观测值
    • 在网络的后半部分,大多数头广泛地关注各个 token
  • 图 11:按头和网络深度划分的注意力区域大小
    • 通过平均查询像素与所有其他像素之间的距离(按注意力权重加权),为 128 个示例图像计算注意力距离
    • 每个点表示在某一层的 16 个头之一在所有图像上的平均注意力距离
    • 图像宽度为 224 像素

D.8 Attention Maps

  • 为计算从输出 token 到输入空间的注意力图(图 6 和 14),本文使用了 Attention Rollout (2020)
    • 简而言之,我们平均 ViT-L/16 在所有头上的注意力权重,然后递归地乘以所有层的权重矩阵
    • 这说明了注意力在所有层中跨 token 的混合

D.9 ObjectNet Results

  • 本文还按照 Kolesnikov 等人 (2020) 的评估设置,在本文的旗舰模型 ViT-H/14 上评估了 ObjectNet 基准,获得了 \(82.1\%\) 的 Top-5 准确率和 \(61.7\%\) 的 Top-1 准确率

D.10 VTAB Breakdown

  • 表 9 显示了在 VTAB-1k 的每个任务上获得的分数

RL——IQL

  • 参考链接
    • 原始论文:ICLR 2022 Poster, Offline reinforcement learning with implicit q-learning
    • 相关论文:(AWR)ADVANTAGE-WEIGHTED REGRESSION: SIMPLE AND SCALABLE OFF-POLICY REINFORCEMENT LEARNING

IQL 的基本思想

  • 常规的方法会直接约束策略或者正则来减少OOD问题,IQL则通过SARSA style的方法仅在见过的state-action上进行学习,不直接面对OOD问题
  • 策略学习使用了AWR(Advantage Weighted Regression)方法

多步动态规划和 Single-step 方法

多步动态规划(Multi-step DP)

  • 多步动态规划方法(multi-step dynamic programming methods,简写作Multi-step DP)
  • 已有Offline RL方法的很大一部分是基于约束或正则化的近似动态规划(例如,Q-learning 或 actor-critic 方法),constraint或Regularization用于限制与行为策略的偏差。 我们将这些方法称为多步动态规划(Multi-step DP)算法,因为它们对多次迭代执行真正的动态规划,因此如果提供高覆盖率数据,原则上可以恢复最优策略。通常情况下Multi-step DP问题也可以分为:
    • 显式密度模型(explicit density model):BRAC,BCQ,BEAR等
    • 隐式差异约束(implicit divergence constraints):AWAC,CRR,AWR等
  • 如何理解显示密度模型和隐式约束模型的定义?
    • 显式密度模型:直接建模State-Action的价值分布,从而得到最优策略
    • 隐式差异约束:不直接建模State-Action的价值分布,更多是模仿优质策略行为的思想
  • 问题:显示密度模型中的“密度”是什么意思?
    • 这里的密度是指概率密度,显示密度模型即会直接定义并学习概率密度函数的模型

Single-step 方法

  • Single-step 方法(Single-step Methods)是指一类方法,这类方法仅依赖于单步策略迭代的方法,即对行为策略的价值函数或Q函数进行拟合,然后提取相应的贪心策略,或者完全避免价值函数并利用行为克隆目标
  • 这类方法避免了访问看不见的状态动作对,因为它们要么根本不使用价值函数,要么学习行为策略的价值函数
  • IQL 就是一种 Single-step 方法
  • 传统的模仿学习也属于 Single-step 方法

多步动态规划和 Single-step 方法的比较

  • from https://zhuanlan.zhihu.com/p/497358947

IQL 之前的方案

一般的 Offline RL 学习方法

  • 思路:按照贝尔曼最优方程迭代
  • 损失函数:
    $$
    L_{TD}(\theta) = \mathbb{E}_{(s,a,s’) \sim D} \left[ (r(s, a) + \gamma \max_{a’} Q_{\theta’}(s’, a’) - Q_\theta(s, a))^2 \right]
    $$
  • 分析:
    • 直接使用上述损失函数存在值高估问题
    • 大多数最近的离线RL方法修改了上述值函数损失(或直接约束argmax这个策略本身选择动作的方位),以正则化值函数,使其生成的策略接近数据,缓解值高估问题

能避免 OOD 的学习方法

  • 思路:按照SARSA-style的方法迭代,即贝尔曼期望方程( \(a’\sim \pi_\beta\) )
  • 损失函数:SARSA-style的损失函数如下
    $$
    L(\theta) = \mathbb{E}_{(s,a,s’,a’) \sim D} \left[ (r(s, a) + \gamma Q_{\theta’}(s’, a’) - Q_\theta(s, a))^2 \right]
    $$
    • 按照上面的损失函数学习,学到的 \(Q_\theta(s,a)\) 本质是行为策略对应的Q值,也就是说,当样本无限时,Q值收敛到
      $$
      Q_\theta^*(s, a) \approx r(s, a) + \gamma \mathbb{E}_{s’ \sim p(\cdot|s,a), a’ \sim \pi_\beta(\cdot|s’)} \left[ Q_{\theta’}(s’, a’) \right]
      $$
  • 分析:
    • 本质上是在估计数据集上的状态和动作分布下,Q值的期望
    • 显然上面学到的只是行为策略对应的Q值,不是我们想要的最优Q值(行为策略不一定是最优策略)
    • 上面的方法更像是在对行为策略进行模仿

Offline RL 的最优 Q 值目标

  • 思路:避免OOD且能学到“最优策略”的迭代形式,限制了argmax动作不访问OOD的状态动作对
  • 损失函数:
    $$
    L(\theta) = \mathbb{E}_{(s,a,s’) \sim D} \left[ (r(s, a) + \gamma \max_{a’ \in A, \pi_\beta(a’|s’) > 0} Q_{\theta’}(s’, a’) - Q_\theta(s, a))^2 \right]
    $$
  • 分析:
    • 既保证使用的最大Q值对饮动作不超过数据集(避免了OOD),又可以在支持集上最大化当前策略
    • 上面的定义实际上也可能访问到支持集以外的动作,后续需要使用期望回归来改进为SARSA-style的形式
  • 注意:IQL 并不直接学习上述目标( \(\pi_\beta(a’|s’) > 0\) 导致无法学习),只是隐式的学习上述目标 ,具体方法是引入期望回归(Expectile Regression)
    • BCQ等方法已经学习过上述目标的改进版本
    • 上述目标无法直接学习,因为判断 \(\pi_\beta(a’|s’) > 0\) 需要维护一个表格,统计所有数据,状态动作空间很大时无法实现,除非像BCQ一样,用一个网络去学习概率

IQL 的解决方案

期望回归与分位数回归

  • 期望回归(Expectile Regression) ,是估计随机变量的各种统计量的方法,定义如下:

    • 某个随机变量 \(X\) 的 \(\tau \in (0, 1)\) 期望值定义为以下非对称最小二乘问题的解:
      $$
      \mathop{\arg\min}_{m_\tau} \mathbb{E}_{x \sim X} \left[ L_\tau^2(x - m_\tau) \right], \quad \text{ Where } \quad L_\tau^2(u) = |\tau - 1(u < 0)| u^2.
      $$
    • \(L_\tau^2(u)\) 也常常写作 \(L_\tau^e(u)\)
    • 给定 \(\tau\), \(m_\tau\) 就是在拟合随机变量的某个 \(\tau\) 期望点,不同的 \(\tau\) 下 \(m_\tau\) 也会不同,学到的,比如 \(\tau=0.5\) 时就是对应期望
    • 分析:
      • 当 \(\tau > 0.5\) 时,这种非对称损失函数会降低小于 \(m_\tau\) 的 \(x\) 值的权重,而增加大于 \(m_\tau\) 的 \(x\) 值的权重
      • 当 \(\tau = 0.5\) 时,损失函数退化成对称的,等价于均方误差MSE(这里把 \(u\) 看做是误差项)
        $$ L^{\tau=0.5}_{2}(u) = |0.5 - \Bbb{1}(u<0)|u^2 = \frac{1}{2}u^2 $$
  • 条件随机变量的期望回归

    • 对于给定的条件随机变量 \(y = f(x)\),假定 \((x,y)\) 成对出现在数据集 \(\mathcal{D}\) 中,则可以定义:
      $$\mathop{\arg\min}_{m_\tau(x)} \mathbb{E}_{(x,y) \sim \mathcal{D}} \left[ L_\tau^2(y - m_\tau(x)) \right]$$
    • 给定 \(\tau\), \(m_\tau(x)\) 是一个关于 \(x\) 的函数,不同的 \(\tau\) 得到的拟合函数不同,相同的 \(\tau\),给定不同的 \(x\) 会得到不同的 \(m_\tau(x)\), \(m_\tau(x)\) 本质是在拟合 \(y\),下图中最右侧的图展示了条件随机变量的期望回归
  • 分位数回归(Quantile Regression)定义如下:
    $$
    \mathop{\arg\min}_{m_\tau} \mathbb{E}_{x \sim X} \left[ L_\tau^1(x - m_\tau) \right], \quad \text{ Where } \quad L_\tau^1(u) = (\tau - 1(u < 0)) u.
    $$

    • \(L_\tau^1(u)\) 也常常写作 \(L_\tau^q(u)\)
    • \((\tau - 1(u < 0)) u\) 不使用绝对值的原因是此时无论 \(u\) 取值正负 \(L_\tau^1(u) \ge 0\) 都成立,相当于已经给整体加了绝对值了,最终目标是类似MAE的形式
  • 分位数回归和期望回归的对比

    • 常规的MSE叫做mean,等价于求均值,等价于 \(\tau = 0.5\) 的期望回归(expectile regression)
    • 常规的MAE叫做median,等价于求中位数,等价于 \(\tau = 0.5\) 的分位数回归(quantile regression)
  • 更多比较

    • 修正:左边第二行需要使用绝对值 \(\mathcal{R}_\tau^e(u) = u^2|\tau - \mathbf{1}(u < 0)|\)
  • 问题:为什么使用期望回归而不是分位数回归?

    • 审稿人也有这个疑问,作者的回答是实验得到的,没有正面给出回答?, \(\tau=0.9\) 时效果最好

基于期望回归的 Q 值学习

  • 借助期望回归来学习Q值:
    $$
    L(\theta) = \mathbb{E}_{(s,a,s’,a’) \sim D} \left[ L_\tau^2(r(s, a) + \gamma Q_{\theta’}(s’, a’) - Q_\theta(s, a)) \right]
    $$
  • 其中 \(\mathcal{D} \sim \pi_\beta\),选择合适的 \(\tau\) 后,可以学到一个大于 \(Q^{\pi_\beta}(s,a)\) (行为策略对应的Q值)的 \(Q(s,a)\)
  • 理解:给定 \((s,a)\) 的情况下,存在许多不同的 \((s’,a’)\) 样本,当 \(\tau > 0.5\) 时,相当于是通过这种非对称损失函数降低小于 \(Q_\theta(s, a)\) 的动作状态对 \((s’, a’)\) 所对应的目标值 \(r(s, a) + \gamma Q_{\theta’}(s’, a’)\) 的权重,增加大于 \(Q_\theta(s, a)\) 的动作状态对 \((s’, a’)\) 所对应的目标值 \(r(s, a) + \gamma Q_{\theta’}(s’, a’)\) 的权重,从而学到较大的 \((s’,a’)\) 对应的目标值,极端情况下,学到的是最大值 \(r(s, a) + \gamma \max_{(s,a,s’,a’) \sim \mathcal{D}} Q_{\theta’}(s’, a’)\)
  • 上面的损失函数还存在一些不足,由于环境可能是动态变化的,状态 \(s’\) 是按照概率 \(p(s’|s,a)\) 出现,所以以上损失函数还使得Q学到了环境转换的信息。具体来说,学到的Q值高不一定是选到了优秀动作的反应,还可能是因为运气好碰上了转移到一个较好的状态 \(s’\) 上
    • 补充说明1:即使是随机环境,在状态 \(s\) 下,选择 \(a\) 后有一定概率得到较优秀的 \(s’\),能说明在状态 \(s\) 下,选择 \(a\) 是较为优秀的吗?回答是不一定!因为在这种随机环境的情况下,最优贝尔曼方程里面,我们也需要对 \(s’\) 计算期望 \(\mathbb{E}_{s’\sim p(s’|s,a)}\) 而不是取最大 \(\max_{s’}\),这是我们的目标是找一个策略,使得按照这个策略交互得到的期望收益最大,而线上推断时,我们不能保证一定能走到最大的 \(s’\),除非是确定性环境,即 \((s,a)\) 确定后, \(s’\) 也是确定的
    • 补充问题1:如果是确定性的环境,是否可以直接使用上述损失函数?

IQL 的 Q 值学习

  • 由于基于期望回归的Q值学习引入了状态转移随机偏差,存在问题,所以需要进行改进:
  • 第一步:使用期望回归去从已知的 \(Q_{\hat{\theta}}(s,a)\) 中学习 \(V(s)\)
    $$ L_V(\psi) = \mathbb{E}_{(s,a) \sim D} \left[ L_\tau^2(Q_{\theta’}(s, a) - V_\psi(s)) \right] $$
    • 这里可以看出 \(V(s)\) 学到的是 \(\max_a Q_{\hat{\theta}}(s,a)\) 的思想,即对应V值的贝尔曼最优方程
  • 第二步:使用最优的 \(V\) 去学习 \(Q\)
    $$L_Q(\theta) = \mathbb{E}_{(s,a,s’) \sim D} \left[ (r(s, a) + \gamma V_\psi(s’) - Q_\theta(s, a))^2 \right] $$
    • 由于 \(V\) 在上一步已经通过期望回归学到了最优形式,这一步不需要继续使用期望回归了
  • 至此,我们已经实现了通过SARSA-style的形式,隐式的学到了近似最优Q值
  • 关于参数 \(\tau\) 的一些分析以及以上贝尔曼方程收敛性见附录

IQL 的策略学习

  • 虽然我们已经得到了近似最优Q值,但为了避免使用样本外的动作,这里做策略学习时,我们不能直接遍历所有动作
  • AWR提供了一种方法从近似最优Q值里面提取策略(因为策略学习并不影响Q值,所以更像是从近似最优Q值中提取策略):
    $$
    L_\pi(\phi) = \mathbb{E}_{(s,a) \sim D} \left[ \exp(\beta (Q_{\theta’}(s, a) - V_\psi(s))) \log \pi_\phi(a|s) \right]
    $$
    • 其中 \(\beta \ge 0\) 是温度系数。对于较小的超参数值,该目标类似于行为克隆(近似所有样本权重相等的策略梯度,原始策略梯度中,样本权重是温度系数为1的Q值),而对于较大的值,它试图恢复Q函数的最大值(Q值越大,对应的样本权重越大)。正如AWR等先前工作所示,此目标学习一个在分布约束下的最大化Q值的策略
  • 注意,策略学习时Q值收敛以后进行的(Q和V是交替更新),Q值学习和策略学习是串行的,且Q值学习彻底完成以后才进行策略学习,并不是交替进行
  • 思考:使用期望回归学到的 \(V\) 值是 \(V^{\pi^*} = \max_a Q_{\hat{\theta}}(s,a)\),为什么可以用最优的 \(V\) 值来更新策略 \(Q_{\theta’}(s, a) - V_\psi(s)\) ?
    • 这种做法是可以的,Q值和V值符合优势函数的定义,因为传统优势函数的定义也是 \(A^\pi(s,a) = Q^\pi(s,a) - V^\pi(s)\),其中 \(V^\pi(s) = \mathbb{E}_{a \sim \pi(\cdot|s)}[Q^\pi(s,a)]\),看似与 IQL 中学到的 \(V\) 值不同,但此时将当前Q值和V值对应策略 \(\pi(a|s)\) 理解为选择Q值最大的动作或近似动作,实际上 \(Q\) 值和 \(V\) 值都满足传统的优势函数了
    • 理解 :(即使不满足原始优势函数)虽然此时的 \(V\) 值是 \(\max_a Q_{\hat{\theta}}(s,a)\),但是 \(Q_{\theta’}(s, a) - V_\psi(s)\) 依然可以对动作的好坏进行区分。实际上,只要可以保证动作越好,优势函数越大即可,即使所有动作都是负的或者都是正的也没问题,因为策略的实现是一个softmax,大家都降低的时候,降的少的动作上对一个的概率自然会提升。实践也告诉我们,\(V\) 值是否是当前状态下动作的期望结果并不重要
    • 特别说明 :AWR 中使用的 \(V\) 值是从历史样本的累计奖励上学习的,相当于是历史样本上的期望,也就是行为策略 \(\mu\)(多轮迭代下可能是混合策略)对应的 \(V^\mu\) 值,AWR 的整个推导中奖励 \(\mathcal{R}^\mu_{\mathbf{s},\mathbf{a}}\) 和 \(V^\mu\) 值都是使用行为策略 \(\mu\) 来表示的,奖励使用的是蒙特卡洛估计 \(\mathcal{R}^D_{\mathbf{s},\mathbf{a}} = \sum_{t=0}^T\gamma^t r_t\)

IQL 训练流程

  • 伪代码如下(说明:伪代码中最后一行策略更新公式有问题,应该是加号,或者把损失函数添上负号,因为这里是想要最大化目标, 作者开源代码中是正确的github.com/ikostrikov/implicit_q_learning,论文中写错了):

附录:为什么 AWR 和策略梯度法损失函数不同?

  • 副标题:不同AC框架算法策略更新公式对比分析,为什么相同的目标推导出来完全不同的更新公式?
  • 问题补充:
    • 普通AC(策略梯度法)更新公式是:
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{(s,a) \sim \pi_{\theta_k}}\Big[(Q^{\pi_{\theta_k}}(s,a)-V^{\pi_{\theta_k}}(s))\log\pi_\theta(a|s)\Big]$$
    • PPO更新公式:
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{(s,a) \sim \pi_{\theta_k}}\Big[\frac{\pi_\theta(a|s)}{\pi_{\theta_k}(a|s)} A^{\pi_{\theta_k}}(s,a) - \beta D_{KL}(\pi_{\theta_{k}}(\cdot|s), \pi_\theta(\cdot|s))\Big]$$
    • DDPG更新公式
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{s_t \sim \rho^\beta(s)} [Q_w(s_t,\mu_\theta(s_t))] $$
    • SAC更新公式
      $$\mathop{\arg\max}_{\theta}\mathbb{E}_{s_t \sim \mathcal{D}, \epsilon_t \sim \mathcal{N}}[\log \pi_\theta(f_\theta(\epsilon_t;s_t)\vert s_t) - Q_\theta(s_t, f_\theta(\epsilon_t; s_t))]$$
    • AWR更新公式:
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{(s,a) \sim \pi_\beta}\Big[exp\Big(\frac{1}{\beta}(R_{s,a}^{\mathcal{D}}-V^{\mathcal{D}}(s))\Big)\log\pi_\theta(a|s)\Big]$$
      • 其中 \(R_{s,a}^{\mathcal{D}} = \sum_{t=0}^\infty \gamma^t r_t\),不是网络,是真实的轨迹收益
    • IQL更新公式:
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{(s,a) \sim \pi_\beta}\Big[exp\Big(\beta (Q_{\theta’}(s, a) - V_\psi(s))\Big)\log\pi_\theta(a|s)\Big]$$
    • AWAC更新公式:
      $$\mathop{\arg\max}_{\theta} \mathbb{E}_{(s,a) \sim \pi_\beta}\Big[exp(\frac{1}{\lambda} A^{\pi_{\theta_k}}(s,a))\log\pi_\theta(a|s)\Big]$$
  • 基本推导思路总结:
    • 策略梯度法 :推导是直接从最初目标出发,视图求最初目标相对策略的梯度
    • PPO :更新公式是从策略提升的视角出发得到梯度提升的目标,通过限制策略变化幅度和重要性采样分别将未知策略的状态和动作采样的问题切换到已知策略
    • DDPG :直接以最大化Q值为目标来更新,可直接传导策略梯度
    • SAC :的目标中增加了熵,可以看成是DDPG的增加熵的版本
    • AWR、IQL和AWAC :更新公式都是相同的形式,是从策略提升的视角出发得到梯度提升的目标,并对该目标进行推导,得到最终的最优策略形式,再带入最优策略形式,从而得到更新公式
  • 也就是说,AWR、IQL和AWAC这三个方法的目标是为了策略提升量最大化 ,而策略梯度法的目标是为了原始目标最大化(梯度提升法)

附录:为什么 IQL 效果比 AWR 好?

  • IQL和 AWR 的 Q 值是不同策略的优势函数,IQL 的优势函数是在 \(\tau\) 分位点期望动作策略分布上的 Q 和 V,即 \(A^{\pi^*}(s,a) = Q^{\pi^*}(s,a) - V^{\pi^*}(s)\),而AWR的优势函数是真实的轨迹回报和V值 \(A^{\pi_k}(s,a) = R_{s,a}^{\mathcal{D}} - V^{\pi_k}(s)\)
  • IQL 不是迭代训练,是先学好 Q 值(不依赖策略),再利用学好的 Q 值一次性提取策略
  • 标准的 AWR 是 off-policy 的,是一种迭代训练的流程,V 值学习依赖策略与环境交互的轨迹数据,策略学习也依赖上一步的V值,V值,策略,轨迹三者是不断优化的
  • 如果把 AWR 直接用到 Offline R L场景下,则不再与环境交互,AWR 退化到学习一次V值,接着一次性学习策略;
    • Offline RL 下学到的 V 值是行为策略对应的 V 值,不是最优的 V 值,但这本身应该没有问题
    • 基于统计的 \(R_{s,a}^{\mathcal{D}}\) 方差可能很大
  • 使用公式 \(L_\pi(\phi) = \mathbb{E}_{(s,a) \sim D} \left[ \exp(\beta (Q_{\theta’}(s, a) - V_\psi(s))) \log \pi_\phi(a|s) \right]\) 来迭代策略时,Q 值和 V 值应该使用什么样的才是最优的?
    • 这个公式是从最大化策略提升项得到的,在推导策略提升时,这里使用的A值(对应到Q值和V值)是上一步策略对应的值 \(A^\mu(s,a)\),即旧策略 \(\mu\) 对应 Q 值和 V 值,而我们的目标是在 \(\mu\) 的基础上有所提升,得到优秀的新策略 \(\pi\),所以 Q 值和 V 值最好是优秀的策略对应的Q值和V值,否则可能我们的策略 \(\pi\) 在不好的策略上提升,结果也可能不是很优秀
  • 补充问题:可以随便使用一个策略来评估优势函数吗?
    • 回答是不可以,因为不同策略下,A 值选择不同动作以后的值是不同的,显然学到的策略也不同,从推导看,必须使用上一步的才可以

附录:贝尔曼方程收敛性及 \(\tau\) 的分析

  • 关于参数 \(\tau\) 的一些分析,原始论文中关于 \(\tau\) 的分析如下:

  • 当 \(\tau = 0.5\),相当于是SARSA算法;当 \(\tau \rightarrow 1\),相当于是Q-Learning算法

  • 对于任意的 \(\tau\),Q值和V值迭代都会收敛,且Q值和V值会收敛到 \(Q_{\tau}(s,a)\) 和 \(V_{\tau}(s)\),Lamma1中最后两行就是两者的贝尔曼方程,其中 \(\mathbb{E}_{a \sim \mu(\cdot|s)}^\tau\) 表示 \(\mu(\cdot|s)\) 分布下的 \(\tau\) 期望分位值(或 \(\tau\) 阶期望分位数)。注意,我们在说分位数时,还需要说明是那个随机变量或者哪个分布的分位数,否则没有意义

  • 为什么说Q值和V值迭代都会收敛到 \(Q_{\tau}(s,a)\) 和 \(V_{\tau}(s)\) 呢?

    • 理解:这里的 \(\tau\) 期望分位动作可以视作是一个策略,每次选择动作时,不选择最优动作,也不选择随机动作,而是选择 \(\tau\) 期望分位点动作,这样,可以得到跟论文中一样的结论:当 \(\tau = 0.5\),相当于是SARSA算法;当 \(\tau \rightarrow 1\),相当于是Q-Learning算法
    • 证明:定义一个策略如下:
      $$\pi_\tau(s) = \mathop{\text{arg_expectile}^\tau}_a(Q(s,a))$$
      该策略表示在状态 \(s\) 下,该策略会选择使得Q值等于 \(Q(s,a)\) 关于动作 \(a\) 的 \(\tau\) 期望分位点的动作,则期望分位动作策略对应的贝尔曼方程跟普通策略下的贝尔曼方程没有区别
    • 更详细的来说:
      • Q值:假定已经有了 \(V_\tau(s’)\),此时Q值的更新是学习当前状态 \(s\) 下,按照当前状态对应的 \(\tau\) 期望分位动作,以及后续策略也采用 \(\tau\) 期望分位动作得到的价值 \(V_\tau(s’)\) 来进行拟合的目标值(注意,这里跟其他贝尔曼方程一样,一旦动作决定了, \(r(s,a)\) 就确定了,我们所说的期望分位动作就是对动作 \(a\) 的分布而言的, \(Q(s,a)\) 的拟合只考虑 \((s,a)\) 状态动作对即可,不需要考虑期望分位动作);
      • V值:假定已经有了 \(Q_{\tau}(s,a)\),V值可以从 \(Q_{\tau}(s,a)\) 中学到 \(V_\tau(s’)\),这里需要使用 \(Q_{\tau}(s,a)\) 而不是 \(Q_{\pi_\beta(s,a)}\) 的原因是,V的本质是 \(Q(s,a)\) 关于动作 \(a\) 期望,但直接求期望只到了当前状态 \(s\) 这一层,如果使用 \(Q_{\pi_\beta(s,a)}\) 来学习那么学到的不是 \(V_\tau(s’)\) ( \(V_\tau(s’)\) 是指后续的动作也是 \(\tau\) 期望分位动作来定义的,正如Q值和V值的常规贝尔曼方程一样)

Implicit 名字的来源

  • Implicit 含义是“隐式的”,与隐式约束的隐式不等价,在IQL中表示通过期望回归隐式的学到了最优价值函数 \(V^*(s) = \max Q(s,a)\)

IQL 可能存在的问题

  • IQL 没有没有像 CQL 一样对非行为策略的 Q 值进行打压(甚至学习过程中全程未学习未知状态动作对的 Q 值),也没有像 BCQ 一样对动作选择进行限制,理论上可能会因为对 OOD 状态动作 Q 值高估而出现问题
  • IQL 源码实现时的解法:采用 Twin Q 来缓解高估问题(理解:对于数据集中存在的,两个 Q 网络都能估准;对于数据集中不存在的,可能都估不准,但是我们取最小的那个,可以缓解对未知状态动作对 Q 值的高估问题)

Python——Ray-分布式架构简单了解


整体介绍

  • Ray 是一个用于分布式计算的开源框架,专为构建和运行分布式应用程序而设计
  • Ray 提供了简洁的 API,让开发者能够轻松地将单机程序扩展到分布式集群上,同时保持代码的可读性和可维护性
  • Ray 最初由 UC Berkeley 的 RISELab 开发,现在由 Anyscale 公司维护,广泛应用于机器学习、强化学习、并行计算等领域
  • Ray 既可以在本地实现并行计算,又可以非常容易的扩展到集群模式,实现分布式计算
  • Ray 与深度学习框架如 TensorFlow、PyTorch 和 MXNet 等互相兼容

Ray 的核心架构

  • Ray的系统架构采用了混合任务调度的思路,遵循典型的 Master-Slave 设计,但与传统分布式系统有所不同

Ray 中的关键组件总结

  • Ray在集群部署模式下启动了以下关键组件:
    • GlobalScheduler(全局调度器) :运行在Master节点上,负责接收本地调度器提交的任务,并将任务分发给合适的本地任务调度器执行
    • RedisServer :Master节点上启动的Redis服务器,用于保存分布式任务的状态信息(ControlState),包括对象机器的映射、任务描述、任务 debug 信息等
    • LocalScheduler(本地调度器) :每个 Slave 节点上启动的本地调度器,用于提交任务到全局调度器,以及分配任务给当前机器的 Worker 进程
    • Worker进程 :每个 Slave 节点上可以启动多个 Worker 进程执行分布式任务,并将计算结果存储到 ObjectStore
    • ObjectStore(对象存储) :每个 Slave 节点上的存储系统,用于存储只读数据对象,Worker 可以通过共享内存的方式访问这些对象数据,有效减少内存拷贝和对象序列化成本。ObjectStore 底层由 Apache Arrow 实现
    • Plasma :每个 Slave 节点上的ObjectStore管理器,当 Worker 访问本地 ObjectStore 上不存在的远程数据对象时,Plasma 会主动拉取其它 Slave 上的对象数据到当前机器

执行模型

  • Ray的执行模型基于动态任务图 ,这与 TensorFlow 中的静态计算图有本质区别:
    • TensorFlow的计算图用于表征神经网络,在单个应用中执行很多次
    • Ray的任务图用于表征整个应用,并仅执行一次
    • 任务图对于前台是未知的,随着应用的运行而动态地构建
    • 一个任务的执行可能创建更多的任务,形成动态依赖关系

代码示例

并行计算示例(无状态)

  • 基于 Ray 的并行计算代码 Demo:
    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
    import ray
    import time
    import numpy as np

    # 初始化 Ray,默认在本地启动
    ray.init()

    # 使用 @ray.remote 装饰器将函数转换为分布式任务
    @ray.remote
    def compute_square(x):
    # 模拟耗时计算
    time.sleep(1)
    return x * x

    # 生成一些数据
    data = np.arange(10)

    # 并行执行任务
    start_time = time.time()
    # 创建任务对象引用
    square_refs = [compute_square.remote(i) for i in data]
    # 等待所有任务完成并获取结果
    results = ray.get(square_refs)
    end_time = time.time()

    print(f"串行计算结果: {[i*i for i in data]}")
    print(f"Ray 并行计算结果: {results}")
    print(f"Ray 并行计算耗时: {end_time - start_time:.4f} 秒")

    # 关闭 Ray
    ray.shutdown()

    # 串行计算结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    # Ray 并行计算结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    # Ray 并行计算耗时: 1.4069 秒

串行计算示例(有状态)

  • 基于 Ray 的串行计算代码 Demo:
    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
    import ray
    import time

    # 初始化 Ray
    ray.init()

    # 使用 @ray.remote 装饰器定义 Actor 类
    @ray.remote
    class Counter:
    def __init__(self):
    self.count = 0

    def increment(self):
    time.sleep(1) # 模拟耗时操作
    self.count += 1
    return self.count

    def get_count(self):
    return self.count

    # 创建 Actor 实例
    counter = Counter.remote()

    # 并行调用 Actor 方法
    start_time = time.time()
    # 提交多个增量任务
    increment_refs = [counter.increment.remote() for _ in range(10)]
    # 获取所有增量任务的结果
    results = ray.get(increment_refs)
    # 获取最终计数
    final_count = ray.get(counter.get_count.remote())
    end_time = time.time()

    print(f"每次增量结果: {results}")
    print(f"最终计数: {final_count}")
    print(f"执行耗时: {end_time - start_time:.4f} 秒")

    # 关闭 Ray
    ray.shutdown()

    # 每次增量结果: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    # 最终计数: 10
    # 执行耗时: 10.1293 秒

分布式调度示例(集群模式)

  • 以上代码经过非常简单的修改即可进入集群模式

  • Ray 集群部署包括三个步骤(下面以 6379 端口为例展示流程)

  • 第一步:启动主节点 ,运行 ray start --head 从主节点启动集群

    1
    ray start --head --port=6379 --redis-password='your_secure_password_123'
    • 注:可通过 --redis-password 设置密码(可选),防止未授权节点加入,也可以不使用该参数
  • 第二步:启动工作节点 ,运行 ray start --address=<主节点IP> 加入集群

    1
    ray start --address='<head-node-ip>:6379' --redis-password='your_secure_password_123'
    • 执行上述命令后工作节点就会:
      • 自动连接到主节点
      • 等待接收任务
      • 执行主节点分配的计算任务
      • 将结果返回给主节点
  • 第三步:在主节点上运行的代码中连接集群

    1
    ray.init(address='auto', _redis_password='your_secure_password_123')
    • 注:以上代码仅在主节点上运行,工作节点不需要显示运行任何代码,仅需要启动并加入集群即可
  • 关闭 Ray 服务:

    1
    ray stop
  • 特别说明:集群模式与普通单机并行模式的区别很小,仅需要增加修改以上代码即可(其他代码都不需要修改)

  • Ray 在分布式下默认有许多默认功能:

    • 自动负载均衡:Ray 会自动将任务分配到空闲节点
    • 容错能力:如果某个工作节点失败,Ray 会重新调度任务
  • 集群模式工作流程总结:

    • 主节点通过 Redis 将任务(remote 函数或者类对象)放入队列
    • 空闲的工作节点从队列中获取任务
    • 工作节点执行任务
    • 将运算结果通过 共享内存/Object Store 返回给主节点

附录:工作节点启动高级配置

  • 可以通过参数调整工作节点行为:
    1
    2
    3
    4
    5
    ray start --address='<head-node-ip>:6379' \
    --redis-password='your_secure_password_123' \
    --num-cpus=8 \ # 限制使用8个CPU核心
    --num-gpus=1 \ # 声明有1个GPU可用
    --object-store-memory=100000000 \ # 设置对象存储大小

附录:Ray 集群状态监控

  • Ray 提供了 Web UI 用于监控集群状态
  • 在主节点启动时已经启用了 Dashboard(默认端口8265)
  • 在浏览器访问:http://<主节点IP>:8265
  • 在 Dashboard 中可以看到:
    • 集群节点列表和资源使用情况
    • 当前运行的任务
    • 历史任务统计
    • 每个节点的CPU/内存使用情况

DL——模型训练预热


整体说明

  • 预热(Warm-up)是一种训练技巧:
    • 在模型训练初期采用一些策略,逐步调整超参数(如学习率、 Batch Size 大小等)或模型状态 ,使得训练过程更加稳定、高效的初始化阶段
    • 通过合理预热,可以显著提升训练稳定性、收敛速度和最终性能
  • 预热的核心目的是避免训练初期因参数随机初始化或学习率过高导致的梯度不稳定、收敛困难等问题
  • 常见的预热技术主要包含两类:
    • 学习率预热(Learning Rate Warm-up) :训练初期从极小的学习率(如0)逐步线性或非线性增加到预设值
    • 优化器预热 :
      • Adam 预热阶段可用小学习率,比如正常值的 \(1/10\)(Adam 优化器的自适应动量在初期可能不准确);
      • Adam 在预热阶段启用偏差修正 ,避免初期估计偏差过大
  • 其他预热技术还包括:Batch Size 预热(Batch Size 从小到大),模型参数预热(逐步解冻模型层) 和 混合精度预热等(初期禁用混合精度)
  • 最常见的预热技术是学习率预热,其中 Transformer 常使用 学习率线性预热(比如 BERT 训练中常用 10,000 步线性预热)
  • 术语:warm-up ratio
    • 如 warm-up ratio 等于 0.03,表示 warm-up 阶段(学习率上升阶段)步数占总训练阶段步数的 3%

学习率预热的相关策略

  • 学习率预热(Learning Rate Warm-up)是训练初期逐步增加学习率的策略,旨在稳定训练并提升最终性能。以下是常见的具体方法及其细节:

线性预热(Linear Warm-up)

  • 在预热步数 \(N\) 内,学习率从 \(0\)(或极小值 \(\epsilon\))线性增长到初始学习率 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon + \left(\frac{t}{N}\right) \cdot (lr_{\text{base} } - \epsilon)
    $$
    • 其中 \(t\) 是当前步数,\(t \leq N\)
  • 最常用的方式之一

余弦预热(Cosine Warm-up)

  • 结合余弦函数曲线调整学习率,初期缓慢增长,后期平滑过渡到目标值
    $$
    lr_t = \frac{1}{2} \left(1 + \cos\left(\pi \cdot \left(1 - \frac{t}{N}\right)\right)\right) \cdot lr_{\text{base} }
    $$
  • 注:也可与余弦退火结合,预热后直接进入衰减阶段
  • 更平滑的过渡,减少初期学习率突变
  • 一些大模型中会使用到

指数预热(Exponential Warm-up)

  • 学习率从 \(\epsilon\) 开始指数增长到 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon \cdot \left(\frac{lr_{\text{base} } }{\epsilon}\right)^{\frac{t}{N} }
    $$
  • 较少使用,因可能过早进入高学习率阶段

阶梯预热(Step Warm-up)

  • 将预热阶段分为多个离散区间,逐步跳跃式增加学习率

附录:torch 自带预热和学习率调度代码示例

  • 一个完整的PyTorch示例:先进行学习率预热,再正常训练模型

  • 以简单的图像分类任务(CIFAR-10)为基础,结合线性预热和余弦退火调度器

  • 代码示例:

    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
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.optim.lr_scheduler import LambdaLR, CosineAnnealingLR
    from torchvision import datasets, transforms
    from torch.utils.data import DataLoader
    import matplotlib.pyplot as plt

    class SimpleCNN(nn.Module):
    def__init__(self):
    super().__init__()
    self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
    self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
    self.fc = nn.Linear(32 * 8 * 8, 10) # CIFAR-10输入为32x32,经过两次池化后为8x8
    self.pool = nn.MaxPool2d(2, 2)
    self.relu = nn.ReLU()

    def forward(self, x):
    x = self.pool(self.relu(self.conv1(x)))
    x = self.pool(self.relu(self.conv2(x)))
    x = x.view(-1, 32 * 8 * 8)
    x = self.fc(x)
    return x

    transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])
    train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    train_loader = DataLoader(train_set, batch_size=128, shuffle=True)

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = SimpleCNN().to(device)
    ## 注:学习率包含在优化器 optimizer 中,使用不同的学习率调度器来执行 step,就可以实现不同的学习率调度
    optimizer = optim.AdamW(model.parameters(), lr=0.001) # 初始学习率设为0.001(预热目标值)

    warmup_steps = 500 # 预热步数
    total_steps = 5000 # 总训练步数

    # 线性预热函数
    def warmup_lambda(current_step):
    if current_step < warmup_steps:
    return float(current_step) / float(max(1, warmup_steps))
    else:
    return 1.0 # 预热结束后保持学习率

    # 预热阶段调度器
    warmup_scheduler = LambdaLR(optimizer, lr_lambda=warmup_lambda) # 基于优化器初始化调度器

    # 预热后的余弦退火调度器(从预热结束开始)
    cosine_scheduler = CosineAnnealingLR(
    optimizer, # 与预热阶段调度器初始化相同的优化器
    T_max=total_steps - warmup_steps, # 余弦周期长度
    eta_min=1e-6 # 最小学习率
    )

    criterion = nn.CrossEntropyLoss()
    lr_history = []
    for step in range(total_steps):
    inputs = torch.randn(128, 3, 32, 32).to(device)
    labels = torch.randint(0, 10, (128,)).to(device)

    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    # 更新学习率
    if step < warmup_steps:
    warmup_scheduler.step() # 预热阶段,step 函数会按照 warmup_scheduler 的定义来修改学习率
    else:
    cosine_scheduler.step() # 预热后余弦退火,step 函数会按照 cosine_scheduler 的定义来修改学习率

    # 记录学习率,可打印出来观测
    lr_history.append(optimizer.param_groups[0]['lr'])

    if step % 200 == 0:
    print(f"Step {step}: LR = {optimizer.param_groups[0]['lr']:.6f}, Loss = {loss.item():.4f}")
  • 预热阶段(前500步):学习率从 0 线性增长到初始值 0.001
    $$ lr = \text{base_lr} \times \frac{\text{current_step} }{\text{warmup_steps} } $$

  • 正常训练阶段(500步后):切换为余弦退火调度器(CosineAnnealingLR),学习率从 0.001 逐渐衰减到 1e-6

    • 注: 余弦退火的周期长度 \( T_{\text{max} } \) 设为总步数减去预热步数
  • 总体来说,学习率曲线是先线性上升,后余弦式下降(平滑振荡衰减)的过程


附录:transformers 库的模型训练预热调度示例

  • transformers 库中使用模型训练预热代码(按照初始学习率 1e-4, epochs= )

    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
    50
    51
    52
    import matplotlib.pyplot as plt
    import transformers
    import torch

    initial_lr = 1.0e-4 # 初始学习率
    warmup_ratio = 0.1 # 预热比例

    num_training_steps = 1000 # 总训练 step 数
    num_warmup_steps = int(num_training_steps * warmup_ratio) # 计算 warmup 的 step 数

    optimizer = torch.optim.AdamW([torch.tensor(0.0)], lr=initial_lr) # [torch.tensor(0.0)] 是虚拟的模型参数,可随意设置

    # 使用 transformers 库创建余弦退火学习率调度器
    lr_scheduler = transformers.get_cosine_schedule_with_warmup(
    optimizer=optimizer,
    num_warmup_steps=num_warmup_steps, # warmup step 数
    num_training_steps=num_training_steps, # 训练总 step 数
    # num_cycles=0.5, # 对应 cosine 曲线的周期,默认值是0.5,也就是半周期(递减)
    # last_epoch=-1, # 用于从 checkpoint 启动时恢复训练,设置为 ckpt 对应 step-1 即可
    # 比如从第 500 步的 ckpt启动,设置为499,从第0步启动,设置为-1(默认值)
    )

    learning_rates = []
    for _ in range(num_training_steps):
    learning_rates.append(optimizer.param_groups[0]["lr"])
    lr_scheduler.step() # 更新 optimizer.param_groups[0]["lr"]

    # 设置中文字体
    plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

    # 以下为可视化代码
    plt.figure(figsize=(10, 6))
    plt.plot(learning_rates)
    plt.title('学习率变化曲线')
    plt.xlabel('训练步骤')
    plt.ylabel('学习率')
    plt.grid(True)
    plt.axvline(x=num_warmup_steps, color='r', linestyle='--', label='预热结束')
    plt.legend()

    plt.annotate(f'初始学习率: {initial_lr}', xy=(num_warmup_steps, initial_lr),
    xytext=(num_warmup_steps + 50, initial_lr * 1.5),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'预热起点: 0', xy=(0, 0),
    xytext=(50, initial_lr * 0.2),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'最终学习率: {learning_rates[-1]:.8f}', xy=(num_training_steps-1, learning_rates[-1]),
    xytext=(num_training_steps-200, learning_rates[-1] * 10),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.tight_layout()
    plt.savefig('warmup_learning_rate_curve_cycles_0.5.png', dpi=300)
    # plt.show()
  • 可视化结果(半周期余弦 num_cycles=0.5 的结果):

    • warmup 阶段,学习率从 0 开始逐步提升到最大值
    • 正式训练阶段,学习率按照余弦调度器波动
  • 如果设置为 num_cycles=1,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=1.5,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=2,则会在指定训练步数内完成两个周期的学习率变化:


附录:预热有什么用?

  • 解决梯度不稳定问题 :模型初始阶段参数随机初始化,直接使用高学习率可能导致梯度爆炸或震荡
  • 解决学习率敏感性问题 :过大的初始学习率可能使模型跳过最优解附近区域;过小则导致收敛缓慢
  • 保证优化器适应性 :如 Adam 等自适应优化器在初期需要积累梯度统计量(如动量、方差),预热阶段可为优化器提供更稳定的初始估计

附录:一般预热多少步更合适?

  • 预热步数通常取决于模型规模和数据集大小:
    • 小规模数据:数百到几千步
    • 大规模训练(如LLM):数万步甚至更长(例如 GPT-3 的数千批次预热)
  • 另一种设置方式是:通常为总训练步数的 5-10%(例如 BERT 的 10k 步预热,总步数 100k)

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) \) 是目标值,可以有多种实现,具体实现见下文
可选 Critic 目标估计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 网络的输出(稳定训练)
      • PPO 每个 episode 可能会更新多个 epoch (这里指同样的数据更新 epoch 次)
        • 需要保证更新过程中, 每一个 epoch 时 Critic 网络的学习目标是不变的
        • 即每个epoch中 ,目标值\( V_{\text{target} }(s_t) \) 始终是不变的
      • 实现时,确保目标值是提前使用第一 epoch 更新前的 Critic 网络 \(\pi_\text{old}\) 计算得到的即可(这里不需要像 DQN 一样使用 Target 网络来稳定训练),具体实现可以如下(参考自动手学强化学习实现):
        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]
    $$

可选 Critic 目标估计2:广义优势估计(GAE)结合多步 TD(PPO 中最常用)
  • 当使用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]
    $$
可选 Critic 目标估计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,且不同游戏使用的次数不同(下面 Algorithm 中给出更新是 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 算法
  • 更广义的说,忽略 Clip(KL 散度约束)的 PPO,本质上就是策略梯度法,只不过 PPO 中 Advantage 的估计强调使用了 GAE

附录: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列出了平均性能
1…222324…66
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

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