🔰PyTorchでニューラルネットワーク基礎 #05 【画像分類・プーリング層】

# 概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第５回は**プーリングを追加した画像分類** になります。

[第４回 「あ・い・う・え・お」画像の画像分類・畳み込み層](https://qiita.com/AzukiImo/items/460880e1555ed93e347c
)

https://qiita.com/AzukiImo/items/460880e1555ed93e347c





第4回畳み込み層のみの画像分類からの追加点は１箇所
* プーリング層の **MaxPool2d()** もしくは **AvgPool2d()** の追加




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




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


# 1. 第４回の要約と今回の目的

第4回はひらがな「あ・い・う・え・お」の5種類を手書き文字画像をデータとして使い、画像分類の演習を行いました。ネットワーク構造は下図のような畳み込み層と全結合層を利用した単純なものでした。
![CNNによる分類画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/0e7a0bf2-63c8-4e4a-adca-3e02daf47f6b.jpeg)

今回の目的は畳み込み層のあとにプーリング層を追加してみることにあります[^0]。具体的には、次のようなネットワークを作成してみたいと思います。ネットワークの図が少し混み合ってきました:smile:



![cnn_pool_net_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/9597cd59-e90d-4e65-8da5-bf2d0b8875df.jpeg)



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

# 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``というファイル名の画像データを使用します[^1]。numpyのloadでnpz形式のファイルを読み込みましょう。**data["x"]** が画像データ、**data["t"]** がラベルIDで０〜４の整数値の教師データとなります。

|画像 | ラベルID |
|:-----:|:---:|
|「あ」|0 |
|「い」|1 |
|「う」|2 |
|「え」|3 |
|「お」|4 |

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が利用できる場合は、xやt、モデルに``to("cuda")``をつけるとGPUを使った計算となります[^2]。

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

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

今回は下図のようなCNN（畳み込みネットワーク）とプーリング層を利用したネットワークを作ってみたいと思います。


![cnn_pool_net_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/9597cd59-e90d-4e65-8da5-bf2d0b8875df.jpeg)


大まかに見ると（１チャンネル、28ピクセル, 28ピクセル) の画像データがCNNやプーリング層を経由し、最終的に全結合層を使って、５次元ベクトル（５種類の画像予測）に変換される形となります。

:::note
**プーリング層**

1. 最大値プーリング層：　**nn.MaxPool2d(kernel_size, stride=None)**
2. 平均値プーリング層：　**nn.AvgPool2d(kernel_size, stride=None)**

* kernel_size : カーネルサイズ
* stride : 移動量、指定しない場合デフォルト値でカーネルサイズと同じだけ移動
* 最大値プーリングは、カーネルの範囲で最大値
* 平均値プーリングは、カーネルの範囲での平均値

:::


プーリング層にも様々な種類があるようです。[PyTorchの公式ドキュメント](https://docs.pytorch.org/docs/stable/nn.html#lpooling-layers
)に詳細が記載されています。


CNNにプーリング層を追加したネットワークを作成していきます。今まで同様、
　(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=5 ,kernel_size=5)
        self.act1 = nn.LeakyReLU(negative_slope=0.01)  # Leaky ReLU導入
        self.pool1= nn.MaxPool2d(kernel_size=2, stride=2)

        self.flat = nn.Flatten()
        self.fc1 =  nn.Linear(in_features=5*12*12, 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)
        h = self.act1(h1)
        h2 = self.pool1(h)
        h = self.flat(h2)
        h = self.fc1(h)
        h = self.act2(h)
        y = self.fc2(h)
        return y, h1, h2
