YOLO论文系列笔记(中)

1. YOLO v3_SPP

YOLOv3论文地址:YOLOv3:An Incremental Improvement
YOLOv3_SPP u版源码:YOLO V3 SPP-ultralytics
V3论文讲解参考:V3理论讲解
其中mosaic技术以及k-means聚类生成anchor是自己的理解。
u版的代码相比于原始版本的V3有不小的精度提升,可以看出U版的V3还是非常强大的。下面分析其改进之处:
在这里插入图片描述

2. Mosaic图像增强

将多张图像进行拼接再输入网络训练,源码中使用4张image拼接,起到的作用有:增强数据的多样性;增加目标个数;BN能一次性统计多张图片的参数。
输入一张由4张image拼接的大图像等同于并行输入4张(batch_size=4)原始图像,使用Mosaic技术,一个tensor包含4张image的mean和std.

实现代码如下:

def get_random_data_with_Mosaic(self, annotation_line, input_shape, hue=.1, sat=1.5, val=1.5):
        '''random preprocessing for real-time data augmentation'''
        h, w = input_shape
        min_offset_x = 0.3
        min_offset_y = 0.3
        scale_low = 1-min(min_offset_x,min_offset_y)
        scale_high = scale_low+0.2

        image_datas = [] 
        box_datas = []
        index = 0

        place_x = [0,0,int(w*min_offset_x),int(w*min_offset_x)]
        place_y = [0,int(h*min_offset_y),int(h*min_offset_y),0]
        for line in annotation_line:
            # 每一行进行分割
            line_content = line.split()
            # 打开图片
            image = Image.open(line_content[0])
            image = image.convert("RGB") 
            # 图片的大小
            iw, ih = image.size
            # 保存框的位置
            box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]])
            
            # 是否翻转图片
            flip = rand()<.5
            if flip and len(box)>0:
                image = image.transpose(Image.FLIP_LEFT_RIGHT)
                box[:, [0,2]] = iw - box[:, [2,0]]

            # 对输入进来的图片进行缩放
            new_ar = w/h
            scale = rand(scale_low, scale_high)
            if new_ar < 1:
                nh = int(scale*h)
                nw = int(nh*new_ar)
            else:
                nw = int(scale*w)
                nh = int(nw/new_ar)
            image = image.resize((nw,nh), Image.BICUBIC)

            # 进行色域变换
            hue = rand(-hue, hue)
            sat = rand(1, sat) if rand()<.5 else 1/rand(1, sat)
            val = rand(1, val) if rand()<.5 else 1/rand(1, val)
            x = cv2.cvtColor(np.array(image,np.float32)/255, cv2.COLOR_RGB2HSV)
            x[..., 0] += hue*360
            x[..., 0][x[..., 0]>1] -= 1
            x[..., 0][x[..., 0]<0] += 1
            x[..., 1] *= sat
            x[..., 2] *= val
            x[x[:,:, 0]>360, 0] = 360
            x[:, :, 1:][x[:, :, 1:]>1] = 1
            x[x<0] = 0
            image = cv2.cvtColor(x, cv2.COLOR_HSV2RGB) # numpy array, 0 to 1
            
            image = Image.fromarray((image*255).astype(np.uint8))
            # 将图片进行放置,分别对应四张分割图片的位置
            dx = place_x[index]
            dy = place_y[index]
            new_image = Image.new('RGB', (w,h), (128,128,128))
            new_image.paste(image, (dx, dy))
            image_data = np.array(new_image)

            
            index = index + 1
            box_data = []
            # 图像本身翻转,缩放,扭曲以及色域变换之后,对图像中的box进行处理
            if len(box)>0:
                np.random.shuffle(box)
                box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx
                box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy
                box[:, 0:2][box[:, 0:2]<0] = 0
                box[:, 2][box[:, 2]>w] = w
                box[:, 3][box[:, 3]>h] = h
                box_w = box[:, 2] - box[:, 0]
                box_h = box[:, 3] - box[:, 1]
                box = box[np.logical_and(box_w>1, box_h>1)]
                box_data = np.zeros((len(box),5))
                box_data[:len(box)] = box
            
            image_datas.append(image_data)
            box_datas.append(box_data)

        # 将图片分割,放在一起 将4张图像拼成一幅图
        cutx = np.random.randint(int(w*min_offset_x), int(w*(1 - min_offset_x)))
        cuty = np.random.randint(int(h*min_offset_y), int(h*(1 - min_offset_y)))

        new_image = np.zeros([h,w,3])
        new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
        new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
        new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
        new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :]

        # mosaic拼接之后,对框进一步的处理:不仅会将超出图像的框去除,而且会进行边缘处理
        new_boxes = np.array(merge_bboxes(box_datas, cutx, cuty))

        if len(new_boxes) == 0:
            return new_image, []
        if (new_boxes[:,:4]>0).any():
            return new_image, new_boxes
        else:
            return new_image, []

