Hexo

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

DL——重参数化技巧

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

重参数化解决的问题

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

重参数化的基本思想

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

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

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

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

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

原版 softmax(原始问题):

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

Gumbel-Max Trick:

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

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

Gumbel-Softmax Trick:

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

Gumbel-Softmax Trick + Straight-Through Estimator:

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

argmax动作的梯度回传

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

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


整体说明

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

分类任务场景符号定义

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

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

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

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

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

CE Loss 和 NLL Loss 对比

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

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

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

使用代码示例

  • 用 CrossEntropyLoss(直接输入 logits)

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

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

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

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

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

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

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

附录:BCE Loss

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

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

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

BCELoss vs CrossEntropyLoss 比较

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

附录:相关损失函数 MultiLabelMarginLoss

  • 使用

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


总结

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

San Ye

Stay Hungry. Stay Foolish.

704 posts
53 tags
© 2026 San Ye
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4