睿智的目標檢測52——Keras搭建YoloX目標檢測平臺
- 學習前言
- 原始碼下載
- YoloX改進的部分(不完全)
- YoloX實作思路
- 一、整體結構決議
- 二、網路結構決議
- 1、主干網路CSPDarknet介紹
- 2、構建FPN特征金字塔進行加強特征提取
- 3、利用Yolo Head獲得預測結果
- 三、預測結果的解碼
- 1、獲得預測框與得分
- 2、得分篩選與非極大抑制
- 四、訓練部分
- 1、計算loss所需內容
- 2、正樣本特征點的必要條件
- 3、SimOTA動態匹配正樣本
- 4、計算Loss
- 訓練自己的YoloX模型
- 一、資料集的準備
- 二、資料集的處理
- 三、開始網路訓練
- 四、訓練結果預測
學習前言
曠視新提出了YoloX,感覺蠻有意思,復現一下哈哈,

原始碼下載
https://github.com/bubbliiiing/yolox-keras
喜歡的可以點個star噢,
YoloX改進的部分(不完全)
1、主干部分:使用了Focus網路結構,這個結構是在YoloV5里面使用到比較有趣的網路結構,具體操作是在一張圖片中每隔一個像素拿到一個值,這個時候獲得了四個獨立的特征層,然后將四個獨立的特征層進行堆疊,此時寬高資訊就集中到了通道資訊,輸入通道擴充了四倍,
2、分類回歸層:Decoupled Head,以前版本的Yolo所用的解耦頭是一起的,也就是分類和回歸在一個1X1卷積里實作,YoloX認為這給網路的識別帶來了不利影響,在YoloX中,Yolo Head被分為了兩部分,分別實作,最后預測的時候才整合在一起,
3、資料增強:Mosaic資料增強、Mosaic利用了四張圖片進行拼接實作資料中增強,根據論文所說其擁有一個巨大的優點是豐富檢測物體的背景!且在BN計算的時候一下子會計算四張圖片的資料!
4、Anchor Free:不使用先驗框,
5、SimOTA :為不同大小的目標動態匹配正樣本,
以上并非全部的改進部分,還存在一些其它的改進,這里只列出來了一些我比較感興趣,而且非常有效的改進,
YoloX實作思路
一、整體結構決議

在學習YoloX之前,我們需要對YoloX所作的作業有一定的了解,這有助于我們后面去了解網路的細節,
和之前版本的Yolo類似,整個YoloX可以依然可以分為三個部分,分別是CSPDarknet,FPN以及Yolo Head,
CSPDarknet可以被稱作YoloX的主干特征提取網路,輸入的圖片首先會在CSPDarknet里面進行特征提取,提取到的特征可以被稱作特征層,是輸入圖片的特征集合,在主干部分,我們獲取了三個特征層進行下一步網路的構建,這三個特征層我稱它為有效特征層,
FPN可以被稱作YoloX的加強特征提取網路,在主干部分獲得的三個有效特征層會在這一部分進行特征融合,特征融合的目的是結合不同尺度的特征資訊,在FPN部分,已經獲得的有效特征層被用于繼續提取特征,在YoloX里面同樣使用了YoloV4中用到的Panet的結構,我們不僅會對特征進行上采樣實作特征融合,還會對特征再次進行下采樣實作特征融合,
Yolo Head是YoloX的分類器與回歸器,通過CSPDarknet和FPN,我們已經可以獲得三個加強過的有效特征層,每一個特征層都有寬、高和通道數,此時我們可以將特征圖看作一個又一個特征點的集合,每一個特征點都有通道數個特征,Yolo Head實際上所做的作業就是對特征點進行判斷,判斷特征點是否有物體與其對應,以前版本的Yolo所用的解耦頭是一起的,也就是分類和回歸在一個1X1卷積里實作,YoloX認為這給網路的識別帶來了不利影響,在YoloX中,Yolo Head被分為了兩部分,分別實作,最后預測的時候才整合在一起,
因此,整個YoloX網路所作的作業就是 特征提取-特征加強-預測特征點對應的物體情況,
二、網路結構決議
1、主干網路CSPDarknet介紹

YoloX所使用的主干特征提取網路為CSPDarknet,它具有五個重要特點:
1、使用了殘差網路Residual,CSPDarknet中的殘差卷積可以分為兩個部分,主干部分是一次1X1的卷積和一次3X3的卷積;殘差邊部分不做任何處理,直接將主干的輸入與輸出結合,整個YoloV3的主干部分都由殘差卷積構成:
def Bottleneck(x, out_channels, shortcut=True, name = ""):
y = compose(
DarknetConv2D_BN_SiLU(out_channels, (1,1), name = name + '.conv1'),
DarknetConv2D_BN_SiLU(out_channels, (3,3), name = name + '.conv2'))(x)
if shortcut:
y = Add()([x, y])
return y

殘差網路的特點是容易優化,并且能夠通過增加相當的深度來提高準確率,其內部的殘差塊使用了跳躍連接,緩解了在深度神經網路中增加深度帶來的梯度消失問題,
2、使用CSPnet網路結構,CSPnet結構并不算復雜,就是將原來的殘差塊的堆疊進行了一個拆分,拆成左右兩部分:主干部分繼續進行原來的殘差塊的堆疊;另一部分則像一個殘差邊一樣,經過少量處理直接連接到最后,因此可以認為CSP中存在一個大的殘差邊,
def CSPLayer(x, num_filters, num_blocks, shortcut=True, expansion=0.5, name=""):
hidden_channels = int(num_filters * expansion) # hidden channels
#----------------------------------------------------------------#
# 主干部分會對num_blocks進行回圈,回圈內部是殘差結構,
#----------------------------------------------------------------#
x_1 = DarknetConv2D_BN_SiLU(hidden_channels, (1,1), name = name + '.conv1')(x)
#--------------------------------------------------------------------#
# 然后建立一個大的殘差邊shortconv、這個大殘差邊繞過了很多的殘差結構
#--------------------------------------------------------------------#
x_2 = DarknetConv2D_BN_SiLU(hidden_channels, (1,1), name = name + '.conv2')(x)
for i in range(num_blocks):
x_1 = Bottleneck(x_1, hidden_channels, shortcut, name = name + '.m.' + str(i))
#----------------------------------------------------------------#
# 將大殘差邊再堆疊回來
#----------------------------------------------------------------#
route = Concatenate()([x_1, x_2])
#----------------------------------------------------------------#
# 最后對通道數進行整合
#----------------------------------------------------------------#
return DarknetConv2D_BN_SiLU(num_filters, (1,1), name = name + '.conv3')(route)
3、使用了Focus網路結構,這個網路結構是在YoloV5里面使用到比較有趣的網路結構,具體操作是在一張圖片中每隔一個像素拿到一個值,這個時候獲得了四個獨立的特征層,然后將四個獨立的特征層進行堆疊,此時寬高資訊就集中到了通道資訊,輸入通道擴充了四倍,拼接起來的特征層相對于原先的三通道變成了十二個通道,下圖很好的展示了Focus結構,一看就能明白,

