睿智的目標檢測34——Keras 搭建YoloV4-Tiny目標檢測平臺

學習前言

Tiny系列一直沒去關注,做一下試試看。
在這裏插入圖片描述

什麼是YOLOV4-Tiny

在這裏插入圖片描述
YOLOV4是YOLOV3的改進版,在YOLOV3的基礎上結合了非常多的小Tricks。
儘管沒有目標檢測上革命性的改變,但是YOLOV4依然很好的結合了速度與精度。
根據上圖也可以看出來,YOLOV4在YOLOV3的基礎上,在FPS不下降的情況下,mAP達到了44,提高非常明顯。

YOLOV4整體上的檢測思路和YOLOV3相比相差並不大,都是使用三個特徵層進行分類與迴歸預測。

YoloV4-Tiny是YoloV4的簡化版,少了一些結構,但是速度大大增加了,YoloV4共有約6000萬參數,YoloV4-Tiny則只有600萬參數。

YoloV4-Tiny僅使用了兩個特徵層進行分類與迴歸預測。

代碼下載

https://github.com/bubbliiiing/yolov4-tiny-keras
喜歡的可以給個star噢!

YoloV4-Tiny結構解析

1、主幹特徵提取網絡Backbone

當輸入是416x416時,特徵結構如下:
在這裏插入圖片描述
當輸入是608x608時,特徵結構如下:
在這裏插入圖片描述
而在YoloV4-Tiny中,其使用了CSPdarknet53_tiny作爲主幹特徵提取網絡。
和CSPdarknet53相比,爲了更快速,將激活函數重新修改爲LeakyReLU。

CSPdarknet53_tiny具有兩個特點:
1、使用了CSPnet結構。
在這裏插入圖片描述
CSPnet結構並不算複雜,就是將原來的殘差塊的堆疊進行了一個拆分,拆成左右兩部分:
主幹部分繼續進行原來的殘差塊的堆疊
另一部分則像一個殘差邊一樣,經過少量處理直接連接到最後。
因此可以認爲CSP中存在一個大的殘差邊。

2、進行通道的分割
在CSPnet的主幹部分,CSPdarknet53_tiny會對一次3x3卷積後的特徵層進行通道的劃分,分成兩部分,取第二部分。
在tensorflow中使用tf.split進行劃分。

#---------------------------------------------------#
#   CSPdarknet的結構塊
#   存在一個大殘差邊
#   這個大殘差邊繞過了很多的殘差結構
#---------------------------------------------------#
def resblock_body(x, num_filters):
    x = DarknetConv2D_BN_Leaky(num_filters, (3,3))(x)
    route = x
    x = Lambda(route_group,arguments={'groups':2, 'group_id':1})(x)
    x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
    route_1 = x
    x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
    x = Concatenate()([x, route_1])
    x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x)
    feat = x
    x = Concatenate()([route, x])
    x = MaxPooling2D(pool_size=[2,2],)(x)

    # 最後對通道數進行整合
    return x, feat

利用主幹特徵提取網絡,我們可以獲得兩個shape的有效特徵層,即CSPdarknet53_tiny最後兩個shape的有效特徵層,傳入加強特徵提取網絡當中進行FPN的構建。

全部實現代碼爲:

from functools import wraps
from keras import backend as K
from keras.layers import Conv2D, Add, ZeroPadding2D, UpSampling2D, Concatenate, MaxPooling2D, Layer, Lambda
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.regularizers import l2
from utils.utils import compose
import tensorflow as tf

def route_group(input_layer, groups, group_id):
    convs = tf.split(input_layer, num_or_size_splits=groups, axis=-1)
    return convs[group_id]

#--------------------------------------------------#
#   單次卷積
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
    darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
    darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
    darknet_conv_kwargs.update(kwargs)
    return Conv2D(*args, **darknet_conv_kwargs)

#---------------------------------------------------#
#   卷積塊
#   DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs):
    no_bias_kwargs = {'use_bias': False}
    no_bias_kwargs.update(kwargs)
    return compose( 
        DarknetConv2D(*args, **no_bias_kwargs),
        BatchNormalization(),
        LeakyReLU(alpha=0.1))

#---------------------------------------------------#
#   CSPdarknet的結構塊
#   存在一個大殘差邊
#   這個大殘差邊繞過了很多的殘差結構
#---------------------------------------------------#
def resblock_body(x, num_filters):
    x = DarknetConv2D_BN_Leaky(num_filters, (3,3))(x)
    route = x
    x = Lambda(route_group,arguments={'groups':2, 'group_id':1})(x)
    x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
    route_1 = x
    x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
    x = Concatenate()([x, route_1])
    x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x)
    feat = x
    x = Concatenate()([route, x])
    x = MaxPooling2D(pool_size=[2,2],)(x)

    # 最後對通道數進行整合
    return x, feat

