Python
Jupyter
TensorFlow

Jupyter Notebookでいじって学ぶTensorFlow - MNIST For ML Beginners

More than 1 year has passed since last update.

概要

qiita2notebook

この記事は、TensorFlowのチュートリアル"MNIST For ML Beginners"Jupyter Notebookで動かしてみるための記事です。

また、記事上部のバッジからJupyter Notebook版をダウンロードできます。ぜひNotebook形式でコードをいじりながら試してみてください。

記事内で使う記号

  • :bulb: で始まる文章はコードをいじる際のヒントです。Jupyter Notebook上で試してみてください。
  • :bangbang: で始まる文章はコードをいじる際に気をつけるポイントです。

準備

  • Python 2.7 (3系では動作未確認)
  • Jupyter Notebook
  • TensorFlow 0.9以上 (MNISTデータセットの読み込みのため)
  • Matplotlib
  • Numpy

1. 必要なライブラリを読み込もう

必要なライブラリをimportしておきます。

%matplotlib inline

import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np

2. MNIST画像を読み込もう

MNISTは、機械学習などによく使われる手書き文字の画像データセットです。最初はダウンロードに少し時間がかかります。

from tensorflow.contrib.learn.python.learn.datasets import mnist as mnist_loader

mnist = mnist_loader.read_data_sets("MNIST_data/", one_hot=True)
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz

ダウンロードされたデータは、訓練データ(mnist.train)55,000件と、テストデータ(mnist.test)5,000件に分かれています。モデルの汎化能力を測るために、訓練に利用しないテストデータを分けるというのが一般的な手法になっています。

全てのMNISTデータは、手書き画像とそれに対応するラベルのデータ、の2つの要素から成り立ちます。ここでは手書き画像をxs、ラベルをysと呼びます。

手書き画像(xs)の解説

ひとつの手書き画像は、784(28ピクセル × 28ピクセル)次元のベクトルになっています。

図

mnist.train.imagesはテンソル(n次元配列)であり、55,000個の784次元ベクトルを格納した[55000,784]の形の配列になります。

図

実際に手書き画像をひとつ見てみます。

plt.imshow(mnist.train.images[0].reshape([28, 28]))
plt.gray()

output_9_0.png

:bulb: 描画する手書き画像のインデックスを変えて、別の手書き画像が表示されるのを確かめてみましょう。

ラベル(ys)の解説

MNISTのテストデータのラベルは、与えられた画像がどの数字かを表す、0〜9の数字です。今回は、ラベルをone-hotベクトルの形式で表現します。例えば、「3」を表すラベルは[0,0,0,1,0,0,0,0,0,0]となります。

mnist.train.labelsは、55,000個の10次元ベクトルを格納した[55000, 10]の形の配列になります。

図

実際にラベルをひとつ見てみます。

plt.imshow(mnist.train.labels[0].reshape([1, -1]))
plt.gray()

output_13_0.png

:bulb: 描画するラベルのインデックスを変えて、別のラベルが表示されるのを確かめてみましょう。

3. モデルを定義しよう

今回は、Softmax関数を用いた回帰モデルを利用します。

Softmax回帰の解説

今回のモデルの目標は、手書き画像を見たときに、それがどの数字であるか確率を得られるようにすることです。例えば、モデルが「9」の手書き画像を見たときに、それが80%の確率で「9」である、同時に5%の確率で「8」である(上部にループがあるため)、というイメージです。

これは、Softmax回帰が適しているユースケースのひとつです。例えば、いくつかの結果の選択肢(今回は10通り)が確率分布として与えられたときに、それらの中から1つの結果を選び出したい場合は、Softmax関数を利用することができます。より複雑なモデルを訓練する場合でも、最後にSoftmax関数を利用することができます。

Softmax回帰は、次の2つのステップからなります。

  1. 入力された手書き画像が、ある特定のラベルに該当するエビデンスを足し合わせる
  2. エビデンスを確率に変換する

今回は、与えられた画像が特定のラベルに該当するエビデンスを合計するために、ピクセル強度の重み付き和を計算します。

次の図は、あるモデルが、これらのクラスのそれぞれについて学習した重みを示しています。
赤い部分が文字の特徴を強めるピクセル、青い部分が文字の特徴を弱めるピクセルです。

図

また、バイアスと呼ばれるエビデンスを追加します。これは、入力によらず該当確率が高いというケースを表現するためです。

Softmax回帰の式

与えられた入力 $x$ がラベル $i$ に該当するというエビデンスは、次式で表されます。

\text{evidence}_i = \sum_j W_{i,~ j} x_j + b_i \tag{3.1}
  • $W_i$ : 重み
  • $b_i$ : ラベル $i$ のバイアス
  • $j$ : 入力画像 $x$ 内のピクセルを加算するためのインデックス

Softmax関数を使ってエビデンスの合計を予測確率 $y$ に変換します。

y = \text{softmax}(\text{evidence}) \tag{3.2}

ここでSoftmax関数は、線形関数の出力フォーマットを整形するための活性化関数として利用されています。

Softmax関数の定義は次式で表されます。

\text{softmax}(x) = \text{normalize}(\exp(x)) \tag{3.3}

式を展開すると、次のようになります。

\text{softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)} \tag{3.4}

直感的には(3.3)式の形で考えるのが一番理解しやすいかもしれません。

Softmax回帰は、次図で表すことができます(実際の $x$ は更に多いです)。入力 $x$ に対して重み付き和を計算し、バイアスを加え、Softmax関数を適用します。

図

方程式で書くと次図のようになります。

図

この手続きを行列の乗算とベクトルの加算に変えることで、ベクトル化することができます。

図

簡単に書くと次のようになります。

