【机器学习】损失函数全解_从MSE到交叉熵

文章目录

摘要 :损失函数把"模型预测得好不好"变成一个可以求梯度的数字。回归任务常用 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 个问题读

  1. 为什么分类任务通常不用 MSE,而用交叉熵?
  2. 为什么 CrossEntropyLoss 前面不要手动 softmax?
  3. 大模型训练时,为什么不是所有 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 是单个样本损失。

损失函数有两个要求:

  1. 能表达任务目标;
  2. 能对模型参数求梯度。

第二点很关键。很多人类评价,比如"回答是否有帮助",很难直接写成可微分函数。所以大模型训练会先用交叉熵做 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 输入 BCEWithLogitsLossCrossEntropyLoss,避免手动 sigmoid/softmax 带来的数值问题。语言模型训练时要特别注意 shift 和 loss mask,否则模型会学习错误目标。

损失函数不是越复杂越好,而是要和任务目标、数据分布、评估指标匹配。大模型预训练、SFT、偏好优化使用不同训练目标,是因为它们解决的问题不同。

第一遍记住一句话:损失函数定义了模型努力优化的方向,目标写错,模型就会认真地学错。

大模型视角

后续你看预训练、SFT、DPO/RLHF、PPL、评估指标时,会反复遇到交叉熵、mask、label smoothing、KL、偏好 loss。理解本篇后,你会更容易判断训练脚本里的 loss 是否真的对应你想要的模型行为。

下一篇

** 过拟合与正则化:L1/L2/Dropout 详解** ------ 损失函数告诉模型怎么学,但模型可能学得太死。下一篇看过拟合和正则化如何影响泛化。

相关推荐
研华科技Advantech1 小时前
Windows Server IoT 2025深度技术解析:功能、安全与AI性能突破
人工智能·物联网·安全
SomeB1oody1 小时前
【RustyML入门】5.1. 回归指标
开发语言·后端·机器学习·rust·教程
pingao1413781 小时前
金属翻斗式雨量筒 承雨口径200mm 高精度雨量监测仪
大数据·人工智能·科技
珐恩AI-人工智能1 小时前
大模型隐性权重衰减机制剖析:详解GEO长效维稳如何避免优质
大数据·人工智能·深度学习·机器学习·geo优化
通问AI1 小时前
人形机器人从实验室到量产:宇树科技IPO背后的技术逻辑
人工智能·科技·机器人
Eloudy1 小时前
GXF 3.2.0 to 4.0 CUDA 12 路径 API 与数据结构增量分析
人工智能
雷焰财经2 小时前
中国经济进入“科技驱动”新阶段:出口、高技术产业与内需如何形成合力
人工智能·科技
火山引擎开发者社区7 小时前
火山 PostgreSQL Serverless × 飞书妙搭:把 AI 装进数据库,一句话唤醒数据智能
人工智能
必须会一定会9 小时前
Agent Plugins 1.0实战:plugin.json、skills、mcp.json目录结构与迁移
开发语言·人工智能·ai编程