
简介面向深度学习入门、人工智能课程实验与智能计算系统实践的学习者这份资料围绕VGG19卷积神经网络完成图像分类任务重点解决模型加载、前向推理、准确率评估与硬件加速对比等实践问题。压缩包共4个文件约95.86MB以3个Python脚本和1个INT8量化模型文件为主前者负责数据预处理、模型前向传播、类别预测及准确率统计后者提供可直接加载的量化权重便于在CPU与MLU两种平台上运行评估并观察性能差异。已有2152人学习下载学习热度较高适合作为CNN实验课与模型部署练习的参考。读者可据此理解VGG19的卷积堆叠、池化降维与全连接分类流程掌握图像尺寸调整、像素归一化、Softmax概率输出、INT8量化带来的内存与速度收益并通过CPU与MLU双平台评估结果形成对推理性能优化的直观认识。1. 先看这个 VGG19 图像分类实验项目它解决的不是“训个模型”而是完整链路人工智能入门实验里图像分类最容易让人产生“我已经会了”的错觉模型一调、训练一跑、准确率一出来好像就结束了。真拿到这份基于 VGG19 的图像分类实验源码第一反应往往是另一个问题——数据放哪、预训练权重怎么接、训练完的模型怎么喂一张自己的图。这个项目适合刚接触深度学习、想跑通一次完整分类链路的人也适合已经会写训练脚本、想把实验代码改成可复用服务的人。VGG19 不新参数量也不小但它的结构直白卷积段和全连接段边界清楚拿来拆迁移学习、数据增强、推理预处理这些环节反而顺手。下面按目录、环境、核心代码、避坑和二次开发几段往下拆。2. 项目结构与技术栈从数据目录到推理入口的模块划分这个实验包如果只看训练脚本会觉得它就是“加载 VGG19、换掉最后一层、跑几个 epoch”。真正读进去会发现代码的价值在于把数据组织、模型构建、训练验证、权重保存和单张推理串成了一条能复现的线。目录清不清楚直接决定后面换数据集时会不会翻车。2.1 目录层级与各模块职责一个常见的实验目录会长成下面这样。不同压缩包里的文件命名可能略有差别但职责划分基本绕不开这几块。vgg19-classification/ ├── data/ │ ├── train/ │ │ ├── class_a/ │ │ └── class_b/ │ └── val/ │ ├── class_a/ │ └── class_b/ ├── src/ │ ├── dataset.py # Dataset、transform、DataLoader 构建 │ ├── model.py # VGG19 加载、分类头替换、冻结策略 │ ├── train.py # 训练与验证循环、权重保存 │ ├── predict.py # 单张图片/批量图片推理 │ └── utils.py # 随机种子、指标统计、路径检查 ├── checkpoints/ ├── outputs/ ├── requirements.txt └── README.mddata/train和data/val按类别分文件夹这是图像分类实验里最省事的组织方式。训练脚本通常不需要额外标签文件而是直接读文件夹名作为类别名。读代码时重点看dataset.py里有没有把class_to_idx固定下来因为训练和推理的类别顺序一旦不一致模型输出就会整体错位。src/model.py负责 VGG19 的加载和分类头替换常见做法是保留features卷积段把classifier最后一层换成当前数据集的类别数。train.py负责训练循环、验证、保存 best 权重和 last 权重。predict.py负责把一张图走一遍和验证集一致的预处理再输出 top-k 类别。checkpoints放权重outputs放日志或可视化结果这两个目录不要和源码混在一起。提示拿到源码后先别急着执行训练命令先把README、requirements.txt和train.py里的参数入口看一遍确认数据路径是相对路径还是绝对路径。2.2 技术栈选型与运行环境这个实验的技术栈不复杂关键是每一层都别缺。下面这张表按职责拆开看。技术/库在项目里的职责读代码时关注点Python训练、推理、脚本入口虚拟环境是否独立路径是否带空格PyTorch张量计算、模型训练、权重保存device是否统一state_dict是否完整TorchVision加载 VGG19、图像变换、ImageFolder预训练权重是否真的加载成功Pillow读取 jpg/png 图片推理时是否convert(RGB)NumPy指标统计、数组处理与 Tensor 转换是否多余tqdm训练进度条不影响训练但方便看每轮耗时VGG19 在这个实验里不是追求 SOTA而是结构足够规整。它的卷积段由多个Conv2d ReLU MaxPool2d堆起来最后接三层全连接。对新手来说冻结卷积段、只训练分类头能明显降低显存压力和训练时间对熟手来说这种结构也方便做层冻结、学习率分组、特征提取对比。常见做法是用预训练权重初始化再替换分类头做迁移学习而不是从随机初始化开始硬训。运行环境上我一般会先确认三件事Python 能不能正常起虚拟环境PyTorch 能不能识别到 GPUTorchVision 能不能加载 VGG19。前两件事决定训练能不能跑第三件事决定迁移学习是不是真的生效。预训练权重加载失败时训练脚本有时不会立刻报错而是静默使用随机权重这种情况后面损失曲线会很难看。2.3 数据流一张图片从磁盘到分类结果数据流这条线要理顺磁盘图片先被 Dataset 读成 RGB再经过验证集 transform 变成固定尺寸张量送进模型得到 logitssoftmax 之后取 top-k。训练时多一步数据增强验证和推理时不做随机增强。from torchvision import transforms 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(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225], ), ]) val_tf 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(224)把训练图随机裁到 224×224scale(0.7, 1.0)控制裁剪面积比例太小会把主体裁没太大会削弱增强效果。RandomHorizontalFlip对猫狗、车辆这类左右不敏感的类别通常安全但如果类别本身有方向性比如交通标志要谨慎。Normalize的均值和方差要与预训练权重匹配训练和推理必须用同一组参数否则推理会偏。验证集用Resize(256)再CenterCrop(224)是为了保留主体并稳定评估。推理脚本里不要图省事直接Resize((224, 224))因为训练验证阶段如果用的是中心裁剪推理阶段拉伸图片会让输入分布发生变化。这个细节在实验里很常见单张图测试时看不出来批量评估时就会掉点。3. 环境搭建与训练跑通把实验代码从“能看”变成“能跑”实验代码跑不起来多数不是模型写错而是环境、路径和参数入口没对齐。下面按从零到单张推理的顺序走一遍命令里的目录名按实际压缩包调整。3.1 依赖安装与目录初始化先建独立虚拟环境避免和本机其他框架版本打架。安装依赖时不要一上来就装一堆用不到的包先保证训练和推理能跑。python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate python -m pip install --upgrade pip pip install torch torchvision pillow numpy tqdm # 如果压缩包自带 requirements.txt再用它补齐 pip install -r requirements.txtpython -m venv .venv创建独立环境source那行在 Linux/macOS 下激活Windows 换成注释里的命令。PyTorch 的安装包要按本机是否有 GPU 来选择对应轮子装完后用下面几行确认环境。python -c import torch, torchvision; print(torch.__version__); print(torch.cuda.is_available())如果输出False训练脚本里的device会落到 CPUVGG19 在 CPU 上训练会非常慢。实验阶段可以先用小批量数据确认流程再切 GPU。torchvision导入失败时先看是不是装成了同名但不对应的包不要直接怀疑模型代码。目录初始化时把data、checkpoints、outputs建好。checkpoints不要放在src里面训练脚本保存权重时路径会绕。路径里尽量不用中文和空格尤其是 Windows 下有些数据加载库对中文路径处理不稳定这类问题排查起来很费时间。3.2 数据准备与配置文件修改数据按类别分文件夹训练集和验证集的类别文件夹名必须完全一致。常见结构是每个类别一个目录目录里放图片。类别数不多时建议先打印一次class_to_idx。DATA_ROOT data TRAIN_DIR f{DATA_ROOT}/train VAL_DIR f{DATA_ROOT}/val NUM_CLASSES 2 BATCH_SIZE 16 EPOCHS 10 LR 1e-3 WEIGHT_DECAY 1e-4 NUM_WORKERS 4 DEVICE cuda if torch.cuda.is_available() else cpuNUM_CLASSES必须和数据目录里的类别数一致少写会直接维度不匹配多写不会报错但会多出一个永远学不会的类。BATCH_SIZE在 VGG19 上不要盲目调大224 输入加未冻结卷积段很吃显存。LR1e-3适合只训练分类头如果解冻卷积层学习率通常要再降。NUM_WORKERS在 Windows 下可以先设 0确认流程没问题再加。数据检查这一步别省。训练前先跑一个 batch看图片张量形状是不是[B, 3, 224, 224]标签范围是不是在[0, NUM_CLASSES-1]。如果形状是[B, 1, 224, 224]说明读进来的是灰度图需要在 Dataset 里convert(RGB)。如果标签全是 0检查验证集目录是不是空的或者类别文件夹名写错。3.3 训练、验证与断点恢复训练入口通常用argparse接收参数下面这条命令是常见形式。python -m src.train \ --train-dir data/train \ --val-dir data/val \ --num-classes 2 \ --epochs 10 \ --batch-size 16 \ --lr 1e-3 \ --save-dir checkpoints--train-dir和--val-dir指向数据目录--num-classes决定分类头输出维度--epochs是训练轮数--batch-size和--lr按显存与收敛情况调。--save-dir用来保存best.pth和last.pth。如果脚本支持断点恢复一般会多一个--resume checkpoints/last.pth恢复时要把优化器状态和 epoch 一起读回来只读模型权重会导致学习率调度重新开始。训练日志重点看三个东西训练损失是否稳定下降验证准确率是否跟着动验证损失有没有在训练损失下降时反而上升。第三种情况通常是过拟合或学习率偏大。只训练分类头时前几个 epoch 指标涨得快是正常的如果连续几个 epoch 验证准确率不动先检查标签和预处理再考虑调学习率。3.4 推理脚本与单张图片测试训练完不要只看验证集数字拿一张自己的图跑一遍推理能提前暴露预处理和标签映射问题。python -m src.predict \ --image test.jpg \ --weights checkpoints/best.pth \ --classes class_a class_b--image是待测图片路径--weights是训练保存的权重--classes的顺序必须和训练时class_to_idx的顺序一致。推理脚本内部要做三件事Image.open(...).convert(RGB)、套用验证集 transform、model.eval()后关闭梯度。输出 top-k 时把类别名和概率一起打印只看最大类容易掩盖置信度异常。如果推理脚本读的是state_dict加载时用model.load_state_dict(state)如果保存的是包含model、optimizer、epoch的字典要先取ckpt[model]。加载后打印 missing keys 和 unexpected keys能快速判断分类头有没有对上。4. 核心代码拆解VGG19 迁移学习、数据增强与训练循环这一章把关键代码拆开看。重点不是抄一遍而是知道每个参数改了之后会影响什么。4.1 模型构建冻结卷积层与替换分类头VGG19 的classifier最后一项是输出 1000 类的全连接层换数据集时只改这一层。冻结卷积段可以显著减少可训练参数。import torch.nn as nn from torchvision.models import vgg19, VGG19_Weights def build_model(num_classes: int, freeze_features: bool True): try: model vgg19(weightsVGG19_Weights.DEFAULT) except Exception: # 兼容旧版 torchvision 的写法 model vgg19(pretrainedTrue) if freeze_features: for p in model.features.parameters(): p.requires_grad False in_features model.classifier[6].in_features model.classifier[6] nn.Linear(in_features, num_classes) return modelweightsVGG19_Weights.DEFAULT表示加载预训练权重旧版接口是pretrainedTrue代码里用try/except兼容两种情况。freeze_featuresTrue会把卷积段参数设为不更新只训练后面的分类头。in_features从原来的最后一层读出来通常是 4096再替换成num_classes。如果数据量和目标域差异都大可以解冻features的后几层但学习率要降下来。优化器只接收需要梯度的参数避免冻结层被更新params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.AdamW(params, lr1e-3, weight_decay1e-4)AdamW的weight_decay是解耦权重衰减实验里比直接加 L2 更稳。lr1e-3适合只训分类头如果解冻卷积层常见做法是给卷积段更小的学习率分类头保持较大学习率。4.2 Dataset 与 DataLoader训练/验证增强差异数据加载这部分决定训练稳不稳。训练增强可以多一点验证和推理必须固定。from torchvision import datasets from torch.utils.data import DataLoader train_ds datasets.ImageFolder(TRAIN_DIR, transformtrain_tf) val_ds datasets.ImageFolder(VAL_DIR, transformval_tf) train_loader DataLoader( train_ds, batch_sizeBATCH_SIZE, shuffleTrue, num_workersNUM_WORKERS, pin_memory(DEVICE cuda), ) val_loader DataLoader( val_ds, batch_sizeBATCH_SIZE, shuffleFalse, num_workersNUM_WORKERS, pin_memory(DEVICE cuda), )ImageFolder会按文件夹名排序生成类别映射训练集和验证集必须一致。shuffleTrue只给训练集验证集不要打乱否则日志里的样本顺序会对不上。pin_memory在 GPU 训练时打开能减少数据拷贝开销。num_workers不是越大越好Windows 下多进程容易出问题先设 0 或 2。训练集和验证集的 transform 要分开写。训练集用随机裁剪、翻转、颜色抖动验证集用固定尺寸和中心裁剪。验证集如果也加随机增强指标会抖动没法判断模型到底有没有变好。4.3 训练循环损失、优化器、学习率与指标训练循环里最容易写错的是设备、梯度清零和指标统计。下面这段是单轮训练的核心。import torch def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) pred logits.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) return total_loss / total, correct / total torch.no_grad() def validate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 for images, labels in loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) logits model(images) loss criterion(logits, labels) total_loss loss.item() * images.size(0) pred logits.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) return total_loss / total, correct / totaloptimizer.zero_grad()必须在反向传播前调用漏掉会累积梯度。loss.item() * images.size(0)是按样本数加权最后除以总样本数避免最后一个 batch 不满时指标偏。验证函数用torch.no_grad()关闭梯度节省显存。model.train()和model.eval()要切换VGG19 里虽然有 Dropout但训练和推理行为不同别忘了。损失函数用nn.CrossEntropyLoss()它内部包含 softmax模型输出直接是 logits不要再手动加 softmax。学习率调度可以接余弦退火criterion nn.CrossEntropyLoss() scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxEPOCHS)T_max一般设总 epoch 数学习率按余弦曲线下降。实验里如果 epoch 很少调度器作用不明显可以先不接等训练稳定后再加。4.4 推理与模型保存/加载保存权重时建议同时存model和classes否则推理时容易把类别顺序搞错。import torch from PIL import Image def predict(image_path, weights, class_names, device): model build_model(len(class_names), freeze_featuresFalse) ckpt torch.load(weights, map_locationdevice) state ckpt[model] if isinstance(ckpt, dict) and model in ckpt else ckpt model.load_state_dict(state) model.to(device).eval() img Image.open(image_path).convert(RGB) x val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob torch.softmax(model(x), dim1)[0] k min(3, len(class_names)) top torch.topk(prob, kk) return [(class_names[i], float(prob[i])) for i in top.indices]map_locationdevice解决训练在 GPU、推理在 CPU 时的加载问题。convert(RGB)防止灰度图或带透明通道的图直接进模型。unsqueeze(0)增加 batch 维度因为模型期望输入是[B, 3, 224, 224]。torch.softmax(..., dim1)把 logits 转成概率topk取前 k 个结果。推理前一定调用eval()否则 Dropout 和 BatchNorm 的行为会和训练时不一致。保存时可以把类别映射一起写进去torch.save({ model: model.state_dict(), classes: train_ds.classes, class_to_idx: train_ds.class_to_idx, }, checkpoints/best.pth)这样推理脚本不用手写类别名直接从权重里读classes能避免训练和推理类别顺序不一致。5. 避坑与排查实验代码跑不通时先看这几处这一章是血泪经验堆出来的。下面几条按“现象 → 原因 → 解决”写遇到问题可以逐条对照。5.1 预训练权重下载失败训练脚本卡在加载模型现象脚本运行到加载 VGG19 时停住或者过一会儿报网络超时后面训练仍然开始但损失下降很慢。原因通常是预训练权重下载失败代码里又用try/except吞掉了异常最后模型用了随机初始化。解决先确认权重是否真的加载成功可以在build_model后打印model.classifier[6]的参数是否非空离线环境把权重文件放到框架默认缓存目录或者手动指定本地权重路径加载。不要看到训练能跑就默认迁移学习生效了这个坑很隐蔽。5.2 数据路径不对或标签错位loss 降不下去现象训练准确率很快到很高验证准确率却像随机猜或者推理时所有图都输出同一类。原因常见有三种训练集和验证集类别文件夹名不一致验证集某个类别目录为空推理时--classes顺序和训练时class_to_idx不一样。解决训练前打印train_ds.class_to_idx和val_ds.class_to_idx确认两个映射完全一致检查每个类别目录下有没有图片把类别映射保存进权重文件推理时直接读不要手写类别名。标签错位属于那种“代码不报错但结果全错”的问题越早查越好。5.3 显存不够batch size 一调大就崩现象训练刚启动就报显存不足或者跑几个 batch 后崩掉。原因VGG19 参数量大224 输入加上未冻结的卷积段显存占用比想象中高batch_size调大、num_workers过多、验证集没有关梯度都会加剧。解决先冻结features只训练分类头把batch_size降到 8 或 4验证函数加torch.no_grad()num_workers先设 0 或 2如果还不够用梯度累积模拟大 batch。不要一上来就换更复杂的模型先把现有链路跑通。5.4 设备不一致或半精度踩坑现象报Expected all tensors to be on the same device或者模型在 GPU、数据在 CPU。原因模型.to(device)了但某个 batch 的图片或标签忘了.to(device)保存权重时在 GPU推理时直接torch.load到 CPU 环境。解决训练循环里统一把images和labels移到device加载权重用map_locationdevice不要在 CPU 上强行用半精度VGG19 在 CPU 上半精度支持并不稳定。设备问题不复杂但报错信息有时候指向不明显先查张量在哪。5.5 推理结果总是同一类或置信度异常高现象拿几张不同类别的图测试输出全是同一类概率还接近 1。原因推理预处理和验证集不一致比如训练用Resize(256)CenterCrop(224)推理直接Resize((224,224))图片读进来是 BGR 或灰度权重没加载成功类别顺序错。解决把验证集 transform 抽成一个函数训练验证和推理共用读图统一convert(RGB)加载权重后打印 missing/unexpected keys从验证集抽一张图回测确认训练时能分对的图推理也能分对。推理链路最好单独写一个小测试不要等上线才发现预处理歪了。6. 二次开发与改造从实验脚本到可复用图像分类服务把实验代码改成能复用的小服务改动不用很大但边界要清楚。我一般先不改模型结构先把数据入口、类别映射和推理接口固定下来再动训练策略。下面这张表是我常见的改造顺序。改造目标改动点验证方式换自己的数据集改data/train、data/val目录重设NUM_CLASSES打印class_to_idx跑一个 batch 看形状减少过拟合加强训练增强冻结卷积段加 weight decay对比验证损失和训练损失曲线提升推理速度导出 TorchScript 或 ONNX固定输入尺寸用同一张图对比导出前后输出封装服务把predict()包成 HTTP 接口入参图片出参 top-k用 curl 或脚本批量请求导出模型前先切到推理模式这一步别省model.eval() example torch.randn(1, 3, 224, 224).to(device) traced torch.jit.trace(model, example) traced.save(checkpoints/vgg19_traced.pt)torch.jit.trace需要固定输入尺寸VGG19 这边用 224×224 正好。导出后拿一张验证集图片对比原模型和 traced 模型的输出概率差在很小范围内才算通过。封装服务时不要把Image.open和模型加载放在每次请求里模型初始化一次预处理函数单独抽出来否则并发一上来就会卡。改造时最容易忽略的是类别映射。训练时train_ds.classes是按文件夹名排序的推理服务如果手写类别列表顺序错了不会报错只会稳定输出错类。我现在的习惯是保存权重时把classes和class_to_idx一起存推理入口只认权重里的映射不认手写参数。换数据集后先跑单张推理再跑一个小批量验证集两个结果对得上再继续改模型。从那以后我每次动分类头或换数据目录都先强制走一遍单张推理回测确认标签映射没歪。希望帮到你。本文还有配套的精品资源点击获取