[TensorFlowで株価予想] 0 - Google のサンプルコードを動かしてみる

  • 1057
    いいね
  • 14
    コメント

TensorFlowで株価予想シリーズ

前置き

猫も杓子もディープラーニングディープラーニング。なにそれ美味いの? って感じだけど、 2015年末に Google が書いた 「Machine Learning with Financial Time Series Data on Google Cloud Platform」 はとても美味しそうだ。Google がリリースした TensorFlow というディープラーニングツールで株価を予想しちゃうんだって!

株価といっても世界中の株価指標を使ってS&P500というアメリカの株価指標が「上がるの?」「下がるの?」の2択を予想するもんらしい。株価指標ってのは日本でいうところの日経平均というやつで、複数の企業の株価を平均して値を出すもの。そしてヨーロッパの株価指数がどうだった?中国の株価指標がどうだった?じゃぁその後のアメリカの株価指標はこうなる!と予想する。

で、 Google さんいわくこんな結果が出たそうだ。

Precision =  0.775862068966
Recall =  0.625
F1 Score =  0.692307692308
Accuracy =  0.722222222222

Accuracy ってのが正解率で、72%で当たり外れを当てれるんだそうだ。Google すごいな。これ社内運用してるでしょ。経常利益をこれで増やしてるでしょ。

ちなみに50%だと半分正解して半分外れるということになるけど、2択問題なのでそれはつまり全く当たらないという意味になる。50%から100%に近づけば予測が当たっているということだ。

50%からの離脱をしよう。めざせ100%。そこに美味しい何かが待っている。

実装コード

で、妻の ento に英語ドキュメントからコードを実装してもらい、それを正しく動作するように修正したコードがこちら(github)


# -*- coding: utf-8 -*-
'''
Code based on:
https://github.com/corrieelston/datalab/blob/master/FinancialTimeSeriesTensorFlow.ipynb
'''
from __future__ import print_function

import datetime
import urllib2
from os import path
import operator as op
from collections import namedtuple
import numpy as np
import pandas as pd
import tensorflow as tf


DAYS_BACK = 3
FROM_YEAR = '1991'
EXCHANGES_DEFINE = [
    #['DOW', '^DJI'],
    ['FTSE', '^FTSE'],
    ['GDAXI', '^GDAXI'],
    ['HSI', '^HSI'],
    ['N225', '^N225'],
    #['NASDAQ', '^IXIC'],
    ['SP500', '^GSPC'],
    ['SSEC', '000001.SS'],
]
EXCHANGES_LABEL = [exchange[0] for exchange in EXCHANGES_DEFINE]

Dataset = namedtuple(
    'Dataset',
    'training_predictors training_classes test_predictors test_classes')
Environ = namedtuple('Environ', 'sess model actual_classes training_step dataset feature_data')


def setupDateURL(urlBase):
    now = datetime.date.today()
    return urlBase.replace('__FROM_YEAR__', FROM_YEAR)\
            .replace('__TO_MONTH__', str(now.month - 1))\
            .replace('__TO_DAY__', str(now.day))\
            .replace('__TO_YEAR__', str(now.year))


def fetchCSV(fileName, url):
    if path.isfile(fileName):
        print('fetch CSV for local: ' + fileName)
        with open(fileName) as f:
            return f.read()
    else:
        print('fetch CSV for url: ' + url)
        csv = urllib2.urlopen(url).read()
        with open(fileName, 'w') as f:
            f.write(csv)
        return csv


def fetchYahooFinance(name, code):
    fileName = 'index_%s.csv' % name
    url = setupDateURL('http://chart.finance.yahoo.com/table.csv?s=%s&a=0&b=1&c=__FROM_YEAR__&d=__TO_MONTH__&e=__TO_DAY__&f=__TO_YEAR__&g=d&ignore=.csv' % code)
    csv = fetchCSV(fileName, url)


