资讯详情

VGG19图像分类实验:迁移学习、冻结策略与数据管线避坑

📅 2026/11/2 15:49:52 | 华诺云谱 👁 阅读
VGG19图像分类实验:迁移学习、冻结策略与数据管线避坑
简介这份压缩包面向智能计算系统课程实验、深度学习入门与部署优化学习者围绕VGG19图像分类任务提供可直接查阅的模型与评估材料。包内共4个文件以3个Python脚本和1个pb模型文件为主压缩包约95.86MB脚本对应CPU与MLU两种运行环境的评估入口pb文件为INT8量化后的已训练模型便于理解模型轻量化与硬件加速的差异。已有2154人学习下载说明其在课程实验与CNN部署练习中具有一定参考价值。读者可据此了解VGG19的卷积堆叠、池化与全连接分类结构参考图像预处理、前向推理和准确率评估的实现方式并对比CPU与MLU上的推理表现为后续模型量化、嵌入式部署和性能优化积累可复用的实验素材。1. 从一次 8 类花卉误判说起VGG19 图像分类实验到底在练什么第一次跑智能计算系统实验 4-1我把 8 类花卉图片塞进 VGG19训练 loss 降到 0.2验证集却只有 0.13。问题不在模型而在数据管线和冻结策略。这个实验是人工智能课程里典型的迁移学习落地拿预训练卷积层当特征提取器只重训最后几层把 224×224 的 RGB 图片分到自己的类别里。适合刚接触深度学习、想跑通「数据进、模型出、指标可复现」完整链路的人也适合需要把现成分类模型接到业务图片上的开发者。2. 把 VGG19 拆开看为什么 19 层卷积堆叠还能跑在实验机上2.1 从 3×3 卷积堆叠到 4096 维全连接VGG19 的结构账本VGG19 的「19」指 16 个卷积层加 3 个全连接层池化层和 ReLU 不记权重。输入固定成 224×224 时特征图一路从 224 降到 112、56、28、14最后落到 7×7。五个卷积段通道数依次是 64、128、256、512、512最后一段输出 512×7×7展平后正好是 25088 维接上 25088→4096→4096→1000 的三层全连接。真正占参数大头的是全连接两个 4096×4096 矩阵加一个 4096×1000 输出层合计一亿两千多万参数卷积部分只有两千万左右。理解这个账本很重要。很多人改输入尺寸到 256×256发现 classifier 第一层维度对不上就是因为 512×8×8 变成了 32768。实验里最稳妥的做法是坚持 224×224如果必须换分辨率就在 features 后面加nn.AdaptiveAvgPool2d((7,7))或者整体换掉 classifier。VGG19 用连续两个 3×3 卷积替代一个 5×5用三个 3×3 替代一个 7×7感受野一样参数更少非线性更多这是它结构上最值得讲清楚的一点。注意VGG19 默认版本没有 BatchNorm显存占用主要来自全连接和中间激活不是卷积核本身。2.2 迁移学习选型冻结到哪一层为什么要换掉 classifier 的最后一层小数据集上从零训练 VGG19基本等于让一亿多参数在几百张图上自由发挥训练集能背下来验证集一塌糊涂。常见做法是加载预训练权重先冻结features只训练 classifier。这样卷基层输出的是通用纹理和边缘特征分类头重新学「这些特征组合起来对应哪一类」。实验里如果要求保持 VGG19 原结构就只把classifier[6]从 1000 类改成自己的类别数如果追求更稳的验证集效果可以把整个 classifier 换成 25088→256→类别数的瓶颈结构参数量立刻降下来。冻结到哪一层没有唯一答案。数据量和预训练域接近时冻结全部卷积、只训分类头最稳数据量稍多、且和预训练图片差异较大时解冻 block4 和 block5让高层语义特征跟着任务微调。我一般先跑一轮全冻结看验证集能不能到 0.7 以上如果卡住再解冻 block4 往后学习率降到 1e-5 级别。不要一上来就全网络解冻血泪经验是 loss 会先给你一个很漂亮的下降然后验证集直接翻车。2.3 用 torchvision 搭一个最小可训练骨架import torch from torch import nn from torchvision.models import vgg19, VGG19_Weights def build_vgg19(num_classes: int, freeze_features: bool True, bottleneck: bool False): # 预训练权重只下载一次离线环境可改成 weightsNone 再加载本地 state_dict model vgg19(weightsVGG19_Weights.IMAGENET1K_V1) if freeze_features: for p in model.features.parameters(): p.requires_grad False in_features model.classifier[0].in_features # 25088 if bottleneck: # 实验部署常用把 4096 宽的全连接换成 256 宽减少过拟合 model.classifier nn.Sequential( nn.Linear(in_features, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) else: # 保持 VGG19 原分类头结构只替换最后一层 model.classifier[6] nn.Linear(4096, num_classes) return model model build_vgg19(num_classes8, freeze_featuresTrue, bottleneckFalse) print(model.classifier)这段代码解决三个问题权重从哪来、哪些层可训练、输出维度怎么对齐。freeze_featuresTrue时卷积层不更新梯度优化器里也不要放它们bottleneckTrue适合自己加实验不适合直接加载别人训好的 4096 分类头。in_features从原模型读避免手写 25088 写错。要是用vgg19_bn冻结 features 后还要把 features 设成eval()否则 BN 的 running mean/var 会在训练时继续更新验证集表现会变得很玄学。3. 数据管线落地从原始图片目录到 DataLoader 的四个关键参数3.1 目录划分与 ImageFolder 的隐含要求ImageFolder要求根目录下每个子目录是一个类别子目录名就是类名图片直接放在类目录里。它不会帮你做训练验证划分也不会检查两个划分的类别顺序是否一致。实验里最容易忽略的是训练集和验证集如果分别从两个目录读取而且验证集少了一个类文件夹class_to_idx就会错位标签 3 可能对应训练时的标签 4准确率直接掉到随机水平。我一般先固定随机种子把原始数据复制成train和val两套目录而不是用random_split切索引。复制虽然多占一点磁盘但后面排查问题简单目录结构就是最终输入不会出现同一个文件同时出现在两边。下面脚本保留原始数据按 8:2 复制并保证每个类至少有 1 张验证图。import random import shutil from pathlib import Path src Path(data/raw) dst Path(data/split) train_ratio 0.8 seed 42 random.seed(seed) for class_dir in sorted([p for p in src.iterdir() if p.is_dir()]): imgs sorted([ p for p in class_dir.glob(*) if p.suffix.lower() in {.jpg, .jpeg, .png, .bmp} ]) if len(imgs) 2: print(f跳过 {class_dir.name}图片不足 2 张) continue random.shuffle(imgs) n_train int(len(imgs) * train_ratio) n_train max(1, min(len(imgs) - 1, n_train)) # 保证验证集至少 1 张 for split, subset in [(train, imgs[:n_train]), (val, imgs[n_train:])]: out_dir dst / split / class_dir.name out_dir.mkdir(parentsTrue, exist_okTrue) for p in subset: shutil.copy2(p, out_dir / p.name) print(划分完成)脚本里copy2保留时间戳方便后面追溯n_train的上下界是为了防止某个类只有两张图时训练集或验证集为空。类别名建议直接用中文或英文短名不要带空格和特殊符号ImageFolder会按目录名排序排序结果就是标签索引。划分完之后打印一次train_ds.class_to_idx和val_ds.class_to_idx两个字典必须完全一致。3.2 训练/验证 transform 不能共用同一套训练要增强验证不能增强这是常识但实验里经常有人把RandomResizedCrop同时套到验证集上导致同一个模型每次评估结果都在跳。训练集用随机裁剪、水平翻转、轻度颜色抖动验证集只做Resize(256)加CenterCrop(224)。归一化参数必须和预训练权重对齐常见值是 mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。如果你改成 0.5/0.5模型不会报错但精度会明显掉而且很难从 loss 曲线上看出来。from torchvision import transforms normalize transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225], ) train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), normalize, ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), normalize, ])RandomResizedCrop(224, scale(0.7, 1.0))表示随机裁出原图 70% 到 100% 的区域再缩到 224适合主体占比较大的分类图。如果做工业缺陷或医学图像水平翻转可能改变语义就不要加。ColorJitter对花卉、商品图通常安全对颜色就是判别依据的任务要慎用。验证集 transform 一旦确定推理阶段必须复用同一个对象不要重新写一遍。3.3 batch_size、num_workers 与 pin_memory 的实测取值VGG19 的显存占用不算温柔224×224、batch_size32 在 8GB 卡上很容易 OOM。实验机如果是 4GB 或 6GB先从 batch_size8 开始冻结 features 后通常能跑如果还炸降到 4或者用梯度累积模拟大 batch。num_workers在 Linux 上设 4 一般够用Windows 上设 0 更稳因为多进程启动方式不同设大了容易在第一个 epoch 卡住。pin_memoryTrue只在 CUDA 训练时有意义能让 CPU 到 GPU 的拷贝更快persistent_workersTrue要求num_workers0否则会报参数错误。参数建议值说明batch_size8 或 164GB 显存从 8 起OOM 再降num_workersLinux 4Windows 0卡在第一个 batch 就调小pin_memoryCUDA 训练 TrueCPU 训练保持 Falsedrop_last训练 True避免最后一个单张 batch 影响统计shuffle训练 True验证 False验证集顺序固定便于复现from torchvision import datasets from torch.utils.data import DataLoader train_ds datasets.ImageFolder(data/split/train, transformtrain_tf) val_ds datasets.ImageFolder(data/split/val, transformval_tf) assert train_ds.class_to_idx val_ds.class_to_idx, ( train_ds.class_to_idx, val_ds.class_to_idx ) train_loader DataLoader( train_ds, batch_size8, shuffleTrue, num_workers4, pin_memoryTrue, persistent_workersTrue, drop_lastTrue, ) val_loader DataLoader( val_ds, batch_size8, shuffleFalse, num_workers4, pin_memoryTrue, )assert这一行是后悔药级别的检查类别顺序不一致时立刻停不要等训练完才发现。drop_lastTrue对 VGG19 这种没有 BN 的模型不是必须但能避免最后一个小 batch 让 loss 曲线抖动。验证集shuffleFalse保证每次评估顺序一致混淆矩阵的类别顺序也稳定。3.4 用一个小脚本先跑通 20 张图正式训练前我习惯先取一个 batch 做 sanity check图片形状是不是(B,3,224,224)标签有没有越界归一化后的像素范围是否合理。这个步骤花不了一分钟但能挡掉后面几个小时的瞎调参。尤其当你换了自己的数据集图片有灰度图、RGBA PNG、CMYK JPEG 时ToTensor出来的通道数可能不是 3卷积层会直接报错。import torch images, labels next(iter(train_loader)) print(images:, images.shape, images.dtype) print(labels:, labels.shape, labels.min().item(), labels.max().item()) print(pixel range:, images.min().item(), images.max().item()) assert images.shape[1] 3, 通道数不是 3检查灰度图或 RGBA 图 assert labels.max().item() len(train_ds.classes), 标签越界 assert images.min().item() -3 and images.max().item() 3, 归一化可能异常像素范围在 -2.2 到 2.7 左右是正常的因为做了均值方差归一化。如果最小值是 0、最大值是 1说明Normalize没生效如果出现 4 通道说明图片没转 RGB。这个检查脚本建议固定成check_batch.py每次换数据集都跑一遍。4. 训练循环与调参冻结、解冻、学习率三者怎么配合4.1 两阶段训练先训 classifier再解冻 block4/block5第一阶段只训练 classifierfeatures 全部冻结。这样即使学习率设到 1e-3也不会把预训练卷积核带偏。等验证集准确率不再明显上升再进入第二阶段解冻 block4 和 block5。VGG19 的features是一个Sequential索引 11 开始是 block4 的第一个卷积层索引 16 开始是 block5。不同版本的 torchvision 索引可能因为 BN 层变化所以解冻前先打印model.features确认。def unfreeze_from(model, start_idx: int 11): # start_idx11 对应 VGG19 的 conv4_1解冻 block4 和 block5 for i, module in enumerate(model.features): for p in module.parameters(): p.requires_grad i start_idx unfreeze_from(model, start_idx11) trainable [n for n, p in model.named_parameters() if p.requires_grad] print(可训练参数数量:, len(trainable))这个函数只改requires_grad不会自动改优化器。解冻后必须重新构建优化器或者把新参数加进 param_groups否则这些层虽然可训练但优化器根本不知道它们存在。另一个常见做法是解冻后先只跑 2 到 3 个 epoch观察验证集如果掉点就回退到全冻结的检查点。4.2 损失函数、优化器与学习率调度分类任务用CrossEntropyLoss就够了它内部包含 softmax不要在前向里再加一层 softmax。优化器我一般用 SGDmomentum0.9weight_decay5e-4预训练模型对 SGD 比较友好Adam 也能用但学习率要更低常见从 1e-4 起。关键是差分学习率classifier 用 1e-3解冻后的 features 用 1e-4 或 1e-5。同一个学习率同时喂给预训练卷积和随机初始化的分类头卷积层会被大梯度冲坏。from torch import nn criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD([ {params: model.features.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-3}, ], momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max20, eta_min1e-5 )param_groups里 features 和 classifier 分开设 lr这是解冻阶段最值得抄的配置。T_max20表示余弦退火周期 20 个 epocheta_min是学习率下限。如果实验只跑 10 个 epoch把T_max改成 10不要让它退到一半就停。调度器每个 epoch 调用一次scheduler.step()不要每个 batch 调。4.3 用混淆矩阵和单类召回看穿「准确率虚高」8 类数据如果某一类占了 70%模型全猜这一类也能有 0.7 准确率但业务上完全不可用。训练日志里我会同时看 loss、整体准确率和单类召回。验证集跑完一次把预测结果和真实标签收集起来用混淆矩阵看哪两类在互相混淆。花卉数据里常见的是同色系、同花型类别混淆工业数据里常见的是光照条件相近的缺陷混淆。import numpy as np from sklearn.metrics import confusion_matrix, classification_report torch.no_grad() def collect_preds(model, loader, device): model.eval() y_true, y_pred [], [] for images, labels in loader: images images.to(device) logits model(images) preds logits.argmax(dim1).cpu().numpy() y_pred.extend(preds.tolist()) y_true.extend(labels.numpy().tolist()) return np.array(y_true), np.array(y_pred) y_true, y_pred collect_preds(model, val_loader, cuda) print(confusion_matrix(y_true, y_pred)) print(classification_report( y_true, y_pred, target_nameslist(train_ds.classes), digits4, zero_division0, ))classification_report里的 recall 就是单类召回zero_division0防止某些类没有预测样本时报警告。混淆矩阵对角线是正确数非对角线是错分去向。如果发现某一类召回特别低先别急着加网络层回去看这一类图片数量、标注质量和 transform 是否把关键区域裁掉了。4.4 保存与恢复检查点别只存 state_dict只存model.state_dict()能恢复权重但恢复不了类别顺序、优化器状态和训练轮次。下次推理时忘了class_to_idx预测结果就会整体错位。我一般把模型、优化器、epoch、best_acc、class_to_idx、输入尺寸一起打包。加载时注意 PyTorch 新版本torch.load默认weights_onlyTrue包含字典和字符串的检查点可能报错本地可信文件可以显式设weights_onlyFalse。ckpt { model: model.state_dict(), optimizer: optimizer.state_dict(), epoch: epoch, best_acc: best_acc, class_to_idx: train_ds.class_to_idx, input_size: 224, } torch.save(ckpt, best_vgg19.pth) # 恢复 ckpt torch.load(best_vgg19.pth, map_locationcpu, weights_onlyFalse) model.load_state_dict(ckpt[model]) idx_to_class {v: k for k, v in ckpt[class_to_idx].items()}检查点里的class_to_idx是推理阶段的标签字典idx_to_class是它的反向映射。恢复后先打印idx_to_class确认类别顺序和验证集一致。如果换了机器、换了 GPUmap_locationcpu先加载再搬到目标设备比直接cuda稳。5. 避坑与排查VGG19 实验里最容易翻车的五个地方5.1 显存炸了但 batch_size 只设了 8现象模型刚跑第一个 batch 就报CUDA out of memorynvidia-smi 显示显存被占满。原因不一定是 batch_size 大VGG19 的两个 4096 全连接层本身参数就重反向传播还要存中间激活。如果 features 没冻结卷积层的梯度也会占显存。解决先确认requires_grad只开在 classifier 上优化器只传可训练参数batch_size 降到 4用torch.cuda.amp.autocast()混合精度还不行就用特征预取把卷积输出缓存成张量后面只训一个小分类头。5.2 验证集准确率一直 0.125类别顺序和标签对不上现象8 类任务验证集准确率稳定在 0.125loss 也不下降但训练集 loss 正常。原因通常是训练集和验证集分别用ImageFolder读取验证集少了某个类文件夹class_to_idx索引错位。解决从同一个根目录划分数据保证两边类别目录完全一致读取后立刻断言train_ds.class_to_idx val_ds.class_to_idx推理时用检查点里保存的class_to_idx不要重新按验证集目录生成。5.3 训练 loss 正常但验证集崩盘归一化和 transform 用错现象训练集准确率能到 0.9验证集只有 0.3且每次评估波动很大。原因常见是验证集用了RandomResizedCrop或RandomHorizontalFlip也可能是归一化均值方差写成了 0.5/0.5。解决验证集只保留Resize(256)、CenterCrop(224)、ToTensor、Normalize训练和验证共用同一个normalize对象推理阶段复用val_tf不要手写一遍预处理。5.4 解冻后 loss 突然飙升学习率没降现象第一阶段全冻结时验证集 0.75解冻 block4/5 后第一个 epoch loss 从 0.3 跳到 2.0验证集掉到 0.4。原因是解冻层和分类头用了同一个 1e-3 学习率预训练卷积核被大梯度冲坏。解决给 features 单独设 1e-5 到 1e-4 的学习率只解冻 block4 和 block5解冻后先跑 1 个 epoch 观察掉点就回退检查点必要时冻结 BN 统计量VGG19 默认没有 BN但换成vgg19_bn时要把冻结部分设成eval()。5.5 推理结果每次都不一样忘了 model.eval()现象同一张图片连续推理三次类别或置信度每次不同。原因模型还在训练模式Dropout 仍然随机丢弃神经元同时没加torch.no_grad()虽然不影响结果但会白白占显存。解决推理前固定写model.eval()并用with torch.no_grad():包住前向预处理必须用验证集 transform图片读取后convert(RGB)避免 RGBA 或灰度图导致通道数不对。6. 把模型推到可用单张推理、批量预测与特征预取提速6.1 单张图片推理的最小闭环训练完最容易被忽略的是推理脚本。很多人直接在 notebook 里把图片塞进模型忘了unsqueeze(0)或者用了训练集 transform结果和验证集对不上。我一般把推理固定成五步加载检查点、恢复类别映射、eval()、验证集 transform、no_grad前向。图片读取时强制convert(RGB)能挡掉大部分通道问题。from PIL import Image import torch ckpt torch.load(best_vgg19.pth, map_locationcpu, weights_onlyFalse) idx_to_class {v: k for k, v in ckpt[class_to_idx].items()} model.load_state_dict(ckpt[model]) model.eval() img Image.open(test.jpg).convert(RGB) x val_tf(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): prob torch.softmax(model(x), dim1) conf, pred prob.max(dim1) print(idx_to_class[pred.item()], round(conf.item(), 4))6.2 批量预测与结果落盘批量预测不要一张一张走模型用DataLoader把batch_size设成 16 或 32shuffleFalse并且把文件路径一起带出来。可以自定义一个只返回(image, path)的 Dataset或者先跑ImageFolder再按索引映射回文件列表。结果写成 CSV列固定为path, pred_class, confidence后面排查误判时直接筛低置信度样本。6.3 一个提速技巧把卷积特征预取一次如果实验机显卡弱又只需要训分类头可以把所有图片过一遍冻结的model.features把 25088 维特征存成torch.save的张量然后只训练一个小 MLP。这样每个 epoch 不再跑卷积训练时间能从几十分钟压到几分钟。代价是失去数据增强的随机性所以预取前要确定训练集 transform 不再改。做法是model.features.eval()no_grad前向torch.flatten(feat, 1)保存特征和标签后面用TensorDataset接DataLoader。从那以后我每次跑 VGG19 实验都先跑一遍 20 张图的 sanity check再决定是全量微调还是特征预取希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