目次


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

Python を使用して、手書きの数字を分類する単純なネットワークを実装する

Comments

皆さんは日常生活をおくる中で、オブジェクト認識アルゴリズムの一種が利用されているのを目にしているはずです。その一例は、スマートフォン上のカメラを使用した顔認識ですが、どのような仕組みになっているのでしょうか?顔認識などのコンピューター・ビジョン・ソリューションの中心となっているのは、畳み込みニューラル・ネットワーク (CNN) です。簡単に説明すると、CNN は単純な特徴から複雑なフィーチャーを構成することを得意とするニューラル・ネットワークです。CNN の典型的な例としては、顔検出器が挙げられます。顔検出器では、最初のほうの層で縦の線と横の線を検出し、それらの結果を積み重ねていって最終的に鼻や口を検出します。

この記事では、このような畳み込みネットワークがどのように機能するのかを説明し、Python を使用して手書きの数字を分類する単純なネットワークを実装する方法を紹介します。早速、本題に入りましょう!

ニューラル・ネットワーク入門

この記事ではニューラル・ネットワーク全般の仕組みを詳しく探ることはしませんが、畳み込みネットワークに取り組む前にある程度必要となる背景情報について説明しておきます。ニューラル・ネットワークは階層化アーキテクチャーとなっています。各層は複数のノードで構成されていて、これらのノードのそれぞれが、入力に対して何らかの数学演算を実行して出力を生成します。どのノードへの入力も、前の層からの出力の合計に重みがかけられた (そして通常は 1 またはゼロのバイアス項を足した) ものです。この出力にかけられる重みが、トレーニング中にアルゴリズムによって学習されます。重みパラメーターを学習するために、トレーニング実行による出力が真値と比較され、その誤差がネットワークに逆伝播されて重みが更新されるという仕組みです。

畳み込み

畳み込みとは、ある関数を何らかの方法で別の関数に「適用」する数学演算を意味します。演算の結果は、2 つの関数が「混じり合ったもの」として理解することができます。畳み込みはアスタリスク (*) によって表現されますが、このアスタリスクは多くのプログラミング言語で一般的に使用される * 演算子と混同されがちです。

画像内のオブジェクトを検出する上で、畳み込みはどのように役立つのでしょうか?畳み込みが大きな効果を発揮するのは、画像内の単純な構造を検出し、検出した単純な特徴を 1 つにまとめて複雑な特徴を組み立てることです。畳み込みネットワーク内では、このプロセスが一連の多数の層で繰り返され、各層で、前の層の出力に対して畳み込みが実行されます。

コンピューター・ビジョンでは、どのような類の畳み込みを使用するのでしょうか?これを理解するには、まず、画像がまさに何であるのかを理解する必要があります。画像とは、ランク 2 または ランク 3 のバイト配列のことです。ランク 2 は幅と高さを持つ 2 次元画像を意味し、ランク 3 は幅、高さ、1 つ以上のチャンネルを持つ 3 次元画像を意味します。したがって、グレースケール画像はランク 2、(3 つのチャンネルを使用する) RGB 画像はランク 3 ということになります。バイトの値は単純に、対応するピクセル上で使用する必要がある、その特定のチャンネルの量を表す整数値として解釈されます。基本的に、コンピューター・ビジョンを扱う際は、画像は数値の 2 次元配列であると想像してください (RGB または RGBA 画像の場合は、3 つまたは 4 つの重なり合った 2 次元数値配列)。

したがって、この記事の例で行う畳み込みでは (とりあえず、画像はグレースケールであることを前提とします)、この数値配列を取り、それをフィルターと呼ばれる 2 番目の数値配列に畳み込みます。この畳み込みプロセスのフローを説明すると、まず、画像配列の左上にフィルターを重ねます。次に、フィルターが現在置かれている画像のサブセクションを使用して、フィルターの要素単位の積を算出します。つまり、フィルターの左上の要素を画像の左上の要素で乗算するということです。これらの結果を合計して 1 つの値にします。続いて、ストライドと呼ばれる距離の分、フィルターを移動して、同じプロセスを繰り返します。その出力は、画像配列とはサイズが異なる新しい配列になります (通常、結果の幅と高さは小さくなりますが、チャンネルの数は増えます)。この仕組みを、例を用いて説明します。3 x 3 のフィルターです。

