news 2026/10/15 2:21:58

VGGNet论文精读:从3×3卷积到VGG16的完整解析与复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VGGNet论文精读:从3×3卷积到VGG16的完整解析与复现

1. 为什么还要读VGGNet这篇老论文

如果你现在打开任何一个深度学习框架的模型库,VGG16和VGG19大概率还躺在预训练权重列表里。很多人第一次做迁移学习,用的就是它。但真正翻回去把论文原文啃一遍的人,其实不多。我当初也是先用了大半年,才回头去读原文,读完的感受是:以前很多“约定俗成”的操作,原来在这篇论文里都有明确的实验依据。

这篇论文的全称是《用于大规模图像识别的深度卷积网络》,2014年由牛津大学的视觉几何组提出,VGG这个名字就是Visual Geometry Group的缩写。它当年拿下了ImageNet挑战赛定位任务第一名、分类任务第二名。放到今天看,它的结构简单到有点“笨”——就是不停地堆3×3卷积和2×2池化,但正是这种简单,让它成了后面无数网络设计的参照系。

这篇文章我想做的事很明确:把这篇论文的核心内容用纯中文梳理一遍,不是逐字逐句的硬翻译,而是把作者想表达的逻辑、实验设计、结论,以及我自己在复现和使用过程中的理解,完整地讲清楚。适合两类人看:一类是刚入门卷积神经网络、想搞懂经典结构到底经典在哪的朋友;另一类是用过VGG但没细究过它为什么这么设计、想补上这块知识的人。读完你应该能自己回答几个问题:为什么全是3×3卷积?为什么池化窗口是2×2?16层和19层差在哪?1×1卷积在这里是干嘛的?

2. 论文要解决的核心问题与整体思路

2.1 当时的环境:深度到底能有多深

在VGG之前,卷积网络在ImageNet上的主流配置大概是这样:第一层用大卷积核,比如11×11或者7×7,步长很大,快速把空间尺寸降下来,然后接几个卷积层,最后接全连接层分类。AlexNet就是典型代表,8层权重层,第一层11×11卷积,步长4。

那时候大家的一个直觉是:大卷积核能捕捉大范围的信息。但VGG的作者提出了一个不同的思路——与其用一个大卷积核,不如用多个小卷积核堆叠。这个想法不是拍脑袋来的,背后有明确的感受野计算逻辑。

我先把这个逻辑讲清楚,因为它是整篇论文的基石。假设输入是H×W,经过两层3×3卷积(步长1,无填充),第一层每个输出像素看到输入的3×3区域,第二层每个输出像素看到第一层的3×3区域,映射回输入就是5×5。三层3×3堆叠,感受野就是7×7。也就是说,三个3×3卷积堆叠起来的感受野,和一个7×7卷积是一样的。

那为什么不直接用7×7?两个原因。第一,参数更少。三个3×3卷积层的参数量是3×(3×3×C×C)=27C²,一个7×7卷积是49C²,差了将近一半。第二,非线性更强。每层卷积后面都跟了非线性激活,三层就有三次非线性变换,而一个7×7只有一次。非线性越多,决策函数的判别能力越强。这个结论在论文里有实验支撑,不是纯理论推导。

2.2 整体设计哲学:简单、统一、可扩展

VGG的整体设计思路可以用三个词概括:简单、统一、可扩展。

简单体现在结构上,整个网络就是卷积层、池化层、全连接层的线性堆叠,没有任何花哨的分支结构、没有跨层连接、没有复杂的模块设计。统一体现在超参数上,所有卷积层统一用3×3卷积、步长1、填充1(保证空间尺寸不变),所有池化层统一用2×2最大池化、步长2。可扩展体现在深度上,作者设计了从11层到19层的多个配置,结构完全一致,只是层数不同,这样可以干净地研究“深度”这一个变量对性能的影响。

这种设计哲学在当时是有点反直觉的。因为同期很多工作都在往“更复杂”的方向走,比如多尺度处理、多分支结构。VGG的作者选择了一条相反的路:把简单的东西做到极致,用深度换性能。事实证明这条路走通了,而且走得非常稳。

2.3 从配置A到配置E:一张表看懂所有变体

论文里最核心的一张表,是六个配置的对比。我把它整理成中文,方便对照。

