ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

InternImage实战:图像分类微调全流程指南

InternImage实战:图像分类微调全流程指南 简介InternImageNet图像分类实战资料包面向计算机视觉初学者与研究者提供从数据集结构、预处理流程到CNN训练与评估的完整工程参考。资源共2000个文件、压缩包约737MB以大量PNG图片数据为主同时包含Python训练脚本、DCNv3的CUDA/C扩展源码、JSON类别映射与Shell辅助文件可直接用于复现图像分类实验。已有707人浏览学习。包内含InternImage_Demo示例覆盖数据加载、模型构建、训练调优与指标分析等关键步骤并交代了图像缩放、归一化、随机翻转、裁剪等预处理方法以及学习率调度、早停、数据增强等细节。数据集按8:1:1划分训练、验证与测试集图像涵盖不同角度、光照和背景模型可基于ResNet等预训练网络微调也可借助DCNv3自研算子提升特征提取能力。方便读者快速上手并在此基础上调整模型架构、优化器或数据策略进而提升分类精度。1. InternImageNet实战图像分类任务从哪入手最稳朋友最近在做一个森林图像分类项目40 个树种其中松树和云杉的幼叶靠肉眼都常分错torchvision 里那套轻量模型在验证集上卡在 91% 上不去。换了一个在 ImageNet 上表现靠前的基础模型InternImage 系列预训练权重同类数据只做了标准微调准确率直接爬了 3 个多点。所谓的 InternImageNet 实战就是把 InternImage 这种大规模预训练模型完整地用起来从 ImageNet 格式数据整理、预训练权重加载和分类头替换到训练参数调整与推理验证。这条路比从零训练自己的网络省太多时间也比纯 Transformer 图像分类方案对显存和调参的容忍度高适合已经跑过基础分类代码、但想把效果再往上推的中小型团队或独立开发者。2. 起步准备搭好环境并把数据集整理成 ImageNet 目录格式2.1 环境依赖与 DCNv3 算子的版本匹配InternImage 的核心算子是 DCNv3可变形卷积 v3它不是 PyTorch 内置算子而是以自定义 CUDA 扩展的方式打包在 mmcv 里。于是环境配置的最大门槛不是装 PyTorch 本身而是让 torch、CUDA、mmcv 三者匹配。我一般用 openmim 来安装让它自动解析当前 torch 版本对应的预编译 mmcv 二进制尽量避免手动源码编译。# 创建独立虚拟环境避免把系统 Python 弄乱 conda create -n internimg python3.10 -y conda activate internimg # 安装 PyTorch版本需要和本机 nvidia-smi 显示的驱动匹配 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 用 openmim 自动匹配 mmcv 与 torch 版本 pip install openmim mim install mmengine mim install mmcv2.0.0第一行 pip 命令里的 cu121 后缀意味着目标 CUDA 版本如果本机驱动只支持老版本 CUDA就得换成对应的 cu118 之类的源这里按实际驱动为准。mmcv 是预编译包不同 torch 版本对应不同二进制用 mim 安装会在安装时去查当前 torch 版本再挑选可用的 wheel如果匹配不到它会提示走源码编译首次编译 DCN 相关算子大约要十几分钟这时候以为卡死就错了。最容易碰到的翻车现场是 mmcv 装上了代码一跑就报ModuleNotFoundError: mmcv.ops或undefined symbol。这多半是 mmcv 二进制和当前 torch 的版本错配解决方式是先pip uninstall mmcv -y再重新用 mim 按当前环境安装而不是去源码里硬找问题。配合 mmcls 生态使用会更省心因为 InternImage 的很多预训练权重和模型定义都习惯用这套命名体系管理直接加载比手动拼 key 可靠。2.2 把任意分类数据集整理成 ImageNet 目录格式提到 ImageNet 格式很多人以为只是“一个文件夹放图片”。实际上 mmcls 这类库在读取数据时默认按train/类别名/图片.jpg类似的目录结构解析标签类别名就是目录名。不少图像分类数据集下载下来是 CSV 标注或者 JSON必须先把标注文件转成目录结构否则库不认。# organize_to_imagenet.py import pandas as pd from pathlib import Path import shutil df pd.read_csv(annotation.csv) # 至少包含 image_path、label、split 三列 root Path(data/forest_cls) # 目标根目录 for split in (train, val): part df[df[split] split] for row in part.itertuples(): src Path(row.image_path) dst_dir root / split / str(row.label) # 目录名直接用类别文本 dst_dir.mkdir(parentsTrue, exist_okTrue) shutil.copy(src, dst_dir / src.name)这段代码的核心逻辑是把 CSV 里的每条记录复制到对应类别目录。label列必须保存成真实类别名而不是整数 id因为后续从目录名读出的标签会直接参与模型输出到类名的映射如果用了数字 id训练集和验证集一旦重新划分id 对应的类别顺序就会乱。类别名里有空格或中文的最好先统一改成英文短横线风格部分库在 Windows 下处理这类路径会出现编码问题。划分验证集时如果原始数据已经分好train/val两个集合就直接照搬如果只有一份完整数据建议按类别做分层抽样保证每个类在 val 里都有样本。森林、病害这类天然长尾的数据尤其不能随机切否则某个稀有树种可能在验证集里一条样本都没有。转换脚本我一般放在项目根目录的tools/下数据一更新就重跑。跑完后花半分钟验证find data/forest_cls/train -maxdepth 1 -type d | wc -l的结果应该是类别数加 1目录层级错了模型会把父目录当标签。2.3 数据增强ImageNet 预训练模型的输入口径InternImage 的预训练权重大多在 224×224 分辨率下训练。微调时不要轻易改输入尺寸否则权重分布和模型实际看到的像素区域不一致细粒度分类比如叶片辨别影响尤其明显。常见做法是训练时用 RandomResizedCrop 做尺度扰动验证时固定 Resize(256) 后 CenterCrop(224)。# augment.py from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.2, 1.0)), # 随机裁剪和缩放 transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3), # 颜色扰动对野外图像很管用 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet 统一口径 std[0.229, 0.224, 0.225]), ]) val_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]), ])scale 用(0.2, 1.0)表示允许裁剪到原图 20% 的最小区域让模型适应不同大小的目标如果任务里目标本身容易只拍到一半就把下限提到 0.4。ColorJitter对森林图像分类几乎是必选项不同季节、天气下叶片颜色偏移很大相当于免费扩增了大量样本。但 brightness 别超过 0.5否则模型会对颜色噪声过拟合。DataLoader 侧num_workers一般设 48pin_memoryTrue值得开。只有在 GPU 利用率低于 60% 时才需要去搞 FFmpeg reader 或 lmdb/recordIO 这类高级存储方案项目初期不要引入这种复杂度徒增排错成本。3. 用 InternImage 做图像分类权重加载、训练循环与参数调整3.1 加载预训练权重替换分类头把 InternImage 用到自己的数据上最核心的改动是分类头。预训练模型输出 ImageNet 的 1000 类而实际任务可能只有几十个类不能直接把整个权重 load 进去必须先替换成匹配尺寸的全连接层。下面的写法以官方模型定义存在internimage包内为前提具体入口按你拿到的仓库为准。# build_model.py import torch import torch.nn as nn import internimage as im # 结构和预训练权重必须严格一致core_dim 和 layers 是模型配置关键 model im.InternImage( core_dim96, layers[3, 4, 18, 3], num_classes1000, ) state torch.load(internimage_t_1k.pth, map_locationcpu) # 官方保存的 key 常带 backbone. 前缀直接加载会报缺失 new_state {k.replace(module.backbone., backbone.): v for k, v in state.items()} missing, unexpected model.load_state_dict(new_state, strictFalse) # 替换分类头特征维度取决于所选模型的最后输出宽度 model.head nn.Linear(1024, num_classes)这里有三个要点。第一core_dim和layers必须与权重存档完全一致不同尺寸的 InternImage 配置差异很大小 backbone 去加载大权重必然报尺寸不匹配。第二权重 key 有时带backbone.前缀有时带module.前缀前者是 mmcls 封装导致的后者是分布式训练导致的加载前最好先把键名打印一遍再写替换规则。第三strictFalse给了容错空间但必须检查打印出来的missing列表里面只有head.fc.weight之类是正常的代表新的分类头已初始化如果骨干参数整片缺失说明权重根本没有匹配上训练等于从随机初始化开始那效果就凭运气了。替换分类头时1024要按实际 backbone 计算。我没见过比直接打印model.head上一层权重形状更稳的方式靠配置文件猜 out_channels 最容易在这里翻车。3.2 训练循环交叉熵、AdamW 与混合精度模型结构就绪后训练循环本身是标准的 PyTorch 流程。值得用心设置的是 weight decay、label smoothing 和混合精度这三样对最终收敛质量影响很大。# train_loop.py import torch import torch.nn as nn optimizer torch.optim.AdamW( model.parameters(), lr1e-4, # 微调起步学习率别拿初始化学习率来用 weight_decay0.05, ) criterion nn.CrossEntropyLoss(label_smoothing0.1) scaler torch.cuda.amp.GradScaler() model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad(set_to_noneTrue) with torch.cuda.amp.autocast(): out model(imgs) loss criterion(out, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()weight_decay 设 0.05 是因为 AdamW 的权重衰减实现方式和 L2 正则有区别它直接在参数更新时把衰减扣掉对预训练模型来说设太高会破坏已经学好的底层特征。label_smoothing 把硬标签变成软标签等于告诉模型“别太自信”对样本少、易过拟合的任务非常有帮助0.1 是比较稳的默认值。混合精度则是一个让人又爱又恨的环节。InternImage 的 DCNv3 算子在老显卡驱动下autocast可能出现 loss 直接 NaN整个训练白跑。遇到这种情况先别怀疑模型把autocast强制关掉batch size 减半往往能稳住。新卡比如 A100、H100 可以试试bfloat16它的指数位更多数值稳定性比 float16 好不少。训练时如果 loss 卡在 2.3 附近不动别慌。预训练模型刚替换头新分类头是随机初始化的前两三个 epoch 主要在让分类头学习类别映射loss 下降慢很正常。我一般先让它跑两个 epoch要是两个 epoch 后 loss 纹丝不动再考虑降学习率或者把模型输出的 logits 打出来看分布。3.3 必调参数分辨率、批大小、学习率和 Drop PathInternImage 这类大模型微调经验上最影响结果的四个参数是输入分辨率、batch size、学习率以及 drop path rate。下面的配置表是我常用的起点具体按数据量调整。参数常见初始值调高后效果踩坑点输入分辨率224细粒度任务可提升显存翻倍超过 384 后收益递减Batch Size64梯度更稳定显存不够先减半别硬扛学习率1e-4收敛更快超过 1e-3 极易发散Drop Path0.10.3正则化更强数据量大时调太高会欠拟合输入分辨率不要一上来就试 384。除非分类任务确实需要看清细节否则 224 是性价比最好的选择。常见误区是看到别人在 ImageNet 上用 384 微调就在自己的小数据集也照搬结果显存不够反而带来一些非线性的性能损失。稳妥做法是先在 224 上收敛再用 384 微调两个 epoch这样收益最明显。学习率我习惯用三段式前 5 个 epoch warmup 到 1e-4再用余弦退火降到 1e-6。warmup 的核心作用不是提速而是防止随机初始化的分类头在前几个 batch 把过大的梯度传回预训练骨干把学好的特征冲乱。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_maxepochs, eta_min1e-6) # 每个 epoch 结束后调用 scheduler.step()Drop path 这个参数容易被忽略。InternImage 是 Transformer 堆叠结构drop path 等价于在每个 block 的残差路径上加随机丢弃正则化效果很明显。数据量在 5k 以下时我建议设 0.2数据量上来之后降到 0.1如果任务只有四五个类别可以先设 0观察过拟合再慢慢往回加。这个参数同时影响训练阶段和验证阶段的行为改完必须重新冷启动训练不能中途加载。4. 避坑清单InternImage 训练与推理的常用排查现场4.1 加载权重文件损坏与键名不匹配现象调用load_state_dict抛Missing key(s) in state_dict或者直接EOFError读取失败。 原因权重文件下载中断或不完整另外模型的 key 前缀与权重存档的命名规范不一致。 解决先torch.load(..., map_locationcpu)打印 state_dict 的全部键名看清前缀再写替换规则。文件完整性用发布方提供的 sha256 校验不要依赖下载工具的“断点续传”几百 MB 的 pth 最容易在这里出问题。4.2 显存溢出DCNv3 算子比普通卷积更吃显存现象某个 epoch 中途CUDA out of memory但nvidia-smi看显存占用似乎并不高。 原因DCNv3 在 forward 过程中要保存采样偏移和输入特征图计算图临时显存比普通卷积大不少分辨率越高越明显。 解决把 batch size 减半是最直接的。如果还想维持等效的更新步数用梯度累积accum_steps 2 scaler.scale(loss).backward() if (step 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_noneTrue)梯度累积能模拟更大的 batch但 BatchNorm 统计量仍按小 batch 更新batch 太小噪声会变大。所以 batch size 至少保持 16 以上实在不够就降分辨率而不是无限调大 accum_steps。4.3 分类任务不收敛先查学习率和类别不平衡现象验证准确率一直停在 5%10%和随机猜测差不多训练 loss 也没明显下降。 原因通常是学习率过大导致 loss 震荡或者类别严重不平衡模型学会了直接预测高频类。 解决把学习率降到 1e-5 重跑一个 epoch先确认 loss 在动。如果类别不平衡给 loss 加类别权重或者在 DataLoader 里配WeightedRandomSamplerfrom torch.utils.data import WeightedRandomSampler import numpy as np class_counts np.bincount(labels) weights 1.0 / class_counts[labels] # 每个样本的权重是类别频率倒数 sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)replacementTrue表示允许重复采样这样稀有类别在每轮里都能被抽到。4.4 部署阶段ONNX 导出与推理引擎的算子兼容问题现象Python 环境里推理正常导出 ONNX 后 TensorRT 报错或者 CPU 推理直接告诉你某个算子在当前平台不可用。 原因DCNv3 是自定义 CUDA 算子导出 ONNX 时往往变成一个自定义节点而不是标准卷积方案推理框架里没有对应插件就挂了。 解决如果部署环境允许加载 mmcv 系列提供的 TensorRT 插件可以用 mmdeploy 的流程走导出如果环境受限不能加自定义算子只能在项目选型阶段就换成你熟悉的普通卷积模型。架构选型阶段就要把部署边界想清楚这里没有后悔药。5. 验证与进阶混淆矩阵、特征可视化与稳定复现技巧5.1 验证脚本Top-1/Top-5 和混淆矩阵单看整体准确率对实际落地远远不够特别是森林图像分类这种错分有明确业务价值的场景。用混淆矩阵能直接看到哪些类互相打架。# evaluate.py import numpy as np import torch from sklearn.metrics import confusion_matrix model.eval() preds, gts [], [] with torch.no_grad(): for imgs, labels in val_loader: out model(imgs.cuda()) preds.extend(out.argmax(dim1).cpu().numpy()) gts.extend(labels.numpy()) preds, gts np.array(preds), np.array(gts) top1 (preds gts).mean() * 100 # 想看 Top-5 时用 torch.topk 取前 5 个索引 # 混淆矩阵按验证集的类别顺序传 labels cm confusion_matrix(gts, preds, labelsclass_list)验证集不要开RandomHorizontalFlip甚至CenterCrop的位置都要固定否则同一条样本不同轮的结果会有波动。混淆矩阵出来后优先看对角线外的误分集中在哪两类再回到原图去看这两类是否真的在视觉上接近还是标注本身有问题。5.2 特征图可视化把黑匣子打开一条缝当模型在某个错误类别上预测异常自信时只看 loss 和准确率看不出原因。我会给最后一个 stage 挂一个 forward hook把特征图抓出来用均值通道叠加到原图上大致判断模型注意力落在哪。# visualize_feature.py feat {} def hook_fn(module, inputs, outputs): feat[out] outputs handle model.blocks[-1].register_forward_hook(hook_fn) model(imgs.unsqueeze(0).cuda()) fm feat[out][0].abs().mean(dim0).detach().cpu().numpy() handle.remove()如果特征图的能量集中区域和物体主体对不上说明模型在学背景特征常见于遮阴、逆光这类光照干扰严重的森林场景。这种样本我会直接看它所属类别的原始图像然后决定是加同类的覆盖样本还是调整增强策略而不是盲目调学习率。5.3 三个利于复现的习惯第一个是固定随机种子。第二个是保存每个 epoch 的 checkpoint 和对应的混淆矩阵不要只留最好的模型否则你永远不会知道模型的稳定性。第三个是训练日志里出现第二次训练结果差异很大这种玄学问题时先检查 DataLoader 的shuffle和增强是否依赖了全局随机状态把 seed 加进worker_init_fn再重跑。这些习惯帮我省了很多次“昨天还能复现、今天就不行”的折腾。希望帮到你。本文还有配套的精品资源点击获取
返回列表