资讯详情 CNN经典模型框架全解析:从LeNet到ResNet的演进与实战对比
📅 2026/10/13 5:53:16
1. 从一张“老图”说起为什么现在还要回头看CNN经典模型如果你翻过任何一本深度学习入门教材大概率会在前几章看到一张密密麻麻的“网络结构演进图”——从最早的卷积雏形到后来动辄十几层、几十层的庞然大物一条时间线串起了整个卷积神经网络的发展史。我做模型复现和教学演示这几年被问得最多的一个问题就是“现在都有各种注意力机制和视觉大模型了为什么还要花时间去看这些老框架”这个问题问得好但答案也很直接经典模型是理解一切现代视觉架构的“语法基础”。你去看现在任何一个新出的视觉骨干网络剥开那些花哨的模块名字底层依然是卷积、池化、归一化、残差连接这些基本单元的组合。如果连LeNet、AlexNet、VGG、GoogLeNet、ResNet这几个里程碑式的结构都没亲手搭过一遍那看论文时基本就是“每个字都认识连起来不知道在说什么”的状态。所以这篇博文我想从一个一线从业者的角度把CNN经典模型框架的展示与介绍这件事讲透。不是照本宣科地念论文摘要而是把每个模型为什么这么设计、关键参数怎么算、代码怎么落地、实际跑起来会遇到什么坑全部摊开来说。适合正在学深度学习基础的学生、需要做模型选型对比的工程师以及想给团队做技术分享但不知道怎么组织内容的开发者。读完你至少能做到两件事一是能对着白板把五个经典模型的骨架画出来并讲清楚设计动机二是能用代码把它们跑起来做一次完整的对比实验。2. 经典CNN框架的整体设计脉络与选型逻辑2.1 五个绕不开的里程碑各自解决了什么问题在动手写代码之前先把这几个模型放在一条逻辑线上看你会理解得更深。它们不是孤立出现的每一个都是为了解决前一个暴露出来的问题。LeNet-5是卷积神经网络的“祖师爷”级别结构诞生于上世纪九十年代用来做手写数字识别。它的贡献在于第一次完整定义了“卷积层提取特征 池化层降维 全连接层分类”这个基本范式。结构非常小总共大约六万个参数放到今天连一个迷你模型都算不上但它是理解卷积操作最干净的样本。AlexNet的关键词是“深度”和“工程突破”。它把网络加深到八层参数量拉到六千万级别并且引入了ReLU激活函数和Dropout来对抗过拟合。更重要的是它证明了卷积网络在大规模图像分类任务上真的能打直接点燃了后续几年的研究热潮。从工程角度看AlexNet还用了数据增强和多GPU训练的思路这些做法至今仍是标配。VGG的核心思想就一个字规整。它用连续的3×3小卷积核堆叠来替代大卷积核通过反复的“卷积-卷积-池化”模块把网络做到十六到十九层。VGG的设计哲学是“用深度换表征能力”结构极其统一所以特别适合用来做特征提取的骨干网络。但它的问题也很明显——全连接层参数量巨大模型文件动辄五百多兆部署起来很吃力。GoogLeNet走的是另一条路不盲目加深而是加宽。它提出了Inception模块在同一层里并行使用不同尺寸的卷积核然后把结果拼接起来。这样做的好处是网络可以同时捕捉不同尺度的特征而且通过1×1卷积做通道降维参数量反而比VGG小了很多只有大约五百万参数。GoogLeNet还引入了辅助分类器的设计来缓解深层网络的梯度消失问题。ResNet则是为了解决“网络越深越难训练”这个根本性难题。它提出的残差连接让梯度可以绕过某些层直接回传使得训练上百层甚至上千层的网络成为可能。残差块的设计极其简洁——一个恒等映射加上两层卷积但效果拔群。ResNet之后深度网络的训练问题基本被解决了后续的很多架构都在这个基础上做文章。2.2 为什么选择这五个模型做展示对比你可能会问经典模型那么多为什么偏偏选这五个我的选型逻辑是这样的覆盖完整演进路径从最初的卷积范式到深度化、模块化、残差化五个模型刚好串成一条完整的技术演进线。结构差异足够大每个模型都有独特的设计决策对比起来信息量大不会让人觉得“都差不多”。代码实现难度适中都能用主流框架在几百行以内实现适合做教学演示和快速实验。参数量跨度合理从六万到六千万能直观感受模型复杂度对训练和推理的影响。提示如果你时间有限建议优先吃透LeNet和ResNet这两个。前者帮你建立最基本的卷积直觉后者帮你理解现代深度网络的核心设计思想。中间三个可以作为扩展阅读。2.3 展示与介绍类项目的整体架构思路做这类“经典模型框架展示与介绍”的项目我的经验是不要一上来就堆代码。合理的组织方式应该分三层第一层是结构可视化用文字、表格或手绘示意图把每个模型的层次结构和数据流向讲清楚。这一层不需要任何代码但要求你对每个模块的输入输出尺寸了如指掌。第二层是代码实现用统一的风格把每个模型写出来保持接口一致方便后续对比。这里要注意的是不要直接复制粘贴网上的代码最好自己从头写一遍哪怕照着论文伪代码翻译也行写的过程中你会被迫理解每一个细节。第三层是实验对比在同一个数据集上跑一遍记录参数量、训练时间、准确率等指标用数据说话。这一层是区分“看过”和“做过”的关键。3. 逐个拆解核心结构细节与参数计算实操3.1 LeNet-5最小可用的卷积范式LeNet-5的结构非常清晰输入是一张32×32的单通道图像然后依次经过卷积层C16个5×5卷积核步长1输出6×28×28池化层S22×2平均池化步长2输出6×14×14卷积层C316个5×5卷积核输出16×10×10池化层S42×2平均池化输出16×5×5卷积层C5120个5×5卷积核输出120×1×1全连接层F684个神经元输出层10个神经元对应数字分类参数量的计算过程值得亲手算一遍。以C1层为例参数量是6×(5×5×11)156其中加1是偏置项。C3层的参数量是16×(5×5×61)2416。整个模型加起来大约六万个参数非常轻量。用PyTorch实现的话核心代码如下import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 6, kernel_size5), nn.Tanh(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.Tanh(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(16, 120, kernel_size5), nn.Tanh(), ) self.classifier nn.Sequential( nn.Linear(120, 84), nn.Tanh(), nn.Linear(84, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)注意原始LeNet用的是Tanh激活函数不是现在常用的ReLU。做展示的时候建议保留原版设计这样更能体现历史原貌。如果你要拿它做实际任务换成ReLU和最大池化会明显提升效果。3.2 AlexNet深度与工程技巧的结合AlexNet把输入尺寸扩大到224×224的三通道图像结构上分为五个卷积层和三个全连接层。关键设计点包括第一层用11×11的大卷积核步长4快速降低空间尺寸使用ReLU替代Tanh训练速度提升明显引入Dropout丢弃率0.5放在全连接层后面使用局部响应归一化LRN不过这个后来被证明作用有限参数量的大头在全连接层。以第六层为例输入是256×6×6的特征图展平后是9216维输出4096维参数量就是9216×40964096大约三千七百万。整个模型的参数量约六千万其中绝大部分集中在全连接层。class AlexNet(nn.Module): def __init__(self, num_classes1000): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 64, kernel_size11, stride4, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(64, 192, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(192, 384, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)3.3 VGG用规整换深度VGG的设计哲学是“极致的规整”。它全部使用3×3卷积核和2×2最大池化通过反复堆叠来增加深度。VGG-16的结构可以分成五个卷积块每个块后面接一个池化层最后是三个全连接层。为什么用两个3×3卷积代替一个5×5卷积这里有一个经典的计算两个3×3卷积的感受野和一個5×5卷积相同但参数量更少2×3×318 vs 5×525而且中间多了一次非线性激活表达能力更强。这个思路后来被广泛采用。VGG的参数量约一亿三千八百万其中全连接层占了绝大部分。第一层全连接输入是512×7×725088维输出4096维参数量就超过一亿。这也是VGG模型文件特别大的原因。def make_vgg_block(in_channels, out_channels, num_convs): layers [] for i in range(num_convs): layers.append(nn.Conv2d(in_channels if i 0 else out_channels, out_channels, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) layers.append(nn.MaxPool2d(kernel_size2, stride2)) return layers class VGG16(nn.Module): def __init__(self, num_classes1000): super().__init__() config [(64, 2), (128, 2), (256, 3), (512, 3), (512, 3)] layers [] in_ch 3 for out_ch, num_convs in config: layers.extend(make_vgg_block(in_ch, out_ch, num_convs)) in_ch out_ch self.features nn.Sequential(*layers) self.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)3.4 GoogLeNetInception模块的精巧设计GoogLeNet的核心创新是Inception模块。一个基础的Inception模块包含四条并行分支1×1卷积分支1×1卷积后接3×3卷积分支1×1卷积后接5×5卷积分支3×3最大池化后接1×1卷积分支四条分支的输出在通道维度上拼接。这里的关键技巧是在3×3和5×5卷积之前先用1×1卷积做通道降维否则参数量和计算量会爆炸。比如输入256通道直接做5×5卷积到64通道参数量是256×5×5×64409600如果先用1×1卷积降到32通道再做5×5卷积到64通道参数量变成256×1×1×32 32×5×5×6481925120059392只有原来的七分之一左右。class InceptionBlock(nn.Module): def __init__(self, in_ch, ch1x1, ch3x3_reduce, ch3x3, ch5x5_reduce, ch5x5, pool_proj): super().__init__() self.branch1 nn.Sequential( nn.Conv2d(in_ch, ch1x1, kernel_size1), nn.ReLU(inplaceTrue), ) self.branch2 nn.Sequential( nn.Conv2d(in_ch, ch3x3_reduce, kernel_size1), nn.ReLU(inplaceTrue), nn.Conv2d(ch3x3_reduce, ch3x3, kernel_size3, padding1), nn.ReLU(inplaceTrue), ) self.branch3 nn.Sequential( nn.Conv2d(in_ch, ch5x5_reduce, kernel_size1), nn.ReLU(inplaceTrue), nn.Conv2d(ch5x5_reduce, ch5x5, kernel_size5, padding2), nn.ReLU(inplaceTrue), ) self.branch4 nn.Sequential( nn.MaxPool2d(kernel_size3, stride1, padding1), nn.Conv2d(in_ch, pool_proj, kernel_size1), nn.ReLU(inplaceTrue), ) def forward(self, x): return torch.cat([self.branch1(x), self.branch2(x), self.branch3(x), self.branch4(x)], dim1)GoogLeNet还有一个值得注意的设计是全局平均池化。它在最后一个Inception模块后直接对每个通道取平均值然后接一个全连接层输出分类结果。这样做大幅减少了参数量这个思路后来被很多网络借鉴。3.5 ResNet残差连接改变游戏规则ResNet的核心是残差块。一个基础残差块包含两层3×3卷积然后把输入直接加到输出上。如果输入输出通道数不一致就需要用一个1×1卷积做投影来匹配维度。class BasicBlock(nn.Module): def __init__(self, in_ch, out_ch, stride1): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, kernel_size3, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) self.relu nn.ReLU(inplaceTrue) self.shortcut nn.Sequential() if stride ! 1 or in_ch ! out_ch: self.shortcut nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_ch), ) def forward(self, x): out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) return self.relu(out)残差连接为什么有效直观理解是如果某些层学不到有用特征网络可以让它们输出接近零此时恒等映射保证信息仍然能传过去。从梯度角度看反向传播时梯度可以通过shortcut路径直接回传避免了逐层相乘导致的梯度消失。这个设计让训练上百层的网络变得可行是深度学习发展史上的关键转折点。4. 完整实操从零搭建对比实验环境4.1 环境准备与依赖安装做这类对比实验环境不需要太复杂。我的建议是用Python 3.9以上版本PyTorch 2.0以上再加一些辅助库。pip install torch torchvision matplotlib pandas tqdm如果你有GPU确认一下CUDA是否可用import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only)数据集方面建议用CIFAR-10做对比实验。它比MNIST更有挑战性又比ImageNet轻量得多单卡就能跑。torchvision自带下载接口几行代码就能搞定。from torchvision import datasets, transforms transform_train transforms.Compose([ transforms.Resize(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ]) train_set datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train)注意LeNet原始输入是32×32单通道用在CIFAR-10上需要把输入改成三通道尺寸保持32×32即可。其他模型统一用224×224输入方便对比。4.2 统一训练脚本的设计为了让对比公平所有模型共用同一套训练循环。关键参数保持一致优化器用SGD加动量学习率0.01权重衰减5e-4批次大小64训练30个epoch。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total images.size(0) return total_loss / total, correct / total训练过程中建议记录每个epoch的损失和准确率最后统一画图对比。用tqdm加个进度条看着舒服也方便判断是否卡住。4.3 参数量与计算量统计参数量可以直接数def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad)计算量FLOPs需要额外工具推荐用thop或者ptflopspip install thopfrom thop import profile input_tensor torch.randn(1, 3, 224, 224) flops, params profile(model, inputs(input_tensor,)) print(fFLOPs: {flops / 1e9:.2f}G, Params: {params / 1e6:.2f}M)把五个模型的数据整理成表格对比效果一目了然模型参数量FLOPsCIFAR-10准确率单epoch训练时间LeNet-5约6万约0.04G约62%约8秒AlexNet约6000万约0.7G约78%约25秒VGG-16约1.38亿约15.5G约85%约60秒GoogLeNet约500万约1.5G约83%约30秒ResNet-18约1100万约1.8G约88%约35秒提示上表中的准确率和时间是参考值实际结果会因硬件、超参数、数据增强策略不同而有波动。重点看相对关系不要纠结绝对数值。4.4 训练过程的可视化与结果分析训练完成后把损失曲线和准确率曲线画出来对比。我习惯用matplotlib画在一张图上不同模型用不同颜色区分。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) for name, history in all_histories.items(): plt.plot(history[train_loss], labelname) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Training Loss Comparison) plt.subplot(1, 2, 2) for name, history in all_histories.items(): plt.plot(history[val_acc], labelname) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Validation Accuracy Comparison) plt.tight_layout() plt.savefig(comparison.png, dpi150)从曲线上能看出几个有意思的现象。LeNet收敛最快但最终准确率最低说明容量不够。VGG准确率不错但训练损失下降很慢因为参数量大需要更多迭代。ResNet收敛稳定且最终效果最好残差连接的作用非常明显。GoogLeNet在参数量远小于VGG的情况下达到了接近的准确率说明Inception模块的参数效率很高。5. 实操中踩过的坑与常见问题排查5.1 尺寸不匹配最常见的报错来源做这类模型展示十次报错有八次是尺寸问题。卷积层输出尺寸的计算公式是输出尺寸 (输入尺寸 2×填充 - 卷积核尺寸) / 步长 1这个公式必须记牢。比如输入224卷积核7步长2填充3输出就是(2246-7)/21112。如果算错了下一层就会报维度不匹配。排查技巧在forward函数里加print语句把每一层的输出尺寸打出来一眼就能定位问题在哪一层。def forward(self, x): for i, layer in enumerate(self.features): x layer(x) print(fLayer {i}: {x.shape}) return x5.2 显存不够批次大小与梯度累积VGG-16在224×224输入下批次大小开到32就可能爆显存。解决办法有两个一是减小批次大小二是用梯度累积模拟大批次。accumulation_steps 4 for i, (images, labels) in enumerate(loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()梯度累积的原理是多次前向传播的梯度累加后再更新参数效果上等价于更大的批次但显存占用不变。5.3 训练不收敛学习率与初始化的影响经典模型对初始化比较敏感。如果你发现损失一直不降或者变成NaN先检查三件事学习率是不是太大了试试降到0.001权重初始化是否合理PyTorch默认初始化通常够用但VGG这种深层网络可能需要手动初始化数据归一化做了没有输入没归一化很容易导致训练不稳定注意ResNet的残差块最后一层BN的权重初始化为零这样初始状态下残差块相当于恒等映射训练更稳定。这个细节在原始论文里有提到但很多复现代码会忽略。5.4 常见问题速查表问题现象可能原因解决方法维度不匹配报错卷积输出尺寸算错用公式重新计算加print调试损失变成NaN学习率过大或梯度爆炸降低学习率加梯度裁剪显存溢出批次太大或模型太深减小批次用梯度累积准确率不上升学习率太小或数据有问题调大学习率检查数据标签训练慢数据加载是瓶颈增加num_workers用pin_memory过拟合严重模型容量过大加Dropout加数据增强加权重衰减5.5 几个容易被忽略的实操心得第一个心得不要一上来就训练完整数据集。先用一小部分数据跑通流程确认没有维度错误、显存够用、损失能下降再上全量数据。这样能省很多等待时间。第二个心得保存检查点比保存最终模型重要。训练过程中每隔几个epoch存一次万一中途出问题不用从头再来。而且你可以对比不同阶段的模型效果有时候中间epoch的泛化能力反而更好。第三个心得对比实验要控制变量。如果你想比较模型结构的影响那数据增强、优化器、学习率这些都要保持一致。我见过有人对比ResNet和VGG结果一个用了数据增强一个没用得出的结论完全没有参考价值。第四个心得可视化中间特征图。把卷积层的输出画出来看看你会发现浅层学的是边缘和纹理深层学的是语义部件。这个直观感受比看多少论文都管用。def visualize_feature_maps(model, image, layer_index0): x image.unsqueeze(0) for i, layer in enumerate(model.features): x layer(x) if i layer_index: break feature_maps x.squeeze(0).detach().cpu() fig, axes plt.subplots(4, 8, figsize(16, 8)) for idx, ax in enumerate(axes.flat): if idx feature_maps.shape[0]: ax.imshow(feature_maps[idx], cmapviridis) ax.axis(off) plt.savefig(feature_maps.png, dpi150)6. 模型选型建议与后续扩展方向6.1 不同场景下该选哪个模型做完这一轮对比实验选型思路其实就清晰了。如果你要做的是教学演示或快速原型验证LeNet足够改起来方便跑起来快。如果是中等规模的图像分类任务数据量在几万到几十万之间ResNet-18或GoogLeNet是性价比最高的选择参数量适中效果稳定。如果任务对精度要求极高且算力充足可以考虑更深的ResNet变体或者在此基础上做迁移学习。VGG和AlexNet现在更多是作为教学样本存在实际项目中直接用它们的情况已经很少了但理解它们的设计思路对用好其他模型很有帮助。6.2 从经典模型到现代架构的延伸把这五个模型吃透之后你再去看后续的架构会轻松很多。比如DenseNet的密集连接可以看作残差连接的极端版本每一层都和前面所有层相连。EfficientNet的复合缩放策略是在深度、宽度、分辨率三个维度上做平衡这个思路的起点就是VGG和ResNet的对比实验。甚至Transformer在视觉领域的应用本质上也是在回答“如何更好地建模全局依赖”这个问题而卷积网络通过堆叠感受野来逼近全局建模的做法正是这个问题的早期答案。6.3 给想深入的朋友几个练手方向如果你想在这个基础上继续深入我建议从三个方向入手。一是手动实现反向传播不依赖框架的自动求导用numpy把LeNet的训练过程写一遍写完你对卷积的理解会上一个台阶。二是做消融实验比如把ResNet的残差连接去掉看看效果差多少把VGG的3×3卷积换成5×5看看参数量和准确率怎么变这种对比能让你对每个设计决策的价值有切身体会。三是迁移学习实践拿预训练的ResNet在你自己领域的小数据集上微调感受一下预训练特征到底有多强。最后分享一个我自己的习惯每学完一个经典模型我都会用白板把它画一遍不看书不查资料凭记忆画。画不出来的地方就是没真正理解的地方回去补课。这个方法看起来笨但效果特别好。你能把五个模型的骨架都默画出来的时候CNN这部分的基础就算真正打牢了。