配置权重层数结构特点参数量级
A118卷积+3全连接,全部3×3约1.3亿
A-LRN11在A基础上第一层后加LRN约1.3亿
B1310卷积+3全连接,部分层用3×3堆叠约1.3亿
C1613卷积+3全连接,部分层插入1×1卷积约1.3亿
D1613卷积+3全连接,全部3×3约1.4亿
E1916卷积+3全连接,全部3×3约1.4亿

这张表里藏着几个关键信息。第一,从A到E,卷积层数从8增加到16,但参数量增长很小,因为大部分参数集中在全连接层。第二,配置C用了1×1卷积,这是为了在不增加感受野的前提下增加非线性,属于一个对照实验。第三,配置D就是后来大家熟知的VGG16,配置E就是VGG19。

作者通过这张表想说明一件事:在相同的结构框架下,深度从11增加到19,性能是持续提升的。这个结论在当时很有冲击力,因为它意味着“更深”确实更好,而且这个趋势还没有看到明显的天花板。

3. 核心细节拆解:每个设计选择背后的理由

3.1 为什么坚持用3×3而不是更大的卷积核

这个问题前面提过感受野的逻辑,但还有一层更实际的原因:3×3是能捕捉“上下左右加中心”这个最小完整邻域的最小奇数尺寸。1×1只能看单点,没有空间信息;2×2是偶数,没有中心点,做填充时不对称;3×3刚好是能表达方向信息的最小窗口。

作者在论文里还做了一个对比实验:把配置A里的3×3卷积换成5×5、7×7、11×11,保持感受野大致相当。结果是3×3堆叠的版本性能最好。这个实验直接证明了小卷积核堆叠的优越性,不是理论上的猜测。

我自己的理解是,小卷积核堆叠还有一个隐性好处:正则化效果。因为参数少、非线性多,模型在训练集上的拟合能力被适度限制,反而不容易过拟合。这在当时的数据规模下(ImageNet 1000类、120万张训练图)是一个很重要的优势。

3.2 2×2最大池化的选择逻辑

池化层的作用是降低空间分辨率、扩大感受野、减少计算量。VGG里统一用2×2窗口、步长2的最大池化,这个选择也有讲究。

首先,2×2步长2意味着每次池化空间尺寸减半,这是最自然的降采样比例。其次,用最大池化而不是平均池化,是因为最大池化保留了每个窗口内最强的响应,这在分类任务里更符合“找最显著特征”的需求。第三,池化窗口不重叠(步长等于窗口大小),这样计算效率最高,没有冗余计算。

论文里还提到一个细节:池化层的位置。VGG的池化层不是每层卷积后都加,而是每2到4个卷积层后加一次。这样做的目的是让网络在降采样之前,先在同一个分辨率上做足够多的非线性变换,把特征提取充分。如果每层卷积后都池化,分辨率下降太快,后面的层就没多少空间信息可用了。

3.3 1×1卷积在配置C里的角色

配置C里插入了几层1×1卷积,这个设计容易被忽略,但值得单独说。1×1卷积的感受野就是1×1,它不捕捉空间信息,只做通道间的线性组合,然后接非线性激活。所以它的作用是在不改变空间分辨率、不增加感受野的前提下,增加模型的非线性表达能力。

作者做这个设计是为了回答一个问题:性能提升到底是因为深度增加,还是因为非线性增加?配置B和配置C的深度一样(都是13个卷积层),但C多了几个1×1卷积。实验结果是C比B略好,但不如D。这说明增加非线性有帮助,但不如直接增加3×3卷积层带来的提升大。这个对照实验设计得很干净,把“深度”和“非线性”两个因素拆开了。

3.4 全连接层的配置与参数量分布

VGG最后有三个全连接层,前两个各4096个神经元,最后一个1000个神经元对应1000个分类。这三个全连接层是整个网络参数量的大头。

我算一下:最后一个卷积层输出是7×7×512,展平后是25088维。第一个全连接层是25088×4096,参数量约1亿。第二个全连接层是4096×4096,参数量约1600万。第三个是4096×1000,参数量约400万。三个加起来约1.2亿参数,占了整个网络参数的绝大部分。

