☰
VGG19图像分类实验指南:迁移学习与PyTorch工程实践
2026/10/10 20:21:09 网站建设 项目流程

简介:基于VGG19实现图像分类的智能计算系统实验4-1资料,面向深度学习初学者与边缘部署开发者,覆盖经典CNN模型构建、INT8量化部署及CPU/MLU双平台评估。压缩包内共4个文件,含3个Python评估脚本与1个pb预训练模型文件,整体约95.86MB;脚本分别针对CPU和MLU硬件环境进行推理验证,pb文件为量化后的可直接加载的模型权重,免去自行训练的时间成本。已有2139人学习下载,资料围绕模型加载、图像预处理、前向传播、类别预测与准确率计算展开,步骤明确,便于按实验手册逐步复现。VGG19通过多层3×3卷积堆叠提取图像特征,理解其结构有助于掌握经典CNN设计思想;而通过CPU与MLU结果对比,可直观感受硬件加速对推理速度和能效的影响,同时体会INT8量化在降低内存占用、提升部署效率方面的实际价值,适合课程实验、模型部署入门及AI应用性能调优参考。

1. 智能计算系统实验4-1到底在做什么:为什么拿 VGG19 当图像分类的入门基准

如果你在课程实验平台上看到别人交的准确率曲线漂亮得像论文插图,自己跑出来的 loss 却像心电图,那你大概率正卡在智能计算系统实验4-1上。这个实验任务很明确:基于 VGG19 实现图像分类。它不要求你从零手写卷积网络,而是把预训练好的 VGG19 当作骨干网络,迁移到自己的数据集上完成训练、验证和推理。它能帮你打通一条完整的图像分类算法落地流水线:数据怎么摆、模型怎么改、参数怎么调、结果怎么验证。适合刚学完深度学习理论、第一次在真实框架里跑通全流程的学生,也适合手里只有几千张图片、想快速做分类基线的工程师。

2. 实验动手前的三张底牌:环境准备、数据集组织与 VGG19 结构认知

2.1 环境与硬件:CPU 也能跑但 GPU 让你不熬夜

我一般建议先看一眼手头机器再定实验方案。VGG19 有 1.43 亿参数,一次前向推理的浮点运算量高达 196 亿次,这在今天看来不算夸张,但如果你只有 CPU,在 ImageNet 规模的数据上训几十个 epoch 会等到怀疑人生。实验4-1的典型数据集是 CIFAR-10、CIFAR-100 或自采的小样本图像集,单张图片 224×224,一个 batch 32 张图在 CPU 上一个 epoch 可能要几分钟到十几分钟,能跑,但不适合反复试参数。

环境建议用 PyTorch 2.x + torchvision,因为 torchvision 直接内置了 VGG19 的预训练权重和标准预处理流程。装环境时踩过的坑多半来自版本错位:torchvision 的版本必须和 PyTorch 主版本匹配,否则torchvision.models.vgg19(weights=...)可能直接报错或者下载权重时静默失败。我自己的做法是先用python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__)"确认版本,再决定要不要升级。

提示:实验机器如果没有独立显卡,优先把 batch_size 调小到 16 或 8,并把图片缓存成 tensor 格式而不是每次读图都做 JPEG 解码,省下的时间能让你多跑两组对照实验。

2.2 数据集怎么摆:ImageFolder 目录约定与 train/val/test 拆分

实验4-1里最容易翻车的不是网络结构,而是数据路径。PyTorch 的torchvision.datasets.ImageFolder约定了一个非常朴素的目录结构:根目录下每个子文件夹代表一个类别,文件夹名字就是类别名。这个约定值得从一开始就遵守,因为后面无论换 ResNet 还是换 ViT,数据加载代码一行都不用改。

data/ train/ cat/ 001.jpg 002.jpg ... dog/ 001.jpg 002.jpg ... val/ cat/ 001.jpg 002.jpg ... dog/ 001.jpg 002.jpg ... test/ cat/ 001.jpg 002.jpg ... dog/ 001.jpg 002.jpg ...

