☰
VGGNet深度解析:3x3卷积堆叠与迁移学习实战
2026/10/16 1:02:08 网站建设 项目流程

VGGNet大概是所有CNN经典模型里,结构最“规整”的一个:清一色的3x3卷积、2x2池化,后面再接三层全连接。我最初学CNN时啃的就是它,当时觉得这模型又大又慢,后来才发现,正是这种规整让它成了最好改、最好迁移、最适合用来理解卷积网络设计逻辑的骨架。直到现在,我做新项目时仍然会把它当baseline,拿来做特征提取、迁移学习的起点都很好用。

这篇内容我会把VGGNet拆开讲清楚:它怎么从一堆小卷积核里堆出大视野、六套配置到底差在哪、参数量花到了什么地方、从头训练和迁移使用时的实操细节,以及我在项目里踩过的那些坑。不管你是刚入门CNN,还是已经跑了不少模型想补补经典底子,这篇都能直接用。

1. VGGNet为什么值得在现在重新看一遍

1.1 在所有模型都在“加深”之前,VGG先把“深”做成了标准答案

回到VGGNet提出的那个时间点,当时大家刚从AlexNet里尝到甜头,主流思路是:卷积核越大越能覆盖更大区域,所以AlexNet用了11x11的大卷积核,后面几层再用5x5、3x3。这个思路本身不奇怪,但有一个副作用:大卷积核参数多,模型容易在有限数据下过拟合,而且网络结构比较“自由发挥”,新人不容易照葫芦画瓢。

VGGNet走的是另一条路:不追求单层卷积核的尺寸,而是把多个小卷积核串起来,把网络做得更深。它的核心主张很简单,深度比单层宽度更重要。在同样的感受野前提下,用小卷积核叠加能得到更多非线性变换,并且参数量更少。这个思路在今天看来几乎是常识,但在当时属于相当反直觉的一击:不做奇怪的技巧,靠最朴素的结构加深,把准确率稳稳推了上去。

这个小细节对后来影响很大。现在看ResNet、DenseNet这些后续网络,都有一个共同特征,就是“深”。VGGNet算是把“深”变成了一个可复制的标准,而不是靠某层卷积核碰巧大一点才赢。如果你去翻各种预训练模型库,会发现很多老模型都还在用VGGNet做backbone,原因就是它普适且稳定。

1.2 3x3卷积堆叠:更少参数,更宽视野,更多非线性

VGGNet最核心的决策是:所有卷积层都用3x3卷积核,步长固定为1,padding固定为1,这样特征图尺寸在卷积前后不变;池化统一用2x2最大池化,步长为2,每经过一个池化,特征图宽高各减半。整张网络因此变得异常整齐。

3x3卷积为什么够用?两个关键点。

第一是感受野。一个3x3卷积的感受野是3x3;叠两层,感受野变成5x5;叠三层,感受野变成7x7。也就是说,用两层3x3替代一层5x5,用三层3x3替代一层7x7,最终能看到的输入区域完全一样。这个替代不是等价替换,因为感受野一样的同时还多了一层非线性激活,模型能表达的函数更复杂。

第二是参数量。假设输入和输出的通道数都是C,单层5x5卷积的参数量大约是25C²,而两层3x3卷积的参数量是2×9C²=18C²,少了接近三成。把“节省参数”和“增加非线性”放在一起,这笔账怎么看都划算:

卷积方案结构感受野参数量(输入输出通道同为C时)非线性层数
单层5x51个5x5卷积5x5约25C²1
两层3x32个3x3卷积5x5约18C²2
单层7x71个7x7卷积7x7约49C²1
三层3x33个3x3卷积7x7约27C²3

还有一个细节:为什么偏偏选3x3而不选2x2?因为2x2卷积没有中心对称的“锚点”,卷积核覆盖不到完整的上下左右邻域,做padding之后特征图对齐也麻烦;4x4是偶数尺寸也会偏移半个像素。3x3是能保留完整中心点信息的最小尺寸,这也是后来几乎所有经典网络都偏爱3x3的原因之一。

1.3 给CNN留下一套“标准脚手架”

VGGNet的经典地位不只是因为它拿了个好名次,更在于它衍生出来的工程价值。很多后续网络和检测模型都直接拿它当主干:语义分割里有基于它的全卷积结构,目标检测里也有拿它做基础特征提取器的经典做法。因为它结构规整、卷积块边界清楚,研究者很容易把前几层特征拿来拼到自己的任务网络上。

