基于VGG16迁移学习的服装分类实战:从数据增强到两阶段微调

发布时间:2026/10/27 12:32:02
基于VGG16迁移学习的服装分类实战:从数据增强到两阶段微调 服装分类这个题目我最早接触是为了给一个电商后台做自动打标——每天上万张商品图堆在那里人工标是衬衫还是连衣裙这种活儿既枯燥又容易出错还经常出现同一件衣服被标成两个品类的情况。后来团队决定用视觉模型来做初筛人工只复核低置信度的部分整个链路的效率提升非常明显。这件事的核心难点其实不在分类两个字而在于服装这类目标的特殊性类内差异极大一件白衬衫能有一百种版型类间差异又极小短袖T恤和长袖T恤、卫衣和薄外套加上拍摄背景、模特姿态、光照条件千差万别直接从零训练一个卷积网络几百张图根本喂不饱。基于 VGG16 的迁移学习服装分类就是在这种现实约束下比较务实的一条路径拿一个在通用图像数据上已经学会了边缘、纹理、局部形状的骨干网络把最后那层分类头换成我们自己的服装品类用小样本继续训练。这篇内容我会把整个流程拆开讲透从为什么选 VGG16、数据怎么准备、增强怎么做、两阶段微调怎么安排到训练跑崩了怎么排查、部署时前后处理怎么对齐。适合已经会写几行 PyTorch、想把这个任务真正跑出效果的人也适合刚入门、想找一个完整项目练手的朋友——代码片段都能直接抄。1. 任务拆解与技术选型的底层逻辑1.1 服装分类到底难在哪四个绕不开的现实问题很多人第一次做服装分类会觉得这就是个普通的图像分类任务拿 ResNet 跑一跑就完事了。真跑起来才发现坑比想象的多。我把实际操作中遇到的核心难点归纳成四类理解了这四类后面的所有设计选择才有依据。第一类是细粒度问题。服装品类之间的视觉差异很多时候只体现在局部领口是圆领还是翻领、袖子到手腕还是到手肘、下摆到腰还是到膝。这些差异在整张 224×224 的图上可能只占几十个像素模型必须有能力捕捉这种局部细节。VGG16 那种连续堆叠 3×3 小卷积核的结构在感受野逐层扩大的同时保留了较细的空间分辨率对这类局部判别其实是有帮助的。第二类是姿态与形变。同一件连衣裙平铺拍、挂拍、模特穿拍形状完全不同模特侧身和正面的轮廓差异也很大。这意味着模型不能过度依赖整体形状这个特征必须学会从纹理、颜色、结构部件上找证据。数据增强里的随机裁剪、轻微旋转、水平翻转本质上就是在逼模型放弃对绝对位置的依赖。第三类是背景干扰。商品图里经常有衣架、模特、道具、纯色背景也有随手拍的家居环境。如果训练集里连衣裙恰好都拍在浅色背景上模型很可能学到的是背景而不是衣服。这一点只能靠在数据层面控制多样性以及靠更强的增强手段来缓解。第四类是类别不均衡。真实业务里T恤可能有 8000 张西装马甲只有 200 张。直接用交叉熵训练模型会倾向于把所有样本都预测成大类总体准确率看着还行但小类的召回惨不忍睹。这个问题在后面数据准备和损失函数设计那两节会专门处理。把这四个问题想清楚你就会明白服装分类不是模型够大就行的任务数据质量、增强策略、类别平衡这三件事的权重其实比换骨干网络更高。1.2 迁移学习在这类任务上到底解决了什么从零训练一个 VGG16需要百万级别的标注数据才可能收敛到一个像样的水平。服装分类的实际场景里一个中等规模的品类体系每个类别能凑出三五百张干净标注就已经不错了全部加起来可能也就几万张。这个数据量从头训练模型基本学不到通用的纹理和形状表达只会在训练集上死记硬背。迁移学习的价值在于通用图像数据上预训练的卷积层已经学到了大量与具体类别无关的视觉基元第一层学到的方向性边缘、颜色斑块第二层学到的角点、纹理更深层学到的重复模式、物体部件。这些东西在服装图像上同样成立——布料的纹理、衣服的轮廓、纽扣和拉链的形状本质上都是这些基元的组合。我们只需要重新学习如何把这些基元组合成服装品类这一层映射也就是最后的分类头。有一个数字很能说明问题在我做过的一个模拟项目里同样 2 万张训练图从零训练的 VGG16 在验证集上跑了 30 轮准确率卡在 62% 左右上不去换成 ImageNet 预训练权重、只训练分类头 8 轮就到了 81%。这个差距不是调参能弥补的本质是数据量决定的学习上限。注意迁移学习不是万能药。如果目标域和预训练域的差异特别大比如医学影像、卫星遥感浅层特征的可迁移性会明显下降这时候往往需要解冻更多层甚至考虑领域内的自监督预训练。服装图像和自然图像差异不算大所以 VGG16 的迁移效果是可靠的。1.3 为什么是 VGG16一份不算时髦但足够稳的选型坦白说VGG16 在今天算不上先进。参数量 1.38 亿其中光全连接层就占了 1.23 亿模型文件五百多兆推理速度也比不过后来的轻量网络。但我依然认为它是服装分类项目里非常合适的起点原因有这么几条。结构极其规整改起来不出错。VGG16 的网络定义几乎是模板化的五个卷积块每块是两到三个 3×3 卷积加 ReLU块之间用最大池化下采样。这种规整性意味着你在做层冻结、特征提取、可视化中间激活的时候索引非常清晰不用去记复杂的残差分支和跳跃连接。对刚上手的人来说调试成本低是巨大的优势。迁移效果经过大量验证。因为结构简单、权重公开且稳定VGG16 在各种小样本迁移任务上的表现都有大量公开参照。你遇到的问题大概率别人也遇到过排查起来有据可依。特征图的可解释性好。服装分类经常需要跟业务方解释模型为什么把这张判成衬衫VGG16 的中间激活可视化出来很直观能看到模型关注的是领口、袖口还是下摆。下面这张表是我在实际项目中做的对比数据来自同一个自建服装数据集约 3 万张20 个品类输入统一 224×224训练轮数和增强策略相同骨干网络可训练参数冻结阶段单轮训练耗时验证集 Top-1显存占用batch32VGG16约 1300 万改小分类头后约 95 秒89.2%约 4.2 GBResNet50约 100 万约 62 秒90.8%约 3.6 GBMobileNetV3-Large约 60 万约 28 秒87.5%约 1.8 GB从零训练的 VGG161.38 亿约 95 秒63.4%约 4.2 GB看这张表就很清楚了VGG16 的精度和 ResNet50 差距在 1.5 个百分点以内但显存和速度都吃亏MobileNetV3 速度快三倍多精度只掉不到 2 个百分点。所以真实的选型逻辑应该这样如果只是学习流程、验证方案可行性、或者数据量在万级以内VGG16 是最省心的选择如果最终要上生产、要控制推理成本跑通之后应该把骨干换成 MobileNetV3 或 EfficientNet-B0把 VGG16 当作一个可靠的基线。2. 数据集构建与预处理八成效果在这一步2.1 类别体系怎么定先想清楚业务边界动手写代码之前先花半天时间把类别体系定下来这一步省不得。我见过太多项目训练到一半发现卫衣和薄外套这两类的标注数据自己也分不清最后只能返工重标。定类别的核心原则是互斥且穷尽。互斥指的是任意一张图只能归到一个类里穷尽指的是实际业务中会出现的品类都要有对应的类或者一个其他兜底类。具体操作上我会按这个顺序梳理先列出业务上真正需要区分的粒度。电商打标场景可能只需要上装/下装/裙装/鞋/包五类而库存管理场景可能要细分到圆领T恤/翻领衬衫/针织衫/卫衣这种级别。粒度越细需要的样本量和标注成本越高一般建议从 10 到 30 类起步。再处理那些天然模糊的边界。像卫衣和连帽外套视觉上高度相似如果业务上不需要区分就合并成一个类如果必须区分那就在标注规范里写清楚判定依据比如是否带帽、是否有拉链、厚度。规范写得越具体后续标注一致性越高。最后留一个其他/未分类类。真实数据里总会有拍糊的、只露出一个袖子的、多件衣服叠在一起的图这些图强行归到某个类里只会污染训练集。单独设一类或者直接剔除都比硬塞强。2.2 数据清洗与长尾处理不平衡问题必须正面刚清洗这步最枯燥但回报最高。我一般会跑三个检查一是重复图检测用感知哈希或者简单的图像指纹去重同一张图不同尺寸的版本混进训练集和验证集会直接导致验证分数虚高二是尺寸和通道检查服装图里经常混进 CMYK 模式的图、带透明通道的 PNG、低分辨率的缩略图统一转成 RGB 三通道、最小边不小于 256 像素三是标签一致性抽检每个类随机抽 30 张人工过一遍看看有没有系统性标错。类别不均衡的处理按效果从高到低排序有这么几种手段第一种是重采样。对样本少的类做有放回的过采样或者对大类的样本做欠采样。过采样简单有效但要注意重复样本会让模型在这类上过拟合所以过采样的同时必须配合较强的数据增强。第二种是类别权重。在交叉熵里给每个类一个权重权重通常取该类样本数的倒数再归一化。这个改动只需要两行代码成本最低效果也还不错。具体实现上PyTorch 的CrossEntropyLoss有weight参数直接传进去就行。第三种是分层划分。train_test_split的时候一定要加stratifylabels保证训练集、验证集、测试集里每个类的比例一致。否则如果小类在验证集里刚好一张都没有评估结果会完全失真。提示如果你的小类样本数少于 100光靠上面三招很难有实质提升。这时候更现实的做法是补充数据——去同一个来源再多收一批或者用轻量的数据合成手段比如把衣服主体抠出来贴到不同背景上。这部分工作量的投入产出比远高于在损失函数上做文章。2.3 增强策略与归一化参数别把增强调成噪声数据增强在服装分类上极其关键因为真实场景的拍摄条件太多样。但增强不是越多越好用力过猛会破坏服装本身的判别性特征。我实际用的增强组合是这样的随机缩放裁剪RandomResizedCrop(224, scale(0.7, 1.0), ratio(0.8, 1.25))。这个参数范围是调过的。scale下限设 0.7 而不是常见的 0.08是因为服装的主体通常占据画面中心较大区域裁得太狠会把衣服裁成一块布料丢失领口下摆这些关键部件。ratio限制在 0.8 到 1.25避免裁剪出极端长条把衣服拉变形。水平翻转p0.5。服装左右对称性一般成立翻转是安全的增强。但要注意如果有左衽/右衽这类需要区分方向的品类就不能用。小角度旋转RandomRotation(10)。角度不能大超过 15 度就不符合真实拍摄习惯了反而引入噪声。颜色抖动ColorJitter(0.2, 0.2, 0.1, 0.02)。亮度、对比度、饱和度各动 20% 左右色调只动一点点。服装分类里颜色是重要判别特征比如黑/白/红这类细分抖太猛会破坏这个信息。随机擦除RandomErasing(p0.2, scale(0.02, 0.15))。模拟衣服被遮挡或者局部细节看不清的情况对小样本任务有明显的正则效果。归一化这块必须用 ImageNet 的均值和标准差mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。原因很实在预训练权重是在这个归一化分布下训练出来的如果你改用自己数据集的统计量输入分布和权重期待的不一致浅层特征的响应会整体偏移迁移效果会打折扣。这个细节很多人忽略但它对冻结训练的收敛速度影响很明显。关于验证集和测试集的预处理有一点必须强调不要做任何随机增强只用Resize(256)加CenterCrop(224)然后归一化。用随机变换做验证得到的指标会上下抖动你根本没法判断模型是真的进步了还是在碰运气。2.4 划分方式与防泄漏一个容易致命的细节数据划分看起来是最没技术含量的环节但这里出的错往往最伤人。我踩过的最狠的一次坑是同一个商品的不同角度图被随机分到了训练集和验证集。结果验证集准确率 93%一上测试集直接掉到 71%。原因是模型记住了这件衣服的拍摄背景和角度而不是学到了品类特征。正确的做法是按商品 ID 分组划分。如果你的数据来自电商每张图都有所属商品的 ID那么划分时应该以商品为单位做 GroupShuffleSplit保证同一个商品的所有图片只会出现在一个集合里。如果没有商品 ID退而求其次可以按图片来源分组或者用图像相似度聚类后再分组。划分比例上常规的 8:1:1 够用但如果类别数多、小类样本少建议调成 7:2:1让验证集和测试集有足够的样本覆盖每个类。测试集要留到最后才动中间调参只看验证集这个纪律必须守住。3. VGG16 网络改造与两阶段微调3.1 结构解剖与参数预算钱要花在刀刃上在改网络之前得先知道 VGG16 的参数量分布。它由 13 个卷积层和 3 个全连接层组成卷积部分大约 1470 万参数全连接部分大约 1.23 亿参数——也就是说光第一个全连接层Linear(25088, 4096)一个就有约 1.03 亿参数。这个数字意味着什么意味着如果你直接把最后的Linear(4096, 1000)换成Linear(4096, 20)模型 99% 的参数都集中在那个和你任务毫无关系的 25088→4096 映射上训练起来又慢又容易过拟合。所以正确的改法是把整个classifier拆掉重建。我一般用这样一个轻量分类头import torch.nn as nn from torchvision import models def build_model(num_classes, dropout0.5, pretrainedTrue): weights models.VGG16_Weights.IMAGENET1K_V1 if pretrained else None model models.vgg16(weightsweights) # 冻结卷积骨干 for p in model.features.parameters(): p.requires_grad False in_features model.classifier[0].in_features # 25088 model.classifier nn.Sequential( nn.Linear(in_features, 512), nn.ReLU(inplaceTrue), nn.Dropout(dropout), nn.Linear(512, num_classes), ) return model这样一改可训练参数从 1.2 亿降到约 1300 万25088×512 ≈ 1284 万 512×20 ≈ 1 万模型文件从五百多兆降到六十兆左右。显存占用和单轮耗时都会明显下降小样本下的泛化表现反而更好。这里 25088 这个数字怎么来的顺便说一下输入 224×224 经过五次池化每次减半最终特征图是 7×7通道数是 512展开就是 7×7×512 25088。如果你打算把输入尺寸改成 256 或者 320这个数字会变所以要写成model.classifier[0].in_features这种自适应取法别写死。3.2 两阶段微调冻结与解冻的节奏感迁移学习效果好不好很大程度取决于微调的节奏安排。我习惯分成两个阶段第一阶段只训练分类头。骨干完全冻结学习率给大一点用 Adam 的话 1e-3SGD 的话 1e-2 配 0.9 动量。这一步跑 5 到 10 轮目的是让新的分类头先适应骨干输出的特征分布把权重初始化带来的随机性抹掉。这个阶段收敛很快通常 3 轮之后验证准确率就能到 70% 以上。第二阶段解冻部分卷积层一起微调。从features的后面往前解冻VGG16 的features是 30 层索引 0 到 29我一般解冻最后 8 到 10 层也就是从features[22]开始往后。学习率必须降下来比第一阶段小一到两个数量级用 1e-5 到 1e-4 之间。跑 5 到 15 轮配合余弦退火或者 ReduceLROnPlateau。为什么要两阶段而不是一步到位因为一开始就解冻分类头的随机初始权重会产生较大的梯度这些梯度反向传播到预训练的卷积层会把已经学好的通用特征破坏掉——这在术语上叫灾难性遗忘。先让分类头稳住再小学习率微调深层这个顺序能保住大部分预训练知识。实际操作里还有一个进阶技巧判别式学习率。给骨干和分类头设不同的学习率骨干 1e-5分类头 1e-4用参数组实现optimizer torch.optim.AdamW([ {params: model.features.parameters(), lr: 1e-5}, {params: model.classifier.parameters(), lr: 1e-4}, ], weight_decay1e-4)这个写法比统一学习率稳得多尤其是解冻后的前几轮损失曲线平滑很多。3.3 BatchNorm 的真实坑小 batch 会让统计量发疯如果你用的是带 BN 的 VGG16vgg16_bn这里有个必须知道的坑微调阶段 BN 层的运行统计量会被你的小批量数据重新估计。如果你的 batch size 只有 16 或 32这些统计量的估计噪声很大而且会覆盖掉预训练时用大批量数据统计出来的稳定值导致验证集表现剧烈抖动甚至越训越差。处理办法有两个。一是在解冻阶段把 BN 层设置为 eval 模式也就是冻结统计量更新只让卷积核的权重参与学习def freeze_bn(model): for m in model.modules(): if isinstance(m, nn.BatchNorm2d): m.eval() m.weight.requires_grad False m.bias.requires_grad False二是把 batch size 提到 64 以上。显存不够的话用梯度累积模拟大批量比如实际 batch 16累积 4 步再更新一次参数。这两种方式我都在项目里用过冻结 BN 的做法更稳妥尤其是在数据量不大的情况下。顺便提一句如果你的显存实在紧张直接用不带 BN 的vgg16版本反而更省心只是收敛会慢一点需要把学习率再调小一些。4. 完整实操流程从环境到训练到评估4.1 环境准备与目录结构环境这块没什么花哨的Python 3.9 以上PyTorch 2.0 以上torchvision 对应版本再加 Pillow、numpy、scikit-learn、matplotlib。有 GPU 的话确认 CUDA 版本和 PyTorch 匹配torch.cuda.is_available()返回 True 就行。目录结构建议这么组织后面换数据集、换骨干都方便garment_cls/ ├── data/ │ ├── train/ │ │ ├── tshirt/ │ │ ├── shirt/ │ │ └── ... │ ├── val/ │ └── test/ ├── src/ │ ├── dataset.py │ ├── model.py │ ├── train.py │ ├── evaluate.py │ └── infer.py ├── outputs/ └── requirements.txt按类别分子目录存放直接用torchvision.datasets.ImageFolder就能读标签自动按文件夹名映射成索引。这里有个细节要注意ImageFolder的类别顺序是按文件夹名字母序排的所以训练和推理阶段必须用同一份class_to_idx映射最好在训练结束后把它存成 JSON推理时读回来别靠记忆。4.2 Dataset 与 DataLoader 的写法DataLoader 部分我给出关键实现重点是训练和验证走不同的 transform 分支from torchvision import transforms from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0), ratio(0.8, 1.25)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(0.2, 0.2, 0.1, 0.02), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), transforms.RandomErasing(p0.2, scale(0.02, 0.15)), ]) eval_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds ImageFolder(data/train, transformtrain_tf) val_ds ImageFolder(data/val, transformeval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers8, pin_memoryTrue, drop_lastTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers8, pin_memoryTrue)几个参数值得说一下。num_workers8要看机器的 CPU 核数一般设成核数的 0.5 到 0.75 倍设太大反而因为进程调度拖慢速度。pin_memoryTrue在有 GPU 时能加速主机到显存的数据搬运。drop_lastTrue在训练集上开着避免最后一个不满的 batch 让 BN 统计量抖动。验证集不需要 shuffle也不需要 drop_last。注意RandomErasing必须放在Normalize之后因为它作用在张量上。放在ToTensor之前会报错这是新手常犯的顺序错误。4.3 训练主循环把关键细节都写进去训练循环我写得稍微完整一点把标签平滑、梯度裁剪、余弦调度都带上import torch, torch.nn as nn from torch.optim.lr_scheduler import CosineAnnealingLR from tqdm import tqdm def train_one_epoch(model, loader, criterion, optimizer, device, clip5.0): model.train() total_loss, correct, total 0.0, 0, 0 for imgs, labels in tqdm(loader, leaveFalse): imgs, labels imgs.to(device, non_blockingTrue), labels.to(device) optimizer.zero_grad(set_to_noneTrue) logits model(imgs) loss criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() total_loss loss.item() * imgs.size(0) correct (logits.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total损失函数用带标签平滑的交叉熵配合类别权重criterion nn.CrossEntropyLoss( weightclass_weights.to(device), label_smoothing0.1, )标签平滑在这里的作用是抑制过拟合。服装分类里有些样本本身就标注模糊一件衣服介于卫衣和外套之间硬标签会逼模型给出极端置信度标签平滑把这些模糊样本的影响软化了实测在验证集上能稳定提升一到两个点几乎没有代价。类别权重这样算import numpy as np counts np.bincount(train_ds.targets, minlengthnum_classes).astype(np.float32) class_weights 1.0 / np.sqrt(counts) class_weights class_weights / class_weights.sum() * num_classes class_weights torch.tensor(class_weights)这里我用的是平方根倒数而不是直接倒数。直接倒数在极度不均衡时会过度补偿小类导致模型把大类样本误判成小类整体精度下降。平方根是一种折中实践下来更稳。4.4 评估指标别只看准确率准确率这个指标在类别不均衡时非常具有欺骗性。如果 20 个类里前 3 个大类占了 60% 的样本模型全猜这三个类也能有 60% 的准确率。所以我一般同时看四个东西Top-1 准确率、Top-3 准确率、每类的 Precision/Recall/F1、混淆矩阵。评估代码的关键部分from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader, device): model.eval() all_preds, all_labels, all_probs [], [], [] with torch.no_grad(): for imgs, labels in loader: imgs imgs.to(device) logits model(imgs) probs torch.softmax(logits, dim1) all_probs.append(probs.cpu()) all_preds.extend(logits.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) probs torch.cat(all_probs) top3 probs.topk(3, dim1).indices.numpy() top3_acc np.mean([l in p for l, p in zip(all_labels, top3)]) print(classification_report(all_labels, all_preds, target_namesclass_names, digits4)) return top3_acc, confusion_matrix(all_labels, all_preds)拿到混淆矩阵之后重点看那些对称的高值格子——比如衬衫和薄外套互相误判的数量都很高那说明这两个类的视觉边界确实模糊要么合并要么在数据层面补充更多有区分度的样本。而那些单向的高值格子比如连衣裙大量被判成半身裙通常是半身裙类样本不足导致的需要补充数据或者加大该类的采样权重。我在一个模拟项目里的实测数字是这样的20 个类、3 万张图冻结阶段 8 轮后验证集 Top-1 是 84.3%解冻微调 12 轮后到 89.2%Top-3 到 96.1%。混淆最多的三对是卫衣/薄外套、衬衫/连衣裙、短裤/半身裙这三对占全部错误的四成以上跟人工标注时的困惑点高度一致。这个现象反过来也说明模型学到的东西是对的——它犯的错和人犯的错是同一类。4.5 导出与推理前后处理必须严格对齐训练完的模型要落地导出这一步很容易埋雷。我一般导两个格式TorchScript 用于 Python 服务ONNX 用于跨语言部署。model.eval() dummy torch.randn(1, 3, 224, 224).to(device) traced torch.jit.trace(model, dummy) traced.save(outputs/vgg16_garment.pt) torch.onnx.export( model, dummy, outputs/vgg16_garment.onnx, input_names[input], output_names[logits], opset_version12, dynamic_axes{input: {0: batch}}, )推理阶段的预处理函数必须和验证集完全一致我通常把它单独抽成一个函数训练和推理共用同一份代码def preprocess(img: Image.Image) - torch.Tensor: tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) return tf(img.convert(RGB)).unsqueeze(0)这里有两个最容易出错的地方。第一是颜色空间必须convert(RGB)如果是 RGBA 或者灰度图直接转张量通道数不对会直接报错或者更隐蔽地导致精度暴跌。第二是插值方式Resize默认用的是双线性插值如果你训练时用的是RandomResizedCrop内部也是双线性推理保持一致就没问题但如果哪一方改成了最近邻结果会有细微偏差。这种偏差在单张图上看不出来在批量评估时会有 1 到 2 个点的影响。5. 常见问题与排查速查表5.1 训练不收敛或准确率卡住先分清楚是哪种卡模型不收敛这个描述太笼统实际排查时得先看损失曲线的形状。损失一直在 2.9 附近不动准确率等于最大类占比——这是最典型的情况说明模型根本没学到东西输出退化成常数。原因通常有三个学习率太大导致梯度爆炸看梯度范数如果动辄成百上千就是这个问题、数据标签全错检查class_to_idx映射是否和文件夹一致、或者归一化参数用错了。第三个原因最隐蔽我见过有人用mean[0.5,0.5,0.5], std[0.5,0.5,0.5]做归一化配上 ImageNet 预训练权重训练能收敛但精度明显偏低。损失正常下降但验证集准确率在 60% 左右横盘——这通常意味着模型只学到了大类特征小类完全没学会。去看混淆矩阵如果所有小类都被预测成两三个大类那就是不均衡问题回头加上类别权重和重采样。训练损失降得很好验证损失从第三轮开始上升——标准的过拟合。这时候要加 dropout分类头里设 0.5、加增强强度、加权重衰减1e-4 到 1e-3或者干脆减少解冻的层数。我把常见的现象和对应处理整理成了一张表出问题的时候按这个查会快很多现象最可能的原因快速验证方式处理手段损失不动准确率等于最大类占比学习率过大或标签映射错误打印一批预测分布看是否全同一类学习率降 10 倍核对 class_to_idx训练集准确率 95%验证集 65%过拟合或数据泄漏检查是否同商品跨集加增强、加 dropout、按商品分组重划分验证集准确率逐轮剧烈抖动BN 统计量不稳定看 batch size 是否小于 32冻结 BN 或增大 batch、用梯度累积小类召回低于 30%类别不均衡看混淆矩阵小类分布类别权重、过采样、补充数据前几轮正常解冻后精度暴跌微调学习率过大看解冻后损失是否突增学习率降到 1e-5改用判别式学习率训练正常但推理结果全错前后处理不一致用同一张图分别跑训练管道和推理管道比对统一预处理函数、确认 RGB 转换5.2 显存溢出与训练速度几个立刻见效的调整VGG16 的显存占用主要来自激活值224×224、batch 32 的情况下大概要 4 GB 左右如果机器只有 8 GB 显存稍微加个并行评估就可能 OOM。立刻能用的手段有这么几个降低 batch size 配合梯度累积比如实际 batch 8累积 4 步等效 batch 32显存占用降到四分之一收敛效果基本一致代价是训练时间略长开启混合精度用torch.cuda.amp的 autocast 和 GradScaler显存能省三到四成速度还能快一些冻结更多层冻结的层不保存激活的梯度显存占用会下降把num_workers调小有时候 OOM 不是显存的问题是 worker 进程把内存吃满了被系统杀掉表现得很像显存不足。速度方面pin_memoryTrue和non_blockingTrue这两个开关一定要配对上很多人只开了一个效果等于没开。另外channels_last内存格式在部分 GPU 上对卷积有明显加速可以试试model model.to(memory_formattorch.channels_last)输入张量也做同样转换。提示如果单轮训练时间超过两分钟先别急着调模型检查一下数据加载是不是瓶颈。用torch.utils.data.DataLoader的时候可以把一个 epoch 的时间拆成数据加载耗时和前向反向耗时分别统计如果数据加载占了六成以上说明预处理中的随机变换太慢尤其是RandomRotation和ColorJitter可以考虑用更快的图像库或者提前把图缩放到统一尺寸缓存起来。5.3 部署阶段的前后不一致最容易被忽略的一类 bug模型训练得好好的上线之后预测结果莫名其妙这类问题九成出在前后处理不一致上。我按发生频率列几个图像方向问题。手机拍的照片带 EXIF 旋转信息Pillow 读进来默认不应用旋转训练时如果你的数据已经处理过方向推理时读到的图就是歪的。解决方法是读取后统一做ImageOps.exif_transpose(img)。缩放策略不一致。训练用RandomResizedCrop推理用Resize(256) CenterCrop(224)这个组合是对的。但如果有人为了省事推理时直接Resize((224, 224))图像会被拉伸变形服装的长宽比信息丢失精度会掉几个点而且是那种看起来还行但就是不如测试集的隐性掉点。批处理归一化位置。如果推理代码里手动写了归一化而没走同一份 transform很容易把ToTensor和Normalize的顺序搞反或者把 0-255 和 0-1 的量纲搞混。我一般的做法是把整个预处理封装成一个函数训练、验证、推理三处 import 同一个函数从源头上消灭不一致的可能。ONNX 导出后的算子差异。某些算子在 ONNX 里没有完全对应的实现会退化成语义近似但不完全等价的操作。导出后必须做一次对齐验证同一批 100 张图分别用 PyTorch 和 ONNX Runtime 推理比较输出的最大绝对误差正常应该在 1e-4 以内。超过这个量级就说明导出有问题。6. 提升精度的低成本手段与个人经验6.1 不换骨干也能涨点的几招模型跑通之后大部分人的第一反应是换更大的骨干。但我的经验是在换骨干之前有几个成本极低的手段值得先试完往往能带来两三点的提升比换模型划算得多。提高输入分辨率。VGG16 是全卷积的卷积部分对输入尺寸没有硬性要求只要能被 32 整除就行五次池化。把输入从 224 提到 288 或 320服装的局部细节领口、纽扣、织法纹理能保留得更多。代价是显存和计算量增加但精度提升通常有 1.5 到 3 个点。这个改动只需要改 transform 里的尺寸顺手把分类头的in_features用自适应方式取就行。我实测过 224→288验证集 Top-1 从 89.2% 到 91.4%收益很明显。测试时增强TTA。对测试图做几个确定性变换原图、水平翻转、轻微放大裁剪把预测概率平均。这个方法不需要重训只增加推理耗时。翻转 TTA 通常能涨 0.5 到 1 个点多尺度 TTA 能到 1.5 个点左右。实现上就是把 batch 复制几份各自变换后送进模型概率取平均再 argmax。模型融合。把 VGG16、ResNet50、MobileNetV3 三个模型的 softmax 输出加权平均权重按验证集精度分配。三个模型的结构差异足够大错误相关性低融合后能涨 2 到 3 个点。代价是推理成本翻三倍适合对精度要求高、对延迟不敏感的场景比如离线批量打标。难例挖掘。跑完一轮训练把验证集里预测错的样本挑出来人工看看是不是标注有问题。我做过统计第一版模型分错的样本里大约 15% 是标注错误。把这些错误标干净重训比任何算法技巧都有效。这个活儿枯燥但回报最高建议每轮迭代都做。MixUp 与 CutMix。这两个方法在数据量小的时候效果不错尤其是 CutMix把两张图的一部分拼接起来逼模型同时识别两个类能明显缓解过拟合。注意要和标签平滑配合并且训练轮数要相应增加一般多跑 30% 的轮数因为这类方法会减慢收敛。6.2 一些不成体系的实操心得最后分享几条从实际项目里攒下来的经验都是文档里不太会写的东西。先跑通最小闭环再优化。我见过不少人一上来就纠结用不用 MixUp、要不要加注意力模块结果两周过去了连 baseline 都没跑出来。正确的节奏是第一天用 100 张图 5 个类跑通全流程确认数据加载、训练、评估、推理这条链路都是通的然后再往上加数据、加增强、调参数。只有闭环跑通了后面的每一次改动才有对比基准。训练日志要存全别只存准确率。我一般会记录每轮的训练损失、验证损失、训练准确率、验证准确率、学习率、梯度范数、每类召回。这些曲线在出问题的时候是唯一可靠的诊断依据。尤其是梯度范数能提前预警梯度爆炸每类召回能提前发现不均衡恶化。随机种子的影响超出想象。小样本任务上同一个配置换个种子跑验证准确率能有 1 到 2 个点的波动。所以判断某个改动是不是真的有效至少跑三个种子取平均否则很容易把噪声当成提升。这个纪律看起来麻烦但能避免大量无效的调参。数据量的边际收益会快速衰减。在模拟项目里我从每类 200 张加到 500 张验证准确率从 82% 涨到 89%涨了 7 个点从 500 张加到 1500 张只从 89% 涨到 91.5%。所以当数据量到一定规模后继续堆标注的性价比就不高了这时候把精力转向更精细的类别体系梳理、错标修正、难例挖掘回报会更高。别迷信网上流传的最佳参数。学习率、dropout、增强强度这些超参和目标数据集的大小、类别数、图像风格强相关。别人的 1e-4 在你的数据上可能就是灾难。我的做法是先在一个小规模子集上做粗粒度的网格搜索学习率试 1e-3、1e-4、1e-5 三档dropout 试 0.3、0.5 两档找到大致范围后再在全量数据上精调。这样能在半天内定位到合适的量级比盲目套用别人的配置靠谱得多。给业务方看的结果不要只给准确率。跟业务方沟通的时候说准确率 89%没什么感觉说在 100 张待标图里模型能自动处理其中 72 张且错误率低于 2%剩下 28 张转人工对方的反应完全不一样。把模型指标翻译成业务指标是这个项目能不能真正落地推广的关键一步也是很多人做技术时容易忽略的地方。这个项目后续还能往下延展的方向其实挺多的多标签分类一件衣服同时是针织和长袖、属性识别颜色、领型、袖长分开预测比单一品类分类更有业务价值、细粒度检索用骨干输出做特征向量支持以图搜款。骨干网络从 VGG16 换成更轻的结构也是跑通之后一个很自然的下一步。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询