Warning: Undefined variable $position in /home/pystyles/pystyle.info/public_html/wp/wp-content/themes/lionblog/functions.php on line 4897

Pytorch – ResNeXt の仕組みと実装について解説

Pytorch – ResNeXt の仕組みと実装について解説

概要

ディープラーニングの画像認識モデルである ResNeXt を解説し、Pytorch の実装例を紹介します。

ResNet

ResNet は、画像認識のコンテスト ILSVRC 2015 にて、top5 error rate で3.57%を記録し、優勝した CNN ネットワークモデルです。その後、いくつか改良したモデルが出てきて、今回紹介する ResNeXt もその亜種になります。

ResNeXt

Aggregated Residual Transformations for Deep Neural Networks で紹介された ResNet の改良モデルです。

cardinality の定義

入力数が $D$、出力数が$1$のシンプルなニューロンの計算について考えます。 入力を $\mathbf{x} \in \R^D$、全結合層の重みを $\mathbf{w} \in \R^D$ としたとき、全結合層の計算は

$$ \sum_{i = 1}^D w_i x_i = \mathbf{w} \cdot \mathbf{x} $$

で計算できます。

ニューロンの計算

この処理は次の組み合わせとして解釈できます。

  1. $\mathbf{x}$ を $x_1, x_2, \cdots, x_D$ という低い次元に分割する (分割)
  2. それぞれ $x_i w_i$ を計算する。(変換)
  3. すべての変換結果を足し合わせる (集計)

これを一般化すると、

$$ \mathcal{F}(\mathbf{x}) = \sum_{i = 1}^C \mathcal{T}_i (\mathbf{x}) $$

$\mathcal{T}_i (\mathbf{x})$ は任意の関数で、必要に応じてより低い次元に次元削減したあと、変換を行います。$C$ は $\mathcal{F}(\mathbf{x})$ を構成する変換の数で、大きいほど複雑な変換を表せます。論文では、$C$ の値を 基数 (cardinality) とよんでいます。

Bottleneck Block を使用した変換

論文では、簡単にするために $\mathcal{T}_i$ はすべて同じ変換とし、これを ResNet の Bottleneck Block にします。Bottleneck Block ブロックの最初の 1×1 の畳み込み層ではチャンネル数が $D$ の入力をより少ないチャンネル数に次元削減します。各 Bottleneck Block の出力を集計し、$\mathcal{F}(\mathbf{x}) = \sum_{i = 1}^C \mathcal{T}_i(\mathbf{x})$ とします。最後に Shortcut Connection と結合し、$\mathcal{F} (\mathbf{x}) + \mathbf{x}$ を出力します。

Bottleneck Block を使用

分割のパラメータについて

Bottleneck の in_channels、out_channels を割り切れる値で分割する必要があります。Bottleneck の in_channels、out_channels がとり得る値は $64, 256, 256, 512, 1024, 2048$ であり、これらの公約数は $1, 2, 4, 8, 16, 32, 64$ です。

次にモデルのパラメータ数は同等に維持しつつ、分割後の畳み込みの出力チャンネル数 $d$ を決めます。

  • 通常の Bottleneck のパラメータ数は in_channels * channels + 3 * 3 * channels * channels + channels * 4 * channels と計算できます。
  • 分割版の Bottleneck のパラメータ数は C * (in_channels * d + 3 * 3 * d * d + d * 4 * channels) と計算できます。

分割数 C をある値にしたとき、通常の Bottleneck のパラメータ数と変わらないように $d$ を決めるには、この2つが同じ値になる $d$ の値を求めればよいことがわかります。

例えば、in_channels = 256, channels=64 の Bottleneck の場合は次のように求められます。

In [1]:
from sympy import N, solve, symbols

d = symbols("d", real=True, positive=True)

# 通常の Bottleneck のパラメータ数
in_channels = 256
channels = 64
n_params = in_channels * channels + 9 * channels * channels + channels * channels * 4

