YOLOv1学习笔记以及代码介绍

目录

  • 一、为什么叫YOLO?
  • 二、YOLOv1简要介绍
    • 1、YOLOv1算法框架
    • 2、YOLO统一的目标检测框架(核心思想)
    • 3、YOLO网络结构
    • 4、关于YOLO网络的最后输出
  • 三、YOLOV1的损失函数
  • 四、YOLOV1的整个过程叙述
  • 五、数据集介绍
    • 数据集格式介绍
  • 六、数据集的处理
  • 七、训练过程
  • 八、预测过程
  • 九、YOLOv1相较于其他算法的优缺点
  • 十、一些问题
    • 1、为什么说YOLOv1是回归问题?
    • 2、YOLOv1的核心思想
    • 3、该怎么理解“以某种网络为骨架实现YOLOv1”这种说法?

  初次学习YOLOV1,以B站up主:同济子豪兄的YOLO系列讲解视频为基础,结合大佬的代码以及自己的一些理解记录一下学习过程,希望可以尽可能从原理上理解YOLOv1。
​ 代码链接:YOLOv1代码
论文链接:YOLOV1
视频链接:YOLOv1视频讲解

以下内容多数为个人理解,如有错误欢迎指正,期待讨论~

一、为什么叫YOLO?

  刚开始了解YOLOv1时,难免不会想到,为什么要给这个算法起名叫YOLO?后来看到了论文的名称:You Only Look Once,发现YOLO是论文名称的首字母组合。
  继而又会想到,为什么要给这个算法起名:你只需看一次?在学完YOLOv1后,我理解了这个名字。留在文章最后说明~

二、YOLOv1简要介绍

  YOLOV1是由以Joseph Redmon为首的学者于2015年提出的一种新的目标检测算法。它与之前的目标检测算法如R-CNN等不同之处在于,R-CNN等目标检测算法是两阶段算法, 步骤为先在图片上生成候选框,然后利用分类器对这些候选框进行逐一的判断甄别;而YOLOv1是一阶段算法,是端到端的算法,它把目标检测问题设计为回归问题,将图片输入单一的神经网络,然后就输出得到了图片的物体边界框,即boundingbox以及分类概率等信息。

什么是boundingbox?个人理解:boundingbox意思是边界框。在目标检测中,我们需要对图片中的每一个物体进行定位分类,如下图:YOLOv1学习笔记以及代码介绍_第1张图片
我们利用一个框对图像中的物体进行定位,这个框就叫做boundingbox,简写为bbox。

什么是groundtruth?在目标检测中,groundtruth就是人工标注的类别以及定位框的信息。

如下图所示:
YOLOv1学习笔记以及代码介绍_第2张图片  这是结合论文所述,加上自己的理解所画的两个框架。上面的框架表示的是 yolo,下面表示的是R-CNN等算法。对于YOLO来说,我们只要将图片输入神经网络,那么就不用管了,神经网络自会输出我们想要的信息,一步到位。从开始到结束中间的所有过程都可以被封装为一个黑箱;而对于R-CNN等算法,在图片上挑选出候选框后我们还要将图片输入到分类器中逐一判断,这是一个具有阶段性的相对复杂的工作流,也就是如下面的框架所示。

1、YOLOv1算法框架

如下图所示:
YOLOv1学习笔记以及代码介绍_第3张图片  整个框架为:将图片大小重设为448*448,然后将修改后的图片输入卷积网络中,得到预测的bbox和分类概率等信息,最后利用非极大值抑制得到最终的检测结果。

2、YOLO统一的目标检测框架(核心思想)

  YOLOv1把目标检测分离的部分整合为一个神经网络,它使用从整张图片中得到的特征去预测每一个物体的边界框,也就是bbox,同时也预测所有框中物体所属类别。
  YOLO将输入图片分割为S×S个方格,如果图像中物体的中心点落在哪一个gridcell里,那么这个gridcell就负责检测这个物体。

什么是gridcell?如下图,每一个小方格就是一个grid cell。YOLOv1学习笔记以及代码介绍_第4张图片

  每一个gridcell会随机预测出B个boundingboxes和每个bbox的置信度,且B个bboxes的中心点都落在该gridcell里,这样就可以知道哪个bbox是由哪个gridcell预测出的。所谓置信度就是bbox是否包含物体的置信度。bbox置信度的计算公式为: P r ( O b j e c t ) × I O U p r e d t r u t h Pr(Object)\times IOU_{pred}^{truth} Pr(Object)×IOUpredtruth,也就是bbox中包含物体的概率×预测出的bbox和groundtruth的交并比。