#---------------------------------------------------#
#   darknet53 的主體部分
#---------------------------------------------------#
def darknet_body(x):
    # 進行長和寬的壓縮
    x = ZeroPadding2D(((1,0),(1,0)))(x)
    x = DarknetConv2D_BN_Leaky(32, (3,3), strides=(2,2))(x)
    # 進行長和寬的壓縮
    x = ZeroPadding2D(((1,0),(1,0)))(x)
    x = DarknetConv2D_BN_Leaky(64, (3,3), strides=(2,2))(x)
    
    x, _ = resblock_body(x,num_filters = 64)
    x, _ = resblock_body(x,num_filters = 128)
    x, feat1 = resblock_body(x,num_filters = 256)

    x = DarknetConv2D_BN_Leaky(512, (3,3))(x)

    feat2 = x
    return feat1, feat2

2、特徵金字塔

當輸入是416x416時,特徵結構如下:
在這裏插入圖片描述
當輸入是608x608時,特徵結構如下:
在這裏插入圖片描述
YoloV4-Tiny中使用了FPN的結構,主要是對第一步獲得的兩個有效特徵層進行特徵融合。

FPN會將最後一個shape的有效特徵層卷積後進行上採樣,然後與上一個shape的有效特徵層進行堆疊並卷積。

實現代碼如下:

#--------------------------------------------------#
#   單次卷積
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
    darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
    darknet_conv_kwargs = {}
    darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
    darknet_conv_kwargs.update(kwargs)
    return Conv2D(*args, **darknet_conv_kwargs)

#---------------------------------------------------#
#   卷積塊
#   DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs):
    no_bias_kwargs = {'use_bias': False}
    no_bias_kwargs.update(kwargs)
    return compose( 
        DarknetConv2D(*args, **no_bias_kwargs),
        BatchNormalization(),
        LeakyReLU(alpha=0.1))

#---------------------------------------------------#
#   特徵層->最後的輸出
#---------------------------------------------------#
def yolo_body(inputs, num_anchors, num_classes):
    # 生成darknet53的主幹模型
    feat1,feat2 = darknet_body(inputs)

    # 第一個特徵層
    # y1=(batch_size,13,13,3,85)
    P5 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2)
    P5_output = DarknetConv2D_BN_Leaky(512, (3,3))(P5)
    P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output)
    
    P5_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P5)
    
    P4 = Concatenate()([feat1, P5_upsample])
    
    P4_output = DarknetConv2D_BN_Leaky(256, (3,3))(P4)
    P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output)
    
    return Model(inputs, [P5_output, P4_output])

3、YoloHead利用獲得到的特徵進行預測

當輸入是416x416時,特徵結構如下:
在這裏插入圖片描述
當輸入是608x608時,特徵結構如下:
在這裏插入圖片描述
1、在特徵利用部分,YoloV4-Tiny提取多特徵層進行目標檢測,一共提取兩個特徵層,兩個特徵層的shape分別爲(38,38,128)、(19,19,512)。

2、輸出層的shape分別爲(19,19,75),(38,38,75),最後一個維度爲75是因爲該圖是基於voc數據集的,它的類爲20種,YoloV4-Tiny只有針對每一個特徵層存在3個先驗框,所以最後維度爲3x25;
如果使用的是coco訓練集,類則爲80種,最後的維度應該爲255 = 3x85
,兩個特徵層的shape爲(19,19,255),(38,38,255)

實現代碼如下:

#---------------------------------------------------#
#   特徵層->最後的輸出
#---------------------------------------------------#
def yolo_body(inputs, num_anchors, num_classes):
    # 生成darknet53的主幹模型
    feat1,feat2 = darknet_body(inputs)

    # 第一個特徵層
    # y1=(batch_size,13,13,3,85)
    P5 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2)
    P5_output = DarknetConv2D_BN_Leaky(512, (3,3))(P5)
    P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output)
    
    P5_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P5)
    
    P4 = Concatenate()([feat1, P5_upsample])
    
    P4_output = DarknetConv2D_BN_Leaky(256, (3,3))(P4)
    P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output)
    
    return Model(inputs, [P5_output, P4_output])

4、預測結果的解碼

由第三步我們可以獲得兩個特徵層的預測結果,shape分別爲(N,19,19,255),(N,38,38,255)的數據,對應每個圖分爲19x19、38x38的網格上3個預測框的位置。

但是這個預測結果並不對應着最終的預測框在圖片上的位置,還需要解碼纔可以完成。

此處要講一下yolo的預測原理,yolo的特徵層分別將整幅圖分爲19x19、38x38的網格,每個網絡點負責一個區域的檢測。

我們知道特徵層的預測結果對應着三個預測框的位置,我們先將其reshape一下,其結果爲(N,19,19,3,85),(N,38,38,3,85)。

最後一個維度中的85包含了4+1+80,分別代表x_offset、y_offset、h和w、置信度、分類結果。

yolo的解碼過程就是將每個網格點加上它對應的x_offset和y_offset,加完後的結果就是預測框的中心,然後再利用 先驗框和h、w結合 計算出預測框的長和寬。這樣就能得到整個預測框的位置了。

在這裏插入圖片描述
當然得到最終的預測結構後還要進行得分排序與非極大抑制篩選
這一部分基本上是所有目標檢測通用的部分。不過該項目的處理方式與其它項目不同。其對於每一個類進行判別。
1、取出每一類得分大於self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。

實現代碼如下,當調用yolo_eval時,就會對每個特徵層進行解碼:

