简介:面向目标检测入门与进阶研究者的Faster R-CNN(Pytorch版本)完整工程资源,由区域建议网络与特征金字塔等模块构成,支持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-CNN:RPN、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_size=16, 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, dtype=np.float32) # 生成示例 anchors = generate_anchors() print(anchors.shape) # (9, 4)这段代码生成的只是单个位置上的 Anchor 基础模板。真正用的时候,特征图有H x W个位置,需要把每个位置的中心点映射回原图坐标,加上偏移量。常见做法是先把基础模板复制H * W份,再给每份加上对应位置的原图坐标偏移,一次向量化生成全部 Anchor。需要注意的是base_size=16对应 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_scale=1.0/16.0, sampling_ratio=2) feature_map = torch.randn(1, 512, 14, 14) boxes = torch.tensor([[0, 16, 16, 48, 48]], dtype=torch.float32) # 原图上 32x32 的框 out = roi_align(feature_map, boxes) print(out.shape) # (1, 512, 7, 7)这里spatial_scale=1.0/16.0是关键,它表示特征图相对于原图的缩放比例。VGG16 是 16 倍下采样,所以把原图坐标除以 16 映射到特征图。如果换成1.0/8.0,ROI 区域在特征图上覆盖的面积会大一倍,计算结果完全不同。sampling_ratio=2表示每个网格采 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 torch==1.12.1+cu116 torchvision==0.13.1+cu116 -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]], dtype=torch.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( backbone="vgg16", num_classes=21, # VOC: 20 类 + 1 背景 pretrained=True ) 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_thresh=0.5)这里pretrained=True会在首次运行时下载 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_set="trainval", transform=None): 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, dtype=torch.float32), \ torch.as_tensor(labels, dtype=torch.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(backbone="vgg16", num_classes=21) optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.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(f"Epoch {epoch+1}, 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) }, f"checkpoints/checkpoint_epoch_{epoch+1}.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_location="cpu"再 load 到 target device,避免显存不足导致加载失败。
训练结束后,可视化验证比 loss 数字更直观。随便抽十张验证集图片,把置信度大于 0.5 的框画出来,数一数漏检数量和错检数量。如果 loss 一直在降但画出来的框质量差,多半是数据标注问题而不是模型问题,这个排查顺序能省下很多无效调参时间。
5. 避坑指南:Anchor 尺寸、显存溢出与精度上不去的排查
5.1 Anchor 尺寸与目标尺度不匹配,小目标全漏
现象:训练十几个 epoch 后 loss 降得很稳,但验证集上小目标基本检测不到,大目标框却偏大。测试集里目标的平均面积只有 32x32 像素,而默认 Anchor 的最小尺寸是 128x128,相当于用大网捞小鱼。
原因:Anchor 的尺度设定没有对齐数据集的目标尺寸分布。VGG16 的 feature stride 是 16,默认base_size=16, 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_threshold=0.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_channels=256): 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, padding=1)) def forward(self, c3, c4, c5): p5 = self.lateral_convs[2](c5) p4 = self.lateral_convs[1](c4) + nn.functional.interpolate( p5, scale_factor=2, mode="nearest" ) p3 = self.lateral_convs[0](c3) + nn.functional.interpolate( p4, scale_factor=2, mode="nearest" ) # 最后过一遍 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 文件,用评估脚本计算AP@0.5:0.95和AP@0.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],这个格式问题我当初踩过一次,调了半天发现全是大框跑偏。评估结果里重点看AP@0.5:0.95,这个是综合指标,比单独看AP@0.5更能反映定位精度。
在那之后我做任何检测项目,都强制把 mAP 评估和训练流程绑在一起,训练完自动出 AP 曲线,而不是靠肉眼判断好坏。改进方案有没有用,数据说了算。这种习惯帮我躲开了不少“看起来很强、实际精度没变”的无效工作,希望也能帮你在 Faster R-CNN 复现路上少走几步弯路。
本文还有配套的精品资源,点击获取