MLエンジニアへの道 #20 - CNN

Last Edited: 9/18/2024

このブログ記事では、ディープラーニングにおける畳み込みニューラルネットワーク(CNN)について紹介します。

ML

私たちはすでに、単純なフィードフォワードニューラルネットワークがいかに強力かを見てきましたが、同時に、 どれほど複雑さに限界があるかも目にしてきました。VAEやGANのような複雑なモデルで、 小さな画像を使ってDense層をトレーニングすると、計算とトレーニングが著しく遅くなり、 ハイパーパラメータの調整がさらに必要になることがわかりました。したがって、 より高次元の大きなデータを効率的に処理する新しい方法が必要だと気づきました。 この問題を解決する方法として、今回は畳み込みニューラルネットワーク(Convolutional Neural Network, CNN)について説明します。

カーネル畳み込み

カーネル畳み込みとは、小さなカーネル(フィルタ)を使用して、画像の上をスライドしながら、 カーネルの値と画像のピクセル値の線形結合(積和)を取り、新しい画像を生成する操作です。 これを視覚的に理解する方が簡単だと思うので、下に畳み込みの視覚化を添付しました。

Convolution

上記の画像は、3x3の画像に2x2のカーネルを使った畳み込みを示しています。特に、この例では、 ぼかしカーネルまたはフィルタを適用しており、これは近隣のピクセルの合計(同じ重み1での加重平均)を取るのと同じです。 また、カーネルのサイズや値を変更することで、エッジ抽出やその他の特徴を取り出すこともできます。(エッジ検出に興味がある方は、 Finding the Edges (Sobel Operator) - Computerphileという動画を見ることをお勧めします。)

カーネルをニューロンとして使用

フィードフォワードニューラルネットワークでは、すべての入力アクティベーションに対して線形結合を行うニューロンを設定します。 これは、各ニューロンが入力アクティベーションの数と同じ数の重みを持つことを意味します。たとえば、784(28x28)サイズの画像を扱う場合、 最初の隠れ層の各ニューロンは、特徴を抽出するために784の重みを持つ必要があります。

しかし、ニューロンの代わりにカーネル畳み込みを適用すると、カーネルのグリッドごとに対応する重みしか持たないため、 同じ重みを異なるピクセル間で共有でき、重みの数を大幅に削減できます。さらに、畳み込みは、重みをピクセル間で共有するため、 画像がシフトや回転してもより頑強であるという大きな利点があります。

カーネルを積み重ねて畳み込み後に非線形の活性化関数を適用することで、畳み込み層を作成できます。また、 これらの畳み込み層を隠れ層として積み重ね、さまざまなレベルの特徴をキャプチャすることで、 畳み込みニューラルネットワークが作成されます。畳み込み層を定義する際には、 出力次元を操作するために、フィルタを画像の上でスライドするピクセル数(ストライド)や、画像周囲に追加するピクセル数(パディング)を決めることができます。

逆伝播

各層のカーネルの重みを学習するためには、損失関数のカーネルの重みおよび入力特徴量に対する偏微分を計算し、さらに逆伝播を行う必要があります。 まず、畳み込み演算を数学的に表現してみましょう。

O=XF O = X * F

ここで、OOは畳み込みの出力、XXは入力特徴量、*は畳み込み演算を示す記号、FFはフィルタまたはカーネルを表しています。 上の図のように離散的な畳み込みを適用する場合、以下のような計算が行われます。

