BizFrameworks and DeepLearning

ビジネスの分かる機械学習エンジニアを目指してます

TensorFlowチュートリアル和訳(Deep MNIST for Experts)

TensorFlowのチュートリアルを愚直に和訳していきます。
今回はディープラーニングを使ってMNISTを解くチュートリアルです。ようやくCNNに入ってきますね。 MNISTや機械学習の基礎がわからない人はMNIST for ML Begginersの和訳から読むと良いです。 共通している部分もあるので、中級者の方も先に読み飛ばしておくと理解がスムーズです。

Deep MNIST for Experts

TensorFlowは、大規模な数値計算を行うための強力なライブラリです。
TensorFlowが優れているタスクは深いニューラルネットワークを実装して訓練することです。
このチュートリアルでは深い畳み込みMNIST識別器を構築することで、TensorFlowでのモデルの構築方法を学びます。

About this tutorial

このチュートリアルの最初の部分では、Tensorflowモデルの基本的な実装であるmnist_softmax.pyで何が起きているのかを説明します。
後半では精度を改善するいつくかの方法を示します。

このチュートリアルの各コードをPython環境にコピペしても、コードを読むだけでもOKです。

このチュートリアルを読むことで以下のことが達成できます。

  • MNISTの数字認識を行うために、画像内の全てのpixcelに注目してSoftmax回帰関数を作ります。
  • 数千の画像を使って数字認識モデルを訓練します。(それを行うTensorFlowの最初のセッションを構築します。)
  • テストデータを使ってモデルの精度を確認する
  • 結果を改善するために多層畳み込みニューラルネットワーク(CNN)を構築、訓練、テストする

Setup

モデルを作成する前に、まずMNISTデータセットをロードし、TensorFlowセッションを開始します。

Load MNIST Data

以下のコードをコピペすれば、たった2行でMNISTデータをダウンロードして自動的に読み込みを行うことができます。

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

上のmnistは訓練(training)、検証(validation)、テスト(test)の用データセットをNumpy配列として格納する軽量クラスです。
また、データミニバッチを反復処理するための関数も用意されています。

Start TensorFlow InteractiveSession

TensorFlowは非常に効率的なC ++バックエンドを使用して計算を行います。
このバックエンドへの接続をセッションと呼びます。
TensorFlowプログラムの一般的な使い方は、まずグラフを作成してからセッションで起動することです。

ここでは、InteractiveSessionという便利なクラスを使用します。
これによりコードの構造がより柔軟になります。
このクラスを使うことでグラフの実行と演算グラフの構築を交互に行うことができます。

これはIPythonのようなインタラクティブなコンテキストで作業するときに特に便利です。
InteractiveSessionを使用しない場合はセッションを開始してグラフを起動する前に演算グラフ全体を構築する必要があります。

import tensorflow as tf
sess = tf.InteractiveSession()

Computation Graph

Pythonで効率的な数値計算を行う際には通常NumPyのようなライブラリを使用します。 しかし、これはまだ不十分でGPUで計算をする場合やデータを転送するコストが高い場合などには多くのオーバーヘッドを生じさせてしまいます。

TensorFlowはPythonの外でも重い作業をしていますが、このオーバーヘッドを避けるためにさらにステップが必要です。 TensorFlowはPythonとは独立した単一の高価なオペレーションを実行するのではなく、Pythonの外部で実行される対話型操作のグラフを記述します。 (この様なアプローチは他の機械学習ライブラリでも同様に行われています。)

Build a Softmax Regression Model

このセクションでは、単一の線形層を持つsoftmax回帰モデルを構築します。 次のセクションではこれを多層畳み込みネットワークに拡張します。

Placeholders

演算グラフの構築を始めます。まず、入力画像と出力クラス用のノードを作成します。

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

上の定義では、xy_には特定の値が入っていません。これらはplaceholderと呼ばれるもので、TensorFlowに計算させるために用意する値です。

入力画像xは2次元テンソル浮動小数点数で構成され、ここでは[None、784]のサイズを割り当てます。
この784は[28\times 28]次元のMNIST画像のピクセル数に対応しており、Noneは任意サイズを示します。
出力クラスy_は2次元テンソルで構成され、10次元の1ホットベクトルで構成されます。
この10はMNIST画像がどの数字(0~9)の10種類を示します。

placeholderの引数であるshapeはオプションですが、明示的に指定することでTensorFlowがテンソルの要素数を間違えるバグを防ぐことができます。

