探索 YOLO v3 原始碼 - 第1篇 訓練

SpikeKing發表於2018-08-03

YOLO,即You Only Look Once(你只看一次)的縮寫,是一個基於卷積神經網路(CNN)的物體檢測演算法。而YOLO v3是YOLO的第3個版本(即YOLOYOLO 9000YOLO v3),檢測效果,更準更強。

更多細節,可以參考YOLO的官網

官網

YOLO是一句美國的俗語,You Only Live Once,人生苦短,及時行樂。

本文介紹YOLO v3演算法的實現細節,Keras框架。這是第1篇,訓練。當然還有第2篇,至第n篇,畢竟,這是一個完整版 :)

本文的GitHub原始碼github.com/SpikeKing/k…

已更新:

歡迎關注,微信公眾號 深度演算法 (ID: DeepAlgorithm) ,瞭解更多深度技術!!


1. 引數

模型的訓練引數,5個引數:

(1) 已標註框的圖片資料集,格式如下:

圖片的位置 框的4個座標和1個類別ID(xmin,ymin,xmax,ymax,label_id) ...
dataset/image.jpg 788,351,832,426,0 805,208,855,270,0
複製程式碼

(2) 標註框類別的彙總,即資料集所標註物體的全部類別列表,如下:

aeroplane
bicycle
bird
...
複製程式碼

(3) 預訓練模型,用於遷移學習(Transfer Learning)中的微調(Fine Tune),可選YOLO v3已訓練完成的COCO模型權重,即:

pretrained_path = 'model_data/yolo_weights.h5'
複製程式碼

(4) 預測特徵圖(Prediction Feature Map)的anchor框(anchor box)集合:

  • 3個尺度(scale)的特徵圖,每個特徵圖3個anchor框,共9個框,從小到大排列;
  • 1~3是大尺度(52x52)特徵圖所使用的,4~6是中尺度(26x26),7~9是小尺度(13x13);
  • 大尺度特徵圖檢測小物體,小尺度檢測大物體;
  • 9個anchor來源於邊界框(Bounding Box)的K-Means聚類。

其中,COCO的anchors,如下:

10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
複製程式碼

(5) 圖片輸入尺寸,預設為416x416。

  • 圖片尺寸滿足32的倍數,在DarkNet網路中,含有5次步長為2的降取樣卷積(32=2^5)。降取樣卷積的實現如下:
x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)
複製程式碼
  • 在最底層時,特徵圖尺寸需要滿足為奇數,如13,以保證中心點落在唯一框中。如果為偶數時,則中心點落在中心的4個框中,導致歧義。

2. 建立模型

建立YOLOv3的網路模型,輸入:

  • input_shape:圖片尺寸;
  • anchors:9個anchor box;
  • num_classes:類別數;
  • freeze_body:凍結模式,1是凍結DarkNet53的層,2是凍結全部只保留最後3層;
  • weights_path:預訓練模型的權重。

實現:

model = create_model(input_shape, anchors, num_classes,
                     freeze_body=2,
                     weights_path=pretrained_path)
複製程式碼

其中,網路的最後3層:

3個1x1的卷積層(代替全連線層),用於將3個尺度的特徵圖,轉換為3個尺度的預測值。

實現:

out_filters = num_anchors * (num_classes + 5)
// ...
DarknetConv2D(out_filters, (1, 1))
複製程式碼

即:

conv2d_59 (Conv2D)      (None, 13, 13, 18)   18450       leaky_re_lu_58[0][0]    
conv2d_67 (Conv2D)      (None, 26, 26, 18)   9234        leaky_re_lu_65[0][0]    
conv2d_75 (Conv2D)      (None, 52, 52, 18)   4626        leaky_re_lu_72[0][0]    
複製程式碼

3. 樣本數量

樣本洗牌(shuffle),將資料集拆分為10份,訓練9份,驗證1份。

實現:

val_split = 0.1  # 訓練和驗證的比例
with open(annotation_path) as f:
    lines = f.readlines()
