YOLOv3 算法的一點理解

    <p>今天講一講 YOLOv3, 目標檢測網絡的巔峯之作, 疾如風,快如閃電。</p>

算法背景

假設我們想對下面這張 416 X 416 大小的圖片進行預測,把圖中 dog、bicycle 和 car 三種物體給框出來,這涉及到以下三個過程:

  • 怎麼在圖片上找出很多有價值的候選框?
  • 接着判斷候選框裏有沒有物體?
  • 如果有物體的話,那麼它屬於哪個類別?

聽起來就像把大象裝進冰箱,分三步走。事實上,目前的 anchor-based 機制算法例如 RCNN、Faster rcnn 以及 YOLO 算法都是這個思想。最早的時候,RCNN 是這麼幹的,它首先利用 Selective Search 的方法通過圖片上像素之間的相似度和紋理特徵進行區域合併,然後提出很多候選框並餵給 CNN 網絡提取出特徵向量 (embeddings),最後利用特徵向量訓練 SVM 來對目標和背景進行分類。

image

這是最早利用神經網絡進行目標檢測的開山之作,雖然現在看來有不少瑕疵,例如:

  • Selective Search 會在圖片上提取2000個候選區域,每個候選區域都會餵給 CNN 進行特徵提取,這個過程太冗餘啦,其實這些候選區域之間很多特徵其實是可以共享的;
  • 由於 CNN 最後一層是全連接層,因此輸入圖片的尺寸大小也有限制,只能進行 Crop 或者 Warp,這樣一來圖片就會扭曲、變形和失真;
  • 在利用 SVM 分類器對候選框進行分類的時候,每個候選框的特徵向量都要保留在磁盤上,很浪費空間!

儘管如此,但仍不可否認它具有劃時代的意義,至少告訴後人我們是可以利用神經網絡進行目標檢測的。後面,一些大神們在此基礎上提出了很多改進,從 Fast RCNN 到 Faster RCNN 再到 Mask RCNN, 目標檢測的 region proposal 過程變得越來越有針對性,並提出了著名的 RPN 網絡去學習如何給出高質量的候選框,然後再去判斷所屬物體的類別。簡單說來就是: 提出候選框,然後分類,這就是我們常說的 two-stage 算法。two-stage 算法的好處就是精度較高,但是檢測速度滿足不了實時性的要求。

在這樣的背景下,YOLO 算法橫空出世,江湖震驚!

YOLO 算法簡介

發展歷程

2015 年 Redmon J 等提出 YOLO 網絡, 其特點是將生成候選框與分類迴歸合併成一個步驟, 預測時特徵圖被分成 7x7 個 cell, 對每個 cell 進行預測, 這就大大降低了計算複雜度, 加快了目標檢測的速度, 幀率最高可達 45 fps!

時隔一年,Redmon J 再次提出了YOLOv2, 與前代相比: YOLOv1是利用全連接層直接預測bounding box的座標,而YOLOv2借鑑了Faster R-CNN的思想,引入anchor。它在VOC2007 測試集上的 mAP 由 67.4% 提高到 78.6%, 然而由於一個 cell 只負責預測一個物體, 面對重疊性的目標的識別得並不夠好。

最終在 2018 年 4 月, 作者又發佈了第三個版本 YOLOv3,它延續了 YOLOv2 的 anchor 策略,沒有太大變化,主要的改變在於融合了多尺度特徵。結果在 COCO 數據集上的 mAP-50 由 YOLOv2 的 44.0% 提高到 57.9%, 與 mAP 61.1% 的 RetinaNet 相比, RetinaNet 在輸入尺寸 500×500 的情況下檢測速度約 98 ms/幀, 而 YOLOv3 在輸入尺寸 416×416 時檢測速 度可達 29 ms/幀。

上面這張圖足以秒殺一切, 說明 YOLOv3 在保證速度的前提下, 也達到了很高的準確率。

基本思想

作者在YOLO算法中把物體檢測(object detection)問題處理成迴歸問題,並將圖像分爲S×S的網格。如果一個目標的中心落入格子,該格子就負責檢測該目標。

If the center of an object falls into a grid cell, that grid cell is responsible for detecting that object.