Variables

ここで、モデルの重みWとバイアスbを定義します。
これらは追加の入力値としてxy_と同様に扱うことができますが、TensorFlowではそれらを処理するための優れた方法を持っています。それがVariableです。
VariableはTensorFlowの演算グラフで使われる値です。
これは演算の中で修正しながら使うことができます。
機械学習の中でモデルパラメータと呼ばれるものの大半がこのVariableに当たります。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

上ではtf.Variableの呼び出しで各パラメータの初期値を与えています。
この場合、Wbは要素値が全て0のテンソルとして初期化しています。
Wは784×10の行列です。(入力が784次元でと出力が10次元のため)
bは10次元のベクトルです。(出力が10次元のため)

Variableがセッション内で使用される前に、セッション内で変数を初期化する必要があります。
このステップでは、すでに指定されている初期値(この場合は全ての要素が0となる)を取り、それらを各Variableに割り当てます。
これは全てのVariableに対して同時に実行されます。

sess.run(tf.global_variables_initializer())

Predicted Class and Loss Function

ここでは回帰モデルを実装します。たった1行書くだけです!
以下のように、ベクトル化された入力画像xに重み行列Wを乗算し、バイアスbを加えます。

y = tf.matmul(x,W) + b

損失関数も簡単に指定することができます。
損失はモデルの予測値がどれほど悪いかを示すものです。
モデルの訓練では全てのデータを使って損失を最小化するように学習します。
ここで、以下で使う損失関数は目標値とモデルの予測にsoftmax関数を適用したものとの間でクロスエントロピーをとります。
for ML Biginersのチュートリアルと同様に、以下の安定した定式化を使用します。

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))

このtf.nn.softmax_cross_entropy_with_logitsは正規化していないモデルの予測値にsoftmaxを内部で適用し、全てのクラスで合計を取ります。
さらにtf.reduce_meanはこの合計に対して平均を取っています。

Train the Model

モデルと損失関数を定義したので、訓練を行うことはもう簡単です。
TensorFlowは計算グラフ全体を把握しているため、自動微分を使用して各変数に対する損失の勾配を見つけることができます。
ensorFlowにはさまざまな最適化アルゴリズムが組み込まれています。
この例では、クロスエントロピーを下降させるために勾配をステップで減少させる方式を取り、ステップ長を0.5とします。

train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

この1行で実際にTensorFlowが行ったことは、新しい演算を演算グラフに追加することです。
この操作には、勾配の計算、パラメーター更新ステップの計算、更新ステップのパラメーターへの適用、が含まれています。

上のtrain_stepが実行されると、勾配降下の更新がパラメータに適用されます。
つまり、train_stepを以下のように繰り返し実行することでモデルの訓練を行うことができます。

for _ in range(1000):
  batch = mnist.train.next_batch(100)
  train_step.run(feed_dict={x: batch[0], y_: batch[1]})

上の各反復では、100個のトレーニングサンプルデータをbathとして読み込みます。
その後、feed_dictを使用してplace_holderテンソルxy_を各反復用のデータに置き換えて、train_stepの演算を実行します。
ここで、feed_dictをう使うことで演算グラフのテンソルを置き換えることができることに注意してください。
この置き換えplace_holderだけに限定されるものではありません。

Evaluate the Model

作ったモデルはどれぐらい良いものになっているのでしょうか?

ここで、最初に正しくラベルを予測するかどうかを検証してみましょう。
tf.argmaxはある軸に沿ったテンソルの最高エントリーのインデックスを与える極めて有用な関数です。 例えば、tf.argmax(y,1)は学習したモデルが各入力に対して最も可能性が高いと考えるラベルを表し、tf.argmax(y_, 1)には正しいラベルが入っています。 この予測値と正解が一致するかどうかはtf.equalを使って調べることができます。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

上の操作の結果はbooleanのリストで返されます。 どの部分が正しいかを判断するために、浮動小数点にキャストして平均をとります。 例えば[True, False, True, True][1, 0, 1, 1]になり、平均をとると0.75になります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最後に、テストデータでの精度を求めます。この結果はだいたい92%くらいになるはずです。

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

Build a Multilayer Convolutional Network

MNISTで精度92%というのは悪い値です。恥ずかしいほどに。
このセクションでは、モデルを小さな畳み込みニューラルネットワーク(CNN)に変更します。
これにより、精度が99.2%になります。 これは最先端の精度ではないですが、それに近いものです。

