睿智的目标檢測34——Keras搭建YoloV4-Tiny目标檢測平台
- 學習前言
- 什麼是YOLOV4-Tiny
- 代碼下載下傳
- YoloV4-Tiny結構解析
-
- 1、主幹特征提取網絡Backbone
- 2、特征金字塔
- 3、YoloHead利用獲得到的特征進行預測
- 4、預測結果的解碼
- 5、在原圖上進行繪制
- YoloV4-Tiny的訓練
-
- 1、YOLOV4的改進訓練技巧
-
- a)、Mosaic資料增強
- b)、Label Smoothing平滑
- c)、CIOU
- d)、學習率餘弦退火衰減
- 2、loss組成
-
- a)、計算loss所需參數
- b)、y_pre是什麼
- c)、y_true是什麼。
- d)、loss的計算過程
- 訓練自己的YoloV4模型
-
- 一、資料集的準備
- 二、資料集的處理
- 三、開始網絡訓練
- 四、訓練結果預測
學習前言
Tiny系列一直沒去關注,做一下試試看。
什麼是YOLOV4-Tiny
YOLOV4是YOLOV3的改進版,在YOLOV3的基礎上結合了非常多的小Tricks。
盡管沒有目标檢測上革命性的改變,但是YOLOV4依然很好的結合了速度與精度。
根據上圖也可以看出來,YOLOV4在YOLOV3的基礎上,在FPS不下降的情況下,mAP達到了44,提高非常明顯。
YOLOV4整體上的檢測思路和YOLOV3相比相差并不大,都是使用三個特征層進行分類與回歸預測。
YoloV4-Tiny是YoloV4的簡化版,少了一些結構,但是速度大大增加了,YoloV4共有約6000萬參數,YoloV4-Tiny則隻有600萬參數。
YoloV4-Tiny僅使用了兩個特征層進行分類與回歸預測。
代碼下載下傳
https://github.com/bubbliiiing/yolov4-tiny-keras
喜歡的可以給個star噢!
YoloV4-Tiny結構解析
1、主幹特征提取網絡Backbone
當輸入是416x416時,特征結構如下:
當輸入是608x608時,特征結構如下:
而在YoloV4-Tiny中,其使用了CSPdarknet53_tiny作為主幹特征提取網絡。
和CSPdarknet53相比,為了更快速,将激活函數重新修改為LeakyReLU。
CSPdarknet53_tiny具有兩個特點:
1、使用了CSPnet結構。
CSPnet結構并不算複雜,就是将原來的殘差塊的堆疊進行了一個拆分,拆成左右兩部分:
主幹部分繼續進行原來的殘差塊的堆疊;
另一部分則像一個殘差邊一樣,經過少量處理直接連接配接到最後。
是以可以認為CSP中存在一個大的殘差邊。
2、進行通道的分割
在CSPnet的主幹部分,CSPdarknet53_tiny會對一次3x3卷積後的特征層進行通道的劃分,分成兩部分,取第二部分。
在tensorflow中使用tf.split進行劃分。
#---------------------------------------------------#
# CSPdarknet的結構塊
# 存在一個大殘差邊
# 這個大殘差邊繞過了很多的殘差結構
#---------------------------------------------------#
def resblock_body(x, num_filters):
x = DarknetConv2D_BN_Leaky(num_filters, (3,3))(x)
route = x
x = Lambda(route_group,arguments={'groups':2, 'group_id':1})(x)
x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
route_1 = x
x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
x = Concatenate()([x, route_1])
x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x)
feat = x
x = Concatenate()([route, x])
x = MaxPooling2D(pool_size=[2,2],)(x)
# 最後對通道數進行整合
return x, feat
利用主幹特征提取網絡,我們可以獲得兩個shape的有效特征層,即CSPdarknet53_tiny最後兩個shape的有效特征層,傳入加強特征提取網絡當中進行FPN的建構。
全部實作代碼為:
from functools import wraps
from keras import backend as K
from keras.layers import Conv2D, Add, ZeroPadding2D, UpSampling2D, Concatenate, MaxPooling2D, Layer, Lambda
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.regularizers import l2
from utils.utils import compose
import tensorflow as tf
def route_group(input_layer, groups, group_id):
convs = tf.split(input_layer, num_or_size_splits=groups, axis=-1)
return convs[group_id]
#--------------------------------------------------#
# 單次卷積
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
darknet_conv_kwargs.update(kwargs)
return Conv2D(*args, **darknet_conv_kwargs)
#---------------------------------------------------#
# 卷積塊
# DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs):
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
return compose(
DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(),
LeakyReLU(alpha=0.1))
#---------------------------------------------------#
# CSPdarknet的結構塊
# 存在一個大殘差邊
# 這個大殘差邊繞過了很多的殘差結構
#---------------------------------------------------#
def resblock_body(x, num_filters):
x = DarknetConv2D_BN_Leaky(num_filters, (3,3))(x)
route = x
x = Lambda(route_group,arguments={'groups':2, 'group_id':1})(x)
x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
route_1 = x
x = DarknetConv2D_BN_Leaky(int(num_filters/2), (3,3))(x)
x = Concatenate()([x, route_1])
x = DarknetConv2D_BN_Leaky(num_filters, (1,1))(x)
feat = x
x = Concatenate()([route, x])
x = MaxPooling2D(pool_size=[2,2],)(x)
# 最後對通道數進行整合
return x, feat
#---------------------------------------------------#
# darknet53 的主體部分
#---------------------------------------------------#
def darknet_body(x):
# 進行長和寬的壓縮
x = ZeroPadding2D(((1,0),(1,0)))(x)
x = DarknetConv2D_BN_Leaky(32, (3,3), strides=(2,2))(x)
# 進行長和寬的壓縮
x = ZeroPadding2D(((1,0),(1,0)))(x)
x = DarknetConv2D_BN_Leaky(64, (3,3), strides=(2,2))(x)
x, _ = resblock_body(x,num_filters = 64)
x, _ = resblock_body(x,num_filters = 128)
x, feat1 = resblock_body(x,num_filters = 256)
x = DarknetConv2D_BN_Leaky(512, (3,3))(x)
feat2 = x
return feat1, feat2
2、特征金字塔
當輸入是416x416時,特征結構如下:
當輸入是608x608時,特征結構如下:
YoloV4-Tiny中使用了FPN的結構,主要是對第一步獲得的兩個有效特征層進行特征融合。
FPN會将最後一個shape的有效特征層卷積後進行上采樣,然後與上一個shape的有效特征層進行堆疊并卷積。
實作代碼如下:
#--------------------------------------------------#
# 單次卷積
#--------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
darknet_conv_kwargs = {}
darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'
darknet_conv_kwargs.update(kwargs)
return Conv2D(*args, **darknet_conv_kwargs)
#---------------------------------------------------#
# 卷積塊
# DarknetConv2D + BatchNormalization + LeakyReLU
#---------------------------------------------------#
def DarknetConv2D_BN_Leaky(*args, **kwargs):
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
return compose(
DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(),
LeakyReLU(alpha=0.1))
#---------------------------------------------------#
# 特征層->最後的輸出
#---------------------------------------------------#
def yolo_body(inputs, num_anchors, num_classes):
# 生成darknet53的主幹模型
feat1,feat2 = darknet_body(inputs)
# 第一個特征層
# y1=(batch_size,13,13,3,85)
P5 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2)
P5_output = DarknetConv2D_BN_Leaky(512, (3,3))(P5)
P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output)
P5_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P5)
P4 = Concatenate()([feat1, P5_upsample])
P4_output = DarknetConv2D_BN_Leaky(256, (3,3))(P4)
P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output)
return Model(inputs, [P5_output, P4_output])
3、YoloHead利用獲得到的特征進行預測
當輸入是416x416時,特征結構如下:
當輸入是608x608時,特征結構如下:
1、在特征利用部分,YoloV4-Tiny提取多特征層進行目标檢測,一共提取兩個特征層,兩個特征層的shape分别為(26,26,128)、(13,13,512)。
2、輸出層的shape分别為(13,13,75),(26,26,75),最後一個次元為75是因為該圖是基于voc資料集的,它的類為20種,YoloV4-Tiny隻有針對每一個特征層存在3個先驗框,是以最後次元為3x25;
如果使用的是coco訓練集,類則為80種,最後的次元應該為255 = 3x85,兩個特征層的shape為(13,13,255),(26,26,255)
實作代碼如下:
#---------------------------------------------------#
# 特征層->最後的輸出
#---------------------------------------------------#
def yolo_body(inputs, num_anchors, num_classes):
# 生成darknet53的主幹模型
feat1,feat2 = darknet_body(inputs)
# 第一個特征層
# y1=(batch_size,13,13,3,85)
P5 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2)
P5_output = DarknetConv2D_BN_Leaky(512, (3,3))(P5)
P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output)
P5_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P5)
P4 = Concatenate()([feat1, P5_upsample])
P4_output = DarknetConv2D_BN_Leaky(256, (3,3))(P4)
P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output)
return Model(inputs, [P5_output, P4_output])
4、預測結果的解碼
由第三步我們可以獲得兩個特征層的預測結果,shape分别為(N,13,13,255),(N,26,26,255)的資料,對應每個圖分為13x13、26x26的網格上3個預測框的位置。
但是這個預測結果并不對應着最終的預測框在圖檔上的位置,還需要解碼才可以完成。
此處要講一下yolo的預測原理,yolo的特征層分别将整幅圖分為13x13、26x26的網格,每個網絡點負責一個區域的檢測。
我們知道特征層的預測結果對應着三個預測框的位置,我們先将其reshape一下,其結果為(N,13,13,3,85),(N,26,26,3,85)。
最後一個次元中的85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分類結果。
yolo的解碼過程就是将每個網格點加上它對應的x_offset和y_offset,加完後的結果就是預測框的中心,然後再利用 先驗框和h、w結合 計算出預測框的長和寬。這樣就能得到整個預測框的位置了。
當然得到最終的預測結構後還要進行得分排序與非極大抑制篩選
這一部分基本上是所有目标檢測通用的部分。不過該項目的處理方式與其它項目不同。其對于每一個類進行判别。
1、取出每一類得分大于self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。
實作代碼如下,當調用yolo_eval時,就會對每個特征層進行解碼:
import tensorflow as tf
from keras import backend as K
#---------------------------------------------------#
# 對box進行調整,使其符合真實圖檔的樣子
#---------------------------------------------------#
def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image):
#-----------------------------------------------------------------#
# 把y軸放前面是因為友善預測框和圖像的寬高進行相乘
#-----------------------------------------------------------------#
box_yx = box_xy[..., ::-1]
box_hw = box_wh[..., ::-1]
input_shape = K.cast(input_shape, K.dtype(box_yx))
image_shape = K.cast(image_shape, K.dtype(box_yx))
if letterbox_image:
#-----------------------------------------------------------------#
# 這裡求出來的offset是圖像有效區域相對于圖像左上角的偏移情況
# new_shape指的是寬高縮放情況
#-----------------------------------------------------------------#
new_shape = K.round(image_shape * K.min(input_shape/image_shape))
offset = (input_shape - new_shape)/2./input_shape
scale = input_shape/new_shape
box_yx = (box_yx - offset) * scale
box_hw *= scale
box_mins = box_yx - (box_hw / 2.)
box_maxes = box_yx + (box_hw / 2.)
boxes = K.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]])
boxes *= K.concatenate([image_shape, image_shape])
return boxes
#---------------------------------------------------#
# 将預測值的每個特征層調成真實值
#---------------------------------------------------#
def get_anchors_and_decode(feats, anchors, num_classes, input_shape, calc_loss=False):
num_anchors = len(anchors)
#------------------------------------------#
# grid_shape指的是特征層的高和寬
#------------------------------------------#
grid_shape = K.shape(feats)[1:3]
#--------------------------------------------------------------------#
# 獲得各個特征點的坐标資訊。生成的shape為(13, 13, num_anchors, 2)
#--------------------------------------------------------------------#
grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]), [grid_shape[0], 1, num_anchors, 1])
grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]), [1, grid_shape[1], num_anchors, 1])
grid = K.cast(K.concatenate([grid_x, grid_y]), K.dtype(feats))
#---------------------------------------------------------------#
# 将先驗框進行拓展,生成的shape為(13, 13, num_anchors, 2)
#---------------------------------------------------------------#
anchors_tensor = K.reshape(K.constant(anchors), [1, 1, num_anchors, 2])
anchors_tensor = K.tile(anchors_tensor, [grid_shape[0], grid_shape[1], 1, 1])
#---------------------------------------------------#
# 将預測結果調整成(batch_size,13,13,3,85)
# 85可拆分成4 + 1 + 80
# 4代表的是中心寬高的調整參數
# 1代表的是框的置信度
# 80代表的是種類的置信度
#---------------------------------------------------#
feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
#------------------------------------------#
# 對先驗框進行解碼,并進行歸一化
#------------------------------------------#
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的時候傳回grid, feats, box_xy, box_wh
# 在預測的時候傳回box_xy, box_wh, box_confidence, box_class_probs
#---------------------------------------------------------------------#
if calc_loss == True:
return grid, feats, box_xy, box_wh
return box_xy, box_wh, box_confidence, box_class_probs
#---------------------------------------------------#
# 圖檔預測
#---------------------------------------------------#
def DecodeBox(outputs,
anchors,
num_classes,
image_shape,
input_shape,
#-----------------------------------------------------------#
# 13x13的特征層對應的anchor是[81,82],[135,169],[344,319]
# 26x26的特征層對應的anchor是[10,14],[23,27],[37,58]
#-----------------------------------------------------------#
anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
max_boxes = 100,
confidence = 0.5,
nms_iou = 0.3,
letterbox_image = True):
box_xy = []
box_wh = []
box_confidence = []
box_class_probs = []
for i in range(len(outputs)):
sub_box_xy, sub_box_wh, sub_box_confidence, sub_box_class_probs = \
get_anchors_and_decode(outputs[i], anchors[anchor_mask[i]], num_classes, input_shape)
box_xy.append(K.reshape(sub_box_xy, [-1, 2]))
box_wh.append(K.reshape(sub_box_wh, [-1, 2]))
box_confidence.append(K.reshape(sub_box_confidence, [-1, 1]))
box_class_probs.append(K.reshape(sub_box_class_probs, [-1, num_classes]))
box_xy = K.concatenate(box_xy, axis = 0)
box_wh = K.concatenate(box_wh, axis = 0)
box_confidence = K.concatenate(box_confidence, axis = 0)
box_class_probs = K.concatenate(box_class_probs, axis = 0)
#------------------------------------------------------------------------------------------------------------#
# 在圖像傳入網絡預測前會進行letterbox_image給圖像周圍添加灰條,是以生成的box_xy, box_wh是相對于有灰條的圖像的
# 我們需要對其進行修改,去除灰條的部分。 将box_xy、和box_wh調節成y_min,y_max,xmin,xmax
# 如果沒有使用letterbox_image也需要将歸一化後的box_xy, box_wh調整成相對于原圖大小的
#------------------------------------------------------------------------------------------------------------#
boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)
box_scores = box_confidence * box_class_probs
#-----------------------------------------------------------#
# 判斷得分是否大于score_threshold
#-----------------------------------------------------------#
mask = box_scores >= confidence
max_boxes_tensor = K.constant(max_boxes, dtype='int32')
boxes_out = []
scores_out = []
classes_out = []
for c in range(num_classes):
#-----------------------------------------------------------#
# 取出所有box_scores >= score_threshold的框,和成績
#-----------------------------------------------------------#
class_boxes = tf.boolean_mask(boxes, mask[:, c])
class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])
#-----------------------------------------------------------#
# 非極大抑制
# 保留一定區域内得分最大的框
#-----------------------------------------------------------#
nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=nms_iou)
#-----------------------------------------------------------#
# 擷取非極大抑制後的結果
# 下列三個分别是:框的位置,得分與種類
#-----------------------------------------------------------#
class_boxes = K.gather(class_boxes, nms_index)
class_box_scores = K.gather(class_box_scores, nms_index)
classes = K.ones_like(class_box_scores, 'int32') * c
boxes_out.append(class_boxes)
scores_out.append(class_box_scores)
classes_out.append(classes)
boxes_out = K.concatenate(boxes_out, axis=0)
scores_out = K.concatenate(scores_out, axis=0)
classes_out = K.concatenate(classes_out, axis=0)
return boxes_out, scores_out, classes_out
5、在原圖上進行繪制
通過第四步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪制在圖檔上,就可以獲得結果了。
YoloV4-Tiny的訓練
1、YOLOV4的改進訓練技巧
a)、Mosaic資料增強
Yolov4的mosaic資料增強參考了CutMix資料增強方式,理論上具有一定的相似性!
CutMix資料增強方式利用兩張圖檔進行拼接。
但是mosaic利用了四張圖檔,根據論文所說其擁有一個巨大的優點是豐富檢測物體的背景!且在BN計算的時候一下子會計算四張圖檔的資料!
就像下圖這樣:
實作思路如下:
1、每次讀取四張圖檔。
2、分别對四張圖檔進行翻轉、縮放、色域變化等,并且按照四個方向位置擺好。
3、進行圖檔的組合和框的組合
def rand(a=0, b=1):
return np.random.rand()*(b-a) + a
def merge_bboxes(bboxes, cutx, cuty):
merge_bbox = []
for i in range(len(bboxes)):
for box in bboxes[i]:
tmp_box = []
x1,y1,x2,y2 = box[0], box[1], box[2], box[3]
if i == 0:
if y1 > cuty or x1 > cutx:
continue
if y2 >= cuty and y1 <= cuty:
y2 = cuty
if y2-y1 < 5:
continue
if x2 >= cutx and x1 <= cutx:
x2 = cutx
if x2-x1 < 5:
continue
if i == 1:
if y2 < cuty or x1 > cutx:
continue
if y2 >= cuty and y1 <= cuty:
y1 = cuty
if y2-y1 < 5:
continue
if x2 >= cutx and x1 <= cutx:
x2 = cutx
if x2-x1 < 5:
continue
if i == 2:
if y2 < cuty or x2 < cutx:
continue
if y2 >= cuty and y1 <= cuty:
y1 = cuty
if y2-y1 < 5:
continue
if x2 >= cutx and x1 <= cutx:
x1 = cutx
if x2-x1 < 5:
continue
if i == 3:
if y1 > cuty or x2 < cutx:
continue
if y2 >= cuty and y1 <= cuty:
y2 = cuty
if y2-y1 < 5:
continue
if x2 >= cutx and x1 <= cutx:
x1 = cutx
if x2-x1 < 5:
continue
tmp_box.append(x1)
tmp_box.append(y1)
tmp_box.append(x2)
tmp_box.append(y2)
tmp_box.append(box[-1])
merge_bbox.append(tmp_box)
return merge_bbox
def get_random_data(annotation_line, input_shape, random=True, hue=.1, sat=1.5, val=1.5, proc_img=True):
'''random preprocessing for real-time data augmentation'''
h, w = input_shape
min_offset_x = 0.4
min_offset_y = 0.4
scale_low = 1-min(min_offset_x,min_offset_y)
scale_high = scale_low+0.2
image_datas = []
box_datas = []
index = 0
place_x = [0,0,int(w*min_offset_x),int(w*min_offset_x)]
place_y = [0,int(h*min_offset_y),int(w*min_offset_y),0]
for line in annotation_line:
# 每一行進行分割
line_content = line.split()
# 打開圖檔
image = Image.open(line_content[0])
image = image.convert("RGB")
# 圖檔的大小
iw, ih = image.size
# 儲存框的位置
box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]])
# image.save(str(index)+".jpg")
# 是否翻轉圖檔
flip = rand()<.5
if flip and len(box)>0:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
box[:, [0,2]] = iw - box[:, [2,0]]
# 對輸入進來的圖檔進行縮放
new_ar = w/h
scale = rand(scale_low, scale_high)
if new_ar < 1:
nh = int(scale*h)
nw = int(nh*new_ar)
else:
nw = int(scale*w)
nh = int(nw/new_ar)
image = image.resize((nw,nh), Image.BICUBIC)
# 進行色域變換
hue = rand(-hue, hue)
sat = rand(1, sat) if rand()<.5 else 1/rand(1, sat)
val = rand(1, val) if rand()<.5 else 1/rand(1, val)
x = rgb_to_hsv(np.array(image)/255.)
x[..., 0] += hue
x[..., 0][x[..., 0]>1] -= 1
x[..., 0][x[..., 0]<0] += 1
x[..., 1] *= sat
x[..., 2] *= val
x[x>1] = 1
x[x<0] = 0
image = hsv_to_rgb(x)
image = Image.fromarray((image*255).astype(np.uint8))
# 将圖檔進行放置,分别對應四張分割圖檔的位置
dx = place_x[index]
dy = place_y[index]
new_image = Image.new('RGB', (w,h), (128,128,128))
new_image.paste(image, (dx, dy))
image_data = np.array(new_image)/255
# Image.fromarray((image_data*255).astype(np.uint8)).save(str(index)+"distort.jpg")
index = index + 1
box_data = []
# 對box進行重新處理
if len(box)>0:
np.random.shuffle(box)
box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx
box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy
box[:, 0:2][box[:, 0:2]<0] = 0
box[:, 2][box[:, 2]>w] = w
box[:, 3][box[:, 3]>h] = h
box_w = box[:, 2] - box[:, 0]
box_h = box[:, 3] - box[:, 1]
box = box[np.logical_and(box_w>1, box_h>1)]
box_data = np.zeros((len(box),5))
box_data[:len(box)] = box
image_datas.append(image_data)
box_datas.append(box_data)
img = Image.fromarray((image_data*255).astype(np.uint8))
for j in range(len(box_data)):
thickness = 3
left, top, right, bottom = box_data[j][0:4]
draw = ImageDraw.Draw(img)
for i in range(thickness):
draw.rectangle([left + i, top + i, right - i, bottom - i],outline=(255,255,255))
img.show()
# 将圖檔分割,放在一起
cutx = np.random.randint(int(w*min_offset_x), int(w*(1 - min_offset_x)))
cuty = np.random.randint(int(h*min_offset_y), int(h*(1 - min_offset_y)))
new_image = np.zeros([h,w,3])
new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :]
# 對框進行進一步的處理
new_boxes = merge_bboxes(box_datas, cutx, cuty)
return new_image, new_boxes
b)、Label Smoothing平滑
标簽平滑的思想很簡單,具體公式如下:
new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes
當label_smoothing的值為0.01得時候,公式變成如下所示:
new_onehot_labels = y * (1 - 0.01) + 0.01 / num_classes
其實Label Smoothing平滑就是将标簽進行一個平滑,原始的标簽是0、1,在平滑後變成0.005(如果是二分類)、0.995,也就是說對分類準确做了一點懲罰,讓模型不可以分類的太準确,太準确容易過拟合。
實作代碼如下:
#---------------------------------------------------#
# 平滑标簽
#---------------------------------------------------#
def _smooth_labels(y_true, label_smoothing):
num_classes = K.shape(y_true)[-1],
label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
c)、CIOU
IoU是比值的概念,對目标物體的scale是不敏感的。然而常用的BBox的回歸損失優化和IoU優化不是完全等價的,尋常的IoU無法直接優化沒有重疊的部分。
于是有人提出直接使用IOU作為回歸優化loss,CIOU是其中非常優秀的一種想法。
CIOU将目标與anchor之間的距離,重疊率、尺度以及懲罰項都考慮進去,使得目标框回歸變得更加穩定,不會像IoU和GIoU一樣出現訓練過程中發散等問題。而懲罰因子把預測框長寬比拟合目标框的長寬比考慮進去。
CIOU公式如下
C I O U = I O U − ρ 2 ( b , b g t ) c 2 − α v CIOU = IOU - \frac{\rho^2(b,b^{gt})}{c^2} - \alpha v CIOU=IOU−c2ρ2(b,bgt)−αv
其中, ρ 2 ( b , b g t ) \rho^2(b,b^{gt}) ρ2(b,bgt)分别代表了預測框和真實框的中心點的歐式距離。 c代表的是能夠同時包含預測框和真實框的最小閉包區域的對角線距離。
而 α \alpha α和 v v v的公式如下
α = v 1 − I O U + v \alpha = \frac{v}{1-IOU+v} α=1−IOU+vv
v = 4 π 2 ( a r c t a n w g t h g t − a r c t a n w h ) 2 v = \frac{4}{\pi ^2}(arctan\frac{w^{gt}}{h^{gt}}-arctan\frac{w}{h})^2 v=π24(arctanhgtwgt−arctanhw)2
把1-CIOU就可以得到相應的LOSS了。
L O S S C I O U = 1 − I O U + ρ 2 ( b , b g t ) c 2 + α v LOSS_{CIOU} = 1 - IOU + \frac{\rho^2(b,b^{gt})}{c^2} + \alpha v LOSSCIOU=1−IOU+c2ρ2(b,bgt)+αv
def box_ciou(b1, b2):
"""
輸入為:
----------
b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
傳回為:
-------
ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
"""
#-----------------------------------------------------------#
# 求出預測框左上角右下角
# b1_mins (batch, feat_w, feat_h, anchor_num, 2)
# b1_maxes (batch, feat_w, feat_h, anchor_num, 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
#-----------------------------------------------------------#
# 求出真實框左上角右下角
# b2_mins (batch, feat_w, feat_h, anchor_num, 2)
# b2_maxes (batch, feat_w, feat_h, anchor_num, 2)
#-----------------------------------------------------------#
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
#-----------------------------------------------------------#
# 求真實框和預測框所有的iou
# iou (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
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]
union_area = b1_area + b2_area - intersect_area
iou = intersect_area / K.maximum(union_area, K.epsilon())
#-----------------------------------------------------------#
# 計算中心的差距
# center_distance (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
enclose_mins = K.minimum(b1_mins, b2_mins)
enclose_maxes = K.maximum(b1_maxes, b2_maxes)
enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
#-----------------------------------------------------------#
# 計算對角線距離
# enclose_diagonal (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
ciou = iou - 1.0 * (center_distance) / K.maximum(enclose_diagonal ,K.epsilon())
v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0], K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi)
alpha = v / K.maximum((1.0 - iou + v), K.epsilon())
ciou = ciou - alpha * v
ciou = K.expand_dims(ciou, -1)
return ciou
d)、學習率餘弦退火衰減
餘弦退火衰減法,學習率會先上升再下降,這是退火優化法的思想。(關于什麼是退火算法可以百度。)
上升的時候使用線性上升,下降的時候模拟cos函數下降。執行多次。
效果如圖所示:
實作方式如下,利用Callback實作,與普通的ReduceLROnPlateau調用方式類似:
class WarmUpCosineDecayScheduler(keras.callbacks.Callback):
def __init__(self, T_max, eta_min=0, verbose=0):
super(WarmUpCosineDecayScheduler, self).__init__()
self.T_max = T_max
self.eta_min = eta_min
self.verbose = verbose
self.init_lr = 0
self.last_epoch = 0
def on_train_begin(self, batch, logs=None):
self.init_lr = K.get_value(self.model.optimizer.lr)
def on_epoch_end(self, batch, logs=None):
learning_rate = self.eta_min + (self.init_lr - self.eta_min) * (1 + math.cos(math.pi * self.last_epoch / self.T_max)) / 2
self.last_epoch += 1
K.set_value(self.model.optimizer.lr, learning_rate)
if self.verbose > 0:
print('Setting learning rate to %s.' % (learning_rate))
2、loss組成
a)、計算loss所需參數
在計算loss的時候,實際上是y_pre和y_true之間的對比:
y_pre就是一幅圖像經過網絡之後的輸出,内部含有兩個特征層的内容;其需要解碼才能夠在圖上作畫
y_true就是一個真實圖像中,它的每個真實框對應的(13,13)、(26,26)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一緻
實際上y_pre和y_true内容的shape都是
(batch_size,13,13,3,85)
(batch_size,26,26,3,85)
b)、y_pre是什麼
網絡最後輸出的内容就是兩個特征層每個網格點對應的預測框及其種類,即兩個特征層分别對應着圖檔被分為不同size的網格後,每個網格點上三個先驗框對應的位置、置信度及其種類。
對于輸出的y1、y2、y3而言,[…, : 2]指的是相對于每個網格點的偏移量,[…, 2: 4]指的是寬和高,[…, 4: 5]指的是該框的置信度,[…, 5: ]指的是每個種類的預測機率。
現在的y_pre還是沒有解碼的,解碼了之後才是真實圖像上的情況。
c)、y_true是什麼。
y_true就是一個真實圖像中,它的每個真實框對應的(13,13)、(26,26)網格上的偏移位置、長寬與種類。其仍需要編碼才能與y_pred的結構一緻
在yolo4中,其使用了一個專門的函數用于處理讀取進來的圖檔的框的真實情況。
其輸入為:
true_boxes:shape為(m, T, 5)代表m張圖T個框的x_min、y_min、x_max、y_max、class_id。
input_shape:輸入的形狀,此處為608、608
anchors:代表9個先驗框的大小
num_classes:種類的數量。
其實對真實框的處理是将真實框轉化成圖檔中相對網格的xyhw,步驟如下:
1、取框的真實值,擷取其框的中心及其寬高,除去input_shape變成比例的模式。
2、建立全為0的y_true,y_true是一個清單,包含兩個特征層,shape分别為(batch_size,13,13,3,85)、(batch_size,26,26,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(self, true_boxes, input_shape, anchors, num_classes):
assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'
#-----------------------------------------------------------#
# 獲得框的坐标和圖檔的大小
#-----------------------------------------------------------#
true_boxes = np.array(true_boxes, dtype='float32')
input_shape = np.array(input_shape, dtype='int32')
#-----------------------------------------------------------#
# 一共有三個特征層數
#-----------------------------------------------------------#
num_layers = len(self.anchors_mask)
#-----------------------------------------------------------#
# m為圖檔數量,grid_shapes為網格的shape
#-----------------------------------------------------------#
m = true_boxes.shape[0]
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)
#-----------------------------------------------------------#
y_true = [np.zeros((m, grid_shapes[l][0], grid_shapes[l][1], len(self.anchors_mask[l]), 5 + num_classes),
dtype='float32') for l in range(num_layers)]
#-----------------------------------------------------------#
# 通過計算獲得真實框的中心和寬高
# 中心點(m,n,2) 寬高(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[::-1]
true_boxes[..., 2:4] = boxes_wh / input_shape[::-1]
#-----------------------------------------------------------#
# [9,2] -> [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,2] -> [n,1,2]
#-----------------------------------------------------------#
wh = np.expand_dims(wh, -2)
box_maxes = wh / 2.
box_mins = - box_maxes
#-----------------------------------------------------------#
# 計算所有真實框和先驗框的交并比
# intersect_area [n,9]
# box_area [n,1]
# anchor_area [1,9]
# iou [n,9]
#-----------------------------------------------------------#
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 self.anchors_mask[l]:
#-----------------------------------------------------------#
# floor用于向下取整,找到真實框所屬的特征層對應的x、y軸坐标
#-----------------------------------------------------------#
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')
#-----------------------------------------------------------#
# k指的的目前這個特征點的第k個先驗框
#-----------------------------------------------------------#
k = self.anchors_mask[l].index(n)
#-----------------------------------------------------------#
# c指的是目前這個真實框的種類
#-----------------------------------------------------------#
c = true_boxes[b, t, 4].astype('int32')
#-----------------------------------------------------------#
# y_true的shape為(m,13,13,3,85)(m,26,26,3,85)
# 最後的85可以拆分成4+1+80,4代表的是框的中心與寬高、
# 1代表的是置信度、80代表的是種類
#-----------------------------------------------------------#
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
d)、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)。還有解碼後的xy,wh。
3、對于每一幅圖,計算其中所有真實框與預測框的IOU,如果某些預測框和真實框的重合程度大于0.5,則忽略。
4、計算ciou作為回歸的loss,這裡隻計算正樣本的回歸loss。
5、計算置信度的loss,其有兩部分構成,第一部分是實際上存在目标的,預測結果中置信度的值與1對比;第二部分是實際上不存在目标的,預測結果中置信度的值與0對比。
6、計算預測種類的loss,其計算的是實際上存在目标的,預測類與真實類的差距。
其實際上計算的總的loss是三個loss的和,這三個loss分别是:
- 實際存在的框,CIOU LOSS。
- 實際存在的框,預測結果中置信度的值與1對比;實際不存在的框,預測結果中置信度的值與0對比,該部分要去除被忽略的不包含目标的框。
- 實際存在的框,種類預測結果與實際結果的對比。
其實際代碼如下,使用yolo_loss就可以獲得loss值:
import math
import tensorflow as tf
from keras import backend as K
from utils.utils_bbox import get_anchors_and_decode
def box_ciou(b1, b2):
"""
輸入為:
----------
b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
傳回為:
-------
ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
"""
#-----------------------------------------------------------#
# 求出預測框左上角右下角
# b1_mins (batch, feat_w, feat_h, anchor_num, 2)
# b1_maxes (batch, feat_w, feat_h, anchor_num, 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
#-----------------------------------------------------------#
# 求出真實框左上角右下角
# b2_mins (batch, feat_w, feat_h, anchor_num, 2)
# b2_maxes (batch, feat_w, feat_h, anchor_num, 2)
#-----------------------------------------------------------#
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
#-----------------------------------------------------------#
# 求真實框和預測框所有的iou
# iou (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
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]
union_area = b1_area + b2_area - intersect_area
iou = intersect_area / K.maximum(union_area, K.epsilon())
#-----------------------------------------------------------#
# 計算中心的差距
# center_distance (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
enclose_mins = K.minimum(b1_mins, b2_mins)
enclose_maxes = K.maximum(b1_maxes, b2_maxes)
enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
#-----------------------------------------------------------#
# 計算對角線距離
# enclose_diagonal (batch, feat_w, feat_h, anchor_num)
#-----------------------------------------------------------#
enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
ciou = iou - 1.0 * (center_distance) / K.maximum(enclose_diagonal ,K.epsilon())
v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0], K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi)
alpha = v / K.maximum((1.0 - iou + v), K.epsilon())
ciou = ciou - alpha * v
ciou = K.expand_dims(ciou, -1)
return ciou
#---------------------------------------------------#
# 平滑标簽
#---------------------------------------------------#
def _smooth_labels(y_true, label_smoothing):
num_classes = tf.cast(K.shape(y_true)[-1], dtype=K.floatx())
label_smoothing = K.constant(label_smoothing, dtype=K.floatx())
return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
#---------------------------------------------------#
# 用于計算每個預測框與真實框的iou
#---------------------------------------------------#
def box_iou(b1, b2):
#---------------------------------------------------#
# num_anchor,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, input_shape, anchors, anchors_mask, num_classes, ignore_thresh=.7, label_smoothing=0.1, print_loss=False):
num_layers = len(anchors_mask)
#---------------------------------------------------------------------------------------------------#
# 将預測結果和實際ground truth分開,args是[*model_body.output, *y_true]
# y_true是一個清單,包含三個特征層,shape分别為:
# (m,13,13,3,85)
# (m,26,26,3,85)
# yolo_outputs是一個清單,包含三個特征層,shape分别為:
# (m,13,13,3,85)
# (m,26,26,3,85)
#---------------------------------------------------------------------------------------------------#
y_true = args[num_layers:]
yolo_outputs = args[:num_layers]
#-----------------------------------------------------------#
# 得到input_shpae為416,416
#-----------------------------------------------------------#
input_shape = K.cast(input_shape, K.dtype(y_true[0]))
#-----------------------------------------------------------#
# 取出每一張圖檔
# m的值就是batch_size
#-----------------------------------------------------------#
m = K.shape(yolo_outputs[0])[0]
loss = 0
num_pos = 0
#---------------------------------------------------------------------------------------------------#
# y_true是一個清單,包含三個特征層,shape分别為(m,13,13,3,85),(m,26,26,3,85)。
# yolo_outputs是一個清單,包含三個特征層,shape分别為(m,13,13,3,85),(m,26,26,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:]
if label_smoothing:
true_class_probs = _smooth_labels(true_class_probs, label_smoothing)
#-----------------------------------------------------------#
# 将yolo_outputs的特征層輸出進行處理、獲得四個傳回值
# 其中:
# grid (13,13,1,2) 網格坐标
# raw_pred (m,13,13,3,85) 尚未處理的預測結果
# pred_xy (m,13,13,3,2) 解碼後的中心坐标
# pred_wh (m,13,13,3,2) 解碼後的寬高坐标
#-----------------------------------------------------------#
grid, raw_pred, pred_xy, pred_wh = get_anchors_and_decode(yolo_outputs[l],
anchors[anchors_mask[l]], num_classes, input_shape, calc_loss=True)
#-----------------------------------------------------------#
# pred_box是解碼後的預測的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):
#-----------------------------------------------------------#
# 取出n個真實框: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 預測框的坐标
# true_box n,4 真實框的坐标
# iou 13,13,3,n 預測框和真實框的iou
#-----------------------------------------------------------#
iou = box_iou(pred_box[b], true_box)
#-----------------------------------------------------------#
# best_iou 13,13,3 每個特征點與真實框的最大重合程度
#-----------------------------------------------------------#
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 = tf.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])
#-----------------------------------------------------------#
# ignore_mask用于提取出作為負樣本的特征點
# (m,13,13,3)
#-----------------------------------------------------------#
ignore_mask = ignore_mask.stack()
# (m,13,13,3,1)
ignore_mask = K.expand_dims(ignore_mask, -1)
#-----------------------------------------------------------#
# 真實框越大,比重越小,小框的比重更大。
#-----------------------------------------------------------#
box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]
#-----------------------------------------------------------#
# 計算Ciou loss
#-----------------------------------------------------------#
raw_true_box = y_true[l][...,0:4]
ciou = box_ciou(pred_box, raw_true_box)
ciou_loss = object_mask * box_loss_scale * (1 - ciou)
#------------------------------------------------------------------------------#
# 如果該位置本來有框,那麼計算1與置信度的交叉熵
# 如果該位置本來沒有框,那麼計算0與置信度的交叉熵
# 在這其中會忽略一部分樣本,這些被忽略的樣本滿足條件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)
location_loss = K.sum(ciou_loss)
confidence_loss = K.sum(confidence_loss)
class_loss = K.sum(class_loss)
#-----------------------------------------------------------#
# 計算正樣本數量
#-----------------------------------------------------------#
num_pos += tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
loss += location_loss + confidence_loss + class_loss
# if print_loss:
# loss = tf.Print(loss, [loss, location_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')
loss = loss / num_pos
return loss
訓練自己的YoloV4模型
首先前往Github下載下傳對應的倉庫,下載下傳完後利用解壓軟體解壓,之後用程式設計軟體打開檔案夾。
注意打開的根目錄必須正确,否則相對目錄不正确的情況下,代碼将無法運作。
一定要注意打開後的根目錄是檔案存放的目錄。
一、資料集的準備
本文使用VOC格式進行訓練,訓練前需要自己制作好資料集,如果沒有自己的資料集,可以通過Github連接配接下載下傳VOC12+07的資料集嘗試下。
訓練前将标簽檔案放在VOCdevkit檔案夾下的VOC2007檔案夾下的Annotation中。
訓練前将圖檔檔案放在VOCdevkit檔案夾下的VOC2007檔案夾下的JPEGImages中。
此時資料集的擺放已經結束。
二、資料集的處理
在完成資料集的擺放之後,我們需要對資料集進行下一步的處理,目的是獲得訓練用的2007_train.txt以及2007_val.txt,需要用到根目錄下的voc_annotation.py。
voc_annotation.py裡面有一些參數需要設定。
分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次訓練可以僅修改classes_path
'''
annotation_mode用于指定該檔案運作時計算的内容
annotation_mode為0代表整個标簽處理過程,包括獲得VOCdevkit/VOC2007/ImageSets裡面的txt以及訓練用的2007_train.txt、2007_val.txt
annotation_mode為1代表獲得VOCdevkit/VOC2007/ImageSets裡面的txt
annotation_mode為2代表獲得訓練用的2007_train.txt、2007_val.txt
'''
annotation_mode = 0
'''
必須要修改,用于生成2007_train.txt、2007_val.txt的目标資訊
與訓練和預測所用的classes_path一緻即可
如果生成的2007_train.txt裡面沒有目标資訊
那麼就是因為classes沒有設定正确
僅在annotation_mode為0和2的時候有效
'''
classes_path = 'model_data/voc_classes.txt'
'''
trainval_percent用于指定(訓練集+驗證集)與測試集的比例,預設情況下 (訓練集+驗證集):測試集 = 9:1
train_percent用于指定(訓練集+驗證集)中訓練集與驗證集的比例,預設情況下 訓練集:驗證集 = 9:1
僅在annotation_mode為0和1的時候有效
'''
trainval_percent = 0.9
train_percent = 0.9
'''
指向VOC資料集所在的檔案夾
預設指向根目錄下的VOC資料集
'''
VOCdevkit_path = 'VOCdevkit'
classes_path用于指向檢測類别所對應的txt,以voc資料集為例,我們用的txt為:
訓練自己的資料集時,可以自己建立一個cls_classes.txt,裡面寫自己所需要區分的類别。
三、開始網絡訓練
通過voc_annotation.py我們已經生成了2007_train.txt以及2007_val.txt,此時我們可以開始訓練了。
訓練的參數較多,大家可以在下載下傳庫後仔細看注釋,其中最重要的部分依然是train.py裡的classes_path。
classes_path用于指向檢測類别所對應的txt,這個txt和voc_annotation.py裡面的txt一樣!訓練自己的資料集必須要修改!
修改完classes_path後就可以運作train.py開始訓練了,在訓練多個epoch後,權值會生成在logs檔案夾中。
其它參數的作用如下:
#--------------------------------------------------------#
# 訓練前一定要修改classes_path,使其對應自己的資料集
#--------------------------------------------------------#
classes_path = 'model_data/voc_classes.txt'
#---------------------------------------------------------------------#
# anchors_path代表先驗框對應的txt檔案,一般不修改。
# anchors_mask用于幫助代碼找到對應的先驗框,一般不修改。
#---------------------------------------------------------------------#
anchors_path = 'model_data/yolo_anchors.txt'
anchors_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
#-------------------------------------------------------------------------------------#
# 權值檔案請看README,百度網盤下載下傳
# 訓練自己的資料集時提示次元不比對正常,預測的東西都不一樣了自然次元不比對
# 預訓練權重對于99%的情況都必須要用,不用的話權值太過随機,特征提取效果不明顯
# 網絡訓練的結果也不會好,資料的預訓練權重對不同資料集是通用的,因為特征是通用的
#------------------------------------------------------------------------------------#
model_path = 'model_data/yolo4_weight.h5'
#------------------------------------------------------#
# 輸入的shape大小,一定要是32的倍數
#------------------------------------------------------#
input_shape = [416, 416]
#------------------------------------------------------#
# Yolov4的tricks應用
# mosaic 馬賽克資料增強 True or False
# 實際測試時mosaic資料增強并不穩定,是以預設為False
# Cosine_scheduler 餘弦退火學習率 True or False
# label_smoothing 标簽平滑 0.01以下一般 如0.01、0.005
#------------------------------------------------------#
mosaic = False
Cosine_scheduler = False
label_smoothing = 0
#----------------------------------------------------#
# 訓練分為兩個階段,分别是當機階段和解凍階段
# 當機階段訓練參數
# 此時模型的主幹被當機了,特征提取網絡不發生改變
# 占用的顯存較小,僅對網絡進行微調
#----------------------------------------------------#
Init_Epoch = 0
Freeze_Epoch = 50
Freeze_batch_size = 4
Freeze_lr = 1e-3
#----------------------------------------------------#
# 解凍階段訓練參數
# 此時模型的主幹不被當機了,特征提取網絡會發生改變
# 占用的顯存較大,網絡所有的參數都會發生改變
# batch不能為1
#----------------------------------------------------#
UnFreeze_Epoch = 100
Unfreeze_batch_size = 4
Unfreeze_lr = 1e-4
#------------------------------------------------------#
# 是否進行當機訓練,預設先當機主幹訓練後解凍訓練。
#------------------------------------------------------#
Freeze_Train = True
#------------------------------------------------------#
# 用于設定是否使用多線程讀取資料,0代表關閉多線程
# 開啟後會加快資料讀取速度,但是會占用更多記憶體
# keras裡開啟多線程有些時候速度反而慢了許多
# 在IO為瓶頸的時候再開啟多線程,即GPU運算速度遠大于讀取圖檔的速度。
#------------------------------------------------------#
num_workers = 0
#----------------------------------------------------#
# 獲得圖檔路徑和标簽
#----------------------------------------------------#
train_annotation_path = '2007_train.txt'
val_annotation_path = '2007_val.txt'
四、訓練結果預測
訓練結果預測需要用到兩個檔案,分别是yolo.py和predict.py。
我們首先需要去yolo.py裡面修改model_path以及classes_path,這兩個參數必須要修改。
model_path指向訓練好的權值檔案,在logs檔案夾裡。
classes_path指向檢測類别所對應的txt。
完成修改後就可以運作predict.py進行檢測了。運作後輸入圖檔路徑即可檢測。