
提到卷积神经网络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的实现并不复杂核心就是用列表定义每组卷积块然后循环堆叠。下面这份代码我按配置DVGG16写的可以根据需要自己改配置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_normTrue): layers [] in_channels 3 for v in cfg: if v M: layers.append(nn.MaxPool2d(kernel_size2, stride2)) else: conv nn.Conv2d(in_channels, v, kernel_size3, padding1) if batch_norm: layers.append(conv) layers.append(nn.BatchNorm2d(v)) layers.append(nn.ReLU(inplaceTrue)) else: layers.append(conv) layers.append(nn.ReLU(inplaceTrue)) in_channels v return nn.Sequential(*layers) class VGGNet(nn.Module): def __init__(self, cfgD, num_classes1000, init_weightsTrue): 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(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), 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, modefan_out, nonlinearityrelu) 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(cfgD, num_classes10) print(model)这段代码有几个值得注意的细节。conv层我加了padding1这样3x3卷积不会改变特征图尺寸尺寸变化只由池化层控制符合VGGNet原设计。BatchNorm是后来常见增强原版网络没有但实际操作中加上能让训练稳定很多我建议默认开。4.2 训练前的数据处理流程VGGNet输入尺寸是224x224。训练时建议做随机裁剪和水平翻转这是最基本的图像增强组合。我自己常用的流程是先缩放到256再随机裁剪到224以增加位置不变性推理时则直接中心裁剪到224。如果数据量不算大我建议再加一点随机亮度对比度扰动。VGGNet对颜色变化比较敏感适当地扰动颜色通道能让模型泛化更好。但注意不要加太强的旋转或缩放增强VGGNet这类全卷积加全连接的结构对几何形变的鲁棒性一般。4.3 训练配置的实际建议优化器用SGD加动量就行不需要花哨的Adam。学习率初始设0.01batch 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最好的起点之一。