摘要
在損失函數計算的過程中,需要對模型的輸出即 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