読者です 読者をやめる 読者になる 読者になる

デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理を基礎からやり直す

TensorFlowコトハジメ Fizz-Buzz問題

はじめに

Fizz-Buzz問題
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

ここ最近、通勤中の車の中でFizz-Buzz問題をTensorFlowを使って予測出来るんじゃないかと考えていたのですが、 どうやってモデルを作ればいいのか機械学習の初心者なんで方法が思い付かない。
訓練では下記ソースのように数式を使えますが、予測させるわけですから倍数も剰余も使えない。

for i in range(1,101):
    if i % 15 == 0:
        print 'FizzBuzz'
    elif i % 5 == 0:
        print 'Buzz'
    elif i % 3 == 0:
        print 'Fizz'
    else:
        print i

今日(2016/06/04)、何気に誰かやってないかと「FizzBuzz 機械学習」で検索したら見つけました。
2016/05/23に少しバズってたようですが気が付かなかったです。その後リンク先追加

ソースコード

いつものようにDockerのJupyter Notebook上で動かしたのですが、4000台あたりで固まって最後まで動作しませんでした。 Ubuntu上で「python fizz_buzz.py」とすれば最後まで動作しました。
github.com

再度挑戦、Jupyter Notebook上で動かす際にprintを100単位(if epoch % 100 == 0)に出力するようにしたところ、最後まで動作することが出来ました。

# coding: utf-8
# Fizz Buzz in Tensorflow!
# see http://joelgrus.com/2016/05/23/fizz-buzz-in-tensorflow/

import numpy as np
import tensorflow as tf

NUM_DIGITS = 10

# Represent each input by an array of its binary digits.
def binary_encode(i, num_digits):
    return np.array([i >> d & 1 for d in range(num_digits)])

# One-hot encode the desired outputs: [number, "fizz", "buzz", "fizzbuzz"]
def fizz_buzz_encode(i):
    if   i % 15 == 0: return np.array([0, 0, 0, 1])
    elif i % 5  == 0: return np.array([0, 0, 1, 0])
    elif i % 3  == 0: return np.array([0, 1, 0, 0])
    else:             return np.array([1, 0, 0, 0])

# Our goal is to produce fizzbuzz for the numbers 1 to 100. So it would be
# unfair to include these in our training data. Accordingly, the training data
# corresponds to the numbers 101 to (2 ** NUM_DIGITS - 1).
trX = np.array([binary_encode(i, NUM_DIGITS) for i in range(101, 2 ** NUM_DIGITS)])
trY = np.array([fizz_buzz_encode(i)          for i in range(101, 2 ** NUM_DIGITS)])

# We'll want to randomly initialize weights.
def init_weights(shape):
    return tf.Variable(tf.random_normal(shape, stddev=0.01))

# Our model is a standard 1-hidden-layer multi-layer-perceptron with ReLU
# activation. The softmax (which turns arbitrary real-valued outputs into
# probabilities) gets applied in the cost function.
def model(X, w_h, w_o):
    h = tf.nn.relu(tf.matmul(X, w_h))
    return tf.matmul(h, w_o)

# Our variables. The input has width NUM_DIGITS, and the output has width 4.
X = tf.placeholder("float", [None, NUM_DIGITS])
Y = tf.placeholder("float", [None, 4])

# How many units in the hidden layer.
NUM_HIDDEN = 100

# Initialize the weights.
w_h = init_weights([NUM_DIGITS, NUM_HIDDEN])
w_o = init_weights([NUM_HIDDEN, 4])

# Predict y given x using the model.
py_x = model(X, w_h, w_o)

# We'll train our model by minimizing a cost function.
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)

# And we'll make predictions by choosing the largest output.
predict_op = tf.argmax(py_x, 1)

# Finally, we need a way to turn a prediction (and an original number)
# into a fizz buzz output
def fizz_buzz(i, prediction):
    return [str(i), "fizz", "buzz", "fizzbuzz"][prediction]

BATCH_SIZE = 128

