主頁 >  其他 > 睿智的目標檢測52——Keras搭建YoloX目標檢測平臺

睿智的目標檢測52——Keras搭建YoloX目標檢測平臺

2021-09-18 17:06:02 其他

睿智的目標檢測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層的構建,構建方式為:

  1. feat3=(20,20,1024)的特征層進行1次1X1卷積調整通道后獲得P5,P5進行上采樣UmSampling2d后與feat2=(40,40,512)特征層進行結合,然后使用CSPLayer進行特征提取獲得P5_upsample,此時獲得的特征層為(40,40,512),
  2. P5_upsample=(40,40,512)的特征層進行1次1X1卷積調整通道后獲得P4,P4進行上采樣UmSampling2d后與feat1=(80,80,256)特征層進行結合,然后使用CSPLayer進行特征提取P3_out,此時獲得的特征層為(80,80,256),
  3. P3_out=(80,80,256)的特征層進行一次3x3卷積進行下采樣,下采樣后與P4堆疊,然后使用CSPLayer進行特征提取P4_out,此時獲得的特征層為(40,40,512),
  4. 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浮雕濾鏡

下一篇:第一篇Markdown博客---資料中心光互連

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more