8.8. 畳み込みネットワークのアーキテクチャ設計

前の節では、コンピュータビジョンにおける現代的なネットワーク設計を一通り見てきた。そこで扱った研究に共通していたのは、科学者の直感に大きく依存していたことである。多くのアーキテクチャは人間の創造性に強く支えられており、深層ネットワークが提供する設計空間を体系的に探索することの比重はそれよりかなり小さいものであった。それでもなお、この ネットワーク工学 的アプローチは非常に成功してきた。

AlexNet (8.1 章) が ImageNet 上で従来のコンピュータビジョンモデルに勝って以来、 同じパターンに従って設計された畳み込みブロックを積み重ねることで、 非常に深いネットワークを構築することが一般的になった。 特に、\(3 \times 3\) の畳み込みは VGG ネットワーク (8.2 章) によって広く普及した。 NiN (8.3 章) は、局所的な非線形性を加えることで \(1 \times 1\) の畳み込みでさえ 有益になりうることを示した。 さらに NiN は、ネットワークの最後で情報を集約する問題を、 全位置にわたって集約することで解決した。 GoogLeNet (8.4 章) は、異なる畳み込み幅を持つ複数の分岐を追加し、 Inception ブロックの中で VGG と NiN の利点を組み合わせた。 ResNet (8.6 章) は帰納バイアスを恒等写像(\(f(x) = 0\) から)へと変えた。これにより非常に深いネットワークが可能になった。ほぼ10年後の今でも、ResNet の設計は依然として人気があり、その設計の優秀さを物語っている。最後に、ResNeXt (8.6.5 章) はグループ畳み込みを追加し、パラメータ数と計算量の間でより良いトレードオフを提供した。Vision Transformer の先駆けともいえる Squeeze-and-Excitation Networks(SENets)は、位置間で効率的に情報を伝達できるようにする (Hu et al., 2018)。これは、チャネルごとのグローバルな注意関数を計算することで実現された。

これまでのところ、ニューラルアーキテクチャ探索(NAS)によって得られるネットワークは扱ってこなかった (Liu et al., 2018, Zoph and Le, 2016)。その理由は、通常そのコストが非常に大きく、総当たり探索、遺伝的アルゴリズム、強化学習、あるいは他の何らかのハイパーパラメータ最適化に依存するからである。固定された探索空間が与えられたとき、 NAS は探索戦略を用いて、 返された性能推定値に基づいてアーキテクチャを自動的に選択する。 NAS の結果は 単一のネットワーク実体である。EfficientNet はこの探索の注目すべき成果である (Tan and Le, 2019)

以下では、単一の最良ネットワーク を探すこととはかなり異なる考え方を議論する。これは計算コストが比較的低く、その過程で科学的な洞察も得られ、しかも結果の品質という点でもかなり有効である。では、Radosavovic et al. (2020) による ネットワーク設計空間を設計する 戦略を見ていこう。この戦略は手作業による設計と NAS の強みを組み合わせたものである。これは ネットワークの分布 を対象にし、その分布を最適化してネットワークのファミリー全体に対して良い性能を得ることで実現される。その成果が RegNet、具体的には RegNetX と RegNetY であり、さらに高性能な CNN を設計するための一連の指針である。

from d2l import torch as d2l
import torch
from torch import nn
from torch.nn import functional as F
from d2l import mxnet as d2l
from mxnet import np, npx, init
from mxnet.gluon import nn

npx.set_np()
from d2l import jax as d2l
from flax import linen as nn
import tensorflow as tf
from d2l import tensorflow as d2l

8.8.1. AnyNet の設計空間