model = DNN()
```
プーリング層に**MaxPool2d**を利用していますが、**AvgPool2d**でも動作します。効果を比較するのも面白いと思います。活性化関数も２種類使ってみました。print(model)は下のような感じです。GPUを使う場合は``model.to("cuda")``も追加します。



~~~python:modelの構造
DNN(
  (cnn1): Conv2d(1, 5, kernel_size=(5, 5), stride=(1, 1))
  (act1): LeakyReLU(negative_slope=0.01)
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)

  (flat): Flatten(start_dim=1, end_dim=-1)
  (fc1): Linear(in_features=720, out_features=100, bias=True)
  (act2): ReLU()
  (fc2): Linear(in_features=100, out_features=5, bias=True)
)
~~~

### ネットワーク層の解説

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の記事にもわかりやすい記事がたくさんあります。２つほど紹介しておきます。CNNのあとにプーリング層の解説があります。

https://qiita.com/sy1010/items/9713d2f6d584d78d1707

https://qiita.com/kuroitu/items/48bf6bc42e4dfee52cde

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

分類問題では予測値$y$ と教師データ$t$の誤差にはクロスエントロピーを使います。

~~~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) とするだけ**です。


## 2.4 変数更新のループ
for文を利用して次の１〜４を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の関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。

~~~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: 検証
model.eval()
y_test,_,_ = model(x_test)
test_acc = accuracy(y_test, t_test)
print(f"精度:{test_acc}")
# 精度:0.9625
~~~
おかしいな〜。第４回で実装したCNNだけのネットワークだとは平均精度が0.98、しかし、今回のCNN+プーリング層だと平均精度が0.96という結果でした:sweat_smile::sweat_smile::sweat_smile:

### ラベルごとで検証してみた    
やはり、「え」と「お」の判別が難しいみたい、と思いきや「あ」と「う」を間違えているだと！
~~~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   0  19
~~~


# 3. CNN・プーリング層の出力結果の可視化
matplotlibを利用してCNNやプーリング層の出力結果を画像で表現してみます。どの文字でもできるのですが、プーリング層の効果は画像「い」が比較的わかりやすいかな？
``prediction, cnn_feature_map, pool_feature_map = model(sample_image)``を使って、CNNの出力値とプーリング層の出力値を抽出します。prediction、cnn_feature_map、pool_reature_mapがそれぞれforwardの戻り値 y、h1、h2に対応しています。

~~~python: 可視化の準備
import matplotlib.pyplot as plt
import japanize_matplotlib

# テスト用の一つの画像を選択
# あ：300, い：500　う：200 え：600　お:100
sample_image = x[500].unsqueeze(0)  # バッチ次元を追加 (1, 1, 28, 28)

# モデルを評価モードに
model.eval()

# 推論実行
with torch.no_grad():
    prediction, cnn_feature_map, pool_feature_map = model(sample_image)
~~~
ライブラリを読み込んで、500番の画像「い」を選択しておきます。cnn_feature_mapがCNNの出力値、pool_feature_mapがプーリング層の出力値となります。

### CNNの出力値
cnn_feature_mapをmatplotlibで表示できるように、形状を行列の形に変換して5つの画像を順番に表示します。

~~~python: CNNの出力結果
# 特徴マップのサイズは (1, c=1, h, w) 
# バッチとチャネル次元を削除して2D画像として表示
cnn_feature_map = cnn_feature_map.squeeze().cpu().numpy()  # 2D配列に変換

fig, axes = plt.subplots(1, 5, figsize=(10, 3))
plt.suptitle('CNN出力 (特徴マップ)')
for i, ax in enumerate(axes):
    ax.imshow(cnn_feature_map[i], cmap="gray")
    ax.axis('off')
plt.tight_layout()
plt.show()
~~~
![５回CNN出力.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/74dc62a4-5fb3-4aed-83fb-3fe8fe6ff7fd.jpeg)

「い」ですね。CNNなのでエッジや傾きなどの特徴量が抽出されているはず！つづいて、プーリング層の出力結果を画像にしてみます。pool_feature_mapの形状を適切に変換して画像化します。


### プーリング層の出力値

~~~python: プーリング層の出力結果
# 特徴マップのサイズは (1, c=1, h, w) 
# バッチとチャネル次元を削除して2D画像として表示
pool_feature_map = pool_feature_map.squeeze().cpu().numpy()

fig, axes = plt.subplots(1, 5, figsize=(10,3))
plt.suptitle("プーリング層の出力結果")
for i, ax in enumerate(axes):
    ax.imshow(pool_feature_map[i], cmap="gray_r")
    ax.axis('off')
plt.tight_layout()
plt.show()
~~~

![５回POOL出力.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/71a8ba35-6981-4e84-aa05-1c355b48cf69.jpeg)


白い部分が重要な部分と考えると、「い」の最大の特徴は、どうも左右のはねている部分やとまっている部分のように感じます。2箇所の大きな白い塊が「い」ということなのでしょうか？

# 参考と発展
CNNやPooling層に加えてDropout層や正規化層を追加しながらCNNやPooling層の形を繰り返すことで精度が飛躍的に向上するようです。CNN系譜の代表的なモデルを5つほど紹介、というか5つしか知らないとも言える。発表年は目安と思ってください:smile::sweat_smile::scream:

|モデル名|発表年|	特徴	|層の数|
|-------|-----|-----|-----|
|AlexNet|	2012年|	CNNを利用した深層学習で画像分類コンペILSVRC2012に勝利　:clap:|8層|
|VGG|	2014年|シンプルな3x3畳み込み層の積み重ね　|16層と19層|
|GoogLeNet|	2014年|	Inceptionモジュール導入|	22層|
|ResNet|	2015年|	スキップ接続（残差接続）の導入|	152層|
|DenseNet|	2017年|	すべての層が後続のすべての層に密に接続|121〜264層|

論文のタイトルも紹介しておきます。モデルの作成者・著者名を記入せず大変申し訳無いですが:bow_tone1::bow::bow_tone1:

|モデル| 論文 |
|-----|-----|
|AlexNet|[ImageNet Classification with Deep Convolutional Neural Networks](https://dl.acm.org/doi/10.1145/3065386)|
|VGG    | [Very Deep Convolutional Networks for Large-Scale Image Recognition](https://arxiv.org/abs/1409.1556) |
|GoogLeNet | [Going Deeper with Convolutions](https://ieeexplore.ieee.org/document/7298594)|
|ResNet| [Deep Residual Learning for Image Recognition](https://ieeexplore.ieee.org/document/7780459) |
| DenseNet | [Densely Connected Convolutional Networks](https://arxiv.org/abs/1608.06993) | 

# 4. 次回
第6回は中間層の出力値を出力させる方法についての予定です。今回はモデルを定義している **forward()の戻り値に直接出力させる値を書き込んでいます**。この方法だけしか知らないと学習完了後に他の層の出力も見たい！という時の対応が困難になります。第6回では学習済みmodelに対して関心のあるネットワーク層を登録して出力値にアクセスするというちょっとした豆知識回になりそうです。


# 注
[^0]: プーリング層に関しては、Qiita記事の[深層学習入門 ~畳み込みとプーリング編](https://qiita.com/kuroitu/items/48bf6bc42e4dfee52cde)に詳しい解説があります。カラフルで非常にわかりやすいです:apple:

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

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