配列の方程式

以下の画像に、このフィルターを適用します。

上記の画像にフィルターを 1 回適用すると、以下の結果になります。

フィルターが縦方向の値に沿って移動していくことを理解していただけたでしょうか?つまり、上記の結果に示されているように、このフィルターは画像内の縦の特徴を抽出します。このように、CNN の実行時に学習されるのはフィルターの値です。

ストライドとフィルター・サイズはハイパーパラメーターであることに注意してください。ハイパーパラメーターとは、モデルによって学習されないパラメーターを意味します。したがって、科学的な考え方を適用して、アプリケーションに最も効果的なストライドとフィルター・サイズを見つけ出さなければなりません。

畳み込みについてもう 1 つ理解しておかなければならない概念は、パディングです。画像のサイズをストライド間隔で割り切れないために、画像とフィルターのサイズが一致しない場合、画像のパディング処理が必要になります。パディングの方法には、VALID パディングと SAME パディングの 2 つがあります。VALID パディングでは基本的に、画像の端の余った値を破棄します。つまり、フィルターが 2 x 2 で、ストライドが 2、画像の幅が 3 の場合、VALID パディングを適用すると、画像の 3 番目の値の列が無視されます。一方、SAME パディングでは画像の端に値 (通常はゼロ) を追加して、画像のサイズを大きくしてフィルターのストライド間隔で割り切れるようにします。一般に、SAME パディングは対称的に行われます (つまり、画像の両端に同じ数の列/行が追加されるようにします)。

興味深いことに、画像の畳み込みはコンピューター・ビジョン以外にも使用できます。畳み込みを使用することで、ぼかしや鮮明化をはじめ、さまざまな画像フィルタリング手法を実装できます。

以下の基本的な Python コードに、畳み込み処理の例を示します (このコードは、numpy などを使用してさらに簡潔にすることができます)。

def basic_conv(image, out, in_width, in_height, out_width, out_height, filter,
filter_dim, stride):
    result_element = 0

    for res_y in range(out_height):
        for res_x in range(out_width):
            for filter_y in range(filter_dim):
                for filter_x in range(filter_dim):
                    image_y = res_y + filter_y
                    image_x = res_x + filter_x
                    result_element += (filter[filter_y][filter_x] *
image[image_y][image_x])

           out[res_y][res_x] = result_element
           result_element = 0
           res_x += (stride - 1)

        res_y += (stride - 1)

    return out

注意する点として、(上記と同じように出力を可視化する目的で) 結果を画像ファイルに書き出すには、出力値の数が 255 以下になるよう制限する必要があります。

プーリング層と全結合層

実際の畳み込みネットワークが畳み込み層だけで構成されることはめったにありません。通常は、他のタイプの層も使用されます。そのような層のうち、最も単純なのは全結合層です。全結合層は、前の層のすべての出力を次の層のすべてのノードに結合するというだけの標準的なニューラル・ネットワークです。通常、全結合層はネットワークの終わりのほうにあります。

畳み込みネットワーク内で目にすることになる主要なタイプの層には、プーリング層もあります。プーリング層にはいくつかの異なる形がありますが、最もよく使用されているのはマックス・プーリング層です。マックス・プーリング層では、入力行列が同じサイズのセグメントに分割され、各セグメントに含まれる最大値が採用されて、出力行列の対応する要素に取り込まれます。

出力行列
出力行列

上記のコードでは、入力行列が 2 x 2 に四分割され、マックス・プーリングが適用されています。したがって、この特定の処理は、サイズが 2、ストライドが 4 のフィルターを適用することとして表現できます。このプロセスの結果が、特徴が存在する大まかなセクターを選び出すことです。例えば、このネットワークで顔を検出するとします。この場合、右下に顔がある可能性が高く、左上に顔がある可能性がある程度あり、右上または左下に顔がある可能性はないことを示すとして、このプーリングの結果を解釈することができます。

