☰
VGGNet经典CNN架构详解:3x3卷积堆叠与迁移学习实践
2026/10/15 14:03:03 网站建设 项目流程

提到卷积神经网络,VGGNet是绕不开的名字。2014年它出现在ImageNet大规模图像识别竞赛中,用一套极简的设计——全是3x3卷积加池化——拿下了分类和定位两个方向的头名。即使过去这么多年,VGGNet依然是很多人接触深度卷积网络时看的第一个完整模型,也是我做迁移学习和特征提取时最常用的基础骨架之一。这篇文章把VGGNet的原理拆开讲清楚:它为什么要用3x3小卷积核,不同深度的配置有什么区别,参数都堆在哪里,以及我实际训练时会踩哪些坑。

如果你是刚入门深度学习、想理解CNN基础结构,或者做项目时需要选一个通用特征提取网络,这篇应该能让你省不少摸索时间。

1. 先把问题背景说清楚:VGGNet到底解决了什么

1.1 2014年之前CNN的尴尬处境

在VGGNet出现之前,主流的CNN设计多少带点"手工味道"。2012年刷新ImageNet纪录的网络,核心思路是"大卷积核+分组结构+局部响应归一化",整体上像是在尝试各种技巧的拼盘。11x11的大卷积核、5x5的中等卷积核都被直接拿来用,网络的前几层感受野很大,但参数量也很惊人。

那个阶段大家心里其实有一个模糊的共识:网络越深,效果越好。但问题在于,没人搞清楚"怎么加深"才是正确姿势。是简单地把所有层数翻倍?还是加一些奇奇怪怪的并行分支?当时没有人给出一个系统性的答案。

1.2 VGGNet给出的回答:规则化加深

VGGNet的核心主张非常朴素——把设计规则收敛到极致,然后靠深度取胜。整个网络从头到尾只做三件事:堆3x3卷积、每隔一段接一次池化、最后接全连接层做分类。没有花哨的分支结构,没有复杂的归一化技巧,就是把"深度"这件事做到了当时的极致。

这个选择的聪明之处在于,它把"网络结构设计"从一个自由度过高的工程题,变成了一个有明确规律可循的堆叠题。你用同样的VGG块往深了叠,叠16层就是VGG16,叠19层就是VGG19。这句话听起来简单,但在当时的背景下,等于给整个学术界提供了一个干净、可控、可复现的基准框架。

我个人觉得,VGGNet最大的贡献不在于它拿了几个第一名,而在于它证明了一件事:在算力允许的范围内,规则化的深度堆叠就能带来稳定的精度提升。这个结论直接推动了后来ResNet、DenseNet等一系列"更深的网络"的研究方向。

2. 骨架拆解:为什么偏偏全是3x3小卷积核

2.1 感受野的数学账本

VGGNet最标志性的设计就是全网络只用3x3卷积核。刚接触的人第一反应通常是:小卷积核感受野不够大,是不是丢失了全局信息?这里有一个很关键的计算。

两个连续不padding的3x3卷积,等效感受野是5x5;三个连续的3x3卷积,等效感受野是7x7。也就是说,你完全可以用"三个3x3卷积"替代"一个7x7卷积",感受野完全一致,但参数量和非线性能力完全不同。

假设输入通道数为C,一个7x7卷积的参数量是7x7xCxC = 49C²,而三个3x3卷积的参数量是3x(3x3xCxC) = 27C²。在感受野相同的前提下,参数量直接省了约45%。这笔账算下来,小卷积核的优势就不是玄学,而是实打实的数学结论。

2.2 非线性能力被低估的部分

参数量只是一半的故事。三个3x3卷积之间各带一个ReLU激活,而一个7x7卷积只有一个ReLU。更多的非线性层意味着网络在同样的感受野下可以拟合更复杂的特征映射。

打个不太严谨但容易理解的比方:同样的信息,让三个人接力各看一遍,比让一个人一次性全看完,能捕捉到的细节更多。这一点在浅层可能差异不明显,但网络越深,非线性层数的累积优势就越突出,这也是VGGNet虽然结构简单但判别力强的原因之一。

2.3 配置C里的1x1卷积是干什么的

VGGNet的配置C里面混入了一种特殊情况:部分卷积层用了1x1卷积核。很多人不理解为什么VGG这种"纯3x3主义"的网络会掺进1x1。

1x1卷积不改变感受野,但可以做跨通道的线性组合。在配置C里加1x1卷积的目的是在不增加感受野的前提下提升网络非线性表达能力,同时控制参数量。它相当于对每个像素位置做了一次"通道间的加权融合",为后续3x3卷积提供更丰富的输入表示。

不过实测中配置C的提升并不明显,配置D(也就是VGG16)才是性价比最高的版本。这也说明1x1卷积虽然有用,但对VGG这个结构本身而言,加深3x3卷积的收益更直接。