什么是IOU交并比?IOU全称intersection over union,在目标检测中用来衡量预测框的准确率,即通过计算预测框与人工标注的真实框之间交集与并集的比,来判断预测的是否准确。

  为什么可以这样计算呢?我们知道置信度其实相当于包含物体的概率,如果越包含,那么置信度就越接近1。越包含物体说明物体的大部分都在这个bbox里,而groundtruth就是包括了整个物体在内,所以此时bbox和groundtruth的重叠部分就越多,交并比也就越大,置信度也就越大;而越不包含则相反,bbox里只有很小甚至没有物体在内,那么其与groundtruth的重叠部分也就越小,并起来的区域就越大,分子变小分母变大,IOU减小直至重叠部分为0.

  如果bbox中没有物体存在,那么置信度就是0;假如有物体存在,那么pr(object)=1,此时置信度就是bbox和groundtruth的IOU。

  每一个boundingbox包含五个信息:x,y,w,h和置信度。其中(x,y)表示boundingbox的中心点相对于其所在的gridcell的左上角坐标,也就是以gridcell左上角为坐标原点,得到的bbox中心点坐标;w,h是相对于整幅图片的宽高,最后置信度表示IOU。
  每一个gridcell也预测在其包含物体的条件下,所包含物体属于每一类的条件概率。在YOLOv1中,每一个gridcell只有一组类条件概率,也就是说该gridcell所预测的B个bboxes共享这一组概率。
  在YOLOV1中,S=7.B=2,使用的数据集是Pascal VOC,该数据集有20个类别。

3、YOLO网络结构

在论文中,作者使用的以下的网络结构:
YOLOv1学习笔记以及代码介绍_第5张图片  YOLOv1的网络为24层卷积层+2个全连接层,卷积层用来从图像中提取特征,而全连接层用来预测分类概率和bbox坐标。最后的输出是7×7×30的张量。

4、关于YOLO网络的最后输出

  最后输出7×7×30的张量,对于每一个gridcell来说,它预测两个bbox,每个bbox包含x,y,w,h,confidence五个信息;同时每个gridcell还包含一组类条件概率,即该gridcell包含物体的条件下,物体所属每一类的概率。所以每个gridcell包含的信息就是:2×5+2×5+20=30,总共有7*7个gridcell,所以最终张量为7×7×30.

三、YOLOV1的损失函数

YOLOV1损失函数计算如下:YOLOv1学习笔记以及代码介绍_第6张图片

  对损失函数做解释:损失函数包含了三部分,分别为坐标误差、置信度误差以及分类误差,其实可以和网络最后的输出联系起来,也就是30=2×5+2×5+20,这30个信息里就包含坐标、置信度以及分类概率这三大信息。
  坐标误差:预测出的bbox的groundtruth框的中心点平方误差,以及相对于整幅图片的宽高平方误差。其中 Ⅱ i j o b j Ⅱ_{ij}^{obj} ijobj表示的是第i个gridcell的第j个bbox是否负责预测物体,如果负责则为1,不负责则为0,也就是说我们只需要调整负责预测的bbox,使其尽可能与groundtruth相近;下面置信度误差中的 Ⅱ i j n o o b j Ⅱ_{ij}^{noobj} ijnoobj相反,不负责预测为1。
  置信度误差:对于每一个gridcell的每一个bbox都有一个confidence score,如果包含物体,那么置信度就接近1,如果不包含物体,那么置信度就接近0。
  分类误差:每个gridcell负责预测一个物体,根据groundtruth可以每一个gridcell的真实分类,从而与预测进行比较。

  怎样确定预测物体的gridcell中哪个bbox负责预测物体呢?通过计算该gridcell中每个bbox与groundtruth的IOU,选择IOU最大的作为负责预测物体的bbox。

四、YOLOV1的整个过程叙述

  上面部分只针对YOLOV1的重点部分介绍了一下,YOLOV1的整个流程按个人理解如下:
  首先将groundtruth,即人工标注好的标签处理为7×7×30的张量,作为target;将处理过的图片输入到网络里进行训练,设计网络最后输出为7×7×30的张量作为prediction,利用网络的输出prediction与target进行比较,对网络进行优化,调整参数权重,直至训练结束。
  预测过程,将处理好的图片输入训练好的网络中,输出7×7×30的张量,然后利用非极大值抑制进行处理,即去除多余的boundingbox,最后得到分类检测结果。

  对于我来说,在看完yolov1的文字或者视频讲解后,有些地方还是很难以理解的,比如boundingbox如何产生的?根据loss函数,如何确定哪一个bbox对其所属的gridcell负责等等,因此我从github上下载了相关代码,通过读代码对YOLOV1进行更进一步的了解。以下为代码解读:

五、数据集介绍

  网上有很多的数据集介绍博客,这里选择了一个以供参考:迷途小书童的note:PASCAL VOC数据集
  数据集xml文件中,xmin/xmax/ymin/ymax是标注框的左上角右下角坐标。

数据集格式介绍

参考:周先森爱吃素:目标检测常用数据集格式
  目标检测中常见的三种数据集格式:VOC、YOLO、COCO,这三种格式之间可以相互转换,转换代码github或者CSDN都有详细介绍。

代码的数据集如下:
YOLOv1学习笔记以及代码介绍_第7张图片
上述是将VOC格式的数据集转换为txt文件。每一行从左至右分别是,图片名称,第一个物体的左上角和右下角坐标,分类;第二个物体的…
eg:第一行该图片中只有一个物体,物体类别为2;第二行该图片有三个物体,类别分别为8,8,19.

六、数据集的处理

#自定义数据集
class yoloDataset(data.Dataset):
    image_size = 448 #输入网络的图片大小
    def __init__(self,root,list_file,train,transform):
        print('data init')
        self.root=root#数据存储的根目录
        self.train = train
        self.transform=transform #对图片的变换组合
        self.fnames = []     #图片文件名
        self.boxes = []        #图像中物体坐标的集合
        self.labels = []       #图片中物体类别的集合
        self.mean = (123,117,104)#RGB均值

        if isinstance(list_file, list):#判断list_file是list类型
            #将voc2007和voc2012两个数据集的标签整合为一个
            tmp_file = '/tmp/listfile.txt'
            os.system('cat %s > %s' % (' '.join(list_file), tmp_file))
            #‘ ’.join(list_file)意思是将list_file列表中的元素以空格分隔开
            #再拼接成一个字符串。
            #函数展开来写应该是:str.join(),str表示字符串or字符,item表示一个成员
            #将处理后的list_file中的内容复制到tmp_file中
            list_file = tmp_file

#with open as f用来读写文件(夹)。
        with open(list_file) as f:
            lines  = f.readlines()#一次读取所有内容并按行返回list

        for line in lines:
            splited = line.strip().split()
            #strip方法用于移除字符串头尾指定的字符(默认为空格)
            #strip().split()表示删掉数据中头尾的空格,然后剩下的字符串以原有的空格分隔开来,形成字符串列表
            self.fnames.append(splited[0])
            #图片名

            #对坐标进行处理
            num_boxes = (len(splited) - 1) // 5
            #在一张图片中不只有一个物体存在,所以会有多个物体的坐标信息
            box=[]
            label=[]
            for i in range(num_boxes):
                x = float(splited[1+5*i])
                y = float(splited[2+5*i])
                x2 = float(splited[3+5*i])
                y2 = float(splited[4+5*i])
                c = splited[5+5*i]
                box.append([x,y,x2,y2])
                label.append(int(c)+1)
            self.boxes.append(torch.Tensor(box))
            #torch.Tensor是默认的tensor类型torch.FloatTensor的简称
            self.labels.append(torch.LongTensor(label))
            #label必须是longtensor的类型
        self.num_samples = len(self.boxes)
        

