lw2333

lw2333

Keep calm and study.

第六章-畳み込みニューラルネットワーク

なぜいわゆる畳み込みニューラルネットワークが必要なのか#

初志:画像にはもともと豊富な構造があり、これらの構造は人間や機械学習モデルによって利用されることができます。これらの構造を利用することで、トレーニングのパラメータ量を減らし、モデルの精度を向上させることができます。

例えば、画像内で物体を探すタスクにおいて、物体の位置が空間的不変性を持つという合理的な仮定があります。畳み込みニューラルネットワークは、空間的不変性(spatial invariance)の概念を体系化し、このモデルに基づいて少ないパラメータで有用な表現を学習します。

Screenshot 2024-02-21 093549

以下の 2 つの特徴を要約できます:

  • 平行移動不変性(translation invariance):検出対象が画像のどの位置に現れても、ニューラルネットワークの前の数層は同じ画像領域に対して類似した反応を示すべきであり、これが「平行移動不変性」です。
  • 局所性(locality):ニューラルネットワークの前の数層は、入力画像の局所領域のみを探索し、画像内の遠く離れた領域の関係を過度に気にしないべきであり、これが「局所性」原則です。最終的には、これらの局所的特徴を集約して、全体の画像レベルで予測を行います。

数学的表現#

H\mathbf{H} は画像行列 X\mathbf{X} の隠れ表現です。U\mathbf{U} はバイアスです。W\mathbf{W} は 4 次元の重みテンソルです。

インデックス (i,j)(i, j) はこの位置のピクセルを示します。したがって、全結合層の隠れ表現は次のように導出できます:

[H]i,j=[U]i,j+kl[W]i,j,k,l[X]k,l=[U]i,j+ab[V]i,j,a,b[X]i+a,j+b.\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned}

ここで W\mathsf{W} から V\mathsf{V} への移行は、単に和の「中心」を (i,j)(i, j) に移動させただけです。


平行移動不変性U\mathsf{U}V\mathsf{V}(i,j)(i, j) の値に依存しないことを意味します。 これは理解しやすいことで、異なる (i,j)(i, j)X\mathbf{X} が同じ(物体が同じ)であれば、得られる隠れ表現も同じであるべきです。 したがって:

[H]i,j=u+ab[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

局所性 は、特定の範囲外の情報を遮断できることを意味します。つまり、[a,b][a,b] の外の V\mathbf{V} を 0 に設定します。

[H]i,j=u+a=Δ1Δ1b=Δ2Δ2[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

この式を畳み込み層と呼びます。V\mathbf{V} は畳み込みカーネル(convolution kernel)またはフィルター(filter)と呼ばれます。


畳み込み層について簡単に言及します。
畳み込み層が表現する演算は実際には相互相関演算(cross-correlation)です。
畳み込み層では、入力テンソルとカーネルテンソルが相互相関演算を通じて出力テンソルを生成します。
真の畳み込み演算は次のようになります(しかし、実際にはあまり重要ではありません):

(fg)(x)=f(z)g(xz)dz.(fg)(i)=af(a)g(ia).(fg)(i,j)=abf(a,b)g(ia,jb).\begin{aligned} (f * g)(\mathbf{x}) &= \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.\\ (f * g)(i) &= \sum_a f(a) g(i-a).\\ (f * g)(i, j) &= \sum_a\sum_b f(a, b) g(i-a, j-b). \end{aligned}

Screenshot 2024-02-21 093524

実際、画像は高さ、幅、色から構成される 3 次元テンソルであり、例えば 1024×1024×31024 \times 1024 \times 3 のピクセルを含みます。
[X]i,j[X]i,j,k[V]a,b[V]a,b,c[\mathsf{X}]_{i, j}\to[\mathsf{X}]_{i, j, k}\quad [\mathbf{V}]_{a,b}\to [\mathsf{V}]_{a,b,c}

H\mathsf{H} も 3 次元テンソルを使用します。
言い換えれば、各空間位置に対して、私たちは 1 つの隠れ表現ではなく、1 組の隠れ表現を使用したいと考えています。このような隠れ表現のセットは、互いに重なり合った 2 次元グリッドのように想像できます。
したがって、隠れ表現を 2 次元テンソルの チャネル(channel)として想像できます。
これらのチャネルは時折 特徴マップ(feature maps)とも呼ばれます。なぜなら、各チャネルは後続の層に一連の空間化された学習特徴を提供するからです。
直感的には、入力に近い底層では、一部のチャネルがエッジを特定し、他のチャネルがテクスチャを特定することを想像できます。

入力 X\mathsf{X} と隠れ表現 H\mathsf{H} に複数のチャネルをサポートするために、V\mathsf{V} に第 4 の座標を追加できます。すなわち、[V]a,b,c,d[\mathsf{V}]_{a, b, c, d}。以上をまとめると、

[H]i,j,d=a=Δ1Δ1b=Δ2Δ2c[V]a,b,c,d[X]i+a,j+b,c,[\mathsf{H}]_{i,j,d} = \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c},

ここで隠れ表現 H\mathsf{H} のインデックス dd は出力チャネルを示し、その後の出力は次の畳み込み層に入力として 3 次元テンソル H\mathsf{H} を持ち続けます。
したがって、複数のチャネルを持つ畳み込み層を定義でき、その中で V\mathsf{V} はその畳み込み層の重みです。


(nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1).

コード練習#

def corr2d(X, K):  
    """2次元相互相関演算

    Args:
        X (Tensor): 2次元行列
        K (Tensor): カーネル

    Returns:
        Tensor: 2次元相互相関演算の結果
    """
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
          # ハダマード積(シュール積、エントリーごと)
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum() 
    return Y

2 次元畳み込み層

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

簡単な例は以下の通りです

import torch
import torch.nn as nn
import torch.optim as optim

conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 10, 12))
Y = Y.reshape((1, 1, 10, 11))

