在上一篇中學(xué)習(xí)了yolov3中的darknet53模型发框,在這一篇中我們繼續(xù)來分析yolov3的源代碼绪抛。
需要說明的是询吴,我學(xué)習(xí)的這個yolov3的源碼出自這里malin9402
這次我們要分析的是yolov3.py的源碼谐算。下面開始吧叫确。
下面先了解一下文件開頭的一些參數(shù)
#yolov3能夠檢測到的類別的數(shù)目
NUM_CLASS = len(utils.read_class_names(cfg.YOLO.CLASSES))
#yolov3中3個尺度的3個先驗框的大小
ANCHORS = utils.get_anchors(cfg.YOLO.ANCHORS)
#yolov3中3個尺度的步長
STRIDES = np.array(cfg.YOLO.STRIDES)
#IOU的閾值
IOU_LOSS_THRESH = cfg.YOLO.IOU_LOSS_THRESH
接下來看yolov3模型
def YOLOv3(input_layer):
route_1, route_2, conv = backbone.darknet53(input_layer)
#[bs,13,13,1024] => [bs,13,13,512]
conv = common.convolutional(conv, (1, 1, 1024, 512))
#[bs,13,13,512] => [bs,13,13,1024]
conv = common.convolutional(conv, (3, 3, 512, 1024))
#[bs,13,13,1024] => [bs,13,13,512]
conv = common.convolutional(conv, (1, 1, 1024, 512))
#[bs,13,13,512] => [bs,13,13,1024]
conv = common.convolutional(conv, (3, 3, 512, 1024))
#[bs,13,13,1024] => [bs,13,13,512]
conv = common.convolutional(conv, (1, 1, 1024, 512))
#[bs,13,13,512] => [bs,13,13,1024]
conv_lobj_branch = common.convolutional(conv, (3, 3, 512, 1024))
#[bs,13,13,1024] => [bs,13,13,255]
conv_lbbox = common.convolutional(conv_lobj_branch, (1, 1, 1024, 3*(NUM_CLASS + 5)), activate=False, bn=False)
#[bs,13,13,512] => [bs,13,13,256]
conv = common.convolutional(conv, (1, 1, 512, 256))
#[bs,13,13,256] => [bs,26,26,256]
conv = common.upsample(conv)
#[bs,26,26,256] + [bs,26,26,512] => [bs,26,26,768]
conv = tf.concat([conv, route_2], axis=-1)
#[bs,26,26,768] => [bs,26,26,256]
conv = common.convolutional(conv, (1, 1, 768, 256))
#[bs,26,26,256] => [bs,26,26,512]
conv = common.convolutional(conv, (3, 3, 256, 512))
#[bs,26,26,512] => [bs,26,26,256]
conv = common.convolutional(conv, (1, 1, 512, 256))
#[bs,26,26,256] => [bs,26,26,512]
conv = common.convolutional(conv, (3, 3, 256, 512))
#[bs,26,26,512] => [bs,26,26,256]
conv = common.convolutional(conv, (1, 1, 512, 256))
#[bs,26,26,256] => [bs,26,26,512]
conv_mobj_branch = common.convolutional(conv, (3, 3, 256, 512))
#[bs,26,26,512] => [bs,26,26,255]
conv_mbbox = common.convolutional(conv_mobj_branch, (1, 1, 512, 3*(NUM_CLASS + 5)), activate=False, bn=False)
#[bs,26,26,256] => [bs,26,26,128]
conv = common.convolutional(conv, (1, 1, 256, 128))
#[bs,26,26,128] => [bs,52,52,128]
conv = common.upsample(conv)
#[bs,52,52,128] + [bs,52,52,256] => [bs,52,52,384]
conv = tf.concat([conv, route_1], axis=-1)
#[bs,52,52,384] => [bs,52,52,128]
conv = common.convolutional(conv, (1, 1, 384, 128))
#[bs,52,52,128] => [bs,52,52,256]
conv = common.convolutional(conv, (3, 3, 128, 256))
#[bs,52,52,256] => [bs,52,52,128]
conv = common.convolutional(conv, (1, 1, 256, 128))
#[bs,52,52,128] => [bs,52,52,256]
conv = common.convolutional(conv, (3, 3, 128, 256))
#[bs,52,52,256] => [bs,52,52,128]
conv = common.convolutional(conv, (1, 1, 256, 128))
#[bs,52,52,128] => [bs,52,52,256]
conv_sobj_branch = common.convolutional(conv, (3, 3, 128, 256))
#[bs,52,52,256] => [bs,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模塊得到3個尺度的特征膀钠,然后通過多個卷積層對這3個尺度的特征進行操作掏湾,最終得到小尺度的特征輸出conv_sbbox,中尺度的特征輸出conv_mbbox肿嘲,大尺度的特征輸出conv_lbbox融击。
下面再詳細介紹一下這些特征圖:
1.conv_sbbox:小尺度特征圖,shape=[bs,52,52,255]雳窟,主要用來檢測圖片中的小尺寸物體尊浪。這個尺度可以這樣理解,它把圖片分成了52x52的網(wǎng)格圖片封救,每個網(wǎng)格有3個預(yù)測框拇涤,每個預(yù)測框有85(5+80)個信息,5的意思是它包含(x,y,w,h, confidence)5個基本參數(shù)誉结,80的意思是它有80個類別的檢測概率鹅士。
2. conv_mbbox::中尺度特征圖,shape=[bs,26,26,255]惩坑,主要用來檢測圖片中的中尺寸物體掉盅,它把圖片分成了26x26的網(wǎng)格圖片也拜,每個網(wǎng)格有3個預(yù)測框,每個預(yù)測框有85(5+80)個信息趾痘,理解意思與conv_sbbox相似慢哈。
3. conv_lbbox:大尺度特征圖,shape=[bs,13,13,255]永票,主要用來檢測圖片中的大尺寸物體卵贱,它把圖片分成了13x13的網(wǎng)格圖片,每個網(wǎng)格有3個預(yù)測框侣集,每個預(yù)測框有85(5+80)個信息艰赞,理解意思與conv_sbbox相似。
下面重點關(guān)注一下(x,y,w,h, confidence)5個基本參數(shù):
- x: 預(yù)測框的中心橫坐標的偏移量肚吏。
- y: 預(yù)測框的中心縱坐標的偏移量方妖。
- w: 預(yù)測框的寬度的偏移量。
- h: 預(yù)測框的高度的偏移量罚攀。
- confidence: 預(yù)測框中檢測到物體的概率党觅。
了解了yolov3的輸出后,接著來看decode方法斋泄,它的主要功能是把yolov3的輸出解碼出來杯瞻,方便后續(xù)計算損失值。
在看代碼之前炫掐,我們先了解一下decode方法的計算流程:
- 假設(shè)輸入的形狀為[4,52,52,255]魁莉,這里的4是指每次訓(xùn)練4張圖片,52是指特征圖的高寬大小募胃,可以理解為特征圖把原始圖片劃分成了52x52的格子旗唁,每個格子中255個通道。
2.將這個輸入的形狀改變?yōu)閇4,52,52,3,85]痹束,3是指每個格子有3個預(yù)測框检疫,85是指每個預(yù)測框有4個位置信息(2個中心位置的偏移量+2個高寬的偏移量+1個置信度+80個類別的概率)
3.將2個中心位置的偏移量,2個高寬的偏移量祷嘶,1個置信度屎媳,80個類別的概率都提取出來。 - 計算每個預(yù)測框的絕對坐標和高寬论巍。
- 計算預(yù)測框的置信值和分類值烛谊。
我們看一下先驗框和預(yù)測框的示意圖。
- bh 和 bw 分別表示預(yù)測框的高寬
- bx 和 by 分別表示預(yù)測框中心位置的橫坐標和縱坐標嘉汰。
- ph 和 pw 分別表示先驗框的高寬
- cx 和 cy 分別表示預(yù)測框左上角的坐標
- th 和 tw 分別表示預(yù)測框高寬的偏移量
- tx 和 ty 分別表示預(yù)測框中心位置距離左上角位置的偏移量
下面我們在代碼中看具體實現(xiàn)流程丹禀。
def decode(conv_output, i=0):
"""
return tensor of shape [batch_size, output_size, output_size, anchor_per_scale, 5 + num_classes]
contains (x, y, w, h, score, probability)
"""
conv_shape = tf.shape(conv_output)
batch_size = conv_shape[0]#樣本數(shù)
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]#預(yù)測框中心位置的偏移量
conv_raw_dwdh = conv_output[:, :, :, :, 2:4]#預(yù)測框高寬的偏移量
conv_raw_conf = conv_output[:, :, :, :, 4:5]#預(yù)測框檢測到物體的置信度
conv_raw_prob = conv_output[:, :, :, :, 5: ]#預(yù)測框的類別的概率
# 1.對每個先驗框生成在特征圖上的相對坐標,以左上角為基準,其坐標單位為格子湃崩,即數(shù)值表示是第幾個格子
y = tf.tile(tf.range(output_size, dtype=tf.int32)[:, tf.newaxis], [1, output_size]) # shape = [52,52]
x = tf.tile(tf.range(output_size, dtype=tf.int32)[tf.newaxis, :], [output_size, 1]) # shape = [52,52]
xy_grid = tf.concat([x[:, :, tf.newaxis], y[:, :, tf.newaxis]], axis=-1) # shape = [52,52,2]
xy_grid = tf.tile(xy_grid[tf.newaxis, :, :, tf.newaxis, :], [batch_size, 1, 1, 3, 1]) # shape = [batch_size, 52,52,3,2]
xy_grid = tf.cast(xy_grid, tf.float32)
# 2.計算預(yù)測框的絕對坐標和高寬度
# 根據(jù)上圖公式計算預(yù)測框的中心位置
pred_xy = (tf.sigmoid(conv_raw_dxdy) + xy_grid) * STRIDES[i] # xy_grid表示特征圖上左上角的位置,即是第幾行第幾列格子接箫,STRIDES表示格子的長度攒读,即特征圖上的一個格子在原圖上的長度
# 根據(jù)上圖公式計算預(yù)測框的高寬
pred_wh = (tf.exp(conv_raw_dwdh) * ANCHORS[i]) * STRIDES[i] # ANCHORS[i]) * STRIDES[i] 表示先驗框在原圖上的大小
pred_xywh = tf.concat([pred_xy, pred_wh], axis=-1)
# 3. 計算預(yù)測框的置信度和分類值
pred_conf = tf.sigmoid(conv_raw_conf)
pred_prob = tf.sigmoid(conv_raw_prob)
return tf.concat([pred_xywh, pred_conf, pred_prob], axis=-1)
bbox_iou
bbox_iou 函數(shù)用來計算兩個預(yù)測框之間的距離,在utils.py文件中有bboxes_iou方法也實現(xiàn)了類似的功能辛友,它們之間的區(qū)別是輸入的預(yù)測框的參數(shù)不同薄扁。
bbox_iou:參數(shù)是預(yù)測框的中心坐標+預(yù)測框的高寬
bboxes_iou:參數(shù)是預(yù)測框的左上角坐標+預(yù)測框的右下角坐標
iou值實際上就是兩個框的交集面積除以并集面積,這個值越大废累,兩個框的距離就越近邓梅。如下圖所示:
下面我們具體看一下代碼是如何實現(xiàn)的。
def bbox_iou(boxes1, boxes2):
boxes1_area = boxes1[..., 2] * boxes1[..., 3]#第一個框的面積
boxes2_area = boxes2[..., 2] * boxes2[..., 3]#第二個框的面積
boxes1 = tf.concat([boxes1[..., :2] - boxes1[..., 2:] * 0.5,
boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)#將第一個框由中心坐標+高寬的形式轉(zhuǎn)換為左上角坐標+右下角坐標的形式
boxes2 = tf.concat([boxes2[..., :2] - boxes2[..., 2:] * 0.5,
boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)#將第二個框由中心坐標+高寬的形式轉(zhuǎn)換為左上角坐標+右下角坐標的形式
left_up = tf.maximum(boxes1[..., :2], boxes2[..., :2])#計算兩個框的交集的左上角坐標邑滨,上圖中是(xmin2,ymin2)
right_down = tf.minimum(boxes1[..., 2:], boxes2[..., 2:])#計算兩個框的交集的右下角坐標日缨,上圖中是(xmax1,ymax1)
inter_section = tf.maximum(right_down - left_up, 0.0)
inter_area = inter_section[..., 0] * inter_section[..., 1]#計算兩個框的交集面積
union_area = boxes1_area + boxes2_area - inter_area#計算兩個框的并集面積
return 1.0 * inter_area / union_area#最后交集面積/并集面積
bbox_giou
bbox_giou的功能也是用來計算兩個預(yù)測框之間的距離,按理說掖看,上面的bbox_iou已經(jīng)可以計算兩個框的面積了匣距,為啥還要重要再弄一個方法呢,
這是因為使用bbox_iou來度量預(yù)測框的距離時存在兩個嚴重的問題:
1:如果兩個預(yù)測框之間沒有重合哎壳,那么iou的值就為0毅待,這樣就會導(dǎo)致計算損失函數(shù)時梯度為0,無法進行優(yōu)化归榕。
2:因為iou的計算方法是交集面積除以并集面積尸红,這樣就會導(dǎo)致同一個iou值會有多種不同的形態(tài),如下圖所示:
上面三幅圖中的iou = 0.33刹泄,但是giou值分別是0.33,0.24,-0.1外里,這表明如果兩個框重疊和對齊得越好,那么giou值就會越高特石。
因此级乐,基于iou存在的問題,yolov3使用了giou作為預(yù)測框的損失函數(shù)县匠,其計算方式為:
其中C代表A和B的最小外接矩形的面積风科,通過這種度量方式,兩個預(yù)測框之間沒有相交時乞旦,也能計算距離贼穆。
下面看具體代碼實現(xiàn)。同樣用下圖舉例兰粉。
def bbox_giou(boxes1, boxes2):
boxes1 = tf.concat([boxes1[..., :2] - boxes1[..., 2:] * 0.5,
boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)#把第一個預(yù)測框從中心坐標+高寬的形式轉(zhuǎn)換為左上角坐標+右下角坐標的形式
boxes2 = tf.concat([boxes2[..., :2] - boxes2[..., 2:] * 0.5,
boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)#把第二個預(yù)測框從中心坐標+高寬的形式轉(zhuǎn)換為左上角坐標+右下角坐標的形式
boxes1 = tf.concat([tf.minimum(boxes1[..., :2], boxes1[..., 2:]),
tf.maximum(boxes1[..., :2], boxes1[..., 2:])], axis=-1)#重新整理一下預(yù)測框的坐標
boxes2 = tf.concat([tf.minimum(boxes2[..., :2], boxes2[..., 2:]),
tf.maximum(boxes2[..., :2], boxes2[..., 2:])], axis=-1)
boxes1_area = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] - boxes1[..., 1])#計算第一個預(yù)測框的面積
boxes2_area = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] - boxes2[..., 1])#計算第二個預(yù)測框的面積
left_up = tf.maximum(boxes1[..., :2], boxes2[..., :2])#計算兩個框的交集的左上角坐標故痊,在上圖中是(xmin2,ymin2)
right_down = tf.minimum(boxes1[..., 2:], boxes2[..., 2:])#計算兩個框的交集的右下角坐標玖姑,在上圖中是(xmax1,ymax1)
inter_section = tf.maximum(right_down - left_up, 0.0)
inter_area = inter_section[..., 0] * inter_section[..., 1]#計算交集的面積
union_area = boxes1_area + boxes2_area - inter_area#計算并集的面積
iou = inter_area / union_area#計算iou值
enclose_left_up = tf.minimum(boxes1[..., :2], boxes2[..., :2])#計算最小外接矩形的左上角坐標愕秫,在上圖中是(xmin1,ymin1)
enclose_right_down = tf.maximum(boxes1[..., 2:], boxes2[..., 2:])#計算最小外接矩形的右下角坐標慨菱,在上圖中是(xmax2,ymax2)
enclose = tf.maximum(enclose_right_down - enclose_left_up, 0.0)
enclose_area = enclose[..., 0] * enclose[..., 1]#計算最小外接矩形的面積
giou = iou - 1.0 * (enclose_area - union_area) / enclose_area#根據(jù)上面的公式計算giou值
return giou
compute_loss
compute_loss 函數(shù)被用來計算損失。
損失分為3類:框回歸損失戴甩,置信度損失和分類損失符喝。
框回歸損失
計算過程:
- 獲得置信度respond_bbox。
- 計算bbox_loss_scale = 2.0 - (真實框的面積)/(輸入原圖的面積)
- 損失 giou_loss = respond_bbox * bbox_loss_scale * (1-giou)
置信度損失
計算過程:
- 對所有預(yù)測框求出它和所有真實框的iou值甜孤。
- 然后找出每個預(yù)測框的iou值中的最大的一個值协饲。
- 如果每個預(yù)測框找出的這個最大iou值小于指定的閾值,那么認為這個預(yù)測框不包含物體缴川,為背景框(負樣本)茉稠,否則這個框是前景框(正樣本)。還有一種是這個iou值大于指定的閾值把夸,但是這個預(yù)測框沒有包含物體的情況而线。這種情況不需要參與損失函數(shù)的計算,在代碼中被巧妙的處理掉了恋日。
- 計算正樣本誤差和負樣本誤差吞获,最后相加。
分類損失
對于分類損失谚鄙,同樣只考慮正樣本誤差各拷,使用交叉熵損失函數(shù)計算誤差。
輸入:
pred: 經(jīng)過decode解碼后的檢測框闷营,即原圖上的檢測框烤黍。
conv: 沒有經(jīng)過解碼的檢測框,即特征圖上的檢測框傻盟。
label: 標簽的格式為 [batch_size, output_size, output_size, anchor_per_scale, 85=(2個中心坐標xy+2個形狀wh+1個置信值+80個類別)]速蕊;
bboxes: 每個尺度的真實框集合,里面存放的是真實框的4個參數(shù)(2個中心點坐標+2個高寬長度)
i: 表示第幾個尺度上的特征圖(總共有3個尺度)娘赴。
搞清楚了損失函數(shù)的計算流程和參數(shù)后规哲,我們看看代碼是如何實現(xiàn)的。
def compute_loss(pred, conv, label, bboxes, i=0):
conv_shape = tf.shape(conv)#特征圖形狀
batch_size = conv_shape[0]#處理的圖片數(shù)量
output_size = conv_shape[1]#特征圖的大小
input_size = STRIDES[i] * output_size#原圖的大小
conv = tf.reshape(conv, (batch_size, output_size, output_size, 3, 5 + NUM_CLASS))#將特征圖轉(zhuǎn)換形式
conv_raw_conf = conv[:, :, :, :, 4:5]#特征圖的置信度
conv_raw_prob = conv[:, :, :, :, 5:]#特征圖中類別的概率
pred_xywh = pred[:, :, :, :, 0:4]#預(yù)測框在原圖上的坐標和高寬
pred_conf = pred[:, :, :, :, 4:5]#預(yù)測框處理后的置信度
label_xywh = label[:, :, :, :, 0:4]#真實框的坐標和高寬
respond_bbox = label[:, :, :, :, 4:5]#真實框的置信度诽表,有目標的為1唉锌,沒目標的為0
label_prob = label[:, :, :, :, 5:]#真實框的類別概率
# 1.框回歸損失
# 計算預(yù)測框和真實框的giou值
giou = tf.expand_dims(bbox_giou(pred_xywh, label_xywh), axis=-1)
input_size = tf.cast(input_size, tf.float32)
# bbox_loss_scale 制衡誤差
bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (input_size ** 2)
# 計算giou_loss
giou_loss = respond_bbox * bbox_loss_scale * (1- giou)
# 2.置信度損失
# 計算所有預(yù)測框和真實框的iou值
iou = bbox_iou(pred_xywh[:, :, :, :, np.newaxis, :], bboxes[:, np.newaxis, np.newaxis, np.newaxis, :, :])
# 找出每個預(yù)測框的最大iou值
max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)
# respond_bgd 形狀為 [batch_size, output_size, output_size, anchor_per_scale, x],當無目標且小于閾值時x為1竿奏,否則為0
respond_bgd = (1.0 - respond_bbox) * tf.cast( max_iou < IOU_LOSS_THRESH, tf.float32 )
conf_focal = tf.pow(respond_bbox - pred_conf, 2)
conf_loss = conf_focal * (
# 正樣本誤差
respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
+
# 負樣本誤差
respond_bgd * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
)
# 3.分類損失
使用交叉熵損失計算損失值
prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)
# 誤差平均
giou_loss = tf.reduce_mean(tf.reduce_sum(giou_loss, axis=[1,2,3,4]))
conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1,2,3,4]))
prob_loss = tf.reduce_mean(tf.reduce_sum(prob_loss, axis=[1,2,3,4]))
return giou_loss, conf_loss, prob_loss
yolov3的損失函數(shù)看起來比較明白易懂袄简,但這可能是經(jīng)過原作者多次試驗后得出來的最優(yōu)解,我雖然看懂了代碼泛啸,但是對于代碼中損失函數(shù)為什么要這樣計算還不是很懂绿语,看來還需要更加深入的學(xué)習(xí)。
這次yolov3模型以及損失函數(shù)的計算分享就結(jié)束了,下篇文章我們進行數(shù)據(jù)集制作代碼的分析吕粹。