Keras搭建M2Det目標檢測平臺示例
什么是M2det目標檢測算法
一起來看看M2det的keras實現(xiàn)吧,順便訓練一下自己的數(shù)據(jù)。
常見的特征提取方法如圖所示有SSD形,F(xiàn)PN形,STDN形:
SSD型:使用了主干網(wǎng)絡(luò)的最后兩層,再加上4個使用stride=2卷積的下采樣層構(gòu)成;
FPN型:也稱為U型網(wǎng)絡(luò),經(jīng)過上采樣操作,然后對應(yīng)融合相同的scale;
STDN型:基于DenseNet的最后一個dense block,通過池化和scale-transfer操作來構(gòu)建;
這三者有一定的缺點:
一是均基于分類網(wǎng)絡(luò)作為主干提取,對目標檢測任務(wù)而言特征表示可能不夠;
二是每個feature map僅由主干網(wǎng)絡(luò)的single level給出,不夠全面
M2det論文新提出MLFPN型,整體思想是Multi-level&Multi-scale。是一種更加有效的適合于檢測的特征金字塔結(jié)構(gòu)。

M2det實現(xiàn)思路
一、預(yù)測部分
1、主干網(wǎng)絡(luò)介紹

M2det采用可以采用VGG和ResNet101作為主干特征提取網(wǎng)絡(luò),上圖的backbone network指的就是VGG和Resnet101,本文以VGG為例介紹。
M2DET采用的主干網(wǎng)絡(luò)是VGG網(wǎng)絡(luò),關(guān)于VGG的介紹大家可以看我的另外一篇博客
http://www.dbjr.com.cn/article/246917.htm
在m2det中,我們?nèi)サ袅巳康娜B接層,只保留了卷積層和最大池化層,即Conv1到Conv5。
1、一張原始圖片被resize到(320,320,3)。
2、conv1兩次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為64,輸出為(320,320,64),再2X2最大池化,輸出net為(160,160,64)。
3、conv2兩次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為128,輸出net為(160,160,128),再2X2最大池化,輸出net為(80,80,128)。
4、conv3三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為256,輸出net為(80,80,256),再2X2最大池化,輸出net為(40,40,256)。
5、conv4三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為512,輸出net為(40,40,512),再2X2最大池化,此時不進行池化,輸出net為(40,40,512)。conv4-3的結(jié)果會進入FFM1進行特征的融合。
6、conv5三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為1024,輸出net為(40,40,1024),再2X2最大池化,輸出net為(20,20,1024)。池化后的結(jié)果會進入FFM1進行特征的融合。
from keras import Model
from keras.layers import Conv2D, MaxPooling2D
def VGG16(inputs):
net = {}
#------------------------#
# 輸入默認為320,320,3
#------------------------#
net['input'] = inputs
#------------------------------------------------#
# 第一個卷積部分 320,320,3 -> 160,160,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'])
#------------------------------------------------#
# 第二個卷積部分 160,160,64 -> 80,80,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'])
y0 = net['pool2']
#------------------------------------------------#
# 第三個卷積部分 80,80,128 -> 40,40,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'])
y1 = net['pool3']
#------------------------------------------------#
# 第四個卷積部分 40,40,256 -> 40,40,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='block4_pool')(net['conv4_3'])
y2 = net['conv4_3']
#------------------------------------------------#
# 第五個卷積部分 40,40,512 -> 20,20,1024
#------------------------------------------------#
net['conv5_1'] = Conv2D(1024, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_1')(net['conv4_3'])
net['conv5_2'] = Conv2D(1024, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_2')(net['conv5_1'])
net['conv5_3'] = Conv2D(1024, kernel_size=(3,3),
activation='relu',
padding='same',
name='conv5_3')(net['conv5_2'])
net['pool5'] = MaxPooling2D((3, 3), strides=(2, 2), padding='same',
name='pool5')(net['conv5_3'])
y3 = net['pool5']
model = Model(inputs, [y0,y1,y2,y3], name='vgg16')
return model
2、FFM1特征初步融合

FFM1具體的結(jié)構(gòu)如下:

FFM1會對VGG提取到的特征進行初步融合。
在利用VGG進行特征提取的時候,我們會取出shape為(40,40,512)、(20,20,1024)的特征層進行下一步的操作。
在FFM1中,其會對(20,20,1024)的特征層進行進行一個通道數(shù)為512、卷積核大小為3x3、步長為1x1的卷積,然后再進行上采樣,使其Shape變?yōu)?40,40,512);
同時會對(40,40,512)的特征層進行進行一個通道數(shù)為256、卷積核大小為1x1,步長為1x1的卷積,使其Shape變?yōu)?40,40,256);
然后將兩個卷積后的結(jié)果進行堆疊,變成一個(40,40,768)的初步融合特征層
實現(xiàn)代碼為:
def FFMv1(C4, C5, feature_size_1=256, feature_size_2=512, name='FFMv1'):
#------------------------------------------------#
# C4特征層 40,40,512
# C5特征層 20,20,1024
#------------------------------------------------#
# 40,40,512 -> 40,40,256
F4 = conv2d(C4, filters=feature_size_1, kernel_size=(3, 3), strides=(1, 1), padding='same', name='F4')
# 20,20,1024 -> 20,20,512
F5 = conv2d(C5, filters=feature_size_2, kernel_size=(1, 1), strides=(1, 1), padding='same', name='F5')
# 20,20,512 -> 40,40,512
F5 = keras.layers.UpSampling2D(size=(2, 2), name='F5_Up')(F5)
# 40,40,256 + 40,40,512 -> 40,40,768
outputs = keras.layers.Concatenate(name=name)([F4, F5])
return outputs
3、細化U型模塊TUM

Tum的結(jié)構(gòu)具體如下:

