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 对应的网络有两个输出值
  • 可以证明, 二分类时 BCELossCrossEntropyLoss等价
    • 证明时,将每个 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 的话,输入的 inputtarget维度都为 (n, 1) 的维度
    • 二分类时使用 CrossEntropyLoss 则输入的 input 为 (n x 2) 的维度
  • 如果使用 NLLLoss 则一定记得在输出层最后加一层 log_softmax
  • 注意,log 指的是以 e 为底的对数函数,而不是以10为底的
    • Mac自带的计算器中 log 是以 10 为底的,ln 才是以 e 为底的