はじめに
前回はランダムフォレストを使って、桜の満開を予測しました。
結果は(個人的には)まずまずの結果かと思います。
今回はランダムフォレストの代わりに、LSTMを使用します。
使用するデータ
使用するデータは前回と同様に、
- 気象庁の桜の満開日のデータ
- OpenWeatherのOne Call API 3.0(ドキュメントはこちら)の過去の天気情報
を使用して検証します。
LSTM
LSTMとはRNNの改良版の機械学習アルゴリズムです。(かなりざっくりですが)
以前、気温をLSTMで予測するということを試したので、そこでLSTMについて詳しく書いています。(記事はこちら)
では、LSTMを実装していきます。
まずは特徴量データを用意します。
import numpy as np
import ast
from datetime import datetime
def extract_features(weather_data):
daily_data = ast.literal_eval(weather_data) # 文字列をリストに変換
print(datetime.utcfromtimestamp(daily_data[0]["dt"]))
return {
'date': datetime.utcfromtimestamp(daily_data[0]["dt"]),
'temp': daily_data[0]["temp"],
'feels_like': daily_data[0]["feels_like"],
'pressure': daily_data[0]["pressure"],
'humidity': daily_data[0]["humidity"],
'dew_point': daily_data[0]["dew_point"],
'dew_point': daily_data[0]["dew_point"],
'clouds': daily_data[0]["clouds"],
'wind_speed': daily_data[0]["wind_speed"],
'wind_deg': daily_data[0]["wind_deg"],
}
features_df = combined_df['data'].apply(extract_features)
features_df = pd.DataFrame(features_df.tolist())
次に学習用のデータを用意します。
LSTMのシーケンス長は110日にしました。
import pandas as pd
import numpy as np
from datetime import datetime
import torch
import torch.nn as nn
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
# 前処理
# 年・月・日を追加し、1月〜4月に絞る
features_df['date'] = pd.to_datetime(features_df['date'])
features_df['year'] = features_df['date'].dt.year
features_df['month'] = features_df['date'].dt.month
features_df['day'] = features_df['date'].dt.day
features_df = features_df[features_df['month'].between(1, 4)]
# 特徴量を標準化
feature_cols = [col for col in features_df.columns if col not in ['date', 'year', 'month', 'day']]
scaler = StandardScaler()
features_df[feature_cols] = scaler.fit_transform(features_df[feature_cols])
# 満開日(ターゲット)と年情報の取得
tokyo_df['満開の日'] = pd.to_datetime(tokyo_df['満開の日'])
tokyo_df['year'] = tokyo_df['満開の日'].dt.year
tokyo_df['day_of_year'] = tokyo_df['満開の日'].dt.dayofyear
y_map = tokyo_df.set_index('year')['day_of_year'].to_dict()
# 年ごとの時系列データを生成
SEQUENCE_LENGTH = 110
X_seq, y_seq, years_seq = [], [], []
for year, group in features_df.groupby('year'):
group_sorted = group.sort_values('date')
if len(group_sorted) >= SEQUENCE_LENGTH and year in y_map:
values = group_sorted[feature_cols].values
X_seq.append(values[:SEQUENCE_LENGTH]) # ← 最初の90日間だけを使う
y_seq.append(y_map[year]) # 満開日
years_seq.append(year)
X_seq = np.array(X_seq)
y_seq = np.array(y_seq)
LSTMを使用した学習の準備をします。
2020年からテストデータとして評価に使用します。
class BlossomDataset(torch.utils.data.Dataset):
def __init__(self, X, y, years):
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
self.years = torch.tensor(years, dtype=torch.int)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx], self.years[idx]
dataset = BlossomDataset(X_seq, y_seq, years_seq)
# 年で train/test 分割
train_idx = [i for i, y in enumerate(years_seq) if y < 2020]
test_idx = [i for i, y in enumerate(years_seq) if y >= 2020]
train_dataset = torch.utils.data.Subset(dataset, train_idx)
test_dataset = torch.utils.data.Subset(dataset, test_idx)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=4, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=4, shuffle=False)
# LSTMモデル
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size=64):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, 1)
def forward(self, x):
_, (hn, _) = self.lstm(x)
return self.fc(hn[-1])
model = LSTMModel(input_size=len(feature_cols))
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
学習を行います。
エポック数は最大で1,000を設定していますが、損失関数が0.05を下回ったら学習をやめるように設定しています。
early_stop_threshold = 0.05
for epoch in range(1000):
model.train()
for X_batch, y_batch, _ in train_loader: # 年は無視
optimizer.zero_grad()
output = model(X_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
if epoch % 50 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
# ある損失以下なら中断
if loss.item() < early_stop_threshold:
print(f"Early stopping at epoch {epoch}, loss: {loss.item():.4f}")
break
モデルの評価を行います。
結果も表示させています。
# 評価
model.eval()
y_true, y_pred, y_years = [], [], []
with torch.no_grad():
for X_batch, y_batch, year_batch in test_loader:
pred = model(X_batch)
y_true.extend(y_batch.numpy().flatten())
y_pred.extend(pred.numpy().flatten())
y_years.extend(year_batch.numpy().flatten())
# 結果表示
for year, doy in zip(y_years, y_pred):
predicted_date = datetime(year, 1, 1) + pd.to_timedelta(doy - 1, unit='D')
print(f"{year}年の予測満開日: {predicted_date.strftime('%Y-%m-%d')}")
結果
実行結果は以下のようになりました。
まずは、学習時の損失関数の推移については以下です。
Epoch 0, Loss: 8122.0400
Epoch 50, Loss: 3703.0417
Epoch 100, Loss: 1634.8743
Epoch 150, Loss: 432.9744
Epoch 200, Loss: 30.7069
Epoch 250, Loss: 8.4507
Epoch 300, Loss: 106.8499
Epoch 350, Loss: 26.9567
Epoch 400, Loss: 6.4260
Epoch 450, Loss: 108.6270
Epoch 500, Loss: 47.5118
Epoch 550, Loss: 59.1875
Early stopping at epoch 587, loss: 0.0033
予測結果は以下のようになりました。
2020年の予測満開日: 2020-04-01
2021年の予測満開日: 2021-04-02
2022年の予測満開日: 2022-04-02
2023年の予測満開日: 2023-04-02
2024年の予測満開日: 2024-04-01
実際の値は以下です。
2020-03-22
2021-03-22
2022-03-27
2023-03-22
2024-04-04
- 2020年 ⇒ 9日の誤差
- 2021年 ⇒ 10日の誤差
- 2022年 ⇒ 5日の誤差
- 2023年 ⇒ 10日の誤差
- 2024年 ⇒ 3日の誤差
一週間以上外れてしまっている結果が3年出てしまっていますね。
うーん、、、って感じです。
最後に
LSTMの結果は少しイマイチな結果に終わりました。
パラメータを変えれば改善はできる可能性を秘めているので、次回以降改善していければいいなと思っています。