# Launch the graph in a session
with tf.Session() as sess:
    tf.initialize_all_variables().run()

    for epoch in range(10000):
        # Shuffle the data before each training iteration.
        p = np.random.permutation(range(len(trX)))
        trX, trY = trX[p], trY[p]

        # Train in batches of 128 inputs.
        for start in range(0, len(trX), BATCH_SIZE):
            end = start + BATCH_SIZE
            sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end]})

        # And print the current accuracy on the training data.
        if epoch % 100 == 0:
            print(epoch, np.mean(np.argmax(trY, axis=1) ==
                             sess.run(predict_op, feed_dict={X: trX, Y: trY})))

    # And now for some fizz buzz
    numbers = np.arange(1, 101)
    teX = np.transpose(binary_encode(numbers, NUM_DIGITS))
    teY = sess.run(predict_op, feed_dict={X: teX})
    output = np.vectorize(fizz_buzz)(numbers, teY)

    print(output)

出力結果

(0, 0.53196099674972919)
(100, 0.53412784398699886)
(200, 0.53412784398699886)
(300, 0.53412784398699886)
(400, 0.53412784398699886)
(500, 0.53412784398699886)
  ︙
(9500, 1.0)
(9600, 1.0)
(9700, 1.0)
(9800, 1.0)
(9900, 1.0)
['1' '2' 'fizz' '4' 'buzz' 'fizz' '7' '8' 'fizz' 'buzz' '11' 'fizz' '13'
 '14' 'fizz' '16' '17' 'fizz' '19' 'buzz' 'fizz' '22' '23' 'fizz' 'buzz'
 '26' 'fizz' '28' '29' 'fizzbuzz' '31' '32' '33' '34' 'buzz' 'fizz' '37'
 'fizz' 'fizz' 'buzz' '41' 'fizz' '43' '44' 'fizzbuzz' '46' '47' 'fizz'
 '49' 'buzz' 'fizz' '52' '53' 'fizz' 'buzz' '56' 'fizz' '58' '59'
 'fizzbuzz' '61' '62' 'fizz' '64' 'buzz' 'fizz' '67' 'fizz' '69' 'buzz'
 '71' 'fizz' '73' '74' 'fizzbuzz' '76' '77' 'fizz' '79' '80' '81' '82' '83'
 'fizz' 'buzz' '86' '87' '88' '89' 'fizzbuzz' '91' '92' 'fizz' '94' 'buzz'
 '96' '97' '98' 'fizz' 'buzz']

幾つか不正解してますね、15が'fizz'だったり。

NUM_DIGITS = 10→14に変更するだけでも下記結果の通り精度があがります。

['1' '2' 'fizz' '4' 'buzz' 'fizz' '7' '8' 'fizz' 'buzz' '11' 'fizz' '13'
 '14' 'fizzbuzz' '16' '17' 'fizz' '19' 'buzz' 'fizz' '22' '23' 'fizz'
 'buzz' '26' 'fizz' '28' '29' 'fizzbuzz' '31' '32' 'fizz' '34' 'buzz'
 'fizz' '37' '38' 'fizz' 'buzz' '41' 'fizz' '43' '44' 'fizzbuzz' '46' '47'
 'fizz' '49' 'buzz' 'fizz' '52' '53' 'fizz' 'buzz' '56' 'fizz' '58' '59'
 'fizzbuzz' '61' '62' 'fizz' '64' 'buzz' 'fizz' '67' '68' 'fizz' 'buzz'
 '71' 'fizz' '73' '74' 'fizzbuzz' '76' '77' 'fizz' '79' 'buzz' '81' '82'
 '83' 'fizz' 'buzz' '86' 'fizz' '88' '89' 'fizzbuzz' '91' '92' 'fizz' '94'
 'buzz' '96' '97' '98' 'fizz' 'buzz']

仕組み

It would be cheating to use the numbers 1 to 100 in our training data, so let's train it on all the remaining numbers up to 1024:

学習用データに1から100までの数値を使うことは不正行為とのことで、101から1023までのデータで学習したニューラルネットワークに対して、1から100までの答えの予測を出力するプログラムになっている。

多クラス識別問題

