40、深度学习中的目标检测:从R - CNN到Faster R - CNN

深度学习中的目标检测:从R - CNN到Faster R - CNN

1. 模型训练与推理

在深度学习模型训练中,我们可以使用如下代码进行训练:

max_epochs=10,
num_sanity_val_steps=0
)
trainer.fit(model, dm)

这里我们无需编写训练循环,只需调用 trainer.fit 即可训练模型。同时,日志记录会自动开启,我们可以通过TensorBoard查看损失和准确率曲线。

对于模型推理,代码如下:

X, y_true = (iter(dm.test_dataloader())).next()
with torch.no_grad():
    y_pred = model.predict(X)
2. 目标检测简介

以往我们主要讨论图像分类问题,即将图像归为N个目标类别之一。但在很多情况下,仅知道类别是不足以完整描述图像的。例如,一张包含4只动物叠在一起的图像,我们不仅需要知道每只动物的类别,还需要知道它们在图像中的位置(边界框坐标),这就是目标检测/定位问题。

3. 早期目标检测方法 - R - CNN

R - CNN目标检测方法主要由三个阶段组成:
- 选择性搜索识别感兴趣区域 :这是一种基于计算机视觉的算法,能够提取候选区域,每张图像大约生成2000个区域建议。
- 特征提取 :使用深度卷积神经网络从每个感兴趣区域提取特征。由于深度神经网络通常需要固定大小的输入,因此在将区域输入到网络之前,会将其变形为固定大小。
- 分类/定位 :在提取的特征上训练特定类别的支持向量机(SVM)对区域进行分类。此外,还会添加边界框回归器来微调区域内目标的位置。在训练过程中,每个区域根据与真实边界框的重叠情况被分配一个真实类别标签。

4. 改进方法 - Fast R - CNN

R - CNN方法存在一些缺点,比如需要为每个区域建议独立提取特征,计算成本高且速度慢,训练过程也是多阶段的。为了解决这些问题,Fast R - CNN应运而生。它有两个主要贡献:
- 感兴趣区域(RoI)池化 :解决了R - CNN中需要多次前向传播提取特征的问题。Fast R - CNN将整个图像作为CNN的输入,然后使用RoI(区域建议边界框)在CNN输出上一次性提取区域特征。
- 多任务损失 :摒弃了SVM的使用,分类和边界框回归都由深度神经网络完成,实现了端到端的训练。

其高级算法流程如下:
1. 使用选择性搜索为每张图像生成2000个区域建议/RoI。
2. 在Fast R - CNN的一次前向传播中,(i) 使用RoI池化一次性提取所有RoI特征;(ii) 使用分类和回归头对目标进行分类和定位。

5. 更快的方法 - Faster R - CNN

Fast R - CNN虽然比R - CNN快很多,但仍依赖选择性搜索来获取区域建议,而选择性搜索只能在CPU上运行,速度慢且耗时,成为了瓶颈。Faster R - CNN的核心思想是使用深度网络生成区域建议,它由两个核心模块组成:
- 区域建议网络(RPN) :负责生成区域建议,能够高效地预测不同尺度和宽高比的区域建议。
- R - CNN模块 :与Fast R - CNN相同,接收区域建议,进行RoI池化,然后进行分类和回归。

RPN和R - CNN模块共享相同的卷积层,这有助于提高效率。

6. Faster R - CNN详细剖析
6.1 卷积骨干网络

在原始实现中,Faster R - CNN使用VGG - 16的卷积层作为卷积骨干网络,去掉了conv5后的最后一个池化层。这样,输入图像的空间尺寸会缩小为原来的1/16。例如,224x224的图像会被缩小为14x14的特征图,800x800的图像会被缩小为50x50的特征图。

6.2 区域建议网络(RPN)

RPN接收任意大小的图像作为输入,输出可能包含目标的矩形建议。它基于共享卷积层输出的卷积特征图进行操作。

锚点(Anchors)
目标检测问题中,目标的大小和形状多种多样。为了简化问题,引入了锚点的概念。锚点是不同形状和大小的参考框,所有建议都是相对于锚点提出的。原始的Faster R - CNN架构支持9种锚点配置,涵盖3种尺度和3种宽高比。

以下是生成锚点的代码:

# 生成特定网格点的锚点
def generate_anchors_at_grid_point(ctr_x, ctr_y, subsample, scales, aspect_ratios):
    anchors = torch.zeros(
        (len(aspect_ratios) * len(scales), 4), dtype=torch.float)
    for i, scale in enumerate(scales):
        for j, aspect_ratio in enumerate(aspect_ratios):
            w = subsample * scale * torch.sqrt(aspect_ratio)
            h = subsample * scale * torch.sqrt(1 / aspect_ratio)
            xtl = ctr_x - w / 2
            ytl = ctr_y - h / 2
            xbr = ctr_x + w / 2
            ybr = ctr_y + h / 2
            index = i * len(aspect_ratios) + j
            anchors[index] = torch.tensor([xtl, ytl, xbr, ybr])
    return anchors

# 为给定图像生成所有锚点
def generate_all_anchors(input_img_size, subsample, scales, aspect_ratios):
    _, h, w = input_img_size
    conv_feature_map_size = (h//subsample, w//subsample)
    all_anchors = []
    ctr_x = torch.arange(
        subsample/2, conv_feature_map_size[1]*subsample+1, subsample)
    ctr_y = torch.arange(
        subsample/2, conv_feature_map_size[0]*subsample+1, subsample)
    for y in ctr_y:
        for x in ctr_x:
            all_anchors.append(
                generate_anchors_at_grid_point(
                    x, y, subsample, scales, aspect_ratios))
    all_anchors = torch.cat(all_anchors)
    return all_anchors

input_img_size = (3, 800, 800)
c, height, width = input_img_size
scales = torch.tensor([8, 16, 32], dtype=torch.float)
aspect_ratios = torch.tensor([0.5, 1, 2])
subsample = 16
anchors = generate_all_anchors(input_img_size, subsample, scales, aspect_ratios)

RPN在卷积特征图上滑动一个小网络,在每个滑动窗口位置生成一个低维特征向量(VGG为512维),并将其输入到边界框回归层(reg)和边界框分类层(cls)。该网络是一个全卷积网络(FCN),具有输入大小不受限制和平移不变性的优点。

以下是RPN全卷积网络的代码:

class RPN_FCN(nn.Module):
    def __init__(self, k, in_channels=512):
        super(RPN_FCN, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(
                in_channels, 512, kernel_size=3,
                stride=1, padding=1),
            nn.ReLU(True))
        self.cls = nn.Conv2d(512, 2*k, kernel_size=1)
        self.reg = nn.Conv2d(512, 4*k, kernel_size=1)

    def forward(self, x):
        out = self.conv(x)
        rpn_cls_scores = self.cls(out).view(
            x.shape[0], -1, 2)
        rpn_loc = self.reg(out).view(
            x.shape[0], -1, 4)
        return rpn_cls_scores, rpn_loc

生成RPN的真实标签
在训练RPN时,需要为每个锚点框提供分类和回归目标。我们通过计算交并比(IoU)来衡量目标是否在锚点内。Faster R - CNN提供了为锚点框分配标签的指导原则:
- 与真实边界框IoU重叠最高的锚点,以及IoU重叠高于0.7的锚点,分配正标签(1)。
- 对于所有真实边界框IoU比率低于0.3的非正锚点,分配负标签(0)。
- 既不是正也不是负的锚点不参与训练目标。

以下是为每个锚点框分配真实标签的代码:

valid_indices = torch.where(
    (anchors[:, 0] >=0) &
    (anchors[:, 1] >=0) &
    (anchors[:, 2] <=width) &
    (anchors[:, 3] <=height))[0]
rpn_valid_labels = -1 * torch.ones_like(
    valid_indices, dtype=torch.int)
valid_anchor_bboxes = anchors[valid_indices]
ious = torchvision.ops.box_iou(
    gt_bboxes, valid_anchor_bboxes)
assert ious.shape == torch.Size(
    [gt_bboxes.shape[0], valid_anchor_bboxes.shape[0]])
gt_ious_max = torch.max(ious, dim=1)[0]
gt_ious_argmax = torch.where(
    gt_ious_max.unsqueeze(1).repeat(1, gt_ious_max.shape[1]) == ious)[1]
anchor_ious_argmax = torch.argmax(ious, dim=0)
anchor_ious = ious[anchor_ious_argmax, torch.arange(len(anchor_ious_argmax))]
pos_iou_threshold = 0.7
neg_iou_threshold = 0.3
rpn_valid_labels[anchor_ious < neg_iou_threshold] = 0
rpn_valid_labels[anchor_ious > pos_iou_threshold] = 1
rpn_valid_labels[gt_ious_argmax] = 1

处理不平衡问题
在为锚点分配标签时,我们会发现负锚点的数量远多于正锚点。如果直接在这样不平衡的数据集上训练,神经网络可能会学习到一个局部最小值,将每个锚点都分类为负锚点。为了解决这个问题,Faster R - CNN采用了欠采样的策略,从数千个锚点中随机采样256个锚点计算损失函数,采样的正、负锚点比例最高为1:1。如果正样本少于128个,则用负样本填充小批量。

为锚点框分配回归目标
- 标签为 - 1 :未采样/无效的锚点,不参与训练目标,回归目标无关紧要。
- 标签为0 :背景锚点,不包含任何目标,也不参与回归。
- 标签为1 :正锚点,包含目标,需要为这些锚点生成回归目标。具体参数化如下:
[
t_x = \frac{x - x_a}{w_a} \
t_y = \frac{y - y_a}{h_a} \
t_w = \log(\frac{w}{w_a}) \
t_h = \log(\frac{h}{h_a})
]
其中,(x, y, w, h) 表示真实边界框的中心坐标、宽度和高度,(x_a, y_a, w_a, h_a) 表示锚点边界框的中心坐标、宽度和高度,(t_x, t_y, t_w, t_h) 是回归目标。

以下是为每个锚点框分配回归目标的代码:

def transform_bboxes(bboxes):
    height = bboxes[:, 3] - bboxes[:, 1]
    width = bboxes[:, 2] - bboxes[:, 0]
    x_ctr = bboxes[:, 0] + width / 2
    y_ctr = bboxes[:, 1] + height /2
    return torch.stack(
        [x_ctr, y_ctr, width, height], dim=1)

def get_regression_targets(roi_bboxes, gt_bboxes):
    assert roi_bboxes.shape == gt_bboxes.shape
    roi_bboxes_t = transform_bboxes(roi_bboxes)
    gt_bboxes_t = transform_bboxes(gt_bboxes)
    tx = (gt_bboxes_t[:, 0] - roi_bboxes_t[:, 0]) / roi_bboxes_t[:, 2]
    ty = (gt_bboxes_t[:, 1] - roi_bboxes_t[:, 1]) / roi_bboxes_t[:, 3]
    tw = torch.log(gt_bboxes_t[:, 2] / roi_bboxes_t[:, 2])
    th = torch.log(gt_bboxes_t[:, 3] / roi_bboxes_t[:, 3])
    return torch.stack([tx, ty, tw, th], dim=1)

RPN损失函数
RPN的损失函数由两部分组成:
- 分类损失 :使用标准的交叉熵损失,适用于正、负锚点。
- 回归损失 :使用平滑L1损失,仅适用于正锚点。平滑L1损失结合了L1和L2损失的优点,当损失值较小时表现为L2损失,损失值较大时表现为L1损失。

整体损失定义如下:
[
L_{cls} = \frac{\sum_{i} CrossEntropy(p_i, p^ i)}{N {cls}} \
L_{reg} = \frac{\sum_{i} p^
i L {1;smooth}(t_i, t^ i)}{N {pos}} \
L_{RPN} = L_{cls} + \lambda L_{reg}
]
其中,(p_i) 是锚点 (i) 的预测目标存在概率,(p^
i) 是真实目标存在标签,(t_i) 是锚点 (i) 的回归预测,(t^*_i) 是回归目标,(N {cls}) 是锚点数量,(N_{pos}) 是正锚点数量。

以下是RPN损失函数的代码:

def rpn_loss(
    rpn_cls_scores, rpn_loc, rpn_labels,
    rpn_loc_targets, lambda_ = 10):
    classification_criterion = nn.CrossEntropyLoss(
        ignore_index=-1)
    reg_criterion = nn.SmoothL1Loss(reduction='sum')
    cls_loss = classification_criterion(rpn_cls_scores, rpn_labels)
    positive_indices = torch.where(rpn_labels==1)[0]
    pred_positive_anchor_offsets = rpn_loc[positive_indices]
    gt_positive_loc_targets = rpn_loc_targets[positive_indices]
    reg_loss = reg_criterion(
        pred_positive_anchor_offsets,
        gt_positive_loc_targets) / len(positive_indices)
    return {
        'rpn_cls_loss': cls_loss,
        'rpn_reg_loss': reg_loss,
        'rpn_total_loss': cls_loss + lambda_* reg_loss
    }
7. 生成区域建议

RPN为每个锚点预测目标存在性和回归偏移量。接下来,我们需要从这些预测中生成好的区域建议(RoI)用于训练R - CNN模块。具体步骤如下:
1. 将预测偏移转换为边界框 :通过反向转换公式将预测偏移转换为边界框。
[
x^ = t^ _x * w_a + x_a \
y^ = t^ _y * h_a + y_a \
w^ = e^{t^ _w} * w_a \
h^ = e^{t^ _h} * h_a
]
2. 裁剪边界框 :将预测的边界框裁剪到图像范围内。
3. 过滤边界框 :移除高度或宽度小于最小RoI阈值的预测边界框。
4. 排序并选择候选框 :根据目标存在性分数对预测边界框进行排序,训练时选择前12000个,测试时选择前6000个。

以下是生成区域建议的代码:

rois = generate_bboxes_from_offset(rpn_loc, anchors)
rois = rois.clamp(min=0, max=width)
roi_heights = rois[:, 3] - rois[:, 1]
roi_widths = rois[:, 2] - rois[:, 0]
min_roi_threshold = 16
valid_idxes = torch.where((roi_heights > min_roi_threshold) &
                          (roi_widths > min_roi_threshold))[0]
rois = rois[valid_idxes]
valid_cls_scores = rpn_loc[valid_idxes]
objectness_scores = valid_cls_scores[:, 1]
sorted_idx = torch.argsort(
    objectness_scores, descending=True)
n_train_pre_nms = 12000
n_val_pre_nms = 300
rois = rois[sorted_idx][:n_train_pre_nms]
objectness_scores = objectness_scores[
    sorted_idx][:n_train_pre_nms]
8. 非极大值抑制(NMS)

由于很多建议会重叠,为了选择最有效的RoI集合,我们使用非极大值抑制(NMS)算法。该算法的输入是边界框列表、对应的分数和重叠阈值,输出是过滤后的边界框列表。具体步骤如下:
1. 选择置信度分数最高的边界框,将其从列表中移除并添加到最终列表中。
2. 使用IoU比较该边界框与剩余边界框,移除IoU大于阈值的边界框。
3. 重复上述步骤,直到可能性不再增加。

训练时使用0.7的NMS阈值,选择前2000个RoI;测试时选择前300个RoI。

以下是非极大值抑制的代码:

# 此处代码未给出完整实现,可参考提供的链接获取完整代码

综上所述,Faster R - CNN通过引入区域建议网络和一系列优化策略,在目标检测的速度和准确性上取得了显著的提升,是目标检测领域的重要进展。

深度学习中的目标检测:从R - CNN到Faster R - CNN

9. 关键技术总结
  • 锚点机制 :引入不同尺度和宽高比的锚点,简化目标检测中目标大小和形状多样的问题,使每个锚点负责检测特定类型的目标,提高了检测效率和准确性。
  • 全卷积网络(FCN) :RPN采用全卷积网络,输入大小不受限制,卷积权重在特征图不同位置共享,具有平移不变性,能够高效地处理任意大小的图像。
  • 交并比(IoU) :用于衡量目标与锚点框的重叠程度,为锚点框分配标签,是确定正、负样本的重要依据。
  • 欠采样策略 :解决了训练数据中正负样本不平衡的问题,避免神经网络学习到局部最优解,提高了模型的泛化能力。
  • 平滑L1损失 :结合了L1和L2损失的优点,在训练回归任务时,对不同大小的损失值采用不同的处理方式,使网络更关注高低损失项。
  • 非极大值抑制(NMS) :消除重叠的边界框建议,选择最有效的区域建议,减少冗余信息,提高目标检测的准确性。
10. 各方法对比
方法 优点 缺点
R - CNN 首次将深度学习应用于目标检测,为后续方法奠定基础 计算成本高,速度慢,训练过程复杂,需要为每个区域建议独立提取特征
Fast R - CNN 引入RoI池化和多任务损失,提高了速度和检测质量,实现端到端训练 仍依赖选择性搜索,存在速度瓶颈
Faster R - CNN 消除了选择性搜索,使用深度网络生成区域建议,速度更快,准确性更高 模型复杂度相对较高,训练和推理需要一定的计算资源
11. 流程总结
graph LR
    A[输入图像] --> B[卷积骨干网络]
    B --> C[区域建议网络(RPN)]
    C --> D[生成锚点]
    D --> E[计算分类和回归偏移]
    E --> F[生成区域建议(RoI)]
    F --> G[非极大值抑制(NMS)]
    G --> H[R - CNN模块]
    H --> I[RoI池化]
    I --> J[分类和回归]
    J --> K[输出检测结果]
12. 代码整合与使用示例

以下是一个简单的示例,展示如何整合上述代码进行目标检测:

import torch
import torch.nn as nn
import torchvision.ops

# 假设已经定义了上述所有函数和类

# 初始化模型
model = RPN_FCN(k=9)

# 准备数据
input_img_size = (3, 800, 800)
c, height, width = input_img_size
scales = torch.tensor([8, 16, 32], dtype=torch.float)
aspect_ratios = torch.tensor([0.5, 1, 2])
subsample = 16
anchors = generate_all_anchors(input_img_size, subsample, scales, aspect_ratios)
gt_bboxes = torch.tensor([[100, 100, 200, 200]], dtype=torch.float)

# 前向传播
rpn_cls_scores, rpn_loc = model(torch.randn(1, 512, height//subsample, width//subsample))

# 生成真实标签
valid_indices = torch.where(
    (anchors[:, 0] >=0) &
    (anchors[:, 1] >=0) &
    (anchors[:, 2] <=width) &
    (anchors[:, 3] <=height))[0]
rpn_valid_labels = -1 * torch.ones_like(
    valid_indices, dtype=torch.int)
valid_anchor_bboxes = anchors[valid_indices]
ious = torchvision.ops.box_iou(
    gt_bboxes, valid_anchor_bboxes)
gt_ious_max = torch.max(ious, dim=1)[0]
gt_ious_argmax = torch.where(
    gt_ious_max.unsqueeze(1).repeat(1, gt_ious_max.shape[1]) == ious)[1]
anchor_ious_argmax = torch.argmax(ious, dim=0)
anchor_ious = ious[anchor_ious_argmax, torch.arange(len(anchor_ious_argmax))]
pos_iou_threshold = 0.7
neg_iou_threshold = 0.3
rpn_valid_labels[anchor_ious < neg_iou_threshold] = 0
rpn_valid_labels[anchor_ious > pos_iou_threshold] = 1
rpn_valid_labels[gt_ious_argmax] = 1

# 计算回归目标
roi_bboxes = anchors[torch.where(rpn_valid_labels == 1)[0]]
gt_bboxes_for_roi = gt_bboxes.repeat(len(roi_bboxes), 1)
rpn_loc_targets = get_regression_targets(roi_bboxes, gt_bboxes_for_roi)

# 计算损失
loss = rpn_loss(rpn_cls_scores.view(-1, 2), rpn_loc.view(-1, 4), rpn_valid_labels, rpn_loc_targets)
print("RPN分类损失:", loss['rpn_cls_loss'])
print("RPN回归损失:", loss['rpn_reg_loss'])
print("RPN总损失:", loss['rpn_total_loss'])

# 生成区域建议
rois = generate_bboxes_from_offset(rpn_loc.view(-1, 4), anchors)
rois = rois.clamp(min=0, max=width)
roi_heights = rois[:, 3] - rois[:, 1]
roi_widths = rois[:, 2] - rois[:, 0]
min_roi_threshold = 16
valid_idxes = torch.where((roi_heights > min_roi_threshold) &
                          (roi_widths > min_roi_threshold))[0]
rois = rois[valid_idxes]
valid_cls_scores = rpn_loc[valid_idxes]
objectness_scores = valid_cls_scores[:, 1]
sorted_idx = torch.argsort(
    objectness_scores, descending=True)
n_train_pre_nms = 12000
rois = rois[sorted_idx][:n_train_pre_nms]

# 非极大值抑制(此处仅示意,未完整实现)
# nms_rois = non_maximal_suppression(rois, objectness_scores[:n_train_pre_nms], threshold=0.7)
13. 注意事项
  • 数据预处理 :在输入图像到模型之前,需要进行适当的预处理,如归一化、缩放等,以提高模型的性能。
  • 超参数调整 :锚点的尺度、宽高比,IoU阈值,损失函数的权重等超参数对模型的性能有重要影响,需要根据具体任务进行调整。
  • 计算资源 :Faster R - CNN模型复杂度较高,训练和推理需要较大的计算资源,建议使用GPU进行加速。
  • 代码优化 :在实际应用中,可以对代码进行优化,如使用更高效的算法实现NMS,减少内存占用和计算时间。

通过对R - CNN、Fast R - CNN和Faster R - CNN的学习,我们了解了目标检测技术的发展历程和关键技术。Faster R - CNN在速度和准确性上取得了显著的提升,为目标检测领域带来了重要的突破。在实际应用中,我们可以根据具体需求选择合适的方法,并结合代码进行实践和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值