PyTorch从零搭建神经网络:MNIST手写数字识别完整教程

环境搭建 → 数据加载 → 模型定义 → 训练 → 测试,全程代码注解


目录

  1. PyTorch框架简介与环境搭建(CUDA)
  2. MNIST数据集与DataLoader批次打包
  3. 设备选择与模型定义(NeuralNetwork类)
  4. 激活函数:Sigmoid的梯度消失与ReLU的崛起
  5. 训练与测试函数详解
  6. 损失函数、优化器与完整运行流程

1. PyTorch框架简介与环境搭建(CUDA)

主流框架对比在这里插入图片描述

框架开发公司优点缺点
PyTorchFacebook (Meta)上手极容易,直接套用模板,动态图机制
TensorFlow 1.xGoogle代码比较冗余,上手有难度
TensorFlow 2.xGoogle收购了Keras,代码不兼容1.x版本与1.x版本不兼容
Keras在TensorFlow基础上做了封装,简化代码难度灵活性不如PyTorch

当前主流框架可以在招聘网站查询。对于学习和研究,PyTorch是首选——上手极容易,直接套用模板,社区活跃度高。

CPU与GPU的区别

理解GPU为什么适合深度学习,需要先了解CPU和GPU的架构差异:

CPU与GPU架构对比

对比项CPUGPU
ALU运算单元约25%约90%
Control控制单元约25%约5%
Cache缓存约50%约5%
设计目标低延迟,串行任务高吞吐,并行任务

为什么深度学习用GPU? 神经网络的本质是矩阵运算,GPU拥有大量ALU运算单元,非常适合并行处理矩阵乘法。

显卡重要参数:

  • 显存容量:决定临时存储数据的能力,如6GB、8GB、24GB
  • 显存频率:反应显存速度,如1600MHz、3800MHz
  • 显存位宽:一个时钟周期内传送数据的位数,如128、256、384

GPU版本安装步骤(CUDA)

CUDA是一种新的操作GPU计算的硬件和软件架构,是建立在NVIDIA的GPU上的一个通用并行计算平台和编程模型,它提供了GPU编程的简易接口,基于CUDA编程可以构建基于GPU计算的应用程序,利用GPUs的并行计算引擎来更加高效地解决比较复杂的计算难题。无需把这些计算映射到图形API。

安装流程:

①查看GPU  ──→  ②查看CUDA  ──→  ③安装CUDA  ──→  ④验证安装  ──→  ⑤pip install
(任务管理器)     (cmd: nvidia-smi)  (下载CUDA驱动)   (cmd: nvcc -V)    (pytorch)

第1步:查看GPU —— 在任务管理器中确认有NVIDIA显卡:

在这里插入图片描述

通过Windows任务管理器查看GPU型号

第2步: 输入 nvidia-smi 查看CUDA版本。
第3步: 下载CUDA驱动,版本必须≤ nvidia-smi 显示的版本。
第4步: 输入 nvcc -V 验证安装。
第5步: 进入PyTorch官网,选择对应CUDA版本用pip安装。


2. MNIST数据集与DataLoader批次打包

MNIST数据集介绍与下载

MNIST数据集: 包含70,000张手写数字图像,60,000张训练 + 10,000张测试。灰度,28×28像素,居中。

在这里插入图片描述

MNIST数据集样本展示

'''MNIST包含70,000张手写数字图像:60,000张用于训练,10,000张用于测试。
图像是灰度的,28x28像素的,并且居中的,以减少预处理和加快运行。'''

import torch
from torch import nn  # 导入神经网络模块
from torch.utils.data import DataLoader  # 数据包管理工具,打包数据
from torchvision import datasets  # 封装了很多与图像相关的模型、数据集
from torchvision.transforms import ToTensor  # 数据转换工具,将图片转换为tensor张量

"""下载训练数据集(包含训练图片+标签)"""
training_data = datasets.MNIST(
    root='data',        # 👈 数据存储路径,下载的手写数字保存到'data'文件夹
    train=True,          # 👈 True=读取训练集(60000张),False=读取测试集(10000张)
    download=True,       # 👈 如果之前已下载过,就不会重复下载
    transform=ToTensor())  # 👈 将PIL图片转为tensor张量(0~1范围的浮点数)
                           # PyTorch只能识别tensor张量,图片不能直接传入模型