每個網格都會輸出 bounding box,confidence 和 class probability map。其中:

  • bounding box 包含4個值:x,y,w,h,(x,y)代表 box 的中心。(w,h)代表 box 的寬和高;
  • confidence 表示這個預測框中包含物體的概率,其實也是預測框與真實框之間的 iou 值;
  • class probability 表示的是該物體的類別概率,在 YOLOv3 中採用的是二分類的方法。

網絡結構

下面這幅圖就是 YOLOv3 網絡的整體結構,在圖中我們可以看到:尺寸爲 416X416 的輸入圖片進入 Darknet-53 網絡後得到了 3 個分支,這些分支在經過一系列的卷積、上採樣以及合併等操作後最終得到了三個尺寸不一的 feature map,形狀分別爲 [13, 13, 255]、[26, 26, 255] 和 [52, 52, 255]。

image

講了這麼多,還是不如看代碼來得親切。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def YOLOv3(input_layer):
# 輸入層進入 Darknet-53 網絡後,得到了三個分支
route_1, route_2, conv = backbone.darknet53(input_layer)
# 見上圖中的橘黃色模塊(DBL),一共需要進行5次卷積操作
conv = common.convolutional(conv, (1, 1, 1024, 512))
conv = common.convolutional(conv, (3, 3, 512, 1024))
conv = common.convolutional(conv, (1, 1, 1024, 512))
conv = common.convolutional(conv, (3, 3, 512, 1024))
conv = common.convolutional(conv, (1, 1, 1024, 512))
conv_lobj_branch = common.convolutional(conv, (3, 3, 512, 1024))
# conv_lbbox 用於預測大尺寸物體,shape = [None, 13, 13, 255]
conv_lbbox = common.convolutional(conv_lobj_branch, (1, 1, 1024, 3*(NUM_CLASS + 5)),
activate=False, bn=False)
conv = common.convolutional(conv, (1, 1, 512, 256))
# 這裏的 upsample 使用的是最近鄰插值方法,這樣的好處在於上採樣過程不需要學習,從而減少了網絡參數
conv = common.upsample(conv)
conv = tf.concat([conv, route_2], axis=-1)
conv = common.convolutional(conv, (1, 1, 768, 256))
conv = common.convolutional(conv, (3, 3, 256, 512))
conv = common.convolutional(conv, (1, 1, 512, 256))
conv = common.convolutional(conv, (3, 3, 256, 512))
conv = common.convolutional(conv, (1, 1, 512, 256))
conv_mobj_branch = common.convolutional(conv, (3, 3, 256, 512))
# conv_mbbox 用於預測中等尺寸物體,shape = [None, 26, 26, 255]
conv_mbbox = common.convolutional(conv_mobj_branch, (1, 1, 512, 3*(NUM_CLASS + 5)),
activate=False, bn=False)
conv = common.convolutional(conv, (1, 1, 256, 128))
conv = common.upsample(conv)
conv = tf.concat([conv, route_1], axis=-1)
conv = common.convolutional(conv, (1, 1, 384, 128))
conv = common.convolutional(conv, (3, 3, 128, 256))
conv = common.convolutional(conv, (1, 1, 256, 128))
conv = common.convolutional(conv, (3, 3, 128, 256))
conv = common.convolutional(conv, (1, 1, 256, 128))

conv_sobj_branch = common.convolutional(conv, (3, 3, 128, 256))
# conv_sbbox 用於預測小尺寸物體,shape = [None, 52, 52, 255]
conv_sbbox = common.convolutional(conv_sobj_branch, (1, 1, 256, 3*(NUM_CLASS +5)),
activate=False, bn=False)
return [conv_sbbox, conv_mbbox, conv_lbbox]

Darknet53 結構

Darknet-53 的主體框架如下圖所示,它主要由 Convolutional 和 Residual 結構所組成。需要特別注意的是,最後三層 Avgpool、Connected 和 softmax layer 是用於在 Imagenet 數據集上作分類訓練用的。當我們用 Darknet-53 層對圖片提取特徵時,是不會用到這三層的。

Darknet-53 有多牛逼?看看下面這張圖,作者進行了比較,得出的結論是 Darknet-53 在精度上可以與最先進的分類器進行媲美,同時它的浮點運算更少,計算速度也最快。和 ReseNet-101 相比,Darknet-53 網絡的速度是前者的1.5倍;雖然 ReseNet-152 和它性能相似,但是用時卻是它的2倍以上。