O1,1=X1,1F1,1+X1,2F1,2+X2,1F2,1+X2,2F2,2O1,2=X1,2F1,1+X1,3F1,2+X2,2F2,1+X2,3F2,2O2,1=X2,1F1,1+X2,2F1,2+X3,1F2,1+X3,2F2,2O2,2=X2,2F1,1+X2,3F1,2+X3,2F2,1+X3,3F2,2 O_{1,1} = X_{1,1} F_{1,1} + X_{1,2} F_{1,2} + X_{2,1} F_{2,1} + X_{2,2} F_{2,2} \\ O_{1,2} = X_{1,2} F_{1,1} + X_{1,3} F_{1,2} + X_{2,2} F_{2,1} + X_{2,3} F_{2,2} \\ O_{2,1} = X_{2,1} F_{1,1} + X_{2,2} F_{1,2} + X_{3,1} F_{2,1} + X_{3,2} F_{2,2} \\ O_{2,2} = X_{2,2} F_{1,1} + X_{2,3} F_{1,2} + X_{3,2} F_{2,1} + X_{3,3} F_{2,2}

まず、カーネルの重みに対する損失勾配を計算します。これは以下のように表現されます。

LFi=k=1MLOkOkFi \frac{\partial L}{\partial F_i} = \sum_{k=1}^{M} \frac{\partial L}{\partial O_k} \frac{\partial O_k}{\partial F_i}

上記は、F1,1F_{1,1}に対して次のように展開できます。

LF1,1=LO1,1O1,1F1,1+LO1,2O1,2F1,1+LO2,1O2,1F1,1+LO2,2O2,2F1,1 \frac{\partial L}{\partial F_{1,1}} = \frac{\partial L}{\partial O_{1,1}} \frac{\partial O_{1,1}}{\partial F_{1,1}} + \frac{\partial L}{\partial O_{1,2}} \frac{\partial O_{1,2}}{\partial F_{1,1}} + \frac{\partial L}{\partial O_{2,1}} \frac{\partial O_{2,1}}{\partial F_{1,1}} + \frac{\partial L}{\partial O_{2,2}} \frac{\partial O_{2,2}}{\partial F_{1,1}}

XXFFを単に掛け算しているため、OF1,1\frac{\partial O}{\partial F_{1,1}}の偏微分は対応するXXに等しくなります。したがって、F1,1F_{1,1}に対する微分を以下のように書き換えることができます。

LF1,1=LO1,1X1,1+LO1,2X1,2+LO2,1X2,1+LO2,2X2,2 \frac{\partial L}{\partial F_{1,1}} = \frac{\partial L}{\partial O_{1,1}} X_{1,1} + \frac{\partial L}{\partial O_{1,2}} X_{1,2} + \frac{\partial L}{\partial O_{2,1}} X_{2,1} + \frac{\partial L}{\partial O_{2,2}} X_{2,2}

これはすべてのフィルタ値FFに適用されます。ここで何かに気づきましたか?そうです、損失関数のカーネルの重みに対する偏微分は、XXと出力に対する損失勾配の畳み込みそのものです。

LF=XLO \frac{\partial L}{\partial F} = X * \frac{\partial L}{\partial O}

次に、入力特徴量XXに対する損失勾配を計算します。これは次のように表現されます。

LXi=k=1MLOkOkXi \frac{\partial L}{\partial X_i} = \sum_{k=1}^{M} \frac{\partial L}{\partial O_k} \frac{\partial O_k}{\partial X_i}

いくつかのXXに対して展開すると、次のようになります。

LX1,1=LO1,1F1,1LX1,2=LO1,1F1,2+LO1,2F1,1 \frac{\partial L}{\partial X_{1,1}} = \frac{\partial L}{\partial O_{1,1}} F_{1,1} \\ \frac{\partial L}{\partial X_{1,2}} = \frac{\partial L}{\partial O_{1,1}} F_{1,2} + \frac{\partial L}{\partial O_{1,2}} F_{1,1}

上記からは気づきにくいかもしれませんが、この結果は、カーネルを180度回転させたものと、出力に対する損失勾配との完全な畳み込みに相当することが確認できます。(完全な畳み込みとは、カーネル全体が重なる必要はなく、 重なる部分があれば畳み込みを適用するものです。パディングを入力に適用したものと捉えることもできます。)

LX=LOfullFrotated \frac{\partial L}{\partial X} = \frac{\partial L}{\partial O} *_{full} F_{rotated}

