本文为365天深度学习训练营 中的学习记录博客 原作者K同学啊文章目录前言一、准备工作1. 配置环境2. 关于MNIST数据集3. 数据下载与导入3.1 创建 训练集与测试集3.2 创建训练数据加载器DataLoader4. 数据可视化二、构建简单的CNN网络1. CNN 网络中的常用层1.1 卷积层 nn.Conv2d1.2 池化层 nn.MaxPool2d1.3 激活函数 ReLU1.4 全连接层 nn.Linear1.5 展平操作 torch.flatten()2. 定义 CNN 模型4. 加载并打印模型结构三、训练模型1. 设置超参数2. 编写训练函数3. 编写测试函数4. 正式训练四、结果可视化总结前言本篇是我训练营的第一次学习主要目标是使用 PyTorch 实现MNIST 手写数字识别。MNIST 是一个非常经典的图像分类数据集任务是让模型识别图片中的手写数字属于0-9中的哪一类。因为我是python和深度学习初学者所以这篇笔记暂时不追求完全理解所有数学原理而是按照老师说的先跑通代码再逐步理解的思路进行学习。本文主要记录从环境配置、MNIST 数据导入、DataLoader 批量加载、数据可视化到构建简单 CNN 网络、训练模型、测试模型和结果可视化的完整流程。感谢K同学啊老师的教学以及ChatGPT和KIMI CODE。一、准备工作1. 配置环境教程的版本是python3.8因为我最近一直用3.12所以又新创建了一个3.12环境虽然各大AI都建议我用3.11但我准备出问题再降版本吧conda create-ntorchstudypython3.12我的显卡是AMD的所以虽然有GPU但不支持CUDA我在网上看到有人新开发了ZLUDA可以将amd显卡伪装之后就能用GPU加速但毕竟是小白把教程跑通最重要先学习知识后面如果尝试成功会再进行记录。想要直接在VS Code里用jupyter别忘记安装ipykernel激活环境conda activate torchstudy安装ipykernel之后就可以去VS Code里按运行代码啦~后面还会用到matplotlib一起安装了condainstall-cconda-forge ipykernel matplotlib安装torch、torchvision我安装了只有cpu的PyTorch相关库建议用 pip 装pip install torch torchvision--index-url https://download.pytorch.org/whl/cpu pip install torchinfo安装之后运行下面代码检查不报错即可importtorchimporttorchvisionimportmatplotlibimporttorchinfoprint(torch版本,torch.__version__)print(torchvision版本,torchvision.__version__)torch版本 2.11.0cpu torchvision版本 0.26.0cpu环境配置完成(-∀-)ノ~*2. 关于MNIST数据集http://yann.lecun.com/exdb/mnist/本次教程用MNIST数据集MNIST 是一个非常经典的手写数字识别数据集数字范围是0-9共10类总共有70000张图片训练集 (Training Set)60,000 张测试集 (Test Set)10,000 张图片分辨率是28 × 28这意味着每张图片在模型眼里其实是一个由28 × 28784个数字组成的向量MNIST 图片是灰度图每个像素表示亮度值原始范围通常是0-255。经过ToTensor()转换后像素值会被缩放到0-1更适合神经网络进行计算。3. 数据下载与导入函数原型torchvision.datasets.MNIST(root, trainTrue, transformNone, target_transformNone, downloadFalse)参数说明● root (string) 数据地址● train (string) True-训练集False-测试集● download (bool,optional) : 如果为True从互联网上下载数据集并把数据集放在root目录下● transform (callable, optional )这里的参数选择一个你想要的数据转化函数直接完成数据转化● target_transform (callable,optional) 接受目标并对其进行转换的函数/转换3.1 创建训练集与测试集train_dstorchvision.datasets.MNIST(data,trainTrue,transformtorchvision.transforms.ToTensor(),# 将数据类型转化为TensordownloadTrue)这一段的意思是请从 torchvision.datasets 中读取 MNIST 数据集把数据放到 data 文件夹里我要的是训练集读取图片时把图片转换成 Tensor如果本地没有数据就自动下载。torchvision是 PyTorch 中专门处理图像任务的工具包datasets是它里面的一个数据集仓库MNIST收录其中data是数据文件存储路径如果用绝对路径可以改写为rootrD:\Datasets\mnisttrainTrueMNIST 数据集本来就分成两部分这里是调取训练集transformtorchvision.transforms.ToTensor()表示读取图片时把图片转换成 Tensor 张量格式。原始图片通常不是 PyTorch 模型可以直接训练的格式。模型需要的是数字张量所以要把图片转换成 Tensor 张量格式。PyTorch 里一张 MNIST 图片经过 ToTensor() 转换后形状通常是[1, 28, 28]其中1是通道数MNIST 是灰度图所以只有 1 个通道28, 28分别是图片高度和图片宽度。同时ToTensor() 还会把像素值从0-255转换成0-1这样更适合神经网络训练downloadTrue如果本地没有 MNIST 数据集就自动从网上下载。第一次运行时它会下载数据如果下载报错很可能由于无法连接下载数据导致的可以手动下载数据集放到代码文件同一个目录下直接从本地加载数据集。test_dstorchvision.datasets.MNIST(data,trainFalse,transformtorchvision.transforms.ToTensor(),# 将数据类型转化为TensordownloadTrue)与训练集创建相同唯一的区别就是trainFalse这里读取测试集。3.2 创建训练数据加载器DataLoader函数原型torch.utils.data.DataLoader(dataset, batch_size1, shuffleNone, samplerNone, batch_samplerNone, num_workers0, collate_fnNone, pin_memoryFalse, drop_lastFalse, timeout0, worker_init_fnNone, multiprocessing_contextNone, generatorNone, *, prefetch_factor2, persistent_workersFalse, pin_memory_device‘’)参数说明● dataset (string) 加载的数据集● batch_size (int,optional) 每批加载的样本大小默认值1● shuffle (bool,optional) : 如果为True每个epoch重新排列数据。● sampler (Sampler or iterable, optional) 定义从数据集中抽取样本的策略。 可以是任何实现了len的 Iterable。 如果指定则不得指定 shuffle 。● batch_sampler (Sampler or iterable, optional) 类似于sampler但一次返回一批索引。与 batch_size、shuffle、sampler 和 drop_last 互斥。● num_workers (int,optional) 用于数据加载的子进程数。 0 表示数据将在主进程中加载默认值0。batch_size32train_dltorch.utils.data.DataLoader(train_ds,batch_sizebatch_size,shuffleTrue)test_dltorch.utils.data.DataLoader(test_ds,batch_sizebatch_size)这一段的意思是每次从数据集中取出 32 张图片打包成一捆一个 batch在深度学习中我们通常不会一次性把全部 60000 张训练图片都送进模型而是分批送进去MNIST 训练集有 60000 张图片那么训练时会分成1875 个批次也就是说模型每次看 32 张图片更新一次参数。等所有批次都训练完就完成了一轮训练也就是一个epoch。batch_size 32设置参数为 32 利用torch.utils.data.DataLoader()可以理解为数据传送带目的就是把原始的训练集和测试集train_dstest_ds转换成7分批传送的train_dl和test_dlshuffleTrue表示每个 epoch 之前把图片顺序打乱。防止模型按顺序记住答案比如先看到全是0再看到全是1让它真正学会识别特征。其余的没有进行设置初学不需要这么复杂为了深刻理解用AI帮助了一下以下进行记录。sampler的作用是指定从数据集中抽取样本的规则如果你想按照某种特殊顺序抽样或者想让某些类别被抽到的概率更高就可以设置 sampler我们已经设置了随机sampler 和 shuffle 通常不要同时设置。batch_sampler是更高级的采样方式它不是一个一个样本抽而是直接规定每一批 batch 里包含哪些样本通常不能和batch_size、shuffle、sampler、drop_last这些参数同时使用。num_workers这个参数控制用几个子进程来加载数据。0表示只用主进程加载数据。collate_fn的作用是规定如何把多个样本拼成一个 batch我们的代码默认会自动把它们拼成32 张图片组成 imgs32 个标签组成 labelsMNIST 每张图片大小都一样所以默认拼接方式完全够用。只有当你的数据长短不一致时比如文本句子长度不同、目标检测中每张图片物体数量不同才经常需要自定义 collate_fn。pin_memory这个参数主要和 GPU 加速有关我安装的是CPU 版 PyTorch不需要改成Truedrop_last这个参数表示最后一个 batch 如果不够 batch_size要不要丢掉在普通训练和测试中我们通常不想丢数据所以保持默认当然我觉得可以在设置size的时候就调整好timeout参数表示加载一个 batch 时最多等待多久。0表示不特别设置超时时间worker_init_fn这个参数和num_workers有关如果你用了多个子进程加载数据有时希望每个子进程初始化时做一些特殊设置比如设置随机种子就可以用它multiprocessing_context这个参数也是和多进程加载数据有关只有在你明确需要控制多进程启动方式时才会设置generator这个参数通常用于控制随机性比如你想让shuffleTrue的打乱结果可以复现就可以传入一个随机数生成器。prefetch_factor预加载因子每个 worker 提前加载几批数据。默认 2 表示提前准备 2 批。数据加载很慢时可以增大到 4 或 8。但配合num_workers0才有效。persistent_workers持久化子进程每个 epoch 结束后子进程是否销毁如果设置为 True多个 worker 在一个 epoch 结束后不会马上关闭可以减少下一轮重新启动 worker 的开销通常用于大数据集训练。pin_memory_device这个参数也是和内存固定、设备传输有关用 CPU 版 PyTorch不需要设置。数据导入完成之后取一个批次查看数据格式数据的shape为[batch_size, channel, height, weight]batch_size为自己设定channelheight和weight分别是图片的通道数高度和宽度。imgs,labelsnext(iter(train_dl))imgs.shapetorch.Size([32, 1, 28, 28])next(iter(train_dl))意思是从训练数据加载器中取出第一批数据。我们设置了batch_size 32DataLoader 每次取出的数据包含两部分图片数据标签数据所以会有32 张图片 32 个标签。torch.Size([32, 1, 28, 28])↑ ↑ ↑ ↑N C H W│ │ │ └── 宽度28 像素│ │ └────── 高度28 像素│ └────────── 通道数1灰度图黑白└─────────────── 批次大小这一捆有 32 张图4. 数据可视化这一步主要是为了查看数据是否正确加载以及理解 imgs 的图片格式。importnumpyasnp# 指定图片大小图像大小为20宽、5高的绘图(单位为英寸inch)plt.figure(figsize(20,5))fori,imgsinenumerate(imgs[:20]):# 维度缩减npimgnp.squeeze(imgs.numpy())# 将整个figure分成2行10列绘制第i1个子图。plt.subplot(2,10,i1)plt.imshow(npimg,cmapplt.cm.binary)plt.axis(off)#plt.show() 如果你使用的是Pycharm编译器请加上这行代码这一段的意思是从一个 batch 中取出前 20 张图片将每张图片从 Tensor 转换成 NumPy 数组并去掉多余维度最后按照2行 × 10列的形式显示出来。plt.figure(figsize(20, 5))这句代码用于创建一个画布。表示画布宽度是20英寸高度是5英寸。for i, imgs in enumerate(imgs[:20])这一句表示从当前 batch 中取出前 20 张图片并且逐张进行绘制。前面通过imgs,labelsnext(iter(train_dl))取出的imgs形状是[32, 1, 28, 28]也就是说当前 batch 中一共有 32 张图片。因此imgs[:20]表示只取前 20 张图片用于显示imgs.numpy()表示把 PyTorch 的 Tensor 转换成 NumPy 数组np.squeeze()表示去掉多余的维度。经过ToTensor()转换后形状是[1, 28, 28]其中1表示灰度图的通道数但是plt.imshow()显示灰度图时一般需要的形状是[28, 28]所以np.squeeze(imgs.numpy())会把图片形状从[1, 28, 28]变成[28, 28]这样才能更方便地用matplotlib显示。plt.subplot(2, 10, i1)表示把整个画布分成2 行 × 10 列一共可以放 20 张小图。其中i 1因为 Python 中i从 0 开始但是subplot()中的位置编号从 1 开始所以这里要写i1。plt.imshow(npimg, cmapplt.cm.binary)这一句用于真正显示图片。npimg是已经处理好的单张图片数组cmapplt.cm.binary表示用黑白灰度方式显示图片。MNIST 本身就是灰度图所以用黑白显示最合适。至此全部准备工作完成啦~对小白来说真的不容易啊二、构建简单的CNN网络对于一般的 CNN 网络来说通常可以分为两部分特征提取网络分类网络。特征提取网络主要负责从图片中提取特征比如边缘、形状、局部结构等分类网络则根据提取到的特征判断图片属于哪一类。对于 MNIST 手写数字识别来说输入是一张28 × 28的灰度图输出是0-9中的一个类别所以最后的输出类别数是num_classes 101. CNN 网络中的常用层1.1 卷积层nn.Conv2d卷积层用于提取图片的局部特征。比如在手写数字识别中卷积层可以帮助模型识别笔画、弯曲、边缘等特征。例如self.conv1nn.Conv2d(1,32,kernel_size3)这一句的意思是输入通道数1 输出通道数32 卷积核大小3 × 3因为 MNIST 是灰度图所以输入通道数是1输出通道数是32表示这一层会提取出 32 个不同的特征图。1.2 池化层nn.MaxPool2d池化层用于下采样也就是缩小特征图的尺寸同时保留主要特征。例如self.pool1nn.MaxPool2d(2)表示使用2 × 2的池化窗口池化可以减少计算量也能让模型更关注重要特征。1.3 激活函数ReLUReLU 是常用的激活函数它可以增强神经网络的非线性表达能力如果没有激活函数神经网络的表达能力会受到很大限制。1.4 全连接层nn.Linear全连接层主要用于分类前面的卷积层和池化层提取了图片特征后面的全连接层根据这些特征进行判断最后输出属于 10 个数字类别的分数。例如self.fc2nn.Linear(64,num_classes)表示最后输出10个类别分数。1.5 展平操作torch.flatten()卷积层输出的数据通常是四维的[batch_size, channel, height, width]但是全连接层需要的是二维数据[batch_size, features]所以在进入全连接层之前需要将特征图展平成一维向量。2. 定义 CNN 模型importtorch.nn.functionalasF num_classes10# 图片的类别数classModel(nn.Module):def__init__(self):super().__init__()# 特征提取网络self.conv1nn.Conv2d(1,32,kernel_size3)# 第一层卷积输入1通道输出32通道卷积核大小3×3self.pool1nn.MaxPool2d(2)# 第一层池化池化核大小2×2self.conv2nn.Conv2d(32,64,kernel_size3)# 第二层卷积输入32通道输出64通道卷积核大小3×3self.pool2nn.MaxPool2d(2)# 分类网络self.fc1nn.Linear(1600,64)self.fc2nn.Linear(64,num_classes)# 前向传播defforward(self,x):xself.pool1(F.relu(self.conv1(x)))xself.pool2(F.relu(self.conv2(x)))xtorch.flatten(x,start_dim1)xF.relu(self.fc1(x))xself.fc2(x)returnx这表示我们定义了一个模型类名字叫Model它继承自nn.Module在 PyTorch 中只要自己定义神经网络模型一般都要继承nn.Module。第一层卷积与池化self.conv1nn.Conv2d(1,32,kernel_size3)self.pool1nn.MaxPool2d(2)输入图片形状为[32, 1, 28, 28]经过第一层卷积通道数从1变成32因为卷积核大小是3 × 3默认没有 padding所以图片边缘会缩小尺寸从28 × 28变成26 × 26经过2 × 2最大池化后尺寸变成13 × 13所以第一轮之后特征图大致变成[32, 32, 13, 13]第二层卷积与池化self.conv2nn.Conv2d(32,64,kernel_size3)self.pool2nn.MaxPool2d(2)第二层卷积输入通道数是32输出通道数是64。尺寸变化大致是13 × 13 → 卷积后 11 × 11 → 池化后 5 × 5所以第二轮之后特征图变成[32, 64, 5, 5]其中32batch_size、64特征图通道数5 高度、5 宽度4. 加载并打印模型结构fromtorchinfoimportsummary modelModel().to(device)summary(model) Layer (type:depth-idx) Param # Model -- ├─Conv2d: 1-1 320 ├─MaxPool2d: 1-2 -- ├─Conv2d: 1-3 18,496 ├─MaxPool2d: 1-4 -- ├─Linear: 1-5 102,464 ├─Linear: 1-6 650 Total params: 121,930 Trainable params: 121,930 Non-trainable params: 0 前面我们设置了devicetorch.device(cudaiftorch.cuda.is_available()elsecpu)因为我安装的是 CPU 版 PyTorch所以这里模型会被放到 CPU 上运行这个模型总共有121,930个可训练参数。三、训练模型模型定义好之后就可以开始训练了。1. 设置超参数loss_fnnn.CrossEntropyLoss()# 创建损失函数learn_rate1e-2# 学习率opttorch.optim.SGD(model.parameters(),lrlearn_rate)CrossEntropyLoss损失函数用于衡量模型预测结果和真实标签之间的差距。对于 MNIST 这种多分类任务常用nn.CrossEntropyLoss()因为 MNIST 有 10 个类别属于典型的多分类问题损失值越小说明模型预测越接近真实标签。learn_rate学习率控制模型参数每次更新的步子大小1e-2等于0.01如果学习率太大模型可能训练不稳定如果学习率太小模型学习速度会很慢。SGD优化器的作用是根据损失函数计算出来的梯度更新模型参数。这里使用的是SGD也就是随机梯度下降。model.parameters()表示把模型中所有需要训练的参数交给优化器。2. 编写训练函数# 训练循环deftrain(dataloader,model,loss_fn,optimizer):sizelen(dataloader.dataset)# 训练集的大小一共60000张图片num_batcheslen(dataloader)# 批次数目187560000/32train_loss,train_acc0,0# 初始化训练损失和正确率forX,yindataloader:# 获取图片及其标签X,yX.to(device),y.to(device)# 计算预测误差predmodel(X)# 网络输出lossloss_fn(pred,y)# 计算网络输出和真实值之间的差距targets为真实值计算二者差值即为损失# 反向传播optimizer.zero_grad()# grad属性归零loss.backward()# 反向传播optimizer.step()# 每一步自动更新# 记录acc与losstrain_acc(pred.argmax(1)y).type(torch.float).sum().item()train_lossloss.item()train_acc/size train_loss/num_batchesreturntrain_acc,train_losssize len(dataloader.dataset)表示训练集的总样本数量对于 MNIST 训练集来说size 60000num_batches len(dataloader)表示一共有多少个 batch。optimizer.zero_grad() loss.backward() optimizer.step()这是 PyTorch 训练神经网络最核心的三步。第一步清空梯度PyTorch 中梯度默认会累加所以每个 batch 开始训练前需要先把上一轮的梯度清空。第二步反向传播根据损失值自动计算每个参数的梯度。第三步更新参数优化器根据梯度更新模型参数。3. 编写测试函数deftest(dataloader,model,loss_fn):sizelen(dataloader.dataset)# 测试集的大小一共10000张图片num_batcheslen(dataloader)# 批次数目31310000/32312.5向上取整test_loss,test_acc0,0# 当不进行训练时停止梯度更新节省计算内存消耗withtorch.no_grad():forimgs,targetindataloader:imgs,targetimgs.to(device),target.to(device)# 计算losstarget_predmodel(imgs)lossloss_fn(target_pred,target)test_lossloss.item()test_acc(target_pred.argmax(1)target).type(torch.float).sum().item()test_acc/size test_loss/num_batchesreturntest_acc,test_loss测试函数和训练函数很像但是有两个区别测试时不更新模型参数测试时使用torch.no_grad()关闭梯度计算。4. 正式训练epochs5train_loss[]train_acc[]test_loss[]test_acc[]forepochinrange(epochs):model.train()epoch_train_acc,epoch_train_losstrain(train_dl,model,loss_fn,opt)model.eval()epoch_test_acc,epoch_test_losstest(test_dl,model,loss_fn)train_acc.append(epoch_train_acc)train_loss.append(epoch_train_loss)test_acc.append(epoch_test_acc)test_loss.append(epoch_test_loss)template(Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%Test_loss:{:.3f})print(template.format(epoch1,epoch_train_acc*100,epoch_train_loss,epoch_test_acc*100,epoch_test_loss))print(Done)输出结果如下Epoch: 1, Train_acc:75.0%, Train_loss:0.812, Test_acc:91.3%Test_loss:0.279 Epoch: 2, Train_acc:93.9%, Train_loss:0.203, Test_acc:95.6%Test_loss:0.135 Epoch: 3, Train_acc:96.2%, Train_loss:0.127, Test_acc:97.1%Test_loss:0.095 Epoch: 4, Train_acc:97.1%, Train_loss:0.097, Test_acc:97.5%Test_loss:0.079 Epoch: 5, Train_acc:97.5%, Train_loss:0.082, Test_acc:98.1%Test_loss:0.062 Done一般来说训练过程中准确率应该逐渐升高损失值应该逐渐降低如果出现这种趋势就说明模型正在学习。四、结果可视化训练结束后可以把训练准确率、测试准确率、训练损失、测试损失画出来这样更直观地观察模型训练效果。importmatplotlib.pyplotasplt#隐藏警告importwarnings warnings.filterwarnings(ignore)#忽略警告信息plt.rcParams[font.sans-serif][SimHei]# 用来正常显示中文标签plt.rcParams[axes.unicode_minus]False# 用来正常显示负号plt.rcParams[figure.dpi]100#分辨率fromdatetimeimportdatetime current_timedatetime.now()# 获取当前时间epochs_rangerange(epochs)plt.figure(figsize(12,3))plt.subplot(1,2,1)plt.plot(epochs_range,train_acc,labelTraining Accuracy)plt.plot(epochs_range,test_acc,labelTest Accuracy)plt.legend(loclower right)plt.title(Training and Validation Accuracy)plt.xlabel(current_time)# 打卡请带上时间戳否则代码截图无效plt.subplot(1,2,2)plt.plot(epochs_range,train_loss,labelTraining Loss)plt.plot(epochs_range,test_loss,labelTest Loss)plt.legend(locupper right)plt.title(Training and Validation Loss)plt.show()这是我的结果总结这篇笔记对我来说主要是完成了从“数据导入 → 模型构建 → 模型训练 → 结果可视化”的完整流程。但学习并不完全下一周需要再一次深化学习后边的内容不太能看懂只是运行了代码后期学习后会再优化笔记。