我自己在实际项目里的感受更直接:遇到一个新的视觉任务,如果想快速搭一个能跑的baseline,我会先加载VGG预训练权重,冻结卷积部分,只在后面接自己的分类头或回归头。VGG虽然没有新模型那样花哨,但它胜在可控、可预期、坑少。把它的结构完全吃透之后,再看ResNet的残差连接、DenseNet的稠密连接,都会觉得自然很多。

2. 从A到E:六套VGG配置与参数真相

2.1 一张表看懂VGG的六种网络配置

VGGNet原论文里给出了一组对照实验,从A到E共六套配置。每一套的卷积部分都用“conv3-64”这样的缩写表示,意思是这一层使用64个3x3卷积核。全连接层是相同的:4096维、4096维、最后是1000分类输出。这里的“VGG16”对应配置D,“VGG19”对应配置E。

配置权值层总数卷积层数卷积块结构是否含1x1卷积常见称呼
A11864, 128, 256, 512, 512否VGG11
A-LRN11864, 128, 256, 512, 512否加LRN的VGG11
B131064x2, 128x2, 256x2, 512x2, 512x2否VGG13
C161364x2, 128x2, 256x3, 512x3, 512x3是(两处)VGG16含1x1变体
D161364x2, 128x2, 256x3, 512x3, 512x3否VGG16
E191664x2, 128x2, 256x4, 512x4, 512x4否VGG19

注意权值层总数是卷积层加全连接层。配置D有13个卷积层和3个全连接层,所以叫VGG16;配置E有16个卷积层加3个全连接层,所以叫VGG19。很多人会记成“有16个卷积层”,这是最常见的误解。

2.2 为什么D和E成了大家口中的VGG16与VGG19

论文把六套配置都做了对照实验,结论很有参考价值。

A-LRN和A对比,说明当时流行的局部响应归一化(LRN)在VGG这种全小卷积核的结构里几乎没用。大卷积核的相邻特征响应差异明显,LRN能起到一定的局部抑制效果;小卷积核覆盖区域本来就不大,LRN反而显得多余。后来大家也基本不在VGG里加LRN,这个操作就被边缘化了。

B比A深了两层,准确率提升明显,说明在同样的卷积核尺寸下,层数加深确实有用。C在部分位置用1x1卷积增加非线性,可以看作“用零参数量增加深度”的尝试,但实测下来C不如D,说明真正的3x3卷积比1x1卷积更能丰富特征,不能光靠堆非线性层数偷懒。

D和E之间的差距没有想象中大,多三层卷积换来的提升有限,但参数量和计算量增加不少。这也是为什么工程上VGG16比VGG19用得更普遍。我自己做迁移学习时也更偏爱VGG16,权重文件相对小一点,加载速度、推理速度都更友好,性能基本不输VGG19。

2.3 亲手把VGG16的参数量算一遍

光看网络结构图很难建立真实体感,不如实打实算一遍参数量。以配置D,也就是VGG16为例,输入是224x224的RGB图像。

单个卷积层的参数量公式:

参数数 = (卷积核高 × 卷积核宽 × 输入通道数 + 1) × 输出通道数

第一层卷积输入是3通道,输出64通道:3×3×3×64,再加上64个bias,等于1792个参数。第二层卷积输入是64通道,输出还是64通道:3×3×64×64,加上64个bias,等于36928个参数。

按这个思路把五段卷积块全算出来:

网络部分具体结构参数量(约)
Block1conv3-64 x23.9万
Block2conv3-128 x222.1万
Block3conv3-256 x3147.5万
Block4conv3-512 x3590.0万
Block5conv3-512 x3590.0万
FC17×7×512 → 40961.027亿
FC24096 → 40961678.1万
FC34096 → 1000409.7万

全部卷积部分合计约1353.5万,全连接部分合计约1.236亿,总参数约1.37亿到1.38亿。用float32保存,每个参数占4字节,整个模型权重文件就要550MB左右。放到现在看这个体积相当夸张,但这就是VGG16的真实体量。

FC1的1.027亿参数是绝对的参数量大头。它的输入是最后一层池化输出的7×7×512特征图,展平后是25088维,乘以4096个神经元,再算上bias,数字一下就上去了。

2.4 参数量分布带来的设计启示

算完这笔账,很多设计取舍就看得懂了。

