VGG16与VGG19实战:从结构解析到Python迁移学习与特征提取
简介这份资源面向深度学习入门与进阶开发者提供VGG网络从模型定义、训练到预测的完整Python实现适合希望理解经典卷积网络结构并动手复现图像分类流程的学习者。压缩包共3个文件均为py脚本整体约3KB分别承担模型构建、训练调度与推理预测三类核心职责结构精简、职责清晰便于快速通读与二次修改。目前已有246人学习下载说明其在同类基础实战资料中具备一定参考价值。通过阅读与运行代码读者可以掌握VGG中3x3卷积堆叠、池化与全连接分类层的组织方式理解损失函数、优化器、批次大小与训练周期等关键参数的配置逻辑并学会加载已训练权重对新图像进行预处理与类别解析。资源还提示了VGG参数庞大、计算资源要求高、训练与推理速度偏慢等现实问题有助于读者建立模型选型与轻量化优化的意识是衔接理论与工程实践的合适练手材料。1. 从一张 224×224 的图说起VGG 为什么还在被拿来跑基线手里有一批口腔病灶图或者工业缺陷图想快速搭一个能跑通的分类基线很多人绕了一圈还是回到 VGG。原因不玄学它的结构规整到近乎笨拙全部用 3×3 小卷积堆叠通道数按 64、128、256、512 逐段翻倍最后接三层全连接输出类别。这种「一块一块往上垒」的写法让它在 Python 里复现成本极低也方便你逐层对照特征图看清卷积到底学到了什么。VGG 是深度学习 CNN 家族里少见的、结构图能直接翻译成代码的模型对刚配好深度学习环境、准备动手深度学习实战项目案例的人来说它是最不容易翻车的第一站。这篇就按「结构怎么读、Python 怎么搭、参数怎么调、坑在哪」四步走把 VGG 从论文里的方块图落到你能跑的脚本上。2. VGG 的结构账本16 层和 19 层到底差在哪2.1 用配置表读懂 VGG16 与 VGG19 的堆叠规律VGG 的论文里给了一张按列排布的配置表A 到 E 五组最常被拿来用的是 DVGG16和 EVGG19。读这张表的诀窍是把它当成「卷积段 池化」的重复序列而不是逐层死记。每个卷积段内部通道数不变段与段之间通道翻倍每段结束接一次 2×2 最大池化把空间尺寸减半。阶段VGG16 卷积配置VGG19 卷积配置输出通道特征图尺寸输入 224段1conv3-64 ×2conv3-64 ×264224→112段2conv3-128 ×2conv3-128 ×2128112→56段3conv3-256 ×3conv3-256 ×425656→28段4conv3-512 ×3conv3-512 ×451228→14段5conv3-512 ×3conv3-512 ×451214→7分类头FC-4096, FC-4096, FC-1000同左10007×7 展平VGG16 和 VGG19 的差别只在段3、段4、段5 各多插了一个 3×3 卷积参数量增加约 2000 万但感受野和通道结构完全一致。这意味着你换 16 和 19 时数据管道、输入尺寸、训练超参几乎不用动只改一处配置即可。对做迁移学习的人来说这个特性很省事先用 VGG16 把流程跑通再换 VGG19 看精度有没有涨对比实验的变量是干净的。2.2 为什么全是 3×3 小卷积感受野与参数量的取舍两个 3×3 卷积堆叠的感受野等于一个 5×5三个 3×3 等于一个 7×7但参数量更少、非线性更多。以 7×7 为例单个 7×7 卷积核参数量是 49×C×C三个 3×3 是 3×9×C×C27×C×C少了将近一半还多插了两次 ReLU。这就是 VGG 坚持小卷积的核心理由也是它在当年能压过更大卷积核方案的原因。落到代码上这个规律让你可以用循环批量生成卷积层而不是手写十几行。下面这段用 PyTorch 的 nn.ModuleList 按配置表动态堆叠改配置就能在 VGG16 和 VGG19 之间切换。import torch import torch.nn as nn # 每个元素是 M最大池化或整数输出通道数 cfgs { 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], } def make_features(cfg): layers [] in_ch 3 # RGB 输入 for v in cfg: if v M: layers.append(nn.MaxPool2d(kernel_size2, stride2)) else: layers.append(nn.Conv2d(in_ch, v, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) in_ch v return nn.Sequential(*layers) class VGG(nn.Module): def __init__(self, namevgg16, num_classes1000): super().__init__() self.features make_features(cfgs[name]) self.avgpool nn.AdaptiveAvgPool2d((7, 7)) # 兼容非 224 输入 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), ) def forward(self, x): x self.features(x) x self.avgpool(x) x torch.flatten(x, 1) return self.classifier(x) if __name__ __main__: net VGG(vgg16, num_classes10) dummy torch.randn(2, 3, 224, 224) print(net(dummy).shape) # torch.Size([2, 10])逻辑说明make_features按配置表顺序生成层遇到 M 就插池化遇到整数就插「卷积 ReLU」in_ch记录上一层的输出通道保证卷积输入输出对得上。AdaptiveAvgPool2d((7,7))是关键改动它让网络在输入不是 224 时也能把特征图压到 7×7避免全连接层维度对不上直接报错。参数说明kernel_size3, padding1保证卷积后空间尺寸不变尺寸缩减全部交给池化Dropout(0.5)放在两个 4096 全连接之间是 VGG 原论文抑制过拟合的标准做法num_classes改成你自己的类别数做口腔疾病识别就填病灶类别数做缺陷分类就填缺陷类型数。2.3 全连接层是 VGG 最重也最该动刀的地方VGG16 的参数量约 1.38 亿其中全连接层占了约 1.02 亿卷积部分只有 1500 万左右。这意味着你显存吃紧、推理慢八成不是卷积的锅而是那三层全连接。常见做法是把classifier换成全局平均池化加一个线性层参数量直接掉到几百万精度在多数迁移任务上不掉甚至更稳。# 轻量分类头全局平均池化替代 4096 全连接 self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 每个通道压成一个数 nn.Flatten(), nn.Dropout(0.3), nn.Linear(512, num_classes), )这样改完输入尺寸可以更自由显存占用大幅下降适合在单张消费级显卡上跑。代价是丢掉了一部分全连接带来的全局拟合能力如果你的数据集很小、类别差异细微可以先保留原版全连接等确认过拟合严重再换轻量头。3. 在 Python 里把 VGG 跑起来数据、训练、微调三步3.1 用 torchvision 加载预训练权重做迁移学习从零训练 VGG 需要百万级数据和大量算力实际项目里几乎都走迁移学习。torchvision 自带 VGG16、VGG19 的预训练权重加载后替换分类头即可。注意 torchvision 的 VGG 输入要求是 224×224且做了 ImageNet 的均值方差归一化。import torch import torch.nn as nn from torchvision import models, transforms from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader # 1. 数据管道训练集做增强验证集只做归一化 train_tf transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), # 随机裁剪增强 transforms.RandomHorizontalFlip(), # 水平翻转 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf 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]), ]) train_ds ImageFolder(data/train, transformtrain_tf) val_ds ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 2. 加载预训练 VGG16替换分类头 net models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) num_classes len(train_ds.classes) net.classifier[6] nn.Linear(4096, num_classes) # 只换最后一层 # 3. 冻结卷积部分先只训分类头 for p in net.features.parameters(): p.requires_grad False device torch.device(cuda if torch.cuda.is_available() else cpu) net net.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD( filter(lambda p: p.requires_grad, net.parameters()), lr1e-3, momentum0.9, weight_decay5e-4)逻辑说明ImageFolder要求数据按类别分文件夹存放data/train/类别名/xxx.jpg这种结构。net.classifier[6]是 VGG 分类头的最后一层线性层替换成你的类别数。冻结features后只训练分类头能在小数据集上快速收敛且不易过拟合。参数说明batch_size32在 8GB 显存上跑 VGG16 比较稳显存不够就降到 16lr1e-3是只训分类头时的常用学习率等解冻卷积后要降到 1e-4 量级weight_decay5e-4配合 SGD 抑制过拟合。3.2 训练循环与学习率调度训练循环本身不复杂关键是加验证、存最优权重、按 epoch 调学习率。VGG 收敛偏慢前几个 epoch 精度不动是正常的别急着换模型。from torch.optim.lr_scheduler import StepLR scheduler StepLR(optimizer, step_size7, gamma0.1) best_acc 0.0 for epoch in range(20): net.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(net(imgs), labels) loss.backward() optimizer.step() # 验证 net.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred net(imgs).argmax(1) correct (pred labels).sum().item() total labels.size(0) acc correct / total scheduler.step() print(fepoch {epoch}: val_acc{acc:.4f}) if acc best_acc: best_acc acc torch.save(net.state_dict(), best_vgg16.pth)逻辑说明每个 epoch 先训练后验证net.eval()关掉 Dropout 和 BatchNorm 的训练行为torch.no_grad()省显存。StepLR每 7 个 epoch 把学习率乘 0.1让后期收敛更细。参数说明step_size7和总 epoch 数相关总轮数 20 时设 7 比较合适gamma0.1是标准衰减系数保存state_dict而不是整个模型加载时更灵活。3.3 解冻卷积做微调什么时候该做、学习率怎么设只训分类头精度到顶后如果还差几个点可以解冻后面几个卷积段做微调。VGG 的浅层学的是边缘、纹理这类通用特征深层才是任务相关所以通常只解冻段4、段5。# 解冻最后两个卷积段段4、段5 for name, p in net.features.named_parameters(): if name.startswith((24, 26, 28)): # 段5 的层索引 p.requires_grad True # 微调阶段用更小的学习率 optimizer torch.optim.SGD( filter(lambda p: p.requires_grad, net.parameters()), lr1e-4, momentum0.9, weight_decay5e-4)逻辑说明net.features是一个 Sequential层索引从 0 开始VGG16 的段5 大致落在索引 24 之后具体索引可以打印net.features确认。解冻后学习率必须降否则预训练权重会被大梯度冲垮前功尽弃。参数说明lr1e-4是微调卷积层的保守值数据量大可以试 5e-4解冻层数越多学习率越要小。4. VGG 落地避坑显存、输入尺寸、过拟合的排查清单4.1 显存爆了先查全连接不是卷积现象batch_size 设到 8 还是 OOMnvidia-smi 显示显存瞬间打满。原因VGG 全连接层参数量占七成以上第一层Linear(512*7*7, 4096)权重矩阵就有约 1 亿参数光这一层就吃掉几百 MB。解决换全局平均池化分类头或把 batch_size 降到 4 并开启梯度累积还可以用torch.cuda.amp混合精度训练显存能省三到四成。4.2 输入尺寸不是 224 时全连接维度报错现象RuntimeError: mat1 and mat2 shapes cannot be multiplied。原因VGG 原版全连接写死了 512×7×7输入不是 224 时特征图尺寸对不上。解决在features和classifier之间加AdaptiveAvgPool2d((7,7))强制压到 7×7或者干脆改成全局平均池化输入尺寸就彻底自由了。注意用预训练权重时输入偏离 224 太多精度会掉别为了省事把 512 的图直接塞进去。4.3 训练精度高验证精度低过拟合的三种压法现象训练集准确率冲到 99%验证集卡在 70% 不动。原因VGG 参数量大小数据集上极易过拟合。解决第一加数据增强随机裁剪、翻转、颜色抖动都上第二提高 Dropout 比例全连接间的 0.5 可以试 0.6第三加权重衰减并早停验证精度连续几个 epoch 不涨就停。如果数据量实在小冻结全部卷积只训分类头往往比全量微调更稳。4.4 BatchNorm 与预训练权重不匹配现象加载预训练权重后 loss 直接变 NaN。原因自己搭的 VGG 如果加了 BatchNorm而预训练权重里没有对应参数加载时键名对不上或者归一化统计量不匹配。解决要么用 torchvision 的原版结构无 BatchNorm要么加载时用strictFalse并确认缺失的键只是新增层。归一化参数必须和预训练时一致ImageNet 的均值方差别自己乱改。4.5 学习率设太大导致预训练权重被冲毁现象微调一开始 loss 暴涨几个 batch 后精度崩盘。原因解冻卷积层后仍用 1e-3 的学习率大梯度把预训练学到的特征直接覆盖。解决微调卷积层学习率降到 1e-4 甚至 1e-5分类头可以单独设更大的学习率用参数组分开配置。这是迁移学习最常见的翻车点没有后悔药只能重训。5. 把 VGG 当特征提取器一个省算力的进阶用法训练到后面你会发现很多任务根本不需要微调整个 VGG把它当固定特征提取器就够用。做法是去掉classifier只保留features对每张图输出 512×7×7 的特征图再做全局平均池化得到 512 维向量拿去喂给 SVM、逻辑回归或者一个小 MLP。这样一次前向就能把整个数据集编码成特征矩阵后续分类器训练秒级完成特别适合数据量不大、又不想反复调网络的场景。import numpy as np import torch from torchvision import models # 只取卷积部分做特征提取 backbone models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1).features backbone.eval().to(device) def extract_features(loader): feats, labels [], [] with torch.no_grad(): for imgs, lbls in loader: imgs imgs.to(device) fmap backbone(imgs) # [B, 512, 7, 7] vec fmap.mean(dim[2, 3]) # 全局平均池化 - [B, 512] feats.append(vec.cpu().numpy()) labels.append(lbls.numpy()) return np.concatenate(feats), np.concatenate(labels) X_train, y_train extract_features(train_loader) X_val, y_val extract_features(val_loader) print(X_train.shape) # (N, 512) # 接一个线性分类器 from sklearn.linear_model import LogisticRegression clf LogisticRegression(max_iter1000, C1.0) clf.fit(X_train, y_train) print(val acc:, clf.score(X_val, y_val))逻辑说明backbone只保留卷积层输出 512 通道的特征图mean(dim[2,3])对空间维度求平均得到每张图的 512 维表示。这个表示已经包含了 ImageNet 学到的通用视觉特征对很多任务直接可用。参数说明C1.0是逻辑回归的正则强度特征维度高时可以调小到 0.1 抑制过拟合如果 512 维不够可以同时取段4 和段5 的特征拼接维度翻倍但信息更全。这个用法最大的好处是把「训练深度网络」变成「训练浅层分类器」算力需求降一个数量级调参也简单。我一般会先用它快速探一下数据集的难度上限如果线性分类器就能到 85%说明特征够好再上微调提升空间有限如果只有 60%那才值得花时间解冻卷积做端到端训练。这个判断习惯帮我省过很多次无谓的 GPU 时间希望帮到你。本文还有配套的精品资源点击获取