てきと〜にPyTorchでニューラルネット

# 各種アルゴリズムと理論
[深層学習基礎概要](https://qiita.com/cot-x/items/671ab755ce77ca10d239)に大雑把にまとめました。

# モデル構築
```python
import torch
import torch.nn as nn
import torch.nn.functional as F

class Network(nn.Module):
    def __init__(self, dim_in, dim_out, dim_inner=512):
        super().__init__()

        self.netIN = nn.Sequential(
            nn.Conv2d(dim_in, dim_inner, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(dim_inner),
            nn.ReLU(inplace=True)
        )
            
        self.netInner = nn.Sequential(
            nn.Conv2d(dim_inner, dim_inner, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(dim_inner),
            nn.ReLU(inplace=True)
        )
        
        self.netOUT = nn.Conv2d(dim_inner, dim_out, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        x = self.netIN(x)
        x = self.netInner(x)
        x = self.netOUT(x)
        x = F.adaptive_avg_pool2d(x, 1).view(x.size(0), -1) # Global Average Pooling
        return x
```
こんな感じ。
- nn.Conv2d: 畳み込み演算（2次元パーセプトロン）
- nn.BatchNorm2d: バッチノーマライゼーション（データの正規化）
- nn.ReLU: 活性化関数
- Global Average Pooling: 全レイヤの平均

# 動作確認
```python
has_cuda = torch.cuda.is_available()
device = torch.device("cuda" if has_cuda else "cpu")

x = torch.randn(1, 3, 32, 32).to(device)
network = Network(dim_in=3, dim_out=5).to(device)
print(network(x).shape)
```
### Output
```
torch.Size([1, 5])
```

# データセット準備
```python
import os
from torchvision.datasets import ImageFolder
from torchvision import transforms
from torch.utils.data import Dataset, DataLoader

dataset = ImageFolder(path, transform=transforms.Compose([
            transforms.Resize(int(size)),
            transforms.RandomCrop(size),
            transforms.ToTensor()
        ]))
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True, num_workers=os.cpu_count(), pin_memory=True)
```
とか。
- ImageFolder: ラベル毎の複数のディレクトリに画像データの入ったディレクトリから画像を読み込みDatasetを返す（Datasetを自分で実装する事も多いです）
- transforms: 画像をリサイズ等してマップする
- DataLoader: バッチサイズに分割しながらデータセットからデータを読み込む

# 誤差逆伝播
```python
import torch.optim as optim
from tqdm import tqdm

network = Network(dim_in=3, dim_out=len(dataset.classes)).to(device)
optimizer = optim.AdamW(network.parameters())
criterion = nn.CrossEntropyLoss()

network.train()
for data, label in tqdm(dataloader):
    data = data.to(device, non_blocking=True)
    label = label.to(device, non_blocking=True)
    
    y_eval = network(data)
    loss = criterion(y_eval, label)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    print(f'Loss: {loss.item()}')
```
こんな感じ。
- optim.Adam: 勾配降下法の最適化アルゴリズムの一つ
- nn.CrossEntropyLoss: ソフトマックス交差エントロピー誤差関数
- tqdm: 進捗状況のプログレスバーを表示する

## 誤差逆伝播-AMP(Automatic Mixed Precision) ver.
```python
import torch_optimizer
from torch.cuda.amp import GradScaler
from tqdm import tqdm

network = Network(dim_in=3, dim_out=len(dataset.classes)).to(device)
optimizer = torch_optimizer.Lamb(network.parameters())
scaler = GradScaler()
criterion = nn.CrossEntropyLoss()

network.train()
for data, label in tqdm(dataloader):
    data = data.to(device, non_blocking=True)
    label = label.to(device, non_blocking=True)

    with torch.autocast(device_type='cuda' if str(device).startswith('cuda') else 'cpu'):
        y_eval = network(data)
        loss = criterion(y_eval, label)
    
    optimizer.zero_grad()
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()
    
    print(f'Loss: {loss.item()}')
```
こんな感じ。
- torch.autocast: 倍精度浮動小数点数から単精度へのキャスト
- GradScaler: 単精度浮動小数点数により勾配消失が起きやすいので、誤差逆伝播で勾配計算を行う時に誤差を大きくして計算し、それを元に戻して最適化アルゴリズムを適用する処理を行う
- Lamb: AMPでミニバッチ数を大きくすると、イテレーションの大数の法則やバッチサイズの大きさの問題が出てくる。そこで、モデル全体ではなくレイヤー毎に学習率を最適化する手法のLARS、更にモーメンタムを追加考慮した最適化手法のLAMBがある

# 評価
```python
from sklearn.metrics import accuracy_score

network.eval()
y_pred = network(eval_data).argmax(dim=1)
y_pred = y_pred.cpu().detach().numpy()
accuracy_score(y_true, y_pred)
```
こんな感じ。

# 保存と読み込み
## 保存
```python
network.cpu()
torch.save(network.state_dict(), 'weight.pth')
network.to(device)
```
## 読み込み
```python
network.load_state_dict(torch.load('weight.pth', map_location=device))
```

# Note
- Batch Normalization
Batch Normalizationで正規化する時、その前の層にバイアスパラメータがあると、その分計算手順が増えて実行時間が遅くなるらしいです。
- DataLoader
```
num_workers=os.cpu_count()
```
とすると並列処理が出来て高速化するらしいです。

```
pin_memory=True
```
とするとメモリ領域がページングされなくなって高速化するらしいです。
- PyTorch JIT
テンソルへの個別操作の関数を作る時は、JIT（C++実行形式）にして高速化するといいらしいです。
```
@torch.jit.script
```
とデコレータを関数の前に付けるだけです。
- non_blocking=True
テンソルデータをGPUに転送する際、ブロッキングしないように、non_blocking=Trueを付けるといいらしいです。
```python
data = data.to(device, non_blocking=True)
```
- GANにおけるAdamの学習率と一次及び二次モーメンタム
経験則としてGANの場合、beta1=0, beta2=0.9、Generator-lr=1e-4, Discriminator-lr=4e-4などですが、
Generator及びDirscriminatorで、beta1=0.5, beta2=0.9, lr=2e-4を推奨する論文があるらしいです。[^1]
また、Adamの実装に理論的ミスがあった様で、それを訂正したAdamWが発表公開されています。[^2]

### 参考
[PyTorchでの学習・推論を高速化するコツ集](https://qiita.com/sugulu_Ogawa_ISID/items/62f5f7adee083d96a587)

[^1]: [敵対的生成ネットワーク](https://tatsy.github.io/programming-for-beginners/python/gan/)
[^2]: [AdamWにおける改善点をきちんと理解する](https://zenn.dev/bilzard/articles/adamw-demistified)