为何要进行数据增强呢?

在深度学习中,一般要求样本的数量要充足,样本数量越多,训练出来的模型效果越好,模型的泛化能力越强。但是实际中,样本数量不足或者样本质量不够好,这就要对样本做数据增强,来提高样本质量。
关于数据增强的作用总结如下:
1,增加训练的数据量,提高模型的泛化能力
2,增加噪声数据,提升模型的鲁棒性

数据增强的方法(我们以图像数据为例):
1,数据翻转:数据翻转是一种常用的数据增强方法,这种方法不同于旋转 180 °。这种方法是做一种类似于镜面的翻折。
2,数据旋转:旋转就是顺时针或者逆时针的旋转,注意在旋转的时候, 最好旋转 90 - 180 度否则会出现尺度的问题
3,图像缩放:图像可以被放大或缩小。放大时,放大后的图像尺寸会大于原始尺寸。大多数图像处理架构会按照原始尺寸对放大后的图像进行裁切而图像缩小会减小图像尺寸,这使我们不得不对图像边界之外的东西做出假设。
4,图像剪裁:这种方法更流行的叫法是随机裁剪,我们随机从图像中选择一部分,然后降这部分图像裁剪出来,然后调整为原图像的大小
5,图像平移:平移是将图像沿着 x 或者 y 方向 (或者两个方向) 移动。我们在平移的时候需对背景进行假设,比如说假设为黑色等等,因为平移的时候有一部分图像是空的,由于图片中的物体可能出现在任意的位置,所以说平移增强方法十分有用。
6,添加噪声:过拟合通常发生在神经网络学习高频特征的时候 (因为低频特征神经网络很容易就可以学到,而高频特征只有在最后的时候才可以学到) 而这些特征对于神经网络所做的任务可能没有帮助,而且会对低频特征产生影响,为了消除高频特征我们随机加入噪声数据来消除这些特征。

什么是正则化?

链接:

L1和L2正则化

正则化(Regularization)是机器学习中一种常用的技术,其主要目的是控制模型复杂度,减小过拟合。最基本的正则化方法是在原目标(代价)函数 中添加惩罚项,对复杂度高的模型进行“惩罚”。
https://zhuanlan.zhihu.com/p/29360425

dropout

Dropout可以作为训练深度神经网络的一种trick供选择。在每个训练批次中,通过忽略一半的特征检测器(让一半的隐层节点值为0),可以明显地减少过拟合现象
https://zhuanlan.zhihu.com/p/38200980

本文主要介绍四种数据增广的方法:mixup,mosaic,cutmix,cutout

Cutout

论文地址:https://arxiv.org/abs/1708.04552

开源代码github地址:https://github.com/uoguelph-mlrg/Cutout

Cutout 是直接对输入的图像进行遮挡。
作者在论文中也进行了说明,这样做法有以下两点优势:
(1) 通过 Cutout 可以模拟真实场景中主体被部分遮挡时的分类场景;
(2) 可以促进模型充分利用图像中更多的内容来进行分类,防止网络只关注显著性的图像区域,从而发生过拟合。

l链接:

小样本数据深度学习 小样本数据增广_数据

Mixup

论文地址:https://arxiv.org/pdf/1710.09412.pdf

开源代码github地址:https://github.com/facebookresearch/mixup-cifar10

Mixup 是最先提出的图像混叠增广方案,其原理简单、方便实现,不仅在图像分类上,在目标检测上也取得了不错的效果。
为了便于实现,通常只对一个 batch 内的数据进行混叠,在 Cutmix 中也是如此。

论文的贡献在于提出了一个一般性的领域分布,称作 mixup:
μ(x˜,y˜|xi,yi)=1n∑jnEλ[δ(x˜=λ⋅xi+(1−λ)⋅xj,y˜=λ⋅yi+(1−λ)⋅yj)],μ(x,y|xi,yi)=1n∑jnEλ[δ(x=λ⋅xi+(1−λ)⋅xj,y=λ⋅yi+(1−λ)⋅yj)],

其中, λ∼Beta(α,α)λ∼Beta(α,α), α∈(0,∞)α∈(0,∞)。
x˜=λxi+(1−λ)xjx~=λxi+(1−λ)xj

y˜=λyi+(1−λ)yjy~=λyi+(1−λ)yj

论文解析:


小样本数据深度学习 小样本数据增广_小样本数据深度学习_02

