機械学習で仮想通貨を予測してみた!

f:id:masaruyagi:20180824234754j:plain

こんにちは、Aidemy研修生の八木です。

皆さんは楽してお金が欲しいと思ったことはないでしょうか?

もし仮想通貨の未来の価格が予想できたら一生遊んで暮らせそうですよね。

そこで、今回はAidemyで学んだ時系列解析を生かし、仮想通貨の予測に挑戦してみました!

また、データの加工でどのように予測が変わるか検証してみました。

対象者

  • 株、FX、仮想通貨などに興味がある方
  • それらを機械学習で予測してみたい方

概要

BCHのチャートデータをPoloniexのAPIから取得。

kerasのLSTMモデルを構築し、BCHの回帰予測を行う。

その結果から次の時間の騰落予測し、正答確率を算出する。

目次

python構築環境

  • MacBook Air (プロセッサ:  2.2 GHz Intel Core i7)
  • Python3.6.0
  • Jupyter4.4.0

また、以下のモジュールを使用しました。


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import poloniex
import time

#TensorFlow
import tensorflow as tf

#Keras
from __future__ import print_function
from keras.layers.core import Activation
from keras.layers.core import Dense
from keras.layers.core import Dropout
from keras.models import Sequential
from keras.optimizers import Adam

#LSTM
from keras.layers.recurrent import LSTM
from keras.callbacks import EarlyStopping

データの取得

まず、通貨価格の価格データを取得するのですが、取引所であるPoloniexがAPIを公開しているためそこからデータを取得することができます。

また、それをPython でさらに使いやすくしたpython-poloniexがGitHubで公開されていて、今回はそれを利用しました。

python-poloniexの導入は以下のようにできます。

  1. GitHubのサイトからzipファイルをダウンロード
    github.com
  2. ダウンロードしたzipを解凍し、ファイル内で以下を実行
    python setup.py install

これでpython-poloniexを使うことができます。

では実際にpoloniexを使いデータを取得して行きます。

今回は、BCH(ビットコインキャッシュ)の4時間おきの価格を500日分取得しました。

BCHを選んだ理由は昔買ったことがあるからです(笑)

データの日数は実際にチャートを見て適当な範囲で決定しました。


polo = poloniex.Poloniex()

chart_data = polo.returnChartData('USDT_BCH', period= 7200, start=time.time()-polo.DAY*500, end=time.time())  #データ取得
df = pd.DataFrame(chart_data)  #dataframe化

polo.returnCharatDataには、通貨ペア、periodに取得間隔、startに開始時刻、endに終了時刻を指定します。

また、polo.DAYで1日分を指定できます。

取得したデータはpandasのdataframeに変換して扱います。

では次に、取得したデータをプロットして正しく取得できているかを確認してみます。


data = df["close"].astype("float32")

#グラフ設定
rcParams['figure.figsize'] = 15, 6
plt.title("BCH_Chart")
plt.xlabel("date")
plt.ylabel("price")
plt.grid(True)

#プロット
plt.plot(data)
plt.show()

f:id:masaruyagi:20180821132724p:plain

正しくプロットすることができました!

データの加工

次にデータを加工し学習しやすいようにしていきます。

時系列データでは移動平均を求めることによってグラフ滑らかにすることがよく行われます。

今回は4時間おきでデータを集めたため、6つ分のデータで平均を取り1日幅での移動平均を求めました。


#移動平均
data1 = df["close"].rolling(6).mean()
data1 = data1.fillna(data1[5])

#プロット
plt.plot(data)
plt.plot(data1)

plt.grid(True)
plt.show()

6つ毎のデータで移動平均を取ると初めの5つの値が空値になってしまうため、最も近い値で埋めることでデータの長さが変わらないようにしています。

f:id:masaruyagi:20180823191136p:plain

このように多少滑らかな線に変わりました。

では次にこのデータから入力変数と出力変数を取り出し訓練データとテストデータに分割するところまで行います。

今回は変換するデータを変えながら検証を行なったため関数化しておきました。


#データから変数とラベルを生成し、訓練データとテストデータに分割する
def data_split(data, v_size=30, train_split_latio=0.7):
    data = data.astype("float32")    #データをfloat32型に変換
    x, t = [], []
    data_len = len(data)    #総データ数
    
    #変数とラベルの生成
    for i in range(data_len - v_size):
        x_valu = data[i : i+v_size]    #連続したmax_len個の値
        t_valu = data[i+v_size]    #x_valuの次の値
        
        x.append(x_valu)    #入力変数ベクトル
        t.append(t_valu)    #出力変数ベクトル

    #ndarray型に変換し形を直す
    x = np.array(x).reshape(data_len-v_size, v_size, 1)
    t = np.array(t).reshape(data_len-v_size, 1)

    #訓練データとテストデータに分割
    border = int(data_len * train_split_latio)    #分割境界値
    x_train, x_test = x[: border], x[border :]    #訓練データ
    t_train, t_test = t[: border], t[border :]     #テストデータ
    
    return x_train, x_test, t_train, t_test

入力変数は与えられたサイズの幅で1つずつずらしながら取集していき、出力変数はその直後の値としています。

また、今回学習に使うLSTMモデル用に、入力変数は3次元ベクトル、出力変数が2次元ベクトルに型を直しています。

訓練データとテストデータの分割は、与えられた割合で前半のデータを訓練用、後半のデータをテスト用に分けています。

モデル定義

今回の学習ではKerasのLSTMモデルを採用しました。

時系列データのような、前のデータが次のデータに影響を与えてしまうデータは通常の学習モデルではうまく学習ができません。

そのため、時系列データの解析ではそういった場合にも対応して学習ができるRNN(リカレントネットワーク)が採用されることが多いです。

近年ではその中でもLSTMモデルがよく使われているため、今回はそれを使って学習を行います。


#LSTMモデルの生成
def create_LSTM(v_size, in_size, out_size, hidden_size):
    tf.set_random_seed = (20180822)
    model = Sequential()
    model.add(LSTM(hidden_size, batch_input_shape = (None, v_size, in_size), 
                   recurrent_dropout = 0.5))
    model.add(Dense(out_size))
    model.add(Activation("linear"))
    
    return model

このモデルは入力データ幅、入力数、出力数、隠れ層の数を指定することで使うことができます。

今回は回帰予測を行うため、活性化関数は'linear'を指定しています。

パラメータの定義

モデルも定義したし早速学習に移りたいのですが、その前にパラメータをまとめて定義しておきましょう。

この部分をいじることで後の学習精度に大きく影響します。


#各パラメータの定義
now_data = data           #扱う元データ
v_size = 30               #入力データ幅
train_split_ratio = 0.7   #訓練データとテストデータの分割割合
x_train, x_test, t_train, t_test= data_split(now_data, v_size, train_split_ratio)  #データの分割

mean = np.mean(x_train)           #平均値の保存
std = np.std(x_train)             #標準偏差の保存
x_train = (x_train - mean) / std  #標準化
x_test = (x_test - mean) / std

tmean = np.mean(t_train)
tstd = np.std(t_train)
t_train = (t_train - tmean) / tstd #出力変数も同じように標準化
t_test = (t_test - tmean) / tstd

in_size = x_train.shape[2]   #入力数
out_size = t_train.shape[1]  #出力数
hidden_size = 300            #隠れ層の数
epochs = 100                 #エポック数
batch_size = 30              #バッチサイズ

見てわかる通り訓練データとテストデータの標準化をこの部分で行なっています。

標準化とはデータを平均が0、分散を1にすることで、値のサイズを小さくし扱いやすくします。

標準化により学習精度も上がります。

データの学習と予測

では学習を始めましょう!


# 学習
early_stopping = EarlyStopping(patience=10)  #ストップカウント                                                              
model = create_LSTM(v_size, in_size, out_size, hidden_size)  #インスタンス生成
model.compile(loss="mean_squared_error", optimizer = Adam(0.0001))  #損失関数定義
model.fit(x_train, t_train, batch_size = batch_size, epochs = epochs, shuffle = True, callbacks = [early_stopping], validation_split = 0.1)  #学習

EarlyStoppingを使うことで、損失が減少しなくなってから一定カウント進んだとことで学習をストップできます。

これにより過学習を防ぐことができます。

今回は回帰問題を扱うため"mean_squared_error"と"Adom"を使用しました。

では早速学習したデータで予測し、プロットしてみましょう!


#予測
pred_train = model.predict(x_train)     #訓練データ予測
pred_train = pred_train * tstd + tmean  #標準化したデータを戻す

a = np.zeros((v_size, 1))
b = pred_train
pred_train = np.vstack((a, b))   #データ長を合わせるため0ベクトルと結合

#プロット
plt.figure()
plt.plot(pred_train, color="r", label="predict")
plt.plot(now_data, color="b", label="real")
plt.grid(True)
plt.legend()
plt.show()


