SSD神經網絡學習——訓練自己的目標檢測模型


  SSD網絡全稱是Single Shot MultiBox Detector,可不是咱電腦上的那個SSD(固態硬盤)    ):

  Single Shot意思代表該模型是屬於one-stage目標檢測方法 ,one-stage又代表什么,代表一步到位,就是從先驗框到預測框的確定是一步到位,而非一步到位的比如Faster RCnn 網絡,由先驗框需要先通過RPN網絡得到建議框,再由建議框得到預測框,需要經過兩個步驟。所有的目標檢測都離不開分類和定位,二者都有才是一個完整的目標檢測;MultiBox說明SSD算法基於多框預測。

SSD主干網絡示意圖:

 

 

從圖中可以看到,SSD的主干網絡是VGG,VGG網絡模型在2014年的 ImageNet圖像分類與定位挑戰賽,取得了優異成績:在分類任務上排名第二,在定位任務上排名第一,所以在講SSD前需要介紹一下VGG網絡。下面是VGG網絡的結構圖:

 

 

   VGG網絡最大的特點就是將原來神經網絡中的大尺度卷積核轉化成為了連續幾個小卷積核,例如:將一個7x7的卷積核換成三個3x3的卷積核,5x5的卷積核換成2個3x3的卷積核。所以可以看到上圖中每一層卷積層都是分成幾次進行卷積。這種將大卷積核換成幾個小卷積核的方式的優勢在於增加了網絡的深度,同時也減少了網絡的參數,因為每次卷積后會經過激活函數,所以整體網絡的非線性也得到了提升。

圖中的VGG每一層主要實現:

1、一張原始圖片被resize到(224,224,3)。
2、conv1兩次[3,3]卷積網絡,輸出的特征層為64,輸出為(224,224,64),再2X2最大池化,輸出net為(112,112,64)。
3、conv2兩次[3,3]卷積網絡,輸出的特征層為128,輸出net為(112,112,128),再2X2最大池化,輸出net為(56,56,128)。
4、conv3三次[3,3]卷積網絡,輸出的特征層為256,輸出net為(56,56,256),再2X2最大池化,輸出net為(28,28,256)。
5、conv3三次[3,3]卷積網絡,輸出的特征層為256,輸出net為(28,28,512),再2X2最大池化,輸出net為(14,14,512)。
6、conv3三次[3,3]卷積網絡,輸出的特征層為256,輸出net為(14,14,512),再2X2最大池化,輸出net為(7,7,512)。
7、利用卷積的方式模擬全連接層,效果等同,輸出net為(1,1,4096)。共進行兩次。
8、利用卷積的方式模擬全連接層,效果等同,輸出net為(1,1,1000)。

最后輸出的就是每個類的預測。

從圖中也可以清楚看到,VGG網絡共有5個卷積層,3個全連接層,SSD網絡就是在這個基礎上進行以下改動:

1、將VGG16的FC6和FC7層轉化為卷積層。
2、去掉所有的Dropout層和FC8層;
3、新增了Conv6、Conv7、Conv8、Conv9。

 

 

 SSD網絡圖

如圖所示,輸入的圖片經過了改進的VGG網絡(Conv1->fc7)和幾個另加的卷積層(Conv6->Conv9),進行特征提取:

a、輸入一張圖片后,被resize到300x300的shape

b、conv1,經過兩次[3,3]卷積網絡,輸出的特征層為64,輸出為(300,300,64),再2X2最大池化,輸出net為(150,150,64)。

c、conv2,經過兩次[3,3]卷積網絡,輸出的特征層為128,輸出net為(150,150,128),再2X2最大池化,輸出net為(75,75,128)。

d、conv3,經過三次[3,3]卷積網絡,輸出的特征層為256,輸出net為(75,75,256),再2X2最大池化,輸出net為(38,38,256)。

e、conv4,經過三次[3,3]卷積網絡,輸出的特征層為512,輸出net為(38,38,512),再2X2最大池化,輸出net為(19,19,512)。

f、conv5,經過三次[3,3]卷積網絡,輸出的特征層為512,輸出net為(19,19,512),再2X2最大池化,輸出net為(19,19,512)。

g、利用卷積代替全連接層,進行了兩次[3,3]卷積網絡,輸出的特征層為1024,因此輸出的net為(19,19,1024)。(從這里往前都是VGG的結構)

h、conv6,經過一次[1,1]卷積網絡,調整通道數,一次步長為2的[3,3]卷積網絡,輸出的特征層為512,因此輸出的net為(10,10,512)。

i、conv7,經過一次[1,1]卷積網絡,調整通道數,一次步長為2的[3,3]卷積網絡,輸出的特征層為256,因此輸出的net為(5,5,256)。

j、conv8,經過一次[1,1]卷積網絡,調整通道數,一次padding為valid的[3,3]卷積網絡,輸出的特征層為256,因此輸出的net為(3,3,256)。

k、conv9,經過一次[1,1]卷積網絡,調整通道數,一次padding為valid的[3,3]卷積網絡,輸出的特征層為256,因此輸出的net為(1,1,256)。

下面就是修改后的VGG網絡,作為SSD的主干網絡的實現代碼

import keras.backend as K
from keras.layers import Activation
from keras.layers import Conv2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import GlobalAveragePooling2D
from keras.layers import Input
from keras.layers import MaxPooling2D
from keras.layers import merge, concatenate
from keras.layers import Reshape
from keras.layers import ZeroPadding2D
from keras.models import Model

def VGG16(input_tensor):
    #----------------------------主干特征提取網絡開始---------------------------#
    # SSD結構,net字典
    net = {} 
    # Block 1
    net['input'] = input_tensor
    # 300,300,3 -> 150,150,64
    net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_1')(net['input'])
    net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_2')(net['conv1_1'])
    net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool1')(net['conv1_2'])

    
    # Block 2
    # 150,150,64 -> 75,75,128
    net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_1')(net['pool1'])
    net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_2')(net['conv2_1'])
    net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool2')(net['conv2_2'])
    # Block 3
    # 75,75,128 -> 38,38,256
    net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_1')(net['pool2'])
    net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_2')(net['conv3_1'])
    net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_3')(net['conv3_2'])
    net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool3')(net['conv3_3'])
    # Block 4
    # 38,38,256 -> 19,19,512
    net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_1')(net['pool3'])
    net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_2')(net['conv4_1'])
    net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_3')(net['conv4_2'])
    net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool4')(net['conv4_3'])
    # Block 5
    # 19,19,512 -> 19,19,512
    net['conv5_1'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_1')(net['pool4'])
    net['conv5_2'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_2')(net['conv5_1'])
    net['conv5_3'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_3')(net['conv5_2'])
    net['pool5'] = MaxPooling2D((3, 3), strides=(1, 1), padding='same',
                                name='pool5')(net['conv5_3'])
    # FC6
    # 19,19,512 -> 19,19,1024
    net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
                                     activation='relu', padding='same',
                                     name='fc6')(net['pool5'])

    # x = Dropout(0.5, name='drop6')(x)
    # FC7
    # 19,19,1024 -> 19,19,1024
    net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
                               padding='same', name='fc7')(net['fc6'])

    # x = Dropout(0.5, name='drop7')(x)
    # Block 6
    # 19,19,512 -> 10,10,512
    net['conv6_1'] = Conv2D(256, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv6_1')(net['fc7'])
    net['conv6_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv6_padding')(net['conv6_1'])
    net['conv6_2'] = Conv2D(512, kernel_size=(3,3), strides=(2, 2),
                                   activation='relu',
                                   name='conv6_2')(net['conv6_2'])

    # Block 7
    # 10,10,512 -> 5,5,256
    net['conv7_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same', 
                                   name='conv7_1')(net['conv6_2'])
    net['conv7_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv7_padding')(net['conv7_1'])
    net['conv7_2'] = Conv2D(256, kernel_size=(3,3), strides=(2, 2),
                                   activation='relu', padding='valid',
                                   name='conv7_2')(net['conv7_2'])
    # Block 8
    # 5,5,256 -> 3,3,256
    net['conv8_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv8_1')(net['conv7_2'])
    net['conv8_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                   activation='relu', padding='valid',
                                   name='conv8_2')(net['conv8_1'])

    # Block 9
    # 3,3,256 -> 1,1,256
    net['conv9_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv9_1')(net['conv8_2'])
    net['conv9_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                   activation='relu', padding='valid',
                                   name='conv9_2')(net['conv9_1'])
    #----------------------------主干特征提取網絡結束---------------------------#
    return net

一、預測部分

原理分析:

  圖中有6個Detector,它們分別提取了conv4的第三次卷積的特征、fc7的特征、conv6的第二次卷積的特征、conv7的第二次卷積的特征、conv8的第二次卷積的特征、conv9的第二次卷積的特征,為了和普通特征層區分,我們稱之為有效特征層,來獲取預測結果。

  得到這6個有效特征層是為了分別對其上以每一個網格點為中心的先驗框變化情況進行預測,並預測每一個網格點上每一個先驗框對應的種類。

  那么先驗框是什么?由於圖片上的信息是隨機的,要對特點的物體進行識別和定位,只能采用最原始的辦法,先隨機定義幾個固定位置的框,將每個框里面的內容與待識別圖形做比較,相似度越高,那自然是該物體的可能性越大。在SSD中,將得到的6個有效特征層尺度不一樣,所以對 每個特征層進行網格划分時划分的網格數不一樣,以每個網格為中心的先驗框數量也不一樣。這里有一個表格可以看到每層有效特征層的差別。

 

 

  比如說conv4-3的特征層就是將整個圖像分成38x38個網格;然后從每個網格中心建立多個先驗框,如conv4-3的特征層就是建立了4個先驗框;對於conv4-3的特征層來講,整個圖片被分成38x38個網格,每個網格中心對應4個先驗框,一共包含了,38x38x4個,5776個先驗框。 這也就解釋了上圖和上表中的每一個數字。

  那么表中還有兩個重要點,一個是對有效特征層進行一次num_priors x 4的卷積、一次num_priors x num_classes的卷積,num_priors代表每層特征層上每個網格點處的先驗框數量,表中的第二列所示,nun_class代表要識別物體的類別數量;num_priors x 4(乘4的原因是每一個先驗框的信息由4個部分組成,分別是先驗框的中心點橫、縱坐標,以及先驗框的長和寬)的卷積用於預測該特征層上每一個網格點上每一個先驗框的變化情況。(為什么說是變化情況呢,這是因為ssd的預測結果需要結合先驗框獲得預測框,預測結果就是先驗框的變化情況。)num_priors x num_classes(voc2007數據集類別有20類+1個背景類共21類,乘4為84)的卷積 用於預測該特征層上每一個網格點上每一個預測框對應的種類。

實現代碼: 對6個有效特征層分別進行num_priors x 4的卷積和num_priors x num_classes的卷積

def SSD300(input_shape, num_classes=21):
    # 300,300,3
    input_tensor = Input(shape=input_shape)
    img_size = (input_shape[1], input_shape[0])

    # SSD結構,net字典
    net = VGG16(input_tensor)
    #-----------------------將提取到的主干特征進行處理---------------------------#
    # 對conv4_3進行處理 38,38,512
    net['conv4_3_norm'] = Normalize(20, name='conv4_3_norm')(net['conv4_3'])
    num_priors = 4
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    net['conv4_3_norm_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same', name='conv4_3_norm_mbox_loc')(net['conv4_3_norm'])
    net['conv4_3_norm_mbox_loc_flat'] = Flatten(name='conv4_3_norm_mbox_loc_flat')(net['conv4_3_norm_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    net['conv4_3_norm_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv4_3_norm_mbox_conf')(net['conv4_3_norm'])
    net['conv4_3_norm_mbox_conf_flat'] = Flatten(name='conv4_3_norm_mbox_conf_flat')(net['conv4_3_norm_mbox_conf'])
    priorbox = PriorBox(img_size, 30.0,max_size = 60.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv4_3_norm_mbox_priorbox')
    net['conv4_3_norm_mbox_priorbox'] = priorbox(net['conv4_3_norm'])
    
    # 對fc7層進行處理 
    num_priors = 6
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    net['fc7_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3),padding='same',name='fc7_mbox_loc')(net['fc7'])
    net['fc7_mbox_loc_flat'] = Flatten(name='fc7_mbox_loc_flat')(net['fc7_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    net['fc7_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3),padding='same',name='fc7_mbox_conf')(net['fc7'])
    net['fc7_mbox_conf_flat'] = Flatten(name='fc7_mbox_conf_flat')(net['fc7_mbox_conf'])

    priorbox = PriorBox(img_size, 60.0, max_size=111.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='fc7_mbox_priorbox')
    net['fc7_mbox_priorbox'] = priorbox(net['fc7'])

    # 對conv6_2進行處理
    num_priors = 6
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
    net['conv6_2_mbox_loc'] = x
    net['conv6_2_mbox_loc_flat'] = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
    net['conv6_2_mbox_conf'] = x
    net['conv6_2_mbox_conf_flat'] = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])

    priorbox = PriorBox(img_size, 111.0, max_size=162.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv6_2_mbox_priorbox')
    net['conv6_2_mbox_priorbox'] = priorbox(net['conv6_2'])

    # 對conv7_2進行處理
    num_priors = 6
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
    net['conv7_2_mbox_loc'] = x
    net['conv7_2_mbox_loc_flat'] = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
    net['conv7_2_mbox_conf'] = x
    net['conv7_2_mbox_conf_flat'] = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])

    priorbox = PriorBox(img_size, 162.0, max_size=213.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv7_2_mbox_priorbox')
    net['conv7_2_mbox_priorbox'] = priorbox(net['conv7_2'])

    # 對conv8_2進行處理
    num_priors = 4
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
    net['conv8_2_mbox_loc'] = x
    net['conv8_2_mbox_loc_flat'] = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
    net['conv8_2_mbox_conf'] = x
    net['conv8_2_mbox_conf_flat'] = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])

    priorbox = PriorBox(img_size, 213.0, max_size=264.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv8_2_mbox_priorbox')
    net['conv8_2_mbox_priorbox'] = priorbox(net['conv8_2'])

    # 對conv9_2進行處理
    num_priors = 4
    # 預測框的處理
    # num_priors表示每個網格點先驗框的數量,4是x,y,h,w的調整
    x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
    net['conv9_2_mbox_loc'] = x
    net['conv9_2_mbox_loc_flat'] = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
    # num_priors表示每個網格點先驗框的數量,num_classes是所分的類
    x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
    net['conv9_2_mbox_conf'] = x
    net['conv9_2_mbox_conf_flat'] = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
    
    priorbox = PriorBox(img_size, 264.0, max_size=315.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv9_2_mbox_priorbox')

    net['conv9_2_mbox_priorbox'] = priorbox(net['conv9_2'])

    # 將所有結果進行堆疊
    net['mbox_loc'] = concatenate([net['conv4_3_norm_mbox_loc_flat'],
                             net['fc7_mbox_loc_flat'],
                             net['conv6_2_mbox_loc_flat'],
                             net['conv7_2_mbox_loc_flat'],
                             net['conv8_2_mbox_loc_flat'],
                             net['conv9_2_mbox_loc_flat']],
                            axis=1, name='mbox_loc')
    net['mbox_conf'] = concatenate([net['conv4_3_norm_mbox_conf_flat'],
                              net['fc7_mbox_conf_flat'],
                              net['conv6_2_mbox_conf_flat'],
                              net['conv7_2_mbox_conf_flat'],
                              net['conv8_2_mbox_conf_flat'],
                              net['conv9_2_mbox_conf_flat']],
                             axis=1, name='mbox_conf')
    net['mbox_priorbox'] = concatenate([net['conv4_3_norm_mbox_priorbox'],
                                  net['fc7_mbox_priorbox'],
                                  net['conv6_2_mbox_priorbox'],
                                  net['conv7_2_mbox_priorbox'],
                                  net['conv8_2_mbox_priorbox'],
                                  net['conv9_2_mbox_priorbox']],
                                  axis=1, name='mbox_priorbox')

    if hasattr(net['mbox_loc'], '_keras_shape'):
        num_boxes = net['mbox_loc']._keras_shape[-1] // 4
    elif hasattr(net['mbox_loc'], 'int_shape'):
        num_boxes = K.int_shape(net['mbox_loc'])[-1] // 4
    # 8732,4
    net['mbox_loc'] = Reshape((num_boxes, 4),name='mbox_loc_final')(net['mbox_loc'])
    # 8732,21
    net['mbox_conf'] = Reshape((num_boxes, num_classes),name='mbox_conf_logits')(net['mbox_conf'])
    net['mbox_conf'] = Activation('softmax',name='mbox_conf_final')(net['mbox_conf'])

    net['predictions'] = concatenate([net['mbox_loc'],
                               net['mbox_conf'],
                               net['mbox_priorbox']],
                               axis=2, name='predictions')
    print(net['predictions'])
    model = Model(net['input'], net['predictions'])
    return model

 

 

預測框解碼:

  先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其實是有限的,無法表示任意情況,因此還需要調整,ssd利用num_priors x 4的卷積的結果對先驗框進行調整。

  num_priors x 4中的num_priors表示了這個網格點所包含的先驗框數量,其中的4表示了x_offset、y_offset、h和w的調整情況。x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。h和w代表了真實框的寬與高相對於先驗框的變化情況。
  SSD解碼過程就是將每個網格的中心點加上它對應的x_offset和y_offset,加完后的結果就是預測框的中心,然后再利用先驗框和h、w結合 計算出預測框的長和寬。這樣就能得到整個預測框的位置了。
  當然得到最終的預測結果后還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
  1、取出每一類得分大於self.obj_threshold的框和得分。
  2、利用框的位置和得分進行非極大抑制。
  實現代碼如下:

def decode_boxes(self, mbox_loc, mbox_priorbox, variances):
    # 獲得先驗框的寬與高
    prior_width = mbox_priorbox[:, 2] - mbox_priorbox[:, 0]
    prior_height = mbox_priorbox[:, 3] - mbox_priorbox[:, 1]
    # 獲得先驗框的中心點
    prior_center_x = 0.5 * (mbox_priorbox[:, 2] + mbox_priorbox[:, 0])
    prior_center_y = 0.5 * (mbox_priorbox[:, 3] + mbox_priorbox[:, 1])

    # 真實框距離先驗框中心的xy軸偏移情況
    decode_bbox_center_x = mbox_loc[:, 0] * prior_width * variances[:, 0]
    decode_bbox_center_x += prior_center_x
    decode_bbox_center_y = mbox_loc[:, 1] * prior_height * variances[:, 1]
    decode_bbox_center_y += prior_center_y
    
    # 真實框的寬與高的求取
    decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[:, 2])
    decode_bbox_width *= prior_width
    decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[:, 3])
    decode_bbox_height *= prior_height

    # 獲取真實框的左上角與右下角
    decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
    decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
    decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
    decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height

    # 真實框的左上角與右下角進行堆疊
    decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
                                    decode_bbox_ymin[:, None],
                                    decode_bbox_xmax[:, None],
                                    decode_bbox_ymax[:, None]), axis=-1)
    # 防止超出0與1
    decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
    return decode_bbox

def detection_out(self, predictions, background_label_id=0, keep_top_k=200,
                    confidence_threshold=0.5):
    # 網絡預測的結果
    mbox_loc = predictions[:, :, :4]
    # 0.1,0.1,0.2,0.2
    variances = predictions[:, :, -4:]
    # 先驗框
    mbox_priorbox = predictions[:, :, -8:-4]
    # 置信度
    mbox_conf = predictions[:, :, 4:-8]
    results = []
    # 對每一個特征層進行處理
    for i in range(len(mbox_loc)):
        results.append([])
        decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox[i],  variances[i])

        for c in range(self.num_classes):
            if c == background_label_id:
                continue
            c_confs = mbox_conf[i, :, c]
            c_confs_m = c_confs > confidence_threshold
            if len(c_confs[c_confs_m]) > 0:
                # 取出得分高於confidence_threshold的框
                boxes_to_process = decode_bbox[c_confs_m]
                confs_to_process = c_confs[c_confs_m]
                # 進行iou的非極大抑制
                feed_dict = {self.boxes: boxes_to_process,
                                self.scores: confs_to_process}
                idx = self.sess.run(self.nms, feed_dict=feed_dict)
                # 取出在非極大抑制中效果較好的內容
                good_boxes = boxes_to_process[idx]
                confs = confs_to_process[idx][:, None]
                # 將label、置信度、框的位置進行堆疊。
                labels = c * np.ones((len(idx), 1))
                c_pred = np.concatenate((labels, confs, good_boxes),
                                        axis=1)
                # 添加進result里
                results[-1].extend(c_pred)
        if len(results[-1]) > 0:
            # 按照置信度進行排序
            results[-1] = np.array(results[-1])
            argsort = np.argsort(results[-1][:, 1])[::-1]
            results[-1] = results[-1][argsort]
            # 選出置信度最大的keep_top_k個
            results[-1] = results[-1][:keep_top_k]
    return results

通過這一步實現了對所有先驗框的篩選,獲得了一些置信度高於閾值的預測框,預測結果就是先驗框的變化情況。將篩選后的框可以直接繪制在圖片上,就可以獲得結果。

 

二、訓練部分

  訓練時我們需要計算loss函數,這個loss函數是相對於ssd網絡的預測結果的。我們需要把標注圖片輸入到當前的ssd網絡中,得到預測結果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉化為ssd預測結果的格式信息。
  也就是,我們需要找到 每一張用於訓練的圖片的每一個真實框對應的先驗框,並求出如果想要得到這樣一個真實框,我們的預測結果應該是怎么樣的。
  從預測結果獲得真實框的過程被稱作解碼,而從真實框獲得預測結果的過程就是編碼的過程。實現代碼如下:

def encode_box(self, box, return_iou=True):
    iou = self.iou(box)
    encoded_box = np.zeros((self.num_priors, 4 + return_iou))

    # 找到每一個真實框,重合程度較高的先驗框
    assign_mask = iou > self.overlap_threshold
    if not assign_mask.any():
        assign_mask[iou.argmax()] = True
    if return_iou:
        encoded_box[:, -1][assign_mask] = iou[assign_mask]
    
    # 找到對應的先驗框
    assigned_priors = self.priors[assign_mask]
    # 逆向編碼,將真實框轉化為ssd預測結果的格式

    # 先計算真實框的中心與長寬
    box_center = 0.5 * (box[:2] + box[2:])
    box_wh = box[2:] - box[:2]
    # 再計算重合度較高的先驗框的中心與長寬
    assigned_priors_center = 0.5 * (assigned_priors[:, :2] +
                                    assigned_priors[:, 2:4])
    assigned_priors_wh = (assigned_priors[:, 2:4] -
                            assigned_priors[:, :2])
    
    # 逆向求取ssd應該有的預測結果
    encoded_box[:, :2][assign_mask] = box_center - assigned_priors_center
    encoded_box[:, :2][assign_mask] /= assigned_priors_wh
    # 除以0.1
    encoded_box[:, :2][assign_mask] /= assigned_priors[:, -4:-2]

    encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_priors_wh)
    # 除以0.2
    encoded_box[:, 2:4][assign_mask] /= assigned_priors[:, -2:]
    return encoded_box.ravel()

  利用上述代碼我們可以獲得真實框對應的所有的iou較大先驗框,並計算了真實框對應的所有iou較大的先驗框應該有的預測結果。在訓練的時候我們只需要選擇iou最大的先驗框就行了,這個iou最大的先驗框就是我們用來預測這個真實框所用的先驗框。因此我們還要經過一次篩選,將上述代碼獲得的真實框對應的所有的iou較大先驗框的預測結果中,iou最大的那個篩選出來。通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什么樣子。IOU代表預測框與真實框的重合部分的面積,越大說明越接近。