输入的annotation_line中的文件格式如下:
在这里插入图片描述
分为三块:输入图像的路径,bounding box4个参数,bbox对应的类别索引
在进行mosaic之前,先对数据集进行一系列的数据增强操作:
1)resize images:对图像进行缩放和宽高比例的扭曲,传递进去的参数jitter就是控制宽高比例的扭曲程度,注意在缩放图像和宽高比例扭曲之后的图像空白部分用灰度填充,以及边界框进行相应比例的缩放和灰度填充;

2)flip images:水平翻转图像,随机生成一个概率值,当概率值小于0.5时,使用Image库中的FILP_LEFT_RIGHT方法进行水平翻转,注意水平翻转图像之后需要对图像中的边界框进行调整,主要调整边界框的水平方向上的参数即xmin和xmax:边界框的左上角和右下角的x坐标。其实实现也很简单,将边界框中的xmin = w - xmax,xmax = w - xmin;

3)distort images:色域扭曲图像,使用HSV颜色模型,传递进去的参数hue表示色调,sat表示饱和度,val表示明度/亮度。使用opencv中的cvtColor方法实现,cvtColor方法是图像颜色转换函数:第一个输入是需要转换的图像,第二个输入是转换的方法,如:将RGB格式的图像转换成HSV格式的,使用方法cv2.COLOR_RGB2HSV即可。调整到HSV格式之后再进行色域扭曲,最后再使用cv2.COLOR_HSV2RBG转换回原来格式的图像。

4)数据增强操作完成之后,再进行mosaic机制,参数min_offset-x和min_offset_y两个参数来控制4张图像的拼接过程,第一张图像成为拼接之后大图的左上位置,第二张图像成为左下位置,第三张图像在右上位置,第四张图像在右下位置。拼接完成之后,对边界框进行调整:不仅要将超出图像的框去除,而且要进行边缘处理。最后才返回mosaic处理之后的结果。

3. 网络整体框架

在这里插入图片描述
这里是以512x512大小为输入的,经过DarkNet53主干特征提取,经过一个1x1,3x3和1x1的卷机操作得到的feature map;再经过SPP Module,注意这里的SPP模块与SPPNet中的SPP结构是不一样的,思想都是相同的为了融合不同尺度的特征。由于SPP模块中的三个maxpool的步长都为1,需要将输入进行zero-padding,保证经过SPP模块的输入输出的尺寸(H, W)都相同,再将4个分支上的输出进行concat,输出的channel将会变成输入的4倍。

其中这里的convolutional包括:conv2d,BN以及LeakyRelu三个部分;
将SPP Module输出的feature map再经过1x1,3x3,1x1卷积得到branch 1的预测特征图,经过Head部分:3x3卷积步长1输出特征channel为1024,得到16x16x1024的特征图,最后经过1x1的输出channel为(num_classes+1+4)xnum_anchors的卷积得到预测结果,代码中其实还加了一个YOLOLayer层,主要是对预测结果进行调整:如果是训练阶段,直接将预测结果进行shape的处理,如下代码:

 p = p.view(bs, self.na, self.no, self.ny, self.nx).permute(0, 1, 3, 4, 2).contiguous()

view: (batch_size, 75, feat_h, feat_w) -> (batch_size, 3, 25, 16, 16) 将255拆分成3*85,预测3类anchor,每一个anchor会预测85个值(80+5);
permute: (batch_size, 3, 25, 16, 16) -> (batch_size, 3, 16, 16, 25)
shape调整完之后,直接返回p,并且p中包含了该特征图上所有anchors的预测参数,计算后面的loss计算部分。

如果是验证模式,进行如下代码的操作:

 io = p.clone()  # inference output P中存储了针对所有anchors的预测回归参数
            io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid  # xy 根据grid cell上的中心来计算在feature map上的xy坐标
            # 前面三个符号表示bs, anchor, grid_h, grid_w,只取85维度上的前两个值即预测的中心坐标tx和ty,再解码成相对应特征图的绝对坐标bx和by
            io[..., 2:4] = torch.exp(io[..., 2:4]) * self.anchor_wh  # wh yolo method 计算在feature map上的wh
            # 再取得所有预测的tw和th,经过解码得到再预测特征图上的w和h
            io[..., :4] *= self.stride  # 将bbox坐标参数乘上特征图相对于原图的缩放比例,可将bbox在预测特征图上的尺寸映射回原图上
            torch.sigmoid_(io[..., 4:]) # 将confidence以及类别分数都 经过sigmoid
            return io.view(bs, -1, self.no), p  # view [batchsize, 3, feat_h, feat_w, 25] --> [batchsize, 3*feat_h*feat_w, 25]
            # 对于验证模式,最终返回预测器的原始输出 和 预测结果经过encode并且调整tensor shape之后的输出