这也是VGG后来被诟病的地方:参数量太大,训练慢,推理也慢。但作者当时的考量是,全连接层可以提供全局的、非线性的特征组合能力,对分类精度有帮助。后来的网络(比如ResNet、GoogLeNet)用全局平均池化替代了全连接层,大幅减少了参数量,这是后话。

3.5 训练策略:小批量、动量、权重衰减

论文里训练部分的细节也值得记录。作者用的批量大小是256,动量0.9,权重衰减5×10⁻⁴,学习率初始0.01,当验证集准确率不再提升时除以10。Dropout用在前两个全连接层,比率0.5。

这些超参数在今天看很常规,但在当时是经过大量实验调出来的。特别是学习率衰减策略,作者明确说了是“当验证集准确率停止提升时”才衰减,而不是按固定步数衰减。这个策略更自适应,但需要持续监控验证集表现。

还有一个细节:作者用了数据增强,包括随机裁剪、水平翻转、RGB颜色偏移。这些增强手段现在已经是标配了,但在当时是提升泛化能力的关键。论文里提到,不用数据增强的话,大模型(比如配置E)会严重过拟合。

4. 实操复现:从零搭建一个VGG16

4.1 环境准备与依赖说明

我平时用PyTorch比较多,所以这里以PyTorch为例。环境很简单,一个能跑GPU的机器,装好PyTorch和torchvision就行。如果你只是想做推理或者迁移学习,CPU也能跑,只是慢一点。

pip install torch torchvision

如果你要用预训练权重,torchvision里直接有vgg16和vgg19的接口,一行代码就能加载。但如果你想自己从零搭一遍,理解每一层的配置,那就得手动写。我建议至少手动搭一次,搭完你对整个结构的理解会完全不一样。

4.2 逐层搭建VGG16的完整代码

VGG16的结构可以分成五个卷积块加三个全连接层。每个卷积块里是若干个3×3卷积,后面跟一个2×2最大池化。我直接上代码,然后逐段解释。