Weight Initialization

このモデルを作成するには、多くのweightとbiasを作成する必要があります。
パラメータを多くする場合、勾配が0になるのを防止したり対称性を破壊するために、少量のノイズでweightを初期化する必要があります。
ここではReLu)を使用しているので、微小な正の初期バイアスをかけて初期化を行うことで「ニューロンが死ぬ」ことを避ける習慣が必要です。
モデルを構築する時にこれを何度も行うのは面倒なので、以下で2つの関数を定義しましょう。

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

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

Convolution and Pooling

TensorFlowは畳み込みとプーリングの操作で多くの柔軟性を持ちます。
境界の処理はどうしよう?stride sizeを何にしよう?という疑問があると思います。
この例では、常にバニラバージョンを選択します。
TensorFlowでの畳み込みのデフォルトではstride sizeが1で出力が入力と同じサイズになるように0で埋められます。
また、プーリングでは2x2ブロックのmax poolingを行います。
コードを綺麗に保つために、この操作を以下の関数で抽象化しましょう。

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

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

First Convolution Layer

これで最初のレイヤーを実装できます。最初のレイヤーは畳み込みとmax poolingで構成されます。
この畳み込みでは5x5パッチごとに32個の特徴を計算します。
この時の重みテンソル[5, 5, 1, 32]の形状を取ります。この形状の最初の2次元はパッチサイズを表しており、その次が入力チャネル数、最後は出力チャネル数を表しています。
さらに、この出力チャネルに対応したバイアスベクトルも用意します。 この時の重みとバイアスは以下のようになります。

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

このレイヤーを適用するには、最初にxを4dテンソルに再構成します。
つまり、第2次元と第3次元を画像の幅と高さにし、カラーチャネルの数を第4次元にしたもので以下のようにxx_imageとして作り直します。

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

その後x_imageを上の重みで畳み込み、バイアスを加え、ReLu関数を適用し、最後にmax poolingを適用します。
max_pool_2x2メソッドは画像のサイズを14x14に縮小します。

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

Second Convolution Layer

より深いネットワークを構築するために、上で作ったタイプのレイヤーを複数スタックします。
2番目のレイヤーは、5x5パッチごとに64個の特徴を計算します。

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

2回目のmax poolingで画像サイズは7x7に縮小されています。
さらに、画像全体を処理できるように1024個のニューロンを備えたフルコネクションレイヤーを追加します。 以下では、テンソルをプーリング層からベクトルのバッチに再構成し、重み行列を乗算し、バイアスを加え、ReLuを適用します。

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)

Dropout

パラメータのオーバーフィット(過学習)を軽減するために、読み出しレイヤーの前にdropoutを適用します。
まず、dropout中にニューロンの出力が保持される確率を表すplaceholderを作成します。
これにより、トレーニング時にはdropoutを適用し、テスト時にはオフにすることができます。
TensorFlowのtf.nn.dropoutはマスキングに加えてスケーリングニューロンの出力を自動的に処理するので、dropout自体は追加のスケーリングなしで動作します。

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

Readout Layer

最後に以前のセクション(単層sofrtmax回帰)でやったとの同様のレイヤーを追加します。

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

このモデルはどれくらいうまく機能するのでしょうか?
それを訓練して評価するのは以前のセクション(単層softmax回帰)と同じコードで可能です。

違う部分は以下です。

  • 最急勾配降下最適化法をより良いADAMオプティマイザーに変更する
  • feed_dictに追加パラメータkeep_probを追加して、dropout率を制御する
  • 訓練100回ごとにログを吐き出す

以下のコードをコピペして実行すると訓練と評価が可能ですが、2万回の訓練を行なっているので、(プロセッサにもよりますが)おそらく30分 ぐらいかかると思います。

cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
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.global_variables_initializer())
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}))

このコードを実行すると、最終的な精度は99.2%になっています。

チュートリアルは以上です。
このチュートリアルを行うことで、TensorFlowを使ってより洗練されたディープな学習モデルを素早く簡単に構築し、訓練し、評価する手法を学ぶことができましたね。

終わりに

MNIST Fro ML Beginnersの和訳を読んでいる方はsoftmax回帰の詳細などについての理解ができているはずなので、今回はすんなり理解できそうですね。 ConvolutionやPoolingに入るまでで躓いたり、理解が少し難しかった方は一読されると良いかもしれません。