P1周:Minist手写识别
📅 2026/7/11 2:49:31
👁️ 阅读次数
📝 编程学习
P1周:Minist手写识别
文章目录
- P1周:Minist手写识别
- 目标
- 一、前期准备
- 1.引入库及前期设置
- 2.导入数据
- 3.数据可视化
- 二、CNN网络构建
- 三、训练模型
- 1.设置超参数
- 2.编写训练函数
- 3.编写测试函数
- 四、正式训练
- 1.训练模型
- 2.可视化图表
- 3.模型测试
- 问题总结
- 为什么只选中部分代码执行到控制台,不在选中代码中的变量也都能用?
- 相对路径的工作目录问题,数据集下载的相对路径与想象的相对路径总是不同
- 数据集维度问题总结
- 整体工作流程总结
- 1. 环境与数据准备
- 2. 模型构建
- 3. 训练准备
- 4. 模型训练与评估
- 5. 模型测试与可视化
目标
- 了解Pytorch
- 手敲简易的深度学习代码
- 弄清函数方法
一、前期准备
1.引入库及前期设置
importtorchimporttorch.nnasnnimportmatplotlib.pyplotaspltimporttorchvisionimportnumpyasnpfromdatetimeimportdatetime plt.rcParams['font.sans-serif']=['SimHei']# 用来正常显示中文标签plt.rcParams['axes.unicode_minus']=False# 用来正常显示负号plt.rcParams['figure.dpi']=100#分辨率MODE="train"device=torch.device("cuda"iftorch.cuda.is_available()else"cpu")print(device)代码输出:
D:\conda\envs\yolo_env\python.exe D:\deeplearning\minist\main.py cuda2.导入数据
##下载数据集##root是保存地址,torchvision.transforms是pytorch的图像处理箱##Tensor为张量,一个数组,格式为(通道C × 高H × 宽W),#正常图片为((高H × 宽W × 通道C),img[0,2,3]表示第二行第三列为黑色,##表层的通道就是不同颜色,img[1, 2, 3] = 0.3←这像素的绿色强度train_ds=torchvision.datasets.MNIST(root='D:/deeplearning/minist/data',train=True,download=True,transform=torchvision.transforms.ToTensor())test_ds=torchvision.datasets.MNIST(root='D:/deeplearning/minist/data',train=False,download=True,transform=torchvision.transforms.ToTensor())##引入数据加载器,用于对数据集进行抽取一批次一批次的样本##形参说明:dataset为样本从哪个数据集抽取,batch_size为每批次抽取数量,sampler为高级抽法# shuffle为打乱抽取,num_workers在windows取0,drop_last为最后一批不足时,是否丢弃batchsize=32train_dl=torch.utils.data.DataLoader(dataset=train_ds,batch_size=batchsize,shuffle=True,num_workers=0,drop_last=False)test_dl=torch.utils.data.DataLoader(dataset=test_ds,batch_size=batchsize)# #把train_dl变成迭代器 ,imgs.shape即张量形状[32,1,28,28]imgs,labels=next(iter(train_dl))3.数据可视化
#matplotlib.pyplot as plt ,创建画布,后面plt.imshow的内容会呈现在画布上ifMODE=="train":plt.figure(figsize=(20,5))fori,imgsinenumerate(imgs[:20]):##张量形状[20,1,28,28] 单张[1,28,28]npimg=np.squeeze(imgs.numpy())##维度缩减 [1,28,28]→ [28,28],把pt张量变为numpy数组,移除为1的维度plt.subplot(2,10,i+1)##行2*列10的网格,激活第i+1个格子(从1开始,从左往右,从上往下)plt.imshow(npimg,cmap=plt.cm.binary)##plt.imshow(data,cmap)cmap是颜色规则plt.axis('off')##隐藏坐标轴plt.show()##用于展示画布二、CNN网络构建
importtorch.nn.functionalasF##函数APInum_class=10classModel(nn.Module):##继承自父类 nn.Moduledef__init__(self):## 构造函数super().__init__()## 调用父类的构造函数#特征提取网络 ##参数计算通用公式:输出尺寸 = 输入尺寸 - 卷积核尺寸 + 1self.conv1=nn.Conv2d(1,32,kernel_size=3)#输入1通道,输出32个特征图 [1,28,28]→[32,26,26] 26=28-3+1self.pool1=nn.MaxPool2d(2)#2*2窗口,尺寸减半 [32,26,26]→[32,13,13] 13=26/2self.conv2=nn.Conv2d(32,64,kernel_size=3)#[32,13,13]→[64,11,11] 13-3+1=11self.pool2=nn.MaxPool2d(2)##[64,11,11]→[64,5,5] 11//2=5#分类网络self.fc1=nn.Linear(1600,64)##1600个输入特征数怎么来的:64*5*5self.fc2=nn.Linear(64,num_class)defforward(self,x):x=self.pool1(F.relu(self.conv1(x)))x=self.pool2(F.relu(self.conv2(x)))x=torch.flatten(x,start_dim=1)##torch.flatten(x,start_dim=1):# start_dim=1 表示从第1维开始压平。# 把64个5*5的列表一共1600个输入特征拉成一个1600维的向量,就像把一个表格的每个格子里的数字抄成一长行# 第 0 维是 batch(批次),必须保留:x=F.relu(self.fc1(x))x=self.fc2(x)returnx##看清模型架构,每层的参数 conv2d 1-1: 1*3*3*32=288 288+32=320#conv2d 1-3: 32*3*3*64=18432 18432+64=18496# from torchinfo import summarymodel=Model().to(device)##实例化模型并且搬到gpu##summary(model)三、训练模型
1.设置超参数
loss_fn=nn.CrossEntropyLoss()##交叉熵损失函数learning_rate=0.001opt=torch.optim.SGD(model.parameters(),lr=learning_rate)## optimizer(优化器)创建一个SGD优化器(随机梯度下降),## model.parameters() — 告诉优化器要去更新模型里的哪些参数2.编写训练函数
deftrain(dataloader,model,loss_fn,optimizer):size=len(dataloader.dataset)num_batches=len(dataloader)train_loss,train_acc=0,0forX,yindataloader:X,y=X.to(device),y.to(device)pred=model(X)#网络输出# pred 的形状是 [32, 10],每列代表的是一种数字的概率,每行代表的是一张图片它的数字概率分布###前面 model=Model().to(device),model是Model的对象,Model继承了父类Module.nn ,# Module.nn包含了.__call__()方法,所以model(X)==model.forward(X)loss=loss_fn(pred,y)#计算损失一张图的概率[2.0,1.0,0.1],softmax,[0.659,0.242,0.099],比如第一列为正确标签,# 将第一列作负对数,-log(0.659)=0.417 以此得到32张图的负对数,相加/32=loss#也就是说交叉熵只关心正确类别的概率,希望越大越好#必要步骤optimizer.zero_grad()##梯度清零loss.backward()##反向传播##链式法则:y=wx x=3 y_true=6 y_pred=w*x=1*x=3 损失函数=(y_pred-y_true)^2=9#我们想知道w变化,loss如何变化,# 即d(loss)/d(w),d(loss)/d(y_pred)*d(y_pred)/d(w)=-6*3=-18(-18即为梯度),,optimizer.step()##自动更新 # 新w=旧w-lr*梯度=1.18 故y_pred=1.18*3=3.54train_acc+=(pred.argmax(1)==y).type(torch.float).sum().item()## pred.argmax(1) 对每行,找出最大概率在第几列,返回列的索引##假设这批 32 张图中,y 的真实标签和 pred 的预测结果如下:#真实标签 y: [3, 7, 1, 0, 9, ...] (32个)#pred.argmax(1): [3, 7, 0, 0, 9, ...] (32个,第3个预测错了)# == 比较: [T, T, F, T, T, ...] (31个True, 1个False)# .float(): [1.0, 1.0, 0.0, 1.0, 1.0, ...]#.sum(): [31.0] (这张卡32张图,对了31张)#.item(): 31 标量值train_loss+=loss.item()##.item()把标量张量的数值提取出来train_acc/=size#一个epoch的准确率train_loss/=num_batches#一个epoch的损失率returntrain_loss,train_acc3.编写测试函数
##编写测试函数deftest(dataloader,model,loss_fn):##测试函数只用于测试,不反向传播,所以不用优化器size=len(dataloader.dataset)num_batches=len(dataloader)test_loss,test_acc=0,0withtorch.no_grad():forX,yindataloader:X,y=X.to(device),y.to(device)#计算损失target_pred=model(X)loss=loss_fn(target_pred,y)test_loss+=loss.item()test_acc+=(target_pred.argmax(1)==y).type(torch.float).sum().item()test_acc/=size test_loss/=num_batchesreturntest_loss,test_acc四、正式训练
1.训练模型
##正式训练ifMODE=="train":epochs=20train_loss,train_acc,test_loss,test_acc=[],[],[],[]forepochinrange(epochs):model.train()##测试模式,模型Dropout启用,BatchNorm 层:使用当前 batch 的均值和方差进行标准化,并更新其内部的运行均值和方差epoch_train_loss,epoch_train_acc=train(train_dl,model,loss_fn,opt)train_loss.append(epoch_train_loss)train_acc.append(epoch_train_acc)model.eval()##评估模式,Dropout 层:关闭(不再随机丢弃神经元) BatchNorm 层:使用训练时记录的均值和方差,不再更新epoch_test_loss,epoch_test_acc=test(test_dl,model,loss_fn)test_loss.append(epoch_test_loss)test_acc.append(epoch_test_acc)template=('Epoch:{:2d},Train_acc{:.1f}%,Train_loss:{:.3f},Test_acc{:.1f}%,Test_loss:{:.3f}')print(template.format(epoch+1,train_acc[-1]*100,train_loss[-1],test_acc[-1]*100,test_loss[-1]))print("Done")torch.save(model.state_dict(),"D:/deeplearning/minist/model.pth")###保存训练好的权重参数2.可视化图表
###############可视化############current_time=datetime.now()# 获取当前时间epochs_range=range(epochs)plt.figure(figsize=(12,3))plt.subplot(1,2,1)plt.plot(epochs_range,train_acc,label='Training Accuracy')plt.plot(epochs_range,test_acc,label='Test Accuracy')plt.legend(loc='lower right')##plt.title('Training and Validation Accuracy')plt.xlabel(current_time)# 时间戳plt.subplot(1,2,2)plt.plot(epochs_range,train_loss,label='Training Loss')plt.plot(epochs_range,test_loss,label='Test Loss')plt.legend(loc='upper right')plt.title('Training and Validation Loss')plt.show()###########可视化###############3.模型测试
elifMODE=="test":# ===== 测试模式:加载权重 → 评估 → 可视化 =====model.load_state_dict(torch.load("D:/deeplearning/minist/model.pth"))model.eval()# 可视化 10 张图(和 use.py 一样)plt.figure(figsize=(15,5))foriinrange(10):img,label=test_ds[i]pred_label=model(img.unsqueeze(0).to(device)).argmax(1).item()plt.subplot(2,5,i+1)plt.imshow(img.squeeze(),cmap="gray")color="green"ifpred_label==labelelse"red"plt.title(f"truth:{label}predict:{pred_label}",color=color)plt.axis("off")plt.show()问题总结
为什么只选中部分代码执行到控制台,不在选中代码中的变量也都能用?
原因:之前已经完整运行过整个 .py 文件,所有变量早已驻留在控制台内存中
- 关键:Python 控制台维护持久化内存命名空间,执行选区只是新增/覆盖变量,不会清除已有变量
验证:重启控制台后只执行前文代码,后文的变量就会报NameError
相对路径的工作目录问题,数据集下载的相对路径与想象的相对路径总是不同
原因:./data 不是相对于 main.py 的位置,而是相对于Python 进程的当前工作目录
- 验证:
import os; print(os.getcwd()) - 修复:改成绝对路径
数据集维度问题总结
| 位置 | 变量/操作 | 形状 (Shape) | 含义 |
|---|---|---|---|
| 原始 MNIST 图片 | PIL Image | [28, 28] | 宽 28 × 高 28 的二维灰度矩阵。 |
ToTensor()转换后 | test_ds[i][0] | [1, 28, 28] | ToTensor()自动添加了通道维度 (Channel),变为三维张量:[通道, 高, 宽]。MNIST 是单通道灰度图,所以通道数为 1。 |
DataLoader取一批数据 | X(在for X, y in dataloader中) | [32, 1, 28, 28] | DataLoader自动堆叠了batch_size(此处为 32) 个样本,添加了批次维度 (Batch),变为四维张量:[批次大小, 通道, 高, 宽]。 |
| 单张图片喂入模型 | img.unsqueeze(0) | [1, 1, 28, 28] | 模型通常要求输入包含批次维度。unsqueeze(0)在dim=0位置手动添加一个维度,模拟一个批次大小为 1 的输入。 |
核心要点:
ToTensor():负责[H, W]→[C, H, W]的转换,并归一化像素值到[0, 1]。DataLoader:负责[C, H, W]→[B, C, H, W]的转换,即组织成批次。unsqueeze(0):当需要将单张图片输入模型进行预测时,必须手动补上批次维度B,否则会因维度不匹配而报错。
整体工作流程总结
本 MNIST 手写识别项目的完整流程可概括为:首先进行环境与数据准备,包括导入必要的 PyTorch 库、设置计算设备、下载 MNIST 数据集并通过ToTensor()和DataLoader进行预处理与批次加载;接着构建一个包含卷积层、池化层和全连接层的 CNN 模型;然后定义交叉熵损失函数和 SGD 优化器,并编写训练与测试函数;随后进入训练循环,在多个 epoch 中交替进行训练(前向传播、损失计算、反向传播、参数更新)和评估,同时保存训练好的模型权重;最后,在测试模式下加载模型对测试集样本进行预测,并通过可视化图表展示预测结果与训练过程曲线,从而完成从数据到模型训练再到评估的端到端深度学习流程。
1. 环境与数据准备
- 引入库:导入
torch、torch.nn、torchvision、matplotlib等核心库。 - 设置设备:检测并指定使用 GPU (
cuda) 或 CPU 进行计算。 - 下载与加载数据:
- 使用
torchvision.datasets.MNIST下载 MNIST 训练集和测试集。 - 通过
torchvision.transforms.ToTensor()将图片数据转换为 PyTorch 张量,并自动归一化像素值到 [0, 1] 区间。 - 使用
torch.utils.data.DataLoader将数据集封装成可按批次 (batch_size) 迭代的数据加载器,便于训练时高效读取。
- 使用
2. 模型构建
- 定义网络结构:创建一个继承自
nn.Module的Model类。 - 特征提取部分:使用两个
Conv2d(卷积层)和MaxPool2d(最大池化层)的组合,逐步提取图像特征并降低空间维度。 - 分类部分:使用
Linear(全连接层)将提取的特征映射到 10 个类别(数字 0-9)的输出。 - 实例化模型:创建
Model实例,并通过.to(device)将其移动到指定的计算设备上。
3. 训练准备
- 定义损失函数:使用
nn.CrossEntropyLoss()(交叉熵损失函数)来衡量模型预测与真实标签之间的差距。 - 定义优化器:使用
torch.optim.SGD(随机梯度下降优化器)来根据损失函数的梯度更新模型的参数。 - 编写训练/测试函数:
train函数:在一个 epoch 内,遍历训练数据加载器,完成前向传播、损失计算、反向传播和参数更新,并计算该 epoch 的平均训练损失和准确率。test函数:在一个 epoch 内,遍历测试数据加载器,完成前向传播和损失计算(不更新参数),并计算该 epoch 的平均测试损失和准确率。
4. 模型训练与评估
- 循环训练:设置训练轮数 (
epochs),在每一轮中:- 调用
model.train()将模型设置为训练模式。 - 调用
train函数在训练集上进行一次完整的训练,得到本轮训练指标。 - 调用
model.eval()将模型设置为评估模式。 - 调用
test函数在测试集上进行一次完整的评估,得到本轮测试指标。 - 打印并记录本轮训练和测试的损失与准确率。
- 调用
- 保存模型:训练完成后,使用
torch.save将训练好的模型权重(state_dict)保存到本地文件(如model.pth)。
5. 模型测试与可视化
- 加载与测试:在测试模式下 (
MODE=="test"):- 使用
torch.load加载之前保存的模型权重。 - 从测试集中抽取若干样本,通过模型进行预测。
- 将预测结果与真实标签对比,并使用
matplotlib可视化展示,绿色标题表示预测正确,红色表示错误。
- 使用
- 训练过程可视化:在训练模式下 (
MODE=="train"),使用matplotlib绘制训练和测试的准确率、损失曲线随 epoch 变化的图表,用于分析模型的学习情况。
编程学习
技术分享
实战经验