经典 CNN 网络 VGG
一、VGG 网络诞生背景
VGGNet 由牛津大学视觉几何组 VGG 联合 Google DeepMind 提出,亮相于 2014 年 ImageNet 图像分类竞赛 ILSVRC-2014,最终拿下亚军,当年冠军为 GoogLeNet。
日常大家口中的 VGG,默认指VGG-16,包含 13 层卷积层 + 3 层全连接层,总权重层 16 层;另有加深版本 VGG-19(19 层权重)。 对比初代深度 CNN AlexNet,VGG 核心优势:
- 网络深度更深,特征提取能力更强;
- 结构规整、模块化,易复现、易迁移;
- 泛化性能优秀,后续检测、分割任务大量用作骨干网络; VGG-16 总参数量约 1.38 亿,相比 AlexNet 参数量更大,对硬件算力、存储有更高要求。
二、VGG 全套网络配置(6 种结构 A/A-LRN/B/C/D/E)
论文一共给出 6 种不同深度的 VGG 结构,区别仅在于卷积层堆叠数量:
表格
| 结构 | 权重层数 | 核心差异 | 总参数量(百万) |
|---|---|---|---|
| A | 11 层 | 最少卷积,无重复堆叠 | 133 |
| A-LRN | 11 层 | 在 A 基础上增加 LRN 局部归一化层 | 133 |
| B | 13 层 | 增加两组 3×3 卷积堆叠 | 133 |
| C | 16 层 | 在 B 基础上插入 1×1 卷积 | 134 |
| D(VGG16) | 16 层 | 全部使用 3×3 卷积堆叠,工业最常用 | 138 |
| E(VGG19) | 19 层 | 进一步加深卷积堆叠层数 | 144 |
实验证明两点关键结论:
- AlexNet 中的 LRN 局部响应归一化层提升极小,后续主流 VGG16/19 全部舍弃;
- 单纯堆叠 3×3 卷积比插入 1×1 卷积效果更好,因此 VGG16 (D) 成为标准版本。
三、VGG 核心结构:VGG Block 模块化设计
VGG 最标志性设计就是VGG 块(vgg-block),整套网络由重复的卷积块 + 池化层串联而成,结构高度统一: 单个 Block 标准流程:多层3×3卷积(ReLU激活) → 2×2最大池化MaxPool
卷积统一超参(所有卷积层通用)
- 卷积核:3×3
- 步幅 stride=1
- 填充 padding=1 作用:卷积前后特征图宽高尺寸不变,方便多层堆叠。
池化统一超参(所有池化层通用)
- 池化核:2×2
- 步幅 stride=2 作用:每经过一个 Block,特征图宽、高减半,尺寸规整递减:
224 → 112 → 56 → 28 → 14 → 7
四、VGG16 逐层完整维度拆解(输入 224×224×3 RGB 图)
VGG16 一共 5 个 VGG 卷积块,5 次下采样池化,末尾接三层全连接 + Softmax 输出。
Block1(2 层 Conv)
- 输入:224×224×3 → Conv3-64 → 224×224×64 + ReLU
- Conv3-64 → 224×224×64 + ReLU
- MaxPool2×2/2 → 输出 112×112×64
Block2(2 层 Conv)
- 输入:112×112×64 → Conv3-128 → 112×112×128 + ReLU
- Conv3-128 → 112×112×128 + ReLU
- MaxPool2×2/2 → 输出 56×56×128
Block3(3 层 Conv)
- 输入:56×56×128 → Conv3-256 ×3 → 56×56×256
- MaxPool2×2/2 → 输出 28×28×256
Block4(3 层 Conv)
- 输入:28×28×256 → Conv3-512 ×3 → 28×28×512
- MaxPool2×2/2 → 输出 14×14×512
Block5(3 层 Conv)
- 输入:14×14×512 → Conv3-512 ×3 → 14×14×512
- MaxPool2×2/2 → 输出 7×7×512
全连接分类头
- Flatten 展平:7×7×512 = 25088 一维特征
- FC1:4096 神经元 + ReLU + Dropout
- FC2:4096 神经元 + ReLU + Dropout
- FC3:1000 神经元(ImageNet1000 分类)
- Softmax:输出各类别概率
模型代码
import torch from torch import nn from torchsummary import summary class VGG16(nn.Module): def __init__(self): super(VGG16,self).__init__() self.block1 = nn.Sequential( nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,padding=1), nn.ReLU(), nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2,stride=2) ) self.block2 = nn.Sequential( nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2) ) self.block3 = nn.Sequential( nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,padding=1), nn.ReLU(), nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2,stride=2) ) self.block4 = nn.Sequential( nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,padding=1), nn.ReLU(), nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2,stride=2) ) self.block5 = nn.Sequential( nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=2,stride=2), ) self.block6 = nn.Sequential( nn.Flatten(), nn.Linear(7 * 7 * 512, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, 10), ) for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, nonlinearity='relu') if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) if m.bias is not None: nn.init.constant_(m.bias, 0) def forward(self,x): x = self.block1(x) x = self.block2(x) x = self.block3(x) x = self.block4(x) x = self.block5(x) x = self.block6(x) return x if __name__ == "__main__": device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model=VGG16().to(device) summary(model,(1,244,244))五、VGG 四大核心设计亮点
1. 全程使用 3×3 小卷积核,替代大尺寸卷积
以往 AlexNet 使用 11×11、5×5 大卷积,VGG 全部统一 3×3:
- 两层 3×3 卷积堆叠感受野等价于 1 层 5×5 卷积;三层 3×3 等价 7×7;
- 多层非线性激活叠加,特征拟合能力更强;
- 同等感受野下,3×3 组合参数量更少、计算更高效。
2. 模块化 Block,结构极简易扩展
5 组结构相似的卷积块重复堆叠,只需要调整每个 Block 内卷积层数,就能轻松实现 VGG11/VGG16/VGG19,工程实现、网络修改成本极低。
3. 特征图尺寸规整递减,便于计算与调试
依靠padding=1、stride=1卷积保尺寸,2×2池化stride=2固定减半,每层输出维度可精准推导,调参、可视化特征更友好。
4. 迁移学习能力极强
虽然 VGG 参数量大、推理速度慢,但浅层卷积提取通用边缘、纹理特征,深层提取高级语义特征,至今仍是图像分类、目标检测、语义分割任务中最常用的预训练骨干网络之一。
六、VGG 网络优缺点总结
优点
- 结构简单、逻辑清晰,新手友好,极易代码复现;
- 小卷积堆叠带来更强非线性表达,分类精度高;
- 预训练权重通用性强,迁移学习效果稳定;
- 网络分层规律,特征可视化、消融实验方便。
缺点
- 参数量巨大(VGG16 1.38 亿),大量参数集中在全连接层,内存占用高;
- 全连接层计算量大,推理速度慢,不适合移动端实时部署;
- 无残差结构,深度无法无限加深,对比后续 ResNet 上限更低;
- 没有分组卷积、空洞卷积等轻量化设计,算力开销大。
七、VGG 实战使用场景
- 图像分类 baseline:小型数据集训练首选预训练 VGG,微调成本低;
- 目标检测骨干:早期 Faster R-CNN 标配骨干网络;
- 图像分割:FCN、U-Net 早期版本使用 VGG 提取编码器特征;
- 特征提取:无需分类头,截取卷积层输出做图像检索、相似度匹配。