上述代码中一些不太懂的点以及解决:
1、图像预处理为什么要减去RGB均值:小小麦田mll:图像预处理之减去RGB均值
2、if isinstance:判断一个对象是否是已知的类型

    def __getitem__(self,idx):
        fname = self.fnames[idx]
        img = cv2.imread(os.path.join(self.root+fname))
        #cv2.imread读取图片后以多维数组保存图片信息,前两维表示图片的像素坐标,最后一位表示图片的通道。
        boxes = self.boxes[idx].clone()
        labels = self.labels[idx].clone()
       #关于拷贝:.clone是深拷贝

        #对图像的一些变换
        if self.train:
            img, boxes = self.random_flip(img, boxes)
            img,boxes = self.randomScale(img,boxes)
            img = self.randomBlur(img)
            img = self.RandomBrightness(img)
            img = self.RandomHue(img)
            img = self.RandomSaturation(img)
            img,boxes,labels = self.randomShift(img,boxes,labels)
            img,boxes,labels = self.randomCrop(img,boxes,labels)
      
        h,w,_ = img.shape
        boxes /= torch.Tensor([w,h,w,h]).expand_as(boxes)
        #torch.Tensor([w,h,w,h]).expand_as(boxes)是将前面的tensor张量扩张成维度与boxes一样的张量
        #归一化处理,将坐标归一化到0-1之间
        img = self.BGR2RGB(img) #将图片转换为RGB格式,至于为什么要转换,
                             #代码作者给出的理由是because pytorch pretrained model use RGB
        img = self.subMean(img,self.mean) #减去均值
        img = cv2.resize(img,(self.image_size,self.image_size))
        #将图片格式改变为448*448
        target = self.encoder(boxes,labels)# 7x7x30
        for t in self.transform:
            img = t(img)

        return img,target
    def __len__(self):
        return self.num_samples

    def encoder(self,boxes,labels):
        '''
        boxes (tensor) [[x1,y1,x2,y2],[]]
        labels (tensor) [...]
        return 7x7x30
        '''
        grid_num = 14#栅格数量

       #这一部分就是将groundtruth即人工标注的标签处理成与网络输出一致的7×7××30的张量,即target
        target = torch.zeros((grid_num,grid_num,30))#30=5+5+20
        cell_size = 1./grid_num#gridCell的大小
        wh = boxes[:,2:]-boxes[:,:2]#groundtruth框的大小,即右下角坐标减去左上角坐标,
                                    #得到框的宽与高,相对于整幅图片的宽高
        cxcy = (boxes[:,2:]+boxes[:,:2])/2#框的中心点坐标
        for i in range(cxcy.size()[0]):
            cxcy_sample = cxcy[i]
            ij = (cxcy_sample/cell_size).ceil()-1 #定位到gridcell
            #ceil函数向上取整,即取不小于x的最小整数
            target[int(ij[1]),int(ij[0]),4] = 1#第一个boundingbox的置信度
            target[int(ij[1]),int(ij[0]),9] = 1#第二个bbox的置信度
            target[int(ij[1]),int(ij[0]),int(labels[i])+9] = 1#相应的类别概率置1
            xy = ij*cell_size #物体中心点所在gridcell的左上角归一化坐标
            delta_xy = (cxcy_sample -xy)/cell_size#中心与左上标的差值
            target[int(ij[1]),int(ij[0]),2:4] = wh[i]#第一个bbox的相对宽高
            target[int(ij[1]),int(ij[0]),:2] = delta_xy#第一个bbox的相对左上角的坐标
            target[int(ij[1]),int(ij[0]),7:9] = wh[i]#第二个 bbox
            target[int(ij[1]),int(ij[0]),5:7] = delta_xy#第二个bbox
        return target

1、 关于拷贝:HouraisanF:详解Python的三种拷贝方式

2、关于 ij = (cxcy_sample/cell_size).ceil()-1 如何定位:

  至于为什么会在最后-1,是因为我们将groundtruth的中心点定位到gridcell里,是为了将其处理为7×7×30的张量target,其中7×7就是7×7个gridcell,而数组索引是从0开始的,因此第二行第三列在数组里索引就是(1,2),所以要-1。

3、从上述代码就很轻易可以看出来,target的两个bbox其实就groundtruth本身,所以target张量中两个bbox对应位置的信息就是groundtruth信息,置信度为1。且可以看出一种思想:物体中心点所在的gridcell负责预测该物体,而不管其他的gridcell。这种思想也说明了每个gridcell只能预测一类,因此YOLOV1不适合预测密集小目标。

总结:上述代码主要就是处理标签信息为7×7×30张量,处理过程为:得到物体框相对于整幅图片的相对宽高(0-1之间);将物体框的中心点对应到gridcell,继而得到物体框的中心点相对于其所在的gridcell左上角的坐标(未归一化),以及每个gridcell的类别信息。

七、训练过程

  整个的训练过程就是,将人工标注的信息处理为7×7×30张量,也就是target;然后将训练图片输入到网络中,得到网络的输出,计算输出pred与target之间的误差并进行优化,不断拟合网络的输出与target,知道训练结束。
  代码中以resnet50为骨架实现YOLOV1,这里对resnet50网络不做叙述,仅仅对训练过程中的LOSS函数做解释:

class yoloLoss(nn.Module):
    def __init__(self,S,B,l_coord,l_noobj):  #S代表图像纵向或者横向划分的gridcell数;
                                             #B代表每个gridcell的boundingbox的个数
                                             #l_coord,l_noobj代表loss函数权重
        super(yoloLoss,self).__init__()
        self.S = S
        self.B = B
        self.l_coord = l_coord #坐标回归误差权重
        self.l_noobj = l_noobj  #不预测物体的bbox置信度权重

    def compute_iou(self, box1, box2):
        '''Compute the intersection over union of two set of boxes, each box is [x1,y1,x2,y2].
        Args:
          box1: (tensor) bounding boxes, sized [N,4].
          box2: (tensor) bounding boxes, sized [M,4].
        Return:
          (tensor) iou, sized [N,M].
        '''
        N = box1.size(0)
        M = box2.size(0)
        
        #lt和rb中的信息分别表示的是预测框和标注框交叉区域的左上角和右下角坐标
        lt = torch.max(
            box1[:,:2].unsqueeze(1).expand(N,M,2),  # [N,2] -> [N,1,2] -> [N,M,2]
            box2[:,:2].unsqueeze(0).expand(N,M,2),  # [M,2] -> [1,M,2] -> [N,M,2]
        )

        rb = torch.min(
            box1[:,2:].unsqueeze(1).expand(N,M,2),  # [N,2] -> [N,1,2] -> [N,M,2]
            box2[:,2:].unsqueeze(0).expand(N,M,2),  # [M,2] -> [1,M,2] -> [N,M,2]
        )

        wh = rb - lt  # [N,M,2],交叉区域的宽高
        wh[wh<0] = 0  # clip at 0,即不相交的情况
        inter = wh[:,:,0] * wh[:,:,1]  # [N,M],交叉区域的面积

        area1 = (box1[:,2]-box1[:,0]) * (box1[:,3]-box1[:,1])  # [N,],预测框的面积
        area2 = (box2[:,2]-box2[:,0]) * (box2[:,3]-box2[:,1])  # [M,],标注框的面积
        area1 = area1.unsqueeze(1).expand_as(inter)  # [N,] -> [N,1] -> [N,M]
        area2 = area2.unsqueeze(0).expand_as(inter)  # [M,] -> [1,M] -> [N,M]

        iou = inter / (area1 + area2 - inter)#相交/相并
        return iou
    def forward(self,pred_tensor,target_tensor):
        '''
        pred_tensor: (tensor) size(batchsize,S,S,Bx5+20=30) [x,y,w,h,c]
        target_tensor: (tensor) size(batchsize,S,S,30)
        '''
        N = pred_tensor.size()[0]#batchsize大小
        coo_mask = target_tensor[:,:,:,4] > 0#置信度大于0,返回的是bool值,大小为bs×7×7
        noo_mask = target_tensor[:,:,:,4] == 0#置信度=0,返回的是bool值,大小为bs×7×7
        coo_mask = coo_mask.unsqueeze(-1).expand_as(target_tensor)
        #将coo_mask增维然后扩展成和target_tensor一样的维度
        noo_mask = noo_mask.unsqueeze(-1).expand_as(target_tensor)



       #对有物体存在的bbox进行处理:分为pred和target
        coo_pred = pred_tensor[coo_mask].view(-1,30)#在pred中选出在target中有物体存在的gridcell
                                                   #扩展为(,30)的大小,每一行表示一个gridcell                              
        box_pred = coo_pred[:,:10].contiguous().view(-1,5) #box[x1,y1,w1,h1,c1]
        #[x2,y2,w2,h2,c2]
        #view用在contiguous的变量上,coo_pred[:,:10]表示预测的每一个gridcell两个bbox的信息
        #reshape成(,5)的大小,每一行表示一个bbox
        class_pred = coo_pred[:,10:]  #每一个有物体存在的gridcell所属每一个类的概率            
        
        coo_target = target_tensor[coo_mask].view(-1,30)#target
        box_target = coo_target[:,:10].contiguous().view(-1,5)
        class_target = coo_target[:,10:]

        # compute not contain obj loss
        #计算不负责预测物体的gridcell中的两个bbox的置信度误差
        noo_pred = pred_tensor[noo_mask].view(-1,30)
        noo_target = target_tensor[noo_mask].view(-1,30)
        noo_pred_mask = torch.cuda.ByteTensor(noo_pred.size())
        noo_pred_mask.zero_()
        noo_pred_mask[:,4]=1;noo_pred_mask[:,9]=1
        noo_pred_c = noo_pred[noo_pred_mask] #noo pred只需要计算 c 的损失 size[-1,2],提取出两个bbox的置信度
        noo_target_c = noo_target[noo_pred_mask]
        nooobj_loss = F.mse_loss(noo_pred_c,noo_target_c,size_average=False)
       

        #compute contain obj loss
        #包含物体的gridcell有包括负责预测物体的bbox和不负责预测物体的bbox
        #误差包括坐标误差负责预测物体的bbox置信度误差,不负责预测物体的bbox置信度误差,以及分类误差
        coo_response_mask = torch.cuda.ByteTensor(box_target.size())
        coo_response_mask.zero_()
        coo_not_response_mask = torch.cuda.ByteTensor(box_target.size())
        coo_not_response_mask.zero_()
        box_target_iou = torch.zeros(box_target.size()).cuda()
        #循环步长为2,因为每一个gridcell包含两个bbox
        for i in range(0,box_target.size()[0],2): #choose the best iou box
        #两个预测框
            box1 = box_pred[i:i+2]
            box1_xyxy = Variable(torch.FloatTensor(box1.size()))
            #计算左上角和右下角的坐标,中心点坐标分别-1/2的框宽高和+1/2的框宽高
            box1_xyxy[:,:2] = box1[:,:2]/14. -0.5*box1[:,2:4]
            box1_xyxy[:,2:4] = box1[:,:2]/14. +0.5*box1[:,2:4]
            #一个标注框
            box2 = box_target[i].view(-1,5)
            box2_xyxy = Variable(torch.FloatTensor(box2.size()))
            box2_xyxy[:,:2] = box2[:,:2]/14. -0.5*box2[:,2:4]
            box2_xyxy[:,2:4] = box2[:,:2]/14. +0.5*box2[:,2:4]
            iou = self.compute_iou(box1_xyxy[:,:4],box2_xyxy[:,:4]) #[2,1]
            #获取最大的IOU以及最大值所在索引
            max_iou,max_index = iou.max(0)
            max_index = max_index.data.cuda()
            #确定哪一个bbox负责预测物体
            coo_response_mask[i+max_index]=1
            coo_not_response_mask[i+1-max_index]=1

            #####
            # we want the confidence score to equal the
            # intersection over union (IOU) between the predicted box
            # and the ground truth
            #####
            box_target_iou[i+max_index,torch.LongTensor([4]).cuda()] = (max_iou).data.cuda()
        box_target_iou = Variable(box_target_iou).cuda()
        #1.response loss
        box_pred_response = box_pred[coo_response_mask].view(-1,5)
        box_target_response_iou = box_target_iou[coo_response_mask].view(-1,5)
        box_target_response = box_target[coo_response_mask].view(-1,5)
        

        contain_loss = F.mse_loss(box_pred_response[:,4],box_target_response_iou[:,4],size_average=False)
        loc_loss = F.mse_loss(box_pred_response[:,:2],box_target_response[:,:2],size_average=False) + F.mse_loss(torch.sqrt(box_pred_response[:,2:4]),torch.sqrt(box_target_response[:,2:4]),size_average=False)
        #2.not response loss
        box_pred_not_response = box_pred[coo_not_response_mask].view(-1,5)
        box_target_not_response = box_target[coo_not_response_mask].view(-1,5)
        box_target_not_response[:,4]= 0
        #not_contain_loss = F.mse_loss(box_pred_response[:,4],box_target_response[:,4],size_average=False)
        
        #I believe this bug is simply a typo
        not_contain_loss = F.mse_loss(box_pred_not_response[:,4], box_target_not_response[:,4],size_average=False)

        #3.class loss
        class_loss = F.mse_loss(class_pred,class_target,size_average=False)

        return (self.l_coord*loc_loss + 2*contain_loss + not_contain_loss + self.l_noobj*nooobj_loss + class_loss)/N

