一、 引言:从 R-CNN 到 Faster R-CNN 的进化

  作为目标检测领域的里程碑式模型,Faster R-CNN 彻底解决了前代模型(R-CNN, Fast R-CNN)依赖外部候选框算法(如 Selective Search)导致的速度瓶颈。在 2026 年的今天,尽管涌现了无数新架构,Faster R-CNN 凭借其极高的稳定性和精度,依然是工业界和竞赛中的常青树。
  本教程将带你从零开始,利用 PyTorch 官方预训练模型 fasterrcnn_resnet50_fpn,完成从环境配置、数据准备(支持自定义数据)、模型微调(Fine-tuning)到推理可视化的全过程。
完整代码在个人GitHub:实战八

1.卷积神经网络CNN里经典网络模型之 AlexNet全网最详解(理论篇)
2.目标检测简介
3.深度学习之目标检测R-CNN模型算法流程详解说明(超详细理论篇)
4.深度学习之目标检测Fast-RCNN模型算法流程详解说明(超详细理论篇)
5.目标检测标注工具常用的三种:LabelImg、CVAT、Roboflow
6.关于R-CNN中Selective Search选择性搜索候选框原理+步骤+代码+参数详细说明
7.PASCAL VOC、ILSVRC和MS COCO三大竞赛简单介绍
8.PASCAL VOC、ILSVRC和MS COCO三大竞赛核心数据集版本总节
9.OC vs COCO vs YOLO格式终极对比:2025年目标检测项目到底该选哪个标注格式?避坑指南+决策树
10.Pascal VOC数据集划分的致命陷阱与最佳实践:为什么99%的开发者都该以JPEGImages图片文件夹为基准,而不是Annotations XML?
11.深度学习pytorch实战七:从零复现经典R-CNN:完整代码+详细原理+训练调优+预测可视化:从数据采集、模型构建到训练部署

二、 原理剖析:Faster R-CNN 的核心架构

  Faster R-CNN 的核心在于端到端(End-to-End)的训练方式。它将检测任务分解为两个阶段(Two-Stage),但所有组件都在同一个网络中协同工作。

1. 整体流程

  • 1.Backbone (骨干网络):输入图像通过 ResNet50 提取特征,并利用 FPN(特征金字塔网络)生成多尺度特征图。
  • 2.RPN (区域提议网络):在特征图上滑动,通过 Anchor 机制预测物体的存在概率和初步位置,生成高质量的 Region Proposals。
  • 3.RoI Head (检测头):将 RPN 提出的候选框映射回特征图,通过 RoI Align 提取固定尺寸特征,最终输出类别标签和精确的边界框坐标。

2. 与 R-CNN 系列的对比

特性R-CNNFast R-CNNFaster R-CNN
候选框生成Selective Search (外部)Selective Search (外部)RPN (网络内部)
特征提取每个框单独提取 (慢)共享卷积特征共享卷积特征
训练方式多阶段单阶段 (部分端到端)完全端到端
速度极慢中等快 (接近实时)

三、 环境配置与数据准备

1. 环境安装

# 推荐使用 Conda
conda create -n faster_rcnn python=3.8
conda activate faster_rcnn
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install opencv-python albumentations matplotlib tqdm

2. 数据集准备 (Pascal VOC 格式)

  • 目录结构
VOCdevkit/
    VOC2026/
        JPEGImages/  # 存放所有图片 (.jpg)
        Annotations/ # 存放所有XML标注文件 (.xml)
        ImageSets/
            Main/
                train.txt # 训练集图片ID列表
                val.txt   # 验证集图片ID列表
  • 数据划分:使用脚本将数据按比例(如 8:2)划分为 train.txt 和 val.txt。
  • 获取 VOCtrainval_11-May-2012.tar 数据集:一种是直接从官方源下载,另一种是使用深度学习框架(如 torchvision)内置的下载工具。
    • (1)使用 torchvision 自动下载:torchvision.datasets 模块内置了 VOC2012 的下载和提取逻辑,它会自动处理文件校验和解压。
import os
import torchvision

# 定义数据保存的根目录
root_dir = "./data"
os.makedirs(root_dir, exist_ok=True)