此外,Darknet-53 還可以實現每秒最高的測量浮點運算,這就意味着網絡結構可以更好地利用 GPU,從而使其測量效率更高,速度也更快。

Convolutional 結構

Convolutional 結構其實很簡單,就是普通的卷積層,其實沒啥講的。但是對於 if downsample 的情況,初學者可能覺得有點陌生, ZeroPadding2D 是什麼層?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def convolutional(input_layer, filters_shape, downsample=False, activate=True, bn=True):
if downsample:
input_layer = tf.keras.layers.ZeroPadding2D(((1, 0), (1, 0)))(input_layer)
padding = 'valid'
strides = 2
else:
strides = 1
padding = 'same'
conv = tf.keras.layers.Conv2D(filters=filters_shape[-1],
kernel_size = filters_shape[0],
strides=strides, padding=padding, use_bias=not bn,
kernel_regularizer=tf.keras.regularizers.l2(0.0005),
kernel_initializer=tf.random_normal_initializer(stddev=0.01),
bias_initializer=tf.constant_initializer(0.))(input_layer)
if bn: conv = BatchNormalization()(conv)
if activate == True: conv = tf.nn.leaky_relu(conv, alpha=0.1)
return conv

講到 ZeroPadding2D層,我們得先了解它是什麼,爲什麼有這個層。對於它的定義,Keras 官方給了很好的解釋:

keras.layers.convolutional.ZeroPadding2D(padding=(1, 1), data_format=None) 說明: 對2D輸入(如圖片)的邊界填充0,以控制卷積以後特徵圖的大小

其實就是對圖片的上下左右四個邊界填充0而已,padding=((top_pad, bottom_pad), (left_pad, right_pad))。 很簡單吧,快打開你的 ipython 試試吧!

1
2
3
4
5
6
7
In [2]: x=tf.keras.layers.Input([416,416,3])

In [3]: tf.keras.layers.ZeroPadding2D(padding=((1,0),(1,0)))(x)
Out[3]: <tf.Tensor 'zero_padding2d/Identity:0' shape=(None, 417, 417, 3) dtype=float32>

In [4]: tf.keras.layers.ZeroPadding2D(padding=((1,1),(1,1)))(x)
Out[4]: <tf.Tensor 'zero_padding2d_1/Identity:0' shape=(None, 418, 418, 3) dtype=float32>

Residual 殘差模塊

殘差模塊最顯著的特點是使用了 short cut 機制(有點類似於電路中的短路機制)來緩解在神經網絡中增加深度帶來的梯度消失問題,從而使得神經網絡變得更容易優化。它通過恆等映射(identity mapping)的方法使得輸入和輸出之間建立了一條直接的關聯通道,從而使得網絡集中學習輸入和輸出之間的殘差。

1
2
3
4
5
6
def residual_block(input_layer, input_channel, filter_num1, filter_num2):
short_cut = input_layer
conv = convolutional(input_layer, filters_shape=(1, 1, input_channel, filter_num1))
conv = convolutional(conv , filters_shape=(3, 3, filter_num1, filter_num2))
residual_output = short_cut + conv
return residual_output

提取特徵

要想詳細地知道 YOLO 的預測過程,就非常有必要先來了解一下什麼是特徵映射 (feature map) 和特徵向量 (embeddings)。

特徵映射

當我們談及 CNN 網絡,總能聽到 feature map 這個詞。它也叫特徵映射,簡單說來就是輸入圖像在與卷積核進行卷積操作後得到圖像特徵

一般而言,CNN 網絡在對圖像自底向上提取特徵時,feature map 的數量(其實也對應的就是卷積核的數目) 會越來越多,而空間信息會越來越少,其特徵也會變得越來越抽象。比如著名的 VGG16 網絡,它的 feature map 變化就是這個樣子。

feature map 在空間尺寸上越來越小,但在通道尺寸上變得越來越深,這就是 VGG16 的特點。

特徵向量

講到 feature map 哦,就不得不提一下人臉識別領域裏經常提到的 embedding. 一般來說,它其實就是 feature map 被最後一層全連接層所提取到特徵向量。早在2006年,深度學習鼻祖 hinton 就在《SCIENCE》上發表了一篇論文,首次利用自編碼網絡對 mnist 手寫數字提取出了特徵向量(一個2維或3維的向量)。值得一提的是,也是這篇論文揭開了深度學習興起的序幕。

