物体检测 RetinaNet

老师说到torchcv物体检测代码中的数据增强非常丰富,其中涵盖了one stage detector的3个模型:SSD FPNSSD RetinaNet。之前和一位大神交流时,她提到,现在在经典的物体检测模型中性能最好的是:Faster R-CNN(缺点是对于小物体的检测效果不好),Retinanet和YOLO V3。现在希望来详细讲解torchcv中的RetinaNet模型

faster R-CNN对于面积较小的物体检测效果不好,这是因为最原始版本的faster R-CNN模型在RPN阶段,只在output stride=16的特征图上进行region proposal的选择和提取,特征图的分辨率较小,像素感受野大,故而适合于对面积较大的物体的检测,而对于分辨率较大的特征图则由于感受野尺寸较小从而能够提取到面积较小的物体的更多特征(参见FPN feature pyramid network特征金字塔网络),故而如果将特征金字塔网络得到的不同尺度的特征图上都进行对于region proposal的设定,则有希望通过在分辨率较大的特征图上的anchor学习以提高对于面积较小的物体的检测。

RetinaNet的主要特点在于其融合了SSD,FPN,Focal loss的结构,故而作为one-stage method,能够达到很高的性能。

下面重点讲解 https://github.com/kuangliu/torchcv 大神的RetinaNet代码。

对于focal loss的代码修改部分感谢https://github.com/yhenon/pytorch-retinanet

一、ListDataset 解析并读取训练图像和ground truth 标签

1.ListDataset(data.Dataset)加载数据

def __init__方法

对于训练数据集中的所有图片的所有ground truth包围框和对应的标签,都变成长tensor    box,labels   
但是根据self.boxes和self.labels这两个列表,依然能够根据索引值idx找到与当前图片所对应的所有ground truth 包围框及其对应的标签
self.boxes是长度为self.num_imgs的列表,即长度为当前训练数据集中的所有训练图片总数,列表中的每个元素是一个列表,
子列表box的长度是当前的训练图片中的ground truth包围框的总数,子列表中的每个元素是一个长度为4的float类型list,表示当前ground truth boxes的4个坐标
self.labels是长度为self.num_imgs的列表,列表中的每个元素是一个列表,子列表box的长度是当前的训练图片中的ground truth包围框的总数的longtensor
子列表中的每个元素是一个长度为1的整数,表示当前的GT框类别标号(类别从0开始标注)

如果训练数据集有两个子文件夹,那么还是保存在一个列表中,即boxes,labels

def __getitem__方法      return img, boxes, labels

2.数据增强

RetinaNet的数据增强包括:

(1)random flip horizontally  对于输入图像进行水平方向的翻转,以0.5的概率进行随机翻转,对图像进行翻转后,对相应的ground truth boxes和ground truth labels标签也应进行翻转

大多数情况下是指进行水平翻转而不进行竖直翻转,这是因为,数据集中所拍摄的图像大多都是经过水平校准的。

(2)resize 将图像的最长边resize到640,并保证输入图像的aspect ratios不变

(3)padding 将输入图像通过padding=0的方式,填充到640*640大小

二、RetinaNet ground truth boxes encode(the same with SSD,Faster R-CNN)

经过_getitem__方法返回的img, boxes, labels,

