原文:https://www.cnblogs.com/zyly/p/9534063.html
yolo源碼來源於網址:https://github.com/hizhangp/yolo_tensorflow
在講解源碼之前,我們需要做一些准備工作:
- 下載源碼,本文所使用的yolo源碼來源於網址:https://github.com/hizhangp/yolo_tensorflow
- 下載訓練所使用的數據集,我們仍然使用以VOC 2012數據集為例,下載地址為:http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar。
- yolo源碼所在目錄下,創建一個目錄data,然后在data里面創建一個pascal_voc目錄,用來保存與VOC 2012數據集相關的數據,我們把下載好的數據集解壓到該目錄下,其中VOCdevkit為數據集解壓得到的文件,剩余三個文件夾我們先不用理會,后面會詳細介紹
- 下載與訓練模型,即YOLO_small文件,我們把下載好之后的文件解壓放在weights文件夾下面。
-
根據自己的需求修改配置文件yolo/config.py。
- 運行train.py文件,開始訓練。
- 運行test.py文件,開始測試。
二 yolo代碼文件結構
我們來粗略的介紹一下每個文件的功能:
- data文件夾,上面已經說過了,存放數據集以及訓練時生成的模型,緩存文件。
- test文件夾,用來存放測試時用到的圖片。
- utils文件夾,包含兩個文件一個是pascal_voc.py,主要用來獲取訓練集圖片文件,以及生成對應的標簽文件,為yolo網絡訓練做准備。另一個文件是timer.py用來計時。
- yolo文件夾,也包含兩個文件,config.py包含yolo網絡的配置參數,yolo_net.py文件包含yolo網絡的結構。
- train.py文件用來訓練yolo網絡。
- test.py文件用來測試yolo網絡。
三 config.py文件講解
我們先從配置文件說起,代碼如下:
# -*- 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網絡的構建。
四 yolo_net.py文件講解
第一部分:
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_network
這部分主要是實現了 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_4、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_6、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_7、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_8、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_9、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_11、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_12、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_13、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_14、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_15、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_16、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_17、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_18、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_19、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_22、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_23、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_24、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_25、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_26、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_28、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_29、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)
五、train.py講解
這部分代碼主要實現的是對已經構建好的網絡和損失函數利用數據進行訓練,在訓練過程中,對變量采用了指數平均數(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.py
最后給出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)
