YOLO系列 — YOLOV7算法(四):YOLO V7算法網(wǎng)絡(luò)結(jié)構(gòu)解析
今天來(lái)講講YOLO V7算法網(wǎng)絡(luò)結(jié)構(gòu)吧~
在train.py
中大概95行的地方開始創(chuàng)建網(wǎng)絡(luò),如下圖(YOLO V7下載的時(shí)間不同,可能代碼有少許的改動(dòng),所以行數(shù)跟我不一定一樣)
我們進(jìn)去發(fā)現(xiàn),其實(shí)就是在yolo.py
里面。后期,我們就會(huì)發(fā)現(xiàn)相關(guān)的網(wǎng)絡(luò)結(jié)構(gòu)都是在該py文件里面。這篇blog就主要講講Model
這個(gè)類。
def __init__(self, cfg='yolor-csp-c.yaml', ch=3, nc=None, anchors=None):
先來(lái)說(shuō)下,傳入的參數(shù):
- cfg:傳入的網(wǎng)絡(luò)結(jié)構(gòu)yaml文件路徑,這里已經(jīng)默認(rèn)的是
yolor-csp-c.yaml
,就是說(shuō)如果你在train.py
中沒(méi)有傳入網(wǎng)絡(luò)結(jié)構(gòu)yaml文件的話默認(rèn)使用yolor-csp-c.yaml這個(gè)網(wǎng)絡(luò)結(jié)構(gòu) - ch:預(yù)測(cè)頭的數(shù)量
- nc:數(shù)據(jù)集類別數(shù)
super(Model, self).__init__()
self.traced = False
if isinstance(cfg, dict):
self.yaml = cfg # model dict
else: # is *.yaml
import yaml # for torch hub
self.yaml_file = Path(cfg).name
with open(cfg) as f:
self.yaml = yaml.load(f, Loader=yaml.SafeLoader) # model dict
首先判斷傳入的yaml網(wǎng)絡(luò)結(jié)構(gòu)文件是不是字典形式的,我們一般直接就是傳入的是yaml的路徑,所以直接用yaml.load
解析yaml文件,如下圖
# Define model
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
if nc and nc != self.yaml['nc']:
logger.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value
if anchors:
logger.info(f'Overriding model.yaml anchors with anchors={anchors}')
self.yaml['anchors'] = round(anchors) # override yaml value
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
這段代碼值得好好看看,首先第一行代碼出現(xiàn)了兩個(gè)=
號(hào),其實(shí)代表的含義就是最右邊的變量同時(shí)賦值給中間的和最左邊的。而self.yaml.get('ch', ch)
就是在尋找yaml字典中是否存在ch
這個(gè)key,這里是不存在的,我們可以在yolov7.yaml
中找找看,里面是沒(méi)有的,如果沒(méi)有的話就直接用Model
類中__init__
初始化定義的ch=3,表示該模型一共有三個(gè)預(yù)測(cè)頭。
然后,判斷一下custom_data.yaml
中的nc
是否與yolov7.yaml
中的nc
是否一致,如果不一致的話就默認(rèn)將custom_data.yaml
中的nc
賦值給self.yaml
中的nc
。
接著,將yolov7.yaml
中的anchors
賦值給self.yaml
。
最后,我們將解析后的yaml字典和預(yù)測(cè)頭數(shù)量傳入parse_model
函數(shù),最后一行代碼其實(shí)就沒(méi)什么的了,就是將所有類別變成[0,1,2,…]
進(jìn)入parse_model
函數(shù):
logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
首先是從yaml
字典中獲取anchor尺寸,類別數(shù),網(wǎng)絡(luò)深度,網(wǎng)絡(luò)寬度。這里,詳細(xì)說(shuō)下網(wǎng)絡(luò)深度,網(wǎng)絡(luò)寬度到底是什么意思:
我們先來(lái)看下yolov7.yaml
中backbone和head中每個(gè)列表的含義:
第一個(gè):表示該層是源自于上面哪一層,一般-1就表示的是上一層
第二個(gè):表示該層一共有幾個(gè),就比如說(shuō)這里有一個(gè)卷積層是2的話,那么這層就是有兩個(gè)串聯(lián)卷積層
第三個(gè):表示該層是什么模塊
第四個(gè):表示的是該層的一些參數(shù),比如說(shuō)如果該層是Conv卷積層的話,那么后面接一個(gè)【32,3,1】就表示的是輸出channel數(shù)是32,卷積核大小為3*3
介紹完每個(gè)參數(shù),我們就可以介紹什么是網(wǎng)絡(luò)深度,網(wǎng)絡(luò)寬度了:
- 網(wǎng)絡(luò)深度:實(shí)際在構(gòu)建網(wǎng)絡(luò)模型的時(shí)候,并不是直接使用上述第二個(gè)參數(shù),即有幾個(gè)模塊層。而是用網(wǎng)絡(luò)深度去乘以第二個(gè)參數(shù),最終獲得的數(shù)量才是真正的層數(shù)量。舉個(gè)例子,此時(shí)網(wǎng)絡(luò)深度是0.33,某個(gè)層的第二個(gè)參數(shù)是3,那么實(shí)際在構(gòu)建網(wǎng)絡(luò)模型的時(shí)候只創(chuàng)建了0.33*3=1個(gè),并不是三個(gè)。
- 網(wǎng)絡(luò)寬度:再舉個(gè)例子吧,比如此時(shí)該層是卷積層Conv,輸出channels數(shù)設(shè)置為64,但是同時(shí)網(wǎng)絡(luò)寬度設(shè)置的是0.5,那么在實(shí)際構(gòu)建網(wǎng)絡(luò)模型的時(shí)候,該層最后的輸出channels數(shù)其實(shí)是64*0.5=32。
同時(shí),在yolov7.yaml
中anchors表示的是每個(gè)預(yù)測(cè)頭所對(duì)應(yīng)的anchors長(zhǎng)寬大小,如下圖(隨意畫的,能理解含義就ok了):
那么na
就表示的是每個(gè)預(yù)測(cè)頭有幾組比例不同的anchor,no
表示的是最后預(yù)測(cè)頭輸出的通道數(shù),其中5表示的是四個(gè)位置信息和置信度大小。
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args
m = eval(m) if isinstance(m, str) else m # eval strings
for j, a in enumerate(args):
try:
args[j] = eval(a) if isinstance(a, str) else a # eval strings
except:
pass
遍歷backbone和head所有層,獲取得到每一層到底是什么模塊,然后利用eval
函數(shù)進(jìn)行解析。這里簡(jiǎn)單介紹下eval函數(shù),遍歷所得到的m只是一個(gè)字符串,表示該層的類型,但是并不是該層的類,而eval函數(shù)就是實(shí)例化該層類。而args也是同理。
n = max(round(n * gd), 1) if n > 1 else n # depth gain
if m in [nn.Conv2d, Conv, RobustConv, RobustConv2, DWConv, GhostConv, RepConv, RepConv_OREPA, DownC,
SPP, SPPF, SPPCSPC, GhostSPPCSPC, MixConv2d, Focus, Stem, GhostStem, CrossConv,
Bottleneck, BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
RepBottleneck, RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
Res, ResCSPA, ResCSPB, ResCSPC,
RepRes, RepResCSPA, RepResCSPB, RepResCSPC,
ResX, ResXCSPA, ResXCSPB, ResXCSPC,
RepResX, RepResXCSPA, RepResXCSPB, RepResXCSPC,
Ghost, GhostCSPA, GhostCSPB, GhostCSPC,
SwinTransformerBlock, STCSPA, STCSPB, STCSPC,
SwinTransformer2Block, ST2CSPA, ST2CSPB, ST2CSPC]:
c1, c2 = ch[f], args[0]
if c2 != no: # if not output
c2 = make_divisible(c2 * gw, 8)
第一行代碼就是我上述所說(shuō)的網(wǎng)絡(luò)深度,真正的層數(shù)量取決于yaml文件中的第二個(gè)參數(shù)和網(wǎng)絡(luò)深度的乘積。前提是yaml中層的數(shù)量是大于1才進(jìn)行計(jì)算。然后判斷該層的類型,這個(gè)判斷大多是判斷是不是卷積層。計(jì)算c1和c2,這兩個(gè)數(shù)字分別表示輸入channels數(shù)和輸出channels數(shù)。接著判斷遍歷的該層是不是最后一層,而判斷的標(biāo)注就是看最后輸出的channels數(shù)c2是不是等于no,no上述已經(jīng)說(shuō)過(guò)了表示最后預(yù)測(cè)頭的通道數(shù)。如果不是最后一層,那表示是網(wǎng)絡(luò)中間的層,那么就來(lái)到了上述說(shuō)的網(wǎng)絡(luò)寬度部分了。
c2 = make_divisible(c2 * gw, 8)
該層最終的輸出通道數(shù)其實(shí)就是網(wǎng)絡(luò)寬度和該層第一個(gè)參數(shù)的乘積,make_divisible
函數(shù)之前已經(jīng)說(shuō)過(guò)了,作用是自動(dòng)調(diào)整為32的倍數(shù)。
args = [c1, c2, *args[1:]]
if m in [DownC, SPPCSPC, GhostSPPCSPC,
BottleneckCSPA, BottleneckCSPB, BottleneckCSPC,
RepBottleneckCSPA, RepBottleneckCSPB, RepBottleneckCSPC,
ResCSPA, ResCSPB, ResCSPC,
RepResCSPA, RepResCSPB, RepResCSPC,
ResXCSPA, ResXCSPB, ResXCSPC,
RepResXCSPA, RepResXCSPB, RepResXCSPC,
GhostCSPA, GhostCSPB, GhostCSPC,
STCSPA, STCSPB, STCSPC,
ST2CSPA, ST2CSPB, ST2CSPC]:
args.insert(2, n) # number of repeats
n = 1
args
是拼湊喂入網(wǎng)絡(luò)的參數(shù)列表, 我們可以看下models/common.py
中Conv
類的__init__
初始化函數(shù):
卷積層需要輸入的參數(shù)格式為:[輸入通道數(shù),輸出通道數(shù),卷積核大小,步長(zhǎng)],正好與args = [c1, c2, *args[1:]]
相對(duì)應(yīng)上了。然后進(jìn)一步判斷層是否屬于列表中這些層,因?yàn)檫@些層的參數(shù)形式與卷積不一樣。
elif m is nn.BatchNorm2d:
args = [ch[f]]
elif m is Concat:
c2 = sum([ch[x] for x in f])
elif m is Chuncat:
c2 = sum([ch[x] for x in f])
elif m is Shortcut:
c2 = ch[f[0]]
elif m is Foldcut:
c2 = ch[f] // 2
elif m in [Detect, IDetect, IAuxDetect, IBin]:
args.append([ch[x] for x in f])
if isinstance(args[1], int): # number of anchors
args[1] = [list(range(args[1] * 2))] * len(f)
elif m is ReOrg:
c2 = ch[f] * 4
elif m is Contract:
c2 = ch[f] * args[0] ** 2
elif m is Expand:
c2 = ch[f] // args[0] ** 2
else:
c2 = ch[f]
判斷如果不是卷積等操作,再看看是不是屬于下面elif中的哪個(gè)層,這里我就不多說(shuō)了,也沒(méi)啥難的。
m_ = nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args) # module
t = str(m)[8:-2].replace('__main__.', '') # module type
np = sum([x.numel() for x in m_.parameters()]) # number params
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
layers.append(m_)
if i == 0:
ch = []
ch.append(c2)
return nn.Sequential(*layers), sorted(save)
torch.nn.Sequential
是一個(gè)Sequential容器,模塊將按照構(gòu)造函數(shù)中傳遞的順序添加到模塊中。然后獲取遍歷的每一層的參數(shù)量大小。save.extend
這一行代碼是遍歷所有的層,找到第一個(gè)參數(shù)不是等于-1的,也就是該層的數(shù)據(jù)不是來(lái)源于上一層,也就是意味著是來(lái)自上面的多個(gè)層,那么就要進(jìn)行保存操作。最后判斷下是不是輸入層,如果是輸入層的話,就將輸出通道數(shù)放進(jìn)ch里面去。最后返回Sequential容器和需要保存的層的索引號(hào)。
我們回到Model
類中:
# Build strides, anchors
m = self.model[-1] # Detect()
if isinstance(m, Detect):
s = 256 # 2x min stride
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
m.anchors /= m.stride.view(-1, 1, 1)
check_anchor_order(m)
self.stride = m.stride
self._initialize_biases() # only run once
# print('Strides: %s' % m.stride.tolist())
if isinstance(m, IDetect):
s = 256 # 2x min stride
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
m.anchors /= m.stride.view(-1, 1, 1)
check_anchor_order(m)
self.stride = m.stride
self._initialize_biases() # only run once
# print('Strides: %s' % m.stride.tolist())
if isinstance(m, IAuxDetect):
s = 256 # 2x min stride
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))[:4]]) # forward
#print(m.stride)
m.anchors /= m.stride.view(-1, 1, 1)
check_anchor_order(m)
self.stride = m.stride
self._initialize_aux_biases() # only run once
# print('Strides: %s' % m.stride.tolist())
if isinstance(m, IBin):
s = 256 # 2x min stride
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward
m.anchors /= m.stride.view(-1, 1, 1)
check_anchor_order(m)
self.stride = m.stride
self._initialize_biases_bin() # only run once
# print('Strides: %s' % m.stride.tolist())
得到一個(gè)Sequential容器,里面包含著所有層。我們先獲取到最后一層,一般來(lái)說(shuō),最后一層都是檢測(cè)層,就是將所有的預(yù)測(cè)頭進(jìn)行融合。判斷下是屬于哪種檢測(cè)層。
我們通過(guò)yolov7.yaml
文件可以看到,這里最后一層是Detect
類型,那么我就進(jìn)入第一個(gè)if分支里面。我們本身是知道這個(gè)網(wǎng)絡(luò)的步長(zhǎng)是32,16和8的,但是模型是不知道的,所以我們需要喂入一個(gè)測(cè)試tensor進(jìn)行一次正向傳播來(lái)得到網(wǎng)絡(luò)的步長(zhǎng)大小。m.anchors
表示的是基于原始圖片的,所以我們要相對(duì)應(yīng)的除以步長(zhǎng),得到的m.anchors
才是真正的基于最后預(yù)測(cè)頭特征圖的anchors尺寸。check_anchor_order
函數(shù)的作用就是檢測(cè)anchor的順序是不是正確的,在yolov7.yaml
中應(yīng)該是從小到大的順序排列。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-448920.html
# Init weights, biases
initialize_weights(self)
self.info()
logger.info('')
最后,就是初始化一下權(quán)重大小。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-448920.html
到了這里,關(guān)于YOLO系列 --- YOLOV7算法(四):YOLO V7算法網(wǎng)絡(luò)結(jié)構(gòu)解析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!