def fetchStockIndexes():
    '''株価指標のデータをダウンロードしファイルに保存
    '''
    for exchange in EXCHANGES_DEFINE:
        fetchYahooFinance(exchange[0], exchange[1])


def load_exchange_dataframes():
    '''EXCHANGESに対応するCSVファイルをPandasのDataFrameとして読み込む。
    Returns:
        {EXCHANGES[n]: pd.DataFrame()}
    '''
    return {exchange: load_exchange_dataframe(exchange)
            for exchange in EXCHANGES_LABEL}


def load_exchange_dataframe(exchange):
    '''exchangeに対応するCSVファイルをPandasのDataFrameとして読み込む。
    Args:
        exchange: 指標名
    Returns:
        pd.DataFrame()
    '''
    return pd.read_csv('index_{}.csv'.format(exchange)).set_index('Date').sort_index()


def get_closing_data(dataframes):
    '''各指標の終値カラムをまとめて1つのDataFrameに詰める。
    Args:
        dataframes: {key: pd.DataFrame()}
    Returns:
        pd.DataFrame()
    '''
    closing_data = pd.DataFrame()
    for exchange, dataframe in dataframes.items():
        closing_data[exchange] = dataframe['Close']
    closing_data = closing_data.fillna(method='ffill')
    return closing_data


def get_log_return_data(closing_data):
    '''各指標について、終値を1日前との比率の対数をとって正規化する。
    Args:
        closing_data: pd.DataFrame()
    Returns:
        pd.DataFrame()
    '''

    log_return_data = pd.DataFrame()
    for exchange in closing_data:
        # np.log(当日終値 / 前日終値) で前日からの変化率を算出
        # 前日よりも上がっていればプラス、下がっていればマイナスになる
        log_return_data[exchange] = np.log(closing_data[exchange]/closing_data[exchange].shift())

    return log_return_data


def build_training_data(log_return_data, target_exchange, max_days_back=DAYS_BACK, use_subset=None):
    '''学習データを作る。分類クラスは、target_exchange指標の終値が前日に比べて上ったか下がったかの2つである。
    また全指標の終値の、当日から数えてmax_days_back日前までを含めて入力データとする。
    Args:
        log_return_data: pd.DataFrame()
        target_exchange: 学習目標とする指標名
        max_days_back: 何日前までの終値を学習データに含めるか
        use_subset (float): 短時間で動作を確認したい時用: log_return_dataのうち一部だけを学習データに含める
    Returns:
        pd.DataFrame()
    '''
    # 「上がる」「下がる」の結果を計算
    columns = []
    for colname, exchange, operator in iter_categories(target_exchange):
        columns.append(colname)
        # 全ての XXX_positive, XXX_negative を 0 に初期化
        log_return_data[colname] = 0
        # XXX_positive の場合は >=  0 の全てのインデックスを
        # XXX_negative の場合は < 0 の全てのインデックスを取得し、それらに 1 を設定する
        indices = operator(log_return_data[exchange], 0)
        log_return_data.ix[indices, colname] = 1

    num_categories = len(columns)

    # 各指標のカラム名を追加
    for colname, _, _ in iter_exchange_days_back(target_exchange, max_days_back):
        columns.append(colname)

    '''
    columns には計算対象の positive, negative と各指標の日数分のラベルが含まれる
    例:[
        'SP500_positive',
        'SP500_negative',
        'DOW_0',
        'DOW_1',
        'DOW_2',
        'FTSE_0',
        'FTSE_1',
        'FTSE_2',
        'GDAXI_0',
        'GDAXI_1',
        'GDAXI_2',
        'HSI_0',
        'HSI_1',
        'HSI_2',
        'N225_0',
        'N225_1',
        'N225_2',
        'NASDAQ_0',
        'NASDAQ_1',
        'NASDAQ_2',
        'SP500_1',
        'SP500_2',
        'SP500_3',
        'SSEC_0',
        'SSEC_1',
        'SSEC_2'
    ]
    計算対象の SP500 だけ当日のデータを含めたらダメなので1〜3が入る
    '''

    # データ数をもとめる
    max_index = len(log_return_data) - max_days_back
    if use_subset is not None:
        # データを少なくしたいとき
        max_index = int(max_index * use_subset)

    # 学習データを作る
    training_test_data = pd.DataFrame(columns=columns)
    for i in range(max_days_back + 10, max_index):
        # 先頭のデータを含めるとなぜか上手くいかないので max_days_back + 10 で少し省く
        values = {}
        # 「上がる」「下がる」の答を入れる
        for colname, _, _ in iter_categories(target_exchange):
            values[colname] = log_return_data[colname].ix[i]
        # 学習データを入れる
        for colname, exchange, days_back in iter_exchange_days_back(target_exchange, max_days_back):
            values[colname] = log_return_data[exchange].ix[i - days_back]
        training_test_data = training_test_data.append(values, ignore_index=True)

    return num_categories, training_test_data


