第 4 章 · 学习是怎么发生的

怎么衡量“错”:损失函数

网络现在能向前算出一个预测了,但它八成是错的。要让它“学”,第一步是给“错” 一个精确、可计算的定义——这就是损失函数(loss function)。 它把“模型这次错得多离谱”压缩成一个数字。这个数字,就是后面所有优化的指南针。

读完这一章,你会明白

  • 为什么需要把“错误”变成一个数字,以及它怎么指导训练;
  • 回归任务用的均方误差(MSE)是怎么回事;
  • 分类任务里,softmax 怎么把分数变成概率,交叉熵怎么衡量概率的差距;
  • “softmax + 交叉熵”为什么会得到一个特别漂亮的结果 δ = 预测 − 答案。

1. 为什么要把“错”变成一个数字

回忆第 0 章的训练循环:预测 → 对答案 → 算差距 → 微调。 这里的“算差距”,必须输出一个具体的数,程序才能比较“调整之前和之后,到底哪个更好”。

这个数,我们希望它满足两个朴素的要求:预测越接近答案,它越小; 完全正确时,它应该是 0(或最小)。剩下的,只是“用什么公式来算这个差距”而已。 不同任务,用不同的公式。

2. 回归任务:均方误差 MSE

如果模型要预测一个连续的数值(比如房价、温度),这叫回归。 最自然的“差距”就是:预测值和真实值差多少,然后平方(平方是为了让正负差距都变成正贡献,并且惩罚大错):

L = 12 (to)2 t = 真实值(target),o = 模型输出(output)。差得越多,平方后涨得越快

看看它在代码里有多直白:

src/deeplearning/loss/mse_loss.cpp
double MSELoss::Loss(double target, double output) {
  return 0.5 * (target - output) * (target - output);   1
}

double MSELoss::DerivLoss(double target, double output) {
  return -2.0 * (target - output);                      2
}
  1. Loss:就是上面的公式,算出“这一个输出错了多少”。
  2. DerivLoss:损失对输出的导数,反向传播要用它(第 6 章)。这里你只要先看懂它的符号:当 ot 小,(t − o) 为正,导数为负——它在告诉网络“你预测得太低了,把输出往大调”。导数前面的常数(这里是 2)不影响方向,只是缩放,会被学习率吸收。

3. 分类任务:先把分数变成“概率”

如果模型要从几个选项里选一个类别(比如这张图是 0–9 里的哪个数字),这叫分类。 网络最后一层会对每个类别输出一个分数(叫 logits),但这些分数可能是任意实数, 不能直接当“概率”。我们需要一个函数,把一组分数变成一组非负、且加起来等于 1 的概率—— 这就是 softmax:

pi = eziΣj ezj 先把每个分数取指数(保证为正),再除以所有指数之和(保证加起来为 1)

直觉:取指数会放大差距(分数高的更突出),除以总和则把它们归一化成概率。代码:

src/deeplearning/softmax/std_softmax.cpp · Normalize
long double sum = 0;
for (int i = 0; i < input.size(); i++)
  sum += std::exp(input[i]);                 1

for (int i = 0; i < input.size(); i++)
  output[i] = std::exp(input[i]) / sum;      2
  1. 第一遍循环:把每个 logit 取指数 exp,全部加起来得到分母 sum
  2. 第二遍循环:每个类别的概率 = 自己的指数 ÷ 总和。这样所有 output[i] 都在 (0, 1) 之间,且加起来正好是 1——一组合法的概率。

4. 用交叉熵衡量两个概率的差距

现在模型给出了一组预测概率 p,而正确答案是一个“只有正确类是 1、其余是 0”的分布 t(叫 one-hot)。怎么衡量这两组概率差多少?用 交叉熵(cross-entropy):

L = − Σi ti log(pi) = − log(p正确类) 因为 t 只有正确类是 1,求和最后只剩“正确类的预测概率”那一项

直觉非常顺:如果模型给正确类的概率很高(接近 1),log 接近 0,损失很小; 如果它给正确类的概率很低(接近 0),−log 会冲向很大——狠狠惩罚“自信地答错”。