以下の説明は、Radosavovic et al. (2020) の考え方にほぼ沿っているが、本書の範囲に収めるためにいくつか省略している。 まず、探索対象となるネットワーク族のテンプレートが必要である。この章で扱う設計に共通する点の一つは、ネットワークが stembodyhead から構成されることである。stem は初期の画像処理を行い、しばしばより大きな窓サイズの畳み込みを通して処理する。body は複数のブロックからなり、生の画像から物体表現へ移るために必要な変換の大部分を担う。最後に head は、たとえば多クラス分類のための softmax 回帰器のように、これを所望の出力へ変換する。 body 自体は、解像度を下げながら画像に作用する複数の stage から構成される。実際、stem もその後の各 stage も空間解像度を 4 分の 1 にする。最後に、各 stage は 1 個以上のブロックからなる。このパターンは VGG から ResNeXt まで、すべてのネットワークに共通である。実際、一般的な AnyNet ネットワークを設計するために、Radosavovic et al. (2020)図 8.6.5 の ResNeXt ブロックを用いた。

../_images/anynet.svg

図 8.8.1 AnyNet の設計空間。各矢印に沿った数値 \((\mathit{c}, \mathit{r})\) は、その時点でのチャネル数 \(c\) と画像の解像度 \(\mathit{r} \times \mathit{r}\) を示す。左から右へ:stem、body、head からなる一般的なネットワーク構造;4つの stage からなる body;stage の詳細構造;ブロックの2つの代替構造、1つはダウンサンプリングなし、もう1つは各次元の解像度を半分にする。設計上の選択には、任意の stage \(\mathit{i}\) に対する深さ \(\mathit{d_i}\)、出力チャネル数 \(\mathit{c_i}\)、グループ数 \(\mathit{g_i}\)、ボトルネック比 \(\mathit{k_i}\) が含まれる。

図 8.8.1 に示した構造を詳しく見ていこう。述べたように、AnyNet は stem、body、head から成る。stem は RGB 画像(3チャネル)を入力とし、stride が \(2\)\(3 \times 3\) 畳み込みの後に batch norm を適用して、解像度を \(r \times r\) から \(r/2 \times r/2\) に半減する。さらに、body への入力となる \(c_0\) チャネルを生成する。

このネットワークは \(224 \times 224 \times 3\) 形状の ImageNet 画像でうまく動作するように設計されているため、body は 4つの stage を通してこれを \(7 \times 7 \times c_4\) に縮小する(\(224 / 2^{1+4} = 7\) を思い出そう)。各 stage は最終的に stride \(2\) を持つ。最後に、head は NiN (8.3 章) と同様に global average pooling を用い、その後に全結合層を置くという完全に標準的な設計で、\(n\) クラス分類のための \(n\) 次元ベクトルを出力する。

関連する設計上の決定の大部分はネットワークの body に内在している。body は stage ごとに進み、各 stage は 8.6.5 章 で議論したのと同じ種類の ResNeXt ブロックから構成される。ここでの設計もまた完全に一般的である。まず、stride を \(2\) にして解像度を半分にするブロックから始める(図 8.8.1 の最右端)。これに合わせるため、ResNeXt ブロックの残差分岐は \(1 \times 1\) 畳み込みを通る必要がある。このブロックの後には、解像度とチャネル数の両方を変えない追加の ResNeXt ブロックが可変個続く。畳み込みブロックの設計では、わずかなボトルネックを加えるのが一般的な慣行であることに注意しよう。 そのため、ボトルネック比 \(k_i \geq 1\) を用いて、stage \(i\) の各ブロック内に \(c_i/k_i\) 個のチャネルを割り当てる(実験が示すように、これは実際にはあまり有効ではなく、使わないほうがよい)。最後に、ResNeXt ブロックを扱っているので、stage \(i\) におけるグループ畳み込みのグループ数 \(g_i\) も選ぶ必要がある。

この一見一般的な設計空間にも、なお多くのパラメータがある。すなわち、ブロック幅(チャネル数) \(c_0, \ldots c_4\)、各 stage の深さ(ブロック数) \(d_1, \ldots d_4\)、ボトルネック比 \(k_1, \ldots k_4\)、そしてグループ幅(グループ数) \(g_1, \ldots g_4\) を設定できる。 合計すると 17 個のパラメータになり、探索すべき構成の数は非常に大きくなる。この巨大な設計空間を効果的に縮小するための道具が必要である。ここで設計空間という概念の美しさが生きてくる。その前に、まず一般的な設計を実装しよう。

