https://mp.weixin.qq.com/s/FlOrkZ3HnqxNIO_pjdJ4cA
你一定從未看過如此通俗易懂的YOLO系列(從V1到V5)模型解讀!
本文采用生動有趣的語言和案例,對YOLO系列模型的原理、設計及改進思路進行了詳細解讀,干貨滿滿,不僅適合新手入門閱讀,也能幫助大家進一步加深理解。
0 前言
本文目的是用盡量淺顯易懂的語言讓零基礎小白能夠理解什么是YOLO系列模型,以及他們的設計思想和改進思路分別是什么。我不會把YOLO的論文給你用軟件翻譯一遍,這樣做毫無意義;也不會使用太專業晦澀的名詞和表達,對於每一個新的概念都會解釋得盡量通俗一些,目的是使得你能像看故事一樣學習YOLO模型,我覺得這樣的學習方式才是知乎博客的意義所在。
為了使本文盡量生動有趣,我用葫蘆娃作為例子展示YOLO的過程。

同時,會對YOLO v1和YOLOv5的代碼進行解讀,其他的版本就只介紹改進了。
1 先從一款強大的app說起

i detection APP
YOLO v5其實一開始是以一款app進入人們的視野的,就是上圖的這個,叫:i detection(圖上標的是YOLO v4,但其實算法是YOLO v5),使用iOS系列的小伙伴呢,就可以立刻點贊后關掉我這篇文章,去下載這個app玩一玩。在任何場景下(工業場景,生活場景等等)都可以試試這個app和這個算法,這個app中間還有一個button,來調節app使用的模型的大小,更大的模型實時性差但精度高,更小的模型實時性好但精度差。
值得一提的是,這款app就是YOLO v5的作者親自完成的。而且,我寫這篇文章的時候YOLO v5的論文還沒有出來,還在實驗中,等論文出來應該是2020年底或者2021年初了。
讀到這里,你覺得YOLO v5的最大特點是什么?
答案就是:一個字:快,應用於移動端,模型小,速度快。
首先我個人覺得任何一個模型都有下面3部分組成:
- 前向傳播部分:90%
- 損失函數部分
- 反向傳播部分
其中前向傳播部分占用的時間應該在90%左右,即搞清楚前向傳播部分也就搞清楚了這模型的實現流程和細節。本着這一原則,我們開始YOLO系列模型的解讀:
2 不得不談的分類模型
在進入目標檢測任務之前首先得學會圖像分類任務,這個任務的特點是輸入一張圖片,輸出是它的類別。
對於輸入圖片,我們一般用一個矩陣表示。
對於輸出結果,我們一般用一個one-hot vector表示: ,哪一維是1,就代表圖片屬於哪一類。
所以,在設計神經網絡時,結構大致應該長這樣:
img cbrp16 cbrp32 cbrp64 cbrp128 ... fc256-fc[10]
這里的cbrp指的是conv,bn,relu,pooling的串聯。
由於輸入要是one-hot形式,所以最后我們設計了2個fc層(fully connencted layer),我們稱之為“分類頭”或者“決策層”。
3 YOLO系列思想的雛形:YOLO v0
有了上面的分類器,我們能不能用它來做檢測呢?
要回答這個問題,首先得看看檢測器和分類器的輸入輸出有什么不一樣。首先他們的輸入都是image,但是分類器的輸出是一個one-hot vector,而檢測器的輸出是一個框(Bounding Box)。
框,該怎么表示?
在一個圖片里面表示一個框,有很多種方法,比如:
- x,y,w,h(如上圖)
- p1,p2,p3,p4(4個點坐標)
- cx,cy,w,h(cx,cy為中心點坐標)
- x,y,w,h,angle(還有的目標是有角度的,這時叫做Rotated Bounding Box)
- ......
所以表示的方法不是一成不變的,但你會發現:不管你用什么形式去表達這個Bounding Box,你模型輸出的結果一定是一個vector,那這個vector和分類模型輸出的vector本質上有什么區別嗎?
答案是:沒有,都是向量而已,只是分類模型輸出是one-hot向量,檢測模型輸出是我們標注的結果。
所以你應該會發現,檢測的方法呼之欲出了。那分類模型可以用來做檢測嗎?
當然可以,這時,你可以把檢測的任務當做是遍歷性的分類任務。
如何遍歷?
我們的目標是一個個框,那就用這個框去遍歷所有的位置,所有的大小。
比如下面這張圖片,我需要你檢測葫蘆娃的臉,如圖1所示:

圖1:檢測葫蘆娃的臉
我們可以對邊框的區域進行二分類:屬於頭或者不屬於頭。
你先預設一個框的大小,然后在圖片上遍歷這個框,比如第一行全都不是頭。第4個框只有一部分目標在,也不算。第5號框算是一個頭,我們記住它的位置。這樣不斷地滑動,就是遍歷性地分類。
接下來要遍歷框的大小:因為你剛才是預設一個框的大小,但葫蘆娃的頭有大有小,你還得遍歷框的大小,如下圖2所示:

圖2:遍歷框的大小
還沒有結束,剛才滑窗時是挨個滑,但其實沒有遍歷所有的位置,更精確的遍歷方法應該如下圖3所示:

圖3:更精確地遍歷框的位置
這種方法其實就是RCNN全家桶的初衷,專業術語叫做:滑動窗口分類方法。
現在需要你思考一個問題:這種方法的精確和什么因素有關?
答案是:遍歷得徹不徹底。遍歷得越精確,檢測器的精度就越高。所以這也就帶來一個問題就是:檢測的耗時非常大。
舉個例子:比如輸入圖片大小是(800,1000)也就意味着有800000個位置。窗口大小最小 ,最大 ,所以這個遍歷的次數是無限次。我們看下偽代碼:

滑動窗口分類方法偽代碼
那這種方法如何訓練呢?
本質上還是訓練一個二分類器。這個二分類器的輸入是一個框的內容,輸出是(前景/背景)。
第1個問題:
框有不同的大小,對於不同大小的框,輸入到相同的二分類器中嗎?
是的。要先把不同大小的input歸一化到統一的大小。
第2個問題:
背景圖片很多,前景圖片很少:二分類樣本不均衡。
確實是這樣,你看看一張圖片有多少框對應的是背景,有多少框才是葫蘆娃的頭。
以上就是傳統檢測方法的主要思路:
- 耗時。
- 操作復雜,需要手動生成大量的樣本。
到現在為止,我們用分類的算法設計了一個檢測器,它存在着各種各樣的問題,現在是優化的時候了(接下來正式進入YOLO系列方法了):
YOLO的作者當時是這么想的:你分類器輸出一個one-hot vector,那我把它換成(x,y,w,h,c),c表示confidence置信度,把問題轉化成一個回歸問題,直接回歸出Bounding Box的位置不就好了嗎?
剛才的分類器是:img cbrp16 cbrp32 cbrp64 cbrp128 ... fc256-fc[10]
現在我變成:img cbrp16 cbrp32 cbrp64 cbrp128 ... fc256-fc[5],這個輸出是(x,y,w,h,c),不就變成了一個檢測器嗎?
本質上都是矩陣映射到矩陣,只是代表的意義不一樣而已。
傳統的方法為什么沒有這么做呢?我想肯定是效果不好,終其原因是算力不行,conv操作還沒有推廣。
好,現在模型是:
img cbrp16 cbrp32 cbrp64 cbrp128 ... fc256-fc[5] c,x,y,w,h
那如何組織訓練呢?找1000張圖片,把label設置為 。這里 代表真值。有了數據和標簽,就完成了設計。
我們會發現,這種方法比剛才的滑動窗口分類方法簡單太多了。這一版的思路我把它叫做YOLO v0,因為它是You Only Look Once最簡單的版本。
4 YOLO v1終於誕生
需求1:YOLO v0只能輸出一個目標,那比如下圖4的多個目標怎么辦呢?
圖4:多個目標情況
你可能會回答:我輸出N個向量不就行了嗎?但具體輸出多少個合適呢?圖4有7個目標,那有的圖片有幾百個目標,你這個N又該如何調整呢?
答:為了保證所有目標都被檢測到,我們應該輸出盡量多的目標。
但這種方法也不是最優的,最優的應該是下圖這樣:

圖5:用一個(c,x,y,w,h)去負責image某個區域的目標
如圖5所示,用一個(c,x,y,w,h)去負責image某個區域的目標。
比如說圖片設置為16個區域,每個區域用1個(c,x,y,w,h)去負責:

圖6:圖片設置為16個區域
就可以一次輸出16個框,每個框是1個(c,x,y,w,h),如圖6所示。
為什么這樣子更優?因為conv操作是位置強相關的,就是原來的目標在哪里,你conv之后的feature map上還在哪里,所以圖片划分為16個區域,結果也應該分布在16個區域上,所以我們的結果(Tensor)的維度size是:(5,4,4)。
那現在你可能會問:c的真值該怎么設置呢?
答:看葫蘆娃的大娃,他的臉跨了4個區域(grid),但只能某一個grid的c=1,其他的c=0。那么該讓哪一個grid的c=1呢?就看他的臉的中心落在了哪個grid里面。根據這一原則,c的真值為下圖7所示:
但是你發現7個葫蘆娃只有6個1,原因是某一個grid里面有2個目標,確實如此,第三行第三列的grid既有水娃又有隱身娃。這種一個區域有多個目標的情況我們目前沒法解決,因為我們的模型現在能力就這么大,只能在一個區域中檢測出一個目標,如何改進我們馬上就討論,你可以現在先自己想一想。
總之現在我們設計出了模型的輸出結果,那距離完成模型的設計還差一個損失函數,那Loss咋設計呢?看下面的偽代碼:
loss = 0
for img in img_all:
for i in range(4):
for j in range(4):
loss_ij = lamda_1*(c_pred-c_label)**2 + c_label*(x_pred-x_label)**2 +\
c_label*(y_pred-y_label)**2 + c_label*(w_pred-w_label)**2 + \
c_label*(h_pred-h_label)**2
loss += loss_ij
loss.backward()
好現在模型設計完了,回到剛才的問題:模型現在能力就這么大,只能在一個區域中檢測出一個目標,如何改進?遍歷所有圖片,遍歷所有位置,計算loss。
答:剛才區域是 ,現在變成 ,或者更大,使區域更密集,就可以緩解多個目標的問題,但無法從根本上去解決。
另一個問題,按上面的設計你檢測得到了16個框,可是圖片上只有7個葫蘆娃的臉,怎么從16個結果中篩選出7個我們要的呢?答:
法1:聚類。聚成7類,在這7個類中,選擇confidence最大的框。聽起來挺好。
法1的bug:2個目標本身比較近聚成了1個類怎么辦?如果不知道到底有幾個目標呢?為何聚成7類?不是3類?
法2:NMS(非極大值抑制)。2個框重合度很高,大概率是一個目標,那就只取一個框。
重合度的計算方法:交並比IoU=兩個框的交集面積/兩個框的並集面積。
具體算法:

面試必考的NMS
法1的bug:2個目標本身比較近怎么辦?依然沒有解決。
如果不知道到底有幾個目標呢?NMS自動解決了這個問題。
面試的時候會問這樣一個問題:NMS的適用情況是什么?
答:1圖多目標檢測時用NMS。
到現在為止我們終於解決了第4節開始提出的多個目標的問題,現在又有了新的需求:
需求2:多類的目標怎么辦呢?比如說我現在既要你檢測葫蘆娃的臉,又要你檢測葫蘆娃的葫蘆,怎么設計?
img cbrp16 cbrp32 cbrp64 cbrp128 ... fc256-fc[5+2]*N [c,x,y,w,h,one-hot]*N
2個類,one-hot就是[0,1],[1,0]這樣子,如下圖8所示:

圖8:多類的目標的label
偽代碼依然是:
loss = 0
for img in img_all:
for i in range(3):
for j in range(4):
c_loss = lamda_1*(c_pred-c_label)**2
geo_loss = c_label*(x_pred-x_label)**2 +\
c_label*(y_pred-y_label)**2 + c_label*(w_pred-w_label)**2 + \
c_label*(h_pred-h_label)**2
class_loss = 1/m * mse_loss(p_pred, p_label)
loss_ij =c_loss + geo_loss + class_loss
loss += loss_ij
loss.backward()
至此,多個類的問題也解決了,現在又有了新的需求:
需求3:小目標檢測怎么辦呢?小目標總是檢測不佳,所以我們專門設計神經元去擬合小目標。

