ゼロから始めるAIシステム開発 #12 「為替予測モデルその2  LSTM編」

## 入門書8章（AWS Step Functions）の予定が……
いつもの入門書に戻って第8章、「BedrockとAWSの**Step Functions**を使った生成AIアプリ開発」のハンズオンの予定でしたが、やってみるとStep Functionsの仕様が書いてある状態からかなり変わっており（チェック入れの必要な項目が別の動作に変わってたりする）、入門書の通りやっても実装できず。ChatGPTやGeminiにも頼りあーでもないこーでもないと格闘しましたがどうもJSON構文やStep Functionsの操作に明るくないと解決できないっぽい。
5章のAIエージェントのところも挫折したので非常に無念ですがここも飛ばし。まだ初版から1年経ってない本なのにこういうことになるとは本当に進化の著しい分野ですね…。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/8caca30b-f97c-489a-b84d-c28f3059ad50.png)
このグラフィカルに設定できるワークフローとかすごく面白そうなんだけどなあ。ていうかAWSの操作いちいちむずくない？エンジニアの人ならなんてことないのかなあ。つまづきが続いたのでかなり気落ちしております…。

## 為替予測モデルVer.2 LSTM編
また挫折しました、で終わるのも情けなさすぎるので、前回Pythonに慣れるためやっていた為替予測モデルの続きを書こうと思います。

前回は機械学習のSVM（サポートベクターマシン）とK近傍法による分類を試したが、今回は**LSTM**と呼ばれる手法を使ってみる。LSTMは**深層学習**のカテゴリでありニューラルネットワークの一種。時系列データの長期的な依存関係を学習するのに向いている、らしい。SVMで作っていたときに、前のデータに影響を受けている為替の値動きをそのまま説明変数にするのはどうなんだと思っていたので、時系列データを扱うのに適しているLSTMを知り、こっちでやってみることにした。

