R-CNN 2014 论文复现:从 Selective Search 到 SVM 分类的 5 个核心步骤拆解

📅 2026/7/8 23:51:39
R-CNN 2014 论文复现:从 Selective Search 到 SVM 分类的 5 个核心步骤拆解
R-CNN 2014 论文复现从 Selective Search 到 SVM 分类的 5 个核心步骤拆解在计算机视觉领域目标检测一直是一个核心挑战。2014年Ross Girshick等人提出的R-CNNRegions with CNN features算法彻底改变了这一领域的格局。本文将带您深入复现这一里程碑式的工作从理论到代码实现一步步拆解其中的关键技术细节。1. 环境准备与数据预处理复现R-CNN的第一步是搭建合适的开发环境。我们推荐使用Python 3.8和PyTorch 1.7的组合这套环境既能保证现代深度学习库的支持又与原始论文使用的Caffe框架保持兼容性。依赖安装清单pip install torch torchvision opencv-python numpy scikit-learn matplotlib对于数据集我们使用PASCAL VOC 2007作为基准。这个数据集包含20个常见物体类别共有5011张训练验证图片和4952张测试图片。数据预处理需要注意几个关键点图像保持原始宽高比仅对候选区域进行resize候选区域周围添加16像素的padding论文中的关键技巧采用各向异性缩放anisotropic warping到227×227像素import cv2 import numpy as np def preprocess_region(region, img, padding16): 处理候选区域添加padding并resize到227x227 :param region: (xmin, ymin, xmax, ymax) :param img: 原始图像 :param padding: 扩展像素数 x1, y1, x2, y2 region h, w img.shape[:2] # 添加padding x1 max(0, x1 - padding) y1 max(0, y1 - padding) x2 min(w, x2 padding) y2 min(h, y2 padding) # 各向异性缩放 region_img img[y1:y2, x1:x2] resized cv2.resize(region_img, (227, 227)) # 减去ImageNet均值 (BGR顺序) mean np.array([103.939, 116.779, 123.68]) normalized resized - mean return normalized.transpose(2, 0, 1) # 转为C×H×W2. Selective Search 区域生成R-CNN的核心创新之一是利用Selective Search算法生成类别无关的候选区域region proposals。相比传统的滑动窗口方法Selective Search通过层次化分组生成约2000个高质量候选框大幅减少了计算量。Selective Search关键步骤基于颜色、纹理、大小和形状相似性初始化区域计算所有相邻区域的相似度合并相似度最高的两个区域重复合并过程直到整张图像合并为一个区域输出所有合并过程中出现过的区域边界框import selectivesearch def generate_proposals(img, scale500, sigma0.9, min_size20): 使用Selective Search生成候选区域 :return: 候选区域列表 [(x1, y1, x2, y2), ...] # 转换图像为Selective Search所需格式 img_lbl, regions selectivesearch.selective_search( img, scalescale, sigmasigma, min_sizemin_size) candidates set() for r in regions: # 排除重复区域 x, y, w, h r[rect] if w 0 or h 0: continue candidates.add((x, y, xw, yh)) return sorted(candidates, keylambda x: (x[2]-x[0])*(x[3]-x[1]), reverseTrue)提示在实际应用中Selective Search的参数需要根据数据集调整。对于PASCAL VOCscale500通常效果较好。3. CNN特征提取与微调R-CNN使用在ImageNet上预训练的CNN通常是AlexNet作为特征提取器。我们需要对网络进行两处关键修改替换最后的1000类分类层为N1类N个目标类背景调整学习率策略对预训练层使用较小学习率微调实现代码import torch import torchvision.models as models from torch import nn, optim class RCNN_AlexNet(nn.Module): def __init__(self, num_classes21): super().__init__() # 加载预训练AlexNet original models.alexnet(pretrainedTrue) # 提取特征提取部分 self.features original.features # 修改分类器部分 self.classifier nn.Sequential( nn.Dropout(), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) # 初始化新分类层 nn.init.normal_(self.classifier[-1].weight, mean0, std0.01) nn.init.constant_(self.classifier[-1].bias, 0) def forward(self, x): x self.features(x) x torch.flatten(x, 1) x self.classifier(x) return x # 微调配置 model RCNN_AlexNet() optimizer optim.SGD([ {params: model.features.parameters(), lr: 1e-5}, {params: model.classifier.parameters(), lr: 1e-4} ], momentum0.9, weight_decay5e-4)特征提取关键点前向传播到最后一个全连接层前停止fc7层输出4096维特征向量对每个候选区域独立进行特征提取def extract_features(model, img_tensor): 提取图像区域的特征向量 :param model: 预训练CNN模型 :param img_tensor: 预处理后的图像张量 (1, 3, 227, 227) :return: 4096维特征向量 with torch.no_grad(): features model.features(img_tensor) features torch.flatten(features, 1) features model.classifier[:5](features) # 提取到fc7层 return features.cpu().numpy()4. SVM分类器训练R-CNN的一个独特设计是使用SVM而非softmax进行分类。这是因为作者发现对于检测任务SVM能提供更好的性能。训练SVM时需要特别注意正负样本的定义正样本与真实框IoU 0.3的候选区域负样本与所有真实框IoU 0.3的候选区域SVM训练实现from sklearn.svm import LinearSVC from sklearn.multiclass import OneVsRestClassifier def train_svms(features, labels): 训练多类SVM分类器 :param features: 特征矩阵 (n_samples, 4096) :param labels: 标签向量 (n_samples,) :return: 训练好的SVM模型 svm OneVsRestClassifier(LinearSVC(C1.0, losshinge), n_jobs-1) svm.fit(features, labels) return svm # 示例用法 # features np.vstack([extract_features(model, x) for x in regions]) # svm train_svms(features, labels)SVM与微调的数据差异参数微调阶段SVM训练阶段IoU阈值≥0.5≥0.3负样本定义IoU∈[0.1,0.5)IoU0.3数据平衡1:3正负比硬负样本挖掘5. 边界框回归与NMS最后阶段我们需要对检测框进行精修并去除冗余检测。边界框回归通过学习从候选框到真实框的变换参数显著提升了定位精度。边界框回归实现class BBoxRegressor: def __init__(self, lambda_1000): self.lambda_ lambda_ # L2正则化系数 self.W None # 回归权重 (4, 4096) def train(self, X, Y): X: 特征矩阵 (n_samples, 4096) Y: 目标偏移量 (n_samples, 4) X np.hstack([X, np.ones((X.shape[0], 1))]) # 添加偏置项 I np.eye(X.shape[1]) self.W np.linalg.inv(X.T X self.lambda_ * I) X.T Y def predict(self, X): X np.hstack([X, np.ones((X.shape[0], 1))]) return X self.W # 示例用法 # regressor BBoxRegressor() # regressor.train(train_features, train_offsets) # offsets regressor.predict(test_features)非极大值抑制(NMS)实现def nms(detections, threshold0.5): 非极大值抑制 :param detections: 检测结果列表 [(x1,y1,x2,y2,score,cls), ...] :param threshold: IoU阈值 :return: 过滤后的检测结果 if not detections: return [] # 按分数降序排序 detections sorted(detections, keylambda x: x[4], reverseTrue) keep [] while detections: # 保留当前最高分检测 best detections.pop(0) keep.append(best) # 计算与剩余检测的IoU boxes np.array([d[:4] for d in detections]) best_box np.array(best[:4]) # 计算IoU inter_x1 np.maximum(best_box[0], boxes[:, 0]) inter_y1 np.maximum(best_box[1], boxes[:, 1]) inter_x2 np.minimum(best_box[2], boxes[:, 2]) inter_y2 np.minimum(best_box[3], boxes[:, 3]) inter_area np.maximum(0, inter_x2 - inter_x1) * np.maximum(0, inter_y2 - inter_y1) best_area (best_box[2] - best_box[0]) * (best_box[3] - best_box[1]) other_area (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) iou inter_area / (best_area other_area - inter_area) # 移除重叠高的检测 detections [d for i, d in enumerate(detections) if iou[i] threshold] return keep6. 完整推理流程与性能优化将上述模块组合起来我们得到完整的R-CNN推理流程使用Selective Search生成约2000个候选区域对每个候选区域进行预处理并提取CNN特征使用SVM对特征进行分类应用边界框回归精修检测框使用NMS去除冗余检测性能优化技巧特征缓存将提取的特征保存到磁盘避免重复计算批量处理使用GPU批量处理候选区域尽管R-CNN本质上是串行的选择性搜索优化调整参数减少候选区域数量而不影响召回率def rcnn_inference(model, svm, bbox_reg, img, nms_thresh0.3, conf_thresh0.8): 完整R-CNN推理流程 :return: 检测结果 [(x1,y1,x2,y2,score,cls_idx), ...] # 生成候选区域 proposals generate_proposals(img) detections [] for prop in proposals[:2000]: # 限制最多2000个 # 预处理 img_tensor preprocess_region(prop, img) img_tensor torch.FloatTensor(img_tensor).unsqueeze(0).to(device) # 特征提取 feat extract_features(model, img_tensor) # SVM分类 scores svm.decision_function(feat) # (1, n_classes) cls_idx np.argmax(scores) conf scores[0, cls_idx] if conf conf_thresh: continue # 边界框回归 offset bbox_reg.predict(feat)[0] x1, y1, x2, y2 prop w, h x2 - x1, y2 - y1 # 应用回归 dx, dy, dw, dh offset cx x1 0.5 * w cy y1 0.5 * h pred_cx cx dx * w pred_cy cy dy * h pred_w w * np.exp(dw) pred_h h * np.exp(dh) pred_x1 pred_cx - 0.5 * pred_w pred_y1 pred_cy - 0.5 * pred_h pred_x2 pred_cx 0.5 * pred_w pred_y2 pred_cy 0.5 * pred_h detections.append((pred_x1, pred_y1, pred_x2, pred_y2, conf, cls_idx)) # 应用NMS return nms(detections, nms_thresh)7. 实验分析与改进方向在VOC2007测试集上完整的R-CNN实现应能达到约53.3%的mAP。下表展示了各模块对最终性能的贡献组件mAP提升关键作用基础模型(AlexNet)30.2%特征提取微调(fine-tuning)8.0%领域适应边界框回归3-4%定位精修SVM分类器4-5%分类优化R-CNN的局限性及改进方向计算效率低每个候选区域独立通过CNN导致大量重复计算。后续的Fast R-CNN通过共享计算解决了这一问题。多阶段训练需要分别训练CNN、SVM和回归器。Faster R-CNN实现了端到端训练。Selective Search限制候选区域生成仍是传统方法。Mask R-CNN引入RPN网络实现了全深度学习方案。特征提取瓶颈AlexNet特征表达能力有限。现代实现可替换为ResNet等更强网络。# 示例使用ResNet作为特征提取器 class RCNN_ResNet(nn.Module): def __init__(self, num_classes21): super().__init__() original models.resnet50(pretrainedTrue) self.features nn.Sequential(*list(original.children())[:-1]) self.classifier nn.Linear(original.fc.in_features, num_classes) # 初始化 nn.init.normal_(self.classifier.weight, std0.01) nn.init.constant_(self.classifier.bias, 0) def forward(self, x): x self.features(x) x torch.flatten(x, 1) x self.classifier(x) return x通过这次复现我们不仅深入理解了R-CNN的工作原理也亲身体验了目标检测领域这一里程碑式工作的精妙之处。虽然现代检测器如YOLO和RetinaNet在速度上远超R-CNN但R-CNN开创的两阶段检测思路至今仍在许多先进算法中发挥着重要作用。