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

CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: skflow を使ってディープラーニングで FizzBuzz 問題を解いてみる

最近 TensorFlow を使ってディープラーニングで FizzBuzz 問題を解くっていうブログ記事を読んだんだけど、これが面白かった。

joelgrus.com

そこで、自分でも同じようにディープラーニングを使って 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 問題は分類問題と見なせるので、別に機械学習の分類器なら何でもいけそう