资讯动态

VGG深度学习网络:从3×3卷积原理到PyTorch迁移学习实战

发布时间:2026/10/27 17:02:13 来源:尧图企业网站定制
简介VGG深度学习网络框架由牛津大学视觉几何组于2014年提出以大量3x3小卷积核逐层堆叠来加深网络在ImageNet分类任务中表现优异是理解现代CNN架构的基础。这份Python实现资源面向深度学习初学者与研究者用三个衔接紧密的脚本串起从模型构建到实际预测的完整链路可帮助快速理解VGG16与VGG19的层数差异、参数规模来源以及分类任务的工程化流程。压缩包共3个文件全部为.py脚本总大小仅3KB但功能完备一个脚本负责搭建卷积层、池化层和全连接层一个脚本用于加载数据集、选择损失函数与优化器并完成多轮训练另一个脚本演示输入图像尺寸调整、归一化预处理及结果解析。已有246人学习下载适合希望通过阅读代码掌握VGG经典结构并进一步尝试模型迁移或剪枝优化的读者是一份轻量而完整的深度学习实践样例。1. VGG 深度学习网络框架为什么一个 2014 年的模型仍是 Python 深度学习的第一课VGG 是 2014 年 ImageNet 竞赛的亚军冠军是 GoogLeNet但真正活到今天的是这个当年的“千年老二”。它的核心贡献是用连续的小卷积核3×3堆出很深的网络让“深度本身就是一个超参数”从口号变成了可复现的结论。对用 Python 做深度学习的人来说VGG 是入门 CNN 最合适的骨架结构极其规整代码一百行左右就能复现还能直接拿来做迁移学习覆盖分类、检索、特征提取这些常见任务。这篇文章会把 VGG 从原理到 Python 实现完整串一遍给你能直接跑的训练脚本也会把显存溢出、冻结层失效、权重加载报错这些坑提前点出来。全程用 PyTorch新手可以照着敲熟手可以直接拿避坑清单。2. VGG 架构拆解为什么 3×3 小卷积核成了后来 CNN 的标配2.1 VGG 用 3×3 小卷积核替代大卷积核的数学账VGG 相对此前 AlexNet 最大的变化是把大卷积核全部换成 3×3 小卷积核。AlexNet 用了 7×7 和 5×5 的大核感受野大但参数量也大。VGG 的思路很直接用两个串联的 3×3 卷积替代一个 5×5 卷积用三个串联的 3×3 卷积替代一个 7×7 卷积。感受野完全相同但参数量更低而且中间多了两次激活特征的非线性表达能力反而更强。算一笔账就清楚了。假设输入输出通道数都是 C一个 7×7 卷积的参数是 49×C²拆成三个 3×3 卷积后参数是 3×(9×C²)27×C²直接少了四成半。对通道数动辄 512 的深层网络这个省下来的显存和训练时间是实打实的。这也是为什么后来几乎所有主流 CNN 主干包括 ResNet、DenseNet默认都用 3×3 卷积。如果你在面试里被问到“为什么 VGG 用 3×3 而不是一个 7×7”答案就是这两条参数少一半非线性更强。VGG 的整体结构也极规整。卷积段分五段逐段把特征图尺寸减半通道数从 64 依次翻到 128、256、512每段末尾接一次 2×2、步长 2 的最大池化。卷积层只负责特征提取和尺寸压缩最终分类交给三个全连接层。这种“特征提取 分类头”的划分让 VGG 做迁移学习时特别顺手把分类头砍掉卷积段输出就是通用特征下游接什么任务都行。还有一个容易被忽略的点VGG 的输入被固定在 224×224因为 ImageNet 原始尺寸就是 224。如果你换到更高分辨率五段池化之后的特征图就不再是 7×7全连接层会直接报维度不匹配。这个设计放到今天看确实有点僵但工程上处理起来也简单后面第 5 章会专门讲。2.2 VGG11 / VGG13 / VGG16 / VGG19 对比选哪一版VGG 家族按深度分四个档位数字代表带权重的层数。VGG11 是 8 层卷积加 3 层全连接VGG13 是 10 层卷积VGG16 是 13 层卷积VGG19 是 16 层卷积。它们的通道数增长规则完全一样差别只在每段内部叠了几个相同通道数的卷积层。版本卷积层数全连接层参数量约适用场景VGG11831.33 亿跑通流程、代码验证VGG131031.33 亿小规模实验VGG161331.38 亿常规分类、迁移学习主力VGG191631.44 亿对照实验、追求极限选型我一般看三条。第一条是精度和成本的权衡。如果只是验证想法、调试代码VGG11 足够训练快显存占用低问题暴露也快。如果要做正经的分类实验或复现论文效果VGG16 是性价比最优的点。VGG19 在 ImageNet 上比 VGG16 的 top-1 精度提升通常不足一个百分点训练时间却要多个三分之一以上。除非你要做“深度与精度”的对照实验否则没必要追最深的。第二条是迁移学习的兼容性。VGG 的预训练权重在 PyTorch 和 TensorFlow 里都能加载但不同框架的预处理不一样。PyTorch 里 ImageNet 归一化用的是mean(0.485, 0.456, 0.406)、std(0.229, 0.224, 0.225)。如果你换到别的框架预处理里写成均值方差 0.5同一个权重推理出来的概率分布会有明显差异。这个坑在做多框架集成时很常见同一个模型两个结果多半就是预处理没对齐。第三条是输入分辨率。如果任务需要更高分辨率比如检测小目标我会把输入改成 448×448同时在特征层后面补一个AdaptiveAvgPool2d((7,7))让任意尺寸输入都能对齐到 7×7 再进全连接层。代价是中间激活变大显存暴涨所以通常只在离线推理时用。提示VGG 的参数量大头其实在全连接层而不是卷积层。VGG16 全连接三层的参数量占总量六成以上这也是它比 ResNet 显存占用高很多的原因之一。3. 用 Python 在 PyTorch 里复现 VGG完整可跑的代码3.1 手写 VGG 的 nn.Module 定义配置表驱动的灵活写法PyTorch 里搭建 VGG 的经典思路是一张配置表驱动结构。列表里的数字代表卷积输出通道数字符串M代表最大池化。用一个构造函数刷这个字典就能生成 VGG11 到 VGG19 的所有版本。import torch import torch.nn as nn # 结构配置表数字代表卷积输出通道数M 代表 nn.MaxPool2d(2, 2) CFG { vgg11: [64, M, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], vgg13: [64, 64, M, 128, 128, M, 256, 256, M, 512, 512, M, 512, 512, M], vgg16: [64, 64, M, 128, 128, M, 256, 256, 256, M, 512, 512, 512, M, 512, 512, 512, M], vgg19: [64, 64, M, 128, 128, M, 256, 256, 256, 256, M, 512, 512, 512, 512, M, 512, 512, 512, 512, M], } class VGG(nn.Module): def __init__(self, cfg, num_classes1000, init_weightsTrue): super().__init__() self.features self._make_layers(cfg) # 224x224 输入经过五段池化后是 7x7最后一个卷积段输出 512 通道 self.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(True), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(True), nn.Dropout(0.5), nn.Linear(4096, num_classes), ) if init_weights: self._initialize_weights() def _make_layers(self, cfg): layers [] in_channels 3 for v in cfg: if v M: layers.append(nn.MaxPool2d(kernel_size2, stride2)) else: # 所有卷积都是 3x3、padding1、stride1不改变空间尺寸 layers.append(nn.Conv2d(in_channels, v, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) in_channels v return nn.Sequential(*layers) 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)代码里有几个细节值得单独说。第一CFG 里每个数字都会同时生成一个 Conv2d 和一个 ReLU而M只做池化不激活所以最后一个卷积层输出会经过 ReLU 后再进池化这跟 VGG 原结构一致。第二全连接层第一层的输入维度512 * 7 * 7是硬编码的它依赖输入尺寸是 224×224换尺寸就报错这在前面的 2.1 已经提到了。第三_initialize_weights里卷积层用 kaiming_normal_全连接层用方差 0.01 的高斯初始化这是 VGG 原论文里的策略。别觉得这一步多余复现论文结果时初始化方式不同会让收敛曲线有明显差异。第四ReLU 里的inplaceTrue能让一部分内存复用对 VGG 这种激活值很多的大网络能省一点显存代价是不能用torch.autograd追踪某些中间变量。调试阶段如果遇到奇怪的反向传播报错可以先把 inplace 关掉试试。3.2 验证前向传播一份假数据跑通全链路写完网络定义第一件事就是用假数据验证前向传播。这一步能筛掉九成以上低级错误比直接上训练集高效太多。# 实例化 VGG16先做前向传播验证 model VGG(CFG[vgg16], num_classes1000) model.eval() # 模拟一个 batch4 张 224x224 的 RGB 图片 dummy torch.randn(4, 3, 224, 224) with torch.no_grad(): out model(dummy) print(out.shape) # 期望输出 torch.Size([4, 1000])如果输出形状不是[4, 1000]说明维度推导出了问题。最常见的两个错误一是512 * 7 * 7手滑写成512 * 14 * 14二是输入尺寸根本不是 224。把特征层输出的实际形状打印出来再跟全连接层in_features对比马上就能定位。注意这里model.eval()不是可选项。VGG 里有 Dropouttrain 模式下每次前向传播都会随机丢弃一半神经元导致同一份假数据两次跑出来的输出不一样。验证网络结构时先切 eval输出才是确定的。3.3 torchvision 的预训练 VGG为什么工程里我更推荐用它手写版本的优点是透明缺点是权重要从零训练。VGG16 在 ImageNet 上从零训练一张高配卡也得跑两到三周工程里几乎没人这么干。实操时我都是用 torchvision 加载带预训练权重的 VGG在这个基础上微调收敛速度会快一个数量级。from torchvision import models # 加载带 ImageNet 预训练权重的 VGG16 model models.vgg16(weightsmodels.VGG16_Weights.DEFAULT) model.eval() # 只保留特征层用于提取通用特征 features_only nn.Sequential(*list(model.features.children())) out features_only(torch.randn(1, 3, 224, 224)) print(out.shape) # torch.Size([1, 512, 7, 7])用list(model.features.children())转成列表再塞进nn.Sequential是因为children()返回的迭代器只能遍历一次。经过这一步模型只剩卷积和池化后面接什么头完全由你决定。如果你的任务分类数不是 1000把最后全连接层的输出维度改成自己的类别数就行前面积累的特征权重可以保留只训练新分类头。权重下载多说一句weightsmodels.VGG16_Weights.DEFAULT会自动解析并下载官方权重不需要手动下载文件之后再去指定路径。首次下载依赖网络环境如果下载超时或断连确认网络稳定之后直接重跑PyTorch 会自动继续断点。有一点要注意PyTorch 2.6 之后部分模型的weights参数类型更严格老代码里写的pretrainedTrue已经不建议用了新代码统一用weights那种写法。4. VGG 训练的关键参数学习率、数据增强与正则化怎么设4.1 优化器与学习率SGD 还是 Adam迁移学习里怎么拆分参数组VGG 是 2014 年的模型那个年代的主流优化器就是带动量的 SGD。放到今天直接用 Adam 训练 VGG 也完全能收敛但你会发现一个现象Adam 收敛快、loss 下降顺利验证集精度却经常打不过带动量的 SGD。原因是 VGG 参数规模大SGD 配合 momentum 能走出一条更稳定的优化路径Adam 的逐参数自适应学习率在深层网络上容易把模型推到一个尖锐极小值附近泛化性偏差。我自己调试阶段用 Adam 快速确认代码和数据逻辑正式训练换回 SGD。标准配置是import torch.optim as optim optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) # 每 30 个 epoch 学习率乘 0.1 scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1)lr0.01是 VGG 在 ImageNet 上常用的起点。如果你用的是 Adam学习率必须降到 1e-4 量级直接拿 0.01 给 Adam 开局第一轮 loss 就会乱跳。step_size和gamma要看数据集规模调整。小数据集上我会把step_size改到 15 到 20 轮让学习率更早降下去因为小数据的训练曲线到后期更容易过拟合。迁移学习场景里特征层和分类头的学习率应该分开。分类头是随机初始化的梯度信号强可以用较大的学习率快速拟合特征层如果是从预训练权重来的学习率太高会直接把预训练特征破坏掉。我常用的写法是给优化器传参数组optimizer optim.SGD([ {params: model.features.parameters(), lr: 0.001}, {params: model.classifier.parameters(), lr: 0.01}, ], momentum0.9, weight_decay5e-4)逻辑是预训练部分的权重已经接近一个“好的特征提取器”即使它当时是针对 ImageNet 优化的也不希望前几轮训练就把它们大幅改动。而分类头是完全新的需要快速拟合。这个技巧对 VGG 特别重要因为它的特征层参数多、分类头参数更大统一学习率往往顾此失彼。4.2 数据增强防过拟合的性价比最高手段VGG 全连接层的参数量占了整个模型一大半小数据集上过拟合几乎是必然。数据增强是成本最低、效果最明显的缓解手段性价比远高于换模型。我常用的训练和验证变换组合如下from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.08, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.4, 0.4, 0.4, 0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])训练和验证的变换不是同一套这一点很关键。训练集用随机裁剪相当于每张图每次裁的位置和缩放比例都随机每一轮的输入都不一样数据量直接扩了几十倍。验证集不随机先 Resize 到 256 再 CenterCrop 到 224保证每个验证样本的处理方式一致指标才有可比性。如果验证集也做随机裁剪你每次评估的输入都不同验证 loss 曲线会像呼吸一样上下波动没法判断模型是真的在变好还是随机噪声。ColorJitter的参数也要小心。亮度、对比度、饱和度分别设 0.4 看起来温和但在细粒度分类任务里比如区分两种外观相近的鸟类颜色扰动过强会把判别性特征污染掉。遇到这种任务我会把ColorJitter的数值调低到 0.1 或直接去掉只保留随机裁剪和翻转。4.3 Dropout 与权重衰减VGG 自带的正则化不能丢VGG 的三个全连接层之间有两个 Dropout概率 0.5这是原论文就有的设计。很多人复现时觉得 Dropout 老套直接删掉小数据集上验证精度会掉得很明显。权重衰减设 5e-4 是标准值但我见过有人从别的代码里复制成 1e-2训练直接发散。如果你拿不准就保持 5e-4。发散时优先检查学习率是不是太大其次再查权重衰减。另外PyTorch 的weight_decay默认对所有参数生效包括 BN 层的缩放和偏移。有些实现会专门排除 BN 参数但对 VGG 这种 BN 在 2014 年还属于辅助结构的模型我倾向于直接全部施加省心也更容易对照原论文。5. VGG 实战避坑显存溢出、冻结失效、权重下载断连的排查记录5.1 改输入尺寸导致全连接层维度报错入门时最容易撞见。训练时把图片从 224×224 换成 256×256第一次 forward 就报in_features不匹配。原因前面说过VGG 全连接层写死了512 * 7 * 7输入尺寸一变五段池化后的特征图就不是 7×7 了。解决方法是保持输入的 224×224或者在特征层后加一个AdaptiveAvgPool2d((7,7))把任意尺寸统一到 7×7。第二种做法省心但会改变特征分布一般只用在推理阶段。5.2 预训练权重加载报 key 不匹配或 size mismatch把 VGG 的num_classes改成自己的类别数之后再用load_state_dict加载 ImageNet 权重会报size mismatch for classifier.6。这个报错是良性的因为你把最后的全连接层维度改了和预训练权重的1000对不上。标准做法是加strictFalse跳过分类头权重只加载特征层部分state_dict torch.load(vgg16_weights.pth, weights_onlyFalse) model.load_state_dict(state_dict, strictFalse)strictFalse会忽略 key 或形状不匹配的参数。这是迁移学习里加载预训练权重的常见姿势。5.3 冻结了特征层但 BN 统计量还在变迁移学习时把features层所有参数都设成requires_gradFalse训练新分类头结果验证 loss 震荡。原因是 PyTorch 的 BatchNorm 在train模式下即使参数不更新running_mean和running_var也会随当前 batch 更新。特征段虽然冻结了但 BN 层仍然处于“在线更新统计量”状态前向传播的结果不稳定。正确冻结姿势是这样的for p in model.features.parameters(): p.requires_grad False model.features.eval() # 关键BN 统计量不再被 batch 更新 model.classifier.train() # 分类头保持训练模式model.features.eval()会把该模块下的所有 BN 层切到推理状态统计量不再更新。检查有没有冻结干净可以打印特征段 BN 的running_mean在训练前后对比一下数值如果变了就说明没冻结住。5.4 显存不够减小 batch size 反而让 loss 震荡VGG16 参数量 1.38 亿训练时的中间激活值占用比参数本身还大。显存溢出时很多人的第一反应是把 batch size 从 32 减到 8结果 loss 开始剧烈震荡。原因是 VGG 的 BN 对 batch size 敏感batch 太小时 BN 统计量噪声大训练不稳定。我的排查顺序是先看是不是有别的地方在占用显存比如同时加载了多个模型再把输入尺寸从 224 压到 160 或 128空间尺寸降下来中间激活按平方级别下降最后才用混合精度 AMP 训练这个对 VGG 很友好显存占用能省一半以上。AMP 的代码模板如下from torch.cuda.amp import autocast, GradScaler scaler GradScaler() with autocast(): out model(inputs) loss criterion(out, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast只在前向和反向计算时用低精度优化器参数仍然保持 FP32所以精度损失很小。VGG 这种结构没有对精度敏感的操作用 AMP 基本无感。5.5 断点续训时学习率恢复到了初始值训练到第 40 轮机器重启你从 checkpoint 恢复训练结果学习率从原本的 0.001 跳回了 0.01训练曲线直接起飞。原因是只保存了模型和优化器状态忘了scheduler。恢复训练时scheduler从第 0 轮重新计数学习率当然回到初始值。解决方法是保存 checkpoint 时把scheduler.state_dict()一起存进去恢复时也一起加载这个习惯能帮你避开很多训练中断后无法对齐曲线的破事。6. 拿 VGG 做迁移学习冻结策略、验证指标和我的习惯6.1 冻结还是微调两条路的适用范围不同从零训练 VGG 在大型数据集上是可行的但你手上如果只有几千张图微调几乎是唯一合理的选择。微调也有两种策略。第一种是特征提取冻结全部特征层只训练新的分类头适合数据量小、目标域和预训练域差异不大的任务。第二种是全局微调用较小学习率更新整个网络特征层学习率通常设为分类头的十分之一适合数据量中等以上、目标域有较大偏移的任务。我的经验是数据量少于三万张时先做特征提取把分类头训收敛再决定要不要解冻最后一段特征层做整体微调。这个顺序能帮你省掉很多不必要的调试时间也比一上来就全局微调更稳定。6.2 验证指标不是训练 loss是验证集损失迁移学习里最常见的误区是只看训练 loss 下降就以为模型在变好。VGG 的分类头很大过拟合出现得比现代轻量网络早得多。正确的做法是盯着训练 loss 和验证 loss 之间的距离训练 loss 持续下降、验证 loss 开始回升说明从那个 epoch 起已经过拟合此时应该回退到之前保存的最优权重。所以我每轮结束都会保存验证集损失最低的模型而不是最后一轮。checkpoint 里除了模型权重和优化器状态还必须包含 epoch 数和 scheduler 状态这个习惯在 5.5 说过值得再强调一遍。6.3 我的习惯先用 VGG11 跑通链路再上 VGG16最后说一条我的个人习惯新任务拿到手先用 VGG11 加一个小型训练流程把数据加载、数据增强、训练循环、验证评估这条链路完整跑通再换成 VGG16。原因很简单——很多 bug 跟网络深度无关VGG11 训练几分钟就能暴露数据或代码问题VGG16 可能要等一两个小时才出结果。链路确认没问题之后再把 VGG11 换成 VGG16batch size 和学习率微调一下其余不动。这个习惯帮我避开了很多“训练了两小时loss 却一路发散”的血泪场面换成你大概率也用得上。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