简介这份资源面向深度学习入门与进阶开发者提供VGG网络从模型构建、训练到预测的完整Python实现适合希望理解经典卷积网络结构并动手复现图像分类流程的学习者。压缩包共3个文件均为py脚本整体约3KB分别承担模型定义、训练调度与推理预测三类核心职责结构精简、职责清晰便于快速通读与二次修改。目前已有246人学习下载可作为深度学习实战的入门参考。通过阅读与运行代码读者能够掌握VGG中3x3卷积堆叠、池化与全连接分类层的组织方式理解损失函数、优化器、批次大小与训练周期等关键参数的配置逻辑并学会加载已训练权重对新图像进行预处理与类别解析。资源同时提示了VGG参数量大、计算资源要求高、训练与推理速度偏慢等现实问题有助于读者在后续实践中权衡模型深度与效率为模型剪枝或轻量化选型提供对照依据。1. 从一份 VGG 源码包说起它到底能跑出什么如果你手里正好有一份名为vgg_VGG深度学习网络框架_VGGpython_vgg_的源码包第一反应大概率是这玩意儿是能直接训练的完整工程还是只是把论文里的网络结构抄了一遍我拆过不少这类以经典网络命名的包结论是——它更像一套「VGG 的 Python 实现骨架」核心价值在于把 VGG16 / VGG19 的卷积块堆叠逻辑、特征通道变化、全连接层映射用可读的代码固定下来让你能拿它当分类任务的底座或者当特征提取器接到自己的检测、分割流程里。它适合两类人一类是刚学完 CNN 理论、想找一个结构干净、不依赖花哨框架的 VGG 实现来对照论文读代码的人另一类是做迁移学习需要一个能加载预训练权重、能改分类头、能冻结卷积层的工程起点的人。不适合指望开箱即得 SOTA 精度的人VGG 参数量大、推理慢这是结构决定的不是代码写得不好。下面我按「结构怎么读 → 环境怎么配 → 训练怎么跑 → 坑在哪 → 怎么榨干它」的顺序把这份包拆开讲透。2. VGG 结构在代码里长什么样从卷积块到分类头2.1 为什么 VGG 用 3×3 小卷积堆叠VGG 最核心的设计决策是用连续的 3×3 卷积代替大卷积核。两个 3×3 卷积堆叠的感受野等于一个 5×5三个堆叠等于 7×7但参数量更少、非线性更强。以 3 通道输入为例一个 7×7 卷积核参数量是3×7×7×C 147C而三个 3×3 堆叠是3×(3×3×C×C) 27C²这里 C 为通道数实际比较要看输入输出通道在通道数不大时小卷积堆叠明显更省。代码里通常用一个make_layers函数按配置列表生成卷积序列这是读这份包的第一个关键点。# 典型的 VGG 卷积块生成逻辑 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_layers(cfg, batch_normFalse): layers [] in_channels 3 # RGB 输入 for v in cfg: if v M: layers [nn.MaxPool2d(kernel_size2, stride2)] else: conv2d nn.Conv2d(in_channels, v, kernel_size3, padding1) if batch_norm: layers [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplaceTrue)] else: layers [conv2d, nn.ReLU(inplaceTrue)] in_channels v return nn.Sequential(*layers)这段代码里M代表最大池化数字代表该层输出通道数。padding1保证 3×3 卷积后空间尺寸不变池化层负责降采样。batch_norm开关是这份包是否「现代化」的分水岭——原版 VGG 论文没有 BN训练极难收敛加了 BN 之后才能用较大学习率。你拿到包后第一件事就是确认这个开关默认值如果是False且你要从头训练建议手动打开。2.2 分类头的三层的全连接与 4096 维瓶颈卷积部分结束后VGG 接的是三层全连接4096 → 4096 → 类别数。第一个全连接层把512×7×7的特征图展平成 25088 维映射到 4096。这个 4096 维向量就是 VGG 最常被拿来当特征用的地方——很多检索、聚类任务直接取这一层输出而不是最后的分类 logits。class VGG(nn.Module): def __init__(self, features, num_classes1000, init_weightsTrue): super(VGG, self).__init__() self.features features self.avgpool nn.AdaptiveAvgPool2d((7, 7)) # 兼容非 224 输入 self.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(True), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(True), nn.Dropout(), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x self.avgpool(x) x torch.flatten(x, 1) x self.classifier(x) return xAdaptiveAvgPool2d((7, 7))是个实用改动它让网络能接受非 224×224 的输入而不报维度错误。Dropout()默认 0.5是 VGG 全连接层防过拟合的主要手段。如果你要改分类数只动num_classes即可但注意预训练权重里最后那层Linear(4096, 1000)的维度会对不上加载时需要跳过或重建。2.3 特征层与分类层分离带来的迁移便利这份包把features和classifier拆成两个独立属性这是迁移学习最舒服的结构。冻结特征层只训分类头或者把features单独拿出来当 backbone都不需要改网络定义。model VGG(make_layers(cfgs[VGG16], batch_normTrue), num_classes10) # 场景一冻结卷积层只训练分类头 for param in model.features.parameters(): param.requires_grad False # 场景二把 features 当特征提取器 feature_extractor model.features with torch.no_grad(): feat feature_extractor(torch.randn(1, 3, 224, 224)) print(feat.shape) # torch.Size([1, 512, 7, 7])冻结卷积层适合小数据集训练快、不易过拟合解冻全部层做微调适合数据量中等以上的场景但学习率要调小通常设为基础学习率的十分之一。特征提取器模式则常见于图像检索、风格迁移这类不需要分类头的任务。3. 环境配置与预训练权重加载让包真正跑起来3.1 Python 环境与 PyTorch 版本选择这类 VGG 包通常基于 PyTorch 编写少数是 TensorFlow/Keras。先看import语句确认框架。PyTorch 版本建议 1.10 以上太老的版本AdaptiveAvgPool2d和权重加载接口有差异。环境配置的常见做法是用 conda 建独立环境避免和系统 Python 冲突。# 创建并激活环境 conda create -n vgg_demo python3.9 -y conda activate vgg_demo # 安装 PyTorch以 CUDA 11.8 为例无显卡去掉 cu118 用 cpu 版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证 python -c import torch; print(torch.__version__, torch.cuda.is_available())如果你用 VSCode 或 PyCharm记得把解释器切到刚建的环境否则会出现「明明装了 torch 却 import 失败」的经典翻车。torch.cuda.is_available()返回False时先查显卡驱动和 CUDA 版本匹配别急着改代码。3.2 加载预训练权重的两种路径VGG 的预训练权重有两种来源torchvision.models.vgg16(pretrainedTrue)自动下载或者手动下载.pth文件用load_state_dict加载。前者方便但依赖网络后者适合离线环境。import torch import torchvision.models as models # 路径一torchvision 自动加载 model models.vgg16(pretrainedTrue) # 路径二手动加载本地权重 model models.vgg16(pretrainedFalse) state_dict torch.load(vgg16-397923af.pth, map_locationcpu) model.load_state_dict(state_dict) # 改分类头适配自己的类别数 num_features model.classifier[6].in_features model.classifier[6] torch.nn.Linear(num_features, 5) # 5 分类 model model.to(cuda if torch.cuda.is_available() else cpu)map_locationcpu是离线加载的关键否则在无 GPU 机器上加载 GPU 保存的权重会报错。改分类头时只替换classifier[6]前面层保留预训练参数这是最省事的迁移方式。注意pretrainedTrue在新版 torchvision 里已改为weightsIMAGENET1K_V1老代码跑不通时按这个改。3.3 数据加载与标准化参数VGG 预训练权重是在 ImageNet 上训的输入必须做相同的标准化否则精度会莫名其妙掉一大截。均值[0.485, 0.456, 0.406]、标准差[0.229, 0.224, 0.225]是 ImageNet 的统计值别改成 0.5。from torchvision import transforms, datasets train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), # 训练时随机裁剪做增强 transforms.RandomHorizontalFlip(), # 随机水平翻转 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]), ]) train_set datasets.ImageFolder(data/train, transformtrain_transform) train_loader torch.utils.data.DataLoader(train_set, batch_size32, shuffleTrue, num_workers4)Resize(256)再RandomCrop(224)是标准做法比直接Resize(224)保留更多信息。num_workers在 Windows 上设 0 更稳Linux 上设 4 或 8。数据目录按ImageFolder要求组织成train/类别名/图片的结构类别名就是标签。4. 训练、微调与特征提取三种用法各跑一遍4.1 从头训练一个小数据集分类器数据量小的时候不建议从头训 VGG参数量 1.38 亿几千张图根本喂不饱。但如果你的数据有几十万张或者只是想验证代码能跑通可以走一遍完整训练流程。import torch.nn as nn import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() # 只优化分类头参数卷积层冻结 optimizer optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr0.001, momentum0.9, weight_decay5e-4) for epoch in range(10): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fepoch {epoch1}, loss {running_loss/len(train_loader):.4f})filter(lambda p: p.requires_grad, ...)确保只更新解冻的参数。学习率 0.001 配 SGD 是冻结卷积层时的常用值如果解冻全部层要降到 0.0001。weight_decay是 L2 正则VGG 全连接层参数多加一点能缓解过拟合。4.2 冻结卷积层做迁移学习这是这份包最实用的场景。把features冻住只训classifier几分钟就能出结果适合快速验证数据标注质量。# 冻结所有卷积层 for param in model.features.parameters(): param.requires_grad False # 替换分类头 model.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 256), nn.ReLU(True), nn.Dropout(0.5), nn.Linear(256, num_classes), ) model model.to(device) optimizer optim.Adam(model.classifier.parameters(), lr1e-3)这里把原来的两层 4096 全连接换成一层 256参数量从 1 亿多降到千万级训练快很多小数据集上反而不容易过拟合。Adam比 SGD 收敛快适合这种只训头部的场景。如果你的类别和 ImageNet 差异大比如医学图像、工业缺陷冻结层数可以少一些解冻最后两个卷积块效果更好。4.3 用 features 做图像特征提取与相似度检索把 VGG 当特征提取器是它在分类之外最常见的用法。取features输出或classifier第一层之后的 4096 维向量做归一化后算余弦相似度。model.eval() feat_extractor nn.Sequential(*list(model.children())[:-1]) # 去掉 classifier def extract_feature(img_tensor): with torch.no_grad(): feat model.features(img_tensor.to(device)) feat model.avgpool(feat) feat torch.flatten(feat, 1) # 取 4096 维全连接输出 feat model.classifier[:4](feat) return feat # 余弦相似度检索 import torch.nn.functional as F feat_a F.normalize(extract_feature(img_a), dim1) feat_b F.normalize(extract_feature(img_b), dim1) similarity (feat_a * feat_b).sum().item() print(f相似度: {similarity:.4f})model.classifier[:4]取的是Linear(25088,4096) → ReLU → Dropout → Linear(4096,4096)这一段输出 4096 维。做检索时记得F.normalize否则内积受向量模长影响。特征提取模式一定要model.eval()否则 Dropout 会随机丢神经元同一张图两次提取结果不一致这是很多人踩过的玄学坑。5. 避坑与排查VGG 跑不起来时先看这几条5.1 现象loss 一直是 2.3 左右不下降原因分类头没换或者换了但没排除预训练权重里的旧分类层导致输出维度和标签对不上或者标准化参数用错。解决确认classifier[6]的输出维度等于你的类别数确认Normalize用的是 ImageNet 均值方差确认标签从 0 开始连续编码。5.2 现象显存爆了batch_size 降到 1 还报 OOM原因VGG 全连接层参数量巨大Linear(25088,4096)一层就占约 400MB 显存float32加上中间激活值batch_size 稍大就撑不住。解决冻结卷积层减少梯度存储把classifier换成更小的结构用混合精度训练torch.cuda.amp或者直接用features做特征提取不走全连接。5.3 现象加载预训练权重报size mismatch for classifier.6.weight原因你改了num_classes但直接load_state_dict会严格匹配所有层。解决用strictFalse跳过不匹配的层或者先加载再替换分类头。state_dict torch.load(vgg16.pth) model.load_state_dict(state_dict, strictFalse) # 忽略分类头维度不匹配5.4 现象验证集准确率远低于训练集原因VGG 全连接层过拟合能力强小数据集上尤其明显。解决加大 Dropout 比例、加 weight_decay、做数据增强、冻结更多卷积层。如果训练集准确率 99% 而验证集 60%基本就是过拟合别怀疑代码有 bug。5.5 现象推理速度慢到无法接受原因VGG16 单张 224×224 图在 CPU 上约 0.51 秒GPU 上约 1020 毫秒这是结构决定的。解决如果只是要特征取features输出后接全局平均池化跳过全连接或者换更轻的 backbone。VGG 的定位是「结构清晰的教学与迁移底座」不是高吞吐推理模型。6. 把 VGG 用出性价比特征层截断与通道剪枝的实操VGG 全连接层占了约 1.2 亿参数是整个网络最臃肿的部分。如果你的任务不需要 1000 类分类完全可以把classifier砍掉只保留features加一个全局平均池化参数量直接降到 1500 万左右推理速度提升数倍精度在迁移任务上往往只掉一两个点。class VGGFeatureOnly(nn.Module): def __init__(self, num_classes10): super().__init__() vgg models.vgg16(pretrainedTrue) self.features vgg.features self.gap nn.AdaptiveAvgPool2d(1) # 全局平均池化输出 512 维 self.fc nn.Linear(512, num_classes) def forward(self, x): x self.features(x) x self.gap(x) x torch.flatten(x, 1) return self.fc(x) model VGGFeatureOnly(num_classes10).to(device)AdaptiveAvgPool2d(1)把512×7×7压成512×1×1再接一个Linear(512,10)整个分类头只有 5000 多参数。这种改法在工业缺陷检测、医学图像分类里很常见因为那些任务类别少、数据量有限全连接层的大容量反而是负担。另一个技巧是截断features。VGG16 的features有 31 层含池化前几个卷积块提取的是边缘、纹理等低级特征后几个块才是语义特征。如果你的任务和 ImageNet 差异大可以只取前 4 个卷积块输出256×28×28再接自己的检测头或分割头。# 截断到第 4 个池化层之前 features_truncated nn.Sequential(*list(vgg.features.children())[:23]) with torch.no_grad(): out features_truncated(torch.randn(1, 3, 224, 224)) print(out.shape) # torch.Size([1, 256, 28, 28])具体截到第几层要看任务。我的习惯是先跑一遍完整 VGG 看验证集精度再逐块截断对比找到精度掉得最少的那个截断点。这个过程不需要改网络定义只改list(...)[:N]里的 N 就行。验证改动是否有效别只看 loss 曲线。我一般会固定一个验证集每次改动后跑一遍记录准确率和单张推理耗时两个指标一起看。如果准确率掉了但速度没提上来说明截断位置不对如果速度提上来了但准确率崩了说明截多了。这个对比表比任何理论分析都直接。从那以后我每次拿到一个以经典网络命名的源码包都先做三件事确认输入标准化参数、确认分类头维度、跑一遍特征提取看输出 shape。这三步走完后面是训练还是迁移心里就有底了。希望帮到你。本文还有配套的精品资源点击获取
