简介基于VGG19实现图像分类的智能计算系统实验4-1完整资料包面向深度学习初学者、智能计算课程学员及需要实践CNN部署的技术人员围绕经典VGG19网络完整覆盖图像分类中的模型加载、预处理、前向推理与性能评估链路。压缩包共4个文件包含3个Python脚本与1个pb预训练模型其中两个脚本分别面向CPU环境与MLU硬件加速平台便于对比不同算力环境下的运行差异vgg19_int8.pb为INT8量化后的权重文件展示模型轻量化的实际形态。资源整体大小约95.86MB目前已有2139人学习下载。通过实际运行两个评估脚本可直观感受CPU与专用MLU加速器在推理速度、能效比上的差别并掌握图像缩放、归一化、前向传播、类别概率输出及准确率统计等完整实验步骤INT8量化模型也体现了压缩部署在资源受限设备中的价值。该资料既适合课程实验的自主完成与报告撰写也可作为入门深度学习分类任务的实践参考。1. 做实验4-1前先想清楚三件事智能计算系统课程里的实验4-1“基于 VGG19 实现图像分类”几乎所有做视觉方向的学生都会被这道题卡一下。卡住的原因不在于能不能写出一行torchvision.models.vgg19()而在于三件绕不开的事一是 VGG19 的参数分布很特殊143.6M 参数里卷积层只占 32.4M真正吃显存、背训练集的是最后三个全连接层这个结构决定了你该冻结哪里、该换掉哪里二是这个模型是按 ImageNet 的统计分布预训练出来的你的数据预处理如果不用同一套均值方差做归一化前面几十层卷积看到的就是陌生输入再好的权重也发挥不出来三是显存和 batch 的取舍全连接层动辄占掉 1GB 以上显存batch 稍大就 OOM调小了 loss 曲线又像心电图。这三个问题想清楚后面的路会顺很多而且这套思路换到 ResNet、换到 transformer 图像分类模型依然成立。2. 先把 VGG19 的骨架读懂层结构、参数数量与迁移学习的最小动作2.1 为什么实验场景总选 VGG19纯卷积堆叠的“笨”反而是优势VGG19 是 2014 年 ILSVRC 上牛津 VGG 组提出的深度卷积网络。它的设计极度简洁所有卷积核都是 3×3没有任何分支结构靠叠加层数把感受野撑大。两个 3×3 卷积级联等价于一个 5×5 卷积的感受野三个 3×3 级联等价于一个 7×7 卷积参数量却更少中间还能多出两次 ReLU 非线性。这种不做花样设计的结构放到今天的图像分类模型里看计算量偏大精度也不是顶尖但它有一个非常适合做实验的特性特征图尺寸的变化完全规律可循每一层的输出形状都能提前预测模型出了问题很容易定位到具体某一层。智能计算系统实验把 VGG19 作为第一个落地对象通常还有一层现实原因torchvision 官方直接提供 ImageNet 预训练权重不需要自己找权重文件、配复杂环境。实验课的合理投入方式是把这个模型当作一个“预训练特征提取器”来用把精力放在数据管线和训练策略上而不是从零训练一个 140M 参数的网络。对现在的工业实践来说这种做法同样主流——除非你有超大数据集否则从零训 CNN 已经很少见了迁移学习是默认起点。所以与其盯着 VGG19“老”这个标签不如先认可它在迁移学习框架里的教学价值结构清楚、坑好定位、和后续所有模型的调参套路完全一致。2.2 层结构和参数量用一张表看清每个 Block 在做什么对一张 224×224×3 的输入VGG19 的特征图变化如下表所示。这张表在实验报告里几乎必现它同时也是之后理解显存占用和全连接层维度的基准。阶段卷积层配置输出特征图尺寸池化后尺寸Block1conv3-64 × 2224×224×64112×112×64Block2conv3-128 × 2112×112×12856×56×128Block3conv3-256 × 456×56×25628×28×256Block4conv3-512 × 428×28×51214×14×512Block5conv3-512 × 414×14×5127×7×512ClassifierLinear(25088,4096) → Linear(4096,4096) → Linear(4096,1000)展平 7×7×512—这里 conv3-64 表示“3×3 卷积、输出 64 个通道”。卷积层总参数量约 32.4M三个全连接层合计约 102.6M全模型约 143.6M。注意全连接层权重占了大头原因有二25088 维展平向量到 4096 的第一层全连接权重矩阵本身就是 25088×4096约 1 亿参数三个全连接层叠加自然就撑起了规模。这些参数不仅数量大反向传播时要保留的中间激活也大所以后面遇到 OOM 不要困惑它就是这几层吃掉的。2.3 加载预训练模型并替换分类头PyTorch 里的最小动作实验的起点几乎一定是用 ImageNet 预训练权重初始化模型然后把最后的 1000 类输出改成你自己的类别数。这步要理解vgg19.classifier这个 Sequential 模块的下标结构才不会改错层。import torch import torchvision.models as models # 加载 ImageNet 预训练权重torchvision 首次会自动下载约 530MB 权重 vgg19 models.vgg19(pretrainedTrue) # 替换分类头从 1000 类改成自己的类别数 num_classes 5 # 例如森林图像分类里的五类林地 vgg19.classifier[6] torch.nn.Linear(in_features4096, out_featuresnum_classes) # 把模型搬到 GPU/CPU 上 device torch.device(cuda if torch.cuda.is_available() else cpu) vgg19 vgg19.to(device) # 确认改动生效输出 Linear(in_features4096, out_features5) print(vgg19.classifier[6])逻辑说明vgg19.classifier是一个torch.nn.Sequential下标 0 是 Linear(25088,4096)下标 1 是 ReLU下标 2 是 Dropout下标 3 是 Linear(4096,4096)下标 4 是 ReLU下标 5 是 Dropout下标 6 才是输出层。改最后一层就足够前面的卷积特征提取部分保持预训练原样。device的判断是为了兼容没有 GPU 的机器也避免在集群上硬编码cuda:0导致多卡环境报错。权重下载会缓存在~/.cache/torch/hub/checkpoints第二次加载就是本地读取不会重复下载。这里有一个容易被忽略的细节vgg19.features和vgg19.classifier是模型的两大子模块前者是卷积特征提取器后者是分类器。后续做冻结或分层学习率时都是绕着这两个名字做操作所以先记住这个结构关系比背下网络层数更重要。2.4 打印每层输出形状用 forward hook 做一次前向自检模型替换完成后我建议不要急着开训练先拿一张随机噪声图做一次“mock forward”确认维度全部对得上。这一步能省掉之后至少二十分钟排错尤其当你想改输入分辨率时它几乎就是必需动作。# 构造随机输入模拟 batch2 的 224x224 彩色图 dummy_input torch.randn(2, 3, 224, 224).to(device) # 定义 hook 函数打印每一层输出张量的形状 def print_shape(name): def hook(module, input, output): print(f{name}: {output.shape}) return hook # 给特征提取部分的每个子层挂 hook for name, module in vgg19.features.named_children(): module.register_forward_hook(print_shape(ffeatures.{name})) vgg19.eval() with torch.no_grad(): vgg19(dummy_input)逻辑说明register_forward_hook是 PyTorch 的钩子机制每次前向传播后会自动调用回调函数这里用来观察中间特征形状。正常输出从features.0: [2, 64, 224, 224]开始经过五次 MaxPool 缩小到features.29: [2, 512, 7, 7]最后经过 classifier输出[2, 5]。如果中途某层 shape 与上面表格对不上说明预处理尺寸不对如果卡在 classifier 报错多半是展平后的维度和Linear(25088, 4096)不匹配。这个自检动作对任何模型通用是我做得最多、也觉得最值得做的前置检查。3. 数据准备这步决定精度上限目录组织、Transform 管线与样本均衡3.1 用 ImageFolder 组织图像分类数据集目录规划与划分脚本不管你下载的是公开的图像分类数据集下载包还是自己收集的照片集进训练前的第一件事是把它整理成torchvision.datasets.ImageFolder能直接消费的目录结构根目录下每个子文件夹一个类别子文件夹名就是类别名。以森林图像分类为例约定如下data/ train/ oak/ # 类别0 0001.jpg 0002.jpg maple/ # 类别1 birch/ # 类别2 val/ oak/ maple/ birch/如果数据是散图加一个标注文件你需要先写划分脚本。常见做法是读取全部图片路径和标签按类别做分层抽样80% 进 train、20% 进 val再移动到对应子目录。分层抽样的目的是保证 train 和 val 里每个类别的比例一致否则 val 里某类只有 3 张图验证指标会抖得没法看。import os import random import shutil def split_val(source_root, target_root, val_ratio0.2, seed42): random.seed(seed) # 固定种子让划分可复现 for class_name in os.listdir(source_root): class_dir os.path.join(source_root, class_name) if not os.path.isdir(class_dir): continue images [f for f in os.listdir(class_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) val_cnt int(len(images) * val_ratio) for split in [train, val]: os.makedirs(os.path.join(target_root, split, class_name), exist_okTrue) for img in images[:val_cnt]: shutil.copy(os.path.join(class_dir, img), os.path.join(target_root, val, class_name, img)) for img in images[val_cnt:]: shutil.copy(os.path.join(class_dir, img), os.path.join(target_root, train, class_name, img))逻辑说明random.seed(seed)是为了让每次运行划分一致这在实验报告要求“结果可复现”时很关键。用lower()判断扩展名避免 .JPG 被漏掉。val_ratio0.2是常用默认值样本总数特别少时可以改成 0.1但不要低于 0.1否则验证集太小指标统计不可信。3.2 Transform 管线Resize、CenterCrop 与 ImageNet 归一化的标准组合torchvision 预训练模型对输入有一套固定预期先缩放到 256×256再中心裁剪出 224×224然后按 ImageNet 的均值方差归一化。训练集可以加随机增强验证集必须保持确定性变换。代码写法如下from torchvision import transforms train_transforms transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放数据增强 transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转 transforms.ToTensor(), # HWC uint8 - CHW float32值域 0~1 transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transforms transforms.Compose([ transforms.Resize(256), # 等比缩放到短边 256 transforms.CenterCrop(224), # 再从中心裁 224x224 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里最讲究的是 Normalize 的均值方差。它们是预训练时在大量自然图像上统计出来的 RGB 均值方差。VGG19 的卷积层权重是在“减均值、除标准差”后的输入上学出来的你的实验如果不做这一步或者换成自己算的统计值相当于把预训练权重拖到一个偏移过的分布上推理迁移效果会明显打折。ToTensor放在 Normalize 之前也很有讲究它先把 HWC 的 PIL 图转成 CHW 的 float 张量像素从 0~255 缩放到 0~1之后 Normalize 才能按 float 计算。训练集和验证集不要共用 transform。原因在于验证集要保证每张图进入模型的区域一致CenterCrop提供的就是这种确定性如果验证集也用RandomResizedCrop同一张图每次跑验证进入模型的区域都不同验证损失曲线会变得不可解释。3.3 DataLoader 参数batch、num_workers、pin_memory 各管什么数据准备最后一步是构造 DataLoader。下面这套配置是实验环境里比较稳妥的起点from torch.utils.data import DataLoader train_dataset ImageFolder(rootdata/train, transformtrain_transforms) val_dataset ImageFolder(rootdata/val, transformval_transforms) # batch 先给 32显存小于 6GB 时改 16 或 8 train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(f训练集样本数: {len(train_dataset)}) print(f验证集样本数: {len(val_dataset)}) print(f类别映射: {train_dataset.class_to_idx})参数说明shuffle 只用于训练集作用是把类别顺序打乱避免一个 batch 内全是同一类。num_workers 是数据加载子进程数一般不超过 CPU 核心数的一半设成 0 表示当前进程加载慢但在某些受限容器里更稳。pin_memoryTrue 对 GPU 训练有益因为页锁定内存到显存的拷贝路径更短。batch_size 是最需要按显存调的参数VGG19 在 224×224 输入下训练时一张图大约占用几百 MB 显存含反向传播保留的中间张量6GB 显存配 batch 32 是偏紧的建议跑一个 epoch 看实际占用再决定。3.4 看一眼每类的样本数不均衡问题要在训练前处理进入训练前我强烈建议把每个类别的训练样本数量打印出来看一眼。类别不均衡在这种实验里几乎必然出现尤其是数据量不大时。做法如下from collections import Counter # 统计完整训练集里的类别分布 counter Counter() for _, label in train_dataset.samples: counter[label] 1 for class_idx, count in sorted(counter.items()): print(f类别 {class_idx}{train_dataset.classes[class_idx]}: {count} 张)如果发现最少的一类只有二三十张、最多的一类上千张至少有两种处理办法一是用WeightedRandomSampler让少数类有更高的被采样概率二是给CrossEntropyLoss传入 class weight。下面以第二种为例它对训练时间几乎没有影响实现也直接import torch.nn as nn total len(train_dataset) class_weights [ total / (num_classes * counter[class_idx]) # 样本越少的类权重越大 for class_idx in range(num_classes) ] loss_fn nn.CrossEntropyLoss( weighttorch.tensor(class_weights, dtypetorch.float32).to(device) )这里的权重用的是“样本数倒数归一化到和为类别数”的常见方案。注意CrossEntropyLoss的 weight 需要和标签在同一个设备上所以要.to(device)。加权重之后整体准确率可能只动一两个点但少数类的召回率会明显改善。在实验报告里这种改动用混淆矩阵展示会比单看准确率更有说服力。4. 训练与推理一条龙超参数配置、训练循环与导出结果4.1 优化器选择SGD 在 VGG19 上仍然比 Adam 稳很多人接到新任务第一反应是上 Adam但在 VGG19 这种全连接层占比很大的网络上我的默认选择是带动量的 SGD。两者在实验里的实际表现差异大致如下优化器前期收敛速度最终精度收敛稳定性典型学习率SGD momentum(0.9)较慢更高稳曲线平滑0.001~0.01Adam快通常低 1~2 个点后期容易震荡1e-4~3e-4换到 ResNet 或 transformer 图像分类模型时Adam 的竞争力会上升但在 VGG19 上SGD 的平滑更新轨迹更有利于在损失面上稳定落点。实验追求可解释性和最终验证指标所以我优先选稳。optimizer torch.optim.SGD(vgg19.parameters(), lr0.001, # 初始学习率 momentum0.9, # 动量 weight_decay5e-4) # L2 正则 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, # 每 10 个 epoch gamma0.1) # 学习率乘 0.1参数说明lr0.001 是迁移学习微调的常用起点比从零训练常用的 0.01 小一个数量级因为预训练权重已经是个局部解步子太大会把它踢出去。weight_decay5e-4 对全连接层多的模型几乎必备——100M 参数不加正则小数据集上几个 epoch 就会背下训练集。StepLR 的 step_size 建议设在总 epoch 数的三分之一量级让它至少生效一次不要设成和总 epoch 数相等那样等于没有调度。4.2 训练循环反向传播五步的标准写法迁移学习的训练循环本身不复杂但步骤顺序错一个就全乱。损失函数用交叉熵而不是 MSE 的原因也很直接分类任务里交叉熵对概率分布的梯度更均匀不会像 MSE 那样在输出饱和区出现梯度消失。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() # 关键打开 Dropout更新 BN 统计 running_loss, correct, total 0.0, 0, 0 for images, labels in loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() # 1. 清空上一轮累计梯度 outputs model(images) # 2. 前向传播 loss criterion(outputs, labels) # 3. 计算交叉熵损失 loss.backward() # 4. 反向传播计算梯度 optimizer.step() # 5. 用梯度更新权重 running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return running_loss / total, correct / total逻辑说明model.train()必须在循环前调用它决定 Dropout 是否生效、BN 是否累计统计量。反向传播五部曲里最容易被忽略的是optimizer.zero_grad()的位置——它必须在loss.backward()之前否则每个 batch 的梯度累加到旧梯度上参数更新方向完全错乱。loss.item()取的是标量images.size(0)是当前 batch 的样本数这样算平均 loss 时不会被最后一个不完整 batch 带偏。这里有个新手容易疑惑的点验证时和训练时模型行为不一样。训练模式下 Dropout 会随机抹掉部分神经元BN 用当前 batch 统计量验证模式下要恢复完整网络。所以训练函数第一行是model.train()后面的验证函数第一行必须是model.eval()两者不能搞反。4.3 验证、保存与断点续训别只会存最后一个 epoch一个 epoch 结束后跑一次验证集同时按验证准确率保存最优权重。这里的关键是用“验证集最好”而不是“最后一次训练”作为交付物因为深度学习的最后几个 epoch 往往在局部最优点附近波动。def validate(model, loader, criterion, device): model.eval() # 关闭 Dropout固定 BN 统计量 running_loss, correct, total 0.0, 0, 0 with torch.no_grad(): # 不建计算图省显存 for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return running_loss / total, correct / total best_acc 0.0 for epoch in range(epochs): train_loss, train_acc train_one_epoch(...) val_loss, val_acc validate(...) if val_acc best_acc: # 只在优于历史最优时保存 best_acc val_acc torch.save({ epoch: epoch, model_state_dict: vgg19.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, }, best_model.pth)保存格式用字典而不是单存model.state_dict()的好处是训练中断后可以torch.load(best_model.pth)拿回优化器状态从这个 epoch 接着跑不用重新来前几个 epoch。这个习惯在长训练里省下的时间很可观。还有个细节保存时顺手把epoch和best_acc写进字典以后你想画“不同 epoch 下模型精度”的曲线都有据可查不至于只能靠回忆。4.4 推理导出从 checkpoint 到单张图片的分类结果实验交付通常需要能加载模型、对单张图片输出分类结果的小脚本。下面是最小可用版本import torch import torch.nn.functional as F from PIL import Image import torchvision.models as models from torchvision import transforms def load_model(path, num_classes, device): model models.vgg19(pretrainedFalse) # 不需要预训练权重用实验训好的 model.classifier[6] torch.nn.Linear(4096, num_classes) checkpoint torch.load(path, map_locationdevice) model.load_state_dict(checkpoint[model_state_dict]) return model.to(device) def predict(model, image_path, class_names, device): 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]) ]) img Image.open(image_path).convert(RGB) x tf(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(x) probs F.softmax(logits, dim1)[0] top3_idx torch.topk(probs, kmin(3, len(class_names))).indices.tolist() for idx in top3_idx: print(f{class_names[idx]}: {probs[idx].item():.3f})逻辑说明加载时pretrainedFalse防止再下载权重我们只需要网络结构骨架。checkpoint[model_state_dict]是保存时字典里的键别直接把整个字典塞给load_state_dict。predict里的 transform 必须和训练时的验证集变换完全一致否则输入分布偏移输出概率没有可比性。convert(RGB)是为了处理灰度图和带 alpha 通道的 PNG。输出 top-3 而不是只看 top-1 的用意如果一张图的 top-1 和 top-2 是两个视觉相似的类说明模型在关键细节上仍有混淆这个诊断信息比一个孤立的 top-1 数字有用得多。5. 避坑VGG19 实验最典型的 5 个翻车现场5.1 显存不足第一个 batch 就 OOM现象训练脚本刚跑完第一个 batch控制台直接报CUDA out of memory进程被杀。原因VGG19 的反向传播需要保留每一层中间激活全连接层的参数量大中间张量也大。以 batch32、输入 224×224 计算显存占用常常超过 8GB如果机器只有 6GB 显存OOM 是必然。另一个隐形推手是pin_memoryTrue配合过高 num_workers在数据拷贝阶段额外占用显存。解决优先把 batch 从 32 降到 16 或 8一般就能跑起来。降到 8 仍然 OOM 时检查是不是把优化器、模型、数据全部放到了 CUDA再考虑model.half()混合精度方案。不建议为了硬撑 batch 32 去调torch.cuda.set_per_process_memory_fraction那只是把崩溃延后到下一个 batch没有解决根源。5.2 验证准确率忽高忽低Dropout 和 BN 的模式没切对现象验证集 loss 在 0.5 到 0.8 之间大幅波动同样一套数据和模型两次推理结果不一样。原因验证代码缺少model.eval()。VGG19 的 classifier 里有 Dropouttrain 模式下它会随机失活部分神经元导致每次前向输出不同BN 层在 train 模式下用当前 batch 统计量进一步加剧波动。解决在验证和推理函数进入循环前调用一次model.eval()训练循环第一行写model.train()。这是一个极容易漏掉、影响却很大的细节。排查这类玄学问题时先确认这两个调用是否存在再看with torch.no_grad()是否覆盖了验证最后才去怀疑数据增强。5.3 改图像尺寸后全连接层维度报错现象为了省显存把输入尺寸改成 128×128一 forward 就报错提示size mismatch for classifier.0.weight报错信息里输入维度是 8192而权重维度是 25088。原因卷积层对输入尺寸不敏感但 VGG19 的classifier[0]写死了输入维度 25088它来自 7×7×512。128×128 的输入经过五次 MaxPool 后变成 4×4×5128192和 25088 对不上。解决要么保持 224×224 输入不变要么修改全连接层首层的in_features。我的建议是保输入尺寸、降 batch因为折腾全连接层等于在和预训练结构唱反调预训练优势会被削弱。如果你确实要改尺寸先按 2.4 节 hook 方法打印展平后的实际维度据此修改classifier[0]和后续结构。5.4 训练 loss 在降、准确率却不动类别不均衡在欺骗你现象训练 loss 从 2.0 平滑下降到 0.3但验证准确率长时间停留在 80% 附近怎么调学习率都上不去。原因数据集严重不均衡例如“森林图像分类”里某一类占 85% 的样本。模型把所有输入都判成多数类loss 依然低整体准确率也有表面高分。这是典型的黑匣子现象——指标本身没有告诉你模型正在“偷懒”。解决训练前打印类别分布发现不均衡就按 3.4 节的方法处理。改完类别权重后整体准确率可能只动一两个点但少数类的召回率会明显上来。实验报告里建议同时给出混淆矩阵或每类的 precision/recall不然整体准确率这个数很容易掩盖分类器退化成频率估计器的事实。5.5 小数据集上严重过拟合全连接层在背答案现象训练准确率第 5 个 epoch 冲到 98%验证准确率停在 72%之后每多训一个 epoch验证准确率不升反降。原因VGG19 全连接层约 100M 参数当每类只有几十张图时这个容量足以完整记住训练集标签。迁移学习里“预训练权重好”和“全连接层容量过大”两个事实并存小数据集上后者会很快盖过前者。解决第一选择是换掉前两层全连接改成Linear(25088, 512) → ReLU → Dropout(0.5) → Linear(512, num_classes)把分类头参数量砍掉 95%。第二选择是冻结卷积层只训练分类头把参与更新的参数量降下来。第三选择才是加数据增强比如ColorJitter(brightness0.2, contrast0.2)、RandomRotation(10)。顺序不能反因为增强只是让模型看到更多变换参数量本身过饱和时增强效果很有限。6. 迁移学习两档调参从冻结骨干到全量微调验证策略是否有效6.1 档位 A数据量小时冻结全部卷积层如果每个类的训练样本只有几十到一两百张我的默认策略是冻结特征提取层只训练新加的分类头。代码上只需遍历vgg19.features.parameters()把requires_grad设为 False优化器只接收classifier.parameters()。这个配置下 VGG19 等价于固定特征提取器加线性分类头5~10 个 epoch 就能收敛而且基本不会发散。如果跑完档位 A 验证准确率在 90% 以上就不需要折腾档位 B直接交付即可。6.2 档位 B数据量大时对卷积层做低学习率微调档位 A 效果不佳或每类样本超过三百张就进入微调。常见做法是给features和classifier设置不同学习率避免大步长破坏预训练卷积核optimizer torch.optim.SGD([ {params: vgg19.features.parameters(), lr: 1e-4}, # 卷积层小步调 {params: vgg19.classifier.parameters(), lr: 1e-3}, # 分类头正常学 ], momentum0.9, weight_decay5e-4)参数说明卷积层用 1e-4、分类头用 1e-3这个比例在多数微调场景里是稳的。如果两类用同一个学习率卷积层更新太大会破坏预训练特征提取能力分类头学得太慢又会拖整体后腿。微调总 epoch 控制在 10 到 20 之间超过后验证指标基本进入平台期再跑收益有限。6.3 验证策略有没有效只看前三个 epoch 的 loss 形状判断一个微调方向值不值得继续我只看前三个 epoch验证 loss 在 2.0 到 1.2 区间平滑下降说明方向对、学习率量级合适第一个 epoch 直接降到 0.01 以下然后反弹说明学习率太大预训练特征被冲掉loss 几乎不动说明学习率过小或数据管线有问题。止损比精调更值钱因为一次失败实验的时间成本远比调参动作本身高。VGG19 这个实验在智能计算系统课程里的位置本质上是让你把模型结构、数据管线、超参数三元组的排错能力完整过一遍。换到 ResNet换到 transformer 图像分类模型流程不变加载预训练权重、替换分类头、按验证曲线微调、检查类别均衡。我自己的教训是头三个 epoch 的验证曲线是所有调参决策的前提先看走势再跑长训永远别跳过观察直接熬通宵。这篇笔记把最常踩的坑和标准解法都列了出来希望帮到你从“跑通”进到“跑得好”这一步。本文还有配套的精品资源点击获取