下面就是上面這張圖片裏的數字在 CNN 空間裏映射後得到的特徵向量在2維和3維空間裏的樣子:

前面我們提到:CNN 網絡在對圖像自底向上提取特徵時,得到的 feature map 一般都是在空間尺寸上越來越小,而在通道尺寸上變得越來越深。 那麼,爲什麼要這麼做?

其實,這就與 ROI (感興趣區域)映射到 Feature Map 有關。在上面這幅圖裏:原圖裏的一塊 ROI 在 CNN 網絡空間裏映射後,在 feature map 上空間尺寸會變得更小,甚至是一個點, 但是這個點的通道信息會很豐富,這些通道信息是 ROI 區域裏的圖片信息在 CNN 網絡裏映射得到的特徵表示。由於圖像中各個相鄰像素在空間上的聯繫很緊密,這在空間上造成具有很大的冗餘性。因此,我們往往會通過在空間上降維,而在通道上升維的方式來消除這種冗餘性,儘量以最小的維度來獲得它最本質的特徵。

原圖左上角紅色 ROI 經 CNN 映射後在 feature map 空間上只得到了一個點,但是這個點有85個通道。那麼,ROI的維度由原來的 [32, 32, 3] 變成了現在的 85 維,這難道又不是降維打擊麼?👊

按照我的理解,這其實就是 CNN 網絡對 ROI 進行特徵提取後得到的一個 85 維的特徵向量。這個特徵向量前4個維度代表的是候選框信息,中間這個維度代表是判斷有無物體的概率,後面80個維度代表的是對 80 個類別的分類概率信息。

如何檢測

多尺度檢測

YOLOv3 對輸入圖片進行了粗、中和細網格劃分,以便分別實現對大、中和小物體的預測。假如輸入圖片的尺寸爲 416X416, 那麼得到粗、中和細網格尺寸分別爲 13X13、26X26 和 52X52。這樣一算,那就是在長寬尺寸上分別縮放了 32、16 和 8 倍。

image

decode 處理

YOLOv3 網絡的三個分支輸出會被送入 decode 函數中對 Feature Map 的通道信息進行解碼。 在下面這幅圖裏:黑色虛線框代表先驗框(anchor),藍色框表示的是預測框.

  • bhbh 則代表網格左上角的座標。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def decode(conv_output, i=0):
# 這裏的 i=0、1 或者 2, 以分別對應三種網格尺度
conv_shape = tf.shape(conv_output)
batch_size = conv_shape[0]
output_size = conv_shape[1]
conv_output = tf.reshape(conv_output, (batch_size, output_size,
output_size, 3, 5 + NUM_CLASS))
conv_raw_dxdy = conv_output[:, :, :, :, 0:2] # 中心位置的偏移量
conv_raw_dwdh = conv_output[:, :, :, :, 2:4] # 預測框長寬的偏移量
conv_raw_conf = conv_output[:, :, :, :, 4:5] # 預測框的置信度
conv_raw_prob = conv_output[:, :, :, :, 5: ] # 預測框的類別概率
# 好了,接下來需要畫網格了。其中,output_size 等於 13、26 或者 52
y = tf.tile(tf.range(output_size, dtype=tf.int32)[:, tf.newaxis], [1, output_size])
x = tf.tile(tf.range(output_size, dtype=tf.int32)[tf.newaxis, :], [output_size, 1])
xy_grid = tf.concat([x[:, :, tf.newaxis], y[:, :, tf.newaxis]], axis=-1)
xy_grid = tf.tile(xy_grid[tf.newaxis, :, :, tf.newaxis, :], [batch_size, 1, 1, 3, 1])
xy_grid = tf.cast(xy_grid, tf.float32) # 計算網格左上角的位置
# 根據上圖公式計算預測框的中心位置
pred_xy = (tf.sigmoid(conv_raw_dxdy) + xy_grid) * STRIDES[i]
# 根據上圖公式計算預測框的長和寬大小
pred_wh = (tf.exp(conv_raw_dwdh) * ANCHORS[i]) * STRIDES[i]
pred_xywh = tf.concat([pred_xy, pred_wh], axis=-1)
pred_conf = tf.sigmoid(conv_raw_conf) # 計算預測框裏object的置信度
pred_prob = tf.sigmoid(conv_raw_prob) # 計算預測框裏object的類別概率
return tf.concat([pred_xywh, pred_conf, pred_prob], axis=-1)

