CL LAB

HOME > CL LAB > DataAnalytics > ディープラーニングでBitcoinの価格を予測して見る #Deep Learning #Keras

ディープラーニングでBitcoinの価格を予測して見る #Deep Learning #Keras

 ★ 3

こんにちは、クリエーションラインの朱です。

最近、ICO(Initial Coin offering)が非常にホットなキーワードになっています。文字通り、ICOは企業がプロジェクトの初期段階でコイン(仮想通貨)を使って資金を調達する仕組みです。ICOによってスタートアップでも簡単に事業を立てるための資金を調達できるようになりました。今回は仮想通貨の中でも特に有名なBitcoinの価格をディープラーニングで予測してみました。系列データの処理に得意とするRNN(Recurrent Neural Network)の一種であるLSTMを使用しました。

今回扱うデータがBitcoinの価格のみ、他のデータは一切使いませんでした。但し、結論から言うと、このままだと上手く予測できず、他のデータ(特徴量)が必要になります。今回は勉強が目的なので、特徴量を探すのはまた別の機会にしたいと思います。

今回の実装はKerasライブラリを使ってみましたが、所感としてはとにかくコーティングの量が少ないです。ディープラーニングの理論は複雑ですが、こんな短い期間で実装できるのが驚きです。

では、早速本題に入りたいと思います。

データを準備

データの取得

今回のデータは大手仮想通貨取引所のPoloniexから取ってきます。Python用のAPIが提供されているので、API経由で取得します。

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

def return_chart_data(pair, period, day):
    polo = poloniex.Poloniex()
    chart_data = polo.returnChartData(pair,
                                      period=period,
                                      start=time.mktime(time.strptime(start_time, "%Y-%m-%d %H:%M")),
                                      end=time.mktime(time.strptime(end_time, "%Y-%m-%d %H:%M")))
    df = pd.DataFrame(chart_data)
    df["datetime"] = pd.to_datetime(df["date"].astype(int) , unit="s")
    df = df.set_index("datetime")
    fig, ax1 = plt.subplots(figsize=(20, 10))
    ax1.plot(pd.to_datetime(df["date"].astype(int) , unit="s"),
             df["close"].astype(np.float32), label = "Coin Price", color="deeppink")
    ax1.legend(loc=2, fontsize=14)
    ax1.tick_params(labelsize=14)
    plt.show()
    return df

# USDT_BTCコインペアのデータを取得、データ間隔は300s、期間は2018-03-30 00:00から2018-04-01 00:00まで
data_df = return_chart_data("USDT_BTC", 300, "2018-03-30 00:00", "2018-04-01 00:00")

※余談ですが、ここのUSDTはUSDではなくTetherという仮想通貨です。しかもTetherが今流行な分散型ではなく集中型です。1Tether=1USDなので、USDとみなしても大丈夫です。

Poloniexから取得したデータのdateとclose列が今回の時系列データになります。以下はdateを横軸に、closeを縦軸にプロットしたグラフです。

データの階差を取る(differencing)

Bitcoin価格のようなファイナンシャルデータは基本Random walkの性質を持っています。Random walkとは以下のことを指します。
- 長期の下落か上昇のトレンドがある
- 急に予測不能な方向に変化することがある

こういう性質を持つデータを非定常データと呼びます。分析を行うためには、データからトレンドを抽出し、定常的なデータに変化させる必要があります。
ここでは、データのトレンドを取り除きます。階差(differencing)という手法を使いますが、以下の数式で行いますが、close_diffed(t)が時刻close(t)と時刻close(t-1)の階差になります。また、Bitcoin価格には特に周期性が見当たらないので、今回は隣接の時刻の階差を取りますので、階数が1になります。

close_diffed(t)=close(t)−close(t−1).

但し、後程の予測結果でお見せしますが、こういうやり方では、close(t)の予測結果がclose(t-1)の観測値と似たようなトレンドになります。予測が観測より一歩遅れている感じになります。
もしデータに周期性(季節性等)が存在するのであれば、かなり有力な方法です。