assign_boxes代碼實現: 

def assign_boxes(self, boxes):
    assignment = np.zeros((self.num_priors, 4 + self.num_classes + 8))
    assignment[:, 4] = 1.0
    if len(boxes) == 0:
        return assignment
    # 對每一個真實框都進行iou計算
    encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
    # 每一個真實框的編碼后的值,和iou
    encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
    
    # 取重合程度最大的先驗框,並且獲取這個先驗框的index
    best_iou = encoded_boxes[:, :, -1].max(axis=0)
    best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
    best_iou_mask = best_iou > 0
    best_iou_idx = best_iou_idx[best_iou_mask]

    assign_num = len(best_iou_idx)
    # 保留重合程度最大的先驗框的應該有的預測結果
    encoded_boxes = encoded_boxes[:, best_iou_mask, :]
    assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
    # 4代表為背景的概率,為0
    assignment[:, 4][best_iou_mask] = 0
    assignment[:, 5:-8][best_iou_mask] = boxes[best_iou_idx, 4:]
    assignment[:, -8][best_iou_mask] = 1
    # 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什么樣子的
    return assignment

 利用處理完的真實框與對應圖片的預測結果計算loss

loss的計算分為三個部分:
1、獲取所有正標簽的框的預測結果的回歸loss。
2、獲取所有正標簽的種類的預測結果的交叉熵loss。
3、獲取一定負標簽的種類的預測結果的交叉熵loss

  由於在ssd的訓練過程中,正負樣本極其不平衡,即存在對應真實框的先驗框可能只有2~3個,但是不存在對應真實框的負樣本卻有幾千個,這就會導致負樣本的loss值極大,因此我們可以考慮減少負樣本的選取,對於ssd的訓練來講,常見的情況是取三倍正樣本數量的負樣本用於訓練

