YOLOv4的Tricks解讀一 --- 多圖融合的數據增強(MixUp/CutMix/Mosaic)


YOLOv4 = CSPDarknet53 + SPP + PAN + YOLOv3
YOLOv4採用的trick可以分爲以下幾類:

  • 用於骨幹網的 Bag of Freebies(BoF):CutMixMosaic數據增強,DropBlock正則化,Label Smooth
  • 用於骨幹網的 Bag of Specials(BoS):Mish,跨階段部分連接(CSP),多輸入加權剩餘連接(MiWRC
  • 用於檢測器的 Bag of Specials(BoS):MishSPP塊,SAM塊,PAN路徑聚集塊,DIoU-NMS

本文就YOLOv4中涉及和採用的部分數據增強tricks進行總結和學習。

MixUp

論文:https://arxiv.org/pdf/1710.09412.pdf
代碼(官方):https://github.com/hongyi-zhang/mixup
復現版本:https://github.com/tengshaofeng/ResidualAttentionNetwork-pytorch
在這裏插入圖片描述
在這裏插入圖片描述
mixup主要是用於圖像分類,從訓練樣本中隨機抽取兩個樣本進行簡單的隨機加權求和,同時樣本的標籤也對應加權求和,然後預測結果與加權求和之後的標籤求損失,在反向求導更新參數,公式定義如下:
在這裏插入圖片描述
由公式可以看到,加權融合同時作用在圖片和label兩個維度。
Pytorch代碼如下:

def mixup_data(x, y, alpha=1.0, use_cuda=True):

    '''Compute the mixup data. Return mixed inputs, pairs of targets, and lambda'''
    if alpha > 0.:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1.
    batch_size = x.size()[0]
    if use_cuda:
        index = torch.randperm(batch_size).cuda()
    else:
        index = torch.randperm(batch_size)

    mixed_x = lam * x + (1 - lam) * x[index,:]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

由代碼看出,mixup_data並不是同時取出兩個batch,而是取一個batch,並將該batch中的樣本ID順序打亂(shuffle),然後再進行加權求和。
整體的MixUp算法流程如下:
在這裏插入圖片描述

  • 對於輸入的一個batch的待測圖片images,我們將其和隨機抽取的圖片進行融合,融合比例爲lam,得到混合張量inputs;
  • 第1步中圖片融合的比例lam是[0,1]之間的隨機實數,符合beta分佈,相加時兩張圖對應的每個像素值直接相加,即 inputs = lam*images + (1-lam)*images_random;將1中得到的混合張量inputs傳遞給model得到輸出張量outpus,
  • 隨後計算損失函數時,我們針對兩個圖片的標籤分別計算損失函數,然後按照比例lam進行損失函數的加權求和,即loss = lam * criterion(outputs, targets_a) + (1 - lam) * criterion(outputs, targets_b);
  • 反向求導更新參數。

Pytorch代碼如下:

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=base_learning_rate, momentum=0.9, weight_decay=args.decay)
""" 訓練 """
def train(epoch):
    print('\nEpoch: %d' % epoch)
    net.train()
    train_loss = 0
    correct = 0
    total = 0
    for batch_idx, (inputs, targets) in enumerate(trainloader):
        if use_cuda:
            inputs, targets = inputs.cuda(), targets.cuda()
        """ generate mixed inputs, two one-hot label vectors and mixing coefficient """
        inputs, targets_a, targets_b, lam = mixup_data(inputs, targets, args.alpha, use_cuda)       
        inputs, targets_a, targets_b = Variable(inputs), Variable(targets_a), Variable(targets_b)
        outputs = net(inputs)
		""" 計算loss """
        loss_func = mixup_criterion(targets_a, targets_b, lam)
        loss = loss_func(criterion, outputs)
        """ 更新梯度 """
		optimizer.zero_grad()
        loss.backward()
        optimizer.step()

其中mixup_criterion定義如下,損失函數是輸出的預測值對這兩組標籤分別求損失.

