Keras搭建M2Det目標(biāo)檢測平臺示例
什么是M2det目標(biāo)檢測算法
一起來看看M2det的keras實現(xiàn)吧,順便訓(xù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ò)作為主干提取,對目標(biāo)檢測任務(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 = {} #------------------------# # 輸入默認(rèn)為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、細(xì)化U型模塊TUM
Tum的結(jié)構(gòu)具體如下:
當(dāng)我們給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層當(dāng)中 # 輸入為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ù)測框的位置了。
當(dāng)然得到最終的預(yù)測結(jié)構(gòu)后還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標(biāo)檢測通用的部分。
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é)果了。
二、訓(xùn)練部分
1、真實框的處理
從預(yù)測部分我們知道,每個特征層的預(yù)測結(jié)果,num_anchors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。
也就是說,我們直接利用M2DET網(wǎng)絡(luò)預(yù)測到的結(jié)果,并不是預(yù)測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓(xùn)練的時候,我們需要計算loss函數(shù),這個loss函數(shù)是相對于M2DET網(wǎng)絡(luò)的預(yù)測結(jié)果的。我們需要把圖片輸入到當(dāng)前的M2DET網(wǎng)絡(luò)中,得到預(yù)測結(jié)果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉(zhuǎn)化為M2DET預(yù)測結(jié)果的格式信息。
也就是,我們需要找到 每一張用于訓(xùn)練的圖片的每一個真實框?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]): #---------------------------------------------# # 計算當(dāng)前真實框和先驗框的重合情況 # iou [self.num_anchors] # encoded_box [self.num_anchors, 5] #---------------------------------------------# iou = self.iou(box) encoded_box = np.zeros((self.num_anchors, 4 + return_iou)) #---------------------------------------------# # 找到每一個真實框,重合程度較高的先驗框 # 真實框可以由這個先驗框來負(fù)責(zé)預(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ù),默認(rèn)為[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é)果。
在訓(xùn)練的時候我們只需要選擇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)的種類,默認(rèn)為背景 # -1 的內(nèi)容為當(dāng)前先驗框是否包含目標(biāo) #---------------------------------------------------# 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、獲取所有正標(biāo)簽的框的預(yù)測結(jié)果的回歸loss。
2、獲取所有正標(biāo)簽的種類的預(yù)測結(jié)果的交叉熵loss。
3、獲取一定負(fù)標(biāo)簽的種類的預(yù)測結(jié)果的交叉熵loss。
由于在M2DET的訓(xùn)練過程中,正負(fù)樣本極其不平衡,即 存在對應(yīng)真實框的先驗框可能只有十來個,但是不存在對應(yīng)真實框的負(fù)樣本卻有幾千個,這就會導(dǎo)致負(fù)樣本的loss值極大,因此我們可以考慮減少負(fù)樣本的選取,對于M2DET的訓(xùn)練來講,常見的情況是取三倍正樣本數(shù)量的負(fù)樣本用于訓(xùn)練。這個三倍呢,也可以修改,調(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]) # --------------------------------------------- # # 獲取所有的正標(biāo)簽的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) # --------------------------------------------- # # 每一張圖的負(fù)樣本的個數(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 # 那么則默認(rèn)選取100個先驗框作為負(fù)樣本 # --------------------------------------------- # 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]]) # --------------------------------------------- # # 從這里往后,與視頻中看到的代碼有些許不同。 # 由于以前的負(fù)樣本選取方式存在一些問題, # 我對該部分代碼進行重構(gòu)。 # 求整個batch應(yīng)該的負(fù)樣本數(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個 # 先驗框作為負(fù)樣本。 # --------------------------------------------------- # 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
訓(xùn)練自己的M2Det模型
首先前往Github下載對應(yīng)的倉庫,下載完后利用解壓軟件解壓,之后用編程軟件打開文件夾。
注意打開的根目錄必須正確,否則相對目錄不正確的情況下,代碼將無法運行。
一定要注意打開后的根目錄是文件存放的目錄。
一、數(shù)據(jù)集的準(zhǔn)備
本文使用VOC格式進行訓(xùn)練,訓(xùn)練前需要自己制作好數(shù)據(jù)集,如果沒有自己的數(shù)據(jù)集,可以通過Github連接下載VOC12+07的數(shù)據(jù)集嘗試下。訓(xùn)練前將標(biāo)簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
訓(xùn)練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
此時數(shù)據(jù)集的擺放已經(jīng)結(jié)束。
二、數(shù)據(jù)集的處理
在完成數(shù)據(jù)集的擺放之后,我們需要對數(shù)據(jù)集進行下一步的處理,目的是獲得訓(xùn)練用的2007_train.txt以及2007_val.txt,需要用到根目錄下的voc_annotation.py。
voc_annotation.py里面有一些參數(shù)需要設(shè)置。
分別是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次訓(xùn)練可以僅修改classes_path
''' annotation_mode用于指定該文件運行時計算的內(nèi)容 annotation_mode為0代表整個標(biāo)簽處理過程,包括獲得VOCdevkit/VOC2007/ImageSets里面的txt以及訓(xùn)練用的2007_train.txt、2007_val.txt annotation_mode為1代表獲得VOCdevkit/VOC2007/ImageSets里面的txt annotation_mode為2代表獲得訓(xùn)練用的2007_train.txt、2007_val.txt ''' annotation_mode = 0 ''' 必須要修改,用于生成2007_train.txt、2007_val.txt的目標(biāo)信息 與訓(xùn)練和預(yù)測所用的classes_path一致即可 如果生成的2007_train.txt里面沒有目標(biāo)信息 那么就是因為classes沒有設(shè)定正確 僅在annotation_mode為0和2的時候有效 ''' classes_path = 'model_data/voc_classes.txt' ''' trainval_percent用于指定(訓(xùn)練集+驗證集)與測試集的比例,默認(rèn)情況下 (訓(xùn)練集+驗證集):測試集 = 9:1 train_percent用于指定(訓(xùn)練集+驗證集)中訓(xùn)練集與驗證集的比例,默認(rèn)情況下 訓(xùn)練集:驗證集 = 9:1 僅在annotation_mode為0和1的時候有效 ''' trainval_percent = 0.9 train_percent = 0.9 ''' 指向VOC數(shù)據(jù)集所在的文件夾 默認(rèn)指向根目錄下的VOC數(shù)據(jù)集 ''' VOCdevkit_path = 'VOCdevkit'
classes_path用于指向檢測類別所對應(yīng)的txt,以voc數(shù)據(jù)集為例,我們用的txt為:
訓(xùn)練自己的數(shù)據(jù)集時,可以自己建立一個cls_classes.txt,里面寫自己所需要區(qū)分的類別。
三、開始網(wǎng)絡(luò)訓(xùn)練
通過voc_annotation.py我們已經(jīng)生成了2007_train.txt以及2007_val.txt,此時我們可以開始訓(xùn)練了。
訓(xùn)練的參數(shù)較多,大家可以在下載庫后仔細(xì)看注釋,其中最重要的部分依然是train.py里的classes_path。
classes_path用于指向檢測類別所對應(yīng)的txt,這個txt和voc_annotation.py里面的txt一樣!訓(xùn)練自己的數(shù)據(jù)集必須要修改!
修改完classes_path后就可以運行train.py開始訓(xùn)練了,在訓(xùn)練多個epoch后,權(quán)值會生成在logs文件夾中。
其它參數(shù)的作用如下:
#--------------------------------------------------------# # 訓(xùn)練前一定要修改classes_path,使其對應(yīng)自己的數(shù)據(jù)集 #--------------------------------------------------------# classes_path = 'model_data/voc_classes.txt' #----------------------------------------------------------------------------------------------------------------------------# # 權(quán)值文件請看README,百度網(wǎng)盤下載。數(shù)據(jù)的預(yù)訓(xùn)練權(quán)重對不同數(shù)據(jù)集是通用的,因為特征是通用的。 # 預(yù)訓(xùn)練權(quán)重對于99%的情況都必須要用,不用的話權(quán)值太過隨機,特征提取效果不明顯,網(wǎng)絡(luò)訓(xùn)練的結(jié)果也不會好。 # 訓(xùn)練自己的數(shù)據(jù)集時提示維度不匹配正常,預(yù)測的東西都不一樣了自然維度不匹配 # # 如果想要斷點續(xù)練就將model_path設(shè)置成logs文件夾下已經(jīng)訓(xùn)練的權(quán)值文件。 # 當(dāng)model_path = ''的時候不加載整個模型的權(quán)值。 # # 此處使用的是整個模型的權(quán)重,因此是在train.py進行加載的。 # 如果想要讓模型從主干的預(yù)訓(xùn)練權(quán)值開始訓(xùn)練,則設(shè)置model_path為主干網(wǎng)絡(luò)的權(quán)值,此時僅加載主干。 # 如果想要讓模型從0開始訓(xùn)練,則設(shè)置model_path = '',F(xiàn)reeze_Train = Fasle,此時從0開始訓(xùn)練,且沒有凍結(jié)主干的過程。 # 一般來講,從0開始訓(xùn)練效果會很差,因為權(quán)值太過隨機,特征提取效果不明顯。 #----------------------------------------------------------------------------------------------------------------------------# model_path = 'model_data/M2det_weights.h5' #------------------------------------------------------# # 輸入的shape大小,32的倍數(shù) #------------------------------------------------------# input_shape = [320, 320] #----------------------------------------------------# # 可用于設(shè)定先驗框的大小,默認(rèn)的anchors_size # 是根據(jù)voc數(shù)據(jù)集設(shè)定的,大多數(shù)情況下都是通用的! # 如果想要檢測小物體,可以修改anchors_size # 一般調(diào)小淺層先驗框的大小就行了!因為淺層負(fù)責(zé)小物體檢測! # 比如anchors_size = [21, 45, 99, 153, 207, 261, 315] #----------------------------------------------------# anchors_size = [26, 48, 106, 163, 221, 278, 336] #----------------------------------------------------# # 訓(xùn)練分為兩個階段,分別是凍結(jié)階段和解凍階段。 # 顯存不足與數(shù)據(jù)集大小無關(guān),提示顯存不足請調(diào)小batch_size。 # 受到BatchNorm層影響,batch_size最小為2,不能為1。 #----------------------------------------------------# #----------------------------------------------------# # 凍結(jié)階段訓(xùn)練參數(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 #----------------------------------------------------# # 解凍階段訓(xùn)練參數(shù) # 此時模型的主干不被凍結(jié)了,特征提取網(wǎng)絡(luò)會發(fā)生改變 # 占用的顯存較大,網(wǎng)絡(luò)所有的參數(shù)都會發(fā)生改變 #----------------------------------------------------# UnFreeze_Epoch = 100 Unfreeze_batch_size = 4 Unfreeze_lr = 1e-4 #------------------------------------------------------# # 是否進行凍結(jié)訓(xùn)練,默認(rèn)先凍結(jié)主干訓(xùn)練后解凍訓(xùn)練。 #------------------------------------------------------# Freeze_Train = True #------------------------------------------------------# # 用于設(shè)置是否使用多線程讀取數(shù)據(jù),0代表關(guān)閉多線程 # 開啟后會加快數(shù)據(jù)讀取速度,但是會占用更多內(nèi)存 # keras里開啟多線程有些時候速度反而慢了許多 # 在IO為瓶頸的時候再開啟多線程,即GPU運算速度遠(yuǎn)大于讀取圖片的速度。 #------------------------------------------------------# num_workers = 0 #----------------------------------------------------# # 獲得圖片路徑和標(biāo)簽 #----------------------------------------------------# train_annotation_path = '2007_train.txt' val_annotation_path = '2007_val.txt'
四、訓(xùn)練結(jié)果預(yù)測
訓(xùn)練結(jié)果預(yù)測需要用到兩個文件,分別是yolo.py和predict.py。
我們首先需要去yolo.py里面修改model_path以及classes_path,這兩個參數(shù)必須要修改。
model_path指向訓(xùn)練好的權(quán)值文件,在logs文件夾里。
classes_path指向檢測類別所對應(yīng)的txt。
完成修改后就可以運行predict.py進行檢測了。運行后輸入圖片路徑即可檢測。
以上就是Keras搭建M2Det目標(biāo)檢測平臺示例的詳細(xì)內(nèi)容,更多關(guān)于Keras M2Det目標(biāo)檢測的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python redis 批量設(shè)置過期key過程解析
這篇文章主要介紹了python redis 批量設(shè)置過期key過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11一起來學(xué)習(xí)一下python的數(shù)據(jù)類型
這篇文章主要為大家詳細(xì)介紹了python的數(shù)據(jù)類型,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下希望能夠給你帶來幫助2022-01-01