variable shape vaue
img [batch_size,3,640,640] RGB顺序,经过transform.ToTensor操作,输出0-1之间的数值
boxes [#objs,4] 在对于当前输入图像进行数据增强之后的图像中,#objs表示ground truth boxes的绝对坐标信息,data type为float类型(因为经历了resize操作),[xmin,ymin,xmax,ymax]结构
labels [#objs,] 经过数据增强变换之后的当前图像中共有多少个object以及对应的类别标签编号,这里的编号从1开始,并不涉及背景

 

编码方法在torchcv-master\torchcv\models\retinanet\box_coder.py中

def encode(self, boxes, labels):
    '''Encode target bounding boxes and class labels.
    传入这个函数中的形参表示训练数据集中的每一张训练图像所对应的ground truth boxes信息

    We obey the Faster RCNN box coder:
      tx = (x - anchor_x) / anchor_w
      ty = (y - anchor_y) / anchor_h
      tw = log(w / anchor_w)
      th = log(h / anchor_h)

    Args:
      boxes: (tensor) bounding boxes of (xmin,ymin,xmax,ymax), sized [#obj, 4].
            行数为当前训练图像中所包含的ground truth boxes个数,
            此时的gt boxes坐标数值已经转换成了在经过数据增强后的图像上的绝对坐标值
            (因为在数据增强的每个步骤中,输入图像发生变化时,对应的gt信息也随之变化)
      labels: (tensor) object class labels, sized [#obj,]. 列数为4
            表示ground truth boxes对应的类别标签信息

    Returns:
      loc_targets: (tensor) encoded bounding boxes, sized [#anchors,4].
      这里和Faster R-CNN模型中的anchor target layer一样,先对所有的anchor boxes进行位置编码
      即找到网络模型需要预测的 所有ground truth boxes所对应的位置偏移量ground truth 和类别标签
      由于输入图像的分辨率相同,故而所有的anchor boxes(没有经过网络预测的,仅仅根据特征图分辨率
      得到的,这些anchor boxes的生成和位置坐标与图像内容无关)对于所有的输入图像都相同,然后再根据所提供的
      对于当前图像的ground truth 信息,看当前的anchor boxes与哪一个gt boxes具有最大的overlap值,
      就将anchor boxes编码成哪个gt_boxes,这是因为我们需要的输出是对于不同的输入图像
      可能需要预测出不同个数的bounding boxes,但是CNN模型只能预测结构化的输出,所以就根据输入图像的分辨率
      设置一些regular boxes(或称之为anchor boxes),这些框的数量和位置坐标对于任意的输入图像(前提当然是
      空间分辨率相同,内容不同)都相同,然后根据每张图像的ground truth boxes信息对于每个anchor boxes进行编码
      让CNN网络模型预测这些编码后的位置偏移量,从而能够保证使得对于不同内容的输入图像,CNN能够产生固定shape
      的结构化输出
      但是要注意的是,在训练时计算损失函数时,并不需要对所有的anchor boxes计算损失函数,而是会根据anchor boxes
      (这里所指的经过网络模型预测之后的)与ground truth boxes之间的IOU,对于anchor boxes区分正负样本,然后按照一定的
      比例进行采样(对于SSD loss,正负样本比例为1:3),再计算分类损失和回归损失
      这是因为在所有的anchor boxes中,绝大多数都是负框,如果将所有的anchor boxes都拿来计算损失,则达不到期望的训练效果
      因为网络模型实际上区分出某些anchor boxes是负样本(background boxes)是很容易的,必须要挖掘出困难的负样本,
      才能有效地提高网络模型的训练准确度(on-line hard example mining)
      cls_targets: (tensor) encoded class labels, sized [#anchors,].
    '''
    anchor_boxes = self.anchor_boxes
    ious = box_iou(anchor_boxes, boxes)#shape   [#anchors,#objs]
    max_ious, max_ids = ious.max(1)
    '''
    对于所有的anchor boxes,将每个anchor boxes的ground truth boxes以及对应的ground truth labels
    设置成与之具有最大IOU数值的gt boxes
    也就是说,在训练之前对于anchor boxes进行预处理编码时,会对于每个anchor boxes分配gt boxes和gt labels
    所对应的就是看anchor boxes和图像中哪一个gt boxes之间的IOU最大     
    max_ids  range [0,#objs]    shape  [#anchors]
    '''
    boxes = boxes[max_ids]#shape [#anchors]挑选出与每个anchor boxes之间具有最大IOU的gt boxes作为anchor boxes的ground truth

    boxes = change_box_order(boxes, 'xyxy2xywh')
    anchor_boxes = change_box_order(anchor_boxes, 'xyxy2xywh')

    loc_xy = (boxes[:,:2]-anchor_boxes[:,:2]) / anchor_boxes[:,2:]
    loc_wh = torch.log(boxes[:,2:]/anchor_boxes[:,2:])
    loc_targets = torch.cat([loc_xy,loc_wh], 1)
    cls_targets = 1 + labels[max_ids]
    '''
   根据当前的anchor boxes和哪一个gt boxes之间的IOU值最大,对于anchor boxes的classification target和
   regression target进行encode
   得到RetinaNet的 loc_targets, cls_targets
   loc_targets    shape   [#anchors,4]
   cls_targets    shape   [#anchors]      
   '''

    # cls_targets[max_ious<0.5] = 0
    # ignore = (max_ious>0.4) & (max_ious<0.5)  # ignore ious between [0.4,0.5]
    # cls_targets[ignore] = -1                  # mark ignored to -1
    return loc_targets, cls_targets

三、RetinaNet model architecture(the same as FPNSSD)

FPN网络结构:

class FPN(nn.Module):
    def __init__(self, block, num_blocks):
        super(FPN, self).__init__()
        self.in_planes = 64

        '''
        FPN50=FPN(Bottleneck, [3,4,6,3])
        
        
       '''


        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        '''第一个卷积层:输入通道数3,输出通道数64'''

        # Bottom-up layers
        self.layer1 = self._make_layer(block,  64, num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
        self.conv6 = nn.Conv2d(2048, 256, kernel_size=3, stride=2, padding=1)
        self.conv7 = nn.Conv2d( 256, 256, kernel_size=3, stride=2, padding=1)
        self.conv8 = nn.Conv2d( 256, 256, kernel_size=3, stride=2, padding=1)
        self.conv9 = nn.Conv2d( 256, 256, kernel_size=3, stride=2, padding=1)

        # Top-down layers
        self.toplayer = nn.Conv2d(2048, 256, kernel_size=1, stride=1, padding=0)

        # Lateral layers
        self.latlayer1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)
        self.latlayer2 = nn.Conv2d( 512, 256, kernel_size=1, stride=1, padding=0)

        # Smooth layers
        self.smooth1 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
        self.smooth2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)



        #self.layer3 = self._make_layer(block,  256, 6, stride=2)
    def _make_layer(self, block, planes, num_blocks, stride):
        '''

        :param block: 所使用的卷积块   nn.Modules
        :param planes: 256   输入到当前卷积块的特征图通道数
        :param num_blocks:   对于block中块重复多少次
        :param stride:   block中的卷积步长
        :return:
        '''
        strides = [stride] + [1]*(num_blocks-1)
        '''
        构造一个长度为num_blocks的list类型,list中第0个元素是第一个block中的步长
        之后的步长都是1
           
        '''

        '''
        +    *    运算符对于列表list对象的特殊操作
        +表示将两个子列表合并成一个长列表,如果每个子列表中只有一个元素,则类似于list.append(element)操作
        *表示将当前列表中的元素乘以倍数(即将列表中的所有元素按照当前次序重复倍数次)
        [1,4,6,5]*2=[1, 4, 6, 5, 1, 4, 6, 5]        
        
        这一操作后,strides 为长度为num_blocks的列表,当前的_make_layer所产生的层数中有num_block个substructure
        每个substructure中包含3部分的卷积,其中num_blocks个stride参数表示当前的_make_layers中的num_blocks中每个
        substructure的第二个卷积(3*3  conv)操作的步长
        
        其中strides列表中元素构成如下:
        第0个元素表示当前_make_layer中的第0个substructure中的3*3卷积核的stride       stride
        后面的第1个元素到第num_blocks-1个元素都是1,表示第1个substructure到最后一个substructure的步长都是1
        
        
        strides  [2,1,1,1,1,1]
        '''
        layers = []
        for stride in strides:#512,
            layers.append(block(self.in_planes, planes, stride))
            self.in_planes = planes * block.expansion
            #block中的expansion表示经过block后,对于输入图特征图的通道数增大了多少倍
        return nn.Sequential(*layers)

    def _upsample_add(self, x, y):
        '''Upsample and add two feature maps.

        函数功能:对特征图x进行2倍上采样,再与特征图y进行element-wise addition
        特征图x与特征图y的通道数相同,只是由于x是更为深度的特征图,其分辨率是y的1/2,所以要先对于x进行2倍上采样,使特征图
        x和y的分辨率相同
        Args:
          x: (Variable) top feature map to be upsampled.
          y: (Variable) lateral feature map.

        Returns:
          (Variable) added feature map.

        Note in PyTorch, when input size is odd, the upsampled feature map
        with `F.upsample(..., scale_factor=2, mode='nearest')`
        maybe not equal to the lateral feature map size.

        e.g.
        original input size: [N,_,15,15] ->
        conv2d feature map size: [N,_,8,8] ->
        upsampled feature map size: [N,_,16,16]

        So we choose bilinear upsample which supports arbitrary output sizes.双线性上采样
        '''
        _,_,H,W = y.size()
        return F.upsample(x, size=(H,W), mode='bilinear', align_corners=False) + y

    def forward(self, x):
        # Bottom-up
        c1 = F.relu(self.bn1(self.conv1(x)))
        c1 = F.max_pool2d(c1, kernel_size=3, stride=2, padding=1)#1/4  channel=64
        c2 = self.layer1(c1)#1/4 ,channel=256
        c3 = self.layer2(c2)#1/8,channel=512
        c4 = self.layer3(c3)#1/16,channel=1024
        c5 = self.layer4(c4)#1/32,channel=2048
        p6 = self.conv6(c5) #1/64,channel=256
        p7 = self.conv7(F.relu(p6))#1/128,channel=256
        p8 = self.conv8(F.relu(p7))#1/256,channel=256
        p9 = self.conv9(F.relu(p8))#1/512,channel=256
        # Top-down
        p5 = self.toplayer(c5)#1/32,channel=256
        p4 = self._upsample_add(p5, self.latlayer1(c4))#self.latlayer1(c4):1/16 channel=256    p4:1/16,channel=256
        p3 = self._upsample_add(p4, self.latlayer2(c3))#self.latlayer2(c3):1/8 channel=256      p3:1/8 channel=256
        p4 = self.smooth1(p4)#p4:1/16,channel=256
        p3 = self.smooth2(p3)#p3:1/8 channel=256

        # print('p3',p3.shape)

        return p3, p4, p5, p6, p7, p8, p9

        #输出7个不同尺度的特征图
        '''
        p3:1/8 channel=256     output stride=8
        p4:1/16 channel=256    output stride=16
        p5:1/32,channel=256    output stride=32
        p6:1/64,channel=256    output stride=64
        p7:1/128,channel=256   output stride=128
        p8:1/256,channel=256   output stride=256
        p9:1/512,channel=256   output stride=512
        特征图的分辨率一直减小,但是特征图通道数不变,都是256
        '''

RetinaNet网络结构:

class RetinaNet(nn.Module):
    num_anchors = 9

    def __init__(self, num_classes):
        super(RetinaNet, self).__init__()
        self.fpn = FPN50()
        self.num_classes = num_classes
        self.loc_head = self._make_head(self.num_anchors*4)
        self.cls_head = self._make_head(self.num_anchors*self.num_classes)

    def forward(self, x):
        cls_preds = []
        loc_preds = []
        fms = self.fpn(x)
        '''
        fms  list列表结构
        通过使用resnet101和FPN产生7个不同尺度的特征图
        p3:1/8 channel=256     output stride=8
        p4:1/16 channel=256    output stride=16
        p5:1/32,channel=256    output stride=32
        p6:1/64,channel=256    output stride=64
        p7:1/128,channel=256   output stride=128
        p8:1/256,channel=256   output stride=256
        p9:1/512,channel=256   output stride=512
        特征图的分辨率一直减小,但是特征图通道数不变,都是256 
        '''
        for fm in fms:
            loc_pred = self.loc_head(fm)
            cls_pred = self.cls_head(fm)
            loc_pred = loc_pred.permute(0,2,3,1).reshape(x.size(0),-1,4)                 # [N, 9*4,H,W] -> [N,H,W, 9*4] -> [N,H*W*9, 4]
            cls_pred = cls_pred.permute(0,2,3,1).reshape(x.size(0),-1,self.num_classes)  # [N,9*NC,H,W] -> [N,H,W,9*NC] -> [N,H*W*9,NC]
            loc_preds.append(loc_pred)
            cls_preds.append(cls_pred)
        '''
        对于Resnet FPN输出的每个尺度的特征图,分别使用相同结构(模型相同,模型参数数量相同,参数数值不同)
        的classifier和regression对特征图上的每个像素点的num_anchor个 anchor boxes进行类别预测和位置偏移量预测
        classification  经过3*3卷积操作之后,输出特征图空间分辨率不变,channel_num=num_anchor*num_classes
        regression      经过3*3卷积操作之后,输出特征图空间分辨率不变,channel_num=num_anchor*4
        这里与Faster R-CNN中的RPN使用的卷积方式不同,RPN中使用的是kernel_size=1的卷积核作为regression和classification
        loc_pred   shape [batch_size,36,H,W]->[batch_size,H,W,36]->[batch_size,(H*W*9),4]
        cls_pred   shape [batch_size,9*num_classes,H,W]->[batch_size,H,W,9*num_classes]->[batch_size,(H*W*9),num_classes]
        
        经过在dimension=1的维度上对于多个不同尺度的特征图进行concatenate后,此时loc_preds.shape[1]=cls_preds.shape[1]
        所表示的含义是:当前RetinaNet模型中总共包含多少个anchor boxes
        '''
        return torch.cat(loc_preds, 1), torch.cat(cls_preds, 1)
        '''
        返回值    loc_preds   torch.tensor  shape [batch_size,num_total_anchors,4]
                 cls_preds   torch.tensor  shape [batch_size,num_total_anchors,num_classes]  
       '''

可以看出,实际上,RetinaNet网络结构使用的实际上就是SSD+FPN,然后将输入图像变换成为640*640,故而RetinaNet的主要贡献在于提出了Focal Loss,而并不是像SSD中直接对正负样本进行1:3的采样,然后加上on-line hard example mining,它的重要改进之处是改变了损失函数的数学表达式。

要注意的是,对于每个不同尺度的特征图上的anchor 进行classification prediction和regress prediction,都只是使用了3*3的卷积操作,并没有使用Relu的激活函数操作,这是因为,对于regression offset prediction task而言,可能需要预测的offset数值比较大,超过了Relu函数的值域,而对于classification任务而言,只需要给出预测数值即可,调用计算cross entropy函数时会自动包含对于prediction map的softmax操作。

四、Focal Loss(RetinaNet的创新点)

def train(epoch):
    print('\nEpoch: %d' % epoch)
    net.train()
    train_loss = 0
    for batch_idx, (inputs, loc_targets, cls_targets) in enumerate(trainloader):
        inputs = inputs.to(device)
        loc_targets = loc_targets.to(device)
        cls_targets = cls_targets.to(device)

        optimizer.zero_grad()
        loc_preds, cls_preds = net(inputs)
        loss = criterion(loc_preds, loc_targets, cls_preds, cls_targets)
        loss.backward()
        optimizer.step()

        train_loss += loss.item()
        print('train_loss: %.3f | avg_loss: %.3f [%d/%d]'
              % (loss.item(), train_loss/(batch_idx+1), batch_idx+1, len(trainloader)))

在训练函数中这样的写法:即由网络模型输出结构化的预测值(这里的结构化,指的是具有固定且相同shape的输出,而并不会对于含有不同数量object的图像输出不同shape的prediction tensor,为了保证使得网络模型对于不同content的image的输出,能够产生结构化的输出,所引入的technique就是anchor,使用CNN网络来预测每个anchor的类别和regression offset,而对于相同分辨率的输入图像,特征图分辨率也相同,由于事先设定了anchor的aspect ratios和scales,则事先产生的anchor也相同,注意:anchor的生成函数实际上只需要输入:特征图分辨率(可以转换成输入原始的输入图像分辨率和每个尺度特征图的output stride)),anchor的宽高比和尺度值设定,就可以了。并不需要任何与CNN网络或者参数相关的信息。

训练函数中,通常是这样的写法:由网络模型的forward方法产生结构化的prediction tensor,然后将prediction和target送入loss函数中计算损失。

loss = criterion(loc_preds, loc_targets, cls_preds, cls_targets)

  shape value
loc_preds [batch_size,num_total_anchors,4] batch_size中的每张图像,在每个不同尺度特征图上的每个像素点的9个anchor boxes,网络模型输出:预测出来的相对于anchor boxes的坐标偏移量
loc_targets [batch_size,num_total_anchors,4] 经过与SSD和Faster R-CNN中编码方式相同的方式,根据图像中的gt_boxes坐标信息对于每个anchor boxes进行编码后的希望CNN网络模型预测出来的gt值,这里是对所有的anchor boxes都进行位置编码,它所对应gt_boxes就是看图像中的所有gt_boxes中哪个gt_boxes与anchor具有最大的IOU值,然后进行位置编码
cls_preds [batch_size,num_total_anchors,num_classes] 网络模型所预测出来的对于所有的anchor boxes的类别,这里所预测的类别中也不包含背景类别
cls_targets [batch_size,num_total_anchors,] batch_size中的每张图像,对于每个anchor boxes的类别gt信息,这里标注方式是:对于每个anchor boxes,看它与中的所有gt_boxes中哪个gt_boxes与anchor具有最大的IOU值,就将anchor的类别设置成什么,这里是假设所有的anchor boxes所对应的ground truth classes类别都是前景类别,并不对anchor区分正框和负框,在计算loss时再做区分,与Faster R-CNN中的RPN操作相同

这里的操作方式很简洁,就是先假设认为所有的anchor boxes都是正样本,其所对应的ground truth regression和ground truth classes信息就来源于(从当前图像中所有的ground truth boxes中挑选出的)与anchor boxes具有最大IOU值的gt boxes,然后网络模型进行类别预测时并没有引入背景的预测,比如Pascal voc20个类别(COCO数据集80个类别),就直接使用3*3卷积操作输出通道数为20/80的prediction map。

好吧,觉得有些坑,发现这份代码里面对于正负样本的区分以及困难样本挖掘代码存在错误。打算换成另外的代码继续阅读。

分类损失:多个类别的cross entropy+focal loss

回归损失:对于所有是正样本的anchor boxes计算:网络模型预测出的anchor boxes偏移量offset与anchor boxes的ground truth 偏移量(根据anchor boxes与图像中的ground truth boxes之间的IOU对gt boxes进行编码)之间的smooth L1 loss

class FocalLoss(nn.Module):
    def __init__(self, num_classes):
        super(FocalLoss, self).__init__()
        self.num_classes = num_classes

    def _focal_loss(self, x, y):
        '''Focal loss.

        This is described in the original paper.
        With BCELoss, the background should not be counted in num_classes.

        x  包含batch size中每张图像所对应的所有anchor boxes,包含正样本和负样本,网络模型所预测出来的类别概率
          (此时x仅仅是经过3*3卷积操作输出,并没有经过任何激活函数的操作)
        y  包含batch size中对应anchor boxes的类别标签
        Args:
          x: (tensor) predictions, sized [N,D].
          y: (tensor) targets, sized [N,].

        Return:
          (tensor) focal loss.
        '''
        alpha = 0.25
        gamma = 2

        t = one_hot_embedding(y-1, self.num_classes)
        '''
        t torch.tensor   shape [#anchor_all_image,num_classses]
        对于batch size中的每张图像中的每个anchor boxes中的类别标号
        进行one hot编码
        因此每个anchor boxes对应的类别标签变成一个长度为num_classes的vector
        '''
        p = x.sigmoid()
        pt = torch.where(t>0, p, 1-p)    # pt = p if t > 0 else 1-p
        '''
        pt torch.tensor   shape [#anchor_all_image,num_classses]
        t所对应的是每个anchor boxes的gt classes类别编码
        在anchor boxes为某类别处,得到当前网络模型对于anchor boxes
        判断为那个ground truth 类别的概率值,其他的地方就转换成1-p_gt
        p_gt 表示anchor boxes被预测成为正确类别的概率值      
        '''
        w = (1-pt).pow(gamma)#shape [#anchor_all_image,num_classses]
        w = torch.where(t>0, alpha*w, (1-alpha)*w)
        loss = F.binary_cross_entropy_with_logits(x, t, w, size_average=False)
        '''
        也不太明白为什么这里就变成2个类别的cross entropy了
        明明ground truth boxes可以是多个类别的
        '''
        return loss

上述代码存在问题,修改后的focal loss.py如下:

'''
    这里对于class_prediction也就是网络模型对于各个类别的概率预测值,只包含对于当前的anchor boxes是前景类别
   (在EAD数据集中是7个前景类别)中每一个类别的分数,并不包含对于背景的分数,这种操作无论是在二分类还是多分类问题中
    都很常见,如果不加入背景类别则使用sigmoid作为激活函数(表示anchor boxes属于某个前景类别的分数),
    如果加入了背景类别则使用softmax作为激活函数,
    class_taregt也就是分类的ground truth经历过很多次的变换,首先在数据加载器dataloader中,由于训练数据集中每个图像样本的
    txt标签文件给出的bounding boxes的类别从0-6(共7个前景类别),
    在retinanet/box_coder.py中的encode编码函数中,对标签进行了如下的变换:
    首先将所有标签值加1    则前景类别  1  2 3 4 5 6 7
    将IOU_max(指的是对于当前anchor boxes,计算出了它与每个ground truth boxes之间的IOU,取出最大的IOU值作为当前anchor 
    boxes的IOU_max)大于0.5的作为正样本anchor,
    IOU_max介于0.4-0.5之间的样本看作是ignore,classification ground truth = -1
    将IOU_max小于0.4的记作为负样本,classification ground truth = 0
    这是训练过程中数据加载器输出的classification target(ground truth)
    在focal loss中的forward函数中,先取出所有classification target>-1的anchor boxes,也就是所有的正样本和负样本
    anchor boxes,将所有正样本和负样本的classification target和classification prediction送入focal_loss函数中
    计算分类损失
    
    classification prediction  shape [#anchor,num_classes]
    表示对于当前anchor所预测为每个num_classes的类别分数,这里的#anchor指的是所有参与到计算分类损失函数计算的
    正样本anchor和负样本anchor,经过了sigmoid激活函数,num_classes不包含背景类别
    classification target  希望得到 shape [#anchor,num_classes]
    其中可以看作是对于shape 为[#anchor,num_classes]的2-dimension 图上的每个像素点进行binary cross entropy的计算
    即表示当前的anchor boxes属于某个前景类别的概率分数值
    classification target 应该具有这样的形式:
    对于类别为第2个类的anchor boxes ,其classification target为
    [0,1,0,0,0,0,0]
    对于负样本
    [0,0,0,0,0,0,0]

    loss = F.binary_cross_entropy_with_logits(x, t, w, size_average=False)
    也可以用
    bce = -(targets * torch.log(classification) + (1.0 - targets) * torch.log(1.0 - classification))
    cls_loss = focal_weight * bce

        这两行代码代替

    具体原因参见https://pytorch.org/docs/stable/nn.htmlhighlight=binary_cross_entropy_with_logits#torch.nn.BCEWithLogitsLoss
    因为torch.nn.functional.binary_cross_entropy_with_logits函数本身就要求输入的prediction tensor和target tensor具有
    相同的shape,然后target tensor每个点是0/1取值

    简单提下,focal loss的要点在于,在原来的二分类交叉熵基础上加上权重,如果是正样本(这里之所以能够使用二分类
    交叉熵就是因为它把对于每个anchor 的多分类问题变成了对于每个类别的二分类问题),则权重(1-p),则p越接近1(表示分类正确),
    样本权重越小,如果是负样本,则权重p,如果p越接近于0(表示分类正确),则样本权重越小。反之越大,就能保证
    网络模型在训练过程中重点focus on hard examples,也就是分类不准确的样本(包含正样本和负样本)
    '''
def _focal_loss(self, x, y):
    '''Focal loss.

    This is described in the original paper.
    With BCELoss, the background should not be counted in num_classes.

    Args:
      x: (tensor) predictions, sized [N,D].
      y: (tensor) targets, sized [N,].

    Return:
      (tensor) focal loss.
    '''
    alpha = 0.25
    gamma = 2

    t = one_hot_embedding(y - 1, self.num_classes)
    # plus one during encode stage , here minus one to let classes   -1  0  1  2  3  4  5  6 for 7 different foreground classes and a background
    # t=-1  ingore
    # t>0
    # t=1

    # positive_indices=torch.ge(y,0)
    # num_positive_anchors=positive_indices.sum()
    
    negative_indices = torch.eq(y, 0)

    t[negative_indices, :] = 0

    # t = t[:, 1:]  # exclude background
    t = Variable(t).cuda()  # [N,20]

    # print(t.shape,x.shape,'xshape')

    p = x.sigmoid()
    pt = torch.where(t > 0, 1 - p, p)  # pt = p if t > 0 else 1-p
    w = pt.pow(gamma)
    w = torch.where(t > 0, alpha * w, (1 - alpha) * w)
    loss = F.binary_cross_entropy_with_logits(x, t, w, size_average=False)

    # loss=torch.where(torch.ne(y,-1.0),loss,torch.zeros(loss.shape).cuda())

    # loss=loss.sum()

    return loss


def forward(self, loc_preds, loc_targets, cls_preds, cls_targets):
    '''Compute loss between (loc_preds, loc_targets) and (cls_preds, cls_targets).

    Args:
      loc_preds: (tensor) predicted locations, sized [batch_size, #anchors, 4].
      loc_targets: (tensor) encoded target locations, sized [batch_size, #anchors, 4].
      cls_preds: (tensor) predicted class confidences, sized [batch_size, #anchors, #classes].
      cls_targets: (tensor) encoded target labels, sized [batch_size, #anchors].

    loss:
      (tensor) loss = SmoothL1Loss(loc_preds, loc_targets) + FocalLoss(cls_preds, cls_targets).
    '''

    # print('cls_targets',cls_targets.shape,loc_targets.shape)

    batch_size, num_boxes = cls_targets.size()
    pos = cls_targets > 0  # [N,#anchors]

    # print('pos',pos.shape,(pos.unsqueeze(2)).shape,loc_preds.shape)

    num_pos = pos.sum().item()

    # ===============================================================
    # loc_loss = SmoothL1Loss(pos_loc_preds, pos_loc_targets)
    # ===============================================================
    mask = pos.unsqueeze(2).expand_as(loc_preds)  # [N,#anchors,4]
    loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=False)

    # ===============================================================
    # cls_loss = FocalLoss(cls_preds, cls_targets)
    # ===============================================================
    pos_neg = cls_targets > -1  # exclude ignored anchors
    mask = pos_neg.unsqueeze(2).expand_as(cls_preds)
    masked_cls_preds = cls_preds[mask].view(-1, self.num_classes)
    cls_loss = self._focal_loss(masked_cls_preds, cls_targets[pos_neg])

    # print('loc_loss: %.3f | cls_loss: %.3f' % (loc_loss.item()/num_pos, cls_loss.item()/num_pos), end=' | ')
    #

    num_pos = float(max(num_pos, 1))

    loss = (loc_loss + cls_loss) / num_pos

    return loss, loc_loss / num_pos, cls_loss / num_pos

你可能感兴趣的:(RetinaNet)