np.random.seed(47)
np.random.shuffle(lines)
np.random.seed(None)
num_val = int(len(lines) * val_split)  # 驗證集數量
num_train = len(lines) - num_val  # 訓練集數量
複製程式碼

4. 第1階段訓練

第1階段,凍結部分網路,只訓練底層權重。

  • 優化器使用常見的Adam;
  • 損失函式,直接使用,模型的輸出y_pred,忽略真值y_true

實現:

model.compile(optimizer=Adam(lr=1e-3), loss={
    # 使用定製的 yolo_loss Lambda層
    'yolo_loss': lambda y_true, y_pred: y_pred})  # 損失函式
複製程式碼

其中,損失函式yolo_loss,以及y_truey_pred

y_true當成一個輸入,構成多輸入模型,把loss寫成層(Lambda層),作為最後的輸出。這樣,構建模型的時候,就只需要將模型的輸出(output)定義為loss即可。而編譯(compile)的時候,直接將loss設定為y_pred,因為模型的輸出就是loss,即y_pred就是loss,因而無視y_true。訓練的時候,隨便新增一個符合形狀的y_true陣列即可。

關於Python的Lambda表示式:

f = lambda y_true, y_pred: y_pred
print(f(1, 2))  # 輸出2
複製程式碼

模型fit資料,使用資料生成包裝器(data_generator_wrapper),按批次生成訓練和驗證資料。最終,模型model儲存權重。實現如下:

batch_size = 32  # batch
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                    steps_per_epoch=max(1, num_train // batch_size),
                    validation_data=data_generator_wrapper(
                        lines[num_train:], batch_size, input_shape, anchors, num_classes),
                    validation_steps=max(1, num_val // batch_size),
                    epochs=50,
                    initial_epoch=0,
                    callbacks=[logging, checkpoint])
# 儲存最終的去權重,再訓練過程中,也通過回撥儲存
model.save_weights(log_dir + 'trained_weights_stage_1.h5')  
複製程式碼

在訓練過程中,也會儲存epoch完成的模型權重,其中,只儲存權重(save_weights_only),只儲存最優結果(save_best_only),每隔3個epoch儲存一次(period),即:

checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
                             monitor='val_loss', save_weights_only=True,
                             save_best_only=True, period=3)  # 只儲存weights權重
複製程式碼

5. 第2階段訓練

第2階段,使用第1階段已訓練的網路權重,繼續訓練:

  • 將全部的權重都設定為可訓練,而第1階段則是凍結(freeze)部分權重;
  • 優化器,仍是Adam,只是學習率(lr)有所下降,從1e-3減少至1e-4,細膩地學習最優權重;
  • 損失函式,仍是隻使用y_pred,忽略y_true

實現:

for i in range(len(model.layers)):
    model.layers[i].trainable = True

model.compile(optimizer=Adam(lr=1e-4),
              loss={'yolo_loss': lambda y_true, y_pred: y_pred})
複製程式碼

第2階段的模型fit資料,與第1階段類似,從第50個epoch開始,一直訓練到第100個epoch,觸發條件,則提前終止。額外增加了兩個回撥reduce_lrearly_stopping,用於控制訓練提取終止:

  • reduce_lr:當評價指標不在提升時,減少學習率,每次減少10%(factor),當驗證損失3次未減少(patience)時,則終止訓練。
  • early_stopping:驗證集準確率,連續增加小於0(min_delta)時,持續10個epoch(patience),則終止訓練。

實現:

reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)  # 當評價指標不在提升時,減少學習率
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)  # 驗證集準確率,下降前終止

batch_size = 32
model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                    steps_per_epoch=max(1, num_train // batch_size),
                    validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors,
                                                           num_classes),
                    validation_steps=max(1, num_val // batch_size),
                    epochs=100,
                    initial_epoch=50,
                    callbacks=[logging, checkpoint, reduce_lr, early_stopping])