ソースコードは以下の通りです。

def difference_data(data_df):
    diff_interval = 1
    data = data_df["close"].values
    diff = np.empty((0,1), np.float32)
    for i in range(diff_interval, len(data)):
        value = data[i] - data[i - diff_interval]
        diff = np.append(diff, value)

    data_df = data_df.iloc[self.diff_interval:, :]
    return diff, data_df

data_diff, data_df = difference_data(data_df)

階差をとったデータが以下のグラフになります。

特徴量の準備

今回扱うデータがBitcoin価格のみなので、学習と予測を行うために、特徴量を作成する必要があります。
今回はt時刻のデータclose(t)の特徴量をclose(t-30)...close(t-1)の系列とします。これで一つの観測値にたいして30個の特徴量が得ました。
この後、close(t-30)...close(t-1)の系列でclose(t)を予測するわけです。

def generate_series_data_for_supervised_learning(data_array, datetime_array):
    t = np.empty((0,1), int)
    x = np.empty((0,30), np.float32)
    y = np.empty((0,1), np.float32)

    m = len(data_array)
    for n in range(30, m):
        new_t = np.array([[datetime_array[n]]])
        new_x = np.array([data_array[n-30: n]])
        new_y = np.array([[data_array[n]]])
        t = np.append(t, new_t, axis=0)
        x = np.append(x, new_x, axis=0)
        y = np.append(y, new_y, axis=0)

    self.dataframe = self.dataframe.iloc[30:, :]
    return t, np.concatenate([x, y], axis=1)

time_array, data_array = generate_series_data_for_supervised_learning(data_diff, data_df.index)

データの分割

今回は教師データ(交差検証データを含む)80%、テストデータ20%に分割します。時系列データのためシャフルは行いません。

def split_data(data_array, datetime_array):
    m = len(data_array)

    train_batches = int(m * 0.8)

    # 80% training data(cv included), 20% test data
    m_train = train_batches

    time_train, time_test = datetime_array[:m_train], datetime_array[m_train:]
    data_train, data_test = data_array[:m_train], data_array[m_train:]

    return time_train, time_test, data_train, data_test

time_train, time_test, data_train, data_test = split_data(data_array, datetime_array)

データの正規化

LSTMについては後程説明しますが、内部の出力ゲートの活性関数がtanhなので、今回のデータをtanh関数のレンジに合わせて-1~1の範囲に再スケールします。

def range_scale_data(self, matrix):
    min_max_scaler = preprocessing.MinMaxScaler(feature_range=(-1, 1))
    return min_max_scaler.fit_transform(matrix), min_max_scaler

# scaler will be used to invert predictions
data_train_scaled, scaler = range_scale_data(data_train)
data_test_scaled = scaler.transform(data_test)

今回のニューラルネットワークの定義

LSTMとは

今回LSTMを使うということで、まずLSTMについて簡単に説明します。LSTMが20年前に提案されたニューラルネットワークですが、最近の自然言語処理の分野では、LSTMで構築された言語モデルのほうが人間の翻訳より良い精度をだしている場合があります。
LSTM(Long-Short Term Memory)は、短い系列は勿論、長い系列でも正しく学習できるような仕組みを持っています。この役目を果たしているのがゲードです。長い系列を学習するため、LSTMの内部では状態というものを持ちます。ゲートは状態にどういう情報を入れるかを決めます。重要でない情報を忘れ、重要な情報を状態に保存します。そうすることで、長い系列の学習を可能とします。

LSTMに関する詳細の説明は以下のサイトをご参照ください。
http://colah.github.io/posts/2015-08-Understanding-LSTMs
* 英語のサイトですが、英語が苦手の方は是非無料のグーグル翻訳でサイトを日本語化してみてください。現在の機械翻訳のすごさが分かります。上記リンクをグーグル翻訳に貼り付け翻訳ボタンを押せば日本語化されます。

StatefulかStatelessか

