概述

卷积神经网络(CNN)的历史是深度学习革命的缩影。从 1998 年 LeCun 提出的 LeNet-5,到 2012 年 AlexNet 引爆 ImageNet 竞赛,再到 2015 年 ResNet 突破 152 层,CNN 在十年间完成了”从实验室到工业”的飞跃。1

本文系统梳理 ILSVRC 时代的关键 CNN 架构,分析每个架构的核心创新点解决的问题带来的启示,并给出 PyTorch 完整实现。


一、ILSVRC 时代背景

ImageNet Large Scale Visual Recognition Challenge(ILSVRC)是深度学习最重要的”试金石”:

  • 数据集:1.4M 训练图像,1000 类
  • 评测指标:top-5 错误率
  • 时间:2010-2017
  • 影响:CNN 架构的演进直接对应 top-5 误差的下降

关键数据:2011 年最佳非深度方法错误率 25.8%,2015 年 ResNet 达到 3.57%,超过人类水平(~5%)。


二、LeNet-5(1998):CNN 的奠基

2.1 背景

LeCun 等人在 1998 年提出 LeNet-52,用于手写数字识别(MNIST)。这是第一个被工业部署的 CNN,部署在 ATM 机和银行支票识别中。

2.2 架构

Input (32×32×1)
  ↓
C1: Conv 5×5, 6 filters, output 28×28×6
  ↓
S2: AvgPool 2×2, output 14×14×6
  ↓
C3: Conv 5×5, 16 filters, output 10×10×16
  ↓
S4: AvgPool 2×2, output 5×5×16
  ↓
C5: Conv 5×5, 120 filters, output 1×1×120
  ↓
F6: FC 84
  ↓
Output: 10 classes (softmax)

参数量:~60K(极小)

2.3 关键设计

  1. 卷积-池化-卷积-池化交替:奠定现代 CNN 范式
  2. 局部感受野:每个神经元只连前一层一小块
  3. 权值共享:同一卷积核在不同位置共享参数
  4. sigmoid/tanh 激活:当时主流
  5. 径向基函数(RBF)输出:连接稀疏

2.4 局限

  • 受限于计算能力数据规模,没有大规模应用
  • 池化后使用 sigmoid,梯度消失问题严重
  • 缺少有效正则化手段

三、AlexNet(2012):深度学习复兴的号角

3.1 历史时刻

2012 年,Krizhevsky、Sutskever、Hinton 提出的 AlexNet315.3% top-5 错误率赢得 ILSVRC,比第二名(传统方法)低 10.8 个百分点——震惊学界

3.2 关键创新

创新点描述影响
ReLU 激活 替代 sigmoid缓解梯度消失,训练快 6 倍
Dropout训练时随机失活 50% 神经元有效防止过拟合
GPU 训练2 块 GTX 580,5-6 天训练开启 GPU 时代
数据增强图像翻转、随机裁剪、PCA 抖动扩大数据规模
LRN局部响应归一化模拟生物侧抑制
重叠池化步长小于核尺寸减少信息损失

3.3 架构

Input (224×224×3) [实际 227×227]
  ↓
Conv1: 11×11, stride 4, 96 filters → 55×55×96
  ↓
LRN + MaxPool 3×3, stride 2 → 27×27×96
  ↓
Conv2: 5×5, pad 2, 256 filters → 27×27×256
  ↓
LRN + MaxPool 3×3, stride 2 → 13×13×256
  ↓
Conv3: 3×3, pad 1, 384 filters → 13×13×384
  ↓
Conv4: 3×3, pad 1, 384 filters → 13×13×384
  ↓
Conv5: 3×3, pad 1, 256 filters → 13×13×256
  ↓
MaxPool 3×3, stride 2 → 6×6×256
  ↓
FC6: 4096 + Dropout
  ↓
FC7: 4096 + Dropout
  ↓
FC8: 1000 (softmax)

参数量:~60M
FLOPs:~0.7G(每张图)

3.4 PyTorch 实现

import torch
import torch.nn as nn
 
