最近,
Scaled-YOLOv4
的作者(也是后來的YOLOR的作者)和YOLOv4
的作者AB大佬再次聯(lián)手推出了YOLOv7,目前來看,這一版的YOLOv7
是一個比較正統(tǒng)的YOLO續(xù)作,畢竟有AB大佬在,得到了過YOLO原作的認(rèn)可。
網(wǎng)上已經(jīng)有了很多文章去從各個方面來測試YOLOv7,但關(guān)于YOLOv7到底長什么樣,似乎還沒有多少人做出介紹。由于YOLOv7再一次平衡好了參數(shù)量、計算量和性能之間的矛盾,所以,筆者也想嘗試YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu)來削減模型的大小,因此,通過查看YOLOv7的config文件,勾勒出了YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu),故而新開此章,斗膽將v7的網(wǎng)絡(luò)結(jié)構(gòu)介紹給各位讀者。請注意,本文只介紹YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu),其余的技術(shù)點如Aux Head是不會涉及到。這一部分,筆者放在了自己的github上,鏈接如下,筆者暫且將YOLOv7的backbone命名為ELAN-Net
。
https://github.com/yjh0410/image_classification_pytorch
一、YOLOv7的backbone結(jié)構(gòu)
我們可以打開官方源碼中的yolov7.yaml
文件,看到如圖1所示的網(wǎng)絡(luò)配置。YOLOv7的項目是繼承自YOLOv5,事實上,YOLOv7的第一作者為YOLO社區(qū)做的貢獻(xiàn),如Pytorch_YOLOv4、caled-YOLOv4、YOLOR等都沿用了YOLOv5的項目,很多超參幾乎就是拿來用了,包括這次的YOLOv7,畢竟YOLOv5項目久經(jīng)考驗,是很適合在它的基礎(chǔ)上做改進(jìn),省去了調(diào)參的麻煩。另外,在上圖中,我們還能看見anchor box,盡管YOLOv7采用的label assignment使用的是SimOTA,但bbox regression還是基于anchor box的,往往在實際任務(wù)中,使用anchor box等人工先驗會對實際任務(wù)帶來些好處。

這里,我們主要看backbone的部分,熟悉YOLOv5的讀者應(yīng)該不難理解這種寫法,我們順著該配置即可勾勒出YOLOv7的backbone。我們詳細(xì)地來說一下,筆者會配合由筆者自己寫的pytorch代碼來幫助讀者理解。畢竟,官方代碼的可讀性實在是一言難盡。
首先,是最開始的stem層
,如圖2所示,就是簡單地堆疊三層Conv卷積,每個Conv就是YOLOv5采用的標(biāo)準(zhǔn)的“卷積+BN+SiLU
”三件套。相應(yīng)的代碼也展示在了下方,筆者將該層命名為“l(fā)ayer1”,輸出一個二倍降采樣的特征圖:

#ELANNetofYOLOv7
classELANNet(nn.Module):
"""
ELAN-NetofYOLOv7.
"""
def__init__(self,depthwise=False,num_classes=1000):
super(ELANNet,self).__init__()
self.layer_1=nn.Sequential(
Conv(3,32,k=3,p=1,depthwise=depthwise),
Conv(32,64,k=3,p=1,s=2,depthwise=depthwise),
Conv(64,64,k=3,p=1,depthwise=depthwise)
)
接下來,YOLOv7再用一層步長為2的卷積得到4倍降采樣圖,然后接了一連串卷積處理這個4被降采樣特征圖,而這些“一連串”的卷積便是就是YOLOv7論文中介紹的ELAN模塊了,官方給出的配置如下所示,我們可以對應(yīng)著論文原圖來一起看:

按照上面的結(jié)構(gòu),我們便可以繪制出YOLOv7的核心模塊:ELAN
的具體網(wǎng)絡(luò)結(jié)構(gòu)了,相應(yīng)的代碼也展示在了下方。請注意,ELAN的這種結(jié)構(gòu)的一個優(yōu)勢就是每個branch的操作中,輸入通道都是和輸出通道保持一致的,僅僅是最開始的兩個1x1卷積
是有通道變化的。關(guān)于輸入輸出通道相等的優(yōu)勢,這一點早在shufflenet-v2中就已經(jīng)論證過了,是一條設(shè)計網(wǎng)絡(luò)的高效準(zhǔn)則之一。
classELANBlock(nn.Module):
"""
ELANBLockofYOLOv7'sbackbone
"""
def__init__(self,in_dim,out_dim,expand_ratio=0.5,depthwise=False):
super(ELANBlock,self).__init__()
inter_dim=int(in_dim*expand_ratio)
self.cv1=Conv(in_dim,inter_dim,k=1)
self.cv2=Conv(in_dim,inter_dim,k=1)
self.cv3=nn.Sequential(
Conv(inter_dim,inter_dim,k=3,p=1,depthwise=depthwise),
Conv(inter_dim,inter_dim,k=3,p=1,depthwise=depthwise)
)
self.cv4=nn.Sequential(
Conv(inter_dim,inter_dim,k=3,p=1,depthwise=depthwise),
Conv(inter_dim,inter_dim,k=3,p=1,depthwise=depthwise)
)
assertinter_dim*4==out_dim
self.out=Conv(inter_dim*4,out_dim,k=1)
defforward(self,x):
"""
Input:
x:[B,C,H,W]
Output:
out:[B,2C,H,W]
"""
x1=self.cv1(x)
x2=self.cv2(x)
x3=self.cv3(x2)
x4=self.cv4(x3)
#[B,C,H,W]->[B,2C,H,W]
out=self.out(torch.cat([x1,x2,x3,x4],dim=1))
returnout

不過,筆者好奇能不能直接拆分通道,一分為二呢?最開始的CSPNet就是這么干的,不過,到了YOLO這里,就換成了1x1卷積來壓縮。最后,ELAN模塊輸出的通道數(shù)是輸入的2倍。
于是,網(wǎng)絡(luò)的第二層也就搭建出來了,第二層輸出的就是4倍降采樣的特征圖了,如下方的代碼所示:
self.layer_2=nn.Sequential(
Conv(64,128,k=3,p=1,s=2,depthwise=depthwise),
ELANBlock(in_dim=128,out_dim=256,expand_ratio=0.5,depthwise=depthwise)
)
接下來,YOLOv7對該4倍降采樣的特征圖再進(jìn)行降采樣操作
,不過,不同于以往的步長為2的卷積那么簡單,YOLOv7這里稍微設(shè)計得精細(xì)了一些,如下圖中的紅框部分所示,左邊的分支主要采用maxpooling(MP)來實現(xiàn)空間降采樣,并緊跟一個1x1卷積壓縮通道;右邊先用1x1卷積壓縮通道,然后再用步長為2的3x3卷積完成降采樣,最后,將兩個分支的結(jié)果合并,通道一個通道數(shù)等于輸入通道數(shù),但空間分辨率縮小2倍的特征圖。筆者暫且將其命名為“DownSample
”層。相應(yīng)代碼已展示在下方。