def mixup_criterion(y_a, y_b, lam):
    return lambda criterion, pred: lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)

目標檢測實現見
mixup for detection

CutMix

論文:https://arxiv.org/abs/1905.04899v2
代碼:https://github.com/clovaai/CutMix-PyTorch

CutMix的處理方式也比較簡單,同樣也是對一對圖片做操作,簡單講就是隨機生成一個裁剪框Box,裁剪掉A圖的相應位置,然後用B圖片相應位置的ROI放到A圖中被裁剪的區域形成新的樣本,計算損失時同樣採用加權求和的方式進行求解。
在這裏插入圖片描述
兩張圖合併操作定義如下:
在這裏插入圖片描述
其中,M表示二進制0,1矩陣,表示從兩個圖像中刪除並填充的位置,實際就是用來標記需要裁剪的區域和保留的區域,裁剪的區域值均爲0,其餘位置爲1。1是所有元素都是1的矩陣,維度大小與M相同。圖像A和B組合得到新樣本,最後兩個圖的標籤也對應求加權和。權值同mixup一樣是採用bata分佈隨機得到,alpha的值爲論文中取值爲1,這樣加權係數就服從beta分佈,請注意,主要區別在於CutMix用另一個訓練圖像中的補丁替換了圖像區域,並且比Mixup生成了更多的本地自然圖像。

爲了對二進制掩碼M進行採樣,首先要對剪裁區域的邊界框B= (r_x, r_y, r_w, r_h}進行採樣,用來對樣本x_A和x_B做裁剪區域的指示標定。

裁剪區域的邊界框採樣公式如下:
在這裏插入圖片描述
W,H是二進制掩碼矩陣M的寬高大小,且剪裁區域的比例滿足:
在這裏插入圖片描述
確定好裁剪區域B之後,將二進制掩碼M中的裁剪區域B置0,其他區域置1,這樣就就完成了掩碼M的採樣,然後將M點乘A將樣本A中的剪裁區域B移除,(1-M)點乘B將樣本B中的剪裁區域B進行裁剪填充到樣本A,形成一個全新樣本。

  • 生成剪裁區域