圖9:多類的小目標的label,分別預測大目標和小目標
對於每個區域,我們用2個五元組(c,x,y,w,h),一個負責回歸大目標,一個負責回歸小目標,同樣添加one-hot vector,one-hot就是[0,1],[1,0]這樣子,來表示屬於哪一類(葫蘆娃的頭or葫蘆娃的葫蘆)。
偽代碼變為了:
loss = 0
for img in img_all:
for i in range(3):
for j in range(4):
c_loss = lamda_1*(c_pred-c_label)**2
geo_loss = c_label_big*(x_big_pred-x_big_label)**2 +\
c_label_big*(y_big_pred-y_big_label)**2 + c_label_big*(w_big_pred-w_big_label)**2 + \
c_label_big*(h_big_pred-h_big_label)**2 +\
c_label_small*(x_small_pred-x_small_label)**2 +\
c_label_small*(y_small_pred-y_small_label)**2 + c_label_small*(w_small_pred-w_small_label)**2 + \
c_label_small*(h_small_pred-h_small_label)**2
class_loss = 1/m * mse_loss(p_pred, p_label)
loss_ij =c_loss + geo_loss + class_loss
loss += loss_ij
loss.backward()
至此,小目標的問題也有了解決方案。
到這里,我們設計的檢測器其實就是YOLO v1,只是有的參數跟它不一樣,我們看論文里的圖:

YOLO v1

YOLO v1
YOLO v1其實就是把我們划分的16個區域變成了 個區域,我們預測16個目標,YOLO v1預測49個目標。我們是2類(葫蘆娃的頭or葫蘆娃的葫蘆),YOLO v1是20類。
backbone也是一堆卷積+檢測頭(FC層),所以說設計到現在,我們其實是把YOLO v1給設計出來了。
再看看作者的解釋:

發現train的時候用的小圖片,檢測的時候用的是大圖片(肯定是經過了無數次試驗證明了效果好)。
結構學完了,再看loss函數,並比較下和我們設計的loss函數有什么區別。

YOLO v1 loss函數
解讀一下這個損失函數:
我們之前說的損失函數是設計了3個for循環,而作者為了方便寫成了求和的形式:
- 前2行計算前景的geo_loss。
- 第3行計算前景的confidence_loss。
- 第4行計算背景的confidence_loss。
- 第5行計算分類損失class_loss。
偽代碼上面已經有了,現在我們總體看一下這個模型:
img cbrp192 cbrp256 cbrp512 cbrp1024 ... fc4096-fc[5+2]*N
檢測層的設計:回歸坐標值+one-hot分類