计算IOU代码的粗略图解:

  Loss的编程过程总结如下:从target中得到负责预测的gridcell和不负责预测物体的gridcell位置索引信息,然后从pred中挑选出来,对于pred中不负责预测物体的gridcell,可以计算出其bbox与target中对应的bbox的置信度误差。(对于target来说,不负责预测物体的gridcell置信度为0,那么其对应的两个bbox置信度也为0)

  而对于pred中负责预测物体的gridcell,首先要计算这些gridcell的bbox与其对应的groundtruth的IOU,选择IOU更大的bbox作为对应gridcell负责预测物体的bbox,而另一个则不负责预测物体。

  对于pred中负责预测物体的bbox,其置信度误差是经过网络得到的bbox的置信度与该bbox和对应groundtruth的IOU的均方误差;坐标误差则是pred的bbox坐标与target的坐标均方误差;分类误差类似。
  对于不负责预测物体的bbox,其置信度误差就是经过网络得到的bbox的置信度与0的均方误差,因为对于target来说,不负责预测物体的bbox置信度为0.

八、预测过程

  预测过程最主要的就是预测图片经过网络输出后,还要经过非极大值抑制得到最终结果:

关于非极大值抑制,参考:谨慎诚恕:YOLOv1前向推断后处理——NMS非极大值抑制
(代码部分的非极大值抑制对我来说比较难理解o(╥﹏╥)o)