"""輸入爲:樣本的size和生成的隨機lamda值"""
def rand_bbox(size, lam):
    W = size[2]
    H = size[3]
    """論文裏的公式2,求出B的rw,rh"""
    cut_rat = np.sqrt(1. - lam)
    cut_w = np.int(W * cut_rat)
    cut_h = np.int(H * cut_rat)

    # uniform
    """論文裏的公式2,求出B的rx,ry(bbox的中心點)"""
    cx = np.random.randint(W)
    cy = np.random.randint(H)
	
	# np.clip限制大小
	"""限制B座標區域不超過樣本大小"""
    bbx1 = np.clip(cx - cut_w // 2, 0, W)
    bby1 = np.clip(cy - cut_h // 2, 0, H)
    bbx2 = np.clip(cx + cut_w // 2, 0, W)
    bby2 = np.clip(cy + cut_h // 2, 0, H)

    return bbx1, bby1, bbx2, bby2
  • 應用CutMix大概流程
for i, (input, target) in enumerate(train_loader):
        # measure data loading time
        data_time.update(time.time() - end)

        input = input.cuda()
        target = target.cuda()

        r = np.random.rand(1)
        if args.beta > 0 and r < args.cutmix_prob:
            # generate mixed sample
            """設定lamda的值,服從beta分佈"""
            lam = np.random.beta(args.beta, args.beta)
            rand_index = torch.randperm(input.size()[0]).cuda()
            """獲取batch裏面的兩個隨機樣本 """
            target_a = target
            target_b = target[rand_index]
            """獲取裁剪區域bbox座標位置 """
            bbx1, bby1, bbx2, bby2 = rand_bbox(input.size(), lam)
            """將原有的樣本A中的B區域,替換成樣本B中的B區域"""
            input[:, :, bbx1:bbx2, bby1:bby2] = input[rand_index, :, bbx1:bbx2, bby1:bby2]
            # adjust lambda to exactly match pixel ratio
            """根據剪裁區域座標框的值調整lam的值 """ 
            lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (input.size()[-1] * input.size()[-2]))
            # compute output
            """計算模型輸出 """
            output = model(input)
            """計算損失 """
            loss = criterion(output, target_a) * lam + criterion(output, target_b) * (1. - lam)
        else:
            # compute output
            output = model(input)
            loss = criterion(output, target)

Mosaic

在這裏插入圖片描述
Mosaic可以說是YOLOv4中的一個亮點,但是Mosaic並不是YOLOv4提出的(我不是槓精),在u版舊版本的yolo3(現已更新)中就已經有mosaic的實現。

Mosaic混合了4個訓練圖像, 因此,混合了4個不同的上下文,而CutMix僅混合了2個輸入圖像,這就是Mosaic更強的原因。
可以理解爲Mosaic混合更多圖像創造了更多的可能性,見多識廣。

順便一提的是YOLOv4後的Stitcher小目標檢測方法與Mosaic有點類似,也是拼接了四個圖像,用以提升小目標檢測。參考我的博文:Stitcher學習筆記:提升小目標檢測 — 簡單而有效

直接上Mosaic的代碼,摘自https://github.com/ultralytics/yolov3/blob/master/utils/datasets.py

def load_mosaic(self, index):
    # loads images in a mosaic

    labels4 = []
    s = self.img_size
    xc, yc = [int(random.uniform(s * 0.5, s * 1.5)) for _ in range(2)]  # mosaic center x, y
    indices = [index] + [random.randint(0, len(self.labels) - 1) for _ in range(3)]  # 3 additional image indices
    for i, index in enumerate(indices):
        # Load image
        img, _, (h, w) = load_image(self, index)

        # place img in img4
        if i == 0:  # top left
            img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8)  # base image with 4 tiles
            x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc  # xmin, ymin, xmax, ymax (large image)
            x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h  # xmin, ymin, xmax, ymax (small image)
        elif i == 1:  # top right
            x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc
            x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h
        elif i == 2:  # bottom left
            x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)
            x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, max(xc, w), min(y2a - y1a, h)
        elif i == 3:  # bottom right
            x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)
            x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)

        img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b]  # img4[ymin:ymax, xmin:xmax]
        padw = x1a - x1b
        padh = y1a - y1b

        # Labels
        x = self.labels[index]
        labels = x.copy()
        if x.size > 0:  # Normalized xywh to pixel xyxy format
            labels[:, 1] = w * (x[:, 1] - x[:, 3] / 2) + padw
            labels[:, 2] = h * (x[:, 2] - x[:, 4] / 2) + padh
            labels[:, 3] = w * (x[:, 1] + x[:, 3] / 2) + padw
            labels[:, 4] = h * (x[:, 2] + x[:, 4] / 2) + padh
        labels4.append(labels)

    # Concat/clip labels
    if len(labels4):
        labels4 = np.concatenate(labels4, 0)
        # np.clip(labels4[:, 1:] - s / 2, 0, s, out=labels4[:, 1:])  # use with center crop
        np.clip(labels4[:, 1:], 0, 2 * s, out=labels4[:, 1:])  # use with random_affine

    # Augment
    # img4 = img4[s // 2: int(s * 1.5), s // 2:int(s * 1.5)]  # center crop (WARNING, requires box pruning)
    img4, labels4 = random_affine(img4, labels4,
                                  degrees=self.hyp['degrees'],
                                  translate=self.hyp['translate'],
                                  scale=self.hyp['scale'],
                                  shear=self.hyp['shear'],
                                  border=-s // 2)  # border to remove

    return img4, labels4

參考:

https://blog.csdn.net/ouyangfushu/article/details/105575258
https://blog.csdn.net/weixin_38715903/article/details/103999227
https://zhuanlan.zhihu.com/p/138855612
https://www.zhihu.com/question/308572298

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章