从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

实现时的几个关键点:

  1. 原始论文使用tanh激活,但现代实现通常用ReLU
  2. MNIST图像尺寸为28x28,比原始输入(32x32)小,因此我们调整了padding
  3. 全连接层的维度需要仔细计算,避免维度不匹配

注意:在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卷积替代大卷积核,这种设计:

  1. 增加了网络深度和非线性
  2. 减少了参数量(两个3×3卷积等效于一个5×5的感受野)
  3. 便于硬件加速

以下是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)

训练过程中的常见问题及解决方案:

  1. 过拟合:增加数据增强、使用更强的正则化
  2. 训练不稳定:检查学习率、梯度裁剪
  3. 内存不足:减小batch size、使用梯度累积

在图像分类任务中,ResNet-18通常是一个不错的起点。当我在处理CIFAR-10数据集时,发现即使不进行复杂调参,ResNet-18也能达到约95%的准确率。对于更复杂的任务,可以尝试更深的ResNet变体或最新的EfficientNet架构。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