一、 导包

import time
import torch
import numpy as np
import torch.nn as nn
from matplotlib import pyplot as plt
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import random
import os

二、 定义我的模型

class myModel(nn.Module):
    def __init__(self, class_num=10):
        super().__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1,32,3,1,1),
            nn.ReLU(),
            nn.Conv2d(32,32,3,1,1),
            nn.ReLU(),
            nn.MaxPool2d(2)    # 32 * 14 * 14
        )

        self.layer2 = nn.Sequential(
            nn.Conv2d(32, 64, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(64, 64, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2)     # 64 * 7 * 7
        )

        self.layer3 = nn.Sequential(
            nn.Linear(3136,1024),
            nn.ReLU(),
            nn.Linear(1024,512),
            nn.ReLU(),
            nn.Linear(512,10)
        )

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = torch.flatten(x,1)   #前向传播这里注意从卷积到全连接需要展平,不然会出现维度不匹配
        x = self.layer3(x)
        return x

由于mnist数据集一张图片大小只有28 * 28 小到可以直接不用卷积进行展平,所以感觉模型也不用设置得太大,所以我的模型分为3层,

第一层两次卷积一层池化:1*28*28 -> 32*14*14

第二层两次卷积一层池化:32*14*14 -> 64*7*7

第三层全连接层:(64*7*7,1024) -> (1024,512) -> (512,10)  模型总体还是比较简单

三、定义参数

# Super parameter ------------------------------------------------------------------------------------
batch_size = 64
learning_rate = 0.001
momentum = 0.5
EPOCH = 20
train_size = 55000      # 训练集大小
val_size = 5000         # 验证集大小
device = "cuda" if torch.cuda.is_available() else "cpu"
model = myModel(10).to(device)
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(),lr=learning_rate,weight_decay=1e-4)
save_path = "model_save/best.model.pth"
epoches = 16

定义 设备,模型放到设备上,损失函数,优化器

这里把学习率设置为0.01的时候训练不起来,设置为0.001的时候能跑起来

可能的原因是:

  • 学习率 = 每一步走多远

🚶 lr = 0.001(小步慢走)

  • 每一步很小,能稳定向最优点逼近,虽然慢,但安全

🏃 lr = 0.01(步子太大)

  • 一步可能直接跨过谷底,来回震荡,甚至直接跑到“悬崖”(loss 变成 nan / inf),导致训练失败

四、 加载数据

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])

full_dataset = datasets.MNIST(root='./data/mnist', train=True, transform=transform, download=False)  # 本地没有就加上download=True
test_dataset = datasets.MNIST(root='./data/mnist', train=False, transform=transform, download=False)  # train=True训练集,=False测试集

train_dataset, val_dataset = torch.utils.data.random_split(full_dataset, [train_size,val_size],
                                                           generator=torch.Generator().manual_seed(42))
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)   # val_loader 上面的数据不打乱
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

transforms.ToTensor() 将PIL图片转换为张量

transforms.Normalize()  这里均值 0.1307、标准差 0.3081 是官方在 完整 MNIST 训练集 上预先算好的全局统计量。

transforms.Compose([

transforms的一些列操作列表

])  

Compose()类会将transforms列表里面的transform操作进行遍历

torchvision.datasets: 一些加载数据的函数及常用的数据集接口

train_loader 上的数据需要shuffle  val_loader和test_loader上面的数据一般不做shuffle处理

五、模型训练

1、 函数调用入口

def train_val(model, train_loader, val_loader, optimizer, loss, epoches, device, save_path):

参数包括:模型、train_loader,val_loader,优化器,损失函数,训练轮次,使用设备,保存路径

其中:epoches 是:模型把 所有训练数据完整看一遍 = 1 个 epoch

epoches 过大可能照成的影响:模型把数据集背下来了,导致过拟合,泛化能力减弱,或者导致训练时间过久,所以一般会根据val_acc早停,即在val_acc最优时及时保存模型