手書き文字認識(MNIST)による多クラス識別問題の時に10クラス(0-9)に分けましたが、今回は4クラスに分けます。
one-hot ベクトル (one-of-K表現) f:id:Yaju3D:20160604231742p:plain

# One-hot encode the desired outputs: [number, "fizz", "buzz", "fizzbuzz"]
def fizz_buzz_encode(i):
    if   i % 15 == 0: return np.array([0, 0, 0, 1])
    elif i % 5  == 0: return np.array([0, 0, 1, 0])
    elif i % 3  == 0: return np.array([0, 1, 0, 0])
    else:             return np.array([1, 0, 0, 0])

入力値と正解値の準備

入力値となる特徴ベクトル「trX」と正解値の「trY」の配列を、101~210まで用意します。
手書き文字認識(MNIST)の入力値は白黒画像(28x28ピクセル=784)でした、Fizz-Buzz問題の入力値は数値をバイナリ変換することである種の白黒画像にするってことになりますね。

NUM_DIGITS  =  10 

# Our goal is to produce fizzbuzz for the numbers 1 to 100. So it would be
# unfair to include these in our training data. Accordingly, the training data
# corresponds to the numbers 101 to (2 ** NUM_DIGITS - 1).
trX = np.array([binary_encode(i, NUM_DIGITS) for i in range(101, 2 ** NUM_DIGITS)])
trY = np.array([fizz_buzz_encode(i)          for i in range(101, 2 ** NUM_DIGITS)])
入力値のtrXは101~1023までの数値のバイナリ表現

trX=
[[1 0 1 ..., 0 0 0] <<101の2進数(0000110101)
 [0 1 1 ..., 0 0 0] <<102の2進数(0000110110)
 [1 1 1 ..., 0 0 0]
 ..., 
 [1 0 1 ..., 1 1 1]
 [0 1 1 ..., 1 1 1]
 [1 1 1 ..., 1 1 1]] <<1023の2進数((1111111111))


正解値のtrYは101~1023までのfizzbuzz値のバイナリ表現
trY= 
[[1 0 0 0] <<101:5でも3でも割り切れない
 [0 1 0 0] <<102:3で割り切れる
 [1 0 0 0]
 ..., 
 [1 0 0 0]
 [1 0 0 0]
 [0 1 0 0]] <<1023:3で割り切れる

ニューラルネット

入力層:10(NUM_DIGITS)ノード, 隠れ層:100(NUM_HIDDEN)ノード, 出力層:4ノード f:id:Yaju3D:20160605163528p:plain

ノードの作成

入力ノードと出力ノードはplaceholderという名前の関数で構築されます.第一引数は型,第二引数は配列の形状です。

# Our variables. The input has width NUM_DIGITS, and the output has width 4.
X = tf.placeholder("float", [None, NUM_DIGITS])
Y = tf.placeholder("float", [None, 4])

ノード重みの作成

モデルのパラメーターとなる結合重み(ノードの間をつなぐ重み)はVariables関数を利用して領域を確保します。
ニューラルネットワークで学習を行う際、ネットワークの各重みの初期値は乱数により決定することが一般的となっている。
重み初期化:正規分布(ガウス分布)による乱数で初期化(平均 0.0, 分散 0.01)

def init_weights(shape):
    return tf.Variable(tf.random_normal(shape, stddev=0.01))

# Initialize the weights.
# モデルパラメータ(入力層:10ノード, 隠れ層:100ノード, 出力層:4ノード)
w_h = init_weights([NUM_DIGITS, NUM_HIDDEN])
w_o = init_weights([NUM_HIDDEN, 4])

予測式(モデル)を定義

def model(X, w_h, w_o):
    h = tf.nn.relu(tf.matmul(X, w_h))
    return tf.matmul(h, w_o)

# Predict y given x using the model.
# 予測式(モデル)
py_x = model(X, w_h, w_o)

入力した値を0から1の間に収めてくれる関数として八百屋で識別問題の際にシグモイド曲線を使いました。 今回は隠れ層の活性化関数としてReLU(ランプ関数)を使用します。 f:id:Yaju3D:20160605005559p:plain
プラスならどこでも一定の勾配になります。中間層で0未満の値を0にするいうことでモデルの精度を上げる目的。

