1. 从一张图片到一组特征:VGG到底在做什么
很多人第一次接触VGG,是在迁移学习的教程里——把预训练权重加载进来,冻住卷积层,只训练最后的分类头。代码跑通了,准确率也不错,但问起“VGG的卷积过程到底发生了什么”,脑子里只有一堆Conv2d、MaxPool2d和ReLU的堆叠,具体每一层在干什么、为什么这样设计,说不清楚。这篇内容就是冲着这个问题来的:把VGG的卷积过程从输入到输出逐层拆开,讲清楚每一步的计算逻辑、设计动机和实际影响。
VGG的核心关键词其实就几个:3×3小卷积核、深度堆叠、通道数翻倍、最大池化降采样、全连接分类头。这些词你可能都见过,但它们之间是怎么配合的、为什么VGG选择了一条和AlexNet完全不同的路,才是理解这个网络的关键。适合阅读这篇内容的人包括:正在学深度学习基础、需要做迁移学习选型、准备面试被问到经典网络结构、或者单纯想把“卷积过程”这四个字从抽象概念变成具体画面的人。不管你是刚入门还是已经跑过几个项目,只要对VGG的内部机制还有模糊的地方,下面的内容应该都能帮你把这块拼图补上。
2. VGG的宏观架构:五段卷积加三层全连接是怎么切出来的
2.1 从输入尺寸到特征图尺寸的完整推演
VGG的输入是固定的224×224×3的RGB图像。这个尺寸不是随便定的,它来自ImageNet数据集的预处理惯例,同时也和网络末端的全连接层结构相匹配。整个网络可以分成五个卷积段(block)加一个分类头,每一段的结构模式高度统一:若干个3×3卷积层堆叠,后面接一个2×2的最大池化层。
先看尺寸变化的全过程。卷积层使用3×3卷积核、步长1、padding为1,这种配置下特征图的空间尺寸不变。池化层使用2×2窗口、步长2,每次池化后空间尺寸减半。所以尺寸变化完全由池化层驱动:
| 阶段 | 操作 | 输出尺寸 | 通道数 |
|---|---|---|---|
| 输入 | — | 224×224 | 3 |
| Block1 | 2层3×3卷积 + 池化 | 112×112 | 64 |
| Block2 | 2层3×3卷积 + 池化 | 56×56 | 128 |
| Block3 | 3层3×3卷积 + 池化 | 28×28 | 256 |
| Block4 | 3层3×3卷积 + 池化 | 14×14 | 512 |
| Block5 | 3层3×3卷积 + 池化 | 7×7 | 512 |
| 全连接 | 3层FC | 1×1 | 4096→4096→1000 |
这张表看起来简单,但里面藏着VGG设计的核心节奏:每次池化后空间尺寸减半,同时通道数翻倍。这个节奏不是巧合,它背后有一个很实在的考量——随着空间信息被压缩,网络需要把更多的“表达能力”转移到通道维度上,用更多的特征图来弥补空间分辨率的损失。你可以把它想象成把一张大图逐步抽象成一组越来越厚的特征描述,每一层都在用更多的“视角”去描述越来越小的区域。
2.2 为什么是五个段而不是四个或六个
VGG有多个版本,最常见的是VGG16和VGG19,区别就在于每个段里卷积层的数量。VGG16的配置是2-2-3-3-3,VGG19是2-2-4-4-4。五个段的划分本身是有讲究的:前两段处理的是较低级的视觉特征(边缘、纹理、简单形状),中间两段开始捕捉中级特征(物体部件、纹理组合),最后一段处理的是高级语义特征(物体整体结构)。
如果段数太少,比如只做三段,那么感受野的增长不够,网络很难从局部特征过渡到全局语义。如果段数太多,比如做到七段,那么到了后面特征图会小到1×1,空间信息几乎完全丢失,而且参数量会急剧膨胀。五个段是一个在感受野、参数量和特征抽象层次之间的平衡点。实测下来,VGG16在ImageNet上的top-5准确率大约在90%左右,VGG19略高一点点,但参数量从1.38亿涨到了1.44亿,边际收益已经很小了。
2.3 全连接层的参数量为什么占了整个网络的绝大部分
这里有一个很多人第一次算VGG参数量时会惊讶的事实:VGG16的1.38亿参数里,卷积层只占了大约1500万,剩下的1.23亿全在全连接层。具体来说,第一个全连接层从7×7×512映射到4096,参数量是7×7×512×4096≈1.02亿,光这一层就占了整个网络的74%。
这个数字说明了一个问题:VGG的卷积部分其实很“轻”,真正吃参数的是分类头。这也是为什么后来很多改进方案(比如用全局平均池化替代全连接层)能大幅压缩参数量。但在VGG提出的那个年代,全连接层被视为必要的“推理”环节,负责把卷积提取的分布式特征整合成最终的类别判断。
3. 3×3卷积核的叠加逻辑:两个小核为什么能顶一个大核
3.1 感受野的等效计算
VGG最标志性的设计就是全部使用3×3卷积核。在它之前,AlexNet用了11×11、5×5、3×3混合的卷积核。VGG的论文里给出了一个关键论证:两个3×3卷积层的堆叠,感受野等效于一个5×5卷积层;三个3×3的堆叠,感受野等效于一个7×7。
这个等效关系怎么来的?感受野的计算公式是:每经过一层k×k卷积(步长1),感受野增加k-1。所以两层3×3的感受野是1+2+2=5,三层是1+2+2+2=7。这个推导很直接,但它的意义在于:用更小的核堆叠,达到了和大核相同的感受野,同时带来了两个好处。
第一个好处是参数更少。一个5×5卷积层的参数量是25×C×C'(C和C'分别是输入和输出通道数),而两个3×3卷积层的参数量是2×9×C×C'=18×C×C'。参数减少了28%。对于7×7的情况,一个7×7是49×C×C',三个3×3是27×C×C',参数减少了45%。
第二个好处是非线性更强。每个卷积层后面都跟着ReLU激活函数,两层3×3意味着中间多了一次非线性变换。这就像用两段折线去逼近一条曲线,比用一段直线去逼近要灵活得多。实测中,这种额外的非线性对特征表达能力有实实在在的提升。
3.2 通道数翻倍策略的数学直觉
VGG每个段之后通道数翻倍:64→128→256→512→512。这个策略的直觉是:随着空间尺寸减半,每个特征图上的信息密度增加了,需要用更多的通道来“分散”这些信息,避免信息被过度压缩。
从信息论的角度看,一个特征图的总信息量大致正比于空间尺寸的平方乘以通道数。当空间尺寸减半时,如果通道数不变,总信息量会降到原来的四分之一。通道数翻倍只能补偿到原来的二分之一,但考虑到池化本身就是在做有损压缩,这个补偿比例在实践中被证明是够用的。你可以把它理解成:池化在“丢细节”,通道翻倍在“加视角”,两者配合让网络在抽象层次提升的同时不至于丢失太多关键信息。
3.3 小卷积核堆叠在实际训练中的表现
在实际训练中,3×3堆叠的一个额外好处是梯度流动更顺畅。因为每一层的卷积核都很小,反向传播时梯度经过的“路径”相对短,不容易出现梯度消失。我在几个图像分类项目里对比过,同样的深度下,全3×3的配置比混合使用大核的配置收敛更稳定,尤其是在没有BatchNorm的年代(VGG原始版本没有BN),这个优势更明显。
当然,小核堆叠也有代价:计算量更大。虽然参数量少了,但两个3×3卷积的浮点运算次数(FLOPs)是2×9×H×W×C×C',而一个5×5是25×H×W×C×C',前者是后者的72%。计算量并没有等比例减少,因为小核需要更多的层数来达到同样的感受野。但在GPU并行计算的环境下,这个代价是可以接受的。
4. 逐层拆解:从Block1到Block5的特征演化过程
4.1 Block1和Block2:边缘、纹理与基础形状
Block1的输入是224×224×3的原始图像。第一层卷积用64个3×3卷积核,输出64个224×224的特征图。这一层学到的是最基础的视觉元素:水平边缘、垂直边缘、对角线边缘、简单的颜色斑块。如果你把第一层的卷积核权重可视化出来,会看到类似Gabor滤波器的图案,这和人类视觉系统初级皮层的感受野非常相似。
Block1的第二层卷积继续在64个通道上做3×3卷积,输出仍然是64个224×224的特征图。这一层开始组合第一层的边缘信息,形成更复杂的纹理模式,比如角点、交叉线、简单的曲线。然后2×2最大池化把尺寸降到112×112。
Block2重复同样的模式,但通道数翻倍到128。这一段的特征图开始捕捉更结构化的纹理,比如网格、条纹、简单的几何形状组合。池化后尺寸降到56×56。到这一步,网络已经完成了从像素级到局部纹理级的抽象。
4.2 Block3:物体部件的出现
Block3是VGG开始出现“语义”迹象的地方。三段3×3卷积,通道数256,输出28×28的特征图。这一层的特征图已经可以对应到物体的部件级别:轮子、眼睛、窗户、叶子。如果你用可视化工具查看Block3的激活图,会发现某些通道对特定部件有强烈的响应。
这里有一个实操中值得注意的细节:Block3的第三层卷积之后,特征图的空间尺寸是28×28,每个位置对应原图大约8×8像素的区域。这意味着网络在这个阶段已经建立了“局部到全局”的映射关系,每个特征向量都编码了原图一个局部区域的高级信息。
4.3 Block4和Block5:从部件到整体语义
Block4和Block5各包含三段3×3卷积,通道数都是512。Block4输出14×14,Block5输出7×7。这两段的特征图已经非常抽象,单个通道的激活往往对应整个物体或场景的某个语义属性,比如“这是猫的头部区域”“这是天空区域”。
Block5的7×7×512特征图是整个卷积部分的最终输出。这7×7的每个位置都编码了原图32×32区域的信息,512个通道则从不同角度描述了这个区域。这组特征图随后被展平成一个7×7×512=25088维的向量,送入全连接层。
4.4 池化层的位置选择与作用边界
VGG的池化层全部放在每个段的末尾,而不是像有些网络那样穿插在卷积层之间。这个位置选择是有道理的:在一个段内部,卷积层在相同的空间尺度上提取特征,池化层负责把这个尺度上的信息压缩后传递给下一个段。如果池化穿插在段内部,会打断同一尺度上的特征提取过程,让某些卷积层不得不在已经压缩过的空间上工作,反而浪费了计算资源。
另一个细节是VGG全部使用最大池化而不是平均池化。最大池化保留了每个窗口内最强的激活,这在图像任务中通常比平均池化更有效,因为最强的激活往往对应最显著的特征。实测中,把VGG的最大池化换成平均池化,ImageNet上的top-1准确率会下降1到2个百分点。
5. 全连接分类头的运作机制与参数分布
5.1 从7×7×512到4096的映射过程
卷积部分的输出是一个7×7×512的三维张量。全连接层的第一步是把这个张量展平成一个长度为25088的一维向量。然后通过一个25088×4096的权重矩阵做线性变换,加上4096维的偏置,再经过ReLU激活。这一层的参数量是25088×4096+4096≈1.027亿。
这个映射的本质是在做一个高维空间到另一个高维空间的线性投影。25088维的输入包含了图像的所有卷积特征,4096维的输出则是一个压缩后的“语义编码”。你可以把它理解成:卷积层负责“看”,全连接层负责“想”。看的过程是分布式的、局部的,想的过程是全局的、整合的。
5.2 两层4096全连接加一层1000分类输出的设计考量
VGG用了两层4096维的全连接层,而不是一层。这个设计来自AlexNet的延续,但在VGG里被推到了极致。两层4096的参数量分别是1.027亿和4096×4096+4096≈1680万。第二层全连接的作用是在第一层的基础上做进一步的语义组合,把4096维的编码再映射到另一个4096维空间,增加模型的非线性表达能力。
最后一层是4096×1000的全连接层,输出1000维的logits,对应ImageNet的1000个类别。这一层后面接Softmax(训练时用交叉熵损失,推理时取argmax)。整个分类头的参数量加起来大约是1.23亿,占了VGG16总参数的89%。
5.3 Dropout在全连接层中的实际效果
VGG在两个4096维全连接层之后都加了Dropout,丢弃率0.5。这个操作在训练时随机把一半的神经元输出置零,强迫网络不依赖任何单个神经元,从而增强泛化能力。实测中,去掉Dropout后VGG在ImageNet上的过拟合非常严重,训练准确率能到99%以上但验证准确率明显下降。加上Dropout后,验证准确率的差距会缩小很多。
需要注意的是,Dropout只在训练时启用,推理时所有神经元都参与计算,但输出需要乘以0.5来保持期望值一致。这个细节在手动实现VGG时容易忽略,导致推理结果偏大。
6. 训练VGG时的实操细节与常见坑
6.1 输入预处理的标准化参数选择
VGG原始论文使用的预处理是:每个像素减去ImageNet训练集的均值(RGB三个通道分别是123.68、116.779、103.939),然后不做除法缩放。这个均值的单位是0到255的像素值。如果你用的是PyTorch的预训练权重,它期望的输入是已经归一化到0到1之间、再按mean=[0.485, 0.456, 0.406]和std=[0.229, 0.224, 0.225]标准化的张量。
这两个预处理方式不能混用。我见过不少人直接拿原始像素值送进PyTorch的预训练VGG,结果准确率惨不忍睹。正确的做法是:如果你从零训练,可以用原始论文的均值减法;如果你用预训练权重做迁移学习,必须严格按预训练时的预处理来。
6.2 学习率与优化器的经验配置
VGG原始论文用的是SGD加动量0.9,初始学习率0.01,当验证准确率不再提升时学习率除以10。整个训练大概需要74个epoch。这个配置在当年是标准做法,但放到现在来看,训练周期太长了。
如果是从零训练VGG16,我建议用SGD加动量0.9,初始学习率0.01,配合余弦退火或者阶梯式衰减。Batch size用256或者更大,因为VGG的参数量大,小batch size会导致梯度噪声太大。如果显存不够,可以用梯度累积来模拟大batch。
如果是迁移学习,冻住卷积层只训练分类头,学习率可以设到0.001到0.01,用Adam或者SGD都可以。解冻部分卷积层做微调时,学习率要降到0.0001级别,否则预训练权重会被破坏。
6.3 显存占用与batch size的平衡
VGG16的参数量是1.38亿,推理时显存占用大约500MB(float32),但训练时因为要存中间激活值和梯度,显存占用会大很多。以batch size 16、输入224×224为例,训练时显存占用大约在4到6GB。如果显存不够,最直接的办法是减小batch size,但太小会影响BatchNorm的效果(VGG原始版本没有BN,所以这个问题相对小一些)。
另一个办法是用混合精度训练,把部分计算转成float16,显存占用能降到原来的60%左右。但要注意,VGG的全连接层参数量大,float16下梯度容易下溢,需要用损失缩放(loss scaling)来补偿。
6.4 迁移学习时冻结哪些层效果最好
做迁移学习时,一个常见的问题是:到底该冻结哪些层。我的经验是,如果目标数据集和ImageNet比较相似(比如都是自然图像),冻结前四个段(Block1到Block4),只训练Block5和分类头,效果通常最好。如果目标数据集和ImageNet差异较大(比如医学图像、卫星图像),可以只冻结前两个段,让后面的层也参与微调。
还有一个细节:VGG的全连接层参数量太大,在小数据集上很容易过拟合。一个实用的做法是把原来的三层全连接替换成全局平均池化加一个小的全连接层,这样参数量能从1.23亿降到几百万,在小数据集上的泛化能力会好很多。
7. 卷积过程的可视化验证:怎么确认网络真的学到了东西
7.1 卷积核权重的直接可视化
最直接的方式是把第一层卷积核的权重拿出来,归一化到0到255之后当成图片显示。VGG16第一层有64个3×3×3的卷积核,你会看到64张小的彩色图案,有的对红色敏感,有的对绿色敏感,有的检测水平边缘,有的检测垂直边缘。如果这些图案看起来像随机噪声,说明训练有问题;如果能看到明显的边缘和颜色模式,说明网络至少在第一层学到了合理的特征。
7.2 中间层激活图的热力图观察
对于中间层,直接看权重意义不大,更有用的是看激活图。选一张输入图片,前向传播到某一层,把该层的输出特征图按通道取平均或者取最大,得到一个二维热力图,叠加在原图上。你会看到Block1的热力图几乎覆盖全图,Block3的热力图开始聚焦到物体的某些部位,Block5的热力图则集中在物体的核心区域。这个从“全图响应”到“局部聚焦”的过程,就是卷积网络逐层抽象的直接证据。
7.3 用遮挡实验验证特征重要性
还有一个更定量的方法:遮挡实验。把输入图片的某个区域用灰色块遮住,看分类结果的变化。如果遮住某个区域后分类置信度大幅下降,说明这个区域对分类很重要。在VGG上做这个实验,你会发现遮住物体的关键部位(比如狗的头部)比遮住背景对结果的影响大得多。这个方法虽然计算量大,但能直观地告诉你网络到底在“看”哪里。
8. VGG卷积设计的遗产与局限
VGG的卷积过程设计影响了很多后来的网络。它的3×3堆叠思路被后续的ResNet、DenseNet等网络继承,通道数翻倍策略也成了很多网络的基本范式。但VGG的局限性也很明显:全连接层参数量太大,导致模型体积大、推理速度慢;没有BatchNorm,训练收敛慢;固定输入尺寸224×224,不够灵活。
在实际项目中,如果非要用VGG,我一般只把它当特征提取器用,取Block4或Block5的输出做下游任务,而不是用它的分类头。这样既能利用它强大的特征提取能力,又能避开全连接层的参数量问题。另外,VGG的卷积部分其实可以接受任意尺寸的输入(只要能被32整除),因为卷积和池化都是位置无关的操作。只有全连接层要求固定尺寸。所以如果你只需要卷积特征,完全可以用更大的输入尺寸,得到更精细的特征图。
踩过几次坑之后,我对VGG的态度是:它是一个很好的学习工具,结构清晰、逻辑规整,适合用来理解卷积网络的基本原理。但在实际项目中,除非有特殊需求,否则有更多更高效的架构可以选择。理解它的卷积过程,更多是为了建立对卷积网络设计思路的直觉,而不是为了在项目中直接使用它。