【超初心者向け】一行一行解説する画像分類CNNチュートリアル(演習Notebook付き)

# Welcome to 画像分類AI tutorial!
ここでは画像を分類するAIを作ります。コードを一つ一つ実行していくとなんと画像を勝手に分類してくれるAIができます。
Google Colab上で動く演習Notebookは[こちら](https://colab.research.google.com/github/Geson-anko/AI_Tutorials/blob/master/ImageClassification/tutorial.ipynb)から
### 対象者  
- とにかくAIを触って動かしてみたい　という方
- なにやってるかわからないから一行ずつ解説してほしい　という方
- 画像分類ってどうするの？　という方

それでは始めていきましょう！

### 目次 
1. ライブラリ
2. データ処理について
3. AIモデルの定義
4. 学習スクリプト
5. 評価スクリプト
6. 実行してみる
7. 結果をみてみる
8. 自分の画像を判別させてみよう

※今回は演算デバイスとしてCPUを使用しています。

```python
# 演算デバイスの指定。cpuのほかにcudaなどのGPUデバイスも使える。（パソコンに搭載されていれば)
# cudaのほうがAIの計算が物理的に速い
device = "cpu"
```

## ・ライブラリを使う
さあAIを作りましょう！といっても本当に処理を一から作っていったのでは埒があきません。そこで先人の知恵と道具を借りましょう。それが「ライブラリ」です。 
```import Namae```や```from Namae1 import Namae2```ですでにインストールされているライブラリを使います。これからもちょくちょく出てきます。
ちなみにPythonのライブラリの主なインストール方法は```pip install Namae```です。詳しくは[公式ドキュメント](https://docs.python.org/ja/3/installing/index.html)を参照ください。

```py
import torch # Deep Learningのためのライブラリです。
import torchvision # 画像処理のためのライブラリです。
import torchvision.transforms as transforms # 画像処理のライブラリから「変換」に特化したものを持ってきます
```
Google Colabでこのチュートリアルを実行するときは次のコードブロックを実行してライブラリをインストールしてください。

```py
!pip install torchsummaryX
```

## ・データ関する処理
AIはただそこにいるだけでできるわけではありません。ちゃんと学習するためのデータが必要です。

### データを前処理する
実はデータを集めただけではAIに読み込ませることができません。ちゃんと「型」や「形式」をそろえる必要があります。
世の中に様々な礼儀作法が存在するように、データにもそれに合わせたお作法があります。
下のコードでは前処理の方法を決めています。
今回は次のような型と形式を取ります。

型      : torch.Tensor
形式    : 各ピクセル0~1の値の範囲、RGBの3カラーチャネル、32x32の大きさの画像

```py
transform = transforms.Compose(
    [transforms.ToTensor()] # ダウンロードしてきたデータを torch.Tensorという「型」にした上で、
                            # 値の範囲を 0 ~ 1 の間に正規化する関数。
                            # ただの画像データからtorch.Tensorという型にすることで初めてAIの
                            # 処理にデータを使うことができる。
                            # 今から作るAIは、データの型と形式さえあっていれば処理することがで
                            # きる。
)
```


### データを集める
AI製作にとって切っても切れないことが、データを集めることです。入力データとその答えをできるだけたくさん集めて学習させます。  
今回はすでに集めてあるCIFAR10というデータセットを用います。入力データは画像で、答えのデータは各ラベルに対応する数字です。
>0 -> plane 
>1 -> car
>...
>9 -> truck

といったように対応付けられています
  
勉強した問題をそのままテストに出したら誰でも100点をとれてしまって意味がないように、AIにも学習用とテスト用のデータを分けます。  
CIFAR10は学習用に50000枚、テスト用に10000枚のデータが用意されています。

```py
trainset = torchvision.datasets.CIFAR10(root='./data', # このファイルと同じ場所の`data`というフォルダに学習用データを保存します。
                                        train=True, # 学習用という意味です。
                                        download=True, # ダウンロードしてきます。
                                        transform=transform, # 先ほどの前処理を適用します。
                                        )

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

classes = [# 各数字に対応するラベルのリスト
    'plane', 'car', 'bird', 'cat','deer', 
    'dog', 'frog', 'horse', 'ship', 'truck'] 
```
### データを見る
さあ集めたデータを見てみましょう。

```py
%matplotlib inline
import matplotlib.pyplot as plt # グラフなどを描画するための道具が詰まったライブラリです。

row, col = 2,5 # 2x5のグラフウィンドウを作ります。
now_index = 0
fig, axes = plt.subplots(row,col,figsize=(col*2+1,row*2+2)) # 空のグラフを作って
for r in range(row):
    for c in range(col):
        out = trainset[now_index] # データを一つ取り出します。(画像,ラベルの数字)の順番で返されます。
        img = out[0].permute(1,2,0).cpu().numpy() # matplotlib描画するための画像形式にします
        label =  out[1] # ラベルデータを取り出して
        ax = axes[r][c] # r行c列目の空のグラフを取り出して
        ax.imshow(img)  # 画像を埋め込みます。
        ax.set_title(classes[label]) # タイトルをclassesから追加して、
        now_index += 1 # 次のデータを取り出します。
plt.show() # 全体のグラフウィンドウを描画します。
```

![cifar_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528087/084e4bc3-a3a2-9aaf-3175-065b2e70ed6e.png)


### データローダーを定義する
さて、データを前処理したとしても、どのように持ってくるか決めたほうが便利です。  
一つずつなのか、３つずつなのかとか、ランダムに取り出してほしいとか、
そんな要望を実装するためのものとして、```DataLoader```というものがあります。

```py
BATCH_SIZE = 64 #一度に取り出すデータ数です。
trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE,
                                          shuffle=True)

testloader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE,
                                         shuffle=False)
```

## ・AI モデルの定義
画像分類の処理に特化したAIを定義します。
一言でAIといっても様々な方式、理論があります。単純に一つのAIが全部できれば良いのですが、現代でもそううまくいきません。  
パラメータを持つ、AIの形（モデル）を決めて行きましょう。  
下のこんなものがAIモデルです。

```py
import torch.nn as nn # ある一つの機能を持った、`Layer`というものをひとまとめにしたものです。
import torch.nn.functional as F # 関数をたくさんまとめたライブラリです。
from torchsummaryX import summary # モデルの大まかな形やパラメータ量などを算出してくれるものです。
from torch.utils.tensorboard import SummaryWriter # 計算グラフ（データの流れ）をグラフィカルに描画するためのものです。

class Net(nn.Module): # クラスは設計図のようなものです。
    def __init__(self):
        super().__init__()
        ## ここの中にレイヤーを定義していきます。######################

        self.conv1 = nn.Conv2d(in_channels=3,out_channels=8,kernel_size=(5,5)) # RGBの3カラーチャネルを8にする5x5の畳み込みレイヤーです。
        self.pool1 = nn.MaxPool2d(kernel_size=2,stride=2) # 画像を縦横1/2のサイズにするレイヤーです。
        self.conv2 = nn.Conv2d(8, 16, 5)# nn.Conv2d(in_channels=8,out_channels=16,kernel_size=(5,5))と同じ意味です。
                                        # kernel_sizeが正方形の正方形の時は一つの値で指定できます。
        self.pool2 = nn.MaxPool2d(2, 2) # 順番があっていれば名前も引数の省略もできます。
        self.fc1 = nn.Linear(in_features=16 * 5 * 5, out_features=128) # 全結合層です。16x5x5=400の入力を128に変換しています。
        self.fc2 = nn.Linear(128, 64) # nn.Linear(in_features=128, out_features=64)と同じ意味です。
        self.fc3 = nn.Linear(64, 10) # 画像を10種類に分類する問題なので、最後の層の出力数は10個です。

        ############################################################

    def forward(self, x): # さて入力データxを先ほど定義したレイヤーに通していきましょう。
        x = self.conv1(x) # self.conv1に通して、
        x = F.relu(x) # `Re`ctified `L`inear `U`nit (ReLU) という関数に通します。
        x = self.pool1(x) # self.pool1に通します。
        x = self.pool2(F.relu(self.conv2(x))) # まとめて通すこともできます。
        # この時点で3x32x32だった入力画像は、16x5x5になっています。
        x = torch.flatten(x, 1) # 全結合層の入力は400なので、データの形を平坦(flatten)にして合わせます。
        x = self.fc1(x) # Full-Connection Layer(全結合層)に通して
        x = F.relu(x) # relu関数に通して
        x = self.fc3(F.relu(self.fc2(x))) # 面倒くさいので全部ひとまとめに通して
        return x # 結果を返します。


net = Net().to(device) # 設計図から実体（インスタンス）を作り上げます。さらに同時にto(device)で演算デバイスへ送っています
writer = SummaryWriter(log_dir="runs") 
```

今回はConv2dだとかMaxPool2dだとかLayerの解説はしませんが、

```python
class ModelNoNamae(nn.Module):
     def __init__(self):
        super().__init__()
        ######################
        #ここの間にLayerを書きます。
        ######################
    
    def forward(self, x):
        ######################
        #ここの間にデータxをLayerにどう通していくかの流れを書きます。
        ######################
        return x
```
という構造でAIの形を定義できることだけは覚えておいてください。  


### モデルの可視化
実はPyTorchという深層学習ライブラリ独特の仕様で、データを流すまでモデルの形が完全に決まりません。  
そこでダミーデータを流して上げることで可視化します。

```py
dummy = torch.randn(1,3,32,32,device=device) # ダミーデータを「標準分布」からサンプルします。3x32x32の形のデータを1個とるので、1x3x32x32 -> 1,3,32,32です。
net(dummy) # これでモデルにデータを通します。
print(net) # printでも表示できます。
summary(net, dummy) # 詳しい内容を知ることができます。
writer.add_graph(net,dummy) # グラフィカルにみることができます。
writer.close() # 使い終わったらちゃんと閉じます。
```
output

```
Net(
  (conv1): Conv2d(3, 8, kernel_size=(5, 5), stride=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(8, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=400, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=64, bias=True)
  (fc3): Linear(in_features=64, out_features=10, bias=True)
)
===========================================================
          Kernel Shape     Output Shape   Params Mult-Adds
Layer                                                     
0_conv1   [3, 8, 5, 5]   [1, 8, 28, 28]    608.0    470.4k
1_pool1              -   [1, 8, 14, 14]        -         -
2_conv2  [8, 16, 5, 5]  [1, 16, 10, 10]   3.216k    320.0k
3_pool2              -    [1, 16, 5, 5]        -         -
4_fc1       [400, 128]         [1, 128]  51.328k     51.2k
5_fc2        [128, 64]          [1, 64]   8.256k    8.192k
6_fc3         [64, 10]          [1, 10]    650.0     640.0
-----------------------------------------------------------
                        Totals
Total params           64.058k
Trainable params       64.058k
Non-trainable params       0.0
Mult-Adds             850.432k
===========================================================
```
TensorBoardで計算グラフを見るためには、ターミナルからこのノートブックのフォルダ内で次のコマンドを実行してください。  
```tensorboard --logdir="runs"```   
Google Colabで実行している場合は次のコードブロックを実行してください。

```py 
%load_ext tensorboard
%tensorboard --logdir="runs"
```
output
![スクリーンショット 2022-02-19 19.03.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528087/8f02caed-5c6d-0121-71ae-7ddb947589ac.png)


## ・ 学習
先ほど定義したAIモデルを学習するための処理（関数）を書きます。  
AIモデルは学習可能(変更可能)な「パラメータ」を持ち、それを使って入力データを処理します。そして出力結果と答えを照らし合わせて、どれだけ答えに近いか（誤差や損失）を計算します。  
その損失をもとに各パラメータの値を大きく/小さくする指標である「勾配」を算出します。 
AIは答えと自分の回答との間違い（誤差や損失）をできるだけ小さくしようと学習しますので、勾配に沿って、できるだけ損失が小さくなるように、損失の山を駆け下って行きます。（最小化問題）  
そうして損失の山を駆け下り切り、損失の減少が止まったら学習終了です。 
[GIFの入手元](https://ichi.pro/python-de-no-nyu-rarunettowa-ku-no-kobai-koka-oyobi-sonshitsu-randosuke-pu-no-anime-shon-254364930818135)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528087/f676c7d1-326f-6cc1-c781-07074baf41fc.gif">

### 誤差の最小化方法の決定
ひとえに誤差（損失）を最小化するといっても、いろんな方法があります。誤差の取り方だとか、山の下り方（重みの更新方法）とかを決めましょう

```py
import torch.optim as optim # 最適化手法（山の下り方）をまとめたライブラリです。

criterion = nn.CrossEntropyLoss() # 損失関数という、出力と答えの差（損失）を計算する関数です。
optimizer = optim.Adam(net.parameters(), lr=0.001) 
# lr : Learning Rate（学習率）のこと。坂を下るスピードを調整する
```
### 学習の流れ

```py
def train(dataloader, model, loss_fn, optimizer): # 学習の流れを関数化して、
    size = len(dataloader.dataset) # データの個数を取り出します。
    model.train() # モデルを学習モードにします。
    for batch, (X, y) in enumerate(dataloader): # データをBATCH_SIZEごとに取り出して、
        X, y = X.to(device), y.to(device) # 演算デバイスに送ります。

        # Compute prediction error
        pred = model(X) # データXから予測して、
        loss = loss_fn(pred, y) # 答えとの損失を取ります。

        # Backpropagation
        optimizer.zero_grad() # optimzierを初期化して
        loss.backward() # 勾配を算出します。
        optimizer.step() # パラメータを更新します。

        # 学習途中の状態を出力します。
        if batch % 100 == 0: # 学習途中の状態を出力します。
            loss, current = loss.item(), batch * len(X) 
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
```
 
## ・評価
ちゃんとうまく学習できているか、評価する処理（関数）を書きます。

```py
def test(dataloader, model, loss_fn): # 学習の流れを関数します
    size = len(dataloader.dataset) # データの個数を取得して
    num_batches = len(dataloader) # Batchの数を取得して
    model.eval() # モデルを評価モードにします。計算効率が上がります。
    test_loss, correct = 0, 0
    with torch.no_grad(): # 勾配計算を無視するモードに入ります。勾配計算をしないので処理が軽くなります。
        for X, y in dataloader: # 一つ一つbatchを取りだして、
            X, y = X.to(device), y.to(device) # 演算デバイスに送って
            pred = model(X) # 入力をモデルに通して
            test_loss += loss_fn(pred, y).item() # 損失を足していきます。
            correct += (pred.argmax(1) == y).type(torch.float).sum().item() # 正答率を足して行きます。
    test_loss /= num_batches # 平均をとって
    correct /= size # 平均をとって
    # 表示！
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
```

## ・実行
さて、学習と評価を実行してみましょう！どんなAIができるでしょうか？

```py
epochs = 10 # EPOCH（世代数）は、データをすべて使って学習する回数のことです。
for t in range(epochs): # 世代数分ループを回して学習します。
    print(f"Epoch {t+1}\n-------------------------------")
    train(trainloader, net, criterion, optimizer) # 学習
    test(testloader, net, criterion) # 評価
print("Done!")
```
output

```
Epoch 1
-------------------------------
loss: 2.299462  [    0/50000]
loss: 2.134034  [ 6400/50000]
loss: 1.912552  [12800/50000]
loss: 1.927557  [19200/50000]
loss: 1.519216  [25600/50000]
loss: 1.591265  [32000/50000]
loss: 1.596261  [38400/50000]
loss: 1.674133  [44800/50000]
Test Error: 
 Accuracy: 41.8%, Avg loss: 1.556587 

Epoch 2

... (中略)

loss: 0.961423  [19200/50000]
loss: 1.404253  [25600/50000]
loss: 1.107436  [32000/50000]
loss: 1.046545  [38400/50000]
loss: 0.890504  [44800/50000]
Test Error: 
 Accuracy: 61.6%, Avg loss: 1.098360 

Done!
```

### 学習後のパラメータを保存する
AIの計算はとても重いですし時間がかかりますのでもし学習結果を保存し忘れたりなんてしたら...

```py
# 学習後のパラメータを保存する
torch.save(net.state_dict(),"trained_parameters.pth")

# 学習後のパラメータをloadする
#net.load_state_dict(torch.load("path/to/parameter"))
```

## ・結果を見る

```py
with torch.no_grad():
    row, col = 2,5 # 2x5のグラフウィンドウを作ります。
    now_index = 0
    fig, axes = plt.subplots(row,col,figsize=(col*2+1,row*2+2)) # 空のグラフを作って
    for r in range(row):
        for c in range(col):
            out = testset[now_index] # データを一つ取り出します。(画像,ラベルの数字)の順番で返されます。
            label =  out[1] # ラベルデータを取り出して
            img = out[0].unsqueeze(0).to(device) # 演算デバイスに送って
            pred = torch.softmax(net(img).squeeze(0),0) # 予測確率分布を出力させて
            pred_label = torch.argmax(pred).item() # その中で最も確率の高いラベルを予測ラベルとします。

            text = f"prediction: {classes[pred_label]}, {pred[pred_label]*100:>0.1f}%\nanswer: {classes[label]}"

            img = out[0].permute(1,2,0).cpu().numpy() # matplotlib描画するための画像形式にします
            ax = axes[r][c] # r行c列目の空のグラフを取り出して
            ax.imshow(img)  # 画像を埋め込みます。
            ax.text(0,45,text)

            now_index += 1 # 次のデータを取り出します。
plt.show() # 全体のグラフウィンドウを描画します。
```

![pred.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528087/07b843b6-3044-28a8-19d7-fd08ac2ea1da.png)

### 自分の画像を判別させてみよう！
AIができました！それでは自分でとってきた画像を判別させてみましょう！
Google colabで実行している方は左側のファイルタブより、自分の好きな画像をcolabのストレージにアップロードしてください。

```py
path = "data/dog.png" # 判別したい画像のファイルパスを指定してください
img = torchvision.io.read_image(path) # 画像を読み込んで
img = torchvision.transforms.functional.resize(img,size=(32,32)) # 32 x 32にリサイズします。
img = img[:3] # RGB チャネルだけにします。
img = img/255 # データの値を 0 ~ 1 にして
plt.imshow(img.permute(1,2,0)) # 表示するための形式に直して、埋め込んで
plt.title(path) # タイトルつけて
plt.show() # 表示！
```
Example output
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528087/5099ed72-1b5f-829b-d1e9-6f64bc4f162f.png width="200">

```py
with torch.no_grad(): # 推論のみなので勾配計算を止めて
    img = img.to(device) # 演算デバイスに送って
    img = img.unsqueeze(0) # batchの軸を足して
    pred_prob = net(img).squeeze(0) # モデルに流して、さっき足したbatchの次元をとって
    label = torch.argmax(pred_prob).item() # 予測ラベルの番号を取得
    print("Your image class is",classes[label]) # 表示!
```
output

```
Your image class is dog
```


これにて画像分類チュートリアルはおしまいです。最後まで見てくださりありがとうございました！

# 参照
学習フロー: 
https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html  
画像認識について: 
https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html