
文章目录
-
- 一、损失函数到底是什么
- 二、MSE:回归任务最常见的损失
- 三、MAE:对异常值更稳
- [四、Huber Loss:折中 MSE 和 MAE](#四、Huber Loss:折中 MSE 和 MAE)
- 五、BCE:二分类交叉熵
- [六、为什么用 BCEWithLogitsLoss](#六、为什么用 BCEWithLogitsLoss)
- 七、多分类交叉熵
- 八、为什么分类任务偏爱交叉熵
- [九、NLL Loss:交叉熵的另一种写法](#九、NLL Loss:交叉熵的另一种写法)
- [十、语言模型的 loss 怎么算](#十、语言模型的 loss 怎么算)
- [十一、Loss Masking:为什么不是所有 token 都算 loss](#十一、Loss Masking:为什么不是所有 token 都算 loss)
- [十二、Label Smoothing:不要让模型过度自信](#十二、Label Smoothing:不要让模型过度自信)
- [十三、类别不平衡和加权 loss](#十三、类别不平衡和加权 loss)
- 十四、损失函数和评估指标不一定一致
- 十五、常见误区
- 十六、你应该记住的最小心智模型
- 总结
- 大模型视角
- 下一篇
摘要 :损失函数把"模型预测得好不好"变成一个可以求梯度的数字。回归任务常用 MSE、MAE、Huber,二分类常用 BCE,多分类和语言模型常用交叉熵。损失函数选错,模型就会朝错误目标优化;损失函数写对但数值处理不稳,也可能训练发散。本文从损失函数的作用讲起,用公式、直觉和代码解释 MSE、MAE、BCE、Cross Entropy、NLL、语言模型 next-token loss、loss masking 和 label smoothing,帮助你看懂训练脚本里
criterion到底在做什么。
前置知识 :第 2 篇概率基础,第 3 篇导数,第 6 篇学习范式;本文只介绍"loss 是什么、怎么写",loss 如何真正回传参数(反向传播/自动求导)在第 11 篇会完整展开
阅读时间 :约 70 分钟
代码环境:Python 3.10+,numpy >= 1.24,torch >= 2.0
入门导读:先抓住主线
模型训练不是直接优化"聪明程度",而是优化你定义的 loss。
text
预测 -> loss -> 梯度 -> 参数更新
如果 loss 能准确表达任务目标,模型才可能朝正确方向学习。
例如:
- 房价预测:预测值离真实房价越近越好,适合 MSE/MAE;
- 垃圾邮件分类:真实类别概率越高越好,适合 BCE/CE;
- 语言模型:真实下一个 token 概率越高越好,适合交叉熵;
- 摘要生成:交叉熵训练能学会模仿参考摘要,但不一定直接优化事实性和用户满意度。
读完先达到这个程度就够了:
- 能解释损失函数在训练循环里的位置;
- 能区分 MSE、MAE、Huber 的适用场景;
- 能理解 BCE 和多分类交叉熵的差别;
- 能知道为什么 PyTorch 里常用 logits 直接喂给 loss;
- 能看懂语言模型 loss 的 shift 和 mask;
- 能理解 label smoothing 为什么能降低过度自信。
带着这 3 个问题读:
- 为什么分类任务通常不用 MSE,而用交叉熵?
- 为什么
CrossEntropyLoss前面不要手动 softmax? - 大模型训练时,为什么不是所有 token 都计算 loss?
一、损失函数到底是什么
损失函数是一个函数:
text
loss = f(prediction, target)
它输入模型预测和真实目标,输出一个标量。这个标量越小,表示模型在当前训练目标下越好。
训练时,我们希望最小化平均 loss:
min θ 1 N ∑ i = 1 N L ( f θ ( x i ) , y i ) \min_\theta \frac{1}{N}\sum_{i=1}^{N} L(f_\theta(x_i), y_i) θminN1i=1∑NL(fθ(xi),yi)
其中:
- θ \theta θ 是模型参数;
- f θ ( x i ) f_\theta(x_i) fθ(xi) 是模型预测;
- y i y_i yi 是目标;
- L L L 是单个样本损失。
损失函数有两个要求:
- 能表达任务目标;
- 能对模型参数求梯度。
第二点很关键。很多人类评价,比如"回答是否有帮助",很难直接写成可微分函数。所以大模型训练会先用交叉熵做 SFT,再用偏好优化、奖励模型或人工评估补充。
二、MSE:回归任务最常见的损失
MSE 全称 Mean Squared Error,均方误差。
公式:
M S E = 1 N ∑ i = 1 N ( y ^ i − y i ) 2 MSE = \frac{1}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i)^2 MSE=N1i=1∑N(y^i−yi)2
它惩罚预测值和真实值的平方差。
代码:
python
import numpy as np
pred = np.array([2.5, 3.0, 4.2])
target = np.array([3.0, 2.5, 5.0])
mse = np.mean((pred - target) ** 2)
print(mse)
MSE 的特点:误差越大,惩罚增长越快。
如果预测错 10,比错 1 的惩罚大 100 倍。这对很多回归任务是合理的,比如房价预测、温度预测、连续数值拟合。
但如果数据里有异常值,MSE 会对异常值特别敏感。
三、MAE:对异常值更稳
MAE 全称 Mean Absolute Error,平均绝对误差。
公式:
M A E = 1 N ∑ i = 1 N ∣ y ^ i − y i ∣ MAE = \frac{1}{N}\sum_{i=1}^{N}|\hat{y}_i - y_i| MAE=N1i=1∑N∣y^i−yi∣
代码:
python
mae = np.mean(np.abs(pred - target))
print(mae)
MAE 对异常值没那么敏感,因为误差线性增长。
对比:
python
errors = np.array([1, 2, 10])
print("MSE contribution:", errors ** 2)
print("MAE contribution:", np.abs(errors))
MSE 更重视大误差,MAE 更稳健。
但 MAE 在 0 附近不可导,虽然深度学习框架可以用次梯度处理,但优化上可能不如 MSE 平滑。
四、Huber Loss:折中 MSE 和 MAE
Huber Loss 在误差小时像 MSE,误差大时像 MAE。
直觉:
text
小误差:平方惩罚,优化平滑
大误差:线性惩罚,降低异常值影响
公式:
L δ ( a ) = { 0.5 a 2 , ∣ a ∣ ≤ δ δ ( ∣ a ∣ − 0.5 δ ) , ∣ a ∣ > δ L_\delta(a) = \begin{cases} 0.5a^2, & |a| \le \delta \\ \delta(|a| - 0.5\delta), & |a| > \delta \end{cases} Lδ(a)={0.5a2,δ(∣a∣−0.5δ),∣a∣≤δ∣a∣>δ
其中 a = y ^ − y a = \hat{y} - y a=y^−y。
PyTorch:
python
import torch
import torch.nn as nn
loss_fn = nn.HuberLoss(delta=1.0)
pred_t = torch.tensor([2.5, 3.0, 10.0])
target_t = torch.tensor([3.0, 2.5, 5.0])
print(loss_fn(pred_t, target_t))
Huber 常用于回归中存在异常值但又希望优化平滑的场景。
五、BCE:二分类交叉熵
二分类任务输出一个概率 p p p,表示样本属于正类的概率。
BCE 公式:
B C E = − y log ( p ) + ( 1 − y ) log ( 1 − p ) BCE = -y\\log(p) + (1-y)\\log(1-p) BCE=−ylog(p)+(1−y)log(1−p)
其中 y y y 是 0 或 1。
如果真实标签是 1,loss 变成:
− log ( p ) -\log(p) −log(p)
模型给正类概率越高,loss 越小。
如果真实标签是 0,loss 变成:
− log ( 1 − p ) -\log(1-p) −log(1−p)
代码:
python
import math
for p in [0.01, 0.1, 0.5, 0.9, 0.99]:
loss_when_y1 = -math.log(p)
print(p, loss_when_y1)
当真实为正类时,模型给 0.99 的概率,loss 很小;给 0.01 的概率,loss 很大。
这就是交叉熵的直觉:让模型把概率质量放到正确答案上。
六、为什么用 BCEWithLogitsLoss
实际训练二分类时,推荐用 logits,而不是先 sigmoid 再 BCE。
python
import torch
import torch.nn as nn
logits = torch.tensor([0.2, -1.0, 3.0])
target = torch.tensor([1.0, 0.0, 1.0])
loss_fn = nn.BCEWithLogitsLoss()
loss = loss_fn(logits, target)
print(loss)
BCEWithLogitsLoss 内部会把 sigmoid 和 BCE 合并成数值更稳定的形式。
不要这样写:
python
# 不推荐:可能数值不稳定
prob = torch.sigmoid(logits)
loss = nn.BCELoss()(prob, target)
对于大数或小数,手动 sigmoid 后再 log 可能出现数值问题。
这是深度学习工程里常见原则:能用框架提供的 logits 版 loss,就不要自己拼 softmax/sigmoid + log。
七、多分类交叉熵
多分类任务中,模型输出每个类别的 logits,然后 softmax 得到概率分布。
交叉熵损失是:
C E = − log p t r u e = − log ( s o f t m a x ( logits ) t r u e ) CE = -\log p_{true} = -\log\bigl(\mathrm{softmax}(\text{logits})_{true}\bigr) CE=−logptrue=−log(softmax(logits)true)
也就是先把 logits 过 softmax 得到每个类别的概率,再取正确类别那一维的概率做 − log -\log −log。所以完整链路是 logits → softmax → 取正确类别 → -log 。后面第九节会看到,这等价于 NLLLoss(log_softmax(logits), labels)。
PyTorch:
python
import torch
import torch.nn as nn
logits = torch.tensor([
[2.0, 0.5, -1.0],
[0.1, 1.2, 0.3],
])
labels = torch.tensor([0, 1])
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits, labels)
print(loss)
注意:CrossEntropyLoss 输入的是 logits,不是 softmax 后的概率。
它内部等价于:
text
log_softmax + nll_loss
这样更数值稳定。
八、为什么分类任务偏爱交叉熵
分类任务关心正确类别概率。交叉熵直接惩罚正确类别概率低的情况。
如果真实类别是 0,两个模型输出:
text
模型 A: [0.6, 0.4]
模型 B: [0.9, 0.1]
它们都预测对了,但模型 B 更自信地给正确类别高概率,交叉熵更低。
MSE 也能用于分类,但它对概率分布优化不如交叉熵自然。交叉熵来自最大似然估计,更适合概率分类模型。
直觉:
text
分类模型不是只要 argmax 对
还希望正确类别概率尽量高
这就是交叉熵的目标。
九、NLL Loss:交叉熵的另一种写法
NLL Loss 全称 Negative Log Likelihood。
如果输入已经是 log probability,那么 NLL Loss 就是取正确类别的负 log 概率。
python
import torch
import torch.nn.functional as F
logits = torch.tensor([[2.0, 0.5, -1.0]])
label = torch.tensor([0])
log_probs = F.log_softmax(logits, dim=-1)
loss_nll = F.nll_loss(log_probs, label)
loss_ce = F.cross_entropy(logits, label)
print(loss_nll, loss_ce)
你会看到两者相同。
所以:
text
CrossEntropyLoss(logits, labels)
= NLLLoss(log_softmax(logits), labels)
这也是为什么训练语言模型时,最后输出通常叫 logits,而不是 probabilities。
十、语言模型的 loss 怎么算
Decoder-Only 语言模型训练目标是预测下一个 token。
假设 token 序列:
text
[我, 喜欢, 机器, 学习]
输入和标签错开:
text
输入:我 喜欢 机器
标签:喜欢 机器 学习
PyTorch 里常见写法:
python
import torch
import torch.nn.functional as F
batch, seq_len, vocab_size = 2, 5, 100
logits = torch.randn(batch, seq_len, vocab_size)
input_ids = torch.randint(0, vocab_size, (batch, seq_len))
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = input_ids[:, 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, vocab_size),
shift_labels.view(-1),
)
print(loss)
这段代码里的 shift 非常重要。当前位置的输出预测下一个 token,而不是预测自己。
如果 shift 写错,模型训练目标就错了。
十一、Loss Masking:为什么不是所有 token 都算 loss
在真实训练里,不是所有位置都应该参与 loss。
常见需要 mask 的情况:
- padding token;
- prompt 部分不训练,只训练 answer 部分;
- 对话模板里的特殊 token;
- 被截断或无效标签;
- 多任务数据中不属于当前目标的位置。
PyTorch 里常用 ignore_index=-100。
python
logits = torch.randn(1, 6, 50)
labels = torch.tensor([[10, 11, -100, -100, 12, 13]])
loss = F.cross_entropy(
logits.view(-1, 50),
labels.view(-1),
ignore_index=-100,
)
print(loss)
-100 的位置不会计算 loss。
SFT 中常见做法是:用户 prompt 不算 loss,只让 assistant answer 算 loss。这样模型学习如何回答,而不是学习复述用户输入。
十二、Label Smoothing:不要让模型过度自信
普通交叉熵使用 one-hot 标签:正确类别概率目标是 1,其他类别是 0。
Label smoothing 会把标签稍微抹平。
例如 3 类,真实类别是 0:
text
one-hot: [1.0, 0.0, 0.0]
smoothed: [0.9, 0.05, 0.05]
这能降低模型过度自信,有时改善泛化。
PyTorch:
python
loss_fn = nn.CrossEntropyLoss(label_smoothing=0.1)
logits = torch.randn(4, 3)
labels = torch.tensor([0, 1, 2, 1])
loss = loss_fn(logits, labels)
print(loss)
Label smoothing 不是所有任务都适合。比如某些需要极高置信校准或指令微调场景,要谨慎验证。
十三、类别不平衡和加权 loss
如果数据类别严重不平衡,普通 loss 可能偏向多数类。
例如 99% 样本是正常,1% 是欺诈。模型全预测正常,accuracy 也有 99%,但业务上失败。
可以给少数类更高权重:
python
weights = torch.tensor([1.0, 10.0])
loss_fn = nn.CrossEntropyLoss(weight=weights)
logits = torch.randn(8, 2)
labels = torch.tensor([0, 0, 0, 0, 0, 0, 0, 1])
loss = loss_fn(logits, labels)
print(loss)
但加权 loss 不是唯一方案,还可以重采样、调整阈值、使用 focal loss、改评估指标等。
损失函数必须和业务目标、数据分布、评估指标一起看。
十四、损失函数和评估指标不一定一致
训练 loss 和最终评估指标经常不是同一个东西。
例如:
- 语言模型训练用交叉熵,评估可能看 PPL、MMLU、人工偏好;
- 分类训练用交叉熵,评估可能看 F1、AUC;
- 代码模型训练用交叉熵,评估看单元测试通过率;
- 摘要模型训练用交叉熵,评估看 ROUGE 和事实一致性;
- 对话模型 SFT 用交叉熵,最终看用户满意度和安全性。
为什么不直接优化最终指标?
因为很多最终指标不可微、成本高、反馈慢或不稳定。交叉熵这类 loss 是可微、稳定、可批量训练的替代目标。
这也解释了为什么大模型训练有多个阶段:预训练 loss 解决基础建模,SFT loss 解决指令模仿,偏好优化解决人类偏好,评估体系再覆盖真实能力。
十五、常见误区
误区 1:loss 越低,模型在所有任务上越好。
loss 只对应训练目标。语言模型 loss 低,不代表事实性、安全性或工具调用一定更好。
误区 2:分类前必须手动 softmax。
使用 CrossEntropyLoss 时不要手动 softmax,它内部包含 log_softmax,更稳定。
误区 3:二分类一定用两个输出。
可以用一个 logit + BCEWithLogitsLoss,也可以用两个 logits + CrossEntropyLoss,看任务设计。
误区 4:padding token 算 loss 没关系。
有关系。padding 是无效位置,参与 loss 会污染训练目标。
误区 5:Label smoothing 一定提升效果。
它可能改善泛化,也可能损害需要精确概率或特定输出格式的任务,需要验证。
误区 6:损失函数只影响训练,不影响产品行为。
训练目标会塑造模型行为。模型最终会优化你给它的目标,而不是你心里想的目标。
十六、你应该记住的最小心智模型
常见 loss 可以这样记:
text
MSE:回归,平方惩罚,大误差惩罚重
MAE:回归,绝对误差,对异常值更稳
Huber:小误差像 MSE,大误差像 MAE
BCE:二分类,让真实标签概率更高
Cross Entropy:多分类/语言模型,让正确类别或 token 概率更高
NLL:对 log probability 取正确类别负 log
语言模型 loss 主线:
text
logits[:, :-1] 预测 input_ids[:, 1:]
ignore_index 屏蔽不该训练的位置
交叉熵让真实下一个 token 概率变高
总结
损失函数是训练目标的数学表达。MSE、MAE、Huber 主要用于回归;BCE 用于二分类;多分类和语言模型通常使用交叉熵。PyTorch 中推荐直接把 logits 输入 BCEWithLogitsLoss 或 CrossEntropyLoss,避免手动 sigmoid/softmax 带来的数值问题。语言模型训练时要特别注意 shift 和 loss mask,否则模型会学习错误目标。
损失函数不是越复杂越好,而是要和任务目标、数据分布、评估指标匹配。大模型预训练、SFT、偏好优化使用不同训练目标,是因为它们解决的问题不同。
第一遍记住一句话:损失函数定义了模型努力优化的方向,目标写错,模型就会认真地学错。
大模型视角
后续你看预训练、SFT、DPO/RLHF、PPL、评估指标时,会反复遇到交叉熵、mask、label smoothing、KL、偏好 loss。理解本篇后,你会更容易判断训练脚本里的 loss 是否真的对应你想要的模型行为。
下一篇
** 过拟合与正则化:L1/L2/Dropout 详解** ------ 损失函数告诉模型怎么学,但模型可能学得太死。下一篇看过拟合和正则化如何影响泛化。