首页 / 资讯中心 / 文章详情

VGGNet深度解析:3x3卷积堆叠与迁移学习实战

VGGNet深度解析:3x3卷积堆叠与迁移学习实战 ★ FEATURED ARTICLE
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卷积核步长固定为1padding固定为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否VGG11A-LRN11864, 128, 256, 512, 512否加LRN的VGG11B131064x2, 128x2, 256x2, 512x2, 512x2否VGG13C161364x2, 128x2, 256x3, 512x3, 512x3是两处VGG16含1x1变体D161364x2, 128x2, 256x3, 512x3, 512x3否VGG16E191664x2, 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最初那篇论文给出的训练配置后来被很多复现实验当作默认参数。我这里直接列出来优化器SGDmomentum0.9权重衰减5e-4初始学习率0.01batch size256学习率调整验证集准确率不再上升时学习率除以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“又大又慢”的名声。如果你在单卡上训练VGG16batch size开256基本是想都不用想的事显存会直接溢出。常见做法是batch size降到32甚至16配合梯度累积来模拟大batch效果再往上可以开混合精度训练把显存占用和训练时间一起降下来。推理时如果显存也紧张可以用半精度或者动态量化。如果只是做特征提取实际显存会小很多因为不需要保存梯度但你也别一次性把超大分辨率图直接送进VGG它前几层卷积是基于固定输入尺寸设计的后期换分辨率需要对结构做适配。4. 新项目里真正在用的VGG迁移学习与特征提取4.1 冻结主干把VGG当成免费且稳定的特征提取器现在的实际项目里很少有人再从零训VGG了更多是把它当预训练特征提取器。这个思路成立的原因是卷积网络前几层学到的是边缘、纹理、颜色块这类通用特征迁移到新任务时仍然有效。你只需要把最后一层分类头换掉重新训练一个适合自己类别数的新分类层。操作起来很直接加载预训练权重后把最后一个全连接层的输出维度改成自己的类别数把前面所有卷积层参数设为不可训练只让新的分类头更新参数。这样一来训练量非常小几百张图也能跑出能用的结果。# 思路示意具体API按你使用的深度学习框架调整 model load_pretrained_vgg(depth16) # 替换最后的分类输出改成自己的类别数 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×类别数几乎可以忽略。操作步骤也不复杂去掉VGG原来的全连接分类器保留所有卷积部分让最后一个池化层输出7×7×512的特征图在卷积特征图后加一个全局平均池化得到512维描述子后面接一个线性层或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类模型你会对“新模型到底赢在哪”有更清楚的认识。经典的意义不在于一直用它拿最高分而在于它替你划出了一条可靠的起点线。
阅读完成 · 觉得有帮助?
咨询建站