深度学习——神经网络(PyTorch 实现 MNIST 手写数字识别案例)

原理:

深度学习------详细教学:神经元、神经网络、感知机、激活函数、损失函数、优化算法(梯度下降)-CSDN博客https://blog.csdn.net/2302_78022640/article/details/150618265?spm=1011.2415.3001.5331


案例教学:PyTorch 实现 MNIST 手写数字识别

本文将通过一个完整的案例,演示如何使用 PyTorch 搭建一个神经网络模型,对经典的 MNIST 手写数字数据集 进行训练与测试。我们会详细拆解代码中涉及到的每个步骤与概念,帮助你理解 PyTorch 的核心流程。

数据 → 加载器/形状 → 设备 → 网络结构(含参数计数)→ 前向/损失/优化器 → 训练流程细节 → 测试流程细节 → 常见坑与改进建议。


完整代码:

复制代码
# import torch
# print(torch.__version__)

'''
 MNIST包含70000张手写数字图像:60000用于训练,10000用于测试
 图像是灰度的,28×28像素的,并且居中的,以减少预处理和加快运行
'''
import torch
from torch import nn    #导入神经网络模块
from torch.utils.data import DataLoader  #数据包管理工具,打包数据
from torchvision import  datasets  #封装了很多与图像相关的模型,数据集
from torchvision.transforms import ToTensor  #数据转换,张量,将其他类型的数据转换为tensor张量,numpy array

'''下载训练数据集(包含训练图片+标签)'''
training_data = datasets.MNIST( #跳转到函数的内部源代码,pycharm按下ctrl + 鼠标点击
    root="data", #表示下载的手写数字  到哪个路径。60000
    train=True, #读取下载后的数据中的训练集
    download=True, #如果你之前已经下载过了,就不用下载
    transform=ToTensor(), #张量,图片是不能直接传入神经网络模型
 )   #对于pytorch库能够识别的数据一般是tensor张量
'''下载测试数据集(包含训练图片+标签)'''
test_data = datasets.MNIST( #跳转到函数的内部源代码,pycharm按下ctrl + 鼠标点击
    root="data", #表示下载的手写数字  到哪个路径。60000
    train=False, #读取下载后的数据中的训练集
    download=True, #如果你之前已经下载过了,就不用下载
    transform=ToTensor(), #Tensor是在深度学习中提出并广泛应用的数据类型
 )   #Numpy数组只能在CPU上运行。Tensor可以在GPU上运行。这在深度学习应用中可以显著提高计算速度。
print(len(training_data))


# '''展示手写数字图片,把训练集中的59000张图片展示'''
# from matplotlib import pyplot as plt
# figure = plt.figure()
# for i in range(9):
#     img,label = training_data[i+59000] #提取第59000张图片
# 
#     figure.add_subplot(3,3,i+1) #图像窗口中创建多个小窗口,小窗口用于显示图片
#     plt.title(label)
#     plt.axis("off")  #plt.show(I) 显示矢量
#     plt.imshow(img.squeeze(),cmap="gray") #plt.imshow()将Numpy数组data中的数据显示为图像,并在图形窗口中显示
#     a = img.squeeze()  #img.squeeze()从张量img中去掉维度为1的,如果该维度的大小不为1,则张量不会改变
# plt.show()


'''创建数据DataLoader(数据加载器)'''
# batch_size:将数据集分为多份,每一份为batch_size个数据
#       优点:可以减少内存的使用,提高训练速度
train_dataloader = DataLoader(training_data,batch_size=64)
test_dataloader = DataLoader(test_data,batch_size=64)
for X,y in test_dataloader:#X是表示打包好的每一个数据包
    print(f"Shape of X[N,C,H,W]:{X.shape}")#
    print(f"Shape of y: f{y.shape} {y.dtype}")
    break


'''判断当前设备是否支持GPU,其中mps是苹果m系列芯片的GPU'''
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")   #字符串的格式化,CUDA驱动软件的功能:pytorch能够去执行cuda的命令
# 神经网络的模型也需要传入到GPU,1个batch_size的数据集也需要传入到GPU,才可以进行训练


''' 定义神经网络  类的继承这种方式'''
class NeuralNetwork(nn.Module): #通过调用类的形式来使用神经网络,神经网络的模型,nn.mdoule
    def __init__(self): #python基础关于类,self类自己本身
        super().__init__() #继承的父类初始化
        self.flatten = nn.Flatten() #展开,创建一个展开对象flatten
        self.hidden1 = nn.Linear(28*28,128) #第1个参数:有多少个神经元传入进来,第2个参数:有多少个数据传出去
        self.hidden2 = nn.Linear(128,256) #第1个参数:有多少个神经元传入进来,第2个参数:有多少个数据传出去
        self.out = nn.Linear(256,10) #输出必须和标签的类别相同,输入必须是上一层的神经元个数
    def forward(self,x):   #前向传播,你得告诉它 数据的流向 是神经网络层连接起来,函数名称不能改
        x = self.flatten(x)  #图像进行展开
        x = self.hidden1(x)
        x = torch.relu(x)   #激活函数,torch使用的relu函数
        x = self.hidden2(x)
        x = torch.relu(x)
        x = self.out(x)
        return x