import torch import torch.nn as nn class VGG16(nn.Module): def __init__(self, num_classes=1000): super(VGG16, self).__init__() self.features = nn.Sequential( # 块1: 2个3x3卷积 + 最大池化 nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 块2: 2个3x3卷积 + 最大池化 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 块3: 3个3x3卷积 + 最大池化 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 块4: 3个3x3卷积 + 最大池化 nn.Conv2d(256, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 块5: 3个3x3卷积 + 最大池化 nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, num_classes), ) def forward(self, x): x = self.features(x) x = torch.flatten(x, 1) x = self.classifier(x) return x

这段代码里几个关键点。第一,所有卷积层都是padding=1,这样输入输出空间尺寸不变,只有池化层才降采样。第二,每个卷积块里的卷积层数分别是2、2、3、3、3,加起来13个卷积层,加上3个全连接层,总共16个权重层,这就是VGG16名字的由来。第三,全连接层的输入维度是512×7×7,这是因为输入图像是224×224,经过5次池化(每次减半),224→112→56→28→14→7,最后是7×7。

4.3 输入尺寸与感受野的对应关系

这里有一个容易踩坑的地方:VGG16的全连接层输入维度是固定的512×7×7,这意味着输入图像尺寸必须是224×224。如果你用其他尺寸,比如256×256,经过5次池化后是8×8,展平后是512×8×8,和全连接层的输入维度对不上,会报错。

那为什么论文里能用其他尺寸?因为作者在测试时用了全卷积化处理:把全连接层看成卷积层,这样网络就能接受任意尺寸的输入,输出一个空间的特征图,然后做全局平均池化得到分类结果。这个技巧在论文里有提到,也是VGG能处理多尺度输入的原因。

如果你想在自己的数据上微调VGG,有两种做法。一种是保持224×224输入,把全连接层换掉,改成你自己类别数的输出。另一种是把全连接层去掉,换成全局平均池化,这样就能接受任意尺寸输入。我一般推荐第二种,参数量小,过拟合风险低。

4.4 迁移学习中的层冻结策略

用预训练的VGG做迁移学习时,冻结哪些层、训练哪些层,是一个需要根据数据量决定的问题。我总结了一个经验规则:

数据量冻结策略理由
很小(<1000张)只训练最后的全连接层底层特征通用,不需要调整
中等(1000-10000张)冻结前三个卷积块,训练后两个卷积块+全连接中层特征可能需要微调
较大(>10000张)全部解冻,用很小的学习率微调数据足够,可以调整所有参数

这个规则不是绝对的,但作为一个起点很实用。我试过在小数据集上解冻太多层,结果验证集准确率反而下降,因为过拟合了。后来老老实实只训练最后几层,效果反而更稳。

4.5 训练过程中的关键参数设置

如果你要从零训练VGG16,有几个参数需要特别注意。批量大小,论文用的是256,但那是用了多GPU并行。单卡的话,根据显存大小,32到64比较现实。学习率,初始0.01配合动量0.9是论文的配置,但如果你用Adam优化器,初始学习率可以设1e-3到1e-4。

权重衰减,论文用的是5×10⁻⁴,这个值对VGG这种大模型很重要,能有效抑制过拟合。Dropout比率0.5,作用在前两个全连接层,这个也是论文验证过的配置。

还有一个容易被忽略的点:权重初始化。论文里提到用了高斯分布初始化,均值0,标准差0.01。偏置初始化为0。这个初始化方式对深层网络很重要,如果初始化不当,训练可能根本起不来。现在框架里一般用Kaiming初始化或者Xavier初始化,效果更好,但如果你要严格复现论文,就用高斯初始化。

5. 常见问题与排查技巧实录

5.1 训练不收敛或损失震荡怎么办

这是最常见的问题。我遇到过的原因大概有这么几类。第一,学习率太大。VGG参数量大,对学习率比较敏感,如果初始学习率设0.1,很可能直接震荡。建议从0.01开始,如果还震荡就降到0.001。第二,批量大小太小。批量太小会导致梯度估计噪声大,损失曲线抖动。如果显存允许,尽量用大一点的批量。第三,数据没有归一化。VGG的输入需要做标准化,一般用ImageNet的均值和标准差,即mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。如果输入数据范围是0到255,没有归一化,训练基本不可能收敛。

5.2 显存不够用的几种解决思路

VGG16的参数量约1.38亿,推理时显存占用还好,但训练时因为要存中间激活值,显存占用很大。如果显存不够,有几个办法。第一,减小批量大小,这是最直接的。第二,用梯度累积,即小批量跑多次,累积梯度后再更新,等效于大批量。第三,用混合精度训练,PyTorch的amp模块可以自动做,显存能省30%到50%。第四,冻结前面的卷积层,只训练后面的层,这样反向传播不需要计算前面层的梯度,显存占用会小很多。

5.3 过拟合的识别与应对

VGG因为参数量大,在小数据集上非常容易过拟合。识别过拟合的信号很简单:训练损失持续下降,但验证损失先降后升,验证准确率停滞甚至下降。应对手段有几个。第一,加数据增强,随机裁剪、翻转、颜色抖动都用上。第二,加大Dropout比率,但不要超过0.7,否则欠拟合。第三,加权重衰减,从5×10⁻⁴加到1×10⁻³试试。第四,早停,验证损失连续几个epoch不降就停。第五,最根本的,换更小的模型或者用预训练权重。

5.4 迁移学习时准确率上不去的排查清单

用预训练VGG做迁移学习,如果准确率不理想,按这个清单排查:

排查项检查内容常见问题
输入预处理是否用了和预训练一致的归一化用了错误的均值方差
层冻结冻结策略是否合理解冻太多导致过拟合
学习率是否用了合适的学习率太大导致震荡,太小导致不收敛
分类头新的全连接层初始化是否正确用了默认初始化但没调
数据划分训练验证划分是否合理验证集太小导致评估不准
类别平衡各类样本是否均衡严重不均衡导致偏向多数类

这个清单我基本每次做迁移学习都会过一遍,能解决大部分问题。

5.5 推理速度慢的优化方向

VGG的推理速度在当年算快的,但现在看确实慢。如果要用在实时场景,有几个优化方向。第一,把全连接层换成全局平均池化,参数量从1.2亿降到几乎为零,速度提升明显。第二,用模型剪枝,把不重要的通道剪掉,我试过剪掉30%的通道,准确率只掉1个点,速度提升40%。第三,用更小的输入尺寸,比如把224改成160,速度能快一倍,准确率掉2到3个点。第四,用TensorRT或者ONNX Runtime做推理优化,这个需要一些工程工作,但效果很好。

6. 从VGG延伸出去的几个思考

6.1 VGG的局限与后来者的改进

VGG最大的问题是参数量大、计算量大。1.38亿参数里,1.2亿都在全连接层,而全连接层对空间信息的利用效率很低。后来的网络主要从三个方向改进:一是用全局平均池化替代全连接层,ResNet和GoogLeNet都是这么做的;二是用更高效的卷积模块,比如Inception的多尺度并行卷积、ResNet的残差连接;三是用深度可分离卷积,MobileNet把计算量降了一个数量级。

但VGG的价值不在于它有多高效,而在于它确立了一个清晰的基准:深度是提升性能的关键因素,而且这个提升可以通过简单、统一的模块堆叠来实现。这个思想影响了后面几乎所有网络的设计。

6.2 特征提取中的VGG:为什么它至今还在用

虽然VGG作为分类网络已经过时了,但它的卷积层作为特征提取器,至今还在很多任务里用。比如风格迁移、图像超分辨率、目标检测的特征金字塔,都有人用VGG的中间层特征。原因是VGG的卷积层学到的特征非常“干净”,低层是边缘和纹理,中层是部件和模式,高层是语义概念,层次分明,迁移性好。

我做过一个对比实验,用VGG16和ResNet50的中间层特征做图像检索,在相同维度下,VGG的特征在纹理相似性上表现更好,ResNet的特征在语义相似性上更好。所以选哪个取决于你的任务需求,没有绝对的好坏。

6.3 复现论文时容易忽略的细节

最后说几个复现论文时容易忽略但很重要的细节。第一,论文里的训练用了多尺度数据增强,即把图像缩放到不同尺寸再随机裁剪224×224,这个技巧对性能提升很明显,但很多人只做了固定尺寸裁剪。第二,论文在测试时用了密集评估和全卷积化,把全连接层转成卷积层,对整张图做一次前向,比多次裁剪效率高很多。第三,论文的学习率衰减是基于验证集准确率的,不是固定步数,这个策略需要自己实现。第四,论文的权重初始化是高斯分布,不是现在默认的Kaiming,如果你要严格复现,需要手动设置。

这些细节在论文里都有写,但容易被跳过。我建议至少精读一遍论文的实验部分,把每个超参数的设置和对应的实验结论对应起来,这样你对整个网络的理解会深很多。

我个人在实际操作中的体会是,VGG这篇论文最大的价值不是它提出了多厉害的模型,而是它用极其严谨的实验设计,回答了一个基础问题:深度对卷积网络意味着什么。这个问题在2014年是开放的,现在虽然有了更明确的答案,但论文里的实验方法和分析思路,依然值得反复看。每次重读,我都能发现之前忽略的细节,比如某个对照实验的设计意图,或者某个超参数的选择理由。这种“常读常新”的感觉,在经典论文里很常见,也是它们被称为经典的原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 2:21:24

Linux进程组、控制终端与守护进程:网络服务为何总被SIGHUP杀掉

如果你在一台Linux服务器上跑网络服务&#xff0c;大概率经历过这种诡异的事&#xff1a;程序在终端前台跑得好好的&#xff0c;能监听端口、能正常回包&#xff0c;可只要你退出登录、或者随手关掉终端窗口&#xff0c;这个服务就像被抽掉了地基一样瞬间没了。我曾经因为这个现…

作者头像 李华
网站建设 2026/10/15 2:21:18

Django开发问答系统:从架构设计到性能优化

1. 项目概述"基于Django的在线问答系统"是一个典型的Web应用开发项目&#xff0c;它模拟了类似知乎、Stack Overflow这样的问答社区平台。作为一名有多年Django开发经验的工程师&#xff0c;我经常被问到如何从零开始构建这样一个系统。今天我就来详细拆解这个项目的…

作者头像 李华
网站建设 2026/10/15 2:18:23

SPIMArena:基于PHP的MIPS模拟器Web前端与寄存器可视化

简介&#xff1a;SPIMArena 是面向 UIUC CS 233&#xff08;计算机体系结构&#xff09;课程 SPIMBot 项目的 PHP 前端系统&#xff0c;适合正在学习 MIPS 汇编、准备完成 SPIMBot 竞赛的学生与助教使用。它把原本命令行式的机器人对战搬到网页界面&#xff0c;让参赛者能在线提…

作者头像 李华