最近 TensorFlow を使ってディープラーニングで FizzBuzz 問題を解くっていうブログ記事を読んだんだけど、これが面白かった。
そこで、自分でも同じようにディープラーニングを使って FizzBuzz 問題を解いてみることにした。 ただし、アレンジとして TensorFlow を直接使うのではなく、代わりに skflow を使ってみる。 skflow というのは TensorFlow を scikit-learn と同じインターフェースで扱えるようにしたラッパーだ。 これなら使い慣れた scikit-learn と同じ雰囲気で TensorFlow を使うことができる。
使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.11.5 BuildVersion: 15F34 $ python --version Python 3.5.1
下準備
まずは TensorFlow と skflow を pip でインストールする。
$ pip install -U https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0-py3-none-any.whl
$ pip install -U git+https://github.com/tensorflow/skflow.git
解いてみる
で、いきなりソースコードなんだけど次のようになった。
#!/usr/bin/env python # -*- coding: utf-8 -*- import skflow from sklearn import metrics import numpy as np def fizzbuzz_encode(n): """整数を FizzBuzz の各答えに対応する整数に変換する""" # FizzBuzz if n % 3 == 0 and n % 5 == 0: return 0 # Fizz if n % 3 == 0: return 1 # Buzz if n % 5 == 0: return 2 return 3 def fizzbuzz_decode(n, prediction): """各答えに対応する整数を文字列の答えに変換する""" mappings = ['FizzBuzz', 'Fizz', 'Buzz', str(n)] return mappings[prediction] def vector_encode(n, digits): """整数を特徴ベクトルに変換する""" return np.array([ # リストの d 番目の要素として n の d ビット目の値を取り出す n >> d & 1 for d in range(digits) ]) def dataset(dataset_len, vector_len, offset=1): """FizzBuzz の入力と答えが含まれるデータセットを作成する""" x = np.array([ vector_encode(n, vector_len) for n in range(offset, dataset_len) ], dtype=np.int32) y = np.array([ fizzbuzz_encode(n) for n in range(offset, dataset_len) ], dtype=np.int32) return x, y def main(): # 特徴ベクトルの次元数 DIGITS = 14 # ホールドアウト検証に使うテストデータ点数 HOLDOUT_LEN = 100 # (2 ^ DIGITS) のサイズでデータセットを作成する x, y = dataset(2 ** DIGITS, DIGITS) # データセットを学習用データと検証用データに分ける train_x, train_y = x[HOLDOUT_LEN:], y[HOLDOUT_LEN:] test_x, test_y = x[:HOLDOUT_LEN], y[:HOLDOUT_LEN] # DNN 分類器 classifier = skflow.TensorFlowDNNClassifier( hidden_units=[100], n_classes=4, batch_size=128, steps=100000, learning_rate=0.05, ) # 学習用データを使って学習する classifier.fit(train_x, train_y) # 検証用データを使った正答率 (汎化性能) を調べる score = metrics.accuracy_score(classifier.predict(test_x), test_y) msg = '正答率: {percent:.2f}%'.format( percent=score * 100, ) print(msg) # ニューラルネットワークの出した答えを表示する vectorizer = np.vectorize(fizzbuzz_decode) answers = classifier.predict(test_x) numbers = np.arange(1, HOLDOUT_LEN + 1) output = vectorizer(numbers, answers) print(output) if __name__ == '__main__': main()
最初のポイントとしては skflow.TensorFlowDNNClassifier かな。 これが scikit-learn でおなじみの API をもった分類器になっている。 具体的には fit() メソッドで学習したり、predict() メソッドで学習した内容を元に分類できたりするところ。
コンセプトは元ネタのブログとほとんど同じ。 FizzBuzz 問題の入力となる特徴ベクトルはビットが入った長さ N のリストで、例えば 1 => [0, ... 0, 0, 1], 2 => [0, ... 0, 1, 0] みたいなかんじになっている。 そして学習データのラベルには FizzBuzz 問題の答えとして FizzBuzz => 0, Fizz => 1, Buzz => 2, それ以外 => 3 という風にしている。 この入力と答えをデータセットとしてあらかじめたくさん用意しておいて、それを学習用データと検証用データに分けて扱っている。
また、最終的に学習には使わなかった 1 ~ 100 の検証用 (ホールドアウト) データを入力したときの汎化性能 (未知のデータに対する対処能力) を調べている。 ついでに学習したニューラルネットワークが出した答えも出力するようにした。
インスパイア元からの変更点は色々とあるけど、パラメータでいえば特徴ベクトルが 10 次元から 14 次元になっていたり、学習ステップ数が 10 倍になっていたりする。 反面、ニューラルネットワークの隠れ層のユニット数なんかは同じ (100) かな。
動かしてみる
早速実行してみよう。 学習に結構な時間がかかる。
$ python fizzbuzz.py Step #99, avg. train loss: 1.19849 Step #200, epoch #1, avg. train loss: 1.14770 Step #300, epoch #2, avg. train loss: 1.14635 Step #400, epoch #3, avg. train loss: 1.14502 Step #500, epoch #3, avg. train loss: 1.14393 ... Step #99500, epoch #777, avg. train loss: 0.10758 Step #99600, epoch #778, avg. train loss: 0.11312 Step #99700, epoch #778, avg. train loss: 0.10891 Step #99800, epoch #779, avg. train loss: 0.10846 Step #99900, epoch #780, avg. train loss: 0.10930 Step #100000, epoch #781, avg. train loss: 0.10990 正答率: 100.00% ['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' 'Fizz' '82' '83' 'Fizz' 'Buzz' '86' 'Fizz' '88' '89' 'FizzBuzz' '91' '92' 'Fizz' '94' 'Buzz' 'Fizz' '97' '98' 'Fizz' 'Buzz']
やったー、元ネタと同じように汎化性能で 100% の精度が得られたー。
これはようするに、色んなデータでニューラルネットワークを学習させたら、教えていないデータについても正しい答えが出せるようになったということ。 こちらは FizzBuzz 問題というものが何なのか一切説明していない。 なのに、ただ色んなデータを突っ込んだら、あたかもそれを理解しているように見えるのは面白いね。
といっても、上記は検証用データの点数を 100 に絞ったとき、たまたま全部当たっていたというだけ。 例えば点数を 1024 まで使うと精度は 97% ほどだった。
$ python fizzbuzz.py Step #99, avg. train loss: 1.20100 Step #200, epoch #1, avg. train loss: 1.14943 Step #300, epoch #2, avg. train loss: 1.14813 Step #400, epoch #3, avg. train loss: 1.14763 Step #500, epoch #4, avg. train loss: 1.13771 ... Step #99500, epoch #829, avg. train loss: 0.09799 Step #99600, epoch #830, avg. train loss: 0.10067 Step #99700, epoch #830, avg. train loss: 0.09853 Step #99800, epoch #831, avg. train loss: 0.10148 Step #99900, epoch #832, avg. train loss: 0.09875 Step #100000, epoch #833, avg. train loss: 0.09879 正答率: 97.07% ['1' '2' 'Fizz' ..., '1022' 'Fizz' '1024']
まあ、ほどほどかな? 問題の難易度に比べると低すぎるような気もする。 学習・検証曲線を見ながら最適なパラメータを選択できれば、もっと上がるのかな。
まとめ
- skflow は TensorFlow のラッパになっていて scikit-learn と同じ API が使えて便利
- ニューラルネットワークが FizzBuzz 問題を理解していくさまを見るのは面白い
- FizzBuzz 問題は分類問題と見なせるので、別に機械学習の分類器なら何でもいけそう
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る