YOLO v1

论文:You Only Look Once(CVPR 2016)
代码https://github.com/JunshengFu/vehicle-detection

模型结构

在这里插入图片描述
YOLOv1的网络模型结构受 GoogLeNet 启发,采用了 1x1 和 3x3 的序列组合替代 Inception 模块,总共 24 个卷积层加上 2 个全连接层。

算法

YOLO 利用整张图作为网络的输入,直接在输出层回归 bounding box(边界框) 位置及其所属的类别:输入图像划分成S*S的格子,每个格子都预测C个类别概率和B个Bounding Box,如果目标的中心落在某个格子中,该格子就负责预测该目标,虽然目标可能覆盖多个格子。每个格子只能检测出一个目标(B个Bounding Box中选一个最好的,即与实际框IoU最大的,这也是YOLO对密集的对象检测效果不好的原因),每个Bounding Box都包含5个预测值: x , y , w , h x,y,w,h x,y,w,h c o n f i d e n c e confidence confidence

输出

class VGG(nn.Module):

    def __init__(self, features, num_classes=1000):
        super(VGG, self).__init__()
        self.features = features
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(),
            nn.Linear(4096, num_classes),
        )
        self._initialize_weights()

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        # 从vgg16得到输出,经过sigmoid 归一化到0-1之间
        x = F.sigmoid(x)
        # 再改变形状,返回(xxx,7,7,30)  xxx代表几张照片,(7,7,30)代表一张照片的信息
        x = x.view(-1, 7, 7, 30)
        return x

输出张量: S ∗ S ∗ ( C + B ∗ ( 4 + 1 ) ) S*S*(C+B*(4+1)) SS(C+B(4+1))
S ∗ S S*S SS为网格数, C C C为对象类别数, B B B为每个网格预测的Bounding box数量, ( 4 + 1 ) (4+1) (4+1)每个框的4个位置信息+置信度
置信度公式为:
C o n f i d e n c e = P r ( O b j e c t ) ∗ I O U p r e d t r u t h Confidence=Pr(Object)*IOU^{truth}_{pred} Confidence=Pr(Object)IOUpredtruth