def max_pool(input, out, in_width, in_height, out_width, out_height, kernel_dim,
stride):
    max = 0

    for res_y in range(out_height):
        for res_x in range(out_width):
            for kernel_y in range(kernel_dim):
                for kernel_x in range(kernel_dim):
                    in_y = (res_y * stride) + kernel_y
                    in_x = (res_x * stride) + kernel_x

                    if input[in_y][in_x] > max:
                       max = input[in_y][in_x]

           out[res_y][res_x] = max
           max = 0

return out

サンプルの背景情報

画像内の手書きの数字を識別するネットワークを作成する過程を通して、単純なコンピューター・ビジョンの問題を解決しましょう。このサンプルは、ニューラル・ネットワークの力を見せつけるために最もよく使われている基本的なサンプルのうちの 1 つです。サンプル・コードは Python で作成されていて、具体的な実装の詳細に気を取られることなく全体的なアーキテクチャーに集中できるよう、TensorFlow ライブラリーが使用されています。TensorFlow には、MNIST データ・セットが組み込まれているという、もう 1 つのメリットとがあります。ただし、このデータ・セットは TensorFlow だけでなく、SciKit-Learn などの機械学習フレームワークにも組み込まれていることを言い添えておくべきでしょう。

トレーニングとテストには、TensorFlow の組み込み MNIST データ・セットを使用します。ここでは、LeNet-5 をベースとした比較的単純な畳み込みネットワーク・アーキテクチャーを使用します。LeNet-5 は MNIST データ・セットに対して誤差率 0.9% を達成しましたが、このサンプルでは、そこまでの精度には達しないでしょう。何故かというと、LeCun や他のコンピューター・サイエンティストがネットワークのパフォーマンスを向上させるために実施したデータ操作の多くを見送り、さらにアーキテクチャーの特定の側面も単純化するためです。

すべてのコードを、私の GitHub リポジトリーに用意しておきました。このリポジトリーには、早期エッジ検出画像フィルターをデモンストレーションするコードも含まれています。

アーキテクチャー

この例では、以下のアーキテクチャーを使用します。

  1. 畳み込み層で、32x32x1 の MNIST 画像を 28x28x6 の出力に縮小します。
  2. マックス・プーリング層で、特徴の幅と高さを 2 分の 1 にします。
  3. 畳み込み層で、サイズを 10x10x16 に縮小します。
  4. マックス・プーリング層で、再度、特徴の幅と高さを 2 分の 1 にします。
  5. 全結合層で、特徴の数を 400 から 120 にまで縮小します。
  6. 2 番目の全結合層で、再度、特徴の数を縮小します。
  7. 最後の全結合層で、サイズ 10 のベクトルを出力します。

各中間層では ReLU 非線形性関数を使用し、畳み込み層のそれぞれで、ストライド 1 の 5x5 フィルターと VALID パディングを適用します。一方、マックス・プーリングのフィルター・サイズは 2 です。

ヘルパー・メソッド

サンプル・コードではヘルパー・メソッドをいくつか使用して、アーキテクチャー内で繰り返されるフィルター (各フィルターは 5x5 で共通していますが、深さは異なります) や畳み込み層を作成する際の詳細を部分的に抽象化しています。重みの初期化で、不完全なガウス分布を使用していることにも注意してください。重みのすべてが同じでない限り、重みの初期値は重要でないため、対称性を崩す目的で不完全なガウス分布を使用します。以下のコードは、ヘルパー・メソッドの一例を示しています。

def make_conv_layer(self, input, in_channels, out_channels):
        layer_weights = self.init_conv_weights(in_channels, out_channels)
        layer_bias = self.make_bias_term(out_channels)
        layer_activations = tf.nn.conv2d(input, layer_weights, strides =
self.conv_strides, padding = self.conv_padding) + layer_bias

        return self.relu(layer_activations)

ネットワークを作成する

層を作成する際の特定の詳細が抽象化されているため、このネットワークは比較的簡単に作成できます。

