TensorFlow2 + Keras で画像分類に挑戦 CNN編2 ～畳み込み処理を視覚的に理解する～

# はじめに

TensorFlow2 + Keras による画像分類の勉強メモ（**<font color='red'>CNN</font>**編の第2弾）です。MLP編（多層パーセプトロンモデル編）については、[こちら](https://qiita.com/code0327/items/7d3c7bd3327ff049243a) をご覧ください。

前回は「[とりあえず動かす](https://qiita.com/code0327/items/32517ff2ff7d63c8ec52)」ということで、**MNIST** を対象に TF/Keras でシンプルな**CNN**（畳み込みニューラルネットワークモデル）を定義し、学習と評価を行ないました。

```py:CNNモデルの定義（TF/Keras）
model = tf.keras.models.Sequential()
model.add( tf.keras.layers.Reshape((28, 28, 1), input_shape=(28, 28)) )
model.add( tf.keras.layers.Conv2D(32, kernel_size=(3,3), activation='relu') ) # 畳み込み層
model.add( tf.keras.layers.MaxPooling2D(pool_size=(2,2)) )
model.add( tf.keras.layers.Flatten() )
model.add( tf.keras.layers.Dense(128, activation='relu') )
model.add( tf.keras.layers.Dropout(0.2) )
model.add( tf.keras.layers.Dense(10, activation='softmax') )
```

今回は、**畳み込み処理（Convolution）**とは、何なのか？どんなことをしているのか？について、図とコードと使ってできるだけ分かりやすく解説していきたいと思います。

# 畳み込み処理（Convolution）

CNNの最大の特徴は、**畳み込み層**が存在することです。TF/Keras で定義するモデル的には `tf.keras.layers.Conv2D(32, kernel_size=(3,3), activation='relu')` のような**「畳み込み層」が含まれていると、CNNモデルと言えます**。

この畳み込み層では「**畳み込みフィルタの適用（フィルタの畳み込み処理）**」が行なわれています。この「**フィルタの適用**」とは **<font color='red'>gimp や フォトショップ などの画像処理ソフトでのフィルタ処理（ぼかし処理、モザイク処理、シャープ化など）と同じこと</font>**です。

（やや正確性を欠きますが）言葉で表現すれば畳み込みフィルタの適用とは「**入力画像を構成している各ピクセルについて、適当な重みを付けて周囲のピクセル情報を取り込んで出力画像を作成すること**」なります。

具体的には、`Conv2D` 層の通過によって、次のようにデータが変化します。

![フィルタ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/9bf22b01-d810-196b-726d-7001321e1575.png)

上記の例では、フィルタの要素値（＝重み）を「すべて $0.11$ 」のように、こちら側で用意して与えています。しかし、CNNではフィルタの要素値（＝重み）は**学習を通じて最適化・決定**していきます。

### Conv2D(...) で任意フィルタで畳み込む

単にフィルタを畳み込むだけなら OpenCV でやったほうが早いのですが、ここでは TF/Keras で CNNモデルを作成するときに使用する `tf.keras.layers.Conv2D(...)` で実行してみたいと思います。

例として、ここでは MNIST の手書き文字画像に、3種のフィルタを畳み込んでみたいと思います。` tf.keras.layers.Conv2D(...)` の単層で構成されるモデルを作成し、`model.set_weights(...)` でフィルタ（＝重み）をセットして、`y=model(x)` で結果を取得します。

```py:任意のフィルタを畳み込む
%tensorflow_version 2.x
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# (0)準備:画像表示関数
def show_image(img,title=None):
  plt.figure(figsize=(2,2),dpi=120)
  plt.gcf().patch.set_facecolor('white')
  plt.xticks([])
  plt.yticks([])
  plt.title(title,fontsize=15)
  plt.imshow(img,cmap='Greys')
  plt.show()
  plt.close()

# (1) 入力データ x の準備
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train/255.0, x_test/255.0
x = x_train[0]  # 訓練用データの0番目の画像を使用
show_image(x,title='Input x')

# (2) モデルに入力できる形に整形
x_size = x.shape
x = x.reshape([1,x_size[0],x_size[1],1]) # shape:(28, 28)=>(1,28,28,1)
x = x.astype(np.float32)

# (3) サイズ 3x3 のフィルタ w を作成
a = np.array([[1,1,1],[1,1,1],[1,1,1]])/9 # 移動平均フィルタ
#a = np.array([[1,2,1],[0,0,0],[-1,-2,-1]]) # ソーベルフィルタ（横方向検出）
#a = np.array([[1,0,-1],[2,0,-2],[1,0,-1]]) # ソーベルフィルタ（縦方向検出）

a = a.astype(np.float32)
a = a.reshape([3,3,1,1]) # shape:(3, 3) => (3,3,1,1)
w = [a]

# (4) フィルタを畳み込むモデルを構築
model = tf.keras.models.Sequential()
model.add( tf.keras.layers.Conv2D(filters=1,
                                  kernel_size=(3,3),
                                  use_bias=False,                                  
                                  padding='same',
                                  activation='relu',
                                  input_shape=(28,28,1)))

# (5) フィルタ w をモデルにセット 
model.set_weights(w)

# (6) 畳み込み処理の実行
y = model(x)
y = y.numpy().reshape(y.numpy().shape[1:3]) # shape:(1,28,28,1)=>(28,28)
show_image(y,title='Output y')
```

フィルタに関する部分のコメントアウトを切り替えながら実行すると、次のような結果が得られます。

**平均化フィルタ**
![f1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/c5d54f68-f7f9-21e7-0a22-2379dd66fc33.png)
**ソーベルフィルタ（横方向検出）**
![f2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/883ea183-c500-a7fa-cd47-e841b3733606.png)
**ソーベルフィルタ（縦方向検出）**
![f3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/7582228d-2efe-a5a8-e33e-5305d754b3d9.png)


## カーネルサイズ

フィルタが、どのぐらい周辺のピクセル情報までを取り込むかという縦横の範囲を**カーネルサイズ（フィルタサイズ）**といいます。一般には「$3\times 3$」や「$5\times 5$」のような奇数値が使われます。`tf.keras.layers.Conv2D(32, kernel_size=(3,3), activation='relu')` の `(3, 3)` がカーネルサイズの指定になります。

カーネルサイズが $3\times 3$ ということは、自身を含め<font color='red'>**周囲** $9$ **マスの情報の重み付き和をフィルタ適用後の値とする**</font>ことを意味します。カーネルサイズ $5\times 5$ では自身を含めて周囲 $25$ マスの情報の重み付き和で適用後の値が決まります。

![kernel_size.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/2f5a6e95-7e52-dbbf-5010-3a908b428a0d.png)

## カーネル値

このカーネル（行列）の各要素には、畳み込み処理のための「**重み**」が格納されます。どのような値を入れるかによって、そのフィルタが、ぼかし効果を与えるものになったり、エッジ強調をするものになったり変化します。