まひろ量子のハックログ

プログラミングや機械学習などの知識を記録・共有します

世界一わかりやすいTensorflowのチュートリアル(を目指す)

Tensorflowの公式チュートリアル、わかりにくくないっすか…?

Get Start with Tensorflow!さぁ、チュートリアルを始めよう! そう思って1番最初のBasic classificationに足を踏み入れた瞬間、そこにはFashion Mnistをmatplotlibで描画するGoogle先生の姿が…( ゚д゚)

しかも、最初のモデルを作ってみようのコーナーで

model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation=tf.nn.relu),
    keras.layers.Dense(10, activation=tf.nn.softmax)
])

…( ゚д゚)

いきなり抽象化されたkerasの関数使ってますやん。便利であることは間違いないのですが、ブラックボックスであるが故に中でどんな計算が行われているのか全然わかりません。 機械学習の教科書を真面目に読んできた初心者たちは、「あれ? わいは入力ベクトルと行列の掛け算がニューラルネットワークやって習ったで? Flattenってなんや? Denseってなんや?」となってしまいそうです。

この記事は、そんな初心者でもピュアなTensorflowの使い方を1発で理解できるチュートリアルとなっております。「ピュアな」というのは、抽象化されたkeras関数を使わず、行列計算を組み合わせてニューラルネットワークをスクラッチで作っていくということです。

世界一わかりやすい記事を目指してがんばります。

最も基本的なTensorflowの流れ

何をするにも「大きな流れ」というものがあります。カレーを作るには、材料をカットして炒めて煮てカレー粉を入れる、という大きな流れがあります。まずは、この「基本」をマスターしましょう。「10分煮込むより15分煮込んだ方が美味しいんじゃないか」といった細かい研究は後で好きなだけやってください。

Tensorflowでは以下の流れが基本です。

  • プレースホルダーの定義
  • ニューラルネットワークの定義
  • 誤差関数の定義
  • 正解率の定義
  • 学習アルゴリズムの選定
  • 教師データの読み込み
  • セッションを張る
  • 学習の実行

それぞれが何を表しているのかについて、これから1つずつ見ていきます。

まずは、この基本をおさえた最も単純なコードを以下に示します。

import tensorflow as tf

# プレースホルダーの定義
x = tf.placeholder(tf.float32, [None, 2])
t = tf.placeholder(tf.float32, [None, 2])

# ネットワークの定義
w = tf.Variable(tf.random_normal([2,2], mean=0.0, stddev=1.0))
b = tf.Variable(tf.random_normal([2], mean=0.0, stddev=1.0))
y = tf.matmul(x, w) + b

# 誤差関数の定義
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=t, logits=y))

# 正解率の定義
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(t, 1))
acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# 学習アルゴリズムの選定
train = tf.train.MomentumOptimizer(0.01, 0.9).minimize(loss)

# おまじない
init = tf.global_variables_initializer()

# 教師データの読み込み
#  ここでは、(1,1) が入力されたときのみ (0,1) を出力し、
#  (0,0), (1,0), (0,1) が入力されたときは (1,0) を出力するAND回路を
#  題材としました。
train_x = [ [0,0], [0,1], [1,0], [1,1] ]
train_t = [ [1,0], [1,0], [1,0], [0,1] ]
test_x  = [ [0,0], [0,1], [1,0], [1,1] ]
test_t  = [ [1,0], [1,0], [1,0], [0,1] ]

# セッションを張る
with tf.Session() as sess:
    sess.run(init) # おまじない

    print('Epoch\tTraining loss\tTest loss\tTraining acc\tTest acc')

    # 学習の実行
    for epoch in range(200):
        sess.run(train, feed_dict={
            x: train_x,
            t: train_t
        })

        # 5エポックに1回の頻度で、学習の状況を描画する
        if (epoch+1) % 5 == 0:
            print('{}\t{}\t{}\t{}\t{}'.format(
                str(epoch+1),
                str(sess.run(loss, feed_dict={x:train_x, t:train_t})),
                str(sess.run(loss, feed_dict={x:test_x, t:test_t})),
                str(sess.run(acc, feed_dict={x:train_x, t:train_t})),
                str(sess.run(acc, feed_dict={x:test_x, t:test_t})),
            ))

