1. 为什么 torchvision 0.3 的微调流程值得单独讲一遍torchvision 0.3 是目标检测 API 刚成型的版本torchvision.models.detection下的 Faster R-CNN、Mask R-CNN 第一次以官方姿态进入工程视野。放到今天看它的接口和后续版本差异不小但很多老项目、老论文复现脚本、以及一些嵌入式部署流水线仍然锁在这个版本上。你要做的是在自定义数据集上微调一个目标检测模型让它在你的类别上跑出可用的 mAP而不是从零训练一个骨干网络。这篇文章解决的核心问题很具体torchvision 0.3 目标检测模型微调时预训练权重怎么加载、类别数怎么改、数据增强怎么配、训练脚本怎么写、mAP 怎么对比验证。适合手里有一批标注好的图片VOC 或 COCO 格式都行、想快速跑通一个检测基线的人。Penn-Fudan 行人数据集只有 170 张图、345 个行人实例正好用来演示小数据集上的迁移学习。我试过在只有几百张图的场景下直接从头训 Faster R-CNNloss 根本压不下去验证集 mAP 长期在 0.1 附近晃。换成加载 COCO 预训练权重、只替换预测头之后第一个 epoch 结束 bbox mAP 就能到 0.6 左右。这个差距就是预训练权重的价值也是这篇教程要交付的东西。下面会按「数据集类 → 模型改造 → 训练脚本 → 验证请求 → 报错排查」的顺序走每一步都给可复制的代码和配置。你不需要 GPU 集群一张 8G 显存的卡就能跑完 Penn-Fudan 的 10 个 epoch。2. 前置准备torchvision 0.3 环境与自定义数据集类怎么写2.1 环境与依赖版本torchvision 0.3 对应的是 PyTorch 1.1 左右的年代但实际工程里很多人会用较新的 PyTorch 配旧版 torchvision 接口。为了减少踩坑建议直接锁定一套能跑通的组合pip install torch1.4.0 torchvision0.5.0 pip install pycocotools numpy pillow如果你确实要严格复现 0.3 的行为torchvision0.3.0配torch1.1.0也可以但pycocotools在 Windows 上编译会比较麻烦Linux 下pip install pycocotools基本能过。检测评估依赖 COCO 指标所以这个包不能省。2.2 自定义 Dataset 的返回结构torchvision 检测模型的训练入口要求Dataset.__getitem__返回(image, target)。image是[C, H, W]的 FloatTensortarget是一个字典字段含义如下字段类型含义boxesFloatTensor[N,4]格式 [x0,y0,x1,y1]范围 0~W / 0~HlabelsInt64Tensor[N]每个框的类别0 通常留给背景image_idInt64Tensor[1]图像唯一标识评估时用areaTensor[N]框面积COCO 指标区分大小框iscrowdUInt8Tensor[N]评估时 True 的实例被忽略masksUInt8Tensor[N,H,W]可选实例分割掩码Penn-Fudan 的掩码是彩色编码的每种颜色对应一个实例0 是背景。写 Dataset 类时要把颜色掩码拆成二值掩码再算每个实例的边界框import os import numpy as np import torch from PIL import Image class PennFudanDataset(object): def __init__(self, root, transforms): self.root root self.transforms transforms self.imgs list(sorted(os.listdir(os.path.join(root, PNGImages)))) self.masks list(sorted(os.listdir(os.path.join(root, PedMasks)))) def __getitem__(self, idx): img_path os.path.join(self.root, PNGImages, self.imgs[idx]) mask_path os.path.join(self.root, PedMasks, self.masks[idx]) img Image.open(img_path).convert(RGB) mask np.array(Image.open(mask_path)) obj_ids np.unique(mask)[1:] masks mask obj_ids[:, None, None] num_objs len(obj_ids) boxes [] for i in range(num_objs): pos np.where(masks[i]) boxes.append([np.min(pos[1]), np.min(pos[0]), np.max(pos[1]), np.max(pos[0])]) boxes torch.as_tensor(boxes, dtypetorch.float32) labels torch.ones((num_objs,), dtypetorch.int64) masks torch.as_tensor(masks, dtypetorch.uint8) image_id torch.tensor([idx]) area (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0]) iscrowd torch.zeros((num_objs,), dtypetorch.int64) target {boxes: boxes, labels: labels, masks: masks, image_id: image_id, area: area, iscrowd: iscrowd} if self.transforms is not None: img, target self.transforms(img, target) return img, target def __len__(self): return len(self.imgs)这里有个容易忽略的点obj_ids np.unique(mask)[1:]去掉了背景 0但如果你的标注工具把背景也编了非零颜色这一步就会多出一个假实例。检查方法是打印np.unique(mask)确认 0 确实是背景。2.3 数据增强配置torchvision 0.3 的检测增强放在references/detection/transforms.py里核心是ToTensor和RandomHorizontalFlip。训练时开翻转验证时只做 ToTensorimport transforms as T def get_transform(train): transforms [] transforms.append(T.ToTensor()) if train: transforms.append(T.RandomHorizontalFlip(0.5)) return T.Compose(transforms)小数据集上不要一上来就堆增强翻转 颜色抖动已经够用。增强太猛会让模型在早期 epoch 学不到稳定特征loss 震荡明显。3. 可复制配置加载预训练权重并调整类别数3.1 微调预训练模型的两种路径torchvision 0.3 里改模型有两种常见做法。第一种是加载 COCO 预训练权重只替换预测头适合自定义类别少、数据量小的场景。第二种是换骨干网络比如把 ResNet50 换成 MobileNetV2适合对推理速度有要求的部署。先看第一种Faster R-CNN 的预测头替换import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedTrue) num_classes 2 # 1 类目标 背景 in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes)num_classes一定要包含背景。Penn-Fudan 只有行人一类所以是 2。如果你有 5 类目标就写 6。这个数字写错是后面IndexError和 mAP 异常的常见根因。3.2 Mask R-CNN 的掩码头替换要做实例分割还得替换 mask predictorimport torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor def get_model_instance_segmentation(num_classes): model torchvision.models.detection.maskrcnn_resnet50_fpn(pretrainedTrue) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) in_features_mask model.roi_heads.mask_predictor.conv5_mask.in_channels hidden_layer 256 model.roi_heads.mask_predictor MaskRCNNPredictor( in_features_mask, hidden_layer, num_classes) return model3.3 换骨干网络的配置片段如果你要换骨干Faster R-CNN 的构造方式如下import torchvision from torchvision.models.detection import FasterRCNN from torchvision.models.detection.rpn import AnchorGenerator backbone torchvision.models.mobilenet_v2(pretrainedTrue).features backbone.out_channels 1280 anchor_generator AnchorGenerator( sizes((32, 64, 128, 256, 512),), aspect_ratios((0.5, 1.0, 2.0),)) roi_pooler torchvision.ops.MultiScaleRoIAlign( featmap_names[0], output_size7, sampling_ratio2) model FasterRCNN(backbone, num_classes2, rpn_anchor_generatoranchor_generator, box_roi_poolroi_pooler)backbone.out_channels必须和骨干输出通道一致MobileNetV2 是 1280ResNet50 是 2048。写错会在 RPN 阶段报维度不匹配。3.4 训练主脚本把references/detection/下的engine.py、utils.py、transforms.py复制到项目目录然后写主函数import torch from engine import train_one_epoch, evaluate import utils import transforms as T def main(): device torch.device(cuda) if torch.cuda.is_available() else torch.device(cpu) num_classes 2 dataset PennFudanDataset(PennFudanPed, get_transform(trainTrue)) dataset_test PennFudanDataset(PennFudanPed, get_transform(trainFalse)) indices torch.randperm(len(dataset)).tolist() dataset torch.utils.data.Subset(dataset, indices[:-50]) dataset_test torch.utils.data.Subset(dataset_test, indices[-50:]) data_loader torch.utils.data.DataLoader( dataset, batch_size2, shuffleTrue, num_workers4, collate_fnutils.collate_fn) data_loader_test torch.utils.data.DataLoader( dataset_test, batch_size1, shuffleFalse, num_workers4, collate_fnutils.collate_fn) model get_model_instance_segmentation(num_classes) model.to(device) params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.SGD(params, lr0.005, momentum0.9, weight_decay0.0005) lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) num_epochs 10 for epoch in range(num_epochs): train_one_epoch(model, optimizer, data_loader, device, epoch, print_freq10) lr_scheduler.step() evaluate(model, data_loader_test, devicedevice) torch.save(model.state_dict(), maskrcnn_pennfudan.pth) if __name__ __main__: main()collate_fnutils.collate_fn不能省检测任务的 batch 里每张图目标数不同默认 collate 会报错。batch_size2是显存和稳定性的折中8G 卡上跑 Mask R-CNN 基本够。4. 验证请求与成功结果mAP 对比与推理检查4.1 训练日志与 mAP 变化第一个 epoch 结束后评估输出大致是这样IoU metric: bbox Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.606 Average Precision (AP) [ IoU0.50 | area all | maxDets100 ] 0.984 IoU metric: segm Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.70410 个 epoch 后IoU metric: bbox Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.799 Average Precision (AP) [ IoU0.50 | area all | maxDets100 ] 0.969 IoU metric: segm Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.761bbox mAP 从 0.606 涨到 0.799segm mAP 从 0.704 到 0.761。这个提升幅度说明预训练权重确实在起作用也说明 10 个 epoch 对这个小数据集是合理的。如果你跑出来第一个 epoch 就 0.9 以上要怀疑验证集和训练集有没有重叠。4.2 单张图推理验证训练完存权重加载后做一次推理import torch from PIL import Image import torchvision.transforms as T model.load_state_dict(torch.load(maskrcnn_pennfudan.pth)) model.eval() img Image.open(PennFudanPed/PNGImages/FudanPed00001.png).convert(RGB) transform T.Compose([T.ToTensor()]) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): pred model(x)[0] print(pred[boxes].shape, pred[labels], pred[scores])正常输出里scores大部分在 0.9 以上labels全是 1行人。如果 scores 普遍低于 0.5检查一下是不是忘了model.eval()或者权重加载时 key 不匹配。4.3 用 TaoToken 做推理结果的语义校验检测模型输出的是框和分数但框里到底是不是你要的类别可以用多模态模型做一轮抽检。把裁剪出来的目标区域发给视觉模型让它判断类别是否正确比纯看 mAP 数字更直观。TaoToken 的模型对话入口可以直接上传图片做识别验证curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 这张图里有几个人}] }API Key 在 TaoToken API Keys 页面生成模型对话入口在 TaoToken 模型对话。把检测框裁剪图丢进去人工核对几轮比只看 COCO 指标更能发现类别混淆问题。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth5.1 401 Unauthorized调用 TaoToken API 时返回 401通常是 Key 没带对或者环境变量没生效。检查echo $TAOTOKEN_API_KEY curl -H Authorization: Bearer $TAOTOKEN_API_KEY https://taotoken.net/api/v1/models如果echo是空的说明 shell 里没 export。Key 本身在控制台可以重新生成注意不要把它写进会提交到 git 的脚本里。5.2 local proxy failed这个报错一般出现在本地网络环境有额外转发层的时候。先确认HTTPS_PROXY和HTTP_PROXY环境变量是否指向了一个不可用的地址env | grep -i proxy unset HTTPS_PROXY HTTP_PROXY清掉之后重试。如果是在容器里跑检查容器的网络配置有没有把出站流量导到不存在的端口。5.3 reading choices 报错reading choices这种报错通常发生在 API 返回体不是预期 JSON 的时候。比如服务端返回了 HTML 错误页客户端却按 JSON 解析choices[0]。排查方法是先打印原始响应import requests r requests.post(url, headersheaders, jsonpayload) print(r.status_code, r.text[:500])看到status_code不是 200就按对应状态码处理。200 但text是 HTML说明请求打到了错误的 endpoint检查 URL 是不是写成了/api而不是/api/v1/chat/completions。5.4 OAuth 相关报错如果你用的是 Claude Code 或 Codex 这类工具OAuth 报错一般和 token 过期有关。以 Claude Code 为例需要配置三件套{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }Base URL、Key、Model ID 三个都要写全缺一个就会在鉴权阶段失败。Codex 的auth.json同理{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: gpt-4o }Cline 的 MCP 配置里也是这三项baseUrl、apiKey、model一个都不能少。配置完重启工具再跑一次请求验证。5.5 训练侧常见报错IndexError: index 1 is out of bounds多半是num_classes设成了 1但标签里有 1。改成 2 即可。RuntimeError: Expected all tensors to be on the same device是模型和数据没放到同一个设备。确认model.to(device)和x.to(device)都执行了。KeyError: boxes出现在自定义 Dataset 返回的 target 缺字段时。对照第 2 节的字段表逐个检查。6. 长期跑检测任务用 Coding Plan 管理训练脚本迭代目标检测的微调不是一次性的活。换数据集、调 anchor、改增强策略、对比不同骨干每个实验都要改脚本、跑训练、记结果。如果每次都手动改参数、手动记录 mAP很快就会乱。比较省事的做法是把训练脚本参数化然后用一个统一的入口管理实验。TaoToken 的 Coding Plan 适合这种长期编码场景把模型配置、训练参数、评估逻辑拆成模块每次实验只改配置文件[model] backbone resnet50 num_classes 2 pretrained true [train] batch_size 2 lr 0.005 epochs 10 step_size 3 gamma 0.1 [eval] metric coco iou_threshold 0.5Coding Plan 入口在 TaoToken Coding Plan适合需要反复迭代训练脚本、维护多个实验分支的场景。接入文档在 TaoToken 接入文档里面有各工具的完整配置示例。回到训练本身10 个 epoch 跑完、mAP 稳定在 0.8 左右之后下一步通常是导出模型做推理部署。torchvision 0.3 的模型可以直接torch.save整个 state_dict加载时用同样的模型结构重建再 load。如果要做 ONNX 导出注意检测模型有动态输出需要指定dynamic_axes。这些是微调之后的工程动作先把这一轮跑通再往下走。
阅读完成 · 觉得有帮助?