【PyTorchで深層学習】前処理の流れ（RNN、CNN）

こんにちは。深層学習のRNNとCNNにおいて、どのような前処理をしていくのかをまとめていきます！モジュールとしては、PyTorchを使用します。

# RNN
RNNは時系列データや自然言語処理等さまざまな場面で使われますが、今回は1時間ごと電力消費量というテーブルデータを例にします。
RNNの仕組みについては是非こちらをご参考ください！
[【深層学習】RNN、LSTM、GRUまとめ（仕組み、ちょっとだけ数式）](https://qiita.com/nakamin/items/e96542d4e69feb56bc73)

## 1. データをtensor型に変換する
深層学習では**テンソル型**が使われます。
多次元配列を表現するためのデータ構造となっており、ベクトルや行列もテンソル型です。
そのため、ここではまず最初に、事前にスケーリングしたデータをテンソル型へと変換します。
```python
data_train = torch.tensor(X_train_scaled, dtype=torch.float32)
data_test = torch.tensor(X_test_scaled, dtype=torch.float32)
targets_train = torch.tensor(y_train_scaled, dtype=torch.float32)
targets_test = torch.tensor(y_test_scaled, dtype=torch.float32)
```
### ポイント
- dataは説明変数、targetは目的変数
- `torch.Tenor()`を使う
- 型（`dtype`）を明示する
- Numpy配列を変換する際は`from_numpy`を使う

## 2. シーケンスデータを作る関数を定義
シーケンスとは、**一定の順序をもつデータ**のことです。
テンソル型に変換したデータをシーケンスデータの形へと整形しRNNに適した入力形式を作ることで、順序や時間依存性を学習できるようになります。
まず関数を作っていきます。
```python
def create_sequences(data, targets, sequence_length):
    sequences = []
    labels = []
    for i in range(len(data) - sequence_length):
        seq = data[i:i+sequence_length] # モデルの入力
        label = targets[i+sequence_length] # 予測すべき次の値
        sequences.append(seq)
        labels.append(label)
    return torch.stack(sequences), torch.stack(labels)
```

### ポイント
- `create_sequences関数`の作成
    - 引数：
    `data` → 説明変数
    `targets` → 目的変数
    `sequence_length` → シーケンスの長さ（指定する）
    - (データの長さ`len(data)`-シーケンスの長さ`sequence_length`)分のfor文をまわす
    - 説明変数：スタート地点からシーケンスの長さ分を`seq`として空リスト`sequences`に足していく
    - 目的変数：現在のシーケンスの次のラベルを空リスト`labels`に足していく
- `stack`メソッドで次元を増やしてテンソル同士を結合する。次元追加の必要がない場合は`torch.cat`を使う
- この関数を使うと説明変数と目的変数のシーケンスデータがそれぞれ返ってくる

## 3. 入力シーケンスの長さを定義
```python
# 過去の一定期間（24時間分）のデータを1つの入力シーケンとしてまとめる
sequence_length = 24
```
### ポイント
- ドメイン知識に基づいて適切な値を選ぶ
- 小さい場合は過去情報が不足、大きい場合は計算不可が増加

## 4. シーケンスデータの作成
さきほど定義した`create_sequences`関数を使ってシーケンスデータを作成します。
```python
train_sequences, train_labels = create_sequences(data_train, targets_train, sequence_length)
test_sequences, test_labels = create_sequences(data_test, targets_test, sequence_length)

print(train_sequences.shape)  # (n_samples, sequence_length, num_features)
print(train_labels.shape)     # (n_samples, 1)
```
### ポイント 
- モデルの入力に適した形にしないとエラーになるので、ここで確認

## 5. データローダーの作成
データローダーとは、データセットを元に、データの取り出し方法（バッチ化、シャッフル、並列処理など）を指定するものです。
```python
train_loader = DataLoader(TensorDataset(train_sequences, train_labels), batch_size=128, shuffle=True)
test_loader = DataLoader(TensorDataset(test_sequences, test_labels), batch_size=128, shuffle=False)
```

### ポイント
- バッチサイズはモデルの性能に影響するため、パラメータ調整に含まれる
- `batch_size`：1つのバッチ（かたまり）に含めるデータ数
- `shuffle=True`：学習時にはデータの偏りを防ぐためにシャッフルする

上記の処理を行った後、モデルを定義していきます。

# CNN
CNNは主に**画像認識**に使われるモデルです。
ここでは、例として、**CIFAR10**（10種類の「物体カラー写真」（乗り物や動物など）の画像データセット）を使用するとします。
CNNの仕組みについては是非こちらをご参考ください！
[【深層学習】CNNまとめ（仕組み、ちょっとだけ数式）](https://qiita.com/nakamin/items/5096924cf4460054077d)

## 1. データの変換を定義
本来、画像データにはいろいろな大きさのものがあります。今回使う画像はサイズが揃っていますが、揃っていない場合はここで定義します。その他にもモデルがデータを読み込んでくれるように変換を施していきます。
```python
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
```
### ポイント
- 複数の前処理を順番に適用するもの（sklearnのpiplineをイメージ）
- `ToTensor()`：テンソル型に変換
- `Normalize()`：標準化する((平均1,平均2,平均3)), ((標準偏差1,標準偏差2,標準偏差3)) ※3チャンネル分だが、同じ場合は一つだけ記載すればよい
- データ拡張（回転、ズーム、反転など）をしたい場合はここで設定
https://www.kikagaku.co.jp/kikagaku-blog/pytorch-torchvision/
    - 例えば、以下のように設定する
```python
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),  # 水平反転
    transforms.RandomRotation(15),     # 15度回転
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))  # 正規化
])
```

## 2. データセットの作成
今のディレクトリにdataディレクトリが完成し、そこから画像を読み込んでいくことになります。
```python
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
```

### ポイント
- `torchvision`：`torchvision.datasets`モジュールに多くの組み込みデータセットを提供しており、そこからCIFAR10のデータをダウンロード
- `root='./data'`：同じディレクトリの下層にdataディレクトリができる
- `transform`：先ほど作った一連の変換を適用する
- `download`：trueにすると、ウェブからPCにダウンロードされる（すでにデータセットがダウンロードされている場合、再ダウンロードはされない）

## 3. データローダーの作成
RNNと共通で、データセットからのデータの取得方法を指定します。
```python
# 訓練用データ
train_loader = torch.utils.data.DataLoader(train_set, batch_size=4, shuffle=True, num_workers=1)
# テスト用データ
test_loader = torch.utils.data.DataLoader(test_set, batch_size=4, shuffle=False, num_workers=1)
```
### ポイント
- 並列処理`num_workers`を設定することでデータローディングが高速化される。データの読み込み時に利用するプロセス数のため、`num_workers=os.cpu_count()`としてもOK

## 4. ラベル名の定義
```python
class_labels = train_set.classes
class_labels
# 出力結果
['airplane',
 'automobile',
 'bird',
 'cat',
 'deer',
 'dog',
 'frog',
 'horse',
 'ship',
 'truck']
```
### ポイント
- 可視化や確認のために使うので変数に代入しておく
- 今回はtrain_setオブジェクトから引っ張ってこれましたが、難しい場合は`class_labels = ("","",,)`のように自分で定義していきます

## 5. 画像の確認
どのような画像が入っているかを可視化します。
```python
class_images = {}  # クラスIDをキー、画像データを値とする辞書
for image, label in train_set:
    # 正規化解除（データが -1 ～ 1 の場合、0 ～ 1 に戻す）
    image = image / 2 + 0.5
    np_image = image.numpy()  # テンソルをNumPy配列に変換
    tr_image = np.transpose(np_image, (1, 2, 0))  # 画像の次元を (C, H, W) から (H, W, C) に変換

    # クラスIDを取得
    class_id = int(label) 

    # クラスごとに1枚の画像を収集
    if class_id not in class_images:
        class_images[class_id] = tr_image

    # 全クラス分の画像が収集できたら終了
    if len(class_images) == len(class_labels):
        break

# 画像を3×3のグリッドで表示
fig, axes = plt.subplots(2, 5, figsize=(10, 5))

# 各クラスの画像をプロット
for ax, (class_id, img) in zip(axes.ravel(), class_images.items()):
    ax.imshow(img)
    ax.set_title(class_labels[class_id])
    ax.axis("off")

# プロット領域を整える
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
```
このように出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/8256d242-df84-f5c9-1463-abdf86870eae.png)

### ポイント
- `axes`：サブプロットオブジェクトの配列
- `ravel()` ：2次元配列を1次元にフラット化 → フラット化することで、ループ処理が簡単になり、zip() で対応する画像やクラス名を割り当てやすくなる
- `tight_layout()`: プロット内の要素が重ならないように自動調整するもの

## 6. 分布の確認
今回はオープンデータであらかじめtrainとtestに分割されたデータなので必要ありませんが、本来であれば自分でtrainとtestに分割するので、クラス分布が不均衡になっていないか確認します。分布が均等でない場合、データのリサンプリングや重み付けが必要です。

```python
from collections import Counter
import matplotlib.pyplot as plt
import japanize_matplotlib

# クラスIDをカウントする
label_counts = Counter(label for _, label in train_set)

fig, ax = plt.subplots(figsize=(8,5))
ax.bar(class_labels, [label_counts[i] for i in range(len(class_labels))])
ax.set_title("クラスごとのラベル分布")
ax.set_xlabel("クラス名")
ax.set_ylabel("サンプル数")
plt.xticks(rotation=45)
plt.show()
```
こんな感じで出力されます。すべて5000件で揃っているようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/971566c1-3ce2-bc88-c9f9-17918726b2ba.png)
### ポイント
- `Counter`：クラスIDごとのカウントが保存される

## 7. 形状の確認
最後に、モデルに入力する形状と一致しているか確認します。
```python
# 1バッチ分のデータを取り出し、形状を確認
train_features_batch, train_labels_batch = next(iter(train_loader))

# データローダーから取得したバッチは特徴量(batch_size, channels, height, width)とラベル(batch_size,1)のペアとして返される
train_features_batch.shape, train_labels_batch.shape
```
### ポイント
- `iter()`：データローダーからイテレータを作成
- `next()`：イテレータから1回分のデータを取得する

上記の処理を行った後、モデルを定義していきます。

---

以上です。読んでいただきありがとうございました。