檢測層的設計
樣本不均衡的問題解決了嗎?沒有計算背景的geo_loss,只計算了前景的geo_loss,這個問題YOLO v1回避了,依然存在。
最后我們解讀下YOLO v1的代碼:
1.模型定義:
定義特征提取層:
class VGG(nn.Module):
def __init__(self):
super(VGG,self).__init__()
# the vgg's layers
#self.features = features
cfg = [64,64,'M',128,128,'M',256,256,256,'M',512,512,512,'M',512,512,512,'M']
layers= []
batch_norm = False
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2,stride = 2)]
else:
conv2d = nn.Conv2d(in_channels,v,kernel_size=3,padding = 1)
if batch_norm:
layers += [conv2d,nn.Batchnorm2d(v),nn.ReLU(inplace=True)]
else:
layers += [conv2d,nn.ReLU(inplace=True)]
in_channels = v
# use the vgg layers to get the feature
self.features = nn.Sequential(*layers)
# 全局池化
self.avgpool = nn.AdaptiveAvgPool2d((7,7))
# 決策層:分類層
self.classifier = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,1000),
)
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias,0)
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,1)
elif isinstance(m,nn.Linear):
nn.init.normal_(m.weight,0,0.01)
nn.init.constant_(m.bias,0)
def forward(self,x):
x = self.features(x)
x_fea = x
x = self.avgpool(x)
x_avg = x
x = x.view(x.size(0),-1)
x = self.classifier(x)
return x,x_fea,x_avg
def extractor(self,x):
x = self.features(x)
return x
定義檢測頭:
self.detector = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,1470),
)
整體模型:
class YOLOV1(nn.Module):
def __init__(self):
super(YOLOV1,self).__init__()
vgg = VGG()
self.extractor = vgg.extractor
self.avgpool = nn.AdaptiveAvgPool2d((7,7))
# 決策層:檢測層
self.detector = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
#nn.Linear(4096,1470),
nn.Linear(4096,245),
#nn.Linear(4096,5),
)
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias,0)
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,1)
elif isinstance(m,nn.Linear):
nn.init.normal_(m.weight,0,0.01)
nn.init.constant_(m.bias,0)
def forward(self,x):
x = self.extractor(x)
#import pdb
#pdb.set_trace()
x = self.avgpool(x)
x = x.view(x.size(0),-1)
x = self.detector(x)
b,_ = x.shape
#x = x.view(b,7,7,30)
x = x.view(b,7,7,5)
#x = x.view(b,1,1,5)
return x
主函數:
if __name__ == '__main__':
vgg = VGG()
x = torch.randn(1,3,512,512)
feature,x_fea,x_avg = vgg(x)
print(feature.shape)
print(x_fea.shape)
print(x_avg.shape)
yolov1 = YOLOV1()
feature = yolov1(x)
# feature_size b*7*7*30
print(feature.shape)
2.模型訓練:
主函數:
if __name__ == "__main__":
train()
下面看train()函數:
def train():
for epoch in range(epochs):
ts = time.time()
for iter, batch in enumerate(train_loader):
optimizer.zero_grad()
# 取圖片
inputs = input_process(batch)
# 取標注
labels = target_process(batch)
# 獲取得到輸出
outputs = yolov1_model(inputs)
#import pdb
#pdb.set_trace()
#loss = criterion(outputs, labels)
loss,lm,glm,clm = lossfunc_details(outputs,labels)
loss.backward()
optimizer.step()
#print(torch.cat([outputs.detach().view(1,5),labels.view(1,5)],0).view(2,5))
if iter % 10 == 0:
# print(torch.cat([outputs.detach().view(1,5),labels.view(1,5)],0).view(2,5))
print("epoch{}, iter{}, loss: {}, lr: {}".format(epoch, iter, loss.data.item(),optimizer.state_dict()['param_groups'][0]['lr']))
#print("Finish epoch {}, time elapsed {}".format(epoch, time.time() - ts))
#print("*"*30)
#val(epoch)
scheduler.step()
訓練過程比較常規,先取1個batch的訓練數據,分別得到inputs和labels,依次計算loss,反傳,step等。
下面說下2個訓練集的數據處理函數:
input_process:
def input_process(batch):
#import pdb
#pdb.set_trace()
batch_size=len(batch[0])
input_batch= torch.zeros(batch_size,3,448,448)
for i in range(batch_size):
inputs_tmp = Variable(batch[0][i])
inputs_tmp1=cv2.resize(inputs_tmp.permute([1,2,0]).numpy(),(448,448))
inputs_tmp2=torch.tensor(inputs_tmp1).permute([2,0,1])
input_batch[i:i+1,:,:,:]= torch.unsqueeze(inputs_tmp2,0)
return input_batch
batch[0]為image,batch[1]為label,batch_size為1個batch的圖片數量。
batch[0][i]為這個batch的第i張圖片,inputs_tmp2為尺寸變成了3,448,448之后的圖片,再經過unsqueeze操作拓展1維,size=[1,3,448,448],存儲在input_batch中。
最后,返回的是size=[batch_size,3,448,448]的輸入數據。
target_process:
def target_process(batch,grid_number=7):
# batch[1]表示label
# batch[0]表示image
batch_size=len(batch[0])
target_batch= torch.zeros(batch_size,grid_number,grid_number,30)
#import pdb
#pdb.set_trace()
for i in range(batch_size):
labels = batch[1]
batch_labels = labels[i]
#import pdb
#pdb.set_trace()
number_box = len(batch_labels['boxes'])
for wi in range(grid_number):
for hi in range(grid_number):
# 遍歷每個標注的框
for bi in range(number_box):
bbox=batch_labels['boxes'][bi]
_,himg,wimg = batch[0][i].numpy().shape
bbox = bbox/ torch.tensor([wimg,himg,wimg,himg])
#import pdb
#pdb.set_trace()
center_x= (bbox[0]+bbox[2])*0.5
center_y= (bbox[1]+bbox[3])*0.5
#print("[%s,%s,%s],[%s,%s,%s]"%(wi/grid_number,center_x,(wi+1)/grid_number,hi/grid_number,center_y,(hi+1)/grid_number))
if center_x<=(wi+1)/grid_number and center_x>=wi/grid_number and center_y<=(hi+1)/grid_number and center_y>= hi/grid_number:
#pdb.set_trace()
cbbox = torch.cat([torch.ones(1),bbox])
# 中心點落在grid內,
target_batch[i:i+1,wi:wi+1,hi:hi+1,:] = torch.unsqueeze(cbbox,0)
#else:
#cbbox = torch.cat([torch.zeros(1),bbox])
#import pdb
#pdb.set_trace()
#rint(target_batch[i:i+1,wi:wi+1,hi:hi+1,:])
#target_batch[i:i+1,wi:wi+1,hi:hi+1,:] = torch.unsqueeze(cbbox,0)
return target_batch
要從batch里面獲得label,首先要想清楚label(就是bounding box)應該是什么size,輸出的結果應該是 的,所以label的size應該是:[batch_size,7,7,30]。在這個程序里我們實現的是輸出 。這個 就是x,y,w,h,所以label的size應該是:[batch_size,7,7,5]
batch_labels表示這個batch的第i個圖片的label,number_box表示這個圖有幾個真值框。
接下來3重循環遍歷每個grid的每個框,bbox表示正在遍歷的這個框。
bbox = bbox/ torch.tensor([wimg,himg,wimg,himg])表示對x,y,w,h進行歸一化。
接下來if語句得到confidence的真值,存儲在target_batch中返回。
最后是loss函數:
def lossfunc_details(outputs,labels):
# 判斷維度
assert ( outputs.shape == labels.shape),"outputs shape[%s] not equal labels shape[%s]"%(outputs.shape,labels.shape)
#import pdb
#pdb.set_trace()
b,w,h,c = outputs.shape
loss = 0
#import pdb
#pdb.set_trace()
conf_loss_matrix = torch.zeros(b,w,h)
geo_loss_matrix = torch.zeros(b,w,h)
loss_matrix = torch.zeros(b,w,h)
for bi in range(b):
for wi in range(w):
for hi in range(h):
#import pdb
#pdb.set_trace()
# detect_vector=[confidence,x,y,w,h]
detect_vector = outputs[bi,wi,hi]
gt_dv = labels[bi,wi,hi]
conf_pred = detect_vector[0]
conf_gt = gt_dv[0]
x_pred = detect_vector[1]
x_gt = gt_dv[1]
y_pred = detect_vector[2]
y_gt = gt_dv[2]
w_pred = detect_vector[3]
w_gt = gt_dv[3]
h_pred = detect_vector[4]
h_gt = gt_dv[4]
loss_confidence = (conf_pred-conf_gt)**2
#loss_geo = (x_pred-x_gt)**2 + (y_pred-y_gt)**2 + (w_pred**0.5-w_gt**0.5)**2 + (h_pred**0.5-h_gt**0.5)**2
loss_geo = (x_pred-x_gt)**2 + (y_pred-y_gt)**2 + (w_pred-w_gt)**2 + (h_pred-h_gt)**2
loss_geo = conf_gt*loss_geo
loss_tmp = loss_confidence + 0.3*loss_geo
#print("loss[%s,%s] = %s,%s"%(wi,hi,loss_confidence.item(),loss_geo.item()))
loss += loss_tmp
conf_loss_matrix[bi,wi,hi]=loss_confidence
geo_loss_matrix[bi,wi,hi]=loss_geo
loss_matrix[bi,wi,hi]=loss_tmp
#打印出batch中每張片的位置loss,和置信度輸出
print(geo_loss_matrix)
print(outputs[0,:,:,0]>0.5)
return loss,loss_matrix,geo_loss_matrix,conf_loss_matrix
首先需要注意:label和output的size應該是:[batch_size,7,7,5]。
outputs[bi,wi,hi]就是一個5位向量: 。
我們分別計算了loss_confidence和loss_geo,因為我們實現的這個模型只檢測1個類,所以沒有class_loss。
總結:
YOLO v1:直接回歸出位置。
YOLO v2:全流程多尺度方法。
YOLO v3:多尺度檢測頭,resblock darknet53
YOLO v4:cspdarknet53,spp,panet,tricks
接下來,先回顧下YOLO v1的模型結構,如下面2圖所示:

YOLO

YOLO
我們認為,檢測模型=特征提取器+檢測頭
在YOLO v1的模型中檢測頭就是最后的2個全連接層(Linear in PyTorch),它們是參數量最大的2個層,也是最值得改進的2個層。后面的YOLO模型都對這里進行改進:
YOLO v1一共預測49個目標,一共98個框。
5 YOLO v2
- 檢測頭的改進:
YOLO v1雖然快,但是預測的框不准確,很多目標找不到:
- 預測的框不准確:准確度不足。
- 很多目標找不到:recall不足。
我們一個問題一個問題解決,首先第1個:
- 問題1:預測的框不准確:
當時別人是怎么做的?
同時代的檢測器有R-CNN,人家預測的是偏移量。
什么是偏移量?

