简介:面向人工智能与智能计算系统方向的学习者,这份实验包围绕VGG19图像分类任务,提供了完整的可运行代码与预训练模型,适合深度学习初学者以及需要了解模型部署优化的开发者实训使用。包内共有4个文件,包括一个采用8位整数量化格式的VGG19模型文件,以及分别用于中央处理器与专用机器学习加速单元上评估的Python脚本,可直接加载模型并完成前向推理,便于对比硬件差异。资源包整体约95.86MB,结构精简,便于快速搭建实验环境。目前已有2139人学习使用。借助这些内容,用户既能掌握VGG19卷积层堆叠与全连接分类的运作逻辑,也能对比不同硬件环境下的推理速度与能效表现,同时熟悉模型压缩与量化部署方法,为后续在嵌入式或移动端应用打下扎实基础,是一份理论与实践结合的优质实验资料。
1. VGG19 图像分类实验:先搞清楚要交什么,再决定怎么跑
第一次拿到“基于 VGG19 实现图像分类”这个实验题的人,多数以为难点在搭模型。实际跑起来你会发现,模型代码十几行就能写完,真正让你熬夜的是数据加载、预处理、显存不够、训练到一半 loss 变成 NaN,以及交实验前发现验证集准确率停在及格线附近。这篇笔记按我自己跑这个实验的路径来写:先讲清 VGG19 为什么被选作智能计算系统课程的常客,再给一套能在本地或服务器上直接复现的最小代码,最后把训练参数和五个高频翻车点挨个说清。适合正准备交实验报告、想换掉官方模板的人,也适合拿这个模型练完手、下一步要切到小样本图像分类任务的人。
2. 为什么是 VGG19:结构拆解与选型理由,读懂它再动手
2.1 VGG19 的网络结构与“19”这个数字从哪来
VGG19 的网络结构非常规整,就是把“卷积 + ReLU + 最大池化”这一组操作重复堆叠。整张计算图可以分成五段卷积特征提取层,外加最后三段全连接分类层。前两段各包含 2 个卷积层,中间三段各包含 4 个卷积层,每段结束接一个 2×2 的最大池化把特征图尺寸减半;最后接 3 个全连接层,前两个是 4096 维,最后一个是 1000 维(对应 ImageNet 的 1000 个类别)。卷积层总数是 2+2+4+4+4=16,全连接层是 3,所以“19”指的是这 16 个卷积层加 3 个全连接层。也有的资料把 Softmax 也算进去,叫它 20 层,但实验里默认按前者理解。
每个卷积层都使用 3×3 卷积核,stride 为 1,padding 为 1,通道数从 64 开始逐段翻倍:64→128→256→512→512。VGG19 的参数量在 1.4 亿左右,其中全连接层占了绝大部分。这一点在实验报告里经常被单独拎出来问:为什么 VGG 层数不深,参数却比 ResNet50 多不少?答案就在最后三个全连接层,它们把 7×7×512 的特征图拉平后接了 4096 维全连接,参数量非常夸张。实际训练时,这些全连接层也是最先需要被替换的部分,因为你的分类任务类别数往往不是 1000。
这个结构的最大优点是“可预期”。每一层的输入输出尺寸你都能手工算出来,推理时间基本跟输入图片分辨率线性相关,不像某些带注意力机制的模型那样难预估。对智能计算系统实验来说,这正适合做性能分析:你可以在每个阶段记录特征图尺寸和耗时,写进实验报告,老师也容易核对。
2.2 为什么课程实验和企业里的图像分类都爱用它
现在最新的图像分类模型很多,ViT、Swin Transformer、ConvNeXt 效果都更好,但 VGG19 仍然是实验课和入门项目的首选。原因有三条。第一,结构透明,feature map 的尺寸变化一眼能看穿,做中间层可视化、剪枝、量化等课程要求都很方便;第二,预训练权重随处可得,torchvision 一行代码就能加载 ImageNet 权重,对小样本任务特别友好——哪怕是 1-shot 或 5-shot 这种极端的图像分类设置,用 VGG19 做特征提取器也比随机初始化的深层模型靠谱得多;第三,它不太挑数据,森林图像分类、工业瑕疵分类这类背景差异较大的数据,用它默认的预处理流程就能跑出可用的基线。
ResNet50 和 VGG19 对比时有一个常见现象:相同训练轮数下,VGG19 收敛后的准确率往往略低一点,但它的训练过程更“老实”,loss 曲线更平滑,不容易出现训练集指标突然掉零的怪事。这对实验报告很有利,因为你可以把每个 epoch 的指标变化写得很规整。而 ViT 这类模型对数据量和超参数更敏感,课程实验的数据量通常只有几千张,直接用 ViT 预训练模型做微调,往往还不如 VGG19 稳。所以我的建议是:先用 VGG19 把全流程跑通,拿到一个可靠基线,再决定要不要换更强的模型。
2.3 从 torchvision 加载预训练模型:命令与三个注意点
torchvision 从 0.13 开始改了权重参数写法,旧的pretrained=True被标记为弃用,新写法是显式指定权重枚举。我一般这样加载:
import torchvision.models as models # 新写法,明确下载 ImageNet-1K 的 VGG19 预训练权重 vgg19 = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1) # 对应旧写法:models.vgg19(pretrained=True),新版仍兼容但会告警这里第一个注意点:权重文件名带IMAGENET1K_V1,表示这是 ImageNet-1K 上训练的第一版权重。如果你的实验环境无法联网,需要提前下载好权重文件放到~/.cache/torch/hub/checkpoints/目录下,否则会卡在下载阶段。
第二个注意点:预训练模型要求输入按 ImageNet 的统计值做归一化,mean 为[0.485, 0.456, 0.406],std 为[0.229, 0.224, 0.225]。很多人在这里翻车:自己随便写了归一化参数,模型也能跑,但准确率低好几个点。后面第 5 章我会专门讲这个坑。
第三个注意点:默认分类头输出是 1000 类,做你自己的图像分类任务时必须替换最后一层。替换代码很简单,但要放在加载权重之后做,顺序错了会报参数不匹配。
import torch.nn as nn num_classes = 10 # 换成你自己的类别数 vgg19.classifier[6] = nn.Linear(in_features=4096, out_features=num_classes)这样改完之后,整个模型仍然以预训练参数初始化,只有最后一层是随机初始化。实验里管这个叫迁移学习,后面第 4 章会讲怎么连前面特征层也一起微调。
3. 跑通最小实验:数据准备、训练循环与三个关键指标
3.1 按文件夹组织图像数据,用 ImageFolder 一行加载
课程实验给的数据集不一定规范,有时候是一堆图片放在一个文件夹里,文件名带类别前缀;有时候是按train/val/test分好的目录。不管哪种,我建议先统一整理成 ImageFolder 能识别的目录格式,因为 VGG19 训练代码里用torchvision.datasets.ImageFolder最省事。
# 目标目录结构:一个类一个子文件夹,文件夹名就是类别名 datasets/ train/ forest/ # 森林图像 urban/ # 城市图像 water/ # 水域图像 val/ forest/ urban/ water/ test/ forest/ urban/ water/如果你手里是一堆无结构图片,可以写个 Python 脚本按文件名或 CSV 标签分组,也可以直接用shutil.move移动文件。这里有一个实操经验:不要用中文目录名,VGG19 训练脚本在部分 Windows 环境下会踩编码问题;类别名尽量是简单的英文字母。
整理好后,用两段代码加载训练集和验证集。这里我直接写完整的数据加载和预处理部分,并解释参数:
from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练阶段:随机裁剪 + 随机翻转 + 归一化 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证阶段:不用随机增强,直接缩放到 224 并归一化 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_loader = DataLoader( datasets.ImageFolder('datasets/train', transform=train_transform), batch_size=32, shuffle=True, num_workers=4 ) val_loader = DataLoader( datasets.ImageFolder('datasets/val', transform=val_transform), batch_size=32, shuffle=False, num_workers=4 )这段代码里的RandomResizedCrop(224)是 VGG19 预训练模型的标准输入尺寸,它会把图片随机裁剪成 224×224,模拟不同尺度的目标;RandomHorizontalFlip()做水平翻转增强,对森林、地貌这类数据效果很好。验证集为什么用Resize(256) + CenterCrop(224)而不是直接 Resize 成 224?因为预训练模型训练时就是这么处理的,输入分布更接近训练时看到的情况,验证分数会更可信。num_workers在 Windows 上建议设成 0,否则可能报多进程错误;Linux 服务器可以设 4 到 8。
3.2 训练主循环:VGG19 的完整训练脚本骨架
模型搭建好、数据加载好之后,训练循环本身不长。下面是一个可以直接跑的骨架,包含损失函数、优化器、单轮训练和验证。为了便于在课程实验里交差,我把每个 epoch 的指标打印都写进去了。
import torch import torch.nn as nn import torchvision.models as models device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') vgg19 = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1) vgg19.classifier[6] = nn.Linear(4096, num_classes) # 替换分类头 vgg19 = vgg19.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(vgg19.parameters(), lr=1e-3, momentum=0.9, weight_decay=5e-4) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() # 进入训练模式,让 BatchNorm 和 Dropout 生效 total_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() # 验证模式:关掉 Dropout,BatchNorm 用全局统计量 total_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total for epoch in range(1, 31): train_loss, train_acc = train_one_epoch(vgg19, train_loader, optimizer, criterion, device) val_loss, val_acc = validate(vgg19, val_loader, criterion, device) print(f'Epoch {epoch:02d} train_loss={train_loss:.4f} train_acc={train_acc:.4f} val_acc={val_acc:.4f}')逻辑说明:训练时model.train()会让 BatchNorm 层根据当前 batch 更新均值方差,Dropout 层随机丢弃神经元;验证时必须切到model.eval(),否则结果会不稳定,这是第一个容易忽略的细节。优化器用的是带动量的 SGD,momentum=0.9是最常用的值,weight_decay=5e-4可以抑制过拟合,课程实验里也默认用它。
这里有一个参数选择问题:初始lr=1e-3是微调的常见起点。如果你是从头训练 VGG19,这个学习率往往太小,收敛非常慢;但智能计算系统实验几乎没有从头训的场景,预训练权重微调用 1e-3 比较合适。想临时看一眼效果,可以先跑 3 个 epoch,如果训练 loss 从 1.x 往下走,再放大训练轮数。
3.3 三个必须关心的指标:tr_loss、val_acc、过拟合判断
跑完几个 epoch 后不要急着看最终 acc,先把三个数字拆开看。第一个是训练集平均损失train_loss,它反映模型在训练数据上的学习程度;第二个是训练集准确率train_acc,如果它接近 100% 而 val_acc 明显低,说明模型开始背答案了;第三个是验证集准确率val_acc,这是实验报告最终要写的核心指标。
判断是否过拟合我常用一个简单标准:当val_acc连续 5 个 epoch 没有上升,而训练集 acc 还在涨,就说明泛化到顶了。这时候该停下来做三件事:加数据增强、调大 weight_decay、减小学习率。反过来,如果 val_acc 和 train_acc 都一直很低,那就是欠拟合,优先检查数据预处理和优化器参数,别急着调网络结构。
打印指标时建议每个 epoch 都顺手保存一次模型,方便回滚。代码里加一行即可:
torch.save(model.state_dict(), f'checkpoints/vgg19_epoch{epoch:02d}_acc{val_acc:.3f}.pth')这里为什么用state_dict()而不是直接torch.save(model, ...)?前者只存参数,文件小且不依赖模型定义;后者把整个对象序列化,换环境后经常因为版本不同加载失败。这就是你实验报告里能写的“后悔药”:每个 epoch 存一份,最后选验证集分数最高的那份做测试,不要用最后一轮的模型。
4. 训练参数怎么设:学习率、Batch Size、冻结训练与迁移学习
4.1 学习率怎么设:VGG19 的敏感度区间
VGG19 对学习率的敏感度比 ResNet 高。同样是微调,ResNet50 在lr=1e-3时可能稳稳当当,VGG19 却会在前几个 epoch 出现 loss 剧烈震荡。原因在于 VGG19 的 BatchNorm 层少(整体只有 5 个 maxpool 之前的卷积块里没有太多 BN),对梯度尺度的控制更依赖学习率本身。
我的经验是把学习率分成三档:全量微调(所有参数都训练)时用1e-4到3e-4;只微调分类头时用1e-3;数据量很小(比如只有几百张图)时直接1e-5。课程实验里如果数据集有几千张,先按3e-4跑 10 个 epoch,看 val_acc 增长曲线,如果前 3 轮没动静,立刻减半再重开。
学习率调度我一般不加太复杂的东西,课程实验用一个固定学习率加一个手动衰减就够了。但如果你想写进报告,cosine衰减是最好解释的:
from torch.optim.lr_scheduler import CosineAnnealingLR scheduler = CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6) # 每个 epoch 结束执行一次 # scheduler.step()T_max=30表示在 30 个 epoch 内把学习率从初始值按余弦曲线降到eta_min=1e-6。这种调度的好处是前期大步走、后期小步磨,不用你盯着手动改。注意顺序:scheduler.step()要在每个 epoch 验证结束后调用,不要放在 batch 循环里,不然每个 batch 都会衰减一次,实际学习率会掉得飞快。
4.2 Batch Size 和显存的关系:小显存怎么跑 VGG19
VGG19 全量训练在单张 8GB 显存的卡上,batch_size=32加 224×224 输入会直接 OOM。课程实验最常见的是 6GB 到 8GB 的 GPU,这就要学会算显存。输入分辨率固定时,显存消耗主要由 batch_size 和模型参数量决定;VGG19 的中间特征图比 ResNet 大,因为卷积层没有降维的 shortcut,每层都保留完整通道数。
如果显存不足,第一选择不是换模型,而是把batch_size降到 8 或 16。注意:batch_size太小会让 BatchNorm 的统计量抖动,VGG19 里卷积层的 BN 不多,影响还勉强能接受。更稳妥的做法是开梯度累积,用多个小 batch 模拟大 batch 的效果:
accumulation_steps = 4 # 相当于 batch_size * 4 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) / accumulation_steps # 先除再累积 loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()这里有一个很多人会忽略的细节:loss要除以accumulation_steps,否则梯度累积后等效学习率偏大,模型容易震荡。公式是:等效 batch size = 实际 batch_size × accumulation_steps,学习率可以保持不变。我个人建议:等效 batch size 控制在 64 以内,超过 128 时 VGG19 的收敛速度反而变慢。
如果连 16×224 都跑不动,那就得考虑冻结部分层了。删掉model.features的某些 block 不可取,因为预训练权重的分布会乱掉;更稳的是下面这种冻结训练方案。
4.3 冻结特征层做迁移学习:改分类头的最小代码
小样本图像分类任务里,1-shot 或 5-shot 意味着训练样本极少,此时微调全部参数极易过拟合。正确做法是冻结 VGG19 的特征提取层,只训练自己替换过的分类头。代码上只是多一个for循环,但效果差异非常大。
# 冻结所有特征层参数,只保留分类头可训练 for param in vgg19.features.parameters(): param.requires_grad = False # 也可以把前面 2 个 stage 冻结,后面 3 个 stage 继续微调 # for param in vgg19.features[:10].parameters(): # param.requires_grad = False # 检查哪些层参与训练 trainable_params = sum(p.numel() for p in vgg19.parameters() if p.requires_grad) print(f'Trainable parameters: {trainable_params / 1e6:.2f}M')冻结后,整张图的 forward 流程不变,但反向传播只更新分类头那几层。这时优化器可以直接把学习率调到1e-3,因为可训练参数少,任务简单,大步长收敛更快。如果你想更进一步,可以分两阶段训练:先冻结特征层训分类头 10 个 epoch,再把分类头训练好的参数作为起点,解冻最后一段卷积层,用1e-5的学习率整体微调。这个做法写进实验报告比一次全量微调更有内容,也经常能换来两个点的提升。
这里顺带回答一个很多人问的问题:用 ViT 这类模型做评估时分类头用调整吗?答案是要。任何预训练模型都自带一套分类头,输出维度和它的预训练数据集类别数绑定。你用自定义数据集做图像分类,必须把分类头替换成自己的类别数,否则会直接报维度不匹配。VGG19 要替换classifier[6],ViT 要替换heads层,原理一样,别只改输入不换输出。
5. 避坑手册:VGG19 训练里最常见的 5 个翻车点
5.1 Loss 不降或直接变成 NaN:先查归一化和输入范围
现象:训练循环能跑通,但 loss 始终在 5、6 附近波动,或者某个 epoch 突然变成 NaN。
原因:绝大多数情况是输入图片没有做归一化。ToTensor()之后像素范围会变成 0 到 1,但 VGG19 的预训练权重是在经过 ImageNet 均值方差归一化后的数据上训练的。如果你只转 Tensor 不 Normalize,输入分布和预训练权重完全错位,特征提取器输出的数值范围偏大,交叉熵损失不稳定甚至溢出色 NaN。另一种常见原因是 RGB 通道顺序反了,比如 OpenCV 读图默认是 BGR,直接转成 Tensor 后颜色错位,loss 会缓慢下降但准确率永远上不去。
解决:在 transform 里强制加Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]);如果是用cv2.imread读图,先执行cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。校验方法很简单:把一张图打印出来,看通道均值是否接近 0.5 以下,而不是忽高忽低。
5.2 训练时间长得离谱:先用小模型小数据跑通,再上全量
现象:第一个 epoch 跑了十几分钟,按这个速度 30 个 epoch 要跑大半天,感觉没法交实验。
原因:课程实验最容易犯的错就是拿到全量数据直接开训。VGG19 参数量大,全量训练一轮就要处理上万张图。如果 CPU 环境连 GPU 都没有,光一个 forward 就要好几秒。
解决:先把datasets.ImageFolder指向一个临时mini目录,每个类只放 20 张图,batch_size 固定,跑 2 个 epoch 验证代码正确性。确认能出结果后再把数据切回全量。如果确实没有 GPU,那就把 batch_size 降到 8,epoch 改到 10,并用预训练权重,不要尝试从头训练。实验报告里可以写清楚“在 CPU 环境下只微调分类头”,老师反而会觉得你理解了迁移学习的价值。
5.3 验证集准确率比训练集还高:Transform 顺序和 Dropout 的坑
现象:训练 5 轮后 train_acc 是 92%,val_acc 却有 95%,怎么看都不合理。
原因:训练时模型处于train()模式,RandomResizedCrop和RandomHorizontalFlip会让部分图片被裁剪或翻转,训练难度更大;而验证集的预处理是固定的CenterCrop,没有随机扰动。另一个因素是 Dropout,VGG19 的 classifier 里有 Dropout 层,训练时会随机丢弃部分神经元,推理时不丢弃,验证自然更顺。这不是 bug,只要训练和验证的 transform 长得不一样就是正常的。
解决:别慌,这是预期现象。你应该改关注 val_acc 是否持续高于 train_acc,如果是,说明你验证集的变换和训练集差异太大,或者数据划分有问题(比如同一个类的大量相似图被分到了两边)。排查方法:打印几个批次的训练和验证图片,看有没有重复图。
5.4 预处理参数不一致:自己瞎写 mean 和 std,导致模型效果大跌
现象:同样代码在 A 机器上跑 acc 90%,在 B 机器上只有 75%,代码没改,权重也一致。
原因:B 机器上可能重写了 transform,用了transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))这类默认参数。VGG19 预训练模型对输入分布敏感,用错 mean/std 相当于把输入整体平移缩放,特征分布全部错位。
解决:把预处理的 mean/std 写死成 ImageNet 的标准值,不要图省事复制网上杂乱的代码。这里给一个自查代码:
from torchvision import transforms from PIL import Image import torch img = Image.open('test.jpg').convert('RGB') t = 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]) ]) x = t(img).unsqueeze(0) print(x.mean().item(), x.std().item())输出均值应该在 -1.5 到 1.5 之间,标准差在 0.5 到 1.2 之间。如果均值整张图都是 0.x 以上,说明归一化参数不对。
5.5 报错尺寸不匹配:分类头维度问题全解
现象:加载预训练模型后直接训练,报错size mismatch for classifier.6.weight;或者干脆在 forward 时报矩阵维度对不上。
原因:你用了官方权重,但权重里最后一层是按 1000 类保存的,你替换成num_classes后,如果你没有替换就去 load 原始 state_dict,PyTorch 会拒绝加载。很多人在model.load_state_dict(torch.load('vgg19.pth'))时中招。
解决:按顺序来,先替换分类头,再加载权重。如果权重是从别人那里来的、不是 torchvision 的标准权重,加载时用strict=False让 PyTorch 跳过不匹配的那一层:
state_dict = torch.load('your_weight.pth') vgg19 = models.vgg19(weights=None) # 不要实例化时自动下载权重 vgg19.classifier[6] = nn.Linear(4096, num_classes) vgg19.load_state_dict(state_dict, strict=False)strict=False会把缺失或者 shape 不一致的参数跳过,只加载能匹配的层。这个参数是双刃剑:它会静默跳过所有不匹配的 key,你很难发现跳过的是哪层。建议加载后打印一条检查信息,确认特征层参数确实被加载了:
loaded = vgg19.load_state_dict(state_dict, strict=False) print(loaded.missing_keys) # 缺失的 key 列表 print(loaded.unexpected_keys) # 多余的 key 列表正常情况是unexpected_keys里只有classifier.6.weight和classifier.6.bias,其他都为空。
6. 从跑通到会调:特征图可视化与小样本对比验证
6.1 用 hook 把 VGG19 中间层变成可视化素材
实验报告想加分,最直接的方式是展示模型“看到了什么”。VGG19 每层卷积输出都是一个特征图序列,取其中某一层的输出,按通道求和或取最大,再缩放到 0-255 就是一张可视化图。关键代码是注册 forward hook:
activation = {} def hook_fn(module, input, output): activation['value'] = output.detach() handle = vgg19.features[28].register_forward_hook(hook_fn) # 跑一次 forward _ = vgg19(img_tensor.unsqueeze(0).to(device)) # 处理特征图:取前 64 个通道,每通道做 min-max 归一化 feat = activation['value'][0] # shape: [C, H, W] feat = feat.permute(1, 2, 0).cpu().numpy() feat_min, feat_max = feat.min(), feat.max() feat = (feat - feat_min) / (feat_max - feat_min + 1e-8) handle.remove()vgg19.features[28]对应的位置要看你的需求:数字越小,画出来越像边缘纹理;越靠近后面的卷积层,越能看出塔楼、森林边界的轮廓。我建议选两个点,比如features[10]和features[28],对比低层边缘特征和高层语义特征,图放报告里很有说服力。
6.2 用小样本对比验证环境和数据有没有问题
课程实验做完后,如果你打算把方向延伸到小样本图像分类,我有个习惯:用 1-shot 和 5-shot 的设定同时跑 VGG19 和 ResNet50,用结果来验证环境和预处理是否可靠。做法是每类只取 1 张或 5 张图做训练集,其他图做测试集;如果 VGG19 在这个设置下准确率明显低于随机(比如 10 类任务只有 12%),说明你的预处理或数据划分有问题,换更好的模型也没有意义。这是最廉价的环境校验手段。
最后一件事:冻结训练时我吃过亏,就是把训练集做增强、验证集也做增强,结果 val_acc 一直上不去。后来养成习惯,任何模型训练前先打印 4 个批次的 image 形状和 label 分布,确认数类别数和通道数,再开训。这套流程从 VGG19 换到 ViT、ResNet50 都适用。希望帮到你。
本文还有配套的精品资源,点击获取