$$ y = \text{softmax}(Wx + b) \tag{3.5} $$

Softmax回帰の実装

まずは入力 $x$ をシンボリックな変数として定義します。

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

xは特定の値ではなく、placeholderと呼ばれます。TensorFlowに計算を依頼するときに入力する値です。手書き画像は784次元ベクトルですから、これを[None, 784]の2次元テンソルとして表現します。

また、weightやbiasesの定義にはVariableを利用します。Variableは変更可能なテンソルです。

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

変数が揃いました。次のようにモデルを定義します。

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

:bulb: 余裕があれば多層パーセプトロンのモデルを作ってみましょう。

4. モデルの訓練方法を定義しよう

モデルの当てはまりの良さを定量的に評価するため、誤差関数(別名:コスト関数、損失関数)を定義します。今回は、交差エントロピーと呼ばれる誤差関数を用います。

訓練方法の解説

交差エントロピーは、2つの確率分布の間に定義されるエントロピーです。
今回は、「予測されたラベル」「正解ラベル」の2つの確率分布がありますから、交差エントロピーが利用できるのです。

交差エントロピーは次式で定義されます。

H_{y'}(y) = -\sum_i y'_i \log(y_i)

ここで、 $y$ は今回予測した結果の確率分布で、 $y'$ が真の分布です。

訓練方法の実装

交差エントロピーの計算を実行するために、まずは正解データを入力するためのplaceholderを定義します。

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

次のように交差エントロピーを定義します。

cross_entropy = -tf.reduce_sum(y_*tf.log(y))

訓練方法を定義します。

learning_rate = 0.01
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)

:bulb: Optimizerやlearning_rateを差し替えて、動作を見てみましょう。後の分類精度にも影響することが分かるはずです。

5. モデルを実際に訓練しよう

訓練方法を定義したら、いよいよ実際に訓練を行います。

訓練を開始する前に、ここまでに定義した変数を初期化するための命令を作成します(まだ実行はされません)。

init = tf.initialize_all_variables()

TensorFlowのセッションを開始し、変数を初期化するための命令を実行します。今回は、TensorFlowのコードをJupyter上でインタラクティブに実行するため、tf.InteractiveSession()を使用しています。

sess = tf.InteractiveSession()
sess.run(init)

訓練を実行します。今回は訓練を1,000回繰り返しています。

:bangbang: 注意:訓練パラメータを変更して再度訓練を行う場合は、毎回 tf.initialize_all_variables() を呼んで、学習済みの変数を初期化してから実行してください :bangbang:

# 訓練パラメータ
n_train = 1000
n_batch = 100

# グラフ描画用
fig, ax = plt.subplots(1, 1, figsize=(15, 5))
xvalues = np.arange(n_train)
yvalues = np.zeros(n_train)
lines, = ax.plot(xvalues, yvalues, label="cross_entropy")

for i in range(n_train):

    # バッチ学習
    batch_xs, batch_ys = mnist.train.next_batch(n_batch)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

    # グラフ描画用
    yvalues[i] = cross_entropy.eval(feed_dict={x: mnist.test.images[0:100], y_: mnist.test.labels[0:100]})
    lines.set_data(xvalues, yvalues)
    ax.set_ylim((yvalues.min(), yvalues.max()))
    plt.legend()
    plt.pause(.00001)

output_50_0.png

:bulb: 訓練パラメータn_trainn_batchの値を変えて、動作を見てみましょう。後の分類精度にも影響することが分かるはずです。

ループの各ステップで、訓練データから100個のランダムデータ(バッチ)を取得しています。こうしたランダムデータの小さなバッチを使う手法を確率的訓練と呼び、今回は確率的勾配降下法になります。

6. 作成したモデルを評価しよう

TensorFlowの関数を利用し、実際の評価結果を次のように導出します。

  • tf.argmax(y,1) : 最も確からしいラベル
  • tf.argmax(y_,1) : 正解ラベル

これらを元に、正解率を計算します。

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

correct_predictionはbool値のテンソルです。正解率を計算するため、float値にキャストして平均値を取ります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

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

正解率は91%程度になるはずです。

ちなみに「正解率(accuracy)」というのは厳密に定義された精度指標です。類似の精度指標と混同しないように気をつけましょう。

さて、学習された$W$の中身を可視化してみます。左上から順に0~9まで並んでいます。赤い部分が文字の特徴を強めるピクセル、青い部分が文字の特徴を弱めるピクセルです。

w = W.eval().T
fig = plt.figure(figsize=(10, 4))

for i in range(10):
    ax = fig.add_subplot(2, 5, i + 1)
    ax.imshow(w[i].reshape([28, 28]), cmap="seismic")

output_59_0.png

:bulb: colormapを変えると色を変えることができます。

7. 作成したモデルを使って分類してみよう

モデルに手書き画像を与えて、分類結果を見てみます。

# 手書き画像のインデックス
index_test_image = 2

# 分類
result = y.eval(feed_dict={x: [mnist.test.images[index_test_image]]})

# 描画の準備
fig = plt.figure(figsize=(8, 6))

# テスト画像を描画
ax0 = fig.add_subplot(2, 1, 1)
ax0.imshow(mnist.test.images[index_test_image].reshape([28, 28]))

# 分類結果を描画
ax1 = fig.add_subplot(2, 1, 2)
ax1.imshow(result)

output_63_1.png

該当する数字の部分が白くなっているはずです。うまく分類できているでしょうか?

:bulb: 与える手書き画像のインデックスを変えて、分類結果を見てみましょう。
:bulb: どの数字の分類精度が一番高いか(低いか)、ヒストグラムなどを作って見てみましょう。

まとめ

Notebook上で色々とコードをいじって理解を深めてみてください。

参考文献