损失函数
这一章讲的是损失函数(Loss Function)——给模型"打分"的尺子。模型预测得好不好不能靠感觉,得有一个统一的量化标准:预测和真实答案差得越多,分数(损失)就越差。学完这一章,你会明白回归任务为什么用平方误差、分类任务为什么用交叉熵而不是直接数"答对几个",并且看到这一切背后统一的思想——最大似然:训练模型,本质就是让"看到手头这批数据"的概率最大。
损失函数:给模型打分
前几章的模型都是一个"函数家族":给定输入 $x$ 和参数 $\phi$,输出预测 $f[x, \phi]$。但怎么判断一组参数好不好?办法是拿它去"考"训练数据:对每个样本,把预测和真实输出 $y_i$ 比一比,把差距量化成一个数字。这个数字就是损失,计算损失的函数就叫损失函数 $L[\phi]$。
损失越小,模型越贴合数据。于是训练的目标就一句话——把参数拧到让损失最小:
$$\hat{\phi} = \arg\min_{\phi} L[\phi]$$这个式子意思是:在所有可能的参数里,找出让损失最小的那个 $\hat{\phi}$。至于怎么找,是下一章梯度下降的活;本章的关键问题是:"差距"应该怎么量化?不同任务、不同数据,合适的尺子不一样。
回归任务:L2 损失与 L1 损失
回归任务要预测一个连续的数,比如房价。最自然的"差"就是预测值减真实值。把每个样本的差平方后加起来,就得到平方损失(也叫 L2 损失;再除以样本数,就是常说的均方误差 MSE):
$$L[\phi] = \sum_{i=1}^{I} (f[x_i, \phi] - y_i)^2$$这个式子的意思是:对每个样本,算预测与真实的差,平方,再全部加起来。平方有两层作用:一是正差、负差都算"错";二是放大错误——差 2 的样本,惩罚是差 1 的 4 倍,错得越离谱,挨的罚越狠。
如果把平方换成绝对值,就得到绝对损失(L1 损失):
$$L[\phi] = \sum_{i=1}^{I} |f[x_i, \phi] - y_i|$$两者最大的区别在于对异常值(outlier)的敏感度。假设房价数据里混进一条"1 块钱的别墅"这种离谱记录:对 L2 来说,这一条的平方误差可能比其余所有样本加起来还大,模型会拼命去讨好它,把整条拟合线都拉歪;对 L1 来说,它只是一个普通大小的误差,掀不起大浪。所以 L1 对异常值更稳健,代价是它在零点不可导、没有 L2 那么好优化。数据里"脏样本"多,用 L1;数据干净,用 L2 更高效。
L2 像"平方罚款":超速一倍罚四倍钱,一次特别离谱的超速会占掉你全年罚款的大头。L1 像"按公里数罚款":每超一公里罚固定金额。所以 L2 怕大错,会被异常值牵着鼻子走;L1 更"佛系",个别离谱记录不影响大局。
最大似然:让"看到的数据"概率最大
上面的损失是"拍脑袋"设计的,有没有更系统的思路?有,而且贯穿全书。换个角度看模型:它不再直接输出一个数,而是输出一个概率分布——"我认为 $y$ 落在各个位置的可能性有多大"。对回归,就是预测一个以某个值为中心的正态分布;对分类,就是预测每个类别的概率。
这样一来,训练目标变得非常自然:让真实答案在这组数据下出现的概率尽可能大。把所有样本的概率乘起来:
$$\hat{\phi} = \arg\max_{\phi} \prod_{i=1}^{I} \Pr(y_i | f[x_i, \phi])$$这个式子意思是:找一组参数,让"这批 $y$ 恰好被看到"的联合概率最大。这就是最大似然(Maximum Likelihood)准则——"似然"就是"数据在模型下有多大概率出现"。顺便解开一个悬念:为什么回归用平方?因为在最大似然框架下假设误差服从正态分布,把正态分布的负对数似然展开、丢掉与参数无关的项,推出来的正好就是 L2 损失——平方不是拍脑袋拍的。
不过连乘有个工程问题:每个概率都小于 1,几万个乘起来会小到计算机都存不下。解决方法是取对数——对数单调递增,不改变最大值的位置,还把连乘变成连加:
$$\hat{\phi} = \arg\min_{\phi} \left[ -\sum_{i=1}^{I} \log \Pr(y_i | f[x_i, \phi]) \right]$$习惯上我们喜欢"最小化",于是取个负号,得到负对数似然(Negative Log-Likelihood, NLL),这就是最终的损失函数。注意三句话说的是同一件事:最大化似然、最大化对数似然、最小化负对数似然。
桌上两个不透明的盒子:A 盒 90% 是红球,B 盒只有 10%。你闭眼连抽 10 次,全是红的——你几乎可以肯定抽的是 A 盒。最大似然就是这个逻辑:既然这堆数据真的出现了,那就选那个"最能解释它们"的模型。
分类任务:为什么用交叉熵
分类任务里,模型对每个类别输出一个分数(logit),分数越高越像这个类。为什么不直接拿"答对几个"(准确率)当损失?因为准确率是台阶函数:参数挪一点点,答对的数量常常不变,梯度几乎处处为 0,梯度下降一步都迈不出去;而且它不区分"差一点"和"差十万八千里"。
正确做法分两步。第一步,用 softmax 把分数变成概率分布:
$$\text{softmax}_k[z] = \frac{\exp[z_k]}{\sum_{k'=1}^{K} \exp[z_{k'}]}$$意思是:把第 $k$ 类的分数取指数,再除以所有类别指数之和。指数保证输出为正,除以总和保证所有类别的概率加起来等于 1。分数最高的类别,得到的概率最大——softmax 把"谁分数高"翻译成了"谁赢的概率大"。
第二步,把真实类别对应的概率取负对数,对所有样本求和:
$$L[\phi] = -\sum_{i=1}^{I} \log \text{softmax}_{y_i}[f[x_i, \phi]]$$这就是多类别交叉熵损失。它平滑——参数稍微变好,损失就降一点,梯度处处有值,可以放心用梯度下降;它还对"自信地错"惩罚特别狠:该给猫 100% 概率却给了狗,这个样本的损失会大得惊人。交叉熵可以通俗地理解为"惊讶程度":真实答案在模型眼里越不可能,损失越大。推断时,直接取概率最大的类别当预测即可。
二分类是它的特例:把网络输出过一个 sigmoid 函数 $\text{sig}[z] = \frac{1}{1 + \exp[-z]}$ 压到 $(0, 1)$ 区间,当作"属于 1 的概率",得到的损失就是二元交叉熵。其实交叉熵和最大似然是同一件事——最小化交叉熵就是最小化"模型分布"与"数据经验分布"之间的距离,推导到最后,正是负对数似然。
import numpy as np
def softmax(z):
# 先减去最大值再取指数,防止 exp 溢出(结果不变)
e = np.exp(z - np.max(z, axis=-1, keepdims=True))
return e / e.sum(axis=-1, keepdims=True)
def cross_entropy(logits, labels):
# logits: 每个样本对每个类别的原始分数
# labels: 每个样本的真实类别索引
probs = softmax(logits)
n = len(labels)
picked = probs[np.arange(n), labels] # 取出真实类别的概率
return -np.mean(np.log(picked + 1e-12)) # 取负对数再求平均
logits = np.array([[2.0, 1.0, 0.1], # 样本 1:分数偏向类别 0
[0.5, 3.0, 1.5]]) # 样本 2:分数偏向类别 1
labels = np.array([0, 1]) # 两个样本都"猜对"了
print(cross_entropy(logits, labels)) # 输出一个较小的损失
把 labels 改成 [2, 2] 再跑一遍(模型在两个样本上全猜错),损失会明显变大——这就是"真实类别概率越低、惩罚越狠"的直观体验。PyTorch 里的 CrossEntropyLoss 内部做的就是这件事:log-softmax 加负对数似然。