はじめに
TensorFlow2 + Keras による画像分類の勉強メモ(**CNN**編の第2弾)です。MLP編(多層パーセプトロンモデル編)については、こちら をご覧ください。
前回は「とりあえず動かす」ということで、MNIST を対象に TF/Keras でシンプルなCNN(畳み込みニューラルネットワークモデル)を定義し、学習と評価を行ないました。
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モデルと言えます**。
この畳み込み層では「畳み込みフィルタの適用(フィルタの畳み込み処理)」が行なわれています。この「フィルタの適用」とは **gimp や フォトショップ などの画像処理ソフトでのフィルタ処理(ぼかし処理、モザイク処理、シャープ化など)と同じこと**です。
(やや正確性を欠きますが)言葉で表現すれば畳み込みフィルタの適用とは「入力画像を構成している各ピクセルについて、適当な重みを付けて周囲のピクセル情報を取り込んで出力画像を作成すること」なります。
具体的には、Conv2D
層の通過によって、次のようにデータが変化します。
上記の例では、フィルタの要素値(=重み)を「すべて $0.11$ 」のように、こちら側で用意して与えています。しかし、CNNではフィルタの要素値(=重み)は学習を通じて最適化・決定していきます。
Conv2D(...) で任意フィルタで畳み込む
単にフィルタを畳み込むだけなら OpenCV でやったほうが早いのですが、ここでは TF/Keras で CNNモデルを作成するときに使用する tf.keras.layers.Conv2D(...)
で実行してみたいと思います。
例として、ここでは MNIST の手書き文字画像に、3種のフィルタを畳み込んでみたいと思います。 tf.keras.layers.Conv2D(...)
の単層で構成されるモデルを作成し、model.set_weights(...)
でフィルタ(=重み)をセットして、y=model(x)
で結果を取得します。
%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')
フィルタに関する部分のコメントアウトを切り替えながら実行すると、次のような結果が得られます。
平均化フィルタ
ソーベルフィルタ(横方向検出)
ソーベルフィルタ(縦方向検出)
カーネルサイズ
フィルタが、どのぐらい周辺のピクセル情報までを取り込むかという縦横の範囲を**カーネルサイズ(フィルタサイズ)**といいます。一般には「$3\times 3$」や「$5\times 5$」のような奇数値が使われます。tf.keras.layers.Conv2D(32, kernel_size=(3,3), activation='relu')
の (3, 3)
がカーネルサイズの指定になります。
カーネルサイズが $3\times 3$ ということは、自身を含め周囲 $9$ マスの情報の重み付き和をフィルタ適用後の値とすることを意味します。カーネルサイズ $5\times 5$ では自身を含めて周囲 $25$ マスの情報の重み付き和で適用後の値が決まります。
カーネル値
このカーネル(行列)の各要素には、畳み込み処理のための「重み」が格納されます。どのような値を入れるかによって、そのフィルタが、ぼかし効果を与えるものになったり、エッジ強調をするものになったり変化します。