训练集、验证集、测试集必须分开目录,而且验证集不能参与任何反向传播。很多同学图省事把所有图片都丢进 train,训练时用train_test_split随机切,这样做的隐患是:如果原始数据里同一个物体的多张相似图片被同时切进训练集和验证集,验证准确率会虚高,换到真实场景立刻现原形。实验报告里的数字看起来很漂亮,但模型的泛化能力其实没被真正评估过。

数据拆分比例我一般用 7:2:1 或 8:1:1,类别数多、每类样本少时要把测试集比例压低,否则每个类只剩三五张图片,评估结果的方差大到没法比。拆分时最好按类别做分层抽样,保证每个类别在三个集合里的占比一致,不要用np.random.shuffle直接打乱所有文件再按比例切,那会切出个别类别在测试集里消失的尴尬情况。

2.3 VGG19 结构速览:16个卷积层+3个全连接层到底在学什么

VGG19 这个名字里的 19 指的是 16 个卷积层加 3 个全连接层,整个网络可以粗略分成三段:前面是一串 3×3 卷积 + ReLU + 最大池化堆出来的特征提取器,中间是自适应平均池化把空间维度压平,最后是三个全连接层组成的分类头。它的核心设计思想是:用多个小卷积核堆叠来替代一个大卷积核,感受野相同但参数量更小,非线性更强。

在实验4-1里,你关心的不是从头训练这 16 层卷积,而是怎么利用它在 ImageNet 上学到的通用特征。VGG19 前面的卷积层学到的是边缘、纹理、颜色块这些低级特征,越往后越抽象,到最后一个池化层之前的特征图已经能描述“猫耳朵的轮廓”“车轮的辐条”这类中高层语义。做图像分类时,如果你的新数据集不算特别古怪,这个特征提取器基本可以直接拿来用,需要重新学习的只有最后那几个全连接层。

理解了这一点,后面的迁移学习操作就顺理成章:冻结前面的卷积层,只训练分类头,是大数据量不够时的稳办法;全部解冻微调,是数据量够大、任务和 ImageNet 差距明显时的更优选择。VGG19 最大的代价是参数多、推理慢,但它结构规整、每一步特征图尺寸变化都清晰可控,用来做实验、画图、debug 都比 ResNet 那种带捷径的网络直观得多,这也是它在智能计算系统实验里被选中的根本原因。

3. 基于 VGG19 搭建图像分类最小代码骨架:从数据加载到训练循环

3.1 用 torchvision 加载预训练 VGG19 并替换分类头

进入代码环节,先别急着写训练循环。第一步是把预训练 VGG19 加载进来,并把它的分类头换成适配你自己类别数的结构。CIFAR-10 是 10 类,森林图像分类可能四五类,你的实验按题目给的类别数改num_classes就行。下面是最小可用的加载代码:

import torch import torch.nn as nn from torchvision import models, transforms num_classes = 10 # 按实验数据集修改 model = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1) model.classifier[6] = nn.Linear(in_features=4096, out_features=num_classes) print(model)

这段代码有两个地方值得说明。weights=models.VGG19_Weights.IMAGENET1K_V1是 torchvision 新版推荐的写法,它会自动下载在 ImageNet-1K 上训练好的权重,比旧版pretrained=True更显式,也避免了一些版本弃用告警。model.classifier[6]是 VGG19 分类头里最后一个全连接层,原来输出 1000 类,现在换成你的类别数。VGG19 的分类头是一串Linear(512*7*7, 4096) -> ReLU -> Dropout -> Linear(4096, 4096) -> ReLU -> Dropout -> Linear(4096, 1000)的结构,classifier[0]是第一个全连接层,classifier[6]是最后一个。注意不要只改第一个全连接层的输入维度,新数据集的图片尺寸如果不是 224×224,展平后的特征维度会变化,这时要改的是classifier[0]的in_features,但通常实验都会统一缩放到 224×224,所以只改最后一层就够了。

注意:替换分类头之后,原分类头的权重全部丢失,但特征层的权重保留。PyTorch 只会对随机初始化的那一个 Linear 层计算梯度,如果后续你不冻结任何层,训练时会连特征层一起更新,这是正常的。