P r ( O b j e c t ) Pr(Object) Pr(Object)表示含目标的概率, I O U p r e d t r u t h IOU^{truth}_{pred} IOUpredtruth由预测的Bounding Box与对象真实Bounding Box计算得到(文中参数为: 7 ∗ 7 ∗ ( 20 + 2 ∗ ( 4 + 1 ) ) 7*7*(20+2*(4+1)) 77(20+2(4+1))
在这里插入图片描述
在这里插入图片描述

损失函数

求Loss首先要了解Loss有哪些部分构成,其实就是分两部分:
1、实际有目标的格子:置信度、坐标、宽高、类别损失
2、实际没有目标的格子:置信度损失
L o s s = λ coord  ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  [ ( x i − x ^ i ) 2 + ( y i − y ^ i ) 2 ]    中心坐标误差 + λ coord  ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  [ ( w i − w ^ i ) 2 + ( h i − h ^ i ) 2 ] 边框宽高误差 + ∑ i = 0 S 2 ∑ j = 0 B 1 i j obj  ( C i − C ^ i ) 2    置信度误差( 框内含对象) + λ noobj  ∑ i = 0 S 2 ∑ j = 0 B 1 i j noobj  ( C i − C ^ i ) 2 置信度误差( 框内无对象) + ∑ i = 0 S 2 1 i obj  ∑ c ∈  classes  ( p i ( c ) − p ^ i ( c ) ) 2 分类误差 \begin{aligned} {\mathcal{\Large{Loss}}} &= \lambda_{\text {coord }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left[\left(x_{i}-\hat{x}_{i}\right)^{2}+\left(y_{i}-\hat{y}_{i}\right)^{2}\right] \qquad\qquad\quad\quad\ \ \color{blue}\text{中心坐标误差}\\ &+\lambda_{\text {coord }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left[(\sqrt{w_{i}}-\sqrt{\hat{w}_{i}})^{2}+(\sqrt{h_{i}}-\sqrt{\hat{h}_{i}})^{2}\right] \qquad\color{blue}\text{边框宽高误差}\\ &+\sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {obj }}\left(C_{i}-\hat{C}_{i}\right)^{2}\qquad\qquad\qquad\qquad\qquad\qquad\qquad\ \ \color{blue}\text{置信度误差( 框内含对象)} \\ &+\lambda_{\text {noobj }} \sum_{i=0}^{S^{2}} \sum_{j=0}^{B} \mathbb{1}_{i j}^{\text {noobj }}\left(C_{i}-\hat{C}_{i}\right)^{2} \qquad\qquad\qquad\qquad\quad\qquad\color{blue}\text{置信度误差( 框内无对象)}\\ &+\sum_{i=0}^{S^{2}} \mathbb{1}_{i}^{\text {obj }} \sum_{c \in \text { classes }}\left(p_{i}(c)-\hat{p}_{i}(c)\right)^{2}\qquad\qquad\qquad\qquad\qquad\quad\color{blue}\text{分类误差} \end{aligned} Loss=λcoord i=0S2j=0B1ijobj [(xix^i)2+(yiy^i)2]  中心坐标误差+λcoord i=0S2j=0B1ijobj [(wi w^i )2+(hi h^i )2]边框宽高误差+i=0S2j=0B1ijobj (CiC^i)2  置信度误差框内含对象)+λnoobj i=0S2j=0B1ijnoobj (CiC^i)2置信度误差框内无对象)+i=0S21iobj c classes (pi(c)p^i(c))2分类误差

  • 1 i o b j \color{blue}1_i^{obj} 1iobj:网格 i i i 中存在对象。
  • 1 i j o b j \color{blue}1_{ij}^{obj} 1ijobj:网格 i i i 的第 j j j 个Bounding Box中存在对象
  • 1 i j n o o b j \color{blue}1_{ij}^{noobj} 1ijnoobj:网格 i i i 的第 j j j 个Bounding Box中不存在对象
  • λ c o o r d \color{blue}\lambda_{coord} λcoord λ n o o b j \color{blue}\lambda_{noobj} λnoobj分别是各个损失的权重,文中前者取5,后者取0.5
    边框宽高Loss项中对宽和高取平方根是为了降低Loss对大目标的敏感度,如果直接用差值作为Loss,那么大目标的预测框就算很准,其Loss也可能比预测效果很差的小目标的Loss大

具体如何求损失?
1、生成标签
根据预测和Loss将原标签变换成相应格式,比如需要中心位置、置信度(原标签给定的只有信息有:目标框坐标及其类别 )
2、生成标签代码

def encoder(self, boxes, labels):
    '''
    boxes (tensor) [[x1,y1,x2,y2],[x1,y1,x2,y2],[]] 坐标已被缩放到 [0-1] 之间
    labels (tensor) [...]
    return 7x7x30
    '''
    target = torch.zeros((7, 7, 30))
    cell_size = 1. / 7
    # boxes[:, 2:]代表  2: 代表xmax,ymax
    # boxes[:, :2]代表  :2  代表xmin,ymin
    # wh代表  bbox的宽(xmax-xmin)和高(ymax-ymin)
    wh = boxes[:, 2:] - boxes[:, :2]
    # bbox的中心点坐标
    cxcy = (boxes[:, 2:] + boxes[:, :2]) / 2
    # cxcy.size()[0]代表 一张图像的物体总数
    # 遍历一张图像的物体总数
    for i in range(cxcy.size()[0]):
        # 拿到第i行数据,即第i个bbox的中心点坐标(相对于整张图,取值在0-1之间)
        cxcy_sample = cxcy[i]
        # ceil返回数字的上入整数
        # cxcy_sample为一个物体的中心点坐标,求该坐标位于7x7网格的哪个网格
        # cxcy_sample坐标在0-1之间  现在求它再0-7之间的值,故乘以7
        # ij长度为2,代表7x7框的某一个框 负责预测一个物体
        ij = (cxcy_sample / cell_size).ceil() - 1
        # 每行的第4和第9的值设置为1,即每个网格提供的两个真实候选框 框住物体的概率是1.
        # xml中坐标理解:原图像左上角为原点,右边为x轴,下边为y轴。
        # 而二维矩阵(x,y)  x代表第几行,y代表第几列
        # 假设ij为(1,2) 代表x轴方向长度为1,y轴方向长度为2
        # 二维矩阵取(2,1) 从0开始,代表第2行,第1列的值
        # 画一下图就明白了
        target[int(ij[1]), int(ij[0]), 4] = 1
        target[int(ij[1]), int(ij[0]), 9] = 1
        # 加9是因为前0-9为两个真实候选款的值。后10-20为20分类   将对应分类标为1
        target[int(ij[1]), int(ij[0]), int(labels[i]) + 9] = 1
        # 匹配到的网格的左上角的坐标(取值在0-1之间)(原作者)
        # 根据二维矩阵的性质,从上到下  从左到右
        xy = ij * cell_size
        # cxcy_sample:第i个bbox的中心点坐标     xy:匹配到的网格的左上角相对坐标
        # delta_xy:真实框的中心点坐标相对于  位于该中心点所在网格的左上角   的相对坐标,此时可以将网格的左上角看做原点,你这点相对于原点的位置。取值在0-1,但是比1/7小
        delta_xy = (cxcy_sample - xy) / cell_size
        # x,y代表了检测框中心相对于网格边框的坐标。w,h的取值相对于整幅图像的尺寸
        # 写入一个网格对应两个框的x,y,   wh:bbox的宽(xmax-xmin)和高(ymax-ymin)(取值在0-1之间)
        target[int(ij[1]), int(ij[0]), 2:4] = wh[i]
        target[int(ij[1]), int(ij[0]), :2] = delta_xy
        target[int(ij[1]), int(ij[0]), 7:9] = wh[i]
        target[int(ij[1]), int(ij[0]), 5:7] = delta_xy
    return target