def run_network(self, x):
        # Layer 1: convolutional, ReLU nonlinearity, 32x32x1 --> 28x28x6
        c1 = self.make_conv_layer(x, 1, 6)

        # Layer 2: Max Pooling. 28x28x6 --> 14x14x6
        p2 = self.make_pool_layer(c1)

        # Layer 3. convolutional, ReLU nonlinearity, 14x14x6 --> 10x10x16
        c3 = self.make_conv_layer(p2, 6, 16)

        # Layer 4. Max Pooling. 10x10x16 --> 5x5x16
        p4 = self.make_pool_layer(c3)

        # Flattening the features to be fed into a fully connected layer
        fc5 = self.flatten_input(p4)

        # Layer 5. Fully connected. 400 --> 120
        fc5 = self.make_fc_layer(fc5, 400, 120)

        # Layer 6. Fully connected. 120 --> 84
        fc6 = self.make_fc_layer(fc5, 120, 84)

        # Layer 7. Fully connected. 84 --> 10. Output layer, so no ReLU.
        fc7 = self.make_fc_layer(fc6, 84, 10, True)

        return fc7

トレーニング

まず、MNIST データ・セットをトレーニング・セット、クロス確認セット、テスト・セットに分割します。

x_train, y_train, x_valid, y_valid, x_test, y_test = split()

x_train = pad(x_train)
x_valid = pad(x_valid)
x_test = pad(x_test)

x_train_tensor = tf.placeholder(tf.float32, (None, 32, 32, 1))
y_train_tensor = tf.placeholder(tf.int32, (None))
y_train_one_hot = tf.one_hot(y_train_tensor, 10)

トレーニング用ラベルは、one-hot ベクトルという形に修正されています。one-hot ベクトルとは、各要素が表すクラスにサンプルが属する場合、その要素は 1 となり、属さない場合はゼロとなるベクトルです。つまり、数字 1 が示されている画像の場合、one-hot ベクトルは以下のように表現されます。

one-hot 表現

続いて、モデルのトレーニング方法を定義するための処理をセットアップします。例えば、トレーニング中にどの量を最小化するかを定義するための処理です。

net = lenet.LeNet5()
logits = net.run_network(x_train_tensor)

learn_rate = 0.001
cross_ent = tf.nn.softmax_cross_entropy_with_logits(logits = logits, labels =
y_train_one_hot)
loss = tf.reduce_mean(cross_ent) # We want to minimise the mean cross entropy
optimisation = tf.train.AdamOptimizer(learning_rate = learn_rate)
train_op = optimisation.minimize(loss)

correct = tf.equal(tf.argmax(logits, 1), tf.argmax(y_train_one_hot, 1))
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

損失の尺度として交差エントロピーを使用し、AdamOptimiser によって最適化を実行します。

トレーニング・セットをサイズ 128 のバッチに分割し、エポック数 (この例の場合は 10) にわたってトレーニングを実行します。各エポックの後、生成された重み一式をクロス確認セットに照らし合わせます。

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    example_count = len(x_train)

    for i in range(num_epochs):
        x_train, y_train = shuffle(x_train, y_train)

        for j in range(0, example_count, batch_size):
            batch_end = j + batch_size
            batch_x, batch_y = x_train[j : batch_end], y_train[j : batch_end]
            sess.run(train_op, feed_dict = {x_train_tensor: batch_x,
y_train_tensor: batch_y})

        accuracy_valid = eval(x_valid, y_valid)
        print("Accuracy: {}".format(accuracy_valid))
        print()

    save.save(sess, "SavedModel/Saved")

最終的なモデルを保存して、このモデルをテスト・セットに対して使用できるようにします。

パフォーマンス

テスト・セットに対して実行した結果、このモデルは精度 98.5% (誤差率 1.5%) を達成しました。このパフォーマンスは、LeCun の実装よりもやや劣りますが、それはデータの準備などに違いがあったためです。ただし、このような比較的単純な実装としては、かなりのハイパフォーマンスを達成したことになります。

まとめ

この記事では、畳み込みニューラル・ネットワークの基礎について、畳み込みプロセス自体とマックス・プーリング、全結合を含めて説明した後、比較的単純な CNN アーキテクチャーの実装を見ていきました。この記事で学んだ知識が、皆さんの取り組みで CNN を使用する際に役立つこと、あるいはこの魅力的な機械学習分野の学習を始めるきっかけとなることを願います。


ダウンロード可能なリソース


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cognitive computing
ArticleID=1063060
ArticleTitle=畳み込みニューラル・ネットワーク (CNN)
publish-date=10042018