睿智的目標檢測24—Keras搭建Mobilenet-SSD目標檢測平臺
更新說明
有小夥伴聯繫我說,我實現的mobilenet-ssd並不是原版的mobilenet-ssd,於是我去查了資料,發現還真不是,又重新制作了原版結構的mobilenet-ssd,主要是修改了特徵層的shape,分別利用19x19,10x10,5x5,3x3,2x2,1x1的有效特徵層進行分類預測與迴歸預測。github地址如下:
https://github.com/bubbliiiing/Mobilenet-SSD-Essay
學習前言
一起來看看Mobilenet-SSD的keras實現吧,順便訓練一下自己的數據。
什麼是SSD目標檢測算法
SSD是一種非常優秀的one-stage目標檢測方法,one-stage算法就是目標檢測和分類是同時完成的,其主要思路是利用CNN提取特徵後,均勻地在圖片的不同位置進行密集抽樣,抽樣時可以採用不同尺度和長寬比,物體分類與預測框的迴歸同時進行,整個過程只需要一步,所以其優勢是速度快。
但是均勻的密集採樣的一個重要缺點是訓練比較困難,這主要是因爲正樣本與負樣本(背景)極其不均衡(參見Focal Loss),導致模型準確度稍低。
SSD的英文全名是Single Shot MultiBox Detector,Single shot說明SSD算法屬於one-stage方法,MultiBox說明SSD算法基於多框預測。
源碼下載
https://github.com/bubbliiiing/mobilenet-ssd-keras
喜歡的可以點個star噢。
另外實現的原版結構的SSD的github地址如下:
https://github.com/bubbliiiing/Mobilenet-SSD-Essay
SSD實現思路
一、預測部分
1、主幹網絡介紹
上圖的SSD採用的主幹網絡是VGG網絡,我們需要將其替換成Mobilenet網絡。Mobilenet網絡的結構可以在這裏瞭解,https://blog.csdn.net/weixin_44791964/article/details/102819915
需要注意兩個部分:
1、Conv4-3是長寬壓縮三次的結果,因此我們取mobilenet長寬壓縮三次的特徵層替代Conv4-3。
2、fc7是長寬壓縮四次的結果,因此我們取mobilenet長寬壓縮四次的特徵層替代fc7。(在SSD中,其將VGG的Conv5的池化層的步長修改爲1,因此本文也將mobilenet的Block5修改成了步長爲1。)
後面的Conv6,Conv7,Conv8,Conv9不變。
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,Add,ZeroPadding2D
from keras.layers import GlobalAveragePooling2D,DepthwiseConv2D,BatchNormalization
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 _depthwise_conv_block(inputs, pointwise_conv_filters,
depth_multiplier=1, strides=(1, 1), block_id=1):
x = DepthwiseConv2D((3, 3),
padding='same',
depth_multiplier=1,
strides=strides,
use_bias=False,
name='conv_dw_%d' % block_id)(inputs)
x = BatchNormalization(name='conv_dw_%d_bn' % block_id)(x)
x = Activation(relu6, name='conv_dw_%d_relu' % block_id)(x)
x = Conv2D(pointwise_conv_filters, (1, 1),
padding='same',
use_bias=False,
strides=(1, 1),
name='conv_pw_%d' % block_id)(x)
x = BatchNormalization(name='conv_pw_%d_bn' % block_id)(x)
return Activation(relu6, name='conv_pw_%d_relu' % block_id)(x)
def relu6(x):
return K.relu(x, max_value=6)
def mobilenet(input_tensor):
#----------------------------主幹特徵提取網絡開始---------------------------#
# SSD結構,net字典
net = {}
# Block 1
x = input_tensor
# 300,300,3 -> 150,150,64
x = Conv2D(32, (3,3),
padding='same',
use_bias=False,
strides=(2, 2),
name='conv1')(input_tensor)
x = BatchNormalization(name='conv1_bn')(x)
x = Activation(relu6, name='conv1_relu')(x)
x = _depthwise_conv_block(x, 64, 1, block_id=1)
# 150,150,64 -> 75,75,128
x = _depthwise_conv_block(x, 128, 1,
strides=(2, 2), block_id=2)
x = _depthwise_conv_block(x, 128, 1, block_id=3)
# Block 3
# 75,75,128 -> 38,38,256
x = _depthwise_conv_block(x, 256, 1,
strides=(2, 2), block_id=4)
x = _depthwise_conv_block(x, 256, 1, block_id=5)
net['conv4_3'] = x
# Block 4
# 38,38,256 -> 19,19,512
x = _depthwise_conv_block(x, 512, 1,
strides=(2, 2), block_id=6)
x = _depthwise_conv_block(x, 512, 1, block_id=7)
x = _depthwise_conv_block(x, 512, 1, block_id=8)
x = _depthwise_conv_block(x, 512, 1, block_id=9)
x = _depthwise_conv_block(x, 512, 1, block_id=10)
x = _depthwise_conv_block(x, 512, 1, block_id=11)
# Block 5
# 19,19,512 -> 19,19,1024
x = _depthwise_conv_block(x, 1024, 1,
strides=(2, 2), block_id=12)
x = _depthwise_conv_block(x, 1024, 1, block_id=13)
net['fc7'] = x
# 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
2、從特徵獲取預測結果
由上圖我們可以知道,我們分別取conv4_3的特徵、fc7的特徵、conv6的第二次卷積的特徵、conv7的第二次卷積的特徵、conv8的第二次卷積的特徵、conv9的第二次卷積的特徵,爲了和普通特徵層區分,我們稱之爲有效特徵層,來獲取預測結果。
對獲取到的每一個有效特徵層,我們分別對其進行一次num_priors x 4的卷積、一次num_priors x num_classes的卷積、並需要計算每一個有效特徵層對應的先驗框。而num_priors指的是該特徵層所擁有的先驗框數量。
其中:
num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。(爲什麼說是變化情況呢,這是因爲ssd的預測結果需要結合先驗框獲得預測框,預測結果就是先驗框的變化情況。)
num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。
每一個有效特徵層對應的先驗框對應着該特徵層上 每一個網格點上 預先設定好的多個框。
所有的特徵層對應的預測結果的shape如下:
實現代碼爲:
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
3、預測結果的解碼
我們通過對每一個特徵層的處理,可以獲得三個內容,分別是:
num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。**
num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。
每一個有效特徵層對應的先驗框對應着該特徵層上 每一個網格點上 預先設定好的多個框。
我們利用 num_priors x 4的卷積 與 每一個有效特徵層對應的先驗框 獲得框的真實位置。
每一個有效特徵層對應的先驗框就是,如圖所示的作用:
每一個有效特徵層將整個圖片分成與其長寬對應的網格,如conv4-3的特徵層就是將整個圖像分成38x38個網格;然後從每個網格中心建立多個先驗框,如conv4-3的特徵層就是建立了4個先驗框;對於conv4-3的特徵層來講,整個圖片被分成38x38個網格,每個網格中心對應4個先驗框,一共包含了,38x38x4個,5776個先驗框。
先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調整,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
4、在原圖上進行繪製
通過第三步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪製在圖片上,就可以獲得結果了。
二、訓練部分
1、真實框的處理
從預測部分我們知道,每個特徵層的預測結果,num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。
也就是說,我們直接利用ssd網絡預測到的結果,並不是預測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓練的時候,我們需要計算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我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的。
實現代碼如下:
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
2、利用處理完的真實框與對應圖片的預測結果計算loss
loss的計算分爲三個部分:
1、獲取所有正標籤的框的預測結果的迴歸loss。
2、獲取所有正標籤的種類的預測結果的交叉熵loss。
3、獲取一定負標籤的種類的預測結果的交叉熵loss。
由於在ssd的訓練過程中,正負樣本極其不平衡,即 存在對應真實框的先驗框可能只有2~3個,但是不存在對應真實框的負樣本卻有幾千個,這就會導致負樣本的loss值極大,因此我們可以考慮減少負樣本的選取,對於ssd的訓練來講,常見的情況是取三倍正樣本數量的負樣本用於訓練。這個三倍呢,也可以修改,調整成自己喜歡的數字。
實現代碼如下:
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模型
ssd整體的文件夾構架如下:
本文使用VOC格式進行訓練。
訓練前將標籤文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在訓練前利用voc2ssd.py文件生成對應的txt。
再運行根目錄下的voc_annotation.py,運行前需要將classes改成你自己的classes。
classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
就會生成對應的2007_train.txt,每一行對應其圖片位置及其真實框的位置。
在訓練前需要修改model_data裏面的voc_classes.txt文件,需要將classes改成你自己的classes。
運行train.py即可開始訓練。