上一篇文章我们系统学习了 ANN 的网络结构、激活函数、参数初始化和损失函数,走完了前向传播的完整路径。但有一个关键问题还没回答------网络是怎么学习的?
答案就是反向传播(Backpropagation)。如果说前向传播是"数据正向走一遍得到预测",那么反向传播就是"误差反向走一遍修正参数"。没有反向传播,神经网络就只是一个固定的函数,根本学不到任何东西。
今天这篇文章,我们走完 ANN 的另一半核心:从反向传播的原理出发,理解梯度是怎么算出来的,再看参数如何沿着梯度的反方向更新,系统梳理六大常用优化器的原理与选型,接着学习四种正则化手段解决过拟合问题,最后用一个 MNIST 手写数字识别的完整案例,把所有知识串起来。这是从"理解神经网络结构"到"能动手训练模型"的完整一跃。
十、反向传播:神经网络的"学习引擎"
什么是反向传播
定义:反向传播是利用链式法则,计算出神经网络每一层参数的梯度,然后利用梯度更新参数,使损失逐步减小的过程。
这句话里有三个关键词:链式法则 、梯度 、更新参数。我们逐一拆解。
链式法则是微积分里的概念------对于复合函数求导,可以把每层函数的导数乘起来。神经网络的前向传播就是一个大的复合函数:输入经过第一层、再经过第二层......最后得到 loss。反向传播就是把这个复合函数的导数,从后往前一层一层地算出来。
梯度是每个参数的偏导数组成的向量,它告诉我们:这个参数往哪个方向动,loss 会增加得最快。知道了梯度,我们把参数沿着梯度的反方向挪一点点,loss 就会下降一点点------这就是梯度下降。
所以反向传播的本质,就是从 loss 出发,从后往前,用链式法则把每一层每个参数的梯度都算出来。有了梯度,后面的参数更新就水到渠成了。
反向传播的三步流程
前向传播: x → 隐藏层1 → 隐藏层2 → 输出 → loss
反向传播: ← ← ← ← ← ←
梯度逐层传递,逐层计算
反向传播可以概括为三步:
第一步:从输出层开始计算梯度。 先用损失函数算出预测值和真实值的误差,再求损失函数对输出层输入的梯度。这是反向传播的起点------误差从这里开始往回传。
第二步:逐层向前传播梯度。 从输出层往输入层的方向,一层一层地计算。每一层做两件事:一是算出本层每个参数(w 和 b)的梯度,二是算出要传给上一层的梯度(即损失对本层输入的梯度)。这样一层一层往前传,直到输入层。
第三步:计算完成。 所有参数的梯度都算出来了,存在各自的 .grad 属性中,等待优化器来更新。
计算图的概念
PyTorch 的自动微分是基于计算图实现的。什么是计算图?就是把前向传播的每一步运算用一张图表示出来------节点是张量,边是运算。
举个简单的例子:y = w * x + b,对应的计算图是:
w ──→ [*] ──→ [+] ──→ y
x ──→ ↑ ↑
b
前向传播时,PyTorch 边计算边构建这张图。调用 loss.backward() 时,PyTorch 从 loss 节点出发,沿着计算图的边反向走,每经过一条边就乘上这条边对应的导数,最后到达每个参数节点时,就得到了该参数的梯度。这就是自动微分的工作原理。
计算图是动态构建的------每一轮前向传播都重新构建一张新的图。这也是 PyTorch 被称为"动态图"框架的原因。
反向传播的特点
第一,方向和前向传播相反。 前向传播从前往后,反向传播从后往前。一前一后,构成一个完整的训练循环。
第二,依赖前向传播的结果。 反向传播需要用到前向传播中每一层的输出值(激活值),因为求导的时候要代入。所以前向传播时,PyTorch 会把中间结果缓存下来,供反向传播使用。
第三,梯度可以累加。 上一篇文章提到过,多次调用 .backward() 梯度会累加。这是 PyTorch 的设计------既方便了梯度累积(用小 batch 模拟大 batch),也要求我们记得每次训练前清零梯度。
十一、更新参数:梯度下降的三种形态
算出梯度之后,下一步就是用梯度来更新参数。最基本的方法是梯度下降,但梯度下降也有不同的"口味"------区别在于每轮更新用多少数据。
三种梯度下降对比
全量梯度下降(BGD) 随机梯度下降(SGD) 小批量梯度下降(Mini-batch)
───────────────── ────────────────── ──────────────────────
每次用全部数据 每次用1条数据 每次用一小批数据(如32条)
更新稳定但慢 更新快但震荡大 兼顾速度与稳定
| 方法 | 每次使用数据量 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全量梯度下降 BGD | 全部样本 | 收敛稳定,梯度准确 | 计算慢,内存压力大 | 小数据集 |
| 随机梯度下降 SGD | 1 条样本 | 更新快,适合大数据 | 震荡大,不稳定 | 很少直接使用 |
| 小批量梯度下降 Mini-batch | 一小批(32/64/128) | 兼顾速度与稳定,最常用 | 需要选 batch_size | 深度学习默认选择 |
全量梯度下降(Batch Gradient Descent):每轮把所有数据都跑一遍,算出总梯度后再更新一次参数。优点是梯度准确、下降稳定;缺点是数据量大的时候慢得离谱,而且内存可能装不下。
随机梯度下降(Stochastic Gradient Descent):每来一条数据就算一次梯度、更新一次参数。优点是更新快、适合在线学习;缺点是单条数据的梯度噪声大,参数更新方向飘忽不定,loss 曲线震荡剧烈。
小批量梯度下降(Mini-batch Gradient Descent) :每次取一小批数据(比如 32 条),算这批的平均梯度,然后更新一次参数。这是前两者的折中------既比全量快很多,又比单条稳定,是深度学习中最常用的方式。我们平时说的"SGD",很多时候其实指的是小批量 SGD。
参数更新公式
不管哪种梯度下降,参数更新的公式都是一样的:
w = w - lr × ∂L/∂w
b = b - lr × ∂L/∂b
其中 lr 是学习率,∂L/∂w 是损失对权重的梯度。减号表示沿着梯度的反方向走------因为梯度指向 loss 增加最快的方向,反方向就是 loss 减少最快的方向。
学习率的选择很关键:太大容易震荡甚至发散,太小收敛太慢。这是调参时第一个要关注的超参数。
十二、优化器:让梯度下降更聪明
基础的 SGD 虽然简单,但有不少问题------遇到狭长的山谷震荡剧烈、对所有参数一视同仁、不会根据情况调整学习率。为了解决这些问题,研究者们提出了各种改进的优化算法,也就是优化器。
PyTorch 把这些优化算法都封装好了,使用时只需一行 optimizer = torch.optim.XXX(model.parameters(), lr=...)。但想要用好它们,得先理解每个优化器的原理和特点。
SGD(随机梯度下降)
特点:最基础的优化器,每次用当前 mini-batch 的梯度更新参数。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
优点:
-
结构简单,容易理解
-
每个batch速度快,占用内存小
-
有一定的随机性,可能有助于跳出局部最优
缺点:
-
学习率是全局的,所有参数用同一个学习率
-
收敛速度慢,容易在梯度小的方向停滞
-
在梯度方向变化剧烈的地方震荡严重
适用场景:小数据集、简单模型、或者想自己精细控制训练过程时使用。
Momentum(动量法)
特点:在 SGD 的基础上加入了"动量"------就像小球从山上滚下来,越滚越快。它记录了历史梯度的加权平均(通常用指数移动平均),当前梯度加上历史动量,共同决定更新方向和幅度。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
优点:
-
加速收敛,特别是在梯度方向一致的维度
-
减小震荡,在梯度方向来回变的地方更稳定
-
有助于跳出局部最优
缺点:
-
多了一个超参数 β(动量系数),需要调
-
速度太快可能冲过最优点
动量系数通常设为 0.9,意思是"保留 90% 的历史动量,加上 10% 的新梯度"。
Nesterov(NAG,涅斯捷罗夫加速梯度)
特点:Momentum 的改进版。Momentum 是"先算当前梯度,再加动量";Nesterov 是"先按动量往前迈一步,再在那个位置算梯度,最后加动量"。相当于有了"前瞻性"------先看看前面的情况,再决定怎么走。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)
优点:
-
比 Momentum 更聪明,减少冲过最优点的情况
-
收敛更快更稳定
缺点:
-
计算稍微复杂一点
-
实际效果提升不一定很明显
Nesterov 在理论上比 Momentum 更好,但在深度学习的实践中,两者差别不大,很多时候 Momentum 就够用了。
Adagrad(自适应梯度)
特点 :每个参数有自己的学习率。它累积每个参数历史梯度的平方和,学习率除以这个累积值------梯度大的参数学习率降得快,梯度小的参数学习率降得慢。这样每个参数能根据自己的情况自适应调整学习率。
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.01)
优点:
-
自适应学习率,不同参数不同对待
-
对稀疏数据效果好(不常出现的特征梯度小,学习率降得慢)
缺点:
-
学习率一直在衰减,训练到后期学习率可能太小,模型学不动了
-
初始梯度大的话,学习率降得太快
Adagrad 的核心问题是学习率单调递减------越训越慢,后期几乎不动。后面的 RMSprop 和 Adam 就是为了解决这个问题。
RMSprop(均方根传播)
特点 :在 Adagrad 的基础上做了改进------用指数移动平均来累积梯度平方,而不是全部累加。这样早期的梯度会逐渐被遗忘,近期的梯度更重要,学习率不会一直衰减下去。
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)
优点:
-
解决了 Adagrad 学习率单调递减的问题
-
自适应学习率,适合非平稳目标
-
在 RNN 等模型上表现不错
缺点:
-
多了一个超参数 α(通常设 0.9)
-
还是依赖全局学习率
RMSprop 和 Adagrad 的关系,就像 EMA 和普通累加的关系------一个会遗忘历史,一个不会遗忘。
Adam(自适应矩估计)
特点 :Momentum + RMSprop 的结合体。它同时维护两个指数移动平均值:一阶矩(梯度的均值,类似动量)和二阶矩(梯度平方的均值,类似 RMSprop)。然后用这两个值来自适应调整每个参数的学习率。
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))
优点:
-
结合了动量和自适应学习率的双重优势
-
收敛快,效果稳定
-
对不同问题鲁棒性好,调参成本低
-
最常用的"万能"优化器
缺点:
-
超参数多(β1、β2、ε),虽然默认值通常就很好
-
有时候在某些任务上泛化性不如精心调过的 SGD+Momentum
Adam 几乎是现在深度学习的默认选择------如果你不知道用什么优化器,先用 Adam 试试,通常不会太差。
怎么选优化器?一张决策表
| 优化器 | 核心思想 | 学习率 | 收敛速度 | 稳定性 | 推荐指数 |
|---|---|---|---|---|---|
| SGD | 基础梯度下降 | 全局固定 | 慢 | 一般 | ★★ |
| Momentum | 加动量加速 | 全局固定 | 中 | 较好 | ★★★ |
| Nesterov | 前瞻性动量 | 全局固定 | 中快 | 较好 | ★★★ |
| Adagrad | 历史梯度平方累积 | 自适应(递减) | 中 | 好 | ★★ |
| RMSprop | 指数平均梯度平方 | 自适应(稳定) | 快 | 好 | ★★★★ |
| Adam | 动量 + RMSprop 结合 | 自适应 | 最快 | 很好 | ★★★★★ |
实用建议:
-
不知道用什么,先试 Adam。学习率设 0.001,大多数情况下都能跑起来,效果还不错。
-
追求最终精度,试试 SGD + Momentum。虽然收敛慢,但如果调参得当,最终泛化效果可能比 Adam 更好。
-
稀疏数据 / 词嵌入:Adagrad 或 RMSprop 效果通常不错。
-
RNN 系列模型:RMSprop 和 Adam 都是常用选择。
-
最终建议:先用 Adam 快速迭代验证想法,确认模型没问题后,如果想刷精度再换成 SGD+Momentum 慢慢调。
十三、正则化:给模型戴上"紧箍咒"
什么是过拟合
在讲正则化之前,先回顾一下过拟合。
训练误差 ↘ ↗ 泛化误差
↘ ↗
↘ ↗
↘ ↗
↙
最低点
训练轮数
过拟合就是:模型在训练集上表现越来越好,但在测试集上先变好后变差。模型把训练数据的细节甚至噪声都记住了,学到的不是通用规律,而是"背诵"了训练集。换一批没见过的数据,表现立刻跳水。
深度学习模型参数多、容量大,天生就容易过拟合。正则化就是一系列防止过拟合的技术,核心思想是给模型增加约束,降低它的"自由度",让它只能学到真正有用的规律。
下面介绍四种最常用的正则化方法。
L1 / L2 正则化
核心思想:在损失函数后面加一个惩罚项,让权重不能太大。
L2 正则化(也叫权重衰减,Weight Decay)的损失函数:
总损失 = 原始损失 + λ × (w₁² + w₂² + ... + wₙ²)
L1 正则化的损失函数:
总损失 = 原始损失 + λ × (|w₁| + |w₂| + ... + |wₙ|)
其中 λ 是正则化强度------λ 越大,惩罚越重,模型越"保守"。
L2 正则化:惩罚权重的平方和。它让所有权重都变得比较小,因为大权重会带来很大的惩罚。权重小了,模型对输入的微小变化就不敏感,泛化能力自然更强。L2 是最常用的正则化方式。
为什么权重小了就能防止过拟合?直觉上的解释是:如果权重很小,输入的微小变化不会导致输出剧烈变化,模型更"平滑"。而如果权重很大,一点点输入扰动就能让输出跳来跳去,模型对噪声过于敏感。L2 正则化就是通过惩罚大权重,强制模型走"平滑路线",从而提升泛化能力。
L1 正则化 :惩罚权重的绝对值。它有一个特殊效果------会让很多权重变成 0,相当于自动做了特征选择,只保留最重要的特征。如果你的数据特征很多但大部分没用,L1 可能更合适。
两者对比:
| 方法 | 惩罚形式 | 效果 | 特点 |
|---|---|---|---|
| L2 正则化 | 权重平方和 | 权重都变小 | 更常用,计算方便 |
| L1 正则化 | 权重绝对值和 | 很多权重变0,稀疏化 | 自动特征选择 |
PyTorch 中的使用也很简单,优化器自带 weight_decay 参数:
# L2 正则化,weight_decay 就是 λ
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=0.0001)
一行代码就搞定了。weight_decay 的值通常很小,比如 0.0001 或 0.001------太大了模型会欠拟合。
Dropout:随机"丢弃"神经元
核心思想:训练的时候,随机让一部分神经元暂时"死掉"(输出设为 0),迫使模型不能依赖任何单个神经元,从而学习更鲁棒的特征。
训练时(随机丢弃): 测试时(全部使用):
○ ○ ○ ○ ○ ○
○ ● ○ ○ ○ ○
● ○ ● → ○ ○ ○
○ ○ ○ ○ ○ ○
(● 表示被丢弃的神经元)
Dropout 的工作方式很巧妙:训练时随机丢,测试时全开。就像军训时每次随机抽几个人休息,剩下的人必须顶上------这样每个人都得学会所有技能,不能指望别人。
Dropout 有一个参数 p,表示神经元被丢弃的概率。比如 p=0.5 就是每个神经元有 50% 的概率被丢掉。p 越大,正则化效果越强;但太大了模型学不到东西,通常设 0.2 ~ 0.5。
PyTorch 中使用 Dropout 非常简单:
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout1 = nn.Dropout(p=0.3) # 30%的神经元被丢弃
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(p=0.3)
self.fc3 = nn.Linear(128, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.dropout1(x) # 在激活之后加 Dropout
x = self.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
注意 Dropout 在 model.train() 和 model.eval() 下的行为不一样:训练时生效,测试时自动关闭。所以别忘了在验证和测试时调用 model.eval()。
Dropout 还有一个有趣的特性------它相当于做了"模型集成"。每次随机丢弃不同的神经元,相当于训练了一个不同的子网络。测试时把所有神经元都用上,相当于把这些子网络的预测结果做了平均。这和随机森林的集成思想异曲同工------一个模型训练,多个模型投票,效果自然更好。
早停(Early Stopping)
核心思想:训练过程中盯着验证集的表现,一旦验证集性能不再提升,就提前停止训练。
性能
↑ 训练集 .
│ . ↗
│ . ↗
│ . ↗ ← 验证集达到最优,停止训练
│ .↗.↘
│ ↗ . ↘
│ ↗ . ↘
└────────────────→ 训练轮数
最优epoch
道理很简单------模型刚开始训练时,训练误差和验证误差都在下降;但当过拟合开始发生时,训练误差还在降,验证误差反而开始上升。早停就是在验证误差开始上升的那个点"踩刹车",把那个时候的模型保存下来。
早停的实施步骤:
-
把数据分成训练集和验证集
-
每训练一轮(epoch),在验证集上评估一次
-
如果验证集性能比之前最好的还好,就保存当前模型
-
如果连续很多轮(叫 patience)验证集都没提升,就停止训练
PyTorch 中没有内置的早停功能,但实现起来很简单:
best_val_loss = float('inf')
patience = 10 # 容忍多少轮不提升
counter = 0
for epoch in range(100):
train(...)
val_loss = validate(...)
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), 'best_model.pth')
counter = 0
else:
counter += 1
if counter >= patience:
print("早停!模型不再提升")
break
早停是最简单也最有效的正则化手段之一------几乎没有额外计算开销,效果还很好。实际项目中,早停几乎是标配。
数据增强(Data Augmentation)
核心思想:不增加数据采集成本,通过对现有数据做各种变换,"人造"出更多训练数据。数据越多,模型越难记住所有样本,泛化能力自然就提升了。
数据增强在图像领域最常用,常见的变换方式:
| 变换方式 | 说明 | 示例 |
|---|---|---|
| 随机旋转 | 图像旋转一定角度 | 旋转 ±15° |
| 随机翻转 | 水平或垂直翻转 | 左右翻转 |
| 随机缩放 | 放大或缩小 | 缩放 0.8~1.2 倍 |
| 随机裁剪 | 随机裁剪一部分再缩放回原尺寸 | 随机裁剪 |
| 亮度/对比度变化 | 调整图像的亮度和对比度 | 亮度 ±20% |
| 加噪声 | 给图像加随机噪声 | 高斯噪声 |
PyTorch 的 torchvision 库提供了丰富的数据增强工具:
from torchvision import transforms
transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(15), # 随机旋转 ±15度
transforms.RandomResizedCrop(224), # 随机裁剪再缩放
transforms.ColorJitter(brightness=0.2, contrast=0.2), # 颜色抖动
transforms.ToTensor(),
transforms.Normalize(mean=[0.5], std=[0.5])
])
数据增强的好处是:只增加训练时间,不增加推理时间------因为推理的时候不需要做增强。它是提升模型泛化能力的"免费午餐"。
需要注意的是,数据增强不是越"猛"越好。增强的程度要和实际场景匹配------比如你识别的是正常的手写数字,就没必要把图片旋转 90 度,因为现实中没人把数字倒着写。增强过度反而会让模型学不到正确的模式,甚至损害性能。
正则化方法总结
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| L2 正则化 | 惩罚大权重 | 简单,一行代码 | 效果有限 | 几乎所有场景 |
| Dropout | 随机丢弃神经元 | 效果好,灵活 | 训练变慢 | 全连接层后常用 |
| 早停 | 验证集不升就停 | 零成本,效果好 | 需要验证集 | 所有训练场景标配 |
| 数据增强 | 人工扩充数据 | 提升明显,不影响推理 | 只增加训练时间 | 图像数据首选 |
实战建议:这几种方法不是互斥的,通常组合使用效果最好。比如 L2 + Dropout + 早停 + 数据增强,四管齐下,过拟合基本就不是问题了。
十四、实战案例:手写数字识别
理论讲了这么多,是时候动手了。我们用经典的 MNIST 手写数字识别数据集,把前面学到的所有知识串起来:数据加载 → 模型搭建 → 损失函数 → 优化器 → 训练循环 → 正则化 → 测试评估。
MNIST 数据集包含 60,000 张训练图片和 10,000 张测试图片,每张是 28×28 的灰度手写数字(0-9)。任务就是让模型识别图片中的数字是几。
完整代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torch.utils.tensorboard import SummaryWriter
# === 1. 数据准备 ===
transform = transforms.Compose([
transforms.ToTensor(), # 转 Tensor
transforms.Normalize((0.1307,), (0.3081,)) # 标准化(MNIST的均值和标准差)
])
train_dataset = datasets.MNIST(root='./data', train=True,
download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False,
download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)
# === 2. 模型搭建(带 Dropout 正则化)===
class ANN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28 * 28, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.3) # Dropout 正则化
def forward(self, x):
x = x.view(-1, 28 * 28) # 展平:[64, 1, 28, 28] → [64, 784]
x = self.relu(self.fc1(x))
x = self.dropout(x)
x = self.relu(self.fc2(x))
x = self.dropout(x)
x = self.fc3(x)
return x
model = ANN()
# === 3. 损失函数和优化器(带 L2 正则化)===
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4) # L2正则化
# === 4. TensorBoard ===
writer = SummaryWriter('runs/mnist_ann')
# === 5. 训练循环(带早停)===
best_acc = 0.0
patience = 5
counter = 0
epochs = 50
for epoch in range(epochs):
# --- 训练阶段 ---
model.train()
train_loss = 0
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad() # 1. 梯度清零
output = model(data) # 2. 前向传播
loss = criterion(output, target) # 3. 计算损失
loss.backward() # 4. 反向传播
optimizer.step() # 5. 参数更新
train_loss += loss.item()
train_loss /= len(train_loader)
# --- 测试阶段 ---
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
output = model(data)
test_loss += criterion(output, target).item()
pred = output.argmax(dim=1, keepdim=True) # 取概率最大的类别
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader)
accuracy = 100. * correct / len(test_loader.dataset)
# 记录到 TensorBoard
writer.add_scalar('Loss/Train', train_loss, epoch)
writer.add_scalar('Loss/Test', test_loss, epoch)
writer.add_scalar('Accuracy/Test', accuracy, epoch)
print(f"Epoch {epoch+1}: 训练Loss={train_loss:.4f}, "
f"测试Loss={test_loss:.4f}, 准确率={accuracy:.2f}%")
# --- 早停 ---
if accuracy > best_acc:
best_acc = accuracy
torch.save(model.state_dict(), 'best_mnist_model.pth')
counter = 0
print(f" 模型已保存,当前最高准确率:{best_acc:.2f}%")
else:
counter += 1
if counter >= patience:
print(f" 早停!连续{patience}轮未提升")
break
print(f"\n训练完成!最高测试准确率:{best_acc:.2f}%")
writer.close()
代码解读
这段代码虽然长,但每一部分都对应着我们学过的知识点:
数据准备 :用 torchvision.datasets.MNIST 加载数据集,做了 ToTensor 和 Normalize 两个变换。Normalize 用的是 MNIST 数据集的全局均值和标准差,这是预处理的标准操作------把数据变成均值 0、方差 1 的分布,训练更稳定。
DataLoader 的 batch_size 设为 64,是一个比较经典的选择------太小了训练慢,太大了显存不够。shuffle=True 让每个 epoch 打乱数据顺序,防止模型记住数据的排列顺序。
模型结构 :三层全连接网络,中间夹着 ReLU 激活和 Dropout。输入层 784(28×28)→ 隐藏层1 256 → 隐藏层2 128 → 输出层 10(10个数字类别)。注意 x.view(-1, 28*28) 把二维图片展平成一维向量,这是全连接网络处理图像数据的标准操作。
损失函数与优化器 :多分类用 CrossEntropyLoss,优化器选 Adam(学习率 0.001),加了 weight_decay=1e-4 做 L2 正则化。这里同时用了 Dropout 和 L2 两种正则化手段,双管齐下。
训练循环 :标准的五步循环------清零梯度 → 前向传播 → 算损失 → 反向传播 → 更新参数。注意训练前 model.train(),测试前 model.eval(),这个切换不能忘。
测试评估 :用 torch.no_grad() 包裹,不计算梯度节省内存。output.argmax(dim=1) 取出概率最大的类别作为预测结果,和真实标签对比算出准确率。
早停机制:每轮测试后看准确率有没有创新高,有就保存模型;连续 5 轮没提升就停止训练,防止过拟合。
TensorBoard:把训练 loss、测试 loss、测试准确率都记录下来,训练过程中可以实时观察曲线变化。如果 loss 曲线下降平滑、准确率稳步上升,说明训练正常;如果测试 loss 先降后升,说明过拟合了,需要加大正则化或提前停止。
对于 MNIST 这样的简单数据集,一个三层的全连接网络加上适当的正则化,测试准确率通常能达到 97% 以上。虽然比不上 CNN 的 99%+,但作为 ANN 的入门实战,这个结果已经足够说明问题了。
十五、学习心得与建议
第一,反向传播是深度学习的"引擎"。 前向传播是计算,反向传播是学习。没有反向传播,神经网络只是一个固定的函数;有了反向传播,神经网络才有了学习的能力。理解了链式法则和计算图,你就理解了 PyTorch 自动微分的底层原理,遇到梯度相关的问题也能更好地排查。
第二,优化器没有绝对的好坏,只有适不适合。 初学者可能会问"哪个优化器最好",但这个问题没有标准答案。Adam 最省心,SGD 潜力最大,Momentum 是经典选择。重要的是理解每个优化器的核心思想和适用场景,根据具体任务做出选择。
第三,正则化是深度学习的"基本功"。 不会正则化,就等于不会调模型------因为深度学习模型天生容易过拟合。L2、Dropout、早停、数据增强,这四个是最常用的,必须掌握。实际项目中通常组合使用,效果叠加。其中早停是性价比最高的------零成本,效果好,几乎是标配。
第四,动手跑一个完整案例胜过看十篇文章。 MNIST 是深度学习的"Hello World",但麻雀虽小五脏俱全------数据加载、模型搭建、训练循环、正则化、评估,该有的都有了。把这个案例跑通、调通、理解透,你就具备了动手做深度学习项目的基础能力。
写在最后
两篇文章,把 ANN 的知识体系完整地串了起来。
上篇讲的是"网络是什么样的"------结构、神经元、激活函数、初始化、损失函数,核心是前向传播。
这篇讲的是"网络是怎么学习的 + 怎么用得好"------反向传播算梯度、梯度下降更新参数、优化器让更新更聪明、正则化防过拟合、实战案例串起全部知识,核心是反向传播 + 参数优化 + 正则化 + 实战。
前向传播和反向传播合在一起,就是神经网络训练的完整故事。从数据输入到预测输出,从误差计算到参数更新,从过拟合防治到完整实战,每一环都有它的位置,每一步都有它的道理。
ANN 是所有深度学习模型的基础。后面学 CNN、RNN、Transformer,变化的只是网络结构和连接方式------前向传播、反向传播、损失函数、优化器、正则化,这些核心机制都是通用的。把 ANN 学透了,后面学任何模型都只是"换个结构,同样的训练流程"。
深度学习的大门已经打开,里面的世界很精彩。一步一步来,你也能成为那个驾驭模型的人。