RNN・LSTMのすゝめ

# はじめに
こんにちは！
僕は研究を行いながら、長期インターンでデータサイエンティストとして働く大学院生です！

学部時代から長期インターンを始め、現在まで４社経験してきました。
この経験から、プログラミングの学習を始めたばかりの人や、長期インターンを行う勇気が出ない人に、学習サポートやデータ分析の実績作り支援、さらにはデータはあるけど、活用する人材がいない企業むけにデータ分析案件を受注しております。

僕自身、プログラミングの習得や長期インターン探しに苦労したので、その経験をお伝えすることで、より多くの人が挫折せずデータサイエンティストになるまで成長して欲しいです！

Xでタランチュラとして、サポートを行なっているのでご興味ある方はご連絡ください！学生・社会人問わず専攻も問わずサポートいたします！

### X(Twitter)
https://x.com/DS_school_1



これまで機械学習アルゴリズムやデータサイエンティストに内定するまでに行ったこと、統計学実践ワークブックの解説などQiitaで記事にしているので、興味ある方はぜひ読んでみてください！

今回は、時系列解析でよく利用される深層学習手法の、<font color='red'>**RNN・LSTM**</font>に関して解説します👍
ニューラルネットワークは、以下の記事で解説しているので、理解が浅い人は、先にこちらを読んでいただけると幸いです🙇

https://qiita.com/tarantula426/items/ca9383c2161ab8fd8e73


# RNNアルゴリズム
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584936/4c8ea9dc-cfb4-1fdf-2d2b-de0ee5e62a7f.png)

**リカレントニューラルネットワーク**（RNN）は、時系列データやシーケンスデータの処理に特化したニューラルネットワークの一種です。RNNは、入力データの時間的依存性をモデル化するために、隠れ状態（hidden state）を用いて過去の情報を保持し、次のステップに影響を与えることができます。この章では、RNNの基本的な仕組みとアルゴリズムについて詳しく説明します。

## 数式によるRNNの計算
RNNは入力層、隠れ層、出力層から成り立ちます。
### 入力層
入力データ$x_t$を受け取る層です。
### 中間層
前のタイムステップの隠れ状態$h_{t-1}$と現在の入力データ$x_t$を用いて、新しい隠れ状態$h_t$を計算します。
### 出力層
隠れ状態$h_t$を用いて出力$y_t$を計算します。

RNNの各タイムステップにおける計算は以下の数式で表されます。
**隠れ状態の更新**：

```math
h_t=tanh(W_hx_t+U_hh_{t-1}+b_h)
```
**出力の計算**:
```math
y_t=W_yh_t+b_y
```
+ $W_h$, $U_h$は重み行列
+ $b_h$はバイアス項
+ tanhは活性化関数(ここではハイパボリックタンフェンと関数)を示します。

## 隠れ状態の役割
RNNの特徴的な点は、隠れ状態$h_t$が過去の情報を保持し、それを次のタイムステップに引き継ぐことです。これにより、RNNは過去の入力データの情報を利用して現在の出力を決定することができます。しかし、これは短期的な依存関係のモデリングには効果的ですが、長期的な依存関係の学習には問題があります。

## 誤差逆伝播法による学習
RNNの学習は、誤差逆伝播法（Backpropagation Through Time, BPTT）によって行われます。BPTTは、時系列データの各タイムステップに対して誤差を逆伝播し、重み行列を更新します。具体的には、以下のようにして勾配を計算します。
### 損失関数による出力誤差
```math
L=\sum_{t=1}^T{L_t(y_t,\hat{y_t})}
```
ここで$\hat{y_t}$は真の出力、$L_t$は時刻$t$における損失です。
### 勾配計算
BPTTでは、時間を遡って勾配を計算します。各時刻$t$における隠れ状態$h_t$に対する損失の勾配は、次のように計算されます。
```math
\frac{\partial{L}}{\partial{h_t}}=\frac{\partial{L}}{\partial{y_t}}\frac{\partial{y_t}}{\partial{h_t}}+\sum_{k=t+1}^T{\frac{\partial{L}}{\partial{h_k}}\frac{\partial{h_k}}{\partial{h_t}}}
```
具体的には以下のステップを踏みます。
+ **出力層の勾配計算**
```math
\displaylines{
\frac{\partial{L}}{\partial{y_t}}=\frac{\partial{L_t}}{\partial{y_t}}\\
\frac{\partial{L}}{\partial{W_{hy}}}=\sum_{t=1}^T{\frac{\partial{L_t}}{\partial{y_t}}\frac{\partial{y_t}}{\partial{W_{hy}}}}=\sum_{t=1}^T{\frac{\partial{L_t}}{\partial{y_t}}h_t}
}
```
+  **隠れ層の勾配計算**
次に、隠れ状態$h_t$に対する勾配を計算します。損失$L$に対する隠れ状態の勾配は次のように表されます。
```math
\frac{\partial{L}}{\partial{h_t}}=\frac{\partial{L}}{\partial{y_t}}\frac{\partial{y_t}}{\partial{h_t}}+\sum_{k=t+1}^T\frac{\partial{L_k}}{\partial{h_k}}\frac{\partial{h_k}}{\partial{h_t}}
```
ここで、次のように分解できます。
```math
\displaylines{
\frac{\partial{y_t}}{\partial{h_t}}=W_{hy}\\
(y_t=W_{hy}h_t+b_yより)
}
```
また、隠れ状態の更新は次のようになります。
```math
h_t=σ(W_{hx}x_t+W_{hh}h_{t-1}+b_h)→\frac{\partial{h_t}}{\partial{h_{t-1}}}=σ'(W_{hx}x_t+W_{hh}h_{t-1}+b_h)W_{hh}
```
従って、隠れ層の勾配は次のように計算されます。
```math
\frac{\partial{L}}{\partial{h_t}}=\frac{\partial{L_t}}{\partial{y_t}}W_{hy}+\sum_{k=t+1}^T\frac{\partial{L_k}}{\partial{h_k}}W_{hh}σ'(W_{hx}x_t+W_{hh}h_{t-1}+b_h)
```
この式を用いて、時間を遡りながら遡りながら各時刻に対する勾配を計算し、すべてのパラメータ($W_{hx}, W_{hh}, W_{hy}, b_h, b_y$)を更新します。


