接着扯YOLO v2
相比較於YOLO v1,作者在之前模型上,先修修補補了一番,提出了YOLO v2模型。並基於imagenet的分類數據集和coco的對象檢測數據集,提出了wordnet模型,並成功的提出了YOLO9000模型。這里暫時只講YOLO v2.
作者說yolo v1相比較其他基於區域的模型比如faster r-cnn還是有些不足的,比如更多定位錯誤,更低召回率,所以第二個版本開始主要解決這兩個問題。
0 - 作者對yolo v1的補丁
1 - 在所有卷積層上用BN,並扔掉dropout
2 - yolo 的訓練過程(兩個版本都是)為:分類模型的預訓練和檢測模型的微調。作者覺得yolo v1的預訓練不夠好,所以這里直接拿imagenet的分辨率為\(448*448\)的圖片微調10個epoch,然后在將前面的卷積層留待檢測模型用。
3 - 使用anchor box(而這幾個anchor box是基於訓練集數據的標簽中\(w,h\)聚類出來的),加快模型收斂
4 - 去掉全連接層,扔掉最后一層池化層,並且將網絡的輸入端resize為\(416*416\),從而輸出的是一個\(13*13*channel\)的卷積層
5 - 在預測\(x,y,w,h\)的時候是基於anchor box,且與RPN略有不同的方法去預測。
6 - 加了一個passthrough層
1 - 基礎網絡
為了結合Googlenet和VGG的優勢,作者提出了圖1這樣一種網絡結構,將其作為yolo分類模型的基礎模型。
圖1.darknet-19
上圖就是darknet-19,作者先在imagenet上以\(224*224\)先初始化訓練,然后用\(448*448\)的分辨率圖片跑10個epoch。跑完之后,丟棄最后的卷積層和全局池化層,換上3個\(3*3*1024\)的卷積層,然后跟一層1*1的卷積層,最后輸出\(13*13*125\),如下圖所示
圖2.yolov2網絡結構圖
上圖是通過官網darknet命令生成的網絡結構,從中可以發現有幾點疑惑,比如論文中的passthrough到底是什么操作?
圖2中layer0-layer22層就是darknet-19的前面部分,,具體對照如下圖:
圖3.darknet19到yolov2結構的示意圖
2 - 訓練
yolov2模型的訓練基本就分下面三步:
i)首先拿darknet19在imagenet上以分辨率\(224*224\)跑160回,然后以分辨率\(448*448\)跑10回;
ii)然后如上面所示,將darknet19網絡變成yolov2網絡結構,並resize輸入為\(416*416\)
iii)對增加的層隨機初始化,並接着在對象檢測數據集上訓練160回,且在【60,90】的時候降低學習率
而第三步中,因為v2的目標函數和增加的anchor box,而與v1在概念上有所不同。
3 - v2中較難理解的補丁
如圖2所示,最后會輸出一個\(13*13*125\)的張量,其中的\(13*13\)如yolo v1一樣,是表示網格划分的意思,后面的125就是當前網格的操作所在。之前yolov1時,作者拿了B=2,而這里B=5,且這里的5個預測候選框是事先得到的(下面待續),也就是給定了5個框的\(w,h\),預測得到的是\(5(4+1+20)\),分別為\(x,y,w,h\);\(confidence\);\(class\)。
沒錯,在yolo1的時候是出現2個候選框,然后只有一條20維度表示類別,這里是每個anchor box都會有預測類別,所以相對的目標函數會略有改動,作者說了在v2還是預測\(IOU_{truth}{pred}\)和基於當前有對象基礎上預測類別\(P(class|object)\)。
3.1 anchor box的生成
如論文中所說,相對其他rpn網絡,yolo v2的事先准備的幾個預測候選框(也叫做anchor box)大小是基於訓練集算出來的。如作者代碼
中:
[region]
anchors = 1.08,1.19, 3.42,4.41, 6.63,11.38, 9.42,5.11, 16.62,10.52
bias_match=1
classes=20
coords=4
num=5
上述代碼中anchors保存的就是各自的width和height,也就是論文中的符號\(p_w,p_h\)。
原理就是基於所有訓練集的標簽中,先計算得出所有的width和對應的height,將其作為樣本,采用kmean的方法,只不過距離函數換成了\(d(box,center)=1-IOU(box,center)\)
這里需要5個anchor,那么就會得到5個不同樣本中心,記得乘以(32/416);假設錨點值為x,則\(\frac{x}{centerids}=\frac{32}{416}\)
如5個樣本中心:
[[ 103.84071465 29.09785553]
[ 40.14028782 21.66632026]
[ 224.79420059 102.18148749]
[ 61.79184575 39.21399017]
[ 126.37797981 60.03186796]]
對應的錨點:
7.98774728074, 2.23829657922, 3.08771444772, 1.66664002, 17.2918615841, 7.86011442226, 4.75321890422, 3.01646078194, 9.72138306269, 4.61783599681
3.2 坐標預測
作者先引用了下faster r-cnn的rpn網絡,然后羅列的2個式子\(x = (t_x*w_a) +x_a\);\(y = (t_y*h_a) +y_a\),並說直接Anchor Box回歸導致模型不穩定,該公式沒有任何約束,中心點可能會出現在圖像任何位置,這就有可能導致回歸過程震盪,甚至無法收斂,然后作者基於此稍微改進了下:
$b_x = \sigma(t_x) + c_x \( \)b_y = \sigma(t_y) + c_y\( \)b_w = p_we^{t_w}\( \)b_h = p_he^{t_h}\( 其中\)t_w=log(\frac{w}{w_p})\(;\)t_h=log(\frac{h}{h_p})\( 如下圖:  圖4.論文中圖3 如上圖所示,模型預測輸出的坐標系中四個值分別為\)t_x,t_y,t_w,t_h\(: (1)通過計算當前網格本身的偏移量,加上在當前網格內部所在的比例,即為第0層輸入層圖片上預測框的中心坐標; (2)而預測的\)t_w,t_h\(是相對當前候選框的一個縮放比例,通過公式\)t_w=log(\frac{w}{w_p})\(;\)t_h=log(\frac{h}{h_p})\(反推回去,即為\)b_w = p_we^{t_w}\(;\)b_h = p_he^{t_h}$
3.3 passthrough操作
通過github找到star最多的yolo 2-pytorch,研讀了部分代碼,找到如下部分:
#darknet.py
self.reorg = ReorgLayer(stride=2) # stride*stride times the channels of conv1s
#reorg_layer.py
def forward(self, x):
stride = self.stride
bsize, c, h, w = x.size()
out_w, out_h, out_c = int(w / stride), int(h / stride), c * (stride * stride)
out = torch.FloatTensor(bsize, out_c, out_h, out_w)
if x.is_cuda:
out = out.cuda()
reorg_layer.reorg_cuda(x, out_w, out_h, out_c, bsize, stride, 0, out)
else:
reorg_layer.reorg_cpu(x, out_w, out_h, out_c, bsize, stride, 0, out)
return out
//reorg_cpu.c
int reorg_cpu(THFloatTensor *x_tensor, int w, int h, int c, int batch, int stride, int forward, THFloatTensor *out_tensor)
{
// Grab the tensor
float * x = THFloatTensor_data(x_tensor);
float * out = THFloatTensor_data(out_tensor);
// https://github.com/pjreddie/darknet/blob/master/src/blas.c
int b,i,j,k;
int out_c = c/(stride*stride);
for(b = 0; b < batch; ++b){
//batch_size
for(k = 0; k < c; ++k){
//channel
for(j = 0; j < h; ++j){
//height
for(i = 0; i < w; ++i){
//width
int in_index = i + w*(j + h*(k + c*b));
int c2 = k % out_c;
int offset = k / out_c;
int w2 = i*stride + offset % stride;
int h2 = j*stride + offset / stride;
int out_index = w2 + w*stride*(h2 + h*stride*(c2 + out_c*b));
if(forward) out[out_index] = x[in_index];
else out[in_index] = x[out_index];
}
}
}
}
return 1;
}
從上述c代碼可以看出,這里ReorgLayer層就是將\(26*26*512\)的張量中\(26*26\)切割成4個\(13*13\)然后連接起來,使得原來的512通道變成了2048。
3.4 目標函數計算
同樣通過github上現有的最多stars的那份代碼,找到對應的損失計算部分
#darknet.py
def loss(self):
#可以看出,損失值也是基於預測框bbox,預測的iou,分類三個不同的誤差和
return self.bbox_loss + self.iou_loss + self.cls_loss
def forward(self, im_data, gt_boxes=None, gt_classes=None, dontcare=None):
conv1s = self.conv1s(im_data)
conv2 = self.conv2(conv1s)
conv3 = self.conv3(conv2)
conv1s_reorg = self.reorg(conv1s)
cat_1_3 = torch.cat([conv1s_reorg, conv3], 1)
conv4 = self.conv4(cat_1_3)
conv5 = self.conv5(conv4) # batch_size, out_channels, h, w
……
……
# tx, ty, tw, th, to -> sig(tx), sig(ty), exp(tw), exp(th), sig(to)
'''預測tx ty'''
xy_pred = F.sigmoid(conv5_reshaped[:, :, :, 0:2])
'''預測tw th '''
wh_pred = torch.exp(conv5_reshaped[:, :, :, 2:4])
bbox_pred = torch.cat([xy_pred, wh_pred], 3)
'''預測置信度to '''
iou_pred = F.sigmoid(conv5_reshaped[:, :, :, 4:5])
'''預測分類class '''
score_pred = conv5_reshaped[:, :, :, 5:].contiguous()
prob_pred = F.softmax(score_pred.view(-1, score_pred.size()[-1])).view_as(score_pred)
# for training
if self.training:
bbox_pred_np = bbox_pred.data.cpu().numpy()
iou_pred_np = iou_pred.data.cpu().numpy()
_boxes, _ious, _classes, _box_mask, _iou_mask, _class_mask = self._build_target(
bbox_pred_np, gt_boxes, gt_classes, dontcare, iou_pred_np)
_boxes = net_utils.np_to_variable(_boxes)
_ious = net_utils.np_to_variable(_ious)
_classes = net_utils.np_to_variable(_classes)
box_mask = net_utils.np_to_variable(_box_mask, dtype=torch.FloatTensor)
iou_mask = net_utils.np_to_variable(_iou_mask, dtype=torch.FloatTensor)
class_mask = net_utils.np_to_variable(_class_mask, dtype=torch.FloatTensor)
num_boxes = sum((len(boxes) for boxes in gt_boxes))
# _boxes[:, :, :, 2:4] = torch.log(_boxes[:, :, :, 2:4])
box_mask = box_mask.expand_as(_boxes)
#計算預測的平均bbox損失值
self.bbox_loss = nn.MSELoss(size_average=False)(bbox_pred * box_mask, _boxes * box_mask) / num_boxes
#計算預測的平均iou損失值
self.iou_loss = nn.MSELoss(size_average=False)(iou_pred * iou_mask, _ious * iou_mask) / num_boxes
#計算預測的平均分類損失值
class_mask = class_mask.expand_as(prob_pred)
self.cls_loss = nn.MSELoss(size_average=False)(prob_pred * class_mask, _classes * class_mask) / num_boxes
return bbox_pred, iou_pred, prob_pred
可以從上述代碼窺見一斑,yolo v2的目標函數和yolo v1在概念及結構上相差無幾。