def decoder(pred):
    '''
    pred (tensor) 1x7x7x30
    return (tensor) box[[x1,y1,x2,y2]] label[...]
    '''
    grid_num = 7#栅格数
    boxes=[]
    cls_indexs=[]
    probs = []
    cell_size = 1./grid_num#gridcell大小
    pred = pred.data
    pred = pred.squeeze(0) #7x7x30
    contain1 = pred[:,:,4].unsqueeze(2)#torch.size(7,7,1),置信度
    contain2 = pred[:,:,9].unsqueeze(2)#torch.size(7,7,1)
    contain = torch.cat((contain1,contain2),2)#torch.size(7,7,2)
    mask1 = contain > 0.1 #大于阈值
    mask2 = (contain==contain.max()) #we always select the best contain_prob what ever it>0.9
    mask = (mask1+mask2).gt(0)
    # min_score,min_index = torch.min(contain,2) #每个cell只选最大概率的那个预测框
    for i in range(grid_num):
        for j in range(grid_num):
            for b in range(2):
                # index = min_index[i,j]
                # mask[i,j,index] = 0
                if mask[i,j,b] == 1:
                    #网络的输出是相对于gridcell左上角的中心点坐标+相对于整幅图的宽高,
                    #需要先转变为bbox的左上角坐标和右下角坐标。
                    box = pred[i,j,b*5:b*5+4]
                    contain_prob = torch.FloatTensor([pred[i,j,b*5+4]])
                    xy = torch.FloatTensor([j,i])*cell_size #cell左上角  up left of cell
                    box[:2] = box[:2]*cell_size + xy # return cxcy relative to image
                    #bbox中心点的相对于gridcell左上角的坐标+左上角的坐标=中心点相对整幅图,归一化
                    box_xy = torch.FloatTensor(box.size())#转换成xy形式    convert[cx,cy,w,h] to [x1,xy1,x2,y2]
                    box_xy[:2] = box[:2] - 0.5*box[2:]#bbox的左上角坐标
                    box_xy[2:] = box[:2] + 0.5*box[2:]#bbox的右下角坐标
                    max_prob,cls_index = torch.max(pred[i,j,10:],0)
                    if float((contain_prob*max_prob)[0]) > 0.1:
                        boxes.append(box_xy.view(1,4))
                        cls_indexs.append(cls_index)#概率最大的类别
                        probs.append(contain_prob*max_prob)
    if len(boxes) ==0:
        boxes = torch.zeros((1,4))
        probs = torch.zeros(1)
        cls_indexs = torch.zeros(1)
    else:
        boxes = torch.cat(boxes,0) #(n,4)
        probs = torch.cat(probs,0) #(n,)
        cls_indexs = torch.cat(cls_indexs,0) #(n,)
    keep = nms(boxes,probs)
    return boxes[keep],cls_indexs[keep],probs[keep]

def nms(bboxes,scores,threshold=0.5):
    '''
    bboxes(tensor) [N,4]
    scores(tensor) [N,]
    '''
    x1 = bboxes[:,0]
    y1 = bboxes[:,1]
    x2 = bboxes[:,2]
    y2 = bboxes[:,3]
    areas = (x2-x1) * (y2-y1)

    _,order = scores.sort(0,descending=True)
    keep = []
    while order.numel() > 0:
        i = order[0]
        keep.append(i)

        if order.numel() == 1:
            break

        xx1 = x1[order[1:]].clamp(min=x1[i])
        yy1 = y1[order[1:]].clamp(min=y1[i])
        xx2 = x2[order[1:]].clamp(max=x2[i])
        yy2 = y2[order[1:]].clamp(max=y2[i])

        w = (xx2-xx1).clamp(min=0)
        h = (yy2-yy1).clamp(min=0)
        inter = w*h

        ovr = inter / (areas[i] + areas[order[1:]] - inter)
        ids = (ovr<=threshold).nonzero().squeeze()
        if ids.numel() == 0:
            break
        order = order[ids+1]
    return torch.LongTensor(keep)
#
#start predict one image
#
def predict_gpu(model,image_name,root_path=''):

    result = []
    image = cv2.imread(root_path+image_name)#读取图片
    h,w,_ = image.shape
    img = cv2.resize(image,(448,448))
    img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    mean = (123,117,104)#RGB
    img = img - np.array(mean,dtype=np.float32)

    transform = transforms.Compose([transforms.ToTensor(),])
    img = transform(img)
    img = Variable(img[None,:,:,:],volatile=True)
    img = img.cuda()

    pred = model(img) #1x7x7x30
    pred = pred.cpu()
    boxes,cls_indexs,probs =  decoder(pred)

    for i,box in enumerate(boxes):
        x1 = int(box[0]*w)
        x2 = int(box[2]*w)
        y1 = int(box[1]*h)
        y2 = int(box[3]*h)
        cls_index = cls_indexs[i]
        cls_index = int(cls_index) # convert LongTensor to int
        prob = probs[i]
        prob = float(prob)
        result.append([(x1,y1),(x2,y2),VOC_CLASSES[cls_index],image_name,prob])
    return result
        