test_data = datasets.MNIST(
    root='data',
    train=False,           # 👈 False=读取测试集
    download=True,
    transform=ToTensor()
)
print(len(training_data), len(test_data)) # 输出: 60000 10000

可视化前9张训练图片:

from matplotlib import pyplot as plt

figure = plt.figure()
for i in range(9):
    img, label = training_data[i]  # 👈 提取第i张图片和标签
    figure.add_subplot(3, 3, i+1)  # 创建3×3的子图网格
    plt.title(label)              # 标题显示标签(数字0-9)
    plt.axis('off')            # 关闭坐标轴
    plt.imshow(img.squeeze(), cmap='gray') # 灰度显示
plt.show()

Tensor与NumPy的区别: NumPy数组只能在CPU上运行;Tensor张量可以在GPU上运行,在深度学习中可显著提高计算速度。

DataLoader:批次打包数据

为什么要分批次? 如果一次性传入6万张图片,内存/显存可能放不下。分成每64张一个包,逐个传入训练。优点:减少内存使用,提高训练速度。

'''
创建DataLoader(数据加载器)
batch_size: 将数据集分成多份,每一份为batch_size个数据。
优点: 可以减少内存的使用,提高训练速度。
'''
train_dataloader = DataLoader(training_data, batch_size=64) # 👈 64张图片为一个包
test_dataloader = DataLoader(test_data, batch_size=64)

# 查看数据维度
for X, y in train_dataloader:
    print(f"shape of X [N,C,H,W] is {X.shape}") # 👈 [64, 1, 28, 28]
    print(f"shape of y: {y.shape} {y.dtype}")   # 👈 [64] int64
    break
训练数据集(60000张)    batch_size=64     神经网络
┌──────────────┐                      ┌──────────┐
│              │   ┌──┐┌──┐┌──┐ ┌──┐  │          │
│  28×28像素    │──→│包1││包2││包3│...│包N│──→│  训练    │
│              │   │64张││64张││64张│ │64张│  │          │
└──────────────┘   └──┘└──┘└──┘ └──┘  └──────────┘
                    [64,1,28,28] (每个包的维度)

数据维度解释:

维度含义本例数值
N(Batch)批次中图片数量64
C(Channel)通道数(灰度图=1,RGB=3)1
H(Height)图片高度28
W(Width)图片宽度28

3. 设备选择与模型定义(NeuralNetwork类)

设备判断:GPU/MPS/CPU三级选择

"""判断当前设备是否支持GPU,其中mps是苹果M系列芯片的GPU。"""
# 返回cuda, mps, 或 cpu
device = "cuda" if torch.cuda.is_available() else \
         "mps" if torch.backends.mps.is_available() else \
         "cpu"
print(f"Using: {device} device")

设备优先级: cuda(NVIDIA显卡)> mps(苹果M系列)> cpu(纯CPU)

神经网络的模型也需要传入到GPU,1个batch_size的数据集也需要传入到GPU,才可以进行训练。

                    有NVIDIA显卡?
                   /            \
                 Yes             No
                 ↓               ↓
              ┌──────┐     有苹果M系列芯片?
              │ cuda │     /            \
              └──────┘   Yes             No
                          ↓               ↓
                       ┌──────┐      ┌──────┐
                       │ mps  │      │ cpu  │
                       └──────┘      └──────┘

自定义神经网络模型