pred_test = model.predict(x_test)     #テストデータ予測
pred_test = pred_test * tstd + tmean  #標準化したデータを戻す

a = np.zeros((v_size + x_train.shape[0], 1))
b = pred_test
pred_test = np.vstack((a, b))   #データ長を合わせるため0ベクトルと結合

#プロット
plt.figure()
plt.plot(pred_test, color="r", label="predict")
plt.plot(now_data, color="b", label="real")
plt.grid(True)
plt.legend()
plt.show()

f:id:masaruyagi:20180823140129p:plain

f:id:masaruyagi:20180823140216p:plain

上が訓練データに対する回帰、下がテストデータに対する回帰となっています。

実測値線に沿った予測値線が引けたのではないでしょうか!

結果解釈

しかし、よく見てみると実測値線と少しずれて予測値線が構成されているようにも見えます。

つまり、今回は30サンプル取って学習しているわけですが、その中の直前の値を参考に次の値の予測を行なっているということです。

実は、仮想通貨や株のチャートはランダムウォークといい、値が上下する確率はほぼ五分五分になっていることが知られています。

そのため、予測をする直前の値の依存性が高くなり、予測値線が実測値線から少しずれたようになっています。

試しに次の値の騰落(値が上がったか下がったか)の予測が当たっている確率を出してみたいと思います。


#trainデータ騰落正答確率測定
data_score = 0
for i in range(pred_train.shape[0] - v_size - 1):
    if pred_train[i + v_size + 1] - pred_train[i + v_size] > 0:
        if data[i + v_size + 1]-data[i + v_size] > 0:
            data_score += 1
    if pred_train[i + v_size + 1] - pred_train[i + v_size] < 0:
        if data[i + v_size + 1] - data[i + v_size] < 0:
            data_score += 1
print("train score: {}".format(data_score / (pred_train.shape[0] - v_size - 1)))

#testデータ騰落正答確率測定
N = x_train.shape[0] + v_size
data_score = 0
for i in range(pred_test.shape[0] - N - 1):
    if pred_test[i + N + 1] - pred_test[i + N] > 0:
        if data[i + N + 1] - data[i + N] > 0:
            data_score += 1
    if pred_test[i + N + 1] - pred_test[i + N] < 0:
        if data[i + N + 1] - data[i + N] < 0:
            data_score += 1
print("test score: {}".format(data_score / (pred_test.shape[0] - N - 1)))

f:id:masaruyagi:20180823160729p:plain

騰落予測結果はランダムウォークの性質通り50%付近になってしまいました。

しかも50%を下回っています。

やはり仮想通貨の騰落を予測するのは難しい......

使用データを変えて再学習

実測値で学習をした場合では正答確率46%という結果になってしまいました。

では、はじめに作った1日幅で移動平均をとったデータで学習した場合はどうでしょうか?

データが滑らかになった分、精度が上がるかもしれません。

学習データを変えるには、パラメータ定義の"now_data"の部分を変更するだけです。

結果をプロットして見ましょう。

f:id:masaruyagi:20180823155420p:plain

f:id:masaruyagi:20180823155432p:plain

どうでしょうか。

さっきよりも実測値線に近くプロットされているように見えます。

では騰落予測の正答確率も出して見ます。

ソースコードは先ほどと同じものを使用しました。

f:id:masaruyagi:20180823161815p:plain

先ほどよりも精度をあげることができました!

しかし結果としてはやはり五分五分というところです。

まとめ

今回はBCHの500日分のデータを使い回帰予測と騰落予測を行なってみました!

回帰に関しては、おおよそ実測値線に沿った予測値線を引くことができましたが、騰落予測は正答確率約50%というところです。

初めは正答確率50%を切っていましたが、移動平均と標準化により越えることができました。

しかし高い確率とは言えないため、綺麗に引けた予測線も、結局は直前の値に大きく影響を受けた線と言えそうですね......

やはり、前の値と依存関係のある時系列データの解析に使えるLSTMモデルでも、ランダムウォークである仮想通貨の予測は難しかったようです。

理由として、仮想通貨はその通貨の価格データだけでなく、他の通貨の情報や世界情勢など多くの要因で変動してしまうことが挙げられます。

また株などのデータと比べ、激しく価格が上下することも解析を難しくしていると考えられます。

ただこのモデルを使えば、どんなに投資が下手な人でも50%の確率では騰落を当てられるのではないでしょうか(笑)