#---------------------------------------------------#
#   將預測值的每個特徵層調成真實值
#---------------------------------------------------#
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
    num_anchors = len(anchors)
    # [1, 1, 1, num_anchors, 2]
    anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])

    # 獲得x,y的網格
    # (13,13, 1, 2)
    grid_shape = K.shape(feats)[1:3] # height, width
    grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
        [1, grid_shape[1], 1, 1])
    grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
        [grid_shape[0], 1, 1, 1])
    grid = K.concatenate([grid_x, grid_y])
    grid = K.cast(grid, K.dtype(feats))

    # (batch_size,13,13,3,85)
    feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])

    # 將預測值調成真實值
    # box_xy對應框的中心點
    # box_wh對應框的寬和高
    box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
    box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
    box_confidence = K.sigmoid(feats[..., 4:5])
    box_class_probs = K.sigmoid(feats[..., 5:])

    # 在計算loss的時候返回如下參數
    if calc_loss == True:
        return grid, feats, box_xy, box_wh
    return box_xy, box_wh, box_confidence, box_class_probs

#---------------------------------------------------#
#   對box進行調整,使其符合真實圖片的樣子
#---------------------------------------------------#
def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape):
    box_yx = box_xy[..., ::-1]
    box_hw = box_wh[..., ::-1]
    
    input_shape = K.cast(input_shape, K.dtype(box_yx))
    image_shape = K.cast(image_shape, K.dtype(box_yx))

    new_shape = K.round(image_shape * K.min(input_shape/image_shape))
    offset = (input_shape-new_shape)/2./input_shape
    scale = input_shape/new_shape

    box_yx = (box_yx - offset) * scale
    box_hw *= scale

    box_mins = box_yx - (box_hw / 2.)
    box_maxes = box_yx + (box_hw / 2.)
    boxes =  K.concatenate([
        box_mins[..., 0:1],  # y_min
        box_mins[..., 1:2],  # x_min
        box_maxes[..., 0:1],  # y_max
        box_maxes[..., 1:2]  # x_max
    ])

    boxes *= K.concatenate([image_shape, image_shape])
    return boxes

#---------------------------------------------------#
#   獲取每個box和它的得分
#---------------------------------------------------#
def yolo_boxes_and_scores(feats, anchors, num_classes, input_shape, image_shape):
    # 將預測值調成真實值
    # box_xy對應框的中心點
    # box_wh對應框的寬和高
    # -1,13,13,3,2; -1,13,13,3,2; -1,13,13,3,1; -1,13,13,3,80
    box_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats, anchors, num_classes, input_shape)
    # 將box_xy、和box_wh調節成y_min,y_max,xmin,xmax
    boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)
    # 獲得得分和box
    boxes = K.reshape(boxes, [-1, 4])
    box_scores = box_confidence * box_class_probs
    box_scores = K.reshape(box_scores, [-1, num_classes])
    return boxes, box_scores

#---------------------------------------------------#
#   圖片預測
#---------------------------------------------------#
def yolo_eval(yolo_outputs,
              anchors,
              num_classes,
              image_shape,
              max_boxes=20,
              score_threshold=.6,
              iou_threshold=.5):
    # 獲得特徵層的數量
    num_layers = len(yolo_outputs)
    # 先驗框
    anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]
    
    input_shape = K.shape(yolo_outputs[0])[1:3] * 32
    boxes = []
    box_scores = []
    # 對每個特徵層進行處理
    for l in range(num_layers):
        _boxes, _box_scores = yolo_boxes_and_scores(yolo_outputs[l], anchors[anchor_mask[l]], num_classes, input_shape, image_shape)
        boxes.append(_boxes)
        box_scores.append(_box_scores)
    # 將每個特徵層的結果進行堆疊
    boxes = K.concatenate(boxes, axis=0)
    box_scores = K.concatenate(box_scores, axis=0)

    mask = box_scores >= score_threshold
    max_boxes_tensor = K.constant(max_boxes, dtype='int32')
    boxes_ = []
    scores_ = []
    classes_ = []
    for c in range(num_classes):
        # 取出所有box_scores >= score_threshold的框,和成績
        class_boxes = tf.boolean_mask(boxes, mask[:, c])
        class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])

        # 非極大抑制,去掉box重合程度高的那一些
        nms_index = tf.image.non_max_suppression(
            class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold)

        # 獲取非極大抑制後的結果
        # 下列三個分別是
        # 框的位置,得分與種類
        class_boxes = K.gather(class_boxes, nms_index)
        class_box_scores = K.gather(class_box_scores, nms_index)
        classes = K.ones_like(class_box_scores, 'int32') * c
        boxes_.append(class_boxes)
        scores_.append(class_box_scores)
        classes_.append(classes)
    boxes_ = K.concatenate(boxes_, axis=0)
    scores_ = K.concatenate(scores_, axis=0)
    classes_ = K.concatenate(classes_, axis=0)

    return boxes_, scores_, classes_

5、在原圖上進行繪製

通過第四步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪製在圖片上,就可以獲得結果了。

YoloV4-Tiny的訓練

1、YOLOV4的改進訓練技巧

a)、Mosaic數據增強