全连接层占了将近九成参数量,但它在网络里只处理最后一层的小特征图。换句话说,VGG16的大部分容量消耗在“把高级特征映射到分类空间”,而不是特征提取本身。这带来了两个直接结果:一是模型文件很大,部署不友好;二是如果任务只需要特征提取,保留卷积部分、甩掉全连接层,模型体积和计算量都会大幅下降。

后续很多网络不约而同地用全局平均池化代替全连接层,出发点就是砍掉这块参数量黑洞。你也可以把VGG当作一个警示案例:结构整齐不等于效率最优,使用经典网络时要有“哪些部分是为当时的数据集设计的、哪些部分可以改掉”的判断力。

3. 从零训练一个VGG16:数据、超参与复现细节

3.1 数据预处理不是随便resize就完事

如果你打算从头训练VGG16,第一步不是写模型,而是想清楚数据怎么喂进去。原论文的做法,是把图像等比缩放到短边为256或384,然后随机裁剪出224x224的patch,同时做水平随机翻转和颜色扰动。测试时则用中心裁剪。还有一个叫尺度抖动(scale jittering)的技巧:每张图缩放尺寸在一定范围内随机变化,相当于变相做数据增强。

很多新手在这里会漏掉一个关键点:归一化。常见做法是用训练集统计出来的通道均值和标准差,把像素值归一化到零均值、单位方差附近。这个统计量一定要从训练数据里算,不能拍脑袋用0到1缩放代替。不同数据集的颜色分布差异很大,用统一的ImageNet式均值也不是万能的;如果你的图像是灰度医学影像或者卫星图,最好重新统计。

还有一点容易踩:训练时做了随机裁剪和翻转,测试时就只做中心裁剪,不再做随机增强。归一化的均值标准差要固定保存下来,部署时也必须用同一套参数。否则训练和推理的输入分布不一致,模型效果会莫名其妙地差。

3.2 一套可以直接抄的训练参数

VGGNet最初那篇论文给出的训练配置,后来被很多复现实验当作默认参数。我这里直接列出来:

  • 优化器:SGD,momentum=0.9
  • 权重衰减:5e-4
  • 初始学习率:0.01
  • batch size:256
  • 学习率调整:验证集准确率不再上升时,学习率除以10
  • 全连接层使用dropout,比例0.5
  • 总训练epoch:约74轮(在百万级数据上)

现在的框架默认初始化方式一般是He初始化或Xavier初始化,和论文里的小方差高斯初始化稍有差别,实际跑起来问题不大。但有几个点要和你实际项目匹配:如果你的数据量远没有百万级,学习率0.01大概率太大,建议从0.001起步;如果多卡训练,batch size放大了,学习率可以适当同步放大。

从零训练VGG16的时间成本是实打实的高。即使现在有GPU加速,想在百万级数据复现论文水平,一张显卡也要按天计算。我的建议是:除非你的研究就是针对VGG结构本身,否则没必要从零开始,直接加载预训练权重更划算。

3.3 复现时最容易翻车的三个细节

第一,搞混“VGG16”含义。它一共16个权值层,其中13个卷积层加3个全连接层,不是16个卷积层。写代码前先数清楚配置表,避免网络深度对不上预训练权重。

第二,dropout要开关正确。VGG在全连接层间用了dropout,训练时要开启,验证和推理时要关闭。现在主流框架用model.train()和model.eval()来切换,新手很容易忘了切回eval模式,导致推理结果稳定性差,你还会误以为是模型没训好。

第三,权重衰减应用在所有可学习参数上。很多人默认只对卷积和全连接权重做weight decay,忽略了bias,结果可能和原论文不一致。更稳妥的做法是统一对全部参数做相同的weight decay,除非你的框架默认配置明确把bias排除并取得了更好效果。

3.4 算力账本:VGG的FLOPs与显存开销

VGG16输入224x224图像时,理论计算量大约在15.5 GFLOPs左右,不同框架统计口径会有差异。这个数字在现在不算夸张,但要注意它的分布:计算量主要消耗在前面的卷积层,因为浅层特征图分辨率还很高;参数量主要消耗在全连接层。两者错开,导致VGG“又大又慢”的名声。

如果你在单卡上训练VGG16,batch size开256基本是想都不用想的事,显存会直接溢出。常见做法是batch size降到32甚至16,配合梯度累积来模拟大batch效果;再往上可以开混合精度训练,把显存占用和训练时间一起降下来。推理时如果显存也紧张,可以用半精度或者动态量化。

