文章目錄
- 前言
- 1、資料增強、建立anchor
-
- 1.1 資料增強
- 1. 2 建立anchor
- 2、分類和回歸的target
- 3、損失函數
-
- 3.1 分類損失
- 3.1 回歸損失
- 下一篇連結
前言
本文是SiamRPN代碼分析的第二部分training。訓練時,使用GOT-10k資料集訓練,是以測試結果性能與論文有些差距,最近在看pysot的訓練過程,後續打算記錄pysot的COCO、ILSVRC2015_DET、ILSVRC2015_VID、YouTube-BB資料集以及pysot提供的資料增強方法來訓練自己的模型。
對模型訓練代碼部分不清晰的小夥伴請翻看之前關于SiamFC代碼分析這篇文章,本文僅僅分析訓練過程中的重點進行分析。
1、資料增強、建立anchor
SiamRPN關于training的詳細流程圖如下:
1.1 資料增強
接下來,分析下資料增強和生成anchor的代碼,資料增強就是在得到原始搜尋圖像的時候對目标進行了随機的小移位以及目标長寬的縮放,當然也可以添加其他資料增強方式,比如模糊處理等,代碼中僅對搜尋圖像的目标進行了這些操作。這裡解釋下代碼中為什麼沒對目标模闆圖像資料增強呢?因為在跟蹤過程中,anchor是反映在搜尋圖像上,是以對搜尋圖像中的目标shift後,訓練過程要找的anchor就不僅僅是整個搜尋圖像的中心位置了,而是在位移所在處,這樣訓練後的網絡可以避免在測試過程中網絡對中心的敏感趨向性。而對目标模闆圖像進行類似的處理顯得就沒那麼重要了,因為可以把目标模闆圖像的特征圖是卷積核,跟蹤過程是在搜尋圖像尋找與卷積核最相似的部分(分類的前景值最大),其實相比于shift,scale和圖像模糊等對目标模闆圖像的意義更大,以上隻是我個人的了解,其實加不加都行,多多少少有點益處。在閱讀pysot源碼時,對目标模闆圖像和搜尋圖像采取了同樣的資料增強方式。
shift_x = np.random.choice(range(-12,12))
shift_y = np.random.choice(range(-12,12))
scale_h = 1.0 + np.random.uniform(-0.15, 0.15)
scale_w = 1.0 + np.random.uniform(-0.15, 0.15)
如代碼所示,對目标的中心12個像素内的shift操作,12個像素的位移是相對于271x271大小的圖像而言,并對其高寬進行0.85~1.15範圍内的的縮放比例。驗證下最終得到的搜尋圖像效果,
gt = np.array(list(map(round, gt)))
print(gt) #前兩個值是shift_x和shift_y
gt[0:2] = gt[0:2]+135 #得到真正的center_x和center_y的坐标
gt = cxcywhtoltwh(gt)
gt = gt.squeeze(axis=0)
print(gt)
plt.imshow(instance_img)
ax = plt.gca()
ax.add_patch(plt.Rectangle((gt[0:2]),gt[2],gt[3],color="red", fill=False, linewidth=1))
plt.show()
1. 2 建立anchor
anchor的建立代碼如下所示:
#擷取所有像素點的anchors
#total_stride=8; base_size=8; scales=[8]; ratios=[0.33, 0.5, 1, 2, 3] ;core_size=19
def generate_anchors(total_stride, base_size, scales, ratios, score_size):
#擷取不同尺度和大小的anchor
anchor_num = len(ratios) * len(scales) #5
anchor = np.zeros((anchor_num, 4), dtype=np.float32) #shepe(5,4)
size = base_size * base_size #8*8=64 面積
count = 0
for ratio in ratios:
#w和h是相對于19x19而言
w = int(np.sqrt(size / ratio))
h = int(w * ratio)
for scale in scales:
#anchor_w、anchor_h是相對于原始圖檔而言,因為stride=8,是以scale一定要是8得整數倍
anchor_w = w * scale
anchor_h = h * scale
anchor[count, 0] = 0
anchor[count, 1] = 0
anchor[count, 2] = anchor_w
anchor[count, 3] = anchor_h
count += 1
#tile複制功能,anchor[5,4]->[5,4*19*19]預設次元是第1維,不是第0維
#anchor[5,0:4]=anchor[5,4*(i-1):4*i], i=1,2,3,,,19*19
anchor= np.tile(anchor, score_size * score_size)
#[1805,4],anchor[0:19*19,4]中每個元素都相同,anchor[19*19:19*19*2,4]中每個元素相同,以此類推
anchor = anchor.reshape((-1, 4))
#取19x19的中心區域畫anchor
ori =-(score_size // 2) * total_stride #ori=-72=-9*8
"""取19x19的feature map的anchor,範圍為19//2,映射回原圖(271*271)就是 19//2 * 8"""
#xx={ndarray:(19,19)}[[-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64, 72], [-72 -64 -56 -48 -40 -32 -2...
#yy={ndarray:(19,19)}[[-72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72 -72, -72], [-64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64 -64, -64], [-56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56 -56, -56], [-48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48 -48, -48], [-40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40, -40], [-32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32 -32, -32], [-24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24 -24, -24], [-16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16 -16, -16], [ -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8 -8, -8], [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0, 0], [ 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8, 8], [ 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16, 16], [ 24 24 24 24 24 24 2...
xx, yy = np.meshgrid([ori + total_stride * dx for dx in range(score_size)],
[ori + total_stride * dy for dy in range(score_size)])
xx = np.tile(xx.flatten(), (anchor_num, 1)).flatten() #(1805,)
yy = np.tile(yy.flatten(), (anchor_num, 1)).flatten() #(1805,)
anchor[:, 0], anchor[:, 1] = xx.astype(np.float32), yy.astype(np.float32)
#anchor.shape=(1805,4) 4代表[center_x,center_y,w,h]
return anchor
anchor的尺度取[0.5,0.66,1,1.5,2],尺度完全可以自己調整為其它,裡面的具體尺度值代表高寬比,大小須為8的倍數,因為anchor是定義在19x19大小的feature map上的,最後得到的anchor要映射會272x272大小的原始圖像,19x19上的8x8的像素面積的對應原圖的面積大小是64x64。在窮舉anchor的時候,論文提到,并不是對19x19每個像素位置都取5種尺度的anchor,而是在規定特征圖範圍内取anchor,
在距離中心不超過7個像素點位置取anchor,即14x14的區域内,而代碼中在19x19整個區域内窮取anchor,這樣各有各的好把,14x14的情況下訓練速度、跟蹤速度都加快,19x19的話可以跟蹤到快速運動的物體。
具體的anchor生成方法是,先得到基準anchor,基準anchor的shape是[5,4],代表4中尺度,每種尺度的向量記錄anchor的[center_x,center_y,w,h],這裡的基準anchor的center_x和center_y是圖像的中心點(0,0),然後再對其進行位移,再19x19的特征圖上是x、y軸都是以1個像素為機關平移,x和y軸的平移坐标是-9.-8.-7…0…7.8.9,乘上步距8映射到原圖的的位移就是-72.-64…0…64.72,是以共得到19x19x5=1805個anchor,下圖展示了對比19x19的feature map的anchor和原圖的anchor,這裡feature map的anchor隻簡單展示,原圖anchor窮舉。有一點值得注意的是,原圖的效果看上去并不是窮舉anchor,在圖像周邊緣還有很多區域無anchor覆寫,這是由于19x8=152不等于272,網絡卷積層no padding所造成的,不過看上去好像又覆寫了大面積,不像是152,至少200+,這是因為原圖中anchor的大小是64x64,anchor給的是中心點坐标,中心位置加上一半的高寬就得到下面的效果。
2、分類和回歸的target
分類的target是根據每個anchor與目标的grundtruth計算IOU得到,計算二者IOU的代碼如下,函數給的參數是1805個anchor以及目标的單個groundtruth,最終傳回1805個IOU值。
def compute_iou(self, anchors, box):
#将box複制1805份,gt_box.shape=(1805,4) 每個[i,4](i=0...1804)都相同
gt_box = np.tile(box.reshape(1, -1), (anchors.shape[0], 1))
anchor_x1 = anchors[:, :1] - anchors[:, 2:3] / 2 + 0.5 #xmin
anchor_x2 = anchors[:, :1] + anchors[:, 2:3] / 2 - 0.5 #xmax
anchor_y1 = anchors[:, 1:2] - anchors[:, 3:] / 2 + 0.5 #ymin
anchor_y2 = anchors[:, 1:2] + anchors[:, 3:] / 2 - 0.5 #ymax
gt_x1 = gt_box[:, :1] - gt_box[:, 2:3] / 2 + 0.5 #xmin
gt_x2 = gt_box[:, :1] + gt_box[:, 2:3] / 2 - 0.5 #xmax
gt_y1 = gt_box[:, 1:2] - gt_box[:, 3:] / 2 + 0.5 #ymin
gt_y2 = gt_box[:, 1:2] + gt_box[:, 3:] / 2 - 0.5 #ymax
xx1 = np.max([anchor_x1, gt_x1], axis=0)
xx2 = np.min([anchor_x2, gt_x2], axis=0)
yy1 = np.max([anchor_y1, gt_y1], axis=0)
yy2 = np.min([anchor_y2, gt_y2], axis=0)
inter_area = np.max([xx2 - xx1, np.zeros(xx1.shape)], axis=0) * np.max([yy2 - yy1, np.zeros(xx1.shape)],axis=0)
area_anchor = (anchor_x2 - anchor_x1) * (anchor_y2 - anchor_y1)
area_gt = (gt_x2 - gt_x1) * (gt_y2 - gt_y1)
iou = inter_area / (area_anchor + area_gt - inter_area + 1e-6)
#傳回iou.shape=(1805,1)
return iou
得到IOU值後,如論文所述,根據所給定的門檻值0.6和0.3,大于0.6為正樣本,target值為1,小于0.3為負樣本,target值為0。代碼中,在IOU在0.3到0.6之間的的anchor設它的target為-1,訓練時,忽略target為-1的部分,是以分類target的次元是(1805,1)。
iou = self.compute_iou(anchors, box).flatten()
pos_index = np.where(iou > config.pos_threshold)[0] #>0.6的為正樣本
neg_index = np.where(iou < config.neg_threshold)[0] #<0.3的為負樣本
#正樣本為1,負樣本為0,抛棄的為-1(iou介于0.3到0.6之間)
label = np.ones_like(iou) * -1
label[pos_index] = 1
label[neg_index] = 0
下面用圖展示一下,左圖紅色框是目标的groundtruth,右圖紅色框是列舉的幾個anchor。
然後就是回歸的target了,在測試過程中,回歸參數的作用是對預測到的目标框進行微調,使它的位置更加準确。計算回歸target的代碼和具體示意圖如下:
#傳回regression_target.shape=(1805,4),4:(dx,dy,dw,dh)
def get_target_reg(self, anchors, gt_box):
#anchor.shape=[1805,4] ge_box.shape(4,)
anchor_xctr = anchors[:, :1]
anchor_yctr = anchors[:, 1:2]
anchor_w = anchors[:, 2:3]
anchor_h = anchors[:, 3:]
gt_cx, gt_cy, gt_w, gt_h = gt_box
#真實gt以及anchor的回歸參數,網絡輸出得到的是預測pre以及anchor的回歸參數
target_x = (gt_cx - anchor_xctr) / anchor_w # (1805,1) dx=(gx-Ax)/Aw A:anchor g:groundtruth
target_y = (gt_cy - anchor_yctr) / anchor_h # (1805,1) dy
target_w = np.log(gt_w / anchor_w) # (1805,1) pw
target_h = np.log(gt_h / anchor_h) # (1805,1) ph
regression_target = np.hstack((target_x, target_y, target_w, target_h)) #(1805,4)
return regression_target
紅色的目标的groundtruth,黃色的是預測的目标框,黑色的經過微調之後得到的目标框,回歸參數共有4個dx、dy、dw和dh,分别調整黃色預測框的中心位置和高寬。訓練時,僅對正樣本(分類target值為1)進行回歸訓練,訓練的時候,就是要把網絡輸出得到的回歸參數與給的回歸target最小化,再用網絡輸出的回歸參數微調預測框,得到的回歸target次元是(1805,4)。
3、損失函數
損失函數分為分類損失和回歸損失,分類損失計算方法采用交叉熵損失函數,回歸損失計算方法采用smooth_L1損失函數。訓練時,采用損失權重的方法,代碼中簡單的使用1:5的權重。
"""———————将模闆和搜尋圖像輸入net,得到回歸參數和分類分數————"""
#score.shape=[8,10,19,19],regression.shape=[8,20,19,19]
pred_score, pred_regression = model(exemplar_imgs.cuda(), instance_imgs.cuda())
#pre_conf.shape=(8,1805,2)
pred_conf = pred_score.reshape(-1, 2, config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
#pred_offset.shape=[8,1805,4]
pred_offset = pred_regression.reshape(-1, 4,config.anchor_num * config.score_size * config.score_size).permute(0,2,1)
"""——————————————計算分類和回歸損失————————————————————-"""
cls_loss = rpn_cross_entropy_balance(pred_conf, conf_target, config.num_pos, config.num_neg, anchors,
nms_pos=config.nms_pos, nms_neg=config.nms__neg)
reg_loss = rpn_smoothL1(pred_offset, regression_target, conf_target, config.num_pos, nms_reg=config.nms_reg)
loss = cls_loss + config.loss_weight * reg_loss #分類權重和回歸權重 1:5
将網絡的輸出與上一節所講的分類和回歸target輸入損失函數進行訓練,接下來分别講解rpn_cross_entropy_balance以及rpn_smoothL1損失函數。
3.1 分類損失
進入rpn_cross_entropy_balance函數一探究竟
def rpn_cross_entropy_balance(input, target, num_pos, num_neg, anchors, nms_pos=None, nms_neg=None):
loss_all = []
for batch_id in range(target.shape[0]): #周遊batch,計算損失
min_pos = min(len(np.where(target[batch_id].cpu() == 1)[0]), num_pos) #num-pos=16,意思target=1的最多取16個
min_neg = int(min(len(np.where(target[batch_id].cpu() == 1)[0]) * num_neg / num_pos, num_neg)) #target=0最多48個,且保證是target=1個數的三倍
pos_index = np.where(target[batch_id].cpu() == 1)[0].tolist() #參數清單
neg_index = np.where(target[batch_id].cpu() == 0)[0].tolist() #參數清單
if nms_pos: #False
pass
else:
pos_index_random = random.sample(pos_index, min_pos) #随機取min_pos個正樣本的索引
if len(pos_index) > 0:
# 交叉熵損失
pos_loss_bid_final = F.cross_entropy(input=input[batch_id][pos_index_random],target=target[batch_id][pos_index_random], reduction='none')
else:
pos_loss_bid_final = torch.FloatTensor([0]).cuda()
if nms_neg:#False
pass
else:
if len(pos_index) > 0:
neg_index_random = random.sample(neg_index, min_neg)
#計算損失時,input要是float型,target為long型,reduction為None表示不計算損失平均
#如果target為1,把input的第二次元當作計算,target為0時,把input的第一位當作計算
neg_loss_bid_final = F.cross_entropy(input=input[batch_id][neg_index_random],target=target[batch_id][neg_index_random], reduction='none')
else:
neg_index_random = random.sample(np.where(target[batch_id].cpu() == 0)[0].tolist(), num_neg)
neg_loss_bid_final = F.cross_entropy(input=input[batch_id][neg_index_random],target=target[batch_id][neg_index_random], reduction='none')
#每張圖檔的損失平均值
loss_bid = (pos_loss_bid_final.mean() + neg_loss_bid_final.mean()) / 2
loss_all.append(loss_bid)
final_loss = torch.stack(loss_all).mean()
return final_loss
首先,代碼沒有使用論文提及的nms非極大抑制比,原因是在分類損失中,隻有前景和後景兩個分數值,這兩個分數值不能直覺展現anchor的重疊率,倘若兩個anchor分數都為0.7,那麼anchor的位置可以在groundtruth的上下左右各個位置,是以代碼中,随機抽取了16個分類target為1的正樣本和48個分類target為0的負樣本進行訓練。
F.cross_entropy
函數是重點,在訓練過程中,正樣本和負樣本單獨訓練,先分析正樣本訓練,輸入從每個batch中取16個樣本,它的索引為分類target為1的索引,target肯定就是16個1喽,那麼訓練時,使用的是輸入的第二次元,白話講,就是讓第input每個anchor分類預測的第二個值,即前景的值訓練得接近于1;負樣本訓練類似,隻是target值為0,是以要訓練使得anchor分類預測的第一個值為0。具體見下圖分析,并且還用代碼作了驗證。這裡提前說下,在測試過程中,要得到預測框的方法是選擇1805個anchor第2個值最大的anchor作為預測框,再進行微調。
if __name__ == '__main__':
input = np.array([[0.2,0.8],[0.4,0.6],[0.9,0.1]],dtype=float)
input = torch.from_numpy(input)
target = np.array([1,1,0],dtype='int64')
target = torch.from_numpy(target)
loss = F.cross_entropy(input,target,reduction='none')
print(loss) #輸出為tensor([0.4375, 0.5981, 0.3711], dtype=torch.float64)
3.1 回歸損失
回歸損失使用的是smooth_L1函數,該函數的詳解檢視這篇文章,極力推薦!anchor位置的回歸訓練僅針對正樣本,首先要得到anchor,那麼根據分類的target為1得到對應正樣本anchor的索引。接下來本該對anchor為1的樣本作nms處理,因為這樣可以得到不重疊的anchor,但是,代碼卻沒有,為什麼?因為之前在計算正樣本的時候,IOU要大于0.6,這樣基本上正樣本anchor都集中在groundtruth附近,如果再剔出重複anchor,那麼門檻值得設定很大才合适,因為設定小了,基本上就得不到16個正樣本了,是以nms操作對回歸訓練影響很小。代碼中選擇損失值最大的16個anchor政策進行回歸訓練來替代nms。
# input(batch,1805,4) target(batch,1805,4) label(16,1805),num_pos=16 ohem=False(不使用nms)
def rpn_smoothL1(input, target, label, num_pos=16, nms_reg=True):
loss_all = []
for batch_id in range(target.shape[0]): #周遊每張圖檔計算回歸損失
min_pos = min(len(np.where(label[batch_id].cpu() == 1)[0]), num_pos) #最多取16個正樣本,回歸隻針對正樣本
if nms_reg:
pos_index = np.where(label[batch_id].cpu() == 1)[0]
if len(pos_index) > 0:
loss_bid = F.smooth_l1_loss(input[batch_id][pos_index], target[batch_id][pos_index], reduction='none')
#得到損失值得索引,損失小到大排列
sort_index = torch.argsort(loss_bid.mean(1))
#從最後取損失值大的用于訓練
loss_bid_nms = loss_bid[sort_index[-num_pos:]]
else:
loss_bid_nms = torch.FloatTensor([0]).cuda()[0]
loss_all.append(loss_bid_nms.mean())
final_loss = torch.stack(loss_all).mean()
return final_loss
至此,training分析代碼完了,最後是test代碼的分析。
下一篇連結
SiamRPN代碼分析:test