◆時系列データ・モデリング編　～ニューラルネットワークで株価予測をしよう～◆

## モチベーション

ここは前回の記事([◇◆機械学習モデリング前編 Jane Street Market Predictionで市場予測をしよう！◆◇](https://qiita.com/kanawoinvest/items/8ac0d2e12b0c8d6c5604))に引き続き、具体的なモデリングの話をしていきたいと思います。

正直、前回の記事は、コンペのデータを探索していたということもあり、目的はそのデータを予測することだったので、この記事では予測に焦点を当てようと思います。

今回はシンプルでとても強いnotebookを見つけたので、そちらを紹介・解説したいと思います。

URL: [OWN Jane Street with Keras NN](https://www.kaggle.com/tarlannazarov/own-jane-street-with-keras-nn)

## とりあえずコードを覗く

**ライブラリの読み込み**

```python
from tensorflow.keras.layers import Input, Dense, BatchNormalization, Dropout, Concatenate, Lambda, GaussianNoise, Activation
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.losses import BinaryCrossentropy
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.layers.experimental.preprocessing import Normalization
import tensorflow as tf
import numpy as np
import pandas as pd
from tqdm import tqdm
from random import choices
```

**初期設定およびデータの読み込み**

```python

SEED = 1111

tf.random.set_seed(SEED)
np.random.seed(SEED)

train = pd.read_csv('../input/jane-street-market-prediction/train.csv')
train = train.query('date > 85').reset_index(drop = True) 
train = train[train['weight'] != 0]

train.fillna(train.mean(),inplace=True)

train['action'] = ((train['resp'].values) > 0).astype(int)


features = [c for c in train.columns if "feature" in c]

f_mean = np.mean(train[features[1:]].values,axis=0)

resp_cols = ['resp_1', 'resp_2', 'resp_3', 'resp', 'resp_4']

X_train = train.loc[:, train.columns.str.contains('feature')]
#y_train = (train.loc[:, 'action'])

y_train = np.stack([(train[c] > 0).astype('int') for c in resp_cols]).T
```

**モデルの定義**

```python

def create_mlp(
    num_columns, num_labels, hidden_units, dropout_rates, label_smoothing, learning_rate
):

    inp = tf.keras.layers.Input(shape=(num_columns,))
    x = tf.keras.layers.BatchNormalization()(inp)
    x = tf.keras.layers.Dropout(dropout_rates[0])(x)
    for i in range(len(hidden_units)):
        x = tf.keras.layers.Dense(hidden_units[i])(x)
        x = tf.keras.layers.BatchNormalization()(x)
        x = tf.keras.layers.Activation(tf.keras.activations.swish)(x)
        x = tf.keras.layers.Dropout(dropout_rates[i + 1])(x)

    x = tf.keras.layers.Dense(num_labels)(x)
    out = tf.keras.layers.Activation("sigmoid")(x)

    model = tf.keras.models.Model(inputs=inp, outputs=out)
    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=label_smoothing),
        metrics=tf.keras.metrics.AUC(name="AUC"),
    )

    return model
```

**予測**

```python

batch_size = 5000
hidden_units = [150, 150, 150]
dropout_rates = [0.2, 0.2, 0.2, 0.2]
label_smoothing = 1e-2
learning_rate = 1e-3

clf = create_mlp(
    len(features), 5, hidden_units, dropout_rates, label_smoothing, learning_rate
    )

clf.fit(X_train, y_train, epochs=200, batch_size=5000)


models = []

models.append(clf)

th = 0.5000


f = np.median
models = models[-3:]
import janestreet
env = janestreet.make_env()
for (test_df, pred_df) in tqdm(env.iter_test()):
    if test_df['weight'].item() > 0:
        x_tt = test_df.loc[:, features].values
        if np.isnan(x_tt[:, 1:].sum()):
            x_tt[:, 1:] = np.nan_to_num(x_tt[:, 1:]) + np.isnan(x_tt[:, 1:]) * f_mean
        pred = np.mean([model(x_tt, training = False).numpy() for model in models],axis=0)
        pred = f(pred)
        pred_df.action = np.where(pred >= th, 1, 0).astype(int)
    else:
        pred_df.action = 0
    env.predict(pred_df)
```

基本的には4ステップだけですので、この記事ではモデルの定義を集中して解説していきたいと思います。

## ニューラルネットワーク解説

該当箇所は次になります。

```python

def create_mlp(
    num_columns, num_labels, hidden_units, dropout_rates, label_smoothing, learning_rate
):

    inp = tf.keras.layers.Input(shape=(num_columns,))
    x = tf.keras.layers.BatchNormalization()(inp)
    x = tf.keras.layers.Dropout(dropout_rates[0])(x)
    for i in range(len(hidden_units)):
        x = tf.keras.layers.Dense(hidden_units[i])(x)
        x = tf.keras.layers.BatchNormalization()(x)
        x = tf.keras.layers.Activation(tf.keras.activations.swish)(x)
        x = tf.keras.layers.Dropout(dropout_rates[i + 1])(x)

    x = tf.keras.layers.Dense(num_labels)(x)
    out = tf.keras.layers.Activation("sigmoid")(x)

    model = tf.keras.models.Model(inputs=inp, outputs=out)
    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=label_smoothing),
        metrics=tf.keras.metrics.AUC(name="AUC"),
    )

    return model
```

![tikz10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729216/bbe950b6-98fc-6932-8df4-4fa1abb9b2ad.png)

(*画像参照:https://nnadl-ja.github.io/nnadl_site_ja/chap1.html*)

上の画像は一般的なニューラルネットワークのレイヤーですが、一番左のレイヤーが入力層、真ん中は、隠れ層、一番右は出力層と呼びます。

それぞれの層にある丸はパーセプトロンと呼びます。
パーセプトロンは複数の二進数(0または1) $x_1,x_2,$… を入力にとり、ひとつの二進数(0または1)を出力します。

![perceptron.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729216/2d0ee0c6-3d4c-87d5-1cc6-67deb21a5ba0.png)

ここから重み $w_1,w_2,$…という概念を導入しました。 重みとは、それぞれの入力が出力に及ぼす影響の大きさを表す実数です。 パーセプトロンの出力が0になるか1になるかは、入力の重みつき和 $\sum w_j x_j$と閾値の大小比較で決まります。 重みと同じく、閾値もパーセプトロンの挙動を決める実数パラメータです。 より正確に、数式で表現するなら、

```math
\mbox{output} = 
\begin{cases}
    0 & (\sum w_j x_j \leq \mbox{threshold}) \\
    1 & (\mbox{otherwise})
\end{cases}
```

また、このような複数層のネットワークは多層パーセプトロン(multilayer perceptrons)、またはMLPsと呼びます。

今回の時系列データの問題は、将来価格で上がっていれば1、それ以外は0としています。この問題は、二値分類問題と呼びます。

コードでは次の通りです。`resp_cols`は将来リターンを含むカラムで、`train[c] > 0`は将来リターンが正なら`True`、それ以外は`False`になり、それを`int`型にしています。`int`型では`True`は1、`False`は0になります。

```python
y_train = np.stack([(train[c] > 0).astype('int') for c in resp_cols]).T
```

では、入力層から説明に入りたいと思います。今回は教師あり学習モデルですので、入力データには様々な説明変数が入っております。

**入力層(Input Layer)**

```python
inp = tf.keras.layers.Input(shape=(num_columns,))
x = tf.keras.layers.BatchNormalization()(inp)
x = tf.keras.layers.Dropout(dropout_rates[0])(x)
```

最初の`inp`では、入力層に用いるための横方向の大きさをまず定義します。複数の特徴量があるので、今回はその特徴量の個数になります。

次に `BatchNormalization`と`Dropout`ですが、Batch Normalizationは、Deep Learningにおける各重みパラメータを再編成することで、ネットワークを最適化するための方法の一つです。

`Dropout`はニューラルネットワークの過学習を防ぐために提案されたテクニックで、ランダムにニューロンを無視して学習を進める正則化の一種です。

**隠れ層(Hidden Layer)**

ここからは少しニューラルネットワーク構造のパラメーターを確認してから、説明に進みたいと思います。

```python
batch_size = 5000
hidden_units = [150, 150, 150]
dropout_rates = [0.2, 0.2, 0.2, 0.2]
label_smoothing = 1e-2
learning_rate = 1e-3
```

では隠れ層について見ていきましょう。

```python
x = tf.keras.layers.Dense(hidden_units[i])(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation(tf.keras.activations.swish)(x)
x = tf.keras.layers.Dropout(dropout_rates[i + 1])(x)
```

ここで`Dense`はレイヤーの一種で、`hidden_units`は上で定義していて150です。これは、入力を150次元に出力します。
具体的には、例えばinputの形が(*, 100)であれば、これによって、(150, *)になります。

次に活性化関数ですが、ここでは`swish`を使用しています。

シグモイド関数は非常に有名で次のように表すことが出来ます。

```math
\sigma(x) = \frac{1}{1+e^{-x}}
```

ここで、`swish`関数を$f(x)$とすると、$f(x) = x \sigma(x)$と書けます。

**出力層**

```python
x = tf.keras.layers.Dense(num_labels)(x)
out = tf.keras.layers.Activation("sigmoid")(x)
```

出力層の活性化関数はシグモイド関数になっておりまして、出力の値はある意味1になる確率と捉えることが出来ます。値が大きければ大きいほど1になる確率は高いですが、関数の定義からも明らかですが、シグモイド関数の出力が0.6だからと言って60%の確率で1になるとは限りません。

**モデルの最適化の準備**

```python
model = tf.keras.models.Model(inputs=inp, outputs=out)
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
    loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=label_smoothing),
    metrics=tf.keras.metrics.AUC(name="AUC"),
)
```

今まではニューラルネットワークのMLPsの構築を行いましたが、ここでは、出力の評価関数について定義します。
Adamとは最適化手法の一つです。パラメーターに`learning_rate`が使用されていますが、これに関しては[こちらの記事(Adamによる最適化)](https://www.renom.jp/ja/notebooks/tutorial/basic_algorithm/adam/notebook.html)から抜粋します。

>最適化の手法は世の中にたくさん提案がされており、ニューラルネットワークのパラメータの最適化に関しては入力変数と出力変数の間に起こりうるパターンを見つけ出し、予測精度が最大になるような重みパラメータをどう見つけるかという問題になります。その際に学習率は実は現れる説明変数の登場回数が大きく異なる場合などにおいて重要な役割を果たすことが知られています。 例えばあまり多くは現れないが、重要なパラメータについては一度の更新で大きく最適な方向へ更新を行いたい、もしくはそれほど重要ではないノイズのようなパラメータが多く現れる場合にはそのパラメータの方向にあまり大きく変化してほしくないケースも存在します。そこでAdaGradは初めにSGDが学習率を全てのパラメータで同じに扱っているという欠点に気づき、それを勾配の二乗で減らしていき、パラメータ毎に学習率を更新する手法を提案しました。

損失関数は`Binary Cross entropy`で次にように定義します。

```math
\displaystyle H(p, q) = - \sum_x p(x) \log(q(x))
```

$p$は実際の確率, $q$は推定した確率になります。先ほど、シグモイド関数では、確率を表していないと言いましたが、損失関数をこのように定義しているため、解釈としては1になる確率と考えて良いと思います。

次にmetricsですが、こちらは厳密にはlossとは異なります。というのも、lossは最適化に使用されますが、metricsはモデルのパフォーマンスの判断に使用します。metricsは今回はAUCを採用しています。

AUCに関しては細かい説明は省きますが、ROC曲線を作成した時に、グラフの曲線より下の部分の面積をAUC(Area Under the Curve)と言います。AUCは0から1までの値をとり、値が1に近いほど判別能が高いことを示します。判別能がランダムであるとき、AUC = 0.5となります。

## まとめ

ニューラルネットワークに関しては学習したことはあるものの、しばらく触っていませんでした。
今回のnotebookはニューラルネットワークを用いて、シンプルで且つ非常に強力だったので、良い復習のきっかけになりました。
では、良い機械学習と投資ライフを！