model = NeuralNetwork().to(device) #把刚刚创建的模型传入到GPU
print(model)


def train(dataloader,model,loss_fn,optimizer):
    model.train() #告诉模型,我要开始训练,模型中w进行随机化操作,已经更新w,在训练过程中,w会被修改的
# pytorch提供2种方式来切换训练和测试的模式,分别是:model.train() 和 mdoel.eval()
# 一般用法是:在训练开始之前写上model.train(),在测试时写上model.eval()
    batch_size_num = 1
    for X,y in dataloader:              #其中batch为每一个数据的编号
        X,y = X.to(device),y.to(device) #把训练数据集和标签传入cpu或GPU
        pred = model.forward(X)         # .forward可以被省略,父类种已经对此功能进行了设置
        loss = loss_fn(pred,y)          # 通过交叉熵损失函数计算损失值loss
        # Backpropagation 进来一个batch的数据,计算一次梯度,更新一次网络
        optimizer.zero_grad()           # 梯度值清零
        loss.backward()                 # 反向传播计算得到每个参数的梯度值w
        optimizer.step()                # 根据梯度更新网络w参数

        loss_value = loss.item()        # 从tensor数据种提取数据出来,tensor获取损失值
        if batch_size_num %100 ==0:
            print(f"loss: {loss_value:>7f} [number:{batch_size_num}]")
        batch_size_num += 1


def Test(dataloader,model,loss_fn):
    size = len(dataloader.dataset)  #10000
    num_batches = len(dataloader)  # 打包的数量
    model.eval()        #测试,w就不能再更新
    test_loss,correct =0,0
    with torch.no_grad():       #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候
        for X,y in dataloader:
            X,y = X.to(device),y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred,y).item() #test_loss是会自动累加每一个批次的损失值
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            a = (pred.argmax(1) == y) #dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
            b = (pred.argmax(1) == y).type(torch.float)
    test_loss /= num_batches #能来衡量模型测试的好坏
    correct /= size  #平均的正确率
    print(f"Test result: \n Accuracy:{(100*correct)}%, Avg loss:{test_loss}")


loss_fn = nn.CrossEntropyLoss()  #创建交叉熵损失函数对象,因为手写字识别一共有十种数字,输出会有10个结果

optimizer = torch.optim.Adam(model.parameters(),lr=0.005) #0.01创建一个优化器,SGD为随机梯度下降算法
# # params:要训练的参数,一般我们传入的都是model.parameters()
# # lr:learning_rate学习率,也就是步长

# # loss表示模型训练后的输出结果与样本标签的差距。如果差距越小,就表示模型训练越好,越逼近真实的模型
# 只跑一轮(可尝试)
# train(train_dataloader,model,loss_fn,optimizer) #训练1次完整的数据。多轮训练
# Test(test_dataloader,model,loss_fn)


epochs = 10
for t in range(epochs):
    print(f"epoch {t+1}\n---------------")
    train(train_dataloader,model,loss_fn,optimizer)
print("Done!")
Test(test_dataloader,model,loss_fn)

一、数据与 transform

复制代码
training_data = datasets.MNIST(..., transform=ToTensor())
test_data = datasets.MNIST(..., transform=ToTensor())
  • MNIST:总共 70,000 张 28×28 灰度图(60000 训练 + 10000 测试)。

  • ToTensor() 的作用:

    • 把 PIL Image / numpy array → torch.Tensor

    • 把像素值从 [0,255] 变为浮点张量并归一化到 [0.0,1.0](内部做了 image/255.)。

    • 还会把通道维度放到最前面(灰度图从 (H,W)(C,H,W),对于单通道 C=1)。

  • 为什么需要 Tensor:PyTorch 的模型、Loss、优化器等都以 Tensor 为输入,且 Tensor 可以 .to(device)(移动到 GPU)。

二、DataLoader 与张量形状

