概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。
日経平均株価が5万円を超えたということで、久しぶりに日経225を利用した演習を行ってみたい衝動にかられてしまった![]()
今回はここ数回の内容である因果畳み込みと分位点回帰の2種類を利用して、幅で予測する時系列分析の演習を行ってみたいと思います。他のモデルとの比較検証も行うので、あえて第8回、第9回で扱ってきた日経225のデータを利用します。因果畳み込みについては第15回の【因果畳み込み・Conv1d】を参考にしてもらえると幸いです。
確認する事項は2つ
- 分位点0.1と0.9の間にテストデータが収まるかどうか?テストデータでも80%が0.1〜0.9の間に入っていればOK
- 中央値である0.5分位点での指標チェック。LSTMモデルよりも改善しているならOK
グラフから2点ともクリアしているような予感です![]()
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル
- データ:nikkei_225.csv
- コード:sample_18.ipynb
1. 因果畳み込みを利用した時系列分位点回帰
追加項目
- CausalConv1dのネットワーク層を作成
- 分位点回帰の損失関数としてPinball損失関数クラスを作成
2. 🤖 コードと解説
PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 損失関数と最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
2.0 データについて
日経225のデータをyfinanceやpandas_datareaderなどで取得します。データの日付が古いですが比較するために第8回と同一のデータを利用します。
| Date | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| 2021-01-04 | 27575.57 | 27602.11 | 27042.32 | 27258.38 | 51500000 |
| 2021-01-05 | 27151.38 | 27279.78 | 27073.46 | 27158.63 | 55000000 |
| 2021-01-06 | 27102.85 | 27196.40 | 27002.18 | 27055.94 | 72700000 |
| ︙ | ︙ | ︙ | ︙ | ︙ | ︙ |
| 2025-06-18 | 38364.16 | 38885.15 | 38364.16 | 38885.15 | 110000000 |
| 2025-06-19 | 38858.52 | 38870.55 | 38488.34 | 38488.34 | 89300000 |
始値(Open)を予測する形で演習を進めていきます。始値のグラフを描画してみましょう。青色の線が日経225の始値の折れ線グラフとなります。
学習用データとテスト用データに分割します。グラフの赤い線の右側100期をテスト用のデータとして使います。残りの左側を学習用のデータとします。学習用データで学習させて、「右側の100期間を予測できるのか?」が主目標となります。
2021年以降の日経225の値は、3万円前後の数値になることがほとんどです。誤差計算時の損失の値が大きくなりすぎないように、変数の更新がうまく行われるように、「1万円で割り算して数値を小さく」 しておきます。これで、ほとんどの値が2.5〜4に収まるはずです。正規化と呼ばれる格好良い手法を使うと更に精度も向上していきます。
2.1 データの読み込みとtorchテンソルへの変換
CSVファイルをpandasで読み込み、RNNで学習できる形にデータを前処理します。具体的には、株価の始値・高値・安値・終値データを窓サイズ5で区切って、その窓を1つずつスライドさせながらデータセットを作成していきます。前処理の具体的な解説は第8回を参照してください。
CSVファイルの読み込みから窓サイズでの分割までのコードです。スマートに一度に変換ではなく、地味に4種類同じことを繰り返す形で書きました![]()
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
# CSVファイルの読み込み
data = pd.read_csv("./data/nikkei_225.csv")
# 日経225の値を10,000円で割り算して値を小さくする
scaling_factor = 10_000
x_open = data["Open"]/scaling_factor
x_high = data["High"]/scaling_factor
x_low = data["Low"]/scaling_factor
x_close = data["Close"]/scaling_factor
# 窓サイズ5で区切る
win_size = 5
XO = [x_open[start:start+win_size] for start in range(len(data)-win_size)]
XH = [x_high[start:start+win_size] for start in range(len(data)-win_size)]
XL = [x_low[start:start+win_size] for start in range(len(data)-win_size)]
XC = [x_close[start:start+win_size] for start in range(len(data)-win_size)]
T = x_open[win_size:]
窓サイズ5で区切った4種類のデータを(XO、XH、XL、XC)、教師データのリストをTとします。今回のポイントは、窓サイズ5で区切った始値・高値・安値・終値の4種類を入力データに使う点です。 非常にシンプルですが、これだけでも改善が見られます ![]()
5個区切りデータ(XO、XH、XL、XC)を結合して、(バッチサイズ、5,4)の形状に変換して入力用のデータにします。
実際に表示するとわかるのですが、上記のコードだとXOやTはタイプが入り乱れています。最終的にtorch.FloatTensor()の形になればよいので、スマートではありませんが力技で押し切るコードにしました
一旦、numpy配列にして形式を整えてしまいましょう![]()
![]()
![]()
# numpy配列に変換!形式を整えるぞ
xo = np.array(XO)
xh = np.array(XH)
xl = np.array(XL)
xc = np.array(XC)
t = np.array(T)
xo = xo.reshape(xo.shape[0], xo.shape[1], 1)
xh = xh.reshape(xh.shape[0], xh.shape[1], 1)
xl = xl.reshape(xl.shape[0], xl.shape[1], 1)
xc = xc.reshape(xc.shape[0], xc.shape[1], 1)
# xo, xh, xl, xcの形状1の部分(axis=2)でデータを並べる
x = np.concatenate([xo, xh, xl, xc], axis=2)
# x.shape => (1087, 5, 4)
# t.shape => (1087,)
利用するデータの形状が、(バッチサイズ、系列長の5、特徴量の4)になっていることが確認できます。xをLSTMに入れることからネットワークが始まります。その前に、xとtをFloatTensorに変換して、学習用データとテスト用データに分割します。
# 今回からGPU使えるときはGPUを利用、それ以外だとCPUになるような設定にします
device = "cuda" if torch.cuda.is_available() else "cpu"
x = torch.FloatTensor(x).to(device)
t = torch.FloatTensor(t).to(device).shape(-1,1) # 回帰問題なので(バッチサイズ, 1)
【注意】
- 教師データの形状:3種類の分位点を利用して回帰するのですが、教師データはスカラーでOKです。損失を計算する部分でPinball関数によって分位点ごとに3種類に分けられるからです。
- 前半部分を学習用、後半部分をテスト用と前後に分割します。あとで数値的な検証や仮説検定をする予定なので100期ほどテストデータとして確保しておきます。
period = 100
x_train = x[:-period]
x_test = x[-period:]
t_train = t[:-period]
t_test = t[-period:]
# 入力する特徴量は1次元
# x_train.shape : torch.size([987, 5, 4])
# x_test.shape : torch.Size([100, 5, 4])
# t_train.shape : torch.Size([987, 1])
# t_test.shape : torch.Size([100, 1])
2.2 ネットワークモデルの定義と作成
今回は下図のような一次元因果畳み込みと線形層(全結合層)を利用したネットワークで時系列予測を扱っていきます。入力データは窓サイズ5の日経225データです。因果畳み込みで特徴量を抽出(causal_layers)し、線形層で分位点回帰(regressor)を行います。3種類の分位点(0.1、0.5、0.9)で予測を行うため、最終層である線形層の出力は3になることを忘れずに![]()
主な流れ
- 因果畳み込みネットワークのクラスを作成
- メインのネットワーククラスを作成
- nn.Sequential()を利用して、特徴量抽出ブロックと回帰分析のブロックを作成
- forward()に流れを記入
import torch
import torch.nn as nn
# 1. 因果畳み込みクラス
class CausalConv1d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, dilation=1):
super().__init__()
self.kernel_size = kernel_size
self.dilation = dilation
self.padding = (kernel_size - 1) * dilation
self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=self.padding, dilation=self.dilation)
def forward(self, x):
h = self.conv(x)
return h[:, :, :-self.padding]
# 2. ネットワーク構造
# nn.Sequentialで特徴量抽出ブロックと回帰分析ブロックに分割して記述
class DNN(nn.Module):
def __init__(self):
super().__init__()
# 特徴量の抽出ブロック
self.causal_layers = nn.Sequential(
CausalConv1d(in_channels=4, out_channels=10, kernel_size=3),
nn.ReLU(),
CausalConv1d(in_channels=10, out_channels=10, kernel_size=3),
nn.ReLU(),
CausalConv1d(in_channels=10, out_channels=10, kernel_size=3)
)
# 回帰分析のブロック 10チャンネルx5(データの系列長)
self.regressor = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=10*5, out_features=3)
)
def forward(self, x):
h = self.causal_layers(x)
y = self.regressor(h)
return y
model = DNN().to(device)
コードのポイント
- 因果畳み込みについては第15回を参考にしてください。
- CausalConv1dクラスで因果畳み込みを、DNNクラスでネットワークモデルを定義しています。
- 回帰分析のブロックのLinearについてです。入力次元が、チャンネル数10と系列長5から10×5となります。出力は分位点の個数なので3となります。
ネットワークモデルの構造をtorchinfoを利用して表示してみました。
# 入力サイズを指定 (bs, channels, length)
from torchinfo import summary
summary(model, (1,4,5))
正規化やドロップアウトなどがないシンプルな構造。パラメータ数は900個くらいか〜。
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
DNN [1, 3] --
├─Sequential: 1-1 [1, 10, 5] --
│ └─CausalConv1d: 2-1 [1, 10, 5] --
│ │ └─Conv1d: 3-1 [1, 10, 7] 130
│ └─ReLU: 2-2 [1, 10, 5] --
│ └─CausalConv1d: 2-3 [1, 10, 5] --
│ │ └─Conv1d: 3-2 [1, 10, 7] 310
│ └─ReLU: 2-4 [1, 10, 5] --
│ └─CausalConv1d: 2-5 [1, 10, 5] --
│ │ └─Conv1d: 3-3 [1, 10, 7] 310
├─Sequential: 1-2 [1, 3] --
│ └─Flatten: 2-6 [1, 50] --
│ └─Linear: 2-7 [1, 3] 153
==========================================================================================
Total params: 903
Trainable params: 903
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.01
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.01
==========================================================================================
2.3 損失関数と最小化の手法の選択
分位点回帰はピンボール関数を利用して損失関数を構成します。回帰問題ですが予測値y と実測値(教師データ)t の二乗誤差を小さくしていく方法ではないので注意が必要です。
# 損失関数のクラスと定義
class PinballLoss(nn.Module):
def __init__(self, quantiles, device="cpu"):
super().__init__()
self.quantiles = torch.FloatTensor(quantiles).to(device)
def forward(self, pred_values, actual_values):
error = actual_values - pred_values
# ピンボール損失の計算: max((q-1)*residual, q*residual)
M = torch.max((self.quantiles - 1) * error, self.quantiles * error)
loss = M.mean()
return loss
コードのポイント
- ピンボール関数と損失関数については、第17回を参考にしてください。
- PinballLossは、大まかに分位点の重みを考慮した絶対誤差平均のようなものになります。
# 分位点の設定
quantiles = [0.1, 0.5, 0.9]
criterion = PinballLoss(quantiles=quantiles, device=device)
optimizer = torch.optim.AdamW(model.parameters())
2.4 変数更新のループ
LOOPで指定した回数
- y=model(x) で予測値を求め、
- criterion(y, t_train) で指定した損失関数を使い予測値と教師データの損失を計算、
- 損失が小さくなるようにoptimizerに従い全結合層の重みとバイアスをアップデート
を繰り返します。
LOOP = 10_000
model.train()
for epoch in range(LOOP):
optimizer.zero_grad()
y = model(x_train)
loss = criterion(y,t_train)
if (epoch+1)%1000 == 0:
print(epoch,"\tloss:", loss.item())
loss.backward()
optimizer.step()
forループで変数を更新することになります。損失の減少を観察しながら、学習回数や学習率を適宜変更することになります。ここまでで、基本的な学習は終わりとなります。回数などは損失の減少を見ながら適当
〜に判断しましょう。
2.5 📈 検証
テストデータ x_test と t_test を利用して学習結果のテストとなります。x_testをmodelに入れた値 y_test = model(x_test) が予測値となります。グラフを利用して視覚的に検証!1期ずれたナイーブ予測よりも改善しているかな![]()
![]()
![]()
import matplotlib.pyplot as plt
import japanize_matplotlib
model.eval()
y_test = model(x_test)
prediction = y_test.detach().cpu().numpy()
real = t_test.detach().cpu().numpy()
y_train = t_train.detach().cpu().numpy()
e = 100
fig, ax = plt.subplots(figsize=(15,8))
#ax.plot(real[:e], label="real", marker="^")
ax.scatter(range(e), real[:e]*scaling_factor, label="real", alpha=0.5)
for i, t in enumerate(quantiles):
# 予測曲線をプロット
ax.plot(prediction.T[i][:e]*scaling_factor, marker=".", label=t, alpha=0.5)
# グラフのタイトルとラベルを設定
ax.set_title(f"100期間中0〜{e-1}期間目まで表示 日経225予測")
ax.set_xlabel("時刻")
ax.set_ylabel("円")
ax.grid(axis="y")
ax.legend()
plt.show()
100期間だとグラフの差がわかりにくいのでテスト用データの0期から49期でグラフ判定!青色の○がテストデータの実測値、3本の折れ線が、上から分位点0.9、0.5、0.1の予測線となります。訓練データで学習した予測線ですが、80%の予測区間の中にテストデータがぼぼ収まっていることが確認できます。
図:0期〜49期での予測値(80%予測区間)
中央値を利用した指標を計算
分位点回帰は一定の範囲内に収まっていることが重要なポイントだと思われるのですが、一応参考までに指標でも結果を検証してみました。
分位点0.5の結果を利用して、第9回のLSTMモデルや1期ずれのナイーブモデルと比較してみました。ここで登場する指標については第9.5回を参考にしてください。
基本的な指標
| 指標 | 今回のモデル | LSTM4変数 | ナイーブモデル | |
|---|---|---|---|---|
| MAE | 平均絶対誤差 | 0.0237 | 0.0257 | 0.0309 |
| RMSE | 平均平方二条誤差 | 0.0291 | 0.0332 | 0.0419 |
| MAPE | 平均絶対誤差率 | 0.6417% | 0.7023% | 0.8513% |
| R² | 決定係数 | 0.9732 | 0.9649 | 0.9435 |
| MASE | 平均絶対スケール誤差 | 0.884 < 1 | 0.915 < 1 | 1.153 |
| Direction Accuracy | 平均方向精度 | 0.687 (68.7%) | 0.687 (68.7%) | 0.571 (57.1%) |
モデル間の比較
| 指標 | 今回のモデル | |
|---|---|---|
| DM統計量 | ダイボールド・マリアーノ検定統計量(ナイーブと比較) | -2.64 |
| p値 | 0.01 < 0.05 |
- MASE<1なので今回のモデルも1期前の値で予測するナイーブモデルより良いことがわかります。
- 今回のモデルの方向性精度は、たまたまですがLSTMと同等です。
- 学習する数値を10_000で割り算しているので、10_000倍すれば、「円」の単位になります。だいたい誤差は240〜300円くらいでしょうか?日経平均は600円とか変動するとニュースになるくらいなのでもう少し誤差が小さくなると嬉しいですね。
指標的にも改善していることがわかります。これは因果畳み込みの要因が大きい!LSTMよりも因果畳み込みを利用したほうが時系列的な予測では有用なのか?
分位点を0.2と0.8に置き換えて推定したグラフが下図になります。
図:0.2〜0.8分位点(60%予測区間)
テストデータの100期間を一度に表示しているのでわかりにくいですが、中央値から上下30%の範囲内で挙動が捉えられていることがわかりますただし、見た目では良さそうなのですが、60%予測区間なので、テストデータの値が分位点0.2以上0.8以下に収まっている割合は、60%程度です![]()
次回
少し変わった内容ですが、混合密度ネットワークやベイズ回帰をニューラルネットワーク化するあたりかな?それとも自然言語処理方面へ移ろうかな?まだ完全に未定です。ただ、準備にやや時間がかかりそうな予感![]()
目次ページ