到这里,branch 1上的预测部分和验证讲解结束。然后将主分支上的特征图进行上采样调整到与backbone中的bottleneck 4中的最后一个特征图尺寸,与其进行特征融合,再经过一系列的卷积操作即Convolutional Set 1,得到branch 2的输入,之后的操作与branch 1中相同;然后branch 3上的输出与branch 2上的操作相同,故不展开细讲。

源码中仅在第一个预测特征层前加上一个SPP模块,通过对比实验,发现如果在3个预测特征层都加上了SPP模块,虽然有0.9个mAP的提升,但是所带来的计算量以及推理速度都会高很多,如下图:
在这里插入图片描述
关于loss计算部分的代码和后处理,网络架构的代码解析留到后面讲解,这里就不展开细说。

4. Loss 部分

4.1. IoU loss

在这里插入图片描述
在这里插入图片描述
如上左图,三组GT与预测的边界框的示例,三组的L2 loss都相同,但实际上GT与预测的边界框的IOU值并不相同,反映了L2 loss并不能很好的反映出GT与预测边界框的拟合程度,
引入IOU loss,如上右图。优点在于:能够很好的反映重合程度;具有尺度不变形。
缺点是:当两个边界框不相交时,此时IOU为0,此时loss为0,没有梯度回传,无法学习;并且仅仅使用IOU loss还是无法精确的反映边界框的重合度。

4.2. Giou(Generalized iou) loss

在这里插入图片描述
Ac为GT与预测边界框的最小外接矩形,U表示两个的并集面积。
当预测边界框与GT完全重合时,即Ac = U,有Giou = IoU = 1;
当预测边界框与GT不相交时,IoU=0,Ac趋向于∞,有(Ac-U)趋向于Ac,那么Giou = -1。
L_Giou = 1 - Giou, 0 ≤ L_Giou ≤ 2
优点:解决了IoU loss为0的问题,存在loss,可进行梯度回传;
缺点:在某些情况下,Giou loss会退化成IoU loss,如下图:
在这里插入图片描述
当预测边界框与GT水平对齐时,有Ac = U,那么Giou的后一项中的分子为0,于是Giou = IoU。

4.3. Diou(Distance iou) loss

在这里插入图片描述
图上的黑色框为anchor,绿色框为GT,蓝色为预测框,其中第一行为使用Giou loss时的收敛程度以及定位精度;而第二行为使用Diou loss时的收敛程度和定位精度,可以看出使用Diou loss得到的收敛效果会更好以及收敛到最佳时用的迭代次数会更少。
Diou的计算公式如下:
在这里插入图片描述
其中,d=b(预测框)与bgt(GT)的两个中心点的欧式距离,c=b与bgt最小外接矩形的主对角线长度。当预测框与GT完美重合时,此时的d = 0,Diou = IoU = 1;
当预测框与GT相距无穷远时,此时的IoU=0,d趋向于c,Diou = -1;
那么,L_Diou loss = 1 - Diou,值域为[0, 2]。
Diou loss直接最小化两个框之间的距离,可以解决Giou收敛速度慢,预测精度的问题。

4.4. Ciou(Complete iou) loss

一个优秀的回归定位loss应该考虑3种几何参数:重叠面积,中心点距离,长宽比:
在这里插入图片描述
L_Ciou = 1 - Ciou,更新参数除了Diou部分,还要考虑v的梯度。BP时,对w求导:
在这里插入图片描述
当长宽为相对坐标时即[0, 1]之间,w^2 + h^2 --> 0,1/(w^2 + h^2) -->∞, 导致对w的偏导数和对h的偏导数值很大,为了防止梯度爆炸问题,将1/(w^2 + h^2) 置为1,虽然改变了两个偏导数的数值,但是梯度的方向并没有改变,可以更快的收敛。
在这里插入图片描述
表格中IoU和GIoU两列表示的意思是:在评价模型时,求AP和AR时使用的阈值是IoU还是Giou;可以看出Lciou的AP和AP75的最高,可以看出不论是以IoU还是GIoU为评价标准时,使用CioU loss相对于IoU,Giou以及Diou loss 都会有明显的提升效果。

5.K-meas聚类生成anchor模板

其实在v2中就已经使用该机制了,但是在上篇博客中没有提及,这次详细讲解一下关于K-means是如何生成anchors模板的。