# 下载并加载 VOC2012 数据集
# download=True 表示如果本地不存在则自动下载
# 该数据集通常用于图像分割 (Segmentation) 或 目标检测 (Detection)
dataset = torchvision.datasets.VOCSegmentation(
    root=root_dir,
    year='2012',
    image_set='train', # 可选: 'train', 'val', 'trainval', 'test'
    download=True,
    transform=None,    # 可在此处添加数据增强
    target_transform=None
)

print("数据集下载/加载完成!")
print(f"数据存放路径: {os.path.join(root_dir, 'VOCdevkit')}")
  • (2)使用 requests 或 wget 直接下载原始文件
import os
import requests

def download_voc2012(save_dir="."):
    url = "http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar"
    filename = os.path.join(save_dir, "VOCtrainval_11-May-2012.tar")
    
    if os.path.exists(filename):
        print(f"文件已存在: {filename}")
        return

    print(f"开始下载 VOC2012 数据集 (约 2GB)...")
    try:
        response = requests.get(url, stream=True)
        response.raise_for_status()
        
        with open(filename, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)
        print("下载完成!")
        
        # 自动解压
        import tarfile
        print("正在解压...")
        with tarfile.open(filename, 'r') as tar:
            tar.extractall(path=save_dir)
        print("解压完成!")
            
    except Exception as e:
        print(f"下载失败: {e}")
        print("提示:如果官方源无法连接,请检查网络或尝试使用代理。")

# 执行下载
download_voc2012()

四、 核心代码实现

  • 代码分为四个模块:数据集、变换、模型构建和训练器。

1. 数据集构建 (voc_dataset.py)

  • 这里使用 torchvision 的 VOCDetection 并进行封装,配合 albumentations 进行增强。
import os
import torch
import numpy as np
import xml.etree.ElementTree as ET
from torch.utils.data import Dataset
from PIL import Image
from torchvision.transforms import ToTensor


class VOCDataset(Dataset):
    def __init__(self, root_dir, split='train', transforms=None):
        self.root_dir = root_dir
        self.transforms = transforms
        self.image_set_dir = os.path.join(root_dir, 'ImageSets', 'Main')
        self.img_dir = os.path.join(root_dir, 'JPEGImages')
        self.ann_dir = os.path.join(root_dir, 'Annotations')

        # 读取图像ID列表
        split_file = os.path.join(self.image_set_dir, f'{split}.txt')
        with open(split_file) as f:
            self.image_ids = [x.strip() for x in f.readlines()]

        # 定义类别名称
        self.classes = [
            'background', 'aeroplane', 'bicycle', 'bird', 'boat',
            'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
            'diningtable', 'dog', 'horse', 'motorbike', 'person',
            'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'
        ]
        self.class_to_idx = {cls: i for i, cls in enumerate(self.classes)}

    def __len__(self):
        return len(self.image_ids)

    def __getitem__(self, idx):
        image_id = self.image_ids[idx]
        img_path = os.path.join(self.img_dir, f'{image_id}.jpg')
        ann_path = os.path.join(self.ann_dir, f'{image_id}.xml')

        # 加载图像
        img = Image.open(img_path).convert('RGB')

        # 解析XML标注
        boxes, labels = self._parse_voc_xml(ann_path)

        # 转换为Tensor
        boxes = torch.as_tensor(boxes, dtype=torch.float32)
        labels = torch.as_tensor(labels, dtype=torch.int64)

        # 创建目标字典
        target = {
            'boxes': boxes,
            'labels': labels,
            'image_id': torch.tensor([idx]),
            'area': (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0]),
            'iscrowd': torch.zeros_like(labels)
        }

        if self.transforms is not None:
            transformed = self.transforms(
                image=np.array(img),
                bboxes=target['boxes'].numpy(),
                labels=target['labels'].numpy()
            )
            img = Image.fromarray(transformed['image'])
            target['boxes'] = torch.as_tensor(transformed['bboxes'], dtype=torch.float32)
            target['labels'] = torch.as_tensor(transformed['labels'], dtype=torch.int64)

        img = ToTensor()(img)

        return img, target

    def _parse_voc_xml(self, xml_path):
        tree = ET.parse(xml_path)
        root = tree.getroot()

        boxes = []
        labels = []

        for obj in root.findall('object'):
            # 获取类别标签
            name = obj.find('name').text
            label = self.class_to_idx[name]

            # 获取边界框坐标
            bbox = obj.find('bndbox')
            xmin = float(bbox.find('xmin').text)
            ymin = float(bbox.find('ymin').text)
            xmax = float(bbox.find('xmax').text)
            ymax = float(bbox.find('ymax').text)

            boxes.append([xmin, ymin, xmax, ymax])
            labels.append(label)

        return boxes, labels