lr = 1e-1  

optimizer = optim.SGD([conv2d.weight], lr=lr)

for epoch in range(100):  
    Y_hat = conv2d(X)
    l = nn.MSELoss()(Y_hat, Y)

    optimizer.zero_grad()
    l.backward()
    optimizer.step()

    print(f"epoch {epoch+1}, loss {l.item():.3f}")

畳み込みニューラルネットワークにおいて、特定の層の任意の要素の受容野(receptive field)は、前方伝播中に計算に影響を与える可能性のあるすべての要素(すべての前の層から)を指します。

受容野は入力の実際のサイズよりも大きくなる可能性があります。特徴マップ内の任意の要素がより広い領域の入力特徴を検出する必要がある場合、より深いネットワークを構築できます。

パディングとストライド#

畳み込みの出力形状は、入力形状と畳み込みカーネルの形状に依存します。複数回の畳み込みを連続して適用すると、得られる出力は入力よりもはるかに小さくなります。このような副作用は、画像の端の情報が失われることです。
境界にパディング(冗長)情報を追加して影響を与えることができます。
さらに、高解像度の冗長情報については、ストライドを使用して改善できます(1 回の移動で複数のステップを進む)。

Screenshot 2024-02-21 093454

このように、パディング後の出力次元は (nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1). から
(nhkh+ph+1)×(nwkw+pw+1)(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) に変わります。

畳み込みニューラルネットワークにおける畳み込みカーネルの高さと幅は通常奇数であり、例えば 1、3、5、または 7 です。奇数を選ぶ利点は、空間次元を維持しながら、上部と下部に同じ数の行をパディングし、左側と右側に同じ数の列をパディングできることです。

通常、ph=kh1,pw=kw1p_h=k_h-1, \,p_w=k_w-1 と設定し、入力と出力が同じ高さと幅を持つようにします。
これにより、ネットワークを構築する際に各層の出力形状を予測しやすくなります。khk_h が奇数であると仮定すると、高さの両側に ph/2p_h/2 行をパディングします。
khk_h が偶数の場合、1 つの可能性は、入力の上部に ph/2\lceil p_h/2\rceil 行をパディングし、下部に ph/2\lfloor p_h/2\rfloor 行をパディングすることです。同様に、幅の両側をパディングします。


ストライド

要素をスライドさせるたびに移動する量を ストライド(stride)と呼びます。
通常、垂直ストライドが shs_h、水平ストライドが sws_w の場合、出力形状は
(nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor となります。

特に、ph=kh1p_h=k_h-1pw=kw1p_w=k_w-1 の場合、出力形状は
(nh+sh1)/sh×(nw+sw1)/sw\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor となります。

さらに、入力の高さと幅が垂直および水平ストライドで割り切れる場合、出力形状は (nh/sh)×(nw/sw)(n_h/s_h) \times (n_w/s_w) となります。

コード練習#

def comp_conv2d(conv2d, X):
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.reshape(Y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

(parameter) padding: _size_2_t | str に注意してください。padding は高さと幅の両側にそれぞれパディングされる要素の数です。


さらに、nn のパディングはデフォルトで除算後に切り上げられます。

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 2))
comp_conv2d(conv2d, X).shape

複数入力・複数出力チャネル#

例えば、各 RGB 入力画像は 3×h×w3\times h\times w の形状を持ちます。このサイズ 3 の軸を チャネル(channel)次元と呼びます。

複数のチャネルを持つ場合、複数の畳み込みカーネルを構築する必要があります。得られるのは ci×kh×kwc_i\times k_h\times k_w の畳み込みカーネルで、相互相関の後に合計します。

def corr2d_multi_in(X, K):
    """多チャネル2次元畳み込み

    Args:
        X (Tensor): 入力
        K (Tensor): カーネル

    Returns:
        Tensor: 結果
    """
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
  
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。