本教程实现一个3层全连接网络:784 → 128 → 256 → 10。

  输入层            隐藏层1           隐藏层2           输出层
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────┐
│          │    │          │    │          │    │      │
│  784     │──→│  128     │──→│  256     │──→│  10  │
│ (28×28)  │nn. │          │ReLU│          │ReLU│      │
│ 展平     │Linear       │          │Linear       │分类  │
│ nn.Flatten│              │          │              │      │
└──────────┘    └──────────┘    └──────────┘    └──────┘
输入输出操作说明
Flatten1×28×28784展平将2D图片展平为1D向量
Linear 1784128全连接第1个参数=输入神经元个数,第2个=输出
ReLU 1128128激活引入非线性
Linear 2128256全连接提升特征维度
ReLU 2256256激活引入非线性
Linear 325610全连接输出必须和标签类别数相同(0~9共10类)
class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()                          # 👈 继承父类nn.Module的初始化
        self.flatten = nn.Flatten()              # 👈 创建展平对象,将28×28图片展平为784维向量
        self.hidden1 = nn.Linear(28*28, 128)     # 👈 第1个全连接层:输入784个神经元,输出128个
                                                # 28*28=784 是输入图片的像素总数
                                                # 128 是输出神经元个数(可自由调整)
        self.relu1 = nn.ReLU()                   # 👈 1. 在初始化时定义激活函数
        self.hidden2 = nn.Linear(128, 256)       # 👈 第2个全连接层:输入128(必须等于上一层输出),输出256
        self.relu2 = nn.ReLU()                   # 👈 2. 在初始化时定义激活函数
        self.out = nn.Linear(256, 10)            # 👈 输出层:输入256,输出10(必须和标签类别数相同)
                                                # MNIST有0-9共10个数字,所以输出10

    def forward(self, x):         # 👈 前向传播:定义数据的流向,函数名不能改
        x = self.flatten(x)                     # 步骤1: 图像展平 [64,1,28,28]→[64,784]
        x = self.hidden1(x)                     # 步骤2: 第一层全连接 [64,784]→[64,128]
        x = self.relu1(x)                       # 步骤3: ReLU激活,不改变维度 [64,128]
        x = self.hidden2(x)                     # 步骤4: 第二层全连接 [64,128]→[64,256]
        x = self.relu2(x)                       # 步骤5: ReLU激活 [64,256]
        x = self.out(x)                         # 步骤6: 输出层 [64,256]→[64,10]
        return x

model = NeuralNetwork().to(device)              # 👈 创建模型实例并传入GPU/CPU
print(model)                                # 打印模型结构

💡 代码关键点:

  • nn.Linear(输入数, 输出数):第一个参数是上一层神经元个数,第二个参数是这层要输出的神经元个数
  • forward() 函数名不能改——调用 model(x) 时,PyTorch会自动调用forward方法
  • .to(device):把模型传入GPU,数据也需要传入GPU才可以训练
  • 输入层和输出层的节点数是固定的(784和10),中间层的节点数可以自由调整

4. 激活函数:Sigmoid的梯度消失与ReLU的崛起

本教程代码中使用了ReLU激活函数而非Sigmoid,这背后有深刻的原因。

梯度消失与梯度爆炸

在这里插入图片描述

深度神经网络结构——梯度反向传播中的连乘效应

⚠️ 梯度消失: 连乘因子大部分小于1,最后乘积趋于0,后面的网络层参数不变化。

梯度爆炸: 连乘因子大部分大于1,乘积趋于无穷。

在反向传播中,梯度通过链式法则逐层相乘传递。如果每层梯度都小于1(Sigmoid导数最大只有0.25),经过多层连乘后,靠近输入层的梯度趋近于0,权重几乎不更新。

Sigmoid的致命缺陷 vs ReLU的解决方案

  loss
   │
   │  σ(x) ─────────────── (蓝色实线)
   │   ╱─────────────╲
   │  ╱               ╲
   │ ╱                 ╲
   │╱                   ╲
   │──────────────────────╲────── x
   │  ↑↑↑               ↑↑↑
   │  饱和区            饱和区
   │  梯度≈0           梯度≈0
   │
   │  σ'(x) - - - - - - (红色虚线)
   │     ╱╲
   │    ╱  ╲
   │  ╱      ╲
   │╱          ╲ - - - - -
   │
   │  ← 导数最大值仅为0.25!

在这里插入图片描述

ReLU激活函数曲线

ReLU ( x ) = max ⁡ ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x)