# RNNの問題点
RNNは時系列データやシーケンスデータを扱う際に非常に有用なモデルですが、いくつかの重大な問題点があります。この章では、RNNの代表的な問題点について詳しく解説します。
+ **長期依存の学習の困難さ**
RNNは隠れ状態$h_t$を利用して過去の情報を保持し、それを次のタイムステップに引き継ぐことでシーケンスデータを処理します。しかし、シーケンスが長くなるにつれて、初期のタイムステップの情報が後のタイムステップに伝わりにくくなるという問題があります。これを 長期依存性の問題 と呼びます。
この問題の主要な原因は、バックプロパゲーションを通じた勾配の消失または爆発です。具体的には、誤差逆伝播法（BPTT）を用いて勾配を計算する際に、勾配が指数関数的に減少する（勾配消失）か、増加する（勾配爆発）ことが起こります。これにより、重みの更新がうまくいかず、ネットワークが適切に学習できなくなります。

+ **計算の非効率性**
RNNはタイムステップごとに隠れ状態を更新するため、長いシーケンスデータに対しては計算負荷が大きくなります。各タイムステップの計算が次のタイムステップに依存するため、並列処理が困難であり、計算時間が長くなるという問題があります。

+ **勾配爆発問題**
勾配消失問題の反対に、勾配が指数関数的に増加する現象を**勾配爆発**と呼びます。これが起こると、モデルのパラメータが非常に大きな値になり、数値的に不安定になってしまいます。勾配爆発を抑えるためには、勾配クリッピングと呼ばれる手法を用いて、勾配の大きさを一定の範囲内に収める必要があります。

RNNは時系列データやシーケンスデータを扱う際に非常に有用ですが、長期依存性の学習の困難さ、計算の非効率性、勾配消失および勾配爆発問題、過学習のリスクなどの問題点があります。これらの問題を解決するために、LSTM（Long Short-Term Memory）という改良モデルが提案されました。次章では、LSTMアルゴリズムについて詳しく解説します。
# LSTMアルゴリズム
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584936/8971ee0b-954d-1c39-da64-1219f89ed8a2.png)

LSTM（Long Short-Term Memory）は、RNNの長期依存性の問題を解決するために設計された特殊なリカレントニューラルネットワークです。LSTMは、シーケンスデータを効果的にモデル化するために、情報の保持と忘却を制御するゲート機構を導入しています。この章では、LSTMアルゴリズムの詳細と、各ゲートの役割について数式を用いて丁寧に解説します。
### LSTMの基本構造
LSTMは、以下の3つのゲートと1つのセル状態（cell state）を持っています。
1. **入力ゲート(Input Gate)**
1. **忘却ゲート(Forget Gate)**
1. **出力ゲート(Output Gate)**