loss函數定義代碼如下:

class MultiboxLoss(object):
    def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
                 background_label_id=0, negatives_for_hard=100.0):
        self.num_classes = num_classes
        self.alpha = alpha
        self.neg_pos_ratio = neg_pos_ratio
        if background_label_id != 0:
            raise Exception('Only 0 as background label id is supported')
        self.background_label_id = background_label_id
        self.negatives_for_hard = negatives_for_hard

    def _l1_smooth_loss(self, y_true, y_pred):
        abs_loss = tf.abs(y_true - y_pred)
        sq_loss = 0.5 * (y_true - y_pred)**2
        l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
        return tf.reduce_sum(l1_loss, -1)

    def _softmax_loss(self, y_true, y_pred):
        y_pred = tf.maximum(tf.minimum(y_pred, 1 - 1e-15), 1e-15)
        softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                                      axis=-1)
        return softmax_loss

    def compute_loss(self, y_true, y_pred):
        batch_size = tf.shape(y_true)[0]
        num_boxes = tf.to_float(tf.shape(y_true)[1])

        # 計算所有的loss
        # 分類的loss
        # batch_size,8732,21 -> batch_size,8732
        conf_loss = self._softmax_loss(y_true[:, :, 4:-8],
                                       y_pred[:, :, 4:-8])
        # 框的位置的loss
        # batch_size,8732,4 -> batch_size,8732
        loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
                                        y_pred[:, :, :4])

        # 獲取所有的正標簽的loss
        # 每一張圖的pos的個數
        num_pos = tf.reduce_sum(y_true[:, :, -8], axis=-1)
        # 每一張圖的pos_loc_loss
        pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -8],
                                     axis=1)
        # 每一張圖的pos_conf_loss
        pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -8],
                                      axis=1)

        # 獲取一定的負樣本
        num_neg = tf.minimum(self.neg_pos_ratio * num_pos,
                             num_boxes - num_pos)

        # 找到了哪些值是大於0的
        pos_num_neg_mask = tf.greater(num_neg, 0)
        # 獲得一個1.0
        has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
        num_neg = tf.concat( axis=0,values=[num_neg,
                                [(1 - has_min) * self.negatives_for_hard]])
        # 求平均每個圖片要取多少個負樣本
        num_neg_batch = tf.reduce_mean(tf.boolean_mask(num_neg,
                                                      tf.greater(num_neg, 0)))
        num_neg_batch = tf.to_int32(num_neg_batch)

        # conf的起始
        confs_start = 4 + self.background_label_id + 1
        # conf的結束
        confs_end = confs_start + self.num_classes - 1

        # 找到實際上在該位置不應該有預測結果的框,求他們最大的置信度。
        max_confs = tf.reduce_max(y_pred[:, :, confs_start:confs_end],
                                  axis=2)
        
        # 取top_k個置信度,作為負樣本
        _, indices = tf.nn.top_k(max_confs * (1 - y_true[:, :, -8]),
                                 k=num_neg_batch)

        # 找到其在1維上的索引
        batch_idx = tf.expand_dims(tf.range(0, batch_size), 1)
        batch_idx = tf.tile(batch_idx, (1, num_neg_batch))
        full_indices = (tf.reshape(batch_idx, [-1]) * tf.to_int32(num_boxes) +
                        tf.reshape(indices, [-1]))
        

        neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]),
                                  full_indices)
        neg_conf_loss = tf.reshape(neg_conf_loss,
                                   [batch_size, num_neg_batch])
        neg_conf_loss = tf.reduce_sum(neg_conf_loss, axis=1)

        # 求loss總和
        total_loss = K.sum(pos_conf_loss + neg_conf_loss)/K.cast(batch_size,K.dtype(pos_conf_loss))

        total_loss +=  K.sum(self.alpha * pos_loc_loss)/K.cast(batch_size,K.dtype(pos_loc_loss))
        return total_loss

 

訓練自己的SSD模型

首先需要制作自己的訓練集,制作方法在前面的文章有講過,可參考https://www.cnblogs.com/victorywr/p/12738878.html制作訓練集

源碼地址  https://pan.baidu.com/s/1c8I9x8ciTH85PcfY4nlKUg  提取碼:c87x

步驟:

訓練前將標簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。

訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。

訓練前利用voc2ssd.py文件生成對應的txt。

運行根目錄下的voc_annotation.py,運行前需要將classes改成自己的classes。

 

 

修改train.py文件里的NUM_CLASSES 為自己的種類數+1,如果待識別類別是10,那么NUM_CLASSES = 11,有一類是背景。

 

運行train.py開始訓練,訓練大概會要幾個小時,耐心等待。

 

測試訓練模型:檢測到不同的類別用不同的顏色框選出來,並標注上confidence。

 

 

 

                                                                                                                                        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM