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

# 概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第４回画像分類の続きで4.5回目となります[^0]。

　　　　　　　　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/d95558f6-25ff-45c5-9b63-fe1a2e8349db.jpeg" width=50%>
[第4回](https://qiita.com/AzukiImo/items/460880e1555ed93e347c) はMNISTに類似したグレースケールの「あいうえお」画像によって画像分類の初歩を行いました[^1]。今回は完全な続きでCNNの出力やカーネルの可視化からスタートします。


### 演習用のファイル
* [データのファイル](https://github.com/azukiimo/qiita/blob/main/data/aiueo.npz)
* [コード sample_04_5.ipynb](https://github.com/azukiimo/qiita/blob/main/sample_04_5.ipynb)


# 1. 第4回目の要約

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

今回の目的は特徴量を画像を表示することです。

:::note
今回の目的
1. 畳み込み層の出力結果を画像として表示
1. 畳み込み層のカーネルを画像として表示
:::


# 2. 画像表示コードと解説

## 2.1 画像データの表示
最初に読み込んだ画像データの表示から行います。matplotlibで画像を表示させるimshow()は、（高さ、幅）もしくは（高さ，幅，チャンネル数）の形になっている必要があります。読み込んだ画像データのチャンネル数の軸を削除する方法を採用します。


~~~python: 画像表示の基本
import numpy as np                 # データの読み込みに利用 
import matplotlib.pyplot as plt    # 画像表示

data = np.load("./aiueo.npz")      # aiueo.npzの読み込み
x = data["x"]                      # "x"キーで画像データにアクセス (792,1,28,28)

img = x[0]                         # チャンネルの部分を削除して行列の形に変換
plt.imshow(img, cmap="gray_r")     # 画像表示, gray_r: 白黒反転
plt.show()
~~~
imshow()のカラーオプションを``cmap="gray_r"``としてあるので白黒が反転した32ピクセル×32ピクセルの画像が表示されます．

　　　　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/467a2710-2600-4845-87fb-623c2567651c.jpeg" width=30%>
通常のグレースケールの画像``cmap="gray"``だとこんな感じになります。
　　　　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/56296620-e432-4e96-9160-24125c495dd3.jpeg" width=30%>


## 2.2 CNNの出力結果の表示
forwardの戻り値として準備してある、予測値（y）・CNNの出力値（h1）・活性化関数後の出力値（h2）の3種類を活用します。
コード内の``prediction, feature_map, act_feature_map = model(sample_image)``が予測値、CNN出力値、活性化関数後の出力値となります。CNNの出力値はマイナスの値になることがありますが、imshow()によって適切な範囲に正規化して画像を表示してくれます。


~~~python: CNNの特徴量画像
import matplotlib.pyplot as plt
import japanize_matplotlib

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

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

# 推論実行
with torch.no_grad():
    prediction, feature_map, act_feature_map = model(sample_image)
    
# 特徴マップのサイズは (1, 1, 24, 24) のはず
# バッチとチャネル次元を削除して2D画像として表示
feature_map = feature_map.squeeze().cpu().numpy()  # (24, 24)の2D配列に変換

#----------------    画像表示    ------------------
# out_channels=5なので5枚画像が出力される
fig, axes = plt.subplots(1, 5, figsize=(10, 3))
plt.suptitle("CNN出力 (特徴マップ)")
for i, ax in enumerate(axes.flatten()):
    ax.imshow(feature_map[i], cmap="gray")
    ax.axis('off')
plt.tight_layout()
plt.show()
~~~

CNN出力画像やカーネルの画像について若干注意があります[^3]。演習用のファイルを実行しても記事と同一の画像にならない可能性があります。解釈などは適宜変更してください:bow_tone1:

### CNNの出力画像
画像の白い部分が注目している部分となります。輪郭の検出、右上がりの線、上下の線、左右の線などに注目するなどの効果がなんとなく見てわかる？ということはなさそうです:sweat_smile:

![cnn_e_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/d8fbf43a-da21-4f12-bb0d-6cc6a0e69267.jpeg)

### LeakyReluのあとの画像
活性化関数を作用させたあとのCNN特徴量を表示させてみました。CNNの出力値がマイナス部分を中心に黒っぽくなるります。先程より少しだけ注目している部分がはっきり見えてくると思われます。上下の線、斜めの線、輪郭など注目している箇所が少しずつ異なっているようにも感じます。

~~~python
# 特徴マップのサイズは (1, 1, 24, 24) のはず
# バッチとチャネル次元を削除して2D画像として表示
act_feature_map = act_feature_map.squeeze().cpu().numpy()

fig, axes = plt.subplots(1, 5, figsize=(10,3))
plt.suptitle("act cnn出力")
for i, ax in enumerate(axes):
    ax.imshow(act_feature_map[i], cmap="gray")
    ax.axis('off')
plt.tight_layout()
plt.show()
~~~


![act_cnn_e_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/41f6bf79-9e21-4fe4-9b35-19d4560a91c3.jpeg)

## 2.3 カーネルの表示
CNNで学習されるカーネルについても画像として表示してみましょう。カーネルの各要素の値は、畳み込み計算時の対応する入力ピクセルへの重み付けを表します。正の大きな値（白っぽい色）はその位置の特徴を強調し、負の大きな値（黒っぽい色）はその位置の特徴を抑制します。**カーネルは特定のパターンや特徴に反応するフィルターとして機能し、学習によってさまざまな視覚的特徴（エッジ、テクスチャ、形状など）を検出する**ものと解釈されています。

ネットワーク名cnn1の重みがカーネルなので、``model.cnn1.weitht.detach()``でその重みを取り出すことができます。out_channels=5からカーネルの枚数は5枚となります。カーネルサイズが5x5を勘案すると、コード内の変数kernelsの形状は(5, 1, 5, 5)となります。


~~~python: カーネルの表示
# カーネルの重みを取得
kernels = model.cnn1.weight.detach().cpu().numpy()  # 形状は (5, 1, 5, 5)


fig, axes = plt.subplots(1, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flatten()):
    ax.imshow(kernels[i].squeeze(), cmap='gray')
    ax.set_title(f'カーネル{i+1}枚目')
    ax.axis('off')
plt.tight_layout()
plt.show()
~~~

![kernel_e.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/306a55f4-67de-4c05-8de7-8181fa129549.jpeg)
無理やり解釈してみましょう:sweat_smile:
* 1枚目は右下がりに反応するように見えます。
* 2枚目はちょっとバラバラでよくわからんぞ〜。
* 3枚目は右下がりには反応しない。それ以外に反応する形かな？
* 4枚目は上下に反応？右下がりにはあまり反応せずかな？
* 5枚目は左右の線に反応するよう見える。


## 2.4 画像表示について
matplotlibを利用して複数枚の画像を表示する方法はいくつかあります。今回の演習では、``plot.subplots``で複数枚の画像配置位置を決めて、forループで順番に埋めていく形にしました。今回は1行5列なので効果が見られませんが、``axes.flatten()``によって多次元の配置（例えば2行3列など）でも簡単にループできるようになっています。画像をタイル状に並べる方法はいくつもあるので好みの方法を利用してもらえると幸いです。

```python
fig, axes = plt.subplots(1, 5, figsize=(10,3))
for i, ax in enumerate(axes.flatten()):
    ...
```

# 次回
画像分類のネットワークはCNN（Convolutional Neural Network）と一緒にプーリング層もよく使われます。第5回はプーリング層を加えた構造で画像分類問題を行い、特徴量を可視化してみたいと思います。

# 注
[^0]: 4回目が長くなったので2回に分けてしまいました:sweat_smile:
[^1]: [独立行政法人産業技術総合研究所のETL文字データベース](http://etlcdb.db.aist.go.jp/)を利用させていただきました。「あ・い・う・え・お」の５種類の画像を抽出しMNISTと対応させるべく28x28サイズに縮小したものを利用しています。
[^3]: **演習用のファイルを実行しても記事と同一の画像表示になるとは限りません。ご注意ください。**