class Focus(Layer):
def __init__(self):
super(Focus, self).__init__()
def compute_output_shape(self, input_shape):
return (input_shape[0], input_shape[1] // 2 if input_shape[1] != None else input_shape[1], input_shape[2] // 2 if input_shape[2] != None else input_shape[2], input_shape[3] * 4)
def call(self, x):
return tf.concat(
[x[..., ::2, ::2, :],
x[..., 1::2, ::2, :],
x[..., ::2, 1::2, :],
x[..., 1::2, 1::2, :]],
axis=-1
)
4、使用了SiLU激活函式,SiLU是Sigmoid和ReLU的改進版,SiLU具備無上界有下界、平滑、非單調的特性,SiLU在深層模型上的效果優于 ReLU,可以看做是平滑的ReLU激活函式,
f
(
x
)
=
x
?
sigmoid
(
x
)
f(x) = x · \text{sigmoid}(x)
f(x)=x?sigmoid(x)

class SiLU(Layer):
def __init__(self, **kwargs):
super(SiLU, self).__init__(**kwargs)
self.supports_masking = True
def call(self, inputs):
return inputs * K.sigmoid(inputs)
def get_config(self):
config = super(SiLU, self).get_config()
return config
def compute_output_shape(self, input_shape):
return input_shape
5、使用了SPP結構,通過不同池化核大小的最大池化進行特征提取,提高網路的感受野,在YoloV4中,SPP是用在FPN里面的,在YoloX中,SPP模塊被用在了主干特征提取網路中,
def SPPBottleneck(x, out_channels, name = ""):
#---------------------------------------------------#
# 使用了SPP結構,即不同尺度的最大池化后堆疊,
#---------------------------------------------------#
x = DarknetConv2D_BN_SiLU(out_channels // 2, (1,1), name = name + '.conv1')(x)
maxpool1 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(x)
maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(x)
maxpool3 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(x)
x = Concatenate()([x, maxpool1, maxpool2, maxpool3])
x = DarknetConv2D_BN_SiLU(out_channels, (1,1), name = name + '.conv2')(x)
return x
整個主干實作代碼為:
from functools import wraps
from re import X
import tensorflow as tf
from keras import backend as K
from keras.initializers import random_normal
from keras.layers import (Add, BatchNormalization, Concatenate, Conv2D, Layer,
MaxPooling2D, ZeroPadding2D)
from keras.layers.normalization import BatchNormalization
from keras.regularizers import l2
from utils.utils import compose
class SiLU(Layer):
def __init__(self, **kwargs):
super(SiLU, self).__init__(**kwargs)
self.supports_masking = True
def call(self, inputs):
return inputs * K.sigmoid(inputs)
def get_config(self):
config = super(SiLU, self).get_config()
return config
def compute_output_shape(self, input_shape):
return input_shape
class Focus(Layer):
def __init__(self):
super(Focus, self).__init__()
def compute_output_shape(self, input_shape):
return (input_shape[0], input_shape[1] // 2 if input_shape[1] != None else input_shape[1], input_shape[2] // 2 if input_shape[2] != None else input_shape[2], input_shape[3] * 4)
def call(self, x):
return tf.concat(
[x[..., ::2, ::2, :],
x[..., 1::2, ::2, :],
x[..., ::2, 1::2, :],
x[..., 1::2, 1::2, :]],
axis=-1
)
#------------------------------------------------------#
# 單次卷積DarknetConv2D
# 如果步長為2則自己設定padding方式,
#------------------------------------------------------#
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
darknet_conv_kwargs = {'kernel_initializer' : random_normal(stddev=0.02)}
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 + SiLU
#---------------------------------------------------#
def DarknetConv2D_BN_SiLU(*args, **kwargs):
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
if "name" in kwargs.keys():
no_bias_kwargs['name'] = kwargs['name'] + '.conv'
return compose(
DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(name = kwargs['name'] + '.bn'),
SiLU())
def SPPBottleneck(x, out_channels, name = ""):
#---------------------------------------------------#
# 使用了SPP結構,即不同尺度的最大池化后堆疊,
#---------------------------------------------------#
x = DarknetConv2D_BN_SiLU(out_channels // 2, (1,1), name = name + '.conv1')(x)
maxpool1 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(x)
maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(x)
maxpool3 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(x)
x = Concatenate()([x, maxpool1, maxpool2, maxpool3])
x = DarknetConv2D_BN_SiLU(out_channels, (1,1), name = name + '.conv2')(x)
return x
def Bottleneck(x, out_channels, shortcut=True, name = ""):
y = compose(
DarknetConv2D_BN_SiLU(out_channels, (1,1), name = name + '.conv1'),
DarknetConv2D_BN_SiLU(out_channels, (3,3), name = name + '.conv2'))(x)
if shortcut:
y = Add()([x, y])
return y
def CSPLayer(x, num_filters, num_blocks, shortcut=True, expansion=0.5, name=""):
hidden_channels = int(num_filters * expansion) # hidden channels
#----------------------------------------------------------------#
# 主干部分會對num_blocks進行回圈,回圈內部是殘差結構,
#----------------------------------------------------------------#
x_1 = DarknetConv2D_BN_SiLU(hidden_channels, (1,1), name = name + '.conv1')(x)
#--------------------------------------------------------------------#
# 然后建立一個大的殘差邊shortconv、這個大殘差邊繞過了很多的殘差結構
#--------------------------------------------------------------------#
x_2 = DarknetConv2D_BN_SiLU(hidden_channels, (1,1), name = name + '.conv2')(x)
for i in range(num_blocks):
x_1 = Bottleneck(x_1, hidden_channels, shortcut, name = name + '.m.' + str(i))
#----------------------------------------------------------------#
# 將大殘差邊再堆疊回來
#----------------------------------------------------------------#
route = Concatenate()([x_1, x_2])
#----------------------------------------------------------------#
# 最后對通道數進行整合
#----------------------------------------------------------------#
return DarknetConv2D_BN_SiLU(num_filters, (1,1), name = name + '.conv3')(route)
def resblock_body(x, num_filters, num_blocks, shortcut=True, expansion=0.5, last = False, name = ""):
#----------------------------------------------------------------#
# 利用ZeroPadding2D和一個步長為2x2的卷積塊進行高和寬的壓縮
#----------------------------------------------------------------#
x = ZeroPadding2D(((1,1),(1,1)))(x)
#----------------------------------------------------------------#
# 利用ZeroPadding2D和一個步長為2x2的卷積塊進行高和寬的壓縮
#----------------------------------------------------------------#
x = DarknetConv2D_BN_SiLU(num_filters, (3,3), strides=(2,2), name = name + '.0')(x)
if last:
x = SPPBottleneck(x, num_filters, name = name + '.1')
return CSPLayer(x, num_filters, num_blocks, shortcut=shortcut, expansion=expansion, name = name + '.1' if not last else name + '.2')
#---------------------------------------------------#
# CSPdarknet53 的主體部分
# 輸入為一張416x416x3的圖片
# 輸出為三個有效特征層
#---------------------------------------------------#
def darknet_body(x, dep_mul, wid_mul):
base_channels = int(wid_mul * 64) # 64
base_depth = max(round(dep_mul * 3), 1) # 3
x = Focus()(x)
x = DarknetConv2D_BN_SiLU(base_channels, (3,3), name = 'backbone.backbone.stem.conv')(x)
x = resblock_body(x, base_channels * 2, base_depth, name = 'backbone.backbone.dark2')
x = resblock_body(x, base_channels * 4, base_depth * 3, name = 'backbone.backbone.dark3')
feat1 = x
x = resblock_body(x, base_channels * 8, base_depth * 3, name = 'backbone.backbone.dark4')
feat2 = x
x = resblock_body(x, base_channels * 16, base_depth, last = True, name = 'backbone.backbone.dark5')
feat3 = x
return feat1,feat2,feat3
2、構建FPN特征金字塔進行加強特征提取

在特征利用部分,YoloX提取多特征層進行目標檢測,一共提取三個特征層,
三個特征層位于主干部分CSPdarknet的不同位置,分別位于中間層,中下層,底層,當輸入為(640,640,3)的時候,三個特征層的shape分別為feat1=(80,80,256)、feat2=(40,40,512)、feat3=(20,20,1024),
在獲得三個有效特征層后,我們利用這三個有效特征層進行FPN層的構建,構建方式為:
- feat3=(20,20,1024)的特征層進行1次1X1卷積調整通道后獲得P5,P5進行上采樣UmSampling2d后與feat2=(40,40,512)特征層進行結合,然后使用CSPLayer進行特征提取獲得P5_upsample,此時獲得的特征層為(40,40,512),
- P5_upsample=(40,40,512)的特征層進行1次1X1卷積調整通道后獲得P4,P4進行上采樣UmSampling2d后與feat1=(80,80,256)特征層進行結合,然后使用CSPLayer進行特征提取P3_out,此時獲得的特征層為(80,80,256),
- P3_out=(80,80,256)的特征層進行一次3x3卷積進行下采樣,下采樣后與P4堆疊,然后使用CSPLayer進行特征提取P4_out,此時獲得的特征層為(40,40,512),
- P4_out=(40,40,512)的特征層進行一次3x3卷積進行下采樣,下采樣后與P5堆疊,然后使用CSPLayer進行特征提取P5_out,此時獲得的特征層為(20,20,1024),
特征金字塔可以將不同shape的特征層進行特征融合,有利于提取出更好的特征,
from keras.layers import (Concatenate, Input, Lambda, UpSampling2D,
ZeroPadding2D)
from keras.layers.convolutional import UpSampling2D
from keras.models import Model
from nets.CSPdarknet53 import (CSPLayer, DarknetConv2D, DarknetConv2D_BN_SiLU,
darknet_body)
from nets.yolo_training import get_yolo_loss
#---------------------------------------------------#
# Panet網路的構建,并且獲得預測結果
#---------------------------------------------------#
def yolo_body(input_shape, num_classes, phi):
depth_dict = {'s' : 0.33, 'm' : 0.67, 'l' : 1.00, 'x' : 1.33,}
width_dict = {'s' : 0.50, 'm' : 0.75, 'l' : 1.00, 'x' : 1.25,}
depth, width = depth_dict[phi], width_dict[phi]
in_channels = [256, 512, 1024]
inputs = Input(input_shape)
feat1, feat2, feat3 = darknet_body(inputs, depth, width)
P5 = DarknetConv2D_BN_SiLU(int(in_channels[1] * width), (1, 1), name = 'backbone.lateral_conv0')(feat3)
P5_upsample = UpSampling2D()(P5) # 512/16
P5_upsample = Concatenate(axis = -1)([P5_upsample, feat2]) # 512->1024/16
P5_upsample = CSPLayer(P5_upsample, int(in_channels[1] * width), round(3 * depth), shortcut = False, name = 'backbone.C3_p4') # 1024->512/16
P4 = DarknetConv2D_BN_SiLU(int(in_channels[0] * width), (1, 1), name = 'backbone.reduce_conv1')(P5_upsample) # 512->256/16
P4_upsample = UpSampling2D()(P4) # 256/8
P4_upsample = Concatenate(axis = -1)([P4_upsample, feat1]) # 256->512/8
P3_out = CSPLayer(P4_upsample, int(in_channels[0] * width), round(3 * depth), shortcut = False, name = 'backbone.C3_p3') # 1024->512/16
P3_downsample = ZeroPadding2D(((1,1),(1,1)))(P3_out)
P3_downsample = DarknetConv2D_BN_SiLU(int(in_channels[0] * width), (3, 3), strides = (2, 2), name = 'backbone.bu_conv2')(P3_downsample) # 256->256/16
P3_downsample = Concatenate(axis = -1)([P3_downsample, P4]) # 256->512/16
P4_out = CSPLayer(P3_downsample, int(in_channels[1] * width), round(3 * depth), shortcut = False, name = 'backbone.C3_n3') # 1024->512/16
P4_downsample = ZeroPadding2D(((1,1),(1,1)))(P4_out)
P4_downsample = DarknetConv2D_BN_SiLU(int(in_channels[1] * width), (3, 3), strides = (2, 2), name = 'backbone.bu_conv1')(P4_downsample) # 256->256/16
P4_downsample = Concatenate(axis = -1)([P4_downsample, P5]) # 512->1024/32
P5_out = CSPLayer(P4_downsample, int(in_channels[2] * width), round(3 * depth), shortcut = False, name = 'backbone.C3_n4') # 1024->512/16
3、利用Yolo Head獲得預測結果

利用FPN特征金字塔,我們可以獲得三個加強特征,這三個加強特征的shape分別為(20,20,1024)、(40,40,512)、(80,80,256),然后我們利用這三個shape的特征層傳入Yolo Head獲得預測結果,
YoloX中的YoloHead與之前版本的YoloHead不同,以前版本的Yolo所用的解耦頭是一起的,也就是分類和回歸在一個1X1卷積里實作,YoloX認為這給網路的識別帶來了不利影響,在YoloX中,Yolo Head被分為了兩部分,分別實作,最后預測的時候才整合在一起,
對于每一個特征層,我們可以獲得三個預測結果,分別是:
1、Reg(h,w,4)用于判斷每一個特征點的回歸引數,回歸引數調整后可以獲得預測框,
2、Obj(h,w,1)用于判斷每一個特征點是否包含物體,
3、Cls(h,w,num_classes)用于判斷每一個特征點所包含的物體種類,
將三個預測結果進行堆疊,每個特征層獲得的結果為:
Out(h,w,4+1+num_classses)前四個引數用于判斷每一個特征點的回歸引數,回歸引數調整后可以獲得預測框;第五個引數用于判斷每一個特征點是否包含物體;最后num_classes個引數用于判斷每一個特征點所包含的物體種類,
實作代碼如下:
fpn_outs = [P3_out, P4_out, P5_out]
yolo_outs = []
for i, out in enumerate(fpn_outs):
stem = DarknetConv2D_BN_SiLU(int(256 * width), (1, 1), strides = (1, 1), name = 'head.stems.' + str(i))(out)
cls_conv = DarknetConv2D_BN_SiLU(int(256 * width), (3, 3), strides = (1, 1), name = 'head.cls_convs.' + str(i) + '.0')(stem)
cls_conv = DarknetConv2D_BN_SiLU(int(256 * width), (3, 3), strides = (1, 1), name = 'head.cls_convs.' + str(i) + '.1')(cls_conv)
cls_pred = DarknetConv2D(num_classes, (1, 1), strides = (1, 1), name = 'head.cls_preds.' + str(i))(cls_conv)
reg_conv = DarknetConv2D_BN_SiLU(int(256 * width), (3, 3), strides = (1, 1), name = 'head.reg_convs.' + str(i) + '.0')(stem)
reg_conv = DarknetConv2D_BN_SiLU(int(256 * width), (3, 3), strides = (1, 1), name = 'head.reg_convs.' + str(i) + '.1')(reg_conv)
reg_pred = DarknetConv2D(4, (1, 1), strides = (1, 1), name = 'head.reg_preds.' + str(i))(reg_conv)
obj_pred = DarknetConv2D(1, (1, 1), strides = (1, 1), name = 'head.obj_preds.' + str(i))(reg_conv)
output = Concatenate(axis = -1)([reg_pred, obj_pred, cls_pred])
yolo_outs.append(output)
return Model(inputs, yolo_outs)
三、預測結果的解碼
1、獲得預測框與得分
在對預測結果進行解碼之前,我們再來看看預測結果代表了什么,預測結果可以分為3個部分:
通過上一步,我們獲得了每個特征層的三個預測結果,
本文以(20,20,1024)對應的三個預測結果為例:
1、Reg預測結果,此時卷積的通道數為4,最終結果為(20,20,4),其中的4可以分為兩個2,第一個2是預測框的中心點相較于該特征點的偏移情況,第二個2是預測框的寬高相較于對數指數的引數
2、Obj預測結果,此時卷積的通道數為1,最終結果為(20,20,1),代表每一個特征點預測框內部包含物體的概率,
3、Cls預測結果,此時卷積的通道數為num_classes,最終結果為(20,20,num_classes),代表每一個特征點對應某類物體的概率,最后一維度num_classes中的預測值代表屬于每一個類的概率;
該特征層相當于將影像劃分成20x20個特征點,如果某個特征點落在物體的對應框內,就用于預測該物體,
如圖所示,藍色的點為20x20的特征點,此時我們對左圖紅色的三個點進行解碼操作演示:
1、進行中心預測點的計算,利用Regression預測結果前兩個序號的內容對特征點坐標進行偏移,左圖紅色的三個特征點偏移后是右圖綠色的三個點;
2、進行預測框寬高的計算,利用Regression預測結果后兩個序號的內容求指數后獲得預測框的寬高;
3、此時獲得的預測框就可以繪制在圖片上了,

除去這樣的解碼操作,還有非極大抑制的操作需要進行,防止同一種類的框的堆積,
#---------------------------------------------------#
# 圖片預測
#---------------------------------------------------#
def DecodeBox(outputs,
num_classes,
image_shape,
input_shape,
max_boxes = 100,
confidence = 0.5,
nms_iou = 0.3,
letterbox_image = True):
bs = K.shape(outputs[0])[0]
grids = []
strides = []
hw = [K.shape(x)[1:3] for x in outputs]
outputs = tf.concat([tf.reshape(x, [bs, -1, 5 + num_classes]) for x in outputs], axis = 1)
for i in range(len(hw)):
#---------------------------#
# 根據特征層生成網格點
#---------------------------#
grid_x, grid_y = tf.meshgrid(K.arange(hw[i][1]), K.arange(hw[i][0]))
grid = tf.reshape(tf.stack((grid_x, grid_y), 2), (1, -1, 2))
shape = tf.shape(grid)[:2]
grids.append(tf.cast(grid, K.dtype(outputs)))
strides.append(tf.ones((shape[0], shape[1], 1)) * input_shape[0] / tf.cast(hw[i][0], K.dtype(outputs)))
#---------------------------#
# 將網格點堆疊到一起
#---------------------------#
grids = tf.concat(grids, axis=1)
strides = tf.concat(strides, axis=1)
#------------------------#
# 根據網格點進行解碼
#------------------------#
box_xy = (outputs[..., :2] + grids) * strides / K.cast(input_shape[::-1], K.dtype(outputs))
box_wh = tf.exp(outputs[..., 2:4]) * strides / K.cast(input_shape[::-1], K.dtype(outputs))
box_confidence = K.sigmoid(outputs[..., 4:5])
box_class_probs = K.sigmoid(outputs[..., 5: ])
#------------------------------------------------------------------------------------------------------------#
# 在影像傳入網路預測前會進行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)
2、得分篩選與非極大抑制
得到最終的預測結果后還要進行得分排序與非極大抑制篩選,
得分篩選就是篩選出得分滿足confidence置信度的預測框,
非極大抑制就是篩選出一定區域內屬于同一種類得分最大的框,
得分篩選與非極大抑制的程序可以概括如下:
1、找出該圖片中得分大于門限函式的框,在進行重合框篩選前就進行得分的篩選可以大幅度減少框的數量,
2、對種類進行回圈,非極大抑制的作用是篩選出一定區域內屬于同一種類得分最大的框,對種類進行回圈可以幫助我們對每一個類分別進行非極大抑制,
3、根據得分對該種類進行從大到小排序,
4、每次取出得分最大的框,計算其與其它所有預測框的重合程度,重合程度過大的則剔除,
得分篩選與非極大抑制后的結果就可以用于繪制預測框了,
下圖是經過非極大抑制的,

下圖是未經過非極大抑制的,

實作代碼為:
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)
四、訓練部分
1、計算loss所需內容
計算loss實際上是網路的預測結果和網路的真實結果的對比,
和網路的預測結果一樣,網路的損失也由三個部分組成,分別是Reg部分、Obj部分、Cls部分,Reg部分是特征點的回歸引數判斷、Obj部分是特征點是否包含物體判斷、Cls部分是特征點包含的物體的種類,
2、正樣本特征點的必要條件
在YoloX中,物體的真實框落在哪些特征點內就由該特征點來預測,
對于每一個真實框,我們會求取所有特征點與它的空間位置情況,作為正樣本的特征點需要滿足以下幾個特點:
1、特征點落在物體的真實框內,
2、特征點距離物體中心盡量要在一定半徑內,
特點1、2保證了屬于正樣本的特征點會落在物體真實框內部,特征點中心與物體真實框中心要相近,
上面兩個條件僅用作正樣本的而初步篩選,在YoloX中,我們使用了SimOTA方法進行動態的正樣本數量分配,
def get_in_boxes_info(gt_bboxes_per_image, x_shifts, y_shifts, expanded_strides, num_gt, total_num_anchors, center_radius = 2.5):
#-------------------------------------------------------#
# expanded_strides_per_image [n_anchors_all]
# x_centers_per_image [num_gt, n_anchors_all]
# x_centers_per_image [num_gt, n_anchors_all]
#-------------------------------------------------------#
expanded_strides_per_image = expanded_strides[0]
x_centers_per_image = tf.tile(tf.expand_dims(((x_shifts[0] + 0.5) * expanded_strides_per_image), 0), [num_gt, 1])
y_centers_per_image = tf.tile(tf.expand_dims(((y_shifts[0] + 0.5) * expanded_strides_per_image), 0), [num_gt, 1])
#-------------------------------------------------------#
# gt_bboxes_per_image_x [num_gt, n_anchors_all]
#-------------------------------------------------------#
gt_bboxes_per_image_l = tf.tile(tf.expand_dims((gt_bboxes_per_image[:, 0] - 0.5 * gt_bboxes_per_image[:, 2]), 1), [1, total_num_anchors])
gt_bboxes_per_image_r = tf.tile(tf.expand_dims((gt_bboxes_per_image[:, 0] + 0.5 * gt_bboxes_per_image[:, 2]), 1), [1, total_num_anchors])
gt_bboxes_per_image_t = tf.tile(tf.expand_dims((gt_bboxes_per_image[:, 1] - 0.5 * gt_bboxes_per_image[:, 3]), 1), [1, total_num_anchors])
gt_bboxes_per_image_b = tf.tile(tf.expand_dims((gt_bboxes_per_image[:, 1] + 0.5 * gt_bboxes_per_image[:, 3]), 1), [1, total_num_anchors])
#-------------------------------------------------------#
# bbox_deltas [num_gt, n_anchors_all, 4]
#-------------------------------------------------------#
b_l = x_centers_per_image - gt_bboxes_per_image_l
b_r = gt_bboxes_per_image_r - x_centers_per_image
b_t = y_centers_per_image - gt_bboxes_per_image_t
b_b = gt_bboxes_per_image_b - y_centers_per_image
bbox_deltas = tf.stack([b_l, b_t, b_r, b_b], 2)
#-------------------------------------------------------#
# is_in_boxes [num_gt, n_anchors_all]
# is_in_boxes_all [n_anchors_all]
#-------------------------------------------------------#
is_in_boxes = tf.reduce_min(bbox_deltas, axis = -1) > 0.0
is_in_boxes_all = tf.reduce_sum(tf.cast(is_in_boxes, K.dtype(gt_bboxes_per_image)), axis = 0) > 0.0
gt_bboxes_per_image_l = tf.tile(tf.expand_dims(gt_bboxes_per_image[:, 0], 1), [1, total_num_anchors]) - center_radius * tf.expand_dims(expanded_strides_per_image, 0)
gt_bboxes_per_image_r = tf.tile(tf.expand_dims(gt_bboxes_per_image[:, 0], 1), [1, total_num_anchors]) + center_radius * tf.expand_dims(expanded_strides_per_image, 0)
gt_bboxes_per_image_t = tf.tile(tf.expand_dims(gt_bboxes_per_image[:, 1], 1), [1, total_num_anchors]) - center_radius * tf.expand_dims(expanded_strides_per_image, 0)
gt_bboxes_per_image_b = tf.tile(tf.expand_dims(gt_bboxes_per_image[:, 1], 1), [1, total_num_anchors]) + center_radius * tf.expand_dims(expanded_strides_per_image, 0)
#-------------------------------------------------------#
# center_deltas [num_gt, n_anchors_all, 4]
#-------------------------------------------------------#
c_l = x_centers_per_image - gt_bboxes_per_image_l
c_r = gt_bboxes_per_image_r - x_centers_per_image
c_t = y_centers_per_image - gt_bboxes_per_image_t
c_b = gt_bboxes_per_image_b - y_centers_per_image
center_deltas = tf.stack([c_l, c_t, c_r, c_b], 2)
#-------------------------------------------------------#
# is_in_centers [num_gt, n_anchors_all]
# is_in_centers_all [n_anchors_all]
#-------------------------------------------------------#
is_in_centers = tf.reduce_min(center_deltas, axis = -1) > 0.0
is_in_centers_all = tf.reduce_sum(tf.cast(is_in_centers, K.dtype(gt_bboxes_per_image)), axis = 0) > 0.0
#-------------------------------------------------------#
# fg_mask [n_anchors_all]
# is_in_boxes_and_center [num_gt, fg_mask]
#-------------------------------------------------------#
fg_mask = tf.cast(is_in_boxes_all | is_in_centers_all, tf.bool)
is_in_boxes_and_center = tf.boolean_mask(is_in_boxes, fg_mask, axis = 1) & tf.boolean_mask(is_in_centers, fg_mask, axis = 1)
return fg_mask, is_in_boxes_and_center
3、SimOTA動態匹配正樣本
在YoloX中,我們會計算一個Cost代價矩陣,代表每個真實框和每個特征點之間的代價關系,Cost代價矩陣由三個部分組成:
1、每個真實框和當前特征點預測框的重合程度;
2、每個真實框和當前特征點預測框的種類預測準確度;
3、每個真實框的中心是否落在了特征點的一定半徑內,
每個真實框和當前特征點預測框的重合程度越高,代表這個特征點已經嘗試去擬合該真實框了,因此它的Cost代價就會越小,
每個真實框和當前特征點預測框的種類預測準確度越高,也代表這個特征點已經嘗試去擬合該真實框了,因此它的Cost代價就會越小,
每個真實框的中心如果落在了特征點的一定半徑內,代表這個特征點應該去擬合該真實框,因此它的Cost代價就會越小,
Cost代價矩陣的目的是自適應的找到當前特征點應該去擬合的真實框,重合度越高越需要擬合,分類越準越需要擬合,在一定半徑內越需要擬合,
在SimOTA中,不同目標設定不同的正樣本數量(dynamick),以曠視科技?官方回答中的螞蟻和西瓜為例子,傳統的正樣本分配方案常常為同一場景下的西瓜和螞蟻分配同樣的正樣本數,那要么螞蟻有很多低質量的正樣本,要么西瓜僅僅只有一兩個正樣本,對于哪個分配方式都是不合適的,
動態的正樣本設定的關鍵在于如何確定k,SimOTA具體的做法是首先計算每個目標Cost最低的10特征點,然后把這十個特征點對應的預測框與真實框的IOU加起來求得最終的k,
因此,SimOTA的程序總結如下:
1、計算每個真實框和當前特征點預測框的重合程度,
2、計算每個真實框和當前特征點預測框的種類預測準確度,
3、判斷真實框的中心是否落在了特征點的一定半徑內,
4、計算Cost代價矩陣,
5、計算每個真實框Cost最低的10特征點,然后把這十個特征點對應的預測框與真實框的IOU加起來求得每個真實框最終的k,
def get_assignments(gt_bboxes_per_image, gt_classes, bboxes_preds_per_image, obj_preds_per_image, cls_preds_per_image, x_shifts, y_shifts, expanded_strides, num_classes, num_gt, total_num_anchors):
#-------------------------------------------------------#
# fg_mask [n_anchors_all]
# is_in_boxes_and_center [num_gt, len(fg_mask)]
#-------------------------------------------------------#
fg_mask, is_in_boxes_and_center = get_in_boxes_info(gt_bboxes_per_image, x_shifts, y_shifts, expanded_strides, num_gt, total_num_anchors)
#-------------------------------------------------------#
# fg_mask [n_anchors_all]
# bboxes_preds_per_image [fg_mask, 4]
# cls_preds_ [fg_mask, num_classes]
# obj_preds_ [fg_mask, 1]
#-------------------------------------------------------#
bboxes_preds_per_image = tf.boolean_mask(bboxes_preds_per_image, fg_mask, axis = 0)
obj_preds_ = tf.boolean_mask(obj_preds_per_image, fg_mask, axis = 0)
cls_preds_ = tf.boolean_mask(cls_preds_per_image, fg_mask, axis = 0)
num_in_boxes_anchor = tf.shape(bboxes_preds_per_image)[0]
#-------------------------------------------------------#
# pair_wise_ious [num_gt, fg_mask]
#-------------------------------------------------------#
pair_wise_ious = bboxes_iou(gt_bboxes_per_image, bboxes_preds_per_image)
pair_wise_ious_loss = -tf.log(pair_wise_ious + 1e-8)
#-------------------------------------------------------#
# cls_preds_ [num_gt, fg_mask, num_classes]
# gt_cls_per_image [num_gt, fg_mask, num_classes]
#-------------------------------------------------------#
gt_cls_per_image = tf.tile(tf.expand_dims(tf.one_hot(tf.cast(gt_classes, tf.int32), num_classes), 1), (1, num_in_boxes_anchor, 1))
cls_preds_ = K.sigmoid(tf.tile(tf.expand_dims(cls_preds_, 0), (num_gt, 1, 1))) *\
K.sigmoid(tf.tile(tf.expand_dims(obj_preds_, 0), (num_gt, 1, 1)))
pair_wise_cls_loss = tf.reduce_sum(K.binary_crossentropy(gt_cls_per_image, tf.sqrt(cls_preds_)), -1)
cost = pair_wise_cls_loss + 3.0 * pair_wise_ious_loss + 100000.0 * tf.cast((~is_in_boxes_and_center), K.dtype(bboxes_preds_per_image))
gt_matched_classes, fg_mask, pred_ious_this_matching, matched_gt_inds, num_fg = dynamic_k_matching(cost, pair_wise_ious, fg_mask, gt_classes, num_gt)
return gt_matched_classes, fg_mask, pred_ious_this_matching, matched_gt_inds, num_fg
def bboxes_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
def dynamic_k_matching(cost, pair_wise_ious, fg_mask, gt_classes, num_gt):
#-------------------------------------------------------#
# cost [num_gt, fg_mask]
# pair_wise_ious [num_gt, fg_mask]
# gt_classes [num_gt]
# fg_mask [n_anchors_all]
# matching_matrix [num_gt, fg_mask]
#-------------------------------------------------------#
matching_matrix = tf.zeros_like(cost)
#------------------------------------------------------------#
# 選取iou最大的n_candidate_k個點
# 然后求和,判斷應該有多少點用于該框預測
# topk_ious [num_gt, n_candidate_k]
# dynamic_ks [num_gt]
# matching_matrix [num_gt, fg_mask]
#------------------------------------------------------------#
n_candidate_k = tf.minimum(10, tf.shape(pair_wise_ious)[1])
topk_ious, _ = tf.nn.top_k(pair_wise_ious, n_candidate_k)
dynamic_ks = tf.maximum(tf.reduce_sum(topk_ious, 1), 1)
# dynamic_ks = tf.Print(dynamic_ks, [topk_ious, dynamic_ks], summarize = 100)
def loop_body_1(b, matching_matrix):
#------------------------------------------------------------#
# 給每個真實框選取最小的動態k個點
#------------------------------------------------------------#
_, pos_idx = tf.nn.top_k(-cost[b], k=tf.cast(dynamic_ks[b], tf.int32))
matching_matrix = tf.concat(
[matching_matrix[:b], tf.expand_dims(tf.reduce_max(tf.one_hot(pos_idx, tf.shape(cost)[1]), 0), 0), matching_matrix[b+1:]], axis = 0
)
# matching_matrix = matching_matrix.write(b, K.cast(tf.reduce_max(tf.one_hot(pos_idx, tf.shape(cost)[1]), 0), K.dtype(cost)))
return b + 1, matching_matrix
#-----------------------------------------------------------#
# 在這個地方進行一個回圈、回圈是對每一張圖片進行的
#-----------------------------------------------------------#
_, matching_matrix = K.control_flow_ops.while_loop(lambda b,*args: b < tf.cast(num_gt, tf.int32), loop_body_1, [0, matching_matrix])
#------------------------------------------------------------#
# anchor_matching_gt [fg_mask]
#------------------------------------------------------------#
anchor_matching_gt = tf.reduce_sum(matching_matrix, 0)
#------------------------------------------------------------#
# 當某一個特征點指向多個真實框的時候
# 選取cost最小的真實框,
#------------------------------------------------------------#
biger_one_indice = tf.reshape(tf.where(anchor_matching_gt > 1), [-1])
def loop_body_2(b, matching_matrix):
indice_anchor = tf.cast(biger_one_indice[b], tf.int32)
indice_gt = tf.math.argmin(cost[:, indice_anchor])
matching_matrix = tf.concat(
[
matching_matrix[:, :indice_anchor],
tf.expand_dims(tf.one_hot(indice_gt, tf.cast(num_gt, tf.int32)), 1),
matching_matrix[:, indice_anchor+1:]
], axis = -1
)
return b + 1, matching_matrix
#-----------------------------------------------------------#
# 在這個地方進行一個回圈、回圈是對每一張圖片進行的
#-----------------------------------------------------------#
_, matching_matrix = K.control_flow_ops.while_loop(lambda b,*args: b < tf.cast(tf.shape(biger_one_indice)[0], tf.int32), loop_body_2, [0, matching_matrix])
#------------------------------------------------------------#
# fg_mask_inboxes [fg_mask]
# num_fg為正樣本的特征點個數
#------------------------------------------------------------#
fg_mask_inboxes = tf.reduce_sum(matching_matrix, 0) > 0.0
num_fg = tf.reduce_sum(tf.cast(fg_mask_inboxes, K.dtype(cost)))
fg_mask_indices = tf.reshape(tf.where(fg_mask), [-1])
fg_mask_inboxes_indices = tf.reshape(tf.where(fg_mask_inboxes), [-1, 1])
fg_mask_select_indices = tf.gather_nd(fg_mask_indices, fg_mask_inboxes_indices)
fg_mask = tf.cast(tf.reduce_max(tf.one_hot(fg_mask_select_indices, tf.shape(fg_mask)[0]), 0), K.dtype(fg_mask))
#------------------------------------------------------------#
# 獲得特征點對應的物品種類
#------------------------------------------------------------#
matched_gt_inds = tf.math.argmax(tf.boolean_mask(matching_matrix, fg_mask_inboxes, axis = 1), 0)
gt_matched_classes = tf.gather_nd(gt_classes, tf.reshape(matched_gt_inds, [-1, 1]))
pred_ious_this_matching = tf.boolean_mask(tf.reduce_sum(matching_matrix * pair_wise_ious, 0), fg_mask_inboxes)
return gt_matched_classes, fg_mask, pred_ious_this_matching, matched_gt_inds, num_fg
4、計算Loss
由第一部分可知,YoloX的損失由三個部分組成:
1、Reg部分,由第三部分可知道每個真實框對應的特征點,獲取到每個框對應的特征點后,取出該特征點的預測框,利用真實框和預測框計算IOU損失,作為Reg部分的Loss組成,
2、Obj部分,由第三部分可知道每個真實框對應的特征點,所有真實框對應的特征點都是正樣本,剩余的特征點均為負樣本,根據正負樣本和特征點的是否包含物體的預測結果計算交叉熵損失,作為Obj部分的Loss組成,
3、Cls部分,由第三部分可知道每個真實框對應的特征點,獲取到每個框對應的特征點后,取出該特征點的種類預測結果,根據真實框的種類和特征點的種類預測結果計算交叉熵損失,作為Cls部分的Loss組成,
def get_yolo_loss(input_shape, num_layers, num_classes):
def yolo_loss(args):
labels, y_pred = args[-1], args[:-1]
x_shifts = []
y_shifts = []
expanded_strides = []
outputs = []
#-----------------------------------------------#
# inputs [[batch_size, 20, 20, num_classes + 5]
# [batch_size, 40, 40, num_classes + 5]
# [batch_size, 80, 80, num_classes + 5]]
# outputs [[batch_size, 400, num_classes + 5]
# [batch_size, 1600, num_classes + 5]
# [batch_size, 6400, num_classes + 5]]
#-----------------------------------------------#
for i in range(num_layers):
output = y_pred[i]
grid_shape = tf.shape(output)[1:3]
stride = input_shape[0] / tf.cast(grid_shape[0], K.dtype(output))
grid_x, grid_y = tf.meshgrid(K.arange(grid_shape[1]), K.arange(grid_shape[0]))
grid = tf.cast(tf.reshape(tf.stack((grid_x, grid_y), 2), (1, -1, 2)), K.dtype(output))
output = tf.reshape(output, [tf.shape(y_pred[i])[0], grid_shape[0] * grid_shape[1], -1])
output_xy = (output[..., :2] + grid) * stride
output_wh = tf.exp(output[..., 2:4]) * stride
output = tf.concat([output_xy, output_wh, output[..., 4:]], -1)
x_shifts.append(grid[..., 0])
y_shifts.append(grid[..., 1])
expanded_strides.append(tf.ones_like(grid[..., 0]) * stride)
outputs.append(output)
#-----------------------------------------------#
# x_shifts [1, n_anchors_all]
# y_shifts [1, n_anchors_all]
# expanded_strides [1, n_anchors_all]
#-----------------------------------------------#
x_shifts = tf.concat(x_shifts, 1)
y_shifts = tf.concat(y_shifts, 1)
expanded_strides = tf.concat(expanded_strides, 1)
outputs = tf.concat(outputs, 1)
return get_losses(x_shifts, y_shifts, expanded_strides, outputs, labels, num_classes)
return yolo_loss
def get_losses(x_shifts, y_shifts, expanded_strides, outputs, labels, num_classes):
#-----------------------------------------------#
# [batch, n_anchors_all, 4]
# [batch, n_anchors_all, 1]
# [batch, n_anchors_all, n_cls]
#-----------------------------------------------#
bbox_preds = outputs[:, :, :4]
obj_preds = outputs[:, :, 4:5]
cls_preds = outputs[:, :, 5:]
#------------------------------------------------------------#
# labels [batch, max_boxes, 5]
# tf.reduce_sum(labels, -1) [batch, max_boxes]
# nlabel [batch]
#------------------------------------------------------------#
nlabel = tf.reduce_sum(tf.cast(tf.reduce_sum(labels, -1) > 0, K.dtype(outputs)), -1)
total_num_anchors = tf.shape(outputs)[1]
num_fg = 0.0
loss_obj = 0.0
loss_cls = 0.0
loss_iou = 0.0
def loop_body(b, num_fg, loss_iou, loss_obj, loss_cls):
num_gt = tf.cast(nlabel[b], tf.int32)
#-----------------------------------------------#
# gt_bboxes_per_image [num_gt, num_classes]
# gt_classes [num_gt]
# bboxes_preds_per_image [n_anchors_all, 4]
# obj_preds_per_image [n_anchors_all, 1]
# cls_preds_per_image [n_anchors_all, num_classes]
#-----------------------------------------------#
gt_bboxes_per_image = labels[b][:num_gt, :4]
gt_classes = labels[b][:num_gt, 4]
bboxes_preds_per_image = bbox_preds[b]
obj_preds_per_image = obj_preds[b]
cls_preds_per_image = cls_preds[b]
def f1():
num_fg_img = tf.cast(tf.constant(0), K.dtype(outputs))
cls_target = tf.cast(tf.zeros((0, num_classes)), K.dtype(outputs))
reg_target = tf.cast(tf.zeros((0, 4)), K.dtype(outputs))
obj_target = tf.cast(tf.zeros((total_num_anchors, 1)), K.dtype(outputs))
fg_mask = tf.cast(tf.zeros(total_num_anchors), tf.bool)
return num_fg_img, cls_target, reg_target, obj_target, fg_mask
def f2():
gt_matched_classes, fg_mask, pred_ious_this_matching, matched_gt_inds, num_fg_img = get_assignments(
gt_bboxes_per_image, gt_classes, bboxes_preds_per_image, obj_preds_per_image, cls_preds_per_image,
x_shifts, y_shifts, expanded_strides, num_classes, num_gt, total_num_anchors,
)
reg_target = tf.cast(tf.gather_nd(gt_bboxes_per_image, tf.reshape(matched_gt_inds, [-1, 1])), K.dtype(outputs))
cls_target = tf.cast(tf.one_hot(tf.cast(gt_matched_classes, tf.int32), num_classes) * tf.expand_dims(pred_ious_this_matching, -1), K.dtype(outputs))
obj_target = tf.cast(tf.expand_dims(fg_mask, -1), K.dtype(outputs))
return num_fg_img, cls_target, reg_target, obj_target, fg_mask
num_fg_img, cls_target, reg_target, obj_target, fg_mask = tf.cond(tf.equal(num_gt, 0), f1, f2)
num_fg += num_fg_img
loss_iou += K.sum(1 - box_ciou(reg_target, tf.boolean_mask(bboxes_preds_per_image, fg_mask)))
loss_obj += K.sum(K.binary_crossentropy(obj_target, obj_preds_per_image, from_logits=True))
loss_cls += K.sum(K.binary_crossentropy(cls_target, tf.boolean_mask(cls_preds_per_image, fg_mask), from_logits=True))
return b + 1, num_fg, loss_iou, loss_obj, loss_cls
#-----------------------------------------------------------#
# 在這個地方進行一個回圈、回圈是對每一張圖片進行的
#-----------------------------------------------------------#
_, num_fg, loss_iou, loss_obj, loss_cls = K.control_flow_ops.while_loop(lambda b,*args: b < tf.cast(tf.shape(outputs)[0], tf.int32), loop_body, [0, num_fg, loss_iou, loss_obj, loss_cls])
num_fg = tf.cast(tf.maximum(num_fg, 1), K.dtype(outputs))
reg_weight = 5.0
loss = reg_weight * loss_iou + loss_obj + loss_cls
return loss / num_fg
訓練自己的YoloX模型
首先前往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檔案夾中,
其它引數的作用如下:
'''
是否使用eager模式訓練
'''
eager = False
'''
訓練前一定要修改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/yolo_weight.h5'
'''
輸入的shape大小,一定要是32的倍數
'''
input_shape = [416, 416]
'''
訓練分為兩個階段,分別是凍結階段和解凍階段
凍結階段訓練引數
此時模型的主干被凍結了,特征提取網路不發生改變
占用的顯存較小,僅對網路進行微調
'''
Init_Epoch = 0
Freeze_Epoch = 50
Freeze_batch_size = 8
Freeze_lr = 1e-3
'''
解凍階段訓練引數
此時模型的主干不被凍結了,特征提取網路會發生改變
占用的顯存較大,網路所有的引數都會發生改變
'''
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進行檢測了,運行后輸入圖片路徑即可檢測,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/301134.html
標籤:其他
上一篇:Pillow浮雕濾鏡