复制代码
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader  = DataLoader(test_data, batch_size=64)
for X,y in test_dataloader:
    print(f"Shape of X[N,C,H,W]:{X.shape}")
    print(f"Shape of y: f{y.shape} {y.dtype}")
    break
  • batch_size=64 意味着每个批次 X 的形状通常是 [64, 1, 28, 28]

    • N(batch size)=64,C=1(灰度),H=28,W=28。
  • y 的形状通常是 [64](每个样本一个整数标签),数据类型 torch.int64(也就是 long),这是 nn.CrossEntropyLoss 所期望的标签类型。

  • 注意小细节 :你写的 print f-string 中有个字母 f 被留在字符串里 f"Shape of y: f{y.shape} {y.dtype}",输出会包含那个字母 f(不会影响功能,但显示上会多一个字符)。

  • 批次数量

    • 训练集 60000 / 64 = 937.5 → len(train_dataloader) 等于 938 批(937 个满批,最后一批为 32 个样本)。

    • 测试集 10000 / 64 = 156.25 → len(test_dataloader) 等于 157 批(最后一批为 16 个样本)。

    • 默认 DataLoaderdrop_last=False,所以会保留最后一个不满的批次。

三、设备选择(CPU / CUDA / MPS)

复制代码
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
  • 逻辑按优先级:cudamps(苹果 M 系芯片)→ cpu

  • torch.cuda.is_available() 检查 CUDA 驱动与 GPU 是否可用。

  • torch.backends.mps.is_available() 检查 macOS Metal Performance Shaders 是否可用。

  • 要点

    • 模型(model.to(device)) 与每个 batch 的 X,y = X.to(device), y.to(device) 都必须迁移到同一 device,否则会报错(device mismatch)。

    • MPS 在特性和稳定性上与 CUDA 有差别(某些操作可能尚不支持或有性能差异),在 macOS 上测试要注意。

四、模型结构详解(逐层、参数计数)

复制代码
class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.hidden1 = nn.Linear(28*28,128)
        self.hidden2 = nn.Linear(128,256)
        self.out = nn.Linear(256,10)
    def forward(self,x):
        x = self.flatten(x)
        x = self.hidden1(x)
        x = torch.relu(x)
        x = self.hidden2(x)
        x = torch.relu(x)
        x = self.out(x)
        return x
model = NeuralNetwork().to(device)
  • Flatten():把输入 (N,1,28,28)(N, 784)(784 = 28×28)。

  • hidden1 = Linear(784,128)

    • 权重形状 (128, 784),权重数量 = 784 * 128 = 100,352

    • bias 数量 = 128

    • hidden1 总参数 = 100,352 + 128 = 100,480

  • hidden2 = Linear(128,256)

    • 权重 128 * 256 = 32,768,bias = 256

    • hidden2 总参数 = 32,768 + 256 = 33,024

  • out = Linear(256,10)

    • 权重 256 * 10 = 2,560,bias = 10

    • out 总参数 = 2,560 + 10 = 2,570

  • 模型总参数 = 100,480 + 33,024 + 2,570 = 136,074 参数(approx)。

  • 激活函数 torch.relu:逐元素做 max(0,x),能够引入非线性。

  • 注意 :模型返回的是 原始 logits(没有 softmax) ------ 这是正确的,因为 nn.CrossEntropyLoss 内部会把 logits 送入 log_softmax + NLLLoss,你不应该在网络末尾手动加 softmax(两次会错)。

五、损失函数与优化器(为什么这么用)

复制代码
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
  • CrossEntropyLoss

    • 输入:pred(形状 [N, 10] 的 raw logits),标签 y(形状 [N],整数类索引)。

    • 内部实现包括 softmax 与负对数似然(log_softmax + NLLLoss)。

    • 默认 reduction='mean',返回单个标量(该批次的平均损失)。

  • Adam 优化器:

    • 自适应一阶优化器,通常比简单 SGD 收敛更快,适合大多数场景。

    • lr=0.005 是学习率(步长);你可以根据训练曲线调整(过大会发散,过小收敛慢)。

  • model.parameters():把模型里可训练的参数传给优化器。

六、训练函数 train(...) 的逐步解析

复制代码
def train(dataloader, model, loss_fn, optimizer):
    model.train()
    batch_size_num = 1
    for X,y in dataloader:
        X,y = X.to(device), y.to(device)
        pred = model.forward(X)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        loss_value = loss.item()
        if batch_size_num % 100 == 0:
            print(f"loss: {loss_value:>7f} [number:{batch_size_num}]")
        batch_size_num += 1
  • model.train():把模型切换到训练模式(启用 dropout、batchnorm 的训练行为)。

  • 每个批次流程:

    1. X,y = X.to(device), y.to(device):迁移张量到 GPU(或 MPS / CPU)。

    2. pred = model.forward(X):前向得到 logits。建议 通常写 pred = model(X)(等价,但会触发钩子/注册的行为更标准),但 model.forward(X) 也能工作。

    3. loss = loss_fn(pred, y):计算当前批次平均损失。

    4. optimizer.zero_grad():把之前累积的梯度清零(PyTorch 默认梯度是累加的)。

    5. loss.backward():反向传播,计算每个参数的梯度。

    6. optimizer.step():根据梯度更新参数。

    7. loss.item():把标量 Tensor 转为 Python float,便于打印/记录。

  • batch_size_num 用来计数并每 100 批打印一次损失(注意从 1 开始)。

  • 为什么要 zero_grad? 如果不清零,梯度会在多个 .backward() 调用中累加,从而导致错误的更新(除非你刻意想累加梯度用于大 batch 模拟)。

七、测试/验证函数 Test(...) 逐步解析

复制代码
def Test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        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()
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy:{(100*correct)}%, Avg loss:{test_loss}")
  • size = len(dataloader.dataset) → 测试样本总数(例:10000)。

  • num_batches = len(dataloader) → 批次数(例:157)。

  • model.eval():把模型切换到评估模式(关闭 dropout、batchnorm 的训练状态)。

  • with torch.no_grad():关闭梯度计算,节约显存和加速推理,因为测试不需要梯度。

  • pred.argmax(1):在类别维度(dim=1)取最大 logit 对应的类索引 → 预测类别。

  • (pred.argmax(1) == y) → 布尔张量(True/False),.type(torch.float) 转为 1.0/0.0,.sum().item() 得到该批次正确预测数(Python number)。

  • 最后:

    • test_loss /= num_batches:得到 每批 平均损失的平均(注意:这是对每个批次均等加权的平均;若要精确按样本加权平均,需要用 loss * batch_size 累加再除以 size)。

    • correct /= size:得到正确率(小数),打印时乘以 100 得到百分比。

  • 小提醒 :代码中也顺手把 ab(中间变量)留了出来,可能是调试/示例用。

八、训练循环与输出

复制代码
epochs = 10
for t in range(epochs):
    print(f"epoch {t+1}\n---------------")
    train(train_dataloader, model, loss_fn, optimizer)
print("Done!")
Test(test_dataloader, model, loss_fn)
  • 外层循环按 epochs 控制完整遍历训练集的次数(每次都会走 len(train_dataloader) 个 batch)。

  • 每个 epoch 会打印若干 loss(每 100 批一次),训练结束后打印 Done!,并调用 Test 做最终评估。

九、常见坑、注意事项与改进建议(不改动你的代码,只是建议

  1. 是否要 shuffle 训练集?

    • 目前 DataLoader(training_data, batch_size=64) 未设置 shuffle=True。训练时通常需要 shuffle=True,避免每个 epoch 数据顺序相同导致模型收敛不佳。代码不改动的前提下我只提醒你注意这一点。
  2. model(X) vs model.forward(X) :一般用 model(X),因为它会处理钩子、预处理等;model.forward(X) 直接调用前向实现,但大多数场景两者等效。

  3. 损失平均方式Test 中的 test_loss 是对"每个批次平均损失"的平均;如果想按样本精确平均,应把 loss_fn(pred,y).item() * X.size(0) 累加,然后最后除以 size

  4. 随机性与可复现 :要想可复现,设置随机种子(torch.manual_seed(...)),并考虑 CUDA 的确定性配置。

  5. 保存模型 :训练好后可 torch.save(model.state_dict(), "mnist.pth") 以便下次加载(model.load_state_dict(...))。

  6. 学习率/优化器lr=0.005 是可行的初值,但可能需要调整;也可加入学习率调度器 torch.optim.lr_scheduler

  7. 批大小、内存 :在 GPU 上如果内存不足,可减小 batch_size;在 CPU 上训练会慢很多。

  8. MPS 注意 :如果使用苹果 M 系统,mps 支持不完全等同 cuda,出现奇怪错误时可尝试切回 cpu

  9. 性能监控 :建议在训练过程中记录 lossaccuracy 曲线以便观察训练/过拟合情况。

十、运行时你会看到的大致输出示例

  • print(len(training_data))60000

  • DataLoader 第一次打印:
    Shape of X[N,C,H,W]: torch.Size([64, 1, 28, 28])
    Shape of y: ftorch.Size([64]) torch.int64 (这里会带 f,如上所述)

  • print(f"Using {device} device")Using cuda device(或 mps/cpu

  • print(model) → 将打印网络结构(每层的 Linear(in_features, out_features))。

  • 训练过程中每 100 批会打印一次 loss,比如:
    loss: 0.123456 [number:100]

  • 最后测试:
    Test result:
    Accuracy:98.25%, Avg loss:0.0456 (数值示例,实际结果依赖训练情况)