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原始碼:github.com/SpikeKing/k…
已更新:
- 第1篇 訓練:mp.weixin.qq.com/s/T9LshbXoe…
- 第2篇 模型:mp.weixin.qq.com/s/N79S9Qf1O…
- 第3篇 網路:mp.weixin.qq.com/s/hC4P7iRGv…
- 第4篇 真值:mp.weixin.qq.com/s/5Sj7QadfV…
- 第5篇 Loss:mp.weixin.qq.com/s/4L9E4WGSh…
歡迎關注,微信公眾號 深度演算法 (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_true
和y_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_lr
和early_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演算法:
- 建立測試點,X是資料,y是標籤,如X:(300,2), y:(300,);
- 將資料聚類為9類;
- 輸入資料X,訓練;
- 預測X的類別,為
y_kmeans
; - 使用scatter繪製散點圖,顏色範圍是viridis;
- 獲取聚類中心
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()
複製程式碼
輸出:
補2. EarlyStopping
EarlyStopping是Callback(回撥類)的子類,Callback用於指定在每個階段開始和結束的時候執行的操作。在Callback中,有一些已經實現的簡單子類,如acc
、val_acc
、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, that's all! Enjoy it!
歡迎關注,微信公眾號 深度演算法 (ID: DeepAlgorithm) ,瞭解更多深度技術!