これらのゲートは、LSTMセルが情報をどのように更新し、次のタイムステップに引き継ぐかを制御します。
LSTMの各タイムステップにおける計算は、以下の数式で表されます。
### 忘却ゲート
過去のセル状態$C_{t-1}$の情報をどの程度忘れるかを決定します。
```math
f_t=σ(W_fx_t+U_fh_{t-1}+b_f)
```
### 入力ゲート
現在の入力$x_t$をどの程度セル状態に反映するかを決定します。
```math
i_t=σ(W_ix_t+U_ih_{t-1}+b_i)
```
### セル状態の更新
新しい候補値$\hat{C_t}$を計算し、入力ゲートと忘却ゲートを組み合わせてセル状態を更新します。
```math
\displaylines{
\hat{C_t}=tanh(W_Cx_t+U_Ch_{t-1}+b_C)\\
C_t=f_t\odot C_{t-1}+i_t	\odot\hat{C_t}
}
```
### 出力ゲート
新しい隠れ状態$h_t$を決定します。
```math
\displaylines{
o_t=σ(W_ox_t+U_oh_{t-1}+b_o)\\
h_t=o_t \odot tanh(C_t)
}
```
ここで、
+ $σ$はシグモイド関数
+ $tanh$はハイパボリックタンジェント関数
+ $W$と$U$はそれぞれ入力と隠れ状態に対する重み行列
+ $b$はバイアス項
+ $\odot$は要素ごとの積

を示します。
### ゲートの役割
#### 忘却ゲート
忘却ゲートは、過去のセル状態の情報をどの程度保持するかを決定します。シグモイド関数$σ$の出力は0から1の範囲にあり、これによりセル状態の一部を「忘れる」ことができます。これにより、不要な情報を除去し、重要な情報のみを保持することができます。

#### 入力ゲート
入力ゲートは、現在の入力$x_t$をどの程度新しいセル状態に反映するかを決定します。シグモイド関数を使用して入力の重要性を評価し、重要な情報をセル状態に追加します。

#### セル状態の更新
セル状態は、長期的な依存性を保持する役割を果たします。忘却ゲートと入力ゲートの組み合わせにより、重要な情報を選択的に更新します。これにより、勾配消失問題を緩和し、長期依存性を効果的に学習できるようになります。

#### 出力ゲート
出力ゲートは、新しい隠れ状態$h_t$を決定します。これにより、セル状態の情報を適切に反映し、次のタイムステップに伝えることができます。

### RNNの問題点の解決
+ **長期依存性の学習の困難さ**
セル状態とゲート機構により、重要な情報を長期間にわたって保持し、勾配消失問題を緩和します。
+ **計算の非効率性**
ゲート機構による選択的な情報更新により、重要な情報のみを効率的に処理し、不要な計算を減少させます。
+ **勾配爆発問題**
勾配クリッピングや正則化手法と組み合わせて使用することで、勾配爆発を防ぎます。

# 実装
気象データの1日の平均気温を予測するLSTMモデルをPytorchを用いて実装します。
```python:model.py
import torch
import torch.nn as nn

class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(LSTMModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        return out
```
```python:run.py
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from sklearn.preprocessing import MinMaxScaler
from model import LSTMModel

# Dataset class
class TimeSeriesDataset(Dataset):
    def __init__(self, data, window_size):
        self.data = data
        self.window_size = window_size

    def __len__(self):
        return len(self.data) - self.window_size * 2  # 修正: データの範囲を正しく設定

    def __getitem__(self, idx):
        return (self.data[idx:idx+self.window_size], 
                self.data[idx+self.window_size:idx+self.window_size*2])

# Data preprocessing
df = pd.read_csv('weather.csv')
df['ave_temperature'] = df['ave_temperature'].interpolate()
df['date'] = pd.to_datetime(df[['year', 'month', 'day']])
df = df[['date', 'ave_temperature']]
df.set_index('date', inplace=True)

scaler = MinMaxScaler()
scaled_data = scaler.fit_transform(df.values)
window_size = 7

dataset = TimeSeriesDataset(scaled_data, window_size)
train_loader = DataLoader(dataset, batch_size=16, shuffle=True)

# Model, loss, and optimizer
input_size = 1
hidden_size = 50
num_layers = 2
output_size = 7

model = LSTMModel(input_size, hidden_size, num_layers, output_size).float()
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Training
num_epochs = 100
for epoch in range(num_epochs):
    for sequences, targets in train_loader:
        sequences = sequences.float()
        targets = targets.float()


        outputs = model(sequences)
        
        loss = criterion(outputs, targets.squeeze())  # targetsの次元を合わせる

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# Predicting
model.eval()
with torch.no_grad():
    test_data = scaled_data[-window_size*2:]
    test_sequences = torch.tensor(test_data[:window_size]).float().unsqueeze(0)
    predictions = model(test_sequences)
    predictions = predictions.numpy().flatten()
    predictions = scaler.inverse_transform(predictions.reshape(-1, 1))

print("Predictions:", predictions)

```

# おわりに
最後まで読んでいただきありがとうございました！
少しでもデータサイエンスを学ぶ方の一助となればと思います。
いいなと思った方は「いいね」お願いいたします。

もし僕の活動にもご興味を持っていただけたら、X(Twitter)もフォローしていただけると嬉しいです！

### X(Twitter)
https://x.com/DS_school_1

# 参考文献
+ 「CVMLエキスパートガイド」https://cvml-expertguide.net/terms/dl/rnn/
+ 「CVMLエキスパートガイド」https://cvml-expertguide.net/terms/dl/rnn/lstm/