3、根据预测和标签选择格子、确定对应的标签
注意这个时候标签并没有定下来,因为并不是所有预测框都是有效的,根据选择的框调整标签,如下图:预测的Bounding box1 效果比较好,就把标签对应置信区间位置处设置为1(标签当然百分百确定这里有目标)
在这里插入图片描述

3、计算各部分Loss,并分配适当权重
4、Loss代码详解

class yoloLoss(nn.Module):
    '''
    torch.nn.Modules相当于是对网络某种层的封装,包括网络结构以及网络参数,和其他有用的操作如输出参数
    继承Modules类,需实现__init__()方法,以及forward()方法
    '''
    def __init__(self,S,B,l_coord,l_noobj):
        super(yoloLoss,self).__init__()
        self.S = S    # 将图像分为SxS的网格
        self.B = B    # 一个网格预测B个框
        self.l_coord = l_coord   # 坐标损失权重
        self.l_noobj = l_noobj   # 无目标处置信度损失权重

    def compute_iou(self, box1, box2):
    “”“
    这里要实现计算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]  # N为batchsize
        # 坐标mask    4:是物体或者背景的confidence 
        coo_mask = target_tensor[:,:,:,4] > 0  # 标记有目标的位置
        # 没有物体mask                               
        noo_mask = target_tensor[:,:,:,4] == 0  # 标记无有目标的位置
        # unsqueeze(-1) 扩展最后一维,用0填充,使得形状与target_tensor一样
        # coo_mask、noo_mask形状扩充到[32,7,7,30]
        # coo_mask 大部分为0   记录为1代表真实有物体的网格
        # noo_mask  大部分为1  记录为1代表真实无物体的网格
        coo_mask = coo_mask.unsqueeze(-1).expand_as(target_tensor)
        noo_mask = noo_mask.unsqueeze(-1).expand_as(target_tensor)
        ###########对于实际存在的目标,需要计算置信度、坐标、宽高、类别损失##########
        # coo_pred 取出预测结果中有物体的网格,并改变形状为(xxx,30)  xxx代表一个batch的图片上的存在物体的网格总数    30代表2*5+20   例如:coo_pred[72,30]
        coo_pred = pred_tensor[coo_mask].view(-1,30)
        # 一个网格预测的两个box  30的前10即为2个x,y,w,h,c,并调整为(xxx,5) xxx为所有真实存在物体的预测框,而非所有真实存在物体的网格     例如:box_pred[144,5]
        # contiguous将不连续的数组调整为连续的数组
        box_pred = coo_pred[:,:10].contiguous().view(-1,5) # box[x1,y1,w1,h1,c1]
                                                           #    [x2,y2,w2,h2,c2]
        # 每个网格预测的类别后20
        class_pred = coo_pred[:,10:]

        # 对真实标签做同样操作
        coo_target = target_tensor[coo_mask].view(-1,30)  # 提取有目标格子及对应属性:位置、置信度、类别
        box_target = coo_target[:,:10].contiguous().view(-1,5)  # 提取位置、置信度
        class_target = coo_target[:,10:]  # 类别

        ########对于实际不存在的目标,预测有计算置信度损失################
        # 在预测结果中拿到真实无物体的网格,并改变形状为(xxx,30)  xxx代表一个batch的图片上的不存在物体的网格总数    30代表2*5+20   例如:[1496,30]
        noo_pred = pred_tensor[noo_mask].view(-1,30)
        noo_target = target_tensor[noo_mask].view(-1,30)      # 例如:[1496,30]
        # ByteTensor:8-bit integer (unsigned)
        noo_pred_mask = torch.cuda.ByteTensor(noo_pred.size())   # 例如:[1496,30]
        noo_pred_mask.zero_()   #初始化全为0
        # 预测的置信度掩码(实际无目标格子处)
        # 将第4、9  即有物体的confidence位置置为1
        noo_pred_mask[:,4]=1;noo_pred_mask[:,9]=1
        # 拿到第4列和第9列里面的值(即拿到真实无物体的网格中,网络预测这些网格有物体的概率值)    一行有两个值(第4和第9位)                           例如noo_pred_c:2992        noo_target_c:2992
        # 拿到预测置信度(实际无目标格子处)
        # noo pred只需要计算类别c的损失
        noo_pred_c = noo_pred[noo_pred_mask]
        # 拿到标签实际无目标格子处置信度(标签当然已经知道有该处有目标概率为0)
        noo_target_c = noo_target[noo_pred_mask]
        ######## 实际没有目标的地方要计算置信度均方误差,本来没有你说有:Loos1 ########
        # reduce = False,返回向量形式的 loss reduce = True, 返回标量形式的los
        # size_average=True 返回 loss.mean();size_average=False 返回 loss.sum()
        nooobj_loss = F.mse_loss(noo_pred_c,noo_target_c,size_average=False)


        #计算包含obj损失  即本来有,预测有  和  本来有,预测无
        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_()
        # 选择与目标框IOU最大的那个
        for i in range(0,box_target.size()[0],2):
        	# 转换一下框格式以计算IoU【c_x,c_y,w,h】-->【x_1,y_1,x_2,y_2】
            box1 = box_pred[i:i+2]
            box1_xyxy = Variable(torch.FloatTensor(box1.size()))
            box1_xyxy[:,:2] = box1[:,:2] -0.5*box1[:,2:4]
            box1_xyxy[:,2:4] = box1[:,:2] +0.5*box1[:,2:4]
            box2 = box_target[i].view(-1,5)
            box2_xyxy = Variable(torch.FloatTensor(box2.size()))
            box2_xyxy[:,:2] = box2[:,:2] -0.5*box2[:,2:4]
            box2_xyxy[:,2:4] = box2[:,:2] +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)  # 得到最大IoU及其索引
            max_index = max_index.data.cuda()
            coo_response_mask[i+max_index] = 1  # 确定哪一个框为有效预测框
            coo_not_response_mask[i+1-max_index] = 1 
        # 1.response loss响应损失,即本来有,预测有   有相应 坐标预测的loss  (x,y,w开方,h开方)
        # box_pred [144,5]   coo_response_mask[144,5]   box_pred_response:[72,5]
        # 选择IOU最好的box来进行调整,负责检测出某物体
        box_pred_response = box_pred[coo_response_mask].view(-1,5)
        # 将标签中的框与有效预测框对应
        box_target_response = box_target[coo_response_mask].view(-1,5) 
        ######## 实际有目标的地方要计算置信度均方误差,本来有你说好像没:Loss2 ########
        # box_pred_response:[72,5]     计算预测 有物体的概率误差,返回一个数
        contain_loss = F.mse_loss(box_pred_response[:,4],box_target_response[:,4],size_average=False)
        ######## 实际有目标的地方要计算中心坐标和宽高误差,预测肯定有偏差:Loos3、Loss4 ########
        # 计算(x,y,w开方,h开方)
        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)

        ######## 实际有目标的地方要计算类别均方误差:Loss5########
        class_loss = F.mse_loss(class_pred,class_target,size_average=False)
        # 除以N  即平均一张图的总损失
        return (self.l_coord*loc_loss + contain_loss  + self.l_noobj*nooobj_loss + class_loss)/N  