在Faster-RCNN中,Anchor都是手动设定的,YOLOv2使用k-means聚类算法对训练集中的边界框做了聚类分析,尝试找到合适尺寸的Anchor。另外作者发现如果采用标准的k-means聚类,在box的尺寸比较大的时候其误差也更大,而我们希望的是误差和box的尺寸没有太大关系。所以通过IOU定义了如下的距离函数,使得误差和box的大小无关:
d(box, centroid) = 1 - IOU(box, centroid)
在结果测试中,v2使用5种anchor就可以达到Avg iou为61,而FasterRCNN采用9种acnhor达到的平均IOU也不过为60.9,即论文中提到的仅仅选择5种anchor 就可以达到FasterRCNN中9种anchor的效果。
K-means cluster:基于点与点之间的距离的相似度来计算最佳类别的归属,在使用K-means之前,需要注意:
1)对数据异常值的处理
2)对数据进行标准化处理 x - xmin / xmax - xmin
3)每一个类别的数量要均衡
4)不同类别的特质值应差异较大

实现步骤:
1)因为是聚类anchors,不需要考虑其所属类别,将训练集中所有图像中的GT box全部取出,放在一起,并且只需要获取其中的w和h参数,并不需要中心点坐标,由于训练集中的图像尺寸并不完全相同,因此需要进行w和h的归一化:只需要将四个坐标x和y除上对应图像的w和h,转换成在对应图像的尺寸,获得w = xmax - xmin,h = ymax - ymin;

2)初始化k个anchor box模板,在所有的GT box中随机选取k个anchor box作为初始值,初始化一个last_clu列表,值全为0 size为GT数目的一维tensor;

3)计算每个GT box与每个anchor box的iou值:假设有GT=(wb,hb),anchor=(wa,ha),则intersection = min(wa, wb) * min(ha,hb),union = waha + wbhb - intersection,
则IOU = intersection/union。需要说明的是,计算IoU值时,不用管box的位置,可以假设所有box的中心点都在原点,如下:
在这里插入图片描述

由于IoU值越大表示拟合得越好,定义一个距离参数d = 1- iou来表示误差
输出distance矩阵,其shape为(GT数目,k),取出每一个GT对应聚类中心的最小值,即每个GT输出一个与之最近的聚类中心,返回的是聚类的索引,得到(GT数目,)的tensor near;

5)如果当前聚类得到的tensor near与上次聚类的结果last_clu相同,那么停止循环,否则继续执行3)中的操作。最后返回cluster聚类anchor的结果列表 (num_anchors, 2);

6)除此之外,k-means聚类还提供对聚类结果的评价:遍历所有的GT box,每一个GT与anchor模板进行IoU计算,得到num_anchors个IoU值,将这个num_anchors个iou值取最大值,得到该GT与哪一个cluster最近即该GT box最有可能属于哪一个anchor类,遍历GT_box_num次,最后将这GT_boxa_num个iou值取平均得到这次聚类的精确度。

实现代码如下:

import glob
import random
import xml.etree.ElementTree as ET

import numpy as np

# 计算每个GT与9个聚类中心的iou值,使用的是IoU来计算,可看成是两个相交的矩阵进行IoU计算
def cas_iou(box,cluster):
    x = np.minimum(cluster[:,0],box[0])
    y = np.minimum(cluster[:,1],box[1])

    intersection = x * y    # 计算两个矩阵中对应的w和h的最小值的乘积即为相交的面积 (9,)
    area1 = box[0] * box[1] # 当前GT的面积(w*h) (1,)

    area2 = cluster[:,0] * cluster[:,1] # 当前聚类中心的面积 (9,)
    iou = intersection / (area1 + area2 -intersection) # (9, )

    return iou 

# 下面的函数计算k-means生成anchors模板的精确度:遍历所有的GT,每一个GT与anchor模板进行IoU计算,对9个iou值取最大
# 表示该GT属于某一个anchors类,再求所有的GT与最佳anchor类的IoU的平均值作为聚类的精确度
def avg_iou(box,cluster):
    return np.mean([np.max(cas_iou(box[i],cluster)) for i in range(box.shape[0])]) # 47223个Iou的平均值