class AnyNet(d2l.Classifier):
    def stem(self, num_channels):
        return nn.Sequential(
            nn.LazyConv2d(num_channels, kernel_size=3, stride=2, padding=1),
            nn.LazyBatchNorm2d(), nn.ReLU())
class AnyNet(d2l.Classifier):
    def stem(self, num_channels):
        net = nn.Sequential()
        net.add(nn.Conv2D(num_channels, kernel_size=3, padding=1, strides=2),
                nn.BatchNorm(), nn.Activation('relu'))
        return net
class AnyNet(d2l.Classifier):
    arch: tuple
    stem_channels: int
    lr: float = 0.1
    num_classes: int = 10
    training: bool = True

    def setup(self):
        self.net = self.create_net()

    def stem(self, num_channels):
        return nn.Sequential([
            nn.Conv(num_channels, kernel_size=(3, 3), strides=(2, 2),
                    padding=(1, 1)),
            nn.BatchNorm(not self.training),
            nn.relu
        ])
class AnyNet(d2l.Classifier):
    def stem(self, num_channels):
        return tf.keras.models.Sequential([
            tf.keras.layers.Conv2D(num_channels, kernel_size=3, strides=2,
                                   padding='same'),
            tf.keras.layers.BatchNormalization(),
            tf.keras.layers.Activation('relu')])

各 stage は depth 個の ResNeXt ブロックからなり、 num_channels はブロック幅を指定する。 最初のブロックが入力画像の高さと幅を半分にすることに注意しよう。

@d2l.add_to_class(AnyNet)
def stage(self, depth, num_channels, groups, bot_mul):
    blk = []
    for i in range(depth):
        if i == 0:
            blk.append(d2l.ResNeXtBlock(num_channels, groups, bot_mul,
                use_1x1conv=True, strides=2))
        else:
            blk.append(d2l.ResNeXtBlock(num_channels, groups, bot_mul))
    return nn.Sequential(*blk)
@d2l.add_to_class(AnyNet)
def stage(self, depth, num_channels, groups, bot_mul):
    net = nn.Sequential()
    for i in range(depth):
        if i == 0:
            net.add(d2l.ResNeXtBlock(
                num_channels, groups, bot_mul, use_1x1conv=True, strides=2))
        else:
            net.add(d2l.ResNeXtBlock(
                num_channels, num_channels, groups, bot_mul))
    return net
@d2l.add_to_class(AnyNet)
def stage(self, depth, num_channels, groups, bot_mul):
    blk = []
    for i in range(depth):
        if i == 0:
            blk.append(d2l.ResNeXtBlock(num_channels, groups, bot_mul,
                use_1x1conv=True, strides=(2, 2), training=self.training))
        else:
            blk.append(d2l.ResNeXtBlock(num_channels, groups, bot_mul,
                                        training=self.training))
    return nn.Sequential(blk)
@d2l.add_to_class(AnyNet)
def stage(self, depth, num_channels, groups, bot_mul):
    net = tf.keras.models.Sequential()
    for i in range(depth):
        if i == 0:
            net.add(d2l.ResNeXtBlock(num_channels, groups, bot_mul,
                use_1x1conv=True, strides=2))
        else:
            net.add(d2l.ResNeXtBlock(num_channels, groups, bot_mul))
    return net

ネットワークの stem、body、head を組み合わせることで、 AnyNet の実装が完成する。

@d2l.add_to_class(AnyNet)
def __init__(self, arch, stem_channels, lr=0.1, num_classes=10):
    super(AnyNet, self).__init__()
    self.save_hyperparameters()
    if tab.selected('mxnet'):
        self.net = nn.Sequential()
        self.net.add(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
        self.net.initialize(init.Xavier())
    if tab.selected('pytorch'):
        self.net = nn.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add_module(f'stage{i+1}', self.stage(*s))
        self.net.add_module('head', nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
            nn.LazyLinear(num_classes)))
        self.net.apply(d2l.init_cnn)
    if tab.selected('tensorflow'):
        self.net = tf.keras.models.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(tf.keras.models.Sequential([
            tf.keras.layers.GlobalAvgPool2D(),
            tf.keras.layers.Dense(units=num_classes)]))
@d2l.add_to_class(AnyNet)
def __init__(self, arch, stem_channels, lr=0.1, num_classes=10):
    super(AnyNet, self).__init__()
    self.save_hyperparameters()
    if tab.selected('mxnet'):
        self.net = nn.Sequential()
        self.net.add(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
        self.net.initialize(init.Xavier())
    if tab.selected('pytorch'):
        self.net = nn.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add_module(f'stage{i+1}', self.stage(*s))
        self.net.add_module('head', nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
            nn.LazyLinear(num_classes)))
        self.net.apply(d2l.init_cnn)
    if tab.selected('tensorflow'):
        self.net = tf.keras.models.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(tf.keras.models.Sequential([
            tf.keras.layers.GlobalAvgPool2D(),
            tf.keras.layers.Dense(units=num_classes)]))
@d2l.add_to_class(AnyNet)
def create_net(self):
    net = nn.Sequential([self.stem(self.stem_channels)])
    for i, s in enumerate(self.arch):
        net.layers.extend([self.stage(*s)])
    net.layers.extend([nn.Sequential([
        lambda x: nn.avg_pool(x, window_shape=x.shape[1:3],
                            strides=x.shape[1:3], padding='valid'),
        lambda x: x.reshape((x.shape[0], -1)),
        nn.Dense(self.num_classes)])])
    return net
@d2l.add_to_class(AnyNet)
def __init__(self, arch, stem_channels, lr=0.1, num_classes=10):
    super(AnyNet, self).__init__()
    self.save_hyperparameters()
    if tab.selected('mxnet'):
        self.net = nn.Sequential()
        self.net.add(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
        self.net.initialize(init.Xavier())
    if tab.selected('pytorch'):
        self.net = nn.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add_module(f'stage{i+1}', self.stage(*s))
        self.net.add_module('head', nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
            nn.LazyLinear(num_classes)))
        self.net.apply(d2l.init_cnn)
    if tab.selected('tensorflow'):
        self.net = tf.keras.models.Sequential(self.stem(stem_channels))
        for i, s in enumerate(arch):
            self.net.add(self.stage(*s))
        self.net.add(tf.keras.models.Sequential([
            tf.keras.layers.GlobalAvgPool2D(),
            tf.keras.layers.Dense(units=num_classes)]))

8.8.2. 設計空間の分布とパラメータ

8.8.1 章 で述べたように、設計空間のパラメータは、その設計空間に属するネットワークのハイパーパラメータである。 AnyNet の設計空間で良いパラメータを見つける問題を考えてみよう。与えられた計算量(たとえば FLOPs や計算時間)に対して、単一の最良 のパラメータ選択を見つけようとすることはできる。各パラメータに対して仮にたった 2つ の候補しか許さないとしても、最良解を見つけるには \(2^{17} = 131072\) 通りの組合せを調べなければならない。これは明らかに、あまりにも高コストで実行不可能である。さらに悪いことに、この作業からは、ネットワークをどう設計すべきかという観点ではほとんど何も学べない。次に X-stage や shift 演算などを追加するときには、また最初からやり直しになる。しかも、学習の確率性(丸め、シャッフル、ビット誤りなど)のため、2回の実行がまったく同じ結果を生む可能性は低いである。より良い戦略は、パラメータの選択がどのように関係すべきかについて一般的な指針を見つけることである。たとえば、ボトルネック比、チャネル数、ブロック数、グループ数、あるいは層間でのそれらの変化は、簡単な規則の集合によって支配されるのが理想である。Radosavovic et al. (2019) のアプローチは、次の4つの仮定に基づいている。

  1. 一般的な設計原理は実際に存在し、それらを満たす多くのネットワークが良い性能を示すと仮定する。したがって、ネットワークの 分布 を特定することは妥当な戦略である。言い換えると、干し草の山の中に良い針がたくさんあると仮定する。

  2. ネットワークが良いかどうかを評価する前に、収束まで学習させる必要はない。最終的な精度の信頼できる指針として、中間結果を使えば十分である。目的関数を最適化するために(近似的な)代理指標を用いることを multi-fidelity optimization と呼ぶ (Forrester et al., 2007)。したがって、設計の最適化は、データセットを数回通しただけで得られる精度に基づいて行われ、コストを大幅に削減できる。

  3. 小規模で得られた結果(より小さなネットワーク)は、より大きなネットワークにも一般化する。したがって、最適化は構造的には似ているが、ブロック数やチャネル数などが少ないネットワークに対して行う。最後に、見つかったネットワークがスケールを大きくしても良い性能を示すことを確認すれば十分である。

  4. 設計の各要素は近似的に因子分解できるので、その結果の品質への影響をある程度独立に推定できる。言い換えると、最適化問題は中程度に容易である。