Cutmix

论文地址:https://arxiv.org/pdf/1905.04899v2.pdf

开源代码github地址:https://github.com/clovaai/CutMix-PyTorch

与 Mixup 直接对两幅图进行相加不一样,Cutmix 是从一幅图中随机裁剪出一个 ROI,然后覆盖当前图像中对应的区域

和是两个不同的训练样本,和是对应的标签值,CutMix需要生成的是新的训练样本和对应标签:和,公式如下:

是为了dropd掉部分区域和进行填充的二进制掩码,是逐像素相乘,是所有元素都为1 的二进制掩码,与Mixup一样属于Beta分布:,令则服从(0,1)的均匀分布。

为了对二进制掩进行采样,首先要对剪裁区域的边界框进行采样,用来对样本和做裁剪区域的指示标定。在论文中对矩形掩码进行采样(长宽与样本大小成比例)。

剪裁区域的边界框采样公式如下:

小样本数据深度学习 小样本数据增广_小样本数据深度学习_03

链接:

小样本数据深度学习 小样本数据增广_计算机视觉_04

几种传统数据增强的区别:Mixup,Cutout,CutMix

  • Mixup:将随机的两张样本按比例混合,分类的结果按比例分配;
  • Cutout:随机的将样本中的部分区域cut掉,并且填充0像素值,分类的结果不变;
  • CutMix:将一部分区域cut掉但不填充0像素,而是随机填充训练集中的其他数据的区域像素值,分类结果按一定的比例分配

上述三种数据增强的区别:

  • cutout和cutmix就是填充区域像素值的区别;
  • mixup和cutmix是混合两种样本方式上的区别:
  • mixup是将两张图按比例进行插值来混合样本,
  • cutmix是采用cut部分区域再补丁的形式去混合图像,不会有图像混合后不自然的情形

而YOLOv4的mosaic 数据增强是参考CutMix数据增强,理论上类似,但是mosaic利用了四张图片,据论文说法,其优点是丰富了检测物体的背景,且在BN计算的时候一下子会计算四张图片的数据,使得mini-batch大小不需要很大,那么一个GPU就可以达到比较好的效果。

Mosaic

论文:yolov4论文中介绍了

代码:https://github.com/klauspa/Yolov4-tensorflow/blob/master/data.py tf复现的版本
https://github.com/AlexeyAB/darknet/blob/master/src/data.c YOLOv4官方的 C++ 版本

在Yolo-V4的paper中,以及在还未发表paper的Yolo-V5中,都有一个很重要的技巧,就是Mosaic数据增强,这种数据增强方式简单来说就是把4张图片,通过随机缩放、随机裁减、随机排布的方式进行拼接。根据论文的说法,优点是丰富了检测物体的背景和小目标,并且在计算Batch Normalization的时候一次会计算四张图片的数据,使得mini-batch大小不需要很大,一个GPU就可以达到比较好的效果。

优点:

  • 丰富数据集:随机使用4张图片,随机缩放,再随机分布进行拼接,大大丰富了检测数据集,特别是随机缩放增加了很多小目标,让网络的鲁棒性更好
  • 减少GPU:直接计算4张图片的数据,使得Mini-batch大小并不需要很大,一个GPU就可以达到比较好的效果

缺点:

  • 如果我们的数据集本身就有很多的小目标,那么Mosaic数据增强会导致本来较小的目标变得更小,导致模型的泛化能力变差

小样本数据深度学习 小样本数据增广_Image_05

小样本数据深度学习 小样本数据增广_小样本数据深度学习_06

链接:

```python
from PIL import Image, ImageDraw
import numpy as np
from matplotlib.colors import rgb_to_hsv, hsv_to_rgb
import math


def rand(a=0, b=1):
    return np.random.rand() * (b - a) + a


def merge_bboxes(bboxes, cutx, cuty):
    merge_bbox = []
    for i in range(len(bboxes)):
        for box in bboxes[i]:
            tmp_box = []
            x1, y1, x2, y2 = box[0], box[1], box[2], box[3]

            if i == 0:
                if y1 > cuty or x1 > cutx:
                    continue
                if y2 >= cuty and y1 <= cuty:
                    y2 = cuty
                    if y2 - y1 < 5:
                        continue
                if x2 >= cutx and x1 <= cutx:
                    x2 = cutx
                    if x2 - x1 < 5:
                        continue
            if i == 1:
                if y2 < cuty or x1 > cutx:
                    continue
                if y2 >= cuty and y1 <= cuty:
                    y1 = cuty
                    if y2 - y1 < 5:
                        continue
                if x2 >= cutx and x1 <= cutx:
                    x2 = cutx
                    if x2 - x1 < 5:
                        continue
            if i == 2:
                if y2 < cuty or x2 < cutx:
                    continue
                if y2 >= cuty and y1 <= cuty:
                    y1 = cuty
                    if y2 - y1 < 5:
                        continue
                if x2 >= cutx and x1 <= cutx:
                    x1 = cutx
                    if x2 - x1 < 5:
                        continue
            if i == 3:
                if y1 > cuty or x2 < cutx:
                    continue
                if y2 >= cuty and y1 <= cuty:
                    y2 = cuty
                    if y2 - y1 < 5:
                        continue
                if x2 >= cutx and x1 <= cutx:
                    x1 = cutx
                    if x2 - x1 < 5:
                        continue
            tmp_box.append(x1)
            tmp_box.append(y1)
            tmp_box.append(x2)
            tmp_box.append(y2)
            tmp_box.append(box[-1])
            merge_bbox.append(tmp_box)
    return merge_bbox


def get_random_data(annotation_line, input_shape, random=True, hue=.1, sat=1.5, val=1.5, proc_img=True):
    '''random preprocessing for real-time data augmentation'''
h, w = input_shape
    min_offset_x = 0.4
    min_offset_y = 0.4
    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(w * 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:]])

        # image.save(str(index)+".jpg")
        # 是否翻转图片
        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 = rgb_to_hsv(np.array(image) / 255.)
        x[..., 0] += hue
        x[..., 0][x[..., 0] > 1] -= 1
        x[..., 0][x[..., 0] < 0] += 1
        x[..., 1] *= sat
        x[..., 2] *= val
        x[x > 1] = 1
        x[x < 0] = 0
        image = hsv_to_rgb(x)

        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) / 255
        # Image.fromarray((image_data*255).astype(np.uint8)).save(str(index)+"distort.jpg")
        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)

        img = Image.fromarray((image_data * 255).astype(np.uint8))
        for j in range(len(box_data)):
            thickness = 3
            left, top, right, bottom = box_data[j][0:4]
            draw = ImageDraw.Draw(img)
            for i in range(thickness):
                draw.rectangle([left + i, top + i, right - i, bottom - i], outline=(255, 255, 255))
        img.show()

    # 将图片分割,放在一起
    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:, :]

    # 对框进行进一步的处理
    new_boxes = merge_bboxes(box_datas, cutx, cuty)

    return new_image, new_boxes


def normal_(annotation_line, input_shape):
    '''random preprocessing for real-time data augmentation'''
line = annotation_line.split()
    image = Image.open(line[0])
    box = np.array([np.array(list(map(int, box.split(',')))) for box in line[1:]])

    iw, ih = image.size
    image = image.transpose(Image.FLIP_LEFT_RIGHT)
    box[:, [0, 2]] = iw - box[:, [2, 0]]

    return image, box


if __name__ == "__main__":
    with open("2007_train.txt") as f:
        lines = f.readlines()
    a = np.random.randint(0, len(lines))
    # index = 0
    # line_all = lines[a:a+4]
    # for line in line_all:
    #     image_data, box_data = normal_(line,[416,416])
    #     img = image_data
    #     for j in range(len(box_data)):
    #         thickness = 3
    #         left, top, right, bottom  = box_data[j][0:4]
    #         draw = ImageDraw.Draw(img)
    #         for i in range(thickness):
    #             draw.rectangle([left + i, top + i, right - i, bottom - i],outline=(255,255,255))
    #     img.show()
    #     # img.save(str(index)+"box.jpg")
    #     index = index+1
    # 传入四张图片
    # line = lines[a:a + 4]
    line = lines[0:4]
    image_data, box_data = get_random_data(line, [416, 416])
    img = Image.fromarray((image_data * 255).astype(np.uint8))
    for j in range(len(box_data)):
        thickness = 3
        left, top, right, bottom = box_data[j][0:4]
        draw = ImageDraw.Draw(img)
        for i in range(thickness):
            draw.rectangle([left + i, top + i, right - i, bottom - i], outline=(255, 255, 255))
    img.show()
    # img.save("box_all.jpg")