qiita.com なんでシグモイド曲線ではなく、ReLU使っているのか分かるかも知れません。

誤差関数と最適化手法を記述

tf.reduce_mean()は、平均を計算する関数。

# We'll train our model by minimizing a cost function.
# 誤差関数(loss)
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
# 最適化手段(最急降下法)
train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)
  • 誤差関数を変更
    多クラス識別(分類)問題
    →多クラス用クロスエントロピー(softmax_cross_entropy_with_logits)

モデルの評価

tf.argmaxは、いくつかの軸に沿ったテンソルで最大値となるインデックスを一つ返す。

# And we'll make predictions by choosing the largest output.
# 1に一番近いインデックス(予測)が正解とあっているか検証
predict_op = tf.argmax(py_x, 1)

fizz-buzz値の出力

predict_opのインデックスからfizz-buzz値を出力する。

# Finally, we need a way to turn a prediction (and an original number)
# into a fizz buzz output
def fizz_buzz(i, prediction):
    return [str(i), "fizz", "buzz", "fizzbuzz"][prediction]

セッションを準備し,変数を初期化

一般的な初期化処理

BATCH_SIZE = 128

# Launch the graph in a session
with tf.Session() as sess:
    tf.initialize_all_variables().run()

訓練開始

10000回繰り返す。 トレーニングデータは順番に並んでいるが、それでは面白くないのでランダムに並び替えている。
np.random.permutationは、配列をランダムに並び替える(多次元配列のときは、第1軸の方向だけでランダム入れ替え)

ミニバッチのバッチサイズ(BATCH_SIZE)を「128」としています。
利用した学習データに対して更新を終えるのが、1エポックというサイクルになります。通常は、このエポックを何回か繰り返して学習していきます。 ただ、単純に繰り返しているとあまりよろしくないので、エポックの度に学習データをシャッフルしたり、ミニバッチの場合はミニバッチの取得位置をずらしたりランダムにサンプリングしたりします。
len(trX)は、1024-101=923なので、startの値は(0,128,256,384,512,640,768,896)となります。

    for epoch in range(10000):
        # Shuffle the data before each training iteration.
        p = np.random.permutation(range(len(trX)))
        trX, trY = trX[p], trY[p]

        # Train in batches of 128 inputs.
        for start in range(0, len(trX), BATCH_SIZE):
            end = start + BATCH_SIZE
            sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end]})

学習データの出力

np.meanは、平均を計算する関数、sess.runには実行結果の値が入ります。
np.mean(1==1)は「1.0」、np.mean(1==0)は「0.0」となります。

        # And print the current accuracy on the training data.
        if epoch % 100 == 0:
            print(epoch, np.mean(np.argmax(trY, axis=1) ==
                             sess.run(predict_op, feed_dict={X: trX, Y: trY})))

※Jupyter Notebook上で動かす際にprintを100単位としました。

予測出力

101から1023までのデータで学習した結果から、1から100までの答えの予測を出力する。

np.transposeは、転置行列にします。
numpy.vectorizeは配列を引数に取って配列として返す関数を作るもの。

    # And now for some fizz buzz
    numbers = np.arange(1, 101)
    teX = np.transpose(binary_encode(numbers, NUM_DIGITS))
    teY = sess.run(predict_op, feed_dict={X: teX})
    output = np.vectorize(fizz_buzz)(numbers, teY)

    print(output)

np.transposeのサンプル

numbers = np.arange(1, 4)
print(np.transpose(binary_encode(numbers, NUM_DIGITS)))

[[1 0 0 0 0 0 0 0 0 0]
 [0 1 0 0 0 0 0 0 0 0]
 [1 1 0 0 0 0 0 0 0 0]]

最後に

処理を分解してみて何となくやっていることは分かりましたが、まだまだしっくりとはいっていません。
基礎的な理解がまだ足りていないので、分かったら記事を修正していきます。
ディープラーニング入門としては、Fizz-Buzz問題はいいと思います。