损失函数:模型真正优化的东西 本节摘要:你的网络做出了预测,真值却另有说法。它究竟错得有多离谱?那个数就是损失(Loss)。挑错损失函数,模型会去优化一个完全错误的东西。模型优化的从来不是准确率、不是 F1、不是你汇报给老板的指标——它优化的只有损失函数。优化器取损失的梯度,调权重让那个数变小;如果损失没抓住你在乎的东西,模型就会找出数学上最省力的办法去满足它,而那条路几乎从来不是你想要的。一个在分类任务上最小化 MSE 的模型,会自信地对所有输入预测 0.5——它在最小化损失,也毫无用处。换成交叉熵,同样的模型被迫把预测推向 0 或 1,因为 −log(0.5) = 0.693 是个糟糕的损失,而 −log(0.99) = 0.01 重奖自信正确的预测。
本节摘要:你的网络做出了预测,真值却另有说法。它究竟错得有多离谱?那个数就是损失(Loss)。挑错损失函数,模型会去优化一个完全错误的东西。模型优化的从来不是准确率、不是 F1、不是你汇报给老板的指标——它优化的只有损失函数。优化器取损失的梯度,调权重让那个数变小;如果损失没抓住你在乎的东西,模型就会找出数学上最省力的办法去满足它,而那条路几乎从来不是你想要的。一个在分类任务上最小化 MSE 的模型,会自信地对所有输入预测 0.5——它在最小化损失,也毫无用处。换成交叉熵,同样的模型被迫把预测推向 0 或 1,因为 −log(0.5) = 0.693 是个糟糕的损失,而 −log(0.99) = 0.01 重奖自信正确的预测。损失函数的选择,就是「会学的模型」与「钻指标空子的模型」之间的分野。本节将从零实现 MSE、二分类/多分类交叉熵、对比损失(InfoNCE),讲透标签平滑与 focal loss,给出按任务选损失的决策树。
阅读完本节,你应当能够:
在分类任务上最小化 MSE 的模型,会自信地对所有输入预测 0.5。它在最小化损失,也毫无用处。
损失函数是模型真正优化的唯一目标——不是准确率、不是 F1、不是你汇报的指标。优化器取损失的梯度、调权重让那个数变小。如果损失没抓住你在乎的东西,模型会找出数学上最省力的方式去满足它,而那条路几乎从来不是你想要的。
具体例子:二分类,两类各 50%,你用 MSE。模型对每个输入都预测 0.5,平均 MSE 是 0.25,这是不真正学任何东西时的最小可能值——模型零区分能力,却技术上把损失最小化了。换成交叉熵,同样模型被迫把预测推向 0 或 1,因为 −log(0.5) = 0.693 是个糟糕损失,而 −log(0.99) = 0.01 重奖自信正确的预测。
更糟的是,自监督学习里你连标签都没有。对比损失完全定义了学习信号:什么算相似、什么算不同、该把它们推开多远。对比损失搞错,你的嵌入会坍缩成一个点——每个输入都映射到同一个向量,技术上是零损失,实际上一文不值。
回归的默认。算预测与目标的平方差,对所有样本求平均。
MSE = (1/n) * sum((y_pred - y_true)^2)
为什么要平方:它对大误差做二次惩罚。误差为 2 的代价是误差为 1 的 4 倍,误差为 10 的代价是 100 倍。这让 MSE 对异常值敏感——一个错得离谱的预测会主导损失。MSE 对预测的梯度:
dMSE/dy_pred = (2/n) * (y_pred - y_true)
与误差线性相关:误差越大梯度越大。对回归这是优点(大误差需大修正),对分类是缺陷(你希望对自信的错误答案做指数级惩罚,而非线性)。
分类的损失函数,根植于信息论——度量预测分布与真实分布的差异。
二分类交叉熵(BCE):
BCE = -(y * log(p) + (1 - y) * log(1 - p))
y 是真标签(0 或 1),p 是预测概率。−log(p) 为何有效:真标签为 1 时预测 p = 0.99,损失 −log(0.99) = 0.01;预测 p = 0.01,损失 −log(0.01) = 4.6。这 460 倍的差距就是交叉熵的力量——它狠狠惩罚自信的错误,几乎不罚自信的正确。梯度讲同样的故事:
dBCE/dp = -(y/p) + (1-y)/(1-p)
y = 1 且 p 接近零时,梯度是 −1/p,趋向负无穷——模型收到巨大信号去纠错;p 接近 1 时梯度极小,已正确,没什么可修。
多分类交叉熵(CCE): 配 one-hot 目标。
CCE = -sum(y_i * log(p_i))
只有真实类别对损失有贡献(其它 y_i 都是 0)。10 类里正确类得概率 0.1(随机猜测),损失 −log(0.1) = 2.3;得 0.9,损失 −log(0.9) = 0.105。模型学会把概率质量集中到正确答案上。
MSE 梯度在预测接近 0 或 1 时(因 sigmoid 饱和)变平;交叉熵梯度补偿了这一点——−log 抵消了 sigmoid 的平坦区,在最需要的地方给出强梯度。
标准 one-hot 标签说「这是 100% 的类 3,其它全是 0%」,这说法太满。标签平滑把它软化:
smooth_label = (1 - alpha) * one_hot + alpha / num_classes
alpha = 0.1、10 类时:目标不再是 [0, 0, 1, 0, ...],而是 [0.01, 0.01, 0.91, 0.01, ...]。模型目标从 1.0 降到 0.91。
为何有效:要让 softmax 输出恰好 1.0,模型得把 logits 推到无穷,这导致过度自信、损害泛化、使模型对分布漂移脆弱。标签平滑把目标封顶在 0.9(alpha=0.1),把 logits 留在合理范围。GPT 及多数现代模型都用标签平滑或其等价物。
没有标签、没有类别,只有成对输入和一个问题:它俩相似还是不同?
SimCLR 式对比损失(NT-Xent / InfoNCE): 取一张图,造两个增强视图(裁剪、旋转、颜色抖动),它们是「正对」——嵌入应相似;batch 里其它图都是「负对」——嵌入应不同。
L = -log(exp(sim(z_i, z_j) / tau) / sum(exp(sim(z_i, z_k) / tau)))
sim() 是余弦相似度,z_i 与 z_j 是正对,求和遍历所有负样本,tau(温度)控制分布锐度——温度越低,负样本越难,分离越激进。batch 256 意味着每个正对有 255 个负样本,温度 tau = 0.07(SimCLR 默认),损失看起来像在相似度上做 softmax——它要正对的相似度在 256 个选项里最高。
三元组损失: 取三输入——锚点、正样本(同类)、负样本(异类)。
L = max(0, d(anchor, positive) - d(anchor, negative) + margin)
margin(典型 0.2~1.0)强制正负距离有最小间距。若负样本已经够远,损失为零——没梯度、不更新。这使训练高效,但需要精心做三元组挖掘(挑接近锚点的难负样本)。
为不平衡数据而生。标准交叉熵对所有已正确分类的样本一视同仁,focal loss 给易分样本降权:
FL = -alpha * (1 - p_t)^gamma * log(p_t)
p_t 是真实类的预测概率,gamma 控制聚焦程度。gamma = 0 时退化为标准交叉熵;gamma = 2(默认)时:
focal loss 由 Lin 等人为目标检测提出——那里 99% 的候选区是背景(易负样本),没有 focal loss,模型淹没在易背景样本里,学不会检测物体;有了它,模型把容量集中到要紧的难、模糊样本上。
完整代码见原课程 phases/03-deep-learning-core/05-loss-functions/code/ 相应文件。
def mse(predictions, targets): n = len(predictions) total = 0.0 for p, t in zip(predictions, targets): total += (p - t) ** 2 return total / n def mse_gradient(predictions, targets): n = len(predictions) return [2.0 * (p - t) / n for p, t in zip(predictions, targets)]
log(0) 是真问题:若模型对正样本预测恰为 0,log(0) = 负无穷,必须 clip。
def binary_cross_entropy(predictions, targets, eps=1e-15): n = len(predictions) total = 0.0 for p, t in zip(predictions, targets): p_clipped = max(eps, min(1 - eps, p)) total += -(t * math.log(p_clipped) + (1 - t) * math.log(1 - p_clipped)) return total / n
def softmax(logits): max_val = max(logits) exps = [math.exp(x - max_val) for x in logits] total = sum(exps) return [e / total for e in exps] def categorical_cross_entropy(logits, target_index, eps=1e-15): probs = softmax(logits) p = max(eps, probs[target_index]) return -math.log(p) def cce_gradient(logits, target_index): probs = softmax(logits) grads = list(probs) grads[target_index] -= 1.0 return grads
softmax + 交叉熵的梯度化简得极漂亮:真实类是(预测概率 − 1),其它类是(预测概率)。这种优雅的化简不是巧合,正是 softmax 与交叉熵成对出现的原因。
def label_smoothed_cce(logits, target_index, num_classes, alpha=0.1, eps=1e-15): probs = softmax(logits) loss = 0.0 for i in range(num_classes): if i == target_index: smooth_target = 1.0 - alpha + alpha / num_classes else: smooth_target = alpha / num_classes p = max(eps, probs[i]) loss += -smooth_target * math.log(p) return loss
基于余弦相似度,在正对相似度与负对相似度上做带温度的 softmax。
用第 04 节的两层网络(圆形数据集),分别配 MSE 与 BCE 训练,眼看交叉熵收敛更快、最终精度更高。这是「损失函数选错,模型钻空子」的最直观证据。
PyTorch 内置全部标准损失,数值稳定性已处理:
import torch import torch.nn as nn import torch.nn.functional as F predictions = torch.tensor([0.9, 0.1, 0.7], requires_grad=True) targets = torch.tensor([1.0, 0.0, 1.0]) mse_loss = F.mse_loss(predictions, targets) bce_loss = F.binary_cross_entropy(predictions, targets) logits = torch.randn(4, 10) labels = torch.tensor([3, 7, 1, 9]) ce_loss = F.cross_entropy(logits, labels) ce_smooth = F.cross_entropy(logits, labels, label_smoothing=0.1)
务必用 F.cross_entropy(而不是 F.nll_loss 配手写 softmax)。它把 log-softmax 与负对数似然合并在一次数值稳定的运算里。先 softmax 再取 log 稳定性更差——大指数相减会丢精度。对比学习大多用自定义实现或 lightly、pytorch-metric-learning 等库,核心循环一致:算两两相似度、在正负样本上做 softmax、反向传播。
本节产出(位于原课程 outputs/):
prompt-loss-function-selector.md:一个可复用提示,帮你选对损失函数。prompt-loss-debugger.md:一个诊断提示,当损失曲线看着不对时用。下一节,我们进入优化器——梯度告诉你方向,优化器决定走多远、多快,从 SGD 一路搭到 AdamW。