これらの仮定により、多くのネットワークを安価に試せる。特に、構成空間から一様に サンプリング して、その性能を評価できる。その後、そうして得られたネットワークで達成可能な誤差/精度の 分布 を調べることで、パラメータ選択の質を評価できる。ある設計空間から確率分布 \(p\) に従って引いたネットワークが犯す誤差について、その累積分布関数(CDF)を \(F(e)\) とする。すなわち、

(8.8.1)\[F(e, p) \stackrel{\textrm{def}}{=} P_{\textrm{net} \sim p} \{e(\textrm{net}) \leq e\}.\]

私たちの目標は、ほとんどのネットワークが非常に低い誤差率を持ち、かつ \(p\) の支持が簡潔であるような、ネットワーク 上の分布 \(p\) を見つけることである。もちろん、これを正確に行うのは計算上不可能である。そこで、分布 \(p\) からネットワークの標本 \(\mathcal{Z} \stackrel{\textrm{def}}{=} \{\textrm{net}_1, \ldots \textrm{net}_n\}\)(それぞれの誤差を \(e_1, \ldots, e_n\) とする)を取り、代わりに経験的 CDF \(\hat{F}(e, \mathcal{Z})\) を用いる。

(8.8.2)\[\hat{F}(e, \mathcal{Z}) = \frac{1}{n}\sum_{i=1}^n \mathbf{1}(e_i \leq e).\]

Radosavovic et al. (2020) は、ネットワークのすべての stage に対してボトルネック比を共有する \(k_i = k\) を試した。これにより、ボトルネック比を支配する4つのパラメータのうち3つが不要になる。これが性能に悪影響を与えるかどうかを評価するには、制約付き分布と非制約分布からネットワークをサンプリングし、対応する CDF を比較すればよい。すると、この制約はネットワーク分布の精度にまったく影響しないことがわかる。これは 図 8.8.2 の最初のパネルに示されている。 同様に、ネットワークのさまざまな stage に現れるグループ幅を同じ \(g_i = g\) にすることもできる。これもまた性能に影響しない。これは 図 8.8.2 の2番目のパネルに示されている。 この2つを合わせると、自由パラメータの数は6つ減る。

../_images/regnet-fig.png

図 8.8.2 設計空間の経験的誤差分布関数の比較。\(\textrm{AnyNet}_\mathit{A}\) は元の設計空間、\(\textrm{AnyNet}_\mathit{B}\) はボトルネック比を共有し、\(\textrm{AnyNet}_\mathit{C}\) はさらにグループ幅も共有し、\(\textrm{AnyNet}_\mathit{D}\) は stage 間でネットワーク深さを増加させる。左から右へ:(i) ボトルネック比を共有しても性能に影響はない;(ii) グループ幅を共有しても性能に影響はない;(iii) stage 間でネットワーク幅(チャネル数)を増やすと性能が向上する;(iv) stage 間でネットワーク深さを増やすと性能が向上する。図は Radosavovic et al. (2020) より。

次に、stage の幅と深さに関する多数の候補を減らす方法を考える。より深くなるにつれてチャネル数は増えるべき、すなわち \(c_i \geq c_{i-1}\)図 8.8.2 における彼らの記法では \(w_{i+1} \geq w_i\))と仮定するのは妥当であり、これにより \(\textrm{AnyNetX}_D\) が得られる。同様に、stage が進むにつれて深くなる、すなわち \(d_i \geq d_{i-1}\) と仮定するのも同様に妥当であり、これにより \(\textrm{AnyNetX}_E\) が得られる。これはそれぞれ 図 8.8.2 の3番目と4番目のパネルで実験的に確認できる。