def iter_categories(target_exchange):
    '''分類クラス名とその値を計算するためのオペレーター関数を列挙する。
    '''
    for polarity, operator in [
            ('positive', op.ge), # >=
            ('negative', op.lt), # <
    ]:
        colname = '{}_{}'.format(target_exchange, polarity)
        yield colname, target_exchange, operator


def iter_exchange_days_back(target_exchange, max_days_back):
    '''指標名、何日前のデータを読むか、カラム名を列挙する。
    '''
    for exchange in EXCHANGES_LABEL:
        # SP500 の結果を予測するのに SP500 の当日の値が含まれてはいけないので1日づらす
        start_days_back = 1 if exchange == target_exchange else 0
        #start_days_back = 1 # N225 で行う場合は全て前日の指標を使うようにする
        end_days_back = start_days_back + max_days_back
        for days_back in range(start_days_back, end_days_back):
            colname = '{}_{}'.format(exchange, days_back)
            yield colname, exchange, days_back


def split_training_test_data(num_categories, training_test_data):
    '''学習データをトレーニング用とテスト用に分割する。
    '''
    # 先頭2つより後ろが学習データ
    predictors_tf = training_test_data[training_test_data.columns[num_categories:]]
    # 先頭2つが「上がる」「下がる」の答えデータ
    classes_tf = training_test_data[training_test_data.columns[:num_categories]]

    # 学習用とテスト用のデータサイズを求める
    training_set_size = int(len(training_test_data) * 0.8)
    test_set_size = len(training_test_data) - training_set_size

    # 古いデータ0.8を学習とし、新しいデータ0.2がテストとなる
    return Dataset(
        training_predictors=predictors_tf[:training_set_size],
        training_classes=classes_tf[:training_set_size],
        test_predictors=predictors_tf[training_set_size:],
        test_classes=classes_tf[training_set_size:],
    )


def tf_confusion_metrics(model, actual_classes, session, feed_dict):
    '''与えられたネットワークの正解率などを出力する。
    '''
    predictions = tf.argmax(model, 1)
    actuals = tf.argmax(actual_classes, 1)

    ones_like_actuals = tf.ones_like(actuals)
    zeros_like_actuals = tf.zeros_like(actuals)
    ones_like_predictions = tf.ones_like(predictions)
    zeros_like_predictions = tf.zeros_like(predictions)

    tp_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
                tf.equal(actuals, ones_like_actuals),
                tf.equal(predictions, ones_like_predictions)
            ),
            "float"
        )
    )

    tn_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
                tf.equal(actuals, zeros_like_actuals),
                tf.equal(predictions, zeros_like_predictions)
            ),
            "float"
        )
    )

    fp_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
                tf.equal(actuals, zeros_like_actuals),
                tf.equal(predictions, ones_like_predictions)
            ),
            "float"
        )
    )

    fn_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
                tf.equal(actuals, ones_like_actuals),
                tf.equal(predictions, zeros_like_predictions)
            ),
            "float"
        )
    )

    tp, tn, fp, fn = session.run(
        [tp_op, tn_op, fp_op, fn_op],
        feed_dict
    )

    tpr = float(tp)/(float(tp) + float(fn))
    fpr = float(fp)/(float(tp) + float(fn))

    accuracy = (float(tp) + float(tn))/(float(tp) + float(fp) + float(fn) + float(tn))

    recall = tpr
    if (float(tp) + float(fp)):
        precision = float(tp)/(float(tp) + float(fp))
        f1_score = (2 * (precision * recall)) / (precision + recall)
    else:
        precision = 0
        f1_score = 0

    print('Precision = ', precision)
    print('Recall = ', recall)
    print('F1 Score = ', f1_score)
    print('Accuracy = ', accuracy)


def simple_network(dataset):
    '''単純な分類モデルを返す。
    '''
    sess = tf.Session()

    # Define variables for the number of predictors and number of classes to remove magic numbers from our code.
    num_predictors = len(dataset.training_predictors.columns)
    num_classes = len(dataset.training_classes.columns)

    # Define placeholders for the data we feed into the process - feature data and actual classes.
    feature_data = tf.placeholder("float", [None, num_predictors])
    actual_classes = tf.placeholder("float", [None, num_classes])

    # Define a matrix of weights and initialize it with some small random values.
    weights = tf.Variable(tf.truncated_normal([num_predictors, num_classes], stddev=0.0001))
    biases = tf.Variable(tf.ones([num_classes]))

    # Define our model...
    # Here we take a softmax regression of the product of our feature data and weights.
    model = tf.nn.softmax(tf.matmul(feature_data, weights) + biases)

    # Define a cost function (we're using the cross entropy).
    cost = -tf.reduce_sum(actual_classes * tf.log(model))

    # Define a training step...
    # Here we use gradient descent with a learning rate of 0.01 using the cost function we just defined.
    training_step = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)

    init = tf.initialize_all_variables()
    sess.run(init)

    return Environ(
        sess=sess,
        model=model,
        actual_classes=actual_classes,
        training_step=training_step,
        dataset=dataset,
        feature_data=feature_data,
    )


def smarter_network(dataset):
    '''隠しレイヤー入りのもうちょっと複雑な分類モデルを返す。
    '''
    sess = tf.Session()

    num_predictors = len(dataset.training_predictors.columns)
    num_classes = len(dataset.training_classes.columns)

    feature_data = tf.placeholder("float", [None, num_predictors])
    actual_classes = tf.placeholder("float", [None, num_classes])

    weights1 = tf.Variable(tf.truncated_normal([(DAYS_BACK * len(EXCHANGES_DEFINE)), 50], stddev=0.0001))
    biases1 = tf.Variable(tf.ones([50]))

    weights2 = tf.Variable(tf.truncated_normal([50, 25], stddev=0.0001))
    biases2 = tf.Variable(tf.ones([25]))

    weights3 = tf.Variable(tf.truncated_normal([25, 2], stddev=0.0001))
    biases3 = tf.Variable(tf.ones([2]))

    # This time we introduce a single hidden layer into our model...
    hidden_layer_1 = tf.nn.relu(tf.matmul(feature_data, weights1) + biases1)
    hidden_layer_2 = tf.nn.relu(tf.matmul(hidden_layer_1, weights2) + biases2)
    model = tf.nn.softmax(tf.matmul(hidden_layer_2, weights3) + biases3)

    cost = -tf.reduce_sum(actual_classes*tf.log(model))

    training_step = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)

    init = tf.initialize_all_variables()
    sess.run(init)

    return Environ(
        sess=sess,
        model=model,
        actual_classes=actual_classes,
        training_step=training_step,
        dataset=dataset,
        feature_data=feature_data,
    )


def train(env, steps=30000, checkin_interval=5000):
    '''学習をsteps回おこなう。
    '''
    correct_prediction = tf.equal(
        tf.argmax(env.model, 1),
        tf.argmax(env.actual_classes, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

    for i in range(1, 1 + steps):
        env.sess.run(
            env.training_step,
            feed_dict=feed_dict(env, test=False),
        )
        if i % checkin_interval == 0:
            print(i, env.sess.run(
                accuracy,
                feed_dict=feed_dict(env, test=False),
            ))

    tf_confusion_metrics(env.model, env.actual_classes, env.sess, feed_dict(env, True))


def feed_dict(env, test=False):
    '''学習/テストに使うデータを生成する。
    '''
    prefix = 'test' if test else 'training'
    predictors = getattr(env.dataset, '{}_predictors'.format(prefix))
    classes = getattr(env.dataset, '{}_classes'.format(prefix))
    return {
        env.feature_data: predictors.values,
        env.actual_classes: classes.values.reshape(len(classes.values), len(classes.columns))
    }


def main(args):
    print('株価指標データをダウンロードしcsvファイルに保存')
    fetchStockIndexes()
    print('株価指標データを読み込む')
    all_data  = load_exchange_dataframes()
    print('終値を取得')
    closing_data = get_closing_data(all_data)
    print('データを学習に使える形式に正規化')
    log_return_data = get_log_return_data(closing_data)
    print('答と学習データを作る')
    num_categories, training_test_data = build_training_data(
        log_return_data, args.target_exchange,
        use_subset=args.use_subset)
    print('学習データをトレーニング用とテスト用に分割する')
    dataset = split_training_test_data(num_categories, training_test_data)

    print('器械学習のネットワークを作成')
    #env = simple_network(dataset)
    env = smarter_network(dataset)

    if args.inspect:
        import code
        print('Press Ctrl-d to proceed')
        code.interact(local=locals())

    print('学習')
    train(env, steps=args.steps, checkin_interval=args.checkin)


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('target_exchange', choices=EXCHANGES_LABEL)
    parser.add_argument('--steps', type=int, default=10000)
    parser.add_argument('--checkin', type=int, default=1000)
    parser.add_argument('--use-subset', type=float, default=None)
    parser.add_argument('--inspect', type=bool, default=False)

    args = parser.parse_args()

    main(args)

解説

解説はとりあえずは Google が書いた記事(「Machine Learning with Financial Time Series Data on Google Cloud Platform」)とコード内のコメントに任せたいと思います。 ほんとごめんね。

データ操作に Pandas を使っているけど、「Python Pandasでのデータ操作の初歩まとめ − 前半:データ作成&操作編」が参考になると思いま

実行

以下のコマンドでライブラリなどをインストールして

$ virtualenv --python=/usr/local/bin/python2.7 .pyenv
$ . .pyenv/bin/activate
$ pip install -r requirements.txt
$ pip install https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0-py2-none-any.whl

これで実行

$ python goognet.py SP500
...
1000 0.546525
2000 0.570082
3000 0.81331
4000 0.887809
5000 0.916667
6000 0.927856
7000 0.93404
8000 0.936396
9000 0.935807
10000 0.936396
Precision =  0.966005665722
Recall =  0.888020833333
F1 Score =  0.925373134328
Accuracy =  0.935217903416

0.93、、、93%の正解率。。。

んなアホな。そんな正解率高いわけないだろ。。。

おねがい

データの確認をして問題なさそうだとは思った。でも、さすがにこの正解率は高すぎるだろう。S&P500と市場が開いている時間が近いDOWとNASDAQを省い(EXCHANGES_DEFINEでの2行をコメントアウトして実行)ても72%の正解率だ。 `$ python goognet.py N225` と 日経225 の結果を得ると 65% の正解率。(N225で実行する場合は226行目と227行目のコードを入れ替える必要がある) 結果がいいので実際に運用してみようと思うけど、誰か間違いに気づいたらこっそり教えてください。m(_ _)m

補足

そんなに高いわけがなかった!DOWとNASDAQはSP500と同じ時間に開かれていて、学習データに含めることはそもそも不可能。同じ時間なので影響も受け合うから、ほぼ答が学習データに含まれていたため93%になったと思われる。再計算したところ73%でした。