# C を変化させた場合のパラメータ数を変えない d を求める
for C in [1, 2, 4, 8, 16, 32, 64]:
    z = C * (256 * d + 9 * d * d + d * 256)
    sol = N(solve(z - n_params, d)[0])
    print(f"C={C}, d={sol:.0f}")
C=1, d=64
C=2, d=40
C=4, d=24
C=8, d=14
C=16, d=8
C=32, d=4
C=64, d=2

論文では、この $C=1, 2, 4, 8, 32$ に対して、ResNet-50 及び ResNet-101 の分割したバージョンの精度の検証を行いました。 その結果、いずれも $C=32, d=4$ が最もよい精度がでることがわかりました。

ResNext

C=32 の場合、各 in_channels, channels の場合において、通常の Bottleneck のパラメータ数と変わらない $d$ を求めると次のようになります。

In [2]:
from sympy import N, solve, symbols

channel_params = [
    [64, 64],
    [256, 64],
    [256, 128],
    [512, 128],
    [512, 256],
    [1024, 256],
    [1024, 512],
    [2048, 512],
]

C = 32

for in_channels, channels in channel_params:
    n_params_normal = (
        in_channels * channels + 9 * channels * channels + channels * 4 * channels
    )
    n_params_split = C * (in_channels * d + 9 * d * d + d * 4 * channels)
    sol = N(solve(n_params_split - n_params_normal, d)[0])

    print(
        f"in_channels={in_channels}, channels={channels}, n_params={n_params_normal}, C={C}, d={sol:.0f}",
    )
in_channels=64, channels=64, n_params=57344, C=32, d=5
in_channels=256, channels=64, n_params=69632, C=32, d=4
in_channels=256, channels=128, n_params=245760, C=32, d=9
in_channels=512, channels=128, n_params=278528, C=32, d=8
in_channels=512, channels=256, n_params=983040, C=32, d=18
in_channels=1024, channels=256, n_params=1114112, C=32, d=16
in_channels=1024, channels=512, n_params=3932160, C=32, d=36
in_channels=2048, channels=512, n_params=4456448, C=32, d=32

dchannels を割り切れる必要があるので、通常の Bottleneck より多少パラメータが少くなりますが、

  • channels=64 のとき、d=4
  • channels=128 のとき、d=8
  • in_channels=256 のとき、d=16
  • in_channels=512 のとき、d=32

つまり、d = channels * 4 / 64 とします。このモデルは cardinary=32、width=4 なので、ResNext 32x4d と表記します。

cardinary を増やした場合

cardinary=64、width=4 とした場合、Bottleneck Block のパラメータ数は元の ResNet の倍になりますが、cardinary を倍にした場合の精度の検証も論文では行っています。

cardinary を倍にした場合

その結果、パラメータ数がほぼ同等である ResNet-200 と ResNeXt-101 64x4d において、ResNeXt のほうが精度がいいことがわかりました。

Grouped Convolution への置き換え

ResNext

これまで考えてきた分割した Bottleneck (上図 a) は下図 b, c と等価です。そのため、実装上は Grouped Convolution を使用することにします。

ResNext

torchvision の ResNeXt の実装

torchvision.models の ResNeXt の実装について解説します。ResNet の実装を一部変更したものなので、ResNet の実装については下記記事で解説し、その変更点のみこの記事で解説します。

Bottleneck Block の実装

Bottleneck クラスで Bottleneck Block を定義しています。順伝搬時の処理は以下のようになっています。分割後のチャンネル数は int(channels * (width / 64)) なので、Grouped Convolution のチャンネル数は int(channels * (width / 64)) * cardinary で計算します。width=64, cardinary=1 のとき、通常の Bottleneck Block と同じになります。

In [3]:
import torch
import torch.nn as nn


def conv3x3(in_channels, out_channels, stride=1, groups=1):
    return nn.Conv2d(
        in_channels,
        out_channels,
        kernel_size=3,
        stride=stride,
        padding=1,
        groups=groups,
        bias=False,
    )


def conv1x1(in_channels, out_channels, stride=1):
    return nn.Conv2d(
        in_channels, out_channels, kernel_size=1, stride=stride, bias=False
    )