def __init__(self, in_dim):
super().__init__()
inter_dim = in_dim // 2
self.mp = nn.MaxPool2d((2, 2), 2)
self.cv1 = Conv(in_dim, inter_dim, k=1)
self.cv2 = nn.Sequential(
Conv(in_dim, inter_dim, k=1),
Conv(inter_dim, inter_dim, k=3, p=1, s=2)
)
def forward(self, x):
"""
Input:
x: [B, C, H, W]
Output:
out: [B, C, H//2, W//2]
"""
# [B, C, H, W] -> [B, C//2, H//2, W//2]
x1 = self.cv1(self.mp(x))
x2 = self.cv2(x)
# [B, C, H//2, W//2]
out = torch.cat([x1, x2], dim=1)
return out
隨后,綠框部分就是上面已經(jīng)介紹過的ELAN模塊,對被降采樣的特征圖進(jìn)行處理。自此往后,就是重復(fù)堆疊這兩塊,直到最后。那么,backbone的整體我們就全部了解了,整個backbone的代碼如下:
#ELANNetofYOLOv7
classELANNet(nn.Module):
"""
ELAN-NetofYOLOv7.
"""
def__init__(self,depthwise=False,num_classes=1000):
super(ELANNet,self).__init__()
self.layer_1=nn.Sequential(
Conv(3,32,k=3,p=1,depthwise=depthwise),
Conv(32,64,k=3,p=1,s=2,depthwise=depthwise),
Conv(64,64,k=3,p=1,depthwise=depthwise)#P1/2
)
self.layer_2=nn.Sequential(
Conv(64,128,k=3,p=1,s=2,depthwise=depthwise),
ELANBlock(in_dim=128,out_dim=256,expand_ratio=0.5,depthwise=depthwise)#P2/4
)
self.layer_3=nn.Sequential(
DownSample(in_dim=256),
ELANBlock(in_dim=256,out_dim=512,expand_ratio=0.5,depthwise=depthwise)#P3/8
)
self.layer_4=nn.Sequential(
DownSample(in_dim=512),
ELANBlock(in_dim=512,out_dim=1024,expand_ratio=0.5,depthwise=depthwise)#P4/16
)
self.layer_5=nn.Sequential(
DownSample(in_dim=1024),
ELANBlock(in_dim=1024,out_dim=1024,expand_ratio=0.25,depthwise=depthwise)#P5/32
)
self.avgpool=nn.AdaptiveAvgPool2d((1,1))
self.fc=nn.Linear(1024,num_classes)
defforward(self,x):
x=self.layer_1(x)
x=self.layer_2(x)
x=self.layer_3(x)
x=self.layer_4(x)
x=self.layer_5(x)
#[B,C,H,W]->[B,C,1,1]
x=self.avgpool(x)
#[B,C,1,1]->[B,C]
x=x.flatten(1)
x=self.fc(x)
returnx
當(dāng)然,以上代碼中的avgpool和fc兩個層請忽略,在檢測任務(wù)里我們是不需要這兩部分的。這里需要說一下的是layer5,按照前面幾層的配置,layer5應(yīng)該順其自然地輸出一個通道數(shù)為2048的32倍降采樣的圖,但這樣似乎會引來過多的計算量,YOLOv7就將其通道數(shù)還是控制在了1024。所以,C3、C4和C5的通道數(shù)就分別是512、1024和1024,不再是以往常見的256、512、1024了。Backbone的整體結(jié)構(gòu)展示在了圖6中。

由于整個YOLOv7是采用了train from scratch
策略,超參沿用久經(jīng)考驗的YOLOv5的,所以,backbone這一部門是沒有imagenet pre-trained的。不過,筆者在搭建了這個backbone后,在ImageNet上進(jìn)行了預(yù)訓(xùn)練,感興趣的讀者可以筆者提供的github的README中獲得預(yù)訓(xùn)練模型的下載鏈接。
二、YOLOv7的PaFPN結(jié)構(gòu)
接下來,我們介紹一下YOLOv7的FPN
結(jié)構(gòu)。和之前的YOLOv4、YOLOv5一樣,YOLOv7仍采用PaFPN
結(jié)構(gòu)。有了之前backbone的經(jīng)驗,這一部分也就容易多了,我們先看一下官方給出的配置文件,如下圖所示:

首先,對于backbone最后輸出的32倍降采樣特征圖C5,我們先使用SPP
處理一下。這部分的SPP是由Scaled-YOLOv4提出的SPP-CSP
,在YOLOv5中已經(jīng)被用到了,沒有變化。經(jīng)過SPP的處理后,C5的通道數(shù)從1024縮減到512。隨后的過程和YOLOv5是一樣的,依循top down的路線,先后和C4、C3去融合,得到P3、P4和P5;再按照bottom-up的路線,再去和P4、P5做融合。唯一與YOLOv5不同的地方就是原先YOLOv5使用的BottleneckCSP被換成了YOLOv7的ELAN模塊。原先YOLOv5所使用的步長為2的下采樣卷積也換成了上面的YOLOv7設(shè)計的DownSample層。不過,Head中的ELAN和DownSample兩部分與Backbone中的這兩塊有些細(xì)微差別,具體結(jié)構(gòu)下面的兩圖所示。相應(yīng)的代碼筆者也給出了。


