资讯动态

VGG19图像分类实战:从网络结构到迁移学习全流程

发布时间:2026/10/15 19:12:46 来源:尧图企业网站定制
简介本资源面向人工智能与深度学习方向的实验学习者聚焦基于VGG19卷积神经网络完成图像分类任务适合已具备一定CNN基础、希望理解模型部署与硬件加速优化的中高级学习者。压缩包共4个文件包含3个Python脚本与1个pb模型文件整体约95.86MB其中脚本分别承担CPU与MLU平台上的模型评估工作pb文件为INT8量化后的VGG19权重便于在嵌入式或移动端高效推理。实验围绕VGG19的19层小卷积核堆叠结构展开涉及图像预处理、前向传播、全连接与Softmax分类并对比CPU与MLU两种硬件下的性能差异帮助读者掌握模型量化与异构部署的关键思路。目前已有2140人学习下载可作为理解VGG19运作机制与推理优化的实践参考。1. 从一次翻车现场说起VGG19 图像分类实验到底在练什么带过几届实验课每次到「智能计算系统实验4-1」这个节点总有人卡在同一个地方模型跑起来了loss 也降了但最后预测结果全是同一类。问下来八成是预处理没对齐——训练时用了归一化推理时忘了或者标签映射顺序和文件夹顺序对不上。这类问题不涉及什么高深算法纯粹是工程细节没抠住但恰恰是新手最容易翻车的地方。这个实验的核心任务很明确基于 VGG19 做图像分类。VGG19 是牛津那套经典卷积网络里比较深的一个版本19 层带权重的结构16 个卷积层加 3 个全连接层卷积核统一 3×3池化统一 2×2。它的设计哲学就一句话——用堆叠的小卷积核换感受野同时把参数量压住。放到今天看VGG19 在 ImageNet 上的表现早就不算顶尖但它结构规整、特征提取层次清晰特别适合拿来理解「卷积网络到底在学什么」。这个实验适合两类人一是刚接触深度学习、需要把训练推理全流程走通的新手二是想拿 VGG19 当 backbone 做迁移学习、但还没摸清微调套路的从业者。2. VGG19 的结构账本为什么是 19 层而不是随便堆2.1 卷积块的堆叠逻辑与感受野计算VGG19 的结构可以拆成 5 个卷积块加 3 个全连接层。前两个块各含 2 个卷积层后三个块各含 4 个卷积层加起来 16 个卷积层再加 3 个全连接层总共 19 层带权重。每个卷积块后面接一个最大池化尺寸 2×2步长 2每过一次池化特征图边长减半。为什么用两个 3×3 卷积堆叠代替一个 5×5感受野是一样的——两个 3×3 卷积的感受野等于一个 5×5但参数量从 25 降到 18而且中间多了一次非线性激活。三个 3×3 堆叠感受野等于 7×7参数量 27 对 49差距更明显。这就是 VGG 系列的核心设计取舍用深度换参数效率。实际写代码时卷积块的通道数变化是 64→128→256→512→512。每次池化后通道翻倍特征图边长减半这样每层的计算量大致持平。全连接层部分第一个 FC 把 7×7×512 展平成 25088 维映射到 4096再映射到 4096最后输出到类别数。这三个 FC 层吃掉了整个网络绝大部分参数约 1.2 亿个这也是 VGG19 权重文件动辄 500MB 以上的原因。2.2 用 PyTorch 搭一个可训练的 VGG19 骨架直接调 torchvision 的 vgg19 当然最省事但实验要求理解结构所以下面手写一个简化版保留 VGG19 的卷积块配置全连接层按实际类别数调整。import torch import torch.nn as nn # VGG19 卷积层配置M 代表最大池化数字代表输出通道数 cfgs { 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) class VGG19Custom(nn.Module): def __init__(self, num_classes10, batch_normFalse): super().__init__() self.features make_layers(cfgs[VGG19], batch_norm) 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) x self.classifier(x) return x # 实例化假设做 10 分类 model VGG19Custom(num_classes10) print(sum(p.numel() for p in model.parameters()) / 1e6, M params)这段代码里几个关键点。make_layers按配置列表逐层构建遇到M就插池化否则插卷积加 ReLU。padding1保证 3×3 卷积后特征图尺寸不变尺寸缩减只靠池化。AdaptiveAvgPool2d((7,7))是个保险措施——如果输入不是 224×224全连接层的 25088 维就对不上自适应池化能强制拉回 7×7。Dropout(0.5)放在两个 4096 维全连接之间是 VGG 原论文就有的设计用来压过拟合。参数量打印出来大概 1.43 亿和原版 VGG19 的 1.44 亿基本一致差在分类头从 1000 改成了 10。提示如果显存吃紧把第一个全连接层从 4096 降到 1024参数量能砍掉一大半精度损失在多数迁移任务里可以接受。3. 数据管线与训练循环把预处理对齐这件事做扎实3.1 图像预处理与数据增强的参数选择VGG19 原论文用的输入是 224×224预处理做了减均值。实际实验里常见做法是直接用 ImageNet 的均值和标准差做归一化因为后面大概率要加载预训练权重。from torchvision import transforms, datasets from torch.utils.data import DataLoader # ImageNet 统计量加载预训练权重时必须对齐 mean [0.485, 0.456, 0.406] std [0.229, 0.224, 0.225] train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 随机裁剪缩放 transforms.RandomHorizontalFlip(p0.5), # 水平翻转 transforms.ColorJitter(brightness0.2, contrast0.2), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(mean, std), ]) val_transform transforms.Compose([ transforms.Resize(256), # 先缩到 256 transforms.CenterCrop(224), # 再中心裁剪到 224 transforms.ToTensor(), transforms.Normalize(mean, std), ]) train_dataset datasets.ImageFolder(data/train, transformtrain_transform) val_dataset datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)RandomResizedCrop的scale(0.7, 1.0)控制裁剪面积比例太小会让物体被切掉太大会退化成普通缩放。ColorJitter的强度别开太大0.2 左右够了再大容易让颜色成为噪声。验证集的处理必须和推理时一致先 Resize 到 256 再 CenterCrop 到 224这是标准做法。ImageFolder要求目录结构是data/train/类别名/图片类别名按字母序映射成标签这个顺序在推理时必须一致否则预测结果全错。3.2 训练循环与学习率调度VGG19 参数量大从头训练容易过拟合实验里一般用两种策略一是加载预训练权重后只微调全连接层二是全部解冻但用很小的学习率。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model VGG19Custom(num_classes10).to(device) # 加载预训练权重如果有 # pretrained_dict torch.load(vgg19_bn.pth) # model.load_state_dict(pretrained_dict, strictFalse) criterion nn.CrossEntropyLoss() # 微调时学习率要小从头训练可以大一些 optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() scheduler.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, fVal Acc: {100*correct/total:.2f}%)weight_decay5e-4是 VGG 原论文的设置配合 SGD 动量 0.9。CosineAnnealingLR让学习率按余弦曲线从初始值降到 0比阶梯下降更平滑。如果加载了预训练权重学习率建议降到 1e-4 甚至 1e-5否则预训练学到的特征会被快速破坏。验证阶段一定要model.eval()加torch.no_grad()前者关掉 Dropout 和 BatchNorm 的训练行为后者省显存。注意如果验证准确率一直卡在随机水平先检查标签映射。打印几个 batch 的 labels 看看是不是全 0 或者全同一类。4. 避坑与排查那些让准确率归零的细节4.1 现象训练 loss 正常下降验证准确率不动原因通常是 BatchNorm 的 running stats 没更新或者验证时忘了切 eval 模式。VGG19 如果加了 BatchNorm训练时用当前 batch 的统计量验证时用滑动平均。如果验证时还在 train 模式Dropout 会随机丢神经元BatchNorm 会用验证 batch 自己的统计量结果波动极大。解决验证循环开头写model.eval()结束后如果要继续训练再写model.train()。这个顺序别搞反。4.2 现象推理时所有图片预测成同一类最常见的原因是预处理不一致。训练用了 Normalize推理时只做了 ToTensor像素值范围从 [-2, 2] 变成 [0, 1]模型看到的输入分布完全变了。另一个原因是标签映射错位——训练时ImageFolder按文件夹字母序编号推理时自己手动写了个类别列表顺序对不上。解决把验证集的 transform 单独存成一个变量推理时直接复用。标签映射用train_dataset.classes打印出来推理时按这个顺序取类别名。4.3 现象显存溢出batch size 降到 1 还是 OOMVGG19 全连接层参数量太大第一个 FC 层 25088×4096 的权重矩阵就占约 400MB 显存float32。如果输入分辨率超过 224展平后的维度更大显存直接爆炸。解决三个方向。一是把输入尺寸严格控制在 224×224别用 256 或 320。二是把第一个 FC 从 4096 降到 1024 或 512。三是用梯度累积模拟大 batch每次前向小 batch累积几次再更新。4.4 现象加载预训练权重报 key 不匹配load_state_dict报一堆 missing keys 和 unexpected keys。missing 的是分类头因为预训练是 1000 类自己的任务类别数不同unexpected 的是预训练里多出来的层。解决用strictFalse加载分类头的权重本来就不需要。如果卷积层也 missing检查模型定义时的层名是否和预训练权重一致。常见做法是只加载features部分分类头随机初始化。pretrained torch.load(vgg19.pth) model_dict model.state_dict() # 只保留形状匹配的键 pretrained {k: v for k, v in pretrained.items() if k in model_dict and v.shape model_dict[k].shape} model_dict.update(pretrained) model.load_state_dict(model_dict)这段过滤逻辑比直接strictFalse更稳能避免形状不匹配的键被强行加载导致后续报错。4.5 现象训练速度极慢GPU 利用率上不去num_workers设成 0 是最常见的原因数据加载在主进程里串行执行GPU 一直在等数据。另一个原因是图片尺寸太大解码耗时。解决num_workers设成 CPU 核数的 2 到 4 倍但别超过 16否则进程调度开销反而拖慢。图片提前缩放到 256 左右存成小图训练时再裁剪能显著减少 IO 时间。5. 迁移学习与特征可视化把 VGG19 用出更多花样5.1 冻结卷积层做特征提取器如果自己的数据集很小比如几百张图从头训练 VGG19 必然过拟合。这时候把卷积层全部冻结只训练分类头效果往往比全量微调好。# 冻结 features 部分的所有参数 for param in model.features.parameters(): param.requires_grad False # 只优化分类头 optimizer optim.Adam(model.classifier.parameters(), lr1e-3)冻结后反向传播只更新分类头显存占用和训练时间都大幅下降。等分类头收敛了如果想进一步提升可以解冻最后两个卷积块做小学习率微调学习率设成 1e-5 级别。5.2 用 hook 提取中间层特征做可视化VGG19 的卷积层输出可以直接拿来做特征图可视化看看每一层到底关注什么。# 注册 hook 抓取指定层的输出 features {} def get_features(name): def hook(model, input, output): features[name] output.detach() return hook # 抓取第一个卷积块的输出 model.features[0].register_forward_hook(get_features(conv1)) # 前向一张图 model.eval() with torch.no_grad(): _ model(sample_img.unsqueeze(0).to(device)) # features[conv1] 形状是 [1, 64, 224, 224] # 取前 16 个通道画出来 import matplotlib.pyplot as plt fig, axes plt.subplots(4, 4, figsize(8, 8)) for i, ax in enumerate(axes.flat): ax.imshow(features[conv1][0, i].cpu().numpy(), cmapviridis) ax.axis(off) plt.show()浅层卷积核学到的是边缘、纹理、颜色块深层学到的是物体部件。这个可视化在实验报告里很加分也能帮你判断模型是不是真的学到了东西。如果浅层特征图看起来像随机噪声说明权重没加载对或者输入归一化有问题。5.3 一个我踩过的坑验证集准确率比训练集高有次实验验证准确率 92%训练准确率 88%当时觉得是模型泛化好。后来发现是验证集太小只有 50 张图而且和训练集有重叠。VGG19 参数量大如果验证集和训练集分布太近eval 模式下 Dropout 关闭相当于用了全部神经元表现自然好。从那以后我每次划分数据集都强制走一遍检查训练集和验证集的图片路径不能有交集验证集至少占总样本的 15%每类至少 10 张。如果数据实在少用 K 折交叉验证别偷懒只切一次。希望这些经验能帮你少走点弯路把 VGG19 这个实验真正跑通、跑明白。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