二、訓練部分
1、計算loss所需參數
在計算loss的時候,實際上是y_pre和y_true之間的對比:
y_pre 就是一幅圖像經過網絡之後的輸出,內部含有三個特徵層的內容;其需要解碼才能夠在圖上作畫
y_true 就是一個真實圖像中,它的每個真實框對應的(13,13)、(26,26)、(52,52)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一致
y_true 是最理想的y_pre
實際上y_pre和y_true內容的shape都是
(batch_size,13,13,3,85)
(batch_size,26,26,3,85)
(batch_size,52,52,3,85)
2、y_pre是什麼
網絡輸出的預測框
對於yolo3的模型來說,網絡最後輸出的內容就是三個特徵層每個網格點對應的預測框及其種類,即三個特徵層分別對應着圖片被分爲不同size的網格後,每個網格點上三個先驗框對應的位置、置信度及其種類。
對於輸出的y1、y2、y3而言,[…, : 2]指的是相對於每個網格點的偏移量,[…, 2: 4]指的是寬和高,[…, 4: 5]指的是該框的置信度,[…, 5: ]指的是每個種類的預測概率。
現在的y_pre還是沒有解碼的,解碼了之後纔是真實圖像上的情況。
y_pre解碼 => 真實圖像上的預測框
y_true編碼 才能與y_true 計算loss
3、y_true是什麼
真實的圖像中真實的框
y_true就是一個真實圖像中,它的每個真實框對應的(13,13)、(26,26)、(52,52)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一致
在yolo3中,其使用了一個專門的函數用於處理讀取進來的圖片的框的真實情況。
def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
其輸入爲:
true_boxes:shape爲(m, T, 5)代表m張圖T個框的x_min、y_min、x_max、y_max、class_id。
input_shape:輸入的形狀,此處爲416、416
anchors:代表9個先驗框的大小
num_classes:種類的數量。
其實對真實框的處理是將真實框轉化成圖片中相對網格的xyhw,步驟如下:
- 1、取框的真實值,獲取其框的中心及其寬高,除去input_shape變成比例的模式。
- 2、建立全爲0的y_true,y_true是一個列表,包含三個特徵層,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
- 3、對每一張圖片處理,將每一張圖片中的真實框的wh和先驗框的wh對比,計算IOU值,選取其中IOU最高的一個,得到其所屬特徵層及其網格點的位置,在對應的y_true中將內容進行保存。
for t, n in enumerate(best_anchor):
for l in range(num_layers):
if n in anchor_mask[l]:
# 計算該目標在第l個特徵層所處網格的位置
i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
# 找到best_anchor索引的索引
k = anchor_mask[l].index(n)
c = true_boxes[b,t, 4].astype('int32')
# 保存到y_true中
y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
y_true[l][b, j, i, k, 4] = 1
y_true[l][b, j, i, k, 5+c] = 1
對於最後輸出的y_true而言,只有每個圖裏每個框最對應的位置有數據,其它的地方都爲0。
preprocess_true_boxes全部的代碼如下:
#---------------------------------------------------#
# 讀入xml文件,並輸出y_true
#---------------------------------------------------#
def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'
# 一共有三個特徵層數
num_layers = len(anchors)//3
# 先驗框
# 678爲116,90, 156,198, 373,326
# 345爲30,61, 62,45, 59,119
# 012爲10,13, 16,30, 33,23,
anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]
true_boxes = np.array(true_boxes, dtype='float32')
input_shape = np.array(input_shape, dtype='int32') # 416,416
# 讀出xy軸,讀出長寬
# 中心點(m,n,2)
boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]
# 計算比例
true_boxes[..., 0:2] = boxes_xy/input_shape[:]
true_boxes[..., 2:4] = boxes_wh/input_shape[:]
# m張圖
m = true_boxes.shape[0]
# 得到網格的shape爲13,13;26,26;52,52
grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)]
# y_true的格式爲(m,13,13,3,85)(m,26,26,3,85)(m,52,52,3,85)
y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes),
dtype='float32') for l in range(num_layers)]
# [1,9,2]
anchors = np.expand_dims(anchors, 0)
anchor_maxes = anchors / 2.
anchor_mins = -anchor_maxes
# 長寬要大於0纔有效
valid_mask = boxes_wh[..., 0]>0
for b in range(m):
# 對每一張圖進行處理
wh = boxes_wh[b, valid_mask[b]]
if len(wh)==0: continue
# [n,1,2]
wh = np.expand_dims(wh, -2)
box_maxes = wh / 2.
box_mins = -box_maxes
# 計算真實框和哪個先驗框最契合
intersect_mins = np.maximum(box_mins, anchor_mins)
intersect_maxes = np.minimum(box_maxes, anchor_maxes)
intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
box_area = wh[..., 0] * wh[..., 1]
anchor_area = anchors[..., 0] * anchors[..., 1]
iou = intersect_area / (box_area + anchor_area - intersect_area)
# (n) 感謝 消盡不死鳥 的提醒
best_anchor = np.argmax(iou, axis=-1)
for t, n in enumerate(best_anchor):
for l in range(num_layers):
if n in anchor_mask[l]:
# floor用於向下取整
i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
# 找到真實框在特徵層l中第b副圖像對應的位置
k = anchor_mask[l].index(n)
c = true_boxes[b,t, 4].astype('int32')
y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
y_true[l][b, j, i, k, 4] = 1
y_true[l][b, j, i, k, 5+c] = 1
return y_true
4、loss的計算過程
在得到了y_pre和y_true後怎麼對比呢?不是簡單的減一下就可以的呢。
loss值需要對三個特徵層進行處理,這裏以最小的特徵層爲例。
1、利用y_true取出該特徵層中真實存在目標的點的位置(m,13,13,3,1)及其對應的種類(m,13,13,3,80)。
2、將yolo_outputs的預測值輸出進行處理,得到reshape後的預測值y_pre,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。還有解碼後的xy,wh。
3、獲取真實框編碼後的值,後面用於計算loss,編碼後的值其含義與y_pre相同,可用於計算loss。
4、對於每一幅圖,計算其中所有真實框與預測框的IOU,取出每個網絡點中IOU最大的先驗框,如果這個最大的IOU都小於ignore_thresh,則保留,一般來說ignore_thresh取0.5,該步的目的是爲了平衡負樣本。
5、計算xy和wh上的loss,其計算的是實際上存在目標的,利用第三步真實框編碼後的的結果和未處理的預測結果進行對比得到loss。
6、計算置信度的loss,其有兩部分構成,第一部分是實際上存在目標的,預測結果中置信度的值與1對比;第二部分是實際上不存在目標的,在第四步中得到其最大IOU的值與0對比。
7、計算預測種類的loss,其計算的是實際上存在目標的,預測類與真實類的差距。
其實際上計算的總的loss是三個loss的和,
這三個loss分別是:
- 實際存在的框,編碼後的長寬與xy軸偏移量與預測值的差距。
- 實際存在的框,預測結果中置信度的值與1對比;實際不存在的框,在上述步驟中,在第四步中得到其最大IOU的值與0對比。
- 實際存在的框,種類預測結果與實際結果的對比。
其實際代碼如下,使用yolo_loss就可以獲得loss值:
import numpy as np
import tensorflow as tf
from keras import backend as K
#---------------------------------------------------#
# 將預測值的每個特徵層調成真實值
#---------------------------------------------------#
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
num_anchors = len(anchors)
# [1, 1, 1, num_anchors, 2]
anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])
# 獲得x,y的網格
# (13, 13, 1, 2)
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))
# (batch_size,13,13,3,85)
feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
# 將預測值調成真實值
# box_xy對應框的中心點
# box_wh對應框的寬和高
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:])
# 在計算loss的時候返回如下參數
if calc_loss == True:
return grid, feats, box_xy, box_wh
return box_xy, box_wh, box_confidence, box_class_probs
#---------------------------------------------------#
# 用於計算每個預測框與真實框的iou
#---------------------------------------------------#
def box_iou(b1, b2):
# 13,13,3,1,4
# 計算左上角的座標和右下角的座標
b1 = K.expand_dims(b1, -2)
b1_xy = b1[..., :2]
b1_wh = b1[..., 2:4]
b1_wh_half = b1_wh/2.
b1_mins = b1_xy - b1_wh_half
b1_maxes = b1_xy + b1_wh_half
# 1,n,4
# 計算左上角和右下角的座標
b2 = K.expand_dims(b2, 0)
b2_xy = b2[..., :2]
b2_wh = b2[..., 2:4]
b2_wh_half = b2_wh/2.
b2_mins = b2_xy - b2_wh_half
b2_maxes = b2_xy + b2_wh_half
# 計算重合面積
intersect_mins = K.maximum(b1_mins, b2_mins)
intersect_maxes = K.minimum(b1_maxes, b2_maxes)
intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
b1_area = b1_wh[..., 0] * b1_wh[..., 1]
b2_area = b2_wh[..., 0] * b2_wh[..., 1]
iou = intersect_area / (b1_area + b2_area - intersect_area)
return iou
#---------------------------------------------------#
# loss值計算
#---------------------------------------------------#
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False):
# 一共有三層
num_layers = len(anchors)//3
# 將預測結果和實際ground truth分開,args是[*model_body.output, *y_true]
# y_true是一個列表,包含三個特徵層,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
# yolo_outputs是一個列表,包含三個特徵層,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
y_true = args[num_layers:]
yolo_outputs = args[:num_layers]
# 先驗框
# 678爲116,90, 156,198, 373,326
# 345爲30,61, 62,45, 59,119
# 012爲10,13, 16,30, 33,23,
anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]
# 得到input_shpae爲416,416
input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))
# 得到網格的shape爲13,13;26,26;52,52
grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]
loss = 0
# 取出每一張圖片
# m的值就是batch_size
m = K.shape(yolo_outputs[0])[0]
mf = K.cast(m, K.dtype(yolo_outputs[0]))
# y_true是一個列表,包含三個特徵層,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
# yolo_outputs是一個列表,包含三個特徵層,shape分別爲(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
for l in range(num_layers):
# 以第一個特徵層(m,13,13,3,85)爲例子
# 取出該特徵層中存在目標的點的位置。(m,13,13,3,1)
object_mask = y_true[l][..., 4:5]
# 取出其對應的種類(m,13,13,3,80)
true_class_probs = y_true[l][..., 5:]
# 將yolo_outputs的特徵層輸出進行處理
# grid爲網格結構(13,13,1,2),raw_pred爲尚未處理的預測結果(m,13,13,3,85)
# 還有解碼後的xy,wh,(m,13,13,3,2)
grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],
anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)
# 這個是解碼後的預測的box的位置
# (m,13,13,3,4)
pred_box = K.concatenate([pred_xy, pred_wh])
# 找到負樣本羣組,第一步是創建一個數組,[]
ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
object_mask_bool = K.cast(object_mask, 'bool')
# 對每一張圖片計算ignore_mask
def loop_body(b, ignore_mask):
# 取出第b副圖內,真實存在的所有的box的參數
# n,4
true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])
# 計算預測結果與真實情況的iou
# pred_box爲13,13,3,4
# 計算的結果是每個pred_box和其它所有真實框的iou
# 13,13,3,n
iou = box_iou(pred_box[b], true_box)
# 13,13,3,1
best_iou = K.max(iou, axis=-1)
# 判斷預測框的iou小於ignore_thresh則認爲該預測框沒有與之對應的真實框
# 則被認爲是這幅圖的負樣本
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()
#(m,13,13,3,1,1)
ignore_mask = K.expand_dims(ignore_mask, -1)
# 將真實框進行編碼,使其格式與預測的相同,後面用於計算loss
raw_true_xy = y_true[l][..., :2]*grid_shapes[l][:] - grid
raw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1])
# object_mask如果真實存在目標則保存其wh值
# switch接口,就是一個if/else條件判斷語句
raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh))
box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]
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])
# 如果該位置本來有框,那麼計算1與置信度的交叉熵
# 如果該位置本來沒有框,而且滿足best_iou<ignore_thresh,則被認定爲負樣本
# best_iou<ignore_thresh用於限制負樣本數量
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
if print_loss:
loss = tf.Print(loss, [loss, xy_loss, wh_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')
return loss