如果只是做特征提取,实际显存会小很多,因为不需要保存梯度;但你也别一次性把超大分辨率图直接送进VGG,它前几层卷积是基于固定输入尺寸设计的,后期换分辨率需要对结构做适配。

4. 新项目里真正在用的VGG:迁移学习与特征提取

4.1 冻结主干:把VGG当成免费且稳定的特征提取器

现在的实际项目里,很少有人再从零训VGG了,更多是把它当预训练特征提取器。这个思路成立的原因是,卷积网络前几层学到的是边缘、纹理、颜色块这类通用特征,迁移到新任务时仍然有效。你只需要把最后一层分类头换掉,重新训练一个适合自己类别数的新分类层。

操作起来很直接:加载预训练权重后,把最后一个全连接层的输出维度改成自己的类别数;把前面所有卷积层参数设为不可训练;只让新的分类头更新参数。这样一来训练量非常小,几百张图也能跑出能用的结果。

# 思路示意,具体API按你使用的深度学习框架调整 model = load_pretrained_vgg(depth=16) # 替换最后的分类输出,改成自己的类别数 model.classifier[-1] = nn.Linear(4096, num_classes) # 冻结卷积主干,只训练分类头 for name, param in model.named_parameters(): if "classifier" not in name: param.requires_grad = False

这里有个我踩过的坑:如果你的新任务图像风格和预训练数据差太远,直接冻结主干不一定好用。比如换到医学影像、红外图像、航空图像这类领域,低层特征的复用价值会大打折扣,最好把主干后半段也放开微调,或者干脆用领域内数据重新预训练。

4.2 全量微调:什么时候做,怎么做安全

数据量稍微多一点,比如有几千张图甚至几万张,就可以考虑对全网络微调。微调的黄金法则是:主干学习率要小,新分类头学习率可以大一些。比如主干用0.0001,分类头用0.001。因为预训练权重已经在一个大数据集上学到了不错的特征,你只需要轻微调整适配新任务;如果把主干和新头用同一个学习率,很容易把预训练信息冲掉,造成灾难性遗忘。

微调时的另一个大坑是BatchNorm。VGG的标准化版本在卷积层之间加了BN层,这些BN层在训练时会不断更新running mean和running variance。如果微调时batch size太小,统计量会剧烈波动,导致训练不稳定。一个保守做法是batch size尽量大于16,如果实在做不到,就把BN层冻结,让它沿用预训练统计量。

还有一个经验法则:数据越少,微调越要小心。只有几百张图时,只动分类头比全量微调可靠得多。数据上万时,全量微调一般能带来明显收益。如果拿不准,直接做一组对比实验:一组只训分类头,一组全量微调,选验证集表现好的。

4.3 把全连接层换成全局平均池化,模型体积立减

VGG16全连接部分占了1.2亿参数,对一个只做特征提取或部署到端侧的场景来说,这个开销纯属浪费。一个非常经典的改法,是把三个全连接层整体删掉,改为全局平均池化(GAP)。

GAP的思路是:对最后一层卷积输出的每个通道,在空间维度上做平均,得到一个和通道数等长的向量。比如最后一个卷积块输出512个通道,GAP后就得到512维向量。这个向量直接后续接一个分类层,参数量只有512×类别数,几乎可以忽略。

操作步骤也不复杂:

  1. 去掉VGG原来的全连接分类器;
  2. 保留所有卷积部分,让最后一个池化层输出7×7×512的特征图;
  3. 在卷积特征图后加一个全局平均池化,得到512维描述子;
  4. 后面接一个线性层或1x1卷积,映射到目标类别数。

改完之后,模型参数量从1.38亿降到约1400万级别,模型文件从550MB降到50MB左右,效果在大多数分类任务上下降很小,有时甚至因为去掉了过拟合风险反而更稳。如果你做检索或特征比对,就更应该用GAP版本,拿512维特征向量直接做距离度量。

4.4 中间层特征与类激活图可视化

把VGG用起来之后,免不了要看看网络到底学到了什么。最朴素的方法是导出中间层的特征图,观察不同卷积层对输入图像的响应。VGG的卷积部分天然适合做这个事,因为每一层输出都是有序的特征图张量。

从工程上讲,导出特征图前一定要把模型切到eval模式,并把requires_grad关掉。否则BN层统计量和dropout都会干扰输出,还白白占显存。特征图往往有几十上百张,直接拼接看容易糊,可以挑几个关键通道,或者对全部通道取均值之后再可视化。

如果想做类激活图(CAM),一个简单路径是:模型用GAP替代全连接层后,取最后一层卷积输出的特征图,用分类层对应某个类别的权重,对特征图做加权求和,再叠加到原图上。这个操作能直观看出模型是根据图像哪个区域做出判断的,对排查误分类帮助很大。

5. 跑VGGNet时我踩过的坑:问题排查与避坑指南

5.1 loss不降,先从过拟合一个小batch开始查

训练VGG时最常见的症状,就是loss从头到尾纹丝不动。很多人第一反应是调学习率、换优化器,忙活半天也没用。我的排查顺序是:先拿一小部分数据,比如32张图,单独训练一个batch,看看模型能不能过拟合。

如果连32张图都过拟合不了,大概率是网络结构实现有bug,比如某个卷积层通道没对上、标签错位、归一化写错。如果小数据能过拟合,但全量数据不收敛,那才是超参问题,优先减小学习率或调大weight decay。这个方法适用于所有CNN训练,VGG也不例外。

还有一种情况是loss降到一个值就卡住,然后用验证集判断发现模型输出全是某一类。这是典型的类别不均衡问题,和VGG结构无关。先检查标签分布,再考虑加权损失或者重采样。

5.2 显存爆掉时的急救顺序

VGG的显存占用非常高,原因是卷积层特征图大,训练时还要保存中间梯度。如果一跑就OOM,按这个顺序处理:

  • 先降batch size,这是最直接的;
  • 开混合精度训练,显存能省将近一半;
  • 用梯度累积,用小batch跑几步再更新一次权重,等价模拟大batch;
  • 检查代码里有没有在batch循环内无意保留了整批特征图或梯度,及时释放;
  • 最后才考虑换更小的输入分辨率。224已经是原设计标准,再低会影响预训练权重效果。

有一点要提醒:混合精度虽然省显存,但VGG的全连接层对精度比较敏感,个别时候会出现数值不稳。开启前先跑几个step看loss是否正常,发现异常就关掉,或者只在卷积部分使用混合精度。

5.3 模型“大”的根本原因与部署思路

部署VGG时最头疼的就是模型文件大。别急着上各种压缩技术,先想清楚你要部署的到底是分类任务还是特征提取任务。

如果只是分类,量化到INT8通常能把模型压到150MB以内,精度损失在1-2%以内;进一步可以用剪枝把全连接层的冗余神经元删掉,因为它们对最终结果贡献很小,剪掉一部分后微调几轮就能恢复。如果是端侧项目,最常用的反而是结构改造,把全连接层换成GAP版本,模型体积直接断崖式下降。

如果任务是特征提取,更好的思路是“用完即扔”:离线把VGG的输出特征抽出来存成文件,线上只做特征比对,不再加载VGG模型。这听着像废话,但我真见过业务团队在线上实时跑VGG提取特征,结果延迟和成本都拉满。能用离线特征解决的,就别在线反复算。

5.4 归一化与train/eval模式不一致的隐形坑

这个坑比较隐蔽,我头一回踩的时候排查了两天才发现。训练时你可能会把图像归一化到均值0、标准差1,或者用特定统计量;推理代码里如果直接读原始像素,只做了简单的0-1缩放,那模型表现会明显变差。问题不在模型,而在预处理不一致。解决方法是把预处理逻辑和模型权重一起导出,写成一个统一入口,所有人都调用同一个预处理函数。

另一个坑出现在提取特征时:模型处于train模式和eval模式,同一张图得到的特征可能完全不同。VGG里的dropout在train模式会随机丢弃全连接层神经元,BN层在train模式还会根据当前batch更新统计量。如果你忘了切eval模式,特征会带随机性,特征库和查询特征对不上,检索准确率突然下降。每次加载模型后先确认模式,再跑推理,这个习惯能省掉大量无效排查。

最后再分享一个我自己的经验:做任何新任务之前,先用VGG跑一个最朴素的baseline,比直接上复杂模型要高效得多。它结构透明,出了问题容易定位,改起来也不心疼;等VGG这个baseline稳定了,再去试ResNet或Transformer类模型,你会对“新模型到底赢在哪”有更清楚的认识。经典的意义不在于一直用它拿最高分,而在于它替你划出了一条可靠的起点线。

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

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

立即咨询