ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PyTorch实现五大经典CNN:从LeNet到ResNet的复现与实验对比

PyTorch实现五大经典CNN:从LeNet到ResNet的复现与实验对比 简介经典CNN架构的Pytorch实现LeNet、AlexNet、VGGNet、GoogLeNet、ResNet实验报告.zip 是一套面向计算机视觉入门与课设、毕设场景的完整代码包将五种经典卷积网络全部用Pytorch实现并按模型拆分文件便于单独学习与改造。压缩包共38个文件主要包括8个Python脚本、27张训练可视化图片、1份实验报告PDF及README说明整体仅3.48MB轻量易用。其中main.py为统一入口compare.py提供已调优参数的模型横向对比plot.py汇总画图功能配合详细实验报告能直观理解各架构在图像分类任务上的表现差异。目前已有69人学习下载。下载后即可获得可直接运行的工程、对比结果图表与理论分析文档适合在校学生、教师或开发者作为项目演示、课程作业乃至入门进阶的参考资料也支持在此代码基础上二次扩展完成其他视觉任务。代码经测试通过若运行中遇到问题可联系作者远程协助解答。1. 一套代码跑通五个经典CNN实验报告和复现细节都在这里做课程设计或者准备面试手撕网络的时候最尴尬的不是看不懂结构图而是光看理论以为自己会了真正把LeNet到ResNet的输入输出shape捋清楚、在PyTorch里跑出可对比的精度曲线才发现每一层参数设置都有讲究。这份资源把LeNet、AlexNet、VGGNet、GoogLeNet、ResNet五个模型拆成独立py文件配好主入口main.py、模型对比compare.py和画图脚本Plot.py适合两类人一是计科、人工智能、自动化专业的本科生直接拿去交课设或做毕设基线二是已经工作但想快速把经典CNN的pytorch实现差异对比清楚、准备迁移到自己数据集上的工程师。和网上那些只贴一个模型定义的博客不同这里每个模型单独成文件从数据加载到训练再到多模型精度对比整套闭环是完整的且实验报告PDF把参数选择逻辑写得很细省去自己重造轮子的时间。2. 从LeNet到VGGNet三套网络形态决定了参数量差两个数量级2.1 LeNet-5的Pytorch实现细节输入尺寸和padding的对应关系LeNet-5是所有CNN复现的第一课。原始论文输入是32×32单通道但MNIST默认是28×28这份代码里在数据预处理阶段做了Resize再进网络。核心结构是卷积层和池化层的交替叠加最后接全连接层做分类。# LeNet.py 核心结构 import torch.nn as nn class LeNet(nn.Module): def __init__(self, num_classes10): super(LeNet, self).__init__() self.features nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), # 28 - 28, 保持尺寸 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 28 - 14 nn.Conv2d(6, 16, kernel_size5), # 14 - 10 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 10 - 5 ) self.classifier nn.Sequential( nn.Linear(16 * 5 * 5, 120), nn.ReLU(inplaceTrue), nn.Linear(120, 84), nn.ReLU(inplaceTrue), nn.Linear(84, num_classes) ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x这里第一个卷积层用了padding2和原始LeNet-5在32×32输入上的逻辑保持一致。如果输入是28×28且不加padding第一个卷积输出会是24×24到全连接层时特征图尺寸全变了代码直接报错。torch.nn.functional.conv2d的尺寸计算公式是(W - kernel_size 2 * padding) / stride 1复现时所有形状推算都基于这个公式。第二个卷积层不加padding是刻意让尺寸从14降到10作者保留了原始设计而不是照搬现在常见的padding策略这个细节值得注意。2.2 AlexNet的LRN层和Dropout过拟合控制策略的演变AlexNet在2012年赢下ImageNet靠的不仅是深度还有ReLU、Dropout、数据增强和局部响应归一化LRN的组合。复现时Dropout容易漏LRN层现在已经不常用但代码里保留它有助于理解规范化思想是怎么演进的。# AlexNet.py 中的关键层定义 nn.Conv2d(3, 96, kernel_size11, stride4, padding2), # 224 - 55 nn.ReLU(inplaceTrue), nn.LocalResponseNorm(size5, alpha0.0001, beta0.75, k2), nn.MaxPool2d(kernel_size3, stride2), # 55 - 27LRN层模拟神经生物学里的侧抑制让局部神经元中响应大的更强、响应小的更弱。它的作用是在ReLU之后对相邻通道做归一化实现的是跨通道的竞争。PyTorch里nn.LocalResponseNorm的size参数表示参与竞争的相邻通道数alpha和beta是控制归一化强度的系数一般情况下保持论文原值即可。后面VGG出现后人们发现LRN增益不大就开始逐步弃用。如果想做消融实验把这层注释掉对比精度变化就能直观感受到这个设计在AlexNet里的真实贡献。2.3 VGGNet的模块化设计为什么Sequential能大幅减少代码量VGGNet的核心贡献是提出小卷积核堆叠替代大卷积核的设计范式。全部使用3×3卷积核步长1padding 1保证卷积不改变特征图尺寸尺寸压缩完全交给max pooling完成。这份代码把VGG的conv block抽成了独立函数和直接复制五段卷积相比清晰很多。# VGGNet.py 中 构造卷积块的方式 def make_layers(cfg, in_channels3): layers [] for v in cfg: if v M: layers [nn.MaxPool2d(kernel_size2, stride2)] else: conv2d nn.Conv2d(in_channels, v, kernel_size3, padding1) layers [conv2d, nn.ReLU(inplaceTrue)] in_channels v return nn.Sequential(*layers) # 对应论文的 D 配置即 VGG16 cfg [64, 64, M, 128, 128, M, 256, 256, 256, M, 512, 512, 512, M, 512, 512, 512, M]两个3×3卷积堆叠等效于一个5×5卷积的感受野三个堆叠等效7×7但参数量更少。一个7×7卷积的参数是49C²三个3×3是27C²计算量直接省下约45%。VGG的代价是计算量大、模型体积大第一层全连接就有102M参数占了整个模型的大半。做实验时这份代码里VGGNet的全连接层可以考虑换成global average pooling来压缩体积不过要保持原始结构对比公平性建议先按原样跑一遍再改。make_layers里的cfg配置直接对应VGG论文的A到E五种深度的配置想改成VGG19把最后的[512, 512, 512]改成[512, 512, 512, 512]就行。2.4 三个模型参数量与FLOPs对比复现时心里要有数模型卷积层数全连接层参数量总参数量(约)单张224×224推理FLOPsLeNet-52约4万约6万(28×28输入)约0.05GAlexNet5约5800万约6000万约0.7GVGG1613约1.02亿约1.38亿约15.5GLeNet参数量小到可以忽略但它是理解卷积、池化、全连接三者关系的骨架。AlexNet第一次把深度、宽度和工程技巧组合出工业级可用性全连接层参数占比极高。VGG16是参数量怪兽也是后面GoogLeNet和ResNet要解决的核心问题之一模型越大不等于效果越好更深的网络需要更聪明的结构来降低参数复杂度。3. GoogLeNet的Inception与ResNet的残差连接两个改变CNN走向的结构创新3.1 Inception模块的四个分支不同感受野的并行特征提取GoogLeNet不再单纯堆深度而是让每一层用不同尺寸的卷积核并行提取信息再拼接。Inception模块并行执行1×1、3×3、5×5的卷积和3×3最大池化最后在通道维度上拼接让网络自己学习哪种尺度的特征更重要。# GoogLeNet.py 中 Inception 模块的结构 class Inception(nn.Module): def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj): super(Inception, self).__init__() self.branch1 nn.Conv2d(in_channels, ch1x1, kernel_size1) self.branch2 nn.Sequential( nn.Conv2d(in_channels, ch3x3red, kernel_size1), nn.Conv2d(ch3x3red, ch3x3, kernel_size3, padding1) ) self.branch3 nn.Sequential( nn.Conv2d(in_channels, ch5x5red, kernel_size1), nn.Conv2d(ch5x5red, ch5x5, kernel_size5, padding2) ) self.branch4 nn.Sequential( nn.MaxPool2d(kernel_size3, stride1, padding1), nn.Conv2d(in_channels, pool_proj, kernel_size1) ) def forward(self, x): b1 self.branch1(x) b2 self.branch2(x) b3 self.branch3(x) b4 self.branch4(x) return torch.cat([b1, b2, b3, b4], dim1)每个分支先做1×1卷积降维再放大卷积核计算目的是控制计算量。5×5卷积直接作用在输入上计算量是25×C×C但先降到原来一半通道再卷积计算量直接减半。这就是NIN提出的1×1卷积做跨通道信息融合和降维的思路。GoogLeNet还加了辅助分类器从中间层引出额外损失来缓解梯度消失这个设计在训练时可以打开、推理时去掉compare.py里是否启用可以在配置里调。3.2 ResNet的残差块和shortcut当plain网络退化时靠什么兜底ResNet解决的是网络退化问题深度增加训练集准确率反而下降。这既不是过拟合也不是梯度消失到零而是深层网络难以拟合恒等映射。残差块让网络去学F(x) H(x) - x当恒等映射最优时只需要把残差学成0就行比直接学H(x)x容易得多。# ResNet.py 中 BasicBlock 实现 class BasicBlock(nn.Module): expansion 1 def __init__(self, in_channels, out_channels, stride1, downsampleNone): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.downsample downsample def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out nn.ReLU(inplaceTrue)(out) out self.conv2(out) out self.bn2(out) if self.downsample is not None: identity self.downsample(x) out identity return nn.ReLU(inplaceTrue)(out)downsample承担维度匹配的任务在stride不为1或者输入输出通道不一致时用1×1卷积调整x的shape再做加法。注意biasFalse和BatchNorm的组合卷积后面接BN时偏置会被BN吃掉加上反而浪费。残差连接要求输入输出shape一致维度不同时用identity mapping补上这是复现时最容易出错的地方。ResNet-18和ResNet-34用BasicBlockResNet-50以上换成Bottleneck用1×1卷积先降后升来控制计算量。3.3 BatchNorm放在哪个位置一个影响收敛速度的细节BatchNorm在ResNet里是标准配置但放ReLU前面还是后面在不同代码库里有差异。PyTorch官方实现是conv - BN - ReLU即先归一化再激活也有实现是conv - ReLU - BN。这份代码遵循官方做法因为BN的目标是让激活前的分布稳定先归一化再进ReLU更合理。还有一个细节是BN的track_running_stats在训练和推理时的行为差异模型保存后加载做推理要确保调用model.eval()否则BN用的batch统计量会直接污染推理结果。4. main.py到compare.py搭建训练、验证、对比的完整实验闭环4.1 main.py的运行流程数据划分、训练循环和模型保存main.py作为主入口通常步骤是实例化模型 - 定义损失函数和优化器 - 循环epoch - 保存最优模型 - 画loss和accuracy曲线。数据集用CIFAR-10或MNIST需要区分清楚LeNet设计为单通道输入AlexNet、VGGNet、GoogLeNet、ResNet设计为三通道输入直接在main.py里切不同数据集会出维度错误。# main.py 中训练循环关键逻辑 for epoch in range(epochs): model.train() train_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * inputs.size(0) model.eval() val_correct 0 val_total 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) val_correct (predicted labels).sum().item() val_total labels.size(0) print(fEpoch {epoch1}/{epochs}, Loss: {train_loss:.4f}, Acc: {100*val_correct/val_total:.2f}%)loss.item()在旧版PyTorch需要放在GPU张量上调用0.4.0以后可以直接用。torch.max(outputs, 1)返回最大值和索引取索引即预测类别。model.eval()影响的是Dropout和BatchNorm这两个层在训练和推理时行为不同这行必须配合torch.no_grad()使用前者切层状态后者关梯度计算。没有eval()就直接做验证VGG的Dropout会引入随机性导致准确率抖动ResNet的BN会错误计算batch统计量。4.2 compare.py的对比逻辑如何公平地控制变量compare.py的核心是在相同数据、相同epoch、相同batch_size、相同优化器的条件下训练全部五个模型画出全模型的准确率和loss对比曲线。这个文件的实用价值在于把结构化对比做成可复用脚本。参数LeNetAlexNetVGGNetGoogLeNetResNet输入尺寸28×28224×224224×224224×224224×224batch_size64128128128128epoch1010101010优化器AdamSGDMomentumSGDMomentumSGDMomentumSGDMomentumLeNet单独用28×28输入和Adam因为它结构浅、数据规模小Adam收敛快另外四个模型用SGD加momentum是因为深网络在ImageNet预训练语境下SGD泛化性更好这在CIFAR-10上也是常见选择。如果想让五个模型在完全相同的输入上对比需要把所有输入resize成统一尺寸这会给LeNet带来信息损失也可能给VGG带来计算压力怎么取舍取决于你到底要对比网络结构能力还是端到端工程效果。Adam和SGD的差异在浅层模型上往往不明显但深模型上SGDmomentum的收敛曲线通常更平滑。学习率一般从0.01到0.001之间网格搜索compare.py注释里提示当前参数已做过基础调优在CPU机器上首次跑通建议减少epoch数先把Pipeline跑通再看曲线。4.3 Plot.py可视化曲线叠到一张图才看得出问题Plot.py把训练过程中的loss和accuracy历史记录以图片形式输出。单独看一条loss曲线很难判断模型是否欠拟合多模型叠加后问题一目了然VGG如果收敛速度远慢于ResNet就要检查是不是学习率太大导致震荡。可视化代码通常用matplotlib中文标题需要设置字体否则显示方框这些细节自己在笔记本上跑的时候都会撞到。# Plot.py 中画多条曲线对比的简化实现 import matplotlib.pyplot as plt def plot_compare(histories, names, metricaccuracy): plt.figure(figsize(10, 6)) for history, name in zip(histories, names): values [h[metric] for h in history] plt.plot(values, labelname) plt.xlabel(Epoch) plt.ylabel(metric) plt.legend() plt.grid(True) plt.savefig(f{metric}_compare.png, dpi150)保存图片时dpi设为150以上论文或课设报告插入时不会模糊。5. 复现实验最容易翻车的四个位置及验证手段5.1 数据集与网络输入维度不匹配LeNet期望单通道28×28其他四个网络期望三通道224×224。用MNIST训练VGG会直接报通道数错误解决办法是数据预处理时统一做Resize和通道转换灰度图转RGB用expand或repeat复制通道即可。一般会在数据加载后打印batch_inputs.shape确认一下维度这是最笨也是最快的定位手段。5.2 模型参数量与硬件显存的平衡VGG16参数量在1.3亿级别一张224×224的图前向推理就占用大量显存batch_size设太大会OOM。GPU不够时优先减batch_size其次把224×224输入降到128×128或112×112但降输入尺寸会直接影响模型设计的感受野匹配。ResNet-18是参数量和精度的平衡点复现顺序建议LeNet - ResNet-18 - AlexNet最后再跑VGG。5.3 验证代码正确性的手段每实现完一个模型先传入随机张量确认forward能跑通、输出shape正确再进入训练阶段。模型文件独立成py的好处是可以在命令行单独调试。查看日志时有loss不降或accuracy低先检查数据归一化像素除以255和ImageNet的mean/std归一化相差很大前者容易让深层网络梯度不稳定。精度复现参考值可以这样估算CIFAR-10上ResNet-18大概90%以上AlexNet约85%~87%VGG16约88%~89%LeNet在MNIST约98%~99%。偏差过大就要逐层检查代码。5.4 一个值得做的高级验证梯度检查在进入完整训练前可以对单个batch做梯度检查确认没有梯度消失或爆炸。浅层AlexNet如果不加ReLU会出现梯度消失ResNet的shortcut如果没有加identity维度匹配loss会直接NaN。把torch.autograd.set_detect_anomaly(True)打开能定位到具体产生NaN的层这个技巧对排查VGG和GoogLeNet的偶发数值溢出非常有效。# 梯度检查的快捷方式 torch.autograd.set_detect_anomaly(True) loss.backward() for name, param in model.named_parameters(): if param.grad is not None: grad_norm param.grad.norm().item() if grad_norm 10.0 or grad_norm ! grad_norm: # NaN 判断 print(f梯度异常: {name}, norm{grad_norm})梯度范数超过阈值时适当调低学习率或者加大BN层的作用范围。NaN的梯度说明输出里有NaN配合detect_anomaly能直接追溯到触发位置。这份代码的compare.py和plot.py都是这个思路的落地工具自己改数据集时这四个检查项能省下大量在校准数据管道和排查维度错误上的时间。本文还有配套的精品资源点击获取
返回列表