深度学习中的目标检测:从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在速度和准确性上取得了显著的提升,为目标检测领域带来了重要的突破。在实际应用中,我们可以根据具体需求选择合适的方法,并结合代码进行实践和优化。
超级会员免费看

被折叠的 条评论
为什么被折叠?