注意: 上記はストライド1でパディングがない通常の畳み込みにのみ適用されます。パディングのある畳み込みや、 ダイレイテッド畳み込み、ピクセルシャッフル畳み込みなどの他のタイプの畳み込みに対しては、 特定の方法で微分を計算する必要があります。(詳しくは、Sadiq, R. (2021)による CNN Backpropagation などのオンラインリソースを参照してください。)

コードの実装

では、畳み込みニューラルネットワーク(CNN)を実装してみましょう。MNISTデータセットを使用して、 手書きの数字を分類するマルチクラスのCNNベースの分類器を実装します。

ステップ 1 & 2. データ探索と前処理

すでにMNISTデータセットを見たことがあるので、データの前処理に直行します。フィードフォワードニューラルネットワークとは異なり、 画像をフラット化する必要はなく、2D画像に直接操作できます。ただし、チャンネル用にもう一つの次元を追加する必要があります。以下は、 前処理のためのコードです。

## TensorFlow Reshape
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)
 
## PyTorch Reshape
X_train = X_train.reshape(X_train.shape[0], 1, X_train.shape[1], X_train.shape[2])
X_test = X_test.reshape(X_test.shape[0], 1, X_test.shape[1], X_test.shape[2])
 
def zscore(X, axis = None):
    X_mean = X.mean(axis=axis, keepdims=True)
    X_std  = np.std(X, axis=axis, keepdims=True)
    zscore = (X-X_mean)/X_std
    return zscore
 
X_train = zscore(X_train)
X_test = zscore(X_test)
 
y_train = keras.utils.to_categorical(y_train)
y_test = keras.utils.to_categorical(y_test)
 
from sklearn.model_selection import train_test_split
 
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=10000, random_state=101)
 
## For PyTorch Only
import torch.nn as nn
 
X_train, X_val, X_test = map(lambda X: torch.tensor(X, dtype=torch.float32), (X_train, X_val, X_test))
y_train, y_val, y_test = map(lambda y: torch.tensor(y, dtype=torch.float32), (y_train, y_val, y_test))
 
train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
val_dataset = torch.utils.data.TensorDataset(X_val, y_val)
test_dataset = torch.utils.data.TensorDataset(X_test, y_test)
 
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=32, shuffle=True)
val_loader = torch.utils.data.DataLoader(dataset=val_dataset, batch_size=32, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=1, shuffle=True)

ステップ 3. モデル

以下は、TensorFlowとPyTorchでのCNNベースの画像分類器の例です。

ここでは、トレーニング結果とステップ 4(モデル評価)を省略します。なぜなら、特に議論することがないからです。 ぜひ、練習として自分で試してみることを強くお勧めします。(ネタバレ:フィードフォワードニューラルネットワークよりも少ないパラメータで、非常にうまく数字を分類することができます。)

次元計算のヒント

畳み込み層に慣れていない場合、特定のカーネルサイズ、ストライド、パディングが使用されたときに層の出力次元に混乱するかもしれません。そのような場合、次の式を使って出力次元を求めることができます。

Dout=Din+2pks+1 D_{out} = \frac{D_{in} + 2p - k}{s} + 1

ここで、DoutD_{out} は畳み込み後の出力次元、DinD_{in} は畳み込み前の入力次元、pp はパディング、kk はカーネルサイズ、ss はストライドです。

結論

この記事では、カーネルがどのようにニューロンとして機能し、パラメータ数を削減して畳み込み層や畳み込みニューラルネットワークを形成する方法を説明しました。また、畳み込み演算の勾配を計算する方法や、TensorFlowとPyTorchで畳み込みニューラルネットワークを実装する方法についても説明しました。

畳み込み層は非常に便利ですが、一つの問題があります。それは、Dense層のように次元を拡張することができないことです。技術的にはパディングを追加することで次元を拡張できますが、有用な情報を含まないパディングを大量に追加することは理想的ではありません。次の記事では、畳み込みの概念をどのように次元拡張に利用できるかについて説明します。