从LeNet到ResNet:用PyTorch手把手复现5个CNN经典模型(附完整代码)
从LeNet到ResNet:用PyTorch实战5大CNN经典架构
当你第一次看到LeNet-5那简洁的架构时,可能不会想到这个诞生于1998年的网络会开启深度学习的新纪元。如今,从AlexNet到ResNet,卷积神经网络(CNN)已经成为计算机视觉领域的基石。本文将带你用PyTorch亲手实现这五大经典模型,理解它们的设计哲学,并掌握实际应用中的关键技巧。
1. 环境准备与基础概念
在开始构建模型前,我们需要确保环境配置正确。推荐使用Python 3.8+和PyTorch 1.10+版本,这些组合在稳定性和功能支持上表现最佳。
pip install torch torchvision matplotlib
CNN的核心在于其局部感知和权值共享的特性。与全连接网络不同,CNN通过卷积核在图像上滑动提取特征,大幅减少了参数量。三个关键组件是:
- 卷积层:使用可学习的滤波器提取特征
- 池化层:降低特征图维度,增强平移不变性
- 全连接层:最终分类决策
提示:在PyTorch中,
nn.Conv2d的padding参数如果设为'SAME'可以保持特征图尺寸不变,这在某些架构中非常有用。
下表对比了我们将实现的五个模型的基本特性:
| 模型 | 提出年份 | 深度 | 核心创新 | 参数量级 |
|---|---|---|---|---|
| LeNet-5 | 1998 | 5层 | 首个成功CNN架构 | 6万 |
| AlexNet | 2012 | 8层 | ReLU、Dropout | 6千万 |
| VGG | 2014 | 16/19层 | 小卷积核堆叠 | 1.38亿 |
| GoogLeNet | 2014 | 22层 | Inception模块 | 500万 |
| ResNet | 2015 | 18-152层 | 残差连接 | 1千万-2亿 |
2. LeNet-5:CNN的起源
让我们从Yann LeCun的经典之作开始。LeNet-5最初用于手写数字识别,其架构清晰地展示了CNN的基本组成:
import torch.nn as nn
class LeNet5(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding=2) # 保持28x28
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16*5*5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
实现时的几个关键点:
- 原始论文使用tanh激活,但现代实现通常用ReLU
- MNIST图像尺寸为28x28,比原始输入(32x32)小,因此我们调整了padding
- 全连接层的维度需要仔细计算,避免维度不匹配
注意:在PyTorch中,MaxPool2d的ceil_mode参数会影响输出尺寸计算,默认False表示向下取整。
3. AlexNet:深度学习的里程碑
AlexNet在2012年ImageNet竞赛中一战成名,其核心创新包括:
- 使用ReLU替代sigmoid,缓解梯度消失
- 引入Dropout减少过拟合
- 采用数据增强和GPU并行训练
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256*6*6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
实际训练AlexNet时需要注意:
- 使用较大的batch size(如256)和动量优化器
- 学习率初始设为0.01,每30个epoch除以10
- 数据增强包括随机裁剪、水平翻转和颜色扰动
4. VGG:简洁的力量
VGG的核心思想是使用连续的3×3卷积替代大卷积核,这种设计:
- 增加了网络深度和非线性
- 减少了参数量(两个3×3卷积等效于一个5×5的感受野)
- 便于硬件加速
以下是VGG-16的实现:
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M',
512, 512, 512, 'M', 512, 512, 512, 'M']
class VGG16(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.features = make_layers(cfg)
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(
nn.Linear(512*7*7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
VGG在实际应用中的技巧:
- 预训练模型在小数据集上表现优异
- 特征提取部分可以冻结,只微调分类器
- 全连接层参数量巨大,可用全局平均池化替代
5. GoogLeNet:Inception的智慧
GoogLeNet的核心创新是Inception模块,它并行应用不同尺寸的卷积核,让网络自行学习最优特征组合方式。基本Inception模块实现如下:
class Inception(nn.Module):
def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
super().__init__()
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, kernel_size=1),
nn.BatchNorm2d(ch1x1),
nn.ReLU(inplace=True),
)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3red, kernel_size=1),
nn.BatchNorm2d(ch3x3red),
nn.ReLU(inplace=True),
nn.Conv2d(ch3x3red, ch3x3, kernel_size=3, padding=1),
nn.BatchNorm2d(ch3x3),
nn.ReLU(inplace=True),
)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5red, kernel_size=1),
nn.BatchNorm2d(ch5x5red),
nn.ReLU(inplace=True),
nn.Conv2d(ch5x5red, ch5x5, kernel_size=5, padding=2),
nn.BatchNorm2d(ch5x5),
nn.ReLU(inplace=True),
)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, kernel_size=1),
nn.BatchNorm2d(pool_proj),
nn.ReLU(inplace=True),
)
def forward(self, x):
return torch.cat([
self.branch1(x),
self.branch2(x),
self.branch3(x),
self.branch4(x)
], 1)
GoogLeNet还引入了辅助分类器来解决梯度消失问题。实际使用中发现:
- 1×1卷积作为"瓶颈层"能有效减少计算量
- 并行结构对硬件加速要求较高
- 在移动端可能需要简化Inception模块
6. ResNet:残差学习的革命
ResNet通过残差连接解决了深度网络的退化问题。其核心思想是学习残差函数F(x) = H(x) - x,而非直接学习H(x)。基本残差块实现:
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = F.relu(out)
return out
构建完整ResNet-18:
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
super().__init__()
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
def resnet18():
return ResNet(BasicBlock, [2, 2, 2, 2])
残差网络在实际应用中的优势:
- 训练更深的网络而不会出现退化
- 前向传播时梯度可以更有效地回传
- 适合迁移学习,预训练模型广泛可用
7. 模型选择与实践建议
面对具体任务时,模型选择需要考虑多个因素:
- 数据集大小:小数据集适合使用预训练的VGG或ResNet
- 计算资源:移动端可考虑轻量级变种如MobileNet
- 延迟要求:实时系统可能需要更浅的网络
以下是在PyTorch中加载预训练模型并进行微调的典型流程:
model = torchvision.models.resnet18(pretrained=True)
# 冻结所有卷积层参数
for param in model.parameters():
param.requires_grad = False
# 替换最后的全连接层
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 只训练最后的分类层
optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.001, momentum=0.9)
训练过程中的常见问题及解决方案:
- 过拟合:增加数据增强、使用更强的正则化
- 训练不稳定:检查学习率、梯度裁剪
- 内存不足:减小batch size、使用梯度累积
在图像分类任务中,ResNet-18通常是一个不错的起点。当我在处理CIFAR-10数据集时,发现即使不进行复杂调参,ResNet-18也能达到约95%的准确率。对于更复杂的任务,可以尝试更深的ResNet变体或最新的EfficientNet架构。
更多推荐


所有评论(0)