尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

VGG19图像分类实战指南:从结构拆解到迁移学习全流程

发布时间:2026/9/29 2:05:32

资讯中心
01
ARTICLE

VGG19图像分类实战指南:从结构拆解到迁移学习全流程

VGG19图像分类实战指南:从结构拆解到迁移学习全流程
简介面向人工智能初学者与深度学习实验人员这份资源围绕“基于VGG19实现图像分类”这一经典任务提供完整可运行的实验代码与预训练模型。VGG19通过多层3×3卷积堆叠提取图像特征配合池化、全连接与Softmax完成分类是理解CNN架构的重要范例。压缩包共4个文件包含3个Python脚本和1个pb模型文件整体约95.86MB。其中CPU与MLU两套评估脚本可分别用于常规环境和MediaTek NeuroPilot机器学习单元便于对比不同硬件上的推理速度与能效INT8量化模型在保证精度的同时显著降低内存占用适合嵌入式与移动端部署。目前已有2139人学习下载。通过运行脚本并观察前向传播、分类概率与准确率计算读者不仅能掌握VGG19的模型结构与推理流程还能体会模型量化、硬件加速等实际部署优化策略为后续在真实场景中高效应用深度学习模型打下基础。1. 智能计算系统实验 4-1VGG19 图像分类到底在教什么拿到智能计算系统实验 4-1 的任务单看到“基于 VGG19 实现图像分类”这个题目第一反应大概率是觉得这模型太老了。但实际跑完一遍我发现 VGG19 恰恰是图像分类实验里把原理和落地说得最清楚的模型之一结构规整、参数可数、中间层特征直观配合 ImageNet 预训练权重做迁移学习几百张标注图也能在可接受的时间内训练出能用的分类器。这篇笔记从网络结构拆到数据预处理、训练循环、评估指标再把最容易翻车的五个场景单独列出来最后落到单图推理和特征可视化验证。适合课程实验、毕设起步也适合想在换数据集前快速搭图像分类基线的从业者。2. VGG19 网络结构拆解为什么 3x3 卷积堆叠到今天还是首选VGG 系列发表于 2014 年VGG19 是其中层数最多的一版。放到今天它已经不是 SOTA但作为教学实验和工业基线它的设计思路仍然干净全部卷积都用 3×3 核所有池化都用 2×2 最大池化层与层之间没有残差、没有注意力、没有分支结构。这意味着每一层的输入输出都能被精确推算实验报告里每一个维度变化都写得出来。2.1 从输入到 7×7×512五段式特征提取器VGG19 的特征提取器由 16 个卷积层和 5 个最大池化层组成按通道数分成五个 stage。输入是 224×224×3 的 RGB 图像经过五段卷积和池化后特征图变成 7×7×512最后展平送入全连接分类器。每个 stage 内的卷积不改变空间尺寸只有池化层把 H 和 W 减半通道数则按 64→128→256→512→512 翻倍。层组卷积配置输出尺寸备注输入RGB 图像224×224×3无预处理只做归一化stage1conv3-64 × 2224×224×64两次 3×3 卷积池化1maxpool 2×2112×112×64尺寸减半stage2conv3-128 × 2112×112×128通道翻倍池化2maxpool 2×256×56×128stage3conv3-256 × 456×56×256VGG19 比 VGG16 多一层池化3maxpool 2×228×28×256stage4conv3-512 × 428×28×512比 VGG16 多一层池化4maxpool 2×214×14×512stage5conv3-512 × 414×14×512比 VGG16 多一层池化5maxpool 2×27×7×512进入全连接前所有卷积层的 padding 都设为 1、stride 为 1所以卷积本身不改变分辨率只有池化负责下采样。这个设计让网络每一层的大致计算量都可以手算对智能计算系统实验来说尤其友好——你要在实验报告里写清楚每一层输出张量的 shape按这个表填就不会错。2.2 为什么坚持 3×3 卷积核而不是更大的核两个 3×3 卷积堆叠感受野等价于一个 5×5 卷积三个 3×3 堆叠等价于一个 7×7 卷积。但 2 个 3×3 的参数总量是 2×3×318一个 5×5 是 253 个 3×3 是 27一个 7×7 是 49。参数减少的同时中间还多了一次 ReLU 非线性变换理论上表达能力反而更强。这就是 VGG 论文里反复强调的核心观点用小核堆出大感受野参数更省判别力不降。我在实验里实际打印过 VGG19 的参数量分布特征提取器部分约占 20M 参数后面的全连接层占了 120M 以上。智能计算系统的实验报告里这个“全连接层吃掉绝大部分参数”的现象值得单独写一段——它直接解释了为什么 VGG19 模型文件有 500M 左右而其中真正做特征提取的部分并没有想象中那么重。from torchvision import models # 加载未预训练的 VGG19打印完整结构 model models.vgg19(weightsNone) print(model)这段代码的作用是查看模型结构。输出里会看到features和classifier两个大模块features对应上表里的五段卷积加池化classifier是三个全连接层 4096-4096-1000中间夹着 ReLU 和 Dropout。实验报告里分析结构时以这个打印输出为准。2.3 迁移学习冻结特征层还是全量微调预训练 VGG19 已经在 ImageNet 上学过 1000 类通用特征这些低层和中层特征边缘、纹理、形状组合对绝大多数图像分类任务是可复用的。所以实验里最常见的做法不是从零训练而是基于torchvision.models.vgg19(weightsIMAGENET1K_V1)加载预训练权重只把最后一层 1000 类分类头换成自己的类别数然后分两种策略走。第一种策略是冻结全部特征提取层只训练新替换的分类头。适合数据量在几百到一千张左右的情况训练快不易过拟合。第二种策略是全量微调让预训练权重跟着你的数据继续更新适合数据量达到几千张以上并且你的图像域和 ImageNet 差异较大时使用。冻结时要把requires_grad关掉否则优化器仍然会更新所有参数等于没冻结。import torch.nn as nn num_classes 5 # 换成你自己的类别数 # 替换最后一层全连接 model.classifier[6] nn.Linear(in_features4096, out_featuresnum_classes) # 策略一冻结所有特征层只训练新分类头 for param in model.parameters(): param.requires_grad False for param in model.classifier[6].parameters(): param.requires_grad True注意classifier[6]这个索引model.classifier是一个Sequential里面依次是线性层、ReLU、Dropout、线性层、ReLU、Dropout、线性层索引 6 正好是最后的全连接层。如果你替换成model.classifier整个模块就把前面两个 4096 全连接层也换了参数量会少很多但准确率往往下降因为新分类头的容量不够。我一般只动最后一层保留前面的 4096 维特征空间。3. 数据预处理与 DataLoader归一化参数和验证集变换的坑模型结构只是骨架数据管线的质量直接决定训练能不能收敛。这个实验里数据预处理踩的坑比模型本身多得多。很多人训练损失掉得很漂亮验证集准确率就是上不去八成问题出在 transforms 上。3.1 环境版本与依赖实验只需要一套标准 PyTorch 环境。我用的组合是 Python 3.9 / PyTorch 2.x / torchvision 0.15 以上这个范围内 VGG19 的预训练权重加载接口是一致的。下面把依赖一次装齐pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install pillow matplotlib scikit-learn tqdm--index-url指定 CUDA 11.8 版本的 PyTorch 安装源如果你本机 CUDA 版本不同改成对应版本即可纯 CPU 环境去掉这个参数直接装 CPU 版 torch 也能跑只是训练慢几倍。pillow 负责图像读取scikit-learn 后面算混淆矩阵和分类报告要用。装完之后先跑一句python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)确认版本再往下走。3.2 数据集目录组织与 ImageFoldertorchvision.datasets.ImageFolder按照“根目录下每个子文件夹代表一个类别”的规则读取数据文件夹名就是标签。实验前先把数据集整理成下面这种结构data/ train/ cat/001.jpg dog/001.jpg val/ cat/001.jpg dog/001.jpg test/ cat/001.jpg dog/001.jpg训练集和验证集分开是底线测试集如果题目没要求可以省。注意ImageFolder会把类别名按字母序排序映射成数字索引比如cat是 0、dog是 1。如果你的类别名是中文或者类别顺序会影响实验报告里的标签对应关系加载后先打印dataset.classes确认一下。from torchvision import datasets train_dataset datasets.ImageFolder( rootdata/train, transformtrain_transform ) print(train_dataset.classes) # 类别名列表 print(train_dataset.class_to_idx) # 类别名到索引的映射这段代码加载训练集并打印类别映射。train_transform是后面定义的预处理流水线ImageFolder会自动读取每张图像并调用 transform。数据量不大时记得把class_to_idx保存下来推理阶段预测出索引后要能映射回类别名否则结果没法解读。3.3 归一化参数与验证集 transform 的正确写法预训练 VGG19 在 ImageNet 上训练时输入图像是按 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225] 做标准化后再送入网络的。因此我们使用预训练权重时必须配套使用同一套归一化参数否则输入分布和模型预期不一致输出概率会异常。这套参数直接抄就行不需要针对自己的数据集重新统计。from torchvision import transforms 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]) ]) 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]) ])训练集用RandomResizedCrop做随机裁剪缩放默认 scale 范围是 0.08 到 1.0相当于每轮训练看到的是同一张图的不同局部和不同尺度这是图像分类最基础的数据增强。验证集绝对不能用随机增强而是先Resize(256)再CenterCrop(224)保证每张验证图都从中心裁剪到 224×224评估结果才稳定可复现。这里有一个非常容易踩的坑训练时RandomResizedCrop会把图缩放到 224×224但验证集如果只写transforms.Resize(224)而不做中心裁剪等于把图像非等比拉伸到 224×224物体形状会被扭曲模型在验证集上的表现会明显变差。验证集 transform 务必按上面这套来写后面避坑章节会再展开讲。DataLoader 的参数同样影响训练稳定性我一般这样配from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue )num_workers4让子进程并行读取图像能明显缩短每个 epoch 的数据加载时间pin_memoryTrue在 GPU 训练时把数据锁页减少 CPU 到 GPU 的拷贝开销。这两项在实验报告里可不写但实际训练速度差距能到一倍以上。CPU 机器上num_workers设 2 就好设太大会因为进程切换反而变慢。4. 迁移学习训练实战替换分类头、超参设置与评估模型和数据管线准备好之后训练本身是套固定流程。这一章给出我实际跑通过的最小训练脚本包括分类头替换、超参选择、训练循环、评估指标计算照着改类别数和数据路径就能用。4.1 加载预训练 VGG19 并替换分类头加载预训练权重的接口在不同 torchvision 版本里写法有变化新版推荐用weights参数而不是pretrainedTrue。pretrainedTrue在新版本里已经标记为弃用继续用会看到 deprecation warning。import torch import torch.nn as nn from torchvision import models num_classes 5 device torch.device(cuda if torch.cuda.is_available() else cpu) weights models.VGG19_Weights.IMAGENET1K_V1 model models.vgg19(weightsweights) model.classifier[6] nn.Linear(4096, num_classes) # 冻结全部特征层只训练新分类头 for param in model.parameters(): param.requires_grad False for param in model.classifier[6].parameters(): param.requires_grad True model model.to(device)这里把最后一层换成输出维度等于类别数的线性层。注意model.classifier[6]的索引依赖 torchvision 的实现换版本前先打印model.classifier确认最后一层位置不要想当然。冻结参数后优化器只接收model.classifier[6].parameters()其他参数虽然还在模型里但不会被更新。4.2 训练循环与关键超参超参选择对我而言有一套默认值大部分自定义数据集在这个范围里都能正常收敛超参数推荐值说明batch_size32GPU 显存不够就降到 16但要同步调小学习率epochs20~30只训练分类头时收敛很快20 轮足够optimizerSGD(momentum0.9)比 Adam 在微调场景下更稳learning rate0.01分类头/ 0.0001全量微调新分类头用大一点预训练层用很小lossCrossEntropyLoss多分类分类任务标配只训练新分类头时新初始化的线性层对学习率不敏感0.01 的 SGD 配 momentum 0.9 是我惯用的起点。如果做全量微调预训练权重已经接近最优点学习率必须降到 1e-4 甚至 1e-5否则一步跨太大把学好的特征冲掉准确率反而不如只训练分类头。import torch.optim as optim optimizer optim.SGD(model.classifier[6].parameters(), lr0.01, momentum0.9) criterion nn.CrossEntropyLoss() def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 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) correct (outputs.argmax(dim1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total训练循环里model.train()必须显式调用它控制 Dropout 层处于工作状态在全连接层之间随机丢弃神经元抑制过拟合。loss.item()取出当前 batch 的 loss 值累加乘以images.size(0)是为了按样本数加权最后除以总样本数得到 epoch 平均 loss。outputs.argmax(dim1)沿类别维取最大概率对应的索引和标签比较计算准确率。验证时切换评估模式def validate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 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) correct (outputs.argmax(dim1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total验证阶段model.eval()加上torch.no_grad()是固定组合前者关闭 Dropout 的随机性后者关闭梯度计算。很多人只写no_grad忘了eval()导致验证结果每轮都抖动因为 Dropout 还在随机丢弃神经元。验证集不需要反向传播不关梯度只会白白消耗显存。训练主循环里逐个 epoch 调用两个函数每轮打印 train loss / train acc / val loss / val acc我就用最简单的方式观察是否过拟合for epoch in range(20): train_loss, train_acc train_one_epoch( model, train_loader, optimizer, criterion, device ) val_loss, val_acc validate(model, val_loader, criterion, device) print(fEpoch {epoch1:02d} | ftrain_loss{train_loss:.4f} train_acc{train_acc:.4f} | fval_loss{val_loss:.4f} val_acc{val_acc:.4f})如果训练准确率持续上升但验证准确率在第 10 轮左右开始下降就是过拟合信号常见的应对是加数据增强、加大 Dropout 概率或者提前停止训练。只训练分类头时过拟合概率不算高因为特征层被冻结可训练参数量很小。4.3 评估指标不能只看 accuracy实验报告里只写一个整体准确率远远不够。类别不平衡时 accuracy 会骗人比如 95% 的样本都是类别 A模型全猜 A 也能达到 95% 准确率但实际一点用处没有。我用 scikit-learn 直接算分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix import numpy as np all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) preds model(images).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, digits3)) print(confusion_matrix(all_labels, all_preds))classification_report会输出每个类别的 precision、recall、f1-score 以及 macro/weighted 平均。重点看每一类的 recall如果某个类别 recall 明显低于其他类别说明模型对该类别的特征没有学到位。混淆矩阵能直观显示哪些类容易互相混淆比如猫和狗如果频繁互相误判说明它们的类间特征差异本来就小需要更多该类别的训练样本或更强的增强。5. 训练避坑指南五个 VGG19 实测翻车现场这一章写的五个问题全部来自实验室里真实跑出来的报错和结果异常每一个都对应一个具体的解决动作。5.1 训练损失下降但验证准确率不动验证集变换不一致现象训练 loss 从 2.3 降到 0.4训练准确率到 95%验证集准确率却卡在 60% 上下怎么都上不去。原因验证集 transform 写成了transforms.Resize(224)直接改变尺寸没有做CenterCrop。非等比拉伸扭曲了图像比例模型在训练时看到的都是等比缩放的 224×224 图验证时输入分布全变了。解决验证集统一用Resize(256)CenterCrop(224)和训练集保持同一套归一化参数。改完再跑准确率通常能回到和训练集接近的水平。5.2 CUDA OOM 后调小 batch_size结果反而变差现象训练到一半报CUDA out of memory把 batch_size 从 64 调到 16 后继续训练验证准确率掉了几个点。原因只调 batch size 没调学习率。SGD 对学习率很敏感batch size 减半后每个 step 的梯度估计噪声增大学习率不变相当于步子迈大了震荡加剧。解决一种做法是保持 batch size 不变把图像尺寸从 224 降到 128 来省显存VGG19 对输入尺寸不敏感只影响中间特征图大小。另一种做法是按线性缩放规则调学习率batch size 从 64 降到 16学习率也从 0.01 降到 0.0025 附近。我一般优先降图像尺寸省显存效果更直接。5.3 加载预训练权重报 size mismatch现象运行outputs model(images)时报错说分类器最后一层维度不匹配或者加载权重时RuntimeError: size mismatch for classifier.6。原因加载预训练权重前没替换最后一层分类头。预训练模型输出维度是 1000你的数据集类别数是 5维度对不上。还有一种情况是替换完最后一层之后又去加载了未替换的完整权重。解决先替换model.classifier[6]再加载权重。如果确实需要跳过某一层不匹配可以用load_state_dict(state_dict, strictFalse)但这是最后手段平时不建议用因为 strictFalse 会静默跳过所有名称匹配不上的参数容易掩盖真正的错误。5.4 训练到一半中断所有结果归零现象训练到第 17 个 epoch 时断电或者进程被杀重新启动后一切从头来前面十几个小时白跑。原因训练脚本里没有设置 checkpoint 保存模型权重只存在内存里进程一断就全没了。解决每个 epoch 结束保存一次断点包含模型权重、优化器状态、当前轮次和最佳准确率。这样中断后可以从最近的 checkpoint 恢复torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, }, fcheckpoint_epoch{epoch:02d}.pth)恢复训练时先torch.load这个字典再分别load_state_dict到 model 和 optimizer。关键点是优化器状态必须一起保存否则恢复后学习率和动量信息丢失相当于换了个新的优化器继续训效果会乱。5.5 accuracy 很高但某个类别完全没被预测出来现象整体准确率 93%打开分类报告发现某个类别 precision 是 0模型从头到尾没把任何样本预测成这个类。原因类别不平衡。该类样本量太少模型为了降低整体 loss 学会了忽略它。accuracy 指标被多数类主导掩盖了少数类完全失效的事实。解决用WeightedRandomSampler对少数类过采样让每个 epoch 里每个类别被抽到的概率更均衡from torch.utils.data import WeightedRandomSampler class_counts [500, 480, 20] # 按 train_dataset.targets 统计每个类别的样本数 sample_weights [1.0 / class_counts[label] for label in train_dataset.targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader( train_dataset, batch_size32, samplersampler, num_workers4, pin_memoryTrue )sample_weights是每个样本被采样的概率少数类样本的权重是 1/20多数类是 1/500相对权重拉大 25 倍。replacementTrue允许同一张图在一个 epoch 里被重复抽到。替换 sampler 后原来 DataLoader 里的shuffle参数要删掉两者互斥。过采样之后少数类 recall 会明显上升代价是多数类 recall 可能略降整体 F1 通常更均衡。6. 把模型用起来单图推理与中间特征可视化训练完的模型不能只躺在 checkpoint 里至少要做两件事验证它真的学到了东西对一张新图做推理并且可视化中间层的特征图看看模型在“看”什么。单图推理脚本很简单核心是让输入走一遍和验证集完全相同的预处理from PIL import Image def predict_image(image_path, model, class_names, device): image Image.open(image_path).convert(RGB) tensor val_transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(tensor) probs torch.softmax(output, dim1).squeeze(0) top5_idx probs.argsort(descendingTrue)[:5] for idx in top5_idx: print(f{class_names[idx]}: {probs[idx].item():.4f}) return top5_idxval_transform直接复用验证集的预处理流程这一步保证了推理时的输入分布和训练评估时一致。unsqueeze(0)把单张图扩成 batch 维度softmax把 logits 转成概率。输出 top-5 而不是只取最高分能看出模型对难分样本的备选判断比如一张图狗和狼的概率接近说明模型学到了二者的相似特征。特征可视化是 VGG19 这类规整结构最值的验证手段。取model.features里某一层的输出比如第 12 个卷积层特征图 size 是 batch×256×28×28直接画出来看import matplotlib.pyplot as plt def visualize_feature_maps(model, tensor, layer_idx12, num_channels8): model.eval() with torch.no_grad(): features model.features[:layer_idx](tensor) feature_maps features[0].cpu() # 取第一张图的特征 fig, axes plt.subplots(2, 4, figsize(12, 6)) for i, ax in enumerate(axes.flat): ax.imshow(feature_maps[i], cmapgray) ax.axis(off) plt.tight_layout() plt.show()浅层卷积核学到的通常是边缘、颜色块深层卷积核学到的是更抽象的部件组合。如果可视化结果一片模糊或全是噪声说明模型根本没学好特征这时候再去看训练 loss 和验证准确率基本能对应上问题。这个可视化结果放进实验报告里比单独贴准确率数字有说服力得多。从那以后我每次拿到新的图像分类任务第一件事不是选模型而是先检查数据集目录结构、归一化参数和验证集变换这三样确认没问题再谈训练。这套流程在 VGG19 上练熟之后换到 ResNet、MobileNet 只是换一行加载代码的事数据管线和训练框架完全复用。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。