1. 简介
由于提出
R
P
N
{\rm RPN}
RPN结构以及
A
n
c
h
o
r
{\rm Anchor}
Anchor的概念,
F
a
s
t
e
r
R
{\rm Faster\ R}
Faster R-
C
N
N
{\rm CNN}
CNN是最为经典的目标检测模型之一。本文将介绍基于
P
y
T
o
r
c
h
{\rm PyTorch}
PyTorch的
R
P
N
{\rm RPN}
RPN实现。代码来自参考
1
1
1部分。如下图是
F
a
s
t
e
r
R
{\rm Faster\ R}
Faster R-
C
N
N
{\rm CNN}
CNN给出的
R
P
N
{\rm RPN}
RPN的结构:
对于输入大小为 H × W × C H×W×C H×W×C的图像,经过特征提取网络后得到的特征图大小为 H / 16 × W / 16 × 512 H/16×W/16×512 H/16×W/16×512。通过得到的特征图产生相对于原图的 A n c h o r {\rm Anchor} Anchor,在原论文中的特征图上每个位置共产生 9 9 9个 A n c h o r {\rm Anchor} Anchor,所以最终产生的 A n c h o r {\rm Anchor} Anchor数为 H × W × 9 H×W×9 H×W×9。在 A n c h o r {\rm Anchor} Anchor的设置中,尺寸分为 128 128 128、 256 256 256、 512 512 512三种,比例分为 1 : 2 1:2 1:2、 2 : 1 2:1 2:1、 1 : 1 1:1 1:1三种,两两组合共形成9种 A n c h o r {\rm Anchor} Anchor。现在,先来看产生这 H × W × 9 H×W×9 H×W×9个 A n c h o r {\rm Anchor} Anchor的代码。代码来自参考 1 1 1。
2. 基于RPN产生~20k个候选框
首先针对特征图的左上角顶点产生 9 9 9个 A n c h o r {\rm Anchor} Anchor:
# 针对特征图左上角顶点产生anchor
def generate_base_anchor(base_size=16, ratios=None, anchor_scale=None):
"""
这里假设得到的特征图的大小为w×h,每个位置共产生9个anchor,所以一共产生的anchor
数为w×h×9。原论文中anchor的比例为1:2、2:1和1:1,尺度为128、256和512(相对于
原图而言)。所以在16倍下采样的特征图的上的实际尺度为8、16和32。
"""
# anchor的比例和尺度
if anchor_scale is None:
anchor_scale = [8, 16, 32]
if ratios is None:
ratios = [0.5, 1, 2]
# 特征图的左上角位置映射回原图的位置
py = base_size / 2
px = base_size / 2
# 初始化变量(9,4),这里以特征图的最左上角顶点为例产生anchor
base_anchor = np.zeros((len(ratios) * len(anchor_scale), 4), dtype=np.float32)
# 循环产生9个anchor
for i in range(len(ratios)):
for j in range(len(anchor_scale)):
# 生成高和宽(相对于原图而言)
# 以i=0、j=0为例,h=16×8×(0.5)^1/2、w=16×8×1/0.5,则h×w=128^2
h = base_size * anchor_scale[j] * np.sqrt(ratios[i])
w = base_size * anchor_scale[j] * np.sqrt(1. / ratios[i])
# 当前生成的anchor的索引(0~8)
index = i * len(anchor_scale) + j
# 计算anchor的左上角和右下角坐标
base_anchor[index, 0] = py - h / 2
base_anchor[index, 1] = px - w / 2
base_anchor[index, 2] = py + h / 2
base_anchor[index, 3] = px + w / 2
# 相对于原图大小的anchor(x_min,y_min,x_max,y_max)
return base_anchor
我们调用上面函数,看一下打印的结果:
我们以结果图的第一行为例说明(现在先不用在意那些越界的 A n c h o r {\rm Anchor} Anchor)。首先计算它的宽和高: w = 53.254833 − ( − 37.254833 ) = 90.5096666 h = 98.50967 − ( − 82.50967 ) = 181.01934 \begin{aligned} w&=53.254833-(-37.254833)=90.5096666\\ \ h&=98.50967-(-82.50967)=181.01934 \end{aligned} w h=53.254833−(−37.254833)=90.5096666=98.50967−(−82.50967)=181.01934
然后计算它的面积: A r e a = w ∗ h ≈ 16384 = 128 × 128 Area=w*h\approx16384=128×128 Area=w∗h≈16384=128×128
由该 A n c h o r {\rm Anchor} Anchor的宽高和面积我们可以看到,它是尺寸为 128 128 128、比例为 1 : 2 1:2 1:2的 A n c h o r {\rm Anchor} Anchor。上面函数是针对特征图的左上角顶点映射回原图产生的 A n c h o r {\rm Anchor} Anchor,我们需要整幅特征图的结果。则定义如下函数:
def generate_all_base_anchor(base_anchor, feat_stride, height, width):
"""
height*feat_stride/width*feat_stride相当于原图的高/宽,相当于从0开始,
每隔feat_stride=16采样一个位置,这相当于在16倍下采样的特征图上逐步采样。这
个过程用于确定每组anchor的中心点位置。
"""
# 纵向偏移量[0,16,32,...]
shift_y = np.arange(0, height * feat_stride, feat_stride)
# 横向偏移量[0,16,32,...]
shift_x = np.arange(0, width * feat_stride, feat_stride)
# np.meshgrid的作用是将两个一维向量变为两个二维矩阵。其中,返回的第一个二维
# 矩阵的行向量为第一个参数、重复次数为第二个参数的长度;第二个二维矩阵的列向量
# 为第二个参数、重复次数为第一个参数的长度。即得到的shift_x和shift_y如下:
# shift_x = [[0,16,32,...],
# [0,16,32,...],
# [0,16,32,...],
# ...]
# shift_y = [[0, 0, 0,... ],
# [16,16,16,...],
# [32,32,32,...],
# ...]
# 注意此时shift_x和shift_y都等于特征图的尺度,且每一个位置的之对应于特征图上
# 的一个点,两个矩阵的值的组合对应于特征图上的点映射回原图的左上角坐标。
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
# np.ravel()将矩阵展开成一个一维向量,即shift_x和shift_y展开后的形式分别为:
# [0,16,32,...,0,16,32,..,0,16,32,...],(1,w*h)
# [0,0,0,...,16,16,16,...,32,32,32,...],(1,w*h)
# axis=0相当于按行堆叠,得到的形状为(4,w*h);
# axis=1相当于按列堆叠,得到的形状为(w*h,4)。该语句得到的shift的值为:
# [[0, 0, 0, 0],
# [16, 0, 16, 0],
# [32, 0, 32, 0],
# ...]
shift = np.stack((shift_y.ravel(), shift_x.ravel(),
shift_y.ravel(), shift_x.ravel()), axis=1)
# 每个位置anchor数
num_anchor_per_loc = base_anchor.shape[0]
# 获取特征图上的总位置数
num_loc = shift.shape[0]
# 用generate_base_anchor产生的左上角位置的anchor加上偏移量即可得到
# 后面anchor的信息(这里只针对anchor中心点位置的改变,不改变anchor的
# 宽和高)。我们首先定义最终anchor的形状,我们知道应该为w*h*9,则所有
# anchor的存储的变量为(w*h*9,4)。首先将首位置产生的anchor形状改变为
# (1,9,4),再将shift的形状改变为(1,w*h,4)。并通过transpose函数改变
# shift的形状为(w*h,1,4),然后使用广播机制将二者相加,即二者的形状分
# 别为(1,num_anchor_per_loc,4)+(num_loc,1,4),最终相加得到的结果
# 形状为(num_loc,num_anchor_per_loc,4)。这里,相加的第一项为:
# [[[x_min_0,y_min_0,x_max_0,y_max_0],
# [x_min_1,y_min_1,x_max_1,y_max_1],
# ...,
# [x_min_8,y_min_8,x_max_8,y_max_8]]]
# 相加的第二项为:
# [[[0, 0, 0, 0]],
# [[0, 16, 0, 16]],
# [[0, 32, 0, 32]],
# ...]
# 在相加的过程中,我们首先将两个加数展开成目标形状。具体地,第一个则可以
# 展开为:
# [[[x_min_0,y_min_0,x_max_0,y_max_0],
# [x_min_1,y_min_1,x_max_1,y_max_1],
# ...,
# [x_min_8,y_min_8,x_max_8,y_max_8]],
# [[x_min_0,y_min_0,x_max_0,y_max_0],
# [x_min_1,y_min_1,x_max_1,y_max_1],
# ...,
# [x_min_8,y_min_8,x_max_8,y_max_8]],
# [[x_min_0,y_min_0,x_max_0,y_max_0],
# [x_min_1,y_min_1,x_max_1,y_max_1],
# ...,
# [x_min_8,y_min_8,x_max_8,y_max_8]],
# ...]
# 第二个可以展开为:
# [[[0, 0, 0, 0],
# [0, 0, 0, 0],
# ...],
# [[0, 16, 0, 16],
# [0, 16, 0, 16],
# ...],
# [[0, 32, 0, 32],
# [0, 32, 0, 32],
# ...],
# ...]
# 现在二者维度一致,可以直接相加。得到的结果的形状为:
# (num_loc,num_anchor_per_loc,4)
anchor = base_anchor.reshape((1, num_anchor_per_loc, 4)) + \
shift.reshape((1, num_loc, 4)).transpose((1, 0, 2))
# 将anchor的形状reshape为最终的形状(num_loc*num_anchor_per_loc,4)。
anchor = anchor.reshape((num_loc * num_anchor_per_loc, 4)).astype(np.float32)
return anchor
上面代码都有非常详细的注释,我们再来看其中几个比较重要的函数。
-
np.arange(start=0, end, step=1)
:以步长为step
生成[start, end)
范围内的一个等差数组。如: -
np.meshgrid(x, y)
:以向量x
和向量y
为基础返回一个(2, y.length(), x.length())
矩阵。这里,如果参数不是一维向量,该函数会首先将其按行展开为一维向量。并且,元素的展开方式有所不同:第一个参数按行展开,第二个参数按列展开。如:
-
np.stack(arrays, axis=0)
:在axis=0
的维度上将arrays
进行堆叠。我们首先以一维向量为例:
由于a
只有一维,在对自身使用stack
函数时不会产生变化。现在做如下变化:
我们可以看到,当axis=0
时,相当于将a
按行堆叠;当axis=1
时,相当于将a
按列堆叠。其他高维的向量亦如此。 -
transpose()
:将矩阵按照某种规律转置,转置方法由具体的参数而定。如:
我们首先将a
的形状固定为(1, 2, 4)
,此时调用函数transpose
得到b
。由于原a.shape = (1, 2, 3)
分别对应于第零维、第一维和第二维,即0、1、2
;transpose(2,0,1)
相当于把原第零维的元素放到第二个位置、将第一维的元素放到第三个位置、将第二维的元素放到第一个位置,即对应于shape: (1, 2, 4)=>(4, 1, 2)
。其他的变换亦如此。
3. ~20k个候选框(1):RPN
由 R P N {\rm RPN} RPN产生约 20000 {\rm 20000} 20000个候选框后,一方面,挑选出一部分用于训练 R P N {\rm RPN} RPN。具体地,从约 20000 {\rm 20000} 20000个候选框中选出 256 {\rm 256} 256个候选框,即 128 {\rm 128} 128个正样本和 128 {\rm 128} 128个负样本。挑选过程如下:
- 对于每个真实框,选择和他具有最大交并比的候选框作为正样本。显然,由于图中的标注目标偏少,无法满足训练要求,我们再进行以下步骤;
- 对于剩下的候选框,如果其和某个真实框的交并比大于设定的阈值,我们也认为它的正样本;
- 同时设定一个负样本阈值,如果候选框同真实框的交兵比小于阈值,则作为负样本。注意,在选择正样本和负样本时,要严格满足数量的要求。
class AnchorTargetCreator(object):
def __init__(self, n_sample=256, pos_iou_thresh=0.7, neg_iou_thresh=0.3, pos_ratio=0.5):
# 总样本采样数
self.n_sample = n_sample
# 正、负样本的阈值
self.pos_iou_thresh = pos_iou_thresh
self.neg_iou_thresh = neg_iou_thresh
# 正负样本采样比率
self.pos_ratio = pos_ratio
def __call__(self, bbox, anchor, img_size):
img_H, img_W = img_size
# ~20k个anchor
n_anchor = len(anchor)
# 只保留合法的Anchor
inside_index = _get_inside_index(anchor, img_H, img_W)
anchor = anchor[inside_index]
# 返回每个anchor与bbox对应的最大交并比索引以及正负样本采样结果
argmax_ious, label = self._create_label(inside_index, anchor, bbox)
# 计算回归目标
loc = bbox2loc(anchor, bbox[argmax_ious])
# 根据索引得到候选框
label = _unmap(label, n_anchor, inside_index, fill=-1)
loc = _unmap(loc, n_anchor, inside_index, fill=0)
return loc, label
def _create_label(self, inside_index, anchor, bbox):
# label: 1表示正样本索引,0表示负样本,-1表示忽略
label = np.empty((len(inside_index),), dtype=np.int32)
label.fill(-1)
# 返回每个anchor与bbox对应的最大交并比和索引以及由第一步产生的正样本索引
argmax_ious, max_ious, gt_argmax_ious = self._calc_ious(anchor, bbox, inside_index)
# 最大交并比小于阈值,首先选择为负样本
label[max_ious < self.neg_iou_thresh] = 0
# 第一步产生的正样本
label[gt_argmax_ious] = 1
# 第二步产生的正样本
label[max_ious >= self.pos_iou_thresh] = 1
# 如果正样本数量大于128,再次随机采样
n_pos = int(self.pos_ratio * self.n_sample)
pos_index = np.where(label == 1)[0]
if len(pos_index) > n_pos:
disable_index = np.random.choice(
pos_index, size=(len(pos_index) - n_pos), replace=False)
label[disable_index] = -1
# 如果负样本数量大于128,再次随机采样
n_neg = self.n_sample - np.sum(label == 1)
neg_index = np.where(label == 0)[0]
if len(neg_index) > n_neg:
disable_index = np.random.choice(
neg_index, size=(len(neg_index) - n_neg), replace=False)
label[disable_index] = -1
return argmax_ious, label
def _calc_ious(self, anchor, bbox, inside_index):
# 计算anchor和bbox之间的交并比,返回形状为(len(anchor),len(bbox))
# 即一个二维矩阵反应anchor与bbox两两之间的交并比大小
ious = bbox_iou(anchor, bbox)
# 对于每一个anchor,求出与之有最大交并比的bbox的索引
# axis=1按列求最大值,返回形状为(1,len(bbox))
argmax_ious = ious.argmax(axis=1)
max_ious = ious[np.arange(len(inside_index)), argmax_ious]
# 对于每一个bbox,求出与之有最大交并比的anchor的索引
# axis=0按行求最大值,返回形状为(len(anchor),1)
gt_argmax_ious = ious.argmax(axis=0)
gt_max_ious = ious[gt_argmax_ious, np.arange(ious.shape[1])]
# 对应于挑选正样本的第一步,与bbox有最大交并比的anchor为正样本,得到其索引
gt_argmax_ious = np.where(ious == gt_max_ious)[0]
return argmax_ious, max_ious, gt_argmax_ious
其中,bbox2loc
为根据真实框和候选框计算偏移的函数。公式如下:
t
x
=
(
x
−
x
a
)
/
w
a
t
y
=
(
y
−
y
a
)
/
h
a
t
w
=
log
(
w
/
w
a
)
t
h
=
log
(
h
/
h
a
)
(1)
t_x=(x-x_a)/w_a\ \ t_y=(y-y_a)/h_a\ \ t_w=\log(w/w_a)\ \ t_h=\log(h/h_a)\tag{1}
tx=(x−xa)/wa ty=(y−ya)/ha tw=log(w/wa) th=log(h/ha)(1)
def bbox2loc(src_bbox, dst_bbox):
# 预测框(xmin,ymin,xmax,ymax) => (x,y,w,h)
height = src_bbox[:, 2] - src_bbox[:, 0]
width = src_bbox[:, 3] - src_bbox[:, 1]
ctr_y = src_bbox[:, 0] + 0.5 * height
ctr_x = src_bbox[:, 1] + 0.5 * width
# 真实框(xmin,ymin,xmax,ymax) => (x,y,w,h)
base_height = dst_bbox[:, 2] - dst_bbox[:, 0]
base_width = dst_bbox[:, 3] - dst_bbox[:, 1]
base_ctr_y = dst_bbox[:, 0] + 0.5 * base_height
base_ctr_x = dst_bbox[:, 1] + 0.5 * base_width
# 极小值,保证除数不为零
eps = np.finfo(height.dtype).eps
height = np.maximum(height, eps)
width = np.maximum(width, eps)
# 套公式
dy = (base_ctr_y - ctr_y) / height
dx = (base_ctr_x - ctr_x) / width
dh = np.log(base_height / height)
dw = np.log(base_width / width)
# 将结果堆叠
loc = np.vstack((dy, dx, dh, dw)).transpose()
return loc
4. ~20k个候选框(2):Fast RCNN
由 R P N {\rm RPN} RPN产生约 20000 {\rm 20000} 20000个候选框后,另一方面,挑选出一部分用于训练 F a s t {\rm Fast} Fast- R C N N {\rm RCNN} RCNN。这里,在训练阶段和推理阶段所挑选处理的候选框的数量不同。在训练阶段,挑选出约 12 k {\rm 12k} 12k个候选框,利用非极大值抑制得到约 2 k {\rm 2k} 2k个候选框;在推理阶段,挑选出约 6 k {\rm 6k} 6k个候选框,利用非极大值抑制得到约 0.3 k {\rm 0.3k} 0.3k个候选框。这里挑选的规则是候选框的分类置信度。
class ProposalCreator:
def __init__(self, parent_model, nms_thresh=0.7, n_train_pre_nms=12000, n_train_post_nms=2000,
n_test_pre_nms=6000, n_test_post_nms=300, min_size=16):
self.parent_model = parent_model
self.nms_thresh = nms_thresh
self.n_train_pre_nms = n_train_pre_nms
self.n_train_post_nms = n_train_post_nms
self.n_test_pre_nms = n_test_pre_nms
self.n_test_post_nms = n_test_post_nms
self.min_size = min_size
def __call__(self, loc, score, anchor, img_size, scale=1.):
# 训练阶段和推理阶段使用不同数量的候选框
if self.parent_model.training:
n_pre_nms = self.n_train_pre_nms
n_post_nms = self.n_train_post_nms
else:
n_pre_nms = self.n_test_pre_nms
n_post_nms = self.n_test_post_nms
# 根据偏移得到anchor的实际信息
roi = loc2bbox(anchor, loc)
# 将预测框的宽高限定在预设的范围内
roi[:, slice(0, 4, 2)] = np.clip(
roi[:, slice(0, 4, 2)], 0, img_size[0])
roi[:, slice(1, 4, 2)] = np.clip(
roi[:, slice(1, 4, 2)], 0, img_size[1])
min_size = self.min_size * scale
hs = roi[:, 2] - roi[:, 0]
ws = roi[:, 3] - roi[:, 1]
keep = np.where((hs >= min_size) & (ws >= min_size))[0]
roi = roi[keep, :]
score = score[keep]
# 排序,得到高置信度部分的候选框
order = score.ravel().argsort()[::-1]
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
score = score[order]
# nms过程,这里不详细展开.pytorch1.2+可以通过from torchvision.ops import nms导入直接使用
keep = nms(torch.from_numpy(roi).cuda(), torch.from_numpy(score).cuda(), self.nms_thresh)
if n_post_nms > 0:
keep = keep[:n_post_nms]
roi = roi[keep.cpu().numpy()]
# 返回生成的候选框
return roi
在最终经由非极大值抑制挑选出候选框后,后面的工作就是
F
a
s
t
{\rm Fast}
Fast-
R
C
N
N
{\rm RCNN}
RCNN的内容,这里不再介绍。其中,loc2bbox
函数就是bbox2loc
函数的逆过程,即根据偏移得到真实框值。
5. RPN主体部分
有图 2 2 2可以看到, R P N {\rm RPN} RPN共有两个方向的输出。一方面是在 R P N {\rm RPN} RPN部分通过卷积得到两个分支,分别为分类和回归;另一方面产生候选区域作为 F a s t {\rm Fast} Fast- R C N N {\rm RCNN} RCNN部分的输入。下面是具体的代码:
class RegionProposalNetwork(nn.Module):
def __init__(self, in_channels=512, mid_channels=512, ratios=[0.5, 1, 2],
anchor_scales=[8, 16, 32], feat_stride=16,
proposal_creator_params=dict(), ):
super(RegionProposalNetwork, self).__init__()
# 特征图左上角顶点对应的anchor
self.anchor_base = generate_anchor_base(anchor_scales=anchor_scales, ratios=ratios)
# 下采样倍数
self.feat_stride = feat_stride
# 产生Fast RCNN的候选框
self.proposal_layer = ProposalCreator(self, **proposal_creator_params)
n_anchor = self.anchor_base.shape[0]
self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
# 权重初始化
normal_init(self.conv1, 0, 0.01)
normal_init(self.score, 0, 0.01)
normal_init(self.loc, 0, 0.01)
def forward(self, x, img_size, scale=1.):
n, _, hh, ww = x.shape
# 产生所有的anchor
anchor = _enumerate_shifted_anchor(np.array(self.anchor_base), self.feat_stride, hh, ww)
n_anchor = anchor.shape[0] // (hh * ww)
# rpn部分的回归分支
h = F.relu(self.conv1(x))
rpn_locs = self.loc(h)
rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4)
# rpn部分的分类分支
rpn_scores = self.score(h)
rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous()
rpn_softmax_scores = F.softmax(rpn_scores.view(n, hh, ww, n_anchor, 2), dim=4)
rpn_fg_scores = rpn_softmax_scores[:, :, :, :, 1].contiguous()
rpn_fg_scores = rpn_fg_scores.view(n, -1)
rpn_scores = rpn_scores.view(n, -1, 2)
# 产生rois部分
rois = list()
roi_indices = list()
for i in range(n):
roi = self.proposal_layer(
rpn_locs[i].cpu().data.numpy(),
rpn_fg_scores[i].cpu().data.numpy(),
anchor, img_size,
scale=scale)
batch_index = i * np.ones((len(roi),), dtype=np.int32)
rois.append(roi)
roi_indices.append(batch_index)
rois = np.concatenate(rois, axis=0)
roi_indices = np.concatenate(roi_indices, axis=0)
return rpn_locs, rpn_scores, rois, roi_indices, anchor
6. RPN部分的损失函数
F a s t e r R {\rm Faster\ R} Faster R- C N N {\rm CNN} CNN整体的损失函数定义如下: L ( { p i } , { t i } ) = 1 N c l s ∑ i L c l s ( p i , p i ∗ ) + λ 1 N r e g ∑ i p i ∗ L r e g ( t i , t i ∗ ) (2) L(\{p_i\},\{t_i\})=\frac{1}{N_{cls}}\sum_iL_{cls}(p_i,p_i^*)+\lambda\frac{1}{N_{reg}}\sum_ip_i^*L_{reg}(t_i,t_i^*)\tag{2} L({pi},{ti})=Ncls1i∑Lcls(pi,pi∗)+λNreg1i∑pi∗Lreg(ti,ti∗)(2)
其中,第一部分是分类损失, N c l s N_{cls} Ncls表示分类分支总计算的样本数。这里, R P N {\rm RPN} RPN和 F a s t R {\rm Fast\ R} Fast R- C N N {\rm CNN} CNN部分的数值不同;第二部分是回归损失, N c l s N_{cls} Ncls表示回归分支总计算的样本数。其中,在回归损失部分乘了一个 p i ∗ p_i^* pi∗表示回归损失只针对正样本。且分类损失部分使用的交叉熵损失,回归损失部分使用的是 S m o o t h L 1 {\rm SmoothL1} SmoothL1损失。首先来看手动实现 S m o o t h L 1 {\rm SmoothL1} SmoothL1损失的部分: S m o o t h L 1 ( x , β ) = { 0.5 ∣ x ∣ 2 / β i f ∣ x ∣ < β ∣ x ∣ − 0.5 β o t h e r w i s e (3) {\rm SmoothL1}(x,\beta)=\left\{ \begin{aligned} &0.5|x|^2/\beta&&{\rm if}\ |x|<\beta\\ &|x|-0.5\beta&&otherwise\\ \end{aligned} \right.\tag{3} SmoothL1(x,β)={0.5∣x∣2/β∣x∣−0.5βif ∣x∣<βotherwise(3)
def _smooth_l1_loss(x, t, in_weight, sigma):
# 相当于公式中的1/β
sigma2 = sigma ** 2
# 相当于公式中的|x|
diff = in_weight * (x - t)
abs_diff = diff.abs()
# 相当于公式中的判断条件
flag = (abs_diff.data < (1. / sigma2)).float()
# 根据|x|的范围选择不同分支计算
y = (flag * (sigma2 / 2.) * (diff ** 2) +
(1 - flag) * (abs_diff - 0.5 / sigma2))
return y.sum()
def _fast_rcnn_loc_loss(pred_loc, gt_loc, gt_label, sigma):
in_weight = torch.zeros(gt_loc.shape).cuda()
in_weight[(gt_label > 0).view(-1, 1).expand_as(in_weight).cuda()] = 1
loc_loss = _smooth_l1_loss(pred_loc, gt_loc, in_weight.detach(), sigma)
# 通过总参与计算的样本数将损失值归一化
loc_loss /= ((gt_label >= 0).sum().float())
return loc_loss
然后是计算 R P N {\rm RPN} RPN部分的损失函数的主体部分:
class FasterRCNNTrainer(nn.Module):
def __init__(self, faster_rcnn):
super(FasterRCNNTrainer, self).__init__()
self.faster_rcnn = faster_rcnn
# smoothl1损失函数的参数
self.rpn_sigma = 3
# 得到rpn部分参与损失计算的样本
self.anchor_target_creator = AnchorTargetCreator()
def forward(self, imgs, bboxes, labels, scale):
# 只支持batch_size=1的计算
n = bboxes.shape[0]
if n != 1:
raise ValueError('Currently only batch size 1 is supported.')
_, _, H, W = imgs.shape
img_size = (H, W)
# 经cnn产生的特征图
features = self.faster_rcnn.extractor(imgs)
# 经rpn产生的候选框
rpn_locs, rpn_scores, rois, roi_indices, anchor = \
self.faster_rcnn.rpn(features, img_size, scale)
# Since batch size is one, convert variables to singular form
bbox = bboxes[0]
rpn_score = rpn_scores[0]
rpn_loc = rpn_locs[0]
# rpn_loss
gt_rpn_loc, gt_rpn_label = self.anchor_target_creator(
at.tonumpy(bbox), anchor, img_size)
gt_rpn_label = at.totensor(gt_rpn_label).long()
gt_rpn_loc = at.totensor(gt_rpn_loc)
# 回归损失,调用自定义的smoothl1损失函数
rpn_loc_loss = _fast_rcnn_loc_loss(
rpn_loc, gt_rpn_loc, gt_rpn_label.data, self.rpn_sigma)
# 分类损失,调用pytorch自带的交叉熵损失函数
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label.cuda(), ignore_index=-1)
_gt_rpn_label = gt_rpn_label[gt_rpn_label > -1]
_rpn_score = at.tonumpy(rpn_score)[at.tonumpy(gt_rpn_label) > -1]
return rpn_loc_loss, rpn_cls_loss
7. 总结
F a s t e r R {\rm Faster\ R} Faster R- C N N {\rm CNN} CNN至今仍是广大研究者关注的重点模型之一,同时它也在工业界被广泛应用。本文基于Faster RCNN的代码大致介绍了 R P N {\rm RPN} RPN的代码实现流程。该代码结构清晰、通俗易懂,是学习 F a s t e r R {\rm Faster\ R} Faster R- C N N {\rm CNN} CNN的一套重要的的代码。如果想对 F a s t e r R {\rm Faster\ R} Faster R- C N N {\rm CNN} CNN做更加细致的了解,可以移步代码仓库仔细阅读代码部分。
参考
- https://github.com/chenyuntc/simple-faster-rcnn-pytorch.
- 《Faster+R-CNN原理和代码讲解_GiantPandaCV》电子书.
- https://shadowthink.com/.