BizFrameworks and DeepLearning

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

TensorFlowチュートリアル和訳(MNIST For ML Beginners)

TensorFlowのチュートリアルを愚直に和訳していきます。
こちらは「MNISTが何か?〜機械学習の基礎をTensorFlowでどう表現するか?」を説明するセクションとなっています。

以下では、TensorFlowのインストールができていることが前提で進めていきます。

MNIST For ML Beginners

このチュートリアルは、機械学習とTensorFlowの両方を初めて使用する読者を対象としています。
すでにMNIST・softmax(多項ロジスティック)回帰が何であるかを知っている人は速めのチュートリアルの方が良いかもしれません。

プログラムの仕方を学ぶとき、最初に行うのは “Hello World"のプリントという伝統がありますね。プログラミングにHello Worldがあるように機械学習にはMNISTがあります。
MNISTは単純な画像認識のデータセットです。これは次のような手書き数字の画像で構成されています。

f:id:minison130:20170504115729p:plain

MNISTの各画像はそれぞれ手書きの数字を表しており、上記の画像のラベルは5,0,4,1を表しています。

このチュートリアルでは、画像を見てその数字が何かを予測するモデルを訓練します。
本章での目標は、最先端のパフォーマンスを実現する精巧なモデルを訓練することではありません。(後でそれを行うコードを提供します!)
TensorFlowを使ってMNISTを解き始めることが本章の目標となっており、Softmax回帰と呼ばれる非常に単純なモデルから始めます。

このチュートリアルで用いるコードは非常に短く、大事な部分は3行しかありません。
しかし、その背後にある考え方、つまりTensorFlowがどのように動作するのか、そして機械学習の中核概念を理解することは非常に重要です。
このため、コードを実行するまでを非常に慎重に説明していきます。

About this tutorial

このチュートリアルではmnist_softmax.pyコードで何が起こっているかを一行ずつ説明していきます。

このチュートリアルで以下が達成できます。 * MNISTデータとソフトマックス回帰について学ぶ * 画像内の全ての画素を見て、数字を認識するためのモデルである関数を作成する * 数千のMNIST画像を"見る"ことによって手書き数字を認識するモデルをTensorFlowで訓練する(そして、そのための最初のTensorFlowセッションを実行する) * 作ったモデルのaccuracyをテストデータを使って確認する

The MNIST Data

MNISTデータはYann LeCunのウェブサイトでホストされています。
以下のコードを使うことで、2行でMNISTデータを自動的にダウンロードして読み込むことができます。

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

MNISTデータは55000個のトレーニングデータ(mnist.train)と10000個のテストデータ(mnist.test)と5000個のバリデーションデータ(mnist.validation)の3つに分割されます。
この分割は非常に重要です。機械学習ではデータの一部を学習に使わずにとっておいてモデルが一般化されているかを確認するために用いることが不可欠です!

MNISTデータは画像と対応する数字のラベルの2つからできており、以降では画像を"x“, ラベルを”y“と呼ぶこととします。
トレーニングセット、テストセットにもそれぞれ両方の要素が含まれています。例えば、トレーニング画像はmnist.train.imagesでラベルはmnist.train.labelsに入っています。

MNISTの各画像は28×28ピクセルでできています。これは次のように28×28次元の行列として解釈することができます。

f:id:minison130:20170504123514p:plain

この行列は 28 * 28=784の要素からなるベクトルに平坦化することができます。画像間で一貫した手法で平坦化している限り、画像をどのように平坦化するかは問題ではありません。
この観点から見ると、MNIST画像は非常に豊富な構造を備えた784次元のベクトル空間内の単なる一点に過ぎません。

データを平坦化すると画像の2D構造に関する情報が破棄されますが、これは悪いことではないのでしょうか?
後のチュートリアルで説明する高度なモデルでは、この構造は悪い使われ方がされしまっています。
ただ、この章で使用するsoftmax回帰は違います。

mnist.train[55000, 784]という形のテンソル( n次元配列)で表されます。第1次元は画像自体のindex、第2次元は各画像 内のピクセルのindexを表します。
MNISTは2値画像なので、各ピクセルの値は0か1をとります。

f:id:minison130:20170504125046p:plain

MNISTの各画像には対応するラベルが付いています。画像に描かれている桁を表す0〜9の数字です。

このチュートリアルでは、ラベルを「ワンホットベクトル」にしたいと考えています。ワンホットベクトルとはある単一次元が1でそれ以外が0からなるベクトルのことを言います。 MNISTでは数字nを表す画像のラベルをn番目の次元が1になるワンホットベクトルとして表します。 例えば3は[0,0,0,1,0,0,0,0,0,0]となります。
したがって、mnist.train.labelsは[55000, 10]の配列となります。

f:id:minison130:20170504125800p:plain

ここまで説明して、やっとモデルを実際に作っていくための準備が整いました!

Softmax Regressions

これまでの説明で、MNISTのすべての画像が0〜9の手書き数字であることは分かっているはずです。 つまり、与えられた画像が何であるかのパターンは10通りしかありません。