数据预处理是紧接着就要做的事。加载预训练权重意味着输入数据必须符合它在 ImageNet 上训练时的分布:resize 到 256、中心裁剪到 224、按 ImageNet 的均值和标准差做标准化。这三个步骤少了任何一个,预训练特征都会大打折扣,因为卷积核学到的颜色分布和尺度和你喂进去的对不上。

transform_train = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_val = 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]) ])

这里训练集加了RandomResizedCrop和RandomHorizontalFlip做数据增强,验证集只做缩放、裁剪和标准化。RandomResizedCrop会随机裁剪图片的不同区域并缩放到 224×224,让模型看到同一个物体的不同构图,这是图像分类算法里性价比最高的增强手段之一。标准化的均值和标准差是 ImageNet 的统计值,除了做迁移学习,自建数据集训练时也可以继续沿用,因为 VGG19 的特征层已经习惯了这种数值分布。

3.2 训练循环与验证循环的标准写法

训练循环本身不复杂,但细节决定成败。下面这段代码是我在实验里反复使用的骨架,换数据集时只需要改数据加载部分:

import torch.optim as optim from torch.utils.data import DataLoader train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) for epoch in range(epochs): 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() * images.size(0) model.eval() correct = 0 total = 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() val_acc = correct / total print(f"epoch {epoch+1}/{epochs}, train_loss={running_loss/len(train_dataset):.4f}, val_acc={val_acc:.4f}")

这段代码有几个关键点。model.train()和model.eval()不是摆设,VGG19 分类头里有 Dropout,BatchNorm 层虽然 VGG19 没有,但如果你后续换成 ResNet,eval模式会决定 BatchNorm 用滑动均值还是当前 batch 的统计量。忘记切eval是验证准确率忽高忽低的常见原因。with torch.no_grad()告诉 PyTorch 验证阶段不构建计算图,省内存也提速。

predictions == labels计算的是 Top-1 准确率,也就是模型预测概率最高的类是否和真实标签一致。图像分类任务里还有 Top-5 指标,即真实标签是否落在概率最高的前 5 个类里,在 ImageNet 这种 1000 类任务上 Top-5 更有区分度,实验4-1如果类别数少,看 Top-1 就足够。如果你想在报告里多展示一个指标,可以在验证循环里用torch.topk(outputs, k=5)统计。

3.3 把训练日志和 checkpoint 落盘:后悔药怎么吃

训练跑到一半断掉、显存被别的进程占满、loss 发散,这些事在实验周里几乎必然发生。训练循环里最该补的代码就是 checkpoint 和日志。我一般会在每个 epoch 结束后保存一份模型权重,再额外保留验证准确率最高的那一份,这样就算后面把模型训崩了也能回到最佳状态重新调参。

import os os.makedirs("checkpoints", exist_ok=True) best_acc = 0.0 for epoch in range(epochs): # 训练和验证逻辑同上一节 val_acc = correct / total torch.save({ "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "val_acc": val_acc, }, f"checkpoints/vgg19_epoch{epoch:02d}.pth") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "checkpoints/vgg19_best.pth")

保存完整checkpoint和只保存model.state_dict()的区别在于:前者给你留了后悔药,模型训练到第 30 个 epoch 时如果发现第 20 个 epoch 之后的权重被污染了,你可以加载第 20 个 epoch 的 checkpoint,连同当时的优化器状态一起恢复到那个时间点,学习率、动量、BatchNorm 滑动均值全都回来了。后者只够做推理,没法接着续训。实验报告里如果要求展示训练曲线,你还需要每轮记录train_loss和val_acc,用 matplotlib 画两条曲线,而不是只贴最后一行的打印输出。

4. 迁移学习微调是实验4-1的真正考点:冻结、学习率与分类头调整

4.1 冻结特征层 vs 全量微调:什么时候选哪个

实验4-1的隐藏考点不是让你从零训练 VGG19,而是看你有没有真正理解迁移学习。从零训练一个 VGG19 在 ImageNet 规模的数据上需要几周,而我们手里的数据集通常只有几千张图,硬训的结果是严重过拟合、准确率只有随机水平多一点。正确打开方式有两种:一种是把 VGG19 当固定特征提取器,冻结所有卷积层,只训练新加的分类头;另一种是全量微调,所有层都参与反向传播。