當我們給Tum輸入一個(40,40,256)的有效特征層之后,Tum會對輸入進來的特征層進行U型的特征提取,這里的結(jié)構(gòu)比較類似特征金字塔的結(jié)構(gòu),先對特征層進行不斷的特征壓縮,然后再不斷的上采樣進行特征融合,利用Tum我們可以獲得6個有效特征層,大小分別是(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)。
def TUM(stage, inputs, feature_size=256, name="TUM"): #---------------------------------# # 進行下采樣的部分 #---------------------------------# # 40,40,256 f1 = inputs # 40,40,256 -> 20,20,256 f2 = conv2d(f1, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f2') # 20,20,256 -> 10,10,256 f3 = conv2d(f2, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f3') # 10,10,256 -> 5,5,256 f4 = conv2d(f3, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f4') # 5,5,256 -> 3,3,256 f5 = conv2d(f4, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f5') # 3,3,256 -> 1,1,256 f6 = conv2d(f5, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='valid',name=name + "_" + str(stage) + '_f6') size_buffer = [] # 40,40 size_buffer.append([int(f1.shape[1]), int(f1.shape[2])]) # 20,20 size_buffer.append([int(f2.shape[1]), int(f2.shape[2])]) # 10,10 size_buffer.append([int(f3.shape[1]), int(f3.shape[2])]) # 5,5 size_buffer.append([int(f4.shape[1]), int(f4.shape[2])]) # 3,3 size_buffer.append([int(f5.shape[1]), int(f5.shape[2])]) #---------------------------------# # 進行上采樣與特征融合的部分 #---------------------------------# c6 = f6 # 1,1,256 -> 1,1,256 c5 = conv2d(c6, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same',name=name + "_" + str(stage) + '_c5') # 1,1,256 -> 3,3,256 c5 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[4]), name=name + "_" + str(stage) + '_upsample_add5')(c5) c5 = keras.layers.Add()([c5, f5]) # 3,3,256 -> 3,3,256 c4 = conv2d(c5, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c4') # 3,3,256 -> 5,5,256 c4 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[3]), name=name + "_" + str(stage) + '_upsample_add4')(c4) c4 = keras.layers.Add()([c4, f4]) # 5,5,256 -> 5,5,256 c3 = conv2d(c4, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c3') # 5,5,256 -> 10,10,256 c3 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[2]), name=name + "_" + str(stage) + '_upsample_add3')(c3) c3 = keras.layers.Add()([c3, f3]) # 10,10,256 -> 10,10,256 c2 = conv2d(c3, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c2') # 10,10,256 -> 20,20,256 c2 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[1]), name=name + "_" + str(stage) + '_upsample_add2')(c2) c2 = keras.layers.Add()([c2, f2]) # 20,20,256 -> 20,20,256 c1 = conv2d(c2, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c1') # 20,20,256 -> 40,40,256 c1 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[0]), name=name + "_" + str(stage) + '_upsample_add1')(c1) c1 = keras.layers.Add()([c1, f1]) #---------------------------------# # 利用1x1卷積調(diào)整通道數(shù)后輸出 #---------------------------------# output_features = feature_size // 2 # 40,40,256 -> 40,40,128 o1 = conv2d(c1, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o1') # 20,20,256 -> 20,20,128 o2 = conv2d(c2, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o2') # 10,10,256 -> 10,10,128 o3 = conv2d(c3, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o3') # 5,5,256 -> 5,5,128 o4 = conv2d(c4, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o4') # 3,3,256 -> 3,3,128 o5 = conv2d(c5, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o5') # 1,1,256 -> 1,1,128 o6 = conv2d(c6, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o6') outputs = [o1, o2, o3, o4, o5, o6] return outputs
def TUM(stage, inputs, feature_size=256, name="TUM"):
#---------------------------------#
# 進行下采樣的部分
#---------------------------------#
# 40,40,256
f1 = inputs
# 40,40,256 -> 20,20,256
f2 = conv2d(f1, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f2')
# 20,20,256 -> 10,10,256
f3 = conv2d(f2, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f3')
# 10,10,256 -> 5,5,256
f4 = conv2d(f3, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f4')
# 5,5,256 -> 3,3,256
f5 = conv2d(f4, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f5')
# 3,3,256 -> 1,1,256
f6 = conv2d(f5, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='valid',name=name + "_" + str(stage) + '_f6')
size_buffer = []
# 40,40
size_buffer.append([int(f1.shape[1]), int(f1.shape[2])])
# 20,20
size_buffer.append([int(f2.shape[1]), int(f2.shape[2])])
# 10,10
size_buffer.append([int(f3.shape[1]), int(f3.shape[2])])
# 5,5
size_buffer.append([int(f4.shape[1]), int(f4.shape[2])])
# 3,3
size_buffer.append([int(f5.shape[1]), int(f5.shape[2])])
#---------------------------------#
# 進行上采樣與特征融合的部分
#---------------------------------#
c6 = f6
# 1,1,256 -> 1,1,256
c5 = conv2d(c6, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same',name=name + "_" + str(stage) + '_c5')
# 1,1,256 -> 3,3,256
c5 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[4]), name=name + "_" + str(stage) + '_upsample_add5')(c5)
c5 = keras.layers.Add()([c5, f5])
# 3,3,256 -> 3,3,256
c4 = conv2d(c5, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c4')
# 3,3,256 -> 5,5,256
c4 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[3]), name=name + "_" + str(stage) + '_upsample_add4')(c4)
c4 = keras.layers.Add()([c4, f4])
# 5,5,256 -> 5,5,256
c3 = conv2d(c4, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c3')
# 5,5,256 -> 10,10,256
c3 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[2]), name=name + "_" + str(stage) + '_upsample_add3')(c3)
c3 = keras.layers.Add()([c3, f3])
# 10,10,256 -> 10,10,256
c2 = conv2d(c3, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c2')
# 10,10,256 -> 20,20,256
c2 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[1]), name=name + "_" + str(stage) + '_upsample_add2')(c2)
c2 = keras.layers.Add()([c2, f2])
# 20,20,256 -> 20,20,256
c1 = conv2d(c2, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c1')
# 20,20,256 -> 40,40,256
c1 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[0]), name=name + "_" + str(stage) + '_upsample_add1')(c1)
c1 = keras.layers.Add()([c1, f1])
#---------------------------------#
# 利用1x1卷積調(diào)整通道數(shù)后輸出
#---------------------------------#
output_features = feature_size // 2
# 40,40,256 -> 40,40,128
o1 = conv2d(c1, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o1')
# 20,20,256 -> 20,20,128
o2 = conv2d(c2, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o2')
# 10,10,256 -> 10,10,128
o3 = conv2d(c3, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o3')
# 5,5,256 -> 5,5,128
o4 = conv2d(c4, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o4')
# 3,3,256 -> 3,3,128
o5 = conv2d(c5, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o5')
# 1,1,256 -> 1,1,128
o6 = conv2d(c6, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o6')
outputs = [o1, o2, o3, o4, o5, o6]
return outputs
4、FFM2特征加強融合
通過TUM,我們可以獲得六個有效特征層,為了進一步加強網(wǎng)絡(luò)的特征提取能力,M2det將6個有效特征層中的(40,40,128)特征層取出,和FFM1提取出來的初步融合特征層進行加強融合,再次輸出一個(40,40,256)的加強融合的特征層。

此時FFM2輸出的加強融合特征層可以再一次傳入到TUM中進行U形特征提取。

如上圖所示,我們可以進一步利用多個TUM模塊進行特征提取,利用多個TUM模塊我們可以獲得多次有效特征層。
TUM模塊的數(shù)量我們可以根據(jù)自身需要進行修改,本文使用4次TUM模塊,可以分別獲得四次(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)的有效特征層。(論文中做了實驗,用8次TUM模塊會有比較好的效果)。
我們可以將獲得的有效特征層,按照shape進行堆疊,最終獲得(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六個有效特征層。
def FFMv2(stage, base, tum, base_size=(40,40,768), tum_size=(40,40,128), feature_size=128, name='FFMv2'):
# 40,40,128
outputs = conv2d(base, filters=feature_size, kernel_size=(1, 1), strides=(1, 1), padding='same', name=name+"_"+str(stage) + '_base_feature')
outputs = keras.layers.Concatenate(name=name+"_"+str(stage))([outputs, tum])
# 40,40,256
return outputs
def _create_feature_pyramid(base_feature, stage=8):
features = [[],[],[],[],[],[]]
# 將輸入進來的
inputs = keras.layers.Conv2D(filters=256, kernel_size=1, strides=1, padding='same')(base_feature)
# 第一個TUM模塊
outputs = TUM(1,inputs)
max_output = outputs[0]
for j in range(len(features)):
features[j].append(outputs[j])
# 第2,3,4個TUM模塊,需要將上一個Tum模塊輸出的40x40x128的內(nèi)容,傳入到下一個Tum模塊中
for i in range(2, stage+1):
# 將Tum模塊的輸出和基礎(chǔ)特征層傳入到FFmv2層當中
# 輸入為base_feature 40x40x768,max_output 40x40x128
# 輸出為40x40x256
inputs = FFMv2(i - 1,base_feature, max_output)
# 輸出為40x40x128、20x20x128、10x10x128、5x5x128、3x3x128、1x1x128
outputs = TUM(i,inputs)
max_output = outputs[0]
for j in range(len(features)):
features[j].append(outputs[j])
# 進行了4次TUM
# 將獲得的同樣大小的特征層堆疊到一起
concatenate_features = []
for feature in features:
concat = keras.layers.Concatenate()([f for f in feature])
concatenate_features.append(concat)
return concatenate_features
5、注意力機制模塊SFAM

注意力機制模塊如下:

其會對上一步獲得的(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六個有效特征層。進行各個通道的注意力機制調(diào)整,判斷每一個通道數(shù)應(yīng)該有的權(quán)重。
# 注意力機制
def SE_block(inputs, input_size, compress_ratio=16, name='SE_block'):
pool = keras.layers.GlobalAveragePooling2D()(inputs)
reshape = keras.layers.Reshape((1, 1, input_size[2]))(pool)
fc1 = keras.layers.Conv2D(filters=input_size[2] // compress_ratio, kernel_size=1, strides=1, padding='valid',
activation='relu', name=name+'_fc1')(reshape)
fc2 = keras.layers.Conv2D(filters=input_size[2], kernel_size=1, strides=1, padding='valid', activation='sigmoid',
name=name+'_fc2')(fc1)
reweight = keras.layers.Multiply(name=name+'_reweight')([inputs, fc2])
return reweight
def SFAM(feature_pyramid,input_sizes, compress_ratio=16, name='SFAM'):
outputs = []
for i in range(len(input_sizes)):
input_size = input_sizes[i]
_input = feature_pyramid[i]
_output = SE_block(_input, input_size, compress_ratio=compress_ratio, name='SE_block_' + str(i))
outputs.append(_output)
return outputs
6、從特征獲取預(yù)測結(jié)果
通過第五步,我們獲取了6個融合了注意力機制的有效特征層。
對獲取到的每一個有效特征層,我們分別對其進行一次num_anchors x 4的卷積、一次num_anchors x num_classes的卷積、并需要計算每一個有效特征層對應(yīng)的先驗框。而num_anchors指的是該特征層所擁有的先驗框數(shù)量。
其中:
num_anchors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。(為什么說是變化情況呢,這是因為M2DET的預(yù)測結(jié)果需要結(jié)合先驗框獲得預(yù)測框,預(yù)測結(jié)果就是先驗框的變化情況。)
num_anchors x num_classes的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個預(yù)測框?qū)?yīng)的種類。
每一個有效特征層對應(yīng)的先驗框?qū)?yīng)著該特征層上 每一個網(wǎng)格點上 預(yù)先設(shè)定好的六個框。
所有的特征層對應(yīng)的預(yù)測結(jié)果的shape如下:

實現(xiàn)代碼為:
def m2det(input_shape, num_classes=21, num_anchors = 6):
inputs = keras.layers.Input(shape=input_shape)
#------------------------------------------------#
# 利用主干特征提取網(wǎng)絡(luò)獲得兩個有效特征層
# 分別是C4 40,40,512
# 分別是C5 20,20,1024
#------------------------------------------------#
C4, C5 = VGG16(inputs).outputs[2:]
# base_feature的shape為40,40,768
base_feature = FFMv1(C4, C5, feature_size_1=256, feature_size_2=512)
#---------------------------------------------------------------------------------------------------#
# 在_create_feature_pyramid函數(shù)里,我們會使用TUM模塊對輸入進來的特征層進行特征提取
# 最終輸出的特征層有六個,由于進行了四次的TUM模塊,所以六個有效特征層由4次TUM模塊的輸出堆疊而成
# o1 40,40,128*4 40,40,512
# o2 20,20,128*4 20,20,512
# o3 10,10,128*4 10,10,512
# o4 5,5,128*4 5,5,512
# o5 3,3,128*4 3,3,512
# o6 1,1,128*4 1,1,512
#---------------------------------------------------------------------------------------------------#
feature_pyramid = _create_feature_pyramid(base_feature, stage=4)
#-------------------------------------------------#
# 給合并后的特征層添加上注意力機制
#-------------------------------------------------#
outputs = SFAM(feature_pyramid)
#-------------------------------------------------#
# 將有效特征層轉(zhuǎn)換成輸出結(jié)果
#-------------------------------------------------#
classifications = []
regressions = []
for feature in outputs:
classification = keras.layers.Conv2D(filters = num_anchors * num_classes, kernel_size=3, strides=1, padding='same')(feature)
classification = keras.layers.Reshape((-1, num_classes))(classification)
classification = keras.layers.Activation('softmax')(classification)
regression = keras.layers.Conv2D(filters = num_anchors * 4, kernel_size=3, strides=1, padding='same')(feature)
regression = keras.layers.Reshape((-1, 4))(regression)
classifications.append(classification)
regressions.append(regression)
classifications = keras.layers.Concatenate(axis=1, name="classification")(classifications)
regressions = keras.layers.Concatenate(axis=1, name="regression")(regressions)
pyramids = keras.layers.Concatenate(axis=-1, name="out")([regressions, classifications])
return keras.models.Model(inputs=inputs, outputs=pyramids)
7、預(yù)測結(jié)果的解碼
我們通過對每一個特征層的處理,可以獲得兩個內(nèi)容,分別是:
num_anchors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。
num_anchors x num_classes的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個預(yù)測框?qū)?yīng)的種類。
每一個有效特征層對應(yīng)的先驗框?qū)?yīng)著該特征層上 每一個網(wǎng)格點上 預(yù)先設(shè)定好的六個框。
我們利用 num_anchors x 4的卷積 與 每一個有效特征層對應(yīng)的先驗框 獲得框的真實位置。
每一個有效特征層對應(yīng)的先驗框就是,如圖所示的作用:
每一個有效特征層將整個圖片分成與其長寬對應(yīng)的網(wǎng)格,如conv4-3和fl7組合成的特征層就是將整個圖像分成38x38個網(wǎng)格;然后從每個網(wǎng)格中心建立多個先驗框,如conv4-3和fl7組合成的有效特征層就是建立了6個先驗框;對于conv4-3和fl7組合成的特征層來講,整個圖片被分成38x38個網(wǎng)格,每個網(wǎng)格中心對應(yīng)6個先驗框,一共包含了,38x38x6個,8664個先驗框。

