yolov4 LOSS代码详解【附代码】

本文章是从代码层面可以更好的了解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形式

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:])

get_target(获得y_true)

获得网络预测结果

        #-----------------------------------------------#
        #   获得网络应该有的预测结果
        #-----------------------------------------------#
        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[真实值映射到特征层]

下面这段代码的是遍历每个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计算并获得anchor索引:

通过定义的函数计算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_ignore:判断预测结果和真实结果的重合度

上面的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计算

接下来就是计算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部分的代码进行解析,可以更好的理解实现过程。有助于大家的理解。

你可能感兴趣的:(深度学习,人工智能,python)