很多人学深度学习卡在同一个地方:理论书翻了几十页,卷积、池化、反向传播都能说个大概,但真让自己从头跑通一个模型,从数据加载到训练循环再到推理验证,中间总有几个环节接不上。我当初也是这样,看了一堆公式,结果连DataLoader返回的batch怎么喂进网络都要查半天。这篇就围绕PyTorch官方那个最经典的LeNet演示项目,把从零跑通第一个卷积网络的完整链路拆开讲一遍。LeNet是个特别适合入门的模型——结构不复杂,两层卷积加三层全连接,但它把卷积神经网络该有的核心组件全包含了,跑通它之后,你再去看ResNet、VGG这些,会发现骨架逻辑是相通的。下面我会按实际动手的顺序,把环境准备、数据管道、模型定义、训练循环、推理验证这几个环节逐个说透,中间穿插我自己踩过的坑和几个容易被教程忽略的细节。
1. 为什么第一个Demo选LeNet而不是别的模型
1.1 LeNet在深度学习入门中的定位
LeNet诞生于上世纪九十年代,最初是为手写数字识别设计的。放到今天看,它的参数量小得可怜,在复杂任务上几乎没有实用价值,但作为学习载体,它的价值恰恰在于"小"。一个模型越小,你越容易在脑子里把它的数据流完整走一遍:一张28×28的灰度图进来,经过第一层卷积变成什么形状,池化之后又变成什么形状,展平后接全连接层时那个维度是怎么算出来的——这些在LeNet里都能手算验证。而如果你一上来就搞一个几十层的网络,光是张量形状的追踪就够你晕的,出了问题根本不知道是哪一层的事。
我在带新手的时候有个固定建议:第一个模型一定要小到你能完全掌控。LeNet的卷积核是5×5,通道数从1到6再到16,全连接层是120、84、10,这些数字都不大,你甚至可以在纸上把每一层的输出尺寸算一遍。这种"可手算"的特性对建立直觉极其重要。等你对数据在网络里的流动有了肌肉记忆,再去理解BatchNorm、残差连接这些进阶组件,才不会觉得是在背天书。
另外LeNet的另一个好处是它对应的问题足够简单——手写数字识别,10个类别,输入是单通道灰度图。这意味着你在调试的时候,可以很直观地判断模型是不是在学:训练几个epoch之后准确率能不能上去,如果上不去,问题大概率出在数据管道或者训练循环,而不是模型结构太复杂导致的玄学问题。这种"问题可定位"的特性,对新手排查bug非常友好。
1.2 官方Demo的代码组织方式与学习价值
PyTorch官方提供的这个LeNet演示,代码组织得相当克制。它没有用Lightning之类的封装框架,也没有搞一堆配置文件,就是把数据加载、模型定义、训练、测试这几块平铺直叙地写出来。这种"裸写"的方式对学习其实是最好的——你能清楚看到每一个环节在做什么,没有黑盒。很多新手喜欢一上来就用高级封装,结果模型跑通了却不知道内部发生了什么,换个场景就抓瞎。
官方Demo里有个细节值得注意:它把模型定义成一个继承nn.Module的类,在__init__里声明各层,在forward里定义前向传播的顺序。这个模式是所有PyTorch模型的标准写法,你后面写任何复杂网络都是这个套路。所以跑通这个Demo,你实际上是在建立一套可复用的代码骨架。我在实际项目里,哪怕是写一个很复杂的多模态网络,__init__加forward这个结构也从来没变过,变的只是里面堆什么层。
还有一点,官方Demo用的是torchvision.datasets里现成的数据集加载接口,配合transforms做预处理。这套数据管道的写法是PyTorch生态里最通用的,你换成自己的数据集,无非是把Dataset类换掉,DataLoader的用法完全一样。所以这个Demo虽然简单,但它覆盖的每一个环节都是真实项目里会用到的,不是那种"教学专用、实际没用"的玩具代码。
2. 环境准备:那些教程里不写的细节
2.1 版本匹配是第一个隐形坑
装PyTorch这件事,看起来就是一行pip install,但实际踩坑的人非常多。核心问题在于版本匹配:PyTorch的版本、CUDA的版本、Python的版本、显卡驱动的版本,这四者之间有一个兼容矩阵。我见过太多人装完之后torch.cuda.is_available()返回False,然后开始怀疑人生,其实多半是版本对不上。
我的建议是,装之前先去PyTorch官网的安装页面,根据你的显卡驱动版本选对应的CUDA版本,然后它会直接给你生成安装命令。不要自己凭记忆拼命令,也不要随便找个博客抄一行了事——博客的时效性很差,半年前的命令可能已经过时了。如果你没有独立显卡,那就装CPU版本,命令里带cpu后缀,虽然训练慢一点,但跑LeNet这种小模型完全够用,一个epoch在CPU上也就几十秒的事。
还有一个细节:如果你用conda管理环境,建议单独建一个虚拟环境,不要装在base环境里。原因是PyTorch的依赖比较重,装在base里以后装别的包容易冲突。建环境的命令很简单:
conda create -n dl-demo python=3.10 conda activate dl-demoPython版本我建议选3.9到3.11之间的,太老的版本可能不支持新版PyTorch,太新的版本有些依赖包还没跟上。装完之后验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available())如果第二行输出True,说明GPU可用;输出False但你确实有N卡,那就回去检查版本匹配。这一步没搞定之前,别往下走,不然后面训练报的错会让你误以为是代码问题。
2.2 数据集下载失败的几种应对
官方Demo用的是MNIST数据集,代码里通过datasets.MNIST加载,第一次运行会自动下载。但下载这一步在国内网络环境下经常出问题——要么卡住不动,要么报连接超时。这不是代码的错,是数据源在境外。
应对办法有几个。最省事的是提前把数据集文件下好,放到指定的目录里,然后加载时把download参数设成False。MNIST的文件不大,训练集加测试集一共也就几十兆,手动下载完全可行。放的位置要注意,datasets.MNIST的第一个参数是root,你把它设成哪个目录,它就会在那个目录下找MNIST/raw子目录里的文件。所以你要保证文件放在root/MNIST/raw/下面,文件名也要对得上,否则它还是会尝试重新下载。
另一个办法是换数据源。torchvision的数据集类一般支持通过参数指定镜像地址,不过这个要看具体版本的支持情况。我个人的习惯是第一次运行时盯着终端,如果发现下载卡住超过一两分钟,就直接中断,改用手动下载的方式,别干等。等数据集下好之后,后面每次运行都是秒加载,因为DataLoader会直接从本地读。
提示:数据集下载失败时,先确认是网络问题还是路径问题。如果终端显示正在下载但进度条不动,基本是网络;如果直接报文件找不到,那是路径没放对。两种情况处理方式完全不同,别混为一谈。
3. 数据管道:从原始图片到网络输入
3.1 transforms到底做了什么
transforms这个模块是很多人第一遍看Demo时容易一带而过的地方,但它其实是数据管道里最关键的一环。官方Demo里通常会有这么两行:
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])第一行ToTensor()把PIL图片或者numpy数组转成PyTorch的张量,同时做了一个很重要的操作:把像素值从0到255缩放到0到1之间,并且把维度从(H, W, C)调整成(C, H, W)。这个维度顺序的调整是PyTorch的约定,卷积层要求的输入就是通道在前。很多人自己写数据加载时忘了这一步,结果网络报维度不匹配的错,排查半天。
第二行Normalize做的是标准化,把数据按均值和标准差做归一化。那两个数字0.1307和0.3081是MNIST训练集统计出来的全局均值和标准差。为什么要做这一步?因为神经网络对输入的尺度比较敏感,如果输入值都挤在0到1之间且分布不均,梯度更新会不稳定。标准化之后数据变成零均值、单位方差附近,训练收敛会明显更顺。这两个数字不是随便写的,是针对MNIST这个特定数据集算出来的,你换数据集就得重新算。
这里有个新手常犯的错:在测试集上也用训练集的均值和标准差做标准化,这是对的;但有人会分别在训练集和测试集上各算各的均值方差,这就错了。标准化的原则是用训练集的统计量去处理所有数据,包括验证集和测试集,因为在实际部署时你面对的是新数据,不可能先统计一遍再推理。
3.2 DataLoader的batch与shuffle策略
DataLoader负责把数据集包装成可迭代的batch。Demo里一般会设batch_size=64、shuffle=True(训练集)。这两个参数的选择有讲究。
batch_size决定了每次喂给网络多少样本。太小的话,梯度更新频繁但噪声大,训练不稳定;太大的话,显存吃紧且泛化可能变差。64是个很中庸的选择,对LeNet这种小模型来说,显存占用可以忽略,训练速度也合适。如果你机器比较弱,可以降到32甚至16,效果差别不大,只是训练慢一点。
shuffle=True只在训练集上设,作用是每个epoch开始前打乱样本顺序。这个操作的意义在于打破样本之间的固定顺序,避免模型学到"顺序"这种无关信息。测试集上要设shuffle=False,因为测试时我们只关心预测结果,不需要打乱,而且保持顺序方便你对照样本和预测。
还有一个参数num_workers,Demo里可能没显式写,默认是0,意思是主进程加载数据。如果你发现GPU利用率很低、训练时GPU经常在等数据,可以把它调大,比如设成4,让多个子进程并行加载。不过在Windows系统上,num_workers大于0有时会有问题,需要把数据加载的代码放在if __name__ == '__main__':保护块里,这个坑我踩过,报的错很隐晦,排查起来费劲。
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False, num_workers=0)测试集的batch_size可以设大一点,因为测试时不需要反向传播,显存占用小,一次多处理一些样本能加快评估速度。
4. 模型定义:把LeNet的结构拆到每一层
4.1 卷积层与池化层的尺寸计算
LeNet的结构是这样的:输入1×28×28,第一层卷积用6个5×5的核,输出6×28×28(这里官方Demo通常做了padding=2,所以尺寸不变),然后2×2池化变成6×14×14;第二层卷积用16个5×5的核,输出16×10×10,再池化变成16×5×5;然后展平成400维,接全连接层120、84,最后输出10。
这里最容易卡住的是展平后的维度怎么算。16×5×5=400,这个400不是拍脑袋来的,是前面每一层尺寸变化累积的结果。我把计算过程列一下,你对照着看:
| 层 | 操作 | 输入尺寸 | 输出尺寸 |
|---|---|---|---|
| 输入 | - | - | 1×28×28 |
| conv1 | 5×5卷积,6通道,padding=2 | 1×28×28 | 6×28×28 |
| pool1 | 2×2最大池化 | 6×28×28 | 6×14×14 |
| conv2 | 5×5卷积,16通道 | 6×14×14 | 16×10×10 |
| pool2 | 2×2最大池化 | 16×10×10 | 16×5×5 |
| flatten | 展平 | 16×5×5 | 400 |
| fc1 | 全连接 | 400 | 120 |
| fc2 | 全连接 | 120 | 84 |
| fc3 | 全连接 | 84 | 10 |
卷积输出尺寸的通用公式是:输出 = (输入 - 核大小 + 2×padding) / stride + 1。以conv1为例,输入28,核5,padding=2,stride=1,代入得(28 - 5 + 4)/1 + 1 = 28,所以尺寸不变。conv2没有padding,输入14,核5,得(14 - 5)/1 + 1 = 10。池化层一般就是尺寸减半,2×2池化把28变14、14变7,但这里conv2输出是10,池化后是5,因为10除以2正好是5。
这个计算你一定要自己动手算一遍,别光看。因为一旦你改了网络结构,比如把卷积核换成3×3,或者加一层,展平维度就变了,如果fc1的输入维度没跟着改,运行时会报维度不匹配的错。这个错很常见,报错信息会告诉你期望多少实际多少,对着公式一算就能定位。
4.2 forward函数里的数据流
模型类的forward方法定义了数据怎么流过这些层。官方Demo的写法大致是:
def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool1(x) x = F.relu(self.conv2(x)) x = self.pool2(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这里有几个点值得说。第一,激活函数用的是F.relu,直接调用函数式接口,而不是在__init__里声明nn.ReLU()。两种写法效果一样,函数式更简洁,声明式更适合需要保存状态的层(比如Dropout在训练和推理时行为不同)。第二,x.view(x.size(0), -1)这一步是展平,x.size(0)是batch大小,-1让PyTorch自动推断剩下的维度。用-1的好处是你不用手写400这个数字,改了前面结构它也能自适应,减少出错。第三,最后一层fc3后面没有接激活函数,因为这是分类任务的输出层,后面会接交叉熵损失,而交叉熵内部已经包含了softmax,不需要额外加。
还有一个容易忽略的点:forward里没有显式处理batch维度。实际上输入x的形状是(batch, 1, 28, 28),卷积层会自动对每个样本独立处理,输出还是带batch维的。展平时x.size(0)取的就是batch大小,所以展平后是(batch, 400)。如果你自己写的时候忘了batch维,直接对一个(1, 28, 28)的张量做view(-1),会得到400维的向量,但网络期望的是带batch的,就会报错。所以调试时打印一下张量形状是个好习惯。
5. 训练循环:每一步在干什么
5.1 损失函数与优化器的选择逻辑
分类任务用交叉熵损失nn.CrossEntropyLoss(),这个几乎是默认选择。它内部做了两件事:先对网络输出做softmax转成概率,再算负对数似然。所以你的模型输出层不需要自己加softmax,加了反而错。优化器Demo里一般用SGD,学习率设0.01或者0.001,动量可以加也可以不加。
为什么用SGD而不是Adam?对LeNet这种小模型、MNIST这种简单任务,SGD完全够用,而且SGD的行为更直观,你能清楚看到学习率对收敛的影响。Adam虽然收敛快,但它有自适应学习率,新手不容易理解内部在干什么。我的建议是入门阶段先用SGD把流程跑通,理解清楚每个epoch loss怎么变,等熟练了再换Adam对比效果。学习率0.01对SGD来说偏大一点,如果发现loss震荡或者不降,可以降到0.001试试。
优化器初始化时要传入模型参数:optim.SGD(model.parameters(), lr=0.01)。这里model.parameters()返回的是模型里所有需要梯度的参数,优化器会负责更新它们。有个细节:如果你在模型里手动创建了一些不需要训练的参数,要确保它们没被混进去,不过LeNet这种标准结构不会有这个问题。
5.2 一个epoch的完整流程
训练循环的骨架是这样的:
for epoch in range(num_epochs): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step()这五行核心操作,每一行都有它的道理。optimizer.zero_grad()必须放在反向传播之前,作用是清空上一步累积的梯度。PyTorch默认会累积梯度,如果不清零,这次算的梯度会加到上次的上面,导致更新错误。这个坑新手极容易踩,表现是loss不降反升或者剧烈震荡。loss.backward()计算梯度,optimizer.step()根据梯度更新参数。顺序不能乱。
model.train()和后面的model.eval()是切换模式用的。LeNet里没有Dropout和BatchNorm,所以这两个调用其实不影响结果,但养成习惯很重要——等你以后用带Dropout的模型,忘了切eval模式会导致推理结果不稳定。model.train()告诉网络当前是训练模式,model.eval()告诉它是评估模式。
设备转移data.to(device)这一步,如果你用GPU训练,数据和模型必须在同一个设备上,否则报错。device一般定义成torch.device('cuda' if torch.cuda.is_available() else 'cpu'),模型也要model.to(device)。这个写法是标准套路,照抄就行。
5.3 训练过程中的监控指标
光看loss不够,还要看准确率。Demo里通常在每个epoch结束后跑一遍测试集,算一下准确率。计算准确率的逻辑是:网络输出是10维的logits,取argmax得到预测类别,和真实标签比较,统计正确的数量除以总数。
correct = 0 total = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) pred = output.argmax(dim=1) correct += (pred == target).sum().item() total += target.size(0)torch.no_grad()这个上下文管理器很重要,它告诉PyTorch评估阶段不需要计算梯度,能省显存也能加速。评估时忘了加它,虽然结果没错,但会白白消耗资源,数据量大时可能爆显存。argmax(dim=1)是在类别维度上取最大值的位置,得到的就是预测类别。
我个人的习惯是每个epoch打印一行:epoch编号、平均loss、测试准确率。这样能直观看到训练趋势。正常情况下,LeNet在MNIST上跑几个epoch准确率就能到98%以上,如果跑了五六个epoch还在90%以下,那肯定有问题,要么数据没标准化,要么学习率不对,要么训练循环写错了。
6. 推理验证:模型训完之后怎么用
6.1 保存与加载模型的正确姿势
训练完的模型要保存下来,否则下次还得重训。PyTorch有两种保存方式:保存整个模型对象,或者只保存参数字典。推荐后者,因为保存整个对象依赖代码结构,换个环境或者改了类定义就可能加载失败。保存参数的写法是:
torch.save(model.state_dict(), 'lenet.pth')加载时先实例化一个同结构的模型,再加载参数:
model = LeNet() model.load_state_dict(torch.load('lenet.pth')) model.eval()这里model.eval()不能省,虽然LeNet没影响,但这是推理前的标准动作。加载完参数后,模型就恢复到了训练好的状态,可以直接拿来预测。
有个细节:torch.load在新版本里有个weights_only参数,默认行为在不同版本间有变化,如果加载时报安全相关的警告,可以显式指定weights_only=True。这个参数的作用是限制加载的内容只能是张量等安全类型,防止加载恶意构造的文件。自己训练的模型放心用,但养成指定它的习惯没坏处。
6.2 单张图片的预测流程
实际用的时候,你往往是拿一张新图片来预测,而不是整个测试集。单张预测的流程和批量预测基本一样,但要注意几个地方。首先图片要经过和训练时完全相同的预处理:转灰度、缩放到28×28、转张量、标准化。任何一步不一致,预测结果都会偏。其次要手动加一个batch维度,因为网络期望输入是(batch, C, H, W),单张图是(C, H, W),用unsqueeze(0)在前面加一维。
img = transform(img).unsqueeze(0).to(device) with torch.no_grad(): output = model(img) pred = output.argmax(dim=1).item()item()把单元素张量转成Python数字,方便打印。预测出来的pred就是0到9之间的一个数字,对应识别结果。
这里有个实战经验:如果你拿自己手写的数字图片去测,准确率可能没有测试集上那么高。原因是MNIST的图片是经过居中、归一化处理的,你自己拍的图片光照、大小、位置都不一样。这不是模型的问题,是数据分布不一致。解决办法是在预处理时做中心化和尺寸对齐,尽量让输入接近训练数据的分布。这个道理放到任何实际项目里都成立——推理时的预处理必须和训练时严格一致,这是模型能正常工作的前提。
6.3 用测试集做最终评估的注意事项
最后用测试集跑一遍完整评估,得到最终的准确率。这一步要注意测试集不能参与任何训练相关的决策,包括调学习率、选模型。如果你用测试集调参,那测试集就不再是"未见过的数据",评估结果会偏乐观。正确的做法是划一个验证集用来调参,测试集只在最后用一次。MNIST的官方划分里训练集和测试集是分开的,Demo直接用就行,但你要有这个意识,以后做真实项目时别把测试集当验证集用。
评估代码和训练中的监控代码一样,只是最后把准确率算出来打印。LeNet在MNIST上正常能到98%到99%,如果你的结果明显低于这个,回去检查预处理和训练循环。这个数字可以作为一个基准,帮你判断自己的实现有没有问题。
7. 跑通之后值得做的几个小实验
7.1 改结构看效果变化
跑通Demo只是起点,真正学到东西是在你动手改的时候。我建议做几个对照实验:把卷积核从5×5改成3×3,看准确率和训练速度怎么变;把通道数从6和16改成8和32,看模型容量增加带来的影响;去掉某一层池化,看尺寸变化和最终效果。每改一次,你都要重新算展平维度,这个过程能让你对网络结构的理解从"背下来"变成"算出来"。
改结构时最容易出的错就是忘了改fc1的输入维度。比如你把第二层池化去掉,那展平维度就不是400了,得重新算。报错信息会明确告诉你期望多少实际多少,对着公式一算就能改对。这种"改-报错-修"的循环,其实是学习最高效的方式,比看十遍教程都管用。
7.2 调整超参数观察收敛
学习率、batch_size、epoch数这几个超参数,调一调能直观感受到它们的影响。学习率调大到0.1,你可能会看到loss震荡甚至发散;调小到0.0001,收敛会变得很慢。batch_size调到16,训练会变慢但每个epoch的更新次数变多。这些现象背后都有原因,你观察到了现象再去查原因,理解会深得多。
我个人的经验是,调参时一次只改一个变量,其他保持不变,这样才能判断出是哪个因素导致的变化。同时改好几个参数,效果好了你不知道是谁的功劳,效果差了也不知道该改哪个。这个原则在真实项目里同样适用,是排查问题的基本功。
7.3 把训练过程可视化
如果想让训练过程更直观,可以把每个epoch的loss和准确率存下来,用matplotlib画成曲线。这一步不是必须的,但画出来之后你能一眼看出模型是欠拟合还是过拟合:训练loss和测试loss都在降但测试降得慢,可能是过拟合;两个都不降,可能是欠拟合或者学习率不对。可视化是调试模型的重要手段,早点养成习惯有好处。
画图时注意把训练和测试的曲线画在同一张图上对比,横轴是epoch,纵轴是loss或准确率。如果两条曲线差距越来越大,说明模型在训练集上过拟合了,可以考虑加正则化或者减少模型容量。LeNet在MNIST上一般不会过拟合,因为模型够小、数据够多,但这个分析方法你要掌握,换到复杂任务上就用得上了。
8. 几个我踩过的坑和对应的排查思路
8.1 维度不匹配报错的定位方法
维度不匹配是新手遇到最多的错误,报错信息一般是RuntimeError: mat1 and mat2 shapes cannot be multiplied或者类似的。看到这个错,第一反应是去检查展平后的维度和fc1的输入维度对不对得上。定位方法是:在forward里展平那一步后面加一行print(x.shape),跑一次看实际输出多少,和fc1定义的输入维度对比。对不上就回去按公式重算前面每一层的尺寸。
这个错还有一种变体是卷积层报通道数不匹配,比如expected input to have 1 channel, but got 3。这通常是因为输入图片是彩色的(3通道),但模型第一层定义的是单通道。解决办法要么把图片转灰度,要么把模型第一层改成3通道。MNIST是灰度图,所以是单通道,如果你换成彩色数据集就要注意这个。
8.2 loss不降的几种可能原因
训练时loss一直不降或者降得很慢,可能的原因有好几个,要逐个排查。第一,学习率太大导致震荡,试着调小一个数量级。第二,忘了optimizer.zero_grad(),梯度累积导致更新错误。第三,数据预处理有问题,比如忘了标准化,或者标签和图片对不上。第四,模型定义有误,比如激活函数用错、层顺序颠倒。
排查顺序我一般是:先确认数据没问题(打印几个batch看看图片和标签对不对),再确认训练循环的五行核心操作顺序没错,最后调学习率。大部分情况是前两个原因,学习率的问题反而少见。数据问题最隐蔽,因为代码不报错,只是效果差,所以一定要养成打印检查数据的习惯。
8.3 GPU相关问题的快速判断
用GPU训练时常见的问题有两个:一是torch.cuda.is_available()返回False,二是运行时报设备不匹配的错。前者是环境问题,回去检查版本匹配;后者是数据和模型没放在同一设备上,检查是不是漏了某个.to(device)。还有一个隐蔽的坑是显存不足,报错是CUDA out of memory,解决办法是调小batch_size,或者用torch.cuda.empty_cache()清理缓存。
显存不足这个问题在LeNet上基本不会遇到,因为模型太小了,但你要知道这个错长什么样,以后跑大模型时能快速识别。另外,如果你在GPU上训练完想用CPU推理,加载模型时要加map_location='cpu',否则会报找不到CUDA设备的错。这个细节在跨设备部署时很关键。
9. 从LeNet往下走的路线建议
跑通LeNet之后,下一步该学什么?我的建议是先把卷积网络的几个核心概念吃透:感受野、padding和stride的作用、池化的意义、全连接层和卷积层的区别。这些概念在LeNet里都有体现,但LeNet太简单,体现得不够充分。你可以拿VGG或者ResNet的某个小变体来对照着看,重点看它们的结构设计为什么和LeNet不一样,解决了什么问题。
再往后就是数据增强、正则化、学习率调度这些训练技巧,以及迁移学习——拿预训练模型改造成自己的任务。这些内容在LeNet这个层面上还接触不到,但LeNet给你打下的基础是通用的:你知道了一个模型从定义到训练到推理的完整链路,后面学任何新东西都是在这个链路上加组件。所以别急着跳过LeNet去追新模型,把这个小Demo的每个环节都动手改一遍、跑一遍,收获比囫囵吞枣看十个大模型都大。
我自己带人的经验是,能在LeNet上把维度计算、训练循环、预处理一致性这几个点讲清楚的人,后面学复杂模型基本不会卡在基础上。反而是那些一上来就搞大模型、基础不牢的人,遇到问题往往不知道从哪查起。所以这个Demo值得你花时间慢慢磨,磨透了再往下走。