def kmeans(box,k):
    # 取出一共有多少GT box 47223
    row = box.shape[0]
    
    # 每个框各个点的位置,初始化一个全为0的size为(47223, 9)的numpyarray数组
    distance = np.empty((row,k))
    
    # 每个GT对应一个聚类位置,初始化全部为0 (47223,)
    last_clu = np.zeros((row,))

    np.random.seed()

    # 从47223个GT中随机选num_anchors个当聚类中心  (9, 2)
    cluster = box[np.random.choice(row,k,replace = False)]
    # cluster = random.sample(row, k)
    while True:
        # 计算每一个GT距离聚类中心的iou情况。由于IoU值越大越好,使用1-IoU来表示误差
        for i in range(row):
            distance[i] = 1 - cas_iou(box[i],cluster) # (47233, 9)
        
        # 取出每个GT对应聚类中心的最小点,即每个GT输出一个与之最近的聚类中心,返回的是聚类的索引 (47233,)
        near = np.argmin(distance,axis=1)
        # 如果当前的每个GT所对应的最近的聚类中心与上次聚类的结果相同,则停止循环,否则继续
        if (last_clu == near).all():
            break
       
        # anchor的更新:上面的步骤已经知道每个GT都有哪些anchor属于他,再对于属于同一个GT box中的anchor,求得这些anchor的宽和高的中位数/均值
        # 下面的near = j 作用:判断当前有多少个GT属于同一类的anchor j,返回一个布尔值,长度为47233,为True的位置就是属于同一类anchor的GT,
        # 最后将这些GT的w和h的中位数作为cluster j/anchor j的w和h值
        for j in range(k):
            cluster[j] = np.median(
                box[near == j],axis=0)

        last_clu = near

    return cluster

def load_data(path):
    data = []
    # 对于每一个xml都寻找box
    for xml_file in glob.glob('{}/*xml'.format(path)):
        tree = ET.parse(xml_file) # 导入xml数据
        height = int(tree.findtext('./size/height')) # 查找节点的元素,获取该张图像的h和w
        width = int(tree.findtext('./size/width'))  
        if height<=0 or width<=0:
            continue
        
        # 对于每一个目标都获得它的宽高,通过iter('子元素名称')来获取该树中名为'object'的子元素迭代器
        # 由于xml中保存的都是GT的绝对坐标,将x和y坐标/对应的w和h得到相对于该图像的相对坐标
        for obj in tree.iter('object'):
            xmin = int(float(obj.findtext('bndbox/xmin'))) / width
            ymin = int(float(obj.findtext('bndbox/ymin'))) / height
            xmax = int(float(obj.findtext('bndbox/xmax'))) / width
            ymax = int(float(obj.findtext('bndbox/ymax'))) / height
            # 转换成float64
            xmin = np.float64(xmin)
            ymin = np.float64(ymin)
            xmax = np.float64(xmax)
            ymax = np.float64(ymax)
            # 得到每个GT的相对宽高值
            data.append([xmax-xmin,ymax-ymin])
    return np.array(data)


if __name__ == '__main__':
    # 运行该程序会计算'./VOCdevkit/VOC2007/Annotations'的xml
    # 会生成yolo_anchors.txt
    SIZE = 608
    anchors_num = 9
    # 载入数据集,可以使用VOC的xml
    path = r'./VOCdevkit/VOC2007/Annotations'
    
    # 载入所有的xml
    # 所有图像中的全部GT的w和h的相对值 (47223, 2)
    data = load_data(path)
    
    # 使用k聚类算法
    out = kmeans(data,anchors_num)  # 得到num_anchors和聚类中心 (9, 2)
    out = out[np.argsort(out[:,0])] # 将这9个anchor模板在w的维度上进行升序排序
    print('acc:{:.2f}%'.format(avg_iou(data,out) * 100)) # 得到这次聚类结果的精确度
    print(out*SIZE) # 将聚类的anchor模板乘上输入图像尺寸,转换成对应输入图像尺寸
    data = out*SIZE
    # 将anchors模板写入txt文件
    f = open("./model_data/yolo_anchors.txt", 'w')
    row = np.shape(data)[0]
    for i in range(row):
        if i == 0:
            x_y = "%d,%d" % (data[i][0], data[i][1])
        else:
            x_y = ", %d,%d" % (data[i][0], data[i][1])
        f.write(x_y)
    f.close()
        # # 选择多个k值进行聚类,查看其准确率
    # for k in range(2, 11):
    #     anchor = kmeans(data, k)
    #     anchor = anchor[np.argsort(anchor[:,0])] 
    #     print("K = {}, AVG ACC = {:.4f}%".format(k, avg_iou(data, anchor)*100))

YOLO V4 :Optimal Speed and Accuracy of Object Detection

1.Abstract

V4的主要目的是设计一个快速检测器,并且优化了并行计算操作,不再使用BFLOP计算指标来衡量模型的好坏。作者指出设计出的检测器应该是很容易训练且使用,每个人都可以使用一个普通的GPU来训练模型以达到实时的高质量的检测结果,如下图:
在这里插入图片描述