if __name__ == '__main__':
    model = resnet50()
    print('load model...')
    model.load_state_dict(torch.load('best.pth'))
    model.eval()
    model.cuda()
    image_name = 'dog.jpg'
    image = cv2.imread(image_name)
    print('predicting...')
    result = predict_gpu(model,image_name)
    for left_up,right_bottom,class_name,_,prob in result:
        color = Color[VOC_CLASSES.index(class_name)]
        cv2.rectangle(image,left_up,right_bottom,color,2)
        label = class_name+str(round(prob,2))
        text_size, baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)
        p1 = (left_up[0], left_up[1]- text_size[1])
        cv2.rectangle(image, (p1[0] - 2//2, p1[1] - 2 - baseline), (p1[0] + text_size[0], p1[1] + text_size[1]), color, -1)
        cv2.putText(image, label, (p1[0], p1[1] + baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1, 8)

    cv2.imwrite('result.jpg',image)

每一类的概率=置信度×该类的条件概率。

九、YOLOv1相较于其他算法的优缺点

优点:简单并且相当快速。YOLOv1将目标检测问题设计为回归问题,将整张图片输入单个卷积网络就可以得到结果,不需要复杂的工作流;YOLOV1从整张图片获取信息,因此相比于其他的目标检测算法,YOLOV1将背景当做目标的错误率会更低;YOLOV1泛化能力较强,其在艺术作品等测试集上的精确度要比其他的算法高。
缺点:YOLOV1的精确度比较低,且定位精度比较差,尤其是针对密集与小物体。个人理解这样的原因是:YOLOV1将整幅图片分割成多个gridcells,每一个gridcell检测一个类别,因此当目标比较密集或者比较小的时候,容易出现一个框里有多个物体。

十、一些问题

1、为什么说YOLOv1是回归问题?

  通过分析YOLOV1的整个过程,这个问题的答案已经比较明显了。回归问题的学习等价于函数拟合,选择一条函数曲线使其很好地拟合已知数据,并对未知的数据做出预测。这样的方法显然在YOLOV1中有所体现,在训练过程中,神经网络权重参数不断调整,使得其最终的输出与groundtruth不断趋向于更加拟合,更重要的体现在于YOLOV1的loss函数采用均方误差。

2、YOLOv1的核心思想

  YOLOV1将目标检测问题设计为回归问题,其核心思想在于将整幅图像分割为S*S个gridcell,每个gridcell预测两个boundingbox,每个物体会产生一个中心点,中心点所在的gridcell负责预测该物体,最终由该gridcell中与groundtruth的IOU最大的那个boundingbox对物体的预测负责。在训练过程中使得负责预测物体的bbox一步步拟合groundtruth。
  每个boundingbox包含五个信息,网络最后的输出为两个bbox各自的信息以及其所属的gridcell在包含物体的情况下每一类的条件概率,为7×7×30的张量。
  LOSS函数采用均方误差。

3、该怎么理解“以某种网络为骨架实现YOLOv1”这种说法?

  之前一直对这种说法不太理解,但是在看了YOLOV1的代码后,有了一点想法。
  在代码中,作者以resnet50为骨架构建了YOLOV1,我起初想从代码作者的网络设计中找到论文中的网络架构,发现基本上就是resnet50的结构,只是略微改动使得最终的输出为7×7×30的张量。因此按我个人理解,以某种网络为骨架实现YOLOV1,并不是说要包含论文中YOLOV1的网络结构,而是利用某种网络,使用YOLOV1的思想去实现YOLOV1,关键点在于思想,而不是结构,对于结构,只需要让其最终输出YOLOV1对应的张量即可。论文中作者采用的网络结构也是借鉴了Googlenet,去实现自己算法的核心思想,所以思想才是关键。

  所以算法为什么叫做“你只需看一次”呢?个人理解是因为YOLOV1只需要将处理好的图片输入到网络中,就可以直接输出结果,即在处理好图片后的整个训练过程中,你只会看到原图片一次,就是图片进入网络之前,下一次看到图片就是最终结果了。(个人理解,如有错误欢迎指正!)

你可能感兴趣的:(YOLO,目标检测,深度学习,python)