反向传播
神经网路的多层结构使其与传统的机器学习模型有本质区别。理解这种差异以及相应的训练方法,是掌握神经网络的关键。
与传统机器学习的区别
传统机器学习模型(如线性回归、逻辑回归、SVM)通常由简单函数组成,模型的参数较少且结构清晰,可以直接对损失函数求导得到解析解,或用数值方法快速优化。更重要的是,这些模型高度依赖人工设计的特征,特征工程的质量直接决定了模型的上限。
神经网络则完全不同。它是一个包含数百万甚至数十亿参数的复杂复合函数,通过多层结构自动学习特征表示。这种设计带来了两个核心优势:无需繁琐的特征工程,网络能够从原始数据中自动提取有意义的特征;强大的表达能力,深层网络理论上可以拟合任意复杂的函数。但代价是训练变得极其困难,无法直接对整个损失函数求导,需要专门的算法来解决。
从训练流程来看,传统机器学习通常一次性将所有数据用于参数更新(批量梯度下降),而现代深度学习受限于内存和计算资源,必须将数据分批次处理。这种差异不仅影响训练效率,也决定了训练的收敛性质——小批次训练带来的梯度噪声反而有助于模型跳出局部最优。
此外,深度学习的生态高度标准化,PyTorch 已经成为事实标准框架。这种标准化带来了统一的接口和工具链,但也意味着工程师必须按照框架规定的方式来思考和实现,很多细节(如计算图、自动微分)被封装在底层,需要理解其原理才能有效调试。
神经网络的训练挑战
训练神经网络的核心难题在于如何计算梯度。对于一个包含
要更新每个权重矩阵,需要计算
另外,即使能够计算梯度,深层网络还会遇到梯度消失和梯度爆炸的问题。梯度在向前传播过程中可能会变得极小(导致浅层参数几乎不更新)或极大(导致训练发散),这使得训练超过几层的网络几乎不可能。这些难题在 20 世纪 80 年代被解决之前,神经网络研究经历了漫长的寒冬。
基本原理
反向传播是解决上述难题的关键算法,其核心思想是从后向前递归计算梯度。
根据链式法则,因为损失
其中
这种动态规划思想将时间复杂度从指数级降低到线性级,使得深度网络的训练成为可能。直观理解,反向传播就像是在网络中回传误差信号,告诉每一层"你的输出应该朝什么方向调整",每一层根据这个信号和本地计算结果更新自己的参数。
计算图与自动微分
现代深度学习框架(如 PyTorch)使用计算图来实现自动微分。前向传播时,框架记录每个操作的输入和输出,构建一个动态图;反向传播时,框架按照图的逆序自动计算梯度。这种设计让用户只需要写前向传播代码,框架自动处理反向传播。
计算图的另一个优势是支持内存优化技术。传统的反向传播需要保存所有中间结果用于梯度计算,这对内存消耗巨大。现代框架支持梯度检查点(Gradient Checkpointing),只保存部分中间结果,需要时重新计算前向传播,用计算时间换内存空间,使得训练超大模型成为可能。
完整的训练流程
理解了反向传播的原理,我们来看完整的训练过程。神经网络训练是一个参数从无序到有序的演化过程,包含以下几个核心步骤。
初始化与前向传播
训练开始时,网络的所有权重参数都是随机初始化的小数值,通常使用正态分布或均匀分布,方差根据层的输入输出维度调整。此时的模型像一张白纸,做出的预测和真实结果相去甚远。
前向传播是训练循环的第一步。输入数据(一批图像或文本)进入网络,经过每一层的线性变换和激活函数,最终得到预测输出。这个过程本质上是在计算一个极其复杂的复合函数,输入是数据,参数是网络的权重和偏置。
损失计算与反向传播
计算损失函数是将模型的预测与真实标签进行比较,量化预测误差的过程。对于分类任务,常用交叉熵损失衡量预测概率分布与真实分布的差异;对于回归任务,常用均方误差衡量预测值与真实值的距离。
反向传播回答了"如何调整参数才能减小损失"这个问题。通过链式法则,从输出层向输入层逐层计算每个参数对损失的偏导数。梯度告诉我们参数应该增大还是减小,以及变化的幅度。现代深度学习框架自动完成了这个复杂的求导过程,工程师只需要调用 loss.backward() 即可。
参数更新与梯度清空
参数更新是训练循环的最后一步,根据计算得到的梯度调整网络参数。最简单的更新规则是梯度下降:
其中
清空梯度是容易被遗忘但至关重要的一步。PyTorch 默认会累积梯度,而不是每次反向传播后自动清空。如果忘记清空梯度,下次反向传播时新的梯度会累加到旧的梯度上,导致参数更新错误。这就是为什么训练循环中 optimizer.zero_grad() 必须在反向传播之前调用。
训练循环代码
PyTorch 中的标准训练循环如下:
for epoch in range(num_epochs):
for batch_x, batch_y in dataloader:
# 前向传播
pred = model(batch_x)
loss = criterion(pred, batch_y)
# 反向传播
optimizer.zero_grad()
loss.backward()
# 参数更新
optimizer.step()经过成千上万次这样的循环,模型的参数逐渐从随机初始化的状态演化到能够准确完成任务的状态。早期训练阶段损失下降很快,模型学习数据中的主要模式;后期训练阶段损失下降变慢,模型在微调细节。当验证集上的损失不再下降甚至开始上升时,说明模型开始过拟合,此时应该停止训练。
训练相关概念
深度学习中的训练涉及一些特有的概念,理解这些概念有助于掌握训练过程的节奏。
Epoch、Batch 与 Step
训练数据通常无法一次性全部送入内存或 GPU,因此需要分批处理。Batch(批次)是指每次用于参数更新的样本数量,通常取 32、64、128、256 等 2 的幂次以充分利用 GPU 并行计算能力。较小的 Batch Size 带来更多梯度噪声但更新更频繁,较大的 Batch Size 梯度估计更精确但单次更新开销更大。
Epoch(轮次)指的是模型完整遍历一次整个训练数据集。实际训练中通常会设置多个 Epoch(几十到几百个),让模型反复看到数据从而逐步收敛。Iteration/Step(步数)是一个 Epoch 内的参数更新次数,等于训练样本总数除以 Batch Size。整个训练过程的总步数等于 Epoch 数乘以每 Epoch 步数,这个数值通常用来设置学习率衰减的周期。
推理与训练
理解深度学习需要区分推理和训练两个过程。推理时模型已经训练完成,我们只关心输入数据经过网络后的输出结果,这个过程只需要前向传播。比如用 GPT-4 生成文本、用 ResNet 识别图片,都是在做推理,此时网络的所有参数都是固定的,不需要计算梯度。
训练则是一个反复迭代优化的过程,需要前向传播、计算损失、反向传播、参数更新四个步骤循环往复。从工程角度看,推理只需要一次前向传播,计算量固定且可预测,对延迟敏感;训练则需要大量迭代,计算量是推理的成千上万倍,对吞吐量敏感。这也是为什么训练通常需要昂贵的 GPU 集群,而推理可以在更便宜的硬件甚至手机端运行。
梯度问题与解决方案
虽然反向传播解决了梯度计算的问题,但深度网络训练中仍然会遇到各种梯度相关的难题。
梯度消失是指梯度在向前传播过程中逐渐变小,导致浅层参数几乎不更新,网络难以学习深层特征。这在使用 Sigmoid 等饱和激活函数时尤为严重,因为导数最大值只有 0.25。解决方案包括使用 ReLU 激活函数、残差连接(ResNet)、归一化层(BatchNorm)等。
梯度爆炸则相反,梯度在传播过程中不断增大,导致参数更新过大、训练发散。这通常发生在网络过深或初始化不当时。解决方案包括梯度裁剪、权重初始化(Xavier/He 初始化)、归一化层等。梯度消失/爆炸是早期深度学习难以训练超过几层网络的主要原因,也是 ResNet 等架构突破的关键所在。
理解反向传播的原理有助于调试训练问题。比如梯度为 NaN 时可以检查是否发生了数值溢出,或者某些层的梯度过小时可能发生了梯度消失。训练深度学习模型不仅是调用框架 API,更需要理解底层的计算原理,才能在出现问题时快速定位和解决。