YOLO可以說是單階段的目標檢測方法的集大成之作,必學的經典論文,從準備面試的角度來學習一下yolo系列。
YOLOv1
1.RCNN系列回顧
RCNN系列,無論哪種算法,核心思路都是Region Proposal(定位)+ classifier(修正定位+分類)。所以也被稱為兩階段算法。但是難以達到實時檢測的效果,因此yolov1將其修改為單階段的算法,yolov1雖然犧牲了一定的精度,但是檢測速度大幅提升,而后續(xù)的yolo版本在其之上改進,現(xiàn)在已經有yolov9和yolo-world了,成為主流的目標檢測模型。
2.YOLOv1
(部分內容和圖參考保姆級教程:圖解目標檢測算法YOLOv1 - 知乎 (zhihu.com))
論文原文:
1506.02640.pdf (arxiv.org)https://arxiv.org/pdf/1506.02640.pdf在講解過程中會出現(xiàn)很多專業(yè)詞匯,會挨著進行說明。
YOLOv1的核心思路就是舍棄Region Proposal這個極其耗時的過程,轉而進行回歸。怎么實現(xiàn)舍棄RP的,就是學習的關鍵。
(1)核心思想
采用利用整張圖作為網絡的輸入,將圖像劃分為S*S個grid,某一個grid只關注于預測物體中心在這個grid中的目標,整個網絡最后直接在輸出層回歸 bounding box 的位置和 bounding box 所屬的類別。
Grid和Bouding Box
這里可能會產生一點誤解,故區(qū)分一下。
Grid:將圖片直接劃分為S*S個grid,位置是固定死的,比如上圖中,劃分為了7*7個grid。
Bouding Box:就是最后檢測出物體的框,如上圖中框住狗狗的紅色框,在算法流程中,可以用兩種數(shù)據(jù)形式表示,一種是使用中心坐標+長寬的形式(Cx,Cy,H,W),一種是使用左上和右下角點坐標的形式(x1,y1,x2,y2)。而每個框除了要包含位置信息,還包含了該框是否包含物體的置信度,這個置信度怎么計算的我們后面講解,這里只需要記住每個Bounding Box其實對應了5個數(shù)據(jù)。
置信度(Confidence)的計算
置信度就是算法的自信心得分,這個值越高,代表這個BoundigBox里越有可能包含物體。計算方式如下:
Pr(Object)為邊界框內存在對象的概率,若存在對象,Pr?(Object)=1,否則Pr?(Object)=0。
但是這里要注意一下,其實我們整個網絡的計算中是不需要用這個公式計算的,網絡輸出一個0~1的值就好。
IOU(Intersection over Union ratio)
IOU又叫做交并比,其實很好理解,就是兩個框計算出來的一個值,意義上來看,IOU值越大,表示兩個框的重合度越高,從公式上來看:
一個實現(xiàn)代碼如下:
def calculate_iou(bbox1,bbox2): """計算bbox1=(x1,y1,x2,y2)和bbox2=(x3,y3,x4,y4)兩個bbox的iou""" intersect_bbox = [0., 0., 0., 0.] # bbox1和bbox2的交集 if bbox1[2]<bbox2[0] or bbox1[0]>bbox2[2] or bbox1[3]<bbox2[1] or bbox1[1]>bbox2[3]: pass else: intersect_bbox[0] = max(bbox1[0],bbox2[0]) intersect_bbox[1] = max(bbox1[1],bbox2[1]) intersect_bbox[2] = min(bbox1[2],bbox2[2]) intersect_bbox[3] = min(bbox1[3],bbox2[3]) area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) # bbox1面積 area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) # bbox2面積 area_intersect = (intersect_bbox[2] - intersect_bbox[0]) * (intersect_bbox[3] - intersect_bbox[1]) # 交集面積 # print(bbox1,bbox2) # print(intersect_bbox) # input() if area_intersect>0: return area_intersect / (area1 + area2 - area_intersect) # 計算iou else: return 0
按照這個思路,我們可以簡要理一下網絡的輸入輸出:
輸入一張固定大小的圖像,規(guī)定劃分的格子數(shù)S*S,規(guī)定每個格子要預測幾個框B。
輸出為一個S*S*(B*5+Class),S和B對應輸入,5就是boundingbox中包含的五個信息,class就是類別的預測,這里類別使用的是one-hot編碼。
以作者在論文里提到的PASCAL VOC上的實驗為例:
S=7,B=5,有20個類別,故輸出tensor的維度為7*7*(5*2)
這里有個很容易錯誤理解的點,就是這個class的分類結果其實是對應了這一個grid的,一個grid輸出一個20維的分類結果,而不是整個grid所得到的兩個BoundingBox的分類結果,
(2)網絡結構
YOLOv1的數(shù)據(jù)流如下:
- resize圖片尺寸(沒有ROI)
- 輸入網絡,輸出tensor
- 非極大值抑制(NMS)
網絡的結構如下:
這里光看圖可能很多初學的同學不是很看的懂,我們來看看一個簡單的pytorch版本:
參考:動手學習深度學習pytorch版——從零開始實現(xiàn)YOLOv1_自己實現(xiàn)的yolov-CSDN博客
這一部分需要說明一下,由于原論文是采用自己設計的20層卷積層先在ImageNet上訓練了一周,完成特征提取部分的訓練。我們作為學習者而非發(fā)明者來說,花一周時間訓練實在是太長了。因此,在這里我打算對原論文的結構做一點改變。YOLOv1的前20層是用于特征提取的,也就是隨便替換為一個分類網絡(除去最后的全連接層)其實都行。因此,我打算使用ResNet34的網絡作為特征提取部分。這樣做的好處是,pytorch的torchvision中提供了ResNet34的預訓練模型,訓練集也是ImageNet,等于說有先成訓練好的模型可以直接使用,從而免去了特征提取部分的訓練時間。然后,除去ResNet34的最后兩層,再連接上YOLOv1的最后4個卷積層和兩個全連接層,作為我們訓練的網絡結構。
??此外,還進行了一些小調整,比如最后增加了一個Sigmoid層,以及在卷積層后增加了BN層等等。具體代碼如下:import torchvision.models as tvmodel import torch.nn as nn import torch class YOLOv1_resnet(nn.Module): def __init__(self): super(YOLOv1_resnet,self).__init__() resnet = tvmodel.resnet34(pretrained=True) # 調用torchvision里的resnet34預訓練模型 resnet_out_channel = resnet.fc.in_features # 記錄resnet全連接層之前的網絡輸出通道數(shù),方便連入后續(xù)卷積網絡中 self.resnet = nn.Sequential(*list(resnet.children())[:-2]) # 去除resnet的最后兩層 # 以下是YOLOv1的最后四個卷積層 self.Conv_layers = nn.Sequential( nn.Conv2d(resnet_out_channel,1024,3,padding=1), nn.BatchNorm2d(1024), # 為了加快訓練,這里增加了BN層,原論文里YOLOv1是沒有的 nn.LeakyReLU(), nn.Conv2d(1024,1024,3,stride=2,padding=1), nn.BatchNorm2d(1024), nn.LeakyReLU(), nn.Conv2d(1024, 1024, 3, padding=1), nn.BatchNorm2d(1024), nn.LeakyReLU(), nn.Conv2d(1024, 1024, 3, padding=1), nn.BatchNorm2d(1024), nn.LeakyReLU(), ) # 以下是YOLOv1的最后2個全連接層 self.Conn_layers = nn.Sequential( nn.Linear(7*7*1024,4096), nn.LeakyReLU(), nn.Linear(4096,7*7*30), nn.Sigmoid() # 增加sigmoid函數(shù)是為了將輸出全部映射到(0,1)之間,因為如果出現(xiàn)負數(shù)或太大的數(shù),后續(xù)計算loss會很麻煩 ) def forward(self, input): input = self.resnet(input) input = self.Conv_layers(input) input = input.view(input.size()[0],-1) input = self.Conn_layers(input) return input.reshape(-1, (5*NUM_BBOX+len(CLASSES)), 7, 7) # 記住最后要reshape一下輸出數(shù)據(jù)
這里我們主要關注最后兩個fc層,是沒有使用池化操作的,直接使用view和resize就實現(xiàn)了三維張量和二維張量的轉換。
(3)非極大值抑制
非極大值抑制的目的就是去掉一些冗余框。
這一部分可以參考一下:目標檢測入門之非最大值抑制(NMS)算法 - 知乎 (zhihu.com)
(4)損失函數(shù)
損失函數(shù)是理解YOLOv1訓練的關鍵,具體形式如下:
這里的損失函數(shù)包括五項:
前兩項對應BoundingBox的損失函數(shù)(針對x, y, H, W進行學習)
接下來兩項對應Confidence的損失函數(shù)(針對置信度進行學習)
最后一項對應分類的損失(針對類別label進行學習)
細節(jié)上來說:
1.公式中每一個均方誤差的系數(shù):?表示的是第i個grid的第j個BoundingBox是否負責Object,每個grid對應的B個BoudingBox中,與GT的IOU最大的BoundingBox才負責這個Object,其余的為,這一部分可以簡單看一下代碼:
if iou1 >= iou2:
coor_loss = coor_loss + 5 * (torch.sum((pred[i,0:2,m,n] - labels[i,0:2,m,n])**2) \
+ torch.sum((pred[i,2:4,m,n].sqrt()-labels[i,2:4,m,n].sqrt())**2))
obj_confi_loss = obj_confi_loss + (pred[i,4,m,n] - iou1)**2
# iou比較小的bbox不負責預測物體,因此confidence loss算在noobj中,注意,對于標簽的置信度應該是iou2
noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i,9,m,n]-iou2)**2)
這里計算obj_confi_loss和noobj_confi_loss使用的pred和IOU都是不一樣的,pred[i,4,m,n]中的4對應的是IOU更大的框,9對應的是IOU更小的框。
2.這里對?(w,?)?在損失函數(shù)中的處理分別取了根號,原因在于,如果不取根號,損失函數(shù)往往更傾向于調整尺寸比較大的預測框。例如,20 個像素點的偏差,對于 800x600 的預測框幾乎沒有影響,此時的IOU數(shù)值還是很大,但是對于 30x40 的預測框影響就很大。取根號是為了盡可能的消除大尺寸框與小尺寸框之間的差異。
3.此時再來看?與??,YOLO 面臨的物體檢測問題,是一個典型的類別數(shù)目不均衡的問題(Focal Loss就是解決這個問題的,一個面試中常問的點)。其中 49 個格點,含有物體的格點往往只有 3、4 個,其余全是不含有物體的格點。此時如果不采取點措施,那么物體檢測的mAP不會太高,因為模型更傾向于不含有物體的格點。?與?的作用,就是讓含有物體的格點,在損失函數(shù)中的權重更大,讓模型更加“重視”含有物體的格點所造成的損失。在論文中,?與?的取值分別為 5 與 0.5 。文章來源:http://www.zghlxwxcb.cn/news/detail-857092.html
最后整個Loss部分的代碼如下:文章來源地址http://www.zghlxwxcb.cn/news/detail-857092.html
class Loss_yolov1(nn.Module):
def __init__(self):
super(Loss_yolov1,self).__init__()
def forward(self, pred, labels):
"""
:param pred: (batchsize,30,7,7)的網絡輸出數(shù)據(jù)
:param labels: (batchsize,30,7,7)的樣本標簽數(shù)據(jù)
:return: 當前批次樣本的平均損失
"""
num_gridx, num_gridy = labels.size()[-2:] # 劃分網格數(shù)量
num_b = 2 # 每個網格的bbox數(shù)量
num_cls = 20 # 類別數(shù)量
noobj_confi_loss = 0. # 不含目標的網格損失(只有置信度損失)
coor_loss = 0. # 含有目標的bbox的坐標損失
obj_confi_loss = 0. # 含有目標的bbox的置信度損失
class_loss = 0. # 含有目標的網格的類別損失
n_batch = labels.size()[0] # batchsize的大小
# 可以考慮用矩陣運算進行優(yōu)化,提高速度,為了準確起見,這里還是用循環(huán)
for i in range(n_batch): # batchsize循環(huán)
for n in range(7): # x方向網格循環(huán)
for m in range(7): # y方向網格循環(huán)
if labels[i,4,m,n]==1:# 如果包含物體
# 將數(shù)據(jù)(px,py,w,h)轉換為(x1,y1,x2,y2)
# 先將px,py轉換為cx,cy,即相對網格的位置轉換為標準化后實際的bbox中心位置cx,xy
# 然后再利用(cx-w/2,cy-h/2,cx+w/2,cy+h/2)轉換為xyxy形式,用于計算iou
bbox1_pred_xyxy = ((pred[i,0,m,n]+n)/num_gridx - pred[i,2,m,n]/2,(pred[i,1,m,n]+m)/num_gridy - pred[i,3,m,n]/2,
(pred[i,0,m,n]+n)/num_gridx + pred[i,2,m,n]/2,(pred[i,1,m,n]+m)/num_gridy + pred[i,3,m,n]/2)
bbox2_pred_xyxy = ((pred[i,5,m,n]+n)/num_gridx - pred[i,7,m,n]/2,(pred[i,6,m,n]+m)/num_gridy - pred[i,8,m,n]/2,
(pred[i,5,m,n]+n)/num_gridx + pred[i,7,m,n]/2,(pred[i,6,m,n]+m)/num_gridy + pred[i,8,m,n]/2)
bbox_gt_xyxy = ((labels[i,0,m,n]+n)/num_gridx - labels[i,2,m,n]/2,(labels[i,1,m,n]+m)/num_gridy - labels[i,3,m,n]/2,
(labels[i,0,m,n]+n)/num_gridx + labels[i,2,m,n]/2,(labels[i,1,m,n]+m)/num_gridy + labels[i,3,m,n]/2)
iou1 = calculate_iou(bbox1_pred_xyxy,bbox_gt_xyxy)
iou2 = calculate_iou(bbox2_pred_xyxy,bbox_gt_xyxy)
# 選擇iou大的bbox作為負責物體
if iou1 >= iou2:
coor_loss = coor_loss + 5 * (torch.sum((pred[i,0:2,m,n] - labels[i,0:2,m,n])**2) \
+ torch.sum((pred[i,2:4,m,n].sqrt()-labels[i,2:4,m,n].sqrt())**2))
obj_confi_loss = obj_confi_loss + (pred[i,4,m,n] - iou1)**2
# iou比較小的bbox不負責預測物體,因此confidence loss算在noobj中,注意,對于標簽的置信度應該是iou2
noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i,9,m,n]-iou2)**2)
else:
coor_loss = coor_loss + 5 * (torch.sum((pred[i,5:7,m,n] - labels[i,5:7,m,n])**2) \
+ torch.sum((pred[i,7:9,m,n].sqrt()-labels[i,7:9,m,n].sqrt())**2))
obj_confi_loss = obj_confi_loss + (pred[i,9,m,n] - iou2)**2
# iou比較小的bbox不負責預測物體,因此confidence loss算在noobj中,注意,對于標簽的置信度應該是iou1
noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i, 4, m, n]-iou1) ** 2)
class_loss = class_loss + torch.sum((pred[i,10:,m,n] - labels[i,10:,m,n])**2)
else: # 如果不包含物體
noobj_confi_loss = noobj_confi_loss + 0.5 * torch.sum(pred[i,[4,9],m,n]**2)
loss = coor_loss + obj_confi_loss + noobj_confi_loss + class_loss
# 此處可以寫代碼驗證一下loss的大致計算是否正確,這個要驗證起來比較麻煩,比較簡潔的辦法是,將輸入的pred置為全1矩陣,再進行誤差檢查,會直觀很多。
return loss/n_batch
3.YOLOv1的缺點
- 由于輸出層為全連接層,因此在檢測時,YOLO訓練模型只支持與訓練圖像相同的輸入分辨率。
- 雖然每個格子可以預測B個bounding box,但是最終只選擇只選擇IOU最高的bounding box作為物體檢測輸出,即每個格子最多只預測出一個物體。當物體占畫面比例較小,如圖像中包含畜群或鳥群時,每個格子包含多個物體,但卻只能檢測出其中一個。這是YOLO方法的一個缺陷。
- YOLO loss函數(shù)中,大物體IOU誤差和小物體IOU誤差對網絡訓練中l(wèi)oss貢獻值接近(雖然采用求平方根方式,但沒有根本解決問題)。因此,對于小物體,小的IOU誤差也會對網絡優(yōu)化過程造成很大的影響,從而降低了物體檢測的定位準確性。
到了這里,關于目標檢測——YOLO系列學習(一)YOLOv1的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!