手書きの数字は100%確実にある数字に読めるとは限りません。例えば0と6などの境目は難しいです。
そのため、機械学習では画像を見てその数字が何であるかを確率で表現します。つまり、学習で出来たモデルは9の画像を見てそれが9であることを80%確信している、というような表現になります。

Softmax回帰は自然でシンプルな古典的手法です。あるオブジェクトに対して何らか確率を割り当てたい場合、softmaxは[0:1]の値を与える手法なので目的とマッチしています。
そのため、後のセクションで説明する高度に洗練されたモデルでも最後のステップはsoftmaxを行う層で出来ています。

Softmax回帰は以下2つのステップから成ります。

  1. 入力から得たevidenceを特定のクラスに追加する
  2. その後evidenceを確率値に変換する

Softmax回帰では与えられた画像が何の数字を表すかを示す証拠(evidence)を集めるためにピクセル強度の加重和をとります。(つまり、各数字の特徴となるエッジを調べている操作に近いです。)
この加重和に使われる重みは、ある数字を表す画像毎に共通した特徴が見られる部分は正になり、共通しない特徴が見られる部分はが負になるように学習されます。

次の図は、1~9までの各ラベルについて学習した重みを表しています。
赤は負の重みを表し、青は正の重みを表しています。

f:id:minison130:20170504203301p:plainf:id:minison130:20170504203301p:plain

図を見てわかるように、重みの学習だけでは数字の証拠をうまく捉えることは出来ていません。 そこで、Softmax回帰ではバイアスと呼ばれる追加の証拠を学習します。基本的に、このバイアスの学習は入力から独立しているように見えます。
以上より、入力ベクトル xを与えた時のクラス iの証拠は次のようになります。

 {\rm evidence}_{i} = \sum_{j}W_{i, j}x_{j} + b_{i}

ここで、 W_iが上で書いていたクラス iの重みであり、 b_iがクラス iのバイアスを表します。

次に、「softmax」関数を使用して求めた証拠の集計値を予測関数 yに変換します。

 y = {\rm softmax}({\rm evidence})

ここでのsoftmaxはディープラーニングにおける「活性化関数」もしくは「リンク関数」と呼ばれる機能を果たします。つまり、線形関数の出力を必要な形、この場合は10以上の確率分布に整形します。
この操作により、求めた証拠の集計値が確率値に変換されます。これは以下の様に定義出来ます。

 {\rm softmax}(x)={\rm normalize(\exp(x))}

上式を拡張すると次の様になります。

 {\rm softmax}(x)_i = \cfrac{\exp(x_i)}{\sum_j \exp(x_j)}

ごちゃごちゃ書いてきましたが、softmax回帰は次の様な図で表すことが出来ます。簡単化のために図は3次元にしていますが、実際はもっと多いです。(MNISTは28*28=784次元)

f:id:minison130:20170504231337p:plain

上図を数式で書きあらわすと、次の様になります。

f:id:minison130:20170504231629p:plain

さらに上式をベクトル演算で簡単化すると次の様になります。

f:id:minison130:20170504231904p:plain

最後に、上式をさらに簡単化すると以下の様になります。

 y = {\rm softmax}(Wx + b)

ここまで来て、やっとTensorFlowで使える形になりました。 以下で実装をしていきましょう!

Implementing the Regression

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

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

TensorFlowを使うための最初のステップは以下のimportを行うことです。

import tensorflow as tf

ここでは、シンボル変数を操作することによってTensorFlowの動作を説明します。まず、以下の様にxを定義してみましょう。

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

上の様に書いた時、xには特定の値は入らず、あくまでplaceholderとなります。
これはTensorFlowに計算を実行させるために、用意する値です。
ここには784次元のベクトルに平坦化された任意の数のMNIST画像を入力することが出来ます。
TensorFlowでは、これを[None, 784]の形状の2Dテンソルとして定義しています。 (ここで、Noneは次元が任意の長さであることを意味しています。)

モデルを作るためには重みWとバイアスbが必要です。
ディープラーニングでは多くの変数を追加して扱うことになるので、TensorFlowはそれを処理する便利な手法であるVariableを持っています。
このVariableはTensorFlowのグラフでの相互動作の際に値を保持しつつ変更が可能なテンソルです。Variableは計算の際に使うことも修正することも出来ます。
一般的に機械学習において、モデルのパラメータは基本的にVariableとなります。

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

Variableの定義の際にはtf.Variableを使って初期値を与えます。
上の場合、Wbの両方を値が全て0のテンソルとして初期化しています。
このWbは学習して更新していく値なので、最初は全て0が入っていても問題ありません。

上ではWが[784, 10]の形状をとることに注意してください。
これは784次元の画像ベクトルxWをかけて、10次元のoutputを得たいためです。 また、bWxに足し合わせるので同じく10次元の形状となります。

以上定義した値を使うことで、説明したSoftmax回帰モデルを実装することができます。なんと1行で以下の様にかけます!

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

上ではまず、xW内積tf.matmul(x, W)という式で計算します。
(x, W)という書き方をしますが、TensorFlowの中で計算が反転して Wxが得られることになります。
その次にbを追加して、最後にtf.nn.softmaxを適用します。