训练

因为卷积层最后接了两个全连接层,输入图片要求缩放到 448 × 448 448\times448 448×448

先取 YOLO 模型前面的 20 个卷积加上 1 平均池化层和全连接层在 ImageNet 上预训练,目的是为了获取目标的特征表达能力,输入图片大小为224*224,然后使用完整的网络(全连接层处使用了dropout),在PASCAL VOC数据集上进行对象识别和定位的训练和预测,输入图片的分辨率调到 448 × 448 448\times448 448×448

在这里插入图片描述

预测(inference)

网络得到每张图片的pred为 1x7x7x30 ,需要的对其进行解码操作得到正常的预测框,然后挑选出一些较好的框作为最终预测结果。
解码代码

def decoder(pred):
    '''
    解码
    pred (tensor) 1x7x7x30
    return (tensor) box[[x1,y1,x2,y2]] label[...]
    '''
    boxes = []
    cls_indexs = []
    probs = []
    cell_size = 1./7
    pred = pred.data
    pred = pred.squeeze(0)  # 7x7x30
    contain1 = pred[:, :, 4].unsqueeze(2)
    contain2 = pred[:, :, 9].unsqueeze(2)
    contain = torch.cat((contain1, contain2), 2)
    mask1 = contain > 0.9  # 大于阈值
    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(mask, 2)  # 每个cell只选最大概率的那个预测框
    for i in range(7):
        for j in range(7):
            for b in range(2):
                index = min_index[i, j]
                mask[i, j, index] = 0
                if mask[i, j, b] == 1:
                    # print(i,j,b)
                    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
                    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:]
                    box_xy[2:] = box[:2] + 0.5*box[2:]
                    max_prob, cls_index = torch.max(pred[i, j, 10:], 0)
                    boxes.append(box_xy.view(1, 4))
                    cls_indexs.append(cls_index)
                    probs.append(contain_prob)
    boxes = torch.cat(boxes, 0)  # (n,4)
    probs = torch.cat(probs, 0)  # (n,)
    # cls_indexs = torch.cat(cls_indexs,  0)   # (n,)
    cls_indexs = torch.stack(cls_indexs, dim=0)
    keep = nms(boxes, probs)
    return boxes[keep], cls_indexs[keep], probs[keep]