ReLU ′ ( x ) = { 1 x > 0 0 x < 0 \text{ReLU}'(x) = \begin{cases} 1 & x > 0 \\ 0 & x < 0 \end{cases} ReLU(x)={10x>0x<0

ReLU为什么能解决梯度消失? 导数是1就没有梯度消失问题了!ReLU在正数区间导数恒为1,梯度可以毫无衰减地反向传播。

对比项SigmoidReLU
导数最大值0.251(正数区间)
梯度消失严重(多层连乘→0)无(正数区间导数=1)
计算复杂度高(指数运算)低(max比较)
适用场景二分类输出层隐藏层(默认选择)

5. 训练与测试函数详解

训练函数:5步训练循环

  ①前向传播    ②计算损失    ③梯度清零    ④反向传播    ⑤更新w
  model(X)  → loss_fn  → zero_grad → backward → step()
  得到预测值    预测vs真实    清空旧梯度    计算新梯度   梯度下降
      ↑                                                    │
      └─────────── 循环:每个batch重复一次 ────────────────┘
def train(dataloader, model, loss_fn, optimizer):
    model.train()  # 👈 告诉模型:我要开始训练了!权重w会被修改
    # PyTorch提供2种模式切换:model.train()(训练) 和 model.eval()(测试)

    batch_size_num = 1  # 统计训练的batch数量
    for X, y in dataloader:                    # 遍历每个批次
        X, y = X.to(device), y.to(device)        # 👈 把训练数据和标签传入GPU

        pred = model.forward(X)                  # 👈 步骤1: 前向传播,得到预测值
        loss = loss_fn(pred, y)                  # 👈 步骤2: 通过交叉熵损失函数计算损失值

        # === Backpropagation 反向传播 ===
        optimizer.zero_grad()                    # 👈 步骤3: 梯度值清零(非常重要!)
        loss.backward()                          # 👈 步骤4: 反向传播,计算每个参数的梯度值w
        optimizer.step()                         # 👈 步骤5: 根据梯度更新网络参数w

        loss_value = loss.item()
        if batch_size_num % 100 == 0:
            print(f'Batch loss: {loss_value:.6f} [number:{batch_size_num}]')
        batch_size_num += 1

⚠️ 为什么要 optimizer.zero_grad()? PyTorch的梯度默认是累加的。如果不清零,新梯度会和上一轮梯度叠加,导致梯度越来越大,训练发散。每次反向传播前必须先清零!

测试函数:评估模型性能

测试函数与训练函数有3个关键区别:

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)   # 👈 测试集总图片数(10000)
    num_batches = len(dataloader)      # 批次数量
    model.eval()                  # 👈 关键区别1: 切换到评估模式,关闭Dropout等
    test_loss, correct = 0, 0

    with torch.no_grad():          # 👈 关键区别2: 不计算梯度,节省内存和计算
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)                         # 前向传播得到预测
            test_loss += loss_fn(pred, y).item()            # 累加每个批次的损失值
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            # 👈 pred.argmax(1): 取每行最大值的索引,即预测的数字
            # == y: 与真实标签比较,得到布尔值数组
            # .type(torch.float): 转为浮点数(True→1.0, False→0.0)
            # .sum().item(): 求和并取出标量值

    test_loss /= num_batches    # 平均损失
    correct /= size            # 平均准确率

    print(f"Test set: \n Accuracy: {(100*correct):.2f}%, Avg loss: {test_loss:.6f}")
对比项训练函数 train()测试函数 test()
模式model.train()model.eval()
梯度计算需要(loss.backward + optimizer.step)不需要(torch.no_grad)
Dropout开启(随机丢弃神经元)关闭(使用全部神经元)
目的更新权重w评估当前模型性能

6. 损失函数、优化器与完整运行流程

损失函数大全:18种Loss函数速查

loss_fn = nn.CrossEntropyLoss()
# 创建交叉熵损失函数对象,因为手写数字识别中一共有10个数字(0~9),输出有10个结果
损失函数中文名适用场景
L1LossL1损失 / MAE回归:预测值与真实值的绝对差平均值
NLLLoss负对数似然损失多分类(需配合LogSoftmax)
MSELoss均方误差回归:差值平方的平均值
BCELoss二元交叉熵二分类问题
BCEWithLogitsLoss带Logits的二元交叉熵二分类(结合Sigmoid,更稳定)
SmoothL1Loss平滑L1损失结合L1和L2,避免梯度爆炸
HuberLossHuber损失可调参数控制L1/L2平衡
SoftMarginLoss软边际损失二分类(Hinge的软化版)
CrossEntropyLoss交叉熵损失多分类(结合LogSoftmax+NLLLoss)
CosineEmbeddingLoss余弦嵌入损失余弦相似度度量
MarginRankingLoss边际排序损失排序学习
TripletMarginLoss三元组边际损失人脸识别、图像检索
CTCLoss连接时序分类语音识别等序列学习

💡 手写数字识别是多分类问题(10类),使用CrossEntropyLoss最合适。它内部已经集成了LogSoftmax + NLLLoss,不需要额外添加softmax层。

优化器对比:SGD vs Adam及更多

# optimizer = torch.optim.SGD(model.parameters(), lr=0.1)  # SGD优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 👈 使用Adam优化器,学习率0.001

三种基础梯度下降法:

