从零训练 vs 预训练微调:TinyNeXt 小数据集图像分类实战

📅 2026/8/12 23:15:02
从零训练 vs 预训练微调:TinyNeXt 小数据集图像分类实战
一份基于真实运行记录的对比教程在只有1000 多张图片的小数据集上用 TinyNeXt 做二分类。同一套代码、同一份数据分别走从零训练与预训练微调两条路线对比精度、稳定性与所需训练时长。全文无私有信息命令与脚本可直接复制到自己的数据集上复用。1. 为什么小数据集要纠结训练还是微调图像分类模型参数量通常很大。以小而轻出名的 TinyNeXt-T 也有约 100 万参数而我们只有1062 张训练图——平均每张图要养活近 1000 个参数。这种情况下从零训练网络必须从随机权重出发仅靠几千张图去学边缘、纹理、形状→类别的完整映射极易过拟合或收敛停滞预训练微调直接继承在 ImageNet 百万图上学到的通用视觉特征只需要在少量数据上微调分类头数据需求大幅降低。这篇教程就用同一份数据实测两条路线用数据说话。2. 克隆代码与整体实操流程2.1 克隆官方仓库TinyNeXt 是 ICCV 2025 论文An Efficient Hybrid Vision Transformer for TinyML Applications的开源实现官方仓库地址gitclone https://github.com/yuffeenn/TinyNeXt.gitcdTinyNeXt2.2 项目结构仓库包含分类、检测、分割三套代码我们只用到classification/TinyNeXt/ ├── classification/ │ ├── main.py # 训练 / 评估入口 │ ├── config.py # 命令行参数 │ ├── models/ │ │ ├── tinynext.py # TinyNeXt-T/S/M 网络定义 │ │ ├── modules.py # MV2 / Former / SE 基础模块 │ │ └── menu.py # 模型注册供 timm create_model 使用 │ ├── util/ │ │ ├── datasets.py # 数据集与数据增强 │ │ ├── engine.py / losses.py / utils.py ... │ └── logs/ # 自带 ImageNet 预训练权重tinynext_t/m.pth ├── detection/ # 目标检测基于 MMDetection └── segmentation/ # 语义分割基于 MMSegmentation2.3 实操流程总览本文的完整操作流水线每一步后续都有对应章节与真实输出克隆代码 → 建环境装依赖 → 验证 GPU → 划分数据集 → 代码适配新版 timm / 单卡修复 → 数值稳定性修复fp32 / 归一化 / 学习率 → 策略A从零训练 → 策略B预训练微调 → 对比评估 → 推理 demo3. 环境准备3.1 创建环境并安装依赖conda create-ntinynextpython3.11conda activate tinynext pipinstalltimm einops torchmetrics tabulate fvcore实测环境Python 3.11.15 PyTorch 2.10.0 (cu128) NVIDIA RTX 5060 Ti 16GBtimm 1.0.28、torchmetrics 1.9.0。3.2 验证 GPUpython-cimport torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))2.10.0cu128 cuda: True NVIDIA GeForce RTX 5060 Ti4. 数据集准备使用 TorchVision 原生支持的ImageFolder 结构/path/to/dataset/ ├── train/ │ ├── class_a/ └── class_b/ └── val/ ├── class_a/ └── class_b/原始数据每个类别一个文件夹。先用通用划分脚本split_dataset.py按 8:2 切成 train/val# split_dataset.py —— 把每个类文件夹按比例划分为 train/valimportargparse,shutil,randomfrompathlibimportPathdefmain():apargparse.ArgumentParser()ap.add_argument(--source,requiredTrue)# 原始数据根目录每类一个子文件夹ap.add_argument(--dest,requiredTrue)# 输出目录含 train/ 与 val/ap.add_argument(--val-ratio,typefloat,default0.2)ap.add_argument(--seed,typeint,default0)ap.add_argument(--classes,nargs,defaultNone)# 只保留指定类别argsap.parse_args()random.seed(args.seed)src,destPath(args.source),Path(args.dest)all_classessorted(p.nameforpinsrc.iterdir()ifp.is_dir())classes[cforcinall_classesifcinargs.classes]ifargs.classeselseall_classesforclsinclasses:imgssorted(pforpin(src/cls).iterdir()ifp.is_file())random.shuffle(imgs)n_valmax(1,int(round(len(imgs)*args.val_ratio)))forsplit,subin((train,imgs[n_val:]),(val,imgs[:n_val])):outdest/split/cls out.mkdir(parentsTrue,exist_okTrue)foriminsub:shutil.copy2(im,out/im.name)print(f{cls}: total{len(imgs)}train{len(imgs)-n_val}val{n_val})if__name____main__:main()运行真实输出python split_dataset.py--source/data/raw--dest/data/dataset\--classeshut no_hut --val-ratio0.2--seed0Classes found: [hut, no_hut] hut: total1038 train830 val208 no_hut: total290 train232 val58 Done.划分后用 TorchVision 验证数据集能被正确加载真实输出fromtorchvisionimportdatasets trdatasets.ImageFolder(/data/dataset/train)print(tr.classes,len(tr))# [hut, no_hut] 1062本数据集存在明显的类别不平衡hut 1038 张 vs no_hut 290 张后面会看到它对两类准确率的影响。5. 代码适配新版环境老仓库代码在timm 1.0 新 PyTorch 下需两处最小改动均为实测真实报错① 模型工厂加**kwargs—— 新版create_model会向模型工厂传入pretrained_cfg等参数TypeError: tinynext_s() got an unexpected keyword argument pretrained_cfg修改models/tinynext.py三个工厂函数签名deftinynext_t(pretrainedFalse,num_classes1000,distillationFalse,**kwargs):deftinynext_s(pretrainedFalse,num_classes1000,distillationFalse,**kwargs):deftinynext_m(pretrainedFalse,num_classes1000,distillationFalse,**kwargs):② 单卡训练访问model.module报错—— 不加torch.distributed.launch时模型未被 DDP 包装AttributeError: TinyNeXt object has no attribute module. Did you mean: modules?main.py训练循环中把model.module改为仓库已定义的model_without_ddp单卡模型本身分布式model.module6. 数值稳定性踩坑关键小数据集从零训练时按仓库默认参数直接跑会连踩三个坑全部导致loss 变 NaN 或发散坑表现根因修复fp16 溢出验证 loss 12 → NaNautocast()下 fp16 溢出改为全 fp32输入幅值过大首层 BNrunning_var1398logits 达 ±53仓库用std1/255归一化输入 ±128用 ImageNet 标准归一化学习率过大第 2 epoch 后发散默认lr6e-3是 ImageNet 量级降到1e-3修复后训练才稳定收敛。记住小数据集从零训练 fp32 标准归一化 适当降低学习率。佐证论文ICCV 2025官方的 ImageNet 训练配置是300 epoch、8×RTX 3090、总 batch 2048、初始 lr 6e-3、weight decay 2.5e-2、无 Mixup/CutMix/Random Erasing——这些参数是为百万级图片设计的直接搬到 1000 张图的小数据集上必然出问题。7. 策略 A从零训练7.1 命令python main.py--modeltinynext_s --data-set IMNET\--data-path /data/dataset--epochs100--batch-size32\--num_workers4--lr1e-3\--reprob0.0--aa--mixup0--cutmix0.0--output_dirlogs7.2 结果训练 26 个 epoch 后收敛停滞、波动加剧按对比计划提前停止转入微调stats.logbest top1: 90.98% best top5: 100.00%关键轨迹epoch, val loss, top1epochval losstop1说明00.7021.8%随机权重20.4490.2%lr 刚起步就冲到 90%40.9069.9%剧烈回落61.8959.0%最低点110.3187.6%回升250.2890.98%最终 best特征验证集 top1 在59% ~ 91%间剧烈震荡训练很久都无法稳定收敛到更高精度。这正是数据量撑不起百万参数的典型表现——模型在噪声与真实特征之间反复摇摆泛化天花板被数据量锁死在 ~90%。8. 策略 B预训练微调8.1 仓库自带预训练权重无需下载TinyNeXt 官方仓库logs/目录自带 ImageNet 预训练权重logs/tinynext_m/tinynext_m.pth (29MB) logs/tinynext_t/tinynext_t.pth (14MB)注意仓库的--pretrained参数并未实现模型工厂函数忽略该标志需要自己写加载逻辑。8.2 给代码加--finetune参数在config.py增加参数定义parser.add_argument(--finetune,default,typestr,helpPath to pretrained weights for fine-tuning (classifier head is skipped))在main.py创建模型后加载预训练权重跳过分类头预训练是 1000 类需换成 2 类新头ifargs.finetune:ckpttorch.load(args.finetune,map_locationcpu,weights_onlyFalse)sdckpt[model]ifisinstance(ckpt,dict)andmodelinckptelseckpt sd{k:vfork,vinsd.items()ifnotk.startswith(class_head.)andnotk.startswith(dist_head.)}missing,unexpectedmodel_without_ddp.load_state_dict(sd,strictFalse)print(f[finetune] loaded{len(sd)}keys from{args.finetune}; fmissing(should be head){len(missing)}, unexpected{len(unexpected)})实测输出真实[finetune] loaded 325 keys from logs/tinynext_t/tinynext_t.pth; missing(should be head)2, unexpected08.3 微调命令预训练特征已经很好学习率要低一个量级且加入正则化抑制过拟合python main.py--modeltinynext_t --data-set IMNET\--data-path /data/dataset--finetunelogs/tinynext_t/tinynext_t.pth\--epochs40--batch-size32--num_workers4\--lr1e-4 --weight-decay0.05\--reprob0.0--aa--mixup0--cutmix0.0--output_dirlogs8.4 结果best top1: 95.11% best top5: 100.00%关键轨迹epochval losstop1说明00.6955.6%预训练特征 随机头就能到 55%50.4287.2%快速爬升80.2793.2%进入 93% 平台期100.2394.7%320.2095.11%最终 best特征仅靠微调分类头/浅层10 个 epoch 就稳定进入 93%之后在92% ~ 95%窄幅波动收敛又快又稳。9. 对比总结9.1 训练曲线9.2 关键指标对比指标从零训练 (tinynext_s)预训练微调 (tinynext_t)参数量1.12M1.0M训练轮数26未收敛完40最佳 top-190.98%95.11%val loss 方差59% ~ 91%剧烈92% ~ 95%稳定收敛速度慢波动大快10 epoch 进入平台需要预训练权重不需要需要仓库自带9.3 逐类准确率一个重要发现由于数据集类别不平衡hut 1038 / no_hut 290两个模型都偏向多数类但微调明显更均衡类别从零训练预训练微调hut96.2%98.1%no_hut72.4%84.5%总体90.98%95.11%预训练微调把少数类no_hut的准确率从 72.4% 提到 84.5%12.1 个百分点说明通用特征让模型不再那么依赖训练集里占多数的类别。10. 图像推理 demo用微调好的checkpoint_best.pth对单张图片分类python inference.py--modeltinynext_t\--checkpointlogs/tinynext_t/时间戳/checkpoint_best.pth\--num-classes2--classeshut no_hut--image/path/to/img.jpg实测输出真实20260810_140430_446_010.jpg - hut prob0.922 (probs: [0.922, 0.078]) 20260810_140448_487_023.jpg - hut prob0.932 (probs: [0.932, 0.068]) 20260810_141636_321_051.jpg - hut prob0.768 (probs: [0.768, 0.232])注意最后一张属于no_hut却被判成hut置信度仅 0.77——这正是上节类别不平衡的体现。若业务对少数类敏感应使用类别加权损失或均衡采样。11. 结论与建议小数据集上预训练微调全面优于从零训练精度更高95.11% vs 90.98%、收敛更快、更稳定、对少数类更友好。从零训练不是不行但成本高收益低需要额外对付 fp16 溢出、归一化、学习率等一系列数值问题且 100 万参数在 1000 张图上很难稳定收敛。实操建议有预训练权重 → 无脑微调本仓库自带无需下载必须从零 → 请用fp32 标准归一化 lr≤1e-3 强正则化 早停类别不平衡 → 加类别加权损失或均衡采样尤其关注少数类指标。本教程所有命令、脚本、修复与对比数据均为真实运行所得可直接复用到你自己的二分类/多分类任务上。