【源碼解讀】YOLO v3 訓練 - 05 損失函數loss


摘要

  在損失函數計算的過程中,需要對模型的輸出即 feats進行相關信息的計算。 ---- 在yolo_head中

  當前小網格相對於大網格的位置(也可以理解為是相對於特征圖的位置)

  loss的計算時每一層結果均與真值進行誤差的累加計算。

  YOLO v3的損失函數與v1 的損失函數略有不同。損失函數的計算是在對應特征圖上的,而不是將其轉化至416,416的圖上,或者轉化到原圖上。

  YOLO v3使用多目標的方式進行分類,而不是使用softmax。

  一張圖像的輸出其最終對應的輸出尺寸為1×(3×(13×13+26×26+52×52)) × (5+k) = 1×10647 × (5+k)

  那其實根據真值篇所述,"正樣本只要IOU最大的,而不是超過某一閾值的都是正樣本",置信度正負樣本比例高達1:10647, 也就是說,在10647中,正樣本只有一個。所以計算損失時使用的是SSE計算,而不是平均,因為平均損失會接近於0

代碼解讀

損失層

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

1 model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',
2     arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})(
3     [*model_body.output, *y_true])
4 
5 model = Model([model_body.input, *y_true], model_loss)

  生成一個損失層。

  

  進入yolo_loss內部:

1 def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False):

  參數含義:

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

  前三個是預測值model_body.output,后三個是真實值y_true。

  

  預測值和真實值分離:

1 num_layers = len(anchors)//3   # default setting 輸出層神經元的個數(三個下采樣尺度)
2 yolo_outputs = args[:num_layers]   # 預測值
3 y_true = args[num_layers:]   # 真實值   預測與真實分離, 前三個是真實值,后三個是預測值

  輸入的維度、網格的維度:

1 anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers == 3 else [[3,4,5], [1,2,3]]  # 先驗框分組
2 input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))    # 13*13 的寬高回歸416*416
3 grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]  # 三種尺度的shape
4 loss = 0

  模型的batch_size

1 m = K.shape(yolo_outputs[0])[0] # 輸入模型的圖片總量 
2 batch_size mf = K.cast(m, K.dtype(yolo_outputs[0])) # 調整類型

  對於每一種尺度進行循環:

  • 提取置信度信息
  • 提取分類信息
1 for l in range(num_layers):
2     object_mask = y_true[l][..., 4:5]
3     true_class_probs = y_true[l][..., 5:]  

  通過最后一層的輸出,構建pred_box。(?,?,?,3,2 + ?,?,?,3,2 -> ?,?,?,6,2(pred_box))

1 grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],
2      anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)
3 pred_box = K.concatenate([pred_xy, pred_wh])  # 組合成預測框pred_box

其中,yolo_head的作用是將最后一層的特征轉換為b-box的信息,其中,會包含b-box特征的計算:

  x, y, 物體置信度以類別置信度部分均經過 Sigmoid 函數激活, 然后采用SSE 計算最終損失。

1 box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
2 box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
3 box_confidence = K.sigmoid(feats[..., 4:5])  # 框置信度
4 box_class_probs = K.sigmoid(feats[..., 5:])   # 類別置信度

  box_xy的計算過程為 將feats中x,y相關的信息sigmoid后(將feats映射到(0,1)), 與所在網格位置加和 再歸一化。

  grid 相當於是Cx和Cy,即目標中心點所在網格左上角距最左上角相差的格子數。雖然實際過程中,grid是0~12等差分布的(13,13,1,2)的張量,但可以實現上述的需求。(當前的feats為13*13中的)

  同理,寬高wh:將feats中w,h的值,經過exp正值化,再乘以anchors_tensor的anchor box,再除以圖片寬高,(歸一化);

  因此,返回的box_xy和box_wh是相對於當前小網格相對於大網格的位置。(相對於特征圖的位置)

  由於在真值的設計中,位置參數是相對於規范化后圖片的位置。此時是相對於小網格的位置,所以后續在loss計算中,應該會有尺度的縮放。

  

  由於此時是用於計算損失函數,返回值為

  • 網格grid:結構是(13, 13, 1, 2),數值為0~12的全遍歷二元組, 包含x和y兩層
  • 預測值feats:經過reshape變換,將18維數據分離出3維anchors,結構是(?, 13, 13, 3, 6)
  • box_xy和box_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計算完成之后,再進行歸一化。 (只有一個類別的情況下 -- 6=5+1)