方法原理优点缺点
批量梯度下降 (BGD)使用全样本数据计算梯度收敛次数少每次迭代需要所有数据,内存大耗时长
随机梯度下降 (SGD)从batch中随机抽一组,训练后更新速度快可能陷入局部最优,搜索较盲目
小批量梯度下降将数据分成小批量计算和更新BGD和SGD的结合,兼顾效率和稳定性需选择合适的batch_size

高级优化器:

优化器特点
Adam (自适应矩估计)结合动量和自适应学习率,当前最常用
Momentum (动量梯度下降)引入动量项,加速收敛
AdaGrad自适应学习率,对频繁更新参数减小学习率
RMSpropAdaGrad改进版,解决学习率过度衰减
AdamWAdam改进版,更好的权重衰减
Adadelta无需设置初始学习率
  loss
   │  ●(起点)
   │   ╲╲                    ●──→ Adam(快且稳定)
   │    ╲╲                 ╱
   │     ╲╲              ╱
   │      ╲╲           ╱
   │       ╲╲        ╱
   │   ●●●●●●●●●●●(最小值)
   │      SGD路径(慢且震荡)
   │
   └──────────────────────────→ w

参数说明: model.parameters() 是要训练的参数;lr=0.001 是学习率,Adam推荐0.001,SGD推荐0.1。学习率越大权重更新越快。训练初期希望学习率大(快速收敛),后期小(精细收敛)。

完整代码与运行流程

# === 损失函数和优化器 ===
loss_fn = nn.CrossEntropyLoss()  # 创建交叉熵损失函数对象

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # Adam优化器,lr=0.001

# === 训练主循环 ===
epochs = 10  # 训练轮数:完整遍历训练集10次
for epoch in range(epochs):
    print(f"Epoch {epoch+1}\n--------------------")
    train(train_dataloader, model, loss_fn, optimizer)  # 训练一轮完整数据
print("DONE!!")

# === 测试评估 ===
test(test_dataloader, model, loss_fn)
① datasets.MNIST 下载/加载数据        60000训练 + 10000测试,28×28灰度图
         │
         ↓
② DataLoader 批次打包                  batch_size=64,每包64张图片
         │
         ↓
③ NeuralNetwork 定义模型               784→128→ReLU→256→ReLU→10
         │
         ↓
④ device 设备判断 + 模型传入GPU        cuda > mps > cpu
         │
         ↓
⑤ 损失函数 + 优化器                    CrossEntropyLoss + Adam(lr=0.001)
         │
         ↓
⑥ 训练 10 epochs ──────────────→ ⑦ 测试评估
   forward→loss→zero_grad          model.eval() + no_grad()
   →backward→step

预期运行结果:

Using:cuda device
NeuralNetwork(
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (hidden1): Linear(in_features=784, out_features=128, bias=True)
  (relu1): ReLU()
  (hidden2): Linear(in_features=128, out_features=256, bias=True)
  (relu2): ReLU()
  (out): Linear(in_features=256, out_features=10, bias=True)
)
Epoch 1
--------------------
Batch loss: 2.301562 [number:100]
Batch loss: 0.452318 [number:200]
...
Epoch 10
--------------------
Batch loss: 0.052318 [number:800]
DONE!!
Test set:
 Accuracy: 97.52%, Avg loss: 0.082156

💡 结果分析: 使用简单的全连接网络(无卷积层),经过10轮训练,在MNIST测试集上可达约97%~98%的准确率。如果需要更高准确率(99%+),需要使用卷积神经网络(CNN),这将在下一篇教程中讲解。


总结

环节核心代码关键要点
数据加载datasets.MNIST + ToTensor()图片必须转为tensor才能传入模型
批次打包DataLoader(batch_size=64)分批训练节省内存,加速收敛
设备选择cuda > mps > cpu模型和数据都要传入同一设备
模型定义class NeuralNetwork(nn.Module)__init__定义层,forward定义数据流
激活函数nn.ReLU()导数为1,解决Sigmoid梯度消失
训练5步forward→loss→zero_grad→backward→step梯度清零是关键!
测试评估model.eval() + torch.no_grad()关闭Dropout,不计算梯度
损失函数nn.CrossEntropyLoss()多分类首选,内部含LogSoftmax
优化器torch.optim.Adam(lr=0.001)自适应学习率,当前最常用

有了这个基础框架,你只需要替换模型结构、数据集和损失函数,就能应用到各种不同的深度学习任务中。


更多推荐