本文章是从代码层面可以更好的了解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
这里先将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形式
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')
]
forward部分
然后我们直接看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:])