Yolov4的mosaic數據增強參考了CutMix數據增強方式,理論上具有一定的相似性!
CutMix數據增強方式利用兩張圖片進行拼接。
在這裏插入圖片描述
但是mosaic利用了四張圖片,根據論文所說其擁有一個巨大的優點是豐富檢測物體的背景!且在BN計算的時候一下子會計算四張圖片的數據!
就像下圖這樣:
在這裏插入圖片描述
實現思路如下:
1、每次讀取四張圖片。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
2、分別對四張圖片進行翻轉、縮放、色域變化等,並且按照四個方向位置擺好。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
3、進行圖片的組合和框的組合
在這裏插入圖片描述

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

b)、Label Smoothing平滑

標籤平滑的思想很簡單,具體公式如下:

new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes

當label_smoothing的值爲0.01得時候,公式變成如下所示:

new_onehot_labels = y * (1 - 0.01) + 0.01 / num_classes

其實Label Smoothing平滑就是將標籤進行一個平滑,原始的標籤是0、1,在平滑後變成0.005(如果是二分類)、0.995,也就是說對分類準確做了一點懲罰,讓模型不可以分類的太準確,太準確容易過擬合。

實現代碼如下:

#---------------------------------------------------#
#   平滑標籤
#---------------------------------------------------#
def _smooth_labels(y_true, label_smoothing):
    num_classes = K.shape(y_true)[-1],
    label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
    return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes

c)、CIOU

IoU是比值的概念,對目標物體的scale是不敏感的。然而常用的BBox的迴歸損失優化和IoU優化不是完全等價的,尋常的IoU無法直接優化沒有重疊的部分。

於是有人提出直接使用IOU作爲迴歸優化loss,CIOU是其中非常優秀的一種想法。

CIOU將目標與anchor之間的距離,重疊率、尺度以及懲罰項都考慮進去,使得目標框迴歸變得更加穩定,不會像IoU和GIoU一樣出現訓練過程中發散等問題。而懲罰因子把預測框長寬比擬合目標框的長寬比考慮進去。

在這裏插入圖片描述
CIOU公式如下
CIOU=IOUρ2(b,bgt)c2αv CIOU = IOU - \frac{\rho^2(b,b^{gt})}{c^2} - \alpha v
其中,ρ2(b,bgt)\rho^2(b,b^{gt})分別代表了預測框和真實框的中心點的歐式距離。 c代表的是能夠同時包含預測框和真實框的最小閉包區域的對角線距離。

α\alphavv的公式如下
α=v1IOU+v \alpha = \frac{v}{1-IOU+v}
v=4π2(arctanwgthgtarctanwh)2 v = \frac{4}{\pi ^2}(arctan\frac{w^{gt}}{h^{gt}}-arctan\frac{w}{h})^2
把1-CIOU就可以得到相應的LOSS了。
LOSSCIOU=1IOU+ρ2(b,bgt)c2+αv LOSS_{CIOU} = 1 - IOU + \frac{\rho^2(b,b^{gt})}{c^2} + \alpha v

def box_ciou(b1, b2):
    """
    輸入爲:
    ----------
    b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh

    返回爲:
    -------
    ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
    """
    # 求出預測框左上角右下角
    b1_xy = b1[..., :2]
    b1_wh = b1[..., 2:4]
    b1_wh_half = b1_wh/2.
    b1_mins = b1_xy - b1_wh_half
    b1_maxes = b1_xy + b1_wh_half
    # 求出真實框左上角右下角
    b2_xy = b2[..., :2]
    b2_wh = b2[..., 2:4]
    b2_wh_half = b2_wh/2.
    b2_mins = b2_xy - b2_wh_half
    b2_maxes = b2_xy + b2_wh_half

    # 求真實框和預測框所有的iou
    intersect_mins = K.maximum(b1_mins, b2_mins)
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)
    intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
    intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
    b1_area = b1_wh[..., 0] * b1_wh[..., 1]
    b2_area = b2_wh[..., 0] * b2_wh[..., 1]
    union_area = b1_area + b2_area - intersect_area
    iou = intersect_area / (union_area + K.epsilon())

    # 計算中心的差距
    center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
    # 找到包裹兩個框的最小框的左上角和右下角
    enclose_mins = K.minimum(b1_mins, b2_mins)
    enclose_maxes = K.maximum(b1_maxes, b2_maxes)
    enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
    # 計算對角線距離
    enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
    # calculate ciou, add epsilon in denominator to avoid dividing by 0
    ciou = iou - 1.0 * (center_distance) / (enclose_diagonal + K.epsilon())

    # calculate param v and alpha to extend to CIoU
    v = 4*K.square(tf.math.atan2(b1_wh[..., 0], b1_wh[..., 1]) - tf.math.atan2(b2_wh[..., 0], b2_wh[..., 1])) / (math.pi * math.pi)
    alpha = v / (1.0 - iou + v)
    ciou = ciou - alpha * v

    ciou = K.expand_dims(ciou, -1)
    return ciou

d)、學習率餘弦退火衰減

餘弦退火衰減法,學習率會先上升再下降,這是退火優化法的思想。(關於什麼是退火算法可以百度。)