YOLO v2
之前YOLO v1直接預測x,y,w,h,范圍比較大,現在我們想預測一個稍微小一點的值,來增加准確度。
不得不先介紹2個新概念:基於grid的偏移量和基於anchor的偏移量。什么意思呢?
基於anchor的偏移量的意思是,anchor的位置是固定的,偏移量=目標位置-anchor的位置。
基於grid的偏移量的意思是,grid的位置是固定的,偏移量=目標位置-grid的位置。
Anchor是什么玩意?
Anchor是R-CNN系列的一個概念,你可以把它理解為一個預先定義好的框,它的位置,寬高都是已知的,是一個參照物,供我們預測時參考。
上面的圖就是YOLO v2給出的改進,你可能現在看得一臉懵逼,我先解釋下各個字母的含義:
:模型最終得到的的檢測結果。
:模型要預測的值。
:grid的左上角坐標,如下圖所示。
:Anchor的寬和高,這里的anchor是人為定好的一個框,寬和高是固定的。

通過這樣的定義我們從直接預測位置改為預測一個偏移量,基於Anchor框的寬和高和grid的先驗位置的偏移量,得到最終目標的位置,這種方法也叫作location prediction。
這里還涉及到一個尺寸問題:
剛才說到 是模型要預測的值,這里 為grid的坐標,畫個圖就明白了:

圖1:原始值
如圖1所示,假設此圖分為9個grid,GT如紅色的框所示,Anchor如紫色的框所示。圖中的數字為image的真實信息。
我們首先會對這些值歸一化,結果如下圖2所示:

圖2:要預測的值
歸一化之后你會發現,要預測的值就變為了:
這是一個偏移量,且值很小,有利於神經網絡的學習。
**你可能會有疑問:**為什么YOLO v2改預測偏移量而不是直接去預測 ?
上面我說了作者看到了同時代的R-CNN,人家預測的是偏移量。另一個重要的原因是:直接預測位置會導致神經網絡在一開始訓練時不穩定,使用偏移量會使得訓練過程更加穩定,性能指標提升了5%左右。
位置上不使用Anchor框,寬高上使用Anchor框。以上就是YOLO v2的一個改進。用了YOLO v2的改進之后確實是更准確了,但別激動,上面還有一個問題呢~
- 問題2:很多目標找不到:
你還記得上一篇講得YOLO v1一次能檢測多少個目標嗎?答案是49個目標,98個框,並且2個框對應一個類別。可以是大目標也可以是小目標。因為輸出的尺寸是:[N, 7, 7, 30]。式中N為圖片數量,7,7為49個區域(grid)。
YOLO v2首先把 個區域改為 個區域,每個區域有5個anchor,且每個anchor對應着1個類別,那么,輸出的尺寸就應該為:[N,13,13,125]。
這里面有個bug,就是YOLO v2先對每個區域得到了5個anchor作為參考,那你就會問了:每個區域的5個anchor是如何得到的?
下圖可以回答你的問題:

methods to get the 5 anchor
方法:對於任意一個數據集,就比如說COCO吧(紫色的anchor),先對訓練集的GT bounding box進行聚類,聚成幾類呢?作者進行了實驗之后發現5類的recall vs. complexity比較好,現在聚成了5類,當然9類的mAP最好,預測的最全面,但是在復雜度上升很多的同時對模型的准確度提升不大,所以采用了一個比較折中的辦法選取了5個聚類簇,即使用5個先驗框。
所以到現在為止,有了anchor再結合剛才的 ,就可以求出目標位置。
anchor是從數據集中統計得到的。
損失函數為:

YOLO v2損失函數
這里的W=13,H=13,A=5。
每個都是一個權重值。c表示類別,r表示rectangle,即(x,y,w,h)。
第1,4行是confidence_loss,注意這里的真值變成了0和IoU(GT, anchor)的值,你看看這些細節......
第5行是class_loss。
第2,3行:t是迭代次數,即前12800步我們計算這個損失,后面不計算了。這部分意義何在?
意思是:前12800步我們會優化預測的(x,y,w,h)與anchor的(x,y,w,h)的距離+預測的(x,y,w,h)與GT的(x,y,w,h)的距離,12800步之后就只優化預測的(x,y,w,h)與GT的(x,y,w,h)的距離,為啥?因為這時的預測結果已經較為准確了,anchor已經滿足我了我們了,而在一開始預測不准的時候,用上anchor可以加速訓練。
你看看這操作多么的細節......
是什么?第k個anchor與所有GT的IoU的maximum,如果大於一個閾值,就,否則的話:。
好,到現在為止,YOLO v2做了這么多改進,整體性能大幅度提高,但是小目標檢測仍然是YOLO v2的痛。直到kaiming大神的ResNet出現,backbone可以更深了,所以darknet53誕生。
最后我們做個比較:
6 YOLO v3
- 檢測頭的改進:
之前在說小目標檢測仍然是YOLO v2的痛,YOLO v3是如何改進的呢?如下圖所示。

YOLO v3
我們知道,YOLO v2的檢測頭已經由YOLO v1的 變為 了,我們看YOLO v3檢測頭分叉了,分成了3部分:
- 13*13*3*(4+1+80)
- 26*26*3*(4+1+80)
- 52*52*3*(4+1+80)
預測的框更多更全面了,並且分級了。
我們發現3個分支分別為32倍下采樣,16倍下采樣,8倍下采樣,分別取預測大,中,小目標。為什么這樣子安排呢?
因為32倍下采樣每個點感受野更大,所以去預測大目標,8倍下采樣每個點感受野最小,所以去預測小目標。專人專事。
發現預測得更准確了,性能又提升了。
又有人會問,你現在是3個分支,我改成5個,6個分支會不會更好?
理論上會,但還是那句話,作者遵循recall vs. complexity的trade off。
圖中的123456789是什么意思?
答:框。每個grid設置9個先驗框,3個大的,3個中的,3個小的。
每個分支預測3個框,每個框預測5元組+80個one-hot vector類別,所以一共size是:
3*(4+1+80)
每個分支的輸出size為:
- [13,13,3*(4+1+80)]
- [26,26,3*(4+1+80)]
- [52,52,3*(4+1+80)]
當然你也可以用5個先驗框,這時每個分支的輸出size為:
- [13,13,5*(4+1+80)]
- [26,26,5*(4+1+80)]
- [52,52,5*(4+1+80)]
就對應了下面這個圖:

YOLO v3
檢測頭是DBL,定義在圖上,沒有了FC。
還有一種畫法,更加直觀一點:

anchor和YOLO v2一樣,依然是從數據集中統計得到的。
- 損失函數為:

