Python 现代开发学习笔记

(十八)logits、softmax 与交叉熵

回归输出一个连续数,分类要在竖线、横线和十字中选择一个类别。先看一个样本输出的三个原始分数 [2,1,0],这组分数称为 logits。

1. 把分数解释成概率

softmax 对每项取指数,再除以指数和。这里的 ee 约为 2.71828,eze^z 把任意实数分数变成正数;之后归一化,各项就能相加为一。为了让数值计算更稳定,可以先减去本行最大分数 2:

𝐩=[e0,e−1,e−2]e0+e−1+e−2. \mathbf{p}=\frac{[e^0,e^{-1},e^{-2}]}{e^0+e^{-1}+e^{-2}}.

𝐩≈[0.665241,0.244728,0.090031]. \mathbf{p}\approx[0.665241,0.244728,0.090031].

如果正确类别编号为 0,我们希望第一项概率增加。ln\ln 是自然对数,与 eze^z 互为反向运算:ln⁡1=0\ln1=0、ln⁡e=1\ln e=1、ln⁡(1/e)=−1\ln(1/e)=-1。用正确类别概率的负对数作为损失,得到:

L=−ln⁡(p0)≈0.407606. L=-\ln(p_0)\approx0.407606.

正确类别概率越接近一,损失越接近零;给正确类别很小的概率时,损失会明显增加。

再推导这三项分数各自收到的梯度。记指数和为 SS,则正确类别为零时,L=−ln⁡(ez0/S)=ln⁡S−z0L=-\ln(e^{z_0}/S)=\ln S-z_0。对某项分数 ziz_i,SS 的变化率为 ezie^{z_i},ln⁡S\ln S 对 SS 的变化率为 1/S1/S,沿路径相乘便是 ezi/S=pie^{z_i}/S=p_i。

最后还有减去 z0z_0 这条路径:对分数零的贡献是 -1,对另外两项是 0。把两条路径贡献相加,得到“预测概率减去正确类别指示值”:

∇logitsL=𝐩−[1,0,0]. \nabla_{logits}L=\mathbf p-[1,0,0].

∇logitsL≈[−0.334759,0.244728,0.090031]. \nabla_{logits}L\approx[-0.334759,0.244728,0.090031].

沿梯度反方向走,正确类分数上升,另外两类分数下降。这与(七)通过误差把变化传回参数的思路相同,只是损失函数换成了分类需要的形式。

# 项目文件:demos/logits_loss.py
import torch
from torch import nn

logits = torch.tensor([[2.0, 1.0, 0.0]], requires_grad=True)
target = torch.tensor([0], dtype=torch.int64)  # 一个样本的正确类别编号为 0。
probabilities = logits.softmax(dim=1)
loss = nn.CrossEntropyLoss()(logits, target)
loss.backward()
print("概率", [round(value, 6) for value in probabilities[0].detach().tolist()])
print("损失", round(loss.item(), 6))
print("梯度", [round(value, 6) for value in logits.grad[0].tolist()])
print("类别", logits.argmax(dim=1).tolist())
三个分数减去最大值后取指数与归一化,正确类概率进入负对数形成零点四零七六损失,回传概率减指示值的三个梯度

2. 把形状与损失接口连起来

nn.CrossEntropyLoss 直接接收 (B,C) logits 和 (B,) 整数类别编号,在内部完成相应的对数概率计算。训练调用前保留原始分数;只有显示概率时才调用 softmax。再传一次 softmax 会改变损失实际接收的分数。

argmax(dim=1) 取每行最大项的编号,softmax 不改变大小次序,因此判断类别时可以直接对 logits 使用它。接口细节见 CrossEntropyLoss。

笔记正文与全部题解 · Ctrl / ⌘ K