1.2 损失函数:模型如何知道自己错了多少
在上一节中,我们把神经网络理解成了一个可学习的参数化函数:
\[ \hat{y} = f(x; \theta) \]
其中,\(x\) 是输入,\(\hat{y}\) 是模型的输出,\(\theta\) 是模型内部需要学习的参数。所谓训练,本质上就是不断调整参数 \(\theta\),让模型的输出越来越接近我们真正想要的结果。
但是这里马上会出现一个问题:
模型怎么知道自己现在做得好不好?
假设我们输入一张猫的图片,模型给出了一个预测结果。仅仅得到这个输出还不够,我们还需要一种方法,将模型的预测和真实答案进行比较,并告诉模型:
- 这次预测错了多少?
- 当前这组参数表现得怎么样?
- 调整参数之后,模型到底变好了还是变差了?
这就是损失函数(loss function)要解决的问题。
损失函数会把模型的预测结果和真实目标进行比较,并最终得到一个数值。这个数值可以理解为模型当前预测的“错误程度”。训练神经网络的目标,就是不断调整参数,使这个损失尽可能小。
这一节,我们先不讨论参数究竟应该怎么调整,而是先弄清楚整个训练过程最重要的目标:
我们到底在优化什么?
1.2.1 从模型输出到训练目标
假设我们有一个简单的监督学习任务。对于输入 \(x\),我们已经知道对应的真实答案 \(y\)。模型接收输入以后,会根据当前参数 \(\theta\) 产生一个预测结果:
\[ \hat{y} = f(x; \theta) \]
这里需要特别区分两个符号:
- \(y\):真实答案,也叫做目标值(target);
- \(\hat{y}\):模型根据当前参数得到的预测结果(prediction)。
如果模型训练得很好,那么 \(\hat{y}\) 应该尽可能接近 \(y\)。
例如,在一个房价预测任务中,某套房子的真实价格是:
\[ y = 300 \]
而模型预测:
\[ \hat{y} = 280 \]
我们很容易看出,模型预测得并不完全正确。但是“预测不正确”只是一个定性的描述。对于训练来说,我们需要一个更加明确的数字来回答:到底错了多少?
一种最简单的想法,就是直接计算预测值和真实值之间的差:
\[ \hat{y} - y \]
在这个例子中:
\[ 280 - 300 = -20 \]
负号说明预测值比真实值小,但如果我们的目的只是衡量“错误有多大”,通常并不希望正负误差互相抵消。
因此,我们可以对误差取平方:
\[ (\hat{y} - y)^2 \]
于是:
\[ (280 - 300)^2 = 400 \]
这个数值就可以用来描述当前预测和真实答案之间的差距。
这其实已经是一个最简单的损失函数了。如果把它写成一般形式:
\[ L(\hat{y}, y) = (\hat{y} - y)^2 \]
其中,\(L\) 表示损失(loss)。损失越小,说明模型预测结果越接近真实答案;损失越大,说明模型当前的预测结果越差。
因此,我们终于有了一个可以直接优化的目标:
不断调整模型参数,让损失函数变小。
1.2.2 损失函数:把做得好不好变成一个数字
从直觉上来说,损失函数就是一个评分标准。只不过和考试不同,损失通常是越小越好。
对于一组预测结果 \(\hat{y}\) 和真实结果 \(y\),损失函数可以写成:
\[ L(\hat{y}, y) \]
由于模型的预测 \(\hat{y}\) 又是由输入 \(x\) 和参数 \(\theta\) 决定的:
\[ \hat{y} = f(x; \theta) \]
所以实际上,损失最终也是模型参数 \(\theta\) 的函数:
\[ L(f(x; \theta), y) \]
这一点非常重要。
表面上看,我们是在比较预测值和真实值;但从训练的角度来看,我们真正关心的是:
当前这组参数 \(\theta\) 会产生多大的损失?
因为输入数据和真实答案已经给定,我们真正能够改变的,是模型内部的参数。
于是,整个神经网络训练问题就可以写成:
\[ \min_{\theta} L(f(x; \theta), y) \]
这里的 \(\min\) 表示我们希望找到一组参数 \(\theta\),使损失函数尽可能小。
如果我们把损失函数 \(L\) 画出来,它大概长这样:

我们所要做的,就是在这个平面里,找到函数值最小的点。
这样一来,上一节所说的“学习”就有了更加具体的数学含义:
神经网络的学习,本质上是在参数空间中寻找一组更好的参数,使损失函数不断下降。
模型本身并不知道什么叫“猫”,也不知道什么叫“正确答案”。对于训练过程来说,它真正看到的,只是一个需要被减小的数值。这个数值,就是损失。
1.2.3 不同任务需要不同的损失函数
看到这里,你可能会产生一个疑问:既然损失函数只是衡量预测和真实答案之间的差距,那是不是所有任务都可以直接使用 \((\hat{y} - y)^2\)?并不是。
不同任务的输出形式不同,我们希望模型学习的目标也不同,因此需要使用不同的损失函数。最常见的情况可以粗略分为两类:回归(regression)问题和分类(classification)问题。
回归问题
回归任务的目标通常是预测一个连续数值。
例如:
- 根据房屋信息预测房价;
- 根据历史气温预测明天气温;
- 根据传感器数据预测某个物理量。
对于这类问题,一个常见的损失函数是均方误差(Mean Squared Error, MSE)。
如果一个 batch 中有 \(N\) 个样本:
\[ (x_1, y_1), (x_2, y_2), \ldots, (x_N, y_N) \]
模型分别得到预测:
\[ \hat{y}_1, \hat{y}_2, \ldots, \hat{y}_N \]
那么均方误差就是:
\[ L_{\text{MSE}} = \frac{1}{N} \sum_{i=1}^{N} (\hat{y}_i-y_i)^2 \]
它做的事情很直观:计算每一个样本预测误差的平方,然后取平均。预测越准确,MSE 就越小。
分类问题
分类任务则有所不同。
例如,在图片分类任务中,模型可能需要判断一张图片属于:
- 小猫;
- 小狗;
- 小鸟;
- 汽车。
此时模型的输出通常不是一个简单的连续数值,而是每个类别对应的一组分数。此时我们真正希望模型做到的是:让正确类别对应的预测概率尽可能大,错误类别对应的预测概率尽可能小。
因此,分类问题通常不会直接使用均方误差,而是使用交叉熵(cross entropy)等更加适合概率分类的损失函数。交叉熵具体是如何计算的,我们会在后面的多层感知机章节中专门介绍。现在只需要记住一点:
损失函数并不是固定的,它由我们希望模型完成的任务决定。
选择什么样的损失函数,本质上就是在告诉模型,什么样的预测才算是“好”的。
1.2.4 一个样本的损失和整个数据集的目标
到目前为止,我们讨论的主要是单个样本。对于一个训练样本 \((x_i, y_i)\),模型得到预测:
\[ \hat{y}_i = f(x_i; \theta) \]
然后计算对应的损失:
\[ L_i = L(\hat{y}_i, y_i) \]
但是神经网络显然不能只在一个样本上表现得好。
假设我们的训练集中一共有 \(N\) 个样本:
\[ \mathcal{D} = \{(x_1,y_1),(x_2,y_2),\ldots,(x_N,y_N)\} \]
那么我们希望找到一组参数,使模型在整个训练数据集上都尽可能表现良好。
最直接的做法,就是把所有样本的损失取平均:
\[ J(\theta) = \frac{1}{N} \sum_{i=1}^{N} L(f(x_i;\theta), y_i) \]
这里我们用 \(J(\theta)\) 表示整个训练目标。
于是,模型训练就变成了:
\[ \min_{\theta} J(\theta) \]
也就是说:
找到一组参数,使模型在训练数据上的平均损失尽可能小。
在实际深度学习训练中,我们通常不会一次把整个数据集都送入模型,而是每次只取一小批数据,也就是一个 batch。
假设一个 batch 中有 \(B\) 个样本,那么当前 batch 的平均损失可以写成:
\[ L_{\text{batch}} = \frac{1}{B} \sum_{i=1}^{B} L_i \]
训练时,我们会不断取出新的 batch,计算损失,然后根据损失调整模型参数。后面在介绍 PyTorch 的 Dataset、DataLoader 和完整训练循环时,我们还会再次看到这个过程。
1.2.5 训练,本质上是在寻找更小的损失
现在,我们终于可以把神经网络训练的整体流程串起来了。
对于一批训练数据,首先将输入送进模型:
\[ \hat{y} = f(x;\theta) \]
然后使用损失函数,将模型预测 \(\hat{y}\) 和真实答案 \(y\) 进行比较:
\[ L = L(\hat{y},y) \]
如果损失很大,说明当前参数并不好;如果损失比较小,说明当前参数产生的预测更加符合我们的目标。于是,我们接下来要做的事情就是:
调整参数 \(\theta\),让下一次计算得到的损失比现在更小。
整个过程可以粗略表示成:
\[ x \rightarrow f(x;\theta) \rightarrow \hat{y} \rightarrow L(\hat{y},y) \]
然后根据损失的信息更新参数:
\[ \theta \rightarrow \theta_{\text{new}} \]
再使用新的参数重新进行计算:
\[ x \rightarrow f(x;\theta_{\text{new}}) \rightarrow \hat{y}_{\text{new}} \rightarrow L_{\text{new}} \]
如果:
\[ L_{\text{new}} < L \]
说明这次参数调整至少让当前训练目标变好了,于是我们可以继续重复这个过程。
所以,神经网络训练就是一个不断循环的过程:
这就是神经网络训练最核心的框架。无论以后学习的是 MLP、CNN、Transformer,还是更复杂的 LLM,训练过程的基本结构都没有发生本质变化。模型结构可能越来越复杂,损失函数也可能不同,但核心仍然是:
\[ \text{Prediction} \rightarrow \text{Loss} \rightarrow \text{Update Parameters} \]
1.2.6 参数到底应该怎么调整?
现在我们已经知道了训练的目标:
\[ \min_{\theta} J(\theta) \]
但是还有一个最关键的问题没有解决:
知道损失很大以后,参数到底应该怎么改?
假设模型中只有一个参数 \(w\)。
当前:
\[ w = 2 \]
模型计算得到的损失是:
\[ L = 10 \]
现在我们知道 \(L=10\) 很大,希望把它减小。可是接下来怎么办?
我们可以让:
\[ w = 2.1 \]
也可以让:
\[ w = 1.9 \]
甚至可以直接改成:
\[ w = 10 \]
问题在于,仅仅知道当前损失是多少,并不能告诉我们参数应该往哪个方向移动。
如果参数只有一个,我们或许还能不断尝试。但现实中的神经网络通常包含大量参数:
\[ \theta = (w_1,w_2,\ldots,w_n) \]
此时我们真正需要知道的是:
- 调大 \(w_1\),损失会变大还是变小?
- 调整 \(w_2\),对损失有多大影响?
- 哪些参数应该调整得更多?
- 每一个参数应该朝哪个方向调整?
换句话说,我们需要知道:
损失函数对每一个参数究竟有多敏感。
而描述这种敏感度的工具,就是梯度(gradient)。
如果我们能够计算:
\[ \frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}, \ldots, \frac{\partial L}{\partial w_n} \]
就能够知道每个参数发生微小变化时,损失函数会如何变化。
但对于一个复杂的神经网络来说,最终的损失可能经过几十层甚至上百层计算才得到。我们又该如何高效地算出每一个参数对最终损失的影响?
这就是我们下一节要讲的几个非常重要的概念:计算图、前向传播、梯度与反向传播。
1.2.7 本章小节
这一节,我们给上一节中的“可学习函数”补上了一个明确的训练目标。
神经网络首先根据当前参数产生预测:
\[ \hat{y} = f(x;\theta) \]
然后通过损失函数比较预测结果 \(\hat{y}\) 和真实答案 \(y\):
\[ L(\hat{y},y) \]
对于整个训练数据集,我们希望找到一组参数 \(\theta\),使平均损失尽可能小:
\[ \min_{\theta} J(\theta) \]
因此,神经网络训练并不是让模型直接理解什么是“正确”或“错误”,而是先把我们的目标写成一个可以计算的损失函数,再不断调整参数,让这个损失逐渐减小。
到这里,训练问题可以概括成三个步骤:
- 使用当前参数进行预测;
- 通过损失函数衡量预测有多差;
- 调整参数,使损失进一步下降。
现在只剩下最后一个关键问题:
参数到底应该朝哪个方向调整?
仅仅知道损失是多少还不够,我们还需要知道每一个参数发生变化时,损失会如何变化。这个信息由梯度提供,而计算这些梯度,则需要用到接下来要介绍的计算图、前向传播和反向传播。
下一节,我们就来看神经网络究竟是怎样把最终的损失一步一步传回每一个参数的。