8.8.3. RegNet

こうして得られる \(\textrm{AnyNetX}_E\) の設計空間は、理解しやすい設計原理に従う単純なネットワークから成る。

  • すべての stage \(i\) でボトルネック比を共有する \(k_i = k\)

  • すべての stage \(i\) でグループ幅を共有する \(g_i = g\)

  • stage 間でネットワーク幅を増やす:\(c_{i} \leq c_{i+1}\)

  • stage 間でネットワーク深さを増やす:\(d_{i} \leq d_{i+1}\)

これで、最終的な選択肢が残る。すなわち、最終的な \(\textrm{AnyNetX}_E\) 設計空間における上記パラメータの具体的な値をどう選ぶかである。\(\textrm{AnyNetX}_E\) の分布から最良性能を示すネットワークを調べると、次のことが観察できる。ネットワークの幅は、理想的にはネットワーク全体のブロックインデックスに対して線形に増加する。すなわち、\(c_j \approx c_0 + c_a j\) であり、ここで \(j\) はブロックインデックス、傾きは \(c_a > 0\) である。stage ごとに異なるブロック幅しか選べないことを考えると、この依存関係に合わせて設計された区分的定数関数に行き着く。さらに、実験はボトルネック比 \(k = 1\) が最も良いことも示しており、つまりボトルネックをまったく使わないのが望ましいということである。

異なる計算量に対する特定のネットワーク設計の詳細については、Radosavovic et al. (2020) を参照することをお勧めする。たとえば、効果的な 32 層の RegNetX 変種は、\(k = 1\)(ボトルネックなし)、\(g = 16\)(グループ幅 16)、第1 stage と第2 stage のチャネル数がそれぞれ \(c_1 = 32\)\(c_2 = 80\) で、深さがそれぞれ \(d_1=4\) ブロック、\(d_2=6\) ブロックとなるように選ばれる。この設計から得られる驚くべき洞察は、より大規模なネットワークを調べる場合でもなお成り立つということである。さらに良いことに、グローバルなチャネル活性化を持つ Squeeze-and-Excitation(SE)ネットワーク設計(RegNetY)にも当てはまる (Hu et al., 2018)

class RegNetX32(AnyNet):
    def __init__(self, lr=0.1, num_classes=10):
        stem_channels, groups, bot_mul = 32, 16, 1
        depths, channels = (4, 6), (32, 80)
        super().__init__(
            ((depths[0], channels[0], groups, bot_mul),
             (depths[1], channels[1], groups, bot_mul)),
            stem_channels, lr, num_classes)
class RegNetX32(AnyNet):
    def __init__(self, lr=0.1, num_classes=10):
        stem_channels, groups, bot_mul = 32, 16, 1
        depths, channels = (4, 6), (32, 80)
        super().__init__(
            ((depths[0], channels[0], groups, bot_mul),
             (depths[1], channels[1], groups, bot_mul)),
            stem_channels, lr, num_classes)
class RegNetX32(AnyNet):
    lr: float = 0.1
    num_classes: int = 10
    stem_channels: int = 32
    arch: tuple = ((4, 32, 16, 1), (6, 80, 16, 1))
class RegNetX32(AnyNet):
    def __init__(self, lr=0.1, num_classes=10):
        stem_channels, groups, bot_mul = 32, 16, 1
        depths, channels = (4, 6), (32, 80)
        super().__init__(
            ((depths[0], channels[0], groups, bot_mul),
             (depths[1], channels[1], groups, bot_mul)),
            stem_channels, lr, num_classes)

各 RegNetX stage が、解像度を段階的に下げつつ出力チャネル数を増やしていくことがわかる。