では、それぞれの要素を順番に見ていきます。

プレースホルダーの定義

# プレースホルダーの定義
x = tf.placeholder(tf.float32, [None, 2])
t = tf.placeholder(tf.float32, [None, 2])

Tensorflowにはプレースホルダーという概念があります。よく文字列のプレースホルダーとして「*」という記号を使いますよね。あれは「*には色々な文字が入りますよ」ということを表しています。 Tensorflowのプレースホルダーも同じです。x = tf.placeholder(…)のように書くと「xには色々な値が入りますよ」ということを宣言したことになります。「色々な値」とは、教師データのことです。 上のように定義したプレースホルダーxtは、それぞれ「入力データ」と「出力データ」を入れるための箱ということになります。( x は数学で変数名としてよく使うエックス、tは目的変数(target variable)の頭文字から来ています。)

引数にtf.float32, [None, 2]と書いてありますが、これは「どんな値が入るのか」を指定しています。第一引数で値の型を指定し、第二引数で値の shape (何×何 行列か)を指定します。

shapeの1つ目がNoneとなっているのは「可変」であることを意味しています。なぜ可変で良いのかというと、バッチサイズが関係しているのですが、初心者の方は「そういうものか」と受け流してもらっても良いです。 shapeの2つ目が2となっているのは、今回の入力データおよび出力データの次元が2次元だからです。

ネットワークの定義

# ネットワークの定義
w = tf.Variable(tf.random_normal([2,2], mean=0.0, stddev=1.0))
b = tf.Variable(tf.random_normal([2], mean=0.0, stddev=1.0))
y = tf.matmul(x, w) + b

tf.Variable()で、学習可能な重みパラメータを定義できます。ここでは、2x2の重み行列としてw、 2次元のバイアスとしてbを学習可能なパラメータとして定義しました。 yはニューラルネットワークの出力となります。ここでは、さきほど定義したxwの積(matmul)を計算し、バイアスbを加えるという、最も単純なニューラルネットワークを定義しました。

誤差関数の定義

# 誤差関数の定義
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=t, logits=y))

softmax_cross_entropy_with_logits()は、2つの行列のソフトマックスクロスエントロピーを計算します。最終的に誤差lossはスカラーにする必要があるため、各バッチの平均をとるためにtf.reduce_meanを呼んでいます。

教師データの出力値tと、ニューラルネットワークの出力値yを、それぞれlabelslogitsに指定します。

正解率の定義

# 正解率の定義
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(t, 1))
acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

教師データtと、ニューラルネットワークの出力値yから得られる予測とを比較し、予測が当たっていたかどうかを判定します。そのためには、1hotベクトルtの1が立っているインデックスと、ベクトルyの最も大きい要素のインデックスが等しいかどうかを見ればOKです。それをやっているのがtf.equal(tf.argmax(y, 1), tf.argmax(t, 1))です。これも、出力は行列になるので平均をとってスカラー化するためにtf.reduce_meanを呼んでいます。

学習アルゴリズムの選定

# 学習アルゴリズムの選定
train = tf.train.MomentumOptimizer(0.01, 0.9).minimize(loss)

Tensorflowでは、任意のOptimizerのがもつminimizeというメソッドに誤差関数を渡します。この左辺trainを使って、今後、学習を実行していくことになります。上の例ではOptimizerとしてMomentumOptimizerを採用していますが他のものを選定してもらっても構いません。

教師データの読み込み

# 教師データの読み込み
#  ここでは、(1,1) が入力されたときのみ (0,1) を出力し、
#  (0,0), (1,0), (0,1) が入力されたときは (1,0) を出力するAND回路を
#  題材としました。
train_x = [ [0,0], [0,1], [1,0], [1,1] ]
train_t = [ [1,0], [1,0], [1,0], [0,1] ]
test_x  = [ [0,0], [0,1], [1,0], [1,1] ]
test_t  = [ [1,0], [1,0], [1,0], [0,1] ]

(1,1) が入力されたときのみ (0,1) を出力し、(0,0), (1,0), (0,1) が入力されたときは (1,0) を出力するAND回路を学習させようと思います。

セッションを張る

# セッションを張る
with tf.Session() as sess:

ほとんど、おまじないと思ってもらってOKです。Tensorflowではセッションというものを張り、セッション中でrun(…)というメソッドを叩くことで、ニューラルネットワークを動かしていきます。

学習の実行

    # 学習の実行
    for epoch in range(200):
        sess.run(train, feed_dict={
            x: train_x,
            t: train_t
        })

        # 5エポックに1回の頻度で、学習の状況を描画する
        if (epoch+1) % 5 == 0:
            print('{}\t{}\t{}\t{}\t{}'.format(
                str(epoch+1),
                str(sess.run(loss, feed_dict={x:train_x, t:train_t})),
                str(sess.run(loss, feed_dict={x:test_x, t:test_t})),
                str(sess.run(acc, feed_dict={x:train_x, t:train_t})),
                str(sess.run(acc, feed_dict={x:test_x, t:test_t})),
            ))

200エポックのループを回し、各ループで sess.run(train, feed_dict={ x: train_x, t: train_t }) というメソッドを呼んでいます。第一引数には#学習アルゴリズムの選定 のところで定義したtrainという変数を、第二引数にはfeed_dictという辞書オブジェクトを指定します。このfeed_dictこそが、プレースホルダーに与える「教師データ」となります。

実行してみる

上述のコードをtrain.pyと名付けて、以下のコマンドを実行しましょう。

python train.py

以下のような出力が得られると思います。(実際の出力は、乱数によって少し変わります。)

Epoch    Training loss   Test loss   Training acc    Test acc
5   1.0601006   1.0601006   0.25    0.25
10  1.0230795   1.0230795   0.25    0.25
15  0.97862685  0.97862685  0.25    0.25
20  0.93293065  0.93293065  0.5 0.5
25  0.8886891   0.8886891   0.5 0.5
30  0.8467394   0.8467394   0.5 0.5
35  0.8071697   0.8071697   0.5 0.5
40  0.7698909   0.7698909   0.5 0.5
45  0.73484254  0.73484254  0.75    0.75
50  0.7020121   0.7020121   0.75    0.75
55  0.67139405  0.67139405  0.75    0.75
60  0.6429507   0.6429507   0.75    0.75
65  0.61659825  0.61659825  0.75    0.75
70  0.59221315  0.59221315  0.75    0.75
75  0.5696492   0.5696492   0.75    0.75
80  0.54875386  0.54875386  0.75    0.75
85  0.52937984  0.52937984  0.75    0.75
90  0.5113907   0.5113907   0.75    0.75
95  0.49466205  0.49466205  0.75    0.75
100 0.47908068  0.47908068  0.75    0.75
105 0.46454394  0.46454394  0.75    0.75
110 0.45095852  0.45095852  0.75    0.75
115 0.43823957  0.43823957  0.75    0.75
120 0.42631024  0.42631024  0.75    0.75
125 0.4151013   0.4151013   0.75    0.75
130 0.40455014  0.40455014  0.75    0.75
135 0.39460057  0.39460057  1.0 1.0
140 0.38520193  0.38520193  1.0 1.0
145 0.3763088   0.3763088   1.0 1.0
150 0.3678801   0.3678801   1.0 1.0
155 0.35987893  0.35987893  1.0 1.0
160 0.35227215  0.35227215  1.0 1.0
165 0.3450296   0.3450296   1.0 1.0
170 0.33812422  0.33812422  1.0 1.0
175 0.33153147  0.33153147  1.0 1.0
180 0.32522893  0.32522893  1.0 1.0
185 0.3191966   0.3191966   1.0 1.0
190 0.31341603  0.31341603  1.0 1.0
195 0.30787042  0.30787042  1.0 1.0
200 0.30254456  0.30254456  1.0 1.0

プロットするとこんな感じです。

f:id:twx:20180821030519p:plain

精度100%で学習できましたね。

大まかな流れは以上となります。

次の記事では、より難しいデータセットで学習する方法や、精度を上げるための様々なテクニックをご紹介する予定です。

Kozuko Mahiro's Hacklog ―― Copyright © 2018 Mahiro Kazuko