我的第一个深度学习——mnist
一、 导包
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() 一起使用
- 假如模型里面有 目的是训练时增加模型的泛化能力。
模型会在训练的时候会以p概率丢弃神经元,剩下的的神经元占比1-p,这些神经元会被激活然后除以(1-p)来保持期望一致nn.Droupout(0.5) -
六、 模型对比
由于考虑到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 适应性更好
更多推荐

所有评论(0)