ANN 全连接神经网络进阶:从反向传播到完整实战

上一篇文章我们系统学习了 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 结合 自适应 最快 很好 ★★★★★

实用建议:

  1. 不知道用什么,先试 Adam。学习率设 0.001,大多数情况下都能跑起来,效果还不错。

  2. 追求最终精度,试试 SGD + Momentum。虽然收敛慢,但如果调参得当,最终泛化效果可能比 Adam 更好。

  3. 稀疏数据 / 词嵌入:Adagrad 或 RMSprop 效果通常不错。

  4. RNN 系列模型:RMSprop 和 Adam 都是常用选择。

  5. 最终建议:先用 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

道理很简单------模型刚开始训练时,训练误差和验证误差都在下降;但当过拟合开始发生时,训练误差还在降,验证误差反而开始上升。早停就是在验证误差开始上升的那个点"踩刹车",把那个时候的模型保存下来。

早停的实施步骤:

  1. 把数据分成训练集和验证集

  2. 每训练一轮(epoch),在验证集上评估一次

  3. 如果验证集性能比之前最好的还好,就保存当前模型

  4. 如果连续很多轮(叫 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 学透了,后面学任何模型都只是"换个结构,同样的训练流程"。

深度学习的大门已经打开,里面的世界很精彩。一步一步来,你也能成为那个驾驭模型的人。

相关推荐
Dawson Zhu1 小时前
《Agentic Design Patterns》第 4 章导读:反思(Reflection)
人工智能·语言模型·架构·aigc·agi
llqbzllll2 小时前
Spring AI 工具调用不是反射一下就结束:用 2.0.1 跑通失败恢复与调用上限
人工智能·后端
北冥有鱼被烹2 小时前
砷化镓与磷化铟:光模块的地基——从化学键到AI缺货潮的底层逻辑
人工智能·python
xiaohaiAIgeo2 小时前
【2026年】通风柜合规检查清单:安全评估打分项解析
人工智能·安全·科普知识
回眸&啤酒鸭2 小时前
【回眸】Space-Bunny-Alpha 智能场景应用实战指南
人工智能
CVer儿2 小时前
基于文本提示 视觉提示的主流模型
人工智能·自然语言处理·知识图谱
看浪的路人2 小时前
第6讲:敏感数据检测与脱敏
人工智能·深度学习
倔强的石头1062 小时前
ChatGLM系列解析_清华智谱大模型技术路线
人工智能·大模型
龙亘川2 小时前
数智驱动民政升级 精准守护民生保障
大数据·数据库·人工智能