原文:https://www.cnblogs.com/zyly/p/9534063.htmlhtml
yolo源碼來源於網址:https://github.com/hizhangp/yolo_tensorflowpython
在講解源碼以前,咱們須要作一些準備工做:git
根據本身的需求修改配置文件yolo/config.py。github
咱們來粗略的介紹一下每一個文件的功能:數組
咱們先從配置文件提及,代碼以下:緩存
# -*- coding: utf-8 -*- """ Created on Tue Jun 12 12:08:15 2018 @author: lenovo """ ''' 配置參數 ''' import os # # 數據集路徑,和模型檢查點文件路徑 # DATA_PATH = 'data' #全部數據所在的根目錄 PASCAL_PATH = os.path.join(DATA_PATH, 'pascal_voc') #VOC2012數據集所在的目錄 CACHE_PATH = os.path.join(PASCAL_PATH, 'cache') #保存生成的數據集標籤緩衝文件所在文件夾 OUTPUT_DIR = os.path.join(PASCAL_PATH, 'output') #保存生成的網絡模型和日誌文件所在的文件夾 WEIGHTS_DIR = os.path.join(PASCAL_PATH, 'weights') #檢查點文件所在的目錄 #WEIGHTS_FILE = None WEIGHTS_FILE = os.path.join(WEIGHTS_DIR, 'YOLO_small.ckpt') #VOC 2012數據集類別名 CLASSES = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'] #使用水平鏡像,擴大一倍數據集? FLIPPED = True ''' 網絡模型參數 ''' #圖片大小 IMAGE_SIZE = 448 #單元格大小S 一共有CELL_SIZExCELL_SIZE個單元格 CELL_SIZE = 7 #每一個單元格邊界框的個數B BOXES_PER_CELL = 2 #泄露修正線性激活函數 係數 ALPHA = 0.1 #控制檯輸出信息 DISP_CONSOLE = False #損失函數 的權重設置 OBJECT_SCALE = 1.0 #有目標時,置信度權重 NOOBJECT_SCALE = 1.0 #沒有目標時,置信度權重 CLASS_SCALE = 2.0 #類別權重 COORD_SCALE = 5.0 #邊界框權重 ''' 訓練參數設置 ''' GPU = '' #學習率 LEARNING_RATE = 0.0001 #退化學習率衰減步數 DECAY_STEPS = 30000 #衰減率 DECAY_RATE = 0.1 STAIRCASE = True #批量大小 BATCH_SIZE = 45 #最大迭代次數 MAX_ITER = 15000 #日誌文件保存間隔步 SUMMARY_ITER = 10 #模型保存間隔步 SAVE_ITER = 500 ''' 測試時的相關參數 ''' #格子有目標的置信度閾值 THRESHOLD = 0.2 #非極大值抑制 IoU閾值 IOU_THRESHOLD = 0.5
各個參數我已經在上面註釋了,下面就不在重複了。下面咱們來介紹yolo網絡的構建。網絡
第一部分:session
def __init__(self, is_training=True): ''' 構造函數 利用 cfg 文件對網絡參數進行初始化,同時定義網絡的輸入和輸出 size 等信息, 其中 offset 的做用應該是一個定長的偏移 boundery1和boundery2 做用是在輸出中肯定每種信息的長度(如類別,置信度等)。 其中 boundery1 指的是對於全部的 cell 的類別的預測的張量維度,因此是 self.cell_size * self.cell_size * self.num_class boundery2 指的是在類別以後每一個cell 所對應的 bounding boxes 的數量的總和,因此是self.boundary1 + self.cell_size * self.cell_size * self.boxes_per_cell args: is_training:訓練? ''' #VOC 2012數據集類別名 self.classes = cfg.CLASSES #類別個數C 20 self.num_class = len(self.classes) #網絡輸入圖像大小448, 448 x 448 self.image_size = cfg.IMAGE_SIZE #單元格大小S=7 將圖像分爲SxS的格子 self.cell_size = cfg.CELL_SIZE #每一個網格邊界框的個數B=2 self.boxes_per_cell = cfg.BOXES_PER_CELL #網絡輸出的大小 S*S*(B*5 + C) = 1470 self.output_size = (self.cell_size * self.cell_size) *\ (self.num_class + self.boxes_per_cell * 5) #圖片的縮放比例 64 self.scale = 1.0 * self.image_size / self.cell_size '''#將網絡輸出分離爲類別和置信度以及邊界框的大小,輸出維度爲7*7*20 + 7*7*2 + 7*7*2*4=1470''' #7*7*20 self.boundary1 = self.cell_size * self.cell_size * self.num_class #7*7*20+7*7*2 self.boundary2 = self.boundary1 +\ self.cell_size * self.cell_size * self.boxes_per_cell #代價函數 權重 self.object_scale = cfg.OBJECT_SCALE #1 self.noobject_scale = cfg.NOOBJECT_SCALE #1 self.class_scale = cfg.CLASS_SCALE #2.0 self.coord_scale = cfg.COORD_SCALE #5.0 #學習率0.0001 self.learning_rate = cfg.LEARNING_RATE #批大小 45 self.batch_size = cfg.BATCH_SIZE #泄露修正線性激活函數 係數0.1 self.alpha = cfg.ALPHA #偏置 形狀[7,7,2] self.offset = np.transpose(np.reshape(np.array( [np.arange(self.cell_size)] * self.cell_size * self.boxes_per_cell), (self.boxes_per_cell, self.cell_size, self.cell_size)), (1, 2, 0)) #輸入圖片佔位符 [NONE,image_size,image_size,3] self.images = tf.placeholder( tf.float32, [None, self.image_size, self.image_size, 3], name='images') #構建網絡 獲取YOLO網絡的輸出(不通過激活函數的輸出) 形狀[None,1470] self.logits = self.build_network( self.images, num_outputs=self.output_size, alpha=self.alpha, is_training=is_training) if is_training: #設置標籤佔位符 [None,S,S,5+C] 即[None,7,7,25] self.labels = tf.placeholder( tf.float32, [None, self.cell_size, self.cell_size, 5 + self.num_class]) #設置損失函數 self.loss_layer(self.logits, self.labels) #加入權重正則化以後的損失函數 self.total_loss = tf.losses.get_total_loss() #將損失以標量形式顯示,該變量命名爲total_loss tf.summary.scalar('total_loss', self.total_loss)
第二部分:build_networkapp
這部分主要是實現了 yolo 網絡模型的構成,能夠清楚的看到網絡的組成,並且爲了使程序更加簡潔,構建網絡使用的是 TensorFlow 中的 slim 模塊,主要的函數有slim.arg_scope slim.conv2d slim.fully_connected 和 slim.dropoout等,具體程序以下所示:函數
def build_network(self, images, num_outputs, alpha, keep_prob=0.5, is_training=True, scope='yolo'): ''' 構建YOLO網絡 args: images:輸入圖片佔位符 [None,image_size,image_size,3] 這裏是[None,448,448,3] num_outputs:標量,網絡輸出節點數 1470 alpha:泄露修正線性激活函數 係數0.1 keep_prob:棄權 保留率 is_training:訓練? scope:命名空間名 return: 返回網絡最後一層,激活函數處理以前的值 形狀[None,1470] ''' #定義變量命名空間 with tf.variable_scope(scope): #定義共享參數 使用l2正則化 with slim.arg_scope( [slim.conv2d, slim.fully_connected], activation_fn=leaky_relu(alpha), weights_regularizer=slim.l2_regularizer(0.0005), weights_initializer=tf.truncated_normal_initializer(0.0, 0.01) ): logging.info('image shape{0}'.format(images.shape)) #pad_1 填充 454x454x3 net = tf.pad( images, np.array([[0, 0], [3, 3], [3, 3], [0, 0]]), name='pad_1') logging.info('Layer pad_1 {0}'.format(net.shape)) #卷積層conv_2 s=2 (n-f+1)/s向上取整 224x224x64 net = slim.conv2d( net, 64, 7, 2, padding='VALID', scope='conv_2') logging.info('Layer conv_2 {0}'.format(net.shape)) #池化層pool_3 112x112x64 net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_3') logging.info('Layer pool_3 {0}'.format(net.shape)) #卷積層conv_四、3x3x192 s=1 n/s向上取整 112x112x192 net = slim.conv2d(net, 192, 3, scope='conv_4') logging.info('Layer conv_4 {0}'.format(net.shape)) #池化層pool_5 56x56x192 net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_5') logging.info('Layer pool_5 {0}'.format(net.shape)) #卷積層conv_六、1x1x128 s=1 n/s向上取整 56x56x128 net = slim.conv2d(net, 128, 1, scope='conv_6') logging.info('Layer conv_6 {0}'.format(net.shape)) #卷積層conv_七、3x3x256 s=1 n/s向上取整 56x56x256 net = slim.conv2d(net, 256, 3, scope='conv_7') logging.info('Layer conv_7 {0}'.format(net.shape)) #卷積層conv_八、1x1x256 s=1 n/s向上取整 56x56x256 net = slim.conv2d(net, 256, 1, scope='conv_8') logging.info('Layer conv_8 {0}'.format(net.shape)) #卷積層conv_九、3x3x512 s=1 n/s向上取整 56x56x512 net = slim.conv2d(net, 512, 3, scope='conv_9') logging.info('Layer conv_9 {0}'.format(net.shape)) #池化層pool_10 28x28x512 net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_10') logging.info('Layer pool_10 {0}'.format(net.shape)) #卷積層conv_十一、1x1x256 s=1 n/s向上取整 28x28x256 net = slim.conv2d(net, 256, 1, scope='conv_11') logging.info('Layer conv_11 {0}'.format(net.shape)) #卷積層conv_十二、3x3x512 s=1 n/s向上取整 28x28x512 net = slim.conv2d(net, 512, 3, scope='conv_12') logging.info('Layer conv_12 {0}'.format(net.shape)) #卷積層conv_1三、1x1x256 s=1 n/s向上取整 28x28x256 net = slim.conv2d(net, 256, 1, scope='conv_13') logging.info('Layer conv_13 {0}'.format(net.shape)) #卷積層conv_1四、3x3x512 s=1 n/s向上取整 28x28x512 net = slim.conv2d(net, 512, 3, scope='conv_14') logging.info('Layer conv_14 {0}'.format(net.shape)) #卷積層conv_1五、1x1x256 s=1 n/s向上取整 28x28x256 net = slim.conv2d(net, 256, 1, scope='conv_15') logging.info('Layer conv_15 {0}'.format(net.shape)) #卷積層conv_1六、3x3x512 s=1 n/s向上取整 28x28x512 net = slim.conv2d(net, 512, 3, scope='conv_16') logging.info('Layer conv_16 {0}'.format(net.shape)) #卷積層conv_1七、1x1x256 s=1 n/s向上取整 28x28x256 net = slim.conv2d(net, 256, 1, scope='conv_17') logging.info('Layer conv_17 {0}'.format(net.shape)) #卷積層conv_1八、3x3x512 s=1 n/s向上取整 28x28x512 net = slim.conv2d(net, 512, 3, scope='conv_18') logging.info('Layer conv_18 {0}'.format(net.shape)) #卷積層conv_1九、1x1x512 s=1 n/s向上取整 28x28x512 net = slim.conv2d(net, 512, 1, scope='conv_19') logging.info('Layer conv_19 {0}'.format(net.shape)) #卷積層conv_20、3x3x1024 s=1 n/s向上取整 28x28x1024 net = slim.conv2d(net, 1024, 3, scope='conv_20') logging.info('Layer conv_20 {0}'.format(net.shape)) #池化層pool_21 14x14x1024 net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_21') logging.info('Layer pool_21 {0}'.format(net.shape)) #卷積層conv_2二、1x1x512 s=1 n/s向上取整 14x14x512 net = slim.conv2d(net, 512, 1, scope='conv_22') logging.info('Layer conv_22 {0}'.format(net.shape)) #卷積層conv_2三、3x3x1024 s=1 n/s向上取整 14x14x1024 net = slim.conv2d(net, 1024, 3, scope='conv_23') logging.info('Layer conv_23 {0}'.format(net.shape)) #卷積層conv_2四、1x1x512 s=1 n/s向上取整 14x14x512 net = slim.conv2d(net, 512, 1, scope='conv_24') logging.info('Layer conv_24 {0}'.format(net.shape)) #卷積層conv_2五、3x3x1024 s=1 n/s向上取整 14x14x1024 net = slim.conv2d(net, 1024, 3, scope='conv_25') logging.info('Layer conv_25 {0}'.format(net.shape)) #卷積層conv_2六、3x3x1024 s=1 n/s向上取整 14x14x1024 net = slim.conv2d(net, 1024, 3, scope='conv_26') logging.info('Layer conv_26 {0}'.format(net.shape)) #pad_27 填充 16x16x2014 net = tf.pad( net, np.array([[0, 0], [1, 1], [1, 1], [0, 0]]), name='pad_27') logging.info('Layer pad_27 {0}'.format(net.shape)) #卷積層conv_2八、3x3x1024 s=2 (n-f+1)/s向上取整 7x7x1024 net = slim.conv2d( net, 1024, 3, 2, padding='VALID', scope='conv_28') logging.info('Layer conv_28 {0}'.format(net.shape)) #卷積層conv_2九、3x3x1024 s=1 n/s向上取整 7x7x1024 net = slim.conv2d(net, 1024, 3, scope='conv_29') logging.info('Layer conv_29 {0}'.format(net.shape)) #卷積層conv_30、3x3x1024 s=1 n/s向上取整 7x7x1024 net = slim.conv2d(net, 1024, 3, scope='conv_30') logging.info('Layer conv_30 {0}'.format(net.shape)) #trans_31 轉置[None,1024,7,7] net = tf.transpose(net, [0, 3, 1, 2], name='trans_31') logging.info('Layer trans_31 {0}'.format(net.shape)) #flat_32 展開 50176 net = slim.flatten(net, scope='flat_32') logging.info('Layer flat_32 {0}'.format(net.shape)) #全鏈接層fc_33 512 net = slim.fully_connected(net, 512, scope='fc_33') logging.info('Layer fc_33 {0}'.format(net.shape)) #全鏈接層fc_34 4096 net = slim.fully_connected(net, 4096, scope='fc_34') logging.info('Layer fc_34 {0}'.format(net.shape)) #棄權層dropout_35 4096 net = slim.dropout( net, keep_prob=keep_prob, is_training=is_training, scope='dropout_35') logging.info('Layer dropout_35 {0}'.format(net.shape)) #全鏈接層fc_36 1470 net = slim.fully_connected( net, num_outputs, activation_fn=None, scope='fc_36') logging.info('Layer fc_36 {0}'.format(net.shape)) return net
網絡最後輸出的是一個1470 維的張量(1470 = 7*7*30)。最後一層全鏈接層的內部以下圖所示:
第三部分: calc_iou
這個函數的主要做用是計算兩個 bounding box 之間的 IoU。輸入是兩個 5 維的bounding box,輸出的兩個 bounding Box 的IoU 。具體程序以下所示:
def calc_iou(self, boxes1, boxes2, scope='iou'): """calculate ious 這個函數的主要做用是計算兩個 bounding box 之間的 IoU。輸入是兩個 5 維的bounding box,輸出的兩個 bounding Box 的IoU Args: boxes1: 5-D tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL, 4] ====> (x_center, y_center, w, h) boxes2: 5-D tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL, 4] ===> (x_center, y_center, w, h) 注意這裏的參數x_center, y_center, w, h都是歸一到[0,1]之間的,分別表示預測邊界框的中心相對整張圖片的座標,寬和高 Return: iou: 4-D tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL] """ with tf.variable_scope(scope): # transform (x_center, y_center, w, h) to (x1, y1, x2, y2) #把之前的中心點座標和長和寬轉換成了左上角和右下角的兩個點的座標 boxes1_t = tf.stack([boxes1[..., 0] - boxes1[..., 2] / 2.0, #左上角x boxes1[..., 1] - boxes1[..., 3] / 2.0, #左上角y boxes1[..., 0] + boxes1[..., 2] / 2.0, #右下角x boxes1[..., 1] + boxes1[..., 3] / 2.0], #右下角y axis=-1) boxes2_t = tf.stack([boxes2[..., 0] - boxes2[..., 2] / 2.0, boxes2[..., 1] - boxes2[..., 3] / 2.0, boxes2[..., 0] + boxes2[..., 2] / 2.0, boxes2[..., 1] + boxes2[..., 3] / 2.0], axis=-1) # calculate the left up point & right down point #lu和rd就是分別求兩個框相交的矩形的左上角的座標和右下角的座標,由於對於左上角, #選擇的是x和y較大的,而右下角是選擇較小的,能夠想一想兩個矩形框相交是否是這中狀況 lu = tf.maximum(boxes1_t[..., :2], boxes2_t[..., :2]) #兩個框相交的矩形的左上角(x1,y1) rd = tf.minimum(boxes1_t[..., 2:], boxes2_t[..., 2:]) #兩個框相交的矩形的右下角(x2,y2) # intersection 這個就是求相交矩形的長和寬,因此有rd-ru,至關於x1-x2和y1-y2, #之因此外面還要加一個tf.maximum是由於刪除那些不合理的框,好比兩個框沒交集, #就會出現左上角座標比右下角還大。 intersection = tf.maximum(0.0, rd - lu) #inter_square這個就是求面積了,就是長乘以寬。 inter_square = intersection[..., 0] * intersection[..., 1] # calculate the boxs1 square and boxs2 square #square1和square2這個就是求面積了,由於以前是中心點座標和長和寬,因此這裏直接用長和寬 square1 = boxes1[..., 2] * boxes1[..., 3] square2 = boxes2[..., 2] * boxes2[..., 3] #union_square就是就兩個框的交面積,由於若是兩個框的面積相加,那就會重複了相交的部分, #因此減去相交的部分,外面有個tf.maximum這個就是保證相交面積不爲0,由於後面要作分母。 union_square = tf.maximum(square1 + square2 - inter_square, 1e-10) #最後有一個tf.clip_by_value,這個是將若是你的交併比大於1,那麼就讓它等於1,若是小於0,那麼就 #讓他變爲0,由於交併比在0-1之間。 return tf.clip_by_value(inter_square / union_square, 0.0, 1.0)
這個函數中主要用到的函數有tf.stack tf.transpose 以及 tf.maximum,下面分別簡單介紹一下這幾個函數:
1. tf.stack(),定義爲:def stack(values, axis=0, name="stack")。該函數的主要做用是對矩陣進行拼接,咱們在 TensorFlow 源碼中能夠看到這句話***tf.stack([x, y, z]) = np.stack([x, y, z])***,也就是說,它和numpy 中的 stack 函數的做用是相同的。都是在指定軸的方向上對矩陣進行拼接。默認值是0。
tf.transpose,定義爲def transpose(a, perm=None, name="transpose") 這個函數的做用是根據 perm 的值對矩陣 a 進行轉置操做,返回數組的 dimension(尺寸、維度) i與輸入的 perm[i]的維度相一致。若是未給定perm,默認設置爲 (n-1…0),這裏的 n 值是輸入變量的 rank 。所以默認狀況下,這個操做執行了一個正規(regular)的2維矩形的轉置。
tf.maximum,定義爲def maximum(x, y, name=None)
這個函數的做用是返回的是a,b之間的最大值。
第四部分:
loss_layer
這個函數的主要做用是計算 Loss。
代價函數是經過loss_layer()實現的,在代碼中,咱們優化如下多部分損失函數:
具體程序以下所示:
def loss_layer(self, predicts, labels, scope='loss_layer'): ''' 計算預測和標籤之間的損失函數 args: predicts:Yolo網絡的輸出 形狀[None,1470] 0:7*7*20:表示預測類別 7*7*20:7*7*20 + 7*7*2:表示預測置信度,即預測的邊界框與實際邊界框之間的IOU 7*7*20 + 7*7*2:1470:預測邊界框 目標中心是相對於當前格子的,寬度和高度的開根號是相對當前整張圖像的(歸一化的) labels:標籤值 形狀[None,7,7,25] 0:1:置信度,表示這個地方是否有目標 1:5:目標邊界框 目標中心,寬度和高度(沒有歸一化) 5:25:目標的類別 ''' with tf.variable_scope(scope): '''#將網絡輸出分離爲類別和置信度以及邊界框的大小,輸出維度爲7*7*20 + 7*7*2 + 7*7*2*4=1470''' #預測每一個格子目標的類別 形狀[45,7,7,20] predict_classes = tf.reshape( predicts[:, :self.boundary1], [self.batch_size, self.cell_size, self.cell_size, self.num_class]) #預測每一個格子中兩個邊界框的置信度 形狀[45,7,7,2] predict_scales = tf.reshape( predicts[:, self.boundary1:self.boundary2], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell]) #預測每一個格子中的兩個邊界框,(x,y)表示邊界框相對於格子邊界框的中心 w,h的開根號相對於整個圖片 形狀[45,7,7,2,4] predict_boxes = tf.reshape( predicts[:, self.boundary2:], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4]) #標籤的置信度,表示這個地方是否有框 形狀[45,7,7,1] response = tf.reshape( labels[..., 0], [self.batch_size, self.cell_size, self.cell_size, 1]) #標籤的邊界框 (x,y)表示邊界框相對於整個圖片的中心 形狀[45,7,7,1,4] boxes = tf.reshape( labels[..., 1:5], [self.batch_size, self.cell_size, self.cell_size, 1, 4]) #標籤的邊界框 歸一化後 張量沿着axis=3重複兩邊,擴充後[45,7,7,2,4] boxes = tf.tile( boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size classes = labels[..., 5:] ''' predict_boxes_tran:offset變量用於把預測邊界框predict_boxes中的座標中心(x,y)由相對當前格子轉換爲相對當前整個圖片 offset,這個是構造的[7,7,2]矩陣,每一行都是[7,2]的矩陣,值爲[[0,0],[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]] 這個變量是爲了將每一個cell的座標對齊,後一個框比前一個框要多加1 好比咱們預測了cell_size的每一箇中心點座標,那麼咱們這個中心點落在第幾個cell_size 就對應座標要加幾,這個用法比較巧妙,構造了這樣一個數組,讓他們對應位置相加 ''' #offset shape爲[1,7,7,2] 若是忽略axis=0,則每一行都是 [[0,0],[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]] offset = tf.reshape( tf.constant(self.offset, dtype=tf.float32), [1, self.cell_size, self.cell_size, self.boxes_per_cell]) #shape爲[45,7,7,2] offset = tf.tile(offset, [self.batch_size, 1, 1, 1]) #shape爲[45,7,7,2] 若是忽略axis=0 第i行爲[[i,i],[i,i],[i,i],[i,i],[i,i],[i,i],[i,i]] offset_tran = tf.transpose(offset, (0, 2, 1, 3)) #shape爲[45,7,7,2,4] 計算每一個格子中的預測邊界框座標(x,y)相對於整個圖片的位置 而不是相對當前格子 #假設當前格子爲(3,3),當前格子的預測邊界框爲(x0,y0),則計算座標(x,y) = ((x0,y0)+(3,3))/7 predict_boxes_tran = tf.stack( [(predict_boxes[..., 0] + offset) / self.cell_size, #x (predict_boxes[..., 1] + offset_tran) / self.cell_size, #y tf.square(predict_boxes[..., 2]), #width tf.square(predict_boxes[..., 3])], axis=-1) #height #計算每一個格子預測邊界框與真實邊界框之間的IOU [45,7,7,2] iou_predict_truth = self.calc_iou(predict_boxes_tran, boxes) # calculate I tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL] #這個是求論文中的1ijobj參數,[45,7,7,2] 1ijobj:表示網格單元i的第j個編輯框預測器’負責‘該預測 #先計算每一個框交併比最大的那個,由於咱們知道,YOLO每一個格子預測兩個邊界框,一個類別。在訓練時,每一個目標只須要 #一個預測器來負責,咱們指定一個預測器"負責",根據哪一個預測器與真實值之間具備當前最高的IOU來預測目標。 #因此object_mask就表示每一個格子中的哪一個邊界框負責該格子中目標預測?哪一個邊界框取值爲1,哪一個邊界框就負責目標預測 #當格子中的確有目標時,取值爲[1,1],[1,0],[0,1] #好比某一個格子的值爲[1,0],表示第一個邊界框負責該格子目標的預測 [0,1]:表示第二個邊界框負責該格子目標的預測 #當格子沒有目標時,取值爲[0,0] object_mask = tf.reduce_max(iou_predict_truth, 3, keep_dims=True) object_mask = tf.cast( (iou_predict_truth >= object_mask), tf.float32) * response # calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL] # noobject_mask就表示每一個邊界框不負責該目標的置信度, #使用tf.onr_like,使得所有爲1,再減去有目標的,也就是有目標的對應座標爲1,這樣一減,就變爲沒有的了。[45,7,7,2] noobject_mask = tf.ones_like( object_mask, dtype=tf.float32) - object_mask # boxes_tran 這個就是把以前的座標換回來(相對整個圖像->相對當前格子),長和寬開方(緣由在論文中有說明),後面求loss就方便。 shape爲(4, 45, 7, 7, 2) boxes_tran = tf.stack( [boxes[..., 0] * self.cell_size - offset, boxes[..., 1] * self.cell_size - offset_tran, tf.sqrt(boxes[..., 2]), tf.sqrt(boxes[..., 3])], axis=-1) #class_loss 分類損失,若是目標出如今網格中 response爲1,不然response爲0 原文代價函數公式第5項 #該項表名當格子中有目標時,預測的類別越接近實際類別,代價值越小 原文代價函數公式第5項 class_delta = response * (predict_classes - classes) class_loss = tf.reduce_mean( tf.reduce_sum(tf.square(class_delta), axis=[1, 2, 3]), name='class_loss') * self.class_scale # object_loss 有目標物體存在的置信度預測損失 原文代價函數公式第3項 #該項表名當格子中有目標時,負責該目標預測的邊界框的置信度越越接近預測的邊界框與實際邊界框之間的IOU時,代價值越小 object_delta = object_mask * (predict_scales - iou_predict_truth) object_loss = tf.reduce_mean( tf.reduce_sum(tf.square(object_delta), axis=[1, 2, 3]), name='object_loss') * self.object_scale #noobject_loss 沒有目標物體存在的置信度的損失(此時iou_predict_truth爲0) 原文代價函數公式第4項 #該項表名當格子中沒有目標時,預測的兩個邊界框的置信度越接近0,代價值越小 noobject_delta = noobject_mask * predict_scales noobject_loss = tf.reduce_mean( tf.reduce_sum(tf.square(noobject_delta), axis=[1, 2, 3]), name='noobject_loss') * self.noobject_scale # coord_loss 邊界框座標損失 shape 爲 [batch_size, 7, 7, 2, 1] 原文代價函數公式1,2項 #該項表名當格子中有目標時,預測的邊界框越接近實際邊界框,代價值越小 coord_mask = tf.expand_dims(object_mask, 4) #1ij boxes_delta = coord_mask * (predict_boxes - boxes_tran) coord_loss = tf.reduce_mean( tf.reduce_sum(tf.square(boxes_delta), axis=[1, 2, 3, 4]), name='coord_loss') * self.coord_scale #將全部損失放在一塊兒 tf.losses.add_loss(class_loss) tf.losses.add_loss(object_loss) tf.losses.add_loss(noobject_loss) tf.losses.add_loss(coord_loss) # 將每一個損失添加到日誌記錄 tf.summary.scalar('class_loss', class_loss) tf.summary.scalar('object_loss', object_loss) tf.summary.scalar('noobject_loss', noobject_loss) tf.summary.scalar('coord_loss', coord_loss) tf.summary.histogram('boxes_delta_x', boxes_delta[..., 0]) tf.summary.histogram('boxes_delta_y', boxes_delta[..., 1]) tf.summary.histogram('boxes_delta_w', boxes_delta[..., 2]) tf.summary.histogram('boxes_delta_h', boxes_delta[..., 3]) tf.summary.histogram('iou', iou_predict_truth)
這部分代碼主要實現的是對已經構建好的網絡和損失函數利用數據進行訓練,在訓練過程當中,對變量採用了指數平均數(exponential moving average (EMA))來提升總體的訓練性能。同時,爲了得到比較好的學習性能,對學習速率同向進行了指數衰減,使用了 exponential_decay 函數來實現這個功能。這個函數的具體計算公式以下所示:
$decayed_learning_rate=learning_rate∗decay_rate(globalstep/decaysteps) decayed\_learning\_rate = learning\_rate *decay\_rate ^ {(global_step / decay_steps)}$
在訓練的同時,對咱們的訓練模型(網絡權重)進行保存,這樣之後能夠直接進行調用這些權重;同時,每隔必定的迭代次數便寫入 TensorBoard,這樣在最後能夠觀察總體的狀況。
class Solver(object): def __init__(self, net, data): self.net = net self.data = data self.weights_file = cfg.WEIGHT_FILE #網絡權重 self.max_iter = cfg.MAX_ITER #最大迭代數目 self.initial_learning_rate = cfg.LEARNING_RATE self.decay_steps = cfg.DECAY_STEPS self.decay_rate = cfg.DECAY_RATE self.staircase = cfg.STAIRCASE self.summary_iter = cfg.SUMMARY_ITER self.save_iter = cfg.SAVE_ITER self.output_dir = os.path.join( cfg.OUTPUT_PATH, datetime.datetime.now().strftime('%Y_%m_%d_%H_%M')) if not os.path.exists(self.output_dir): os.makedirs(self.output_dir) self.save_cfg() # tf.get_variable 和tf.Variable不一樣的一點是,前者擁有一個變量檢查機制, # 會檢測已經存在的變量是否設置爲共享變量,若是已經存在的變量沒有設置爲共享變量, # TensorFlow 運行到第二個擁有相同名字的變量的時候,就會報錯。 self.variable_to_restore = tf.global_variables() self.restorer = tf.train.Saver(self.variable_to_restore, max_to_keep = None) self.saver = tf.train.Saver(self.variable_to_restore, max_to_keep = None) self.ckpt_file = os.path.join(self.output_dir, 'save.ckpt') self.summary_op = tf.summary.merge_all() self.writer = tf.summary.FileWriter(self.output_dir, flush_secs = 60) self.global_step = tf.get_variable( 'global_step', [], initializer = tf.constant_initializer(0), trainable = False) # 產生一個指數衰減的學習速率 self.learning_rate = tf.train.exponential_decay( self.initial_learning_rate, self.global_step, self.decay_steps, self.decay_rate, self.staircase, name = 'learning_rate') self.optimizer = tf.train.GradientDescentOptimizer( learning_rate = self.learning_rate).minimize( self.net.total_loss, global_step = self.global_step) self.ema = tf.train.ExponentialMovingAverage(decay = 0.9999) self.average_op = self.ema.apply(tf.trainable_variables()) with tf.control_dependencies([self.optimizer]): self.train_op = tf.group(self.average_op) gpu_options = tf.GPUOptions() config = tf.ConfigProto(gpu_options=gpu_options) self.sess = tf.Session(config = config) self.sess.run(tf.global_variables_initializer()) if self.weights_file is not None: print('Restoring weights from: '+ self.weights_file) self.restorer.restore(self.sess, self.weights_file) self.writer.add_graph(self.sess.graph) def train(self): train_timer = Timer() load_timer = Timer() for step in range(1, self.max_iter+1): load_timer.tic() images, labels = self.data.get() load_timer.toc() feec_dict = {self.net.images: images, self.net.labels: labels} if step % self.summary_iter == 0: if step % (self.summary_iter * 10) == 0: train_timer.tic() summary_str, loss, _ = self.sess.run( [self.summary_op, self.net.total_loss, self.train_op], feed_dict = feec_dict) train_timer.toc() log_str = ('{} Epoch: {}, Step: {}, Learning rate : {},' 'Loss: {:5.3f}\nSpeed: {:.3f}s/iter,' ' Load: {:.3f}s/iter, Remain: {}').format( datetime.datetime.now().strftime('%m/%d %H:%M:%S'), self.data.epoch, int(step), round(self.learning_rate.eval(session = self.sess), 6), loss, train_timer.average_time, load_timer.average_time, train_timer.remain(step, self.max_iter)) print(log_str) else: train_timer.tic() summary_str, _ = self.sess.run( [self.summary_op, self.train_op], feed_dict = feec_dict) train_timer.toc() self.writer.add_summary(summary_str, step) else: train_timer.tic() self.sess.run(self.train_op, feed_dict = feec_dict) train_timer.toc() if step % self.save_iter == 0: print('{} Saving checkpoint file to: {}'.format( datetime.datetime.now().strftime('%m/%d %H:%M:%S'), self.output_dir)) self.saver.save(self.sess, self.ckpt_file, global_step = self.global_step) def save_cfg(self): with open(os.path.join(self.output_dir, 'config.txt'), 'w') as f: cfg_dict = cfg.__dict__ for key in sorted(cfg_dict.keys()): if key[0].isupper(): cfg_str = '{}: {}\n'.format(key, cfg_dict[key]) f.write(cfg_str)
最後給出test 部分的源碼,這部分須要使用咱們下載好的 「YOLO_small.ckpt」 權重文件,固然,也可使用咱們以前訓練好的權重文件。
這部分的主要內容就是利用訓練好的權重進行預測,獲得預測輸出後利用 OpenCV 的相關函數進行畫框等操做。同時,還能夠利用 OpenCV 進行視頻處理,使程序可以實時地對視頻流進行檢測。所以,在閱讀本段程序以前,你們應該對 OpenCV 有一個大體的瞭解。
具體代碼以下所示:
class Detector(object): def __init__(self, net, weight_file): self.net = net self.weights_file = weight_file self.classes = cfg.CLASSES self.num_class = len(self.classes) self.image_size = cfg.IMAGE_SIZE self.cell_size = cfg.CELL_SIZE self.boxes_per_cell = cfg.BOXES_PER_CELL self.threshold = cfg.THRESHOLD self.iou_threshold = cfg.IOU_THRESHOLD self.boundary1 = self.cell_size * self.cell_size * self.num_class self.boundary2 = self.boundary1 + self.cell_size * self.cell_size * self.boxes_per_cell self.sess = tf.Session() self.sess.run(tf.global_variables_initializer()) print('Restoring weights from: ' + self.weights_file) self.saver = tf.train.Saver() self.saver.restore(self.sess, self.weights_file) def draw_result(self, img, result): for i in range(len(result)): x = int(result[i][1]) y = int(result[i][2]) w = int(result[i][3] / 2) h = int(result[i][4] / 2) cv2.rectangle(img, (x - w, y - h), (x + w, y + h), (0, 255, 0), 2) cv2.rectangle(img, (x - w, y - h - 20), (x + w, y - h), (125, 125, 125), -1) cv2.putText(img, result[i][0] + ' : %.2f' % result[i][5], (x - w + 5, y - h - 7), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) def detect(self, img): img_h, img_w, _ = img.shape inputs = cv2.resize(img, (self.image_size, self.image_size)) inputs = cv2.cvtColor(inputs, cv2.COLOR_BGR2RGB).astype(np.float32) inputs = (inputs / 255.0) * 2.0 - 1.0 inputs = np.reshape(inputs, (1, self.image_size, self.image_size, 3)) result = self.detect_from_cvmat(inputs)[0] for i in range(len(result)): result[i][1] *= (1.0 * img_w / self.image_size) result[i][2] *= (1.0 * img_h / self.image_size) result[i][3] *= (1.0 * img_w / self.image_size) result[i][4] *= (1.0 * img_h / self.image_size) return result def detect_from_cvmat(self, inputs): net_output = self.sess.run(self.net.logits, feed_dict={self.net.images: inputs}) results = [] for i in range(net_output.shape[0]): results.append(self.interpret_output(net_output[i])) return results def interpret_output(self, output): probs = np.zeros((self.cell_size, self.cell_size, self.boxes_per_cell, self.num_class)) class_probs = np.reshape(output[0:self.boundary1], (self.cell_size, self.cell_size, self.num_class)) scales = np.reshape(output[self.boundary1:self.boundary2], (self.cell_size, self.cell_size, self.boxes_per_cell)) boxes = np.reshape(output[self.boundary2:], (self.cell_size, self.cell_size, self.boxes_per_cell, 4)) offset = np.transpose(np.reshape(np.array([np.arange(self.cell_size)] * self.cell_size * self.boxes_per_cell), [self.boxes_per_cell, self.cell_size, self.cell_size]), (1, 2, 0)) boxes[:, :, :, 0] += offset boxes[:, :, :, 1] += np.transpose(offset, (1, 0, 2)) boxes[:, :, :, :2] = 1.0 * boxes[:, :, :, 0:2] / self.cell_size boxes[:, :, :, 2:] = np.square(boxes[:, :, :, 2:]) boxes *= self.image_size for i in range(self.boxes_per_cell): for j in range(self.num_class): probs[:, :, i, j] = np.multiply( class_probs[:, :, j], scales[:, :, i]) filter_mat_probs = np.array(probs >= self.threshold, dtype='bool') filter_mat_boxes = np.nonzero(filter_mat_probs) boxes_filtered = boxes[filter_mat_boxes[0], filter_mat_boxes[1], filter_mat_boxes[2]] probs_filtered = probs[filter_mat_probs] classes_num_filtered = np.argmax(filter_mat_probs, axis=3)[filter_mat_boxes[ 0], filter_mat_boxes[1], filter_mat_boxes[2]] argsort = np.array(np.argsort(probs_filtered))[::-1] boxes_filtered = boxes_filtered[argsort] probs_filtered = probs_filtered[argsort] classes_num_filtered = classes_num_filtered[argsort] for i in range(len(boxes_filtered)): if probs_filtered[i] == 0: continue for j in range(i + 1, len(boxes_filtered)): if self.iou(boxes_filtered[i], boxes_filtered[j]) > self.iou_threshold: probs_filtered[j] = 0.0 filter_iou = np.array(probs_filtered > 0.0, dtype='bool') boxes_filtered = boxes_filtered[filter_iou] probs_filtered = probs_filtered[filter_iou] classes_num_filtered = classes_num_filtered[filter_iou] result = [] for i in range(len(boxes_filtered)): result.append([self.classes[classes_num_filtered[i]], boxes_filtered[i][0], boxes_filtered[ i][1], boxes_filtered[i][2], boxes_filtered[i][3], probs_filtered[i]]) return result def iou(self, box1, box2): tb = min(box1[0] + 0.5 * box1[2], box2[0] + 0.5 * box2[2]) - \ max(box1[0] - 0.5 * box1[2], box2[0] - 0.5 * box2[2]) lr = min(box1[1] + 0.5 * box1[3], box2[1] + 0.5 * box2[3]) - \ max(box1[1] - 0.5 * box1[3], box2[1] - 0.5 * box2[3]) if tb < 0 or lr < 0: intersection = 0 else: intersection = tb * lr return intersection / (box1[2] * box1[3] + box2[2] * box2[3] - intersection) def camera_detector(self, cap, wait=10): detect_timer = Timer() ret, _ = cap.read() while ret: ret, frame = cap.read() detect_timer.tic() result = self.detect(frame) detect_timer.toc() print('Average detecting time: {:.3f}s'.format(detect_timer.average_time)) self.draw_result(frame, result) cv2.namedWindow('Camera',0) cv2.imshow('Camera', frame) cv2.waitKey(wait) ret, frame = cap.read() def image_detector(self, imname, wait=0): detect_timer = Timer() image = cv2.imread(imname) detect_timer.tic() result = self.detect(image) detect_timer.toc() print('Average detecting time: {:.3f}s'.format(detect_timer.average_time)) self.draw_result(image, result) cv2.imshow('Image', image) cv2.waitKey(wait)