2. 数据增强与变换 (transforms.py)

  • 重点:目标检测的增强必须同时作用于图像和边界框。
import albumentations as A
from albumentations.pytorch import ToTensorV2

def get_transform(train):
    if train:
        return A.Compose([
            A.Resize(height=800, width=800),
            A.HorizontalFlip(p=0.5),
            A.RandomBrightnessContrast(p=0.2),
        ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels']))
    else:
        return A.Compose([
            A.Resize(height=800, width=800),
        ], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['labels']))

3. 模型构建 (model_builder.py)

  • 利用 TorchVision 的预训练模型进行迁移学习。
import torchvision
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor


def get_model(num_classes):
    # 加载预训练模型
    model = fasterrcnn_resnet50_fpn(pretrained=True)

    # 获取分类器的输入特征数
    in_features = model.roi_heads.box_predictor.cls_score.in_features

    # 替换预训练的头部
    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

    return model

4. 训练与推理引擎 (trainer.py)

  • 包含训练循环、验证逻辑和预测可视化。
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
from tqdm import tqdm
from voc_dataset import VOCDataset
from transforms import get_transform
from model_builder import get_model


# 定义显式的collate函数
def collate_fn(batch):
    return tuple(zip(*batch))


def train_one_epoch(model, optimizer, data_loader, device, epoch):
    model.train()
    progress_bar = tqdm(data_loader, desc=f'Epoch {epoch}')

    for images, targets in progress_bar:
        images = list(image.to(device) for image in images)
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]

        loss_dict = model(images, targets)
        losses = sum(loss for loss in loss_dict.values())

        optimizer.zero_grad()
        losses.backward()
        optimizer.step()

        progress_bar.set_postfix({'total_loss': losses.item()})


def main():
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    root_dir = './VOCtrainval_11-May-2012/VOCdevkit/VOC2012'

    train_dataset = VOCDataset(root_dir, split='train', transforms=get_transform(train=True))
    val_dataset = VOCDataset(root_dir, split='val', transforms=get_transform(train=False))

    # 使用定义好的collate_fn
    train_loader = DataLoader(
        train_dataset, batch_size=4, shuffle=True, num_workers=4,
        collate_fn=collate_fn  # 替换lambda
    )
    val_loader = DataLoader(
        val_dataset, batch_size=4, shuffle=False, num_workers=4,
        collate_fn=collate_fn  # 替换lambda
    )

    num_classes = 21
    model = get_model(num_classes)
    model.to(device)

    params = [p for p in model.parameters() if p.requires_grad]
    optimizer = optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005)
    lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

    num_epochs = 10
    for epoch in range(num_epochs):
        train_one_epoch(model, optimizer, train_loader, device, epoch)
        lr_scheduler.step()
        torch.save(model.state_dict(), f'faster_rcnn_voc_epoch_{epoch}.pth')

    print("训练完成!")


if __name__ == "__main__":
    main()

5.调优

  • 学习率策略:使用 CosineAnnealingLR 或 OneCycleLR 代替固定学习率,收敛更快。
  • 混合精度训练:在 trainer.py 中加入 torch.cuda.amp 自动混合精度,显存占用减少一半,速度提升。
  • 预训练权重:切勿从头训练。利用 COCO 预训练权重(Feature Extractor 已经学会了识别边缘、纹理、物体部件),只需微调最后的检测头。
  • Anchor 匹配策略:如果检测小物体困难,可以调整 RPN 中的 IoU 阈值或修改 Anchor 的尺寸比例。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