class AlexNet(nn.Module):
    """AlexNet 简化版(适配现代输入尺寸)"""
    
    def __init__(self, num_classes=1000):
        super().__init__()
        self.features = nn.Sequential(
            # Conv1: 大核大步长
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.LocalResponseNorm(size=5, alpha=1e-4, beta=0.75, k=2),
            nn.MaxPool2d(kernel_size=3, stride=2),
            # Conv2
            nn.Conv2d(96, 256, kernel_size=5, padding=2, groups=1),
            nn.ReLU(inplace=True),
            nn.LocalResponseNorm(size=5, alpha=1e-4, beta=0.75, k=2),
            nn.MaxPool2d(kernel_size=3, stride=2),
            # Conv3-5: 堆叠小核
            nn.Conv2d(256, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )
    
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), 256 * 6 * 6)
        x = self.classifier(x)
        return x

3.5 局限与启示

  • 参数量大(60M),主要在 FC 层
  • 大卷积核(11×11)计算昂贵
  • 启示:ReLU + Dropout + GPU 是当时突破的关键

四、ZFNet(2013):可视化驱动的设计

Zeiler & Fergus 用反卷积可视化 CNN 内部表示,提出改进版 AlexNet:

  • Conv1:11×11 → 7×7(保留更多信息)
  • Conv1, Conv2 stride:4 → 2(保留更多空间信息)
  • top-5 错误率:15.3% → 11.7%

核心贡献让 CNN 不再是黑箱——通过可视化,发现浅层学边缘/纹理,深层学物体部分/整体。


五、VGG(2014):深度与简单性的胜利

5.1 核心思想

VGG(Visual Geometry Group, Oxford)4 团队提出一个极简原则

用 3×3 小卷积核堆叠代替大卷积核

5.2 为什么是 3×3?

两个 3×3 卷积等效于一个 5×5 卷积(感受野相同),但:

  • 参数更少: vs (节省 28%)
  • 非线性更多:2 次 ReLU vs 1 次
  • 表达力更强

三个 3×3 卷积等效于一个 7×7,参数 vs (节省 45%)。

5.3 VGG-16 架构

Input (224×224×3)
  ↓
Block 1: 2× [Conv 3×3, 64] + MaxPool 2×2 → 112×112×64
  ↓
Block 2: 2× [Conv 3×3, 128] + MaxPool 2×2 → 56×56×128
  ↓
Block 3: 3× [Conv 3×3, 256] + MaxPool 2×2 → 28×28×256
  ↓
Block 4: 3× [Conv 3×3, 512] + MaxPool 2×2 → 14×14×512
  ↓
Block 5: 3× [Conv 3×3, 512] + MaxPool 2×2 → 7×7×512
  ↓
FC: 4096 → 4096 → 1000

参数量:138M(VGG-16)
top-5 错误率:7.3%
FLOPs:15.5G

5.4 PyTorch 实现

class VGG(nn.Module):
    """VGG-16 配置:每块卷积的通道数"""
    cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 
           512, 512, 512, 'M', 512, 512, 512, 'M']
    
    def __init__(self, num_classes=1000):
        super().__init__()
        self.features = self._make_layers()
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(0.5),
            nn.Linear(4096, num_classes),
        )
        self._initialize_weights()
    
    def _make_layers(self):
        layers = []
        in_channels = 3
        for v in self.cfg:
            if v == 'M':
                layers.append(nn.MaxPool2d(2, 2))
            else:
                layers += [
                    nn.Conv2d(in_channels, v, 3, padding=1),
                    nn.BatchNorm2d(v),  # 现代改进:加 BN
                    nn.ReLU(inplace=True),
                ]
                in_channels = v
        return nn.Sequential(*layers)
    
    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)
    
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

5.5 局限

  • 138M 参数集中在 FC 层:占 119M
  • 计算量大:FLOPs 比 AlexNet 高 20 倍
  • 不适用于大图像:224×224 是上限

但 VGG 证明了一件事:深度 + 简单性 > 浅而复杂的网络


