最近做了一下FasterRcnn相关实验,觉得有必要写一下原理和代码方面的分析,作为一个经典的网络,FasterRcnn还是值得学习的。
总体架构:
此算法主要分为两个网络RPN卷积和后端ROI使用的全连接网络,这里使用VOC数据集演示。
class FasterRCNN(nn.Module):
def __init__(self, num_classes,
mode = "training",
feat_stride = 16,
anchor_scales = [8, 16, 32],
ratios = [0.5, 1, 2],
backbone = 'vgg',
pretrained = False):
super(FasterRCNN, self).__init__()
self.feat_stride = feat_stride
#---------------------------------#
# vgg
#---------------------------------#
if backbone == 'vgg':
self.extractor, classifier = decom_vgg16(pretrained)
#---------------------------------#
# 构建建议框网络
#---------------------------------#
self.rpn = RegionProposalNetwork(
512, 512,
ratios = ratios,
anchor_scales = anchor_scales,
feat_stride = self.feat_stride,
mode = mode
)
#---------------------------------#
# 构建分类器网络
#---------------------------------#
self.head = VGG16RoIHead(
n_class = num_classes + 1,
roi_size = 7,
spatial_scale = 1,
classifier = classifier
)
可以看到此网络主要分为建议框网络和分类器网络,其中图像经过self.extractor函数可得到特征图,得到的特征图主要用于RPN网络,classifier用于ROI网络这里简单概述一下,接下来进入RPN网络函数。
RPN网络作用详解
self.anchor_base = generate_anchor_base(anchor_scales = anchor_scales, ratios = ratios)
n_anchor = self.anchor_base.shape[0]
#-----------------------------------------#
# 先进行一个3x3的卷积,可理解为特征整合
#-----------------------------------------#
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)
#-----------------------------------------#
# 特征点间距步长
#-----------------------------------------#
self.feat_stride = feat_stride
#-----------------------------------------#
# 用于对建议框解码并进行非极大抑制
#-----------------------------------------#
self.proposal_layer = ProposalCreator(mode)
RPN网络类别的结构,主要做了前背景的分数的网络,和回归预测的网络,因为特征图的每个点对应着9个框信息,每个框信息有偏移量xyxy信息所以self.loc就形成9*4的通道,self.score形成2*9的通道
创建了结构为[9,4]的先验框,对应如下结构。self.feat_stride就是下采样图像缩小的倍数。
ratios = [0.5, 1, 2], anchor_scales = [8, 16, 32],
def forward(self, x, img_size, scale=1.):
n, _, h, w = x.shape
#-----------------------------------------#
# 先进行一个3x3的卷积,可理解为特征整合
#-----------------------------------------#
x = F.relu(self.conv1(x))
#-----------------------------------------#
# 回归预测对先验框进行调整
#-----------------------------------------#
rpn_locs = self.loc(x)
rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4) #验证回归框
#-----------------------------------------#
# 分类预测先验框内部是否包含物体
#-----------------------------------------#
rpn_scores = self.score(x)
rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous().view(n, -1, 2) #看看前景背景
#--------------------------------------------------------------------------------------#
# 进行softmax概率计算,每个先验框只有两个判别结果
# 内部包含物体或者内部不包含物体,rpn_softmax_scores[:, :, 1]的内容为包含物体的概率
#--------------------------------------------------------------------------------------#
rpn_softmax_scores = F.softmax(rpn_scores, dim=-1)
rpn_fg_scores = rpn_softmax_scores[:, :, 1].contiguous()
rpn_fg_scores = rpn_fg_scores.view(n, -1)
#------------------------------------------------------------------------------------------------#
# 生成先验框,此时获得的anchor是布满网格点的,当输入图片为600,600,3的时候,shape为(12996, 4)
#------------------------------------------------------------------------------------------------#
anchor = _enumerate_shifted_anchor(np.array(self.anchor_base), self.feat_stride, h, w)
rois = list()
roi_indices = list()
for i in range(n):
roi = self.proposal_layer(rpn_locs[i], rpn_fg_scores[i], anchor, img_size, scale = scale)
batch_index = i * torch.ones((len(roi),))
rois.append(roi.unsqueeze(0))
roi_indices.append(batch_index.unsqueeze(0))
rois = torch.cat(rois, dim=0).type_as(x)
roi_indices = torch.cat(roi_indices, dim=0).type_as(x)
anchor = torch.from_numpy(anchor).unsqueeze(0).float().to(x.device)
return rpn_locs, rpn_scores, rois, roi_indices, anchor
RPN网络需要做的,将得到的特征图输入,形成框信息结构 ->(batch,特征图面积*9,4),形成前景背景信息结构->(batch,特征图面积*9,2) 其中最后一个维度做softmax,0索引概率大的话证明这个信息为背景,1索引概率大就是有物体信息,同时做了生成先验框的操作,请注意先验框是对于原图像大小的,不是对应特征图大小的。随后根据RPN网络预测的框偏移量信息,与先验框合并计算,得到roi,并且剔除面积小于16的框并且限制先验框大小,剔除面积小于16的框是因为他对应的是原图先验框大小,16倍下采样框信息小于1所以剔除。随后将计算好的roi根据置信度排序,限制在一定数量,这里是12000,最后使用非极大值抑制方法进一步缩小roi数量,具体如下:
#-----------------------------------#
# 将先验框转换成tensor
#-----------------------------------#
anchor = torch.from_numpy(anchor).type_as(loc)
#-----------------------------------#
# 将RPN网络预测结果转化成建议框
#-----------------------------------#
roi = loc2bbox(anchor, loc)
#-----------------------------------#
# 防止建议框超出图像边缘
#-----------------------------------#
roi[:, [0, 2]] = torch.clamp(roi[:, [0, 2]], min = 0, max = img_size[1])
roi[:, [1, 3]] = torch.clamp(roi[:, [1, 3]], min = 0, max = img_size[0])
#-----------------------------------#
# 建议框的宽高的最小值不可以小于16
#-----------------------------------#
min_size = self.min_size * scale
keep = torch.where(((roi[:, 2] - roi[:, 0]) >= min_size) & ((roi[:, 3] - roi[:, 1]) >= min_size))[0]
#-----------------------------------#
# 将对应的建议框保留下来 把建议框保存下来
#-----------------------------------#
roi = roi[keep, :]
score = score[keep]
#-----------------------------------#
# 根据得分进行排序,取出建议框
#-----------------------------------#
order = torch.argsort(score, descending=True)
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
score = score[order]
#-----------------------------------#
# 对建议框进行非极大抑制
# 使用官方的非极大抑制会快非常多
#-----------------------------------#
keep = nms(roi, score, self.nms_iou)
if len(keep) < n_post_nms:
index_extra = np.random.choice(range(len(keep)), size=(n_post_nms - len(keep)), replace=True)
keep = torch.cat([keep, keep[index_extra]])
keep = keep[:n_post_nms]
roi = roi[keep] #600筛选框
return roi
接下来求一下,真实框与先验框的“距离”,得到真实的真实框与先验框偏移量,具体求法:首先需要根据真实框与先验框的位置信息,计算IOU找到每个先验框对应真实框最大iou的那个,并且还要得到对应的IOU值,根据这个得到的IOU值,设定是否大于阈值,大于阈值我就把label标签设置为1,小于则0,这一步主要是做前背景分离,随后将样本进一步缩小,缩小为256个,缩小方式就是根据IOU值的大小,并且保证正负样本均衡,分别为128,128,可能会有正样本小于128的情况,那么就从负样本多抽出来一些。并且多出来的负样本的label号设为-1,表示忽略此框信息。
# ------------------------------------------ #
# 1是正样本,0是负样本,-1忽略
# 初始化的时候全部设置为-1
# ------------------------------------------ #
label = np.empty((len(anchor),), dtype=np.int32)
label.fill(-1)
# ------------------------------------------------------------------------ #
# argmax_ious为每个先验框对应的最大的真实框的序号 [num_anchors, ]
# max_ious为每个xianyan框对应的最大的真实框的iou [num_anchors, ]
# gt_argmax_ious为每一个真实框对应的最大的先验框的序号 [num_gt, ]
# ------------------------------------------------------------------------ #
argmax_ious, max_ious, gt_argmax_ious = self._calc_ious(anchor, bbox)
# ----------------------------------------------------- #
# 如果小于门限值则设置为负样本
# 如果大于门限值则设置为正样本
# 每个真实框至少对应一个先验框
# ----------------------------------------------------- #
label[max_ious < self.neg_iou_thresh] = 0
label[max_ious >= self.pos_iou_thresh] = 1
if len(gt_argmax_ious)>0:
label[gt_argmax_ious] = 1
# ----------------------------------------------------- #
# 判断正样本数量是否大于128,如果大于则限制在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
# ----------------------------------------------------- #
# 平衡正负样本,保持总数量为256
# ----------------------------------------------------- #
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
上面已经求了先验框对应真实框最大的那个IOU信息的真实框,二者可以对应,求一下二者之间的偏移量大小,这个就作为我们的真实偏移量大小,真实偏移量大小,随着label一起输出。有了真实偏移量,还有预测偏移量,有了前背景真实label 还有预测的前背景label,接下来直接求LOSS。
# -------------------------------------------------- #
# 分别计算建议框网络的回归损失和分类损失
# -------------------------------------------------- #
rpn_loc_loss = self._fast_rcnn_loc_loss(rpn_loc, gt_rpn_loc, gt_rpn_label, self.rpn_sigma)
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label, ignore_index=-1)
rpn_loc_loss_all += rpn_loc_loss
rpn_cls_loss_all += rpn_cls_loss
RPN部分结束,接下来是ROI部分。
ROI网络作用详解:
上面所述,我们经过RPN网络得到的预测偏移量信息,与先验框进行计算,得到了ROI,接下来了,我们需要真实的ROI信息,首先将roi与真实框求IOU,得到与roi对应的真实框最大的IOU,然后根据置信度筛选是否为正或者负样本,随后将其筛选出128个roi,与对应的真实框计算偏移量,当做真实的ROI值,随后真实标签值需要进行全部加1,因为有背景的存在,随后根据正样本索引,将不是正样本的label值给0。
roi = np.concatenate((roi.detach().cpu().numpy(), bbox), axis=0)
# ----------------------------------------------------- #
# 计算建议框和真实框的重合程度
# ----------------------------------------------------- #
iou = bbox_iou(roi, bbox)
if len(bbox)==0:
gt_assignment = np.zeros(len(roi), np.int32)
max_iou = np.zeros(len(roi))
gt_roi_label = np.zeros(len(roi))
else:
#---------------------------------------------------------#
# 获得每一个建议框最对应的真实框 [num_roi, ]
#---------------------------------------------------------#
gt_assignment = iou.argmax(axis=1)
#---------------------------------------------------------#
# 获得每一个建议框最对应的真实框的iou [num_roi, ]
#---------------------------------------------------------#
max_iou = iou.max(axis=1)
#---------------------------------------------------------#
# 真实框的标签要+1因为有背景的存在
#---------------------------------------------------------#
gt_roi_label = label[gt_assignment] + 1
#----------------------------------------------------------------#
# 满足建议框和真实框重合程度大于neg_iou_thresh_high的作为负样本
# 将正样本的数量限制在self.pos_roi_per_image以内
#----------------------------------------------------------------#
pos_index = np.where(max_iou >= self.pos_iou_thresh)[0]
pos_roi_per_this_image = int(min(self.pos_roi_per_image, pos_index.size))
if pos_index.size > 0:
pos_index = np.random.choice(pos_index, size=pos_roi_per_this_image, replace=False)
#-----------------------------------------------------------------------------------------------------#
# 满足建议框和真实框重合程度小于neg_iou_thresh_high大于neg_iou_thresh_low作为负样本
# 将正样本的数量和负样本的数量的总和固定成self.n_sample
#-----------------------------------------------------------------------------------------------------#
neg_index = np.where((max_iou < self.neg_iou_thresh_high) & (max_iou >= self.neg_iou_thresh_low))[0]
neg_roi_per_this_image = self.n_sample - pos_roi_per_this_image
neg_roi_per_this_image = int(min(neg_roi_per_this_image, neg_index.size))
if neg_index.size > 0:
neg_index = np.random.choice(neg_index, size=neg_roi_per_this_image, replace=False)
#---------------------------------------------------------#
# sample_roi [n_sample, ]
# gt_roi_loc [n_sample, 4]
# gt_roi_label [n_sample, ]
#---------------------------------------------------------#
keep_index = np.append(pos_index, neg_index)
sample_roi = roi[keep_index]
if len(bbox)==0:
return sample_roi, np.zeros_like(sample_roi), gt_roi_label[keep_index]
#看差了多少
gt_roi_loc = bbox2loc(sample_roi, bbox[gt_assignment[keep_index]])
gt_roi_loc = (gt_roi_loc / np.array(loc_normalize_std, np.float32))
gt_roi_label = gt_roi_label[keep_index]
gt_roi_label[pos_roi_per_this_image:] = 0
return sample_roi, gt_roi_loc, gt_roi_label
随后将128个roi输入到roi网络,在经过全连接层,得到输出节点为84,和21的信息,将84扩展为21-4的形式,因为是VOC20个类别在加一个背景类别一共21类,随后根据得到的真实标签值筛选出对应真实标签值的框信息,与上面得到的真实ROI做回归LOSS,全连接层输出21个节点的信息作为类别信息,与真实标签值求LOSS。 这样一个完整的训练网络就形成了。
roi_cls_loc = roi_cls_loc.view(n_sample, -1, 4) #只取出来真实框的值
roi_loc = roi_cls_loc[torch.arange(0, n_sample), gt_roi_label]
# -------------------------------------------------- #
# 分别计算Classifier网络的回归损失和分类损失
# -------------------------------------------------- #
roi_loc_loss = self._fast_rcnn_loc_loss(roi_loc, gt_roi_loc, gt_roi_label.data, self.roi_sigma)
roi_cls_loss = nn.CrossEntropyLoss()(roi_score, gt_roi_label)
roi_loc_loss_all += roi_loc_loss
roi_cls_loss_all += roi_cls_loss