Kerasライブラリでは、LSTMがデフォルトではStatelessですが、今回はStateful LSTMを使用したいと思います。Stateful LSTMはバッチ処理後に状態を保持し、Stateless LSTMは状態をリセットします。(ディープラーニングのバッチ処理はm件のデータを同時に処理することで、計算時間を短縮します。バッチサイズが1の場合はオンライン処理とも呼びます)
自然言語処理では、長い文章が2つの異なるバッチに分割さてたとします。この文章を正しく学習するために、2番目のバッチ処理時に1番目のバッチ処理後のLSTMの状態を知る必要がありますので、Stateful LSTMによって状態を保持します。
今回は正直、階差(differencing)でトレンドを取りましたので、各時刻のデータが独立性をもっているはずなので、Statelessを使うか迷ってしまいますが、時系列データなので、トレンドを除いて比較的に定常データになったとしても、完全に独立とは言えませんので、Stateful LSTMを使います。

ネットワークの階層

ディープラーニングの初心者として、ここでは深くせずに浅いネットワークを作ります。
一般的に、深いニューラルネットワークがより良い性能を得ることが出来ますが、当然計算時間が上昇します。
今回は入力層として1つのLSTM層を追加し、出力層として全結合層(KerasではDenseと呼ぶ)を追加します。

SEQ_LENGTH = 30
BATCH_SIZE = 1
DATA_DIM = 1
model = Sequential()
model.add(LSTM(32, batch_input_shape=(BATCH_SIZE, SEQ_LENGTH, DATA_DIM), stateful=True))
model.add(Dense(1, activation='linear'))

ネットワークは次のようになります。

LSTM層のニューロン数を32にしていますが、特に根拠がありません。基本的にはニューロン数を増やすことでニューラルネットワークの表現力を上げることができます。

KerasのLSTMコンストラクタのbatch_input_shape引数が少し理解に時間がかかってしまっていたので、ここで少し説明を加えたいと思います。
LSTMは入力として3次元配列を必要とします。次元を(m,n,k)で表現すると、mはサンプル数です。nは特徴量系列長であり、今回は30固定です。また、特徴量系列内の各要素は、独自の次元を持つこともできます。今回は各要素には1つの値しか含まれていないので、kは1固定です。LSTMがm個のサンプルを1つずつ処理するようにbatch_sizeを1に設定しました。バッチサイズを1に設定する理由は後程説明します。

次に誤差を最小化する(ディープラーニング流で言うと損失関数を最小ですね)ために、逆伝播(逆伝播については説明しませんが)の最適化関数を設定します。誤差の評価指標はMSEです。

model.compile(loss="mean_squared_error", optimizer="rmsprop")

モデルを学習する

モデルを定義しましたので、教師データをモデルに学習させます。

EPOCHS = 30

# split CV set from training set.
train_data_scaled, cv_data_scaled = train_test_split(
    data_train_scaled, test_size=0.25, random_state=2, shuffle=False)
x_train_scaled, y_train_scaled = train_data_scaled[:, 0:-1], train_data_scaled[:, -1]
x_cv_scaled, y_cv_scaled = cv_data_scaled[:, 0:-1], cv_data_scaled[:, -1]

# reshape data as a tensor(3 dims)
x_train_scaled = x_train_scaled.reshape((x_train_scaled.shape[0], SEQ_LENGTH, DATA_DIM))
x_cv_scaled = x_cv_scaled.reshape((x_cv_scaled.shape[0], SEQ_LENGTH, DATA_DIM))

# fit data
model.fit(x_train_scaled, y_train_scaled,
          batch_size=BATCH_SIZE,
          epochs=EPOCHS,
          verbose=1,
          validation_data=(x_cv_scaled, y_cv_scaled),
          shuffle=False)

実装はKerasライブラリのおかげで非常に簡単(model.fitを呼ぶだけ)ですが、いくつか重要なパラメータを説明したいと思います。

バッチサイズ

私が前に述べたモデルでは、close(t-30)...close(t-1)を使ってclose(t)を予測します。次にclose(t+1)を予測するためには、close(t)の観測値が必要あります。
なので、close(t)の観測値が出るまでclose(t+1)の予測ができません。
これはone-step forecastと呼ばれますが、バッチサイズはステップのサイズと同じにしなければなりません。