YOLO V4可以达到EfficientDet-D3级别的检测效果,但是速度是EfficientDet-D3的两倍,相比于V3,的AP以及FPS分别提升了10%和12%。
V4的主要贡献在于:
1)设计出一个高效的精确的检测模型,任何人都可以使用一张1080Ti或者2080Ti来训练出一个快速且精确的检测器;
2)在训练时候提出了一些’Bag-of-Freebies’,其实就是在不增加计算代价的前提下来提高模型的精度;在验证阶段仅增减少量的推理成本但是会显著提高检测精度,这种插件模块和后处理方法,称之为”Bag of Specials”.
3)改进了一些检测算法,使其在单个GPU上训练时更加有效,如:Cross-iteration batch normalization,CBN;Convolutional block attention module,SAM;Path aggregation network,PAN等等。

2.Bag of freebies

在不增加计算代价的前提下,采用更好的训练方法使得检测器获得更高的精度,称之为训练策略trick或者是训练代价几乎为0—’免费的塑料袋’。通常满足Bag of freebies的方法就是Data Augmentation数据增强,增加模型输入图像的不确定性,得到的检测器对于不同环境中的图像有更好的鲁棒性。举例:光度学失真以及几何变形是两种在数据增强中使用较多的策略,光学失真包括:图像的亮度,对比度,色度,饱和度以及噪声;几何变形包括:图像的随机缩放,裁剪,翻转以及旋转。

上述的数据增强方法都是像素级别的调整,并且保留调整区域中的所有原始像素信息。并且还有像目标遮挡问题的数据增强方法,如随机删除,随机选择一张图像中的矩形区域,用随机值来填充或者zeros-padding。

其他的方法还有致力于解决数据集中的语义分布可能存在偏差的问题,其中最主要的一个问题是:不同类别之间的数据分布不平衡,其实也就是正负样本不均衡的问题。这种问题在two-stage检测算法中一般是用hard negative mining来解决。但是这种方法并不适用于one-stage检测算法中,因为one-stage往往是密集的检测结构,何凯明的RetinaNet中使用Focal loss来绝解决正负样本不均衡问题。另外一个问题是:很难表示出不同类比与ont-hot编码表示的关联程度。解决方法有:将onr-hot编码转换成soft label,为了获得更好的soft label,提出了知识蒸馏的概念来设计标签细化网络。

最后一个bag of freebies是关于bounding box的,传统的检测器通常使用MSE来计算回归参数的偏差,如:{xcenter,ycenter,w,h}或者{xtop_left,ytop_left,xbottom_right,ybottom_right},对于anchor-based的检测算法,一般是预测bounding box的offset来获得最后的边界框。但是仅仅是MSE来计算bounding box的loss会造成预测上的不准确,因此又引入了IoU loss,实现预测框向GT更好的收敛,但是由于IoU loss存在梯度为0的情况即预测框和GT不相交的时候,此时对于训练是不友好的。因此又引入了GIoU loss,Diou loss直到现在的Ciou loss,并且Ciou loss可以实现更快的收敛速度以及回归的精度。

3.Bag of specials

对于那些仅仅是增加少量的推理延迟但是能显著提高检测精度的插件模块和后处理方法,称之为’Bag of specials’特殊包。通常来说,插件模块用于增强模型中的某些属性,如:扩大感受野,引入Attention机制以及提升特征的融合能力等;而后处理是用于筛选模型检测结果的一种方法。

常见的模块有:SPP,ASPP,RFB等。SPP模块是将不同尺度的输入经过池化操作输出同一维度的特征向量,但是SPP模块并不适用于完全卷积的网络。因此在V3中使用了SPP模块,即将输入经过4个不同池化窗口的池化层,分别为{1,5,9,13},步长均为1,将输出的4个相同尺度的特征图在channel维度上进行concat.

在检测算法中使用注意力机制通常分为:channel方向上的attention,对应的方法是SE attention;在point/pixel 级别上的attention,对应的方法是Spatial Attention Module (SAM)。
使用SE注意力机制后,精度会有1%的提升但是在GPU上运行时的推理速度会降低10%;
使用SAM注意力机制后,精度虽然只有0.5%的提升但是并不会影响在GPU上的推理速度。

特征融合早先的实践有skip connection和hyper-column,将低级别高分辨率的特征图融合到高语义信息低分辨率的特征图中。当前的多尺度融合模块有:FPN,PANet,NAS-FPN,BiFPN,ASFF和SEAM等等。SFAM的主要思想是使用SE模块在多尺度连接的特征图上执行channel维度上的再次加权;ASFF使用softmax作为point级别的重新加权,然后将不同尺度的feature map直接相加;BiFPN使用的是带有残差连接的双向加权融合的feature fusion。
激活函数的发展:RELU,RELU6,LeakyRelu,Swish,Hard-Swish以及Mish等。Hard-Swish(x)= x * {Relu6(x+3)/6}, Relu6 = min{6, max{0,x}}, Swish = x * sigmoid(x),
Mish(x) = x * tanh(ln(1+e^x)).

