第 3 章:神经网络如何学习
在第二章中,我们详细了解了神经网络的静态结构:由神经元组成的层,以及连接它们的权重和偏置。现在,我们将进入整个教程最核心的部分:神经网络是如何从数据中"学习"的?
这个学习过程是一个动态的、不断调整自身参数以求更佳预测的过程。我们将通过四个关键概念来揭示这个秘密:
- 前向传播 (Forward Propagation):数据如何通过网络产生一个预测?
- 损失函数 (Loss Function):如何量化这个预测的"好坏"?
- 梯度下降 (Gradient Descent):如何根据"好坏"程度,找到参数优化的方向?
- 反向传播 (Backpropagation):如何高效地在整个网络中执行这个优化?
让我们从第一步开始。
3.1 前向传播:从输入到输出
前向传播,顾名思义,是信息在神经网络中 从前向后 传递的过程。它描述了当给定一个输入样本时,网络是如何一步步进行计算,并最终在输出层得到一个预测值的完整流程。
这个过程非常直观,就是将我们在第二章学到的所有知识串联起来。
前向传播的步骤
我们以一个简单的、用于二元分类的网络为例。假设它有一个输入层、一个隐藏层和一个输出层。
图 3.1: 前向传播流程示意图。数据从输入层(绿色)开始,流经隐藏层(蓝色),最终到达输出层(红色)产生预测值。
对于一个输入样本 X
,其前向传播的计算流程如下:
-
输入层 -> 隐藏层
- 首先,隐藏层的每一个神经元都会接收来自输入层所有神经元的信号。
- 对于隐藏层中的 第 j 个神经元,它会计算一个加权和
z_j
,这和我们在感知器中学到的一样:
z j = ( ∑ i ( x i ⋅ w i j ) ) + b j z_j = (\sum_{i} (x_i \cdot w_{ij})) + b_j zj=(i∑(xi⋅wij))+bj
其中,x_i
是第i
个输入,w_ij
是从输入层第i
个神经元到隐藏层第j
个神经元的权重,b_j
是隐藏层第j
个神经元的偏置。 - 然后,将这个加权和
z_j
通过一个激活函数(比如我们学过的 ReLU 或 Sigmoid)处理,得到该神经元的输出a_j
:
a j = Activation ( z j ) a_j = \text{Activation}(z_j) aj=Activation(zj) - 对隐藏层中的所有神经元重复这个过程,我们就得到了整个隐藏层的输出
A_hidden
。
-
隐藏层 -> 输出层
- 现在,隐藏层的输出
A_hidden
成为了输出层的输入。 - 输出层的计算过程与隐藏层完全相同。假设我们的输出层只有一个神经元(用于二元分类),它的计算过程是:
- 计算加权和
z_output
:
z output = ( ∑ j ( a j ⋅ w j , output ) ) + b output z_{\text{output}} = (\sum_{j} (a_j \cdot w_{j,\text{output}})) + b_{\text{output}} zoutput=(j∑(aj⋅wj,output))+boutput - 应用激活函数得到最终预测
y_pred
:
y pred = Activation output ( z output ) y_{\text{pred}} = \text{Activation}_{\text{output}}(z_{\text{output}}) ypred=Activationoutput(zoutput)
(对于二元分类,这里的激活函数通常是 Sigmoid)
- 计算加权和
- 现在,隐藏层的输出
至此,一次完整的前向传播就完成了。 我们从一个原始输入 X
开始,通过网络中预设的权重和偏置,一步步计算,最终得到了一个预测结果 y_pred
。
值得注意的是,在网络未经训练时,由于权重和偏置都是随机初始化的,这个 y_pred
几乎肯定是错误的。
那么,我们如何知道它"错得有多离谱"?又该如何利用这个"错误"来指导网络调整参数,让下一次的预测更准一些呢?
这便是我们下一节要讨论的 损失函数。
3.2 损失函数:衡量预测的"错误"程度
损失函数(Loss Function),有时也被称为 成本函数(Cost Function) 或 目标函数(Objective Function),是神经网络学习过程中的"导航员"和"裁判"。
它的作用非常明确:用一个具体的数值来量化模型的预测值(y_pred
)与真实值(y_true
)之间的差距。
这个差距,我们称之为"损失"(Loss)或"误差"(Error)。
- 损失值越大,说明模型的预测越不准确,离真实答案"越远"。
- 损失值越小,说明模型的预测越精准,离真实答案"越近"。
因此,整个神经网络训练的 最终目标,就是通过调整权重和偏置,来 最小化这个损失函数的值。
选择哪种损失函数取决于我们正在处理的任务类型。下面我们介绍两种最常见的场景。
场景一:回归问题(Regression)
在回归任务中,我们的目标是预测一个连续的数值,比如房价、气温或者股票价格。对于这类问题,最常用的损失函数是 均方误差(Mean Squared Error, MSE)。
工作原理:MSE 计算的是所有样本的"预测值与真实值之差的平方"的平均值。
图 3.2: 均方误差(MSE)的可视化。它计算的是每个数据点(蓝点)到模型预测线(红线)的垂直距离(绿色虚线,即残差)的平方的平均值。(来源: Neuromatch Academy)
数学公式(假设我们有 n
个样本):
L MSE = 1 n ∑ i = 1 n ( y true ( i ) − y pred ( i ) ) 2 L_{\text{MSE}} = \frac{1}{n} \sum_{i=1}^{n} (y_{\text{true}}^{(i)} - y_{\text{pred}}^{(i)})^2 LMSE=n1i=1∑n(ytrue(i)−ypred(i))2
- 我们先计算每个样本的预测值和真实值之差
(y_true - y_pred)
。 - 然后将这个差值平方,这有两个好处:1) 保证结果是正数;2) 对较大的误差给予更重的"惩罚"。
- 最后将所有样本的平方误差加起来,求一个平均值。
场景二:分类问题(Classification)
在分类任务中,我们的目标是预测一个离散的类别标签,例如判断一封邮件是否为垃圾邮件(二元分类),或者识别一张图片中的动物是猫、狗还是鸟(多元分类)。
对于分类问题,最强大的损失函数是 交叉熵损失(Cross-Entropy Loss)。
工作原理:它的核心思想是:对于正确的预测,我们给予较小的"惩罚"(损失);对于错误的预测,我们给予巨大的"惩罚"。
图 3.3: 不同损失函数在二元分类中的对比(当真实标签为1时)。交叉熵损失(绿色实线)显示,当模型对正确类别的预测概率接近1时,损失趋近于0;而当预测概率接近0时,损失会急剧增加,给予错误的预测巨大的惩罚。(来源: Wikimedia Commons)
对于最常见的 二元分类(Binary Classification),其交叉熵损失(也称为 BCE Loss)公式如下:
L BCE = − 1 n ∑ i = 1 n [ y true ( i ) log ( y pred ( i ) ) + ( 1 − y true ( i ) ) log ( 1 − y pred ( i ) ) ] L_{\text{BCE}} = - \frac{1}{n} \sum_{i=1}^{n} \left[ y_{\text{true}}^{(i)} \log(y_{\text{pred}}^{(i)}) + (1 - y_{\text{true}}^{(i)}) \log(1 - y_{\text{pred}}^{(i)}) \right] LBCE=−n1i=1∑n[ytrue(i)log(ypred(i))+(1−ytrue(i))log(1−ypred(i))]
让我们来理解一下这个公式:
- 如果真实标签
y_true
是 1:公式简化为-log(y_pred)
。- 如果我们的预测
y_pred
也很接近 1(比如 0.99),那么log(y_pred)
接近 0,损失就很小。 - 如果我们的预测
y_pred
离谱地接近 0(比如 0.01),那么log(y_pred)
会趋近于负无穷,损失就会变得非常大。
- 如果我们的预测
- 如果真实标签
y_true
是 0:公式简化为-log(1 - y_pred)
。- 如果我们的预测
y_pred
也很接近 0(比如 0.01),那么1 - y_pred
接近 1,log(1-y_pred)
接近 0,损失就很小。 - 如果我们的预测
y_pred
离谱地接近 1(比如 0.99),那么1 - y_pred
接近 0,log(1-y_pred)
会趋近于负无穷,损失就会变得非常大。
- 如果我们的预测
这正是我们想要的:预测越有信心且越正确,损失越小;预测越有信心但越错误,损失就越大。
现在,我们有了一个明确的目标(最小化损失函数),也知道了如何衡量我们距离这个目标有多远。
接下来的问题是:我们应该 如何 调整那成千上万的权重和偏置,才能让这个损失值降低呢?我们是应该把某个权重调高一点,还是调低一点?调多少才合适?
明确的目标(最小化损失函数),也知道了如何衡量我们距离这个目标有多远。
接下来的问题是:我们应该 如何 调整那成千上万的权重和偏置,才能让这个损失值降低呢?我们是应该把某个权重调高一点,还是调低一点?调多少才合适?
这就是下一节 “梯度下降” 将要为我们解答的问题。
3.3 梯度下降:找到最小化损失的路径
现在,我们站在了问题的核心:我们有了一个可以量化错误的损失函数,我们如何系统地调整网络中成千上万的参数(权重 w
和偏置 b
),来让损失值变得越来越小呢?
暴力尝试显然是行不通的。我们需要一个聪明且高效的策略。这个策略就是 梯度下降(Gradient Descent)。
一个下山的类比
为了理解梯度下降,想象一个非常生动的场景:
你正置身于一座连绵起伏的山脉中,四周一片浓雾,你看不清山谷的最低点在哪里。你的任务是尽快到达山谷的底部。你该怎么办?
一个非常直观的策略是:环顾四周,找到脚下最陡峭的下坡方向,然后朝着这个方向迈出一步。 到达新位置后,你再次重复这个过程:环顾四周,找到新的最陡峭的下坡方向,再迈出一步。只要你坚持这么做,最终你将一步步地走到山谷的最低点。
这就是梯度下降算法的全部直觉。在这个类比中:
- 山脉的地形:就是我们的 损失函数。这是一个由所有网络参数(权重和偏置)决定的复杂、高维度的"地形"。
- 你在山上的位置:由当前网络的 所有参数值 决定。
- 你的海拔高度:就是当前参数所对应的 损失值。
- 我们的目标:找到这片地形的 最低点(Global Minimum),也就是损失函数的最小值。
- 最陡峭的下坡方向:这就是 梯度(Gradient) 的反方向。
图 3.4: 梯度下降的可视化。在这个损失函数的"地形"上,无论从哪个点开始(红点),梯度下降算法都会引导参数沿着最陡峭的下坡路径前进,最终到达一个局部或全局的最低点。
核心概念:梯度与学习率
梯度下降算法的核心由两个概念组成:
-
梯度(Gradient, ∇)
在数学上,梯度是一个向量,它指向函数值 增长最快 的方向。换句话说,梯度就是函数在当前位置的 最陡峭的上坡方向。那么,最陡峭的 下坡方向 自然就是梯度的 反方向(
-∇
)。在神经网络中,这个"函数"就是我们的损失函数
L
。梯度∇L
就是损失函数L
对所有参数(w_1, w_2, ..., b_1, b_2, ...
)求偏导数后组成的向量。这个向量告诉我们,在当前的位置,如何微调每一个参数,才能让损失值上升得最快。而我们只需要沿着它的反方向更新参数,就能最高效地降低损失。 -
学习率(Learning Rate, α)
找到了下山的方向后,我们还需要决定 每一步该迈多大。这个步长,就是 学习率。它是一个超参数(需要我们手动设定的值),用来控制每次参数更新的幅度。- 学习率太小:我们会像个谨小慎微的婴儿一样,每次只挪动一小步。虽然方向是对的,但下山速度会非常非常慢,训练过程会极其漫长。
- 学习率太大:我们可能会因为步子迈得太大而"冲过头",直接越过了山谷的最低点,甚至可能跳到了对面的山坡上,导致损失值不降反升,永远无法收敛到最低点。
因此,选择一个合适的学习率是训练神经网络中最关键的环节之一。
梯度下降的更新规则
结合梯度和学习率,我们就得到了梯度下降的参数更新规则。对于网络中的任何一个参数 θ
(它可以是任何权重 w
或偏置 b
),其更新过程如下:
θ new = θ old − α ⋅ ∇ θ L \theta_{\text{new}} = \theta_{\text{old}} - \alpha \cdot \nabla_{\theta}L θnew=θold−α⋅∇θL
这个公式的含义是:
- 计算损失函数
L
在当前参数θ_old
处的梯度∇_θ L
。 - 将梯度乘以学习率
α
,得到本次更新的步长。 - 从旧的参数值
θ_old
中减去这个步长,得到新的参数值θ_new
。
通过在整个训练数据集上反复迭代这个过程(即,对于每个样本或每个批次的样本,都计算梯度并更新一次参数),网络中的所有参数都会被逐步推向能使总损失最小化的最优值。
但是,这里又出现了一个巨大的挑战:一个现代神经网络的参数动辄成千上万,甚至数百万、数十亿。我们该如何高效地计算出损失函数对这每一个参数的梯度呢?
这就要引出神经网络优化中的最后一块,也是最神奇的一块拼图——反向传播。我们将在下一节详细探讨它。
3.4 反向传播:高效的梯度计算引擎
**反向传播(Backpropagation, BP)**是迄今为止训练神经网络最成功的算法。可以说,没有反向传播,就没有深度学习的今天。它解决的正是上一节末尾提出的那个核心挑战:如何在一个具有数百万甚至数十亿参数的复杂网络中,快速、高效地计算出损失函数对每一个参数的梯度。
它的基本思想非常优雅,完全建立在微积分的 链式法则(Chain Rule) 之上。
直观理解:责任的层层回溯
让我们先抛开复杂的数学公式,用一个直观的方式来理解反向传播。
在前向传播中,信息从输入层流向输出层,最终产生一个预测,并计算出总损失。现在,想象一下,这个最终的损失(误差)是一个"责任",我们需要将这个"总责任"公平且准确地分配回网络中的每一个参数(权重和偏置),看看它们各自对这个最终的错误贡献了多少"力量"。
反向传播做的就是这件事:它将损失 L
这个"总责任",从网络的 输出层开始,一层一层地向后传递,直到输入层。
- 输出层:在输出层,我们可以直接计算出损失对该层参数(权重和偏置)的梯度。这相对简单,因为它们离损失函数最近。
- 倒数第二层:这一层的参数并没有直接影响最终的损失,而是通过影响了输出层的输出来间接影响损失。那么,这一层某个参数的"责任"有多大呢?根据链式法则,它的大小等于:(它对输出层的影响) x (输出层对最终损失的影响)。由于后者我们已经在第一步算出来了,我们只需要计算前者,就能得到当前层参数的梯度。
- 继续向后:这个逻辑可以一直向后传递。任何一层参数的梯度,都可以通过它对下一层的影响,乘以【下一层已经计算出的梯度】来得到。
图 3.5: 反向传播中的梯度(或误差信号 δ)流动示意图。误差从最后一层(右侧)产生,并利用链式法则逐层向后传递,计算出每一层参数所应承担的"责任"。
就这样,误差信号像涟漪一样从后向前传播,每经过一层,我们就利用链式法则计算出该层参数的梯度。当这个过程到达输入层时,我们就已经拥有了网络中所有参数的梯度。
反向传播的两个阶段
因此,一次完整的参数更新(即梯度下降的一步)实际上包含两个阶段:
-
前向传播(Forward Pass):
- 将一批训练数据输入网络。
- 从输入层开始,逐层计算,直到输出层得到预测值。
- 根据预测值和真实值,计算出这一批数据的总损失。
- 在这个过程中,每一层的计算结果(比如加权和
z
和激活值a
)都需要被缓存下来,因为它们在反向传播阶段需要被用到。
-
反向传播(Backward Pass):
- 从最终的损失开始,计算损失函数对输出层参数的梯度。
- 利用链式法则,逐层向后计算每一层参数的梯度,直到输入层。
- 这个过程会用到前向传播中缓存的中间值。
当反向传播完成后,我们就得到了所有参数的梯度。此时,我们就可以使用上一节学到的梯度下降更新规则,来更新所有的权重和偏置了:
θ new = θ old − α ⋅ ∇ θ L \theta_{\text{new}} = \theta_{\text{old}} - \alpha \cdot \nabla_{\theta}L θnew=θold−α⋅∇θL
这个 “前向计算 -> 反向求导 -> 更新参数” 的循环,就是神经网络训练的核心。这个循环会不断地重复,成千上万次,直到损失函数的值收敛到一个足够小的范围,我们的网络也就"学会"了如何处理这类任务。
至此,我们已经完整地解构了神经网络学习的四大核心组件。在下一章,我们将把这些理论知识应用到实践中,使用 PyTorch 这个强大的深度学习框架,来亲手搭建和训练我们的第一个神经网络。