上升的時候使用線性上升,下降的時候模擬cos函數下降。執行多次。

效果如圖所示:
在這裏插入圖片描述
餘弦退火衰減有幾個比較必要的參數:
1、learning_rate_base:學習率最高值。
2、warmup_learning_rate:最開始的學習率。
3、warmup_steps:多少步長後到達頂峯值。

實現方式如下,利用Callback實現,與普通的ReduceLROnPlateau調用方式類似:

import numpy as np
import matplotlib.pyplot as plt
import keras
from keras import backend as K
from keras.layers import Flatten,Conv2D,Dropout,Input,Dense,MaxPooling2D
from keras.models import Model

def cosine_decay_with_warmup(global_step,
                             learning_rate_base,
                             total_steps,
                             warmup_learning_rate=0.0,
                             warmup_steps=0,
                             hold_base_rate_steps=0,
                             min_learn_rate=0,
                             ):
    """
    參數:
            global_step: 上面定義的Tcur,記錄當前執行的步數。
            learning_rate_base:預先設置的學習率,當warm_up階段學習率增加到learning_rate_base,就開始學習率下降。
            total_steps: 是總的訓練的步數,等於epoch*sample_count/batch_size,(sample_count是樣本總數,epoch是總的循環次數)
            warmup_learning_rate: 這是warm up階段線性增長的初始值
            warmup_steps: warm_up總的需要持續的步數
            hold_base_rate_steps: 這是可選的參數,即當warm up階段結束後保持學習率不變,知道hold_base_rate_steps結束後纔開始學習率下降
    """
    if total_steps < warmup_steps:
        raise ValueError('total_steps must be larger or equal to '
                            'warmup_steps.')
    #這裏實現了餘弦退火的原理,設置學習率的最小值爲0,所以簡化了表達式
    learning_rate = 0.5 * learning_rate_base * (1 + np.cos(np.pi *
        (global_step - warmup_steps - hold_base_rate_steps) / float(total_steps - warmup_steps - hold_base_rate_steps)))
    #如果hold_base_rate_steps大於0,表明在warm up結束後學習率在一定步數內保持不變
    if hold_base_rate_steps > 0:
        learning_rate = np.where(global_step > warmup_steps + hold_base_rate_steps,
                                    learning_rate, learning_rate_base)
    if warmup_steps > 0:
        if learning_rate_base < warmup_learning_rate:
            raise ValueError('learning_rate_base must be larger or equal to '
                                'warmup_learning_rate.')
        #線性增長的實現
        slope = (learning_rate_base - warmup_learning_rate) / warmup_steps
        warmup_rate = slope * global_step + warmup_learning_rate
        #只有當global_step 仍然處於warm up階段纔會使用線性增長的學習率warmup_rate,否則使用餘弦退火的學習率learning_rate
        learning_rate = np.where(global_step < warmup_steps, warmup_rate,
                                    learning_rate)

    learning_rate = max(learning_rate,min_learn_rate)
    return learning_rate


class WarmUpCosineDecayScheduler(keras.callbacks.Callback):
    """
    繼承Callback,實現對學習率的調度
    """
    def __init__(self,
                 learning_rate_base,
                 total_steps,
                 global_step_init=0,
                 warmup_learning_rate=0.0,
                 warmup_steps=0,
                 hold_base_rate_steps=0,
                 min_learn_rate=0,
                 # interval_epoch代表餘弦退火之間的最低點
                 interval_epoch=[0.05, 0.15, 0.30, 0.50],
                 verbose=0):
        super(WarmUpCosineDecayScheduler, self).__init__()
        # 基礎的學習率
        self.learning_rate_base = learning_rate_base
        # 熱調整參數
        self.warmup_learning_rate = warmup_learning_rate
        # 參數顯示  
        self.verbose = verbose
        # learning_rates用於記錄每次更新後的學習率,方便圖形化觀察
        self.min_learn_rate = min_learn_rate
        self.learning_rates = []

        self.interval_epoch = interval_epoch
        # 貫穿全局的步長
        self.global_step_for_interval = global_step_init
        # 用於上升的總步長
        self.warmup_steps_for_interval = warmup_steps
        # 保持最高峯的總步長
        self.hold_steps_for_interval = hold_base_rate_steps
        # 整個訓練的總步長
        self.total_steps_for_interval = total_steps

        self.interval_index = 0
        # 計算出來兩個最低點的間隔
        self.interval_reset = [self.interval_epoch[0]]
        for i in range(len(self.interval_epoch)-1):
            self.interval_reset.append(self.interval_epoch[i+1]-self.interval_epoch[i])
        self.interval_reset.append(1-self.interval_epoch[-1])

	#更新global_step,並記錄當前學習率
    def on_batch_end(self, batch, logs=None):
        self.global_step = self.global_step + 1
        self.global_step_for_interval = self.global_step_for_interval + 1
        lr = K.get_value(self.model.optimizer.lr)
        self.learning_rates.append(lr)

	#更新學習率
    def on_batch_begin(self, batch, logs=None):
        # 每到一次最低點就重新更新參數
        if self.global_step_for_interval in [0]+[int(i*self.total_steps_for_interval) for i in self.interval_epoch]:
            self.total_steps = self.total_steps_for_interval * self.interval_reset[self.interval_index]
            self.warmup_steps = self.warmup_steps_for_interval * self.interval_reset[self.interval_index]
            self.hold_base_rate_steps = self.hold_steps_for_interval * self.interval_reset[self.interval_index]
            self.global_step = 0
            self.interval_index += 1

        lr = cosine_decay_with_warmup(global_step=self.global_step,
                                      learning_rate_base=self.learning_rate_base,
                                      total_steps=self.total_steps,
                                      warmup_learning_rate=self.warmup_learning_rate,
                                      warmup_steps=self.warmup_steps,
                                      hold_base_rate_steps=self.hold_base_rate_steps,
                                      min_learn_rate = self.min_learn_rate)
        K.set_value(self.model.optimizer.lr, lr)
        if self.verbose > 0:
            print('\nBatch %05d: setting learning '
                  'rate to %s.' % (self.global_step + 1, lr))

2、loss組成

a)、計算loss所需參數

在計算loss的時候,實際上是y_pre和y_true之間的對比:
y_pre就是一幅圖像經過網絡之後的輸出,內部含有兩個特徵層的內容;其需要解碼才能夠在圖上作畫
y_true就是一個真實圖像中,它的每個真實框對應的(19,19)、(38,38)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一致
實際上y_pre和y_true內容的shape都是
(batch_size,19,19,3,85)
(batch_size,38,38,3,85)

b)、y_pre是什麼

網絡最後輸出的內容就是兩個特徵層每個網格點對應的預測框及其種類,即兩個特徵層分別對應着圖片被分爲不同size的網格後,每個網格點上三個先驗框對應的位置、置信度及其種類。
對於輸出的y1、y2、y3而言,[…, : 2]指的是相對於每個網格點的偏移量,[…, 2: 4]指的是寬和高,[…, 4: 5]指的是該框的置信度,[…, 5: ]指的是每個種類的預測概率。
現在的y_pre還是沒有解碼的,解碼了之後纔是真實圖像上的情況。

c)、y_true是什麼。

y_true就是一個真實圖像中,它的每個真實框對應的(19,19)、(38,38)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一致
在yolo4中,其使用了一個專門的函數用於處理讀取進來的圖片的框的真實情況。

def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):

其輸入爲:
true_boxes:shape爲(m, T, 5)代表m張圖T個框的x_min、y_min、x_max、y_max、class_id。
input_shape:輸入的形狀,此處爲608、608
anchors:代表9個先驗框的大小
num_classes:種類的數量。

其實對真實框的處理是將真實框轉化成圖片中相對網格的xyhw,步驟如下:
1、取框的真實值,獲取其框的中心及其寬高,除去input_shape變成比例的模式。
2、建立全爲0的y_true,y_true是一個列表,包含兩個特徵層,shape分別爲(batch_size,19,19,3,85)、(batch_size,38,38,3,85)
3、對每一張圖片處理,將每一張圖片中的真實框的wh和先驗框的wh對比,計算IOU值,選取其中IOU最高的一個,得到其所屬特徵層及其網格點的位置,在對應的y_true中將內容進行保存。

for t, n in enumerate(best_anchor):
    for l in range(num_layers):
        if n in anchor_mask[l]:

            # 計算該目標在第l個特徵層所處網格的位置
            i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
            j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')

            # 找到best_anchor索引的索引
            k = anchor_mask[l].index(n)
            c = true_boxes[b,t, 4].astype('int32')
            
            # 保存到y_true中
            y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
            y_true[l][b, j, i, k, 4] = 1
            y_true[l][b, j, i, k, 5+c] = 1

對於最後輸出的y_true而言,只有每個圖裏每個框最對應的位置有數據,其它的地方都爲0。
preprocess_true_boxes全部的代碼如下:

#---------------------------------------------------#
#   讀入xml文件,並輸出y_true
#---------------------------------------------------#
def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
    assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'
    # 一共有三個特徵層數
    num_layers = len(anchors)//3
    # 先驗框
    # 678爲116,90,  156,198,  373,326
    # 345爲30,61,  62,45,  59,119
    # 012爲10,13,  16,30,  33,23,  
    anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]

    true_boxes = np.array(true_boxes, dtype='float32')
    input_shape = np.array(input_shape, dtype='int32') # 416,416
    # 讀出xy軸,讀出長寬
    # 中心點(m,n,2)
    boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
    boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]
    # 計算比例
    true_boxes[..., 0:2] = boxes_xy/input_shape[:]
    true_boxes[..., 2:4] = boxes_wh/input_shape[:]

    # m張圖
    m = true_boxes.shape[0]
    # 得到網格的shape爲19,19;38,38;76,76
    grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)]
    # y_true的格式爲(m,19,19,3,85)(m,38,38,3,85)(m,76,76,3,85)
    y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes),
        dtype='float32') for l in range(num_layers)]
    # [1,9,2]
    anchors = np.expand_dims(anchors, 0)
    anchor_maxes = anchors / 2.
    anchor_mins = -anchor_maxes
    # 長寬要大於0纔有效
    valid_mask = boxes_wh[..., 0]>0

    for b in range(m):
        # 對每一張圖進行處理
        wh = boxes_wh[b, valid_mask[b]]
        if len(wh)==0: continue
        # [n,1,2]
        wh = np.expand_dims(wh, -2)
        box_maxes = wh / 2.
        box_mins = -box_maxes

        # 計算真實框和哪個先驗框最契合
        intersect_mins = np.maximum(box_mins, anchor_mins)
        intersect_maxes = np.minimum(box_maxes, anchor_maxes)
        intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
        intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
        box_area = wh[..., 0] * wh[..., 1]
        anchor_area = anchors[..., 0] * anchors[..., 1]
        iou = intersect_area / (box_area + anchor_area - intersect_area)
        # 維度是(n) 感謝 消盡不死鳥 的提醒
        best_anchor = np.argmax(iou, axis=-1)

        for t, n in enumerate(best_anchor):
            for l in range(num_layers):
                if n in anchor_mask[l]:
                    # floor用於向下取整
                    i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
                    j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
                    # 找到真實框在特徵層l中第b副圖像對應的位置
                    k = anchor_mask[l].index(n)
                    c = true_boxes[b,t, 4].astype('int32')
                    y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
                    y_true[l][b, j, i, k, 4] = 1
                    y_true[l][b, j, i, k, 5+c] = 1

    return y_true

d)、loss的計算過程

在得到了y_pre和y_true後怎麼對比呢?不是簡單的減一下!

loss值需要對兩個特徵層進行處理,這裏以最小的特徵層爲例。
1、利用y_true取出該特徵層中真實存在目標的點的位置(m,19,19,3,1)及其對應的種類(m,19,19,3,80)。
2、將yolo_outputs的預測值輸出進行處理,得到reshape後的預測值y_pre,shape爲(m,19,19,3,85)。還有解碼後的xy,wh。
3、對於每一幅圖,計算其中所有真實框與預測框的IOU,如果某些預測框和真實框的重合程度大於0.5,則忽略。
4、計算ciou作爲迴歸的loss,這裏只計算正樣本的迴歸loss。
5、計算置信度的loss,其有兩部分構成,第一部分是實際上存在目標的,預測結果中置信度的值與1對比;第二部分是實際上不存在目標的,預測結果中置信度的值與0對比。
6、計算預測種類的loss,其計算的是實際上存在目標的,預測類與真實類的差距。

其實際上計算的總的loss是三個loss的和,這三個loss分別是:

  • 實際存在的框,CIOU LOSS
  • 實際存在的框,預測結果中置信度的值與1對比;實際不存在的框,預測結果中置信度的值與0對比,該部分要去除被忽略的不包含目標的框
  • 實際存在的框,種類預測結果與實際結果的對比

其實際代碼如下,使用yolo_loss就可以獲得loss值:

#---------------------------------------------------#
#   平滑標籤
#---------------------------------------------------#
def _smooth_labels(y_true, label_smoothing):
    num_classes = K.shape(y_true)[-1],
    label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
    return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
#---------------------------------------------------#
#   將預測值的每個特徵層調成真實值
#---------------------------------------------------#
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
    num_anchors = len(anchors)
    # [1, 1, 1, num_anchors, 2]
    anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])

    # 獲得x,y的網格
    # (19,19, 1, 2)
    grid_shape = K.shape(feats)[1:3] # height, width
    grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
        [1, grid_shape[1], 1, 1])
    grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
        [grid_shape[0], 1, 1, 1])
    grid = K.concatenate([grid_x, grid_y])
    grid = K.cast(grid, K.dtype(feats))

    # (batch_size,19,19,3,85)
    feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])

    # 將預測值調成真實值
    # box_xy對應框的中心點
    # box_wh對應框的寬和高
    box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
    box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
    box_confidence = K.sigmoid(feats[..., 4:5])
    box_class_probs = K.sigmoid(feats[..., 5:])

    # 在計算loss的時候返回如下參數
    if calc_loss == True:
        return grid, feats, box_xy, box_wh
    return box_xy, box_wh, box_confidence, box_class_probs

#---------------------------------------------------#
#   用於計算每個預測框與真實框的iou
#---------------------------------------------------#
def box_iou(b1, b2):
    # 19,19,3,1,4
    # 計算左上角的座標和右下角的座標
    b1 = K.expand_dims(b1, -2)
    b1_xy = b1[..., :2]
    b1_wh = b1[..., 2:4]
    b1_wh_half = b1_wh/2.
    b1_mins = b1_xy - b1_wh_half
    b1_maxes = b1_xy + b1_wh_half

    # 1,n,4
    # 計算左上角和右下角的座標
    b2 = K.expand_dims(b2, 0)
    b2_xy = b2[..., :2]
    b2_wh = b2[..., 2:4]
    b2_wh_half = b2_wh/2.
    b2_mins = b2_xy - b2_wh_half
    b2_maxes = b2_xy + b2_wh_half

    # 計算重合面積
    intersect_mins = K.maximum(b1_mins, b2_mins)
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)
    intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
    intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
    b1_area = b1_wh[..., 0] * b1_wh[..., 1]
    b2_area = b2_wh[..., 0] * b2_wh[..., 1]
    iou = intersect_area / (b1_area + b2_area - intersect_area)

    return iou

