ViT微调实战指南:pytorch-image-models 中 Vision Transformer 小数据集微调的完整调参清单
【免费下载链接】pytorch-image-modelsThe largest collection of PyTorch image encoders / backbones. Including train, eval, inference, export scripts, and pretrained weights -- ResNet, ResNeXT, EfficientNet, NFNet, Vision Transformer (ViT), MobileNetV4, MobileNet-V3 & V2, RegNet, DPN, CSPNet, Swin Transformer, MaxViT, CoAtNet, ConvNeXt, and more项目地址: https://gitcode.com/GitHub_Trending/py/pytorch-image-models
先说一个很多新手都会撞上的场面:你把 ImageNet 上预训练好的 ViT 直接换成自己的分类头,拿去跑自己的数据集,验证集准确率稳稳卡在某个数字上——再跑几轮,纹丝不动。训练 loss 一路往下掉,验证 loss 却开始爬,曲线像两个人在吵架。
问题通常不在模型,而在微调的"配套动作"没做。pytorch-image-models(也就是 timm)里的 Vision Transformer 实现位于 timm/models/vision_transformer.py,它给了你完整的模型骨架;而学习率怎么退、数据怎么搅、权重怎么稳,仓库里也都有现成工具——只是没人把它们串成一条线。
这篇指南就干这件事:给你一套可直接落地的 ViT 微调决策流程,从"先判断该动多少层"到"怎么算调好了",一次讲完。
学完能拿到手的东西
- 一眼判断自己的数据量该走哪种微调档位
- 一套不会崩的"小学习率 + 预热 + 余弦衰减"组合配置
- 数据增强和随机擦除的开关取舍,以及归一化这个最容易忽略的坑
- EMA、标签平滑、DropPath 三件套各自救什么火,什么时候该上
- 一个最小可跑的训练循环,加上"什么时候该停"的判断标准
先判断——微调的"三选一"
⚠️ 先别急着调参。打开训练脚本之前,先回答一个问题:你的数据有多少张?
数据量决定了你该"动多少层"。ViT 的编码器是叠了很多层的注意力块(实现在 timm/models/vision_transformer.py),层与层之间学到的东西差别很大——浅层偏通用纹理,深层偏语义。动得多学得快,但也容易把预训练权重搅坏;动得少稳,但上限低。按下面的档位选:
| 你的数据量 | 建议策略 | 理由 |
|---|---|---|
| 几百到一两千张 | 只微调分类头 + 最后 1~2 个 Transformer 块 | 数据撑不起全模型,先让头学会"说什么",再慢慢放开身体 |
| 几千到几万张 | 全参数微调,配 DropPath 和更强正则 | 数据够多了,放开全模型,收益最大 |
| 大规模(接近预训练规模) | 从头训练或全参微调 + 强增强 | 这时预训练权重只是个好起点,不是拐杖 |
一个常见误区:数据少却把全模型都打开调,学习率还给得偏大——结果预训练权重前 100 步就被冲掉了。所以"先判断动多少层",比任何超参都优先。
学习率这条线——小学习率 + 预热 + 余弦怎么配成一套
微调预训练 ViT 的第一铁律:学习率要比从头训练小一到两个数量级。从头训练 ViT 常用 1e-3 左右,微调时你从 5e-5 到 1e-4 这个区间起手就够了。给大了,预训练知识会像被橡皮擦掉一样,几个 epoch 就白搭。
调度器这边,timm/scheduler/scheduler_factory.py 里的create_scheduler_v2支持余弦、tanh、阶梯等多种曲线。微调推荐余弦这一套,原因是它把两件事合成了一条线:开头有一段低学习率的"热身",让新换上的分类头先稳下来;之后按余弦曲线慢慢降到地板。
from timm.scheduler import create_scheduler_v2 # 30 轮总长,前 3 轮热身;热身后从 1e-5 爬到峰值,再按余弦回落到 1e-6 sched, total_epochs = create_scheduler_v2( optimizer, sched='cosine', # 余弦衰减曲线 num_epochs=30, warmup_epochs=3, # 热身轮数:头几轮小步慢走,防止新头被大梯度掀翻 warmup_lr=1e-5, # 热身起点 min_lr=1e-6, # 余弦回落的地板,别设成 0,收尾阶段还能微调 )三个数字的记忆法:warmup_epochs约为总轮数的 10%;warmup_lr约为峰值学习率的 1/5;min_lr约为峰值的 1/50。改数据量和模型规模时,按这个比例缩放,基本不会出错。
数据这一端——增强、随机擦除、归一化的取舍
数据侧三件事:几何+颜色增强、随机擦除、归一化。前三件里归一化最容易翻车。
归一化要匹配预训练。ImageNet 预训练的 ViT 在训练时见过的是均值/标准差归一化后的图,timm 的create_transform默认就按 ImageNet 统计量处理(见 timm/data/transforms_factory.py)。如果你自己的流水线里换了别的均值(或者干脆没归一化),模型等于戴着一副错配的老花镜看新图,准确率会莫名偏低且难排查。
增强强度按数据量反着来:数据越少,增强越要"狠一点";数据多,就收敛些。
from timm.data import create_transform # 训练用的变换:随机裁剪+翻转打底,auto_augment 做几何/颜色扰动 # re_prob=0.25 是随机擦除:随机抹掉一块区域,逼模型别靠单点特征认东西 train_tf = create_transform( input_size=(3, 224, 224), is_training=True, auto_augment='rand-m9-mstd0.5-inc1', # RandAugment 配置串,可换其他策略 interpolation='bicubic', re_prob=0.25, # 四分之一概率擦除一块 re_mode='pixel', # 用随机像素填,比填灰度值更难"猜" re_count=1, )验证集这边原则相反:只缩放、只居中裁剪,不做任何随机化。增强只做在训练侧,验证侧一沾随机,你的指标就不可信了。
稳定器——EMA、标签平滑、DropPath 各自救的是哪个问题
三个常被混用的正则手段,其实各管一摊:
- EMA(权重指数滑动平均):训练中每一步都留一份"历史平均版"权重,验证和部署用它。它救的是震荡——小数据微调时单步梯度噪声大,EMA 把毛刺磨平,通常能白捡 0.5~1 个点。工具在 timm/utils/model_ema.py。
- 标签平滑:把"100% 是猫"的标签软化成"99% 是猫,1% 摊给其他类"。它救的是过度自信,让模型输出别贴死,泛化更稳。
- DropPath:前向时按概率整条随机丢弃残差分支。它救的是过拟合,层数深的 ViT 尤其需要,因为它限制模型对单条通路的依赖。
from timm.utils import ModelEmaV3 from timm.loss import LabelSmoothingCrossEntropy ema = ModelEmaV3( model, decay=0.9999, # 训练总步数少就调低(如 0.999),步数多保持 0.9999 device='cuda', foreach=True, # 批量更新更快 ) # 平滑系数 0.1 是常用值:100 类任务里,1% 的不确定度摊到 99 个其他类上 loss_fn = LabelSmoothingCrossEntropy(smoothing=0.1)DropPath 则在建模时就指定,不用训练期干预:
import timm model = timm.create_model( 'vit_base_patch16_224', pretrained=True, num_classes=10, drop_path_rate=0.1, # 数据量小调 0.1 起步,过拟合明显再加到 0.2~0.3 )一个顺序上的坑:DropPath、标签平滑在建模/建 loss 时就定了,EMA 是训练循环里持续更新的。三者互相独立,别指望调 EMA 衰减系数去补 DropPath 没设够。
跑起来——最小可运行流程 + 怎么算"调好了"
把前面几段拼成最小闭环。优化器用 AdamW,权重衰减 0.05,bias 和归一化层参数不衰减(create_optimizer_v2默认就会这么分组,见 timm/optim/_optim_factory.py):
from timm.optim import create_optimizer_v2 from torch.nn import functional as F optimizer = create_optimizer_v2( model, opt='adamw', lr=1e-4, # 微调峰值学习率 weight_decay=0.05, ) for epoch in range(total_epochs): model.train() for x, y in train_loader: x, y = x.cuda(non_blocking=True), y.cuda(non_blocking=True) logit = model(x) loss = loss_fn(logit, y) # 标签平滑已包含在 loss_fn 里 optimizer.zero_grad(set_to_none=True) loss.backward() optimizer.step() sched.step_update(num_examples=x.shape[0]) # 按样本推进调度 ema.update(model) # 每步刷新 EMA 权重怎么算调好了?别看训练 loss,只看验证集。两个信号:
- 验证 top-1 连续 5 轮不再涨 → 停,取 EMA 权重的最近最优。
- 训练 loss 和验证 loss 的差距每轮都在拉大 → 过拟合进行中,进下一节的急救表。
另外提醒一句:验证和导出权重时用ema.module,不是model——EMA 那份才是你真正打磨出来的模型。完整工程化流程(分布式、断点续训、日志)可参考 train.py。
翻车急救表
| 症状 | 可能的原因 | 直接能用的对策 |
|---|---|---|
| 过拟合:训练好、验证差 | 数据少而模型全放开 | ①drop_path_rate提到 0.2~0.3;② 换"只调头+尾部块"档位;③ 增强再狠一档 |
| 不收敛:loss 不动或乱跳 | 学习率失配 | ① 峰值 LR 降到 3e-5;② 确认预热段存在且warmup_lr不是 0;③ 加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
| 准确率莫名偏低且稳定 | 归一化不匹配 | ① 确认用了 ImageNet 均值/标准差;② 确认输入通道顺序是 RGB 而不是 BGR |
| 推理慢 | 精度和编译没开 | ① 半精度推理(torch.amp.autocast);②torch.compile(model);③ 换更小的变体如 patch32/384 以下的尺寸 |
📌 排查顺序建议:先归一化,再学习率,最后才动正则。多数"玄学掉点"都出在前两步。
还能往哪走
- 更大更强的变体:仓库里从 tiny 到 large、patch16 到 patch32 的 ViT 系列都有注册,数据够就多试试尺寸上限。
- 混合精度训练:
torch.amp包住训练循环,显存和速度都赚,微调场景几乎无损。 - 层衰减:
create_optimizer_v2支持layer_decay参数,给深层更低的学习率,长序列模型上效果不错。 - 蒸馏:用大模型输出当软标签带小模型,小数据场景收益明显,timm/task/ 里有现成的蒸馏组件。
微调 ViT 没有银弹,但上面这套"先定档位、再配调度、后上稳定器"的顺序,能帮你把大多数掉点场景挡在门外。按清单走完一遍,你的验证曲线会诚实得多。
【免费下载链接】pytorch-image-modelsThe largest collection of PyTorch image encoders / backbones. Including train, eval, inference, export scripts, and pretrained weights -- ResNet, ResNeXT, EfficientNet, NFNet, Vision Transformer (ViT), MobileNetV4, MobileNet-V3 & V2, RegNet, DPN, CSPNet, Swin Transformer, MaxViT, CoAtNet, ConvNeXt, and more项目地址: https://gitcode.com/GitHub_Trending/py/pytorch-image-models
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考