Deep Learning入門としてのFizzBuzz問題

FizzBuzz問題とは何か。 こちらから引用させてもらう。

Fizz-Buzz問題の例はこんな感じだ。
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
ちゃんとしたプログラマであれば、これを実行するプログラムを2分とかからずに紙に書き出せるはずだ。怖い事実を聞きたい? コンピュータサイエンス学科卒業生の過半数にはそれができないのだ。自称上級プログラマが答えを書くのに10-15分もかかっているのを見たこともある。

これをDeep Learningで学習したモデルに行わせる。 なぜDeep Learning入門用ベンチマークかと言うと、 教師データの101〜1023とテストデータの0〜100の作成コードが、ラベリングも含めて簡単に実装できるし、 モデルの設計も隠れ層が1つとシンプルなので速く収束し、そこそこ高い精度が出せるから。 データ作成とDeep Learningの実装はTensorFlowコトハジメ Fizz-Buzz問題が詳しい。 以下では同様にTensorFlowを使っているが、実装やパラメーターが異なるし、説明は先のリンクの方が丁寧である。 FizzBuzz問題、DeepLearning、TensorFlowについて、すべて詳しくない方はこちらの記事を読んでおくことをお勧めする。

フロー

main関数の3ステップがフローとなる。

class DLFizzBuzz:
    def main(self):
        # 1. FizzBuzzデータを生成して取得する。
        data = DataFizzBuzz().main()

        # 2. Deep Learnigモデルを設計する。
        model = self.design_model(data)

        # 3. Deep Learningモデルを学習させる。
        self.train_model(data, model)

データ生成

生成コードは以下の通り。 main()を呼び出すと教師データ、教師ラベル、テストデータ、テストラベルをリストで取得できる。

BORDER = 101
NUM_DIGITS = 10

class DataFizzBuzz:
    def main(self):
        # Train
        train_data = np.array([self.binary_encode(i, NUM_DIGITS) for i in range(BORDER, 2**NUM_DIGITS)])
        train_label = np.array([self.fizz_buzz_encode(i) for i in range(BORDER, 2**NUM_DIGITS)])

        # Test
        test_data = np.array([self.binary_encode(i, NUM_DIGITS) for i in range(0, BORDER)])
        test_label = np.array([self.fizz_buzz_encode(i) for i in range(0, BORDER)])

        # Collect
        data = [train_data, train_label, test_data, test_label]

        return data


    def binary_encode(self, i, num_digit):
        binary = np.array([i >> d & 1 for d in range(NUM_DIGITS)])

        return binary


    def fizz_buzz_encode(self, i):
        if i % 15 == 0:
            result = np.array([0, 0, 0, 1])
        elif i % 5 == 0:
            result = np.array([0, 0, 1, 0])
        elif i % 3 == 0:
            result = np.array([0, 1, 0, 0])
        else:
            result = np.array([1, 0, 0, 0])

        return result

モデル設計

教師データは101〜1023なので、range(101, 210)となり、2進数で10桁に収まる。 またテストデータは0〜100だが、とりあえずFizz/Buzz/FizzBuzz/その他が区別できればよいので、4次元ベクトルで表現できる。 そのためノード数は、入力層が10、出力層が4となる。 また隠れ層を100とした場合、モデルの設計は以下のようになる。 なお、重み、バイアス、学習関数等は初歩的なものを用いている。

def design_model(self, data):
    # 入力層
    X  = tf.placeholder(tf.float32, [None, data[0].shape[1]])

    # 隠れ層
    W1 = tf.Variable(tf.random_normal([data[0].shape[1], 100], stddev=0.01))
    B1 = tf.Variable(tf.zeros([100]))
    H1 = tf.nn.relu(tf.matmul(X, W1) + B1)

    # 出力層
    W2 = tf.Variable(tf.random_normal([100, data[1].shape[1]], stddev=0.01))
    B2 = tf.Variable(tf.zeros([data[1].shape[1]]))
    Y = tf.matmul(H1, W2) + B2

    # 正解
    Y_ = tf.placeholder(tf.float32, [None, data[1].shape[1]])

    # 学習関数
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(Y, Y_))
    train_step = tf.train.GradientDescentOptimizer(0.01).minimize(loss)

    model = {'X': X, 'Y': Y, 'Y_': Y_, 'loss': loss, 'train_step': train_step}

    return model

モデル学習

まずデータとモデルを引数として渡し、変数にセットする。

def train_model(self, data, model):
    # dataのセット
    train_X = data[0]
    train_Y = data[1]
    test_X = data[2]
    test_Y = data[3]

    # modelのセット
    X = model['X']
    Y = model['Y']
    Y_ = model['Y_']
    loss = model['loss']
    train_step = model['train_step']

学習中、定期的に精度を検証する。 以下の1行についてはTensorFlowによる精度計算の流れを追うで解説している。

    # 定義
    accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1)), tf.float32))

教師データは101〜1023の923レコードあり、これらをランダマイズして、10レコードずつ学習させる。 この過程を1エポックとして10000回行う。 また100エポックごとにテストを行い、コスト、教師データの精度、テストデータの精度を算出して標準出力させる。

    # 初期化
    sess = tf.InteractiveSession()
    tf.initialize_all_variables().run()

    for epoch in range(10000+1):
        # データのランダマイズ
        p = np.random.permutation(range(len(train_X)))
        train_X, train_Y = train_X[p], train_Y[p]

        # 学習
        for start in range(0, train_X.shape[0], 10):
            end = start + 10
            sess.run(train_step, feed_dict={X: train_X[start:end], Y_: train_Y[start:end]})

        # テスト
        if epoch % 100 == 0:
            # コスト
            lo55 = sess.run(loss, feed_dict={X: train_X, Y_: train_Y})
            # 教師データの精度
            accu_train = sess.run(accuracy, feed_dict={X: train_X, Y_: train_Y})
            # テストデータの精度
            accu_test = sess.run(accuracy, feed_dict={X: test_X, Y_: test_Y})
            # 標準出力
            print 'Epoch: %s, \t Loss: %-8s, \t Train Accracy: %-8s, \t Test Accracy: %-8s' % (epoch, lo55, accu_train, accu_test)

結果と課題

最終的に、コストが0.002、教師データの精度が100%、テストデータの精度が95%となった。 初期の段階では、教師データの精度が94%にも関わらず、テストデータの精度が100%になったが、 その後、過学習が起こり精度が逆転した。 今後はDropoutを実装して過学習を防いだり、パラメーターの最適化を行う。 あと学習曲線も表示させる。

参考文献