YOLO1 學習筆記

本文是對YOLO1的學習筆記,主要是從術語、網絡結構、損失函數三個方面對這個網絡進行學習,YOLO3據說比一代更優秀,所以我也是簡單學了學基礎的概念,沒有去深入研究如何實現,下面是我對YOLO1的理解,相關參考我已給出鏈接,個人理解的內容肯定有不夠準確的地方,希望能和大家交流討論。

基本術語

網格

YOLO將輸入圖像劃分爲S*S的網格,每個網格負責檢測中心落在該柵格中的物體。每一個柵格預測B個bounding boxes,以及這些 bounding boxes 的 confidence scores。這個 confidence scores 反映了模型對於這個網格的預測:該網格是否含有物體,以及這個box的座標預測的有多準。

bounding box

YOLO的預測框,一個 bounding box 包含(x,y,h,w,confidence )五個值,同樣,每一組(x,y,h,w,confidence)確定一個bounding box 框

confidence scores

confidence=Pr(Object)IOUpredtruth confidence=Pr(Object)\ast IOU_{pred}^{truth}

若bounding box包含物體,則Pr(object)=1Pr(object) = 1;否則Pr(object)=0Pr(object) = 0

下面損失函數中的 C^\hat C 就是包含物體的 bounding box 的 confidence 分。

IOU

IOU(intersection over union)爲檢測結果(最終生成的框)與真實值Ground Truth的交集比上它們的並集。

參考:https://blog.csdn.net/weixin_40922744/article/details/102988751

def calculate_IOU(rec1,rec2):
    """ 計算兩個矩形框的交併比
    
    Args:
    	rec1: [left1,top1,right1,bottom1]  # 其中(left1,top1)爲矩形框rect1左上角的座標,(right1, bottom1)爲右下角的座標,下同。
     	rec2: [left2,top2,right2,bottom2]
     	
    Returns: 
    	交併比IoU值
    """
    left_max  = max(rec1[0],rec2[0])
    top_max = max(rec1[1],rec2[1])
    right_min = min(rec1[2],rec2[2])
    bottom_min = min(rec1[3],rec2[3])
    #兩矩形相交時計算IoU
    if (left_max < right_min or bottom_min > top_max):  # 判斷時加不加=都行,當兩者相等時,重疊部分的面積也等於0
        rect1_area = (rec1[2]-rec1[0])*(rec1[3]-rec1[1])
        rect2_area = (rec2[2]-rec2[0])*(rec2[3]-rec2[1])
        area_cross = (bottom_min - top_max)*(right_min - left_max)
	    return area_cross / (rect1_area + rect2_area - area_cross)
	 else:
	 	return 0

類別條件概率

Pr(ClassiObject)P_r(Class_i |Object) 表示網格內存在物體且屬於第i類的概率

一個網格中的物體屬於i類的概率是 Pr(ClassiObject)confidenceP_r(Class_i |Object) *confidence,即:
Pr(ClassiObject)Pr(Object)IOUpredtruth P_r(Class_i|Object) ∗ P_r(Object) ∗ IOU^{truth}_{pred} %3D P_r(Class_i) ∗ IOU^{truth}_{pred}

輸入數據與預處理方法

數據要求

輸入數據應該統一大小且帶標籤,標籤包含目標框位置和物體類別,這裏注意,目標框的位置和大小(x,y,h,w)都是相對於圖片大小(圖片大小爲應該爲448×448)的比例值,比如假設實際位置是(1,2,3,4),則標註位置應該爲(1/448,2/448,3/448,4/448),其中x,y是中心點座標,h,w是目標框的高和寬。

給數據打標

打標可以用軟件labelImg,打標後可以得到每張圖片對應的XML文件,文件內容類似

在這裏插入圖片描述

但是此時XML文件中標註的(x,y,h,w)不是YOLOv1想要的那種,所以還得對標準文件進行轉換

import xml.etree.ElementTree as ET  
import pickle
import os                         
from os import listdir, getcwd    
from os.path import join           
 
sets=[('2018', 'VOC')]
classes = ["zuoyuting"] #需要處理的標籤

def convert(size, box): 
    dw = 1./size[0]
    dh = 1./size[1]
    x = (box[0] + box[1])/2.0
    y = (box[2] + box[3])/2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)
 