1 if calc_loss == True:
2     return grid, feats, box_xy, box_wh  

  有了預測值,便可以進行損失函數的計算。

  循環計算每1層的損失值,累加到一起。

1 for l in range(num_layers):
2     ...
3     loss += xy_loss + wh_loss + confidence_loss + class_loss

  ...部分:

  • 生成真實數據
  • 根據置信度 生成 二值向量
  • 損失的計算

1. 生成真實數據

1 raw_true_xy = y_true[l][..., :2]*grid_shapes[l][::-1] - grid
2 raw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1])
3 raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh)) # avoid log(0)=-inf
4 box_loss_scale = 2 - y_true[l][..., 2:3]*y_true[l][..., 3:4] # 2-box_ares)避免大框的誤差對loss 比小框誤差對loss影響大

  真實框尺度縮放到尺度下的方式。

  代碼第4行,有w*h越小,則box_loss_scale 越大;

  同時w*h越小,其面積(w*h就是面積)就越小,面積越小,在和anchor做比較的時候,iou必然就小,導致"存在物體"的置信度就越小。也就是object_mask越小。

  於是,object_mask * box_loss_scale在這里形成了一個制衡條件,這也就是我把box_loss_scale看做一個制衡值的原因。

  損失函數的計算是在特征圖上的,而不是將其轉化至416,416的圖上,或者轉化到原圖上。

  # 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);  

2. 根據置信度生成二值向量

  接着,根據IoU忽略閾值生成ignore_mask,將預測框pred_box和真值框true_box計算IoU

  抑制不需要的anchor框的值,即IoU小於最大閾值的anchor框

  ignore_mask的shape是(?, ?, ?, 3, 1),第0位是批次數,第1~2位是特征圖尺寸。

  並且找一個最大的負責預測該物體

 1 ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
 2 object_mask_bool = K.cast(object_mask, 'bool')
 3 
 4 def loop_body(b, ignore_mask):
 5     true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])   # 抑制不需要的anchor框
 6     iou = box_iou(pred_box[b], true_box)  # 計算了iou
 7     best_iou = K.max(iou, axis=-1)  # 找一個 iou最大的
 8     ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))   #當iou小於閾值時記錄,即認為這個預測框不包含物體
 9     return b+1, ignore_mask
10 
11 _, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])   #傳入loop_body函數初值為b=0,ignore_mask
12 ignore_mask = ignore_mask.stack() 
13 ignore_mask = K.expand_dims(ignore_mask, -1)   # 擴展維度用於計算loss

  此處box_iou的計算時帶中心點坐標的計算,即包含位置的計算,而不是像Kmeans中那樣拼在一起的計算。

3. 損失的計算

  上述Loss的關於損失的描述應該是最為直接恰當的, 但和代碼對比 好像有些不一樣

  可以看出,YOLO v3中主要包含三種損失,即坐標的損失,置信度的損失分類的損失。λobj表示當前的網格中是否存在物體,存在為1,不存在為0。誤差采用SSE計算

1 xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[...,0:2], from_logits=True)
2 wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh-raw_pred[...,2:4])
3 confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \
4     (1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask
5 class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True)

  binary_crossentropy和sigmoid的應用(體現在K.binary_crossentropy(raw_true_xy, raw_pred [...,0:2], from_logits=True)),算是非常大膽的了。因為二分類是得到比較"絕對"的輸出,非黑即白,非0即1,在鏈式結構中,非常容易造成"一失足成千古恨"的結果。比如:每次預測值都是0.9,十次以后(0.9的10次方)就非常非常小了。

  當真實輸出與期望輸出接近時,代價函數接近於0.  

  為了實現多標簽分類,模型不再使用softmax函數作為最終的分類器,而是使用logistic作為分類器,使用 binary cross-entropy作為損失函數。

  從 Sigmoid 函數的導數圖像 (圖 6)可以看到, 當神經網絡的輸出較大時, 會變得非常小, 此時使用平方誤差得到的誤差值很小, 導致網絡收斂很慢, 出現誤差越大收斂越慢, 也就是梯度消失的情況。(梯度消失看的是導數的曲線)。 應對該問題,一般會采用交叉熵(從代碼中可以看出,使用sigmoid計算的xy, 物體置信度、類別置信度中均使用了交叉熵)  

  模型最終的損失(取均值)

1 xy_loss = K.sum(xy_loss) / mf
2 wh_loss = K.sum(wh_loss) / mf
3 confidence_loss = K.sum(confidence_loss) / mf
4 class_loss = K.sum(class_loss) / mf
5 loss += xy_loss + wh_loss + confidence_loss + class_loss

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM