睿智的目標檢測17——Keras搭建Retinanet目標檢測平臺
學習前言
一起來看看Retinanet的keras實現吧,順便訓練一下自己的數據。
什麼是Retinanet目標檢測算法
Retinanet是在何凱明大神提出Focal loss同時提出的一種新的目標檢測方案,來驗證Focal Loss的有效性。
One-Stage目標檢測方法常常使用先驗框提高預測性能,一張圖像可能生成成千上萬的候選框,但是其中只有很少一部分是包含目標的的,有目標的就是正樣本,沒有目標的就是負樣本。這種情況造成了One-Stage目標檢測方法的正負樣本不平衡,也使得One-Stage目標檢測方法的檢測效果比不上Two-Stage目標檢測方法。
Focal Loss是一種新的用於平衡One-Stage目標檢測方法正負樣本的Loss方案。
Retinane的結構非常簡單,但是其存在非常多的先驗框,以輸入600x600x3的圖片爲例,就存在着67995個先驗框,這些先驗框裏面大多包含的是背景,存在非常多的負樣本。以Focal Loss訓練的Retinanet可以有效的平衡正負樣本,實現有效的訓練。
源碼下載
https://github.com/bubbliiiing/retinanet-keras
喜歡的可以點個star噢。
Retinanet實現思路
一、預測部分
1、主幹網絡介紹
Retinanet採用的主幹網絡是Resnet網絡,關於Resnet的介紹大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102790260。
本例子假設輸入的圖片大小爲600x600x3。
ResNet50有兩個基本的塊,分別名爲Conv Block和Identity Block,其中Conv Block輸入和輸出的維度是不一樣的,所以不能連續串聯,它的作用是改變網絡的維度;Identity Block輸入維度和輸出維度相同,可以串聯,用於加深網絡的。
Conv Block的結構如下:
Identity Block的結構如下:
這兩個都是殘差網絡結構。
當輸入的圖片爲600x600x3的時候,shape變化與總的網絡結構如下:
我們取出長寬壓縮了三次、四次、五次的結果來進行網絡金字塔結構的構造。
實現代碼:
#-------------------------------------------------------------#
# ResNet50的網絡部分
#-------------------------------------------------------------#
from __future__ import print_function
import numpy as np
from keras import layers
from keras.layers import Input
from keras.layers import Dense,Conv2D,MaxPooling2D,ZeroPadding2D,AveragePooling2D
from keras.layers import Activation,BatchNormalization,Flatten
from keras.models import Model
from keras.preprocessing import image
import keras.backend as K
from keras.utils.data_utils import get_file
from keras.applications.imagenet_utils import decode_predictions
from keras.applications.imagenet_utils import preprocess_input
def identity_block(input_tensor, kernel_size, filters, stage, block):
filters1, filters2, filters3 = filters
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a',use_bias=False)(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b',use_bias=False)(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c',use_bias=False)(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
x = layers.add([x, input_tensor])
x = Activation('relu')(x)
return x
def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):
filters1, filters2, filters3 = filters
conv_name_base = 'res' + str(stage) + block + '_branch'
bn_name_base = 'bn' + str(stage) + block + '_branch'
x = Conv2D(filters1, (1, 1), strides=strides,
name=conv_name_base + '2a',use_bias=False)(input_tensor)
x = BatchNormalization(name=bn_name_base + '2a')(x)
x = Activation('relu')(x)
x = Conv2D(filters2, kernel_size, padding='same',
name=conv_name_base + '2b',use_bias=False)(x)
x = BatchNormalization(name=bn_name_base + '2b')(x)
x = Activation('relu')(x)
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c',use_bias=False)(x)
x = BatchNormalization(name=bn_name_base + '2c')(x)
shortcut = Conv2D(filters3, (1, 1), strides=strides,
name=conv_name_base + '1',use_bias=False)(input_tensor)
shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
x = layers.add([x, shortcut])
x = Activation('relu')(x)
return x
def ResNet50(inputs):
img_input = inputs
x = ZeroPadding2D((3, 3))(img_input)
x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1',use_bias=False)(x)
x = BatchNormalization(name='bn_conv1')(x)
x = Activation('relu')(x)
x = MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)
x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')
y0 = x
x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')
y1 = x
x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')
y2 = x
x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b')
x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c')
y3 = x
model = Model(img_input, [y0,y1,y2,y3], name='resnet50')
return model
2、從特徵獲取預測結果
由抽象的結構圖可知,獲得到的特徵還需要經過圖像金字塔的處理,這樣的結構可以融合多尺度的特徵,實現更有效的預測。
圖像金字塔的具體結構如下:
通過圖像金字塔我們可以獲得五個有效的特徵層,分別是P3、P4、P5、P6、P7,
爲了和普通特徵層區分,我們稱之爲有效特徵層,將這五個有效的特徵層傳輸過class+box subnets就可以獲得預測結果了。
class subnet採用4次256通道的卷積和1次num_priors x num_classes的卷積,num_priors指的是該特徵層所擁有的先驗框數量,num_classes指的是網絡一共對多少類的目標進行檢測。
box subnet採用4次256通道的卷積和1次num_priors x 4的卷積,num_priors指的是該特徵層所擁有的先驗框數量,4指的是先驗框的調整情況。
需要注意的是,每個特徵層所用的class subnet是同一個class subnet;每個特徵層所用的box subnet是同一個box subnet。
其中:
num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。(爲什麼說是變化情況呢,這是因爲ssd的預測結果需要結合先驗框獲得預測框,預測結果就是先驗框的變化情況。)
num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。
實現代碼爲:
import keras
import keras.layers
from nets.resnet import ResNet50
from utils.anchors import AnchorParameters
from utils.utils import PriorProbability
from nets import layers
def make_last_layer_loc(num_classes,num_anchors,pyramid_feature_size=256):
inputs = keras.layers.Input(shape=(None, None, pyramid_feature_size))
options = {
'kernel_size' : 3,
'strides' : 1,
'padding' : 'same',
'kernel_initializer' : keras.initializers.normal(mean=0.0, stddev=0.01, seed=None),
'bias_initializer' : 'zeros'
}
outputs = inputs
for i in range(4):
outputs = keras.layers.Conv2D(filters=256,activation='relu',name='pyramid_regression_{}'.format(i),**options)(outputs)
outputs = keras.layers.Conv2D(num_anchors * 4, name='pyramid_regression', **options)(outputs)
regression = keras.layers.Reshape((-1, 4), name='pyramid_regression_reshape')(outputs)
regression_model = keras.models.Model(inputs=inputs, outputs=regression, name="regression_submodel")
return regression_model
def make_last_layer_cls(num_classes,num_anchors,pyramid_feature_size=256):
inputs = keras.layers.Input(shape=(None, None, pyramid_feature_size))
options = {
'kernel_size' : 3,
'strides' : 1,
'padding' : 'same',
}
classification = []
outputs = inputs
for i in range(4):
outputs = keras.layers.Conv2D(
filters=256,
activation='relu',
name='pyramid_classification_{}'.format(i),
kernel_initializer=keras.initializers.normal(mean=0.0, stddev=0.01, seed=None),
bias_initializer='zeros',
**options
)(outputs)
outputs = keras.layers.Conv2D(filters=num_classes * num_anchors,
kernel_initializer=keras.initializers.normal(mean=0.0, stddev=0.01, seed=None),
bias_initializer=PriorProbability(probability=0.01),
name='pyramid_classification'.format(),
**options
)(outputs)
outputs = keras.layers.Reshape((-1, num_classes), name='pyramid_classification_reshape')(outputs)
classification = keras.layers.Activation('sigmoid', name='pyramid_classification_sigmoid')(outputs)
classification_model = keras.models.Model(inputs=inputs, outputs=classification, name="classification_submodel")
return classification_model
def resnet_retinanet(num_classes, inputs=None, num_anchors=None, submodels=None, name='retinanet'):
if inputs==None:
inputs = keras.layers.Input(shape=(600, 600, 3))
else:
inputs = inputs
resnet = ResNet50(inputs)
if num_anchors is None:
num_anchors = AnchorParameters.default.num_anchors()
C3, C4, C5 = resnet.outputs[1:]
P5 = keras.layers.Conv2D(256, kernel_size=1, strides=1, padding='same', name='C5_reduced')(C5)
P5_upsampled = layers.UpsampleLike(name='P5_upsampled')([P5, C4])
P5 = keras.layers.Conv2D(256, kernel_size=3, strides=1, padding='same', name='P5')(P5)
P4 = keras.layers.Conv2D(256, kernel_size=1, strides=1, padding='same', name='C4_reduced')(C4)
P4 = keras.layers.Add(name='P4_merged')([P5_upsampled, P4])
P4_upsampled = layers.UpsampleLike(name='P4_upsampled')([P4, C3])
P4 = keras.layers.Conv2D(256, kernel_size=3, strides=1, padding='same', name='P4')(P4)
P3 = keras.layers.Conv2D(256, kernel_size=1, strides=1, padding='same', name='C3_reduced')(C3)
P3 = keras.layers.Add(name='P3_merged')([P4_upsampled, P3])
P3 = keras.layers.Conv2D(256, kernel_size=3, strides=1, padding='same', name='P3')(P3)
P6 = keras.layers.Conv2D(256, kernel_size=3, strides=2, padding='same', name='P6')(C5)
P7 = keras.layers.Activation('relu', name='C6_relu')(P6)
P7 = keras.layers.Conv2D(256, kernel_size=3, strides=2, padding='same', name='P7')(P7)
features = [P3, P4, P5, P6, P7]
regression_model = make_last_layer_loc(num_classes,num_anchors)
classification_model = make_last_layer_cls(num_classes,num_anchors)
regressions = []
classifications = []
for feature in features:
regression = regression_model(feature)
classification = classification_model(feature)
regressions.append(regression)
classifications.append(classification)
regressions = keras.layers.Concatenate(axis=1, name="regression")(regressions)
classifications = keras.layers.Concatenate(axis=1, name="classification")(classifications)
pyramids = [regressions,classifications]
model = keras.models.Model(inputs=inputs, outputs=pyramids, name=name)
return model
3、預測結果的解碼
我們通過對每一個特徵層的處理,可以獲得三個內容,分別是:
num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。**
num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。
每一個有效特徵層對應的先驗框對應着該特徵層上 每一個網格點上 預先設定好的9個框。
我們利用 num_priors x 4的卷積 與 每一個有效特徵層對應的先驗框 獲得框的真實位置。
每一個有效特徵層對應的先驗框就是,如圖所示的作用:
每一個有效特徵層將整個圖片分成與其長寬對應的網格,如P3的特徵層就是將整個圖像分成75x75個網格;然後從每個網格中心建立9個先驗框,一共75x75x9個,50625個先驗框。
先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調整,Retinanet利用4次256通道的卷積+num_priors x 4的卷積的結果對先驗框進行調整。
num_priors x 4中的num_priors表示了這個網格點所包含的先驗框數量,其中的4表示了框的左上角xy軸,右下角xy的調整情況。
Retinanet解碼過程就是將對應的先驗框的左上角和右下角進行位置的調整,調整完的結果就是預測框的位置了。
當然得到最終的預測結構後還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
1、取出每一類得分大於confidence_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。
實現代碼如下:
def decode_boxes(self, mbox_loc, mbox_priorbox):
# 獲得先驗框的寬與高
prior_width = mbox_priorbox[:, 2] - mbox_priorbox[:, 0]
prior_height = mbox_priorbox[:, 3] - mbox_priorbox[:, 1]
# 獲取真實框的左上角與右下角
decode_bbox_xmin = mbox_loc[:,0]*prior_width*0.2 + mbox_priorbox[:, 0]
decode_bbox_ymin = mbox_loc[:,1]*prior_height*0.2 + mbox_priorbox[:, 1]
decode_bbox_xmax = mbox_loc[:,2]*prior_width*0.2 + mbox_priorbox[:, 2]
decode_bbox_ymax = mbox_loc[:,3]*prior_height*0.2 + mbox_priorbox[:, 3]
# 真實框的左上角與右下角進行堆疊
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, mbox_priorbox, keep_top_k=200,
confidence_threshold=0.4):
# 網絡預測的結果
mbox_loc = predictions[0]
# 先驗框
mbox_priorbox = mbox_priorbox
# 置信度
mbox_conf = predictions[1]
results = []
# 對每一個圖片進行處理
for i in range(len(mbox_loc)):
results.append([])
decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox)
for c in range(self.num_classes):
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]
# 獲得,在所有預測結果裏面,置信度比較高的框
# 還有,利用先驗框和Retinanet的預測結果,處理獲得了真實框(預測框)的位置
return results
4、在原圖上進行繪製
通過第三步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪製在圖片上,就可以獲得結果了。
二、訓練部分
1、真實框的處理
從預測部分我們知道,每個特徵層的預測結果,num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。
也就是說,我們直接利用retinanet網絡預測到的結果,並不是預測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓練的時候,我們需要計算loss函數,這個loss函數是相對於Retinanet網絡的預測結果的。我們需要把圖片輸入到當前的Retinanet網絡中,得到預測結果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉化爲Retinanet預測結果的格式信息。
也就是,我們需要找到 每一張用於訓練的圖片的每一個真實框對應的先驗框,並求出如果想要得到這樣一個真實框,我們的預測結果應該是怎麼樣的。
從預測結果獲得真實框的過程被稱作解碼,而從真實框獲得預測結果的過程就是編碼的過程。
因此我們只需要將解碼過程逆過來就是編碼過程了。
實現代碼如下:
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]
# 逆向編碼,將真實框轉化爲Retinanet預測結果的格式
assigned_priors_w = (assigned_priors[:, 2] -
assigned_priors[:, 0])
assigned_priors_h = (assigned_priors[:, 3] -
assigned_priors[:, 1])
encoded_box[:,0][assign_mask] = (box[0] - assigned_priors[:, 0])/assigned_priors_w/0.2
encoded_box[:,1][assign_mask] = (box[1] - assigned_priors[:, 1])/assigned_priors_h/0.2
encoded_box[:,2][assign_mask] = (box[2] - assigned_priors[:, 2])/assigned_priors_w/0.2
encoded_box[:,3][assign_mask] = (box[3] - assigned_priors[:, 3])/assigned_priors_h/0.2
return encoded_box.ravel()
利用上述代碼我們可以獲得,真實框對應的所有的iou較大先驗框,並計算了真實框對應的所有iou較大的先驗框應該有的預測結果。
但是由於原始圖片中可能存在多個真實框,可能同一個先驗框會與多個真實框重合度較高,我們只取其中與真實框重合度最高的就可以了。
因此我們還要經過一次篩選,將上述代碼獲得的真實框對應的所有的iou較大先驗框的預測結果中,iou最大的那個真實框篩選出來。
通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的。
實現代碼如下:
def assign_boxes(self, boxes):
assignment = np.zeros((self.num_priors, 4 + 1 + self.num_classes + 1))
assignment[:, 4] = 0.0
assignment[:, -1] = 0.0
if len(boxes) == 0:
return assignment
# 對每一個真實框都進行iou計算
ingored_boxes = np.apply_along_axis(self.ignore_box, 1, boxes[:, :4])
# 取重合程度最大的先驗框,並且獲取這個先驗框的index
ingored_boxes = ingored_boxes.reshape(-1, self.num_priors, 1)
# (num_priors)
ignore_iou = ingored_boxes[:, :, 0].max(axis=0)
# (num_priors)
ignore_iou_mask = ignore_iou > 0
assignment[:, 4][ignore_iou_mask] = -1
assignment[:, -1][ignore_iou_mask] = -1
# (n, num_priors, 5)
encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
# 每一個真實框的編碼後的值,和iou
# (n, num_priors)
encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
# 取重合程度最大的先驗框,並且獲取這個先驗框的index
# (num_priors)
best_iou = encoded_boxes[:, :, -1].max(axis=0)
# (num_priors)
best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
# (num_priors)
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] = 1
assignment[:, 5:-1][best_iou_mask] = boxes[best_iou_idx, 4:]
assignment[:, -1][best_iou_mask] = 1
# 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的
return assignment
focal會忽略一些重合度相對較高但是不是非常高的先驗框,一般將重合度在0.4-0.5之間的先驗框進行忽略。
實現代碼如下:
def ignore_box(self, box):
iou = self.iou(box)
ignored_box = np.zeros((self.num_priors, 1))
# 找到每一個真實框,重合程度較高的先驗框
assign_mask = (iou > self.ignore_threshold)&(iou<self.overlap_threshold)
if not assign_mask.any():
assign_mask[iou.argmax()] = True
ignored_box[:, 0][assign_mask] = iou[assign_mask]
return ignored_box.ravel()
2、利用處理完的真實框與對應圖片的預測結果計算loss
loss的計算分爲兩個部分:
1、Smooth Loss:獲取所有正標籤的框的預測結果的迴歸loss。
2、Focal Loss:獲取所有未被忽略的種類的預測結果的交叉熵loss。
由於在Retinanet的訓練過程中,正負樣本極其不平衡,即 存在對應真實框的先驗框可能只有若干個,但是不存在對應真實框的負樣本卻有上萬個,這就會導致負樣本的loss值極大,因此引入了Focal Loss進行正負樣本的平衡,關於Focal Loss的介紹可以看這個博客。
https://blog.csdn.net/weixin_44791964/article/details/102853782
實現代碼如下:
def focal(alpha=0.25, gamma=2.0):
def _focal(y_true, y_pred):
# y_true [batch_size, num_anchor, num_classes+1]
# y_pred [batch_size, num_anchor, num_classes]
labels = y_true[:, :, :-1]
anchor_state = y_true[:, :, -1] # -1 是需要忽略的, 0 是背景, 1 是存在目標
classification = y_pred
# 找出存在目標的先驗框
indices_for_object = tf.where(keras.backend.equal(anchor_state, 1))
labels_for_object = tf.gather_nd(labels, indices_for_object)
classification_for_object = tf.gather_nd(classification, indices_for_object)
# 計算每一個先驗框應該有的權重
alpha_factor_for_object = keras.backend.ones_like(labels_for_object) * alpha
focal_weight_for_object = 1 - classification_for_object
focal_weight_for_object = alpha_factor_for_object * focal_weight_for_object ** gamma
# 將權重乘上所求得的交叉熵
cls_loss_for_object = focal_weight_for_object * keras.backend.binary_crossentropy(labels_for_object, classification_for_object)
# 找出實際上爲背景的先驗框
indices_for_back = tf.where(keras.backend.equal(anchor_state, 0))
labels_for_back = tf.gather_nd(labels, indices_for_back)
classification_for_back = tf.gather_nd(classification, indices_for_back)
# 計算每一個先驗框應該有的權重
alpha_factor_for_back = keras.backend.ones_like(labels_for_back) * (1-alpha)
focal_weight_for_back = classification_for_back
focal_weight_for_back = alpha_factor_for_back * focal_weight_for_back ** gamma
# 將權重乘上所求得的交叉熵
cls_loss_for_back = focal_weight_for_back * keras.backend.binary_crossentropy(labels_for_back, classification_for_back)
# 標準化,實際上是正樣本的數量
normalizer = tf.where(keras.backend.equal(anchor_state, 1))
normalizer = keras.backend.cast(keras.backend.shape(normalizer)[0], keras.backend.floatx())
normalizer = keras.backend.maximum(keras.backend.cast_to_floatx(1.0), normalizer)
# 將所獲得的loss除上正樣本的數量
cls_loss_for_object = keras.backend.sum(cls_loss_for_object)/normalizer
cls_loss_for_back = keras.backend.sum(cls_loss_for_back)/normalizer
# 總的loss
loss = cls_loss_for_object + cls_loss_for_back
# loss = tf.Print(loss, [loss, cls_loss_for_object, cls_loss_for_back], message='\nloss: ')
return loss
return _focal
def smooth_l1(sigma=1.0):
sigma_squared = sigma ** 2
def _smooth_l1(y_true, y_pred):
# y_true [batch_size, num_anchor, 4+1]
# y_pred [batch_size, num_anchor, 4]
regression = y_pred
regression_target = y_true[:, :, :-1]
anchor_state = y_true[:, :, -1]
# 找到正樣本
indices = tf.where(keras.backend.equal(anchor_state, 1))
regression = tf.gather_nd(regression, indices)
regression_target = tf.gather_nd(regression_target, indices)
# 計算 smooth L1 loss
# f(x) = 0.5 * (sigma * x)^2 if |x| < 1 / sigma / sigma
# |x| - 0.5 / sigma / sigma otherwise
regression_diff = regression - regression_target
regression_diff = keras.backend.abs(regression_diff)
regression_loss = backend.where(
keras.backend.less(regression_diff, 1.0 / sigma_squared),
0.5 * sigma_squared * keras.backend.pow(regression_diff, 2),
regression_diff - 0.5 / sigma_squared
)
normalizer = keras.backend.maximum(1, keras.backend.shape(indices)[0])
normalizer = keras.backend.cast(normalizer, dtype=keras.backend.floatx())
loss = keras.backend.sum(regression_loss) / normalizer
return loss
return _smooth_l1
訓練自己的Retinanet模型
Retinanet整體的文件夾構架如下:
本文使用VOC格式進行訓練。
訓練前將標籤文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在訓練前利用voc2retinanet.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即可開始訓練。