時系列データとは、その名の通り、時間と共に変化するデータのことである。
例えば、24時間の気温、1ヶ月の様々な商品の価格、1年間の特定企業の株価などです。
LSTM(Long Short Term Memory Networks)などの高度な深層学習モデルは、時系列データのパターンを捉えることができるため、データの将来の傾向に関する予測に利用することが可能です。
この記事では、LSTMアルゴリズムを使って、時系列データを使って将来の予測を行う方法を紹介します。
以前の記事で、将来の株価を予測するために、KerasライブラリのLSTMを使って時系列分析を行う方法を説明しました。
今回は、ディープラーニングによく使われるPythonライブラリの1つであるPyTorchライブラリを使用します。
先に進む前に、プログラミング言語Pythonの中級レベルの習熟度があり、PyTorchライブラリがインストールされていることを前提にしています。
また、機械学習の基本的な考え方やディープラーニングの概念についてのノウハウがあれば、より効果的です。
PyTorchをインストールしていない場合は、以下のpipコマンドでインストールすることができます。
$ pip install pytorch
データセットと問題定義
今回使用するデータセットは、PythonのSeabornライブラリに組み込まれています。
まず、必要なライブラリをインポートし、次にデータセットをインポートします。
import torch
import torch.nn as nn
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
Seabornライブラリに組み込まれているデータセットの一覧を表示してみよう。
sns.get_dataset_names()
出力してみましょう。
['anscombe',
'attention',
'brain_networks',
'car_crashes',
'diamonds',
'dots',
'exercise',
'flights',
'fmri',
'gammas',
'iris',
'mpg',
'planets',
'tips',
'titanic']
これから使うデータセットは flights
データセットです。
このデータセットをアプリケーションにロードして、どのように見えるか見てみましょう。
flight_data = sns.load_dataset("flights")
flight_data.head()
出力
このデータセットには 3 つのカラムがあります。
年、月
、そして乗客である。
乗客数の列には、指定された月に移動した乗客の総数が含まれている。
データセットの形状をプロットしてみよう。
flight_data.shape
出力
出力:“`
(144, 3)
データセットには144の行と3つの列があり、12年間の乗客の移動記録が含まれていることがわかります。
タスクは、最初の132ヶ月を基に、過去12ヶ月に旅行した乗客の数を予測することである。つまり、最初の132ヶ月のデータがLSTMモデルの学習に使用され、最後の12ヶ月の値を使用してモデル性能が評価されます。
月ごとの乗客の移動の頻度をプロットしてみましょう。以下のスクリプトは、デフォルトのプロットサイズを大きくします。
fig_size = plt.rcParams[“figure.figsize”]
fig_size[0] = 15
fig_size[1] = 5
plt.rcParams[“figure.figsize”] = fig_size
そして、この次のスクリプトは、乗客数の月別頻度をプロットします。
plt.title(‘Month vs Passenger’)
plt.ylabel(‘Total Passengers’)
plt.xlabel(‘Months’)
plt.grid(True)
plt.autoscale(axis=’x’,tight=True)
plt.plot(flight_data[‘passengers’])
出力
<div class="lazyload-wrapper"<div class="lazyload-placeholder" style="height:24rem"</div</div
この出力は、何年もの間、飛行機で旅行する乗客の平均数が増加していることを示している。1年間に移動する乗客の数は変動しており、夏休みや冬休みの間は、他の時期に比べて移動する乗客の数が増えるので、これは理にかなっています。
### データ前処理
このデータセットのカラムの型は、以下のコードで示されるように `object` である。
flight_data.columns
出力
Index([‘year’, ‘month’, ‘passengers’], dtype=’object’)
最初の前処理は、`passengers` カラムの型を `float` に変更することです。
all_data = flight_data[‘passengers’].values.astype(float)
ここで、`all_data`のnumpy配列を出力すると、以下のような浮動小数点型の値が表示されるはずです。
print(all_data)
出力されます。
出力: ```
[112. 118. 132. 129. 121. 135. 148. 148. 136. 119. 104. 118. 115. 126.
141. 135. 125. 149. 170. 170. 158. 133. 114. 140. 145. 150. 178. 163.
172. 178. 199. 199. 184. 162. 146. 166. 171. 180. 193. 181. 183. 218.
230. 242. 209. 191. 172. 194. 196. 196. 236. 235. 229. 243. 264. 272.
237. 211. 180. 201. 204. 188. 235. 227. 234. 264. 302. 293. 259. 229.
203. 229. 242. 233. 267. 269. 270. 315. 364. 347. 312. 274. 237. 278.
284. 277. 317. 313. 318. 374. 413. 405. 355. 306. 271. 306. 315. 301.
356. 348. 355. 422. 465. 467. 404. 347. 305. 336. 340. 318. 362. 348.
363. 435. 491. 505. 404. 359. 310. 337. 360. 342. 406. 396. 420. 472.
548. 559. 463. 407. 362. 405. 417. 391. 419. 461. 472. 535. 622. 606.
508. 461. 390. 432.]
次に、データセットをトレーニングセットとテストセットに分割します。
LSTM アルゴリズムはトレーニングセットで学習されます。
そしてそのモデルはテストセットで予測をするために使われる。
予測値はテストセットの実際の値と比較され、学習されたモデルの性能が評価される。
最初の132レコードがモデルの学習に使用され、最後の12レコードがテストセットとして使用されます。
以下のスクリプトはデータをトレーニングセットとテストセットに分割する。
test_data_size = 12
train_data = all_data[:-test_data_size]
test_data = all_data[-test_data_size:]
テストセットとトレーニングセットの長さを表示します。
print(len(train_data))
print(len(test_data))
出力してみましょう。
132
12
テストデータを表示すると、all_data
のnumpy配列から最後の12レコードが含まれていることがわかります。
print(test_data)
出力
[417. 391. 419. 461. 472. 535. 622. 606. 508. 461. 390. 432.]
我々のデータセットは現時点では正規化されていません。
最初の年の乗客の総数は、後の年の乗客の総数よりはるかに少ないです。
時系列予測には、データを正規化することが非常に重要です。
我々はデータセットに対して最小/最大スケーリングを行い、最小値と最大値のある範囲内でデータを正規化します。
データのスケーリングには sklearn.preprocessing
モジュールの MinMaxScaler
クラスを使用します。
最小値/最大値スケーラの実装の詳細については、このリンクを参照してください。
次のコードは、最小値と最大値をそれぞれ -1 と 1 とする min/max スケーラーを使用して、データを正規化するものです。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(-1, 1))
train_data_normalized = scaler.fit_transform(train_data .reshape(-1, 1))
正規化された列車データの最初の5レコードと最後の5レコードを表示します。
print(train_data_normalized[:5])
print(train_data_normalized[-5:])
出力します。
[[-0.96483516]
[-0.93846154]
[-0.87692308]
[-0.89010989]
[-0.92527473]]
[[1. ]
[0.57802198]
[0.33186813]
[0.13406593]
[0.32307692]]
データセットの値が-1〜1の間になっていることがわかります。
ここで重要なのは、データの正規化は訓練データに対してのみ適用され、テストデータには適用されないということです。
テストデータに対して正規化を行うと、トレーニングセットからテストセットへ情報が漏れてしまう可能性があります。
PyTorchのモデルはテンソルを使って学習されるため、次のステップではデータセットをテンソルに変換します。
データセットをテンソルに変換するには、以下のように FloatTensor
オブジェクトのコンストラクタにデータセットを渡せばよい。
train_data_normalized = torch.FloatTensor(train_data_normalized).view(-1)
最後の前処理は、学習データをシーケンスとそれに対応するラベルに変換することである。
配列の長さは任意であり、ドメインの知識に依存する。
しかし、今回のデータセットでは、月別のデータがあり、1年は12ヶ月あるので、シーケンス長を12とするのが便利です。
もし日単位のデータがあれば、配列の長さは1年の日数である365が適当であったろう。
したがって、学習用の入力列の長さを12に設定する。
train_window = 12
次に、create_inout_sequences
という名前の関数を定義する。
この関数は生の入力データを受け取り、タプルのリストを返す。
各タプルの最初の要素には、12ヶ月間の乗客数に対応する12項目のリストが含まれ、2番目のタプル要素には12+1ヶ月目の乗客数など1つの項目が含まれる。
def create_inout_sequences(input_data, tw):
inout_seq = []
L = len(input_data)
for i in range(L-tw):
train_seq = input_data[i:i+tw]
train_label = input_data[i+tw:i+tw+1]
inout_seq.append((train_seq ,train_label))
return inout_seq
以下のスクリプトを実行し、学習用のシーケンスとそれに対応するラベルを作成します。
train_inout_seq = create_inout_sequences(train_data_normalized, train_window)
train_inout_seq` リストの長さを表示すると、120個のアイテムが含まれていることがわかります。
これは、トレーニングセットには132個の要素がありますが、シーケンス長は12であるため、最初のシーケンスは最初の12個のアイテムで構成され、13番目のアイテムが最初のシーケンスのラベルとなることを意味します。
それでは、train_inout_seq
リストの最初の 5 つの項目を表示してみましょう。
train_inout_seq[:5]
出力してみましょう。
[(tensor([-0.9648, -0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066,
-0.8593, -0.9341, -1.0000, -0.9385]), tensor([-0.9516])),
(tensor([-0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593,
-0.9341, -1.0000, -0.9385, -0.9516]),
tensor([-0.9033])),
(tensor([-0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341,
-1.0000, -0.9385, -0.9516, -0.9033]), tensor([-0.8374])),
(tensor([-0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000,
-0.9385, -0.9516, -0.9033, -0.8374]), tensor([-0.8637])),
(tensor([-0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385,
-0.9516, -0.9033, -0.8374, -0.8637]), tensor([-0.9077]))]
各項目はタプルで、最初の要素はシーケンスの12項目からなり、2番目のタプルの要素には対応するラベルが含まれていることがわかります。
LSTMモデルの作成
データの前処理が終わったので、いよいよモデルを学習させる。
ここでは、PyTorchライブラリの nn.Module
クラスを継承した LSTM
クラスを定義します。
PyTorchで分類モデルを作成する方法については前回の記事をご覧ください。
その記事を読めば、以下のコードで何が起こっているのかが理解できるはずです。
class LSTM(nn.Module):
def __init__(self, input_size=1, hidden_layer_size=100, output_size=1):
super().__init__()
self.hidden_layer_size = hidden_layer_size
self.lstm = nn.LSTM(input_size, hidden_layer_size)
self.linear = nn.Linear(hidden_layer_size, output_size)
self.hidden_cell = (torch.zeros(1,1,self.hidden_layer_size),
torch.zeros(1,1,self.hidden_layer_size))
def forward(self, input_seq):
lstm_out, self.hidden_cell = self.lstm(input_seq.view(len(input_seq) ,1, -1), self.hidden_cell)
predictions = self.linear(lstm_out.view(len(input_seq), -1))
return predictions[-1]
上のコードで何が起こっているのかをまとめてみます。
LSTM` クラスのコンストラクタは3つのパラメータを受け取ります。
-
input_size
: 入力に含まれる素性の数に相当する。シーケンス長は12であるが、各月には1つの値、すなわち乗客の総数しかないので、入力サイズは1になる。 -
-
hidden_layer_size
: 隠れ層の数と、各層のニューロン数を指定する。ここでは、100ニューロンからなる層を1層とする。
-
-
-
output_size
: 1ヶ月先の乗客数を予測したいので、出力サイズは1になります。
-
次に、コンストラクタで変数 hidden_layer_size
、lstm
、linear
、hidden_cell
を作成する。
LSTM アルゴリズムは 3 つの入力、すなわち、前の隠された状態、前のセルの状態、そして現在の入力を受け取る。
変数 hidden_cell
には直前の hidden 状態と cell 状態が格納される。
変数 lstm
と linear
は、LSTM と線形レイヤーを作成するために使用される。
forwardメソッドの内部では、
input_seqがパラメータとして渡され、最初に
lstmレイヤーを通過する。
lstm 層の出力は、現在の時間ステップにおける隠れ層とセルの状態であり、出力も同様である。
lstm層からの出力は
linear層に渡される。
予測された乗客数はpredictions` リストの最後の項目に格納され、呼び出し関数に返される。
次のステップは、LSTM()
クラスのオブジェクトを作成し、損失関数とオプティマイザを定義することです。
今回は分類問題を解くので、クロスエントロピー損失を使用します。
オプティマイザは adam optimizer を使用します。
model = LSTM()
loss_function = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
モデルを出力してみよう。
print(model)
出力してみましょう。
LSTM(
(lstm): LSTM(1, 100)
(linear): Linear(in_features=100, out_features=1, bias=True)
)
モデルのトレーニング
150エポックについてモデルを学習させる。
必要であれば、もっと多くのエポック数で試すことができる。
25エポックごとに損失が表示される。
epochs = 150
for i in range(epochs):
for seq, labels in train_inout_seq:
optimizer.zero_grad()
model.hidden_cell = (torch.zeros(1, 1, model.hidden_layer_size),
torch.zeros(1, 1, model.hidden_layer_size))
y_pred = model(seq)
single_loss = loss_function(y_pred, labels)
single_loss.backward()
optimizer.step()
if i%25 == 1:
print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')
print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')
出力。
epoch: 1 loss: 0.00517058
epoch: 26 loss: 0.00390285
epoch: 51 loss: 0.00473305
epoch: 76 loss: 0.00187001
epoch: 101 loss: 0.00000075
epoch: 126 loss: 0.00608046
epoch: 149 loss: 0.0004329932
PyTorchのニューラルネットワークでは、デフォルトで重みがランダムに初期化されるため、異なる値が出力される可能性があります。
予想すること
モデルの学習が完了したので、予測を開始することができます。
テストセットには過去12ヶ月間の乗客データが含まれており、モデルは12個のシーケンス長を使用して予測を行うようにトレーニングされています。
まず、トレーニングセットから最後の12個の値をフィルタリングします。
fut_pred = 12
test_inputs = train_data_normalized[-train_window:].tolist()
print(test_inputs)
出力
出力:“`
[0.12527473270893097, 0.04615384712815285, 0.3274725377559662, 0.2835164964199066, 0.3890109956264496, 0.6175824403762817, 0.9516483545303345, 1.0, 0.5780220031738281, 0.33186814188957214, 0.13406594097614288, 0.32307693362236023]
上記の値を `train_data_normalized` のデータリストの最後の12個の値と比較することができます。
最初は `test_inputs` の項目に 12 個の項目があります。for` ループの中で、これらの 12 個のアイテムは、テストセットからの最初のアイテム、つまりアイテム番号 133 についての予測を行うために使用されます。予測値は `test_inputs` リストに追加される。2 回目の繰り返しでは、最後の 12 個の項目が入力として使用され、新しい予測が作成され、それが再び `test_inputs` リストに追加される。テストセットには 12 個の要素があるので、`for`ループは 12 回実行されます。ループの最後には `test_inputs` リストに 24 個のアイテムが含まれることになります。最後の12個の項目がテストセットの予測値となる。
次のスクリプトは予測を行うために使用される。
model.eval()
for i in range(fut_pred):
seq = torch.FloatTensor(test_inputs[-train_window:])
with torch.no_grad():
model.hidden = (torch.zeros(1, 1, model.hidden_layer_size),
torch.zeros(1, 1, model.hidden_layer_size))
test_inputs.append(model(seq).item())
test_inputs` リストの長さを表示すると、24 個のアイテムが含まれていることがわかります。最後の12個の予測された項目は次のようにプリントされます。
test_inputs[fut_pred:]
出力する。
[0.4574652910232544,
0.9810629487037659,
1.279405951499939,
1.0621851682662964,
1.5830546617507935,
1.8899496793746948,
1.323508620262146,
1.8764172792434692,
2.1249167919158936,
1.7745600938796997,
1.7952896356582642,
1.977765679359436]
LSTMの学習に用いた重みに応じて異なる値を得ることができることを再度言及しておく。
学習のためにデータセットを正規化したので、予測値も正規化されます。正規化された予測値を実際の予測値に変換する必要があります。データセットを正規化するために使った min/max scaler オブジェクトの `inverse_transform` メソッドに正規化された値を渡すことで、それを行うことができます。
actual_predictions = scaler.inverse_transform(np.array(test_inputs[train_window:] ).reshape(-1, 1))
print(actual_predictions)
出力
[[435.57335371]
[554.69182083]
[622.56485397]
[573.14712578]
[691.64493555]
[761.46355206]
[632.59821111]
[758.38493103]
[814.91857016]
[735.21242136]
[739.92839211]
[781.44169205]]
では、実際の値に対して予測値をプロットしてみましょう。次のコードを見てください。
x = np.arange(132, 144, 1)
print(x)
出力
[132 133 134 135 136 137 138 139 140 141 142 143]
上のスクリプトでは、過去12ヶ月の数値を含むリストを作成しています。最初の月のインデックス値は0であり、したがって最後の月はインデックス143となる。
次のスクリプトでは、144ヶ月の総乗客数と、過去12ヶ月の予測乗員数をプロットします。
plt.title(‘Month vs Passenger’)
plt.ylabel(‘Total Passengers’)
plt.grid(True)
plt.autoscale(axis=’x’, tight=True)
plt.plot(flight_data[‘passengers’])
plt.plot(x,actual_predictions)
plt.show()
出力
<div class="lazyload-wrapper"<div class="lazyload-placeholder" style="height:24rem"</div</div
我々のLSTMによる予測はオレンジの線で描かれている。このアルゴリズムはあまり正確ではありませんが、過去12ヶ月の総旅客数の増加傾向を、時折の変動とともに捉えることができることがわかります。エポック数を増やし、LSTM層のニューロン数を増やすことで、より良いパフォーマンスが得られるかどうか試してみることができます。
出力をより良く見るために、過去12ヶ月の実際の乗客数と予測された乗客数を以下のようにプロットすることができます。
plt.title(‘Month vs Passenger’)
plt.ylabel(‘Total Passengers’)
plt.grid(True)
plt.autoscale(axis=’x’, tight=True)
plt.plot(flight_data[‘passengers’][-train_window:])
plt.plot(x,actual_predictions)
plt.show()
“`
出力
繰り返しますが、予測はあまり正確ではありませんが、アルゴリズムは、将来の月の乗客数が前の月よりも高くなるはずだという傾向を、時折の変動はあるものの、捉えることができました。
結論
LSTMはシーケンス問題を解くために最も広く使われているアルゴリズムの1つである。
この記事では、LSTMを使って時系列データを使って未来予測をする方法を見ました。
また、LSTMをPyTorchライブラリで実装し、予測結果を実際の値に対してプロットして、学習したアルゴリズムがどの程度うまく機能しているかを見る方法を見ました。