class Bottleneck(nn.Module):
    expansion = 4  # 出力のチャンネル数を入力のチャンネル数の何倍に拡大するか

    def __init__(
        self,
        in_channels,
        channels,
        stride=1,
        cardinality=1,
        base_width=64,
    ):
        super().__init__()
        width = int(channels * (base_width / 64)) * cardinality
        self.conv1 = conv1x1(in_channels, width)
        self.bn1 = nn.BatchNorm2d(width)
        self.conv2 = conv3x3(width, width, stride, groups=cardinality)
        self.bn2 = nn.BatchNorm2d(width)
        self.conv3 = conv1x1(width, channels * self.expansion)
        self.bn3 = nn.BatchNorm2d(channels * self.expansion)
        self.relu = nn.ReLU(inplace=True)

        # 入力と出力のチャンネル数が異なる場合、x をダウンサンプリングする。
        if in_channels != channels * self.expansion:
            self.shortcut = nn.Sequential(
                conv1x1(in_channels, channels * self.expansion, stride),
                nn.BatchNorm2d(channels * self.expansion),
            )
        else:
            self.shortcut = nn.Sequential()

    def forward(self, x):
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += self.shortcut(x)

        out = self.relu(out)

        return out

ResNet を定義する

ResNext クラスで ResNeXt 全体のモデルを作成します。こちらの実装は cardinality, width のパラメータをとるようになった点以外は ResNet と同じです。

In [4]:
class ResNeXt(nn.Module):
    def __init__(self, layers, num_classes=1000, cardinality=1, base_width=64):
        super().__init__()

        self.in_channels = 64  # Residual Block の入力チャンネル数
        self.cardinality = cardinality
        self.base_width = base_width
        self.conv1 = nn.Conv2d(
            3, self.in_channels, kernel_size=7, stride=2, padding=3, bias=False
        )
        self.bn1 = nn.BatchNorm2d(self.in_channels)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(64, layers[0])
        self.layer2 = self._make_layer(128, layers[1], stride=2)
        self.layer3 = self._make_layer(256, layers[2], stride=2)
        self.layer4 = self._make_layer(512, layers[3], stride=2)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * Bottleneck.expansion, num_classes)

        # 重みを初期化する。
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def _make_layer(self, channels, blocks, stride=1):
        layers = []

        # 最初の Residual Block
        layers.append(
            Bottleneck(
                self.in_channels,
                channels,
                stride,
                cardinality=self.cardinality,
                base_width=self.base_width,
            )
        )

        # 残りの Residual Block
        self.in_channels = channels * Bottleneck.expansion
        for _ in range(1, blocks):
            layers.append(
                Bottleneck(
                    self.in_channels,
                    channels,
                    cardinality=self.cardinality,
                    base_width=self.base_width,
                )
            )

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

torchvision では、ResNet-50 の 32x4d のバージョンと、ResNet-101 の 32x8d のバージョンが提供されています。

In [5]:
def resnext50_32x4d():
    return ResNeXt([3, 4, 6, 3], cardinality=32, base_width=4)


def resnext101_32x8d():
    return ResNeXt([3, 4, 23, 3], cardinality=32, base_width=8)

ResNet、ResNext のモデル一覧

モデル名 関数 パラメータ数 Top-1 エラー率 Top-5 エラー率
ResNet-18 resnet18() 11689512 30.24 10.92
ResNet-34 resnet34() 21797672 26.7 8.58
ResNet-50 resnet50() 25557032 23.85 7.13
ResNet-101 resnet101() 44549160 22.63 6.44
ResNet-152 resnet152() 60192808 21.69 5.94
ResNeXt-50-32x4d resnext50_32x4d() 25028904 22.38 6.3
ResNeXt-101-32x8d resnext101_32x8d() 88791336 20.69 5.47
Wide ResNet-50-2 wide_resnet50_2() 68883240 21.49 5.91
Wide ResNet-101-2 wide_resnet101_2() 126886696 21.16 5.72