项目里实现的是二元交叉熵(配合 Sigmoid 输出,判断“是/不是”):

src/deeplearning/loss/cross_entropy_loss.cpp
double CrossEntropyLoss::Loss(double target, double output) {
  return -target * log(output)
         - (1.0 - target) * log(1.0 - output);     1
}

double CrossEntropyLoss::DerivLoss(double target, double output) {
  return (output - target) / (output * (1.0 - output));  2
}
  1. 当真实标签 target = 1,只剩前半项 -log(output):输出越接近 1 损失越小;当 target = 0,只剩后半项 -log(1-output):输出越接近 0 损失越小。
  2. 导数看起来有点吓人(分母里有 output(1-output)),但下一节你会看到,它和 Sigmoid/softmax 的导数正好抵消,变得无比清爽。

5. 一个美妙的巧合:δ = 预测 − 答案

单独看,交叉熵的导数和 softmax 的导数都挺丑。但当你把它们合在一起 (softmax 算概率 + 交叉熵算损失),中间那些复杂的项会奇迹般地约掉, 最后末层要往回传的“误差信号” δ 简洁到不可思议:

δ = pt 预测概率 减去 真实答案。就这么简单
src/deeplearning/softmax/std_softmax.cpp · CalcDelta
double StdSoftmax::CalcDelta(double output, double target,
                            std::shared_ptr<LossFunction> loss_function) {
  switch (loss_function->GetLossType()) {
  case LOSS_MSE:           return output - target;   1
  case LOSS_CROSS_ENTROPY: return output - target;   2
  }
  return 0;
}
  1. 这就是上面说的 δ = pt。它会作为反向传播的“起点”(第 6 章)。
  2. 直觉太顺了:预测概率比答案高多少,就往回传多少“正向误差”;低多少,就传多少“负向误差”。这也是为什么分类任务几乎总是用“softmax + 交叉熵”这对黄金搭档。

6. 一批样本的损失,取个平均

训练时我们一次看很多样本,把每个样本、每个输出的损失加起来再求平均,得到这一批的整体损失:

src/deeplearning/loss/loss_base.cpp · AverageLoss
double LossFunction::AverageLoss(const std::vector<double> &target,
                                const std::vector<double> &output) {
  double result = 0;
  for (int i = 0; i < target.size(); i++)
    result += Loss(target[i], output[i]);   1
  result /= target.size();                  2
  return result;
}
  1. 把每个输出维度的损失累加起来。
  2. 除以个数得到平均损失。整个训练的目标,就是让这个平均损失尽可能小。

小结

  • 损失函数把“模型错得多离谱”变成一个数字:越准越小,完全正确时最小。
  • 回归用均方误差 MSE:差值的平方。
  • 分类先用 softmax 把分数变概率(非负、和为 1),再用交叉熵衡量与正确答案的差距,狠罚“自信地答错”。
  • “softmax + 交叉熵”的末层误差 δ = pt,异常简洁,是分类的黄金搭档。
  • DerivLoss 是损失对输出的导数,它是下一步“反向传播”的起点。

动手与思考

问题 1:模型对正确类给出 0.99 的概率,和给出 0.01 的概率,交叉熵损失差别大吗?

差别巨大。−log(0.99) ≈ 0.01(几乎没损失);−log(0.01) ≈ 4.6(损失很大)。交叉熵会狠狠惩罚“自信地答错”,这正是我们想要的。

问题 2:softmax 为什么要先取指数,而不是直接把分数除以总和?

直接除以总和无法处理负分数(可能出现负概率),也无法保证“分数越高越突出”。取指数能把任意实数变成正数,并放大高分与低分的差距,再归一化就得到一组合理的概率。

问题 3:为什么分类几乎总是“softmax + 交叉熵”一起用?

除了它们各自合理,组合起来梯度还特别干净:末层误差 δ = pt。计算简单、数值稳定,反向传播也更不容易出问题。

有了“错得多少”这个数字,下一章我们就要回答那个核心问题: 该往哪个方向、走多大一步,才能让这个数字变小?——梯度下降登场。