现代卷积神经网络(一):从AlexNet到VGG,开启深度学习的狂飙时代
前面我们已经认识了卷积、填充、步幅、通道、汇聚层和 LeNet。到这里,你已经知道 CNN 为什么适合处理图片:它像拿着小模板一样,在图片局部寻找边缘、纹理和形状。
但 LeNet 毕竟是早期网络。它擅长识别手写数字这种“小而规整”的图片,一旦面对真实世界的彩色照片,就会明显吃力:猫可能趴着、跑着、被遮住一半;汽车可能从不同角度出现;背景里还会有树、路牌和人群。图片越复杂,模型就越需要更大的容量、更强的特征提取能力和更稳定的训练方法。
这一篇,我们进入“现代卷积神经网络”的第一站:AlexNet 和 VGG。
你可以把这篇看成 CNN 从“能用”走向“强大”的关键转折:
- AlexNet 告诉我们:网络可以做得更深更大,只要有足够数据、算力和合适的训练技巧。
- VGG 告诉我们:复杂网络不一定要靠复杂设计,很多时候用简单模块反复堆叠,反而更清晰、更强大。
一、为什么 LeNet 不够用了?
LeNet 的经典任务是手写数字识别,比如 MNIST 数据集里的 28x28 灰度图片。这个任务很适合入门,因为图片小、背景干净、类别少,模型不用太复杂也能学得不错。
但真实图片要难很多:
-
图片更大
手写数字是 28x28,ImageNet 里的图片通常会被处理成 224x224 左右,像素数量一下子增加几十倍。 -
颜色更多
手写数字通常是单通道灰度图,而真实图片是 RGB 三通道。模型不只要看亮暗,还要理解颜色组合。 -
类别更多
MNIST 只有 10 类数字,ImageNet 有 1000 类物体,难度完全不是一个量级。 -
变化更多
同样是“狗”,可能有不同品种、姿势、光照、背景和遮挡。模型必须学到更抽象、更稳定的特征。
所以,现代 CNN 的核心问题就变成了:
如何让网络更深、更宽、更会提取特征,同时还能训练得动?
AlexNet 就是在这个问题上,给出了震撼全场的答案。
先看一张对比表,帮助你快速建立整体印象:
| 项目 | LeNet | AlexNet |
|---|---|---|
| 典型任务 | 手写数字识别 | 大规模自然图片识别 |
| 输入图片 | 28x28 灰度图 | 更大的彩色图片,常见为 224x224 左右 |
| 网络规模 | 较小,结构简单 | 更深更宽,参数量大幅增加 |
| 卷积设计 | 早期经典卷积结构 | 更深的卷积堆叠,特征提取能力更强 |
| 激活函数 | 常见为 Sigmoid 或 Tanh | 大量使用 ReLU |
| 训练技巧 | 主要依赖基础训练流程 | 引入 Dropout、GPU 加速等关键技巧 |
| 历史意义 | CNN 的早期代表 | 让深度学习在视觉领域真正爆发 |
二、AlexNet:深度学习复兴的代表作
2012 年,AlexNet 在 ImageNet 图像识别比赛中表现惊人,让大家重新意识到:深度神经网络真的能在真实图片识别上击败传统方法。
如果 LeNet 像一个小型识别器,那么 AlexNet 就像全面升级后的视觉系统:更大的卷积层、更多通道、更深的结构、更强的算力支持。
1. AlexNet 做对了什么?
AlexNet 的成功不是只靠“层数更多”,而是几件事情同时到位。
更深、更宽
AlexNet 有 5 个卷积层和 3 个全连接层,参数量大约 6000 万。和 LeNet 相比,它的“脑容量”大了很多,可以学习更复杂的图片特征。
通俗理解:
- 低层卷积:识别边缘、颜色块、简单纹理。
- 中层卷积:组合出局部形状,比如眼睛、轮子、花瓣。
- 高层卷积:组合出更完整的物体部件,甚至接近“这是一只猫”的判断。
使用 ReLU 激活函数
早期神经网络常用 Sigmoid 或 Tanh,但它们在深层网络中容易出现梯度变小的问题,训练会很慢。
AlexNet 大量使用 ReLU:
nn.ReLU()
ReLU 的规则很简单:
- 正数保留;
- 负数变成 0。
它看起来朴素,但非常好用。对初学者来说,你只要先记住一句话:
ReLU 让深层网络训练得更快,也更不容易卡住。
使用 GPU 加速
AlexNet 的训练离不开 GPU。卷积网络里的大量计算,本质上是矩阵和张量运算,GPU 正好擅长并行处理这些计算。
这件事对深度学习发展非常关键:不是只有算法重要,算力也直接决定了模型能不能训练出来。
使用 Dropout 防止过拟合
AlexNet 的全连接层很大,参数很多,很容易“死记硬背”训练集。为了解决这个问题,它使用了 Dropout。
Dropout 的思想很像课堂随机点名:
训练时随机让一部分神经元暂时不工作,逼迫其他神经元也学会独立完成任务。
这样模型不能过度依赖某几个神经元,泛化能力会更好。
nn.Dropout(p=0.5)
p=0.5 表示训练时大约随机关闭一半神经元。
2. AlexNet 的结构长什么样?
下面是一份适合学习理解的 AlexNet 写法。为了贴近真实彩色图片,输入通道写成 3;如果你用灰度图数据集练习,可以把第一层的 3 改成 1。
先看这张示意图,你不用运行代码,也能先把“过程”和“结果”看明白:
易读表格(按层总结):
| 层 | 类型 & 参数 | 计算公式 | 输出尺寸 |
|---|---|---|---|
| 输入 | 原始图像 | — | 224 × 224 × 3 |
| Conv1 | Conv2d(3,96,k=11,s=4,p=2) | (224 - 11 + 2×2)/4 + 1 = 55 | 55 × 55 × 96 |
| Pool1 | MaxPool2d(k=3,s=2) | floor((55 - 3)/2) + 1 = 27 | 27 × 27 × 96 |
| Conv2 | Conv2d(96,256,k=5,p=2) | (27 - 5 + 2×2)/1 + 1 = 27 | 27 × 27 × 256 |
| Pool2 | MaxPool2d(k=3,s=2) | floor((27 - 3)/2) + 1 = 13 | 13 × 13 × 256 |
| Conv3 | Conv2d(256,384,k=3,p=1) | (13 - 3 + 2×1)/1 + 1 = 13 | 13 × 13 × 384 |
| Conv4 | Conv2d(384,384,k=3,p=1) | 同上 = 13 | 13 × 13 × 384 |
| Conv5 | Conv2d(384,256,k=3,p=1) | 同上 = 13 | 13 × 13 × 256 |
| Pool3 | MaxPool2d(k=3,s=2) | floor((13 - 3)/2) + 1 = 6 | 6 × 6 × 256 |
| Flatten | 展平 | 6 × 6 × 256 = 9216 | 9216 (一维向量) |
读这张表,抓住三个点:
- 前半段是“找线索”,先抓边缘、纹理,再慢慢组合成更复杂的形状。
- 汇聚层的作用是“缩小图片”,让特征图越来越紧凑,计算也越来越轻。
- 后半段是“做判断”,把整理好的特征交给全连接层,最后输出类别分数。
一句话概括:
前面负责“看图找线索”,后面负责“把线索汇总成答案”。
import torch
from torch import nn
net = nn.Sequential(
# 第一层卷积:用较大的卷积核先快速提取边缘和轮廓
# Conv1: nn.Conv2d(3,96,k=11,s=4,p=2)
# 计算:_out = (224 - 11 + 2*2) / 4 + 1 = 55 -> 输出 55x55x96
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), nn.ReLU(),
# Pool1: nn.MaxPool2d(k=3,s=2)
# 计算:_out = floor((55 - 3) / 2) + 1 = 27 -> 输出 27x27x96
nn.MaxPool2d(kernel_size=3, stride=2),
# 第二组卷积:进一步组合局部纹理和形状信息
# Conv2: nn.Conv2d(96,256,k=5,p=2)
# 计算:_out = (27 - 5 + 2*2) / 1 + 1 = 27 -> 输出 27x27x256
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
# Pool2: nn.MaxPool2d(k=3,s=2)
# 计算:_out = floor((27 - 3) / 2) + 1 = 13 -> 输出 13x13x256
nn.MaxPool2d(kernel_size=3, stride=2),
# 第三组卷积:逐步提取更抽象的高层特征
# Conv3: nn.Conv2d(256,384,k=3,p=1)
# 计算:_out = (13 - 3 + 2*1) / 1 + 1 = 13 -> 输出 13x13x384
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
# Conv4: nn.Conv2d(384,384,k=3,p=1)
# 计算:保持尺寸 13x13 -> 输出 13x13x384
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
# Conv5: nn.Conv2d(384,256,k=3,p=1)
# 计算:保持尺寸 13x13 -> 输出 13x13x256
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
# Pool3: nn.MaxPool2d(k=3,s=2)
# 计算:_out = floor((13 - 3) / 2) + 1 = 6 -> 输出 6x6x256
nn.MaxPool2d(kernel_size=3, stride=2),
# 把二维特征图展开成一维向量,送入全连接层
nn.Flatten(),
# 展平计算:6 x 6 x 256 = 9216 -> 所以 Linear 的输入为 9216
# 第一个全连接层:开始把图像特征汇总成类别线索
nn.Linear(9216, 4096), nn.ReLU(),
# Dropout:训练时随机丢掉一部分神经元,减少过拟合
nn.Dropout(p=0.5),
# 第二个全连接层:继续整合高层特征
nn.Linear(4096, 4096), nn.ReLU(),
# 再做一次 Dropout:让模型不要太依赖少数神经元
nn.Dropout(p=0.5),
# 输出层:给出 1000 个类别的预测分数
nn.Linear(4096, 1000)
)
这里最容易让人疑惑的是:9216 为什么会变成 4096?
答案是,它不是“自己变过去”的,而是被这一层全连接层重新映射了:nn.Linear(9216, 4096)。
你可以把它理解成三步:
nn.Flatten()先把 6 x 6 x 256 的特征图拉平成一个 9216 维向量。nn.Linear(9216, 4096)再用一组可学习的权重,把 9216 个输入特征组合成 4096 个更紧凑、更适合分类的特征。- 后面的
nn.Linear(4096, 4096)和nn.Linear(4096, 1000)继续把这些特征加工成最终的类别分数。
所以,9216 不是“直接等于” 4096,而是“经过全连接层压缩和重组后,输出成 4096 维表示”。
再往下看这几层:
- 第一个
nn.Linear(9216, 4096):把特征从 9216 维压到 4096 维,相当于做一次信息筛选和重组。 nn.ReLU():引入非线性,让模型不只是线性叠加,而是能学到更复杂的关系。nn.Dropout(p=0.5):训练时随机丢掉一部分神经元,减少过拟合。- 第二个
nn.Linear(4096, 4096):继续融合和提炼特征,维度不变,但表示会更抽象。 - 第二个
nn.ReLU():再加一次非线性变换。 - 第二个
nn.Dropout(p=0.5):再做一次正则化,进一步抑制过拟合。 - 最后的
nn.Linear(4096, 1000):把 4096 维特征映射到 1000 个类别分数,对应 ImageNet 的 1000 类。
第一次看这段代码,不用急着记每个数字。先看清这条主线:
- 前面连续用卷积层提取图片特征。
- 中间用汇聚层逐步缩小图片尺寸。
- 后面用全连接层把特征汇总成类别判断。
如果再把结果说得更直白一点,就是:
- 输入一张图片;
- 中间不断把“大图”变成“更抽象、更紧凑的特征图”;
- 最后输出一个类别分数列表,分数最高的类别就是模型的预测结果。
3. AlexNet 的入门记忆法
AlexNet 的意义可以浓缩成一句话:
它证明了“大数据 + 大模型 + GPU + ReLU + Dropout”这条路真的能走通。
从这以后,计算机视觉正式进入深度学习时代。
三、VGG:用简单积木搭出强大网络
AlexNet 很厉害,但它的结构看起来有点“不规整”:有 11x11 卷积、5x5 卷积、3x3 卷积,设计上有不少经验味道。
研究者自然会问:
有没有一种更简单、更标准的方式来搭建深层 CNN?
VGG 的答案是:有。全部用小卷积核,像搭积木一样叠起来。
1. VGG 的核心思想
VGG 最重要的设计非常朴素:
- 卷积层尽量使用 3x3 卷积核,并配合
padding=1保持高宽不变。 - 汇聚层使用 2x2 最大汇聚,并配合
stride=2让高宽减半。 - 多个卷积层组成一个块,多个块再组成整个网络。
这就是 VGG 的魅力:结构非常整齐。
2. 为什么喜欢 3x3 卷积?
你可能会问:如果想看更大范围,直接用 5x5 或 7x7 不就行了吗?
可以,但 3x3 有两个好处。
好处一:参数更少
一个 5x5 卷积核有 25 个位置,一个 3x3 卷积核只有 9 个位置。小卷积核通常更省参数。
好处二:非线性更多
两个 3x3 卷积层叠起来,感受野大致相当于一个 5x5 卷积层,但中间多了一次 ReLU。
也就是说,网络不仅看到了更大范围,还多了一次“思考和变换”的机会。
通俗理解:
与其让一个大侦探一眼看完,不如让两个小侦探连续分析,每一步都做一次判断。
3. VGG 块:把重复结构封装起来
VGG 的代码很好理解,因为它本来就是模块化思想。
下面先看一张直观示意图,再看代码:
如何读这张图:每个 Block 先用若干个 3x3 卷积提取特征(padding=1 保持尺寸),之后用 2x2 最大汇聚把高宽减半;通道数在后面逐步增加以学习更抽象的特征。
易读表格(VGG — 按 Block 汇总):
| 层 | 类型 & 参数 | 计算公式/说明 | 输出尺寸 |
|---|---|---|---|
| 输入 | 原始图像 | — | 224 × 224 × 3 |
| Block1 Conv | 1 × Conv(3x3), out=64, p=1 | (224 - 3 + 2×1)/1 + 1 = 224 | 224 × 224 × 64 |
| Block1 Pool | MaxPool(2x2, s=2) | floor((224 - 2)/2) + 1 = 112 | 112 × 112 × 64 |
| Block2 Conv | 1 × Conv(3x3), out=128, p=1 | (112 - 3 + 2×1)/1 + 1 = 112 | 112 × 112 × 128 |
| Block2 Pool | MaxPool(2x2, s=2) | floor((112 - 2)/2) + 1 = 56 | 56 × 56 × 128 |
| Block3 Conv | 2 × Conv(3x3), out=256, p=1 | 保持 56 → 56 × 56 × 256 | 56 × 56 × 256 |
| Block3 Pool | MaxPool(2x2, s=2) | floor((56 - 2)/2) + 1 = 28 | 28 × 28 × 256 |
| Block4 Conv | 2 × Conv(3x3), out=512, p=1 | 保持 28 → 28 × 28 × 512 | 28 × 28 × 512 |
| Block4 Pool | MaxPool(2x2, s=2) | floor((28 - 2)/2) + 1 = 14 | 14 × 14 × 512 |
| Block5 Conv | 2 × Conv(3x3), out=512, p=1 | 保持 14 → 14 × 14 × 512 | 14 × 14 × 512 |
| Block5 Pool | MaxPool(2x2, s=2) | floor((14 - 2)/2) + 1 = 7 | 7 × 7 × 512 |
| Flatten | 展平 | 7 × 7 × 512 = 25088 | 25088 (一维向量) |
读这张表,抓住三个重点:
- 每个 Block 先做若干次 3x3 卷积,再用一次 2x2 最大汇聚把高宽减半。
- 越往后,特征图越小,但通道数越多,说明网络在学习更抽象的表示。
- 最后展平后的 25088 维向量,就是分类头真正接收的输入。
一句话概括:
VGG 用统一的小卷积块,不断提特征、降尺寸,最后把抽象后的信息送去分类。
from torch import nn
def vgg_block(num_convs, in_channels, out_channels):
# layers 列表用于按顺序收集本块的层
layers = []
for _ in range(num_convs):
# 每个卷积使用 3x3 卷积核,padding=1 保持高宽不变
# 这样在堆叠多个卷积层时,感受野逐步增大,但每层仍保留空间分辨率
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
# 每个卷积后接一个非线性激活,通常用 ReLU
layers.append(nn.ReLU())
# 为下一层卷积更新输入通道数
in_channels = out_channels
# 块末尾添加最大汇聚层,将高宽减半,压缩空间并保留重要特征
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
# 返回一个按序组合的模块,便于在主网络中重复使用
return nn.Sequential(*layers)
这里有三个参数:
num_convs:这个块里放几个卷积层;in_channels:输入通道数;out_channels:输出通道数。
每个 VGG 块都做两件事:
- 用若干个 3x3 卷积提取特征;
- 用最大汇聚把图片高宽减半。
4. 用 VGG 块搭网络
有了 vgg_block,搭网络就像写配置表。
conv_arch = (
(1, 64),
(1, 128),
(2, 256),
(2, 512),
(2, 512)
)
这里的意思是:前两个 Block 先各放 1 个卷积层,后面三个 Block 变成 2 个卷积层。
为什么这样设计?核心原因有两个:
- 前面的特征图还比较大,先用 1 个 3x3 卷积就足够提取边缘、纹理这类浅层特征,同时计算量不会太重。
- 越往后,图片已经被汇聚层不断缩小,信息也越来越抽象,这时就更适合叠加更多卷积层,让网络在同一个尺度上做更细致的特征组合。
可以把它概括成:
- 前面像“先扫一遍”,快速找出轮廓和局部线索。
- 后面像“再细看几遍”,把这些线索进一步组合成更复杂的语义特征。
所以,1 × Conv(3x3) 和 2 × Conv(3x3) 的区别,不是随便写的,而是在控制“计算成本”和“特征表达能力”之间做平衡:
前面少堆一点,后面多堆一点,既保留了效率,也让深层特征学得更充分。
根据 conv_arch 构建完整的 VGG 特征提取部分:
def vgg(conv_arch):
net = []
in_channels = 3
for (num_convs, out_channels) in conv_arch:
# 使用前面定义的 vgg_block,按配置堆叠
net.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(*net)
# 示例:构建 VGG 特征提取器,并接上简单的分类头(说明用途,实际训练时可替换)
vgg_features = vgg(conv_arch)
vgg_classifier_example = nn.Sequential(
nn.Flatten(),
nn.Linear(7*7*512, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 1000)
)
每一项 (卷积层个数, 输出通道数) 表示一个 VGG 块。越往后,图片的高宽越来越小,但通道数越来越多。
这很符合 CNN 的基本规律:
- 前面:图片大,特征简单,通道少。
- 后面:图片小,特征抽象,通道多。
5. VGG 的价值
VGG 不一定是最高效的网络,但它非常适合学习。因为它让我们看清了一个现代 CNN 的常见设计套路:
卷积提特征,汇聚降尺寸;重复多次后,再做分类。
更重要的是,VGG 让大家意识到:
深度网络可以由标准模块堆叠出来,结构越清晰,越容易复用和改进。
四、AlexNet 和 VGG 对比
为了让你更容易记住,我们把两者放在一起看:
| 网络 | 核心特点 | 解决的问题 | 小白记忆 |
|---|---|---|---|
| AlexNet | 更大网络、ReLU、Dropout、GPU | 证明深度 CNN 能处理复杂真实图片 | 深度学习视觉时代的引爆点 |
| VGG | 统一使用 3x3 卷积块 | 让网络结构更标准、更容易加深 | 用标准积木搭深层网络 |
AlexNet 更像“打响第一枪”,VGG 更像“整理出一套清晰施工方法”。
五、小结
这一篇你不需要记住所有层数和参数,只要抓住下面几件事:
- LeNet 适合小图片,面对复杂真实图片能力有限。
- AlexNet 通过更大模型、ReLU、Dropout 和 GPU,证明深度 CNN 可以大规模成功。
- VGG 用简单的 3x3 卷积块搭建深层网络,让 CNN 结构变得更标准。
- CNN 越往后,通常高宽越小、通道越多、特征越抽象。
如果用一句话收尾:
AlexNet 让深度学习在视觉领域爆发,VGG 让深层卷积网络变得像搭积木一样清楚。
下一篇,我们继续往前走:既然 VGG 后面的全连接层又大又重,能不能把它去掉?既然一层里到底该用 1x1、3x3 还是 5x5 卷积很难选,能不能干脆让它们一起上?
下一站:NiN 与 GoogLeNet。
(注:文档部分内容参考《动手学深度学习》)
《动手学深度学习》现代卷积神经网络:https://zh.d2l.ai/chapter_convolutional-modern/index.html
更多推荐




所有评论(0)