深度解析博客,有些地方难理解可以看看博客。
retinafece采用的损失函数和SSD的相似,在SSD基础上添加了人脸关键点分支
一、先验框生成代码
在三个不同尺度的特征图上生成先验框,
def get_anchors(self):anchors = []# 输出三个尺度:# [80,80],[40,40],[20,20]# 遍历每个特征图,并在每个特征图的每个网格上生成两个先验框# 先验框大小为:# [16,32],[64,128],[256,512]此时的尺寸为在原图[640,640]上# 在[80,80]特征图的每个像素点上生成两个先验框,两个先验框尺寸为[16,32]/640# 在[40,40]特征图的每个像素点上生成两个先验框,两个先验框尺寸为[64,128]/640# 在[20,20]特征图的每个像素点上生成两个先验框,两个先验框尺寸为[256,512]/640for k, f in enumerate(self.feature_maps):#遍历三个特征层min_sizes = self.min_sizes[k]#-----------------------------------------## 对特征层的高和宽进行循环迭代#-----------------------------------------#for i, j in product(range(f[0]), range(f[1])):#遍历每个特征层的每个网格for min_size in min_sizes:#每次生成一个尺寸的先验框#生成每个网格先验框的尺寸s_kx = min_size / self.image_size[1]#16/640s_ky = min_size / self.image_size[0]#16/640#对坐标进行归一化,生成每个网格中心坐标dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]]dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]]for cy, cx in product(dense_cy, dense_cx):anchors += [cx, cy, s_kx, s_ky]#转换为[16800, 4]的矩阵,即总共16800个先验框,每个先验框四个信息[x,y,w,h]output = torch.Tensor(anchors).view(-1, 4)if self.clip:output.clamp_(max=1, min=0)return output
二、损失函数代码
def forward(self, predictions, priors, targets):#--------------------------------------------------------------------## predictions代表网络预测得到的信息# priors代表先验框信息# targets代表真实框信息# 取出预测结果的三个值:框的回归信息,置信度,人脸关键点的回归信息#--------------------------------------------------------------------#loc_data, conf_data, landm_data = predictions#--------------------------------------------------## 计算出batch_size和先验框的数量#--------------------------------------------------#num = loc_data.size(0)num_priors = (priors.size(0))#--------------------------------------------------## 创建一个tensor进行处理#--------------------------------------------------#loc_t = torch.Tensor(num, num_priors, 4)landm_t = torch.Tensor(num, num_priors, 10)conf_t = torch.LongTensor(num, num_priors)for idx in range(num):#取出一张图片进行处理# 获得真实框与标签truths = targets[idx][:, :4].datalabels = targets[idx][:, -1].datalandms = targets[idx][:, 4:14].data# 获得先验框defaults = priors.data#--------------------------------------------------## 利用真实框和先验框进行匹配。# 如果真实框和先验框的重合度较高,则认为匹配上了。# 该先验框用于负责检测出该真实框。#--------------------------------------------------#match(self.threshold, truths, defaults, self.variance, labels, landms, loc_t, conf_t, landm_t, idx)#--------------------------------------------------## 转化成Variable# loc_t (num, num_priors, 4)# conf_t (num, num_priors)# landm_t (num, num_priors, 10)#--------------------------------------------------#zeros = torch.tensor(0)if self.cuda:loc_t = loc_t.cuda()conf_t = conf_t.cuda()landm_t = landm_t.cuda()zeros = zeros.cuda()#------------------------------------------------------------------------## 有人脸关键点的人脸真实框的标签为1,没有人脸关键点的人脸真实框标签为-1# 所以计算人脸关键点loss的时候pos1 = conf_t > zeros# 计算人脸框的loss的时候pos = conf_t != zeros#------------------------------------------------------------------------#pos1 = conf_t > zerospos_idx1 = pos1.unsqueeze(pos1.dim()).expand_as(landm_data)landm_p = landm_data[pos_idx1].view(-1, 10)landm_t = landm_t[pos_idx1].view(-1, 10)loss_landm = F.smooth_l1_loss(landm_p, landm_t, reduction='sum')pos = conf_t != zerospos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)loc_p = loc_data[pos_idx].view(-1, 4)loc_t = loc_t[pos_idx].view(-1, 4)loss_l = F.smooth_l1_loss(loc_p, loc_t, reduction='sum')#--------------------------------------------------## batch_conf (num * num_priors, 2)# loss_c (num, num_priors)#--------------------------------------------------#conf_t[pos] = 1batch_conf = conf_data.view(-1, self.num_classes)# 这个地方是在寻找难分类的先验框loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))# 难分类的先验框不把正样本考虑进去,只考虑难分类的负样本loss_c[pos.view(-1, 1)] = 0loss_c = loss_c.view(num, -1)#--------------------------------------------------## loss_idx (num, num_priors)# idx_rank (num, num_priors)#--------------------------------------------------#_, loss_idx = loss_c.sort(1, descending=True)_, idx_rank = loss_idx.sort(1)#--------------------------------------------------## 求和得到每一个图片内部有多少正样本# num_pos (num, )# neg (num, num_priors)#--------------------------------------------------#num_pos = pos.long().sum(1, keepdim=True)# 限制负样本数量num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)neg = idx_rank < num_neg.expand_as(idx_rank)#--------------------------------------------------## 求和得到每一个图片内部有多少正样本# pos_idx (num, num_priors, num_classes)# neg_idx (num, num_priors, num_classes)#--------------------------------------------------#pos_idx = pos.unsqueeze(2).expand_as(conf_data)neg_idx = neg.unsqueeze(2).expand_as(conf_data)# 选取出用于训练的正样本与负样本,计算lossconf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1,self.num_classes)targets_weighted = conf_t[(pos+neg).gt(0)]loss_c = F.cross_entropy(conf_p, targets_weighted, reduction='sum')N = max(num_pos.data.sum().float(), 1)loss_l /= Nloss_c /= Nnum_pos_landm = pos1.long().sum(1, keepdim=True)N1 = max(num_pos_landm.data.sum().float(), 1)loss_landm /= N1return loss_l, loss_c, loss_landm
三、先验框匹配算法
因为正样本相比于负样本数量太少,采用匹配的方式为真实框匹配多个先验框,更好的平衡正负样本比例
将所有先验框和真实框进行匹配,重合度高的用于预测目标,重合度低的作为背景,匹配后的信息进行了编码,编码为与先验框的偏差,网络学习如何预测偏差。
在训练过程中,首先要确定训练图片中的ground truth(真实目标)与哪个prior boxes(先验框)来进行匹配,与之匹配的先验框所对应的边界框将负责预测它。Retinaface的prior boxes与ground truth的匹配原则主要有两点:
- 首先,对于图片中每个ground truth,找到与其IOU最大的prior boxes,该prior boxes与其匹配,这样,可以保证每个ground truth一定与某个prior boxes匹配。通常称与ground truth匹配的prior boxes为正样本(其实应该是先验框对应的预测box,不过由于是一一对应的就这样称呼了),反之,若一个prior boxes没有与任何ground truth进行匹配,那么该prior boxes只能与背景匹配,就是负样本。
- 对于剩余的未匹配prior boxes,若某个ground truth的 IOU大于某个阈值(一般是0.5),那么该先验框也与这个ground truth进行匹配。一个图片中ground truth是非常少的, 而prior boxes却很多,如果仅按第一个原则匹配,很多prior boxes会是负样本,正负样本极其不平衡,所以需要第二个原则。
尽管一个ground truth可以与多个prior boxes匹配,但是ground truth相对prior boxes还是太少了,所以负样本相对正样本会很多。为了保证正负样本尽量平衡,Retinaface采用了hard negative mining,就是对负样本进行抽样,抽样时按照置信度误差(预测背景的置信度越小,误差越大)进行降序排列,选取误差的较大的top-k作为训练的负样本,以保证正负样本比例接近1:3。
Hard Negative Mining技术
一般情况下negative default boxes数量是远大于positive default boxes数量,如果随机选取样本训练会导致网络过于重视负样本(因为抽取到负样本的概率值更大一些),这会使得loss不稳定。因此需要平衡正负样本的个数,我们常用的方法就是Hard Ngative Mining,即依据confidience score对default box进行排序,挑选其中confidience高的box进行训练,将正负样本的比例控制在positive:negative=1:3,这样会取得更好的效果。如果我们不加控制的话,很可能会出现Sample到的所有样本都是负样本(即让网络从这些负样本中找正确目标,这显然是不可以的),这样就会使得网络的性能变差
def match(threshold, truths, priors, variances, labels, landms, loc_t, conf_t, landm_t, idx):#----------------------------------------------## 计算所有的先验框和真实框的重合程度#----------------------------------------------#overlaps = jaccard(truths,point_form(priors))#----------------------------------------------## 所有真实框和先验框的最好重合程度,为每个真实框找出IOU最大的先验框# best_prior_overlap [truth_box,1]# best_prior_idx [truth_box,1]#----------------------------------------------#best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)best_prior_idx.squeeze_(1)best_prior_overlap.squeeze_(1)#----------------------------------------------## 所有先验框和真实框的最好重合程度,为每个先验框找出IOU最大的真实框# best_truth_overlap [1,prior]# best_truth_idx [1,prior]#----------------------------------------------#best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)best_truth_idx.squeeze_(0)best_truth_overlap.squeeze_(0)#----------------------------------------------## 用于保证每个真实框都至少有对应的一个先验框# 因为有可能存在没有先验框和真实框匹配,或IOU太低被过滤掉#----------------------------------------------##将与真实框匹配程度最高的先验框IOU置为2,确保它能被保留下来best_truth_overlap.index_fill_(0, best_prior_idx, 2)# 对best_truth_idx内容进行设置# 更改上面置为2的先验框索引for j in range(best_prior_idx.size(0)):best_truth_idx[best_prior_idx[j]] = j#----------------------------------------------## 获取每一个先验框对应的真实框[num_priors,4]#----------------------------------------------#matches = truths[best_truth_idx]# Shape: [num_priors] 此处为将每一个anchor对应的label取出来conf = labels[best_truth_idx]matches_landm = landms[best_truth_idx]#----------------------------------------------## 如果重合程度小于threhold则认为是背景#----------------------------------------------#conf[best_truth_overlap < threshold] = 0#----------------------------------------------## 利用真实框和先验框进行编码# 编码后的结果就是网络应该有的预测结果#----------------------------------------------#loc = encode(matches, priors, variances)landm = encode_landm(matches_landm, priors, variances)#----------------------------------------------## [num_priors, 4]#----------------------------------------------#loc_t[idx] = loc#----------------------------------------------## [num_priors]#----------------------------------------------#conf_t[idx] = conf#----------------------------------------------## [num_priors, 10]#----------------------------------------------#landm_t[idx] = landm
3.1 point_from()
转换坐标
#将[中心点x,中心点y,w,h]形式转化为[左上角x,左上角y,右下角x,右下角y]def point_form(boxes):return torch.cat((boxes[:, :2] - boxes[:, 2:]/2,boxes[:, :2] + boxes[:, 2:]/2), 1)
3.2 jaccard()
计算IOU
#计算IOUdef jaccard(box_a, box_b):#-------------------------------------## 返回的inter的shape为[A,B]# 代表每一个真实框和先验框的交矩形#-------------------------------------#inter = intersect(box_a, box_b)#-------------------------------------## 计算先验框和真实框各自的面积#-------------------------------------#area_a = ((box_a[:, 2]-box_a[:, 0]) *(box_a[:, 3]-box_a[:, 1])).unsqueeze(1).expand_as(inter) # [A,B]area_b = ((box_b[:, 2]-box_b[:, 0]) *(box_b[:, 3]-box_b[:, 1])).unsqueeze(0).expand_as(inter) # [A,B]union = area_a + area_b - inter#-------------------------------------## 每一个真实框和先验框的交并比[A,B]#-------------------------------------#return inter / union # [A,B]
3.3 先验框编码
坐标编码公式如下
g_cxcy求的是gt bx 和prior box的偏差
求的是gt box和prior box的宽高比例值,通过
函数进行函数映射
def encode(matched, priors, variances):# 进行编码的操作g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]# 中心编码g_cxcy /= (variances[0] * priors[:, 2:])# 宽高编码g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]g_wh = torch.log(g_wh) / variances[1]return torch.cat([g_cxcy, g_wh], 1) # [num_priors,4]
3.4 人脸关键点编码
求得人脸关键点和先验框的偏差。
def encode_landm(matched, priors, variances):# 将人脸关键点信息分开,总共五个关键点,每个关键点两个坐标信息[x,y]matched = torch.reshape(matched, (matched.size(0), 5, 2))priors_cx = priors[:, 0].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)priors_cy = priors[:, 1].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)priors_w = priors[:, 2].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)priors_h = priors[:, 3].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)priors = torch.cat([priors_cx, priors_cy, priors_w, priors_h], dim=2)# 减去中心后除上宽高g_cxcy = matched[:, :, :2] - priors[:, :, :2]g_cxcy /= (variances[0] * priors[:, :, 2:])g_cxcy = g_cxcy.reshape(g_cxcy.size(0), -1)return g_cxcy
四、smooth_l1_loss
PyTorch smooth_l1_loss计算公式:
Localization Loss
