計(jì)算機(jī)能理解它們所看到的嗎?他們能區(qū)分狗和貓,男人和女人,汽車(chē)和自行車(chē)嗎?
目標(biāo)檢測(cè)和識(shí)別是當(dāng)今計(jì)算機(jī)視覺(jué)研究的主要領(lǐng)域之一。研究人員正在尋找新的方法使計(jì)算機(jī)理解它們所看到的東西。新的最先進(jìn)的模型正在逐步完善,這些模型大幅超過(guò)了以前的模型。但是,計(jì)算機(jī)實(shí)際上還遠(yuǎn)遠(yuǎn)沒(méi)有看到它們所看到的東西。
在這篇文章中,我將詳細(xì)闡述 YOLO [ You Only Look Once ] —— 一項(xiàng)2016年的研究工作,創(chuàng)造了目標(biāo)檢測(cè)實(shí)時(shí)新高。本文簡(jiǎn)要描述并實(shí)現(xiàn)了 YOLO 模型,幫助您進(jìn)入計(jì)算機(jī)視覺(jué)和計(jì)算機(jī)視覺(jué)目標(biāo)檢測(cè)領(lǐng)域。
YOLO 簡(jiǎn)介
YOLO 將目標(biāo)檢測(cè)重構(gòu)為一個(gè)單一的回歸問(wèn)題,直接從圖像像素到 bounding box 坐標(biāo)和類(lèi)別概率。YOLO 使用單一卷積層同時(shí)預(yù)測(cè)多個(gè) bounding box 及其類(lèi)別概率。因此,YOLO 的一個(gè)主要優(yōu)點(diǎn)就是我們可以用它來(lái)實(shí)現(xiàn)以幀為單位的速度檢測(cè)。即使在這樣的速度下,YOLO 仍然能夠達(dá)到其他實(shí)時(shí)系統(tǒng)的平均精度(mAP)的兩倍以上!
平均平均精度是每個(gè)類(lèi)的平均精度的平均值。換句話說(shuō),平均精度是所有類(lèi)的平均精度。
平均精度方程
檢測(cè)
YOLO 是一個(gè)統(tǒng)一的目標(biāo)檢測(cè)過(guò)程 —— 統(tǒng)一?YOLO 將幾個(gè)涉及目標(biāo)檢測(cè)的任務(wù)統(tǒng)一到一個(gè)神經(jīng)網(wǎng)絡(luò)中。網(wǎng)絡(luò)在進(jìn)行 bounding box 預(yù)測(cè)時(shí)會(huì)考慮到整個(gè)圖像。因此,它能夠在同一時(shí)間預(yù)測(cè)所有 bounding box 及其類(lèi)別。
首先,圖像被分割成一個(gè) S x S 的網(wǎng)格。如果網(wǎng)格的中心位于網(wǎng)格單元格中,則網(wǎng)格單元格應(yīng)該能夠檢測(cè)到對(duì)象。那么,我們所說(shuō)的檢測(cè)是什么意思呢?更準(zhǔn)確地說(shuō),網(wǎng)格單元預(yù)測(cè) B 個(gè) bounding box,并為每個(gè) box 預(yù)測(cè)其對(duì)應(yīng)的分?jǐn)?shù),這個(gè)分?jǐn)?shù)告訴我們模型對(duì)該 box 中包含物體有多高的置信度,以及這個(gè) box 在覆蓋物體方面有多精確。因此,這個(gè)方框給出了5個(gè)預(yù)測(cè)ー x,y,w,h 和置信度評(píng)分。因此,每個(gè)網(wǎng)格給了我們 B 個(gè)預(yù)測(cè)。
除了 bounding box 上的預(yù)測(cè)外,網(wǎng)格單元還給出了 C 個(gè)條件類(lèi)的概率。這些基本上給出了類(lèi)的概率,假設(shè)網(wǎng)格單元包含一個(gè)對(duì)象。換句話說(shuō),假設(shè)一個(gè)對(duì)象存在,條件類(lèi)概率給出了這個(gè)對(duì)象可能屬于哪個(gè)類(lèi)的概率。每個(gè)網(wǎng)格單元只能預(yù)測(cè)一組類(lèi)別概率 —— 與 bounding box 的數(shù)目無(wú)關(guān)。
概述該模型的作用
網(wǎng)絡(luò)
該模型的靈感來(lái)自于圖像分類(lèi)的 GoogleNet 模型。它有24個(gè)卷積層和2個(gè)全連接的層。與 GoogLeNet 使用的初始模塊不同,它使用1 × 1縮減層和3 × 3卷積層。
模型架構(gòu)概述
損失和激活函數(shù)
最后一層使用線性激活函數(shù),而其他所有層使用 Leaky-ReLU 激活函數(shù)。Leaky-ReLU 激活可以表示為:
損失函數(shù)是簡(jiǎn)單和平方誤差,表示為:
平方和誤差
然而,如果我們這樣定義損失函數(shù),就會(huì)把分類(lèi)誤差和局部化誤差混為一談,從模型的角度來(lái)看,兩者都非常重要。
為了防止這種情況發(fā)生,bounding box 坐標(biāo)預(yù)測(cè)的損失會(huì)增加,不包含對(duì)象的 box 的預(yù)測(cè)的損失會(huì)減少。利用兩個(gè)參數(shù) λcoord 和 λnoobj 來(lái)實(shí)現(xiàn)這一結(jié)果:
λcoord 設(shè)為5,λnoobj 設(shè)為0.5(關(guān)于這兩個(gè)超參數(shù)在損失函數(shù)方程中的詳細(xì)說(shuō)明)
SSE 還引入了大小 bounding box 加權(quán)誤差相等的問(wèn)題。為了使這個(gè)問(wèn)題形象化,請(qǐng)看下面的圖片:
狗和碗的圖片(僅供說(shuō)明)ーー谷歌圖片
在這里我們可以看到,狗和碗已經(jīng)被標(biāo)注為兩個(gè)對(duì)象。狗的框的尺寸比較大,碗的框的尺寸比較小,F(xiàn)在,如果狗和碗的邊界框減少了同樣的方形像素,那么碗的標(biāo)注會(huì)更糟糕。為什么?因?yàn)?bounding box 的精度在移位和大小方面應(yīng)該與該 bounding box 的大小相比,而不是圖像的大小。
現(xiàn)在,來(lái)到我們的損失函數(shù),我們可以看到,損失函數(shù)沒(méi)有給任何具體的 bounding box 的大小。它沒(méi)有考慮到這一點(diǎn)。為了克服這一障礙,網(wǎng)絡(luò)不預(yù)測(cè) bounding box 的高度和寬度,而是預(yù)測(cè) bounding box 的平方根。這有助于將差異保持在最低限度。
綜合考慮這些因素,可以將多部分損失函數(shù)寫(xiě)成:
網(wǎng)絡(luò)的總損失函數(shù)
現(xiàn)在我們已經(jīng)完成了 YOLO/目標(biāo)檢測(cè)模型的基礎(chǔ),讓我們深入到代碼中去吧!
構(gòu)建模型
YOLO 網(wǎng)絡(luò)簡(jiǎn)單易于構(gòu)建。問(wèn)題在于 bounding box。繪制邊界框和保存圖像、標(biāo)注置信度得分和類(lèi)別以及配置整個(gè)訓(xùn)練代碼將使本文不必要地冗余。因此,我將實(shí)現(xiàn)該模型。
import torch
from torch import nn
#o = [i + 2*p - k - (k-1)*(d-1)]/s + 1--formula to calculate padding
class Net(nn.Module):
#YOLO model
'''
Input size of the model is
448x448x3
In tensor notation, expressed as [batchsize,3,448,448]
output--
[batchsize,30,7,7]
'''
def __init__(self):
super(Net,self).__init__()
self.t1=nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=(7,7),stride=2,padding=(2,2)),
nn.MaxPool2d(kernel_size=(2,2),stride=2),
)
self.t2=nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=192,kernel_size=(3,3),padding=(1,1)),
nn.MaxPool2d(kernel_size=(2,2),stride=2),
)
self.t3=nn.Sequential(
nn.Conv2d(in_channels=192,out_channels=128,kernel_size=(1,1)),
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=(1,1)),
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=(3,3),padding=(1,1)),
nn.MaxPool2d(kernel_size=(2,2),stride=2),
)
self.t4=nn.Sequential(
nn.Conv2d(in_channels=512,out_channels=256,kernel_size=(1,1)),
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=512,out_channels=256,kernel_size=(1,1)),
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=512,out_channels=256,kernel_size=(1,1)),
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=512,out_channels=256,kernel_size=(1,1)),
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=512,out_channels=512,kernel_size=(1,1)),
nn.Conv2d(in_channels=512,out_channels=1024,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=1024,out_channels=1024,kernel_size=(3,3),stride=2)
)
self.t5=nn.Sequential(
nn.Conv2d(in_channels=1024,out_channels=512,kernel_size=(1,1)),
nn.Conv2d(in_channels=512,out_channels=1024,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=1024,out_channels=512,kernel_size=(1,1)),
nn.Conv2d(in_channels=512,out_channels=1024,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=1024,out_channels=1024,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=1024,out_channels=1024,kernel_size=(3,3),stride=2,padding=(1,1))
)
self.t6=nn.Sequential(
nn.Conv2d(in_channels=1024,out_channels=1024,kernel_size=(3,3),padding=(1,1)),
nn.Conv2d(in_channels=1024,out_channels=1024,kernel_size=(3,3),padding=(1,1))
)
def forward(self,x):
x=self.t1(x)
x=self.t2(x)
x=self.t3(x)
x=self.t4(x)
x=self.t5(x)
x=self.t6(x)
x=torch.flatten(x,1)
x=nn.Linear(x.size(),4096)(x)
x=nn.Linear(4096,7*7*30)(x)
x=x.view(-1,30,7,7)
return x #output of model
YOLO 網(wǎng)絡(luò)模塊