NMS算法步骤如下:

1、设置一个Score的阈值,低于该阈值的候选对象排除掉(将该Score设为02、遍历每一个对象类别
	1)遍历该对象的98个得分
	2)找到Score最大的那个对象及其bounding box,添加到输出列表
	3)对每个Score不为0的候选对象,计算其与最大Score输出对象的bounding box的IOU
	4)根据预先设置的IOU阈值,排除高于该阈值的候选对象
	5)遍历完该对象类别所有的box,返回步骤2处理下一类对象
3、输出列表即为预测的对象

补充

激活函数采用的Leaky ReLU
f ( x ) = { x , i f x > 0 0.1 x ,     o t h e r w i s e \begin{aligned} f(x)=\left\{ \begin{aligned} &x,\qquad if x>0\\ &0.1x,\ \ \ otherwise \end{aligned}\right. \end{aligned} f(x)={x,ifx>00.1x,   otherwise

YOLO优点

  • 速度快
  • 泛化能力强
  • 基于图像的全局信息预测,误检(将背景检测为物体)率低

缺点

  • 定位不准确
  • 与基于region proposal的方法相比召回率较低

YOLO的bounding box和Faster RCNN的Anchor不一样,并不是预设好的框,只是对一个对象预测出2个bounding box,挑选出预测得相对比较准(与实际框IoU大的那个)
Anchor有点像先给一个基准,然后让网络修正,而YOLO则是直接预测一个框,一开始就是瞎预测,但每次都选一个比较好的(根据与实际Bounding Box的IoU取更好的那个),然后慢慢表现越来越好

参考文献

【1】YOLO(You Only Look Once)算法详解
【2】YOLO v1深入理解
【3】Yolo三部曲解读——Yolov1
【4】YOLO slices
【5】GoogLeNet网络结构学习
【6】死磕YOLO系列,YOLOv1 的大脑、躯干和手脚

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值