バックグラウンド
CVPR2023 ターゲット検出の分野における新しい研究として、Yolov7 はまだ時間をかけて研究する必要があります。
論文: https://arxiv.org/abs/2207.02696
コード: https://github.com/WongKinYiu/yolov7
ネットワーク構造
予備知識
VoVNet
軽量ネットワークを設計する場合、FLOP とモデル パラメーターが主な考慮事項になりますが、モデル サイズと FLOP を削減することは、推論時間を短縮することにはなりません。メモリ アクセス コスト (メモリ アクセス コスト、MAC) と GPU コンピューティング効率という 2 つの重要な要素も考慮する必要があります。
- マック
ShuffleNetV2 の論文に記載されている畳み込み層の MAC 計算方法によると、
MAC = hw ( ci + co ) + k 2 cico MAC=hw(c_i+c_o)+k^2c_ic_oマック_ _=うわ( c _私は+cああ)+k2c _私はcああ
其中 k , h , w , c i , c o k,h,w,c_i,c_o k 、h、w、c私は、cああそれぞれ、コンボリューション カーネルのサイズ、出力特徴マップの幅と高さ、入力チャネルと出力チャネルの数を表します。畳み込み層の計算量B = k 2 hwcico B=k^2hwc_ic_oB=k2時間__私はcああ、B が固定されている場合、次のようになります:
MAC ≥ 2 hw B k 2 + B hw MAC \ge 2\sqrt{\frac{hwB}{k^2}} + \frac{B}{hw}マック_ _≥2k2ああ、 WB+うわー_B
平均不等式によれば、入力チャネルと出力チャネルの数が同じ場合に限り、MAC は下限を取り、設計は最も効率的になります。
- GPUの計算効率
GPU コンピューティングの利点は並列コンピューティングのメカニズムにあり、大きなコンボリューション カーネルを持つコンボリューション層をいくつかの小さなコンボリューション層に分割すると、効果は同じで FLOP が削減されますが、GPU コンピューティングは非効率になります。したがって、FLOP と比較して、1 秒あたりの FLOP、つまり総 FLOP を総 GPU 推論時間で割った値にさらに注意を払う必要があり、指数が高いほど GPU 使用率が高くなります。
VoVNet は DenseNet 用に改良されており、以下の図に示すように、DenseNet のブロック内の各層は、追加の入力として以前のすべての層を受け取り、concat を通じて異なる層からの特徴マップを統合して、特徴の再利用を実現し、精度を向上させます。各層の入力は直線的に増加し、出力チャネル数は固定であるため、入力チャネル数と出力チャネル数が一致しない、つまり MAC が最適ではないという問題が発生します。さらに、入力チャネルの数が多いため、最初に次元を削減するために 1×1 畳み込みが使用されますが、この追加のレイヤーの導入は GPU の効率的な計算には役に立ちません。
DenseNet の問題は、各層が前の層の機能を集約し、その結果、機能の冗長性が生じることです。この目的を達成するために、VoVNet は、次の図に示すように、OSA (One-Shot Aggregation) モジュールを提案しました。
簡単に言えば、それまでのすべての層の特徴をブロックの最後の層に集約することです。入出力チャネル数の不一致の問題を解決し、1×1 畳み込み次元削減を必要としないため、最小の MAC が得られ、GPU 計算が効率的になります。
CSPネット
CSPNet(Cross Stage Partial Network)は、推論過程で多量の計算量を必要とする問題をネットワーク構造設計の観点から解決し、計算量を20%削減しながらモデルの能力を維持または向上させることができます。
著者のタスク推論の過剰な計算の問題は、ネットワーク最適化における勾配情報の繰り返しによって引き起こされますが、CSPNet では勾配の変化を最初から最後まで特徴マップに統合することで、精度を確保しながら計算を削減します。
CSPNet の基本的な考え方は、チャネル数に応じて入力特徴マップを 2 つの部分に分割し、一方は通常の密ブロック演算を実行し、もう一方は直接連結演算を実行します。ターゲット検出アルゴリズム Yolov5 の CSPBottleBlock の構造は次のとおりです:
入力は 2 つのブランチに分割され、1 つのブランチは最初に CBL を通過し、次に複数の残差構造を通過してから畳み込みを実行し、もう 1 つのブランチは直接畳み込みを実行します。ブランチの出力は concat 操作を実行し、次に BN、LeakyReLU を経由し、最後に CBL を実行します。
エラン
ELAN 高効率レイヤ アグリゲーション ネットワークは、ネットワーク レベルでは、勾配パス設計ネットワークのカテゴリに属します。ELAN を設計する主な目的は、モデルのスケーリングを実行すると、深いモデルの収束が徐々に悪化するという問題を解決することです。
背骨
Yolov7 のバックボーン特徴抽出ネットワークには、主に次の 2 つのモジュールが含まれています。
- ELAN: 画像の特徴を抽出するために使用されます。
- 遷移ブロック: 特徴マップをダウンサンプリングするために使用されます。通常、特徴マップは、カーネル サイズ 3×3、ストライド 2 の畳み込み、またはストライド 2 の MaxPooling レイヤーを使用してダウンサンプリングされます。Yolov7 では、ダウンサンプリングはこれら 2 つの操作を組み合わせて行われます。以下の図に示すように、遷移モジュールには 2 つのブランチがあり、左側のブランチはステップ サイズ 2 の MaxPooling と 1×1 コンボリューション、右側のブランチは 1×1 コンボリューションとサイズが 2 のコンボリューション カーネルです。長さ 2 の畳み込み。2 つのブランチの出力はチャネル スタックされます。
Yolov7 のバックボーン特徴抽出ネットワークの基本モジュールは Conv2d + BatchNorm2D + SiLU であり、そのコードは次のように実装されます。
import torch
import torch.nn as nn
class SiLU(nn.Module):
@staticmethod
def forward(x):
return x * nn.Sigmoid(x)
def autopad(k, p=None):
# 根据kernel_size计算padding的大小
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k]
return p
def Conv(nn.Module):
# Conv2d + BatchNorm + SiLU
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=SiLU()):
# in_channels, out_channels, kernel_size, 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, eps=0.001, momentum=0.03)
self.act = nn.LeakyReLU(0.01, inplace=True) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
return self.act(self.bn(self.conv(x)))
次に、CBS モジュールに基づいて ELAN モジュールを構築します。具体的なコードは次のとおりです。
class ELAN(nn.Module):
def __init__(self, c1, c2, c3, n=4, e=1, ids=[0]) -> None:
super(ELAN, self).__init__()
c_ = int(c2 * e)
self.ids = ids # [0, 1, 2, 3, 4, 5]中用于concat的tensor有哪些
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = nn.ModuleList([
Conv(c_ if i==0 else c2, c2, 3, 1) for i in range(n)
])
self.cv4 = Conv(c_ * 2 + c2 * (len(ids) -2), c3, 1, 1)
def forward(self, x):
x_1 = self.cv1(x)
x_2 = self.cv2(x)
x_all = [x_1, x_2]
for i in range(len(self.cv3)):
x_2 = self.cv3[i](x_2)
x_all.append(x_2)
# self.ids = [-1, -3, -5, -6] <=> [5, 3, 1, 0]
out = self.cv4(torch.cat([x_all[id] for id in self.ids], 1))
return out
次に、Transition Block の実装は次のようになります。
class MP(nn.Module):
def __init__(self, k=2) -> None:
super(MP, self).__init__()
self.m = nn.MaxPool2d(kernel_size=k, stride=k)
def forward(self, x):
return self.m(x)
class Transition_Block(nn.Module):
def __init__(self, c1, c2) -> None:
super(Transition_Block, self).__init__()
self.cv1 = Conv(c1, c2, 1, 1)
self.cv2 = Conv(c1, c2, 1, 1)
self.cv3 = Conv(c2, c2, 3, 2)
self.mp = MP()
def forward(self, x):
# h,w,c1-> h/2, w/2,c1 ->h/2, w/2, c2
x_1 = self.mp(x)
x_1 = self.cv1(x_1)
# h,w,c1 -> h, w, c2 -> h/2, w/2 c2
x_2 = self.cv2(x)
x_2 = self.cv3(x_2)
# h/2, w/2, c2 cat h/2, w/2, c2 -> h/2, w/2, c1
return torch.cat([x_2, x_1], 1)
次に、ELAN と Transition Block に基づいて、Yolov7 のバックボーン特徴抽出ネットワーク構造を次のように取得できます。
class Backbone(nn.Module):
def __init__(self, transition_channels, block_channels, n, pretrained=False) -> None:
super(Backbone, self).__init__()
#-------------------------------------#
# 输入图像尺寸大小为640, 640, 3
#-------------------------------------#
self.ids = [-1, -3, -5, -6]
# 640, 640, 3 -> 640, 640, 32 -> 320, 320, 64 -> 320, 320, 64
self.stem = nn.Sequential(
Conv(3, transition_channels, 3, 1),
Conv(transition_channels, transition_channels * 2, 3, 2),
Conv(transition_channels * 2, transition_channels * 2, 3, 1)
)
# 320, 320, 64 -> 160, 160, 128 -> 160, 160, 256
self.dark2 = nn.Sequential(
Conv(transition_channels * 2, transition_channels * 4, 3, 2),
ELAN(transition_channels* 4, block_channels * 2, transition_channels * 8, n=n, ids=self.ids)
)
# 160, 160, 256 -> 80, 80, 256 -> 80, 80, 512
self.dark3 = nn.Sequential(
Transition_Block(transition_channels* 8, transition_channels * 4),
ELAN(transition_channels*8, block_channels*4, transition_channels*16, n=n, ids=self.ids)
)
# 80, 80, 512 -> 40, 40, 512 -> 40, 40, 1024
self.dark4 = nn.Sequential(
Transition_Block(transition_channels * 16, transition_channels * 8),
ELAN(transition_channels * 16, block_channels * 8, transition_channels * 32, n=n, ids=self.ids)
)
# 40, 40, 1024 -> 20, 20, 1024 -> 20, 20, 1024
self.dark5 = nn.Sequential(
Transition_Block(transition_channels * 32, transition_channels * 16),
ELAN(transition_channels * 32, block_channels * 8, transition_channels * 32, n=n, ids=self.ids)
)
if pretrained:
url = 'https://github.com/bubbliiiing/yolov7-pytorch/releases/download/v1.0/yolov7_backbone_weights.pth'
checkpoint = torch.hub.load_state_dict_from_url(url=url, map_location="cpu", model_dir="./model_data")
self.load_state_dict(checkpoint, strict=False)
print("Load weights from " + url)
def forward(self, x):
x = self.stem(x)
x = self.dark2(x)
#---------------------------------------------------#
# dark3的输出为80, 80, 512, 是一个有效输出特征层
#---------------------------------------------------#
x = self.dark3(x)
feat1 = x
#---------------------------------------------------#
# dark4的输出为40, 40, 1024, 是一个有效输出特征层
#---------------------------------------------------#
x = self.dark4(x)
feat2 = x
#---------------------------------------------------#
# dark5的输出为20, 20, 1024, 是一个有效输出特征层
#---------------------------------------------------#
x = self.dark5(x)
feat3 = x
return feat1, feat2, feat3
Yolov7 のバックボーン特徴抽出ネットワークの構造図を次の図に示します。
首
ネック部分では、Yolov7 はマルチスケール特徴融合のために多層特徴を抽出し、合計 3 つの特徴レイヤーを抽出します。つまり、バックボーン特徴抽出ネットワークの出力です。入力画像が (640,640,3) の場合、3 つの特徴レイヤーのサイズは (80,80,512)、(40,40,1024)、および (20, 20, 1024)。
Yolov7 の特徴融合法の主な手順は次のとおりです。
- 最初に (20, 20, 1024) の特徴マップが SPPCSPC 構造を使用して抽出されます。これにより、P5 に設定された yolov7 の受容野が改善されます。SPPCSPS の構造を次の図に示します。
対応するコード実装は次のとおりです。
class SPPCSPC(nn.Module):
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5, k=(5, 9, 13)):
super(SPPCSPC, self).__init__()
c_ = int(2 * c2 * e)
# 左分支
self.cv1 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(c_, c_, 3, 1)
self.cv4 = Conv(c_, c_, 1, 1)
self.m = nn.ModuleList([nn.Maxpool2d(kernel_size=x, stride=1, padding=x//2)for x in k])
self.cv5 = Conv(4 * c_, c_, 1, 1)
self.cv6 = Conv(c_, c_, 3, 1)
# 右分支
self.cv2 = Conv(c1, c_, 1, 1)
# concat 输出
self.cv7 = Conv(2 * c_, c2, 1, 1)
def forward(self, x):
x1 = self.cv4(self.cv3(self.cv1(x)))
y1 = self.cv6(self.cv5(torch.cat([x1] + [m(x1) for m in self.m], 1)))
y2 = self.cv2(x)
return self.cv7(torch.cat((y1, y2), dim=1))
- P5 を 1×1 畳み込みに入力してチャネルを調整し、P5 特徴マップ UpSample をアップサンプリングし、(40,40,1024) 特徴マップとの畳み込み後に特徴マップに対して連結演算を実行してから、ELAN を使用します。 module 特徴を抽出して P4 を取得します。サイズは (40,40,256) です。
- P4 は、1×1 畳み込み調整チャネルを実行し、次にアップサンプリング Upsample 操作を実行し、((80,80,512) 特徴マップとの畳み込み後の特徴マップに対して連結操作を実行し、ELAN を使用して統合特徴マップから特徴を抽出します。 P3、 P3_out とも呼ばれ、特徴マップのサイズは (80,80,128) です。
- P3_out の特徴マップは遷移ブロック操作によってダウンサンプリングされ、取得された特徴マップは P4 と連結され、ELAN を使用して特徴が抽出されて P4_out が取得されます。サイズは (40,40,256) です。
- P4_out の特徴マップは遷移ブロック操作によってダウンサンプリングされ、取得された特徴マップは P5 と連結され、ELAN を使用して特徴が抽出され、サイズ (20,20,128) の P5_out が取得されます。
特徴融合モジュール PANet は、異なるサイズの特徴レイヤーを融合し、特徴抽出の向上に役立ちます。全体的な構造図を次の図に示します。
コードの実装は次のとおりです。
class YoloBody(nn.Module):
def __init__(self) -> None:
super(YoloBody, self).__init__()
#-----------------------------------#
# 定义yolov7的参数
#-----------------------------------#
transition_channels = 32
block_channels = 32
panet_channels = 32
n = 4 # ELAN模块中右边两个分支卷积的个数
e = 2
ids = [-1, -2, -3, -4, -5, -6]
#-----------------------------------#
# 输入图像尺寸为640, 640,3
#-----------------------------------#
#-----------------------------------#
# 主干网络提取特征
# 输出三个有效特征层, 尺寸分别为:
# 80, 80, 512
# 40, 40, 1024
# 20, 20, 1024
#-----------------------------------#
self.backbone = Backbone(transition_channels=transition_channels, block_channels=block_channels, n=n)
#-----------------------------------#
# 特征融合网络
#-----------------------------------#
self.upsample = nn.Upsample(scale_factor=2, mode="nearest") # 最邻近方式
self.sppcspc = SPPCSPC(transition_channels*32, transition_channels*16)
self.conv_for_P5 = Conv(transition_channels*16, transition_channels*8)
self.conv_for_feat2 = Conv(transition_channels*32, transition_channels*8)
self.elan_for_concat1 = ELAN(transition_channels*16, panet_channels*4, transition_channels*8, e=e, n=n, ids=ids)
self.conv_for_P4 = Conv(transition_channels*8, transition_channels*4)
self.conv_for_feat1 = Conv(transition_channels*16, transition_channels*4)
self.elan_for_concat2 = ELAN(transition_channels*8, panet_channels*2, transition_channels*4, e=e, n=n, ids=ids)
self.down_sample1 = Transition_Block(transition_channels*4, transition_channels*4)
self.elan_for_concat3 = ELAN(transition_channels*16, panet_channels*4, transition_channels*8, e=e, n=n, ids=ids)
self.down_sample2 = Transition_Block(transition_channels*8, transition_channels*8)
self.elan_for_concat4 = ELAN(transition_channels * 32, panet_channels * 8, transition_channels * 16, e=e, n=n, ids=ids)
def forward(self, x):
#-----------------------------------#
# 主干网络提取特征
# 输出三个有效特征层, 尺寸分别为:
# 80, 80, 512
# 40, 40, 1024
# 20, 20, 1024
#-----------------------------------#
feat1, feat2, feat3 = self.backbone(x)
#-----------------------------------#
# 特征融合网络, 输出三个特征图
# P3_out: 80, 80, 128
# P4_out: 40, 40, 256
# P5_out: 20, 20, 512
#-----------------------------------#
P5 = self.sppcspc(feat3)
P5_conv = self.conv_for_P5(P5)
P5_upsample=self.upsample(P5_conv)
P4 = torch.cat([self.conv_for_feat2(feat2), P5_upsample], 1)
P4 = self.elan_for_concat1(P4)
P4_conv = self.conv_for_P4(P4)
P4_upsample= self.upsample(P4_conv)
P3 = torch.cat([self.conv_for_feat1(feat1), P4_upsample], 1)
P3_out = self.elan_for_concat2(P3)
P3_downsample = self.down_sample1(P3_out)
P4 = torch.cat([P3_downsample, P4], 1)
P4_out = self.elan_for_concat3(P4)
P4_downsample = self.down_sample2(P4_out)
P5 = torch.cat([P4_downsample, P5], 1)
P5_out = self.elan_for_concat4(P5)
頭
特徴融合モジュールを通じて、3 つの拡張特徴を取得できます。サイズは (80, 80, 128)、(40, 40, 256)、(20, 20, 512) で、これら 3 つの特徴マップが Yolo に渡されます。 headモジュールで予測結果を取得します。
予測に畳み込み層を直接使用する Yolov5 とは異なり、Yolov7 は畳み込みの前に RepConv 構造を使用します。RepConv モジュールは RepVGG ネットワークから来ています。アイデンティティ ブランチと残差ブランチはトレーニング フェーズで追加されます。推論フェーズでは、RepConv モジュールは再パラメータ化テクノロジを通じて通常の 3×3 畳み込みに変換され、モデル。
コードは次のように実装されます。
class RepConv(nn.Module):
def __init__(self, c1, c2, k=3, s=1, p=None, g=1, act=SiLU()) -> None:
super(RepConv, self).__init__()
self.groups = g
self.in_channels = c1
self.out_channels = c2
assert k == 3
assert autopad(k, p) == 1
padding_11 = autopad(k, p) - k // 2
self.rbr_identity = (nn.BatchNorm2d(num_features=c1, eps=0.001, momentum=0.03) if c2==c1 and s == 1 else None)
self.rbr_dense = nn.Sequential(
nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False),
nn.BatchNorm2d(num_features=c2, eps=0.001, momentum=0.03)
)
self.rbr_1x1 = nn.Sequential(
nn.Conv2d(c1, c2, 1, s, padding_11, groups=g, bias=False),
nn.BatchNorm2d(num_features=c2, eps=0.001, momentum=0.03)
)
self.act = nn.LeakyReLU(0.1, inplace=True) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
if self.rbr_identity is None:
identity_out = 0
else:
identity_out = self.rbr_identity(x)
return self.act(self.rbr_dense(x) + self.rbr_1x1(x) + identity_out)
class YoloBody(nn.Module):
def __init__(self, anchor_masks, num_classes) -> None:
super(YoloBody, self).__init__()
#-----------------------------------#
# 定义yolov7的参数
#-----------------------------------#
transition_channels = 32
block_channels = 32
panet_channels = 32
n = 4 # ELAN模块中右边两个分支卷积的个数
e = 2
ids = [-1, -2, -3, -4, -5, -6]
#-----------------------------------#
# 输入图像尺寸为640, 640,3
#-----------------------------------#
#-----------------------------------#
# 主干网络提取特征
# 输出三个有效特征层, 尺寸分别为:
# 80, 80, 512
# 40, 40, 1024
# 20, 20, 1024
#-----------------------------------#
self.backbone = Backbone(transition_channels=transition_channels, block_channels=block_channels, n=n)
#-----------------------------------#
# 特征融合网络
#-----------------------------------#
self.upsample = nn.Upsample(scale_factor=2, mode="nearest") # 最邻近方式
self.sppcspc = SPPCSPC(transition_channels*32, transition_channels*16)
self.conv_for_P5 = Conv(transition_channels*16, transition_channels*8)
self.conv_for_feat2 = Conv(transition_channels*32, transition_channels*8)
self.elan_for_concat1 = ELAN(transition_channels*16, panet_channels*4, transition_channels*8, e=e, n=n, ids=ids)
self.conv_for_P4 = Conv(transition_channels*8, transition_channels*4)
self.conv_for_feat1 = Conv(transition_channels*16, transition_channels*4)
self.elan_for_concat2 = ELAN(transition_channels*8, panet_channels*2, transition_channels*4, e=e, n=n, ids=ids)
self.down_sample1 = Transition_Block(transition_channels*4, transition_channels*4)
self.elan_for_concat3 = ELAN(transition_channels*16, panet_channels*4, transition_channels*8, e=e, n=n, ids=ids)
self.down_sample2 = Transition_Block(transition_channels*8, transition_channels*8)
self.elan_for_concat4 = ELAN(transition_channels * 32, panet_channels * 8, transition_channels * 16, e=e, n=n, ids=ids)
#-----------------------------------#
# Yolo Head预测模块
#-----------------------------------#
self.rep_conv1 = RepConv(transition_channels * 4, transition_channels * 8, 3, 1)
self.rep_conv2 = RepConv(transition_channels * 8, transition_channels * 16, 3, 1)
self.rep_conv3 = RepConv(transition_channels * 16, transition_channels * 32, 3, 1)
self.yolo_head_P3 = nn.Conv2d(transition_channels * 8, len(anchor_masks[2]) * (5 + num_classes), 1)
self.yolo_head_P4 = nn.Conv2d(transition_channels * 16, len(anchor_masks[1]) * (5 + num_classes), 1)
self.yolo_head_P5 = nn.Conv2d(transition_channels * 32, len(anchor_masks[0]) * (5 + num_classes), 1)
def forward(self, x):
#-----------------------------------#
# 主干网络提取特征
# 输出三个有效特征层, 尺寸分别为:
# 80, 80, 512
# 40, 40, 1024
# 20, 20, 1024
#-----------------------------------#
feat1, feat2, feat3 = self.backbone(x)
#-----------------------------------#
# 特征融合网络, 输出三个特征图
# P3_out: 80, 80, 128
# P4_out: 40, 40, 256
# P5_out: 20, 20, 512
#-----------------------------------#
P5 = self.sppcspc(feat3)
P5_conv = self.conv_for_P5(P5)
P5_upsample=self.upsample(P5_conv)
P4 = torch.cat([self.conv_for_feat2(feat2), P5_upsample], 1)
P4 = self.elan_for_concat1(P4)
P4_conv = self.conv_for_P4(P4)
P4_upsample= self.upsample(P4_conv)
P3 = torch.cat([self.conv_for_feat1(feat1), P4_upsample], 1)
P3_out = self.elan_for_concat2(P3)
P3_downsample = self.down_sample1(P3_out)
P4 = torch.cat([P3_downsample, P4], 1)
P4_out = self.elan_for_concat3(P4)
P4_downsample = self.down_sample2(P4_out)
P5 = torch.cat([P4_downsample, P5], 1)
P5_out = self.elan_for_concat4(P5)
#-----------------------------------#
# Yolo Head预测模块
#-----------------------------------#
P3_out = self.rep_conv1(P3_out)
P4_out = self.rep_conv2(P4_out)
P5_out = self.rep_conv3(P5_out)
#-----------------------------------#
# 第三个预测特征层
#-----------------------------------#
out3 = self.yolo_head_P3(P3_out)
#-----------------------------------#
# 第二个预测特征层
#-----------------------------------#
out2 = self.yolo_head_P4(P4_out)
#-----------------------------------#
# 第一个预测特征层
#-----------------------------------#
out1 = self.yolo_head_P5(P5_out)
return [out1, out2, out3]
デコードとNMS
Yolo Headモジュールはネットワークの予測結果を出力するモジュールで、レイヤーは全部で3層あり、サイズは(N, 80, 80, 255)、(N, 40, 40, 255)、(N, 20, 20, 255)。このうち、255=3×85は、単一グリッドの過去3フレームの予測情報を表し、85=4+1+80は、実際のフレームと以前のフレームとの間のオフセット、ターゲットが含まれるかどうか、および各カテゴリの確率値の予測。
- デコード
各ボックスの予測結果の最初の 4 ビットは、実際のボックスと前のボックスの間のオフセットであるため、ネットワーク入力に対応するようにデコードのために位置合わせする必要があります。このうち、最初の 2 桁は前のフレームの中心座標のオフセット、最後の 2 桁は幅と高さで、デコードの式は次のようになります: bx = 2 σ ( tx ) − 0.5 + cx b_x
= 2\sigma(t_x) - 0.5 + c_xb×=2p ( t _×)−0.5+c×
by = 2 σ ( ty ) − 0.5 + cy b_y = 2\sigma(t_y) - 0.5 + c_ybはい=2p ( t _はい)−0.5+cはい
bw = pw ( 2 σ ( tw ) ) 2 b_w = p_w(2\sigma(t_w))^2bw=pw( 2秒( tw) )2
bh = ph ( 2 σ ( th ) ) 2 b_h = p_h(2\sigma(t_h))^2bふ=pふ( 2秒( tふ) )2
前フレームとオフセットの情報を組み合わせて、予測フレームを取得するプロセスは次の図に示されており、
復号プロセスの具体的な実装コードは次のとおりです。
def sigmoid(self, x):
return 1 / ( 1 + np.exp(-x))
def decode_box(self, predictions):
outputs = list()
for i, pred in enumerate(predictions):
#----------------------------------------------#
# 输入的predictions包含3个元素, 尺寸分别为:
# batch_size, 255, 20, 20
# batch_size, 255, 40, 40
# batch_size, 255, 80, 80
#----------------------------------------------#
batch_size = pred.size(0)
feature_height = pred.size(1)
feature_width = pred.size(2)
stride_h = self.input_shape[0] / feature_height
stride_w = self.input_shape[1] / feature_width
# 此时获得的scaled_anchors大小是相对于特征图而言的
scaled_anchors = [(w/stride_w, h/stride_h) for w, h in self.anchors[self.anchor_mask[i]]]
#----------------------------------------------#
# 转换predications的维度:
# batch_size, 3, 20, 20, 85
# batch_size, 3, 40, 40, 85
# batch_size, 3, 80, 80, 85
#----------------------------------------------#
pred = pred.view(batch_size, len(self.anchor_masks[i], self.bbox_attrs, feature_height, feature_width))
pred = pred.permute(0, 1, 3, 4, 2).contigous()
#----------------------------------------------#
# 调整预测框参数:
#----------------------------------------------#
x = self.sigmoid(pred[..., 0])
y = self.sigmoid(pred[..., 1])
w = self.sigmoid(pred[..., 2])
h = self.sigmoid(pred[..., 3])
# 获取目标置信度, 是否包含物体
box_conf = self.sigmoid(pred[..., 4])
# 获取每个类别的概率值
cls_conf = self.sigmoid(pred[..., 5:])
# 根据特征图大小, 生成网格, 特征中心点为左上角点
grid_x = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.linspace(0, feature_width - 1, feature_width), 0), feature_height, axis=0), 0), batch_size * len(self.anchors_mask[i]), axis=0)
grid_x = np.reshape(grid_x, np.shape(x))
grid_y = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.linspace(0, feature_height - 1, feature_height), 0), feature_width, axis=0).T, 0), batch_size * len(self.anchors_mask[i]), axis=0)
grid_y = np.reshape(grid_y, np.shape(y))
#----------------------------------------------------------#
# 按照网格格式生成先验框的宽高
# batch_size,3,20,20
#----------------------------------------------------------#
anchor_w = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.array(scaled_anchors)[:, 0], 0), batch_size, axis=0), -1), feature_height * feature_width, axis=-1)
anchor_h = np.repeat(np.expand_dims(np.repeat(np.expand_dims(np.array(scaled_anchors)[:, 1], 0), batch_size, axis=0), -1), feature_height * feature_width, axis=-1)
anchor_w = np.reshape(anchor_w, np.shape(w))
anchor_h = np.reshape(anchor_h, np.shape(h))
#---------------------------------------------#
# 结合先验框和offset得到预测框
#---------------------------------------------#
pred_boxes = np.zeros(pred[..., :4].shape)
pred_boxes[..., 0] = x * 2. - 0.5 + grid_x
pred_boxes[..., 1] = y * 2. - 0.5 + grid_y
pred_boxes[..., 2] = (w * 2.)** 2 * anchor_w
pred_boxes[..., 3] = (h * 2.) ** 2 * anchor_h
#----------------------------------------------------------#
# 将输出结果归一化成0-1小数的形式
#----------------------------------------------------------#
_scale = np.array([feature_width, feature_height, feature_width, feature_height])
#----------------------------------------------------------#
# 3个输出, 尺寸分别为:
# batch_size, 3 * 20 * 20, 85
# batch_size, 3 * 40 * 40, 85
# batch_size, 3 * 80 * 80, 85
#---------------------------------------------------------#
output = np.concatenate([np.reshape(pred_boxes, (batch_size, -1, 4)) / _scale,
np.reshape(box_conf, (batch_size, -1, 1)), np.reshape(cls_conf, (batch_size, -1, self.num_classes))], -1)
outputs.append(output)
return outputs
- NMS
NMS、非最大値抑制。その考え方は、極大値を検索し、非最大値を抑制することです。ターゲット検出タスクにおける非最大抑制のプロセスは次のとおりです。
- すべての予測ボックスについて、最初にカテゴリ別にグループ化します。
- 各カテゴリの予測ボックスは、信頼度に応じて大きいものから小さいものの順に並べ替えられます。
- 最も信頼度の高い予測ボックスを最終出力リストに追加し、他のすべての予測ボックスを使用してその IoU を計算します。
- IoU がしきい値より大きい予測ボックスを削除します。
- 予測ボックスのリストが空になるまで cd ステップを繰り返します。
def non_max_supperession(self, predication, num_classes, conf_thres=0.5, nms_thres=0.4):
#---------------------------------------------------------#
# predication输入尺寸为[batch_size, num_anchors, 85]
# num_anchors = 20*20*3 + 40*40*3 + 80*80*3
#---------------------------------------------------------#
#---------------------------------------------------------#
# 将预测框从(center_x, center_y, width, height)格式
# 转换为(left, top, right, bottom)
#---------------------------------------------------------#
box_corner = torch.zeros_like(predication)
box_corner[..., 0] = predication[..., 0] - predication[..., 2] / 2
box_corner[..., 1] = predication[..., 1] - predication[..., 3] / 2
box_corner[..., 2] = predication[..., 0] + predication[..., 2] / 2
box_corner[..., 1] = predication[..., 1] + predication[..., 3] / 2
predication[..., :4] = box_corner[..., :4]
outputs = [None for _ in range(len(predication))]
for i, image_pred in enumerate(predication):
#----------------------------------------#
# 每个预测框会预测80类, 求得当前预测框
# 最大的预测置信度, 以及对应的类别
# class_conf [num_anchors, 1] 种类置信度
# class_pred [num_anchors, 1] 对应的类别
#----------------------------------------#
class_conf, class_pred = torch.max(image_pred[:, 5:5+num_classes], 1, keepdim=True)
#-------------------------------------#
# 利用置信度进行第一轮筛选
# 目标概率和类别概率是否大于阈值
#-------------------------------------#
conf_mask = (image_pred[:, 4] * class_conf[:, 0] >= conf_thres).squeeze()
#-------------------------------------#
# 根据置信度筛选结果进行预测结果筛选
#-------------------------------------#
image_pred = image_pred[conf_mask]
class_conf = class_conf[conf_mask]
class_pred = class_pred[conf_mask]
# 判断预测结果筛选之后是否还存在预测框
if not image_pred.size(0):
continue
#--------------------------------------------#
# detections [num_anchors, 7]
# 7分别代表[x1, y1, x2, y2, obj_conf, class_conf, class_pred]
#--------------------------------------------#
detections = torch.cat((image_pred[:, :5], class_conf.float(), class_conf.float()), 1)
#-----------------------------------#
# 获取预测结果中包含的类别
#----------------------------------#
unique_labels = detections[:, -1].cpu().unique()
if predication.is_cuda:
unique_labels = unique_labels.cuda()
detections = detections.cuda()
for c in unique_labels:
#-----------------------------------#
# 获得某一类的预测结果
#----------------------------------#
detection_class = detections[detections[:, -1] == c]
#-------------------------#
# 按照预测框的置信度排序
#-------------------------#
_, conf_sort_index = torch.sort(detection_class[:, 4] * detection_class[:, 5], descending=True)
detection_class = detection_class[conf_sort_index]
# 进行非极大值抑制
max_detections = list()
while detection_class.size(0):
# 取出该类置信度最高的, 与剩余的预测框进行IoU, 判断重合程度是否大于nms_thres
max_detections.append(detection_class[0].unsqueeze())
if len(detection_class) == 1:
break
ious = bbox_iou(max_detections[-1], detection_class[1:])
detection_class = detection_class[1:][ious < nms_thres]
max_detections = torch.cat(max_detections).data
outputs[i] = max_detections if outputs[i] is None else torch.cat((outputs[i], max_detections))
return outputs
モデル推論
- 画像の前処理
入力画像は推論の前に前処理する必要があり、具体的な手順は次のとおりです。
image_name = "example.jpg"
image_data = cv2.imread(image_name)
#---------------------------------#
# 将图像转换为RGB图像
#---------------------------------#
image_data = cvtColor(image)
#---------------------------------#
# 维持图像宽高比, 添加灰条
#---------------------------------#
image_data = resize_image(image, (input_width, input_height))
#---------------------------------#
# 归一化
#---------------------------------#
image_data = np.array(image_data, dtype=np.float32)
image_data = image_data / 255
#--------------------------------#
# HWC转NCHW
#--------------------------------#
image_data = np.expand_dims(np.transpose(image_data, (2, 0, 1)), 0)
- ネットワーク推論
前処理された画像は順伝播のモデルに入力され、Decode と NMS の後、予測ボックスが取得されます。
- 予測結果をリスケールする
前のレイヤーから渡された予測フレームは、前処理された画像に関連しています。つまり、サイズは 640 × 640 で、画像には灰色のバーが含まれています。有効画像の左上隅の始点を求め、対象フレーム情報を元の画像にリスケールする処理です。
# ---------------------------------------#
# 将box从输入图像维度转换为原图
# img1_shape 网络输入大小
# boxes当前图中box的信息
# img0_shape 原图大小
# ---------------------------------------#
def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None):
# Rescale boxes (xyxy) from img1_shape to img0_shape
if ratio_pad is None: # calculate from img0_shape
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
else:
gain = ratio_pad[0][0]
pad = ratio_pad[1]
boxes[..., [0, 2]] -= pad[0] # x padding
boxes[..., [1, 3]] -= pad[1] # y padding
boxes[..., :4] /= gain
clip_boxes(boxes, img0_shape)
return boxes
- レンダリング結果
レンダリング結果とは、元の画像上にボックスを描画し、保存または表示することを指します。
p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3]))
cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA)
if label:
tf = max(self.lw - 1, 1) # font thickness
w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0] # text width, height
outside = p1[1] - h >= 3
p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3
cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA) # filled
cv2.putText(self.im,
label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2),
0,
self.lw / 3,
txt_color,
thickness=tf,
lineType=cv2.LINE_AA)
表示された結果を次の図に示します。