エポック

エポックは、ディープラーニングのもう一つの重要なパラメータです。ディープラーニングは同じデータセットに対して複数回学習を行いますので、回数を設定するのがエポックです。大きいエポック数が精度の向上につながりますが、計算時間とのトレードオフです。

交差検証

交差検証は、過学習を防ぎ、より良い学習モデルを構築するための一般的な方法です。通常信頼性の高い検証結果を得るために交差検証データを変えながら複数回検証する必要があります。但し、ディープラーニングの仕組み上、複数回の検証のコストが高いため、交差検証は行いません。
上記の実装では交差検証のデータをKerasに渡していますが、これは通常の交差検証ではありません。各エポックの学習後の誤差を出すために交差検証のデータが必要になります。

以下の図のようにKerasがモデルの損失(教師データのMSE)と交差検証データの損失(交差検証データのMSE)を出力しますので、損失(loss)を確認することで自分が定義したニューラルネットワークが正しいかどうかが分かります。

ディープラーニングでは、交差検証の代わりに、過学習を防ぐための手法としてドロップアウトが提案されています。以下の論文ではドロップアウトが効果的であることを証明されています。
"Dropout: A Simple Way to Prevent Neural Networks from Overfitting". Jmlr.org. Retrieved July 26, 2015.

今回はドロップアウトを適用しません。

LSTMに渡すため、データの次元を変える

今回作成したデータが2次元の配列(m行30カラムの配列)になりますが、前述したとおり、LSTMの入力が3次元配列なので、入力データを3次元配列に再構成する必要があります。
次元の定義は、上で説明したbatch_input_shapeと同じです。

予測

学習が完了しましたのえ、次にテストデータを使って予測を行っています。
全述の通り、one-step forecastを行います。
注意が必要なのが、階差、正規化等の処理を行いましたので、予測値を逆の処理をして戻す必要があります。
実装は省略しますが、興味がある方は是非自分で実装してみてください。

では、テストデータの予測結果を見てみましょう。

予測値は観測値とよく似ていますが、予測値が観測値より一歩遅れているのが分かります。
これが前述の通り、階差(differencing)を行った結果になります。前述のRandom walkの特性で、予測値close(t)が観測値close(t-1)とほぼ同値とります。
予測が成功したとは言えませんが、Bitcoin価格以外の特徴量が必要であることが分かりました。

結論

KerasでLSTMを使用して時系列データの予測を行う方法を学びました。
非定常データのトレンド抽出、季節調整について勉強しました。
他の特徴量なしでBitcoin価格を予測するのは難しいことを知りました。Bitcoinの価格は株と同じく全世界の経済、政治等の外部要因、他の経済指数等に強く影響されているので、特徴量の選定等の特徴エンジニアリングが必要です。

しかし、初心者としては、調整が必要なパラメータがまだまだ多くあります。

今回の学習で使っていたパラメータ

  • エポック: 30
  • ネットワーク: LSTM-layer➛Dense-layer
  • LSTM層の出力ニューロン数: 32
  • ネットワーク出力の活性関数: linear
  • ドロップアウト: 未使用
  • 逆伝播最適化アルゴリズム: rmsprop
  • 損失関数: MSE

参考

[1]. Understanding LSTM Networks
[2]. How to Use Timesteps in LSTM Networks for Time Series Forecasting
[3]. "Dropout: A Simple Way to Prevent Neural Networks from Overfitting". Jmlr.org. Retrieved July 26, 2015.

今回のソースは以下のリポジトリに公開していますので、興味がある方はご参照ください。
https://github.com/george-j-zhu/cryptocurrency-price-prediction

Author

朱

ソフトウェア開発出身の機械学習エンジニアです。

朱の記事一覧

朱へお問い合わせ

CL LAB Mail Magazine

CL LABの情報を逃さずチェックしよう!

メールアドレスを登録すると記事が投稿されるとメールで通知します。

メールアドレス: 登録

Related post