先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調(diào)整,RFBnet利用num_anchors x 4的卷積的結(jié)果對先驗框進行調(diào)整。
num_anchors x 4中的num_anchors表示了這個網(wǎng)格點所包含的先驗框數(shù)量,其中的4表示了x_offset、y_offset、h和w的調(diào)整情況。
x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。h和w代表了真實框的寬與高相對于先驗框的變化情況。
RFBnet解碼過程就是將每個網(wǎng)格的中心點加上它對應(yīng)的x_offset和y_offset,加完后的結(jié)果就是預(yù)測框的中心,然后再利用 先驗框和h、w結(jié)合 計算出預(yù)測框的長和寬。這樣就能得到整個預(yù)測框的位置了。
當然得到最終的預(yù)測結(jié)構(gòu)后還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
1、取出每一類得分大于self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。
實現(xiàn)代碼如下:
import numpy as np
import tensorflow as tf
import keras.backend as K
class BBoxUtility(object):
def __init__(self, num_classes, nms_thresh=0.45, top_k=300):
self.num_classes = num_classes
self._nms_thresh = nms_thresh
self._top_k = top_k
self.boxes = K.placeholder(dtype='float32', shape=(None, 4))
self.scores = K.placeholder(dtype='float32', shape=(None,))
self.nms = tf.image.non_max_suppression(self.boxes, self.scores, self._top_k, iou_threshold=self._nms_thresh)
self.sess = K.get_session()
def ssd_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image):
#-----------------------------------------------------------------#
# 把y軸放前面是因為方便預(yù)測框和圖像的寬高進行相乘
#-----------------------------------------------------------------#
box_yx = box_xy[..., ::-1]
box_hw = box_wh[..., ::-1]
input_shape = np.array(input_shape)
image_shape = np.array(image_shape)
if letterbox_image:
#-----------------------------------------------------------------#
# 這里求出來的offset是圖像有效區(qū)域相對于圖像左上角的偏移情況
# new_shape指的是寬高縮放情況
#-----------------------------------------------------------------#
new_shape = np.round(image_shape * np.min(input_shape/image_shape))
offset = (input_shape - new_shape)/2./input_shape
scale = input_shape/new_shape
box_yx = (box_yx - offset) * scale
box_hw *= scale
box_mins = box_yx - (box_hw / 2.)
box_maxes = box_yx + (box_hw / 2.)
boxes = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1)
boxes *= np.concatenate([image_shape, image_shape], axis=-1)
return boxes
def decode_boxes(self, mbox_loc, anchors, variances):
# 獲得先驗框的寬與高
anchor_width = anchors[:, 2] - anchors[:, 0]
anchor_height = anchors[:, 3] - anchors[:, 1]
# 獲得先驗框的中心點
anchor_center_x = 0.5 * (anchors[:, 2] + anchors[:, 0])
anchor_center_y = 0.5 * (anchors[:, 3] + anchors[:, 1])
# 真實框距離先驗框中心的xy軸偏移情況
decode_bbox_center_x = mbox_loc[:, 0] * anchor_width * variances[0]
decode_bbox_center_x += anchor_center_x
decode_bbox_center_y = mbox_loc[:, 1] * anchor_height * variances[1]
decode_bbox_center_y += anchor_center_y
# 真實框的寬與高的求取
decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[2])
decode_bbox_width *= anchor_width
decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[3])
decode_bbox_height *= anchor_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 decode_box(self, predictions, anchors, image_shape, input_shape, letterbox_image, variances = [0.1, 0.1, 0.2, 0.2], confidence=0.5):
#---------------------------------------------------#
# :4是回歸預(yù)測結(jié)果
#---------------------------------------------------#
mbox_loc = predictions[:, :, :4]
#---------------------------------------------------#
# 獲得種類的置信度
#---------------------------------------------------#
mbox_conf = predictions[:, :, 4:]
results = []
#----------------------------------------------------------------------------------------------------------------#
# 對每一張圖片進行處理,由于在predict.py的時候,我們只輸入一張圖片,所以for i in range(len(mbox_loc))只進行一次
#----------------------------------------------------------------------------------------------------------------#
for i in range(len(mbox_loc)):
results.append([])
#--------------------------------#
# 利用回歸結(jié)果對先驗框進行解碼
#--------------------------------#
decode_bbox = self.decode_boxes(mbox_loc[i], anchors, variances)
for c in range(1, self.num_classes):
#--------------------------------#
# 取出屬于該類的所有框的置信度
# 判斷是否大于門限
#--------------------------------#
c_confs = mbox_conf[i, :, c]
c_confs_m = c_confs > confidence
if len(c_confs[c_confs_m]) > 0:
#-----------------------------------------#
# 取出得分高于confidence的框
#-----------------------------------------#
boxes_to_process = decode_bbox[c_confs_m]
confs_to_process = c_confs[c_confs_m]
#-----------------------------------------#
# 進行iou的非極大抑制
#-----------------------------------------#
idx = self.sess.run(self.nms, feed_dict={self.boxes: boxes_to_process, self.scores: confs_to_process})
#-----------------------------------------#
# 取出在非極大抑制中效果較好的內(nèi)容
#-----------------------------------------#
good_boxes = boxes_to_process[idx]
confs = confs_to_process[idx][:, None]
labels = (c - 1) * np.ones((len(idx), 1))
#-----------------------------------------#
# 將label、置信度、框的位置進行堆疊。
#-----------------------------------------#
c_pred = np.concatenate((good_boxes, labels, confs), axis=1)
# 添加進result里
results[-1].extend(c_pred)
if len(results[-1]) > 0:
results[-1] = np.array(results[-1])
box_xy, box_wh = (results[-1][:, 0:2] + results[-1][:, 2:4])/2, results[-1][:, 2:4] - results[-1][:, 0:2]
results[-1][:, :4] = self.ssd_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)
return results
8、在原圖上進行繪制
通過第三步,我們可以獲得預(yù)測框在原圖上的位置,而且這些預(yù)測框都是經(jīng)過篩選的。這些篩選后的框可以直接繪制在圖片上,就可以獲得結(jié)果了。
二、訓練部分
1、真實框的處理
從預(yù)測部分我們知道,每個特征層的預(yù)測結(jié)果,num_anchors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。
也就是說,我們直接利用M2DET網(wǎng)絡(luò)預(yù)測到的結(jié)果,并不是預(yù)測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓練的時候,我們需要計算loss函數(shù),這個loss函數(shù)是相對于M2DET網(wǎng)絡(luò)的預(yù)測結(jié)果的。我們需要把圖片輸入到當前的M2DET網(wǎng)絡(luò)中,得到預(yù)測結(jié)果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉(zhuǎn)化為M2DET預(yù)測結(jié)果的格式信息。
也就是,我們需要找到 每一張用于訓練的圖片的每一個真實框?qū)?yīng)的先驗框,并求出如果想要得到這樣一個真實框,我們的預(yù)測結(jié)果應(yīng)該是怎么樣的。
從預(yù)測結(jié)果獲得真實框的過程被稱作解碼,而從真實框獲得預(yù)測結(jié)果的過程就是編碼的過程。
因此我們只需要將解碼過程逆過來就是編碼過程了。
實現(xiàn)代碼如下:
def iou(self, box):
#---------------------------------------------#
# 計算出每個真實框與所有的先驗框的iou
# 判斷真實框與先驗框的重合情況
#---------------------------------------------#
inter_upleft = np.maximum(self.anchors[:, :2], box[:2])
inter_botright = np.minimum(self.anchors[:, 2:4], box[2:])
inter_wh = inter_botright - inter_upleft
inter_wh = np.maximum(inter_wh, 0)
inter = inter_wh[:, 0] * inter_wh[:, 1]
#---------------------------------------------#
# 真實框的面積
#---------------------------------------------#
area_true = (box[2] - box[0]) * (box[3] - box[1])
#---------------------------------------------#
# 先驗框的面積
#---------------------------------------------#
area_gt = (self.anchors[:, 2] - self.anchors[:, 0])*(self.anchors[:, 3] - self.anchors[:, 1])
#---------------------------------------------#
# 計算iou
#---------------------------------------------#
union = area_true + area_gt - inter
iou = inter / union
return iou
def encode_box(self, box, return_iou=True, variances = [0.1, 0.1, 0.2, 0.2]):
#---------------------------------------------#
# 計算當前真實框和先驗框的重合情況
# iou [self.num_anchors]
# encoded_box [self.num_anchors, 5]
#---------------------------------------------#
iou = self.iou(box)
encoded_box = np.zeros((self.num_anchors, 4 + return_iou))
#---------------------------------------------#
# 找到每一個真實框,重合程度較高的先驗框
# 真實框可以由這個先驗框來負責預(yù)測
#---------------------------------------------#
assign_mask = iou > self.overlap_threshold
#---------------------------------------------#
# 如果沒有一個先驗框重合度大于self.overlap_threshold
# 則選擇重合度最大的為正樣本
#---------------------------------------------#
if not assign_mask.any():
assign_mask[iou.argmax()] = True
#---------------------------------------------#
# 利用iou進行賦值
#---------------------------------------------#
if return_iou:
encoded_box[:, -1][assign_mask] = iou[assign_mask]
#---------------------------------------------#
# 找到對應(yīng)的先驗框
#---------------------------------------------#
assigned_anchors = self.anchors[assign_mask]
#---------------------------------------------#
# 逆向編碼,將真實框轉(zhuǎn)化為M2det預(yù)測結(jié)果的格式
# 先計算真實框的中心與長寬
#---------------------------------------------#
box_center = 0.5 * (box[:2] + box[2:])
box_wh = box[2:] - box[:2]
#---------------------------------------------#
# 再計算重合度較高的先驗框的中心與長寬
#---------------------------------------------#
assigned_anchors_center = (assigned_anchors[:, 0:2] + assigned_anchors[:, 2:4]) * 0.5
assigned_anchors_wh = (assigned_anchors[:, 2:4] - assigned_anchors[:, 0:2])
#------------------------------------------------#
# 逆向求取M2det應(yīng)該有的預(yù)測結(jié)果
# 先求取中心的預(yù)測結(jié)果,再求取寬高的預(yù)測結(jié)果
# 存在改變數(shù)量級的參數(shù),默認為[0.1,0.1,0.2,0.2]
#------------------------------------------------#
encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]
encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
return encoded_box.ravel()
利用上述代碼我們可以獲得,真實框?qū)?yīng)的所有的iou較大先驗框,并計算了真實框?qū)?yīng)的所有iou較大的先驗框應(yīng)該有的預(yù)測結(jié)果。
在訓練的時候我們只需要選擇iou最大的先驗框就行了,這個iou最大的先驗框就是我們用來預(yù)測這個真實框所用的先驗框。
因此我們還要經(jīng)過一次篩選,將上述代碼獲得的真實框?qū)?yīng)的所有的iou較大先驗框的預(yù)測結(jié)果中,iou最大的那個篩選出來。
通過assign_boxes我們就獲得了,輸入進來的這張圖片,應(yīng)該有的預(yù)測結(jié)果是什么樣子的。
實現(xiàn)代碼如下:
def assign_boxes(self, boxes):
#---------------------------------------------------#
# assignment分為3個部分
# :4 的內(nèi)容為網(wǎng)絡(luò)應(yīng)該有的回歸預(yù)測結(jié)果
# 4:-1 的內(nèi)容為先驗框所對應(yīng)的種類,默認為背景
# -1 的內(nèi)容為當前先驗框是否包含目標
#---------------------------------------------------#
assignment = np.zeros((self.num_anchors, 4 + self.num_classes + 1))
assignment[:, 4] = 1.0
if len(boxes) == 0:
return assignment
# 對每一個真實框都進行iou計算
encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
#---------------------------------------------------#
# 在reshape后,獲得的encoded_boxes的shape為:
# [num_true_box, num_anchors, 4 + 1]
# 4是編碼后的結(jié)果,1為iou
#---------------------------------------------------#
encoded_boxes = encoded_boxes.reshape(-1, self.num_anchors, 5)
#---------------------------------------------------#
# [num_anchors]求取每一個先驗框重合度最大的真實框
#---------------------------------------------------#
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代表為背景的概率,設(shè)定為0,因為這些先驗框有對應(yīng)的物體
#----------------------------------------------------------#
assignment[:, 4][best_iou_mask] = 0
assignment[:, 5:-1][best_iou_mask] = boxes[best_iou_idx, 4:]
#----------------------------------------------------------#
# -1表示先驗框是否有對應(yīng)的物體
#----------------------------------------------------------#
assignment[:, -1][best_iou_mask] = 1
# 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應(yīng)該有的預(yù)測結(jié)果是什么樣子的
return assignment
2、利用處理完的真實框與對應(yīng)圖片的預(yù)測結(jié)果計算loss
loss的計算分為三個部分:
1、獲取所有正標簽的框的預(yù)測結(jié)果的回歸loss。
2、獲取所有正標簽的種類的預(yù)測結(jié)果的交叉熵loss。
3、獲取一定負標簽的種類的預(yù)測結(jié)果的交叉熵loss。
由于在M2DET的訓練過程中,正負樣本極其不平衡,即 存在對應(yīng)真實框的先驗框可能只有十來個,但是不存在對應(yīng)真實框的負樣本卻有幾千個,這就會導致負樣本的loss值極大,因此我們可以考慮減少負樣本的選取,對于M2DET的訓練來講,常見的情況是取三倍正樣本數(shù)量的負樣本用于訓練。這個三倍呢,也可以修改,調(diào)整成自己喜歡的數(shù)字。
實現(xiàn)代碼如下:
import tensorflow as tf
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(y_pred, 1e-7)
softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
axis=-1)
return softmax_loss
def compute_loss(self, y_true, y_pred):
num_boxes = tf.to_float(tf.shape(y_true)[1])
# --------------------------------------------- #
# 分類的loss
# batch_size,8732,21 -> batch_size,8732
# --------------------------------------------- #
conf_loss = self._softmax_loss(y_true[:, :, 4:-1],
y_pred[:, :, 4:])
# --------------------------------------------- #
# 框的位置的loss
# batch_size,8732,4 -> batch_size,8732
# --------------------------------------------- #
loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
y_pred[:, :, :4])
# --------------------------------------------- #
# 獲取所有的正標簽的loss
# --------------------------------------------- #
pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -1],
axis=1)
pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -1],
axis=1)
# --------------------------------------------- #
# 每一張圖的正樣本的個數(shù)
# batch_size,
# --------------------------------------------- #
num_pos = tf.reduce_sum(y_true[:, :, -1], axis=-1)
# --------------------------------------------- #
# 每一張圖的負樣本的個數(shù)
# batch_size,
# --------------------------------------------- #
num_neg = tf.minimum(self.neg_pos_ratio * num_pos, num_boxes - num_pos)
# 找到了哪些值是大于0的
pos_num_neg_mask = tf.greater(num_neg, 0)
# --------------------------------------------- #
# 如果所有的圖,正樣本的數(shù)量均為0
# 那么則默認選取100個先驗框作為負樣本
# --------------------------------------------- #
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]])
# --------------------------------------------- #
# 從這里往后,與視頻中看到的代碼有些許不同。
# 由于以前的負樣本選取方式存在一些問題,
# 我對該部分代碼進行重構(gòu)。
# 求整個batch應(yīng)該的負樣本數(shù)量總和
# --------------------------------------------- #
num_neg_batch = tf.reduce_sum(tf.boolean_mask(num_neg, tf.greater(num_neg, 0)))
num_neg_batch = tf.to_int32(num_neg_batch)
# --------------------------------------------- #
# 對預(yù)測結(jié)果進行判斷,如果該先驗框沒有包含物體
# 那么它的不屬于背景的預(yù)測概率過大的話
# 就是難分類樣本
# --------------------------------------------- #
confs_start = 4 + self.background_label_id + 1
confs_end = confs_start + self.num_classes - 1
# --------------------------------------------- #
# batch_size,8732
# 把不是背景的概率求和,求和后的概率越大
# 代表越難分類。
# --------------------------------------------- #
max_confs = tf.reduce_sum(y_pred[:, :, confs_start:confs_end], axis=2)
# --------------------------------------------------- #
# 只有沒有包含物體的先驗框才得到保留
# 我們在整個batch里面選取最難分類的num_neg_batch個
# 先驗框作為負樣本。
# --------------------------------------------------- #
max_confs = tf.reshape(max_confs * (1 - y_true[:, :, -1]), [-1])
_, indices = tf.nn.top_k(max_confs, k=num_neg_batch)
neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]), indices)
# 進行歸一化
num_pos = tf.where(tf.not_equal(num_pos, 0), num_pos, tf.ones_like(num_pos))
total_loss = tf.reduce_sum(pos_conf_loss) + tf.reduce_sum(neg_conf_loss) + tf.reduce_sum(self.alpha * pos_loc_loss)
total_loss /= tf.reduce_sum(num_pos)
return total_loss
訓練自己的M2Det模型
首先前往Github下載對應(yīng)的倉庫,下載完后利用解壓軟件解壓,之后用編程軟件打開文件夾。
注意打開的根目錄必須正確,否則相對目錄不正確的情況下,代碼將無法運行。
一定要注意打開后的根目錄是文件存放的目錄。