后处理算法中最主要的就是NMS,过滤掉每一个目标中冗余的框,对于每个GT只保留一个最佳的预测框。随后的改进有:greedy NMS,Soft NMS以及DIoU NMS。需要注意的是在进行NMS过程中,并没有使用所捕捉到的visual feature,仅仅是根据scores来排序获取得分最高的一个proposal,因此在anchor-free检测算法中并不需要后处理过程。

4.Methodology

针对GPU训练,使用的backbone为:CSPResNeXt50/CSPDarkNet53,这里的Cross Stage Partial是将输入channel一分为二,一半什么都不做,一半经过ResNeXt50的bottleneck,最后将两部分的特征图在channel维度上进行concat。如下图:
在这里插入图片描述

针对TPU训练,使用的backbone为:EfficientNet/MobileNetV3.

4.1. Selection of Architecture

在输入网络的图像分辨率,卷积层数目,参数量(卷积核大小filterschannel/groups)以及卷积核数目filters之间做均衡,虽然CSPResNeXt50在ImageNet上的分类任务性能比CSPDarknet53要好,但是在COCO上的检测任务CSPDarkNet53要更胜一筹。因此在分类任务上性能较好的模型不一定在检测任务上的性能也好。
选择额外的block来增大感受野以及从不同尺寸的特征图中聚合特征,如:FPN,PAN,ASFF,BiFPN等。一个好的检测器需要满足以下条件:
1)高分辨率的输入图像----有利于网络检测小目标
2)更多的网络层----可以得到更大的感受野来覆盖出输入网络的图像大小
3)更多的参数量----模型有更大的能力来检测一张图中不同尺寸的目标

CSPResNeXt50有16个3x3的卷积层,可获得425x425的感受野以及20.6M的参数量;CSPDarkNet53有29个3x3的卷积层,可获得725x725的感受野以及27.6M的参数量。
在这里插入图片描述

关于不同尺寸的感受野的影响如下:
1)达到目标的大小----可捕捉目标的整个特征信息
2)达到网络的大小(应该是达到输入网路的图像大小)----可捕捉目标特征以及目标的上下文信息
3)超过网络的大小(应该是感受野超过输入网络的图像大小)----增加图像像素点和最终激活函数的连接数
YOLOV4在CSPDarkNet53中增加一个SPP模块,因为不仅可以增大感受野,而且可以分离出最重要的上下文信息并且对网络的运行速度没有影响。使用PANet来从不同尺寸的特征图中进行feature fusion以及得到最后的预测特征图。依旧使用V3中anchor-based head(其实就是V3中的head部分),不再使用Cross-GPU BN或者同步BN等耗资源的操作。

4.2.Selection of BoF and BoS

1.Activation:Relu,Leaky-Relu,parametric Relu,Relu6,SELU,Swish,Mish
2.Bounding box regression loss:MSE,IoU,GIoU,DIoI,CIoU
3.Data augmentation:CutOut,Mixup,CutMix
4.Regularization method:Dropout,DropPath,Spatial Dropout,DropBlock
5.Normalization of the network activations by their mean and variance:BN,Cross GPU BN(CGBN or SyncBN),Cross-iteration BN(CBN
6.Skip-connection:residual connections, Weighted residual connections,Multi-input Weighted residual connections,Cross Stage partial connections(CSP).
关于激活函数,由于PRelu和SELU很难去训练,并且Relu6适用于量化网络,将这三个激活函数从列表中除去;关于正则化方法,使用DropBlock

DropBLock模块有两个参数:block_size,γ。block_size表示dropout的方块的大小,当block_size=1时,DropBlock就退化成普通的Dropout,一般可以取3,5,7;γ表示Dropout过程中的概率。首先需要保证:DropBlock丢弃的元素个数和传统Dropout的元素个数相等,于是传统的Dropout的丢弃元素个数为:
(1-keep_prob)x(feat_hxfeat_w),为了保证DropBlock丢弃的Block不会超过原始图像,需要先设置一个drop的有效区域,即(feat_size-block_szie+1),因此有Dropout = DropBlock,如下:
(1-keep_prob)x(feat_sizexfeat_size)=γx (block_sizexblock_size)x(feat_size-block_size+1)x(feat_size-block_size+1)
得到γ = (1-keep_prob)x(feat_sizexfeat_size) / (block_sizexblock_size)x(feat_size-block_size+1)x(feat_size-block_size+1).
关于归一化方法,由于专注于一个GPU上进行训练,因此不考虑CGBN和SyncBN。

4.3.Additional improvements

为了在单个GPU上更容易训练,做如下的设计:
1)引入Mosaic数据增强技术,Self-Adversarial Training(SAT)
2)选择遗传算法来进行超参数的更新
3)对一些模块进行了改进:modified Spatial Attention Module,modified PANet,以及对CBN的改进:Cross mini-Batch Normalization(CmBN)
Mosaic技术将四张不同的图像进行拼接成一张大的图像,相比于CutMix技术只混合了两张图像,具体如下图:
在这里插入图片描述
关于mosaic技术怎么实现的?在上面的v3部分已经讲解过了,这里不再另外说明。