NMS 處理

非極大值抑制(Non-Maximum Suppression,NMS),顧名思義就是抑制不是極大值的元素,說白了就是去除掉那些重疊率較高並且 score 評分較低的邊界框。 NMS 的算法非常簡單,迭代流程如下:

  • 流程1: 判斷邊界框的數目是否大於0,如果不是則結束迭代;
  • 流程2: 按照 socre 排序選出評分最大的邊界框 A 並取出;
  • 流程3: 計算這個邊界框 A 與剩下所有邊界框的 iou 並剔除那些 iou 值高於閾值的邊界框,重複上述步驟;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 流程1: 判斷邊界框的數目是否大於0
while len(cls_bboxes) > 0:
# 流程2: 按照 socre 排序選出評分最大的邊界框 A
max_ind = np.argmax(cls_bboxes[:, 4])
# 將邊界框 A 取出並剔除
best_bbox = cls_bboxes[max_ind]
best_bboxes.append(best_bbox)
cls_bboxes = np.concatenate([cls_bboxes[: max_ind], cls_bboxes[max_ind + 1:]])
# 流程3: 計算這個邊界框 A 與剩下所有邊界框的 iou 並剔除那些 iou 值高於閾值的邊界框
iou = bboxes_iou(best_bbox[np.newaxis, :4], cls_bboxes[:, :4])
weight = np.ones((len(iou),), dtype=np.float32)
iou_mask = iou > iou_threshold
weight[iou_mask] = 0.0
cls_bboxes[:, 4] = cls_bboxes[:, 4] * weight
score_mask = cls_bboxes[:, 4] > 0.
cls_bboxes = cls_bboxes[score_mask]

最後所有取出來的邊界框 A 就是我們想要的。不妨舉個簡單的例子:假如5個邊界框及評分爲: A: 0.9,B: 0.08,C: 0.8, D: 0.6,E: 0.5,設定的評分閾值爲 0.3,計算步驟如下。

  • 步驟1: 邊界框的個數爲5,滿足迭代條件;
  • 步驟2: 按照 socre 排序選出評分最大的邊界框 A 並取出;
  • 步驟3: 計算邊界框 A 與其他 4 個邊界框的 iou,假設得到的 iou 值爲:B: 0.1,C: 0.7, D: 0.02, E: 0.09, 剔除邊界框 C;
  • 步驟4: 現在只剩下邊界框 B、D、E,滿足迭代條件;
  • 步驟5: 按照 socre 排序選出評分最大的邊界框 D 並取出;
  • 步驟6: 計算邊界框 D 與其他 2 個邊界框的 iou,假設得到的 iou 值爲:B: 0.06,E: 0.8,剔除邊界框 E;
  • 步驟7: 現在只剩下邊界框 B,滿足迭代條件;
  • 步驟8: 按照 socre 排序選出評分最大的邊界框 B 並取出;
  • 步驟9: 此時邊界框的個數爲零,結束迭代。

最後我們得到了邊界框 A、B、D,但其中邊界框 B 的評分非常低,這表明該邊界框是沒有物體的,因此應當拋棄掉。在代碼中:

1
2
3
4
# # (5) discard some boxes with low scores
classes = np.argmax(pred_prob, axis=-1)
scores = pred_conf * pred_prob[np.arange(len(pred_coor)), classes]
score_mask = scores > score_threshold

在 YOLO 算法中,NMS 的處理有兩種情況:一種是所有的預測框一起做 NMS 處理,另一種情況是分別對每個類別的預測框做 NMS 處理。後者會出現一個預測框既屬於類別 A 又屬於類別 B 的現象,這比較適合於一個小單元格中同時存在多個物體的情況。

anchor 響應機制

K-means 聚類

首先需要拋出一個問題:先驗框 anchor 是怎麼來的?對於這點,作者在 YOLOv2 論文裏給出了很好的解釋:

we run k-means clustering on the training set bounding boxes to automatically find good priors.

其實就是使用 k-means 算法對訓練集上的 boudnding box 尺度做聚類。此外,考慮到訓練集上的圖片尺寸不一,因此對此過程進行歸一化處理。

