畳み込みニューラルネットワーク(CNN)

本章ではコンピュータービジョンのタスクでよく用いられる畳み込みニューラルネットワーク(CNN)について紹介します。

CNNは基本的に畳み込み層とプーリング層から成り立っているニューラルネットワークです。よって、本章は基本的に畳み込み層とプーリング層について紹介します。畳み込み層に関しては複雑な数式が関わってくるために、興味の無い方、また数式を余り得意としない場合は数学的な部分を飛ばしても構いません。

畳み込み

畳み込み層について説明する前に、まず始めに、畳込みという数学的操作について説明します。畳み込みは2つの関数に関する数学的操作です。畳み込みの式は以下の式で表せます。

(f*g)=\int_{-\infty}^{\infty} f(\tau)g(t-\tau)d\tau

上記の式は関数が連続である場合の式であるが、画像は不連続であるために、上記の式を以下の式に書き換えます。

(f*g)=\sum_{\tau} f(\tau)g(t-\tau)

畳み込み層

畳み込み層は2つの学習可能なパラメータ、ウェイト(weight)とバイアス(bias)を持った層です。そして特にこれをカーネル(フィルタ)と呼びます。それぞれのカーネルは画像に全体に沿って畳み込みの計算が行われます。ここで仮に2次元の画像を I 、カーネルを K とすると、畳込みの計算は以下の式のもと行われます。

(I*K)(i, t) = \sum_{m}\sum_{n}I(m, n)K(i-m, j-n)

この流れを経て、畳込み層は、画像上に特徴を見つけた際に活性化させることができるような最適なカーネルを見つけます。したがって、これにより畳み込みそうは与えられた画像から特徴量を取り出し、圧縮することができます。

次の画像は畳み込みそうのプロセスを表したものです。

Convolutiona Layer

Convolutiona Layer

プーリング層

畳み込み層と同じようにプーリング層も小さいウィンドウ(カーネル)を持っています。プーリング層はこのカーネルを画像全体に対して適用し統計的な処理を行います。

頻繁に使われるプーリング層はAverage Pooling層とMax Pooling層の2つであります。よって以下にこれら2つのそうに関する説明をします。

Max Pooling

Max Pooling層は適用されたカーネル内の最大値を取る層です。

Max Pooling

Max Pooling

Average Pooling

Average Pooling層は適用されたカーネル内の画像の値(画素)の平均値を取ります。以下の画像はAverage Pooling層のプロセスを表した画像です。

出力のShape

畳み込み層とプーリング層の出力のShapeに関するコンセプトはどちらも同じです。出力のShapeはカーネル、パディングそしてストライドに依存します。始めに、カーネルのみで考えます。畳み込み層とプーリング層の出力のShapeは次の用に計算することができます。

W-2[H/2] × W-2[H/2]

Wは画像サイズであり、Hはカーネルサイズとなっています。

つまり、仮に画像サイズが10 pixel、そしてカーネルの長さが2pixel であるとすると、出力のshapeは 10-2[2/2] × 10-2[2/2] ,つまり 8 × 8 となります。しかし、出力のshapeと入力のshapeを同じにしたいことがしばしばあります。これを解決するためにパディングという方法を用います。

パディング

パディングとは画像の周りに値を埋める方法です。特に0が画像の周りに埋められます。この方法を特に ゼロ-パディング といいます。次の画像はこのパディングについて表している画像です。この方法を利用することにより、入力のShapeと出力のShapeを一致させることができます。

zero padding

パディング

ストライド

他にも ストライド と呼ばれる方法があります。基本的にカーネルは縦横1pixel隣に画像に沿って移動しますがストライドを1pixel以上に設定することで、カーネルが指定したサイズ分動きます。例えば、仮にストライドのパラメータを2pixelに設定した場合、カーネルは縦横2ピクセル隣に動きます。よって出力のサイズは入力のサイズの半分となります。

したがってこのパディングとストライドを導入することにより、出力のShapeは次のように求めることができます。

((W-1)/S + 1, (W-1)/S + 1)

ReNomにおける畳み込み層とプーリング層

今までは畳み込み層とプーリング層に関する理論を説明してきましたが、ここでは実際にReNomでどのように使うかを説明します。ReNomにはConv2dクラスが実装されており、引数として channel , filter , padding , stride をとる。引数 channel は何枚のカーネルを利用するかを決める。またフィルタはカーネルのサイズを表している。では、ここからMNISTと呼ばれる手書き数字の画像が入ったデータセットに対して画像分類を行います。

必要なライブラリ

  • scikit-learn 0.18.2
  • matplotlib 2.0.2
  • numpy 1.12.1
  • tqdm 4.15.0
In [1]:
import renom as rm
from renom.cuda.cuda import set_cuda_active
import numpy as np
from sklearn.datasets import fetch_mldata
from tqdm import tqdm
from sklearn.preprocessing import LabelBinarizer
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler

GPU起動

モデルの学習を加速させたい場合は、GPUを起動する必要があります。使っているパソコンにGPUがあることを確認した上で、 set_cuda_active の関数を呼びます。