RegNetX32().layer_summary((1, 1, 96, 96))
Sequential output shape:     torch.Size([1, 32, 48, 48])
Sequential output shape:     torch.Size([1, 32, 24, 24])
Sequential output shape:     torch.Size([1, 80, 12, 12])
Sequential output shape:     torch.Size([1, 10])
RegNetX32().layer_summary((1, 1, 96, 96))
[07:49:42] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
Sequential output shape:     (1, 32, 48, 48)
Sequential output shape:     (1, 32, 24, 24)
Sequential output shape:     (1, 80, 12, 12)
GlobalAvgPool2D output shape:        (1, 80, 1, 1)
Dense output shape:  (1, 10)
RegNetX32(training=False).layer_summary((1, 96, 96, 1))
Sequential output shape:     (1, 48, 48, 32)
Sequential output shape:     (1, 24, 24, 32)
Sequential output shape:     (1, 12, 12, 80)
Sequential output shape:     (1, 10)
RegNetX32().layer_summary((1, 96, 96, 1))
Sequential output shape:     (1, 48, 48, 32)
Sequential output shape:     (1, 24, 24, 32)
Sequential output shape:     (1, 12, 12, 80)
Sequential output shape:     (1, 10)

8.8.4. 学習

Fashion-MNIST データセットで 32 層の RegNetX を学習するのは、これまでと同じである。

model = RegNetX32(lr=0.05)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
trainer.fit(model, data)
../_images/output_cnn-design_71856f_93_0.svg
model = RegNetX32(lr=0.05)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
trainer.fit(model, data)
../_images/output_cnn-design_71856f_96_0.svg
model = RegNetX32(lr=0.05)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
trainer.fit(model, data)
../_images/output_cnn-design_71856f_99_0.svg
trainer = d2l.Trainer(max_epochs=10)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
with d2l.try_gpu():
    model = RegNetX32(lr=0.01)
    trainer.fit(model, data)
../_images/output_cnn-design_71856f_102_0.svg

8.8.5. 議論

局所性や平行移動不変性(7.1 章)のような、視覚に対する望ましい帰納バイアス(仮定や好み)を持つため、CNN はこの分野で支配的なアーキテクチャでした。これは LeNet から、Transformers (11.7 章) (Dosovitskiy et al., 2021, Touvron et al., 2021) が精度の面で CNN を上回り始めるまで続きました。最近の vision Transformer の進歩の多くは CNN に 移植可能 ですが (Liu et al., 2022)、それはより高い計算コストを伴う場合に限られる。同様に重要なのは、NVIDIA Ampere や Hopper といった最近のハードウェア最適化が、Transformer に有利な差をさらに広げていることである。

Transformer は、CNN に比べて局所性や平行移動不変性に対する帰納バイアスの度合いがかなり低いことに注意する価値がある。学習された構造が優勢になったのは、最大50億枚の画像を含む LAION-400m や LAION-5B (Schuhmann et al., 2022) のような大規模画像コレクションが利用可能になったことも大きな要因である。驚くべきことに、この文脈でより重要な研究の中には MLP さえ含まれている (Tolstikhin et al., 2021)

要するに、vision Transformer (11.8 章) は現在、大規模画像分類における最先端性能の面で先行しており、スケーラビリティは帰納バイアスに勝る ことを示している (Dosovitskiy et al., 2021)。 これには、multi-head self-attention (11.5 章) を用いた大規模 Transformer の事前学習 (11.9 章) も含まれる。これらの章をぜひ読み進めて、より詳細な議論に触れよ。

8.8.6. 演習

  1. stage の数を4に増やせ。より深く、より良い性能を示す RegNetX を設計できるか。

  2. ResNeXt ブロックを ResNet ブロックに置き換えて、RegNet を De-ResNeXt 化せよ。新しいモデルの性能はどうなるか。

  3. RegNetX の設計原理を 破る ことで、「VioNet」ファミリーの複数の実体を実装せよ。それらの性能はどうなるか。(\(d_i\), \(c_i\), \(g_i\), \(b_i\)) のうち、最も重要な要因はどれか。

  4. あなたの目標は「完璧な」MLP を設計することである。上で導入した設計原理を使って、良いアーキテクチャを見つけられるだろうか。小規模ネットワークから大規模ネットワークへ外挿することは可能だろうか。