3. 完整配置对照:从A到E的五套网络

3.1 配置细节和参数量对比

VGGNet原文一共提了六个配置,常用的是A、B、D、E这四套。我用表格整理一下核心差异:

配置卷积层总数网络别名特点参数量约
A8VGG11最浅版本,适合快速实验1.33亿
B10VGG13在A基础上增加3x3卷积1.33亿
C12VGG16(带1x1)混入1x1卷积1.34亿
D13VGG16(经典)最经典的均衡版本1.38亿
E16VGG19最深版本,参数量最大1.44亿

每一组卷积块后面接一个2x2的最大池化,步长为2,将特征图尺寸减半。从输入224x224开始,经过五组池化后,最后的特征图尺寸是7x7,然后进入全连接层。

3.2 参数量的大头竟然在全连接层

很多人第一次算VGGNet参数量时都会愣一下:卷积层的参数量居然不是主力?以VGG16为例,总参数量约1.38亿,其中最后三个全连接层的参数量约占80%以上。

第一个全连接层,输入是7x7x512,输出是4096,这一层就有大约1.03亿参数。后面两个全连接层单个都有约1670万参数。卷积层部分即使堆了13层,总参数量也不过2000多万,比三个全连接层少得多。

这也是后期大家优化VGG结构时几乎都会动刀的地方:把全连接层换成全局平均池化,或者直接把第一个全连接层的通道数砍掉。参数总量骤降,精度损失却很小,这正是很多轻量化网络设计的基本思路。

3.3 训练时的特殊处理和技巧

VGGNet训练阶段有一个容易被忽略的细节:它先训练配置A这个较浅的网络,然后用A的权重初始化更深配置的前面层,再继续训练。这个操作放在今天看起来很原始,但当时确实帮助深层网络解决了收敛不稳定问题。

另外VGGNet原论文用了多尺度的数据增强:训练图片在被裁切成224x224之前,会先被缩放到一个随机尺寸,尺寸范围从256到512。相当于同一张图在训练中以不同尺度反复出现,提升了模型对物体尺度变化的容忍度。

我在实际复现时试过直接用随机初始化训练VGG19,在小数据集上收敛很不稳定,损失经常出现平台期。换成先训练VGG11再加载权重的方案后,整个训练过程顺畅非常多,这个"由浅入深"的思路到今天依然值得借鉴。

4. 从零搭建VGGNet:一份可直接复用的PyTorch实现

4.1 网络结构代码

VGGNet的实现并不复杂,核心就是用列表定义每组卷积块,然后循环堆叠。下面这份代码我按配置D(VGG16)写的,可以根据需要自己改配置:

import torch import torch.nn as nn # 定义五组卷积块结构:M表示最大池化,数字表示卷积层输出通道数 cfgs = { 'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'], 'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'], 'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'] } def make_layers(cfg, batch_norm=True): layers = [] in_channels = 3 for v in cfg: if v == 'M': layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) else: conv = nn.Conv2d(in_channels, v, kernel_size=3, padding=1) if batch_norm: layers.append(conv) layers.append(nn.BatchNorm2d(v)) layers.append(nn.ReLU(inplace=True)) else: layers.append(conv) layers.append(nn.ReLU(inplace=True)) in_channels = v return nn.Sequential(*layers) class VGGNet(nn.Module): def __init__(self, cfg='D', num_classes=1000, init_weights=True): super(VGGNet, self).__init__() self.features = make_layers(cfgs[cfg]) self.avgpool = nn.AdaptiveAvgPool2d((7, 7)) self.classifier = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, num_classes) ) if init_weights: self._initialize_weights() 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.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) def forward(self, x): x = self.features(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.classifier(x) return x # 实例化VGG16 model = VGGNet(cfg='D', num_classes=10) print(model)

这段代码有几个值得注意的细节。conv层我加了padding=1,这样3x3卷积不会改变特征图尺寸,尺寸变化只由池化层控制,符合VGGNet原设计。BatchNorm是后来常见增强,原版网络没有,但实际操作中加上能让训练稳定很多,我建议默认开。

4.2 训练前的数据处理流程

VGGNet输入尺寸是224x224。训练时建议做随机裁剪和水平翻转,这是最基本的图像增强组合。我自己常用的流程是:先缩放到256,再随机裁剪到224,以增加位置不变性;推理时则直接中心裁剪到224。

如果数据量不算大,我建议再加一点随机亮度对比度扰动。VGGNet对颜色变化比较敏感,适当地扰动颜色通道能让模型泛化更好。但注意不要加太强的旋转或缩放增强,VGGNet这类全卷积加全连接的结构对几何形变的鲁棒性一般。

4.3 训练配置的实际建议

优化器用SGD加动量就行,不需要花哨的Adam。学习率初始设0.01,batch size设为64或128,配合阶梯下降策略。我在单卡环境下用这个小模型训练CIFAR-10或自建数据集,通常60到80个epoch能收敛到不错的效果。

如果你显存有限,有几个立竿见影的调整方案:把输入分辨率缩到160或128,全连接层的4096换成2048,或者直接用全局平均池化替代第一个全连接层。这三个改动叠加起来,参数量能减少一大半,精度只掉一两个点。

5. 实际训练VGGNet的常见问题和排查技巧

5.1 显存爆炸:改成小batch还是换结构

VGGNet最大的槽点之一就是显存占用高,尤其是最后一个全连接层和前面的512通道卷积特征图叠在一起,训练时中间激活值非常吃显存。我实际遇到的情况:batch size设成128,一张12GB的卡直接报OOM。

我的建议是优先降低batch size到32,同时打开混合精度训练,这样大多数情况下能救回来。如果还不行,就该考虑替换全连接层了。全局平均池化接一个全连接输出,显存占用能降一半以上,精度损失常常在可接受范围内。

5.2 过拟合:Dropout和权重衰减配合使用

VGGNet的参数量巨大,在小数据集上非常容易过拟合。训练集loss降到很低,验证集loss却不降反升,这是最常见的翻车现场。

VGG结构里自带了Dropout,默认是0.5,但光靠它不够。我的经验是再加weight decay,也就是L2正则化,设到5e-4左右。这两个手段配合起来,过拟合情况会明显缓解。如果你在更小的数据集上训练,还可以把第一个全连接层的Dropout提到0.7,效果更狠但小心欠拟合。

5.3 训练收敛慢:注意学习率和初始化方式

很多人在小数据集上训练VGGNet,发现loss降得很慢,半天看不到精度上升。这里最常见的原因是学习率设置太大或太小,其次就是初始化方式不对。

VGGNet原论文用的是方差较小的随机初始化,但在现代框架里我更建议Kaiming初始化,尤其是配ReLU的时候。如果用了预训练权重做迁移学习,记得把新分类层的学习率调到主干层的十倍左右,这样分类层能快速收敛,主干层又不会剧烈震荡。

5.4 常见问题速查

现象原因排查方向
训练时显存OOM全连接层参数和激活值过大减小batch、开混合精度、换全局平均池化
验证集loss上升过拟合提高Dropout、加权重衰减、增加数据增强
初始loss不下降学习率不当或初始化问题检查学习率、换成Kaiming初始化
浅层输出全为常数梯度消失检查是否加了BatchNorm、尝试加载浅层预训练权重
推理速度太慢参数量过大剪枝全连接层、推理时用半精度

6. VGGNet的影响和它留下的技术遗产

6.1 对后续网络结构的深远影响

VGGNet的"规则化堆叠"思想直接影响了后续几乎所有主流CNN结构。ResNet提出残差连接时,主干部分就直接复用了VGG的"3x3卷积+池化"的堆叠思路;后来大量轻量化网络做设计时,第一件事也是先看VGG各个配置的参数量和计算量分布。

而且VGGNet为"预训练权重迁移学习"提供了一个极好的基底。预训练好的VGG16在图像分类、目标检测、语义分割三大任务上都被广泛用作特征提取器。即使到了今天,很多视觉类项目做特征对比实验时,依然把VGG系列作为基准之一。

6.2 迁移学习时的实际用法

用VGGNet做迁移学习的标准套路是这样的:加载在ImageNet上预训练好的权重,冻结前面的卷积层,只训练后面的全连接层或新增分类层。这种方式在小数据集上效果立竿见影,训练也很快。

我做场景分类项目时,经常把VGG16的最后几层卷积解冻,用较小的学习率微调,效果比冻结全部卷积层要好不少。关键在于前几层学到的是通用边缘和纹理特征,后几层学到的更多是任务相关的语义特征,所以微调要偏重后面层。

6.3 我的个人体会

VGGNet结构老,但并不等于过时。它更像一把标尺,帮你理解CNN里"深度、宽度、参数、感受野"这几者之间的关系。我见过不少人一上来就折腾各种复杂结构,结果连最基本的参数量、感受野都算不清。如果有时间,把VGGNet从头搭一遍、训练一遍,比看十篇网络结构解读都有用。

最后再分享一个实操小技巧:用VGGNet做特征提取时,不要把最后一个池化层的输出直接当特征,试着取倒数第二层全连接层之前的输出,也就是Flatten之后的那一层。这一层特征经过高层语义整合但还没有被分类层强耦合,做图像检索或者相似度计算的时候,效果比直接用分类特征好得多。

这个内容后续想继续扩展的话,可以试着给VGGNet换上深度可分离卷积做轻量化,或者把全连接层换成注意力机制再看看效果。但不管做多少改造,VGGNet这套"小卷积核+规则堆叠"的设计思路始终是理解现代CNN最好的起点之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询