本文章是从代码层面可以更好的了解YOLOv4的损失函数,从实践过程中去了解这部分的处理过程。
这里先大致说一下这一实现过程:
1)获得target形式【就是我们标注的目标真实信息】
2)batch_target【获取1)中target映射到特征层上box信息类别】
3)计算batch_target中的box和缩放后anchor的IOU,获得anchor和gt最大iou,得到anchor的索引【表示这些anchor内是ground truth】
4)步骤3可以获得由哪些anchor来表示gt,或者说知道了由哪些anchor来预测。但还不知道目标落在了哪个cell内,可对步骤2中的中心点取整来判断目标落在哪个cell.
5)获得y_true和noobj_mask【记录cell中有无目标以及类别信息,根据步骤4即可获得】
6)获得预测框位于cell网格坐标信息【比如x+grid_x,y_grid_y】
7)将步骤6中的预测框和步骤5中y_true中box计算loc loss.
8)将网络输出的类别置信度和步骤5中y_truth计算分类loss
9)将网络有无目标置信度和步骤5中y_true有无目标置信度计算conf loss
目录
target形式
forward部分
get_target(获得y_true)
batch_target[真实值映射到特征层]
IOU计算并获得anchor索引:
判断目标落在哪个特征层的哪个先验框
get_ignore:判断预测结果和真实结果的重合度
生成网格
计算调整后的先验框中心和宽高:
计算交并比:
loss计算
这里先将LOSS中的初始化参数列出来。
class YOLOLoss(nn.Module):
def __init__(self, anchors, num_classes, input_shape, cuda, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]], label_smoothing = 0, focal_loss = False, alpha = 0.25, gamma = 2):
super(YOLOLoss, self).__init__()
#-----------------------------------------------------------#
# 13x13的特征层对应的anchor是[142, 110],[192, 243],[459, 401]
# 26x26的特征层对应的anchor是[36, 75],[76, 55],[72, 146]
# 52x52的特征层对应的anchor是[12, 16],[19, 36],[40, 28]
#-----------------------------------------------------------#
self.anchors = anchors # 先验框
self.num_classes = num_classes # 类的数量
self.bbox_attrs = 5 + num_classes # bbox参数 5:x,y,w,h,p
self.input_shape = input_shape # 网络输入大小
self.anchors_mask = anchors_mask
self.label_smoothing = label_smoothing # 标签平滑
self.balance = [0.4, 1.0, 4]
self.box_ratio = 0.05
self.obj_ratio = 5 * (input_shape[0] * input_shape[1]) / (416 ** 2)
self.cls_ratio = 1 * (num_classes / 80)
self.focal_loss = focal_loss
self.alpha = alpha
self.gamma = gamma
self.ignore_threshold = 0.5
self.cuda = cuda
在训练代码中outputs是我们的三个预测特征层,outputs[l]就是分别遍历这三个特征层,如果输入大小是416 * 416,那么三个特征层就是13 * 13,26 * 26, 52 *52,如果是608的就是19 * 19, 38 * 38,76 * 76,可以看到yolo_loss,传入了三个参数,l是预测层的索引,outputs[l]是对应第几个层,targets就是我们的真实值:
for l in range(len(outputs)):
loss_item = yolo_loss(l, outputs[l], targets)
当我们第一次遍历的时候,此刻l=0,outputs[0]的shape为【batch_size,3*(5+num_classe),19 * 19,5指的是box的参数(x,y,w,h,conf)】,我这里网络输入大小为608的,只有一个类,batch_size=4,所以我这里的大小是【4,18,19,19】。
target是我们的真实值,大小【batch_size,5】,target的形式是用list进行存储的,由于我这里有4个batch,所以列表长度为4,每个元素又是5列【x,y,w,h,class】。我们再仔细看一下target的具体内容,前面说了列表的长度为4即代表了4个batch【4张图】,但我们可以看到第一个target[0]的长度又为4,target[1]、target[2]、target[3]的长度又变成了1,这里是怎么回事呢?target[0]的长度为4这是因为在这张图里,我标注了4个目标即存在4个真实值,其他的三张样本均标注了一个目标【这个标注的目标就是你用标注工具标注的】。前面的4列代表了x,y,w,h【我们一般标注的box是左上和右下坐标,这里转成了中心点坐标和宽与高了】,最后一列全是0,即代表了类,我这里只有一个类。
[
tensor([
[0.6028, 0.2599, 0.1398, 0.2664, 0.0000],
[0.4391, 0.2870, 0.3026, 0.4194, 0.0000],
[0.3775, 0.8988, 0.1595, 0.2023, 0.0000],
[0.9194, 0.7442, 0.1612, 0.2944, 0.0000],
[0.8092, 0.3158, 0.2171, 0.3520, 0.0000]], device='cuda:0'),tensor([[0.4359, 0.5592, 0.2467, 0.7566, 0.0000]], device='cuda:0'),
tensor([[0.7401, 0.4868, 0.2105, 0.2171, 0.0000]], device='cuda:0'),
tensor([[0.3873, 0.5518, 0.7747, 0.8964, 0.0000]], device='cuda:0')
]
然后我们直接看loss函数的forward()。这里有三个值,l是对应0,1,2【特征层索引】,input就是前面说的model产生的outputs,target是前面提到的真实值。
def forward(self, l, input, targets=None):
这里input的shape为【4,3*(5+1),19,19】=【4,18,19,19】。
从input获得batch_size,特征层的尺寸h,w.
#--------------------------------#
# 获得图片数量,特征层的高和宽
#--------------------------------#
bs = input.size(0) # batch_size input.size(1)=3*(5+num_classes)
in_h = input.size(2)
in_w = input.size(3)
bs=4
in_h=19
in_w=19
计算步长
这里的步长实际就是指的缩放比,比如我的输入大小为608,此刻的特征层为19 * 19,那么608缩小了32倍。相当于此刻特征层的一个特征点【像素点】对应我原图中的32个像素点。
#-----------------------------------------------------------------------#
# 计算步长
# 每一个特征点对应原来的图片上多少个像素点
#
# 如果特征层为13x13的话,一个特征点就对应原来的图片上的32个像素点 实际就是原图缩小了多少倍 32倍
# 如果特征层为26x26的话,一个特征点就对应原来的图片上的16个像素点 16倍
# 如果特征层为52x52的话,一个特征点就对应原来的图片上的8个像素点 8倍
# stride_h = stride_w = 32、16、8
# a_h, a_w
# (anchor([[ 12., 16.], # 从这开始是52*52的
# [ 19., 36.],
# [ 40., 28.],
# [ 36., 75.], # 这开始是26*26的
# [ 76., 55.],
# [ 72., 146.],
# [142., 110.], # 从这往下是13*13的
# [192., 243.],
# [459., 401.]]), 9)
#-----------------------------------------------------------------------#
stride_h = self.input_shape[0] / in_h
stride_w = self.input_shape[1] / in_w
stride_h = 32.0
stride_w = 32.0
同理的,我们的anchor也需要进行一个缩放。
我们原来的anchor大小为,分别对应我们三个特征层的大中小设置的anchor:
[[ 12. 16.],
[ 19. 36.],
[ 40. 28.],------------->大特征层
[ 36. 75.],
[ 76. 55.],
[ 72. 146.],------------>中特征层
[142. 110.],
[192. 243.],
[459. 401.]] ----------->小特征层
anchor的缩放
scaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in self.anchors]
此刻的步长是32,得到缩放后的anchor大小为:
[(0.375, 0.5),
(0.59375, 1.125),
(1.25, 0.875),
(1.125, 2.34375),
(2.375, 1.71875),
(2.25, 4.5625),
(4.4375, 3.4375),
(6.0, 7.59375),
(14.34375, 12.53125)]
然后我们对input【也就是网络的output】进行一个reshape,我们原来的shape是【4,3*(5+1),19,19】,通过view变为【4,3,5+1,19,19】=[4,3,6,19,19],再通过permute对维度进行转化变为【4,3,19,19,6】.
prediction = input.view(bs, len(self.anchors_mask[l]), self.bbox_attrs, in_h, in_w).permute(0, 1, 3, 4, 2).contiguous()
通过上面的操作,我们把(5+num_classes)这个维度放在了最后,这里再说一下5值哪些【center_x,center_y,w,h,conf】。
获得先验框的中心位置:
#-----------------------------------------------#
# 先验框的中心位置的调整参数
# prediction[...,0]=prediction[:,:,:,:,0]
# x,y是经过sigmoid后的0到1之间的数
#-----------------------------------------------#
x = torch.sigmoid(prediction[..., 0])
y = torch.sigmoid(prediction[..., 1])
获得先验框的宽和高:
#-----------------------------------------------#
# 先验框的宽高调整参数
#-----------------------------------------------#
w = prediction[..., 2]
h = prediction[..., 3]
获得置信度,是否有物体:
表示每个cell内的每个anchor有物体的概率.shape为【batch_size,3,19,19】
#-----------------------------------------------#
# 获得置信度,是否有物体
#-----------------------------------------------#
conf = torch.sigmoid(prediction[..., 4])
获得类的置信度:
表示每个cell分类概率,shape为【batch_size,3,19,19,num_classes】
#-----------------------------------------------#
# 种类置信度 prediction[..., 5:]取的是类别,shape(batchsize,3,13,13,num_classes)
#-----------------------------------------------#
pred_cls = torch.sigmoid(prediction[..., 5:])
获得网络预测结果
#-----------------------------------------------#
# 获得网络应该有的预测结果
#-----------------------------------------------#
y_true, noobj_mask, box_loss_scale = self.get_target(l, targets, scaled_anchors, in_h, in_w)
这里用到了get_target这个函数。先附上该函数的代码
def get_target(self, l, targets, anchors, in_h, in_w):
# 特征图索引:0,1,2,
# targets:列表形式,长度batchsize,每个元素又是5列,边界框信息和类别
# scaled_anchors:缩放到特征层的anchor
# in_h, in_w:特征层大小
#-----------------------------------------------------#
# 计算一共有多少张图片
#-----------------------------------------------------#
bs = len(targets)
#-----------------------------------------------------#
# 用于选取哪些先验框不包含物体
#-----------------------------------------------------#
# 创建一个(batshzie,3,13,13)全1矩阵
noobj_mask = torch.ones(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False)
#-----------------------------------------------------#
# 让网络更加去关注小目标
# 创建一个(batshzie,3,13,13)全0矩阵
#-----------------------------------------------------#
box_loss_scale = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False)
#-----------------------------------------------------#
# batch_size, 3, 13, 13, 5 + num_classes
# y_true用来存放ground truth 信息
#-----------------------------------------------------#
y_true = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, self.bbox_attrs, requires_grad = False)
for b in range(bs):
if len(targets[b])==0:
continue
batch_target = torch.zeros_like(targets[b])
#-------------------------------------------------------#
# 计算出正样本在特征层上的中心点?
#-------------------------------------------------------#
batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w
batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h
batch_target[:, 4] = targets[b][:, 4]
batch_target = batch_target.cpu()
#-------------------------------------------------------#
# 将真实框转换一个形式
# num_true_box, 4
#-------------------------------------------------------#
gt_box = torch.FloatTensor(torch.cat((torch.zeros((batch_target.size(0), 2)), batch_target[:, 2:4]), 1))
#-------------------------------------------------------#
# 将先验框转换一个形式
# 9, 4
#-------------------------------------------------------#
anchor_shapes = torch.FloatTensor(torch.cat((torch.zeros((len(anchors), 2)), torch.FloatTensor(anchors)), 1))
#-------------------------------------------------------#
# 计算交并比
# self.calculate_iou(gt_box, anchor_shapes) = [num_true_box, 9]每一个真实框和9个先验框的重合情况
# best_ns:
# [每个真实框最大的重合度max_iou, 每一个真实框最重合的先验框的序号]
#-------------------------------------------------------#
best_ns = torch.argmax(self.calculate_iou(gt_box, anchor_shapes), dim=-1)
for t, best_n in enumerate(best_ns):
if best_n not in self.anchors_mask[l]:
continue
#----------------------------------------#
# 判断这个先验框是当前特征点的哪一个先验框
# [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
#----------------------------------------#
k = self.anchors_mask[l].index(best_n)
#----------------------------------------#
# 获得真实框属于哪个网格点
# floor 返回一个新张量,包含输入input张量每个元素的floor,即取不大于元素的最大整数。
#----------------------------------------#
i = torch.floor(batch_target[t, 0]).long()
j = torch.floor(batch_target[t, 1]).long()
#----------------------------------------#
# 取出真实框的种类
#----------------------------------------#
c = batch_target[t, 4].long()
#----------------------------------------#
# noobj_mask代表无目标的特征点
#----------------------------------------#
noobj_mask[b, k, j, i] = 0
#----------------------------------------#
# tx、ty代表中心调整参数的真实值
# y_true[b, k, j, i, 0] 意思是取第几各个batch的第k个锚框(每个层有三个),在特征层上第j行第i列网格点
#----------------------------------------#
y_true[b, k, j, i, 0] = batch_target[t, 0]
y_true[b, k, j, i, 1] = batch_target[t, 1]
y_true[b, k, j, i, 2] = batch_target[t, 2]
y_true[b, k, j, i, 3] = batch_target[t, 3] # 前几行这是box的信息
y_true[b, k, j, i, 4] = 1 # bbox由5个信息组成,前四个是坐标信息,第5个是Pc,是否由目标
y_true[b, k, j, i, c + 5] = 1 # c + 5指定获得的第几个类,5是前5个维度(x,y,w,h,Pc),从下个维度开始是类
#----------------------------------------#
# 用于获得xywh的比例
# 大目标loss权重小,小目标loss权重大
#----------------------------------------#
box_loss_scale[b, k, j, i] = batch_target[t, 2] * batch_target[t, 3] / in_w / in_h
return y_true, noobj_mask, box_loss_scale
get_target函数需要传入5个参数,l【特征层索引】,targets【真实值】,anchors【已经缩放后的anchors】,in_h=in_w=19.
我们先创建一个全1的mask tensor【后面用来记录不含目标的地方】,此时的noobj_mask shape为【batch_size,3,19,19】
#-----------------------------------------------------#
# 计算一共有多少张图片
#-----------------------------------------------------#
bs = len(targets)
#-----------------------------------------------------#
# 用于选取哪些先验框不包含物体
#-----------------------------------------------------#
# 创建一个(batshzie,3,13,13)全1矩阵
noobj_mask = torch.ones(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False)
在创建一个全0的tensor box_loss_scale。shape 【batch_size,3,19,19】
#-----------------------------------------------------#
# 让网络更加去关注小目标
# 创建一个(batshzie,3,13,13)全0矩阵
#-----------------------------------------------------#
box_loss_scale = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False)
再创建一个全0的tensor,用来记录ground truth信息。shape【batch_size,3,19,19,5+num_classes】=【4,3,19,19,6】
这里再多嘴一句,上面这种shape的张量可以这样理解:有3种anchor,每种anchor都会负责一个19 * 19的cell网格,而每个cell内也都均有5+num_classes个参数需要预测。
# y_true用来存放ground truth 信息
#-----------------------------------------------------#
y_true = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, self.bbox_attrs, requires_grad = False)
下面这段代码的是遍历每个batch,然后判断一下target[b]中是否有目标,如果有目标就创建一个与target[b]shape一样的全0张量。由于我这里target长度是4【表示4个batch】,第一个target[0]的长度为5【表示这张图像有5个标注的目标】。 target[b]的shape为【该图有几个目标,5(这个5坐标信息和类)】
for b in range(bs):
if len(targets[b]) == 0:
continue
batch_target = torch.zeros_like(targets[b])
此刻的target[0]为:【center_x,center_y,w,h,class】。
tensor([
[0.2352, 0.1637, 0.2928, 0.3273, 0.0000],
[0.2097, 0.5896, 0.1135, 0.4490, 0.0000],
[0.8569, 0.5296, 0.1382, 0.2500, 0.0000],
[0.6867, 0.5395, 0.1201, 0.2763, 0.0000],
[0.6061, 0.1637, 0.2911, 0.3273, 0.0000]], device='cuda:0')
计算出正样本在特征层上的中心点。可以计算出我们的target对应到我们19 *19 的特征层上坐标是多少。
#-------------------------------------------------------#
# 计算出正样本在特征层上的中心点?
#-------------------------------------------------------#
batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w
batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h
batch_target[:, 4] = targets[b][:, 4]
batch_target = batch_target.cpu()
则现在我们得到batch_target就记录下了我们的第一个batch中ground truth坐标映射到特征层的坐标信息。
tensor([[ 4.4688, 3.1094, 5.5625, 6.2188, 0.0000],
[ 3.9844, 11.2031, 2.1562, 8.5312, 0.0000],
[16.2812, 10.0625, 2.6250, 4.7500, 0.0000],
[13.0469, 10.2500, 2.2813, 5.2500, 0.0000],
[11.5156, 3.1094, 5.5312, 6.2188, 0.0000]], device='cuda:0')
我们对上面在特征层上得到gt坐标形式稍加改变。batch_target shape为【5,5】。size(0)=5,先创建一个【5,2】的全0tensor,然后与batch_target的第2列(w)至第3列(h)进行拼接。
#-------------------------------------------------------#
# 将真实框转换一个形式
# num_true_box, 4
#-------------------------------------------------------#
gt_box = torch.FloatTensor(torch.cat((torch.zeros((batch_target.size(0), 2)), batch_target[:, 2:4]), 1))
此刻的gt_box:
tensor([
[0.0000, 0.0000, 5.5625, 6.2188],
[0.0000, 0.0000, 2.1562, 8.5312],
[0.0000, 0.0000, 2.6250, 4.7500],
[0.0000, 0.0000, 2.2813, 5.2500],
[0.0000, 0.0000, 5.5312, 6.2188]])
同时将先验框也转变一个形式:将一个【3,2】的全零tensor和anchors进行拼接。
#-------------------------------------------------------#
# 将先验框转换一个形式
# 9, 4
#-------------------------------------------------------#
anchor_shapes = torch.FloatTensor(torch.cat((torch.zeros((len(anchors), 2)), torch.FloatTensor(anchors)), 1))
此刻anchor_shape:【实际就是给anchor前面多加了两列0】
tensor([
[ 0.0000, 0.0000, 0.3750, 0.5000],
[ 0.0000, 0.0000, 0.5938, 1.1250],
[ 0.0000, 0.0000, 1.2500, 0.8750],
[ 0.0000, 0.0000, 1.1250, 2.3438],
[ 0.0000, 0.0000, 2.3750, 1.7188],
[ 0.0000, 0.0000, 2.2500, 4.5625],
[ 0.0000, 0.0000, 4.4375, 3.4375],
[ 0.0000, 0.0000, 6.0000, 7.5938],
[ 0.0000, 0.0000, 14.3438, 12.5312]])
通过定义的函数计算IOU,主要传入两个参数,gt_box和anchor_shapes【前者是target映射到特征层上的gt,后者是anchor映射到特征层上缩放后的anchor】
注:这里的iou并不是预测的box和gt的。 iou的计算我这里不再写,另一篇文章有。
best_ns返回的是每个最大iou索引序号【表示哪些anchor有目标的以上】
#-------------------------------------------------------#
# 计算交并比
# self.calculate_iou(gt_box, anchor_shapes) = [num_true_box, 9]每一个真实框和9个先验框的重合情况
# best_ns:
# [每个真实框最大的重合度max_iou, 每一个真实框最重合的先验框的序号]
#-------------------------------------------------------#
best_ns = torch.argmax(self.calculate_iou(gt_box, anchor_shapes), dim=-1)
此刻得到的best_ns为,即这几个anchor预测到了目标。可以这样理解,这张图里出现了5个目标,第一个目标由7号anchor预测到了,第二个目标到第四个由5号anchor预测到了,最后一个目标由7号anchor预测到了。
tensor([7, 5, 5, 5, 7])
最初前面我们定义了一个self.anchors_mask=[[6,7,8],[3,4,5],[0,1,2]]-->对应于19,38,56特征层的anchor。通过上面的索引也就知道了,出现的这5个目标分别落在了19 * 19特征层,38*38,38*38,38*38,38*38,19 * 19上。
for t, best_n in enumerate(best_ns):
if best_n not in self.anchors_mask[l]:
continue
#----------------------------------------#
# 判断这个先验框是当前特征点的哪一个先验框
# [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
#----------------------------------------#
k = self.anchors_mask[l].index(best_n)
通过for循环对上面的bast_ns进行遍历,先确定一下我们一个best_n到底在不在anchors_mask上。
因为此时l为0,对应的19 * 19的特征层,所以anchors_mask[0] = [6,7,8],则k=1.
通过上面的操作,我们知道了当前目标应该由几号anchor进行预测,但还不知道当前的gt属于19 * 19中的哪个cell,所以我们现在需要再确定一下gt落在了哪个cell。
方法也很简单,前面我们通过batch_target获得了gt在当前特征层上的box坐标,那么我们仅需要获得这些box的(center_x,center_y)即可,并对这些中心点坐标取整就可以知道目标落在哪个cell。
#----------------------------------------#
# 获得真实框属于哪个网格点
# floor 返回一个新张量,包含输入input张量每个元素的floor,即取不大于元素的最大整数。
#----------------------------------------#
i = torch.floor(batch_target[t, 0]).long()
j = torch.floor(batch_target[t, 1]).long()
得到i和j如下,说明我这个目标的中心点落在了19 * 19 的第4行第3列的cell处。
i=4;
j=3;
然后我们还可以获得落在该cell处的类是什么。
#----------------------------------------#
# 取出真实框的种类
#----------------------------------------#
c = batch_target[t, 4].long()
是否还记得我们前面定义了一个全1的tensor noobj_mask。此刻我们可以获得所有无目标的地方,或者说有目标的地方【1表示无目标,0表示有目标,当然你反过来也可以】。此时由于是第一次的遍历,b=0,k=1,j=3.i=4,表示第一个batch,中第k anchor,3,4有目标。
#----------------------------------------#
# noobj_mask代表无目标的特征点
#----------------------------------------#
noobj_mask[b, k, j, i] = 0
前面我们还定义了一个y_true的全0 tensor用来记录gt,shape是【batch_size,3,19,19,5+num_classes】。然后我们记录下当前目标box坐标信息。前4列是box信息,然后又看到y_true[...,4]=1表示有目标,后面c+5表示当前为获得第几个类。
#----------------------------------------#
# tx、ty代表中心调整参数的真实值
# y_true[b, k, j, i, 0] 意思是取第几各个batch的第k个锚框(每个层有三个),在特征层上第j行第i列网格点
#----------------------------------------#
y_true[b, k, j, i, 0] = batch_target[t, 0]
y_true[b, k, j, i, 1] = batch_target[t, 1]
y_true[b, k, j, i, 2] = batch_target[t, 2]
y_true[b, k, j, i, 3] = batch_target[t, 3] # 前几行这是box的信息
y_true[b, k, j, i, 4] = 1 # bbox由5个信息组成,前四个是坐标信息,第5个是Pc,是否由目标
y_true[b, k, j, i, c + 5] = 1 # c + 5指定获得的第几个类,5是前5个维度(x,y,w,h,Pc),从下个维度开始是类
此时y_true[0,1,3,4,:]:这就表示第一张图(batch 0),设置的第二个anchor(k=1),在19 *19 的特征层的3行4列处有目标设置为1,该类对应为第1个类。
tensor([4.4688, 3.1094, 5.5625, 6.2188, 1.0000, 1.0000])
batch_target存放着当前batch[当前图像]所有目标的box信息和类信息,那肯定每个目标的box有大有小,小目标的box面积就小,大目标的box面积就大,那么我们就可以得到每个目标的box面积,与当前特征层(19 * 19)面积做个比值,这个值是什么意思呢?我们就可以得到大小目标占当前特征层的比例值。
#----------------------------------------#
# 用于获得xywh的比例
# 大目标loss权重小,小目标loss权重大
#----------------------------------------#
box_loss_scale[b, k, j, i] = batch_target[t, 2] * batch_target[t, 3] / in_w / in_h
上面的get_target我们获得了gt在特征层的各种信息,它返回了y_true(记录当前特征层是由第几个anchor预测以及目标落在了哪个cell处);noobj_mask记录了哪些cell是没有目标的,box_loss_scale是各个目标对应于特征层比例值。
get_ignore这个函数是对预测结果进行解码,判断预测结果和真实值的重合度,如果重合度大则可以忽略,因为这部分说明预测的很准了。
该函数需要传入l(特征层索引),x,y,h,w(预测的box信息,即model输出的),targets(真实值),scaled_anchors(缩放后的anchors),in_h,in_w(特征层尺寸),noobj_mask(无目标的mask,shape[batch_size,3,19,19])。
#---------------------------------------------------------------#
# 将预测结果进行解码,判断预测结果和真实值的重合程度
# 如果重合程度过大则忽略,因为这些特征点属于预测比较准确的特征点
# 作为负样本不合适
#----------------------------------------------------------------#
noobj_mask, pred_boxes = self.get_ignore(l, x, y, h, w, targets, scaled_anchors, in_h, in_w, noobj_mask)
先把完整代码列出来:
def get_ignore(self, l, x, y, h, w, targets, scaled_anchors, in_h, in_w, noobj_mask):
#-----------------------------------------------------#
# 计算一共有多少张图片
#-----------------------------------------------------#
bs = len(targets)
FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
#-----------------------------------------------------#
# 生成网格,先验框中心,网格左上角
#-----------------------------------------------------#
grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_h, 1).repeat(
int(bs * len(self.anchors_mask[l])), 1, 1).view(x.shape).type(FloatTensor)
grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_w, 1).t().repeat(
int(bs * len(self.anchors_mask[l])), 1, 1).view(y.shape).type(FloatTensor)
# 生成先验框的宽高
scaled_anchors_l = np.array(scaled_anchors)[self.anchors_mask[l]]
anchor_w = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([0]))
anchor_h = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([1]))
anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape)
anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape)
#-------------------------------------------------------#
# 计算调整后的先验框中心与宽高
#-------------------------------------------------------#
pred_boxes_x = torch.unsqueeze(x + grid_x, -1)
pred_boxes_y = torch.unsqueeze(y + grid_y, -1)
pred_boxes_w = torch.unsqueeze(torch.exp(w) * anchor_w, -1)
pred_boxes_h = torch.unsqueeze(torch.exp(h) * anchor_h, -1)
pred_boxes = torch.cat([pred_boxes_x, pred_boxes_y, pred_boxes_w, pred_boxes_h], dim = -1)
for b in range(bs):
#-------------------------------------------------------#
# 将预测结果转换一个形式
# pred_boxes_for_ignore num_anchors, 4
#-------------------------------------------------------#
pred_boxes_for_ignore = pred_boxes[b].view(-1, 4)
#-------------------------------------------------------#
# 计算真实框,并把真实框转换成相对于特征层的大小
# gt_box num_true_box, 4
#-------------------------------------------------------#
if len(targets[b]) > 0:
batch_target = torch.zeros_like(targets[b])
#-------------------------------------------------------#
# 计算出正样本在特征层上的中心点
#-------------------------------------------------------#
batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w
batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h
batch_target = batch_target[:, :4]
#-------------------------------------------------------#
# 计算交并比
# anch_ious num_true_box, num_anchors
#-------------------------------------------------------#
anch_ious = self.calculate_iou(batch_target, pred_boxes_for_ignore)
#-------------------------------------------------------#
# 每个先验框对应真实框的最大重合度
# anch_ious_max num_anchors
#-------------------------------------------------------#
anch_ious_max, _ = torch.max(anch_ious, dim = 0)
anch_ious_max = anch_ious_max.view(pred_boxes[b].size()[:3])
noobj_mask[b][anch_ious_max > self.ignore_threshold] = 0
return noobj_mask, pred_boxes
通过in_w,in_h我们可以划分cell,grid_x和grid_y的shape均为【batch_size,3,19,19】.
可以这样理解一下,每个anchor都对应19 * 19个网格。
#-----------------------------------------------------#
# 生成网格,先验框中心,网格左上角
#-----------------------------------------------------#
grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_h, 1).repeat(
int(bs * len(self.anchors_mask[l])), 1, 1).view(x.shape).type(FloatTensor)
grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_w, 1).t().repeat(
int(bs * len(self.anchors_mask[l])), 1, 1).view(y.shape).type(FloatTensor)
grid_x:【这里我只取第一个batch和第一个anchor为例】
tensor([
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.],
[ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13.,14., 15., 16., 17., 18.]], device='cuda:0')
grid_y:(大家猛的一看是不是看这种形式有点怪,实际这个grid_y是需要和上面grid_x进行对应的)
tensor([
[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0.],
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,1., 1., 1., 1., 1.],
[ 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,2., 2., 2., 2., 2.],
[ 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.,3., 3., 3., 3., 3.],
[ 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4.,4., 4., 4., 4., 4.],
[ 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5.],
[ 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6.,6., 6., 6., 6., 6.],
[ 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7.,7., 7., 7., 7., 7.],
[ 8., 8., 8., 8., 8., 8., 8., 8., 8., 8., 8., 8., 8., 8.,8., 8., 8., 8., 8.],
[ 9., 9., 9., 9., 9., 9., 9., 9., 9., 9., 9., 9., 9., 9.,9., 9., 9., 9., 9.],
[10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10.,10., 10., 10., 10., 10.],
[11., 11., 11., 11., 11., 11., 11., 11., 11., 11., 11., 11., 11., 11.,11., 11., 11., 11., 11.],
[12., 12., 12., 12., 12., 12., 12., 12., 12., 12., 12., 12., 12., 12.,12., 12., 12., 12., 12.],
[13., 13., 13., 13., 13., 13., 13., 13., 13., 13., 13., 13., 13., 13.,13., 13., 13., 13., 13.],
[14., 14., 14., 14., 14., 14., 14., 14., 14., 14., 14., 14., 14., 14.,14., 14., 14., 14., 14.],
[15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15.,15., 15., 15., 15., 15.],
[16., 16., 16., 16., 16., 16., 16., 16., 16., 16., 16., 16., 16., 16.,16., 16., 16., 16., 16.],
[17., 17., 17., 17., 17., 17., 17., 17., 17., 17., 17., 17., 17., 17.,17., 17., 17., 17., 17.],
[18., 18., 18., 18., 18., 18., 18., 18., 18., 18., 18., 18., 18., 18.,18., 18., 18., 18., 18.]], device='cuda:0')
获得先验框的w和h:
# 生成先验框的宽高
scaled_anchors_l = np.array(scaled_anchors)[self.anchors_mask[l]]
anchor_w = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([0]))
anchor_h = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([1]))
anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape)
anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape)
x,y,w,h是model输出的box信息【预测值】,通过x+grid_x可以获得预测box位于19 * 19网格的哪个坐标处。得到的pred_boxes就是我们得到的在网格图上的预测box。shape为【batch_size,3,19,19,4】
#-------------------------------------------------------#
# 计算调整后的先验框中心与宽高
#-------------------------------------------------------#
pred_boxes_x = torch.unsqueeze(x + grid_x, -1)
pred_boxes_y = torch.unsqueeze(y + grid_y, -1)
pred_boxes_w = torch.unsqueeze(torch.exp(w) * anchor_w, -1)
pred_boxes_h = torch.unsqueeze(torch.exp(h) * anchor_h, -1)
pred_boxes = torch.cat([pred_boxes_x, pred_boxes_y, pred_boxes_w, pred_boxes_h], dim = -1)
对上面的pred_boxes转换一些shape的形式,pred_boxes[0]的shape是【3,19,19,4】,对其进行平铺,变成【3*19*19,4】=【1083,4】,也就是我们得到的pred_boxes_for_ignore。
for b in range(bs):
#-------------------------------------------------------#
# 将预测结果转换一个形式
# pred_boxes_for_ignore num_anchors, 4
#-------------------------------------------------------#
pred_boxes_for_ignore = pred_boxes[b].view(-1, 4)
这里再创建一个batch_target的全0tensor,功能和get_target函数中的batch_target一样,记录每个batch中所有目标真实值信息。
#-------------------------------------------------------#
# 计算真实框,并把真实框转换成相对于特征层的大小
# gt_box num_true_box, 4
#-------------------------------------------------------#
if len(targets[b]) > 0:
batch_target = torch.zeros_like(targets[b])
#-------------------------------------------------------#
# 计算出正样本在特征层上的中心点
#-------------------------------------------------------#
batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w
batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h
batch_target = batch_target[:, :4]
targets的第一个batch中,出现了5个目标。 这里再说一下target表示的内容(主要怕大家看到这里又忘记了),分别表示center_x, center_y, w, h, class。
tensor([[0.2352, 0.1637, 0.2928, 0.3273, 0.0000],
[0.2097, 0.5896, 0.1135, 0.4490, 0.0000],
[0.8569, 0.5296, 0.1382, 0.2500, 0.0000],
[0.6867, 0.5395, 0.1201, 0.2763, 0.0000],
[0.6061, 0.1637, 0.2911, 0.3273, 0.0000]], device='cuda:0'),
由于上面的target信息是归一化到0~1间,我们需要映射到特征层上,进过上述操作得到batch_target:
tensor([
[ 4.4688, 3.1094, 5.5625, 6.2188],
[ 3.9844, 11.2031, 2.1562, 8.5312],
[16.2812, 10.0625, 2.6250, 4.7500],
[13.0469, 10.2500, 2.2813, 5.2500],
[11.5156, 3.1094, 5.5312, 6.2188]], device='cuda:0')
anch_ious的shape为【5,1083】,5指的就是当前batch中出现目标的数量,1083=3*19*19(当前特征层有多少anchors)。也就是说我们现在获得了所有真实值和预测值的box iou【和前面的get_target注意区分,get_target iou是先验框和真实框的iou,现在算的iou是真实值的预测值的】。
#-------------------------------------------------------#
# 计算交并比
# anch_ious num_true_box, num_anchors
#-------------------------------------------------------#
anch_ious = self.calculate_iou(batch_target, pred_boxes_for_ignore)
计算得到每个目标的真实框和预测框最大的iou了,并reshape成(3,19,19),就相当于知道每个cell内真实框和预测框的iou。
进而可以在nooj_mask进行筛选,将iou大于阈值(0.5)的置0.[表示这些地方有目标,就是正样本]
#-------------------------------------------------------#
# 每个先验框对应真实框的最大重合度
# anch_ious_max num_anchors
#-------------------------------------------------------#
anch_ious_max, _ = torch.max(anch_ious, dim = 0)
anch_ious_max = anch_ious_max.view(pred_boxes[b].size()[:3])
noobj_mask[b][anch_ious_max > self.ignore_threshold] = 0
y_true的shape是【batch_size,3,19,19,5+classes】。5+classes指的是x,y.w.h,有无目标,当前为什么类。因此将y_true[...,4]置为1表示有目标,利用sum可以获得有多少正样本。
loss = 0
obj_mask = y_true[..., 4] == 1
n = torch.sum(obj_mask) # 有多少正样本
接下来就是计算loss。
先计算ciou【具体计算过程和可视化在我另一篇文章有写】。【这里计算的是预测框和真实框的ciou】
1-ciou就是边界回归的loss_loc.
然后利用二分类交叉熵计算分类损失loss_cls。利用obj_mask在预测结果中进行筛选,pred_cls的shape为【batch_size,3,19,19,num_class[conf]】,obj_mask的shape【4,3,19,19】,表示在所有cell筛选出有目标的cell。y_true也是利用obj_mask进行筛选y_true[...,5:]表示对应的类。
if n != 0:
#---------------------------------------------------------------#
# 计算预测结果和真实结果的ciou
# ciou.shape = [batch_size,3,feature_w,feature_h]
#----------------------------------------------------------------#
ciou = self.box_ciou(pred_boxes, y_true[..., :4])
# loss_loc = torch.mean((1 - ciou)[obj_mask] * box_loss_scale[obj_mask])
loss_loc = torch.mean((1 - ciou)[obj_mask]) # 边界回归
loss_cls = torch.mean(self.BCELoss(pred_cls[obj_mask], y_true[..., 5:][obj_mask])) # 分类回归(只是判断有没有目标)
loss += loss_loc * self.box_ratio + loss_cls * self.cls_ratio
if self.focal_loss:
ratio = torch.where(obj_mask, torch.ones_like(conf) * self.alpha, torch.ones_like(conf) * (1 - self.alpha)) * torch.where(obj_mask, torch.ones_like(conf) - conf, conf) ** self.gamma
loss_conf = torch.mean((self.BCELoss(conf, obj_mask.type_as(conf)) * ratio)[noobj_mask.bool() | obj_mask])
else:
loss_conf = torch.mean(self.BCELoss(conf, obj_mask.type_as(conf))[noobj_mask.bool() | obj_mask]) # 置信度回归
loss += loss_conf * self.balance[l] * self.obj_ratio
# if n != 0:
# print(loss_loc * self.box_ratio, loss_cls * self.cls_ratio, loss_conf * self.balance[l] * self.obj_ratio)
return loss
所以此时loc_loss=0.5211,loss_cls=0.8044.
置信度损失:
loss_conf = torch.mean(self.BCELoss(conf, obj_mask.type_as(conf))[noobj_mask.bool() | obj_mask]) # 置信度回归
三者损失相加就是最后的loss损失。
上面就是针对loss部分的代码进行解析,可以更好的理解实现过程。有助于大家的理解。