YOLO v3損失函數
第4行說明:loss分3部分組成。
第1行代表geo_loss,S代表13,26,52,就是grid是幾乘幾的。B=5。
第2行代表class_loss,和YOLO v2的區別是改成了交叉熵。
第3行代表confidence_loss,和YOLO v2一模一樣。
最后我們做個比較:

YOLO v1 v2和v3的比較
7 疫情都擋不住的YOLO v4
第一次看到YOLO v4公眾號發文是在疫情期間,那時候還來不了學校。不得不說疫情也擋不住作者科研的動力。。。
- 檢測頭的改進:
YOLO v4的作者換成了Alexey Bochkovskiy大神,檢測頭總的來說還是多尺度的,3個尺度,分別負責大中小目標。只不過多了一些細節的改進:
1.Using multi-anchors for single ground truth
之前的YOLO v3是1個anchor負責一個GT,YOLO v4中用多個anchor去負責一個GT。方法是:對於 來說,只要 ,就讓 去負責 。
這就相當於你anchor框的數量沒變,但是選擇的正樣本的比例增加了,就緩解了正負樣本不均衡的問題。
2.Eliminate_grid sensitivity
還記得之前的YOLO v2的這幅圖嗎?YOLO v2,YOLO v3都是預測4個這樣的偏移量

圖3:YOLO v2,YOLO v3要預測的值
這里其實還隱藏着一個問題:
模型預測的結果是: ,那么最終的結果是: 。這個 按理說應該能取到一個grid里面的任意位置。但是實際上邊界的位置是取不到的,因為sigmoid函數的值域是: ,它不是 。所以作者提出的Eliminate_grid sensitivity的意思是:將 的計算公式改為:
這里的1.1就是一個示例,你也可以是1.05,1.2等等,反正要乘上一個略大於1的數,作者發現經過這樣的改動以后效果會再次提升。
3.CIoU-loss
之前的YOLO v2,YOLO v3在計算geo_loss時都是用的MSE Loss,之后人們開始使用IoU Loss。
它可以反映預測檢測框與真實檢測框的檢測效果。
但是問題也很多:不能反映兩者的距離大小(重合度)。同時因為loss=0,沒有梯度回傳,無法進行學習訓練。如下圖4所示,三種情況IoU都相等,但看得出來他們的重合度是不一樣的,左邊的圖回歸的效果最好,右邊的最差:
- 所以接下來的改進是:
, 為同時包含了預測框和真實框的最小框的面積。
GIoU Loss可以解決上面IoU Loss對距離不敏感的問題。但是GIoU Loss存在訓練過程中發散等問題。
- 接下來的改進是:
其中, , 分別代表了預測框和真實框的中心點,且代表的是計算兩個中心點間的歐式距離。代表的是能夠同時包含預測框和真實框的最小閉包區域的對角線距離。
DIoU loss可以直接最小化兩個目標框的距離,因此比GIoU loss收斂快得多。
但是DIoU loss依然存在包含的問題,即:
這2種情況 和 是重合的,DIoU loss的第3項沒有區別,所以在這個意義上DIoU loss依然存在問題。
- 接下來的改進是:
懲罰項如下面公式:
其中 是權重函數,
而 用來度量長寬比的相似性,定義為
完整的 CIoU 損失函數定義:
最后,CIoU loss的梯度類似於DIoU loss,但還要考慮 的梯度。在長寬在 的情況下, 的值通常很小,會導致梯度爆炸,因此在 實現時將替換成1。
CIoU loss實現代碼:
def bbox_overlaps_ciou(bboxes1, bboxes2):
rows = bboxes1.shape[0]
cols = bboxes2.shape[0]
cious = torch.zeros((rows, cols))
if rows * cols == 0:
return cious
exchange = False
if bboxes1.shape[0] > bboxes2.shape[0]:
bboxes1, bboxes2 = bboxes2, bboxes1
cious = torch.zeros((cols, rows))
exchange = True
w1 = bboxes1[:, 2] - bboxes1[:, 0]
h1 = bboxes1[:, 3] - bboxes1[:, 1]
w2 = bboxes2[:, 2] - bboxes2[:, 0]
h2 = bboxes2[:, 3] - bboxes2[:, 1]
area1 = w1 * h1
area2 = w2 * h2
center_x1 = (bboxes1[:, 2] + bboxes1[:, 0]) / 2
center_y1 = (bboxes1[:, 3] + bboxes1[:, 1]) / 2
center_x2 = (bboxes2[:, 2] + bboxes2[:, 0]) / 2
center_y2 = (bboxes2[:, 3] + bboxes2[:, 1]) / 2
inter_max_xy = torch.min(bboxes1[:, 2:],bboxes2[:, 2:])
inter_min_xy = torch.max(bboxes1[:, :2],bboxes2[:, :2])
out_max_xy = torch.max(bboxes1[:, 2:],bboxes2[:, 2:])
out_min_xy = torch.min(bboxes1[:, :2],bboxes2[:, :2])
inter = torch.clamp((inter_max_xy - inter_min_xy), min=0)
inter_area = inter[:, 0] * inter[:, 1]
inter_diag = (center_x2 - center_x1)**2 + (center_y2 - center_y1)**2
outer = torch.clamp((out_max_xy - out_min_xy), min=0)
outer_diag = (outer[:, 0] ** 2) + (outer[:, 1] ** 2)
union = area1+area2-inter_area
u = (inter_diag) / outer_diag
iou = inter_area / union
with torch.no_grad():
arctan = torch.atan(w2 / h2) - torch.atan(w1 / h1)
v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(w2 / h2) - torch.atan(w1 / h1)), 2)
S = 1 - iou
alpha = v / (S + v)
w_temp = 2 * w1
ar = (8 / (math.pi ** 2)) * arctan * ((w1 - w_temp) * h1)
cious = iou - (u + alpha * ar)
cious = torch.clamp(cious,min=-1.0,max = 1.0)
if exchange:
cious = cious.T
return cious
所以最終的演化過程是:
MSE Loss IoU Loss GIoU Loss DIoU Loss CIoU Loss
8 代碼比論文都早的YOLO v5
- 檢測頭的改進:
head部分沒有任何改動,和yolov3和yolov4完全相同,也是三個輸出頭,stride分別是8,16,32,大輸出特征圖檢測小物體,小輸出特征圖檢測大物體。
但采用了自適應anchor,而且這個功能還可以手動打開/關掉,具體是什么意思呢?
加上了自適應anchor的功能,個人感覺YOLO v5其實變成了2階段方法。
先回顧下之前的檢測器得到anchor的方法:
Yolo v2 v3 v4:聚類得到anchor,不是完全基於anchor的,w,h是基於anchor的,而x,y是基於grid的坐標,所以人家叫location prediction。
R-CNN系列:手動指定anchor的位置。
基於anchor的方法是怎么用的:

anchor是怎么用的
有了anchor的 ,和我們預測的偏移量 ,就可以計算出最終的output: 。
之前anchor是固定的,自適應anchor利用網絡的學習功能,讓 也是可以學習的。我個人覺得自適應anchor策略,影響應該不是很大,除非是剛開始設置的anchor是隨意設置的,一般我們都會基於實際項目數據重新運用kmean算法聚類得到anchor,這一步本身就不能少。
最后總結一下:

目標檢測器模型的結構如下圖1所示,之前看過了YOLO v2 v3 v4 v5對於檢測頭和loss函數的改進,如下圖2所示,下面着重介紹backbone的改進:

圖1:檢測器的結構

圖2:YOLO系列比較
我們發現YOLO v1只是把最后的特征分成了 個grid,到了YOLO v2就變成了 個grid,再到YOLO v3 v4 v5就變成了多尺度的**(strides=8,16,32),更加復雜了。
那為什么一代比一代檢測頭更加復雜呢?答案是:因為它們的提特征網絡更加強大了,能夠支撐起檢測頭做更加復雜的操作。**換句話說,如果沒有backbone方面的優化,你即使用這么復雜的檢測頭,可能性能還會更弱。所以引出了今天的話題:backbone的改進。
9 YOLO v1
我們先看看YOLO v1的backbone長什么樣子:

YOLO v1 backbone
最后2層是全連接層,其他使用了大量的卷積層,網絡逐漸變寬,是非常標准化的操作。注意這里面試官可能會問你一個問題:為什么都是卷積,圖上要分開畫出來,不寫在一起?答案是:按照feature map的分辨率畫出來。分辨率A變化到分辨率B的所有卷積畫在了一起。因為寫代碼時經常會這么做,所以問這個問題的意圖是看看你是否經常寫代碼。
然后我們看下檢測類網絡的結構,如下圖3所示,這個圖是YOLO v4中總結的:

圖3:檢測類網絡的結構
YOLO v1沒有Neck,Backbone是GoogLeNet,屬於Dense Prediction。1階段的檢測器屬於Dense Prediction,而2階段的檢測器既有Dense Prediction,又有Sparse Prediction。
10 YOLO v2
為了進一步提升性能,YOLO v2重新訓練了一個darknet-19,如下圖4所示:

圖4:darknet-19
仔細觀察上面的backbone的結構(雙橫線上方),提出3個問題:
- 為什么沒有 卷積了?只剩下了 卷積和 卷積了?
答:vgg net論文得到一個結論, 卷積可以用更小的卷積代替,且 卷積更加節約參數,使模型更小。
網絡可以做得更深,更好地提取到特征。為什么?因為每做一次卷積,后面都會接一個非線性的激活函數,更深意味着非線性的能力更強了。所以,你可能以后再也見不到 卷積了。
另外還用了bottleneck結構(紅色框):
卷積負責擴大感受野, 卷積負責減少參數量。
- 為什么沒有FC層了?
答:使用了GAP(Global Average Pooling)層,把 映射為 ,滿足了輸入不同尺度的image的需求。你不管輸入圖片是 還是 ,最后都給你映射為 。
這對提高檢測器的性能有什么作用呢?
對於小目標的檢測,之前輸入圖片是固定的大小的,小目標很難被檢測准確;現在允許多尺度輸入圖片了,只要把圖片放大,小目標就變成了大目標,提高檢測的精度。
- 為什么最后一層是softmax?
答:因為backbone網絡darknet-19是單獨train的,是按照分類網絡去train的,用的數據集是imagenet,是1000個classes,所以最后加了一個softmax層,使用cross entropy loss。
接下來總結下YOLO v2的網絡結構:
- 圖4中的雙橫線的上半部分(第0-22層)為backbone,train的方法如上文。
- 后面的結構如下圖5所示:

圖5:YOLO v2網絡結構
- 從第23層開始為檢測頭,其實是3個 3 * 3 * 1024 的卷積層。
- 同時增加了一個 passthrough 層(27層),最后使用 1 * 1 卷積層輸出預測結果,輸出結果的size為 。
- route層的作用是進行層的合並(concat),后面的數字指的是合並誰和誰。
- passthrough 層可以把 。
YOLO2 的訓練主要包括三個階段:
1、先在 ImageNet 分類數據集上 預訓練 Darknet-19,此時模型輸入為 224 * 224 ,共訓練 160 個 epochs。(為什么可以這樣訓練?因為有GAP)2、將網絡的輸入調整為 448 * 448(注意在測試的時候使用 416 * 416 大小) ,繼續在 ImageNet 數據集上 finetune 分類模型,訓練 10 個 epochs。3、修改 Darknet-19 分類模型為檢測模型為圖5形態,即:移除最后一個卷積層、global avgpooling 層以及 softmax 層,並且新增了3個 3 * 3 * 1024 卷積層,同時增加了一個 passthrough 層,最后使用 1 * 1 卷積層輸出預測結果,並在 檢測數據集上繼續 finetune 網絡。注意這里圖5有個地方得解釋一下:第25層把第16層進行reorg,即passthrough操作,得到的結果為27層,再與第24層進行route,即concat操作,得到第28層。可視化的圖為:

圖5:YOLO v2可視化結構
11 YOLO v3
先看下YOLO v3的backbone,如下圖6所示:

圖6:YOLO v3 backbone
先聲明下darknet 53指的是convolution層有52層+1個conv層把1024個channel調整為1000個,你會發現YOLO v2中使用的GAP層在YOLO v3中還在用,他還是在ImageNet上先train的backbone,
觀察發現依然是有bottleneck的結構和殘差網絡。
為什么YOLO v3敢用3個檢測頭?因為他的backbone更強大了。
為什么更強大了?因為當時已經出現了ResNet結構。
所以YOLO v3的提高,有一部分功勞應該給ResNet。
再觀察發現YOLO v3沒有Pooling layer了,用的是conv(stride=2)進行下采樣,為什么?
因為Pooling layer,不管是MaxPooling還是Average Pooling,本質上都是下采樣減少計算量,本質上就是不更新參數的conv,但是他們會損失信息,所以用的是conv(stride = 2)進行下采樣。
下圖7是YOLO v3的網絡結構:

圖7:YOLO v3 Structure

圖8:YOLO v3 Structure
特征融合的方式更加直接,沒有YOLO v2的passthrough操作,直接上采樣之后concat在一起。
12 YOLO v4
圖9和圖10展示了YOLO v4的結構:

圖9:YOLO v4 Structure

圖10:YOLO v4 Structure
Yolov4的結構圖和Yolov3相比,因為多了CSP結構,PAN結構,如果單純看可視化流程圖,會覺得很繞,不過在繪制出上面的圖形后,會覺得豁然開朗,其實整體架構和Yolov3是相同的,不過使用各種新的算法思想對各個子結構都進行了改進。
Yolov4的五個基本組件:
- CBM:Yolov4網絡結構中的最小組件,由Conv+Bn+Mish激活函數三者組成。
- CBL:由Conv+Bn+Leaky_relu激活函數三者組成。
- Res unit:借鑒Resnet網絡中的殘差結構,讓網絡可以構建的更深。
- CSPX:借鑒CSPNet網絡結構,由三個卷積層和X個Res unint模塊Concate組成。
- SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,進行多尺度融合。
其他基礎操作:
- Concat:張量拼接,維度會擴充,和Yolov3中的解釋一樣,對應於cfg文件中的route操作。
- add:張量相加,不會擴充維度,對應於cfg文件中的shortcut操作。
Backbone中卷積層的數量:
和Yolov3一樣,再來數一下Backbone里面的卷積層數量。
每個CSPX中包含3+2*X個卷積層,因此整個主干網絡Backbone中一共包含2+(3+2*1)+2+(3+2*2)+2+(3+2*8)+2+(3+2*8)+2+(3+2*4)+1=72。
- 輸入端的改進:
YOLO v4對輸入端進行了改進,主要包括數據增強Mosaic、cmBN、SAT自對抗訓練,使得在卡不是很多時也能取得不錯的結果。
這里介紹下數據增強Mosaic:

Mosaic數據增強
CutMix只使用了兩張圖片進行拼接,而Mosaic數據增強則采用了4張圖片,隨機縮放、隨機裁剪、隨機排布的方式進行拼接。
Yolov4的作者采用了Mosaic數據增強的方式。
主要有幾個優點:- 豐富數據集:隨機使用4張圖片,隨機縮放,再隨機分布進行拼接,大大豐富了檢測數據集,特別是隨機縮放增加了很多小目標,讓網絡的魯棒性更好。
- 減少GPU:可能會有人說,隨機縮放,普通的數據增強也可以做,但作者考慮到很多人可能只有一個GPU,因此Mosaic增強訓練時,可以直接計算4張圖片的數據,使得Mini-batch大小並不需要很大,一個GPU就可以達到比較好的效果。
cmBN的方法如下圖:

cmBN的方法
13 YOLO v5
圖11和圖12展示了YOLO v5的結構:

圖11:YOLO v5 Structure

圖12:YOLO v5 Structure
檢測頭的結構基本上是一樣的,融合方法也是一樣。
Yolov5的基本組件:
- Focus:基本上就是YOLO v2的passthrough。
- CBL:由Conv+Bn+Leaky_relu激活函數三者組成。
- CSP1_X:借鑒CSPNet網絡結構,由三個卷積層和X個Res unint模塊Concate組成。
- CSP2_X:不再用Res unint模塊,而是改為CBL。
- SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,進行多尺度融合,如圖13所示。
提特征的網絡變短了,速度更快。YOLO v5的結構沒有定下來,作者的代碼還在持續更新。

圖13:SPP結構
Focus的slice操作如下圖所示:
這里解釋以下PAN結構是什么意思,PAN結構來自論文Path Aggregation Network,可視化結果如圖15所示:

圖15:PAN結構
可以看到包含了自底向上和自頂向下的連接,值得注意的是這里的紅色虛線和綠色虛線:
FPN的結構把淺層特征傳遞給頂層要經歷幾十甚至上百層,顯然經過這么多層的傳遞,淺層信息(小目標)丟失比較厲害。這里的紅色虛線就象征着ResNet的幾十甚至上百層。
自下而上的路徑由不到10層組成,淺層特征經過FPN的laterial connection連接到 ,再經過bottom-up path augmentation連接到頂層,經過的層數不到10層,能較好地保留淺層的信息。這里的綠色虛線就象征着自下而上的路徑的不到10層。
- 輸入端的改進:
1.Mosaic數據增強,和YOLO v4一樣。
2.自適應錨框計算:
在Yolo算法中,針對不同的數據集,都會有初始設定長寬的錨框。
在網絡訓練中,網絡在初始錨框的基礎上輸出預測框,進而和真實框groundtruth進行比對,計算兩者差距,再反向更新,迭代網絡參數。
因此初始錨框也是比較重要的一部分,比如Yolov5在Coco數據集上初始設定的錨框:
在Yolov3、Yolov4中,訓練不同的數據集時,計算初始錨框的值是通過單獨的程序運行的。
但Yolov5中將此功能嵌入到代碼中,每次訓練時,自適應的計算不同訓練集中的最佳錨框值。
當然,如果覺得計算的錨框效果不是很好,也可以在代碼中將自動計算錨框功能關閉。
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
設置成False,每次訓練時,不會自動計算。
3.自適應圖片縮放
在常用的目標檢測算法中,不同的圖片長寬都不相同,因此常用的方式是將原始圖片統一縮放到一個標准尺寸,再送入檢測網絡中。
比如Yolo算法中常用416*416,608*608等尺寸,比如對下面800*600的圖像進行縮放:,如圖15所示:

圖15:Yolo算法的縮放填充
但Yolov5代碼中對此進行了改進,也是Yolov5推理速度能夠很快的一個不錯的trick。
作者認為,在項目實際使用時,很多圖片的長寬比不同,因此縮放填充后,兩端的黑邊大小都不同,而如果填充的比較多,則存在信息冗余,影響推理速度。
因此在Yolov5的代碼中datasets.py的letterbox函數中進行了修改,對原始圖像自適應的添加最少的黑邊,如圖16所示:

圖16:YOLO v5的自適應填充
圖像高度上兩端的黑邊變少了,在推理時,計算量也會減少,即目標檢測速度會得到提升。
通過這種簡單的改進,推理速度得到了37%的提升,可以說效果很明顯。
Yolov5中填充的是灰色,即(114,114,114),都是一樣的效果,且訓練時沒有采用縮減黑邊的方式,還是采用傳統填充的方式,即縮放到416*416大小。只是在測試,使用模型推理時,才采用縮減黑邊的方式,提高目標檢測,推理的速度。
最后我們對YOLO series做個總體的比較,結束這個系列的解讀:

YOLO v5 series的比較
部分圖源:https://zhuanlan.zhihu.com/p/14374720