k-means 聚類算法有個坑爹的地方在於,類別的個數需要人爲事先指定。這就帶來一個問題,先驗框 anchor 的數目等於多少最合適?一般來說,anchor 的類別越多,那麼 YOLO 算法就越能在不同尺度下與真實框進行迴歸,但是這樣就會導致模型的複雜度更高,網絡的參數量更龐大。

We choose k = 5 as a good tradeoff between model complexity and high recall. If we use 9 centroids we see a much higher average IOU. This indicates that using k-means to generate our bounding box starts the model off with a better representation and makes the task easier to learn.

在上面這幅圖裏,作者發現 k = 5 時就能較好地實現高召回率與模型複雜度之間的平衡。由於在 YOLOv3 算法裏一共有3種尺度預測,因此只能是3的倍數,所以最終選擇了 9 個先驗框。這裏還有個問題需要解決,k-means 度量距離的選取很關鍵。距離度量如果使用標準的歐氏距離,大框框就會比小框產生更多的錯誤。在目標檢測領域,我們度量兩個邊界框之間的相似度往往以 IOU 大小作爲標準。因此,這裏的度量距離也和 IOU 有關。需要特別注意的是,這裏的IOU計算只用到了 boudnding box 的長和寬。在我的代碼裏,是認爲兩個先驗框的左上角位置是相重合的。(其實在這裏偏移至哪都無所謂,因爲聚類的時候是不考慮 anchor 框的位置信息的。)

d( box, centroid )=1IOU(box, centroid )d( box, centroid )=1−IOU(box, centroid )
,即歐幾里德範數,常用於計算向量的長度;

當 L1 或 L2 範數都相同的時候,發現 IoU 和 GIoU 的值差別都很大,這表明使用 L 範數來度量邊界框的距離是不合適的。在這種情況下,學術界普遍使用 IoU 來衡量兩個邊界框之間的相似性。作者發現使用 IoU 會有兩個缺點,導致其不太適合做損失函數:

  • 預測框和真實框之間沒有重合時,IoU 值爲 0, 導致優化損失函數時梯度也爲 0,意味着無法優化。例如,場景 A 和場景 B 的 IoU 值都爲 0,但是顯然場景 B 的預測效果較 A 更佳,因爲兩個邊界框的距離更近( L 範數更小)。

儘管場景 A 和場景 B 的 IoU 值都爲 0,但是場景 B 的預測效果較 A 更佳,這是因爲兩個邊界框的距離更近。

  • 即使預測框和真實框之間相重合且具有相同的 IoU 值時,檢測的效果也具有較大差異,如下圖所示。

上面三幅圖的 IoU = 0.33, 但是 GIoU 值分別是 0.33, 0.24 和 -0.1, 這表明如果兩個邊界框重疊和對齊得越好,那麼得到的 GIoU 值就會越高。

GIoU 的計算公式

the smallest enclosing convex object C 指的是最小閉合凸面 C,例如在上述場景 A 和 B 中,C 的形狀分別爲:

圖中綠色包含的區域就是最小閉合凸面 C,the smallest enclosing convex object。

1
2
3
4
5
6
7
8
9
10
11
12
13
def bbox_giou(boxes1, boxes2):
......
# 計算兩個邊界框之間的 iou 值
iou = inter_area / union_area
# 計算最小閉合凸面 C 左上角和右下角的座標
enclose_left_up = tf.minimum(boxes1[..., :2], boxes2[..., :2])
enclose_right_down = tf.maximum(boxes1[..., 2:], boxes2[..., 2:])
enclose = tf.maximum(enclose_right_down - enclose_left_up, 0.0)
# 計算最小閉合凸面 C 的面積
enclose_area = enclose[..., 0] * enclose[..., 1]
# 根據 GIoU 公式計算 GIoU 值
giou = iou - 1.0 * (enclose_area - union_area) / enclose_area
return giou

模型訓練

權重初始化

訓練神經網絡尤其是深度神經網絡所面臨的一個問題是,梯度消失或梯度爆炸,也就是說 當你訓練深度網絡時,導數或坡度有時會變得非常大,或非常小甚至以指數方式變小,這個時候我們看到的損失就會變成了 NaN。假設你正在訓練下面這樣一個極深的神經網絡,爲了簡單起見,這裏激活函數 g(z) = z 並且忽略偏置參數。

這裏我們首先假定 g(z)=z,b[l]=0g(z)=z,b[l]=0,激活函數的值將以指數形式遞減;