model.save_weights(log_dir + 'trained_weights_final.h5')
複製程式碼

至此,在第2階段訓練完成之後,輸出的網路權重,就是最終的模型權重。


補1. K-Means

K-Means演算法是聚類演算法,將一組資料劃分為多個組(group),每個組都含有一箇中心。

YOLOv3,獲取資料集中全部的anchor box,通過K-Means演算法,將這些框聚類為9類,獲取9個聚類中心,面積從小到大排列,作為9個anchor box。

模擬K-Means演算法:

  1. 建立測試點,X是資料,y是標籤,如X:(300,2), y:(300,);
  2. 將資料聚類為9類;
  3. 輸入資料X,訓練;
  4. 預測X的類別,為y_kmeans
  5. 使用scatter繪製散點圖,顏色範圍是viridis
  6. 獲取聚類中心cluster_centers_,以黑色(black)點表示;

原始碼:

import matplotlib.pyplot as plt
import seaborn as sns
sns.set()  # for plot styling
from sklearn.cluster import KMeans
from sklearn.datasets.samples_generator import make_blobs


def test_of_k_means():
    # 建立測試點,X是資料,y是標籤,X:(300,2), y:(300,)
    X, y_true = make_blobs(n_samples=300, centers=9, cluster_std=0.60, random_state=0)
    kmeans = KMeans(n_clusters=9)  # 將資料聚類
    kmeans.fit(X)  # 資料X
    y_kmeans = kmeans.predict(X)  # 預測

    # 顏色範圍viridis: https://matplotlib.org/examples/color/colormaps_reference.html
    plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, s=20, cmap='viridis')  # c是顏色,s是大小

    centers = kmeans.cluster_centers_  # 聚類的中心
    plt.scatter(centers[:, 0], centers[:, 1], c='black', s=40, alpha=0.5)  # 中心點為黑色

    plt.show()  # 展示


if __name__ == '__main__':
    test_of_k_means()
複製程式碼

輸出:

K-Means


補2. EarlyStopping

EarlyStopping是Callback(回撥類)的子類,Callback用於指定在每個階段開始和結束的時候執行的操作。在Callback中,有一些已經實現的簡單子類,如accval_acclossval_loss等,還有一些複雜子類,如ModelCheckpoint(用於儲存模型權重)和TensorBoard(用於畫圖)等。

Callback的回撥介面,如下:

def on_epoch_begin(self, epoch, logs=None):
def on_epoch_end(self, epoch, logs=None):
def on_batch_begin(self, batch, logs=None):
def on_batch_end(self, batch, logs=None):
def on_train_begin(self, logs=None):
def on_train_end(self, logs=None):
複製程式碼

EarlyStopping是用於提前停止訓練的Callback子類。具體地,當訓練或驗證集中的loss不再減小,即減小的程度小於某個閾值時,則會停止訓練。這樣做,可以提高調參效率,避免浪費資源。

在model的fit資料中,以列表形式設定callbacks回撥,支援設定多個Callback,如:

callbacks=[logging, checkpoint, reduce_lr, early_stopping]
複製程式碼

EarlyStopping的引數:

  • monitor:監控資料的型別,支援acc、val_acc、loss、val_loss等;
  • min_delta:停止的閾值,與mode引數配合,增加或下降最少的閾值;
  • mode:min是最少,max是最多,auto是自動,與min_delta配合;
  • patience:達到閾值之後,能夠容忍的epoch數,避免停止在抖動中;
  • verbose:日誌的繁雜程度,值越大,輸出的資訊越多。

min_delta和patience需要相互配合,避免模型停止在抖動過程中,在設定的時候,需要相互協調。min_delta降低,patience減少;min_delta增加,則patience增加。

例項:

early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)
複製程式碼

OK, that's all! Enjoy it!

歡迎關注,微信公眾號 深度演算法 (ID: DeepAlgorithm) ,瞭解更多深度技術!

相關文章