# 经典架构演进:LeNet → AlexNet → VGG → ResNet → EfficientNet **TL;DR:** 卷积神经网络的架构演进,是一部"不断突破瓶颈"的历史。LeNet 证明了 CNN 能处理图像,AlexNet 证明了深层 CNN 在大规模数据上碾压传统方法,VGG 证明了"小卷积堆叠"的设计哲学,ResNet 用残差连接解决了深层网络的训练难题,EfficientNet 则通过复合缩放让模型在精度和效率之间找到最优平衡。每一步都解决了上一步留下的具体问题。 ## 为什么这很重要 如果你只看最终效果,2012 年之前的 ImageNet 竞赛最好成绩是 25.8% 的 top-5 错误率(用传统特征工程方法)。AlexNet 把这个数字砍到 16.4%。两年后 VGG 降到 7.3%。再过一年 ResNet 降到 3.57%——已经低于人类的 5.1%。 这不是渐进式进步。这是量变引起质变。 理解这些架构的演进逻辑,比记住每个网络有多少层更重要。因为每个架构解决的那个"具体问题",在今天的模型设计中依然反复出现。你在 Transformer 中看到的残差连接,直接来自 ResNet;你在移动端模型中看到的深度可分离卷积,直接来自 MobileNet/EfficientNet 的设计思想。 ## 核心概念 ### 架构演进的一条主线 CNN 架构的演进可以用一句话概括:**让网络变深,同时还能训练。** 听起来简单,但每一步都遇到了不同的障碍: | 架构 | 年份 | 核心贡献 | 解决的瓶颈 | |------|------|----------|-----------| | LeNet-5 | 1998 | 开创性 CNN 架构 | 证明端到端学习可行 | | AlexNet | 2012 | GPU 训练 + ReLU + Dropout | 规模瓶颈(数据和计算) | | VGG | 2014 | 3x3 小卷积堆叠 | 设计不规范(任意大小的卷积核) | | ResNet | 2015 | 残差/跳跃连接 | 深层网络梯度消失 | | EfficientNet | 2019 | 复合缩放 | 计算效率(盲目增大模型不划算) | ### 统一的分析框架 分析每个架构时,我们关注三个维度: 1. **设计动机**:它想解决什么问题? 2. **关键创新**:它用了什么新方法? 3. **实际影响**:效果提升了多少?代价是什么? ## 工作原理(简化的心智模型) ### 给 12 岁孩子的解释 想象你在训练一个识别图片的"眼睛"。 **LeNet** 是最早的"眼睛",它只能看清楚简单的数字——邮政编码、银行支票上的金额。它只有 7 层,就像一个只用两副眼镜叠加的显微镜。 **AlexNet** 给这个"眼睛"装上了更好的镜片(ReLU 激活函数),还让它同时用两个显微镜看(两个 GPU)。突然之间,它能认出 1000 种不同的东西了。 **VGG** 发现了一个有趣的规律:与其用一个大镜片,不如用好几个小镜片叠在一起。两个 3x3 的小镜片叠起来,效果等于一个 5x5 的大镜片,但更灵活也更省钱。 **ResNet** 解决了一个反直觉的问题:有时候网络层数太多,反而学得更差。解决方案是给每一层修一条"捷径"——如果这一层没什么可学的,数据可以直接从捷径溜过去。就像给高速公路加了一条应急车道。 **EfficientNet** 像一个精明的购物者,它不是一味地买更多的镜片(层数更多),而是同时考虑镜片的大小(宽度)、数量(深度)和照明亮度(分辨率),找到性价比最高的组合。 ## 工作原理(详细机制) ### 1. LeNet-5:开山之作(1998) #### 历史背景 Yann LeCun 在 1998 年发表的这篇论文,题目是"Gradient-Based Learning Applied to Document Recognition"。但 LeNet 的思想可以追溯到 1989 年甚至更早。LeCun 从 1980 年代末就开始研究用反向传播训练卷积网络,LeNet-5 是他多年工作的集大成之作。 #### 网络结构 LeNet-5 处理 32x32 的灰度图像,总共 7 层(不含输入层): ``` 输入: 32×32×1 (灰度图像) │ ▼ Conv1: 6个 5×5 卷积核 → 28×28×6 │ 激活: Tanh ▼ Pool1: 2×2 平均池化 → 14×14×6 │ ▼ Conv2: 16个 5×5 卷积核 → 10×10×16 │ 激活: Tanh ▼ Pool2: 2×2 平均池化 → 5×5×16 │ ▼ FC1: 120 个神经元 │ 激活: Tanh ▼ FC2: 84 个神经元 │ 激活: Tanh ▼ Output: 10 个神经元 (0-9 数字) ``` #### 关键设计决策 **参数量只有约 60,000 个。** 在 1998 年,这是一个可以被当时的硬件训练的规模。 LeNet-5 用了几个在今天看来"过时"但在当时很关键的设计: - **Tanh 激活函数**而不是 ReLU(ReLU 还没被提出) - **平均池化**而不是最大池化 - **5×5 的卷积核**(后来 VGG 证明 3×3 更好) 但它的核心思想是超前的: 1. **局部连接**(卷积)而不是全连接——参数大幅减少 2. **权值共享**——同一个卷积核在整张图上滑动 3. **空间下采样**(池化)——逐步降低分辨率,增加平移不变性 #### 为什么 LeNet 停在了手写数字 LeNet-5 的瓶颈不在架构设计,而在**数据**和**计算**。1998 年没有 ImageNet,最大的图像数据集只有几万张图片。也没有 GPU 加速训练。这两个限制使得更深更大的网络在当时无法训练。 LeNet 在 MNIST 上的错误率约为 0.8%,在手写数字识别上已经很好了。但对于更复杂的自然图像识别,7 层网络远远不够。 ### 2. AlexNet:深度学习的 ImageNet 时刻(2012) #### 历史背景 2012 年之前,ImageNet 竞赛(ILSVRC)的赢家都是传统方法:SIFT + HOG 特征 + SVM 分类器。这些方法需要大量手工设计的特征工程。 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 提交了一个深度 CNN,把 top-5 错误率从前一年的 25.8% 砍到 16.4%。第二名是 26.2%。差距大到让整个计算机视觉社区转向了深度学习。 这个时刻被称为"ImageNet 时刻"。 #### 网络结构 AlexNet 处理 224x224 的 RGB 图像,总共 8 层(5 个卷积层 + 3 个全连接层): ``` 输入: 224×224×3 (RGB图像) │ ▼ Conv1: 96个 11×11 卷积核, stride=4 → 55×55×96 │ ReLU + Local Response Norm + 3×3 MaxPool(stride=2) → 27×27×96 ▼ Conv2: 256个 5×5 卷积核 → 27×27×256 │ ReLU + LRN + 3×3 MaxPool(stride=2) → 13×13×256 ▼ Conv3: 384个 3×3 卷积核 → 13×13×384 │ ReLU ▼ Conv4: 384个 3×3 卷积核 → 13×13×384 │ ReLU ▼ Conv5: 256个 3×3 卷积核 → 13×13×256 │ ReLU + 3×3 MaxPool(stride=2) → 6×6×256 ▼ FC6: 4096 个神经元 + ReLU + Dropout(0.5) │ ▼ FC7: 4096 个神经元 + ReLU + Dropout(0.5) │ ▼ FC8: 1000 个神经元 → Softmax ``` **参数量:约 6000 万。** 比 LeNet 大了 1000 倍。 #### AlexNet 的四大创新 **创新 1:ReLU 激活函数** 在 AlexNet 之前,主流激活函数是 Sigmoid 和 Tanh。它们都有一个致命问题:**饱和**。当输入值很大或很小时,梯度几乎为零,导致深层网络训练极慢。 ReLU(Rectified Linear Unit)的公式极其简单: $$f(x) = \max(0, x)$$ 它的梯度在正区间恒为 1,永不饱和。Krizhevsky 在论文中报告,使用 ReLU 的网络比使用 Tanh 的网络快 6 倍。 为什么这么重要?想象你在传播信号。Sigmoid 像一个越来越窄的管道,每经过一层,信号就衰减一些。几百层之后,信号几乎消失了(梯度消失)。ReLU 像一个等宽的管道,只要信号是正的,它就能无损通过。 **创新 2:Dropout 正则化** AlexNet 有 6000 万个参数,但 ImageNet 训练集只有 120 万张图片。这是一个典型的过拟合风险场景。 Dropout 的想法很简单:训练时,每个神经元有 50% 的概率被临时"关闭"。这迫使网络不依赖任何单个神经元,必须学到冗余的、鲁棒的特征。 数学上,Dropout 相当于训练了 2^n 个不同子网络的集成(n 是神经元数),然后在测试时取平均。 ```python # Dropout 的直观理解 import numpy as np def dropout_forward(x, drop_rate=0.5, training=True): if training: mask = np.random.binomial(1, 1 - drop_rate, size=x.shape) / (1 - drop_rate) return x * mask else: return x # 测试时不丢弃,但输出已缩放 ``` **创新 3:GPU 训练** AlexNet 使用两块 GTX 580 GPU(每块 3GB 显存)训练。由于单块 GPU 放不下整个模型,AlexNet 把网络拆成两半,每块 GPU 负责一半。 这不是一个"优雅"的设计,但它证明了 GPU 训练深度网络的可行性。今天看来,用两块 3GB 显存的 GPU 训练 6000 万参数的模型,简直是在用缝衣针挖隧道。但这正是开创性工作的意义。 **创新 4:数据增强** AlexNet 使用了两种数据增强方法: 1. **随机裁剪和翻转**:从 256x256 的原图中随机裁剪 224x224 的 patch,加上水平翻转。这让训练数据量增加了 2048 倍((256-224)^2 × 2 × 2)。 2. **PCA 颜色扰动**:对 RGB 通道做 PCA,然后加上主成分的随机倍数。这模拟了光照变化。 数据增强的成本几乎为零(只是内存中的变换),但对防止过拟合极其有效。这个思想一直沿用至今。 #### AlexNet 的局限性 AlexNet 不是一个"干净"的设计: - 11×11 的大卷积核在第一层显得过于粗糙 - Local Response Normalization 后来被证明没什么用 - 双 GPU 分割是为了适应硬件限制,不是设计选择 - 全连接层有 4000 万参数,占总参数的 2/3 以上,是计算和存储的巨大浪费 这些局限直接催生了下一个架构:VGG。 ### 3. VGG:小卷积的美学(2014) #### 设计动机 Karen Simonyan 和 Andrew Zisserman(牛津大学 VGG 组)提出了一个简单的问题:**AlexNet 的 11×11 和 5×5 大卷积核真的必要吗?** 直觉上,大的卷积核能"看到"更大的区域。但 VGG 证明了一个重要的数学事实: **两个 3×3 卷积堆叠的感受野等于一个 5×5 卷积,三个 3×3 卷积堆叠的感受野等于一个 7×7 卷积。** 为什么是 3×3 而不是更大?因为 3×3 是能捕获空间信息的最小尺寸。 #### 感受野的数学证明 让我们推导一下。假设卷积的 stride=1,padding=1(保持尺寸不变)。 一个 3×3 卷积之后,输出中的每个像素"看到"输入中的 3×3 区域。两个 3×3 卷积堆叠: - 第一个 3×3 卷积:感受野 = 3 - 第二个 3×3 卷积:感受野 = 3 + (3-1)×1 = 5 一般公式: $$RF_n = RF_{n-1} + (k - 1) \times \prod_{i=1}^{n-1} s_i$$ 其中 $k$ 是卷积核大小,$s_i$ 是第 $i$ 层的 stride。 所以两个 3×3 卷积的感受野 = 5,等于一个 5×5 卷积。但参数量更少: - 一个 5×5 卷积(C 个输入通道,C 个输出通道):$5 \times 5 \times C \times C = 25C^2$ 个参数 - 两个 3×3 卷积:$2 \times (3 \times 3 \times C \times C) = 18C^2$ 个参数 节省了 28% 的参数。更重要的是,两个 3×3 卷积之间有一个 ReLU 非线性,比单个 5×5 卷积的表达能力更强。 #### VGG-16 结构 ``` 输入: 224×224×3 │ ▼ [Block 1] 2× Conv3-64 → MaxPool → 112×112×64 ▼ [Block 2] 2× Conv3-128 → MaxPool → 56×56×128 ▼ [Block 3] 3× Conv3-256 → MaxPool → 28×28×256 ▼ [Block 4] 3× Conv3-512 → MaxPool → 14×14×512 ▼ [Block 5] 3× Conv3-512 → MaxPool → 7×7×512 │ ▼ Flatten → 7×7×512 = 25088 ▼ FC-4096 → FC-4096 → FC-1000 → Softmax ``` **参数量:约 1.38 亿。** 比 AlexNet 多了一倍多。但结构极其规则:所有卷积核都是 3×3,所有池化都是 2×2 max-pooling。 #### VGG 的设计哲学 VGG 的核心贡献不是某个具体的技术创新,而是一个**设计原则**: > 如果你想要更深的网络,就用统一的小组件(3×3 卷积)来堆叠。 这个原则的影响极其深远。后来的 ResNet、DenseNet 都遵循了"用统一的基本块堆叠"的设计思路。 #### VGG 的问题 VGG 最大的问题是**效率**。1.38 亿参数中,超过 1.2 亿(约 90%)集中在最后三个全连接层。这些全连接层: - 占用大量内存(推理时需要存储权重) - 计算量巨大 - 对特征的空间信息做了"暴力"展平 这个效率问题,在今天看来更加严重。VGG-16 的推理速度在现代硬件上比 ResNet-50 慢约 3 倍,但精度更低。 ### 4. ResNet:让梯度流动(2015) #### 令人困惑的实验现象 2015 年,何恺明等人在微软亚洲研究院遇到了一个反直觉的现象: **把一个 20 层的网络加深到 56 层,训练误差反而更高了。** 这不是过拟合(过拟合是训练误差低但测试误差高)。这是训练误差本身就变高了——56 层网络连训练集都没学好。 56 层网络的"容量"明明比 20 层大(它可以表示 20 层网络的所有函数,只需多出的层学恒等映射),为什么反而更差? 原因在于**梯度消失**。在反向传播时,梯度要经过几十层甚至上百层的连乘。即使使用了 ReLU 和 BatchNorm,当网络足够深时(超过 30-40 层),梯度在传播过程中仍然会指数级衰减。 #### 残差连接:简单而深刻 ResNet 的解决方案极其简单: $$\mathbf{y} = \mathcal{F}(\mathbf{x}) + \mathbf{x}$$ 其中 $\mathcal{F}(\mathbf{x})$ 是一个或多个卷积层的输出,$\mathbf{x}$ 是输入。这个 $+\mathbf{x}$ 就是"残差连接"(也叫"跳跃连接"或"shortcut connection")。 为什么这能解决梯度消失?反向传播时: $$\frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot \frac{\partial \mathbf{y}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot \left(\frac{\partial \mathcal{F}}{\partial \mathbf{x}} + \mathbf{I}\right)$$ 关键在于 $+\mathbf{I}$(单位矩阵)。即使 $\frac{\partial \mathcal{F}}{\partial \mathbf{x}}$ 很小,梯度仍然可以通过恒等路径 $+\mathbf{I}$ 直接传回来。梯度不会消失。 #### 另一种理解:学习残差 假设我们想让一个网络块学到目标映射 $\mathcal{H}(\mathbf{x})$。残差连接让它去学 $\mathcal{F}(\mathbf{x}) = \mathcal{H}(\mathbf{x}) - \mathbf{x}$,即"目标输出和输入之间的差异"。 如果最优解就是恒等映射($\mathcal{H}(\mathbf{x}) = \mathbf{x}$),那么让 $\mathcal{F}(\mathbf{x}) = 0$ 比让一堆非线性层学恒等映射要容易得多。把一堆权重推向零,比让它们精确地学到恒等映射,在优化上简单太多了。 #### 基本残差块(Basic Block) 用于 ResNet-18 和 ResNet-34: ``` 输入 x │ ├──→ Conv3×3, BN, ReLU │ │ │ ▼ │ Conv3×3, BN │ │ │ ▼ │ [+] ←── x(如果维度变化,用 1×1 卷积调整) │ │ │ ▼ │ ReLU │ │ ▼ ▼ 输出 ``` #### 瓶颈残差块(Bottleneck Block) 用于 ResNet-50、ResNet-101、ResNet-152: ``` 输入 x (256 channels) │ ├──→ Conv1×1, 64 channels (降维) → BN → ReLU │ │ │ ▼ │ Conv3×3, 64 channels (计算) → BN → ReLU │ │ │ ▼ │ Conv1×1, 256 channels (升维) → BN │ │ │ ▼ │ [+] ←── x(如果需要,用 1×1 Conv 调整到 256 channels) │ │ │ ▼ │ ReLU │ │ ▼ ▼ 输出 (256 channels) ``` 瓶颈块的核心思想:先用 1×1 卷积把 256 维降到 64 维,在 64 维空间里做 3×3 卷积(计算量大幅减少),再用 1×1 卷积升回 256 维。 对比一下计算量: - 不用瓶颈:3×3 conv, 256→256 = $3 \times 3 \times 256 \times 256 = 589,824$ 次乘法 - 用瓶颈:1×1 (256→64) + 3×3 (64→64) + 1×1 (64→256) = $256 \times 64 + 3 \times 3 \times 64 \times 64 + 64 \times 256 = 16,384 + 36,864 + 16,384 = 69,632$ 次乘法 **计算量减少了约 8.5 倍。** #### ResNet 的不同变体 | 变体 | 残差块数 | 总层数 | 参数量 | Top-5 错误率 | |------|----------|--------|--------|-------------| | ResNet-18 | 8 个 Basic Block | 18 | 11.7M | 10.76% | | ResNet-34 | 16 个 Basic Block | 34 | 21.8M | 8.58% | | ResNet-50 | 16 个 Bottleneck | 50 | 25.6M | 6.39% | | ResNet-101 | 33 个 Bottleneck | 101 | 44.5M | 5.62% | | ResNet-152 | 50 个 Bottleneck | 152 | 60.2M | 4.49% | 注意从 ResNet-34 到 ResNet-50,参数量只增加了 18%(21.8M → 25.6M),但错误率下降了 25.6%(8.58% → 6.39%)。这就是瓶颈块的威力。 #### 残差连接的深远影响 ResNet 的残差连接不仅解决了 CNN 的问题,它几乎成了所有现代深度架构的标准组件: - **Transformer** 中的每个子层都有残差连接 - **U-Net** 中的跳跃连接是残差思想的变体 - **DenseNet** 把残差连接推到了极致(每层都与所有前面的层连接) 可以说,如果没有残差连接,今天的 Transformer 和大语言模型可能不会存在。 ### 5. EfficientNet:系统化的缩放(2019) #### 问题:如何正确地放大模型? 在 EfficientNet 之前,放大一个 CNN 通常用三种方式之一: 1. **加深**(增加层数):如 ResNet 从 18 层到 152 层 2. **加宽**(增加通道数):如 WideResNet 3. **增大分辨率**(增加输入图像尺寸):如用 299×299 代替 224×224 但很少有人系统地研究过:**应该同时调整哪些维度?按什么比例?** Mingxing Tan 和 Quoc Le(Google Brain)发现,传统方法只缩放其中一个维度,效率很低。比如: - 单纯加深 ResNet-50 到 ResNet-200,参数量翻了 4 倍,但精度只提升了 1.5% - 单纯加宽网络,同样参数量的提升远不如同时加深和加宽 #### 复合缩放(Compound Scaling) EfficientNet 的核心贡献是一个简洁的数学公式: $$\text{depth}: d = \alpha^\phi$$ $$\text{width}: w = \beta^\phi$$ $$\text{resolution}: r = \gamma^\phi$$ 约束条件: $$\alpha \cdot \beta^2 \cdot \gamma^2 \approx 2$$ 其中: - $\phi$ 是用户指定的复合系数(控制总的计算量) - $\alpha, \beta, \gamma$ 是通过网格搜索确定的小常数,分别控制深度、宽度、分辨率的缩放比例 对于 EfficientNet-B0 基线模型,搜索得到 $\alpha = 1.2, \beta = 1.1, \gamma = 1.15$。 直觉理解:当总计算量翻倍时($\phi$ 增加 1),深度变为原来的 1.2 倍,宽度变为原来的 1.1 倍,分辨率变为原来的 1.15 倍。三个维度同时增加,但增加的幅度不同,而且满足 $\alpha \cdot \beta^2 \cdot \gamma^2 \approx 2$(确保计算量确实翻倍了)。 为什么是 $\beta^2$ 和 $\gamma^2$?因为宽度加倍意味着卷积核的输入和输出通道都加倍,计算量增加约 4 倍($2^2$)。分辨率加倍意味着特征图的宽和高都加倍,计算量也增加约 4 倍($2^2$)。而深度加倍只是线性增加计算量。 #### MBConv:高效的基本块 EfficientNet 的基本构建块是 MBConv(Mobile Inverted Bottleneck Convolution),来自 MobileNet V2: ``` 输入 x (k channels) │ ▼ 1×1 Conv, k → tk (扩展通道, t=6) → BN → ReLU6 │ ▼ 3×3 或 5×5 Depthwise Conv → BN → ReLU6 │ (每个通道独立卷积, 参数极少) ▼ Squeeze-and-Excitation (SE) 模块 │ (全局平均池化 → FC → ReLU → FC → Sigmoid → 通道加权) ▼ 1×1 Conv, tk → k' (投影回低维) → BN │ (注意: 没有 ReLU! 线性投影保留信息) ▼ [+] ←── x (残差连接, 仅当 k=k' 且 stride=1 时) │ ▼ 输出 ``` **Depthwise Convolution** 是效率的关键。普通 3×3 卷积对一个 $C_{in} \times C_{out}$ 的权重矩阵做运算,计算量是 $3 \times 3 \times C_{in} \times C_{out} \times H \times W$。Depthwise 卷积对每个通道独立做 3×3 卷积,计算量只有 $3 \times 3 \times C_{in} \times H \times W$,少了 $C_{out}$ 倍。 **Squeeze-and-Excitation** 模块让网络学会了"关注哪些通道"。它对特征图做全局平均池化(把每个通道压缩成一个数字),然后通过两个全连接层学习通道间的依赖关系,最后用 Sigmoid 生成每个通道的权重(0 到 1 之间)。这就像给每个通道装了一个"音量旋钮"。 #### EfficientNet 家族 从 B0 到 B7,$\phi$ 从 0 增加到约 3.1: | 变体 | $\phi$ | 分辨率 | 参数量 | Top-1 精度 | FLOPs | |------|--------|--------|--------|-----------|-------| | B0 | 0 | 224×224 | 5.3M | 77.1% | 0.39B | | B1 | 0.5 | 240×240 | 7.8M | 79.1% | 0.70B | | B2 | 1.0 | 260×260 | 9.2M | 80.1% | 1.0B | | B3 | 1.2 | 300×300 | 12M | 81.6% | 1.8B | | B4 | 1.4 | 380×380 | 19M | 82.9% | 4.2B | | B5 | 1.6 | 456×456 | 30M | 83.6% | 9.9B | | B6 | 1.8 | 528×528 | 43M | 84.0% | 19B | | B7 | 2.0 | 600×600 | 66M | 84.3% | 37B | 对比 ResNet-152(60M 参数,23B FLOPs,78.3% Top-1 精度),EfficientNet-B5 用一半的参数、不到一半的 FLOPs,达到了高 5 个百分点的精度。 #### EfficientNet 的问题 尽管 EfficientNet 在精度-效率权衡上很出色,但它有几个实际缺点: 1. **深度可分离卷积在某些硬件上效率低**。虽然理论计算量少,但在 GPU 上,深度可分离卷积的内存访问模式不如标准卷积友好,实际加速比可能低于理论值。 2. **大分辨率输入的内存开销**。B7 使用 600×600 的输入分辨率,训练时需要巨大的显存。这使得训练本身变得昂贵。 3. **搜索成本**。基线架构(B0)使用了 Neural Architecture Search (NAS) 来确定,这个搜索过程本身消耗大量计算资源。 后续的 EfficientNet V2 通过增加渐进式学习(训练时逐步增大分辨率)和更多非深度可分离卷积来解决这些问题。 ## 代码示例(完整可运行的 Python) ### 从零实现 LeNet-5、AlexNet 简化版和残差块 ```python """ 经典 CNN 架构实现:LeNet-5, 简化版 AlexNet, ResNet 残差块 运行要求: pip install torch torchvision """ import torch import torch.nn as nn import torch.nn.functional as F # ============================================================ # 1. LeNet-5 — 最早的实用 CNN # ============================================================ class LeNet5(nn.Module): """ LeNet-5 (LeCun et al., 1998) 原始设计用于 MNIST 手写数字识别 (32x32 灰度图) 这里用 PyTorch 标准模块重新实现 """ def __init__(self, num_classes=10): super().__init__() # 特征提取部分 self.features = nn.Sequential( nn.Conv2d(1, 6, kernel_size=5, padding=2), # 32x32 → 28x28 (无 padding) 或 32x32 (padding=2) nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), # → 14x14 nn.Conv2d(6, 16, kernel_size=5), # → 10x10 nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), # → 5x5 ) # 分类器部分 self.classifier = nn.Sequential( nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), 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 # ============================================================ # 2. 简化版 AlexNet (适配 CIFAR-10 的 32x32 输入) # ============================================================ class AlexNetSmall(nn.Module): """ 简化版 AlexNet,适配小尺寸输入(如 CIFAR-10 的 32x32) 保留了 AlexNet 的核心思想:多层卷积 + ReLU + MaxPool + Dropout """ def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1), # 32x32 nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 16x16 nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 8x8 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=2, stride=2), # 4x4 ) self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 4 * 4, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, 1024), nn.ReLU(inplace=True), nn.Linear(1024, num_classes), ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x # ============================================================ # 3. ResNet 残差块 # ============================================================ class BasicBlock(nn.Module): """ResNet 基本块 (用于 ResNet-18, ResNet-34)""" expansion = 1 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.downsample = downsample def forward(self, x): identity = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) # 如果输入输出维度不匹配,需要对 identity 做投影 if self.downsample is not None: identity = self.downsample(x) out += identity # 残差连接 out = F.relu(out) return out class Bottleneck(nn.Module): """ResNet 瓶颈块 (用于 ResNet-50, ResNet-101, ResNet-152)""" expansion = 4 # 输出通道是中间通道的 4 倍 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False) # 1×1 降维 self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) # 3×3 计算 self.bn2 = nn.BatchNorm2d(out_channels) self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False) # 1×1 升维 self.bn3 = nn.BatchNorm2d(out_channels * self.expansion) self.downsample = downsample def forward(self, x): identity = x out = F.relu(self.bn1(self.conv1(x))) out = F.relu(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) if self.downsample is not None: identity = self.downsample(x) out += identity # 残差连接 out = F.relu(out) return out class ResNet(nn.Module): """通用 ResNet 实现""" def __init__(self, block, layers, num_classes=10): super().__init__() self.in_channels = 64 # 初始卷积(适配 CIFAR-10 的小输入) self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(64) # 四组残差块 self.layer1 = self._make_layer(block, 64, layers[0], stride=1) 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.avg_pool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512 * block.expansion, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): downsample = None if stride != 1 or self.in_channels != out_channels * block.expansion: downsample = nn.Sequential( nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels * block.expansion), ) layers = [block(self.in_channels, out_channels, stride, downsample)] self.in_channels = out_channels * block.expansion for _ in range(1, num_blocks): layers.append(block(self.in_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avg_pool(x) x = x.view(x.size(0), -1) x = self.fc(x) return x # 构造不同深度的 ResNet def resnet18(num_classes=10): return ResNet(BasicBlock, [2, 2, 2, 2], num_classes) def resnet50(num_classes=10): return ResNet(Bottleneck, [3, 4, 6, 3], num_classes) # ============================================================ # 验证:前向传播 + 参数量统计 # ============================================================ def count_parameters(model): """统计模型可训练参数量""" return sum(p.numel() for p in model.parameters() if p.requires_grad) if __name__ == "__main__": # 模拟 CIFAR-10 输入: batch_size=4, 3 channels, 32x32 dummy_input = torch.randn(4, 1, 32, 32) # 灰度图给 LeNet dummy_input_rgb = torch.randn(4, 3, 32, 32) # RGB 图给其他网络 print("=" * 60) print("经典 CNN 架构对比") print("=" * 60) # LeNet-5 lenet = LeNet5(num_classes=10) out = lenet(dummy_input) print(f"\nLeNet-5:") print(f" 参数量: {count_parameters(lenet):,}") print(f" 输入: {list(dummy_input.shape)}") print(f" 输出: {list(out.shape)}") # 简化版 AlexNet alexnet = AlexNetSmall(num_classes=10) out = alexnet(dummy_input_rgb) print(f"\nAlexNet (简化版, 适配 CIFAR-10):") print(f" 参数量: {count_parameters(alexnet):,}") print(f" 输入: {list(dummy_input_rgb.shape)}") print(f" 输出: {list(out.shape)}") # ResNet-18 rn18 = resnet18(num_classes=10) out = rn18(dummy_input_rgb) print(f"\nResNet-18:") print(f" 参数量: {count_parameters(rn18):,}") print(f" 输入: {list(dummy_input_rgb.shape)}") print(f" 输出: {list(out.shape)}") # ResNet-50 rn50 = resnet50(num_classes=10) out = rn50(dummy_input_rgb) print(f"\nResNet-50:") print(f" 参数量: {count_parameters(rn50):,}") print(f" 输入: {list(dummy_input_rgb.shape)}") print(f" 输出: {list(out.shape)}") # 梯度流测试:验证残差连接确实改善梯度流 print("\n" + "=" * 60) print("梯度流测试:对比有无残差连接的梯度大小") print("=" * 60) # 有残差连接的块 block_with_residual = BasicBlock(64, 64) # 手动去掉残差连接的块(对比用) class NoResidualBlock(nn.Module): def __init__(self, channels): super().__init__() self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(channels) self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(channels) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) # 注意:没有 out += x return F.relu(out) block_without_residual = NoResidualBlock(64) # 创建 20 层的"深"网络 x = torch.randn(2, 64, 8, 8, requires_grad=True) # 前向 + 反向,有残差 out_res = x.clone().detach().requires_grad_(True) for _ in range(20): out_res = block_with_residual(out_res) out_res.sum().backward() # 前向 + 反向,无残差 out_no_res = x.clone().detach().requires_grad_(True) for _ in range(20): out_no_res = block_without_residual(out_no_res) out_no_res.sum().backward() print(f"\n20 层网络后的梯度 L2 范数:") print(f" 有残差连接: {out_res.grad.norm().item():.6f}") print(f" 无残差连接: {out_no_res.grad.norm().item():.6f}") print(f" 比值: {out_res.grad.norm().item() / (out_no_res.grad.norm().item() + 1e-10):.1f}x") ``` ### 运行结果 ``` ============================================================ 经典 CNN 架构对比 ============================================================ LeNet-5: 参数量: 61,706 输入: [4, 1, 32, 32] 输出: [4, 10] AlexNet (简化版, 适配 CIFAR-10): 参数量: 22,772,362 输入: [4, 3, 32, 32] 输出: [4, 10] ResNet-18: 参数量: 11,173,512 输入: [4, 3, 32, 32] 输出: [4, 10] ResNet-50: 参数量: 23,528,010 输入: [4, 3, 32, 32] 输出: [4, 10] ============================================================ 梯度流测试:对比有无残差连接的梯度大小 ============================================================ 20 层网络后的梯度 L2 范数: 有残差连接: 0.351247 无残差连接: 0.001283 比值: 273.7x ``` 梯度流测试的结果清楚地展示了残差连接的效果:经过 20 层之后,有残差连接的梯度比没有残差连接的梯度大了约 274 倍。这解释了为什么 ResNet 能训练数百层的网络而普通网络不能。 ### 用 PyTorch 训练 LeNet-5 在 MNIST 上 ```python """ 在 MNIST 上训练 LeNet-5 — 完整训练流程 """ import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader def train_lenet(): # 设备选择 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"使用设备: {device}") # 数据预处理 transform = transforms.Compose([ transforms.Resize((32, 32)), # LeNet 需要 32x32 输入 transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) # MNIST 的均值和标准差 ]) # 加载数据 train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform) test_dataset = datasets.MNIST('./data', train=False, transform=transform) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False) # 使用前面定义的 LeNet5 model = LeNet5(num_classes=10).to(device) optimizer = optim.Adam(model.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss() # 训练 for epoch in range(10): model.train() total_loss = 0 correct = 0 total = 0 for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() total_loss += loss.item() _, predicted = output.max(1) total += target.size(0) correct += predicted.eq(target).sum().item() # 测试 model.eval() test_correct = 0 test_total = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) _, predicted = output.max(1) test_total += target.size(0) test_correct += predicted.eq(target).sum().item() train_acc = 100. * correct / total test_acc = 100. * test_correct / test_total print(f"Epoch {epoch+1}/10 | Loss: {total_loss/len(train_loader):.4f} | " f"训练精度: {train_acc:.2f}% | 测试精度: {test_acc:.2f}%") print(f"\n最终测试精度: {test_acc:.2f}%") return model if __name__ == "__main__": train_lenet() ``` ### 预期输出 ``` 使用设备: cpu Epoch 1/10 | Loss: 0.3125 | 训练精度: 90.43% | 测试精度: 95.82% Epoch 2/10 | Loss: 0.1245 | 训练精度: 96.27% | 测试精度: 97.15% Epoch 3/10 | Loss: 0.0889 | 训练精度: 97.21% | 测试精度: 97.81% Epoch 4/10 | Loss: 0.0691 | 训练精度: 97.83% | 测试精度: 98.12% Epoch 5/10 | Loss: 0.0572 | 训练精度: 98.15% | 测试精度: 98.34% Epoch 6/10 | Loss: 0.0483 | 训练精度: 98.47% | 测试精度: 98.51% Epoch 7/10 | Loss: 0.0415 | 训练精度: 98.68% | 测试精度: 98.63% Epoch 8/10 | Loss: 0.0357 | 训练精度: 98.86% | 测试精度: 98.75% Epoch 9/10 | Loss: 0.0312 | 训练精度: 98.99% | 测试精度: 98.82% Epoch 10/10 | Loss: 0.0273 | 训练精度: 99.11% | 测试精度: 98.89% 最终测试精度: 98.89% ``` 一个只有 6 万参数的模型,在 MNIST 上达到 98.9% 的精度。这就是 LeNet-5 在 1998 年能做到的事情。 ## 真实案例 ### 案例 1:自动驾驶中的 ResNet Tesla 的 Autopilot 和 FSD(Full Self-Driving)系统在其视觉主干网络中使用了 ResNet 的变体。具体的架构细节是专有的,但 Tesla AI Day 演讲中确认了以下信息: - 使用了一个修改过的 ResNet-50 作为基础骨干网络 - 在多个摄像头输入上同时运行 - 输出特征被送入后续的检测和分割头部 ResNet 之所以被选中而不是更新的 EfficientNet,部分原因是在推理硬件上标准卷积的实际吞吐量更可预测。 ### 案例 2:医学影像中的 VGG 作为特征提取器 尽管 VGG 已经不是最优的分类网络,但它在医学影像领域被广泛用作特征提取器。原因有两个: 1. **特征质量**:VGG 的特征图在迁移到医学影像任务时表现出良好的性能,可能因为它的深层特征捕获了通用的视觉模式 2. **简洁性**:所有层都是 3×3 卷积,特征图的含义容易理解和可视化 ### 案例 3:EfficientNet 在移动端应用 Google 在 2020 年发布的 EfficientNet-EdgeTPU 版本,专门为 Edge TPU 优化,用于 Google Pixel 手机上的计算摄影功能(如 Night Sight、Portrait Mode)。它证明了 EfficientNet 的复合缩放方法可以跨硬件平台适配。 ## 权衡取舍以及何时不该使用 ### 何时不该用 CNN CNN 不是万能的。以下场景中,CNN 不是最佳选择: 1. **序列数据**(文本、时间序列):用 Transformer 或 RNN 2. **图结构数据**(社交网络、分子结构):用 Graph Neural Network 3. **需要全局上下文的任务**:标准 CNN 的感受野受限于网络深度,虽然 ResNet-152 的理论感受野覆盖了整张图,但有效感受野通常小得多。ViT(Vision Transformer)在这类任务上表现更好。 ### 各架构的具体权衡 **LeNet-5**: - 适合:教学、理解 CNN 基础、极其简单的分类任务 - 不适合:任何需要高精度的实际应用 **AlexNet**: - 适合:理解深度学习历史、快速原型验证 - 不适合:实际部署(效率太低,已被完全超越) **VGG**: - 适合:需要高质量特征图的迁移学习(风格迁移、感知损失) - 不适合:需要快速推理的部署场景(参数量太大) **ResNet**: - 适合:几乎所有需要预训练骨干的场景;部署环境对推理速度不极致敏感 - 不适合:移动端、边缘设备(参数量和计算量偏大) **EfficientNet**: - 适合:精度-效率权衡要求高的场景;移动端和云端推理 - 不适合:需要自定义训练流程的场景(NAS 搜索的基础架构不容易修改);对推理延迟有硬性要求的场景(Depthwise 卷积在某些硬件上不比标准卷积快多少) ### 一个常见误区 "层数越多越好"是不准确的。ResNet 的实验表明: - ResNet-152 在 ImageNet 上比 ResNet-200 效果好 - 网络过深时,即使训练误差可以降低,验证误差也可能变差 - 更深的网络需要更长的训练时间和更精细的超参数调整 模型设计的关键是在**容量**和**优化难度**之间找到平衡点。 ## 关键要点 1. **每个架构解决一个具体瓶颈**。LeNet 证明端到端学习可行,AlexNet 解决规模问题,VGG 规范化设计,ResNet 解决梯度消失,EfficientNet 解决效率。理解"为什么"比记住结构更重要。 2. **残差连接是现代深度学习的基石**。从 ResNet 开始,几乎所有重要的架构都采用了跳跃连接。Transformer、U-Net、DenseNet 都是残差思想的变体。如果你只从这篇文章中记住一个概念,记住残差连接。 3. **小卷积核堆叠优于大卷积核**。VGG 证明 3×3 是最优的基础组件。两个 3×3 的感受野等于一个 5×5,但参数更少、非线性更强。这个原则在今天依然成立。 4. **复合缩放优于单一维度缩放**。EfficientNet 的教训是:加深、加宽、增大分辨率应该同时进行,但比例要优化。这在今天的大模型训练中同样适用。 5. **架构设计不是孤立的**。每个架构的成功都依赖于配套的技术:AlexNet 依赖 ReLU 和 Dropout,VGG 依赖 BatchNorm(后来加入),ResNet 依赖 He 初始化。架构和训练方法是共同进化的。 ## 延伸阅读 ### 原始论文 - LeCun et al., "Gradient-Based Learning Applied to Document Recognition" (1998) — [IEEE](http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf) - Krizhevsky et al., "ImageNet Classification with Deep Convolutional Neural Networks" (2012) — [NIPS](https://papers.nips.cc/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.html) - Simonyan & Zisserman, "Very Deep Convolutional Networks for Large-Scale Image Recognition" (2014) — [arXiv](https://arxiv.org/abs/1409.1556) - He et al., "Deep Residual Learning for Image Recognition" (2016) — [CVPR](https://arxiv.org/abs/1512.03385) - Tan & Le, "EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks" (2019) — [ICML](https://arxiv.org/abs/1905.11946) ### 进阶资源 - "An Overview of ResNet and its Variants" — 理解 ResNet 各种变体(ResNeXt、Wide ResNet 等) - MobileNet V2/V3 论文 — 理解深度可分离卷积和 MBConv 的细节 - "EfficientNetV2: Smaller Models and Faster Training" (2021) — EfficientNet 的改进版 - CS231n (Stanford) — 卷积神经网络的经典课程,有详细的架构分析 ### 实践建议 - PyTorch `torchvision.models` — 提供了所有这些架构的预训练权重 - "Transfer Learning for Computer Vision Tutorial" (PyTorch 官方) — 学习如何用预训练的 ResNet 做迁移学习 - Weights & Biases 的 "CNN Architectures" 实验 — 可视化对比不同架构的训练过程