探索 YOLO v3 實現細節 - 第5篇 Loss

SpikeKing發表於2018-08-15

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

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

YOLO

YOLO是一句美國的俗語,You Only Live Once,你只能活一次,即人生苦短,及時行樂。

本文主要分享,如何實現YOLO v3的演算法細節,Keras框架。這是第5篇,損失函式Loss,精巧地設計,中心點、寬高、框置信度和類別置信度等4個部分的損失值。當然還有第6篇,至第n篇,畢竟,這是一個完整版 :)。

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

已更新:

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


1. 損失層

在模型的訓練過程中,不斷調整網路中的引數,優化損失函式loss的值達到最小,完成模型的訓練。在YOLO v3中,損失函式yolo_loss封裝自定義Lambda的損失層中,作為模型的最後一層,參於訓練。損失層Lambda的輸入是已有模型的輸出model_body.output和真值y_true,輸出是1個值,即損失值。

損失層的核心邏輯位於yolo_loss中,yolo_loss除了接收Lambda層的輸入model_body.output和y_true,還接收錨框anchors、類別數num_classes和過濾閾值ignore_thresh等3個引數。

實現:

model_loss = Lambda(yolo_loss,
                    output_shape=(1,), name='yolo_loss',
                    arguments={'anchors': anchors,
                               'num_classes': num_classes,
                               'ignore_thresh': 0.5}
                    )(model_body.output + y_true)
複製程式碼

其中,model_body.output是已有模型的預測值,y_true是真實值,兩者的格式相同,如下:

model_body: [(?, 13, 13, 18), (?, 26, 26, 18), (?, 52, 52, 18)]
y_true: [(?, 13, 13, 18), (?, 26, 26, 18), (?, 52, 52, 18)]
複製程式碼

接著,在yolo_loss方法中,引數是:

  • args是Lambda層的輸入,即model_body.output和y_true的組合;
  • anchors是二維陣列,結構是(9, 2),即9個anchor box;
  • num_classes是類別數;
  • ignore_thresh是過濾閾值;
  • print_loss是列印損失函式的開關;

即:

def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=True):
複製程式碼

2. 引數

在損失方法yolo_loss中,需要設定若干引數:

  • num_layers:層的數量,是anchors數量的3分之1;
  • yolo_outputsy_true:分離args,前3個是yolo_outputs預測值,後3個是y_true真值;
  • anchor_mask:anchor box的索引陣列,3個1組倒序排序,678對應13x13,345對應26x26,123對應52x52;即[[6, 7, 8], [3, 4, 5], [0, 1, 2]];
  • input_shapeK.shape(yolo_outputs[0])[1:3],第1個預測矩陣yolo_outputs[0]的結構(shape)的第1~2位,即(?, 13, 13, 18)中的(13, 13)。再x32,就是YOLO網路的輸入尺寸,即(416, 416),因為在網路中,含有5個步長為(2, 2)的卷積操作,降維32=5^2倍;
  • grid_shapes:與input_shape類似,K.shape(yolo_outputs[l])[1:3],以列表的形式,選擇3個尺寸的預測圖維度,即[(13, 13), (26, 26), (52, 52)];
  • m:第1個預測圖的結構的第1位,即K.shape(yolo_outputs[0])[0],輸入模型的圖片總量,即批次數;
  • mf:m的float型別,即K.cast(m, K.dtype(yolo_outputs[0]))
  • loss:損失值為0;

即:

num_layers = len(anchors) // 3  # default setting
yolo_outputs = args[:num_layers]
y_true = args[num_layers:]
anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] if num_layers == 3 else [[3, 4, 5], [1, 2, 3]]
# input_shape是輸出的尺寸*32, 就是原始的輸入尺寸,[1:3]是尺寸的位置,即416x416
input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))
# 每個網格的尺寸,組成列表
grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]

m = K.shape(yolo_outputs[0])[0]  # batch size, tensor
mf = K.cast(m, K.dtype(yolo_outputs[0]))

loss = 0
複製程式碼

3. 預測資料

yolo_head中,將預測圖yolo_outputs[l],拆分為邊界框的起始點xy、寬高wh、置信度confidence和類別概率class_probs。輸入引數:

  • yolo_outputs[l]或feats:第l個預測圖,如(?, 13, 13, 18);
  • anchors[anchor_mask[l]]或anchors:第l個anchor box,如[(116, 90), (156,198), (373,326)];
  • num_classes:類別數,如1個;
  • input_shape:輸入圖片的尺寸,Tensor,值為(416, 416);
  • calc_loss:計算loss的開關,在計算損失值時,calc_loss開啟,為True;

即:

grid, raw_pred, pred_xy, pred_wh = \
    yolo_head(yolo_outputs[l], anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)
    
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
複製程式碼

接著,統計anchors的數量num_anchors,即3個。將anchors轉換為與預測圖feats維度相同的Tensor,即anchors_tensor的結構是(1, 1, 1, 3, 2),即:

num_anchors = len(anchors)
# Reshape to batch, height, width, num_anchors, box_params.
anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])
複製程式碼

下一步,建立網格grid:

  • 獲取網格的尺寸grid_shape,即預測圖feats的第1~2位,如13x13;
  • grid_ygrid_x用於生成網格grid,通過arange、reshape、tile的組合,建立y軸的0~12的組合grid_y,再建立x軸的0~12的組合grid_x,將兩者拼接concatenate,就是grid;
  • grid是遍歷二元數值組合的數值,結構是(13, 13, 1, 2);

即:

grid_shape = K.shape(feats)[1:3]
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))
複製程式碼

下一步,將feats的最後一維展開,將anchors與其他資料(類別數+4個框值+框置信度)分離

feats = K.reshape(
    feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
複製程式碼

下一步,計算起始點xy、寬高wh、框置信度box_confidence和類別置信度box_class_probs

  • 起始點xy:將feats中xy的值,經過sigmoid歸一化,再加上相應的grid的二元組,再除以網格邊長,歸一化;
  • 寬高wh:將feats中wh的值,經過exp正值化,再乘以anchors_tensor的anchor box,再除以圖片寬高,歸一化;
  • 框置信度box_confidence:將feats中confidence值,經過sigmoid歸一化;
  • 類別置信度box_class_probs:將feats中class_probs值,經過sigmoid歸一化;

即:

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:])
複製程式碼

其中,xywh的計算公式,tx、ty、tw和th是feats值,而bx、by、bw和bh是輸出值,如下:

xywh

這4個值box_xy, box_wh, box_confidence, box_class_probs的範圍均在0~1之間。

由於計算損失值,calc_loss為True,則返回:

  • 網格grid:結構是(13, 13, 1, 2),數值為0~12的全遍歷二元組;
  • 預測值feats:經過reshape變換,將18維資料分離出3維anchors,結構是(?, 13, 13, 3, 6)
  • box_xybox_wh歸一化的起始點xy和寬高wh,xy的結構是(?, 13, 13, 3, 2),wh的結構是(?, 13, 13, 3, 2);box_xy的範圍是(0~1),box_wh的範圍是(0~1);即bx、by、bw、bh計算完成之後,再進行歸一化。

即:

if calc_loss == True:
    return grid, feats, box_xy, box_wh
複製程式碼

4. 損失函式

在計算損失值時,迴圈計算每1層的損失值,累加到一起,即

for l in range(num_layers):
        // ... 
        loss += xy_loss + wh_loss + confidence_loss + class_loss
複製程式碼

在每個迴圈體中:

  • 獲取物體置信度object_mask,最後1個維度的第4位,第0~3位是框,第4位是物體置信度;
  • 類別置信度true_class_probs,最後1個維度的第5位;

即:

object_mask = y_true[l][..., 4:5]
true_class_probs = y_true[l][..., 5:]
複製程式碼

接著,呼叫yolo_head重構預測圖,輸出:

  • 網格grid:結構是(13, 13, 1, 2),數值為0~12的全遍歷二元組;
  • 預測值raw_pred:經過reshape變換,將anchors分離,結構是(?, 13, 13, 3, 6)
  • pred_xypred_wh歸一化的起始點xy和寬高wh,xy的結構是(?, 13, 13, 3, 2),wh的結構是(?, 13, 13, 3, 2);

再將xy和wh組合成預測框pred_box,結構是(?, 13, 13, 3, 4)。

grid, raw_pred, pred_xy, pred_wh = \
    yolo_head(yolo_outputs[l], anchors[anchor_mask[l]], 
              num_classes, input_shape, calc_loss=True)
pred_box = K.concatenate([pred_xy, pred_wh])
複製程式碼

接著,生成真值資料:

  • raw_true_xy:在網格中的中心點xy,偏移資料,值的範圍是0~1;y_true的第0和1位是中心點xy的相對位置,範圍是0~1;
  • raw_true_wh:在網路中的wh針對於anchors的比例,再轉換為log形式,範圍是有正有負;y_true的第2和3位是寬高wh的相對位置,範圍是0~1;
  • box_loss_scale:計算wh權重,取值範圍(1~2);

實現:

# Darknet raw box to calculate loss.
raw_true_xy = y_true[l][..., :2] * grid_shapes[l][::-1] - grid
raw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1])  # 1
raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh))  # avoid log(0)=-inf
box_loss_scale = 2 - y_true[l][..., 2:3] * y_true[l][..., 3:4]  # 2-w*h
複製程式碼

接著,根據IoU忽略閾值生成ignore_mask,將預測框pred_box和真值框true_box計算IoU,抑制不需要的anchor框的值,即IoU小於最大閾值的anchor框。ignore_mask的shape是(?, ?, ?, 3, 1),第0位是批次數,第1~2位是特徵圖尺寸。

實現:

ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
object_mask_bool = K.cast(object_mask, 'bool')

def loop_body(b, ignore_mask):
    true_box = tf.boolean_mask(y_true[l][b, ..., 0:4], object_mask_bool[b, ..., 0])
    iou = box_iou(pred_box[b], true_box)
    best_iou = K.max(iou, axis=-1)
    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()
ignore_mask = K.expand_dims(ignore_mask, -1)
複製程式碼

損失函式:

  • xy_loss:中心點的損失值。object_masky_true的第4位,即是否含有物體,含有是1,不含是0。box_loss_scale的值,與物體框的大小有關,2減去相對面積,值得範圍是(1~2)。binary_crossentropy是二值交叉熵。
  • wh_loss:寬高的損失值。除此之外,額外乘以係數0.5,平方K.square()。
  • confidence_loss:框的損失值。兩部分組成,第1部分是存在物體的損失值,第2部分是不存在物體的損失值,其中乘以忽略掩碼ignore_mask,忽略預測框中IoU小於閾值的框。
  • class_loss:類別損失值。
  • 將各部分損失值的和,除以均值,累加,作為最終的圖片損失值。

細節實現:

object_mask = y_true[l][..., 4:5]  # 物體掩碼
box_loss_scale = 2 - y_true[l][..., 2:3] * y_true[l][..., 3:4]  # 框損失比例
z * -log(sigmoid(x)) + (1 - z) * -log(1 - sigmoid(x))  # 二值交叉熵函式
iou = box_iou(pred_box[b], true_box)  # 預測框與真正框的IoU
複製程式碼

損失函式實現:

xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[..., 0:2],
                                                               from_logits=True)
wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh - raw_pred[..., 2:4])
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)

xy_loss = K.sum(xy_loss) / mf
wh_loss = K.sum(wh_loss) / mf
confidence_loss = K.sum(confidence_loss) / mf
class_loss = K.sum(class_loss) / mf
loss += xy_loss + wh_loss + confidence_loss + class_loss
複製程式碼

YOLO v1的損失函式公式,與v3略有不同,作為參考:

Loss


補充

1. “...”操作符

在Python中,“...”(ellipsis)操作符,表示其他維度不變,只操作最前或最後1維;

import numpy as np

x = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
"""
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
"""
print(x.shape)  # (3, 4)
y = x[1:2, ...]
"""
[[5 6 7 8]]
"""
print(y)
複製程式碼

2. 遍歷數值組合

在YOLO v3中,當計算網格值時,需要由相對位置,轉換為絕對位置,就是相對值,加上網格的左上角的值,如相對值(0.2, 0.3)在第(1, 1)網格中的絕對值是(1.2, 1.3)。當轉換座標值時,根據座標點的位置,新增相應的初始值即可。這樣,就需要遍歷兩兩的數值組合,如生成0至12的網格矩陣。

通過arange -> reshape -> tile -> concatenate的組合,即可快速完成。

原始碼:

from keras import backend as K

grid_y = K.tile(K.reshape(K.arange(0, stop=3), [-1, 1, 1]), [1, 3, 1])
grid_x = K.tile(K.reshape(K.arange(0, stop=3), [1, -1, 1]), [3, 1, 1])

sess = K.get_session()
print(grid_x.shape)  # (3, 3, 1)
print(grid_y.shape)  # (3, 3, 1)
z = K.concatenate([grid_x, grid_y])
print(z.shape)  # (3, 3, 2)
print(sess.run(z))
"""
建立3x3的二維矩陣,遍歷全部陣列0~2
"""
複製程式碼

3. ::-1

“::-1”是顛倒陣列的值,例如:

import numpy as np

a = np.array([1, 2, 3, 4, 5])
print a[::-1]
"""
[5 4 3 2 1]
"""
複製程式碼

4. Session

在Keras中,使用Session測試驗證資料,實現:

from keras import backend as K

sess = K.get_session()
a = K.constant([2, 4])
b = K.constant([3, 2])
c = K.square(a - b)
print(sess.run(c))
複製程式碼

OK, that's all! Enjoy it!

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

相關文章