この記事では、yolov3と同様にyolov5のソースコードの分析を開始します。また、モデルの構築から開始します。この部分のコアコードはyolo.pyファイルにあります。私が使用したyoloコードバージョンは公式バージョン4.0(2021年1月にリリースされた最新バージョン)です。
解析する前に、ソースコードを理解するための最良の方法は、画像とタグを構成してからデバッグすることであると言う必要があります。coco128の公式データセットは、すばやくダウンロードして実行できるため、デバッグに便利です。
ソースコードの各行に含まれるテンソルの寸法を印刷し、関数の関数をマークします。
yolo.pyソースコード分析
1.init()関数
まず、モデル構築のソースコード部分を見てみましょう。クラスModel()
1つ目は、このクラスのinit()関数のソースコードです。モデルは、init()の構成ファイルを介して構築されています。
def __init __(self、cfg = 'yolov5s.yaml'、ch = 3、nc = None):#モデル、入力チャネル、クラス数
super(Model、self).__ init __()
isinstance(cfg、dict)の場合:
self.yaml = cfg#モデル辞書
else:#は* .yaml
トーチハブのyaml#をインポートします
self.yaml_file = Path(cfg).name
open(cfg)をfとして使用:
self.yaml = yaml.load(f、Loader = yaml.SafeLoader)#model dict
#モデルを定義する
ch = self.yaml ['ch'] = self.yaml.get( 'ch'、ch)#入力チャネル
if nc and nc!= self.yaml ['nc']:
logger.info( 'model.yaml nc =%gをnc =%gでオーバーライド'%(self.yaml ['nc']、nc))
self.yaml ['nc'] = nc#yaml値をオーバーライドする
self.model、self.save = parse_model(deepcopy(self.yaml)、ch = [ch])#モデル、保存リスト
self.names = [str(i)for i in range(self.yaml ['nc'])]#デフォルト名
#print([x.shape for x in self.forward(torch.zeros(1、ch、64、64))])
#ストライド、アンカーを構築する
m = self.model [-1] #Detect()
isinstance(m、Detect)の場合:
s = 256#2x最小ストライド
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()#1回だけ実行
#print( 'ストライド:%s'%m.stride.tolist())
#初期化の重み、バイアス
initialize_weights(self)
self.info()
logger.info( '')
言うまでもなく、モデルはまだyamlを解析する構成ファイルです。
デバッグの例としてyolov5sを使用します。
open(cfg)をfとして使用:
self.yaml = yaml.load(f、Loader = yaml.SafeLoader)#model dict
解析されたyamlがdict形式であることがわかります。
ncはカテゴリの数を表します
depth_multipleは、モデルの深さを制御するパラメーターです。
width_multipleは、モデルの幅を制御するパラメーターです。
アンカーはプリセットアンカーフレームです。FPNの各レイヤーには3つあり、合計で3 * 3 = 9です。
バックボーンはバックボーンネットワークの構築パラメータであり、この構成に従ってバックボーンネットワークをロードできます。
headは、yoloヘッドネットワークの構築パラメータです。この構成に従って、yoloヘッドネットワークをロードできます。(実際、この部分は首と頭と考えることができます)
ch = self.yaml ['ch'] = self.yaml.get( 'ch'、ch)#入力チャネル
if nc and nc!= self.yaml ['nc']:
logger.info( 'model.yaml nc =%gをnc =%gでオーバーライド'%(self.yaml ['nc']、nc))
self.yaml ['nc'] = nc#yaml値をオーバーライドする
ここで、入力チャネルと構成ファイルが同じであるかどうかを判断します。不整合がある場合は、入力パラメータが優先されます。
self.model、self.save = parse_model(deepcopy(self.yaml)、ch = [ch])
次に、コア関数parse_model()に入ります。この関数のソースコードを以下に示します。
ef parse_model(d、ch):#model_dict、input_channels(3)
logger.info( '\ n%3s%18s%3s%10s%-40s%-30s'%( ''、 'from'、 'n'、 'params'、 'module'、 'arguments'))
アンカー、nc、gd、gw = d ['anchors']、d ['nc']、d ['depth_multiple']、d ['width_multiple']
na =(len(anchors [0])// 2)if isinstance(anchors、list)elseアンカー#アンカーの数
no = na *(nc + 5)#出力数=アンカー*(クラス+ 5)
レイヤー、保存、c2 = []、[]、ch [-1]#レイヤー、保存リスト、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文字列
jの場合、enumerate(args)のa:
試してください:
args [j] = eval(a)if isinstance(a、str)else a#eval文字列
例外:
パス
n = max(round(n * gd)、1)if n> 1 else n#深度ゲイン
[Conv、GhostConv、Bottleneck、GhostBottleneck、SPP、DWConv、MixConv2d、Focus、CrossConv、BottleneckCSP、
C3]:
c1、c2 = ch [f]、args [0]
if c2!= no:#出力されない場合
c2 = make_divisible(c2 * gw、8)
args = [c1、c2、* args [1:]]
[BottleneckCSP、C3]のmの場合:
args.insert(2、n)#繰り返し回数
n = 1
elif mはnn.BatchNorm2dです:
args = [ch [f]]
elif mはConcatです:
c2 = sum([ch [x] for x in f])
elif mは検出です:
args.append([ch [x] for x in f])
if isinstance(args [1]、int):#アンカーの数
args [1] = [list(range(args [1] * 2))] * len(f)
elif mは契約です:
c2 = ch [f] * args [0] ** 2
elif mはExpandです:
c2 = ch [f] // args [0] ** 2
そうしないと:
c2 = ch [f]
m_ = nn.Sequential(* [m(* args)for _ in range(n)])if n> 1 else m(* args)#モジュール
t = str(m)[8:-2] .replace( '__ main __。'、 '')#モジュールタイプ
np = sum([x.numel()for x in m_.parameters()])#number params
m_.i、m_.f、m_.type、m_.np = i、f、t、np#インデックスを添付、 'from'インデックス、タイプ、数値パラメータ
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)#保存リストに追加
layers.append(m_)
i == 0の場合:
ch = []
ch.append(c2)
nn.Sequential(* layers)、sorted(save)を返します
この関数を段階的に分析してみましょう。
logger.info( '\ n%3s%18s%3s%10s%-40s%-30s'%( ''、 'from'、 'n'、 'params'、 'module'、 'arguments'))
アンカー、nc、gd、gw = d ['anchors']、d ['nc']、d ['depth_multiple']、d ['width_multiple']
na =(len(anchors [0])// 2)if isinstance(anchors、list)elseアンカー#アンカーの数
no = na *(nc + 5)#出力数=アンカー*(クラス+ 5)
この部分は非常に単純で、構成dictのパラメーターを読み取り、naはアンカーの数を判断し、noはアンカーの数から推測される出力次元です(たとえば、cocoの場合は255)。出力次元=アンカーの数*(カテゴリの数+信頼度+ xywh 4つの回帰座標)。
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文字列
jの場合、enumerate(args)のa:
試してください:
args [j] = eval(a)if isinstance(a、str)else a#eval文字列
例外:
パス
n = max(round(n * gd)、1)if n> 1 else n#深度ゲイン
ここから、バックボーンとヘッドの反復ループ構成を開始します。f、n、m、argsはそれぞれ、開始レベル、モジュールのデフォルトの深さ、モジュールのタイプ、およびモジュールのパラメーターを表します。
n = max(round(n * gd)、1)if n> 1 else n
ネットワークはn * gdを使用して、モジュールの深度スケーリングを制御します。たとえば、yolo5sの場合、gdは0.33です。これは、デフォルトの深度が元の深度の1/3にスケーリングされることを意味します。
[Conv、GhostConv、Bottleneck、GhostBottleneck、SPP、DWConv、MixConv2d、Focus、CrossConv、BottleneckCSP、
C3]:
c1、c2 = ch [f]、args [0]
if c2!= no:#出力されない場合
c2 = make_divisible(c2 * gw、8)
args = [c1、c2、* args [1:]]
[BottleneckCSP、C3]のmの場合:
args.insert(2、n)#繰り返し回数
n = 1
上記のタイプのモジュールの場合、chは前のすべてのモジュールの出力を保存するために使用されるチャネルであり、ch [-1]は前のモジュールの出力チャネルを表します。args [0]はデフォルトの出力チャネルです。
def make_divisible(x、divisor):
#除数で均等に割り切れるxを返します
math.ceil(x /除数)を返す*除数
ここで、make_divisible()関数を使用すると、ネットワークモジュールの幅(つまり、出力チャネルの数)をスケーリングできます。たとえば、最初のモジュール「Focus」の場合、デフォルトの出力チャネルは64で、 yolov5sのスケーリング係数は0.5です。したがって、上記のコード変換により、最終的な出力チャネルは32になります。make_divisible()関数は、出力チャネルが8の倍数であることを保証します。
args = [c1、c2、* args [1:]]
[BottleneckCSP、C3]のmの場合:
args.insert(2、n)#繰り返し回数
n = 1
上記の処理の後、argsに格納される最初の2つのパラメーターは、モジュールの入力チャネルと出力チャネルの数です。ボトルネックCSPとC3の2つのモジュールのみが、モジュールの繰り返し回数の深さパラメーターに従って調整されます。
elif mはnn.BatchNorm2dです:
args = [ch [f]]
elif mはConcatです:
c2 = sum([ch [x] for x in f])
elif mは検出です:
args.append([ch [x] for x in f])
if isinstance(args [1]、int):#アンカーの数
args [1] = [list(range(args [1] * 2))] * len(f)
elif mは契約です:
c2 = ch [f] * args [0] ** 2
elif mはExpandです:
c2 = ch [f] // args [0] ** 2
そうしないと:
c2 = ch [f]
上記は他のいくつかのタイプのモジュールです。
nn.BatchNorm2dの場合、チャネル数は変更されません。
Concatの場合、fはスプライスする必要のあるすべてのレイヤーのインデックスであり、出力チャネルc2はすべてのレイヤーの合計です。
Detectの場合は、検出ヘッドに対応します。これについては、このパートの後半で詳しく説明します。
コントラクトとエキスパンドは現在、モデルでは使用されていません。
m_ = nn.Sequential(* [m(* args)for _ in range(n)])if n> 1 else m(* args)#モジュール
ここで、argsのパラメーターはモジュールmを構築するために使用され、モジュールのサイクル数はパラメーターnによって制御されます。C3モジュールがn = 1に制御されていること、つまり、外部ビルドが1回だけループすることは注目に値します。
t = str(m)[8:-2] .replace( '__ main __。'、 '')#モジュールタイプ
np = sum([x.numel()for x in m_.parameters()])#number params
m_.i、m_.f、m_.type、m_.np = i、f、t、np#インデックスを添付、 'from'インデックス、タイプ、数値パラメータ
logger.info( '%3s%18s%3s%10.0f%-40s%-30s'%(i、f、n、np、t、args))#print
以下にいくつかの出力印刷を示します。次のように、モジュール構造の各レイヤーの数とパラメーターの量を確認できます。
n個のparamsモジュール引数から
0 -1 1 3520 models.common.Focus [3、32、3]
save.extend(x%i for x in([f] if isinstance(f、int)else f)if x!= -1)#保存リストに追加
layers.append(m_)
i == 0の場合:
ch = []
ch.append(c2)
nn.Sequential(* layers)、sorted(save)を返します
最後に、構築したモジュールをレイヤーに保存し、このレイヤーの出力チャンネル数をchリストに書き込みます。
すべてのサイクルが終了したら、モデルを作成できます。この時点で、モデルはすべて構築されます。
ここで、yolo.pyでparse_modelが呼び出された位置に戻り、init()関数の学習を完了し続けます。
self.model、self.save = parse_model(deepcopy(self.yaml)、ch = [ch])#モデル、保存リスト
self.names = [str(i)for i in range(self.yaml ['nc'])]#デフォルト名
#print([x.shape for x in self.forward(torch.zeros(1、ch、64、64))])
#ストライド、アンカーを構築する
m = self.model [-1] #Detect()
isinstance(m、Detect)の場合:
s = 256#2x最小ストライド
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()#1回だけ実行
#print( 'ストライド:%s'%m.stride.tolist())
#初期化の重み、バイアス
initialize_weights(self)
self.info()
logger.info( '')
ここで、forward()関数を1回呼び出すことにより、[1、C、256、256]のテンソルが入力され、FPN出力結果の次元が取得されます。次に、ダウンサンプリングのマルチストライドが計算されます:8、16、32。
最後に、アンカーは上記の値で除算され、アンカーは3つの異なるスケールにスケーリングされます。アンカーの最終的な形状は[3,3,2]です。
これまでのところ、init()関数は完全に通過しています。
2.さまざまなモジュールのソースコード分析
ネットワーク構築のプロセスにはさまざまなモジュールが関係しています。これらのモジュールは、デフォルトで、modelsフォルダーの下のcommon.pyファイルにあります。これらの機能を以下で確認します。
(1)通常の畳み込み変換
クラスConv(nn.Module):
#標準の畳み込み
def __init __(self、c1、c2、k = 1、s = 1、p = None、g = 1、act = True):#ch_in、ch_out、kernel、stride、padding、groups
super(Conv、self).__ init __()
self.conv = nn.Conv2d(c1、c2、k、s、autopad(k、p)、groups = g、bias = False)
self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU()if act is True else(act if isinstance(act、nn.Module)else nn.Identity())
def forward(self、x):
self.act(self.bn(self.conv(x)))を返します
defuseforward(self、x):
self.act(self.conv(x))を返します
通常の畳み込み。autopad()関数を呼び出して、同じパディングに必要なパディングの量を計算します。
デフォルトの活性化関数はSiLU()です。
SiLU関数形式:f(x)=x⋅σ(x)
微分関数の形式:f(x)= f(x)+σ(x)(1-f(x))
(2)ボトルネック構造
クラスBottleneck(nn.Module):
#標準的なボトルネック
def __init __(self、c1、c2、shortcut = True、g = 1、e = 0.5):#ch_in、ch_out、shortcut、groups、expansion
super(Bottleneck、self).__ init __()
c_ = int(c2 * e)#非表示チャネル
self.cv1 = Conv(c1、c_、1、1)
self.cv2 = Conv(c_、c2、3、1、g = g)
self.add =ショートカットおよびc1 == c2
def forward(self、x):
return x + self.cv2(self.cv1(x))if self.add else self.cv2(self.cv1(x))
BottleNeck構造は、デフォルトで最初の1x1畳み込みに設定され、チャネルが元の1/2に縮小されてから、3x3畳み込みによって特徴が抽出されることがわかります。入力チャネルc1と3x3畳み込み出力チャネルc2が等しい場合、残差出力が実行されます。ショートカットパラメータは、残りの接続を行うかどうかを制御します。
(3)BottleNeckCSPとC3Dalian 、人々の流れはどこにありますか?http: //www.dl-fkw.com/
クラスBottleneckCSP(nn.Module):
#CSPボトルネックhttps://github.com/WongKinYiu/CrossStagePartialNetworks
def __init __(self、c1、c2、n = 1、shortcut = True、g = 1、e = 0.5):#ch_in、ch_out、number、shortcut、groups、expansion
super(BottleneckCSP、self).__ init __()
c_ = int(c2 * e)#非表示チャネル
self.cv1 = Conv(c1、c_、1、1)
self.cv2 = nn.Conv2d(c1、c_、1、1、bias = False)
self.cv3 = nn.Conv2d(c_、c_、1、1、bias = False)
self.cv4 = Conv(2 * c_、c2、1、1)
self.bn = nn.BatchNorm2d(2 * c _)#cat(cv2、cv3)に適用
self.act = nn.LeakyReLU(0.1、inplace = True)
self.m = nn.Sequential(* [Bottleneck(c_、c_、shortcut、g、e = 1.0)for _ in range(n)])
def forward(self、x):
y1 = self.cv3(self.m(self.cv1(x)))
y2 = self.cv2(x)
self.cv4(self.act(self.bn(torch.cat((y1、y2)、dim = 1))))を返す
クラスC3(nn.Module):
#3つの畳み込みを伴うCSPボトルネック
def __init __(self、c1、c2、n = 1、shortcut = True、g = 1、e = 0.5):#ch_in、ch_out、number、shortcut、groups、expansion
super(C3、self).__ init __()
c_ = int(c2 * e)#非表示チャネル
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):
self.cv3(torch.cat((self.m(self.cv1(x))、self.cv2(x))、dim = 1))を返します
common.pyには2つのcsp構造が実装されています。
BottleneckCSPは、上記の構造に正確に対応しています。しかし、作者は、yoloV54.0のバージョンで構造のこの部分をC3に変更しました。
残余が除去された後のConv、および活性化関数が上記のLeakyReluからSiLUに変更されます。
(4)SPP
クラスSPP(nn.Module):
#YOLOv3-SPPで使用される空間ピラミッドプーリングレイヤー
def __init __(self、c1、c2、k =(5、9、13)):
super(SPP、self).__ init __()
c_ = c1 // 2#非表示チャネル
self.cv1 = Conv(c1、c_、1、1)
self.cv2 = Conv(c_ *(len(k)+ 1)、c2、1、1)
self.m = nn.ModuleList([nn.MaxPool2d(kernel_size = x、stride = 1、padding = x // 2)for x in k])
def forward(self、x):
x = self.cv1(x)
self.cv2(torch.cat([x] + [m(x)for m in self.m]、1))を返します。
SPPモジュールは、入力チャネルを半分にし、カーネルサイズがそれぞれ5、9、13のmaxpoolingを実行します。最後に、スプライシングが完了します。元の入力を含む4セットの結果の結合チャネルは、元の2倍である必要があります。
(5)フォーカス
クラスFocus(nn.Module):
#wh情報をc-spaceに集中させる
def __init __(self、c1、c2、k = 1、s = 1、p = None、g = 1、act = True):#ch_in、ch_out、kernel、stride、padding、groups
super(Focus、self).__ init __()
self.conv = Conv(c1 * 4、c2、k、s、p、g、act)
#self.contract = Contract(gain = 2)
def forward(self、x):#x(b、c、w、h)-> y(b、4c、w / 2、h / 2)
self.conv(torch.cat([x [...、:: 2、:: 2]、x [...、1 :: 2、:: 2]、x [...、:: 2を返す、1 :: 2]、x [...、1 :: 2、1 :: 2]]、1))
#self.conv(self.contract(x))を返す
フィーチャマップを4分の1に切り、それらを一緒に追加します。最終的な結果として、チャネル数は元の4倍になり、解像度は元の1/4になります(HとWはそれぞれ半分になります)。最後に、畳み込みによってチャンネル数がプリセット値に調整されます。