1. 从零上手 mmclassification:自定义数据集训练全流程拆解
做图像分类项目的人,绕不开一个现实问题:公开数据集跑通了,换成自己的数据就各种报错。我最初接触 mmclassification 的时候,也是被它的配置文件体系和数据集注册机制折腾了好一阵。但用熟了之后发现,这套框架在自定义数据集训练这件事上,其实设计得相当规整,只是入门门槛稍微高了一点。
mmclassification 是 OpenMMLab 生态里的图像分类工具箱,基于 PyTorch 构建,核心优势在于模块化设计——骨干网络、颈部、分类头、数据增强策略都可以通过配置文件灵活组合。它内置了大量经典模型(ResNet、VGG、MobileNet、Vision Transformer 等)和训练技巧(Mixup、CutMix、Label Smoothing、Cosine Annealing 等),适合想快速验证想法又不想从零搭训练框架的人。
这篇文章面向两类读者:一是刚接触 mmclassification、想用自己的图片数据跑通训练流程的开发者;二是用过但被配置文件搞晕、想系统理清自定义数据集接入逻辑的从业者。我会从目录结构设计、标注文件生成、配置文件改写、训练启动与调试、常见报错排查几个维度,把整条链路拆开讲清楚,附带可直接复用的代码和配置片段。
2. 自定义数据集接入的整体设计思路
2.1 为什么 mmclassification 的数据集接入让人困惑
mmclassification 的数据集体系继承自 MMDetection 的设计哲学,采用注册器机制加配置文件驱动。好处是扩展性强,坏处是新手不知道数据从哪进来、标签怎么对应、配置文件里哪些字段必须改。
核心逻辑其实就三层:数据集类负责读取标注文件并返回图像路径和标签;数据流水线负责图像加载、增强、格式化;数据加载器负责批处理和并行加速。自定义数据集的工作,本质上就是让这三层都认识你的数据。
2.2 两种主流接入方案的选择
接入自定义数据集有两条路可走。第一条是复用内置的 CustomDataset 类,只需要按照它约定的目录结构和标注格式组织数据,然后写一个继承自该类的子类,指定类别列表即可。第二条是完全自定义 Dataset 类,适合标注格式特殊、需要复杂预处理逻辑的场景。
对于绝大多数图像分类任务,我强烈建议走第一条路。原因很简单:CustomDataset 已经处理好了标注解析、类别映射、图像读取这些通用逻辑,你只需要把数据摆放整齐,代码量极少。只有当你需要从数据库读取图像、或者标注信息嵌在文件名里这类特殊情况,才考虑完全自定义。
2.3 目录结构设计的经验法则
我见过太多人把数据随便丢在一个文件夹里就开始改配置,结果训练时找不到图或者标签对不上。建议从一开始就按标准结构组织:
mydata/ ├── train/ │ ├── cat/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── dog/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── bird/ │ └── ... ├── val/ │ ├── cat/ │ ├── dog/ │ └── bird/ └── test/ └── ...每个类别一个子文件夹,文件夹名就是类别名。这种结构的好处是标注文件可以自动生成,不需要手工写。另外训练集、验证集、测试集分开存放,避免数据泄露。
注意:类别文件夹的命名不要用中文或特殊字符,虽然 Python 能处理,但在某些系统的文件编码下容易出问题。用英文小写加下划线最稳妥。
3. 标注文件生成与数据集类编写实操
3.1 自动生成标注文件的脚本
mmclassification 支持的标注格式有两种:txt 文件和 json 文件。txt 格式每行是“图像路径 标签”的形式,json 格式则包含更丰富的元信息。我习惯用 txt,简单直接。
下面这个脚本可以扫描目录结构自动生成三个标注文件:
import os def generate_annotation(data_root, split, class_names): lines = [] split_dir = os.path.join(data_root, split) for label, cls_name in enumerate(class_names): cls_dir = os.path.join(split_dir, cls_name) if not os.path.isdir(cls_dir): continue for fname in sorted(os.listdir(cls_dir)): if fname.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): rel_path = os.path.join(split, cls_name, fname) lines.append(f"{rel_path} {label}") out_file = os.path.join(data_root, f"{split}.txt") with open(out_file, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) print(f"{split}: {len(lines)} samples written to {out_file}") if __name__ == '__main__': data_root = './mydata' classes = ['cat', 'dog', 'bird'] for split in ['train', 'val', 'test']: generate_annotation(data_root, split, classes)这里有个细节值得说明:路径写的是相对路径,相对于 data_root。这样整个数据集文件夹可以随意移动,只要配置文件里的 data_root 跟着改就行,不用重新生成标注文件。
3.2 自定义 Dataset 类的编写
有了标注文件,接下来写数据集类。继承 CustomDataset,只需要指定 classes 和 data_root:
from mmcls.datasets import CustomDataset from mmcls.registry import DATASETS @DATASETS.register_module() class MyDataset(CustomDataset): CLASSES = ('cat', 'dog', 'bird')就这么几行。CLASSES 元组的顺序必须和标注文件里的标签编号一致,否则训练出来的模型会把猫认成狗。这个类可以放在项目目录下的 datasets 文件夹里,然后在配置文件中通过 custom_imports 引入。
如果你不想新建文件,也可以直接在配置文件中用 type='CustomDataset' 并传入 classes 参数,效果一样:
dataset_type = 'CustomDataset' classes = ['cat', 'dog', 'bird']3.3 数据增强流水线的配置要点
数据流水线决定了图像在送入网络前经历哪些变换。训练和验证阶段的流水线不同:训练阶段需要随机增强来提升泛化能力,验证阶段只需要确定性的预处理。
一个典型的训练流水线配置:
train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='RandomResizedCrop', scale=224), dict(type='RandomFlip', prob=0.5, direction='horizontal'), dict(type='PackInputs'), ]验证流水线:
test_pipeline = [ dict(type='LoadImageFromFile'), dict(type='ResizeEdge', scale=256, edge='short'), dict(type='CenterCrop', crop_size=224), dict(type='PackInputs'), ]RandomResizedCrop 的 scale=224 表示随机裁剪并缩放到 224x224,这是 ImageNet 系列模型的标准输入尺寸。如果你的模型输入尺寸不同,这里要跟着改。ResizeEdge 加 CenterCrop 是验证阶段的标准做法,保证每次评估结果一致。
实操心得:如果数据集较小(每类少于 500 张),建议加上 RandAugment 或 AutoAugment,增强效果比手动堆叠变换更好。但要注意增强过强可能导致欠拟合,需要根据验证集准确率调整。
4. 配置文件改写与训练启动全流程
4.1 配置文件继承机制的理解
mmclassification 的配置文件采用继承机制,基础配置定义了模型结构、训练策略、数据流水线等默认值,你只需要在子配置中覆盖需要改的字段。这种设计避免了重复配置,但也容易让人搞不清最终生效的值是什么。
建议的做法是:先选一个和你的任务最接近的官方配置作为基础,比如 resnet18_8xb32_in1k.py,然后新建一个配置文件继承它,只改数据集相关和类别数相关的字段。
_base_ = [ '../_base_/models/resnet18.py', '../_base_/datasets/imagenet_bs32.py', '../_base_/schedules/imagenet_bs256.py', '../_base_/default_runtime.py', ] data_root = './mydata' classes = ['cat', 'dog', 'bird'] num_classes = len(classes) model = dict( head=dict(num_classes=num_classes), ) train_dataloader = dict( dataset=dict( type='CustomDataset', data_root=data_root, ann_file='train.txt', data_prefix='', classes=classes, pipeline=train_pipeline, ), )这里有几个关键点需要解释。head 里的 num_classes 必须改成你的类别数,否则最后的全连接层输出维度不对,训练直接报错。data_prefix 设为空字符串是因为标注文件里的路径已经包含了 split 目录名,如果标注文件里只有文件名,这里就要设为对应的子目录。
4.2 学习率与批次大小的适配计算
官方配置的默认学习率是针对 8 卡、每卡 32 批次、共 256 总批次设定的。如果你只有单卡、批次大小设为 16,学习率需要按线性缩放规则调整:
新学习率 = 基础学习率 × (新总批次 / 基础总批次)
假设基础学习率是 0.1,基础总批次 256,你的总批次是 16,那么新学习率 = 0.1 × 16 / 256 = 0.00625。这个规则在批次大小变化不大时比较准,如果批次小到 8 以下,建议用更保守的缩放或者配合梯度累积。
optim_wrapper = dict( optimizer=dict(type='SGD', lr=0.00625, momentum=0.9, weight_decay=0.0001), )4.3 训练启动命令与参数说明
配置文件准备好后,用 tools/train.py 启动训练:
python tools/train.py configs/mydataset/resnet18_my.py \ --work-dir work_dirs/resnet18_my \ --seed 42--work-dir 指定日志和权重保存目录,--seed 固定随机种子方便复现。如果要从断点恢复训练,加上 --resume 参数。想用预训练权重加速收敛,在配置文件的 model 里加 init_cfg:
model = dict( backbone=dict( init_cfg=dict( type='Pretrained', checkpoint='https://download.openmmlab.com/mmclassification/v0/resnet/resnet18_8xb32_in1k_20210831-fbbb1da6.pth', ), ), head=dict(num_classes=num_classes), )预训练权重能显著加快小数据集的收敛速度,通常能把训练轮数减少一半以上。
4.4 训练过程监控与日志解读
训练启动后,终端会输出每个 epoch 的损失和准确率。重点关注几个指标:训练损失是否稳定下降、验证准确率是否在提升、两者差距是否过大。如果训练损失降但验证准确率不升,说明过拟合了,需要加数据增强或正则化。如果训练损失震荡严重,可能是学习率太大。
日志文件保存在 work-dir 下的时间戳目录里,可以用 tools/analysis_tools/analyze_logs.py 画曲线图,直观判断训练状态。
5. 常见报错与排查技巧实录
5.1 数据集路径类报错
最常见的报错是 FileNotFoundError,提示找不到图像文件。排查思路:先确认 data_root 和 ann_file 拼接后的路径是否正确,再检查标注文件里的路径是相对路径还是绝对路径。如果标注文件里写的是绝对路径,data_prefix 就不起作用了。
另一个容易忽略的点是路径分隔符。Windows 下生成的标注文件用的是反斜杠,在 Linux 环境读取时会出问题。生成标注文件时统一用 os.path.join 或者正斜杠,能避免跨平台问题。
5.2 类别数不匹配报错
RuntimeError: size mismatch 通常是因为模型输出维度和数据集类别数不一致。检查两处:配置文件中 model.head.num_classes 是否等于实际类别数,数据集类的 CLASSES 长度是否和标注文件中的最大标签加一相等。
5.3 显存不足的处理策略
显存不够时,优先降低批次大小,其次减小输入尺寸。如果降到批次 4 还是不够,可以启用梯度累积:
train_cfg = dict( type='EpochBasedTrainLoop', max_epochs=100, val_begin=1, val_interval=1, ) optim_wrapper = dict( accumulative_counts=4, )accumulative_counts=4 表示每 4 个批次更新一次参数,等效于批次大小扩大 4 倍,但显存占用不变。
5.4 常见问题速查表
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
| FileNotFoundError | 路径拼接错误 | 检查 data_root 和 ann_file |
| size mismatch | 类别数不一致 | 核对 num_classes 和 CLASSES |
| CUDA out of memory | 批次过大 | 降低批次或启用梯度累积 |
| KeyError: 'xxx' | 配置字段名错误 | 对照官方配置检查字段 |
| 准确率不上升 | 学习率不当 | 按线性缩放调整学习率 |
| 损失为 nan | 学习率过大 | 降低学习率或加梯度裁剪 |
避坑技巧:每次改完配置文件,先用 tools/misc/print_config.py 打印最终生效的完整配置,确认所有字段都符合预期再启动训练。这个习惯能省下大量排查时间。
6. 训练调优与模型评估的实战经验
6.1 小数据集的迁移学习策略
自定义数据集通常规模不大,从零训练很难收敛。我的做法是分阶段训练:先冻结骨干网络,只训练分类头 5 到 10 个 epoch,让分类头适应新类别;然后解冻全部参数,用较小的学习率微调。这样比直接端到端训练稳定得多。
冻结骨干网络的配置方式:
model = dict( backbone=dict( frozen_stages=4, init_cfg=dict(type='Pretrained', checkpoint='...'), ), )frozen_stages=4 表示冻结前四个 stage,ResNet18 一共四个 stage,相当于整个骨干都冻住了。
6.2 评估指标的选择与解读
mmclassification 默认用 Top-1 和 Top-5 准确率。对于类别数少于 10 的任务,Top-5 意义不大,重点看 Top-1。如果类别不平衡,还要关注每类的召回率,避免模型偏向多数类。
评估命令:
python tools/test.py configs/mydataset/resnet18_my.py \ work_dirs/resnet18_my/best_top1_acc_epoch_50.pth \ --metrics accuracy6.3 模型导出与部署准备
训练完成后,可以把模型导出为 ONNX 格式方便部署:
python tools/deployment/pytorch2onnx.py \ configs/mydataset/resnet18_my.py \ work_dirs/resnet18_my/best_top1_acc_epoch_50.pth \ --output-file resnet18_my.onnx \ --shape 1 3 224 224导出时注意输入 shape 要和训练时一致,否则推理结果会偏差很大。
我个人在实际操作中的体会是,mmclassification 的自定义数据集接入最难的不是写代码,而是理解它的配置继承逻辑和数据流走向。一旦把数据从标注文件到数据加载器这条链路理清楚,后面换模型、换增强策略都是改几行配置的事。另外强烈建议第一次跑通时用少量数据快速验证流程,确认无误再上全量数据,能避免很多无效等待。