六、GoogLeNet / Inception(2014):宽度探索

6.1 核心思想

GoogLeNet5(Szegedy 等,Google)提出一个深刻的问题:

必须串行堆叠卷积吗?能否并行尝试不同尺寸?

答案是 Inception Module

6.2 Inception Module(v1)

        Input
       /  |  \  \
      ↓   ↓   ↓   ↓
   1×1  3×3  5×5  3×3
                   pool
      ↓   ↓   ↓   ↓
       \  |  /  /
        Concat
          ↓
        Output

四个分支并行,捕获不同尺度的特征:

  • 1×1:点级特征
  • 3×3:局部特征
  • 5×5:全局特征
  • Pool:保留底层信息

最后沿通道维度拼接。

6.3 1×1 卷积的关键作用

朴素 Inception 的 5×5 卷积计算量巨大。在 3×3、5×5 之前先用 1×1 卷积降维

        Input
       /  |  \  \
    1×1  1×1   1×1  1×1
      ↓   ↓     ↓   ↓
   1×1  3×3   5×5  3×3
      ↓   ↓     ↓   ↓
       \  |   /  /
        Concat

1×1 卷积是跨通道的线性组合,可大幅降维。例如:

  • 5×5 卷积前 1×1 把 256 通道压到 32,计算量减少到原来的 1/8

6.4 辅助分类器

GoogLeNet 在中间层额外加了两个分类头(auxiliary classifier):

  • 训练时提供额外梯度,缓解深层网络梯度消失
  • 测试时不使用
  • 损失加权 0.3

6.5 架构概览

GoogLeNet 有 22 层,但参数仅 5M(比 AlexNet 少 12 倍!)。

阶段输出尺寸
Conv1-2112×1127×7 + 3×3
Conv356×563×3
Inception (4a, 4b, 4c)28×283 个模块
Inception (5a, 5b)14×142 个模块
Inception (6a-6c)7×73 个模块 + aux
Inception (7a, 7b)7×72 个模块 + aux
Pool + Linear1×1avg pool + FC

top-5 错误率:6.7%

6.6 PyTorch 实现

class InceptionModule(nn.Module):
    """带降维的 Inception Module"""
    
    def __init__(self, in_channels, ch_1x1, ch_3x3_red, ch_3x3, 
                 ch_5x5_red, ch_5x5, pool_proj):
        super().__init__()
        # 1×1 分支
        self.branch1 = nn.Conv2d(in_channels, ch_1x1, 1)
        # 3×3 分支(1×1 降维 + 3×3)
        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, ch_3x3_red, 1),
            nn.Conv2d(ch_3x3_red, ch_3x3, 3, padding=1),
        )
        # 5×5 分支(1×1 降维 + 5×5)
        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, ch_5x5_red, 1),
            nn.Conv2d(ch_5x5_red, ch_5x5, 5, padding=2),
        )
        # Pool 分支
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(3, stride=1, padding=1),
            nn.Conv2d(in_channels, pool_proj, 1),
        )
    
    def forward(self, x):
        return torch.cat([
            self.branch1(x),
            self.branch2(x),
            self.branch3(x),
            self.branch4(x),
        ], dim=1)
 
 
