🔰PyTorchでニューラルネットワーク基礎 #04 【画像分類・畳み込み層】

# 概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第４回は画像の分類問題になります。

![あいうえお.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/d95558f6-25ff-45c5-9b63-fe1a2e8349db.jpeg)

画像分類の第一歩といえば「0〜9の手書き数値データ」であるMNISTが有名です。今回はMNISTに類似したグレースケールの「あいうえお」画像を利用して画像分類の初歩をまとめていきます。「あ・い・う・え・お」の5種類に分類された「28ピクセル」×「28ピクセル」のひらがな画像データを用いた画像分類の簡単なネットワークを構築していきます[^1]。

[前回「数値データの分類問題」](https://qiita.com/AzukiImo/items/01b348f5beb3d2979d44)からの追加箇所は大きく１箇所
* CNN（Convolutional Neural Network）：**nn.Conv2d()** を使う




:::note
**方針** 
1. できるだけ同じコード進行
1. できるだけ簡潔（細かい内容は割愛）
1. 特徴量などの部分，あえて数値で記入（どのように変わるかがわかりやすい）
:::




### 演習用のファイル（Githubからダウンロード）
* [データのファイル](https://github.com/azukiimo/qiita/blob/main/data/aiueo.npz)
* [コード sample_04.ipynb](https://github.com/azukiimo/qiita/blob/main/sample_04.ipynb)


# 1. 分類問題
分類問題とは、データをカテゴリー・クラスに振り分ける問題となります:sweat:。画像の分類はその典型的な例となります。代表的なデータセットを４つまとめておきます。

|データセット名|内容|分類数|カラー|
|------------|---|-----:|------|
|MNIST | 0〜9の手書き数値| 10 |グレースケール|
|Fashin-MNIST | 靴・服などファッション画像|10|グレースケール|
|CIFAR10|32x32の飛行機、トラックなどの画像|10|３チャンネルRGB|
|CIFAR100|CIAFR10の増強版|100|３チャンネルRGB|

いずれも、PyTorchのDatasetsライブラリから利用することが可能です。

https://pytorch.org/vision/main/datasets.html

PyTorchによるプログラムの流れを確認します。基本的に下記の５つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
1. データの読み込みとtorchテンソルへの変換　(2.1)
2. ネットワークモデルの定義と作成　(2.2)
3. 誤差関数と誤差最小化の手法の選択　(2.3)
4. 変数更新のループ　(2.4)
5. 検証　(2.5)

# 2. コードと解説

## 2.0 データについて

演習用に利用するデータは1チャンネルグレースケールの「あ・い・う・え・お」画像です。画像サイズはMNISTやFashon-MNISTに応用できるように28×28に調整してあります。下図が今回利用するサンプル画像となります。

　　　　　　　　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/d95558f6-25ff-45c5-9b63-fe1a2e8349db.jpeg" width=50%>

利用するデータセットはすでに（バッチサイズ、チャンネル数、高さ、横幅）、具体的には(792, 1, 28, 28)に変更してあります[^2]。バッチサイズという用語は **「一度の学習（パラメータ更新）で使用するデータの数」**、今回の場合は単純にデータ数となります。


## 2.1 データの読み込みとtorchテンソルへの変換

まず利用するライブラリを読み込みます。

~~~python: ライブラリ
import numpy as np                 # データの読み込みに利用 
import torch                       # PyTorchのライブラリ
import torch.nn as nn              # PyTorchのネットワークのライブラリ
from sklearn.model_selection import train_test_split   # データ分割時に利用
~~~

### データの読み込み
演習では``aiueo.npz``というファイル名の画像データを使用します。numpyのloadでnpz形式のファイルを読み込みましょう。**data["x"]** が画像データ、**data["t"]** がラベルIDで０〜４の整数値の教師データとなります。
* ０：画像「あ」
* １：画像「い」
* ２：画像「う」
* ３：画像「え」
* ４：画像「お」

に対応しています。scikit-learnのtrain_test_splitを利用して、学習に利用するデータとテスト（検証）に利用するデータとに分割します。

~~~python: データの読み込み
data = np.load("./aiueo.npz")
x = data["x"]     # 画像データ
t = data["t"]     # ラベル　0〜4の５種類

# numpyからtorchテンソルに変換
x = torch.FloatTensor(x)    # 入力するデータはFloat
t = torch.LongTensor(t)     # ラベル側はLong
# データの分割 
x, x_test, t, t_test = train_test_split(x,t, test_size=0.1, random_state=55)
~~~
* **np.load** : Num配列をファイルから読み込むための関数です。npy形式やnpz形式のファイルを読み込めます。
* **data["x"]** : 配列を保存した時のキーが**x**の場合、**data["x"]** で配列にアクセスできます。
* **data.keys()** : 保存してあるキーを表示できます。

画像の分類はCPUだと若干時間がかかります。GPUが利用できる場合は、データやモデルに``to("cuda")``をつけるとGPUを使った計算となります[^3]。

~~~python
x = torch.FloatTensor(x).to("cuda")
t = torch.LongTensor(t) .to("cuda")
~~~

## 2.2 ネットワークモデルの定義と作成

今回は下図のようなCNN（畳み込みネットワーク）と分類のための全結合層を利用したネットワークで分類問題を扱ってみたいと思います。



![CNNによる分類画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/0e7a0bf2-63c8-4e4a-adca-3e02daf47f6b.jpeg)


具体的には（１チャンネル、28ピクセル, 28ピクセル) の画像データ「あ・い・う・え・お」を入力データとする。CNN、一列化、全結合層を経て、５次元ベクトル（５種類の画像予測）を出力する形となります。

:::note
**畳み込み層**
**nn.Conv2d(in_channels, out_channels, kernel_size, stride)**

* in_channels : 入力されるチャンネル数
* out_channels : 出力されるチャンネル数
* kernel_size : カーネルサイズ（kernel_size $\times$ kernel_size）
* stride : カーネルの移動量
:::

詳細は[PyTorchの公式ドキュメント](https://pytorch.org/docs/stable/nn.html#linear-layers)に記載されています。ネットワークの書き方はこれまで同様クラスを利用して記述します。
　(1)　**\_\_init___()** ： 利用するネットワーク名・活性化関数をすべて記述
　(2)　**forward()** ： 実際の流れを記す


```python: ネットワークモデルの定義と作成
class DNN(nn.Module):
    def __init__(self):
        super(DNN, self).__init__()
        self.cnn1 = nn.Conv2d(in_channels=1, out_channels=20 ,kernel_size=5)
        self.act1 = nn.LeakyReLU(negative_slope=0.01)  # Leaky ReLU導入してみた
        
        self.flat = nn.Flatten()     # 行列データを一行のデータにつなげる
        self.fc1 =  nn.Linear(in_fatures=5*24*24, out_features=100)
        self.act2 = nn.ReLU()
        self.fc2 = nn.Linear(in_features=100, out_features=5)
        
    def forward(self, x):
        h1 = self.cnn1(x)
        h2 = self.act1(h1)
        h = self.flat(h2)
        h = self.act2(self.fc1(h))
        y = self.fc2(h)
        return y, h1, h2

model = DNN()
```

print(model)は、\_\_init()__に書かれているネットワーク層を上から順番に出力します。ネットワークの定義に明示的に書いていない内容も表示されます。ネットワークの途中の特徴量を可視化するために、**forward()** の戻り値を最終出力の$y$、cnnの出力$h1$、活性化関数後の出力$h2$の３種類を用意してあります。GPUを利用する場合は、``model.to("cuda")``を追加すればOKですが今回の演習はCPU仕様となっています。

~~~python:modelの構造
DNN(
  (cnn1): Conv2d(1, 5, kernel_size=(5, 5), stride=(1, 1))
  (act1): LeakyReLU(negative_slope=0.01)
  (flat): Flatten(start_dim=1, end_dim=-1)
  (fc1): Linear(in_features=2880, out_features=100, bias=True)
  (act2): ReLU()
  (fc2): Linear(in_features=100, out_features=5, bias=True)
)
~~~



### ネットワークの定義

* 入力されるデータは（1チャンネル、高さ28ピクセル、横28ピクセル）のグレースケール画像で(1, 28, 28)のテンソル
* 28x28の画像に対して、カーネルサイズ5x5のCNNを適用します。出力される特徴量の形状は24x24
* **out_channels=5**としているので24x24の特徴量が5種類となります。(5, 24, 24)のテンソル
* (5, 24, 24)を一列化(Flatten)するので、5x24x24が最初の全結合層fc1への入力となります。つまり、``Linear(in_features=5*24*24, ...)``となります。
* 出力は5種類の文字なので全結合層fc2について、``Linear(..., out_features=5)``となります。
* 今回は活性化関数として**LeakyReLU**や**ReLU**を利用してみました。\_\_init__()の活性化関数の部分を他の関数に変更することで、様々な活性化関数の効果を検証することができます。


### ネットワーク層の詳しい解説は？

CNN（畳み込み層）については、[AIcia Solid ProjectさんのYouTube解説](https://www.youtube.com/watch?v=vU-JfZNBdYU)がとても丁寧です。深層学習、強化学習など様々なものが**とてもわかりやすく丁寧に解説されています本当に感謝** :bow::bow_tone1::bow_tone2:。

www.youtube.com/watch?v=vU-JfZNBdYU

Qiitaの記事にもわかりやすい記事がたくさんあります。いくつか紹介しておきます。

https://qiita.com/tan0ry0shiny/items/34767ab4e09369f72c50

https://qiita.com/hara_tatsu/items/8dcd0a339ad2f67932e7

https://qiita.com/DaigakuinnseiNo/items/c8cf1923a84c7707d706



## 2.3 誤差関数と誤差最小化の手法の選択

分類問題では予測値$y$ と教師データ$t$の誤差を計算するのに、クロスエントロピーを用いることが一般的なようです。PyTorchでは**CrossEntropyLoss()** と記述します。

~~~python: 誤差関数と最適化
criterion = nn.CrossEntropyLoss()                          # 損失関数：cross_entropy
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)  # 学習率：lr=0.001がデフォルト
~~~

モデルの出力は「あ・い・う・え・お」の予測値である５次元のベクトルです。損失計算は、単なる5次元ベクトル$y$と画像のラベルIDを比較することになります。５次元のベクトル$y$のsoftmaxを求めることで予測値が確率の形で表現されます。一方、画像のラベルIDは、0なら(1, 0, 0, 0 ,0)のように確率１で「あ」が選ばれるように変形します。２つの操作によって予測値$y$と教師データ$t$の誤差が２つの確率ベクトルの差となります。``実装上、この一連の作業は CrossEntropyLoss() を使うことで、criterion(y,t) として誤差計算が行われることになります``。**モデルの予測値$y$や教師データ$t$をそのまま使い criterion(y,t) とすればOK**です。


## 2.4 変数更新のループ
LOOPで指定した回数、
1. **y=model(x)** で予測値を求め、
1. **criterion(y, t)** で指定した誤差関数を使い予測値と教師データの誤差を計算、
1. **accuracy(Y, t)** で精度を計算、
1. 誤差が小さくなるようにoptimizerに従い変数をアップデート

します。

~~~python: 変数更新
LOOP = 150                                # LOOP : 学習回数
for epoch in range(LOOP):
    optimizer.zero_grad()                # 勾配初期化PyTorchの約束事項
    y = model(x)                         # ネットワークモデルによる予測
    loss = criterion(y, t)               # 誤差計算PyTorchの約束事項
    acc  = accuracy(y,t)                 # 精度の計算
    print(f"{epoch}: loss: {loss.item()}, acc:{acc}")   # 損失と精度の表示
    loss.backward()                      # backward(微分する部分)
    optimizer.step()                     # パラメータ更新PyTorchの約束事項
~~~
forループで変数を更新することになります。損失の減少や精度を表すaccの値を観察しながら、学習回数の**LOOP** や学習率の**lr** を適宜変更することになります。

ここまでで、基本的な学習は終わりとなります。

### 平均精度について
accuracyの関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。forループよりも前に記述すればOKです。


~~~python: accuracy関数
def accuracy(y, t):
    _,argmax_list = torch.max(y, dim=1)
    accuracy = sum(argmax_list == t).item()/len(t)
    return accuracy
~~~


## 2.5 検証
2.1のデータ分割で作成したテストデータ **x_test** と **t_test** を利用して学習結果をテストしてみましょう。x_testをmodelに入れた値 y_test = model(x_test) が予測値となります。accuracyで平均精度を求めれば完成です。


~~~python: 検証
y_test = model(x_test)
acc = accuracy(y_test, t_test)
print("平均精度", acc)
# 平均精度 0.98
~~~

ちょっと頑張れば個別の精度も求めることができますぞ。「え」と「お」が似ているのかな？きっとクセのある「え」や「お」が紛らわしいに違いない。

~~~python: 個別精度のサンプル
       prediction                
                あ   い   う   え   お
true あ         12   0   1   0   0
     い          0  23   0   0   0
     う          0   0  10   0   0
     え          0   0   0  13   2
     お          0   0   0   1  18

~~~

# 3. CNN出力結果の可視化
CNNの出力値やカーネルを画像として表示する予定でしたが、長くなってきたので4.5に移動することにしました:bow_tone1::bow::bow_tone1:

https://qiita.com/AzukiImo/items/16e33d56fc1611c23a94


# 注

[^1]: [独立行政法人産業技術総合研究所のETL文字データベース](http://etlcdb.db.aist.go.jp/)を利用させていただきました。「あ・い・う・え・お」の５種類の画像を抽出しMNISTと対応させるべく28x28サイズに縮小したものを利用しています。

[^2]: MNISTを利用する時は形状を変更する必要があります。

[^3]: GPUを使う計算方法の詳細は、[GPUとCPUを使い分ける！](https://iifx.dev/ja/articles/357443137)や [PytorchでのGPUの使用方法](https://deideeplearning.com/2020/04/03/pytorch-gpu/)などを参考にしてください。