一、數(shù)據(jù)集的準備
本文使用VOC格式進行訓練,訓練前需要自己制作好數(shù)據(jù)集,如果沒有自己的數(shù)據(jù)集,可以通過Github連接下載VOC12+07的數(shù)據(jù)集嘗試下。訓練前將標簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。

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

此時數(shù)據(jù)集的擺放已經(jīng)結(jié)束。
二、數(shù)據(jù)集的處理
在完成數(shù)據(jù)集的擺放之后,我們需要對數(shù)據(jù)集進行下一步的處理,目的是獲得訓練用的2007_train.txt以及2007_val.txt,需要用到根目錄下的voc_annotation.py。
voc_annotation.py里面有一些參數(shù)需要設(shè)置。
分別是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次訓練可以僅修改classes_path
''' annotation_mode用于指定該文件運行時計算的內(nèi)容 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的目標信息 與訓練和預(yù)測所用的classes_path一致即可 如果生成的2007_train.txt里面沒有目標信息 那么就是因為classes沒有設(shè)定正確 僅在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數(shù)據(jù)集所在的文件夾 默認指向根目錄下的VOC數(shù)據(jù)集 ''' VOCdevkit_path = 'VOCdevkit'
classes_path用于指向檢測類別所對應(yīng)的txt,以voc數(shù)據(jù)集為例,我們用的txt為:

訓練自己的數(shù)據(jù)集時,可以自己建立一個cls_classes.txt,里面寫自己所需要區(qū)分的類別。
三、開始網(wǎng)絡(luò)訓練
通過voc_annotation.py我們已經(jīng)生成了2007_train.txt以及2007_val.txt,此時我們可以開始訓練了。
訓練的參數(shù)較多,大家可以在下載庫后仔細看注釋,其中最重要的部分依然是train.py里的classes_path。
classes_path用于指向檢測類別所對應(yīng)的txt,這個txt和voc_annotation.py里面的txt一樣!訓練自己的數(shù)據(jù)集必須要修改!

修改完classes_path后就可以運行train.py開始訓練了,在訓練多個epoch后,權(quán)值會生成在logs文件夾中。
其它參數(shù)的作用如下:
#--------------------------------------------------------# # 訓練前一定要修改classes_path,使其對應(yīng)自己的數(shù)據(jù)集 #--------------------------------------------------------# classes_path = 'model_data/voc_classes.txt' #----------------------------------------------------------------------------------------------------------------------------# # 權(quán)值文件請看README,百度網(wǎng)盤下載。數(shù)據(jù)的預(yù)訓練權(quán)重對不同數(shù)據(jù)集是通用的,因為特征是通用的。 # 預(yù)訓練權(quán)重對于99%的情況都必須要用,不用的話權(quán)值太過隨機,特征提取效果不明顯,網(wǎng)絡(luò)訓練的結(jié)果也不會好。 # 訓練自己的數(shù)據(jù)集時提示維度不匹配正常,預(yù)測的東西都不一樣了自然維度不匹配 # # 如果想要斷點續(xù)練就將model_path設(shè)置成logs文件夾下已經(jīng)訓練的權(quán)值文件。 # 當model_path = ''的時候不加載整個模型的權(quán)值。 # # 此處使用的是整個模型的權(quán)重,因此是在train.py進行加載的。 # 如果想要讓模型從主干的預(yù)訓練權(quán)值開始訓練,則設(shè)置model_path為主干網(wǎng)絡(luò)的權(quán)值,此時僅加載主干。 # 如果想要讓模型從0開始訓練,則設(shè)置model_path = '',F(xiàn)reeze_Train = Fasle,此時從0開始訓練,且沒有凍結(jié)主干的過程。 # 一般來講,從0開始訓練效果會很差,因為權(quán)值太過隨機,特征提取效果不明顯。 #----------------------------------------------------------------------------------------------------------------------------# model_path = 'model_data/M2det_weights.h5' #------------------------------------------------------# # 輸入的shape大小,32的倍數(shù) #------------------------------------------------------# input_shape = [320, 320] #----------------------------------------------------# # 可用于設(shè)定先驗框的大小,默認的anchors_size # 是根據(jù)voc數(shù)據(jù)集設(shè)定的,大多數(shù)情況下都是通用的! # 如果想要檢測小物體,可以修改anchors_size # 一般調(diào)小淺層先驗框的大小就行了!因為淺層負責小物體檢測! # 比如anchors_size = [21, 45, 99, 153, 207, 261, 315] #----------------------------------------------------# anchors_size = [26, 48, 106, 163, 221, 278, 336] #----------------------------------------------------# # 訓練分為兩個階段,分別是凍結(jié)階段和解凍階段。 # 顯存不足與數(shù)據(jù)集大小無關(guān),提示顯存不足請調(diào)小batch_size。 # 受到BatchNorm層影響,batch_size最小為2,不能為1。 #----------------------------------------------------# #----------------------------------------------------# # 凍結(jié)階段訓練參數(shù) # 此時模型的主干被凍結(jié)了,特征提取網(wǎng)絡(luò)不發(fā)生改變 # 占用的顯存較小,僅對網(wǎng)絡(luò)進行微調(diào) #----------------------------------------------------# Init_Epoch = 0 Freeze_Epoch = 50 Freeze_batch_size = 8 Freeze_lr = 5e-4 #----------------------------------------------------# # 解凍階段訓練參數(shù) # 此時模型的主干不被凍結(jié)了,特征提取網(wǎng)絡(luò)會發(fā)生改變 # 占用的顯存較大,網(wǎng)絡(luò)所有的參數(shù)都會發(fā)生改變 #----------------------------------------------------# UnFreeze_Epoch = 100 Unfreeze_batch_size = 4 Unfreeze_lr = 1e-4 #------------------------------------------------------# # 是否進行凍結(jié)訓練,默認先凍結(jié)主干訓練后解凍訓練。 #------------------------------------------------------# Freeze_Train = True #------------------------------------------------------# # 用于設(shè)置是否使用多線程讀取數(shù)據(jù),0代表關(guān)閉多線程 # 開啟后會加快數(shù)據(jù)讀取速度,但是會占用更多內(nèi)存 # keras里開啟多線程有些時候速度反而慢了許多 # 在IO為瓶頸的時候再開啟多線程,即GPU運算速度遠大于讀取圖片的速度。 #------------------------------------------------------# num_workers = 0 #----------------------------------------------------# # 獲得圖片路徑和標簽 #----------------------------------------------------# train_annotation_path = '2007_train.txt' val_annotation_path = '2007_val.txt'
四、訓練結(jié)果預(yù)測
訓練結(jié)果預(yù)測需要用到兩個文件,分別是yolo.py和predict.py。
我們首先需要去yolo.py里面修改model_path以及classes_path,這兩個參數(shù)必須要修改。
model_path指向訓練好的權(quán)值文件,在logs文件夾里。
classes_path指向檢測類別所對應(yīng)的txt。

完成修改后就可以運行predict.py進行檢測了。運行后輸入圖片路徑即可檢測。
以上就是Keras搭建M2Det目標檢測平臺示例的詳細內(nèi)容,更多關(guān)于Keras M2Det目標檢測的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python redis 批量設(shè)置過期key過程解析
這篇文章主要介紹了python redis 批量設(shè)置過期key過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-11-11

