简介本资源面向深度学习与人工智能方向的实验学习者围绕经典卷积神经网络VGG19展开图像分类实践适合已具备一定CNN基础、希望理解模型部署与推理优化的学生和开发者。压缩包共4个文件包含3个Python脚本与1个pb模型文件整体约95.86MB其中脚本分别用于CPU与MLU平台上的模型评估pb文件为INT8量化后的VGG19权重便于在嵌入式或移动端高效推理。实验覆盖模型加载、图像预处理、前向传播、Softmax分类及准确率统计等完整流程并支持在CPU与硬件加速器上对比性能差异帮助读者掌握模型量化与硬件加速的部署思路。目前已有2140人学习适合作为智能计算系统课程实验的参考方案也可用于理解VGG19的运作机制与跨平台推理评估方法。1. 从一次翻车说起为什么我劝你先跑通 VGG19 图像分类再谈模型优化去年帮一个学弟看他的实验报告他花了三天调一个图像分类模型准确率死活卡在 60% 上不去。我让他把数据预处理和网络结构打印出来结果发现他把 VGG19 的输入尺寸改成了 64×64还去掉了全部 BatchNorm 层。这不是模型不行是根本没搞懂 VGG19 的结构约束。智能计算系统实验4-1 这个题目核心就是基于 VGG19 实现图像分类看起来简单但真正动手跑一遍你会踩到输入尺寸、预训练权重加载、类别映射、显存溢出这一连串坑。这篇文章面向的是拿到这份实验资源、想把它跑起来并做二次开发的开发者。不管你是第一次接触迁移学习还是想把这个实验改造成自己的项目底座下面从环境搭建到代码改造我会把每个环节拆开讲清楚。2. 项目定位与技术选型VGG19 为什么还在被拿来当实验底座2.1 这个实验到底在做什么智能计算系统实验4-1 的任务很明确用 VGG19 网络完成一个图像分类任务。输入是一批带类别标签的图片输出是每张图片属于各个类别的概率。实验的完整链路包括数据加载与预处理、VGG19 网络构建、预训练权重加载、分类头替换、模型训练与验证、推理测试。VGG19 是牛津大学视觉几何组提出的经典卷积网络19 层带权重的结构核心特点是全部使用 3×3 小卷积核堆叠通过反复的卷积-激活-池化组合逐层提取特征。虽然现在 ViT、ConvNeXt 这些新架构在精度上已经超过它但 VGG19 结构规整、层次清晰非常适合用来理解卷积网络的特征提取流程。这也是为什么大量智能计算系统类实验仍然选它作为底座。实际做的时候没人会从零训练 VGG19。标准做法是加载 ImageNet 预训练权重把最后的分类层替换成自己任务的类别数然后做迁移学习。这个实验的核心训练点也在这里理解预训练权重怎么加载、分类头怎么改、哪些层冻结哪些层微调。2.2 技术栈与依赖选型这个实验的常见技术栈是 PyTorch torchvision。PyTorch 的动态图机制对调试友好torchvision 直接提供了 VGG19 的网络定义和预训练权重接口省去手写网络结构的麻烦。数据处理用 torchvision.transforms 做增强和归一化训练循环用标准的 PyTorch 训练模板。我一般会建议用以下版本组合兼容性最稳# 环境搭建创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装 PyTorch以 CUDA 11.8 为例CPU 版本去掉 index-url 即可 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 其余依赖 pip install numpy matplotlib pillow tqdm这里有几个参数需要说明。torch 和 torchvision 的版本要匹配比如 torch 2.1.x 对应 torchvision 0.16.x版本不匹配会在导入时直接报错。CUDA 版本根据你机器的显卡驱动来选不确定的话先用 CPU 版本跑通流程再换 GPU 加速。tqdm 用来显示训练进度条不是必须的但调试时能直观看到每个 epoch 的耗时。注意如果你用的是实验室服务器先确认 CUDA 驱动版本再决定装哪个版本的 PyTorch。装错了卸载重装很折腾。2.3 目录结构设计拿到实验资源后建议按下面的结构组织代码。这个结构不复杂但能让训练、推理、配置各归其位后面改起来不会一团乱。vgg19-classification/ ├── configs/ │ └── default.yaml # 超参数配置文件 ├── data/ │ ├── train/ # 训练集按类别分文件夹 │ │ ├── class_0/ │ │ └── class_1/ │ └── val/ # 验证集结构同训练集 ├── models/ │ └── vgg19_cls.py # 模型定义与分类头替换 ├── utils/ │ ├── dataset.py # 数据加载与预处理 │ └── engine.py # 训练与验证循环 ├── train.py # 训练入口 ├── predict.py # 单张图片推理入口 └── requirements.txtdata 目录下按类别分文件夹是 torchvision.datasets.ImageFolder 的硬性要求每个子文件夹名就是类别名框架会自动读取并映射成标签索引。这个约定省去了手写标签映射的麻烦但如果你拿到的数据集标签是 CSV 格式就需要自己写 Dataset 类来适配。3. 核心模块拆解从数据加载到 VGG19 分类头替换3.1 数据预处理与加载VGG19 在 ImageNet 上训练时输入图片统一缩放到 224×224归一化参数是 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]。这两个参数不能随便改因为预训练权重就是在这个分布下学到的。如果你用自己的均值和方差预训练特征提取层会“看不懂”输入精度直接崩。# utils/dataset.py import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader def build_dataloader(data_dir, batch_size32, num_workers4, is_trainTrue): 构建数据加载器 data_dir: 数据集根目录子文件夹为类别名 batch_size: 批大小显存不够就调小 num_workers: 数据加载线程数Windows 下建议设为 0 is_train: 是否为训练模式决定是否加数据增强 # VGG19 标准归一化参数不要改 normalize transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) if is_train: # 训练集加随机增强提升泛化 transform transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放到 224 transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.ToTensor(), # 转成 Tensor值域 [0,1] normalize # 标准化 ]) else: # 验证集不做随机增强只做确定性缩放 transform transforms.Compose([ transforms.Resize(256), # 短边缩放到 256 transforms.CenterCrop(224), # 中心裁剪 224 transforms.ToTensor(), normalize ]) dataset datasets.ImageFolder(data_dir, transformtransform) dataloader DataLoader( dataset, batch_sizebatch_size, shuffleis_train, # 训练集打乱验证集不打乱 num_workersnum_workers, pin_memoryTrue # 加速 GPU 传输 ) return dataloader, dataset.classes这段代码里几个关键点。RandomResizedCrop(224) 是训练时的标准增强它随机裁剪图片的不同区域再缩放让模型看到更多样的样本。验证集用 Resize(256) CenterCrop(224)保证每次评估结果一致。pin_memoryTrue 在 GPU 训练时能减少数据传输开销但如果你的机器内存紧张可以关掉。num_workers 在 Windows 上设成大于 0 有时会卡死这是 PyTorch 在 Windows 下的老问题设成 0 最稳。3.2 VGG19 网络构建与分类头替换torchvision.models.vgg19 提供了现成的网络定义。加载预训练权重后原来的分类层是 4096→1000 的全连接你需要把它换成 4096→你的类别数。# models/vgg19_cls.py import torch.nn as nn from torchvision import models def build_vgg19(num_classes, pretrainedTrue, freeze_featuresFalse): 构建 VGG19 分类模型 num_classes: 你的任务类别数 pretrained: 是否加载 ImageNet 预训练权重 freeze_features: 是否冻结特征提取层 # 加载预训练 VGG19 # 旧版本 torchvision 用 pretrainedTrue新版本用 weightsVGG19_Weights.DEFAULT model models.vgg19(pretrainedpretrained) if freeze_features: # 冻结特征提取部分只训练分类头 for param in model.features.parameters(): param.requires_grad False # 替换分类头VGG19 的 classifier 是 Sequential # 原结构: Linear(25088, 4096) - ReLU - Dropout - Linear(4096, 4096) - ReLU - Dropout - Linear(4096, 1000) # 只替换最后一层 Linear(4096, 1000) 为 Linear(4096, num_classes) in_features model.classifier[6].in_features model.classifier[6] nn.Linear(in_features, num_classes) return model这里有个容易翻车的地方VGG19 的 classifier 是一个 Sequential索引 6 才是最后一层全连接。如果你直接写 model.classifier nn.Linear(...)会把前面两个 4096 的全连接和 Dropout 全丢掉模型结构就变了。正确做法是只替换索引 6 那一层。freeze_features 参数控制是否冻结特征提取层。数据量小的时候比如每类只有几十张图冻结特征层只训练分类头能防止过拟合。数据量大的时候解冻后面几层卷积做微调精度会更高。我一般会先冻结跑一轮看看验证集精度再决定要不要解冻。3.3 训练循环与验证逻辑训练循环是标准模板但有几个细节决定了你能不能跑出合理的结果。# utils/engine.py import torch from tqdm import tqdm def train_one_epoch(model, dataloader, criterion, optimizer, device): 训练一个 epoch model.train() # 切换到训练模式启用 Dropout 和 BatchNorm 更新 total_loss 0.0 correct 0 total 0 for images, labels in tqdm(dataloader, descTraining): 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 outputs.max(1) # 取概率最大的类别 correct predicted.eq(labels).sum().item() total labels.size(0) avg_loss total_loss / total acc correct / total return avg_loss, acc torch.no_grad() # 验证时不计算梯度省显存 def evaluate(model, dataloader, criterion, device): 验证集评估 model.eval() # 切换到评估模式关闭 Dropout固定 BatchNorm total_loss 0.0 correct 0 total 0 for images, labels in dataloader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) avg_loss total_loss / total acc correct / total return avg_loss, accmodel.train() 和 model.eval() 这两个切换是必须的。VGG19 里有 Dropout 层训练时随机丢弃神经元评估时必须关闭否则每次推理结果都不一样。BatchNorm 层在训练时更新 running_mean 和 running_var评估时用固定值。忘了切换 eval 模式验证精度会莫名其妙偏低这个坑我踩过不止一次。优化器一般用 SGD 或 Adam。SGD 配 momentum0.9、lr0.001 是迁移学习的经典组合收敛稳定但慢一些。Adam 用 lr1e-4 起步收敛快但有时会震荡。学习率调度用 StepLR 或 CosineAnnealingLR每 10 个 epoch 降一次学习率。4. 跑通实验从零到第一个可用模型的完整步骤4.1 数据准备与检查拿到实验数据后第一步不是写代码是检查数据。确认三件事类别数、每类样本量、图片格式是否统一。# 统计每个类别的图片数量 for dir in data/train/*/; do echo $dir: $(ls $dir | wc -l) images done如果发现某个类别样本特别少比如只有个位数训练时会出现严重的类别不平衡模型会偏向多数类。解决办法是用 WeightedRandomSampler 给少数类更高采样权重或者在损失函数里给少数类更大权重。图片格式也要检查。ImageFolder 默认用 PIL 读取支持 jpg、png、bmp 等常见格式。如果数据里有 gif 或 tiff可能会报错。统一转成 jpg 最省事。4.2 训练脚本与超参数配置把前面的模块串起来训练入口大概长这样# train.py import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import StepLR from utils.dataset import build_dataloader from models.vgg19_cls import build_vgg19 from utils.engine import train_one_epoch, evaluate def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 超参数 num_classes 10 # 根据你的数据集改 batch_size 32 epochs 30 lr 0.001 momentum 0.9 weight_decay 1e-4 # 数据 train_loader, classes build_dataloader(data/train, batch_size, is_trainTrue) val_loader, _ build_dataloader(data/val, batch_size, is_trainFalse) print(fClasses: {classes}) # 模型 model build_vgg19(num_classes, pretrainedTrue, freeze_featuresTrue) model model.to(device) # 损失函数与优化器 criterion nn.CrossEntropyLoss() # 只优化需要梯度的参数 optimizer optim.SGD( filter(lambda p: p.requires_grad, model.parameters()), lrlr, momentummomentum, weight_decayweight_decay ) scheduler StepLR(optimizer, step_size10, gamma0.1) best_acc 0.0 for epoch in range(epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1}/{epochs} | fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f}) # 保存最佳模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fSaved best model with acc: {best_acc:.4f}) if __name__ __main__: main()几个参数需要根据实际情况调整。batch_size 受显存限制VGG19 参数量大11GB 显存大概能跑 batch_size328GB 只能跑 16 或更小。epochs 看数据量几千张图跑 30 个 epoch 通常够收敛。lr0.001 是 SGD 的常用起点如果 loss 震荡厉害就降到 0.0001。filter(lambda p: p.requires_grad, model.parameters()) 这行很重要。冻结特征层后那些层的参数不需要更新优化器里排除它们能省显存、加速训练。如果你忘了加这个过滤优化器还是会尝试更新所有参数虽然梯度是 None 不会真的更新但会浪费计算。4.3 推理与结果验证训练完拿到 best_model.pth写一个推理脚本验证效果# predict.py import torch from PIL import Image from torchvision import transforms from models.vgg19_cls import build_vgg19 def predict(image_path, model_path, class_names, device): 对单张图片进行分类预测 # 构建模型并加载权重 model build_vgg19(len(class_names), pretrainedFalse) model.load_state_dict(torch.load(model_path, map_locationdevice)) model model.to(device) model.eval() # 预处理与验证集保持一致 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]) ]) image Image.open(image_path).convert(RGB) # 强制转 RGB防止灰度图报错 tensor transform(image).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) conf, pred probs.max(1) return class_names[pred.item()], conf.item() if __name__ __main__: classes [class_0, class_1, class_2] # 替换成你的类别名 label, confidence predict(test.jpg, best_model.pth, classes, cuda) print(fPredicted: {label}, Confidence: {confidence:.4f})Image.open().convert(RGB) 这步不能省。如果你的测试图是灰度图或带透明通道的 PNG不转 RGB 会在 ToTensor 时报错。unsqueeze(0) 是给图片加一个 batch 维度因为模型 forward 要求输入是 4 维张量 [B, C, H, W]。验证结果时不要只看准确率。打印混淆矩阵看看哪些类别容易被混淆。如果两个类别互相误判严重可能是它们的视觉特征太接近需要更多数据或更强的数据增强。5. 避坑与排查VGG19 实验里最容易翻车的五个地方5.1 输入尺寸不匹配导致维度报错现象运行时报错 RuntimeError: mat1 and mat2 shapes cannot be multiplied或者 size mismatch for classifier.6.weight。原因VGG19 的特征提取层输出是 512×7×7展平后是 25088 维对应 classifier 第一层的输入。如果你改了输入图片尺寸比如用 128×128特征图会变成 512×4×4展平后是 8192 维和 25088 对不上。解决要么把输入统一改成 224×224要么改 classifier 第一层的输入维度。最省事的做法是保持 224×224 不变这也是预训练权重的标准输入。5.2 预训练权重加载失败现象下载权重时卡住或报 URLError或者加载后精度极低。原因torchvision 的预训练权重存在境外服务器网络不稳定时下载会失败。另外如果你手动下载了权重文件但路径不对模型会随机初始化精度自然上不去。解决先确认权重文件是否完整下载。如果网络有问题可以手动下载后放到 ~/.cache/torch/hub/checkpoints/ 目录下。加载后打印一下 classifier[6] 的权重看看是不是随机值。5.3 显存溢出现象RuntimeError: CUDA out of memory。原因VGG19 参数量约 1.4 亿加上中间激活值batch_size32 时大概需要 8-10GB 显存。如果你的显卡显存不够就会溢出。解决先把 batch_size 降到 8 或 16再试。如果还是不够冻结更多层减少梯度计算或者用 torch.cuda.amp 混合精度训练。混合精度能把显存占用降一半左右但需要改几行代码。5.4 验证精度远低于训练精度现象训练集准确率 95%验证集只有 60%。原因过拟合。数据量太少、模型太复杂、训练太久都会导致。另外如果验证集预处理和训练集不一致比如验证集忘了归一化也会出现这个问题。解决先检查验证集预处理是否和训练集一致。然后加数据增强、加 Dropout、加 weight_decay。如果数据量确实太少冻结全部特征层只训练分类头或者用更小的学习率。5.5 类别索引与标签不对应现象推理结果看起来合理但类别名对不上。原因ImageFolder 按文件夹名的字母顺序分配标签索引不是按你想要的顺序。比如文件夹是 cat、dog、bird索引是 bird0、cat1、dog2不是 cat0。解决用 dataset.classes 打印实际映射关系保存下来供推理时使用。不要自己手写类别列表容易搞错顺序。6. 二次开发改造把 VGG19 实验变成你自己的项目底座跑通实验只是起点。真正有价值的是把这个底座改造成能解决你自己问题的工具。下面说几个改造方向和我踩过的坑。换网络结构。VGG19 跑通后把 models/vgg19_cls.py 里的 models.vgg19 换成 models.resnet50 或 models.efficientnet_b0分类头替换逻辑基本一样。但要注意ResNet 的最后一层是 model.fc不是 model.classifier[6]改的时候别照搬。EfficientNet 的输出维度也不同需要打印一下模型结构确认。换任务类型。从单标签分类改成多标签分类把 CrossEntropyLoss 换成 BCEWithLogitsLoss分类头输出维度改成标签数推理时用 sigmoid 代替 softmax。数据加载也要改ImageFolder 不支持多标签需要自己写 Dataset 类从 CSV 读取标签。加注意力机制。在 VGG19 的特征提取层后面接一个 SE 模块或 CBAM 模块能让模型关注更重要的通道或空间区域。我一般会在每个 stage 的最后一个卷积层后面加 SE代码量不大但效果提升明显。注意加完之后要重新训练不能直接加载原来的权重。部署成 API。用 FastAPI 或 Flask 包一层 HTTP 接口接收图片返回分类结果。推理时记得用 torch.no_grad() 和 model.eval()否则每次请求都会构建计算图内存越吃越多。如果并发量高考虑用 ONNX Runtime 或 TensorRT 加速。改造验证方法。改完模型后不要只看准确率。打印每个类别的 precision、recall、F1看看有没有类别被牺牲了。用混淆矩阵定位问题类别再针对性补数据或调权重。有个习惯我每次改完模型都会走一遍先用小批量数据比如 100 张图跑 2 个 epoch确认没有维度错误、没有显存溢出、loss 在下降再上全量数据。这个习惯帮我省了很多次通宵重跑的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?