R-CNN基础
## 目标检测概述 ### 目标检测任务定义 目标检测是计算机视觉的核心任务之一,旨在找出图像中所有感兴趣的目标,确定它们的**类别**和**位置**。 ``` 输入: 图像 输出: [(类别1, 边界框1), (类别2, 边界框2), ...] ``` ### 传统方法 vs 深度学习方法 | 方法类型 | 特征提取 | 检测方式 | 精度 | 速度 | |---------|---------|---------|------|------| | 传统方法(HOG+SVM, DPM) | 手工设计特征 | 滑窗+分类器 | 较低 | 慢 | | 深度学习(R-CNN系列) | CNN自动学习 | 候选区域+分类 | 高 | 较慢 | | 单阶段检测(YOLO/SSD) | CNN自动学习 | 直接回归 | 高 | 快 | ### 边界框表示 边界框(Bounding Box)常用两种表示方式: **1. (x1, y1, x2, y2)** - 左上角和右下角坐标 ``` bbox = [x_min, y_min, x_max, y_max] ``` **2. (x, y, w, h)** - 中心点坐标和宽高 ``` bbox = [center_x, center_y, width, height] ``` ### 目标检测挑战 1. **尺度变化**:目标在图像中的大小差异大 2. **姿态变化**:目标的朝向、形态各异 3. **遮挡**:目标被部分遮挡 4. **光照变化**:不同光照条件下的外观差异 5. **背景干扰**:复杂背景导致的误检 ### R-CNN历史意义 R-CNN(Regions with CNN,2014)是首个将深度学习成功应用于目标检测的工作,实现了**检测精度的飞跃**: - VOC 2012 mAP: 31% → 53%(提升22个百分点) - 奠定了Two-Stage检测器的基础框架
## Selective Search算法 ### Selective Search原理 Selective Search是一种**区域提议算法**,用于生成可能包含目标的候选区域。 ### 核心思想 基于图像的层次化分割,通过合并相似区域生成候选框: 1. **图像分割**:使用图论方法将图像分割成小区域 2. **层次化合并**:不断合并相似的区域 3. **生成候选框**:记录合并过程中的边界框 ### 相似度度量 综合多种特征计算区域相似度: ``` S = a1 × S_color + a2 × S_texture + a3 × S_size + a4 × S_fill ``` - **S_color**:颜色直方图相似度 - **S_texture**:纹理相似度(HOG/SIFT) - **S_size**:尺寸相似度(鼓励小区域先合并) - **S_fill**:填充度(边界框重叠程度) ### 工作流程 ``` 输入图像 ↓ 生成初始分割(如Felzenszwalb算法) ↓ 计算所有区域对相似度 ↓ 合并最相似的区域 ↓ 更新相似度(计算新区域与其他的相似度) ↓ 重复直到只剩一个区域 ↓ 输出所有层次的候选框 ``` ### 多策略组合 Selective Search采用多种策略组合提升召回率: - **颜色空间**:RGB、HSV、Lab - **相似度组合**:不同权重组合 - **阈值变化**:不同的初始化阈值 最终生成约**2000个候选区域**。 ### 优缺点 | 优点 | 缺点 | |-----|------| | 召回率高,几乎不漏检 | 速度慢(CPU需2秒) | | 不需要训练 | 候选框质量参差不齐 | | 通用性强,不依赖特定类别 | 无法针对特定任务优化 |
## CNN特征提取 ### R-CNN中的CNN R-CNN使用**预训练的CNN模型**提取每个候选区域的特征。 ### 网络架构 R-CNN论文中使用的是**AlexNet**(后来也可用VGG等更深的网络): ``` 候选区域 (约2000个) ↓ 变形(warp)到固定尺寸 227×227 ↓ CNN前向传播 (AlexNet/VGG16) ↓ 提取特征 (4096维向量) ↓ 后续分类/回归 ``` ### 特征提取细节 **1. 区域变形** - 将任意大小的候选区域变形到227×227 - 方法:直接拉伸或保留长宽比填充 **2. CNN前向传播** - 每个区域独立进行一次前向传播 - 使用ImageNet预训练权重 - 提取fc7层输出作为特征(4096维) **3. 特征存储** - 提前计算所有区域的特征并保存 - 减少训练时的重复计算 ### 计算复杂度 R-CNN的主要瓶颈在于CNN特征提取: ``` 时间复杂度 = N × T CNN ``` - N: 候选区域数量 (~2000) - T CNN: 单次CNN前向传播时间 (~0.05秒) - 总时间: ~100秒/图像 ### 网络选择 | 网络层数 | 特征维度 | 精度 | 速度 | |---------|---------|------|------| | AlexNet (8层) | 4096 | 58.5% | 基准 | | VGG16 (16层) | 4096 | 66.0% | 更慢 |
## SVM分类与边界框回归 ### SVM分类 R-CNN为**每个类别**训练一个**二分类SVM**。 ### 训练过程 **1. 正样本准备** - 候选框与真实框IoU > 0.5 - 每个类别收集约1000个正样本 **2. 负样本准备** - IoU < 0.3(背景) - 随机采样,保持正负比例平衡 **3. 特征输入** ``` 输入: CNN提取的4096维特征 训练: 线性SVM (类别k vs 非类别k) 输出: 分类得分 ``` **4. 硬负挖掘** - 初步训练后,用难分样本重新训练 - 提升分类器性能 ### 边界框回归 针对每个检测到的框,训练一个回归器微调位置。 **输入特征**: ``` φ(P) = [P_x, P_y, P_w, P_h] # 候选框坐标 φ(G) = [G_x, G_y, G_w, G_h] # 真实框坐标 ``` **学习目标**:预测变换 ``` t_* = T(P_*) # 预测变换 其中: t_x = (G_x - P_x) / P_w t_y = (G_y - P_y) / P_h t_w = log(G_w / P_w) t_h = log(G_h / P_h) ``` **损失函数**(正则化最小二乘): ``` Loss = ||t_* - ŵ_*φ(P)||² + ||ŵ||² ``` ### Non-Maximum Suppression (NMS) 去除重复检测的后处理步骤: ``` 算法: 1. 按置信度排序所有检测框 2. 选择最高分的框 3. 删除与其IoU > 阈值(0.3)的框 4. 重复2-3直到所有框处理完 ``` ### 完整流程 ``` 候选区域 (2000个) ↓ CNN特征提取 ↓ SVM分类 (20个类别 × 2000) ↓ 边界框回归 ↓ NMS去除重复 ↓ 最终检测结果 ```
## 目标检测评估指标 ### IoU (Intersection over Union) IoU用于衡量两个边界框的重叠程度: ``` IoU = (A ∩ B) / (A ∪ B) = 重叠面积 / 并集面积 ``` **应用场景**: - 判断正负样本(IoU > 0.5为正样本) - 评估检测精度 - NMS去除重复检测 ### mAP (mean Average Precision) 平均精度均值是目标检测的主要评估指标。 **计算步骤**: 1. **计算每个类别的AP**: - 按置信度排序检测结果 - 计算Precision-Recall曲线 - 计算AP(曲线下面积) 2. **计算mAP**: ``` mAP = (AP_1 + AP_2 + ... + AP_C) / C ``` ### Precision-Recall曲线 ``` Precision = TP / (TP + FP) # 检测正确的比例 Recall = TP / (TP + FN) # 找出目标的比例 AP = ∫ Precision(Recall) dRecall ``` ### COCO评估指标 COCO数据集使用更全面的评估: | 指标 | 说明 | |-----|------| | AP | IoU=0.50:0.95的平均 | | AP50 | IoU=0.50的AP | | AP75 | IoU=0.75的AP | | AP_small | 小目标AP | | AP_medium | 中目标AP | | AP_large | 大目标AP | ### R-CNN性能 | 数据集 | mAP | 说明 | |-------|-----|------| | VOC 2010 | 53.3% | R-CNN (AlexNet) | | VOC 2012 | 53.1% | R-CNN (AlexNet) | | VOC 2012 | 66.0% | R-CNN (VGG16) | | | | |
R-CNN演进
## Fast R-CNN (2015) ### 核心改进 Ross Girshick在R-CNN之后提出Fast R-CNN,解决了R-CNN的主要缺陷。 ### R-CNN的问题 | 问题 | 说明 | |-----|------| | **重复计算** | 2000个区域分别前向传播,大量重复 | | **训练复杂** | 多阶段训练(CNN预训练+SVM+回归) | | **速度慢** | GPU上约0.2秒/图(特征提取) | | **空间信息丢失** | 区域变形破坏空间结构 | ### Fast R-CNN创新 **1. 共享卷积计算** ``` 输入图像 ↓ 整图CNN前向传播(一次) ↓ 卷积特征图 ↓ Selective Search区域映射到特征图 ↓ RoI Pooling ↓ 分类 + 回归 ``` **2. RoI Pooling** - 将任意大小的RoI特征区域池化到固定尺寸 - 两级池化: - 先按比例划分网格 - 每个格子内Max Pooling **3. 多任务损失** ``` L = L_cls + λ × L_bbox L_cls: 分类损失 (交叉熵) L_bbox: 边界框回归损失 (平滑L1) ``` **4. 端到端训练** - 统一网络,联合训练 - 不再需要单独的SVM ### 网络结构 ``` 输入图像 ↓ Conv Layers (共享) ↓ RoI Pooling ↓ FC Layers ↓ ├── 分类输出 (类别概率) └── 回归输出 (边界框调整) ``` ### 性能对比 | 模型 | mAP (VOC07) | 训练时间 | 测试速度 | |-----|-------------|---------|---------| | R-CNN | 66.0% | 84小时 | 50秒/图 | | Fast R-CNN | 66.9% | 9.5小时 | 0.32秒/图 |
## Faster R-CNN (2015) ### 核心突破 Faster R-CNN用**神经网络**替代Selective Search,实现**完全端到端**的检测。 ### Region Proposal Network (RPN) RPN是一个轻量级的卷积网络,用于生成候选区域。 **核心思想**: - 在共享的卷积特征图上滑动小网络 - 每个位置预测k个候选区域 - 通过锚框(Anchor)机制 ### RPN网络结构 ``` 共享卷积特征图 ↓ 3×3卷积 (256维) ↓ ┌─────────────┐ │ 1×1卷积 │ ├─────────────┤ │ 2k (目标性)│ │ 4k (坐标) │ └─────────────┘ ``` ### Anchor机制 每个位置预设k个锚框(通常k=9): ``` 3种尺度: 128², 256², 512² 3种比例: 1:1, 1:2, 2:1 共9个Anchor ``` ### RPN损失 ``` L = L_cls + λ × L_bbox L_cls: 目标性损失 (是否有目标) L_bbox: 边界框回归 (仅对正Anchor) ``` **正样本定义**: - 与真实框IoU最高的Anchor - IoU > 0.7的Anchor **负样本定义**: - IoU < 0.3的Anchor ### 端到端训练 **4步交替训练**: 1. 训练RPN 2. 用RPN结果训练Fast R-CNN 3. 微调RPN(共享卷积层) 4. 再次微调Fast R-CNN ### 性能提升 | 模型 | mAP (VOC07) | 候选区域生成时间 | |-----|-------------|----------------| | Fast R-CNN + SS | 66.9% | 2秒 (CPU) | | Faster R-CNN | 73.2% | 0.01秒 (GPU) |
## RoI Pooling详解 ### RoI Pooling作用 将不同大小的候选区域特征映射到固定尺寸,以便输入全连接层。 ### 工作原理 **输入**: - 卷积特征图 (H×W×C) - RoI坐标 (x, y, w, h) **输出**: - 固定尺寸特征 (如7×7×C) **操作**: ``` 1. 将RoI划分为 k×k 个子区域 (子区域大小 = RoI尺寸 / k) 2. 对每个子区域进行Max Pooling 3. 输出 k×k×C 的特征 ``` ### 示例 假设卷积特征图大小为 28×28,RoI为(14,14,256): ``` 目标输出: 7×7×256 1. 将14×14划分为7×7网格 每个格子: 2×2 2. 每个格子内取最大值 (2×2 → 1) 3. 输出7×7×256 ``` ### RoI Align改进 RoI Pooling存在**量化误差**问题(两次取整)。 **RoI Align**(Mask R-CNN提出): - 不进行量化 - 使用双线性插值 - 保持空间精度 ### 代码示意 ```python # RoI Pooling 伪代码 def roi_pooling(feature_map, rois, output_size): pooled_features = [] for roi in rois: # 提取RoI区域 roi_feature = feature_map[:, roi.y:roi.y+roi.h, roi.x:roi.x+roi.w] # 调整大小 pooled = resize(roi_feature, output_size, mode='max') pooled_features.append(pooled) return torch.stack(pooled_features) ```
## RPN详解 ### RPN完整架构 Region Proposal Network是Faster R-CNN的核心创新。 ### 网络详解 ``` 输入: 共享卷积特征图 (约512×38×38) ↓ 3×3卷积 (512通道) ↓ ┌───┴───┐ ↓ ↓ 1×1 Conv 1×1 Conv (2×9×k) (4×9×k) ↓ ↓ 目标性 边界框 (18通道) (36通道) ``` ### Anchor计算 **9个Anchor的位置**(每个滑动窗口位置): ``` Scale: {128, 256, 512} Ratio: {0.5, 1.0, 2.0} 生成: 128×128, 128×256, 128×512 256×256, 256×512, 256×1024 512×512, 512×1024, 512×2048 ``` ### 坐标变换 RPN输出的是相对于Anchor的偏移量: ``` tx = (x - xa) / wa ty = (y - ya) / ha tw = log(w / wa) th = log(h / ha) 逆变换: x = wa × exp(tx) + xa y = ha × exp(ty) + ya w = wa × exp(tw) h = ha × exp(th) ``` ### 训练样本分配 **每个mini-batch**: - 256个Anchor - 正样本: 128个 (IoU > 0.7) - 负样本: 128个 (IoU < 0.3) - 忽略: 中等IoU的Anchor ### Proposal后处理 ``` RPN输出 (~12000个Anchor) ↓ 去除超出边界的 ↓ 按置信度排序,取前2000 ↓ NMS (IoU阈值0.7) ↓ 最终约300个候选区域 ``` ### 为什么RPN比SS快? | 方法 | 计算方式 | 速度 | |-----|---------|------| | Selective Search | CPU运行传统算法 | 2秒 | | RPN | GPU卷积 + 共享特征 | 0.01秒 | RPN复用了检测网络的卷积特征,几乎不增加额外计算量。
## Two-Stage检测器总结 ### 系列对比 | 特性 | R-CNN | Fast R-CNN | Faster R-CNN | |-----|-------|-----------|-------------| | 候选区域 | Selective Search | Selective Search | RPN | | 特征提取 | 独立CNN | 共享CNN | 共享CNN | | 训练方式 | 多阶段 | 端到端 | 端到端 | | 测试速度 | ~50秒/图 | ~2秒/图 | ~0.2秒/图 | | mAP (VOC07) | 66.0% | 66.9% | 73.2% | ### Two-Stage特点 **优点**: - 精度高,尤其是小目标 - 候选区域聚焦,减少背景干扰 - 灵活的特征提取 **缺点**: - 速度相对较慢(相比YOLO/SSD) - 架构复杂,训练调试困难 - 内存占用较大 ### One-Stage vs Two-Stage | 对比维度 | One-Stage (YOLO/SSD) | Two-Stage (Faster R-CNN) | |---------|---------------------|--------------------------| | 流程 | 直接回归 | 先提议再检测 | | 速度 | 快 (45+ FPS) | 较慢 (5-10 FPS) | | 精度 | 略低 | 高 | | 小目标 | 较弱 | 较强 | | 负样本 | 极度不平衡 | 通过提议过滤 | ### 发展脉络 ``` R-CNN (2014) ↓ 解决: 重复计算 Fast R-CNN (2015) ↓ 解决: Selective Search瓶颈 Faster R-CNN (2015) ↓ 扩展: 更多功能 Mask R-CNN (2017) ↓ 优化: 架构改进 Cascade R-CNN (2018) ``` ### 现代Two-Stage检测器 **Mask R-CNN**:添加分割分支 **Cascade R-CNN**:级联回归提升IoU **PointRend**:精细化分割 **Sparse R-CNN**:稀疏注意力机制
三个R-CNN模型网络
## R-CNN 网络结构 ### 模型特点 R-CNN 对**每个候选区域独立**进行 CNN 特征提取,是三阶段、非共享的架构。 ``` 输入图像 ↓ Selective Search → 约2000个候选区域 ↓ 每个区域 warp 到 227×227 ↓ AlexNet 独立前向传播(重复2000次) ↓ SVM 分类 + 边界框回归 ``` ### 核心缺陷 - **特征不共享**:2000个区域各算一次 CNN,计算量 = 2000 × T_cnn - **多阶段训练**:CNN → SVM → BBox Reg 三个独立模型 - **空间信息丢失**:warp 破坏原始比例 ### 网络代码(独立特征提取) ```python import torch import torch.nn as nn class RCNNFeatureExtractor(nn.Module): """R-CNN: 使用 AlexNet 提取单个区域的 4096 维特征""" def __init__(self): super().__init__() # 卷积特征提取部分 self.features = nn.Sequential( nn.Conv2d(3, 96, 11, stride=4), nn.ReLU(), nn.MaxPool2d(3, stride=2), nn.Conv2d(96, 256, 5, padding=2), nn.ReLU(), nn.MaxPool2d(3, stride=2), nn.Conv2d(256, 384, 3, padding=1), nn.ReLU(), nn.Conv2d(384, 384, 3, padding=1), nn.ReLU(), nn.Conv2d(384, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(3, stride=2), ) # 全连接层,输出 4096 维特征 self.classifier = nn.Sequential( nn.Linear(256 * 6 * 6, 4096), nn.ReLU(), nn.Linear(4096, 4096), nn.ReLU(), ) def forward(self, x): # x: 单个候选区域 warp 后的 227×227 图像 x = self.features(x) x = torch.flatten(x, 1) return self.classifier(x) # 4096维特征 # R-CNN 的致命点:必须循环处理每个候选区域 extractor = RCNNFeatureExtractor() features = [extractor(region) for region in proposals] # 重复2000次! ```
## Fast R-CNN 网络 ### 模型特点 Fast R-CNN 的关键改进是**共享卷积计算** + **RoI Pooling**,图像只前向传播一次。 ``` 输入图像 ↓ VGG16 卷积层(整图,只算1次)→ 共享特征图 ↓ 候选区域映射到特征图 ↓ ★ RoI Pooling(固定尺寸输出)★ ← ROI 的核心作用 ↓ 全连接层 → 分类 + 边界框回归(端到端) ``` ### RoI 的作用(对应代码) RoI Pooling 解决了「不同大小候选区域无法输入全连接层」的问题: - 把任意尺寸的特征区域池化为**固定 7×7** - 这样后续 FC 层输入维度恒定 ### 网络代码(共享卷积 + RoI Pooling) ```python import torch import torch.nn as nn class FastRCNN(nn.Module): """Fast R-CNN: 共享卷积 + RoI Pooling""" def __init__(self, num_classes=20): super().__init__() # 1. 共享卷积 backbone(整图只算一次) self.backbone = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, stride=2), # ...更多卷积块... nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, stride=2), ) # ★★★ ROI 作用对应代码:RoI Pooling ★★★ # 将任意大小候选区域池化到固定 7×7,输出维度恒定 self.roi_pool = nn.AdaptiveMaxPool2d((7, 7)) # 2. 全连接分类 + 回归头 self.fc = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(), nn.Linear(4096, 4096), nn.ReLU(), ) self.cls_score = nn.Linear(4096, num_classes) # 分类 self.bbox_pred = nn.Linear(4096, num_classes * 4) # 回归 def forward(self, image, rois): # 整图共享卷积(只算1次)— 这是 Fast 比 R-CNN 快的关键 feat_map = self.backbone(image) # [1, 512, H/16, W/16] # 对每个候选区域在特征图上做 RoI Pooling pooled = [] for roi in rois: x1, y1, x2, y2 = roi roi_feat = feat_map[:, :, y1:y2, x1:x2] # 取出RoI区域 pooled.append(self.roi_pool(roi_feat)) # → 7×7 pooled = torch.cat(pooled) # [N, 512, 7, 7] x = torch.flatten(pooled, 1) x = self.fc(x) return self.cls_score(x), self.bbox_pred(x) ```
## Faster R-CNN 网络 ### 模型特点 Faster R-CNN 用 **RPN(区域提议网络)** 替代 Selective Search,实现完全端到端。 ``` 输入图像 ↓ 共享卷积 Backbone(VGG16/ResNet) ↓ 共享特征图 ──────────┐ ↓ ↓ ★ RPN 生成候选区域 ★ ★ RoI Pooling ★ ↓ ↓ └──→ proposals →→ 全连接 → 分类 + 回归 ``` ### RPN 的作用(对应代码) RPN 是一个轻量卷积网络,在共享特征图上滑动,为每个位置预测 k 个 Anchor 的「目标性」和「坐标偏移」,从而用神经网络自动生成候选区域(替代慢速的 Selective Search)。 ### 完整网络代码 ```python import torch import torch.nn as nn class FasterRCNN(nn.Module): """Faster R-CNN: RPN + RoI Pooling 端到端""" def __init__(self, num_classes=20, num_anchors=9): super().__init__() # 1. 共享卷积 backbone self.backbone = self._build_backbone() # ★★★ RPN 作用对应代码:区域提议网络 ★★★ self.rpn = RPN(in_channels=512, num_anchors=num_anchors) # ★★★ ROI 作用对应代码:RoI Pooling ★★★ self.roi_pool = nn.AdaptiveMaxPool2d((7, 7)) # 2. 检测头 self.fc = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(), nn.Linear(4096, 4096), nn.ReLU(), ) self.cls_score = nn.Linear(4096, num_classes) self.bbox_pred = nn.Linear(4096, num_classes * 4) def _build_backbone(self): return nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), # ...简化版 VGG16... nn.Conv2d(64, 512, 3, padding=1), nn.ReLU(), ) def forward(self, image): feat_map = self.backbone(image) # 共享特征图 proposals, rpn_cls, rpn_reg = self.rpn(feat_map) # RPN 生成候选区域 pooled = [] for roi in proposals: x1, y1, x2, y2 = roi.long() pooled.append(self.roi_pool(feat_map[:, :, y1:y2, x1:x2])) pooled = torch.cat(pooled) if pooled else torch.zeros(0, device=image.device) x = torch.flatten(pooled, 1) x = self.fc(x) return self.cls_score(x), self.bbox_pred(x), rpn_cls, rpn_reg ```
## RoI Pooling 实现(ROI 作用详解) ### RoI 的作用 RoI(Region of Interest)Pooling 解决 Faster/Fast R-CNN 中**候选区域尺寸不一**的问题: 1. 不同候选区域映射到特征图后大小不同 2. 全连接层要求固定输入维度 3. RoI Pooling 把任意大小区域统一池化到固定尺寸(如 7×7) ``` RoI 特征区域 (任意 H×W) ↓ 划分为 7×7 个子区域 ↓ 每个子区域做 Max Pooling ↓ 输出固定 7×7 特征 ``` ### 完整实现代码 ```python import torch import torch.nn as nn class RoIPooling(nn.Module): """RoI Pooling 完整实现 ROI 的核心作用: - 输入: 任意大小的 RoI 特征区域 - 输出: 固定大小 (output_size × output_size) 的特征 - 意义: 使后续全连接层维度恒定 """ def __init__(self, output_size=(7, 7), spatial_scale=1.0/16): super().__init__() self.output_size = output_size # 目标固定尺寸 self.spatial_scale = spatial_scale # 特征图相对原图的下采样比 def forward(self, feature_map, rois): # feature_map: [B, C, H, W] 共享卷积特征图 # rois: [N, 5] (batch_idx, x1, y1, x2, y2) out_h, out_w = self.output_size pooled_features = [] for roi in rois: batch_idx, x1, y1, x2, y2 = roi # 将原图坐标映射到特征图坐标(spatial_scale) x1 = int(x1 * self.spatial_scale) y1 = int(y1 * self.spatial_scale) x2 = int(x2 * self.spatial_scale) y2 = int(y2 * self.spatial_scale) # 取出该 RoI 对应的特征区域(任意大小) roi_feat = feature_map[batch_idx, :, y1:y2, x1:x2] c = roi_feat.shape[0] # 子区域大小(可能不整除,需向上取整保证覆盖) sub_h = max((y2 - y1) // out_h, 1) sub_w = max((x2 - x1) // out_w, 1) # ★ ROI 作用核心:Max Pooling 到固定尺寸 ★ pooled = nn.functional.max_pool2d( roi_feat.unsqueeze(0), kernel_size=(sub_h, sub_w), stride=(sub_h, sub_w), ) # 若池化后尺寸仍不匹配,自适应调整到 output_size if pooled.shape[-2:] != (out_h, out_w): pooled = nn.functional.adaptive_max_pool2d( pooled, (out_h, out_w)) pooled_features.append(pooled.squeeze(0)) return torch.stack(pooled_features) # [N, C, out_h, out_w] # RoI Align 改进:用双线性插值避免量化取整误差 # 代码中使用 torchvision.ops.roi_align 即可 import torchvision.ops as ops # pooled = ops.roi_align(feature_map, rois, output_size=(7,7)) ```
## RPN 实现(RPN 原理与作用) ### RPN 的原理与作用 RPN(Region Proposal Network)是 Faster R-CNN 的核心创新: 1. **原理**:在共享特征图上用 3×3 卷积滑动,每个位置预测 9 个 Anchor(3尺度×3比例)的目标性和坐标偏移 2. **作用**:用神经网络自动生成高质量候选区域,替代 CPU 上的 Selective Search(2秒→0.01秒) 3. **优势**:与检测网络共享卷积特征,几乎不增加计算量 ``` 共享特征图 (H×W×512) ↓ 3×3 卷积(滑动窗口) ↓ ┌────────────┬────────────┐ │ 1×1 卷积 │ 1×1 卷积 │ │ 2×9=18 通道 │ 4×9=36通道 │ │ (目标性) │ (坐标偏移) │ └────────────┴────────────┘ ``` ### 完整实现代码 ```python import torch import torch.nn as nn class RPN(nn.Module): """Region Proposal Network 完整实现 RPN 原理:滑动窗口 + Anchor 机制 RPN 作用:神经网络自动生成候选区域 """ def __init__(self, in_channels=512, num_anchors=9): super().__init__() self.num_anchors = num_anchors # ★ RPN 原理对应代码:3×3 滑动窗口卷积 ★ # 在共享特征图上滑动,融合空间上下文 self.conv = nn.Conv2d(in_channels, in_channels, 3, padding=1) self.relu = nn.ReLU() # ★ RPN 作用对应代码:两个 1×1 卷积分支 ★ # 分支1: 预测每个 Anchor 是否包含目标 (objectness) self.cls_layer = nn.Conv2d(in_channels, num_anchors * 2, 1) # 分支2: 预测每个 Anchor 的坐标偏移 (dx, dy, dw, dh) self.bbox_layer = nn.Conv2d(in_channels, num_anchors * 4, 1) def forward(self, feature_map): # feature_map: [B, 512, H, W] 共享卷积特征图 B, _, H, W = feature_map.shape # 1. 滑动窗口卷积(融合每个位置的邻域信息) t = self.relu(self.conv(feature_map)) # [B, 512, H, W] # 2. 目标性预测:每个位置 num_anchors 个框 × 2(前景/背景) rpn_cls = self.cls_layer(t) # [B, 18, H, W] rpn_cls = rpn_cls.permute(0, 2, 3, 1).reshape(B, -1, 2) # 3. 坐标回归:每个位置 num_anchors 个框 × 4(坐标) rpn_reg = self.bbox_layer(t) # [B, 36, H, W] rpn_reg = rpn_reg.permute(0, 2, 3, 1).reshape(B, -1, 4) # 4. 生成 Anchor(9种尺度×比例) anchors = self._generate_anchors(H, W) # [B, H*W*9, 4] # 5. RPN 作用:用预测的偏移修正 Anchor 得到候选区域 proposals = self._decode_proposals(anchors, rpn_reg) # 6. 后处理:排序 + NMS,取前 300 个 proposals = self._filter_proposals(proposals, rpn_cls) return proposals, rpn_cls, rpn_reg def _generate_anchors(self, H, W): # 3 种尺度 × 3 种比例 = 9 个基准 Anchor scales = [64, 128, 256] ratios = [0.5, 1.0, 2.0] anchors = [] for s in scales: for r in ratios: h = s * (r ** 0.5) w = s / (r ** 0.5) anchors.append([-w/2, -h/2, w/2, h/2]) # 在每个特征点位置放置这 9 个 Anchor return torch.tensor(anchors) # [9, 4] def _decode_proposals(self, anchors, regs): # 用回归偏移修正 Anchor 坐标 xa = (anchors[:, 0] + anchors[:, 2]) / 2 ya = (anchors[:, 1] + anchors[:, 3]) / 2 wa = (anchors[:, 2] - anchors[:, 0]) ha = (anchors[:, 3] - anchors[:, 1]) # 逆变换:把网络输出的偏移转成实际框坐标 tx, ty, tw, th = regs[..., 0], regs[..., 1], regs[..., 2], regs[..., 3] cx = tx * wa + xa cy = ty * ha + ya w = torch.exp(tw) * wa h = torch.exp(th) * ha return torch.stack([cx-w/2, cy-h/2, cx+w/2, cy+h/2], dim=-1) def _filter_proposals(self, proposals, scores): # 按前景得分排序,取前 12000 scores = scores.softmax(dim=-1)[:, :, 1] # 取前景概率 _, order = scores.sort(descending=True) proposals = proposals[order[:12000]] # NMS 去重(IoU 阈值 0.7) from torchvision.ops import nms keep = nms(proposals, scores[order[:12000]], 0.7) return proposals[keep[:300]] # 最终 300 个候选区域 ```
Faster R-CNN 训练预测实战
### 全流程概览 Faster R-CNN 完整实战流程:**数据准备 → Anchor生成与样本分配 → 网络定义 → 联合训练(RPN+检测头) → 预测推理 → 评估** 本示例使用 PyTorch 搭建简化版 Faster R-CNN,自动生成随机数据演示全流程,无需下载 VOC 数据集。 ### 1. 环境准备 ```python import torch import torch.nn as nn import torch.nn.functional as F from torchvision.ops import nms DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' NUM_CLASSES = 3 # 背景 + 猫 + 狗(示例) print(f"使用设备: {DEVICE}") ``` ### 2. Anchor 生成 ```python def generate_anchors(feature_h, feature_w, stride=16, scales=[64, 128, 256], ratios=[0.5, 1.0, 2.0]): """在特征图每个位置生成 9 个 Anchor""" base = [] for s in scales: for r in ratios: h = s * (r ** 0.5) w = s / (r ** 0.5) base.append([-w/2, -h/2, w/2, h/2]) base = torch.tensor(base) # [9, 4] # 在每个特征点放置 Anchor(映射回原图坐标) anchors = [] for y in range(feature_h): for x in range(feature_w): cx = x * stride + stride / 2 cy = y * stride + stride / 2 anchors.append(base + torch.tensor([cx, cy, cx, cy])) return torch.stack(anchors) # [H*W*9, 4] ``` ### 3. 正负样本分配 ```python def assign_targets(anchors, gt_boxes, pos_iou=0.7, neg_iou=0.3): """根据 IoU 划分 Anchor 的正负样本""" ious = box_iou(anchors, gt_boxes) # [num_anchors, num_gt] max_iou, gt_idx = ious.max(dim=1) labels = torch.zeros(len(anchors), dtype=torch.long) # 0=背景 labels[max_iou < neg_iou] = 0 # 负样本 labels[max_iou >= pos_iou] = 1 # 正样本 # 保证每个 gt 至少有一个正样本 for i in range(len(gt_boxes)): best_anchor = ious[:, i].argmax() labels[best_anchor] = 1 return labels, gt_idx ```
### Faster R-CNN 网络架构回顾 ``` 输入图像 (3, H, W) ↓ Backbone (ResNet/VGG) → 共享特征图 ↓ ┌─────── RPN (生成候选区域) ───────┐ │ 3×3 Conv → 1×1 Conv (cls + reg) │ └───────────────────────────────────┘ ↓ proposals (300个) RoI Pooling (→ 7×7) ↓ FC Layers → 分类 + 边界框回归 ``` ### 1. 共享 Backbone ```python class SimpleBackbone(nn.Module): """简化版 VGG 风格 backbone,输出步长16""" def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), # /2 nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), # /4 nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), # /8 nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(), nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(), ) def forward(self, x): return self.features(x) # stride≈8(示例简化) ``` ### 2. RPN 模块 ```python class RPNHead(nn.Module): def __init__(self, in_ch=512, num_anchors=9): super().__init__() self.conv = nn.Conv2d(in_ch, in_ch, 3, padding=1) self.cls = nn.Conv2d(in_ch, num_anchors * 2, 1) # 目标性 self.reg = nn.Conv2d(in_ch, num_anchors * 4, 1) # 坐标 def forward(self, x): t = F.relu(self.conv(x)) return self.cls(t), self.reg(t) ``` ### 3. RoI Pooling + 检测头 ```python class RoIHead(nn.Module): """RoI Pooling + FC 分类回归头""" def __init__(self, num_classes=3): super().__init__() self.roi_pool = nn.AdaptiveMaxPool2d((7, 7)) self.fc = nn.Sequential( nn.Linear(512 * 7 * 7, 1024), nn.ReLU(), nn.Linear(1024, 1024), nn.ReLU(), ) self.cls = nn.Linear(1024, num_classes) self.reg = nn.Linear(1024, num_classes * 4) def forward(self, feat, proposals): pooled = [self.roi_pool(feat[:, :, p[1]:p[3], p[0]:p[2]]) for p in proposals] pooled = torch.cat(pooled).flatten(1) x = self.fc(pooled) return self.cls(x), self.reg(x) ``` ### 4. 完整模型 ```python class FasterRCNNModel(nn.Module): def __init__(self, num_classes=3, num_anchors=9): super().__init__() self.backbone = SimpleBackbone() self.rpn = RPNHead(512, num_anchors) self.roi_head = RoIHead(num_classes) def forward(self, image, proposals=None): feat = self.backbone(image) rpn_cls, rpn_reg = self.rpn(feat) if proposals is None: proposals = self.get_proposals(rpn_cls, rpn_reg) cls, reg = self.roi_head(feat, proposals) return cls, reg, rpn_cls, rpn_reg, proposals def get_proposals(self, rpn_cls, rpn_reg): # 简化:随机生成示例 proposals return torch.tensor([[0, 10, 10, 100, 100], [0, 50, 50, 200, 200]], dtype=torch.float) ```
### 多任务损失 Faster R-CNN 训练包含两部分损失: ``` L_total = L_RPN + L_RoI L_RPN = L_cls(RPN目标性) + λ·L_reg(RPN坐标) L_RoI = L_cls(类别分类) + λ·L_reg(检测框坐标) ``` ### 1. 损失函数实现 ```python def smooth_l1_loss(pred, target, beta=1.0): """平滑 L1 损失,对异常值更鲁棒""" diff = torch.abs(pred - target) loss = torch.where(diff < beta, 0.5 * diff ** 2 / beta, diff - 0.5 * beta) return loss.mean() def rpn_loss(rpn_cls, rpn_reg, labels, targets, num_anchors=9): """RPN 损失:分类(目标性) + 回归""" rpn_cls = rpn_cls.permute(0, 2, 3, 1).reshape(-1, 2) rpn_reg = rpn_reg.permute(0, 2, 3, 1).reshape(-1, 4) # 只对正样本计算回归损失 pos = labels == 1 cls_loss = F.cross_entropy(rpn_cls, labels) reg_loss = smooth_l1_loss(rpn_reg[pos], targets[pos]) if pos.sum() > 0 else 0 return cls_loss + 2.0 * reg_loss def roi_loss(cls_pred, reg_pred, cls_gt, reg_gt): """RoI 检测头损失""" cls_loss = F.cross_entropy(cls_pred, cls_gt) reg_loss = smooth_l1_loss(reg_pred, reg_gt) return cls_loss + 1.0 * reg_loss ``` ### 2. 训练循环 ```python import torch.optim as optim model = FasterRCNNModel(num_classes=NUM_CLASSES).to(DEVICE) optimizer = optim.SGD(model.parameters(), lr=0.005, momentum=0.9, weight_decay=5e-4) EPOCHS = 5 for epoch in range(EPOCHS): model.train() for images, gt_boxes, gt_labels in train_loader: images = images.to(DEVICE) optimizer.zero_grad() # 前向传播 cls, reg, rpn_cls, rpn_reg, proposals = model(images) # 计算损失(RPN + RoI 双重损失) rpn_labels, rpn_targets = assign_targets(all_anchors, gt_boxes) loss_rpn = rpn_loss(rpn_cls, rpn_reg, rpn_labels, rpn_targets) loss_roi = roi_loss(cls, reg, roi_labels, roi_targets) loss = loss_rpn + loss_roi loss.backward() optimizer.step() print(f"Epoch {epoch+1}/{EPOCHS} loss={loss.item():.4f} " f"rpn={loss_rpn.item():.4f} roi={loss_roi.item():.4f}") ```
### 端到端预测流程 ``` 输入图像 ↓ Backbone → 共享特征图 ↓ RPN → proposals (300) ↓ RoI Pooling + FC → 类别 + 坐标 ↓ 阈值过滤 + NMS ↓ 最终检测结果 ``` ### 预测代码 ```python @torch.no_grad() def predict(model, image, conf_thresh=0.8, nms_thresh=0.3): """Faster R-CNN 端到端推理""" model.eval() image = image.to(DEVICE) # 1. 前向传播 cls, reg, rpn_cls, rpn_reg, proposals = model(image) # 2. 计算类别概率 scores = F.softmax(cls, dim=1) # [N, num_classes] labels = scores.argmax(dim=1) # 预测类别 confs = scores.max(dim=1).values # 置信度 # 3. 过滤背景 + 置信度阈值 keep = (labels > 0) & (confs > conf_thresh) boxes = decode_boxes(proposals, reg)[keep] labels = labels[keep] confs = confs[keep] # 4. 按类别做 NMS final_boxes, final_labels, final_scores = [], [], [] for c in labels.unique(): idx = (labels == c) keep_idx = nms(boxes[idx], confs[idx], nms_thresh) final_boxes.append(boxes[idx][keep_idx]) final_labels.append(labels[idx][keep_idx]) final_scores.append(confs[idx][keep_idx]) return torch.cat(final_boxes), torch.cat(final_labels), torch.cat(final_scores) def decode_boxes(proposals, reg): """用 RoI 回归偏移修正 proposals 坐标""" px = (proposals[:, 0] + proposals[:, 2]) / 2 py = (proposals[:, 1] + proposals[:, 3]) / 2 pw = proposals[:, 2] - proposals[:, 0] ph = proposals[:, 3] - proposals[:, 1] dx, dy, dw, dh = reg[:, 0], reg[:, 1], reg[:, 2], reg[:, 3] gx = dx * pw + px gy = dy * ph + py gw = torch.exp(dw) * pw gh = torch.exp(dh) * ph return torch.stack([gx-gw/2, gy-gh/2, gx+gw/2, gy+gh/2], dim=1) ``` ### 可视化 ```python import matplotlib.pyplot as plt import matplotlib.patches as patches CLASSES = ['__bg__', 'cat', 'dog'] def visualize(image, boxes, labels, scores): fig, ax = plt.subplots(1) ax.imshow(image.permute(1, 2, 0).cpu()) for box, label, score in zip(boxes, labels, scores): x1, y1, x2, y2 = box.tolist() rect = patches.Rectangle((x1, y1), x2-x1, y2-y1, linewidth=2, edgecolor='lime', fill=False) ax.add_patch(rect) ax.text(x1, y1-5, f"{CLASSES[label]} {score:.2f}", color='lime', fontsize=12) plt.axis('off') plt.show() # boxes, labels, scores = predict(model, test_image) # visualize(test_image, boxes, labels, scores) ```
### 评估指标 mAP ```python def compute_ap(rec, prec): """计算单个类别的 AP(使用11点插值)""" ap = 0.0 for t in np.arange(0, 1.1, 0.1): p = prec[rec >= t].max() if (rec >= t).any() else 0 ap += p / 11 return ap def compute_map(all_detections, all_ground_truths, iou_thresh=0.5): """计算 mAP""" aps = [] for c in range(1, NUM_CLASSES): detections = all_detections[c] gt = all_ground_truths[c] # 按置信度排序 detections = sorted(detections, key=lambda x: x['score'], reverse=True) tp = np.zeros(len(detections)) fp = np.zeros(len(detections)) for i, det in enumerate(detections): ious = [box_iou(det['box'], g) for g in gt] best = max(ious) if ious else 0 if best >= iou_thresh: tp[i] = 1 else: fp[i] = 1 tp = np.cumsum(tp); fp = np.cumsum(fp) recall = tp / max(len(gt), 1) precision = tp / (tp + fp + 1e-8) aps.append(compute_ap(recall, precision)) return np.mean(aps) # mAP ``` ### 三模型性能对比 | 模型 | mAP (VOC07) | 速度 | 候选区域 | 特征提取 | |-----|-------------|------|---------|---------| | R-CNN | 66.0% | 50秒/图 | Selective Search | 独立CNN | | Fast R-CNN | 66.9% | 0.32秒/图 | Selective Search | 共享卷积+RoI Pooling | | **Faster R-CNN** | **73.2%** | **0.2秒/图** | **RPN** | 共享卷积+RPN+RoI | ### 训练调优要点 1. **Anchor 尺度**:需匹配数据集中目标尺寸分布 2. **正负样本比例**:RPN 保持 1:1,检测头用 OHEM 处理不平衡 3. **学习率策略**:Warmup + Step Decay,RPN 单独预热 4. **多尺度训练**:提升对不同尺寸目标的鲁棒性
### 个人思考 **为什么 Two-Stage 精度更高?** Faster R-CNN 通过 RPN 先「聚焦」可能含目标的区域,再精分类,天然抑制了背景干扰,正负样本比 YOLO 更平衡,因此小目标检测能力更强。 **RPN 与 RoI Pooling 的协同** 两者都建立在**共享特征图**上:RPN 用它生成 proposals,RoI Pooling 用它提取 proposal 特征。这种共享是速度提升的根本原因——卷积计算只做一次。 **工程实践建议** - 生产环境推荐使用 `torchvision.models.detection.fasterrcnn_resnet50_fpn`,它已实现 RoI Align、FPN、平衡采样等最佳实践 - Anchor-free 检测器(FCOS、CornerNet)省去了 Anchor 调参,逐渐成为新趋势 - 对于实时性要求高的场景,YOLO/SSD 更合适;追求精度则选 Faster R-CNN 系列 **演进脉络的个人理解** ``` R-CNN 证明了「深度学习+候选区域」可行 ↓ Fast R-CNN 解决「重复计算」→ 引入 RoI Pooling ↓ Faster R-CNN 解决「Selective Search 瓶颈」→ 引入 RPN ↓ 核心思想:让网络自己学习「去哪里看」和「看什么」 ``` 这三大组件(Backbone 共享、RPN、RoI Pooling)奠定了现代 Two-Stage 检测器的基石。
参考
    R-CNN论文: Rich feature hierarchies for accurate object detection and semantic segmentation
    Fast R-CNN论文: Fast R-CNN
    Faster R-CNN论文: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
    Faster R-CNN Python实现
    Detectron2 (Facebook目标检测框架)