ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

PyTorch入门实战:手把手跑通LeNet官方Demo

PyTorch入门实战:手把手跑通LeNet官方Demo 深度学习入门这条路上很多人卡在同一个地方理论看了一堆反向传播能推卷积核尺寸会算但真让自己从头跑通一个能训练、能收敛、能出结果的模型手就生了。我当年也是这样书翻了三遍不如亲手把官方 Demo 敲一遍。这篇就围绕 PyTorch 官方那个最经典的 LeNet 示例展开把从零开始学深度学习的第三站讲透——为什么是 LeNet、这个 Demo 到底在做什么、每一行代码背后的意图是什么、跑通之后还能怎么改。不管你是刚装完 PyTorch 的新手还是想回头把基础打扎实的老手这篇都能给你一份可以直接照着复现的完整路径。核心关键词就三个PyTorch、LeNet、官方 Demo全文围绕它们把环境、数据、网络、训练、评估、调参、排错一条龙讲清楚。1. 为什么入门第一站选 LeNet 这个官方 Demo1.1 LeNet 在深度学习史上的位置决定了它的教学价值LeNet 是上世纪九十年代为手写数字识别设计的卷积神经网络结构极简两个卷积层、两个池化层、三个全连接层参数量不到十万。放到今天看这个规模连小模型都算不上但它的教学价值恰恰在于小。我见过太多新手一上来就啃 ResNet、Transformer结果被残差连接、多头注意力、位置编码这些概念砸晕连张量形状怎么变都没搞明白。LeNet 的好处是每一层的输入输出形状你都能手算出来卷积核 5×5、步长 1、填充 0池化 2×2、步长 2这些数字在纸上画一画就清楚了。PyTorch 官方把它作为入门 Demo逻辑很实在它覆盖了深度学习最核心的几个环节——数据加载、网络定义、前向传播、损失计算、反向传播、参数更新、模型评估一个都不少但每个环节都做到了最简。你跑通这一个 Demo等于把整个训练流程的骨架摸了一遍后面换更复杂的网络无非是往这个骨架上挂更复杂的模块。这就是我常跟人说的一句话LeNet 不是让你学会识别手写数字是让你学会一个模型是怎么被训练出来的。1.2 官方 Demo 相比网上教程的独特优势网上 LeNet 的教程一抓一大把为什么还要专门讲官方 Demo我踩过的坑告诉我第三方教程最大的问题是省略。作者为了篇幅把数据预处理简化了、把设备切换省了、把评估逻辑砍了你照着抄能跑但换个数据集就崩报错都不知道从哪查。官方 Demo 的价值在于它是完整可运行的数据下载、归一化、批处理、训练循环、测试评估、准确率统计每一块都在而且用的是 PyTorch 官方推荐的写法。更关键的是官方 Demo 的代码风格是教科书级的规范。比如它用torchvision.datasets和DataLoader的组合来管理数据用nn.Module的子类来定义网络用optim模块管理优化器这些写法是后面所有 PyTorch 项目的通用范式。你把这个 Demo 的每一行都吃透后面看别人的开源代码就不会有陌生感。我个人的经验是新手最该做的不是到处找更简单的教程而是把一个官方 Demo 反复改、反复跑改到你能预测每次修改的结果为止。1.3 这个 Demo 适合谁来学学到什么程度算过关先说适合谁。完全没碰过 PyTorch 的适合学过一点但没完整跑过训练流程的适合跑过别人的代码但说不清每行在干嘛的也适合。不太适合的是已经能独立写训练脚本、调过超参的老手对他们来说这个 Demo 太浅了。学到什么程度算过关我给自己定的标准是三条第一能不看代码凭记忆把网络结构写出来包括每层的输入输出通道数和卷积核尺寸第二能解释训练循环里zero_grad、backward、step这三步为什么是这个顺序调换会怎样第三能自己改一个地方——比如把激活函数从 ReLU 换成 Sigmoid或者把优化器从 SGD 换成 Adam——然后预测结果会怎么变再跑一遍验证。三条都做到了这个 Demo 才算真正吃透而不是跑通了。2. 环境准备与数据管线的搭建细节2.1 环境安装版本匹配比什么都重要装 PyTorch 这件事新手最容易犯的错是随便装个最新版。我见过有人装完发现torchvision和torch版本不匹配导入就报错折腾一下午。正确的做法是先确定 Python 版本再按官方给的组合装。以常见的 Python 3.9 到 3.11 为例torch和torchvision的版本必须对应比如 torch 2.x 系列要配对应版本的 torchvision具体对应关系在官方安装页面上有表格装之前一定对一眼。安装命令本身不复杂CPU 版本直接 pip 装就行有独立显卡的按官方给的索引地址装对应计算平台的版本。我这里不写具体命令因为版本迭代快写死了反而误导你去官方安装页面选好系统、包管理器、计算平台它会直接生成命令复制粘贴最稳。装完验证很简单import torch import torchvision print(torch.__version__) print(torchvision.__version__) print(torch.cuda.is_available())最后一行输出 True 说明能用显卡加速False 就是纯 CPU 跑。LeNet 这个规模CPU 跑也完全够MNIST 数据集六万张训练图CPU 上几分钟一轮别被必须用显卡吓住。提示装完先跑上面三行验证别急着写模型。环境问题在写代码前解决比写到一半报错再回头查要省事得多。2.2 MNIST 数据集为什么它是入门标配MNIST 是七万张 28×28 的灰度手写数字图六万训练、一万测试十个类别0 到 9。它成为入门标配不是偶然图像小、类别少、干净、标注准模型很容易就能跑到 98% 以上的准确率新手能快速获得正反馈。这一点很重要我见过太多人被复杂数据集虐到怀疑人生其实换个简单的数据集同样的代码立刻就能跑出漂亮结果问题不在你在数据。用torchvision.datasets.MNIST加载关键参数有三个root指定下载目录train区分训练集和测试集downloadTrue让它在本地没有数据时自动下载。这里有个细节很多人忽略训练集和测试集要分别实例化trainTrue和trainFalse各来一次别想着一个对象搞定。from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)2.3 归一化参数 0.1307 和 0.3081 是怎么来的上面代码里那两个数字不是随便写的它们是 MNIST 训练集的全局均值和标准差。ToTensor()会把像素值从 0 到 255 缩放到 0 到 1Normalize再按(x - mean) / std做标准化。为什么要标准化因为神经网络对输入尺度敏感输入值集中在 0 附近、方差为 1 时梯度下降收敛更稳。你可以自己算一遍验证把所有训练图拉平求均值和标准差结果就是这两个数。我建议新手真去算一次不是为了记住数字是为了理解归一化参数应该从训练集统计而不是拍脑袋定这个原则——测试集要用训练集的参数不能自己再算一遍否则就是数据泄露。2.4 DataLoader批处理与打乱的实操要点数据集对象只是数据在哪真正喂给模型的是DataLoader。它负责三件事按batch_size切批、每个 epoch 打乱顺序、多进程预取数据。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers0) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse, num_workers0)batch_size训练集用 64 是官方 Demo 的选择小批量能让梯度带点噪声反而有助于跳出局部极小。测试集用 1000 是因为测试不需要反向传播大批量一次算完更快。shuffleTrue只在训练集开测试集必须关否则评估结果没法复现。num_workers在 Windows 上建议设 0设大了容易出多进程相关的报错Linux 上可以设 2 到 4 加速数据加载。这些都是我实际踩过的坑官方文档不会专门提醒你 Windows 的这个问题。3. LeNet 网络结构的逐层拆解与实现3.1 从论文原图到代码每一层的形状变化LeNet 的结构用一句话概括卷积、池化、卷积、池化、展平、三个全连接。但光背这句话没用你得能算出每一层的张量形状。输入是[batch, 1, 28, 28]注意 MNIST 是单通道灰度图所以通道数是 1这是新手最容易写错的地方写成 3 就会报通道不匹配。第一层卷积Conv2d(1, 6, 5)输入 1 通道、输出 6 通道、卷积核 5×5、无填充。28 - 5 1 24所以输出是[batch, 6, 24, 24]。接着MaxPool2d(2, 2)24 / 2 12输出[batch, 6, 12, 12]。第二层卷积Conv2d(6, 16, 5)12 - 5 1 8输出[batch, 16, 8, 8]。再池化8 / 2 4输出[batch, 16, 4, 4]。展平后是 16 × 4 × 4 256 维接全连接Linear(256, 120)、Linear(120, 84)、Linear(84, 10)最后输出 10 个类别的分数。这套计算我建议你拿纸笔自己推一遍推完再对照代码比直接看代码印象深十倍。很多人卡在为什么展平是 256就是因为没算这一步。3.2 用 nn.Module 定义网络的规范写法PyTorch 定义网络的标准姿势是继承nn.Module在__init__里声明层在forward里定义数据流向。这个范式必须刻进肌肉记忆因为后面所有模型都这么写。import torch.nn as nn import torch.nn.functional as F class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 nn.Conv2d(1, 6, 5) self.pool1 nn.MaxPool2d(2, 2) self.conv2 nn.Conv2d(6, 16, 5) self.pool2 nn.MaxPool2d(2, 2) self.fc1 nn.Linear(16 * 4 * 4, 120) self.fc2 nn.Linear(120, 84) self.fc3 nn.Linear(84, 10) def forward(self, x): x self.pool1(F.relu(self.conv1(x))) x self.pool2(F.relu(self.conv2(x))) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) x self.fc3(x) return x几个细节值得说。super(LeNet, self).__init__()这行不能省它初始化父类省了会报错。x.view(x.size(0), -1)里的x.size(0)是 batch 维度-1让 PyTorch 自动算展平后的维度这样写比写死 256 更稳改网络结构时不容易漏改。最后一层fc3后面不接激活函数因为输出的是类别分数logits激活交给损失函数CrossEntropyLoss内部处理它自带 softmax。这一点新手常搞错在最后加个 softmax 再用交叉熵等于做了两次结果就偏了。3.3 激活函数为什么选 ReLU 而不是 SigmoidLeNet 原论文用的是 Sigmoid 或 tanh但现代实现基本都换成了 ReLU。原因很实际Sigmoid 在输入较大或较小时梯度接近 0深层网络容易出现梯度消失训练慢甚至训不动。ReLU 在正区间梯度恒为 1反向传播时梯度不容易衰减收敛快得多。我实测过同一个 LeNetSigmoid 版本跑五个 epoch 准确率还在 90% 附近晃ReLU 版本三个 epoch 就上 98% 了。不过 ReLU 也有坑负区间梯度为 0神经元可能死掉输入一直是负的话这个神经元就再也不更新了。LeNet 这么浅的网络基本不用担心但你要知道这个特性后面搭深网络时才会理解为什么会有 LeakyReLU、ELU 这些变体。3.4 参数量核算为什么这个网络轻到可以随便跑把每层参数量加起来conv1 是 1×6×5×5 6 156conv2 是 6×16×5×5 16 2416fc1 是 256×120 120 30840fc2 是 120×84 84 10164fc3 是 84×10 10 850。总计约 4.4 万参数。这个量级意味着什么意味着模型文件不到 200KB意味着 CPU 上训练一轮只要几十秒意味着你可以在笔记本上反复实验几十次而不用等。我特别建议新手在这个阶段多改多试因为成本低试错快正是建立直觉的黄金期。4. 训练循环的完整实现与关键步骤解析4.1 损失函数与优化器的选择逻辑分类任务用CrossEntropyLoss这是 PyTorch 里把 softmax 和负对数似然打包在一起的损失函数输入是 logits不需要你手动 softmax。优化器官方 Demo 用的是 SGD学习率 0.01动量 0.5。为什么用 SGD 不用 Adam不是 Adam 不好是 SGD 在这个简单任务上足够而且能让你看清学习率这个超参的作用。Adam 自适应调整学习率新手反而感受不到调参的乐趣和痛感。import torch.optim as optim model LeNet() criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.5)model.parameters()把网络里所有可训练参数交给优化器管理这一步别漏漏了优化器不知道要更新谁。动量 0.5 的作用是让更新方向带点惯性减少震荡加快收敛这个值在 LeNet 上是官方调好的你可以试着改成 0 或 0.9 感受差异。4.2 训练循环的三步核心zero_grad、backward、step训练循环的骨架就这几行但每一步的意图必须清楚def train(model, train_loader, criterion, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f})optimizer.zero_grad()必须放在最前面因为 PyTorch 的梯度是累加的不清零的话上一批的梯度会叠加到这一批结果全错。这是新手第一大坑我当年就是漏了这行loss 一直不降查了半天。loss.backward()做反向传播算出每个参数的梯度。optimizer.step()按梯度更新参数。顺序不能乱先清零、再前向、再算损失、再反向、最后更新。model.train()这行也别省它把网络切到训练模式影响 Dropout 和 BatchNorm 的行为。LeNet 里虽然没有这两层但养成习惯很重要后面加了就会感谢自己。4.3 测试评估为什么必须切 eval 模式并关闭梯度测试阶段和训练阶段写法不同核心是两点model.eval()和torch.no_grad()。def test(model, test_loader, criterion): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: output model(data) test_loss criterion(output, target).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader) accuracy 100. * correct / len(test_loader.dataset) print(fTest Loss: {test_loss:.4f}, Accuracy: {accuracy:.2f}%) return accuracymodel.eval()切评估模式和train()对应。torch.no_grad()关闭梯度计算测试不需要反向传播关掉能省显存、提速。output.argmax(dim1)取分数最高的类别作为预测pred.eq(target.view_as(pred))比较预测和真实标签sum().item()统计正确数。这套写法是标准范式建议直接背下来。4.4 完整训练流程串联与实测结果把上面拼起来主循环大概是这样for epoch in range(1, 6): train(model, train_loader, criterion, optimizer, epoch) test(model, test_loader, criterion)五个 epochCPU 上跑下来大概两三分钟。我实测的结果是第一个 epoch 结束准确率约 97%第三个 epoch 到 98.5%第五个 epoch 稳定在 98.8% 到 99% 之间。这个曲线很典型——前期涨得快后期趋于平缓。如果你跑出来第一个 epoch 只有 10% 左右等于随机猜那基本可以确定是zero_grad漏了或者学习率设错了。如果 loss 变成 nan多半是学习率太大降到 0.001 试试。注意训练前记得把模型和数据放到同一个设备上。纯 CPU 跑不用管用显卡的话要model.to(device)且每个 batch 的data、target也要.to(device)漏一个就报设备不匹配。5. 常见报错与调参排错实录5.1 新手最常撞的五个报错及解决思路我把这些年带人时见过的高频报错整理成表对照着查能省不少时间报错信息关键词常见原因解决方向expected scalar type Byte but found Float输入没转成 float 张量检查ToTensor()是否在 transform 里mat1 and mat2 shapes cannot be multiplied展平维度算错重新核算卷积池化后的尺寸Expected input batch_size to match target标签形状不对检查view_as或squeezeCUDA out of memory显存不够减小 batch_size 或换 CPUloss 一直是 nan学习率过大或数据异常降学习率、检查归一化第一个报错特别常见本质是数据类型不匹配ToTensor()会把 PIL 图像转成 0 到 1 的 float 张量忘了加就会拿到原始整数类型。第二个是形状问题LeNet 展平是 256你如果改了卷积层没同步改全连接层就会撞上。这些报错看着吓人其实都是形状或类型对不上养成打印x.shape的习惯问题一目了然。5.2 准确率上不去的排查顺序如果模型能跑但准确率卡在某个值上不去按这个顺序查先看训练集准确率如果训练集都上不去是模型或训练流程有问题如果训练集高、测试集低是过拟合如果两者都低是欠拟合。LeNet 在 MNIST 上正常应该轻松到 98% 以上到不了就逐项排查——学习率是不是太大导致震荡batch_size 是不是太小导致梯度噪声过大归一化是不是漏了epoch 是不是太少。我遇到过一次准确率死活卡在 90%最后发现是shuffle忘了开数据按类别顺序喂进去模型学偏了。这种问题不看代码根本想不到所以排查时要把数据管线也纳入检查范围别只盯着模型。5.3 几个能立刻提升效果的实操技巧第一个技巧是学习率衰减训练到后期把学习率降下来能让模型在最优解附近更精细地收敛。PyTorch 有lr_scheduler几行就能加上。第二个技巧是数据增强MNIST 上可以加轻微旋转、平移能提升泛化但对这个简单任务提升有限属于锦上添花。第三个技巧是保存和加载模型训练完用torch.save(model.state_dict(), lenet.pth)存参数下次model.load_state_dict(torch.load(lenet.pth))加载这是实际项目必备技能Demo 里没写但你必须会。提示保存模型存state_dict而不是整个模型对象前者只存参数、体积小、跨版本兼容好后者依赖类定义换环境容易加载失败。6. 从跑通到改通让这个 Demo 真正变成你的6.1 三个循序渐进的改造练习跑通只是起点真正学到东西靠改。我给自己安排的第一个练习是把激活函数换成 Sigmoid观察收敛速度的变化验证前面说的ReLU 更快。第二个练习是把优化器换成 Adam学习率设 0.001看准确率曲线是不是更平滑。第三个练习是改网络结构比如在 conv1 后加一个 BatchNorm 层或者把两个卷积核都改成 3×3然后重新核算展平维度。这三个练习做完你对改一处、动全身的理解会深很多。改造时有个原则一次只改一个变量改完跑一遍记录结果再改下一个。同时改好几处出了问题根本不知道是哪处引起的。这个习惯我从入门保持到现在做任何实验都遵守。6.2 把这个 Demo 迁移到自己的数据集上LeNet 跑通 MNIST 之后最有价值的下一步是换成自己的数据。迁移时要改的地方其实不多数据集类换成ImageFolder或自定义Dataset输入通道数按你的图改彩色图是 3全连接层第一层的输入维度按新的特征图尺寸重算输出类别数改成你的类别数。改完先在小样本上跑通确认形状都对再上全量数据。我特别想强调通道数这一处很多人拿彩色图直接喂给Conv2d(1, 6, 5)报错还找不到原因。记住网络第一层的输入通道必须和数据通道一致这是硬约束。6.3 我个人在这个 Demo 上踩过的坑和体会最后说点掏心窝的。我最早跑这个 Demo 时卡在x.view那行整整一个下午因为我不理解-1是什么意思硬写了个 256结果改了 batch_size 就报错。后来才明白-1是让 PyTorch 自己算这种让框架帮你算的思路贯穿整个 PyTorch 使用。还有一次我把optimizer.zero_grad()写在了loss.backward()后面loss 一直不降查了两小时才发现顺序错了。这些坑现在看很蠢但正是这些蠢坑让我真正记住了每一步的作用。我的体会是官方 Demo 的价值不在于它多完美而在于它是一个活的标本你可以随便解剖、随便改。跑通它只花你半小时但把它改到面目全非、每一处改动你都能预测结果这个过程才真正把知识变成你自己的。后面你再去学更复杂的网络会发现无非是在这个骨架上加模块而骨架你已经烂熟于心了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表