(三)深度学习实战 | 基于PyTorch的RPN实现


1. 简介

由于提出 R P N {\rm RPN} 结构以及 A n c h o r {\rm Anchor} 的概念, F a s t e r   R {\rm Faster\ R} - C N N {\rm CNN} 是最为经典的目标检测模型之一。本文将介绍基于 P y T o r c h {\rm PyTorch} R P N {\rm RPN} 实现。代码来自参考 1 1 部分。如下图是 F a s t e r   R {\rm Faster\ R} - C N N {\rm CNN} 给出的 R P N {\rm RPN} 的结构:
在这里插入图片描述

图1:RPN

对于输入大小为 H × W × C H×W×C 的图像,经过特征提取网络后得到的特征图大小为 H / 16 × W / 16 × 512 H/16×W/16×512 。通过得到的特征图产生相对于原图的 A n c h o r {\rm Anchor} ,在原论文中的特征图上每个位置共产生 9 9 A n c h o r {\rm Anchor} ,所以最终产生的 A n c h o r {\rm Anchor} 数为 H × W × 9 H×W×9 。在 A n c h o r {\rm Anchor} 的设置中,尺寸分为 128 128 256 256 512 512 三种,比例分为 1 : 2 1:2 2 : 1 2:1 1 : 1 1:1 三种,两两组合共形成9种 A n c h o r {\rm Anchor} 。现在,先来看产生这 H × W × 9 H×W×9 A n c h o r {\rm Anchor} 的代码。代码来自参考 1 1


2. 基于RPN产生~20k个候选框

首先针对特征图的左上角顶点产生 9 9 A n c h o r {\rm 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} )。首先计算它的宽和高: 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}

然后计算它的面积: A r e a = w h 16384 = 128 × 128 Area=w*h\approx16384=128×128

由该 A n c h o r {\rm Anchor} 的宽高和面积我们可以看到,它是尺寸为 128 128 、比例为 1 : 2 1:2 A n c h o r {\rm Anchor} 。上面函数是针对特征图的左上角顶点映射回原图产生的 A n c h o r {\rm 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)范围内的一个等差数组。如:在这里插入图片描述

    扫描二维码关注公众号,回复: 11515636 查看本文章
  • 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、2transpose(2,0,1)相当于把原第零维的元素放到第二个位置、将第一维的元素放到第三个位置、将第二维的元素放到第一个位置,即对应于shape: (1, 2, 4)=>(4, 1, 2)。其他的变换亦如此。


3. ~20k个候选框(1):RPN

在这里插入图片描述

图2:Faster RCNN(图来自参考3)

R P N {\rm RPN} 产生约 20000 {\rm 20000} 个候选框后,一方面,挑选出一部分用于训练 R P N {\rm RPN} 。具体地,从约 20000 {\rm 20000} 个候选框中选出 256 {\rm 256} 个候选框,即 128 {\rm 128} 个正样本和 128 {\rm 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}

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} 产生约 20000 {\rm 20000} 个候选框后,另一方面,挑选出一部分用于训练 F a s t {\rm Fast} - R C N N {\rm RCNN} 。这里,在训练阶段和推理阶段所挑选处理的候选框的数量不同。在训练阶段,挑选出约 12 k {\rm 12k} 个候选框,利用非极大值抑制得到约 2 k {\rm 2k} 个候选框;在推理阶段,挑选出约 6 k {\rm 6k} 个候选框,利用非极大值抑制得到约 0.3 k {\rm 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} - R C N N {\rm RCNN} 的内容,这里不再介绍。其中,loc2bbox函数就是bbox2loc函数的逆过程,即根据偏移得到真实框值。

5. RPN主体部分

有图 2 2 可以看到, R P N {\rm RPN} 共有两个方向的输出。一方面是在 R P N {\rm RPN} 部分通过卷积得到两个分支,分别为分类和回归;另一方面产生候选区域作为 F a s t {\rm Fast} - R C N N {\rm 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} - C N N {\rm 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}

其中,第一部分是分类损失, N c l s N_{cls} 表示分类分支总计算的样本数。这里, R P N {\rm RPN} F a s t   R {\rm Fast\ R} - C N N {\rm CNN} 部分的数值不同;第二部分是回归损失, N c l s N_{cls} 表示回归分支总计算的样本数。其中,在回归损失部分乘了一个 p i p_i^* 表示回归损失只针对正样本。且分类损失部分使用的交叉熵损失,回归损失部分使用的是 S m o o t h L 1 {\rm SmoothL1} 损失。首先来看手动实现 S m o o t h L 1 {\rm 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}

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} 部分的损失函数的主体部分:

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} - C N N {\rm CNN} 至今仍是广大研究者关注的重点模型之一,同时它也在工业界被广泛应用。本文基于Faster RCNN的代码大致介绍了 R P N {\rm RPN} 的代码实现流程。该代码结构清晰、通俗易懂,是学习 F a s t e r   R {\rm Faster\ R} - C N N {\rm CNN} 的一套重要的的代码。如果想对 F a s t e r   R {\rm Faster\ R} - C N N {\rm CNN} 做更加细致的了解,可以移步代码仓库仔细阅读代码部分。


参考

  1. https://github.com/chenyuntc/simple-faster-rcnn-pytorch.
  2. 《Faster+R-CNN原理和代码讲解_GiantPandaCV》电子书.
  3. https://shadowthink.com/.


猜你喜欢

转载自blog.csdn.net/Skies_/article/details/107339095