classELANBlock(nn.Module):
"""
ELANBLockofYOLOv7'shead
"""
def__init__(self,in_dim,out_dim,expand_ratio=0.5,depthwise=False,act_type='silu',norm_type='BN'):
super(ELANBlock,self).__init__()
inter_dim=int(in_dim*expand_ratio)
inter_dim2=int(inter_dim*expand_ratio)
self.cv1=Conv(in_dim,inter_dim,k=1,act_type=act_type,norm_type=norm_type)
self.cv2=Conv(in_dim,inter_dim,k=1,act_type=act_type,norm_type=norm_type)
self.cv3=Conv(inter_dim,inter_dim2,k=3,p=1,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.cv4=Conv(inter_dim2,inter_dim2,k=3,p=1,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.cv5=Conv(inter_dim2,inter_dim2,k=3,p=1,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.cv6=Conv(inter_dim2,inter_dim2,k=3,p=1,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.out=Conv(inter_dim*2+inter_dim2*4,out_dim,k=1)
defforward(self,x):
"""
Input:
x:[B,C_in,H,W]
Output:
out:[B,C_out,H,W]
"""
x1=self.cv1(x)
x2=self.cv2(x)
x3=self.cv3(x2)
x4=self.cv4(x3)
x5=self.cv5(x4)
x6=self.cv6(x5)
#[B,C_in,H,W]->[B,C_out,H,W]
out=self.out(torch.cat([x1,x2,x3,x4,x5,x6],dim=1))
returnout
classDownSample(nn.Module):
def__init__(self,in_dim,depthwise=False,act_type='silu',norm_type='BN'):
super().__init__()
inter_dim=in_dim
self.mp=nn.MaxPool2d((2,2),2)
self.cv1=Conv(in_dim,inter_dim,k=1,act_type=act_type,norm_type=norm_type)
self.cv2=nn.Sequential(
Conv(in_dim,inter_dim,k=1,act_type=act_type,norm_type=norm_type),
Conv(inter_dim,inter_dim,k=3,p=1,s=2,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
)
defforward(self,x):
"""
Input:
x:[B,C,H,W]
Output:
out:[B,2C,H//2,W//2]
"""
#[B,C,H,W]->[B,C//2,H//2,W//2]
x1=self.cv1(self.mp(x))
x2=self.cv2(x)
#[B,C,H//2,W//2]
out=torch.cat([x1,x2],dim=1)
returnout
整個PaFPN的融合過程如下圖所示,筆者在途中標(biāo)記出了通道的變化,在PaFPN的最后,YOLOv7使用了兩層RepConv去調(diào)整最終輸出的P3、P4和P5的通道數(shù)。在最后,YOLOv7還是一如既往地使用三層1x1卷積去預(yù)測objectness、class和bbox三部分。注意,YOLOv7還是一如既往地采用coupled head,而非YOLOX中的decoupled head。因為decoupled head會帶來過多的參數(shù)量和計算量,性能提升很微小,性價比不高。Head部分的代碼,筆者也在下方給出了,感興趣的讀者可以查看。

#PaFPN-ELAN(YOLOv7's)
classPaFPNELAN(nn.Module):
def__init__(self,
in_dims=[256,512,512],
out_dim=[256,512,1024],
depthwise=False,
norm_type='BN',
act_type='silu'):
super(PaFPNELAN,self).__init__()
self.in_dims=in_dims
self.out_dim=out_dim
c3,c4,c5=in_dims
#topdwon
##P5->P4
self.cv1=Conv(c5,256,k=1,norm_type=norm_type,act_type=act_type)
self.cv2=Conv(c4,256,k=1,norm_type=norm_type,act_type=act_type)
self.head_elan_1=ELANBlock(in_dim=256+256,
out_dim=256,
depthwise=depthwise,
norm_type=norm_type,
act_type=act_type)
#P4->P3
self.cv3=Conv(256,128,k=1,norm_type=norm_type,act_type=act_type)
self.cv4=Conv(c3,128,k=1,norm_type=norm_type,act_type=act_type)
self.head_elan_2=ELANBlock(in_dim=128+128,
out_dim=128,#128
depthwise=depthwise,
norm_type=norm_type,
act_type=act_type)
#bottomup
#P3->P4
self.mp1=DownSample(128,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.head_elan_3=ELANBlock(in_dim=256+256,
out_dim=256,#256
depthwise=depthwise,
norm_type=norm_type,
act_type=act_type)
#P4->P5
self.mp2=DownSample(256,act_type=act_type,norm_type=norm_type,depthwise=depthwise)
self.head_elan_4=ELANBlock(in_dim=512+512,
out_dim=512,#512
depthwise=depthwise,
norm_type=norm_type,
act_type=act_type)
#RepConv
self.repconv_1=RepConv(128,out_dim[0],k=3,s=1,p=1)
self.repconv_2=RepConv(256,out_dim[1],k=3,s=1,p=1)
self.repconv_3=RepConv(512,out_dim[2],k=3,s=1,p=1)
defforward(self,features):
c3,c4,c5=features
#Topdown
##P5->P4
c6=self.cv1(c5)
c7=F.interpolate(c6,scale_factor=2.0)
c8=torch.cat([c7,self.cv2(c4)],dim=1)
c9=self.head_elan_1(c8)
##P4->P3
c10=self.cv3(c9)
c11=F.interpolate(c10,scale_factor=2.0)
c12=torch.cat([c11,self.cv4(c3)],dim=1)
c13=self.head_elan_2(c12)
#Bottomup
#p3->P4
c14=self.mp1(c13)
c15=torch.cat([c14,c9],dim=1)
c16=self.head_elan_3(c15)
#P4->P5
c17=self.mp2(c16)
c18=torch.cat([c17,c5],dim=1)
c19=self.head_elan_4(c18)
#RepCpnv
c20=self.repconv_1(c13)
c21=self.repconv_2(c16)
c22=self.repconv_3(c19)
out_feats=[c20,c21,c22]#[P3,P4,P5]
returnout_feats
三、結(jié)束語
那么,至此,YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu)就全部繪制出了,本文的目的也達(dá)到了。當(dāng)然,YOLOv7還有E-ELAN
結(jié)構(gòu),這一點,筆者暫且不做介紹了,有了相關(guān)基礎(chǔ),相信讀者們也不難自行分析YOLOv7的其他網(wǎng)絡(luò)結(jié)構(gòu)。對于YOLOv7的其他技術(shù)點,如Aux Head、RepConv的設(shè)計、YOLOR中的隱性知識(Implicit knowledge)等,筆者就不做介紹了。
最后,感謝各位讀者的支持。對于YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu),讀者有任何問題都可以在評論區(qū)留言,僅憑筆者的一己之見,不免會落入某種獨斷與偏見之中。
審核編輯:湯梓紅
-
網(wǎng)絡(luò)結(jié)構(gòu)
+關(guān)注
關(guān)注
0文章
48瀏覽量
11427
原文標(biāo)題:長文詳解YOLOv7的網(wǎng)絡(luò)結(jié)構(gòu)
文章出處:【微信號:zenRRan,微信公眾號:深度學(xué)習(xí)自然語言處理】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)搜索有什么優(yōu)勢?
YOLOv5網(wǎng)絡(luò)結(jié)構(gòu)解析
yolov7 onnx模型在NPU上太慢了怎么解決?
無法使用MYRIAD在OpenVINO trade中運行YOLOv7自定義模型怎么解決?
環(huán)形網(wǎng)絡(luò),環(huán)形網(wǎng)絡(luò)結(jié)構(gòu)是什么?
網(wǎng)絡(luò)結(jié)構(gòu)自動設(shè)計算法——BlockQNN

一種改進(jìn)的深度神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)搜索方法

YOLOv7訓(xùn)練自己的數(shù)據(jù)集包括哪些

一文徹底搞懂YOLOv8【網(wǎng)絡(luò)結(jié)構(gòu)+代碼+實操】

一文徹底搞懂YOLOv8(網(wǎng)絡(luò)結(jié)構(gòu)+代碼+實操)

yolov5和YOLOX正負(fù)樣本分配策略

使用OpenVINO優(yōu)化并部署訓(xùn)練好的YOLOv7模型

深度學(xué)習(xí)YOLOv3 模型設(shè)計的基本思想

詳細(xì)解讀YOLOV7網(wǎng)絡(luò)架構(gòu)設(shè)計

評論