#---------------------------------------------------#
#   loss值計算
#---------------------------------------------------#
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, label_smoothing=0.1, print_loss=False):

    # 一共有三層
    num_layers = len(anchors)//3 

    # 將預測結果和實際ground truth分開,args是[*model_body.output, *y_true]
    # y_true是一個列表,包含三個特徵層,shape分別爲(m,19,19,3,85),(m,38,38,3,85),(m,76,76,3,85)。
    # yolo_outputs是一個列表,包含三個特徵層,shape分別爲(m,19,19,3,85),(m,38,38,3,85),(m,76,76,3,85)。
    y_true = args[num_layers:]
    yolo_outputs = args[:num_layers]

    # 先驗框
    # 678爲116,90,  156,198,  373,326
    # 345爲30,61,  62,45,  59,119
    # 012爲10,13,  16,30,  33,23,  
    anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]

    # 得到input_shpae爲608,608 
    input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))

    loss = 0

    # 取出每一張圖片
    # m的值就是batch_size
    m = K.shape(yolo_outputs[0])[0]
    mf = K.cast(m, K.dtype(yolo_outputs[0]))

    # y_true是一個列表,包含三個特徵層,shape分別爲(m,19,19,3,85),(m,38,38,3,85),(m,76,76,3,85)。
    # yolo_outputs是一個列表,包含三個特徵層,shape分別爲(m,19,19,3,85),(m,38,38,3,85),(m,76,76,3,85)。
    for l in range(num_layers):
        # 以第一個特徵層(m,19,19,3,85)爲例子
        # 取出該特徵層中存在目標的點的位置。(m,19,19,3,1)
        object_mask = y_true[l][..., 4:5]
        # 取出其對應的種類(m,19,19,3,80)
        true_class_probs = y_true[l][..., 5:]
        if label_smoothing:
            true_class_probs = _smooth_labels(true_class_probs, label_smoothing)

        # 將yolo_outputs的特徵層輸出進行處理
        # grid爲網格結構(19,19,1,2),raw_pred爲尚未處理的預測結果(m,19,19,3,85)
        # 還有解碼後的xy,wh,(m,19,19,3,2)
        grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],
             anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)
        
        # 這個是解碼後的預測的box的位置
        # (m,19,19,3,4)
        pred_box = K.concatenate([pred_xy, pred_wh])

        # 找到負樣本羣組,第一步是創建一個數組,[]
        ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
        object_mask_bool = K.cast(object_mask, 'bool')
        
        # 對每一張圖片計算ignore_mask
        def loop_body(b, ignore_mask):
            # 取出第b副圖內,真實存在的所有的box的參數
            # n,4
            true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])
            # 計算預測結果與真實情況的iou
            # pred_box爲19,19,3,4
            # 計算的結果是每個pred_box和其它所有真實框的iou
            # 19,19,3,n
            iou = box_iou(pred_box[b], true_box)

            # 19,19,3,1
            best_iou = K.max(iou, axis=-1)

            # 如果某些預測框和真實框的重合程度大於0.5,則忽略。
            ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
            return b+1, ignore_mask

        # 遍歷所有的圖片
        _, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])

        # 將每幅圖的內容壓縮,進行處理
        ignore_mask = ignore_mask.stack()
        #(m,19,19,3,1,1)
        ignore_mask = K.expand_dims(ignore_mask, -1)

        box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]

        # Calculate ciou loss as location loss
        raw_true_box = y_true[l][...,0:4]
        ciou = box_ciou(pred_box, raw_true_box)
        ciou_loss = object_mask * box_loss_scale * (1 - ciou)
        ciou_loss = K.sum(ciou_loss) / mf
        location_loss = ciou_loss
        
        # 如果該位置本來有框,那麼計算1與置信度的交叉熵
        # 如果該位置本來沒有框,而且滿足best_iou<ignore_thresh,則被認定爲負樣本
        # best_iou<ignore_thresh用於限制負樣本數量
        confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \
            (1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask
        
        class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True)

        confidence_loss = K.sum(confidence_loss) / mf
        class_loss = K.sum(class_loss) / mf
        loss += location_loss + confidence_loss + class_loss
        if print_loss:
            loss = tf.Print(loss, [loss, location_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')
    return loss

訓練自己的YOLOV4模型

yolo4整體的文件夾構架如下:
在這裏插入圖片描述
本文使用VOC格式進行訓練。
訓練前將標籤文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
在這裏插入圖片描述
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在這裏插入圖片描述
在訓練前利用voc2yolo3.py文件生成對應的txt。
在這裏插入圖片描述
再運行根目錄下的voc_annotation.py,運行前需要將classes改成你自己的classes。

classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]

在這裏插入圖片描述
就會生成對應的2007_train.txt,每一行對應其圖片位置及其真實框的位置。
在這裏插入圖片描述
在訓練前需要修改model_data裏面的voc_classes.txt文件,需要將classes改成你自己的classes。
在這裏插入圖片描述
運行train.py即可開始訓練。
在這裏插入圖片描述

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