)
引言在深度学习与计算机视觉中卷积神经网络CNN是最基础、最核心的模型之一。而MNIST手写数字识别则是每个入门者必经的“Hello World”项目。它不仅数据规模适中还能直观展示CNN如何从像素中自动提取特征并完成分类。本文将使用PyTorch从零构建一个CNN模型完整走通数据加载、模型定义、训练与测试的全流程。我们不仅会给出可直接运行的代码还会详细解释每一层的维度变化、核心API的含义并通过最终的98.54%准确率结果验证CNN在图像任务中的强大能力。读完本文你将彻底搞懂为什么CNN比全连接网络更适合图像卷积、池化、ReLU各自的作用是什么数据在CNN中是如何流转并最终输出分类结果的环境与依赖Python 3.8PyTorch 1.10torchvision任务描述我们使用经典的MNIST手写数字数据集完成以下任务自动下载并加载60000张训练图片、10000张测试图片定义一个由3个卷积模块和1个全连接输出层组成的CNN训练模型10个轮次epoch观察损失下降在测试集上评估模型输出准确率和平均损失。实现步骤详解1. 导入库并检查设备import torch from torch import nn from torch.utils.data import DataLoader from torchvision import datasets from torchvision.transforms import ToTensor # 自动选择设备CUDANVIDIA MPSApple CPU device cuda if torch.cuda.is_available() else mps if torch.backends.mps.is_available() else cpu print(fUsing device: {device})运行结果示例Using device: cpu2. 加载MNIST数据集# 训练集60000张transformToTensor() 将PIL图像转为张量并归一化到[0,1] training_data datasets.MNIST( rootdata, trainTrue, downloadTrue, transformToTensor(), ) # 测试集10000张 test_data datasets.MNIST( rootdata, trainFalse, downloadTrue, transformToTensor(), ) # 创建DataLoader每批32张训练时打乱顺序 train_dataloader DataLoader(training_data, batch_size32, shuffleTrue) test_dataloader DataLoader(test_data, batch_size32)说明ToTensor()将原始像素值从0~255缩放到0~1是神经网络训练前的标准预处理。3. 定义CNN模型我们使用nn.Sequential将卷积、激活、池化打包成模块使代码更简洁。class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() # 模块1提取底层特征边缘、线条 self.conv1 nn.Sequential( nn.Conv2d(1, 64, kernel_size5, stride1, padding2), # 输入1通道输出64通道 nn.ReLU(), nn.MaxPool2d(kernel_size2) # 尺寸减半 ) # 模块2提取中层特征笔画组合 self.conv2 nn.Sequential( nn.Conv2d(64, 32, 5, 1, 2), nn.ReLU(), nn.Conv2d(32, 16, 5, 1, 2), nn.ReLU(), nn.MaxPool2d(2) ) # 模块3提取高层特征数字整体形态 self.conv3 nn.Sequential( nn.Conv2d(16, 32, 5, 1, 2), nn.ReLU(), ) # 全连接输出层 self.out nn.Sequential( nn.Linear(32 * 7 * 7, 10), # 输入1568维输出10类 nn.ReLU() ) def forward(self, x): x self.conv1(x) # [batch, 64, 14, 14] x self.conv2(x) # [batch, 16, 7, 7] x self.conv3(x) # [batch, 32, 7, 7] x x.view(x.size(0), -1) # 展平[batch, 1568] output self.out(x) return output model CNN().to(device)维度变化详解层输入尺寸操作输出尺寸输入[32, 1, 28, 28]--conv1[32, 1, 28, 28]卷积(1→64, 5×5, padding2)[32, 64, 28, 28]ReLU[32, 64, 28, 28]非线性激活[32, 64, 28, 28]MaxPool[32, 64, 28, 28]2×2池化步长2[32, 64, 14, 14]conv2第一层[32, 64, 14, 14]卷积(64→32)[32, 32, 14, 14]conv2第二层[32, 32, 14, 14]卷积(32→16)[32, 16, 14, 14]MaxPool[32, 16, 14, 14]2×2池化[32, 16, 7, 7]conv3[32, 16, 7, 7]卷积(16→32)[32, 32, 7, 7]view展平[32, 32, 7, 7]保留batch合并其余[32, 1568]全连接[32, 1568]线性映射[32, 10]4. 定义损失函数与优化器loss_fn nn.CrossEntropyLoss() # 多分类交叉熵损失 optimizer torch.optim.SGD(model.parameters(), lr0.01) # 随机梯度下降5. 训练函数def train(dataloader, model, loss_fn, optimizer): model.train() # 设置为训练模式 batch_num 1 for X, y in dataloader: X, y X.to(device), y.to(device) pred model(X) # 前向传播 loss loss_fn(pred, y) # 计算损失 # 反向传播三件套 optimizer.zero_grad() loss.backward() optimizer.step() # 每100个batch打印一次当前损失 if batch_num % 100 0: loss_value loss.item() # 将标量张量转为Python浮点数 print(floss: {loss_value:7f} [number:{batch_num}]) batch_num 1关键点loss.item()提取损失张量的数值便于打印和记录同时不再保留计算图节省内存。model.train()启用BatchNorm、Dropout等层的训练行为本例虽未使用但属良好习惯。6. 测试函数def test(dataloader, model, loss_fn): total_samples len(dataloader.dataset) # 测试集总样本数10000 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(X) test_loss loss_fn(pred, y).item() # 统计正确预测数argmax(1)取概率最大的类别 correct (pred.argmax(1) y).type(torch.float).sum().item() test_loss / num_batches correct / total_samples print(fTest result:\n {(100 * correct):.2f}% ,avg loss:\n{test_loss:.6f})关键点pred.argmax(1)沿着类别维度维度1取最大值索引得到每个样本的预测类别。torch.no_grad()禁用梯度计算大幅减少内存消耗。7. 训练与测试主循环epochs 10 for epoch in range(epochs): print(fEpoch {epoch1}\n-----------) train(train_dataloader, model, loss_fn, optimizer) print(Done) test(test_dataloader, model, loss_fn)运行结果与分析运行上述代码输出如下节选Using device: cpu Epoch1 ----------- loss: 2.300039 [number:100] loss: 2.299318 [number:200] ... loss: 0.165141 [number:1800] Epoch2 ----------- ... Epoch10 ----------- loss: 0.000902 [number:1800] Done Test result: 98.54% ,avg loss: 0.043030结果分析损失下降趋势第一个epoch结束时损失已降至0.165后续每个epoch损失持续走低最终稳定在0.001左右表明模型收敛良好。测试准确率98.54%在仅10个epoch、简单CNN结构下表现优秀说明CNN能有效提取手写数字特征。泛化能力测试集与训练集独立准确率接近99%无明显过拟合。关键点解析1. 为什么图像任务首选CNN局部感受野卷积核只关注局部区域保留像素间空间关系。参数共享同一卷积核在整张图上复用参数量远小于全连接网络。池化下采样降低分辨率同时增强平移不变性。2.nn.Sequential的作用将多个层打包成一个模块forward中只需一行代码即可依次执行提高代码可读性。若网络存在分支或跳跃连接则需单独定义各层。3.view(x.size(0), -1)的理解x.size(0)是batch大小。-1让PyTorch自动计算剩余维度大小即channels × height × width。作用将四维特征图[batch, channels, h, w]展平为二维[batch, features]以输入全连接层。4.loss.item()的必要性loss是一个包含梯度信息的标量张量直接打印会显示tensor(0.2345, grad_fn...)。使用.item()提取为Python浮点数输出更简洁且不再保留计算图节省内存。5.pred.argmax(1)详解pred形状为[batch, 10]每行是模型对10个类别的原始分数logits。argmax(1)表示在维度1类别维度上取最大值的索引即模型预测的类别编号。例如某样本输出[2.3, 0.5, ...]则预测为0类。6. 训练与测试模式的差异model.train()启用Dropout、BatchNorm等层的训练行为。model.eval()固定BatchNorm统计量关闭Dropout确保推理结果稳定。torch.no_grad()在测试阶段关闭梯度计算加快计算速度并降低显存占用。常见问题与改进方向Q1为什么使用SGD而不是AdamSGD是最简单的优化器适合入门Adam通常收敛更快可尝试替换并观察效果。Q2如何提高准确率增加网络深度或宽度如再添加卷积层。使用数据增强随机旋转、平移等。调整学习率、使用学习率衰减。尝试更深的预训练模型如ResNet进行微调。Q3遇到“CUDA out of memory”怎么办减小batch_size或使用CPU训练本例CPU即可完成。完整代码import torch from torch import nn #导入神经网络 from torch.utils.data import DataLoader #数据包管理工具打包数据 from torchvision import datasets #封装了很多自带的图像数据集 from torchvision.transforms import ToTensor #数据转换张量。 下载训练数据集包含图片和标签 training_data datasets.MNIST( rootdata,#表示下载的手写数字到那个路径当前代码下 trainTrue,#读取下载后的数据中的训练数据 downloadTrue, #如果之前下载了就不用下载了 transformToTensor(),#张量图片不能直接导入神经网络 ) 下载测试数据集 test_data datasets.MNIST( rootdata, trainFalse, downloadTrue, transformToTensor(), ) 打包 train_dataloader DataLoader(training_data, batch_size32) test_dataloader DataLoader(test_data, batch_size32) 判断当前设备是否支持GPU其中mps是苹果的GPU device cuda if torch.cuda.is_available() else mps if torch.backends.mps.is_available() else cpu print(fUsing device: {device}) 定义卷积神经网络cnn class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1nn.Sequential( nn.Conv2d( in_channels1, #输入的通道 64*1*28*28 out_channels64, #得到的特征图64 kernel_size5, #卷积和 5 * 5 stride1, #步长1 padding2, ), nn.ReLU(), #64*64*28*28 nn.MaxPool2d(kernel_size2) #64*64*14*14 ) self.conv2nn.Sequential( nn.Conv2d(64,32,5,1,2),#64*32*14*14 nn.ReLU(), nn.Conv2d(32,16,5,1,2),#64*16*14*14 nn.ReLU(), nn.MaxPool2d(2)#64*16*7*7 ) self.conv3nn.Sequential( nn.Conv2d(16,32,5,1,2),#64*32*7*7 nn.ReLU(), ) self.out nn.Sequential( nn.Linear(32*7*7,10), nn.ReLU() ) def forward(self,x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x x.view(x.size(0),-1) output self.out(x) return output model CNN().to(device) loss_fn nn.CrossEntropyLoss() #交叉熵损失函数 optimizer torch.optim.SGD(model.parameters(), lr0.01) def train(dataloader,model,loss_fn,optimizer): model.train() batch_num 1 for X,y in dataloader: X,y X.to(device),y.to(device) pred model(X) loss loss_fn(pred,y) optimizer.zero_grad() # 这三是固定模版梯度清零 loss.backward() # 反向传播计算得到每个参数 optimizer.step() # 根据选择的优化器更新w我这个选择的是Adam loss_value loss.item() # 从tensor数据中提取出来tensor获取损失值 if batch_num % 100 0: # 打包训练100次输出一次损失值 print(floss: {loss_value:7f} [number:{batch_num}]) batch_num 1 def test(dataloader,model,loss_fn): zongshu len(dataloader.dataset) num_batches len(dataloader) model.eval() # 测试 w就不更新了 test_loss, count 0, 0 with torch.no_grad(): # 上下文管理器 关闭梯度计算减少计算所用的内存消耗 for X, y in dataloader: X, y X.to(device), y.to(device) pred model.forward(X) # .forward可以省略 test_loss loss_fn(pred, y).item() count (pred.argmax(1) y).type(torch.float).sum().item() test_loss / num_batches count / zongshu print(fTest result:\n {(100 * count)}% ,avg loss:\n{test_loss}]) S 10 for i in range(S): print(fEpoch{i 1}\n-----------) train(train_dataloader,model,loss_fn,optimizer) print(Done) test(test_dataloader,model,loss_fn)总结本文通过一个完整的PyTorch代码示例详细讲解了CNN从数据加载、模型定义到训练测试的全过程。我们不仅给出了可直接运行的代码还对每一层的维度变化、关键API如view、item、argmax进行了深入剖析并结合最终98.54%的测试准确率验证了CNN在图像分类任务上的强大能力。CNN中的卷积与池化则是现代计算机视觉的基石。掌握本文内容你将具备进一步学习更复杂模型如ResNet、目标检测的能力。完整代码已内嵌于文中复制即可运行。参考资料PyTorch官方教程https://pytorch.org/tutorials/MNIST数据集http://yann.lecun.com/exdb/mnist/