```exchange_modelset_lstm.py
import pandas as pd
import numpy as np

# 表示オプションを設定
pd.set_option('display.float_format', lambda x: '%.3f' % x)
# CSVの読み込み
df = pd.read_csv("usdjpn_5m_sonomama.csv")  # 為替データ（5分足など）

print("時間のデータ型をdatetime64型に-----------------")
df['Datetime'] = pd.to_datetime(df['Datetime'])
print("単純移動平均カラムを追加-----------------------")
df['SMA21'] = df['Close'].rolling(window=21, min_periods=1).mean()
print("始値-終値のカラムを追加-----------------------")
df['Body'] = df['Open'] - df['Close']
print("終値前日比のカラムを追加-----------------------")
df['Comparison'] = (df['Close'] - df['Close'].shift(1)) / df['Close'].shift(1) * 100
print("日本時間とそれ以外の区別を追加-----------------------")
df['JPNtime'] = df['Datetime'].dt.time.between(pd.to_datetime('09:00').time(), pd.to_datetime('17:00').time()).astype(int)

print("目的変数を計算し追加-----------------------")
df['Rise'] = np.where((df['High'] - df['Open']) >= 0.06, 2, np.where((df['High'] - df['Open']) >= 0.03, 1,0))
df['Fall'] = np.where((df['Open'] - df['Low']) >= 0.06, -2, np.where((df['Open'] - df['Low']) >= 0.03, -1,0))
df['Target'] = df['Rise'].shift(-2) + df['Fall'].shift(-2)

<= pd.to_datetime('15:50').time())]
df = df.drop('Volume',axis=1)
df = df.drop('Dividends',axis=1)
df = df.drop('Stock Splits',axis=1)
df = df.drop('Rise',axis=1)
df = df.drop('Fall',axis=1)
df = df.drop(df.index[[0]])

features = ['Open', 'High', 'Low', 'Close','SMA21','Body','Comparison','JPNtime']
df_target = df['Target']
df = df[features]

# 特徴量をスケーリング＆3次元にしてnp配列に変換
from sklearn.preprocessing import StandardScaler

lookback = 21  # 21本のローソク足を使用
future = 2 #いくつ先の足を予測するか

scaler = StandardScaler()
df_scal = scaler.fit_transform(df)

def to_np(df_scal):
    df_list = []
    df_scal = np.array(df_scal)
    for i in range(0, len(df_scal) - lookback -1 , 1):
        df_s = df_scal[i:i + lookback]
        df_list.append(df_s)
    return np.array(df_list)
X = to_np(df_scal)

# 目的変数を有効な分だけ抽出してnp配列に変換
df_target = df_target.iloc[lookback-1:len(df_target)-future]
y = np.array(df_target)
# スパースエンコーディング対応
y = y + 2 
print("X shape:", X.shape)
print("y shape:", y.shape)

X_train, y_train = X, y

print("Train shape:", X_train.shape, y_train.shape)

# Nanチェック
#print(np.where(np.isnan(X_train)))
#print("X_train NaN:", np.isnan(X_train).sum())  # 0 ならOK
#print("y_train NaN:", np.isnan(y_train).sum())  # 0 ならOK
#print("X_test NaN:", np.isnan(X_test).sum())  # 0 ならOK
#print("y_test NaN:", np.isnan(y_test).sum())  # 0 ならOK

# モデル構築
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Input
from tensorflow.keras.optimizers import Adam

model = Sequential([
    Input(shape=(lookback, len(features))),
    LSTM(32, return_sequences=True), 
    LSTM(32, return_sequences=False),
    Dense(5, activation='softmax') 
])
model.compile(optimizer=Adam(learning_rate=0.001, clipnorm=0.5), loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# モデルの概要を表示
model.summary()
# 学習
model.fit(X_train, y_train, epochs=40, batch_size=32, validation_split=0.2)

# モデルを保存
model.save("exc_lstm_model_2502.keras") 
```
### 必要なデータを追加
始値、高値、安値、終値に加えて単純移動平均や終値の前日比などを算出しカラムを追加
日本時間でトレードする予定なので日本時間を1、それ以外を0と区別するカラムを追加
目的変数は強い下降、下降、横這い（乱高下）、上昇、強い上昇の5つに分類
### データセットをLSTM用に整える
ニューラルネットワークは入力データのスケールに敏感なためスケーリングする
LSTMの説明変数は**データの数、参照する過去の長さ、特徴量の数**の3つが必要なので3次元のNumpy配列に変換
NaNがあると上手くいかないのでNaNチェック
### モデル構築・学習・保存
LSTMモデル構築に必要なtensorflowをインポート
入力層にデータセットを渡し、32個のセルを持つ中間層を二つ設定。セルの数は変えられるが、いろいろ試してみた結果32個くらいが一番的中率が高かった。
エポックやバッチサイズを設定しモデルを学習。エポックは学習回数、バッチサイズはデータを分割した塊の大きさ。これらも設定でき、それぞれ40と32くらいが一番的中率が高かった。
### 予測検証を経て実践
↑のコードは予測精度の検証を終えたあとのモデル保存用のコードで、検証は別のコードでやった。結果としては5択の分類で的中率が20％をやや超えるくらいまで上げられたので、これで10日ほど実際にトレードしてみた。結果がこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/d572ff4f-31de-4331-98ce-cb43a1ad6ae8.png)
（アカン）
うーん全然ダメ。まあ-769円のうちスプレッド分（手数料）が618円であり、人力トレードなので5分毎にポジション建て→解消を繰り返していたから、APIやPython自動化を用いて完全に自動トレードにできれば損失は半分くらいになるはず。もうちょっと精度を上げられればチャンスはあるかも。ボリンジャーバンドなどの特徴量を追加したり、目的変数を見直してみたり、CNN（畳み込みニューラルネットワークワーク）とのハイブリッドを試してみたり、いろいろとやりようがありそう。
## おわりに
入門書のハンズオンはあと一つなのでさすがにそろそろ終わらせたい。あるいはPythonでTkinterやFlaskあたりを使ってChatGPTのAPIを利用するアプリにチャレンジしてみるか…。為替予測モデルも手が空いたときに進めたいですね。