def convert_annotation(year, image_id):
    in_file = open('Annotations/%s.xml'%(image_id), encoding='UTF-8')
    out_file = open('labels/%s.txt'%(image_id), 'w', encoding='UTF-8')

    #從xml文件中獲取圖片標註的寬與高
    tree=ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size') #size結點
    w = int(size.find('width').text) #寬和高
    h = int(size.find('height').text)
 
    for obj in root.iter('object'): #一張圖片可能有多個類別標籤,遍歷
        difficult = obj.find('difficult').text
        cls = obj.find('name').text
        
        #過濾掉難以識別和不在訓練類別範圍內的物體
        if cls not in classes or int(difficult) == 1: 
            continue
        
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        
        #獲取目標框的四角座標點
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
        
        #由目標框的四角座標的生成我們想要的目標框標籤
        bb = convert((w,h), b)
        
        #按一定格式輸出新的目標狂暴標籤
        #out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

更多參考:

https://blog.csdn.net/qq_34806812/article/details/81673798

https://blog.csdn.net/shuiyixin/article/details/82623613

https://blog.csdn.net/shuiyixin/article/details/82915105

網絡結構

論文中的結構圖:

在這裏插入圖片描述

論文中的結構圖有誤,第二個特徵圖深度應該是64,第三個特徵圖深度是192,第三個特徵圖以後的深度就沒問題了。

pytroch 源碼 中的模型結構:

YOLO(
  (features): DarkNet(
    (net): Sequential(
      (0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3))
      (1): LeakyReLU(negative_slope=0.1, inplace)
      (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (3): Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (4): LeakyReLU(negative_slope=0.1, inplace)
      (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (6): Conv2d(192, 128, kernel_size=(1, 1), stride=(1, 1))
      (7): LeakyReLU(negative_slope=0.1, inplace)
      (8): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (9): LeakyReLU(negative_slope=0.1, inplace)
      (10): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
      (11): LeakyReLU(negative_slope=0.1, inplace)
      (12): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (13): LeakyReLU(negative_slope=0.1, inplace)
      (14): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (15): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
      (16): LeakyReLU(negative_slope=0.1, inplace)
      (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (18): LeakyReLU(negative_slope=0.1, inplace)
      (19): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
      (20): LeakyReLU(negative_slope=0.1, inplace)
      (21): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (22): LeakyReLU(negative_slope=0.1, inplace)
      (23): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
      (24): LeakyReLU(negative_slope=0.1, inplace)
      (25): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (26): LeakyReLU(negative_slope=0.1, inplace)
      (27): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
      (28): LeakyReLU(negative_slope=0.1, inplace)
      (29): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (30): LeakyReLU(negative_slope=0.1, inplace)
      (31): Conv2d(512, 512, kernel_size=(1, 1), stride=(1, 1))
      (32): LeakyReLU(negative_slope=0.1, inplace)
      (33): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (34): LeakyReLU(negative_slope=0.1, inplace)
      (35): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (36): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1))
      (37): LeakyReLU(negative_slope=0.1, inplace)
      (38): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (39): LeakyReLU(negative_slope=0.1, inplace)
      (40): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1))
      (41): LeakyReLU(negative_slope=0.1, inplace)
      (42): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (43): LeakyReLU(negative_slope=0.1, inplace)
    )
  )
  (yolo): Sequential(
    (0): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): LeakyReLU(negative_slope=0.1)
    (2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (3): LeakyReLU(negative_slope=0.1)
    (4): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (5): LeakyReLU(negative_slope=0.1)
    (6): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): LeakyReLU(negative_slope=0.1)
  )
  (flatten): Flatten()
  (fc1): Linear(in_features=81920, out_features=4096, bias=True)
  (dropout): Dropout(p=0.5)
  (fc2): Linear(in_features=4096, out_features=539, bias=True)
)

模型的最後一層使用線性激活函數,而所有其它的層使用下面的leaky rectified activation:

在這裏插入圖片描述

目標函數

在這裏插入圖片描述

