Tensorflow tutorial series 1: MNIST for ML beginners

あゆたで AI を担当をしているデータサイエンティストの佐々木です。

業務で Tensorflow を扱っているので、今日はその使い方を解説しようと思います。 ということで今回は公式チュートリアルのMNIST For ML Beginnersを解説していきます。

MNIST は手書き文字分類を行うタスクのことで、0 〜 9 までの手書き文字の画像を入力し、書いてある数字を判別させるものです。
MNIST は機械学習の Hallo World と呼ばれるほど定番のもので、ネットワークの構造などはとてもシンプルなものになりますが、その分短いコードの中に重要なエッセンスが凝縮されています。そのため、Tensorflow の使い方やディープラーニングのいろはを習得するにはもってこいの題材です。

以下の解説では Tensorflow 0.11 を元にしたコードで解説をしていきます。


1. 概要

はじめに、このチュートリアルの目的を確認しておきましょう。
このチュートリアルは、以下の項目の達成を目的としています。

  • MNIST データと、ソフトマックスについて学ぶ
  • 文字認識に用いるモデルを構築する
  • Tensorflow でモデルを学習させる方法を学ぶ
  • テストデータに対するモデルの精度を確認する

それでは、次章から順に見ていきましょう!

 

2. MNIST データ

MNIST に用いるデータセットは、Tensorflow の中に標準で組み込まれているので以下のコードで取得できます。

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

MNIST データセットは全体で3つの構成になっています。

  • 55000枚のトレーニング用画像
  • 10000枚のテスト用画像
  • 5000枚のバリデーション用画像

上記の各画像はまた、以下の2つの要素で構成されています。

  • 手書き文字の画像
  • その画像の正解データ(ラベル)

これにより、画像とその答えデータを与えることで教師あり学習ができます。 ここで、なぜデータを分ける必要があるのか?ということについて考えてみましょう。

なぜデータを分けるのか

機械学習では学習を繰り返すことでモデルはどんどん賢くなり、正解に近づいていきます。 ところが、時としてトレーニングに用いたデータに適合しすぎてしまい、未知の新しいデータに対して良い結果を出せない結果となることがあります。 この現象は「過学習」と呼ばれ、多くの研究者たちを悩ませている問題です。

さて、そんな訳でモデルが過学習を起こさず、未知のデータに適応できる形で進化しているか?というのを確認する必要があります。 そのためにはテスト時にはトレーニング中で使用していない、全く新しいデータを用いる必要があり、これを行うためにデータセットをトレーニング用とテスト用とに分けています。

バリデーション用のデータについてですが、これはトレーニングで訓練した後のモデルの、パラメータ調整を行うために使います。では、なぜテスト用のデータとバリデーション用のデータを分けなければならないのでしょうか。その理由は以下、

————————————————————————
パラメータのチューニングは未知のデータに対して精度が
上がるようにやってほしい!

なので未知の画像を使ってパラメータのチューニングは行う必要がある

けど、テスト用データで最適化を行ってしまったら?
そのデータに対して最適化を行っているので、当然エラー率は低くなる
その結果、未知のデータに対する適切な性能評価が行えなくなってしまう
————————————————————————

上記の理由から、バリデーションセットとテスト用のデータも分ける必要があり、結果として3つに分割することとなっています。

【まとめ】

  • 未知のデータに適合しているか検証するために、トレーニング用のデータとテスト用のデータは分ける必要がある。
  • 未知のデータの対して性能を最適化したいので、パラメータ調整に用いるデータはトレーニング時に
    使用しなかったものを用いるのが望ましい。
  • ただ、パラメータを最適化する際に用いたデータについては
    未知のデータよりもエラー率が低くなると考えられる。
    そのため、最終的な性能評価はトレーニングにも、
    パラメータ調整にも使用していないものを用いる必要がある。

 

ソフトマックス

MNIST は入力された画像が 0 ~ 9 のどれに該当するか?を推論する多クラス分類の問題です。では、ニューラルネットワークの出力から、どうすれば各クラスへの分類ができるでしょうか。
この、ニューラルネットワークの出力を、「どのクラスに属するか?」という情報に変換してくれるのがソフトマックスという処理です。

ソフトマックスの処理は、数式で表すと上記のようになります。 ここで、分母の x_j はニューラルネットワークの出力層における各ニューロンの出力で、MNIST の場合は10クラス分類なので、1 <= j <= 10 です。 全ての出力の合計を分母に、ある1つの出力が分子にあるので、要は割合を出しています。
ソフトマックスの出力はすべて足すと1になり、このことからソフトマックスの出力は各クラスごとのそれっぽさ。すなわち、どれだけそのクラスに属していそうか?を表す確率として解釈できます。

ソフトマックスの出力が各クラスに属する確率として解釈できるおかげで、出力の中からもっとも大きな数値を選べば、入力画像はそのクラスに分類されると考えることができます。

 

モデルを実装してみよう!