class GoogLeNet(nn.Module):
    """GoogLeNet 简化版"""
    
    def __init__(self, num_classes=1000, aux_logits=True):
        super().__init__()
        self.aux_logits = aux_logits
        
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, 7, stride=2, padding=3),
            nn.ReLU(True),
            nn.MaxPool2d(3, stride=2, padding=1),
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(64, 192, 3, padding=1),
            nn.ReLU(True),
            nn.MaxPool2d(3, stride=2, padding=1),
        )
        
        # Inception 3a, 3b
        self.inception3a = InceptionModule(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = InceptionModule(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(3, stride=2, padding=1)
        
        # Inception 4a-4e
        self.inception4a = InceptionModule(480, 192, 96, 208, 16, 48, 64)
        # ... 中间层省略 ...
        
        # 辅助分类器
        if self.aux_logits:
            self.aux1 = self._aux_head(512, num_classes)
            self.aux2 = self._aux_head(528, num_classes)
    
    def _aux_head(self, in_channels, num_classes):
        return nn.Sequential(
            nn.AdaptiveAvgPool2d((4, 4)),
            nn.Conv2d(in_channels, 128, 1),
            nn.ReLU(True),
            nn.Flatten(),
            nn.Linear(128 * 16, 1024),
            nn.ReLU(True),
            nn.Dropout(0.7),
            nn.Linear(1024, num_classes),
        )
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        # ... 中间层前向 ...
        if self.training and self.aux_logits:
            aux1 = self.aux1(x)
        # ... 
        return x  # 主输出(+ aux1, aux2 训练时)

6.7 Inception 后续改进

版本年份改进
v12014原始 Inception
v22015加入 BatchNorm;5×5 → 两个 3×3
v320157×7 → 三个 3×3;辅助分类器改为 BN
v42016与残差连接结合

七、ResNet(2015):极深网络的突破

7.1 问题:深度瓶颈

2014-2015 年的实验观察:

网络越深,训练误差越大(不是过拟合!)

56 层网络比 20 层网络训练误差和测试误差都高。这说明优化困难而非泛化问题。

7.2 残差学习

何恺明等人提出残差学习(Residual Learning)6 解决此问题:

即让网络学习残差 ,而非直接学习

7.3 为什么有效?

核心洞察:恒等映射 很难学,但残差 0 很容易学。

反向传播时,梯度可以通过恒等映射路径直接回传:

右侧的 “1” 保证梯度不会消失。

7.4 瓶颈设计(Bottleneck)

为了减少计算量,深层 ResNet 用三层瓶颈结构:

        Input
          ↓
        1×1 Conv (降维)
          ↓
        3×3 Conv
          ↓
        1×1 Conv (升维)
          ↓
        + Input (skip)
          ↓
        Output

例如从 256 维降为 64 维计算 3×3,再升回 256 维,计算量减少为原来的 1/4。

7.5 ResNet-50 架构

阶段输出
Conv1112×1127×7, 64, stride 2
Conv2_x56×561×1, 64
3×3, 64
1×1, 256
Conv3_x28×281×1, 128
3×3, 128
1×1, 512
Conv4_x14×141×1, 256
3×3, 256
1×1, 1024
Conv5_x7×71×1, 512
3×3, 512
1×1, 2048
Pool + FC1×1avg pool + FC

参数量:25.6M
top-5 错误率:5.25%
FLOPs:4.1G

7.6 PyTorch 实现

class Bottleneck(nn.Module):
    """ResNet 瓶颈块(ResNet-50/101/152)"""
    expansion = 4
    
    def __init__(self, in_channels, mid_channels, stride=1, downsample=None):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, mid_channels, 1, bias=False)
        self.bn1 = nn.BatchNorm2d(mid_channels)
        self.conv2 = nn.Conv2d(mid_channels, mid_channels, 3, 
                               stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.conv3 = nn.Conv2d(mid_channels, mid_channels * 4, 1, bias=False)
        self.bn3 = nn.BatchNorm2d(mid_channels * 4)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
    
    def forward(self, x):
        identity = x
        
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        
        if self.downsample is not None:
            identity = self.downsample(x)
        
        out += identity
        return self.relu(out)
 
 
class ResNet(nn.Module):
    """ResNet-50 完整实现"""
    
    def __init__(self, block, num_blocks, num_classes=1000):
        super().__init__()
        self.in_channels = 64
        
        self.conv1 = nn.Conv2d(3, 64, 7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(3, stride=2, padding=1)
        
        # 四个阶段的残差块
        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
        
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)
        
        # 初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
    
    def _make_layer(self, block, mid_channels, num_blocks, stride):
        downsample = None
        if stride != 1 or self.in_channels != mid_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, mid_channels * block.expansion, 1, 
                          stride=stride, bias=False),
                nn.BatchNorm2d(mid_channels * block.expansion),
            )
        
        layers = [block(self.in_channels, mid_channels, stride, downsample)]
        self.in_channels = mid_channels * block.expansion
        for _ in range(1, num_blocks):
            layers.append(block(self.in_channels, mid_channels))
        
        return nn.Sequential(*layers)
    
    def forward(self, x):
        x = self.relu(self.bn1(self.conv1(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 = x.view(x.size(0), -1)
        return self.fc(x)
 
 
def resnet50(num_classes=1000):
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)

7.7 ResNet 变体

模型深度top-5 错误率
ResNet-181810.63%
ResNet-34348.58%
ResNet-50505.25%
ResNet-1011014.60%
ResNet-1521524.49%

八、横向对比:架构设计的演进逻辑

8.1 ImageNet 误差曲线

年份模型top-5 错误率创新点
2010传统方法28.2%-
2011传统方法25.8%-
2012AlexNet15.3%ReLU + Dropout + GPU
2013ZFNet11.7%可视化优化
2014VGG7.3%更深 + 小核
2014GoogLeNet6.7%Inception 模块
2015ResNet3.57%残差连接
2016Ensemble3.0%模型融合

8.2 参数量 vs 性能

模型参数量top-5性价比
AlexNet60M15.3%
VGG-16138M7.3%
GoogLeNet5M6.7%
ResNet-5025M5.25%

8.3 三种设计哲学

  1. VGG 哲学:深 + 简单(深度优先)
  2. GoogLeNet 哲学:多尺度 + 宽度(结构先验)
  3. ResNet 哲学:残差 + 极深(优化友好)

九、历史启示

9.1 架构设计的”动机驱动”

每个成功的架构都源于对前一个架构局限的洞察:

LeNet → AlexNet:增加 ReLU/Dropout/GPU,应对"深度无法训练"
AlexNet → VGG:用小核堆叠应对"参数爆炸"
VGG → GoogLeNet:并行多尺度应对"参数量仍大"
GoogLeNet → ResNet:残差连接应对"深度达到极限"

9.2 共同的设计原则

  1. 小核优于大核:3×3 几乎成为默认
  2. BN 是默认配置:训练稳定性
  3. 瓶颈设计:1×1 降维 + 3×3 + 1×1 升维
  4. 全局平均池化代替 FC:减少参数
  5. 残差连接:解决深度瓶颈

9.3 CNN 之后的演进

ResNet 之后,CNN 架构继续演进:

方向代表
更高效MobileNet, EfficientNet, ShuffleNet
注意力SENet, CBAM, ECA-Net
神经架构搜索NASNet, DARTS, EfficientNet
视觉 TransformerViT, Swin, ConvNeXt

但所有后续工作都建立在 LeNet→ResNet 奠定的基础上。


十、参考文献


附录:完整 PyTorch 模型工厂

def build_classic_cnn(name, num_classes=1000):
    """构建经典 CNN 模型"""
    if name == 'alexnet':
        return AlexNet(num_classes)
    elif name == 'vgg16':
        return VGG(num_classes)
    elif name == 'googlenet':
        return GoogLeNet(num_classes, aux_logits=True)
    elif name == 'resnet18':
        return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)
    elif name == 'resnet50':
        return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
    elif name == 'resnet101':
        return ResNet(Bottleneck, [3, 4, 23, 3], num_classes)
    else:
        raise ValueError(f"Unknown model: {name}")

最后更新:2026-06-22

Footnotes

  1. Russakovsky, O., Deng, J., Su, H., et al. (2015). ImageNet Large Scale Visual Recognition Challenge. IJCV.

  2. LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE.

  3. Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). ImageNet Classification with Deep Convolutional Neural Networks. NeurIPS.

  4. Simonyan, K., & Zisserman, A. (2014). Very Deep Convolutional Networks for Large-Scale Image Recognition. arXiv:1409.1556.

  5. Szegedy, C., et al. (2014). Going Deeper with Convolutions. arXiv:1409.4842.

  6. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Deep Residual Learning for Image Recognition. arXiv:1512.03385.