In [2]:
set_cuda_active(True)

Dataの取得

MNISTの画像データを利用します。次のプログラムを実行することで、データセットをダウンロードすることができます。

In [3]:
mnist = fetch_mldata('MNIST original', data_home='./')

モデル定義

ここで畳み込みニューラルネットワークを定義します。利用する画像データは複雑ではないので、2つの畳み込み層と2つのプーリング層、そして全結合層という単純な構造のモデルを定義します。ここでReNomに定義されてあるSequentialクラスを利用すると簡単に実装することができます。

In [4]:
cnn = rm.Sequential([
    rm.Conv2d(channel=32, filter=3, padding=1),
    rm.Relu(),
    rm.Conv2d(channel=64, filter=3, padding=1),
    rm.Relu(),
    rm.MaxPool2d(filter=2, stride=2),
    rm.Dropout(0.5),
    rm.Flatten(),
    rm.Dense(128),
    rm.Relu(),
    rm.Dense(10)
])

Dataの変換

データをTrainingデータとValidationdデータに分けます。ここでValidationデータを利用することで過学習をしていない最適なモデルを探すことが可能になります。さらに、教師データはOne Hot vectorでなければならないので、教師データも変換をします。

In [5]:
data = mnist['data']
targets = mnist['target']
train_num = int(0.8 * len(data))
train_data = np.expand_dims(data[:train_num].reshape(train_num, 28, 28), axis=1)
test_data = np.expand_dims(data[train_num:].reshape(len(data) - train_num, 28, 28), axis=1)
train_targets = targets[:train_num]
train_targets = LabelBinarizer().fit_transform(train_targets).astype(np.float32)
test_targets = targets[train_num:]
test_targets = LabelBinarizer().fit_transform(test_targets).astype(np.float32)

学習

In [6]:
batch_size = 64
epochs = 1
optimizer = rm.Sgd(lr=0.001)
N = train_num
for epoch in range(epochs):
    perm = np.random.permutation(N)
    loss = 0
    test_loss = 0
    bar = tqdm(range(N//batch_size))
    for j in range(N//batch_size):
        train_batch = train_data[perm[j*batch_size:(j+1)*batch_size]]
        train_targets_batch = train_targets[perm[j*batch_size:(j+1)*batch_size]]
        with cnn.train():
            l = rm.softmax_cross_entropy(cnn(train_batch), train_targets_batch)

        l.grad().update(optimizer)
        bar.set_description("epoch {:03d} train loss:{:6.4f} ".format(epoch, float(l.as_ndarray())))
        bar.update(1)
        loss += l.as_ndarray()
    for k in range(len(test_data)//batch_size):
        test_batch = test_data[k*batch_size:(k+1)*batch_size]
        test_targets_batch = test_targets[k*batch_size:(k+1)*batch_size]
        test_l = rm.softmax_cross_entropy(cnn(test_batch), test_targets_batch)
        test_loss += test_l.as_ndarray()
    bar.set_description("epoch {:03d} avg loss:{:6.4f} val loss:{:6.4f}".format(epoch, float((loss/(j+1))), float((test_loss/(k+1)))))
    bar.update(0)
    bar.refresh()
    bar.close()
epoch 000 avg loss:0.4712 val loss:0.3454: 100%|██████████| 875/875 [00:16<00:00, 53.02it/s]

カーネルの可視化

以前に説明したように、畳み込み層のそれぞれのカーネルが画像に沿って畳み込みの計算を行います。そこでここでは学習されたデータセットに対して最適なカーネルを以下に表示します。

In [7]:
W = cnn._layers[0].params.w.as_ndarray()
nb_filter, nb_channel, h, w = W.shape
plt.figure()
for i in range(nb_filter):
    im = W[i, 0]
    scalar = MinMaxScaler(feature_range=(0, 255))
    im = scalar.fit_transform(im)
    plt.subplot(4, 8, i+1)
    plt.axis('off')
    plt.imshow(im, cmap='gray')

../../../_images/notebooks_basic_algorithm_convolutional_neural_network_notebook_19_0.png

さらに、上記のカーネルを通す前のオリジナル画像と通した後の画像を比較します。このようなプロセスを経て画像にある特徴量を抽出します。

In [8]:
print('Original Image')
x = test_data[:1]
t = cnn._layers[0](x).as_ndarray()
nb_filter, nb_channel, h, w = t.shape
plt.figure()
plt.imshow(x[0][0], cmap='gray')
plt.show()
Original Image
../../../_images/notebooks_basic_algorithm_convolutional_neural_network_notebook_21_1.png
In [9]:
print('Feature maps after the first convolutional layer')
plt.figure()
for i in range(nb_channel):
    im = t[0, i, :, :]
    scalar = MinMaxScaler(feature_range=(0, 255))
    im = scalar.fit_transform(im)
    plt.subplot(4, 8, i+1)
    plt.axis('off')
    plt.imshow(im, cmap='gray')

plt.show()
Feature maps after the first convolutional layer
../../../_images/notebooks_basic_algorithm_convolutional_neural_network_notebook_22_1.png