YOLO v4论文中提及的五种数据增强:MixUp、Random Erasing、CutOut、CutMix、Mosic, 其中CutMix、Mosic是证实非常有效的,也是在YOLO v4的代码中使用的。
一、图像裁剪类
1.1、MixUp
来源:
https://arxiv.org/pdf/1710.09412.pdf
1.1.1、背景理论
Mixup是MIT和FAIR在ICLR 2018上发表的文章中提到的一种数据增强算法。在介绍mixup之前,我们首先简单了解两个概念:经验风险最小化(Empirical risk minimization,ERM)和邻域风险最小化(Vicinal Risk Minimization,VRM)。
经验风险最小化(ERM):“经验风险最小化”是目前大多数网络优化都遵循的一个原则,即使用已知的经验数据(训练样本)训练得到的学习器的误差或风险,也叫作“经验误差”或“训练误差”。相对的,在新样本(未知样本)上的误差称为“泛化误差”,显然,我们希望学习器的“泛化误差”越小越好。然而,通常我们事先并不知道新样本是什么样的,实际能做的是努力使经验误差越小越好。但是,过分的减小经验误差,通常会在未知样本上产生很差的结果,也就是我们常说的“过拟合”。
为了提高模型泛化性(模型在验证集的表现能力),通常可以通过使用大规模训练数据来提高,但是实际上,获取有标签的大规模数据需要耗费巨大的人工成本,甚至有些情况下根本无法获取数据。解决这个问题的一个有效途径是“邻域风险最小化”,即通过先验知识构造训练样本的邻域值。一般的做法就是传统的数据增强方法,比如加噪、翻转、缩放等,但是这种做法很依赖于特定的数据集和人类的先验知识。
Mixup是一种一般性(不针对特定数据集)的邻域分布方式。
更多理论学习理解: link.
1.1.2、算法介绍
算法非常简单:
其中, (xi,yi)(x_i, y_i )(xi?,yi?) 和 (xj,yj)(x_j, y_j )(xj?,yj?)是从原始训练数据中随机选取的两个样本, λ∈[0,1]\lambda∈ [0, 1]λ∈[0,1] 。 λ\lambdaλ是mixup的超参数,控制两个样本插值的强度,当 λ→0\lambda→ 0λ→0或 λ→1\lambda→ 1λ→1时,则退化到了ERM的情况。
1.1.3、代码
下面的代码是我在论文作者的源码上进行修改的: 源码.
目标检测的MixUp:link.
def mixup(batch_x, batch_y, alpha=1, use_cuda=False):'''Returns mixed inputs, pairs of targets, and lambda'''if alpha > 0:lam = np.random.beta(alpha, alpha)print("mixup")else:lam = 1batch_size = len(batch_x)if use_cuda:index = torch.randperm(batch_size).cuda()else:index = torch.randperm(batch_size)mixed_x = lam * batch_x + (1 - lam) * batch_x[index, :]y_a, y_b = batch_y, batch_y[index]return mixed_x, y_a, y_b, lamdef mixup_criterion(criterion, pred, y_a, y_b, lam):return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)if __name__ == "__main__":
# 1、mixup_datatransform = transforms.Compose([transforms.RandomResizedCrop(300),transforms.ToTensor(),])train_root = "F:\\yolov4\\module\\data\\flowers"trainset = datasets.ImageFolder(root=train_root, transform=transform)trainloader = torch.utils.data.DataLoader(trainset,batch_size=2,shuffle=True)for batch_idx, (inputs, targets) in enumerate(trainloader):img1 = inputs[0].numpy().transpose([1, 2, 0])mixed_x, _, _, _ = mixup(inputs, targets)mixed_img1 = mixed_x[0, :].numpy().transpose([1, 2, 0])mixed_img1 = cv2.cvtColor(mixed_img1, cv2.COLOR_BGR2RGB)mixed_img2 = mixed_x[1, :].numpy().transpose([1, 2, 0])mixed_img2 = cv2.cvtColor(mixed_img2, cv2.COLOR_BGR2RGB)cv2.imshow('img1', mixed_img2)cv2.waitKey(0)
train.py
...
inputs, targets_a, targets_b, lam = mixup_data(inputs, targets, args.alpha, use_cuda)
#映射为Variable
inputs, targets_a, targets_b = map(Variable, (inputs,targets_a,targets_b))
outputs = net(inputs)
loss = mixup_criterion(criterion, outputs, targets_a, targets_b, lam)
train_loss += loss.data[0]
_, predicted = torch.max(outputs.data, 1)
total += targets.size(0)
correct += (lam * predicted.eq(targets_a.data).cpu().sum().float()+ (1 - lam) * predicted.eq(targets_b.data).cpu().sum().float())
...
1.1.4、结果
1.2、Random Erasing
1.2.1、理论
随机擦除(Random Erasing, RE)增强,目的主要是模拟遮挡,从而提高模型泛化能力,这种操作其实非常有意义的模拟现实场景。因为我把物体遮挡一部分后依然能够分类正确,那么肯定会迫使网络利用局部未遮挡的数据进行识别,加大了训练难度,一定程度会提高泛化能力。其也可以被视为add noise的一种,并且与随机裁剪、随机水平翻转具有一定的互补性,综合应用他们,可以取得更好的模型表现,尤其是对噪声和遮挡具有更好的鲁棒性(健壮性,模型克服不利条件的能力)。
具体操作就是: 训练模型时,随机选取一个图片的矩形区域,将这个矩形区域的像素值用随机值或者平均像素值代替,产生局部遮挡的效果。
RE的随机性包含5个方面:
- 对于图像 I,随机擦除与否是以一定概率p进行的;
- 矩形框与图像 I 的面积比例是随机的;
- 矩形框的位置(如左上角坐标或中心坐标)是随机的;
- 矩形框的宽高比是随机的;
- 填充值是随机的,其范围为[ 0 , 255 ]。
1.2.2、代码
下面的代码是我在论文作者的源码上进行修改的: 源码.
import random
import math
import cv2
import numpy as npclass RandomErasing:"""Random erasing the an rectangle region in Image.Class that performs Random Erasing in Random Erasing Data Augmentation by Zhong et al.Args:sl: min erasing area regionsh: max erasing area regionr1: random erasing region的最小 长宽比p: 执行random erasing的可能性"""def __init__(self, p=0.5, sl=0.02, sh=0.4, r1=0.3):self.p = pself.s = (sl, sh) # (0.02, 0.4)self.r = (r1, 1 / r1)def __call__(self, img):"""perform random erasingArgs:img: opencv numpy array in form of [w, h, c] rangefrom [0, 255]Returns:erased img"""assert len(img.shape) == 3, 'image should be a 3 dimension numpy array'if random.random() > self.p:# 1、对于图像 I,随机擦除与否是以一定概率p进行的return imgelse:while True:# random.uniform(*self.s)从(0.02, 0.4)随机生成一个实数# 2、矩形框与图像 I 的面积比例是随机的Se = random.uniform(*self.s) * img.shape[0] * img.shape[1]# 3、矩形框的宽高比是随机的re = random.uniform(*self.r)He = int(round(math.sqrt(Se * re))) # 根据随机生成的面积和宽高比生成生成高宽We = int(round(math.sqrt(Se / re)))# 4、random erasing region的位置(左上角坐标)是随机的xe = random.randint(0, img.shape[1])ye = random.randint(0, img.shape[0])if xe + We <= img.shape[1] and ye + He <= img.shape[0]:# 填充值是随机的,其范围为[ 0 , 255 ]img[ye: ye + He, xe: xe + We, :] = np.random.randint(low=0, high=255, size=(He, We, img.shape[2]))return imgif __name__ == "__main__":img = cv2.imread("test.jpeg")RE = RandomErasing(p=0.7)for i in range(20):img1 = RE(img.copy())cv2.imshow("test", img1)cv2.waitKey(1000)
1.2.3、结果
二、图像混叠类
图像混叠主要对 Batch 后的数据进行混合。是对两幅图像进行融合,生成一幅图像。
2.1、CutOut
2.1.1、理论
Cutout是一种新的正则化方法。Cutout的出发点和随机擦除一样,也是模拟遮挡,目的是提高泛化能力和鲁棒性。
实现:随机选择一个固定大小的正方形区域,然后采用全0填充就OK了,当然为了避免填充0值对训练的影响,应该要对数据进行中心归一化操作,norm到0。
注意:
- 作者发现cutout区域的大小比形状重要,所以cutout只要是正方形就行,非常简单。
- 擦除矩形区域存在一定概率不完全在原图像中的(文中设置为50%)
Cutout为什么能有意义:
直接引用论文中的原话就是This technique encourages the network to better utilize the full context of the image, rather than relying on the presence of a small set of specific visual features.简单点说就是,CutOut能够让CNN利用整幅图像的全局信息,而不是一些小特征组成的局部信息。其实这种思想和大部分细粒度论文的思想是类似的。
2.1.2、代码
原论文代码:
https://github.com/uoguelph-mlrg/Cutout
在源码基础上自己实现:
class Cutout(object):"""Randomly mask out one or more patches from an image.Args:n_holes (int): Number of patches to cut out of each image.length (int): The length (in pixels) of each square patch."""def __init__(self, n_holes, length, fill_value):self.n_holes = n_holesself.length = lengthself.fill_value= fill_valuedef __call__(self, img):"""Args:img (Tensor): Tensor image of size (C, H, W).Returns:Tensor: Image with n_holes of dimension length x length cut out of it."""h = img.size(1)w = img.size(2)mask = np.ones((h, w), np.float32)for n in range(self.n_holes):y = np.random.randint(h)x = np.random.randint(w)y1 = np.clip(y - self.length // 2, 0, h)y2 = np.clip(y + self.length // 2, 0, h)x1 = np.clip(x - self.length // 2, 0, w)x2 = np.clip(x + self.length // 2, 0, w)mask[y1: y2, x1: x2] = self.fill_valuemask = torch.from_numpy(mask)mask = mask.expand_as(img)img = img * maskreturn imgif __name__ == "__main__":# # 2、Cutoutimg = cv2.imread("test.jpeg")transform = Compose([transforms.ToTensor(),Cutout(n_holes=30, length=10, fill_value=0.)])img2 = transform(img=img)img2 = img2.numpy().transpose([1, 2, 0])cv2.imshow("test", img2)cv2.waitKey(0)
2.1.3、结果
2.2、CutMix
YOLO v4论文中使用的就是CutMix
2.2.1、理论
论文地址
https://arxiv.org/abs/1905.04899v2
CutMix的处理方式也比较简单,同样也是对一对图片做操作,简单讲就是随机生成一个裁剪框Box,裁剪掉A图的相应位置,然后用B图片相应位置的ROI放到A图中被裁剪的区域形成新的样本,计算损失时同样采用加权求和的方式进行求解。
2.2.2、代码
论文源码: link.
根据源码写的自己的实现如下:
def cutmix_criterion(criterion, pred, y_a, y_b, lam):return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)def rand_bbox(size, lam):"""找到裁剪的区域位置(左上角和右下角)"""W = size[2] # 图片的宽度H = size[3] # 图片的高度cut_rat = np.sqrt(1. - lam)cut_w = np.int(W * cut_rat) # 要裁剪的图片宽度cut_h = np.int(H * cut_rat) # 要裁剪的图片高度# uniformcx = np.random.randint(W) # 随机裁剪位置cy = np.random.randint(H)# 限制B坐标区域不超过样本大小bbx1 = np.clip(cx - cut_w // 2, 0, W) # 左上角xbby1 = np.clip(cy - cut_h // 2, 0, H) # 左上角ybbx2 = np.clip(cx + cut_w // 2, 0, W) # 右下角xbby2 = np.clip(cy + cut_h // 2, 0, H) # 右下角yreturn bbx1, bby1, bbx2, bby2 # 要裁剪区域(左上角和右下角)def cutmix(batch_x, y, alpha=1.0, use_cuda=False):'''Returns mixed inputs, pairs of targets, and lambdaargs:batch_x: [batch_size, channel, w, h]'''assert alpha > 0lam = np.random.beta(alpha, alpha) # 设定lamda的值,服从beta分布batch_size = batch_x.size()[0]if use_cuda:index = torch.randperm(batch_size).cuda()else:index = torch.randperm(batch_size)y_a, y_b = y, y[index]bbx1, bby1, bbx2, bby2 = rand_bbox(batch_x.size(), lam)batch_x[:, :, bbx1:bbx2, bby1:bby2] = batch_x[index, :, bbx1:bbx2, bby1:bby2]# adjust lambda to exactly match pixel ratio 随机lamda值lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (batch_x.size()[-1] * batch_x.size()[-2]))return batch_x, y_a, y_b, lamif __name__ == "__main__":# 4、Cutmixtransform = transforms.Compose([transforms.RandomResizedCrop((400, 400)),transforms.ToTensor(),])train_root = "F:\\yolov4\\module\\data\\flowers"trainset = datasets.ImageFolder(root=train_root, transform=transform)trainloader = torch.utils.data.DataLoader(trainset,batch_size=2,shuffle=True)for batch_idx, (inputs, targets) in enumerate(trainloader):img1 = inputs[0].numpy().transpose([1, 2, 0])mixed_x, _, _, _ = cutmix(inputs, targets)for i in range(2):img = cv2.cvtColor(mixed_x[i, :].numpy().transpose([1, 2, 0]), cv2.COLOR_BGR2RGB)cv2.imshow('img', img)cv2.waitKey(0)
train.py
...
for (inputs, targets) in tqdm(trainloader):inputs, targets = inputs.to(device), targets.to(device)r = np.random.rand(1)if r < 0.5: # 做cutmix的概率为0.5inputs, targets_a, targets_b, lam = cutmix_data(inputs, targets)inputs, targets_a, targets_b = map(Variable, (inputs, targets_a, targets_b))outputs = net(inputs)loss = cutmix_criterion(criterion, outputs, targets_a.long(), targets_b.long(), lam)else:outputs = net(inputs)loss = criterion(outputs, targets.long())
...
2.2.3、结果
这里图片的大小变了是因为transform里做了resize操作。
2.3、Mosic
YOLO v4论文中使用的就是Mosic
感兴趣请看我的另一篇博文:Mosic.
Reference
- MixUp1 : link.
- MixUp2:https://arxiv.org/pdf/1710.09412.pdf
- Random Erasing1: link.
- Random Erasing2:https://blog.csdn.net/weixin_41560402/article/details/106029459
- Cutout1: link.
- Cutout2: link.
- CutMix1: link.
- CutMix2: link.