关于各网络的作用及输入输出张量形状
- 关于主干网络与resnet50为例子,网络被拆开使用,layer3及其之前的网络将作为特征提取器进行公共特征的提取,layer4及平均值池化将作为classifier的一部分。
- 主干提取网络
会将特征图的宽高缩减16倍,输出的特征图层数为1024,这些是由网络模型确定的参数,以输入图片为(bn,3,600,600)作为输入,经过特征提取网络后获得(bn,1024,38,38)的公用特征图 - rpn网络
输入是公用特征图和input_size(这里就是600 600),之所以要输入input_size,是因为存储的anchor及roi的信息x1 y1 x2 y2是在原图上的坐标值。首先生成九个锚框,这是完全根据经验值生成三个横框三个竖框三个方框作为初始锚框,并获得anchor形状为(38*38*9, 4),原图上均匀分布38*38个点,作为中心点,每个点上生成9个锚框,这个时候每个点的9个锚框是完全相同的。公用特征层(bn,1024,38,38)进行一次卷积进行特征整合得到(bn,1024,38,38),此特征经过第一个1*1卷积层loc得到(bn,9*4,38,38),进一步变形为rpn_locs(bn,38*38*9,4)即输出对38*38*9个锚框的调整方案。(bn,1024,38,38)整合特征经过第二个1*1卷积层score得到(bn,9*2,38,38),进一步变形为rpn_scores(bn,38*38*9,2)即对锚框调整后的rpn框内包含物体的概率,0代表不包含物体,1代表包含物体。rpn_locs对锚框进行调整后得到38389个预测框,先把边长小于16的预测框删除掉(因为这里预测框的边长是在输入图片上的长度,而共享特征层上的一个点相当于输入图片上的16个像素点,这种情况下如果预测框的边长小于16就意味着缩放到共享特征层尺度上后上连一个像素值都涵盖不了),再一次根据scores值(框内有物体的概率)把scores值较小的一部分删掉,在训练时留下12000个框,在预测时留下6000个,再经过nms,训练时留下600个,测试时留下300个。因此经过proposal后得到roi(bn,600,4)这里4就是x1 y1 x2 y2相对于输入图像的尺寸,roi_indices(bn,600)记录属于那个图片roi_indices[0]全是0,roi_indices[1]全是1…。
rpn网络的输出为:
rpn_locs (bn, 38*38*9, 4) rpn网络输出的对锚框的建议调整数据
rpn_scores (bn, 38*38*9, 2) rpn网络输出的对认为框内有物体的概率
rois (bn, 600, 4) 锚框经过rpn_locs调整后位置
roi_indices (bn, 600) 记录这些框属于那个图片
anchor (1,38*38*9, 4) rois与anchor均是不归一化的x1 yx x2 y2信息且均是相对于原图的尺寸 - classifier网络
输入是共享特征层,rois,roi_indice。先将rois的位置信息从原图尺寸上映射到共享特征图尺寸上,之后将roi与roi_indice混合得到indices_and_rois(bn*600,5),把图片数与rois数进行了合并,5位中第一位是这个roi框是batch中的那个图片的,后4位是x1 y1 x2 y2相对与共享特征图(这里因为每个图片都有600个roi所以在第一位记录这个roi属于那个图片显得有些多余,但实际上进行roi_pooling时每张图片的roi数可以是不同的,这个时候就必须要在第一位指明此roi属于那个图片了)。之后用indices_and_rois对共享特征图进行roi_pooling得到(bn*600,1024,14,14),这个特征层是对共享特征层进行roi池化得到的,之后进行特征整合,使用resnet50中还没有用到的layer4得到(bn*600,2048,7,7)和平均值池化(bn*600,2048),之后分别经过两个1*1卷积层得到 roi_cls_locs (bn*600, (num_classes+1)*4) ,表示bn*600个roi框的调整方式,每一个框提供(num_classes+1)个调整方案,对应(num_classes+1)个类别 +1指的是背景类,roi_scores (bn*600,num_classes+1) 即bn*600个roi框中物体的具体类别概率。进一步进行形状上的调整。
classifier网络的输出为:
roi_cls_locs (bn, 600, (num_classes+1)*4)
roi_scores (bn, 600, (num_classes+1))
关于损失函数的计算
位置损失与分类损失
无论是rpn网络的输出结果
rpn_locs (bn, 38*38*9, 4) rpn网络输出的对锚框的建议调整数据
rpn_scores (bn, 38*38*9, 2) rpn网络输出的对认为框内有物体的概率
还是classifier网络的输出结果
roi_cls_locs (bn, 600, (num_classes+1)*4)
roi_scores (bn, 600, (num_classes+1))
locs均为对框进行调整的方案,那么真值自然是如何对当前的框进行调整得到真实框,计算预测值和真值之间的平滑一范数损失,百度搜索smooth_l1_loss,一种一范数损失的平滑变体,至于scores均为分类交叉熵损失函数
对于rpn网络输出结果计算损失函数
- 再次回顾rpn网络的输出为:
rpn_locs (bn, 38*38*9, 4) rpn网络输出的对锚框的建议调整数据
rpn_scores (bn, 38*38*9, 2) rpn网络输出的对认为框内有物体的概率
rois (bn, 600, 4) 锚框经过rpn_locs调整后位置
roi_indices (bn, 600) 记录这些框属于那个图片
anchor (1,38*38*9, 4) rois与anchor均是不归一化的x1 yx x2 y2信息且均是相对于原图的尺寸
注意rpn_locs与rpn_scores是rpn网络的直接输出结果,而rois与roi_indices是经过proposal这么一个无参数过程选择出来的,因此这里需要根据真实框对rpn_locs与rpn_scores构造标签,去训练rpn网络,这个构建标签的类就是AnchorTargetCreator类,值得一提的是这个类是没有批处理能力的,因为每张图片的真实框的个数都不尽相同,所以是这个类是对单个图片进行单独处理的。 - 首先构建了一些超参数n_sample=256是样本点的个数(总计有38*38*9个输出结果,我们并不对所有输出结果进行训练,只选取一定量正样本与负样本进行训练), pos_iou_thresh=0.7(选定正样本的标准), neg_iou_thresh=0.3(选定负样本的标准), pos_ratio=0.5(正样本比例)
- _calc_ious函数是对比iou值的
输入是
anchors (1, 38*38*9, 4)即rpn网络生成的初始化锚框位置信息
bbox (num_gt, 4)即此图片的真实框信息
返还
argmax_ious (38*38*9,) 记录了锚框将对应的真实框的编号,每个锚框对应那个真实框代码中先是用锚框选真实框(即把与该锚框iou值最大的真实框匹配给该锚框),这会导致有的真实框没有被选中,为了防止这种情况,又用真实框去选锚框,这样每个真实框一定会选中一个锚框,此时把这个真实框匹配给这个锚框,这保证了每个真实框都有锚框与之对应。
max_ious (38*38*9,) 记录了锚框与其iou值最大的真实框的iou值,这里与argmax_ious不同,这里不保证每个真实框都被选中。
gt_argmax_ious (num_gt,) 每个真实框最对应的锚框 - _create_label
输入是
anchors (1, 38*38*9, 4)即rpn网络生成的初始化锚框位置信息
bbox (num_gt, 4)即此图片的真实框信息
过程是根据argmax_ious,及阈值pos_iou_thresh=0.7(选定正样本的标准), neg_iou_thresh=0.3(选定负样本的标准)去选择正负样本,大于阈值是正样本点,小于阈值是负样本点,且先确保正样本点最多为n_sample*pos_ratio个,不足则无所谓,大于则从中随机选n_sample*pos_ratio个正样本点,之后确保负样本点为n_sample - n_positive个,即确保正负样本总计为n_sample个
返还是
argmax_ious (38*38*9,) 锚框对应那个真实框
label (38*38*9,)) 锚框是否是正样本点或负样本点或忽略点 - call 函数
输入是
anchors (1, 38*38*9, 4)即rpn网络生成的初始化锚框位置信息
bbox (num_gt, 4)即此图片的真实框信息
过程是在argmax_ious已知的情况下我们知道了锚框希望被调整为那个真实框,而怎么调整可以通过计算得到,于是得到loc (38*38*9,4) 记录把anchors转化为bbox的信息
输出是
loc (38*38*9,4) 记录把anchors转化为bbox的信息
label (38*38*9,) 锚框是否是正样本点或负样本点或忽略点 - 总结_calc_ious函数给出了真实框与锚框之间iou值的各种信息,并确定了锚框期望被调整为那个真实框,_create_label根据iou值等信息,确认了锚框中那些是正样本点那些是负样本点,那些是忽略样本点,得到了关键的label,最后call函数根据argmax_ious,计算了将锚框调整为真实框所需要的偏移量得到了loc。
- loc (38*38*9,4)与rpn_loc (38*38*9, 4)求smoth L1损失
label (38*38*9,)与rpn_score (38*38*9, 2)求交叉熵损失这里值得一提的是
rpn_cls_loss = F.cross_entropy(rpn_score, label, ignore_index=-1),指的是忽略掉label中为-1的标签,这意味着计算分类损失时只计算了网络输出的中正样本点与负样本点的分类结果的损失,在计算位置损失时计算了网络输出的对所有锚框的调整方案的损失。
class AnchorTargetCreator(object): # 获取rpn网络的输出结果的标签只能对单张样本进行处理,没有批处理的能力,因为每张样本的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):
argmax_ious, label = self._create_label(anchor, bbox)
if (label > 0).any():
loc = bbox2loc(anchor, bbox[argmax_ious])
# loc (h*w*9,4) # 如何把anchors转化为bbox的信息,作为
return loc, label
else:
return np.zeros_like(anchor), label
def _calc_ious(self, anchor, bbox):
# anchors (h*w*9,4)
# bbox (nb, 4)
#----------------------------------------------#
# anchor和bbox的iou
# 获得的ious的shape为[num_anchors, num_gt]
#----------------------------------------------#
ious = bbox_iou(anchor, bbox)
if len(bbox)==0:
return np.zeros(len(anchor), np.int32), np.zeros(len(anchor)), np.zeros(len(bbox))
#---------------------------------------------------------#
# 获得每一个先验框最对应的真实框 [num_anchors, ]
#---------------------------------------------------------#
argmax_ious = ious.argmax(axis=1)
#---------------------------------------------------------#
# 找出每一个先验框最对应的真实框的iou [num_anchors, ]
#---------------------------------------------------------#
max_ious = np.max(ious, axis=1)
#---------------------------------------------------------#
# 获得每一个真实框最对应的先验框 [num_gt, ]
#---------------------------------------------------------#
gt_argmax_ious = ious.argmax(axis=0)
#---------------------------------------------------------#
# 保证每一个真实框都存在对应的先验框
#---------------------------------------------------------#
# argmax_ious (h*w*9,) 每个先验框对应那个真实框 这里每个真真实框都至少被选中一次
# max_ious (h*w*9,) 每个先验框对应的iou值最大的真实框的iou
# gt_argmax_ious (num_gt,) 每个真实框最对应的先验框
for i in range(len(gt_argmax_ious)):
argmax_ious[gt_argmax_ious[i]] = i
return argmax_ious, max_ious, gt_argmax_ious
def _create_label(self, anchor, bbox):
# anchors (h*w*9,4)
# bbox (nb, 4)
# ------------------------------------------ #
# 1是正样本,0是负样本,-1忽略
# 初始化的时候全部设置为-1
# ------------------------------------------ #
label = np.empty((len(anchor),), dtype=np.int32)
label.fill(-1)
# ------------------------------------------------------------------------ #
# argmax_ious为每个先验框对应的最大的真实框的序号 [num_anchors, ]
# max_ious为每个先验框对应的最大的真实框的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 # 真实框iou值最大的先验框也是正样本点,无论iou值如何
# ----------------------------------------------------- #
# 判断正样本数量是否大于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
# argmax_ious (h*w*9,) 先验框对应那个真实框
# label (h*w*9,) 先验框是否是正样本点或负样本点或忽略点
return argmax_ious, label
对于classifier网络输出结果计算损失函数
- 回顾classifier网络的输出结果
roi_cls_locs (bn, 600, (num_classes+1)*4)
roi_scores (bn, 600, (num_classes+1)) - ProposalTargetCreator类其实不仅提供了classifier输出的标签,还提供了classifier类的输入
- 构建类时提供的属性
n_sample=128 样本个数
pos_ratio=0.5 正样本比例
pos_iou_thresh=0.5 正样本阈值下限
neg_iou_thresh_high=0.5 负样本阈值上限
neg_iou_thresh_low=0 负样本阈值下限 - call函数
输入
roi (600, 4)
bbox (num_gt, 4)
label (num_gt,) 保存了真实框中物体的具体类别
过程依然是根据roi框与真实框的iou来确定正负样本点,其他的样本点直接忽略
输出
sample_roi (128, 4) 选出128个roi框,并且有这些roi框的位置坐标
gt_roi_loc (128, 4) 这128个roi框应该酱紫调整才能得到其对应的真实框
gt_roi_label (128, ) 这128个roi框中物体的类别与输入label的不同在于这里类别数加一,所有负样本点认为其中框中的是背景类即为第0类 - 于是这批图片共有bn*128个感兴趣区域,sample_rois经过classifier后得到的输出是
roi_cls_locs (bn, 128, (num_classes+1)*4)
roi_scores (bn, 128, (num_classes+1)) - 之后仍然是对每张图片单独计算损失
roi_cls_loc (128, (num_classes+1)*4)其实对每个roi框生成了num_classes+1个调整方案,因为真实的类别是已知的(gt_roi_label),所以只选择将其调整为真实类别的那个方案进行优化,即
roi_loc = roi_cls_loc[torch.arange(0, n_sample), gt_roi_label]
从而roi_loc(128, 4) 与 gt_roi_loc (128, 4)求取smoth L1损失
roi_score (128, (num_classes+1))与gt_roi_label (128, )求取交叉熵损失 - tips:call函数一上来有这么一行
roi = np.concatenate((roi.detach().cpu().numpy(), bbox), axis=0)
即把真实框混入roi中生成新的roi,我对这个的理解是:第一,如果rpn网络训练不良好,则roi区域预选的感兴趣区域并不好,classifier学习对感兴趣区域进行微调得到最终的预测框的难度较大,第二,将真实框混入roi中后,后续sample_roi中的128个样本点可能包含有真实框,这个时候gt_roi_loc将告诉网络这些真实框roi不需要进行调整,这也是告诉网路这些roi就是真实框不需要进行调整,因此也给classifer网络提供了有用的信息
class ProposalTargetCreator(object):
def __init__(self, n_sample=128, pos_ratio=0.5, pos_iou_thresh=0.5, neg_iou_thresh_high=0.5, neg_iou_thresh_low=0):
self.n_sample = n_sample
self.pos_ratio = pos_ratio
self.pos_roi_per_image = np.round(self.n_sample * self.pos_ratio)
self.pos_iou_thresh = pos_iou_thresh
self.neg_iou_thresh_high = neg_iou_thresh_high
self.neg_iou_thresh_low = neg_iou_thresh_low
def __call__(self, roi, bbox, label, loc_normalize_std=(0.1, 0.1, 0.2, 0.2)):
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 + num_gt, ]
#---------------------------------------------------------#
gt_assignment = iou.argmax(axis=1)
#---------------------------------------------------------#
# 获得每一个建议框最对应的真实框的iou [num_roi + num_gt, ]
#---------------------------------------------------------#
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, 4]
# 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
关于训练流程
b导给的代码中,rpn网路与classifier网络是同训练的,但好像普遍的faster-rcnn训练方式是4步走,详情可以参考其他博客
感谢b导的代码:https://github.com/bubbliiiing/faster-rcnn-pytorch
感谢b导的博客:https://blog.csdn.net/weixin_44791964/article/details/105739918
最后:tte的PhD读不了一点。