Yolo-V4算法中對網絡進行了改進,使用CSPDarknet53。網絡結構如下:
Yolo-V4與Yolo-V3上相比較:
(1)對主干網絡進行了修改,將原先的Darknet53改為CSPDarknet53,其中是將激活函數改為Mish激活函數,並且在網絡中加入了CSP結構。
(2)對特征提取過程的加強,添加了SPP,PANet結構。
(3)在數據預處理階段加入Mosaic方法。
(4)在損失函數中做了改進使用了CIOU作為回歸Loss。
Mish激活函數:
Mish() = x×tanh(ln(1+ex)),使用Mish函數可以對負值有更好的梯度流,而不是像ReLU函數中那樣的全零。這樣平滑的激活函數允許更好的信息深入神經網絡,從而得到更好的准確信息。
CSPNet結構:
CSP是可以增強CNN學習能力的新型結構,CSPNet將底層的特征映射分為兩部分,一部分經過密集塊和過渡層,另一部分與傳輸的特征映射結合到下一階段。
from functools import wraps from keras import backend as K from keras.layers import Conv2D,Add,ZeroPadding2D,UpSampling2D,Concatenate,MaxPooling2D,Layer,Input from keras.layers.advanced_activations import LeakyReLU from keras.layers.normalization import BatchNormalization from keras.regularizers import l2 from keras.layers import Activation from keras import Model #將定義好的函數添加到keras系統中 from keras.utils import get_custom_objects # class Mish(Activation): # def __init__(self,activation): # super(Mish, self).__init__(activation) # self.__name__ ="Mish" # def mish(inputs): # return inputs*K.tanh(K.softplus(inputs)) # get_custom_objects().update({"Mish":Mish(mish)}) class Mish(Layer): def __init__(self): super(Mish, self).__init__() def call(self,inputs): return inputs * K.tanh(K.softplus(inputs)) #darknet單次卷積 def DarknetConv2D(*args,**kwargs): darknet_conv_kwargs = {"kernel_regularizer":l2(5e-4)} darknet_conv_kwargs["padding"]="valid" if kwargs.get("strides")==(2,2) else "same" darknet_conv_kwargs.update(kwargs) return Conv2D(*args,**darknet_conv_kwargs) #卷積塊 def DarknetConv2D_BN_Mish(x,*args,**kwargs): no_bias_kwargs={"use_bias":False} no_bias_kwargs.update(kwargs) x=DarknetConv2D(*args,**no_bias_kwargs)(x) x=BatchNormalization()(x) #x=Activation("Mish")(x) x=Mish()(x) return x def DarknetConv2D_BN_Leaky(x, *args, **kwargs): no_bias_kwargs = {"use_bias": False} no_bias_kwargs.update(kwargs) x = DarknetConv2D(*args, **no_bias_kwargs)(x) x = BatchNormalization()(x) # x=Activation("Mish")(x) x = LeakyReLU(alpha=0.1)(x) return x def resblock_body(x,num_filters,num_blocks,all_narrow=True): preconv1 =ZeroPadding2D(padding=((1,0),(1,0)))(x) preconv1 =DarknetConv2D_BN_Mish(preconv1,num_filters,(3,3),strides=(2,2)) #生成一個大的殘差邊 shortconv =DarknetConv2D_BN_Mish(preconv1,num_filters//2 if all_narrow else num_filters,(1,1)) #主干部分的卷積 mainconv =DarknetConv2D_BN_Mish(preconv1,num_filters//2 if all_narrow else num_filters,(1,1)) for i in range(num_blocks): x=DarknetConv2D_BN_Mish(mainconv,num_filters//2,(1,1)) x=DarknetConv2D_BN_Mish(x,num_filters//2 if all_narrow else num_filters,(3,3)) mainconv=Add()([mainconv,x]) postconv =DarknetConv2D_BN_Mish(mainconv,num_filters//2 if all_narrow else num_filters,(1,1)) route = Concatenate()([postconv,shortconv]) return DarknetConv2D_BN_Mish(route,num_filters,(1,1)) def darknet_body(x): x =DarknetConv2D_BN_Mish(x,32,(3,3)) x=resblock_body(x,64,1,False) x=resblock_body(x,128,2) x=resblock_body(x,256,8) feat1=x x=resblock_body(x,512,8) feat2=x x=resblock_body(x,1024,4) feat3=x return feat1,feat2,feat3
Mosaic數據增強:
(1)每次讀取四張圖片
(2)分別對這四張圖片進行翻轉,縮放,色域變化等,並且按照四個方向位置擺好。
(3)進行圖片的組合和框的組合。
Label Smoothing-防止過擬合:
在分類模型當中,經常對標簽使用one-hot的形式,然后去預測屬於每一個標簽的概率,如果不考慮多標簽的情況下,選擇概率最大的作為我們的預測標簽。但是在實際過程中可能會存在兩個問題。
(1)可能導致過擬合。
(2)模型對於預測過於自信,以至於忽略可能的小樣本標簽。
產生上述問題的原因就是因為我們真正在計算交叉熵損失函數的時候,對於真實標簽概率的取值要么是1,要么是0,表征我們已知樣本屬於某一類別的概率是為1的確定事件,屬於其他類別的概率則均為0。Label Smoothing的原理就是為損失函數增強其他標簽的損失函數值,類似於其為非標簽增加了一定的可選擇性。
注:如果分類准確,也就是說交叉熵對分類正確給的是最大激勵,但實際上有一些標注數據並不一定是准確的。所以使用上述標簽並不一定是最優的。
Label Smoothing:標簽×(1-ξ)+ξ/標簽個數×[1,1,...1]
def Label_Smoothing(y_true,label_smoothing): y_true = np.cast(y_true,tf.float32) num_classes = float((y_true.shape[-1])) label_smoothing = K.constant(label_smoothing,dtype=K.floatx()) return y_true * (1.0-label_smoothing) + label_smoothing/num_classes
Loss函數:
Loss-IOU
使用Loss-IOU可能會產生梯度消失,因為當BBOX和真實框無交集的時候,這時候Loss-IOU始終為1。反向傳播的時候梯度就為0了,產生了梯度消失。Loss-IOU=1-|B∩Bgt|/|B∪Bgt|
Loss-IOU會產生以下幾點問題:
(1)如果兩個框沒有相交,則IOU=0,這是無法反映兩個框的距離,並且損失函數此時不存在梯度,無法通過梯度下降訓練。
(2)即使相同的IOU也不能代表檢測框的定位效果相同。
Loss-GIOU
Loss-GIOU=1-IOU+|c-B∪Bgt|/|c|,與IOU相比,GIOU不僅關注重疊區域,當B和Bgt相對於彼此沒有很好的對准時,封閉形狀c中的兩個對稱形狀B和Bgt之間的空白空間增加,因此,GIOU的值可以更好的反映兩個對稱物體之間如何發生重疊,GIOU在兩者無交集且無限遠的時候是取最小值-1,因此GIOU是一個非常好的距離度量指標。
IOU和GIOU沒有考慮到真實框與預測中心之間的距離。實際情況下,中心點的距離越小框預測的越准。GIOU在水平和垂直方向誤差很大也就是包含關系。
Loss-DIOU
Loss-DIOU = 1-IOU+δ2(b,bgt)/c2,在Loss-DIOU中,b,bgt分別代表了anchor框和目標框的中心點,且δ代表的是計算兩個中心點間的歐式距離,c代表的是能夠同時覆蓋anchor和目標框的最小矩形的對角線的距離。
(1)DIOU在與目標框不重疊時,仍然可以為邊界框提供移動方向。
(2)Loss-DIOU可以直接最小化兩個目標的距離,因此比Loss-GIOU收斂的塊。
(3)對於包含兩個框的水平方向和垂直方向上這種情況,Loss-DIOU可以回歸的非常快。
Loss-CIOU
Loss-CIOU=1-IOU+δ(b,bgt)/c2+αV,V=4/Π2[arctan(wgt/hgt)-arctan(w/h)]2,α=V/(1-IOU)+V,從α參數來看,損失函數會更加傾向於往重疊的區域增多方向優化。
#b1預測框,b2真實框 def box_ciou(b1,b2): b1_xy = b1[...,:2] b1_wh = b1[...,2:4] b1_wh_half = b1_wh/2. b1_mins = b1_xy-b1_wh_half b1_maxes = b1_xy+b1_wh_half b2_xy = b2[...,:2] b2_wh = b2[...,2:4] b2_wh_half = b2_wh/2.0 b2_mins = b2_xy-b2_wh_half b2_maxes = b2_xy+b2_wh_half intersect_mins = K.maximum(b1_mins,b2_mins) intersect_maxes = K.minimum(b1_maxes,b2_maxes) intersect_wh = K.maximum(intersect_maxes-intersect_mins,0.) intersect_area = intersect_wh[...,0]*intersect_wh[...,1] b1_area = b1_wh[...,0]*b1_wh[...,1] b2_area = b2_wh[...,0]*b2_wh[...,1] union_area = b1_area+b2_area-intersect_area #K.epsilon()返回一個浮點數 iou = intersect_area/(union_area+K.epsilon()) #計算中心距離 center_distance = K.sum(K.square(b1_xy-b2_xy),axis=-1) #找到包裹兩個框的最小框的左上角跟右下角 enclose_mins = K.minimum(b1_mins,b2_mins) enclose_maxes = K.maximum(b1_maxes,b2_maxes) enclose_wh = K.maximum(enclose_maxes-enclose_mins,0.0) #計算對角線距離 enclose_diagonal = K.sum(K.square(enclose_wh),axis=-1) ciou = iou - 1.0 *(center_distance)/(enclose_diagonal+K.epsilon()) v = 4*K.square(tf.math.atan2(b1_wh[...,0],b1_wh[...,1])-tf.math.atan2(b2_wh[...,0],b2_wh[...,1]))/(math.pi*math.pi) alpha = v/(1.0-iou+v) ciou = ciou-alpha*v ciou = K.expand_dims(ciou,axis=-1) return ciou def get_loss_con(ytrue,ypre,noobj_scale,object_mask,IOU): object_mask = K.squeeze(object_mask,axis=-1) con_delta = object_mask*(ypre*IOU-ytrue) + noobj_scale*(1-object_mask)*(ypre*IOU-ytrue) loss_con = K.sum(K.square(con_delta),list(range(1,4))) return loss_con
其他的代碼跟V3一樣。
模擬余弦退火(學習率):
ηt = ηimin + 1/2(ηimax-ηimin) (1-cos(Tcur/Ti)Π),ηimax和ηimin是學習率的范圍,Tcur是隨着iteration變化的,Ti是當前run總共的epoch數目。余弦退火衰減算法,學習率會先上升再下降,上升的時候使用線性上升,下降的時候模擬cos函數下降。Tcur/Ti =iteration/TotalIterations。
import numpy as np import matplotlib.pyplot as plt def compute_eta_t(eta_min, eta_max, T_cur, Ti): pi = np.pi eta_t = eta_min + 0.5 * (eta_max - eta_min) * (np.cos(pi * T_cur / Ti) + 1) return eta_t # 每Ti個epoch進行一次restart。 Ti = [20, 40, 80, 160] n_batches = 200 eta_ts = [] for ti in Ti: T_cur = np.arange(0, ti, 1 / n_batches) for t_cur in T_cur: eta_ts.append(compute_eta_t(0, 1, t_cur, ti)) n_iterations = sum(Ti) * n_batches epoch = np.arange(0, n_iterations) / n_batches plt.plot(epoch, eta_ts) plt.show()