model = model.to(device)
plt_train_loss = []
plt_val_loss = []
plt_train_acc = []
plt_val_acc = []
max_acc = 0.0
  • model = model.to(device)  把模型的所有参数(权重、偏置)移动到指定设备上运行(CPU 或 GPU)。
  •  [ ]列表记录的是每一个 epoch 的平均 loss 和平均 accuracy。每一个epoch初始都会把该轮的train_loss记为0,用于记录该轮总的loss 
  • plt_train_loss[ ] 是对该轮batch数量的平均
  • plt_train_acc[ ]  记录的是每一个epoch的整体训练准确率
  • 2. 开始训练

    for epoch in range(epoches):
        start_time = time.time()   #记录每一轮训练所用的时间
        train_loss = 0.0           #累计每一个batch的loss,最后对batch数量做平均放到plt_train_loss里面
        val_loss = 0.0             #记录验证集上面的loss
        train_acc = 0.0            #记录训练集上面的准确度
        val_acc =  0.0             #记录验证集上面的准确度

        model.train()               #把模型切换到“训练模式”,启用只在训练时才生效的层行为。
        for batch_x, batch_y in train_loader:
            x, target = batch_x.to(device),batch_y.to(device)
            pred = model(x)
            train_batch_loss = loss(pred, target)
            optimizer.zero_grad()  # 1. 先把旧梯度清零
            train_batch_loss.backward()  # 2. 反向传播,计算新梯度
            optimizer.step()  # 3. 用新梯度更新参数
            train_loss += train_batch_loss.cpu().item()
            train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())

        plt_train_loss.append(train_loss / train_loader.__len__())
        plt_train_acc.append(train_acc / train_loader.dataset.__len__())

        model.eval()
        with torch.no_grad():
            for batch_x, batch_y in val_loader:
                x, target = batch_x.to(device), batch_y.to(device)
                pred = model(x)
                val_bat_loss = loss(pred, target)
                val_loss += val_bat_loss.cpu().item()
                val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
        plt_val_loss.append(val_loss / val_loader.__len__())
        plt_val_acc.append(val_acc / val_loader.dataset.__len__())
        if val_acc > max_acc:
            torch.save(model, save_path)
            max_acc = val_acc

        print("[%03d/%03d] %2.2f sec(s) Trainloss: %.6f |Valloss: %.6f  train_acc: %.6f | val_acc: %.6f" % \
              (epoch, epoches, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1],
               plt_val_acc[-1]))
  • model.train()  把模型切换到“训练模式”,启用只在训练时才生效的层行为,       比如说Dropout会在训练时以一定的概率丢弃神经元,但是在测试/验证的时候需要使用所有的神经元,    同样的BatchNorm在训练时使用当前 batch 的均值方差在测试和验证时使用历史统计均
  • x,target = batch_x.to(device),batch_y.to(device)把 Tensor 移动到指定设备
  • pred = model(x)  使用模型进行预测
  • 根据loss函数计算这个batch的loss
  • optimizer.zero_grad()  # 1. 先把旧梯度清零
    train_batch_loss.backward()  # 2. 反向传播,计算新梯度
    optimizer.step()  # 3. 用新梯度更新参数   这三个步骤顺序颐一定不要搞错了
  • train_loss 用于累加每一个batch的loss
  • train_acc 用于累加每一个batch的准确个数
  • plt_train_loss.append(train_loss / train_loader.__len__())
    plt_train_acc.append(train_acc / train_loader.dataset.__len__())   训练完所有的batch就需要将这一轮的train_loss和train_acc放到plt_train_loss / acc 里面
  • model.eval() 将模型调到评估模式,禁用训练专用行为(如 Dropout 和 BatchNorm 的 batch 统计),保证验证或测试时行为稳定。   
  • with torch.no_grad():不计算梯度,节省显存,通常和上面的model.eval() 一起使用
  • 假如模型里面有  目的是训练时增加模型的泛化能力。
    nn.Droupout(0.5)
           模型会在训练的时候会以p概率丢弃神经元,剩下的的神经元占比1-p,这些神经元会被激活然后除以(1-p)来保持期望一致

六、 模型对比

由于考虑到mnist数据集图片大小较小,尝试不经过卷积直接展平,得到的模型精度下降了10+%

全连接:        

  • 直接把每个像素当作独立特征

  • 忽略空间结构(上下左右关系)

  • 参数量大,容易过拟合

卷积+全连接

  • 卷积层 提取局部特征(边缘、角点、纹理)

  • 共享权重,每个卷积核扫描整个图像

  • 池化层 降维,增加平移不变性

  • 参数量少,泛化能力强

为什么卷积网络效果更好?

(1) 保留空间结构

  • 图像是二维的,像素之间有空间相关性

  • CNN 利用卷积核只看小邻域 → 学到局部模式

  • FCN 展平后,每个像素被当作独立输入 → 空间信息丢失

例如:

  • MNIST 数字 “8”:

    • 卷积可以捕捉上下圆圈的局部形状

    • FCN 展平后,全局线性组合难以学习局部关系


(2) 参数量少,训练更稳定

  • FCN 输入 28x28=784 → 第一层 512 个神经元

  • 参数量 = 784 × 512 = 401,408 个

  • CNN 第一层 32 个 3x3 卷积核 → 参数量 = 32×3×3=288 个

  • 卷积层参数共享 → 总参数量远小于 FCN

  • ✅ 参数少 → 更容易训练 → 更高精度


(3) 权重共享与局部感受野

  • 卷积核共享权重 → 可以在整个图像扫描相同模式

  • 局部感受野 → 捕捉局部特征而不是全局混合

  • FCN 的全连接层没有这种优势,每个像素独立学习 → 很难捕捉边缘、形状


(4) 平移和变形鲁棒性

  • CNN 的池化层 + 卷积 → 对轻微平移、旋转、缩放有鲁棒性

  • FCN 对平移特别敏感

  • MNIST 数据集中,手写数字位置略有偏移 → CNN 适应性更好

更多推荐