季節の推しを判別させた

# はじめに
この記事は限界開発鯖アドベントカレンダー1日目(12/1)の記事です。
遅刻しました。
ハロウィンネタは間に合いませんでした。

## 自己紹介
isso0424です。
goとtsを一生書いてるオタクです。
[Twitter](https://twitter.com/kousou4129)   [GitHub](https://github.com/isso0424)

# きっかけ
ハロウィン、クリスマス、正月のような季節のイベント、オタクになった結果これにちなんだ推しのイラストを見ることで季節を感じるようになりました。PCくんにもこの感情を教えてやりたいと思い立ったので作りました。

# 実装
今回はpytorchを用いました。
ハロウィン、クリスマス、その他の3クラスの分別を行います。
AI触るの2年ぶりで全てを忘れてるので雑なのは許してください。
ディレクトリの構造はこんな感じです。

```
main.py
# データセット用のディレクトリ
data
  - halloween
  - christmas
  - normal
```

## データを読み込む
まず、データセットからデータを読み込みます
今回は3クラスそれぞれ60枚ずつ、合計180枚のデータを用います。
ちなみに遅刻の原因はこのデータを集めてたことです。
バッチサイズは16、学習用データと検証用データの比は7:3でいきます。

```python
# 256x256の画像として読み込む
transform = transforms.Compose([transforms.Resize(256), transforms.ToTensor()])
dataset = ImageFolder("./data", transform)

sample_count = len(dataset)
train_count = int(sample_count * 0.7)
test_count = sample_count - train_count

train_dataset, test_dataset = random_split(dataset, [train_count, test_count])

batch_size = 16

train_dataloader = DataLoader(train_dataset, batch_size=batch_size)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size)
```

## ネットワークの定義
次に学習させるニューラルネットワークを定義していきます。
今回は以下の流れで通しています。(層の詳細には今回触れないので気になったら調べて見てください)
2次元畳み込み -> ReLU(活性化関数)へ通す -> プーリングでサイズ半減 -> もう1度繰り返す -> ベクトルへ変換 -> 全結合層*3 -> 3クラスへ

```python
class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.image_stack = nn.Sequential(
            nn.Conv2d(3, 8, 3),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(8, 16, 3),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
        )
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(2**6*31**2, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 3),
        )

    def forward(self, x):
        x = self.image_stack(x)
        x = self.flatten(x)
        return self.linear_relu_stack(x
```

## 損失関数・最適化関数の選択
ネットワークの定義を行ったらネットワークをモデル化し、CPUに贈ります。
この際に損失関数と最適化関数の選択を行います。
損失関数は多クラス分類に向いているソフトマックス交差エントロピー誤差、最適化関数関数はAdamを利用します。

```python
device = "cpu"
model = NeuralNetwork().to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
```

## 学習用関数の定義
データローダーに読み込んだデータを用いてモデルの学習を行います。
損失関数のデータを用いて最適化関数によりモデルの学習を勧めています。

```python
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)

    model.train()
    for batch, (x, y) in enumerate(dataloader):
        x, y = x.to(device), y.to(device)

        pred = model(x)
        loss = loss_fn(pred, y)

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

        loss, current = loss.item(), batch * len(x)
        print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
```

## 検証用関数の定義
学習用関数で学習されたモデルに検証用のデータを入れることでモデルがどれぐらいの精度であるかを確かめます。

```python
def test(dataloader, model, loss_fn):
    size = len(dataloader)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)
            pred = model(x)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).sum().item()

    test_loss /= num_batches
    correct /= size * batch_size
    print(f"Test Error \n Accuracy: {(100*correct):>0.1f}% Avg loss: {test_loss:>8f} \n")
```

## 学習用関数、検証用関数を回す
最後に、定義した関数を一定回数回すことでモデルの学習を行わせます。

```python
epochs = 30
for t in range(epochs):
    print(f"Epoch {t + 1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
```

# 完成!
という訳でできたコードがこれです。

```python
from pathlib import Path

from PIL import Image
import torch
import torchvision
from torch import nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms
from torchvision.datasets import ImageFolder

transform = transforms.Compose([transforms.Resize(256), transforms.ToTensor()])

dataset = ImageFolder("./pixiv", transform)

sample_count = len(dataset)
train_count = int(sample_count * 0.7)
test_count = sample_count - train_count

train_dataset, test_dataset = random_split(dataset, [train_count, test_count])

batch_size = 16

train_dataloader = DataLoader(train_dataset, batch_size=batch_size)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size)

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.image_stack = nn.Sequential(
            nn.Conv2d(3, 8, 3),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(8, 16, 3),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
        )
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(2**6*31**2, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 3),
        )

    def forward(self, x):
        x = self.image_stack(x)
        x = self.flatten(x)
        return self.linear_relu_stack(x)

device = "cpu"
model = NeuralNetwork().to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)

    model.train()
    for batch, (x, y) in enumerate(dataloader):
        x, y = x.to(device), y.to(device)

        pred = model(x)
        loss = loss_fn(pred, y)

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

        loss, current = loss.item(), batch * len(x)
        print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")

def test(dataloader, model, loss_fn):
    size = len(dataloader)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)
            pred = model(x)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).sum().item()

    test_loss /= num_batches
    correct /= size * batch_size
    print(f"Test Error \n Accuracy: {(100*correct):>0.1f}% Avg loss: {test_loss:>8f} \n")

epochs = 30
for t in range(epochs):
    print(f"Epoch {t + 1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)

print("Done!")
```

# 実行
実行はコマンドライン上で `python main.py` で行えます

## 実行結果
```
Epoch 1
-------------------------------
loss: 1.089609 [    0/  133]
loss: 1.125430 [   16/  133]
loss: 1.057397 [   32/  133]
loss: 1.316751 [   48/  133]
loss: 1.466839 [   64/  133]
loss: 1.003903 [   80/  133]
loss: 1.409296 [   96/  133]
loss: 0.958725 [  112/  133]
loss: 1.126692 [   40/  133]
Test Error
 Accuracy: 40.6% Avg loss: 1.113808

Epoch 2
-------------------------------
loss: 0.998735 [    0/  133]
loss: 0.971585 [   16/  133]
loss: 0.943458 [   32/  133]
loss: 0.857073 [   48/  133]
loss: 0.925722 [   64/  133]
loss: 0.927712 [   80/  133]
loss: 0.721942 [   96/  133]
loss: 0.766629 [  112/  133]
loss: 0.645847 [   40/  133]
Test Error
 Accuracy: 43.8% Avg loss: 1.026091

Epoch 3
-------------------------------
loss: 0.641363 [    0/  133]
loss: 0.626481 [   16/  133]
loss: 0.982890 [   32/  133]
loss: 0.719489 [   48/  133]
loss: 0.644581 [   64/  133]
loss: 0.794168 [   80/  133]
loss: 0.522072 [   96/  133]
loss: 0.652429 [  112/  133]
loss: 0.318842 [   40/  133]
Test Error
 Accuracy: 50.0% Avg loss: 0.944454
=====(省略)=====
Epoch 28
-------------------------------
loss: 0.002750 [    0/  133]
loss: 0.000667 [   16/  133]
loss: 0.002484 [   32/  133]
loss: 0.001230 [   48/  133]
loss: 0.076246 [   64/  133]
loss: 0.001946 [   80/  133]
loss: 0.044754 [   96/  133]
loss: 0.002072 [  112/  133]
loss: 0.000153 [   40/  133]
Test Error
 Accuracy: 60.9% Avg loss: 1.523149

Epoch 29
-------------------------------
loss: 0.002148 [    0/  133]
loss: 0.000520 [   16/  133]
loss: 0.001764 [   32/  133]
loss: 0.000820 [   48/  133]
loss: 0.052940 [   64/  133]
loss: 0.001987 [   80/  133]
loss: 0.066464 [   96/  133]
loss: 0.001716 [  112/  133]
loss: 0.000110 [   40/  133]
Test Error
 Accuracy: 59.4% Avg loss: 1.558621

Epoch 30
-------------------------------
loss: 0.001537 [    0/  133]
loss: 0.000431 [   16/  133]
loss: 0.001381 [   32/  133]
loss: 0.000679 [   48/  133]
loss: 0.055763 [   64/  133]
loss: 0.001720 [   80/  133]
loss: 0.058872 [   96/  133]
loss: 0.001472 [  112/  133]
loss: 0.000098 [   40/  133]
Test Error
 Accuracy: 60.9% Avg loss: 1.584297

Done!
```
しっかり動いていますね!
めちゃめちゃ過学習になってそうですが適当に作ったにしては許容範囲ということにします。

# 最後に
久しぶりのディープラーニングだったのでだいぶ忘れてましたね...
許容範囲内とは言いましたが流石に正解率が低すぎるので学習用データ増やして再チャレンジしたいですね。
ぜひこの機械に皆さんもPyTorch、触ってみてください。

# 参考文献
[Pytorch公式ドキュメント](https://pytorch.org/docs/stable/index.html)
[pyTorchでCNNsを徹底解説](https://qiita.com/mathlive/items/8e1f9a8467fff8dfd03c)