
前两天有个朋友发来一段代码说他在某个小数据集上用VGG16做迁移学习效果比一些看起来“更新”的网络还好就是跑得慢。我顺手把2014年那篇《Very Deep Convolutional Networks for Large-Scale Image Recognition》翻出来又读了一遍。这篇论文对我而言确实是“常读常新”当年它用最笨拙的方式把卷积网络从8层直接推到16层、19层靠堆叠3×3小卷积核刷新了ImageNet上的识别记录并且之后几年里几乎所有检测、分割、风格迁移项目都在用它的预训练权重做底座。如果你正在学卷积网络结构或者要复现一篇旧模型并做迁移学习这篇文章基本就是绕不开的教科书。接下来我会把这篇论文的动机、结构设计、小卷积核的数学直觉、论文里的消融实验、训练细节和工程遗产完整展开顺便穿插一些我复现和调参时的真实体会。1. VGG解决的核心问题深度到底值多少精度1.1 2014年前后卷积网络的“魔法时代”2012年的AlexNet一举把ImageNet图像分类的错误率大幅压低卷积网络从此成为视觉任务的主角。但是当时的普遍认知是分散且模糊的有人觉得大卷积核性能好于是用7×7甚至11×11有人觉得网络不能太深因为训练时会遇到梯度消失、收敛特别慢。没有一篇工作能清楚地回答“网络深度到底对精度有多大的影响”。VGG这篇论文本质上做了一件很朴素但极有意义的事把“深度”作为可控变量用几乎一致的结构去增加卷积层数量从11层一路加到19层然后在同样数据集、同样训练流程下看精度的变化。结果证明了深度在当时的网络结构下是有效且必要的而且加深到16到19层时仍然能稳定收益。这一点放在今天看似乎习以为常但在当时需要很大的实验量来支撑。1.2 我理解的论文真实贡献不只是VGG16结构本身很多初学者会把VGG等同于“VGG16那个网络结构”这当然没错但论文更值钱的地方在三点系统验证了“小卷积核堆叠加深网络”可以稳定提高精度并把感受野、参数量、非线性三者之间的账算得明明白白后来的很多网络结构设计都从这个思想里延伸出来。给出了一组从浅到深、可复现的配置论文里叫A到E并且公开权重。当时大家随便下载一下就能用来做特征提取或迁移学习这种开源红利对整个社区推进作用巨大。提供了完整的训练和调参经验比如先训浅层网络再拿浅层权重初始化深层网络这个操作后来成了很多深层网络训练的救命稻草。1.3 这篇论文适合谁细读如果你是刚接触深度学习的初学者VGG是一个极好的“骨架模板”每条卷积层、每个池化层都规规矩矩通道数翻倍、尺寸减半的节奏一目了然比很多花哨的结构容易理解得多。如果你是有几年经验的工程师重新读一遍也会发现大量容易被忽略的细节比如LRN局部响应归一化为什么被放弃、测试时多尺度裁剪为什么有用、权重初始化对深层网络有多重要。这篇文章接下来的内容就是把这些关键细节一次拆开。2. 逐层拆解VGG16从224×224到分类输出的全路径2.1 五种配置的差异A、A-LRN、B、C、D、E论文里最让人印象深刻的是那张配置总表。VGG团队用几乎一样的思路设计了一组网络区别主要在总层数和少量结构改动。为了方便记忆我通常用每个卷积块的通道数序列来区分配置代码大概长这样cfgs { A: [64, 64, M, 128, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], B: [64, 64, M, 128, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], C: [64, 64, M, 128, 128, M, 256, 256, 256, M, 512, 512, 512, M, 512, 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], }其中M代表尺寸减半的max-pooling。A是基准的11层网络A-LRN是在A的基础上加局部响应归一化B在A的基础上把某些位置的单层卷积变成了两层C则在B的基础上又加了一些1×1卷积D把C的1×1卷积换成了3×3E在D基础上继续加深到19层。最常用的VGG16对应D配置VGG19对应E配置。我当时看到这个配置表的第一反应是原来VGG16其实就是把一堆卷积层拼成五个块每块第一层之后池化一次最后接三个全连接层。搞清楚这个规律之后看很多后续网络就非常轻松因为后来的ResNet、DenseNet基本也是“分阶段设计”的套路只是内部变换方式不同。2.2 VGG16逐层参数与特征图尺寸变化以224×224×3的输入为例VGG16的完整数据流是这样走的第一阶段两组conv3-64输出224×224×64然后max-pool尺寸变为112×112×64。第二阶段两组conv3-128输出112×112×128然后max-pool尺寸变为56×56×128。第三阶段三组conv3-256输出56×56×256然后max-pool尺寸变为28×28×256。第四阶段三组conv3-512输出28×28×512然后max-pool尺寸变为14×14×512。第五阶段三组conv3-512输出14×14×512然后max-pool尺寸变为7×7×512。最后将7×7×512的特征图展开成25088维向量依次经过4096、4096、1000三个全连接层最后通过softmax输出分类概率。整个过程最反直觉的地方是卷积层几乎不改变特征图长宽只增加通道数真正让尺寸逐级减半的是那5个max-pool。一张224×224的图片经过5次池化之后变成7×7这里刚好是2的5次方关系224除以32等于7。所以后面换输入大小时比如改成384×384那pool四次之后就会变成12×12结构需要稍微调整一下。我在做目标检测项目时就经常把VGG的输入从224加大到384甚至512目的就是保留更多空间位置信息。2.3 参数量统计全连接层才是吃参数的“哑铃”VGG16的参数量大约是1.38亿这个数字惊人但更惊人的是它的分布。我把卷积层和一个全连接层的参数量拆开算过部分参数量占比全部卷积层约1472万约10.6%fc67×7×512→4096约1.028亿约74.2%fc74096→4096约1678万约12.1%fc84096→1000约410万约3.0%也就是说卷积层加起来只占十分之一多一点光一个fc6就吞掉了整个网络七成以上的参数量。我第一次算完这个表之后立刻明白为什么后来全局平均池化会流行把最后的多维特征图直接平均成一个向量既能保留语义信息又能省掉那个巨无霸的展开矩阵。VGG之所以慢和占用显存很大一部分原因就是这三个全连接层在“拖后腿”。3. 3×3小卷积核VGG最精妙的两笔账3.1 等效感受野为什么两个3×3等于一个5×5VGG全文的灵魂不是“深”而是“用什么样的方式深”。它坚持在整条网络里只用3×3卷积核步长为1padding为1。这里有一个非常关键的计算两个3×3卷积堆叠之后输出特征图上的一个像素在原图上覆盖的区域等于一个5×5卷积的感受野也就是25个像素范围三个3×3卷积堆叠则等价于一个7×7卷积的感受野覆盖49个像素范围。当时我看到这个结论的第一反应是把公式推一遍。假设输入是一个5×5的区域经过第一次3×3卷积之后输出图的每个位置对应原图的3×3窗口再经过第二次3×3卷积输出图的每个位置对应前一层的3×3窗口而前一层的3×3窗口又对应原图5×5范围这样两级映射叠加起来刚好等效于一次5×5卷积。如果把中间特征图忽略掉只看输入输出关系两个小卷积核确实“拼”成了一个大卷积核。这个等效关系就是小卷积核能撑起深度的底层逻辑网络并没有因为换用3×3而丢失感受野反而因为多了中间层获得了更丰富的非线性表达能力。3.2 参数账与非线性账49对比271对比3用7×7卷积替代三个3×3卷积从参数量上算一笔账。假设输入输出通道数都为C一个7×7卷积的参数是7×7×C×C49C²三个3×3卷积的参数是3×(3×3×C×C)27C²。也就是说在感受野完全等价的前提下小卷积核堆叠能节省大约45%的参数。这个比例不依赖C的具体值是一个通用的结论。如果只看参数节省可能有人觉得“少一点又怎样”但VGG真正赢在非线性层数。一个大7×7卷积只有一层卷积加一层ReLU而三个3×3卷积堆叠起来是三层卷积加三层ReLU。神经网络的深度意义就在于这些非线性层的堆叠更多非线性让网络能够拟合更复杂的函数曲面。这就好比一个三人小组各自观察一小片信息然后层层汇总比一个人拿大扫把看一大片区域要更细致中间多出来的“讨论环节”很重要。我后来在看很多轻量化网络的论文时发现它们都在反复使用这个思想用更小的卷积核叠加替代大卷积核换取参数效率和非线性能力VGG在这方面是祖师爷级别的存在。3.3 为什么1×1卷积不够也不直接上大卷积核论文里有一个配置C是在部分层中用1×1卷积代替3×3卷积。1×1卷积可以在不改变特征图空间尺寸的情况下做通道混合和增加非线性参数量也更低但实验结果显示C配置的精度明显不如D配置也就是同样深度下3×3卷积比1×1卷积更有效。我当时反复想这段实验结论是1×1卷积完全不会做空间信息交互每个输出像素只看到输入对应位置那个像素的通道信息等于把画面剪成无数个点再逐点处理缺少邻域上下文。而3×3卷积每次都整合了周围3×3邻域的信息虽然网络设计上“每一点只看到一小块”但正是这些小块的层层传递才让感受野逐步扩大。再看“为什么不直接用7×7卷积”参数多、非线性少是一个原因另一个原因是7×7大卷积核感受野过大时提取的特征相对粗糙而三个3×3卷积相当于在同样的视觉范围内做了三次逐步精炼的特征提取精度自然更稳。VGG的核心思路其实是把“加大感受野”的动作拆成三次小的移动这让网络能够以更细腻的方式理解图像。4. 论文里的消融实验与训练细节那些被忽略的参考值4.1 深度对精度的影响从11层到19层的台阶论文里最直接的对比就是不同深度的网络在ImageNet验证集上的表现。只增加深度、保持其他结构基本不变时性能是随着层数增加逐步提升的。比如A配置大概在11层B和C在13到16层之间D和E在16到19层整体趋势是越深越好直到19层时收益边际开始变小。这条曲线给了当时整个领域一个非常明确的信号网络深度还有很大挖掘空间。但这里有一个容易被人忽略的背景更深的网络意味着训练困难。论文用“先训练浅层网络再用浅层网络的权重初始化深层网络”来解决这个问题这个方法在今天很多任务上仍然好用。我自己在搭大规模分类模型时如果训练资源不足也会先跑一个浅层模型再用它的部分权重去初始化深层模型收敛速度和最终精度都有改善。4.2 LRN被否定1×1不如3×3消融实验的价值VGG论文里有一组很经典的对照实验A-LRN。A-LRN在A的基础上增加了局部响应归一化LRN这在AlexNet时代被很多人视为标配。但实验结果非常打脸加了LRN的A-LRN精度反而比不加LRN的A略低。既然无益且增加计算量VGG后面的配置全部取消了LRN。这件事给我的启发比结构本身更大不要盲从既有的“经验技巧”任何设计都应该用消融实验去验证。很多后来出现的模块比如各种归一化层、注意力模块、激活函数变体在一个新的网络里到底有没有效果只有通过严格的对比才能下结论。VGG当年就是靠这种扎实的实验态度把LRN从主流网络里清了出去。4.3 训练细节清单初始化、缩放、参数配置论文在训练细节上的信息量很大这部分对实际复现非常关键权重初始化先训练配置A获得权重再用A的浅层权重初始化B、C、D、E的对应层。这比从头随机初始化深层网络稳定得多收敛也更快。输入尺寸训练时把图片短边随机缩放到256到512之间的某个尺度再从中随机裁剪224×224区域。这种多尺度训练相当于免费的数据增强。数据增强随机水平翻转、RGB颜色抖动也是标配。优化配置SGDmini-batch为256momentum为0.9权重衰减设为5e-4前两个全连接层使用dropout且概率为0.5。学习率策略初始学习率0.01当验证集准确率停止提升时将学习率除以10整个训练过程大约下降三次学习率。我复现时发现如果跳过浅层初始化这一步直接从深层随机初始化开始训练模型其实也能收敛但需要更多的epoch和更小的学习率前期损失下降非常慢。论文里这个“从浅到深”的思路本质上是给梯度传播一个更平滑的起点。4.4 测试阶段的多尺度技巧测试阶段VGG团队也没有只做一次中心裁剪。他们用了两种策略叠加一个是对多尺度缩放的图片做密集评估也就是让卷积网络在全图上滑动计算另一个是multi-crop从不同位置裁剪多块区域分别预测最后平均所有结果。这些技巧叠加起来能带来一到两个百分点的提升。放在今天看这种多尺度测试思路已经演化成了很多竞赛和工业项目的“测试时增强”。不过也要提醒一句multi-crop会成倍增加推理时间实际部署时需要在精度和速度之间做取舍。我做在线推理时基本不用多尺度但离线评测和竞赛时会开效果确实立竿见影。5. VGG的工程遗产与教训迁移价值、参数包袱与后辈改进5.1 预训练模型的迁移帝国如果说VGG的论文是技术胜利那么它开源的预训练权重就是工程胜利。2014年之后很长一段时间目标检测网络的骨干、语义分割网络的编码器、风格迁移的特征提取器几乎都能看到VGG16的影子和它的预训练权重。我自己的感觉是当时用VGG16做backbone比后来用很多新颖网络还要省心结构简单预训练模型好找上下采样路径清晰fine-tune时调整空间又大。就算到现在在某些中小规模数据集上VGG16的预训练特征依然非常能打。我做过一次对比把VGG16换成一个“更新”但更轻的网络在小样本迁移任务上反而是VGG16的表现更稳。原因在于大参数量的模型在预训练阶段已经记住了大量通用视觉模式迁移到下游任务时只需要小幅度调整。5.2 VGG的不足之处显存、参数与全连接层的“重税”VGG的缺点也和它的成功一样突出参数量大、计算量大、显存占用高。从上面的参数分布就能看出来最后三个全连接层贡献了绝大多数参数而全连接层的参数数量由输入特征图大小决定一旦输入尺寸变大fc6的参数还会进一步膨胀。这意味着VGG对输入分辨率非常敏感想要高分辨率输入几乎等于把显存花在矩阵展开上。在显存只有几个GB的年代训练VGG16的batch size不敢设太大。我的经验是一张224×224的图片在VGG16上做一次前向激活值占据的显存比模型参数本身还大尤其最后几个卷积层的特征图通道数512、尺寸适中加起来非常吃内存。后来很多网络改用全局平均池化替代全连接层把参数减少到几百万级别正是针对这个痛点。残差结构的出现更是让“更深”不再等同于“更难训练”。5.3 它对后续网络设计留下的三条启示读完VGG再看后面的ResNet、DenseNet会发现很多思路都能溯源到这篇论文小卷积核是大势所趋增大感受野不一定要增大卷积核用层数堆叠更划算。网络设计要有清晰的阶段性通道数逐级翻倍、分辨率逐级减半这种瀑布结构非常有利于梯度流动和工程实现。每个设计选择都必须经过实验验证不管是LRN还是1×1卷积只有数据能说服人。VGG本身也像一个路标它证明了深度有潜力但也暴露了单纯加深网络会出现收敛困难和高参数负担这就直接催生了后续的残差连接和批量归一化。可以说没有VGG踩出来的这些坑后面很多网络不会走得那么顺。6. 我复现VGG时踩过的几个小坑6.1 显存不够时的处理顺序我第一次在单卡上跑VGG16的时候batch size开到32直接显存溢出。当时急着把batch调小到8结果训练非常不稳定。后来我养成一个习惯优先保持batch size在16以上如果显存不够先把输入图片从224降到192或者关闭验证阶段的数据增强而不是一味缩小batch。batch缩到很小会让BatchNorm如果加了的话统计量不准也会让梯度的噪声变大。VGG本身没有BatchNorm但后来的变体加了这时候小batch问题会更明显。6.2 迁移学习时忘了输入归一化VGG官方预训练权重在训练时有特定的输入预处理方式大致是每个像素减去训练集的均值。很多人直接拿来fine-tune时不做这个归一化结果准确率总差一大截。我第一次犯这个错误时反复调学习率都没用最后检查数据流才发现问题。如果你用任何一个现成模型库加载VGG权重务必先确认对方实现的预处理是否与你使用的数据加载代码一致这一步出错非常隐蔽。6.3 用VGG做特征提取器时冻结层数要谨慎我的另一个教训是做小数据集的迁移学习时不要一开始就把所有层都解冻。比较稳的做法是先把新任务的全连接层从头训练卷积层暂时冻结等损失降到一个平台之后再解冻最后一两个卷积块用小学习率微调。VGG的前半部分提取的是通用边缘、纹理、形状特征后面几层更偏向任务相关的语义特征所以解冻顺序应该从最后往前逐渐放开。这样训练速度更快也不容易把预训练学到的知识冲掉。6.4 关于训练VGG的一点心得体会很多人觉得VGG结构老、参数多已经不值得学了。我的看法恰恰相反正是因为它的结构规整、问题暴露得明显它才是一个完美的“实验田”。你可以在VGG上验证数据增强的效果可以对比不同初始化的影响可以测试不同池化策略这些实验结论往往能推广到更新的结构上。如果让我给刚入门的人推荐第一个要反复精读的卷积网络我大概率还是会把这篇论文和VGG16的代码一起丢过去读懂了它再看后来的网络会轻松很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。