冻结特征层的做法在 PyTorch 里非常直观,把特征层参数的requires_grad全部置为False:

for param in model.features.parameters(): param.requires_grad = False # 只把分类头参数传给优化器 optimizer = optim.SGD( filter(lambda p: p.requires_grad, model.parameters()), lr=0.01, momentum=0.9, weight_decay=5e-4 )

这样优化器实际管理的参数只有model.classifier里的三层,参数量从 1.4 亿降到大概 1.23 亿里的最后几百万,训练速度快得多,也不容易过拟合。什么时候选这种方案?答案是:数据集小(每类少于几百张)、新任务和 ImageNet 像(都是自然图像)、实验时间紧。什么时候该全量微调?数据集达到每类几千张、或者你的图像是 X 光片、卫星图这种和自然图像分布差异很大的类型,这时候只训练分类头会卡在一个次优解上,必须让特征层也适应新数据。

一种折中方案是折中地解冻:先冻结特征层训练几个 epoch,让分类头收敛,再解冻后面几层卷积做低学习率微调。这个方案在实验4-1里很推荐,因为课程实验通常只需一晚上跑完,但能在报告里写清楚“两阶段训练策略”,比单纯跑个结果更能体现你对图像分类算法的理解。PyTorch 里解冻部分层可以这样定位:VGG19 的model.features是一个nn.Sequential,你可以对索引大于某个值的层重新打开梯度,比如只解冻最后两个卷积块:

for name, param in model.named_parameters(): if "features" in name: param.requires_grad = False # 解冻最后两个卷积块(索引 24 之后是 block5 的卷积层) for i in range(24, len(model.features)): for param in model.features[i].parameters(): param.requires_grad = True

VGG19 的features里卷积层和池化层交替排列,第 24 层之后对应最深层的特征图,这些特征图已经包含比较具体的语义信息,对新数据集的适应价值最大,浅层学到的边缘纹理对几乎所有视觉任务都通用,冻结它们几乎无损。

4.2 四个必调参数:batch_size、lr、weight_decay 与 epoch

实验4-1能直接抄作业的参数组合,我会用一张表列出来,但你必须理解每个参数的改动理由,否则换个数据集直接翻车:

参数典型值调整方向与理由
batch_size16 ~ 64GPU 显存不够就减半;batch 太小梯度噪声大,太大收敛变慢,实验数据量小的时候 32 是安全起点
learning_rate全量微调 1e-4,只训分类头 1e-2预训练模型已经在一个好的损失曲面位置,lr 太大会把权重直接踢出去,loss 爆炸或准确率暴跌
weight_decay5e-4抑制过拟合,数据量小、训练轮数长时可以加大到 1e-3
epoch20 ~ 50看验证准确率是否平台期,连续 5 个 epoch 不涨就早停

SGD 和 Adam 的取舍我多说一句。实验4-1用 SGD + momentum 是经典做法,因为 VGG19 在 ImageNet 上就是用 SGD 训练的,沿用同样的优化器风格迁移更顺滑。Adam 的优点是收敛快、对 lr 不敏感,但如果 lr 设成默认的 1e-3 在全量微调时很容易让预训练权重震荡。我的习惯是:只训分类头时用 SGD,lr=0.01;全量微调时换 SGD,lr=1e-4;如果你非要省事用 AdamW,lr 从 1e-5 开始试。

一个反直觉的点是:预训练模型迁移时,分类头的随机初始化权重和特征层预训练权重的量级不匹配。分类头新初始化的层梯度更新快,特征层更新慢,同一个 lr 往往顾此失彼。更讲究的做法是给新分类头单独设更大的学习率,给特征层设更小的学习率:

optimizer = optim.SGD([ {"params": model.features.parameters(), "lr": 1e-5}, {"params": model.classifier.parameters(), "lr": 1e-3}, ], momentum=0.9, weight_decay=5e-4)

4.3 用学习率预热+余弦退火把验证准确率再抬一点

如果你的实验要求在固定 epoch 数量下尽量刷高准确率,学习率调度是最后一块能稳定吃分的蛋糕。我一般在实验里用LambdaLR组合预热和余弦退火:前 5 个 epoch 学习率从很小线性升到目标值,让分类头先稳定下来;之后按余弦曲线逐步降到接近零,让权重在损失面底部附近做精细搜索。

from torch.optim.lr_scheduler import LambdaLR import math total_epochs = 40 warmup_epochs = 5 def lr_lambda(epoch): if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return 0.5 * (1 + math.cos(math.pi * progress)) scheduler = LambdaLR(optimizer, lr_lambda=lr_lambda) for epoch in range(total_epochs): # 训练验证逻辑 scheduler.step()

这个调度在 CIFAR-10 上通常能比固定学习率多出 1 到 3 个百分点的 Top-1 准确率。原理是:预训练权重已经在 ImageNet 上收敛过,新分类头需要先快速学习,所以预热期很有必要;之后如果学习率一直不变,会在损失面底部来回震荡,余弦退火则允许权重逐步落到底部,减少最终模型的方差。调度器的scheduler.step()放在每个 epoch 结束时,如果和优化器step()的位置搞混,你会发现学习率根本没按预期变化,这也是 PyTorch 新手最常见的调度器翻车现场。

提示:跑实验时把每个 epoch 的当前学习率打出来,确认它确实在按曲线变化。打印学习率可以用optimizer.param_groups[0]['lr'],这一步能帮你排除一半的“模型不收敛”假故障。

5. 实验4-1 避坑清单:五条真实翻车记录与排查路径

5.1 图像尺寸导致的维度不匹配

现象:训练脚本跑起来,第一个 batch 前向传播就报错,错误信息里出现size mismatch,比如Expected input batch_size (32) to match target或者mat1 and mat2 shapes cannot be multiplied。

原因:VGG19 的全连接层写死了输入维度。最后一个卷积层输出的特征图是 7×7×512,展平后 25088 维,经过classifier[0]变成 4096。如果你的输入图像不是 224×224,或者预处理里少了CenterCrop,实际展平后的维度就不是 25088,全连接层的权重矩阵当然对不上。

解决:先检查数据加载部分的transforms,确认Resize(256)和CenterCrop(224)都存在;再用for images, _ in train_loader: print(images.shape); break打印实际 tensor 形状。如果图像尺寸五花八门,比CenterCrop更稳的做法是Resize((224, 224))直接拉伸,虽然会变形,但至少维度一定对。

5.2 预训练归一化参数照搬出错

现象:训练能跑,loss 在下降,准确率却一直在 10% 左右徘徊(10 类数据集的随机水平),换网络结构也没用。

原因:加载了别人写的模型脚本,但transforms.Normalize用了自己算的均值和方差,或者干脆忘了归一化,直接把 0~255 的像素值喂给了按 0~1 分布训练的预训练权重。VGG19 的卷积核统计特征是在特定分布下学的,输入分布变了,预测概率自然接近均匀分布。

解决:迁移学习就用 ImageNet 的mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225],不要自作主张换。如果是自建数据集想从头训练 VGG19,再单独统计训练集的均值和标准差,而且要在训练集上统计验证集也用它,不能在验证集上重新算。顺手检查ToTensor()是否在Normalize之前,顺序错了数值范围会完全乱掉。

5.3 迁移学习时把分类头也冻住了

现象:loss 完全不动,准确率恒定在随机水平,但代码看起来没问题。

原因:有些人为了省内存把model.features冻结后,顺手对模型所有参数做了for param in model.parameters(): param.requires_grad = False,新加的model.classifier[6]也被冻住,整个网络没有任何可学习参数,优化器收到的梯度全是 None,权重一分不差。

解决:在冻结操作后打印一层新分类头的requires_grad:

print(model.classifier[6].weight.requires_grad) # 必须是 True

如果你用了filter(lambda p: p.requires_grad, model.parameters())传给优化器,可以再打印优化器的参数量:sum(p.numel() for p in optimizer.param_groups[0]['params']),如果结果比总参数量小很多,说明确实在正确冻结,如果等于总参数就说明没冻住。这个检查 10 秒钟能做完,但能救回一整晚。

5.4 loss 变成 NaN 的常见链条

现象:训练前几个 epoch 正常,突然 loss 变成nan,准确率跟着变成 0,重启训练后随机重现。

原因:最常见的链条是学习率过大导致梯度爆炸,权重更新到数值溢出;另一个原因是数据里有异常值,比如损坏的图片被解码成全零矩阵或者极端像素值。VGG19 里没有 BatchNorm,梯度经过十几层卷积链式相乘,对学习率比 ResNet 更敏感,全量微调时用 1e-3 的初始学习率几乎必炸。

解决:先把lr降到 1e-4 重跑;如果还炸,检查数据加载器里是否混入了None或缺失后缀的图片文件,把DataLoader的num_workers暂时调成 0 跑一个 epoch 确认不是多进程数据读取的偶发问题;最后在loss.backward()之前加一行assert torch.isfinite(loss), "loss is NaN",触发时打印当前 batch 的图片索引,直接定位异常样本。

5.5 验证准确率不涨:先怀疑数据增强和标签对齐

现象:训练 loss 一路下降,验证 loss 也跟着降,但验证准确率在某个值上横盘好几个 epoch,或者训练准确率 98%、验证准确率 60%,差距越拉越大。

原因:两个方向。横盘不动先检查验证集的shuffle=False是否被误设成True,以及验证集是否和训练集有大量重叠,重叠会导致验证准确率虚高但不涨;训练验证差距大,则说明模型过拟合了,你需要增强正则化而不是继续加 epoch。

解决:过拟合的先手是数据增强。只做翻转不够的话,加transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2)和RandomRotation(10),让模型不能靠记忆颜色分布混过去。另一个容易忽视的点是标签对齐:如果你的数据集目录是0_cat、1_dog这种名字,ImageFolder会按字典序分配 label,如果训练脚本里手写了一个class_to_idx和它不一致,数据增强再强也白搭。打印一次train_dataset.class_to_idx和验证集的对应关系,对比一下就能排除这个坑。

6. 从实验4-1走向真实图像分类任务:混淆矩阵、CAM 与小样本思路

6.1 用混淆矩阵和误分类图验证模型到底错在哪

验证准确率只是一个数字,实验报告里真正给我信心的图是混淆矩阵。用sklearn.metrics.confusion_matrix很容易画出来,但重点是怎么看:对角线上的值代表正确分类,非对角线上的亮点代表模型在混淆哪两个类。比如森林图像分类里,松树和杉树的混淆大概率来自背景纹理相似,而不是模型没学会。找出错误样本,把那批图拼成网格重点看,比调三天参数更有效。

6.2 把 VGG19 的感知能力迁移到小样本(1-shot/5-shot)场景

实验做完后,下一步很多人会碰小样本图像分类:每类只有一两张标注图。VGG19 的预训练特征在这里还能再战。常见做法是提取model.features的输出做特征向量,然后计算查询图与支持集特征的余弦相似度,这就是最简单的 1-shot 基线。它比从头训一个分类器靠谱得多,因为预训练特征已经把图像映射到了语义相近的区域,同类图片在特征空间里天然靠近。如果类别数多,可以在这组特征上训练一个线性分类器,相当于把 VGG19 当成特征工程工具,这也是小样本场景下性价比最高的路线,比直接端到端微调稳。

6.3 CAM 可视化:让 VGG19 的黑匣子开口说话

实验4-1交差后,想更深入理解模型到底看了图像的哪个区域,可以做类激活映射(CAM)。VGG19 结构简单,最后一个卷积层的特征图直接经过池化和全连接层,非常适合做梯度加权类激活映射。把最后一个卷积层输出的特征图按分类得分梯度加权求和,得到一张热力图,叠回原图上就能看到模型是盯着鸟的喙还是背景。这个可视化放进实验报告里,比任何文字说明都更有说服力。我之前做森林图像分类时,CAM 热力图直接暴露出模型在靠路面识别类别,因为训练集里某个类的图片总是带公路背景,这是准确率虚高的直接证据,也是下一轮数据清洗的方向。从那以后我养成了习惯:每个图像分类实验除了跑指标,必须看一眼 CAM 图和误分类图,否则不敢说模型真的学会了。希望这个习惯也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询