其實這裏直觀的理解是:如果權重 W 只比 1 略大一點,或者說只比單位矩陣大一點,深度神經網絡的輸出將會以爆炸式增長,而如果 W 比 1 略小一點,可能是 0.9, 0.9,每層網絡的輸出值將會以指數級遞減。因此合適的初始化權重值就顯得尤爲重要! 下面就寫個簡單的代碼給大家演示一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

x = np.random.randn(2000, 800) * 0.01 # 製作輸入數據
stds = [0.1, 0.05, 0.01, 0.005, 0.001] # 嘗試使用不同標準差,這樣初始權重大小也不一樣

for i, std in enumerate(stds):
# 第一層全連接層
dense_1 = tf.keras.layers.Dense(750, kernel_initializer=tf.random_normal_initializer(std), activation='tanh')
output_1 = dense_1(x)
# 第二層全連接層
dense_2 = tf.keras.layers.Dense(700, kernel_initializer=tf.random_normal_initializer(std), activation='tanh')
output_2 = dense_2(output_1)
# 第三層全連接層
dense_3 = tf.keras.layers.Dense(650, kernel_initializer=tf.random_normal_initializer(std), activation='tanh')
output_3 = dense_3(output_2).numpy().flatten()

plt.subplot(1, len(stds), i+1)
plt.hist(output_3, bins=60, range=[-1, 1])
plt.xlabel('std = %.3f' %std)
plt.yticks([])
plt.show()

我們可以看到當標準差較大( std = 0.1 和 0.05 )時,幾乎所有的輸出值集中在 -1 或1 附近,這表明此時的神經網絡發生了梯度爆炸;當標準差較小( std = 0.005 和 0.001)時,我們看到輸出值迅速向 0 靠攏,這表明此時的神經網絡發生了梯度消失。其實筆者也曾在 YOLOv3 網絡裏做過實驗,初始化權重的標準差如果太大或太小,都容易出現 NaN 。

學習率的設置

學習率是最影響性能的超參數之一,如果我們只能調整一個超參數,那麼最好的選擇就是它。 其實在我們的大多數的煉丹過程中,遇到 loss 變成 NaN 的情況大多數是由於學習率選擇不當引起的。

有句話講得好啊,步子大了容易扯到蛋。由於神經網絡在剛開始訓練的時候是非常不穩定的,因此剛開始的學習率應當設置得很低很低,這樣可以保證網絡能夠具有良好的收斂性。但是較低的學習率會使得訓練過程變得非常緩慢,因此這裏會採用以較低學習率逐漸增大至較高學習率的方式實現網絡訓練的“熱身”階段,稱爲 warmup stage。但是如果我們使得網絡訓練的 loss 最小,那麼一直使用較高學習率是不合適的,因爲它會使得權重的梯度一直來回震盪,很難使訓練的損失值達到全局最低谷。因此最後採用了這篇論文裏[8]的 cosine 的衰減方式,這個階段可以稱爲 consine decay stage。

1
2
3
4
5
6
if global_steps < warmup_steps:
lr = global_steps / warmup_steps *cfg.TRAIN.LR_INIT
else:
lr = cfg.TRAIN.LR_END + 0.5 * (cfg.TRAIN.LR_INIT - cfg.TRAIN.LR_END) * (
(1 + tf.cos((global_steps - warmup_steps) / (total_steps - warmup_steps) * np.pi))
)

加載預訓練模型

目前針對目標檢測的主流做法是基於 Imagenet 數據集預訓練的模型來提取特徵,然後在 COCO 數據集進行目標檢測fine-tunning訓練(比如 yolo 算法),也就是大家常說的遷移學習。其實遷移學習是建立在數據集分佈相似的基礎上的,像 yymnist 這種與 COCO 數據集分佈完全不同的情況,就沒有必要加載 COCO 預訓練模型的必要了吧。

在 tensorflow-yolov3 版本里,由於 README 裏訓練的是 VOC 數據集,因此推薦加載預訓練模型。由於在 YOLOv3 網絡的三個分支裏的最後卷積層與訓練的類別數目有關,因此除掉這三層的網絡權重以外,其餘所有的網絡權重都加載進來了。

下面是 tensorflow-yolov3 在 PASCAL VOC 2012 上比賽刷的成績,最後進了榜單的前十名。

參考文獻

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