其中 $ 1_i^{obj}$ 表示目標是否出現在網格單元i中,$ 1_{ij}^{obj}$ 表示單元格i中的第j個邊界框預測器“負責”該預測。注意,如果目標存在於該網格單元中(前面討論的條件類別概率),則損失函數僅懲罰分類錯誤。如果預測器“負責”實際邊界框(即該網格單元中具有最高IOU的預測器),則它也僅懲罰邊界框座標錯誤。

這裏的出現/負責意味着值取 1 , 否則取0;$ 1_{ij}^{noobj}$ 則相反,出現取0,不出現取1。

一個 bounding box(邊界框預測器) 對object負責,其實就是這個網格非極大值抑制(NMS)後剩下的最好的那個bounding box,詳見原論文:

YOLO爲每個網格單元預測多個邊界框。在訓練時,每個目標我們只需要一個邊界框預測器來負責。若某預測器的預測值與目標的實際值的IOU值最高,則這個預測器被指定爲“負責”預測該目標。這導致邊界框預測器的專業化。每個預測器可以更好地預測特定大小,方向角,或目標的類別,從而改善整體召回率。

補充解釋一下:

  • LOSS函數建模目的可以整體理解爲:如果預測框內包含物體(即真實框的中心落在了預測框所在的網格中),那麼就通過梯度使預測框的(x,y,h,w)向真實框趨近,且讓預測框的confidence向IOU(預測框,真實框)趨近,讓類別概率分佈向真實概率分佈趨近。如果預測框不包含物體,那麼就只需要讓confidence向0趨近。
  • confidence 受預測框的(x,y,h,w)影響,爲何還在LOSS函數中增加關於confidence的懲罰項?原因是YOLO網絡最終輸出的張量內的confidence值,並非是由輸出的預測框的(x,y,h,w)計算而來,而是在梯度下降過程中,網絡不斷嘗試擬合 $ confidence=Pr(Object)\ast IOU_{pred}^{truth}$ 得到一系列權重參數,通過圖片特徵與這些權重參數進行運算得到。在Loss函數中增加關於confidence的懲罰項的目的,就是希望網絡能夠學會在不確切知道真是框的位置時也能夠計算預測框的置信度。

其他細節:

我們對模型輸出的平方和誤差進行優化。我們選擇使用平方和誤差,是因爲它易於優化,但是它並不完全符合最大化平均精度(average precision)的目標。它給分類誤差與定位誤差的權重是一樣的,這點可能並不理想。另外,每個圖像都有很多網格單元並沒有包含任何目標,這將這些單元格的“置信度”分數推向零,通常壓制了包含目標的單元格的梯度。這可能導致模型不穩定,從而導致訓練在早期就發散。爲了彌補平方和誤差的缺陷,我們增加了邊界框座標預測的損失,並減少了不包含目標的框的置信度預測的損失。 我們使用兩個參數λcoord和λnoobj來實現這一點。 我們設定λcoord= 5和λnoobj= .5。

使用平方根的原因是,平方根的特點是數值越小導數越大,數值越大,導數越小,所以誤差分析這,使用平方根說明當w和h很大時所帶來的懲罰比w和h很小時帶來的懲罰小。

平方和誤差對大框和小框的誤差權衡是一樣的,而我們的錯誤指標應該要體現出,大框的小偏差的重要性不如小框的小偏差的重要性。爲了部分解決這個問題,我們直接預測邊界框寬度和高度的平方根,而不是寬度和高度

參考:

https://blog.csdn.net/c20081052/article/details/80236015

https://www.zhihu.com/tardis/sogou/art/35416826

最終輸出

原論文最終輸出的是7*7*30的張量,7*7表示的,一開始將圖片分成了7*7個網格。最終是每一個網格內還有30個數值標量,這30個數值標量分別是兩個bounding box的(x,y,w,h,confidence),和20個類別各自出現的概率(p1,…,p20)

所以假設分成S*S的網格,每個網格B個bounding box,需要識別C個類別,則最後輸出的形狀應該是(S,S,5B+C)

通過對最終輸出的張量切片可以知道每一個網格的bounding box位置、大小、是否存在物體、屬於各個類別的概率。
預測時判斷某一個網格內物體屬於某類應該用類別條件概率乘存在置信度作爲判斷類別的標準:

在這裏插入圖片描述在這裏插入圖片描述參考:https://www.jianshu.com/p/13ec2aa50c12

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章