探索 YOLO v3 實現細節 - 第1篇 訓練
來自專欄視覺隨筆67 人贊了文章
YOLO,即You Only Look Once的縮寫,是一個基於卷積神經網路(CNN)的物體檢測演算法。而YOLO v3是YOLO的第3個版本,即YOLO、YOLO 9000、YOLO v3,檢測效果,更准更強。
更多細節,可以參考YOLO的官網。
YOLO是一句美國的俗語,You Only Live Once,人生苦短,及時行樂。
本文主要分享,如何實現YOLO v3的演算法細節,Keras框架。這是第1篇,訓練。當然還有第2篇,至第n篇,畢竟,這是一個完整版 :)
本文的GitHub源碼:https://github.com/SpikeKing/keras-yolo3-detection
已更新:
- 第1篇 訓練:https://mp.weixin.qq.com/s/T9LshbXoervdJDBuP564dQ
- 第2篇 模型:https://mp.weixin.qq.com/s/N79S9Qf1OgKsQ0VU5QvuHg
- 第3篇 網路:https://mp.weixin.qq.com/s/hC4P7iRGv5JSvvPe-ri_8g
歡迎關注我的知乎:https://www.zhihu.com/people/spikeking
歡迎關注,微信公眾號深度演算法(ID: DeepAlgorithm) ,了解更多深度技術!
1. 參數
模型的訓練參數,共有5個,即:
(1) 已標註邊界框(Bounding Box)的圖片數據集,其格式如下:
圖片的位置 框的4個坐標和1個類別ID (xmin,ymin,xmax,ymax,id) ...dataset/image.jpg 788,351,832,426,0 805,208,855,270,0
(2) 標註框類別的匯總,即數據集中所標註物體的全部類別,例如:
aeroplanebicyclebird...
(3) 預訓練模型,用於遷移學習(Transfer Learning)中的微調(Fine Tune),可選YOLO v3已訓練完成的COCO(Common Objects in Context)模型權重,即:
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,選擇416的原因是:
- 圖片尺寸滿足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_true和y_pred:
- 把y_true當成輸入,作為模型的多輸入,把loss封裝為層(即Lambda層),作為模型的輸出;
- 在模型中,最終輸出的y_pred就是loss;
- 在編譯(compile)時,將loss設置為y_pred即可,無視y_true;
- 在訓練時,隨意添加一個符合結構的y_true即可。
Python的Lambda表達式:
f = lambda y_true, y_pred: y_predprint(f(1, 2)) # 輸出2
模型fit數據,使用數據生成包裝器(data_generator_wrapper
),按批次生成訓練和驗證數據。最終,模型model存儲權重。實現如下:
batch_size = 32 # batchmodel.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 = Truemodel.compile(optimizer=Adam(lr=1e-4), loss={yolo_loss: lambda y_true, y_pred: y_pred})
第2階段的模型fit數據,與第1階段類似,從第50個epoch開始,一直訓練到第100個epoch,當觸發條件時,則提前終止。額外增加了兩個回調reduce_lr
和early_stopping
,用於控制訓練提取終止的時機:
- reduce_lr:當評價指標不在提升時,減少學習率,每次減少10%(factor),當驗證損失值(val_loss),持續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 = 32model.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演算法:
- 創建測試點,X是數據,y是標籤,如X:(300,2), y:(300,);
- 將數據聚類為9類;
- 輸入數據X,訓練;
- 預測X的類別,為y_kmeans;
- 使用scatter繪製散點圖,顏色範圍是viridis;
- 獲取聚類中心cluster_centers_,以黑色(black)點表示;
源碼:
import matplotlib.pyplot as pltimport seaborn as snssns.set() # for plot stylingfrom sklearn.cluster import KMeansfrom sklearn.datasets.samples_generator import make_blobsdef 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()
輸出:
補充2. EarlyStopping
EarlyStopping是Callback(回調類)的子類,Callback用於指定在每個階段開始和結束時,執行的操作。在Callback中,有一些已經實現的簡單子類,如acc、val、loss和val_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, thats all! Enjoy it!
By C. L. Wang @ 美圖雲 視覺技術部
歡迎關注,微信公眾號深度演算法(ID: DeepAlgorithm) ,了解更多深度技術!
推薦閱讀:
※從DensNet到CliqueNet,解讀北大在卷積架構上的探索
※一次有趣的面試(上)
※複雜環境下的目標視覺檢測 | 人工場景與實際場景平行研究三部曲 | 新損失函數與組歸一化
※KIT: Automotive Vision汽車視覺學習筆記(1)Introduction
※Image Process PipeLine 之 DM(上)
TAG:圖像處理 | 計算機視覺 | 深度學習DeepLearning |