通過(guò)底層源碼理解YOLOv5的Backbone
YOLOv5的Backbone設(shè)計(jì)
在上一篇文章《YOLOV5的anchor設(shè)定》中我們討論了anchor的產(chǎn)生原理和檢測(cè)過(guò)程,對(duì)YOLOv5的網(wǎng)絡(luò)結(jié)構(gòu)有了大致的了解。接下來(lái),我們將聚焦于YOLOv5的Backbone,深入到底層源碼中體會(huì)v5的Backbone設(shè)計(jì)。
1 Backbone概覽及參數(shù)
# Parameters nc: 80 # number of classes depth_multiple: 0.33 # model depth multiple width_multiple: 0.50 # layer channel multiple # YOLOv5 v6.0 backbone backbone: # [from, number, module, args] [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 [-1, 3, C3, [128]], [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 [-1, 6, C3, [256]], [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 [-1, 9, C3, [512]], [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 [-1, 3, C3, [1024]], [-1, 1, SPPF, [1024, 5]], # 9 ]
yolov5s的backbone部分如上,其網(wǎng)絡(luò)結(jié)構(gòu)使用yaml文件配置,通過(guò)./models/yolo.py解析文件加了一個(gè)輸入構(gòu)成的網(wǎng)絡(luò)模塊。與v3和v4所使用的config設(shè)置的網(wǎng)絡(luò)不同,yaml文件中的網(wǎng)絡(luò)組件不需要進(jìn)行疊加,只需要在配置文件中設(shè)置number即可。
1.1 Param
# Parameters nc: 80 # number of classes depth_multiple: 0.33 # model depth multiple width_multiple: 0.50 # layer channel multiple
nc: 8
代表數(shù)據(jù)集中的類別數(shù)目,例如MNIST中含有0-9共10個(gè)類.
depth_multiple: 0.33
用來(lái)控制模型的深度,僅在number≠1時(shí)啟用。 如第一個(gè)C3層(c3具體是什么后續(xù)介紹)的參數(shù)設(shè)置為[-1, 3, C3, [128]]
,其中number=3,表示在v5s中含有1個(gè)C3(3*0.33);同理,v5l中的C3個(gè)數(shù)就是3(v5l的depth_multiple參數(shù)為1)。
width_multiple: 0.50
用來(lái)控制模型的寬度,主要作用于args中的ch_out。如第一個(gè)Conv層,ch_out=64,那么在v5s實(shí)際運(yùn)算過(guò)程中,會(huì)將卷積過(guò)程中的卷積核設(shè)為64x0.5,所以會(huì)輸出32通道的特征圖。
1.2 backbone
# YOLOv5 v6.0 backbone backbone: # [from, number, module, args] [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 [-1, 3, C3, [128]], [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 [-1, 6, C3, [256]], [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 [-1, 9, C3, [512]], [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 [-1, 3, C3, [1024]], [-1, 1, SPPF, [1024, 5]], # 9 ]
- from:-n代表是從前n層獲得的輸入,如-1表示從前一層獲得輸入
- number:表示網(wǎng)絡(luò)模塊的數(shù)目,如[-1, 3, C3, [128]]表示含有3個(gè)C3模塊
- model:表示網(wǎng)絡(luò)模塊的名稱,具體細(xì)節(jié)可以在./models/common.py查看,如Conv、C3、SPPF都是已經(jīng)在common中定義好的模塊
- args:表示向不同模塊內(nèi)傳遞的參數(shù),即[ch_out, kernel, stride, padding, groups],這里連ch_in都省去了,因?yàn)檩斎攵际巧蠈拥妮敵觯ǔ跏糲h_in為3)。為了修改過(guò)于麻煩,這里輸入的獲取是從./models/yolo.py的def parse_model(md, ch)函數(shù)中解析得到的。
1.3 Exp
[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
input:3x640x640
[ch_out, kernel, stride, padding]=[64, 6, 2, 2]
故新的通道數(shù)為64x0.5=32
根據(jù)特征圖計(jì)算公式:Feature_new=(Feature_old-kernel+2xpadding)/stride+1可得:
新的特征圖尺寸為:Feature_new=(640-6+2x2)/2+1=320
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
input:32x320x320
[ch_out, kernel, stride]=[128, 3, 2]
同理可得:新的通道數(shù)為64,新的特征圖尺寸為160
2 Backbone組成
v6.0版本的Backbone去除了Focus模塊(便于模型導(dǎo)出部署),Backbone主要由CBL、BottleneckCSP/C3以及SPP/SPPF等組成,具體如下圖所示:
3.1 CBS
CBS模塊其實(shí)沒(méi)什么好稀奇的,就是Conv+BatchNorm+SiLU,這里著重講一下Conv的參數(shù),就當(dāng)復(fù)習(xí)pytorch的卷積操作了,先上CBL源碼:
class Conv(nn.Module): # Standard convolution def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2) #其中nn.Identity()是網(wǎng)絡(luò)中的占位符,并沒(méi)有實(shí)際操作,在增減網(wǎng)絡(luò)過(guò)程中,可以使得整個(gè)網(wǎng)絡(luò)層數(shù)據(jù)不變,便于遷移權(quán)重?cái)?shù)據(jù);nn.SiLU()一種激活函數(shù)(S形加權(quán)線性單元)。 self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) def forward(self, x):#正態(tài)分布型的前向傳播 return self.act(self.bn(self.conv(x))) def forward_fuse(self, x):#普通前向傳播 return self.act(self.conv(x))
由源碼可知:Conv()包含7個(gè)參數(shù),這些參數(shù)也是二維卷積Conv2d()中的重要參數(shù)。ch_in, ch_out, kernel, stride沒(méi)什么好說(shuō)的,展開(kāi)說(shuō)一下后三個(gè)參數(shù):
padding
從我現(xiàn)在看到的主流卷積操作來(lái)看,大多數(shù)的研究者不會(huì)通過(guò)kernel來(lái)改變特征圖的尺寸,如googlenet中3x3的kernel設(shè)定了padding=1,所以當(dāng)kernel≠1時(shí)需要對(duì)輸入特征圖進(jìn)行填充。當(dāng)指定p值時(shí)按照p值進(jìn)行填充,當(dāng)p值為默認(rèn)時(shí)則通過(guò)autopad函數(shù)進(jìn)行填充:
def autopad(k, p=None): # kernel, padding # Pad to 'same' if p is None: p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad #如果k是整數(shù),p為k與2整除后向下取整;如果k是列表等,p對(duì)應(yīng)的是列表中每個(gè)元素整除2。 return p
這里作者考慮到對(duì)不同的卷積操作使用不同大小的卷積核時(shí)padding也需要做出改變,所以這里在為p賦值時(shí)會(huì)首先檢查k是否為int,如果k為列表則對(duì)列表中的每個(gè)元素整除。
groups
代表分組卷積,如下圖所示
groups – Number of blocked connections from input channels to output
- At groups=1, all inputs are convolved to all outputs.
- At groups=2, the operation becomes equivalent to having two conv layers side by side, each seeing half the input channels, and producing half the output channels, and both subsequently concatenated.
- At groups= in_channels, each input channel is convolved with its own set of filters, of size: ⌊(out_channels)/(in_channels)⌋.
act
決定是否對(duì)特征圖進(jìn)行激活操作,SiLU表示使用Sigmoid進(jìn)行激活。
one more thing:dilation
Conv2d中還有一個(gè)重要的參數(shù)就是空洞卷積dilation,通俗解釋就是控制kernel點(diǎn)(卷積核點(diǎn))間距的參數(shù),通過(guò)改變卷積核間距實(shí)現(xiàn)特征圖及特征信息的保留,在語(yǔ)義分割任務(wù)中空洞卷積比較有效。
3.2 CSP/C3
CSP即backbone中的C3,因?yàn)樵赽ackbone中C3存在shortcut,而在neck中C3不使用shortcut,所以backbone中的C3層使用CSP1_x表示,neck中的C3使用CSP2_x表示。
3.2.1 CSP結(jié)構(gòu)
接下來(lái)讓我們來(lái)好好梳理一下backbone中的C3層的模塊組成。先上源碼:
class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
從源碼中可以看出:輸入特征圖一條分支先經(jīng)過(guò).cv1,再經(jīng)過(guò).m,得到子特征圖1;另一分支經(jīng)過(guò).cv2后得到子特征圖2。最后將子特征圖1和子特征圖2拼接后輸入.cv3得到C3層的輸出,如下圖所示。 這里的CV操作容易理解,就是前面的Conv2d+BN+SiLU,關(guān)鍵是.m操作。
.m操作使用nn.Sequential將多個(gè)Bottleneck(圖示中我以Resx命名)串接到網(wǎng)絡(luò)中,for loop中的n即網(wǎng)絡(luò)配置文件args中的number,也就是將number×depth_multiple個(gè)Bottleneck串接到網(wǎng)絡(luò)中。那么,Bottleneck又是個(gè)什么玩意呢?
3.2.2 Bottleneck
要想了解Bottleneck,還要從Resnet說(shuō)起。在Resnet出現(xiàn)之前,人們的普遍為網(wǎng)絡(luò)越深獲取信息也越多,模型泛化效果越好。然而隨后大量的研究表明,網(wǎng)絡(luò)深度到達(dá)一定的程度后,模型的準(zhǔn)確率反而大大降低。這并不是過(guò)擬合造成的,而是由于反向傳播過(guò)程中的梯度爆炸和梯度消失。也就是說(shuō),網(wǎng)絡(luò)越深,模型越難優(yōu)化,而不是學(xué)習(xí)不到更多的特征。
為了能讓深層次的網(wǎng)絡(luò)模型達(dá)到更好的訓(xùn)練效果,殘差網(wǎng)絡(luò)中提出的殘差映射替換了以往的基礎(chǔ)映射。對(duì)于輸入x,期望輸出H(x),網(wǎng)絡(luò)利用恒等映射將x作為初始結(jié)果,將原來(lái)的映射關(guān)系變成F(x)+x。與其讓多層卷積去近似估計(jì)H(x) ,不如近似估計(jì)H(x)-x,即近似估計(jì)殘差F(x)。因此,ResNet相當(dāng)于將學(xué)習(xí)目標(biāo)改變?yōu)槟繕?biāo)值H(x)和x的差值,后面的訓(xùn)練目標(biāo)就是要將殘差結(jié)果逼近于0。
殘差模塊有什么好處呢?
1.梯度彌散方面。加入ResNet中的shortcut結(jié)構(gòu)之后,在反傳時(shí),每?jī)蓚€(gè)block之間不僅傳遞了梯度,還加上了求導(dǎo)之前的梯度,這相當(dāng)于把每一個(gè)block中向前傳遞的梯度人為加大了,也就會(huì)減小梯度彌散的可能性。
2.特征冗余方面。正向卷積時(shí),對(duì)每一層做卷積其實(shí)只提取了圖像的一部分信息,這樣一來(lái),越到深層,原始圖像信息的丟失越嚴(yán)重,而僅僅是對(duì)原始圖像中的一小部分特征做提取。這顯然會(huì)發(fā)生類似欠擬合的現(xiàn)象。加入shortcut結(jié)構(gòu),相當(dāng)于在每個(gè)block中又加入了上一層圖像的全部信息,一定程度上保留了更多的原始信息。
在resnet中,人們可以使用帶有shortcut的殘差模塊搭建幾百層甚至上千層的網(wǎng)絡(luò),而淺層的殘差模塊被命名為Basicblock(18、34),深層網(wǎng)絡(luò)所使用的的殘差模塊,就被命名為了Bottleneck(50+)。
Bottleneck與Basicblock最大的區(qū)別是卷積核的組成。 Basicblock由兩個(gè)3x3的卷積層組成,Bottleneck由兩個(gè)1x1卷積層夾一個(gè)3x3卷積層組成:其中1x1卷積層降維后再恢復(fù)維數(shù),讓3x3卷積在計(jì)算過(guò)程中的參數(shù)量更少、速度更快。
第一個(gè)1x1的卷積把256維channel降到64維,然后在最后通過(guò)1x1卷積恢復(fù),整體上用的參數(shù)數(shù)目:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,而不使用bottleneck的話就是兩個(gè)3x3x256的卷積,參數(shù)數(shù)目: 3x3x256x256x2 = 1179648,差了16.94倍。
Bottleneck減少了參數(shù)量,優(yōu)化了計(jì)算,保持了原有的精度。
說(shuō)了這么多,都是為了給CSP中的Bottleneck做前情提要,我們?cè)倩仡^看CSP中的Bottleneck其實(shí)就更清楚了:
class Bottleneck(nn.Module): # Standard bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
可以看到,CSP中的Bottleneck同resnet模塊中的類似,先是1x1的卷積層(CBS),然后再是3x3的卷積層,最后通過(guò)shortcut與初始輸入相加。但是這里與resnet的不通點(diǎn)在于:CSP將輸入維度減半運(yùn)算后并未再使用1x1卷積核進(jìn)行升維,而是將原始輸入x也降了維,采取concat的方法進(jìn)行張量的拼接,得到與原始輸入相同維度的輸出。其實(shí)這里能區(qū)分一點(diǎn)就夠了:resnet中的shortcut通過(guò)add實(shí)現(xiàn),是特征圖對(duì)應(yīng)位置相加而通道數(shù)不變;而CSP中的shortcut通過(guò)concat實(shí)現(xiàn),是通道數(shù)的增加。二者雖然都是信息融合的主要方式,但是對(duì)張量的具體操作又不相同.
其次,對(duì)于shortcut是可根據(jù)任務(wù)要求設(shè)置的,比如在backbone中shortcut=True,neck中shortcut=False。
當(dāng)shortcut=True時(shí),Resx如圖:
當(dāng)shortcut=False時(shí),Resx如圖:
這其實(shí)也是YOLOv5為人稱贊的地方,代碼更體系、代碼冗余更少,僅需要指定一個(gè)參數(shù)便可以將Bottleneck和普通卷積聯(lián)合在一起使用,減少了代碼量的同時(shí)也使整體感觀得到提升。
3.3 SSPF
class SPPF(nn.Module): # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * 4, c2, 1, 1) self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning y1 = self.m(x) y2 = self.m(y1) return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1))
SSPF模塊將經(jīng)過(guò)CBS的x、一次池化后的y1、兩次池化后的y2和3次池化后的self.m(y2)先進(jìn)行拼接,然后再CBS提取特征。 仔細(xì)觀察不難發(fā)現(xiàn),雖然SSPF對(duì)特征圖進(jìn)行了多次池化,但是特征圖尺寸并未發(fā)生變化,通道數(shù)更不會(huì)變化,所以后續(xù)的4個(gè)輸出能夠在channel維度進(jìn)行融合。這一模塊的主要作用是對(duì)高層特征進(jìn)行提取并融合,在融合的過(guò)程中作者多次運(yùn)用最大池化,盡可能多的去提取高層次的語(yǔ)義特征。
YOLOv5s的Backbone總覽
最后,結(jié)合上述的講解應(yīng)該就不難理解v5s的backbone了
總結(jié)
到此這篇關(guān)于通過(guò)底層源碼理解YOLOv5中Backbone的文章就介紹到這了,更多相關(guān)YOLOv5 Backbone詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python實(shí)現(xiàn)QQ郵箱/163郵箱的郵件發(fā)送
這篇文章主要為大家詳細(xì)介紹了Python實(shí)現(xiàn)QQ郵箱和163郵箱的郵件發(fā)送,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01學(xué)會(huì)這29個(gè)常用函數(shù),你就是Pandas專家
Pandas?無(wú)疑是?Python?處理表格數(shù)據(jù)最好的庫(kù)之一,但是很多新手無(wú)從下手,這里總結(jié)出最常用的?29?個(gè)函數(shù),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-11-11Python爬蟲(chóng)之Spider類用法簡(jiǎn)單介紹
這篇文章主要介紹了Python爬蟲(chóng)之Spider類用法簡(jiǎn)單介紹,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Python?虛擬環(huán)境的價(jià)值和常用命令詳解
在實(shí)際項(xiàng)目開(kāi)發(fā)中,我們通常會(huì)根據(jù)自己的需求去下載各種相應(yīng)的框架庫(kù),如Scrapy、Beautiful?Soup等,但是可能每個(gè)項(xiàng)目使用的框架庫(kù)并不一樣,或使用框架的版本不一樣,今天給大家分享下Python?虛擬環(huán)境的價(jià)值和常用命令,感興趣的朋友一起看看吧2022-05-05