SAT技术包括两个阶段:a. 第一阶段NN会改变原始图像而不是网络权重,NN将会攻击其本身,改变一张图像中的概念:图像中不存在目标;b. 第二阶段网络以正常的方式来训练这些已经被modified过的图像。

关于BN,CBN以及CmBN的区别:BN在每个mini-batch时刻只会计算当前统计的均值和方差,但是当batch比较小时,BN在batch维度上统计不准确,导致准确率不下降。
在无法扩大batch训练的前提下,通过收集最近k次的迭代信息来更新当前迭代时刻的均值和方差,这样就变相的实现了扩大batch的目的。如下图:
在这里插入图片描述
由于梯度下降机制,模型训练过程中相近的几个iteration对应的模型参数的变化是平滑的,可通过基于泰勒多项式的拟合来补偿网络权重的变化,从而可以准确的估计统计量。
在这里插入图片描述
而CmBN:每n个batch设置为一组,每组的第一个batch只用一个mini-Batch来进行均值和方差的计算,第二个用两个mini-batch来计算,…,第n个用n个mini-batch来计算,与CBN最大的区别在于每次计算均值方差使用的mini-batch数不再是固定的而是逐渐递增的。

Modified SAM:将Spatial-wise Attention修改成point-wise attention;
Modified PAN:将PAN中的残差连接也就是feature fusion中的ADD修改成concat
在这里插入图片描述
原先的SAM是将feature map经过max Pooling和avg pool得到的两个feature map,进行concat之后在通过卷机操作,并使用sigmoid激活函数得到的spatial attention特征与原先的特征图相乘;modified SAM:不在使用池化层而是通过一个卷积层实现,使用sigmoid激活函数对每个数进行缩放到0-1之间,最后与原始特征图进行Element-wise乘。
在这里插入图片描述
上左图是PAN中feature fusion的add,modified PAN进行feature fusion不再将特征图进行直接相加而是将其concat。

4.4.YOLOv4组成

Backbone:CSPDarkNet53。
Neck:SPP,modified PAN。
Head:YOLOv3。
Bag of Freebies for Backbone:CutMix,Mosaic,DropBlock,Class label smoothing。
Bag of Specials for Backbone:Mish,Cross-stage partial connections,Multi-input weighted residual connections(MiWRC)多输入加权残差连接。
Bag of Freebies for Detector:CioU-loss,CmBN,DropBlock,Mosaic,SAT,Eliminate grid sensitivity也就是在bbox的编码解码过程中使用bx/y = sigmoid(tx/y) + cx/y,保证预测框不会出现在图像中的任意位置;Using multiple anchors for a single GT每一个cell上预测多个anchor,Cosine annealing schedule学习率计划,Optimal hyper-parameters优化超参数,Random training shapes类似V3中的多尺度训练。
Bag of Specials for Detector:Mish激活函数(y = x*tanh(ln(1+e^x))), SPP-block,PAN path-aggregation block,Diou-NMS。
DIoU-NMS相比于传统的NMS不仅考虑预测框和GT的IOU,而且考虑两个box的中心点距离,对于scores最高的预测框box M,可以讲Diou-NMS的si更新公式定义为:
在这里插入图片描述
对于每一个目标,先找出与该目标的IoU最大的预测框M,将M从B中删除(B是预测框list),再将B中剩下的预测框与M进行Diou计算,删除Diou大于等于某一个阈值的预测框,重复上面步骤直到所有类别的预测框全部判别结束。与传统NMS的区别在于:DIOU-NMS建议两个中心点较远的box可能处于不同的目标上,不应该将其删除。
DIoU = IOU - 中心点的距离平方/两个box最小外接矩形的对角线长度的平方.

PS:实验部分就不再细讲了,有兴趣的 小伙伴可以去看看原论文,注意这一版本的V4是AlexyAB写的,不是YOLO系列团队。
源码地址:YOLOv4
论文地址:YOLOv4:Optimal Speed and Accuracy of Object Detection
另外,这篇论文非常详细的介绍了BOF以及BOS两个部分出于什么原因做的以及实现的方法/trick有哪些,其中一些tricks可以根据自己的需求使用。
而YOLO系列团队写的是Scaled YOLO v4,分为YOLOv4-tiny,YOLOv4-large以及YOLOv4-CSP三个不同模型复杂度的版本。

后面会讲V4的代码部分。。。。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从现在开始壹并超

你的鼓励,我们就是hxd

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值