モデルの定義は以上でおしまいです。
モデルを定義するには、セットアップ数行とモデル定義の1行のみが必要なだけです。
これはTensorFlowによって簡単化されている訳ではなく、様々な数値計算に対応した非常に柔軟な方法なので、同様の操作を他の機械学習モデルから物理シミュレーションまで、様々な場面で使うことができます。
さらに一度定義したモデルはコンピュータのCPU、GPU、モバイル端末など様々なデバイスで実行することができます!

Traning

モデルを訓練するためには、モデルの良さ・悪さを定義する必要があります。  これは、コストや損失と呼ばれるものを利用することで可能です。
この損失はモデルが希望の結果からどれだけ離れているかを表すものです。
モデルの訓練ではこの誤差を最小限に抑える様にパラメータを学習させ、最も誤差マージンが少ないモデルが得られると良いモデルができたことになります。

モデルの損失を決定するための最も一般的で優れた関数の1つが「cross-entropy」です。
クロスエントロピーは情報を圧縮するための学問から生まれた関数ですが、今ではギャンブルから機械学習まで多くの分野で重要なアイデアとなっています。
それは次のように定義されます。

 H_{\grave{y}}(y)=-\sum_{i} {\grave{y}_{i}}\log(y_{i})

ここで、 yは予測確率分布、 \grave{y}は真分布(桁ラベル付きのワンホットベクトル)を指します。
粗い意味で、クロスエントロピーは、我々の予測が真実を記述するためにどれほど非効率的であるかを測定しています。
クロスエントロピーの詳細については、このチュートリアルの範囲を超えていますが、ここで理解を深めるだけの価値があります。

クロスエントロピーを実装するには、最初に新しいplaceholderを追加して正解を入力する必要があります。

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

次にクロスエントロピー関数 -\sum \grave{y} \log(y)を実装します。

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

上のcroos_entropyの定義において、まず、tf.logyの各要素の対数を計算します。
次にy_の各要素にtf.log(y)の対応する要素をかけます。
その次に、tf.reduce_sumyの2番目の次元に要素を追加します。
最後にtf.reduce_meanでバッチ内の全ての例の平均を計算します。

実際のソースコードでは数値的な不安定さがあるため、この定式は使用しないことに注意してください。
代わりにtf.nn.softmax_cross_entropy_with_logitを利用しています。

モデルがしたいことが分かっているので、TensorFlowにそれを訓練させることはとても簡単です。
TensorFlowは計算のグラフ全体を把握しているため、自動的にバックプロパゲーションアルゴリズムを使用して、変数が最小化する損失にどのように影響するかを効率的に判断できます。
次に、以下で最適化アルゴリズムの選択を適用して変数を変更し、損失を減らすことができます。

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

上では、TensorFlowに学習率0.5の勾配降下アルゴリズムを使用してcross_entropyを最小にするように頼んでいます。
勾配降下は簡単な手順で、コストを削減する方向に各変数を少しだけシフトさせているだけです。
TensorFlowは他にも多くの最適化アルゴリズムを提供しています。
使い分けるのは上の1行を微調整するだけです。

裏でTensorFlowが実際に行っていることは、バックプロパゲーションと勾配降下を実装する新しい操作をグラフに追加することです。
そして実行時に勾配降下訓練のステップを行い変数を微調整して損失を減らす、という1回の操作を返します。

これでモデルをInteractiveSessionで開始することができます。

sess = tf.InteractiveSession()

セッションを開始すると、以下の操作で最初に作成した変数を初期化する必要があります。

tf.global_variables_initializer().run()

それでは、1000回の学習を行ってみましょう!

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

ループの各ステップでは、トレーニングセットから100個のランダムなデータポイントの「バッチ」を取得します。 そして各バッチごとにtrain_stepフィードを実行してplaceholderを置き換えます。

ランダムデータからなる小さなバッチを使って学習することは、この場合、確率的勾配降下法と呼びます。
理想的には、訓練のすべての段階ですべてのデータを使用したいと思っています。
その方がやりたいことをより深く理解させることができますが、高価になります。
したがって代わりに、毎回異なるサブセットを使用することで安価で同じ利益を得ています。

Evaluating Our 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))

最後に、テストデータでの精度を求めます。

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

この結果はだいたい92%くらいになるはずです。

これは良い結果なのでしょうか?実は違っていて、実際はなかなか悪い値です。
非常に単純なモデルを利用しているため、まだまだ精度が出ていません。
しかし、モデルに小さな変更を加えることで精度は97%まであげることができます。
また、最高のモデルだと精度は99.7%に到達しています!
(詳細については結果リストを見てください)

重要なことは精度ではなく、モデルの組み方や機械学習の基礎などを学んだことです。 より精度をだすモデルがきになる人は次のチュートリアルに進めば、より洗練されたモデルの構築方法を知ることができます!

終わりに

疲れた。。
softmax回帰の操作などはところどころ意訳して分かりやすくしたつもりですが、文章だけで理解するのは難しいですね。 機械学習の理論的な部分の理解が難しい方はWeightとbiasをパラメータとして学習することで、モデルが出来上がることが理解できれば問題ないように思います。