PyTorch复现Faster R-CNN:RPN与ROI Pooling深度解析 简介面向目标检测入门与进阶研究者的Faster R-CNNPytorch版本完整工程资源由区域建议网络与特征金字塔等模块构成支持ResNet50、MobileNetV2、VGG等骨干网络切换可满足从原理复现到自定义数据集训练推理的全流程需求。资源共68个文件压缩包约239.44MB其中py源码33个涵盖训练、验证、预测、FPS测试及多GPU分布式训练脚本pth权重2个包含预训练模型与ResNet50基础权重另有xml标签配置、txt记录与json类别映射等配套文件目录结构清晰便于直接对照学习。目前已有1075人学习使用。通过该工程可快速搭建端到端检测流程理解RPN候选区域生成、ROI池化与边界框回归等关键机制并借助VOC2012数据集划分脚本、mAP记录及可视化工具完成实验验证适合论文复现、课程设计或工程落地参考。1. Faster R-CNN 的 PyTorch 复现目标检测入门绕不开的两阶段基线做目标检测的人早晚要直面 Faster R-CNN。虽然现在 YOLO 系列在工程里更流行但论原理完整度Faster R-CNN 仍然是两阶段检测的教科书先用 RPN 产出候选框再对每个候选框做分类和回归。这套设计后来被大量检测模型借鉴像 Cascade R-CNN、Mask R-CNN 都是它的变体。这份 PyTorch 版本代码包适合两类人一是刚学完 CNN 分类、想进入检测领域的学生二是想复现两阶段检测基线、给后续改进做对照的工程师。它把 RPN、Anchor、ROI Pooling、损失函数全部用 PyTorch 写清楚改起来比黑盒调用 torchvision 接口直观得多。2. 拆解 Faster R-CNNRPN、Anchor 与 ROI Pooling 的工程视角2.1 backbone 怎么选VGG16 教学实现与 ResNet50-FPN 的取舍Faster R-CNN 原文用的是 VGG16后来 torchvision 官方实现换成了 ResNet50-FPN。这两条路线在复现时各有优劣先想清楚再动手能省掉后面很多重写的时间。VGG16 的优点是结构简单、每一层输出分辨率变化很直观。conv4_3 输出的特征图是输入图像的 1/16可以直接在这个特征图上铺 Anchor。代码量小适合理解 RPN 的 IoU 计算、边界回归这些核心机制。缺点是感受野和语义层次比较单一小目标检测效果一般。ResNet50-FPN 的优点是金字塔特征能覆盖多尺度目标Faster R-CNN 论文的后续改进实践几乎都跑在 FPN 上。但它的代码量明显更大反向传播链路长新手调起来容易懵。我的建议是如果是第一次复现用 VGG16 做 backbone把注意力集中在 RPN 和 ROI Head 上如果是要在真实数据集上刷精度再用 ResNet50-FPN。这份资源里两种实现都留了接口切换时只需要改模型定义里的一行 backbone 配置。# backbone 配置示例两种结构切换 BACKBONE { vgg16: { name: vgg16, out_channels: 512, # VGG16 conv4_3 输出通道数 downsample_ratio: 16 # 特征图相对原图的缩放倍数 }, resnet50_fpn: { name: resnet50_fpn, out_channels: 256, # FPN 每层输出通道数统一为 256 downsample_ratio: None # FPN 从 P2-P5 多尺度输出 } }这个配置表的核心价值有两个out_channels决定 RPN 分类头和回归头的输入维度改 backbone 时必须同步downsample_ratio决定 Anchor 在原图上的映射范围VGG16 是 16 倍下采样原始 Anchor 的 base_size 就要按 16 的倍数设计。如果换了 backbone 没改这两个参数最常见的结果是检测框整体偏移而且很难排查。2.2 RPN 的 Anchor 生成与正负样本采样RPN 做的事情可以一句话说清在特征图的每个位置生成一组预设框然后判断哪些框里有目标哪些是背景。这些预设框就是 Anchor。VGG16 实现里特征图每个位置通常生成 9 个 Anchor由 3 种尺度和 3 种长宽比组合而来。尺度是8, 16, 32长宽比是0.5, 1.0, 2.0。注意这些数字不是随便拍的它们要覆盖原始数据集里目标尺寸的大致范围。如果跑的是交通标志这类小目标数据集默认尺度明显不够后面第 5 章我会专门讲这个坑。import numpy as np def generate_anchors(base_size16, ratios[0.5, 1.0, 2.0], scales[8, 16, 32]): 生成一组 anchor返回形状为 (9, 4) 的数组每行是 [x1, y1, x2, y2] cx cy base_size / 2.0 # Anchor 中心点相对于 base_size 的本地坐标 anchors [] for ratio in ratios: for scale in scales: w base_size * scale * np.sqrt(1.0 / ratio) h base_size * scale * np.sqrt(ratio) # 计算左上角和右下角坐标 x1 cx - w / 2.0 y1 cy - h / 2.0 x2 cx w / 2.0 y2 cy h / 2.0 anchors.append([x1, y1, x2, y2]) return np.array(anchors, dtypenp.float32) # 生成示例 anchors generate_anchors() print(anchors.shape) # (9, 4)这段代码生成的只是单个位置上的 Anchor 基础模板。真正用的时候特征图有H x W个位置需要把每个位置的中心点映射回原图坐标加上偏移量。常见做法是先把基础模板复制H * W份再给每份加上对应位置的原图坐标偏移一次向量化生成全部 Anchor。需要注意的是base_size16对应 VGG16 的 16 倍下采样如果换了 ResNet50 的 backbone需要重新对齐这个值。RPN 的正负样本采样也不是简单阈值一刀切。常见做法是给每个 Anchor 计算与所有 GT 的 IoU与任一 GT 的 IoU 大于 0.7 算正样本小于 0.3 算负样本介于中间的直接忽略。每个 batch 从正负样本里各抽 128 个凑满 256 个参与损失计算。这个阈值直接影响训练稳定性后面第 5 章会再次提到。2.3 ROI Pooling 为什么用 7×7以及 ROIAlign 的替代RPN 输出的候选框大小不一但后面的全连接层需要固定长度的输入。ROI Pooling 就是干这个的把任意大小的候选框区域切成固定网格对每个格子做最大池化。Faster R-CNN 原文用 7×7原因是当时在 ImageNet 上预训练的 VGG16 全连接层输入是 7×7 特征图沿用这个尺寸能直接复用预训练权重。这个细节经常被忽略有人改成 14×14 之后分类精度反而下降就是因为没有重新训练全连接层。ROI Pooling 有一个理论上的缺陷两次量化过程会产生坐标偏移。第一次是把候选框坐标量化到特征图整数格点上第二次是把每个格子平均切分后再次量化。对于小目标来说几像素的偏移在 7×7 的输出里可能对应 10% 的信息丢失。torchvision 官方实现用 ROIAlign 替代通过双线性插值消除量化误差。原理上这是更优的方案但在 CPU 环境或者自定义算子支持不全的平台上ROIAlign 的速度可能不如传统的 ROI Pooling。从零复现时先用 ROI Pooling 跑通流程再换 ROIAlign 对比精度是不错的路线。import torch from torchvision.ops import RoIAlign # 输入特征图: (N, C, H, W) # boxes: (M, 5)每行是 [batch_index, x1, y1, x2, y2] roi_align RoIAlign(output_size(7, 7), spatial_scale1.0/16.0, sampling_ratio2) feature_map torch.randn(1, 512, 14, 14) boxes torch.tensor([[0, 16, 16, 48, 48]], dtypetorch.float32) # 原图上 32x32 的框 out roi_align(feature_map, boxes) print(out.shape) # (1, 512, 7, 7)这里spatial_scale1.0/16.0是关键它表示特征图相对于原图的缩放比例。VGG16 是 16 倍下采样所以把原图坐标除以 16 映射到特征图。如果换成1.0/8.0ROI 区域在特征图上覆盖的面积会大一倍计算结果完全不同。sampling_ratio2表示每个网格采 2x2 个点做双线性插值数值越大越接近精确积分但耗时也会增加。3. 工程落地目录结构、环境配置与一份能跑的代码包3.1 目录结构数据、配置、模型、工具分开放一份能跑的 PyTorch 检测项目目录结构直接影响调试效率。我见过很多复现者把全部代码塞进两个脚本里改了一处模型定义训练脚本跟着崩最后直接重写。这份资源里的目录是分模块组织的照着这个结构建项目后面加功能会省很多事。faster_rcnn_pytorch/ ├── config.py # 全局超参数学习率、batch、anchor 配置 ├── dataset/ │ ├── voc_dataset.py # VOC 格式数据集解析 │ └── transforms.py # 数据增强与归一化 ├── models/ │ ├── faster_rcnn.py # 整体模型组装 │ ├── rpn.py # Region Proposal Network │ ├── roi_head.py # ROI Pooling 分类回归头 │ └── backbone.py # VGG16 / ResNet50 特征提取 ├── utils/ │ ├── anchor.py # Anchor 生成与匹配 │ ├── nms.py # NMS 实现或调用 torchvision.ops │ └── visualize.py # 检测框可视化 ├── train.py # 训练主脚本 ├── evaluate.py # mAP 评估脚本 └── requirements.txt # 依赖清单每个模块只做一件事失败时能快速定位。比如训练时 loss 变成 NaN先看utils/anchor.py里有没有除零再看models/rpn.py的损失函数不用整个项目翻。config.py独立出来的价值更大Anchor 尺度、IoU 阈值、学习率这些需要反复试的参数集中在一个文件里改动比在模型代码里到处找要可靠得多。3.2 环境配置PyTorch 与 torchvision 版本匹配PyTorch 版本选择是很多初学者的第一个翻车点。torch和torchvision必须配套版本不匹配最常见的报错是cannot import name nms from torchvision.ops或者 CUDA 算子编译失败。我一般会先确认本机 CUDA 版本再装对应的 PyTorch而不是直接pip install torch拉最新版。# 查看本机 CUDA 版本 nvidia-smi # 以 Python 3.8、CUDA 11.6 环境为例 pip install torch1.12.1cu116 torchvision0.13.1cu116 -f https://download.pytorch.org/whl/torch_stable.html安装完成后做一步快速验证跑一个 RPN 的前向计算确认 ROIAlign 能正常工作。这一步能提前发现 CPU 版和 GPU 版算子的差异避免训练到一半才发现算子不支持。import torch import torchvision print(torch:, torch.__version__) print(torchvision:, torchvision.__version__) print(CUDA available:, torch.cuda.is_available()) # 验证 torchvision.ops 的 ROIAlign 在 GPU 上可用 from torchvision.ops import RoIAlign roi_align RoIAlign((7, 7), 1.0 / 16.0, 2).cuda() x torch.randn(1, 512, 14, 14).cuda() boxes torch.tensor([[0, 16, 16, 48, 48]], dtypetorch.float32).cuda() assert roi_align(x, boxes).shape[-2:] (7, 7) print(RoIAlign OK)环境不一致导致的报错在检测项目里尤其多。Faster R-CNN 依赖的torchvision.ops在 0.13 前后改过接口旧代码里RoIAlign的spatial_scale参数类型是 float新版本接受 Tensor如果直接升级库老代码大概率报类型错误。所以复现别人代码之前先看 requirements.txt 里锁定的版本别盲目装新版。3.3 推理脚本先跑通一张图的完整链路训练是慢功夫先把推理链路跑通能验证模型结构、数据流、坐标映射全部正确。常见做法是准备一张有目标的图加载 init 权重或者干脆随机权重输出几个候选框画在图上看框的位置大致在目标附近。import torch import cv2 from models.faster_rcnn import FasterRCNN from utils.visualize import draw_boxes # 加载模型 model FasterRCNN( backbonevgg16, num_classes21, # VOC: 20 类 1 背景 pretrainedTrue ) model.eval() # 读取并预处理图像 img cv2.imread(demo.jpg) img_tensor torch.from_numpy(img / 255.0).permute(2, 0, 1).float().unsqueeze(0) # 前向推理返回预测框、置信度、类别 with torch.no_grad(): boxes, scores, labels model(img_tensor) # 绘制置信度大于 0.5 的检测结果 draw_boxes(img, boxes[0], scores[0], labels[0], score_thresh0.5)这里pretrainedTrue会在首次运行时下载 ImageNet 预训练权重如果网络不稳定建议提前下载放到缓存目录。推理模式下要把model.eval()和torch.no_grad()都写上否则 BatchNorm 和 Dropout 会干扰输出。第一次跑通的标准是能看到几个真实目标的框类别大致正确背景区域没有被误检覆盖。如果全是错位框优先检查预处理里的归一化方式RGB 顺序、缩放范围哪个不对都会导致特征分布异常。4. 训练与验证数据管道、损失函数和模型保存的完整流程4.1 VOC 数据集的解析与 Dataset 封装检测训练和分类训练的最大区别是标签格式。分类任务的标签是一个整数检测任务的标签是每个目标的类别加真实框坐标。VOC 格式是最常见的标签载体标注信息存在 XML 文件里需要先解析成统一的数据结构。import os import xml.etree.ElementTree as ET import torch from torch.utils.data import Dataset import cv2 class VOCDataset(Dataset): def __init__(self, root, image_settrainval, transformNone): self.root root self.image_set image_set self.transform transform self.image_dir os.path.join(root, JPEGImages) self.anno_dir os.path.join(root, Annotations) self.image_paths self._load_image_paths() def _load_image_paths(self): # 读取 ImageSets/Main/trainval.txt 中的图像编号 list_file os.path.join( self.root, ImageSets, Main, f{self.image_set}.txt ) with open(list_file, r) as f: ids [line.strip() for line in f.readlines()] return [os.path.join(self.image_dir, f{i}.jpg) for i in ids] def __getitem__(self, idx): img_path self.image_paths[idx] img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 解析对应 XML xml_path os.path.join( self.anno_dir, os.path.splitext(os.path.basename(img_path))[0] .xml ) boxes, labels self._parse_xml(xml_path) if self.transform is not None: img, boxes, labels self.transform(img, boxes, labels) return img, boxes, labels def _parse_xml(self, xml_path): tree ET.parse(xml_path) root tree.getroot() boxes, labels [], [] for obj in root.iter(object): label obj.find(name).text bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) boxes.append([x1, y1, x2, y2]) labels.append(label_to_id[label]) return torch.as_tensor(boxes, dtypetorch.float32), \ torch.as_tensor(labels, dtypetorch.int64)这个 Dataset 封装有三点值得注意。第一图像和标注对应关系靠文件名关联训练时如果发现框和图像内容对不上优先检查 image_set 列表和 JPEGImages/Annotations 两个目录的文件是否一一对应。第二transform接收的参数是图像、框、标签三件套不是像分类任务那样只处理图像写随机翻转时框的 x 坐标也要跟着翻转。第三返回的 boxes 是 float32标签是 int64模型里后续计算损失时对数据类型很敏感混用 float64 偶尔会引起 mismatched type 报错。4.2 训练主循环四个损失一起反传Faster R-CNN 的损失由四部分组成RPN 的分类损失、RPN 的框回归损失、ROI Head 的分类损失、ROI Head 的框回归损失。训练时四个损失相加一次反向传播同时更新所有参数。import torch import torch.optim as optim model FasterRCNN(backbonevgg16, num_classes21) optimizer optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) for epoch in range(12): model.train() total_loss 0.0 for images, boxes, labels in train_loader: images images.cuda() boxes [b.cuda() for b in boxes] labels [l.cuda() for l in labels] # 前向计算返回四部分损失 losses model(images, boxes, labels) loss sum(losses.values()) # rpn_cls rpn_box roi_cls roi_box optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch1}, Loss: {total_loss / len(train_loader):.4f})四个损失相加是两阶段检测训练的核心逻辑。单独优化 RPN 或单独优化 ROI Head 都行不通因为 RPN 提的框质量直接决定 ROI Head 的输入分布两者必须联合训练。这里用了StepLR每 5 个 epoch 学习率衰减到原来的 1/10是两阶段检测训练里常见的配置前几个 epoch 让模型快速收敛后期用小学习率精细调整。训练时显存溢出是最常见的问题batch size 调小一点就能解决。VGG16 单 GPU 环境下batch size 从 8 降到 2显存占用能降一半以上。如果 batch size 太小导致 BN 层统计不稳定可以把 backbone 的 BatchNorm 参数冻结只训练检测头这是工程上常用的折中方案。4.3 模型保存与训练恢复别让中断白跑训练经常要跑十几个小时中途机器重启、显存出错都可能打断进程。只保存最后一轮权重的策略风险太大我习惯每个 epoch 结束都存一个 checkpoint再保留一个 best 模型按验证 mAP 更新。# 保存完整的训练状态 torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), loss: total_loss / len(train_loader) }, fcheckpoints/checkpoint_epoch_{epoch1}.pth) # 恢复训练 checkpoint torch.load(checkpoints/checkpoint_epoch_6.pth) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) scheduler.load_state_dict(checkpoint[scheduler_state_dict]) start_epoch checkpoint[epoch] 1保存 optimizer 和 scheduler 状态是关键。很多人只保存模型权重恢复训练时学习率被重置回初始值后面几个 epoch 的 loss 走势全部乱掉。torch.load在 GPU 环境下默认会把张量加载到显存如果换了设备恢复训练记得加map_locationcpu再 load 到 target device避免显存不足导致加载失败。训练结束后可视化验证比 loss 数字更直观。随便抽十张验证集图片把置信度大于 0.5 的框画出来数一数漏检数量和错检数量。如果 loss 一直在降但画出来的框质量差多半是数据标注问题而不是模型问题这个排查顺序能省下很多无效调参时间。5. 避坑指南Anchor 尺寸、显存溢出与精度上不去的排查5.1 Anchor 尺寸与目标尺度不匹配小目标全漏现象训练十几个 epoch 后 loss 降得很稳但验证集上小目标基本检测不到大目标框却偏大。测试集里目标的平均面积只有 32x32 像素而默认 Anchor 的最小尺寸是 128x128相当于用大网捞小鱼。原因Anchor 的尺度设定没有对齐数据集的目标尺寸分布。VGG16 的 feature stride 是 16默认base_size16, scales[8,16,32]实际生成的 Anchor 尺寸在 128 到 512 之间。如果数据集里大部分目标都小于 64 像素RPN 几乎找不到合适的正样本。解决先统计训练集所有标注框的宽高分布再按统计结果调整 Scale。常见做法是取标注框宽高的中位数和四分位数把 Anchor 的 scales 改成[4, 8, 16]这类更小的组合同时适当下调 IoU 正样本阈值到 0.5让更多中等质量的框参与训练。改完之后重新跑训练小目标的 Recall 会有明显提升。5.2 显存溢出batch size 与图片尺寸的权衡现象训练刚开始就报CUDA out of memory有时降到 batch size 为 1 还是溢出只能换更小的分辨率。原因Faster R-CNN 的显存占用有几个隐藏大头。RPN 在特征图上每个点生成 9 个 Anchor一张 800x600 的图会产生约 20 万个候选框这些框的 IoU 计算矩阵需要额外显存。ROI Pooling 阶段每个候选框都要从特征图上切片池化候选框数量多时中间张量体积巨大。解决除了常规地调小 batch size更有效的是在config.py里限制每张图的候选框数量。常见做法是把 RPN 输出到 ROI Head 的 proposal 数量从2000降到512训练时够用显存直接砍掉一大半。另一个技巧是限制图像的短边长度比如统一 resize 到短边 600同时按比例缩放坐标能稳定控制显存峰值。5.3 训练不收敛学习率与 RPN 正负样本阈值现象loss 前几个 epoch 不降反升或者在某个值附近反复震荡完全看不到收敛的趋势。原因最常见的是学习率设置过大SGD 优化器在检测任务上对学习率极敏感。另一个隐蔽原因是 RPN 的正负样本 IoU 阈值设得不当比如正样本阈值设成 0.5负样本阈值设成 0.5导致正负样本重叠区域过大分类头反复摇摆。解决先把学习率降到1e-4试跑 2 个 epoch如果 loss 能稳步下降再逐步调大。RPN 的 IoU 阈值要保证正负样本之间有间隔用正样本 IoU 0.7负样本 IoU 0.3这种经典配置边界区域直接忽略减少分类头的歧义。训练日志里单独打印 RPN 的 loss 和 ROI Head 的 loss哪部分不收敛就针对哪部分调参。5.4 验证集精度高、测试集精度低数据分布漂移现象验证集 mAP 到 0.75测试集只有 0.5差距不是几个点而是断崖式下跌。检查发现验证集和训练集来自同一批数据采集测试集是新的场景。原因数据集的场景结构、光照、目标尺度分布和训练集差异过大。检测模型对训练集场景的纹理和背景产生了过拟合换到新场景后 RPN 找到的候选框质量下降ROI Head 的分类置信度跟着崩。解决训练时把图像随机翻转、随机亮度和色彩扰动这些增强全部打开让模型见过更多样的背景。更简单直接的办法是保证训练集覆盖多个场景不要只在一个地方采集数据。推理阶段可以用测试集做一次统计如果测试集中目标普遍较小训练时把短边 resize 到 800小目标特征保留更多精度也能回涨。5.5 torchvision 与手写实现的 NMS 差异现象同一张图自己写的 NMS 输出结果和 torchvision 的nms函数不一致框的位置和数量都有差别精度评估也差了几个点。原因手写 NMS 的排序策略和比较顺序实现方式容易写跑偏。常见错误是按置信度排序后用全局抑制的方式一次性过滤掉所有低分框但没有递归地去比较剩余框之间是否重叠。逻辑上有点绕写出来就是结果不稳定。解决直接用torchvision.ops.nms输入boxes和scores返回保留的 index一行代码搞定。如果必须手写两个关键参数别弄错iou_threshold一般设 0.5留框数上限top_k一般是 100。还要注意测试时和训练时的 NMS 阈值可以不同训练时用 0.7 保留更多负样本参与损失计算测试时用 0.3 或者 0.5 去重更激进。from torchvision.ops import nms keep nms(boxes, scores, iou_threshold0.5) final_boxes boxes[keep] final_scores scores[keep]6. 进阶手段FPN 改造与 mAP 评估用数据验证你的改进6.1 在 VGG16 上加一层简易 FPN想要提升小目标精度最直接的手段是引入 FPN。标准做法是取 VGG16 的conv3_3、conv4_3、conv5_3三层输出通过 1x1 卷积统一通道数再自上而下做相邻层相加融合。核心代码如下class LightFPN(nn.Module): def __init__(self, in_channels_list[256, 512, 512], out_channels256): super().__init__() self.lateral_convs nn.ModuleList() self.fpn_convs nn.ModuleList() for in_ch in in_channels_list: # 1x1 卷积统一通道到 256 self.lateral_convs.append(nn.Conv2d(in_ch, out_channels, 1)) # 3x3 卷积消除上采样引起的混叠 self.fpn_convs.append(nn.Conv2d(out_channels, out_channels, 3, padding1)) def forward(self, c3, c4, c5): p5 self.lateral_convs[2](c5) p4 self.lateral_convs[1](c4) nn.functional.interpolate( p5, scale_factor2, modenearest ) p3 self.lateral_convs[0](c3) nn.functional.interpolate( p4, scale_factor2, modenearest ) # 最后过一遍 3x3 卷积 p5 self.fpn_convs[2](p5) p4 self.fpn_convs[1](p4) p3 self.fpn_convs[0](p3) return [p3, p4, p5]改造后 RPN 需要在每层金字塔上都生成 Anchor不同层用不同尺度大尺度 Anchor 放深层小尺度 Anchor 放浅层。改动量不大但 mAP 通常能提升 3 到 5 个点尤其是在小目标占比较高的数据集上。6.2 用 COCO 评估脚本算 mAP替代肉眼看图直观上看起来效果不错不代表 mAP 就高。标准做法是生成 COCO 格式的预测结果 JSON 文件用评估脚本计算AP0.5:0.95和AP0.5两个指标。我一般会在每次训练后强制跑一遍评估把结果写进日志对比不同超参数和模型结构改进到底有没有效果。import json from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval # 保存模型输出为 COCO 格式 results [] for i, (boxes, scores, labels) in enumerate(predictions): for box, score, label in zip(boxes, scores, labels): results.append({ image_id: i, category_id: int(label), bbox: [box[0], box[1], box[2]-box[0], box[3]-box[1]], score: float(score) }) with open(pred.json, w) as f: json.dump(results, f) # 计算 mAP coco_gt COCO(annotations/instances_val.json) coco_dt coco_gt.loadRes(pred.json) coco_eval COCOeval(coco_gt, coco_dt, bbox) coco_eval.evaluate() coco_eval.accumulate() coco_eval.summarize()bbox格式必须是从左上角开始的[x, y, w, h]不是[x1, y1, x2, y2]这个格式问题我当初踩过一次调了半天发现全是大框跑偏。评估结果里重点看AP0.5:0.95这个是综合指标比单独看AP0.5更能反映定位精度。在那之后我做任何检测项目都强制把 mAP 评估和训练流程绑在一起训练完自动出 AP 曲线而不是靠肉眼判断好坏。改进方案有没有用数据说了算。这种习惯帮我躲开了不少“看起来很强、实际精度没变”的无效工作希望也能帮你在 Faster R-CNN 复现路上少走几步弯路。本文还有配套的精品资源点击获取