Tensorlow tutorial series 2: MNIST Experts

あゆた AI 担当の佐々木です。

前回の MNIST ビギナーズ編に続き、今回はその続編であるエキスパート編の解説をしていきます。
このチュートリアルは前後半の2部構成になっているのですが、
前半はほぼビギナー編のおさらいなので割愛します。
また、この記事では機械学習の原理的な説明は省略し、チュートリアルをコードとして理解できるようになるということに焦点を当てて解説していきます。なので、なぜこのようなコードになっているのか?という部分を主に解説していきます。
原理的なところのアカデミックな話は、後日別の記事でできればなと思います。

さて、ビギナー編で作ったシンプルなネットワークでは精度が92%でした。
精度をより一層高めるため、画像認識の分野で定番の CNN (Convolutional Neural Network) を実装していきます。
CNN の原理については既にわかっているという前提で、それを Tensorflow でどう実装するのか見ていきます。
CNN の原理についてあまり馴染みのない方や、詳しく知りたい方はこちらの記事で分かりやすく解説されているので、読んでみてください。


Weight Initialization

CNN の実装では多くの重み・バイアスを保持する変数を作る必要があります。
そのため、繰り返し行うこの処理をメソッドとして定義しておきます。

def weight_variable(shape):  
    initial = tf.truncated_normal(shape, stddev= 0.1)
    return  tf.Variable(initial)

重みの初期化では、全て同じ値にしてしまうと学習によるパラメータの更新が行われなくなるという問題が生じます。 そのため、ここでは tf.truncated_normal() を用いて正規分布のランダムな値で初期化を行っています。ここでは、標準偏差 stddev を 0.1 とした正規分布で初期化しています。

※ なぜ重みの値を全て同じ値で初期化すると学習が行われなくなるか
  という理由は、解説すると数式の絡む専門的な内容となるため
  割愛します。
  チュートリアルを理解するだけなら、
  とりあえずそういうものだということを覚えておけばおkです。

def bias_variable(shape):  
    initial = tf.constant(0.1, shape= shape)
    return  tf.Variable(initial)

バイアスは一様な値で初期化します。tf.constant(0.1, shape= shape) で全要素を定数 0.1 で埋めた形状 shape のテンソルが生成されます。


Convolution and Pooling

畳み込み・プーリングはそれぞれこの記事の「The Convolution Step」、「The Pooling Step」で解説されている処理になります。
上記記事の The Convolution Step を見てもらえば分かると思うのですが、畳み込み層 (Convolution layer)では入力画像より小さい行列 (アニメーションで動いている黄色の、3×3の行列) をスライドさせていき、それぞれの場所で積和演算を行った結果が出力となります。ちなみに、このスライドさせている行列は文献によりけりですが一般的に「フィルター」「カーネル」「ウィンドウ」などと呼ばれています。
また、プーリング層でも同様にカーネルをスライドさせて行くのですが、こちらは積和演算ではなく最も大きな値を出力とします。

では、上記の点をおさえた上でコードを見ていきましょう!

def conv2d(x, W):  
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding= 'SAME')

conv2d()では畳み込み層の定義を行っています。
strides はカーネルを各次元ごとに動かす量を指定します。Tensorflow の tf.nn.conv2d() では入力画像を [バッチサイズ、幅、高さ、チャンネル数]の4次元テンソルとして受け取り、strides=[]の各値がそれぞれの次元に対応しています。

・padding
また、畳み込み演算を行うと出力のサイズが入力画像よりも小さくなってしまいます。そこで、出力と入力のサイズを同じにするため、畳み込み演算を行う前に入力画像のサイズを縦横何ピクセル分か拡大することがあります。例えば下の画像のように 4×4 の入力画像を、6×6 にしてから 3×3 のカーネルを適用することで、出力画像のサイズは 4×4 のままとなります。
この処理をパディングといい、Tensorflow では SAME を指定すると入力と出力のサイズが同じになるように外周を0で埋めてパディングされます。パディングを行わない場合は VALID を指定します。



def max_pool_2x2(x):  
    return tf.nn.max_pool(x, ksize=[1,2,2,1],
                          strides=[1,2,2,1], padding='SAME')

・ksize
ksize はカーネルのサイズを決めるのに使います。4つの数値を指定し、それぞれ[バッチ、高さ、幅、チャンネル]の指定となります。バッチ中の全画像、全チャンネルに高さ Y、幅 X のカーネルを適用したい場合には、ksize=[1,Y,X,1]とすればおkです。

・strides & padding
これは tf.nn.conv2d() のそれと同じです。

ここまでで CNN を構成する個別のパーツ(畳み込み&プーリング層、重み、バイアス)が定義できたので、後はこれらを組み合わせることで CNN を構築できます。


First Convolutional Layer

畳み込みの第1層を構築していきます。

W_conv1 = weight_variable([5, 5, 1, 32])  
b_conv1 = bias_variable([32])  

weight_variable() の引数[5, 5, 1, 32]ですが、各次元の数値の意味は[幅, 高さ, 入力のチャンネル数、出力のチャンネル数]です。
ここでは、重みの出力チャンネル数と、バイアスに指定する値を同じにすることがポイントです。

x_image = tf.reshape(x, [-1,28,28,1])  

入力画像の形状を畳み込み層に合わせた形へ変形します。
もともとの入力画像は[幅,高さ,チャンネル数]の3次元なので、これを[バッチサイズ,幅,高さ,チャンネル数]の形式に変換してやる必要があります。Tensorflow の reshape() では -1 を指定するとその次元の数は自動で計算されます。バッチサイズに -1 を指定しておくことで、実行時に入力画像の枚数が変わってもコードの修正をせずに動作するようにできます。
MNIST の画像はそれぞれが 28×28pixel, 1channel なので、上記のコードのようになります。

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)  
h_pool1 = max_pool_2x2(h_conv1)  

conv2d(x_image, W_conv1)で畳み込み層の計算を行い、その出力結果にバイアスを足して、さらにそこに Relu を使ってアクティベーションを行っています。


Second Convolution Layer

続いて、畳み込みの第2層を実装していきます。
といっても、第1層のコードをコピペして、shape と変数名を変えるだけです。簡単ですね!
層の数が3層、4層と増える場合にも、基本単位である1層分の実装さえ理解してしまえば、あとは同じものを繰り返し書くだけで層を増やしていくことができます。なので、一見複雑そうに見えるディープなネットワークでも、各層に分解して考えれば案外単純な構造の繰り返しだったりします。

W_conv2 = weight_variable([5, 5, 32, 64])  
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)  
h_pool2 = max_pool_2x2(h_conv2)  


Densely Connected Layer

この時点でサイズ 7×7 の画像が 64枚ある状態です。
64枚の画像に分かれているデータを1つにまとめる働きをするのがこの層です。

W_fc1 = weight_variable([7 * 7 * 64, 1024])  
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])  
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)  

weight_variable() の引数が2次元になりました。これにより 7 * 7 * 64個の入力が、1024個の出力に変換されます。ちなみにここで指定されている 1024という数値は適当に決めておkです。任意の値が使えます。 重みの shape を[7 * 7 * 64, 1024] としたので、これに合う形状にプーリング層の出力を変形してやる必要があります。tf.reshape(h_pool2, [-1, 7*7*64])でこの変形を行っています。


Dropout

過学習を抑制するため、ドロップアウトを入れます。
過学習とは、簡単に言うと学習用のデータに対しては高い性能を発揮するものの、未知のデータに対しては良い性能を発揮できない状態になることです。すなわち、トレーニング用のデータでは高い精度が出るけれど、テスト用のデータでは精度が低くなっているという状態です。

keep_prob  = tf.placeholder(tf.float32)  
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)  

ドロップアウトでは入力層の中からランダムに選択されたニューロンの出力を無効化することで、過学習を抑制します。そのため、全体の何割のニューロンを無効化するか指定する必要があります。その指定に用いるのが tf.nn.dropout() の第2引数である keep_prop です。
ここでは、実行時に keep_prop の値を変更可能にするため、placeholder() として変数を定義して使っています。


Readout Layer

ここまでで出力が 1024個の値になっています。MNIST では 10クラスに分類を行うので、1024個の出力を 10個に変換してやる必要があります。その処理を行うのがこの層です。

W_fc2 = weight_variable([1024, 10])  
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2  

ここまでで学習に用いるモデルの構築は終わりました。あとはこのモデルを使って学習を行うだけです。


Train and Evaluate the Model

それでは、モデルのトレーニングと性能の評価を行って行きましょう!
ここはモデルが変わっても基本的にいつも同じロジックになるので、ほとんどビギナー編で作ったものと同じです。ただ、次の3点のみ変更があります。

  • パラメータ更新のアルゴリズムが
     SGD(Stochastic Gradient Descent: 確率的勾配降下法)から、
     より高性能な Adam に変更されている
  • ドロップアウトを追加したため、feed_dict に keep_prob が増えた
  • 精度をトレーニング100回毎に出力するようになった
  • cross_entropy = tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits(y_conv, y_) )  
    train_step    = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    
    correct_prediction = tf.equal( tf.argmax(y_conv, 1), tf.argmax(y_, 1) )  
    accuracy           = tf.reduce_mean( tf.cast(correct_prediction, tf.float32) )
    
    sess.run( tf.initialize_all_variables() )  
    for i in range(20000):  
        batch = mnist.train.next_batch(50)
        if i % 100 == 0:
            train_accuracy = accuracy.eval(
                feed_dict= { x: batch[0], y_: batch[1], keep_prob: 1.0 } )
            print('Step %d,  training accuracy %g' % (i, train_accuracy) )
        train_step.run( feed_dict= { x: batch[0], y_: batch[1], keep_prob: 0.5 } )
    
    print('test accuracy %g' % accuracy.eval(  
        feed_dict= {x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0 } ))
    

    tf.train.AdamOptimizer(1e-4) で Adam を定義し、学習率に 1e-4 を指定しています。
    また、 tf.initialize_all_variable() は Tensorflow 0.12 以降では
    tf.global_variables_initializer() という別名のメソッドに変更されているので注意してください。

    上記のコードを実行すると精度99.2% ほどになります!!
    学習時間は MacBookPro Retina 15-inch Mid 2015, 2.2 GHz Intel Core i7 の CPU で36分ほどかかりました。