Huawei NoahがVanillaNetを提案 - ミニマリストで強力な新しいビジュアルバックボーン
论文地址:https://arxiv.org/pdf/2305.12972.pdf
コード アドレス:GitHub - huawei-noah/VanillaNet
VanillaNet の紹介
基礎となるモデルの中心となるのは、コンピューター ビジョンと自然言語処理の驚くべき成功によって実証される、「さらなる違い」の哲学です。ただし、最適化の課題と Transformer モデルに固有の複雑さのため、シンプルさへのパラダイム シフトが必要です。この研究では、設計に優雅さを組み込んだニューラル ネットワーク アーキテクチャである VanillaNet を紹介します。 VanillaNet は、深い深さ、ショートカット、自己注意などの複雑な操作を回避することで、驚くほどシンプルでありながら非常に強力です。各層は慎重にコンパクトでシンプルな構造に作られており、非線形活性化関数はトレーニング後に枝刈りされて元のアーキテクチャが復元されます。 VanillaNet は、固有の複雑さという課題を克服し、リソースに制約のある環境に最適です。理解しやすく非常に簡素化されたアーキテクチャにより、効率的な導入のための新たな可能性が開かれます。広範な実験により、VanillaNet がよく知られたディープ ニューラル ネットワークやビジュアル トランスフォーマーと同等のパフォーマンスを提供することが示され、ディープ ラーニングにおけるミニマリズムの力が証明されています。 VanillaNet のこの先見の明に満ちた旅は、基盤となるモデルを再定義して現状に挑戦し、エレガントで効果的なモデル設計への新しい道を生み出す大きな可能性を秘めています。
過去数十年にわたり、研究者はニューラル ネットワークの基本設計についてある程度の合意に達しました。最先端の画像分類ネットワーク アーキテクチャのほとんどは、次の 3 つの部分で構成されます。
- バックボーン ブロックは、入力画像を 3 チャネルから複数のチャネルに変換し、ダウンサンプリングを実行するために使用されます。これは、学習に役立つ情報トピックです。
- 本体には通常 4 つのステージがあり、各ステージは同一のブロックを積み重ねることによって導出されます。各段階の後、フィーチャのチャネルは拡大しますが、高さと幅は減少します。さまざまなネットワークがさまざまな種類のブロックを利用してスタックし、深いモデルを構築します。
- 全結合層分類出力。
既存のディープ ネットワークは成功しているにもかかわらず、多数の複雑なレイヤーを利用して、次のタスクのための高レベルの機能を抽出しています。たとえば、有名な ResNet では、ImageNet でトップ 1 の 70% 以上の精度を達成するには、shortcat を使用した 34 層または 50 層が必要です。 Vit の基本バージョンは 62 レイヤーで構成されています。これは、セルフ アテンションの K、Q、V の計算に複数のレイヤーが必要であるためです。 AI チップが大量に登場するにつれ、最新の GPU は並列計算を簡単に実行できるため、ニューラル ネットワークの推論速度のボトルネックはもはや FLOP やパラメータではなくなりました。対照的に、その複雑な設計と大きな深さにより、速度が妨げられます。この目的を達成するために、図 1 にそのフレームワーク図を示す Vanilla ネットワーク、つまり VanillaNet を提案します。バックボーン、ボディ、完全に接続されたレイヤーを含む、一般的なニューラル ネットワーク設計に従っています。既存のディープネットワークとは異なり、各段階で 1 つのレイヤーのみを使用して、可能な限り少ないレイヤーで非常にシンプルなネットワークを構築します。このネットワークの特徴は、ショートカットを使用しないこと(ショートカットを使用するとメモリアクセス時間が長くなる)、セルフアテンションなどの複雑なモジュールを持たないことです。
深層学習では、トレーニング段階でより強力な能力を導入することでモデルのパフォーマンスを向上させるのが一般的です。この目的を達成するために、ディープ トレーニング技術を利用して、トレーニング中に提案されている VanillaNet の機能を向上させることを提案します。
最適化戦略 1: 深いトレーニング、浅い推論 VanillaNet アーキテクチャの非線形性を改善するために、最初に深いトレーニング (ディープ トレーニング) 戦略を提案しました。トレーニング プロセス中に分割します。 1 つの畳み込み層を 2 つの畳み込み層に分割し、間に次の非線形演算を挿入します。
このうち、A は伝統的な非線形活性化関数であり、最も単純なものは ReLU であり、モデルが最適化されるにつれて λ は徐々に 1 になり、VanillaNet の構造を変更することなく 2 つの畳み込み層を 1 つの層にマージできます。 。
最適化戦略 2: 活性化関数を変更する VanillaNet の非線形性を改善したいため、より直接的な解決策は、より強い非線形性を持つ活性化関数を用意することです。この活性化関数は、優れた並列処理と高速な速度?この必要かつ必要な構成を達成するために、複数の ReLU の重みとバイアスを積み重ねる、系列インスピレーションに基づく活性化関数を提案します。
?
YOLOの改善
yolov7-tiny のバックボーンを例にすると、v5v8 を使用する場合は、バックボーンを v5v8 構成にコピーするだけです。
まず、yolov7-tiny-vanilla.yaml ファイルを構成します。
# parameters
nc: 80 # number of classes
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
activation: nn.ReLU()
# anchors
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# yolov7-tiny backbone
backbone:
# [from, number, module, args] c2, k=1, s=1, p=None, g=1, act=True
[[-1, 1, VanillaStem, [64, 4, 4, None, 1]], # 0-P1/4
[-1, 1, VanillaBlock, [256, 1, 2, None, 1]], # 1-P2/8
[-1, 1, VanillaBlock, [512, 1, 2, None, 1]], # 2-P3/16
[-1, 1, VanillaBlock, [1024, 1, 2, None, 1]], # 3-P4/32
]
# yolov7-tiny head
head:
[[1, 1, Conv, [128, 1, 1, None, 1]], # 4
[2, 1, Conv, [256, 1, 1, None, 1]], # 5
[3, 1, Conv, [512, 1, 1, None, 1]], # 6
[-1, 1, SPPCSPCSIM, [256]], # 7
[-1, 1, Conv, [128, 1, 1, None, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[5, 1, Conv, [128, 1, 1, None, 1]], # route backbone P3
[[-1, -2], 1, Concat, [1]], # 11
[-1, 1, ELAN, [128, 1, 1, None, 1]], # 12
[-1, 1, Conv, [64, 1, 1, None, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[4, 1, Conv, [64, 1, 1, None, 1]], # route backbone P2
[[-1, -2], 1, Concat, [1]], # 16
[-1, 1, ELAN, [64, 1, 1, None, 1]], # 17
[-1, 1, Conv, [128, 3, 2, None, 1]],
[[-1, 12], 1, Concat, [1]],
[-1, 1, ELAN, [128, 1, 1, None, 1]], # 20
[-1, 1, Conv, [256, 3, 2, None, 1]],
[[-1, 7], 1, Concat, [1]],
[-1, 1, ELAN, [256, 1, 1, None, 1]], # 23
[17, 1, Conv, [128, 3, 1, None, 1]],
[20, 1, Conv, [256, 3, 1, None, 1]],
[23, 1, Conv, [512, 3, 1, None, 1]],
[[24,25,26], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
ELAN モジュールは、元の構成ファイル内の対応するモジュールの簡易バージョンです。
簡素化された方法:yolov7 はネットワーク yaml 構成ファイルを簡素化します_athrunsunny のブログ - CSDN ブログ
モデルの全体的なパラメーターが v5s および v7tiny のパラメーターと大きく異なることを確認するために、VanillaBlock のチャネル数は元の記事よりも少なく設定されています。
common.pyに追加
class activation(nn.ReLU):
def __init__(self, dim, act_num=3, deploy=False):
super(activation, self).__init__()
self.act_num = act_num
self.deploy = deploy
self.dim = dim
self.weight = torch.nn.Parameter(torch.randn(dim, 1, act_num * 2 + 1, act_num * 2 + 1))
if deploy:
self.bias = torch.nn.Parameter(torch.zeros(dim))
else:
self.bias = None
self.bn = nn.BatchNorm2d(dim, eps=1e-6)
nn.init.trunc_normal_(self.weight, std=.02)
def forward(self, x):
if self.deploy:
return torch.nn.functional.conv2d(
super(activation, self).forward(x),
self.weight, self.bias, padding=self.act_num, groups=self.dim)
else:
return self.bn(torch.nn.functional.conv2d(
super(activation, self).forward(x),
self.weight, padding=self.act_num, groups=self.dim))
def _fuse_bn_tensor(self, weight, bn):
kernel = weight
running_mean = bn.running_mean
running_var = bn.running_var
gamma = bn.weight
beta = bn.bias
eps = bn.eps
std = (running_var + eps).sqrt()
t = (gamma / std).reshape(-1, 1, 1, 1)
return kernel * t, beta + (0 - running_mean) * gamma / std
def switch_to_deploy(self):
kernel, bias = self._fuse_bn_tensor(self.weight, self.bn)
self.weight.data = kernel
self.bias = torch.nn.Parameter(torch.zeros(self.dim))
self.bias.data = bias
self.__delattr__('bn')
self.deploy = True
class VanillaStem(nn.Module):
def __init__(self, in_chans=3, dims=96,
k=0, s=0, p=None,g=0, act_num=3, deploy=False, ada_pool=None, **kwargs):
super().__init__()
self.deploy = deploy
stride, padding = (4, 0) if not ada_pool else (3, 1)
if self.deploy:
self.stem = nn.Sequential(
nn.Conv2d(in_chans, dims, kernel_size=k, stride=stride, padding=padding),
activation(dims, act_num, deploy=self.deploy)
)
else:
self.stem1 = nn.Sequential(
nn.Conv2d(in_chans, dims, kernel_size=k, stride=stride, padding=padding),
nn.BatchNorm2d(dims, eps=1e-6),
)
self.stem2 = nn.Sequential(
nn.Conv2d(dims, dims, kernel_size=1, stride=1),
nn.BatchNorm2d(dims, eps=1e-6),
activation(dims, act_num)
)
self.act_learn = 1
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, (nn.Conv2d, nn.Linear)):
nn.init.trunc_normal_(m.weight, std=.02)
nn.init.constant_(m.bias, 0)
def forward(self, x):
if self.deploy:
x = self.stem(x)
else:
x = self.stem1(x)
x = torch.nn.functional.leaky_relu(x, self.act_learn)
x = self.stem2(x)
return x
def _fuse_bn_tensor(self, conv, bn):
kernel = conv.weight
bias = conv.bias
running_mean = bn.running_mean
running_var = bn.running_var
gamma = bn.weight
beta = bn.bias
eps = bn.eps
std = (running_var + eps).sqrt()
t = (gamma / std).reshape(-1, 1, 1, 1)
return kernel * t, beta + (bias - running_mean) * gamma / std
def switch_to_deploy(self):
self.stem2[2].switch_to_deploy()
kernel, bias = self._fuse_bn_tensor(self.stem1[0], self.stem1[1])
self.stem1[0].weight.data = kernel
self.stem1[0].bias.data = bias
kernel, bias = self._fuse_bn_tensor(self.stem2[0], self.stem2[1])
self.stem1[0].weight.data = torch.einsum('oi,icjk->ocjk', kernel.squeeze(3).squeeze(2),
self.stem1[0].weight.data)
self.stem1[0].bias.data = bias + (self.stem1[0].bias.data.view(1, -1, 1, 1) * kernel).sum(3).sum(2).sum(1)
self.stem = torch.nn.Sequential(*[self.stem1[0], self.stem2[2]])
self.__delattr__('stem1')
self.__delattr__('stem2')
self.deploy = True
class VanillaBlock(nn.Module):
def __init__(self, dim, dim_out,k=0 , stride=2,p=None,g=0, ada_pool=None,act_num=3 ,deploy=False):
super().__init__()
self.act_learn = 1
self.deploy = deploy
if self.deploy:
self.conv = nn.Conv2d(dim, dim_out, kernel_size=1)
else:
self.conv1 = nn.Sequential(
nn.Conv2d(dim, dim, kernel_size=1),
nn.BatchNorm2d(dim, eps=1e-6),
)
self.conv2 = nn.Sequential(
nn.Conv2d(dim, dim_out, kernel_size=1),
nn.BatchNorm2d(dim_out, eps=1e-6)
)
if not ada_pool:
self.pool = nn.Identity() if stride == 1 else nn.MaxPool2d(stride)
else:
self.pool = nn.Identity() if stride == 1 else nn.AdaptiveMaxPool2d((ada_pool, ada_pool))
self.act = activation(dim_out, act_num, deploy=self.deploy)
def forward(self, x):
if self.deploy:
x = self.conv(x)
else:
x = self.conv1(x)
# We use leakyrelu to implement the deep training technique.
x = torch.nn.functional.leaky_relu(x, self.act_learn)
x = self.conv2(x)
x = self.pool(x)
x = self.act(x)
return x
def _fuse_bn_tensor(self, conv, bn):
kernel = conv.weight
bias = conv.bias
running_mean = bn.running_mean
running_var = bn.running_var
gamma = bn.weight
beta = bn.bias
eps = bn.eps
std = (running_var + eps).sqrt()
t = (gamma / std).reshape(-1, 1, 1, 1)
return kernel * t, beta + (bias - running_mean) * gamma / std
def switch_to_deploy(self):
kernel, bias = self._fuse_bn_tensor(self.conv1[0], self.conv1[1])
self.conv1[0].weight.data = kernel
self.conv1[0].bias.data = bias
# kernel, bias = self.conv2[0].weight.data, self.conv2[0].bias.data
kernel, bias = self._fuse_bn_tensor(self.conv2[0], self.conv2[1])
self.conv = self.conv2[0]
self.conv.weight.data = torch.matmul(kernel.transpose(1, 3),
self.conv1[0].weight.data.squeeze(3).squeeze(2)).transpose(1, 3)
self.conv.bias.data = bias + (self.conv1[0].bias.data.view(1, -1, 1, 1) * kernel).sum(3).sum(2).sum(1)
self.__delattr__('conv1')
self.__delattr__('conv2')
self.act.switch_to_deploy()
self.deploy = True
同時に、yolo.pyに次の変更を加えます。
1. parse_model関数内
if m in (Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, SPPCSPC, RepConv,
RFEM, ELAN, SPPCSPCSIM,VanillaBlock,VanillaStem):
c1, c2 = ch[f], args[0]
if c2 != no: # if not output
c2 = make_divisible(c2 * gw, 8)
args = [c1, c2, *args[1:]]
if m in [BottleneckCSP, C3, C3TR, C3Ghost, C3x]:
args.insert(2, n) # number of repeats
n = 1
2.BaseModelのヒューズ機能
if isinstance(m, (VanillaStem, VanillaBlock)):
# print(m)
m.deploy = True
m.switch_to_deploy()
yolo.py で yolov7-tiny-vanilla.yaml をテストする