さて、まずはモデル構築のために必要な変数の定義を行います。
Tensorflow ではまず placeholder というものを作ります。
これは変数を格納するための箱のようなもので、事前にテンソルの形状と型のみを定義しておき、具体的な値は計算を行う際に feed_dict として入力することになります。

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

x は入力データを入れるための変数で、[None, 784]はテンソルの形状を表します。
それぞれ[画像の枚数、1画像当たりのデータ数]となっています。ここで、None を指定した項目は実行時に具体的な値が決まります。入力される画像の数はその時々で変えられるようにしたいので、ここでは None にしています。784は MNIST の画像1枚が 28 × 28 pixcel, 計784ピクセルから成ることから来ています。

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

変数の定義には tf.Valiable() を用います。 W は重み、b はバイアスをを表します。b の [10]は出力層が10個であるためです。
W は 「784個の入力を受け取り、10個の出力を出す」という動きをさせるために[784, 10]としています。

ここで、各ニューロンの出力は入力 x, 重み W, バイアス b を用いて以下の数式で表されます。

Σ とか入っていて一見難しそうに見えますが、やってることは W と x という2つの行列の積を計算し、バイアスを足しているだけです。
行列の積は tf.matmul() で計算できます。
ニューラルネットワークの最終的な出力は、上記の式で計算した出力にソフトマックス処理をしてあげれば得られます。
上記の処理をまとめて、Tensorflow では以下のように書けます。

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

 

モデルの学習

モデルの学習は正解とニューラルネットワークの出力との誤差を小さくするように、重み W とバイアス b の値を調整していくことで実現します。
では、どのようにして「誤差」を求めればよいでしょうか。
誤差の計算法には様々ありますが、チュートリアルでは交差エントロピーという指標を用います。数式で表すと以下の通り。

y はソフトマックスの出力で、y_i は答えのラベルデータです。
これも一見複雑そうにみえる式ですが、やってることは簡単です。
というのも、y_i は答えのラベルのみが 1で、それ以外が 0 の例えば[0,0,0,1,0,0,0,0,0,0] のようなベクトルだからです。そのため、上記の式は実質、1 が立っているクラスの -log(y) を取り出しているだけです。

また、上記の[0,0,0,1,0,0,0,0,0,0] のように、正解のラベルだけを 1、それ以外を 0にしたものを「One hot encording」と呼びます。

さて、誤差を求めるためには正解のデータが必要でした。
正解データを格納するための placeholder を定義しましょう。

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

正解データの形状は [画像の枚数, 分類するクラス数(10)] なのでこのようになります。

また、交差エントロピーの計算は以下のコードでできます。

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

tf.reduce_sum() で入力画像それぞれの誤差を計算し、
tf.reduce_mean() で全画像の誤差の平均をとっています。
reduction_indices は tf.reduce_sum() で合計を計算する時の軸を指定するものです。また、reduction_indices は Tensorflow 1.0 から axis に改名されています。

次に、誤差を最小化する処理を書いていきましょう!

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

tf.train.GradientDescentOptimizer() というものが誤差の値を元にパラメータの調整を行ってくれます。GradientDescentOptimizer() は勾配降下法というアルゴリズムに基づいてパラメータの調整を行います。
tf.train.GradientDescentOptimizer(learning_rate).minimize(error)
とすることで、「error を最小化するように各変数の値を調整する。値を変化させる度合いとして、learning_rate をもちいる」という意味になります。

 

学習してみよう!

Tensorflow では作成した式を計算する前に、
「変数の初期化」と「セッションの定義」を行う必要があります。 コードとしては以下のように書けば OK です。

init = tf.initialize_all_variables()  
sess = tf.Session()  
sess.run(init)  

tf.initialize_all_variables()は Tensorflow 0.12 以降では
tf.global_variables_initializer()と改名されているので注意してください。

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

先程作成した学習のための式「train_step」を繰り返し呼び出すことで、徐々に重みとバイアスの調整が行われ、学習を進めて行くことができます。 ここで、事前に placeholder で作成した変数については、実行時に
feed_dict で具体的な値を入れてやる必要があります。これは placeholder の変数名を key, 入れたい値を value とした辞書をいれてやれば OK です。
ここでは、全データの中から 100枚ずつ入力用と答え用のデータを取り出し、学習に用いています。データ全体ではなく一部のみを使用する理由は、全てのデータを学習に用いると、計算時間が膨大になってしまうためです。このようなデータ全体のうちの一部のサンプルのみをランダムで抽出し、その勾配を元に学習を進めていく手法を確率的勾配降下法と呼びます。

 

性能を評価しよう!

性能の評価は、テスト用のデータを用いて推論を行い、どれだけの割合で正解したかによって求められます。 コードにすると以下のようになります。

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

tf.argmax() は入力されたデータの中から最も大きな値をもつ要素の添字を返します。tf.equal() は与えた引数どうしが等しければ True, 異なれば False を返します。
これにより、正解・不正解の結果を集めたリストが得られます。

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

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

tf.reduce_mean() で正解数を全要素数で割ることで、正解率を出しています。
これを実行するとだいたい正解率 92% になります。
小規模なネットワークなので、計算は CPU でも一瞬で終わります。