前回はConv2Dを用いてSobelフィルタを実現しましたが,今回はもっとConv2Dの細かい動作について理解していきます.
前回の記事はこちら↓
複数の画像を入力する
まずは前回とほぼ同じモデルを作ります.
#モデルの定義
model1 = Sequential()
model1.add(Input((10,10,1))) #チャネルは1
model1.add(Conv2D(1, (3,3), padding='same')) #フィルタ数は1
# フィルタの重みを設定
w = model1.layers[0].get_weights()
w[0] = np.array([[1,1,1],[1,1,1],[1,1,1]])[:,:,None,None]
w[1] = np.array([0])
model1.layers[0].set_weights(w)
入力画像は10×10ピクセルのモノクロという小さい画像にし,フィルタ係数もすべて1にします(動作をみるため).フィルタ係数がすべて1というのは,画像の3×3の領域のピクセル値を加算するという処理になります.
モデルの構造をみるため,
model1.summary()
を実行すると以下のようになりました.
出力のテンソルの形は,(None, 10,10,1)です.10×10ピクセルの画像が1チャネル,Noneは画像の数を表します.それで,前回は1枚の画像のみを入力しましたが,今回は3枚の画像を入力して動作をみてみます.
複数の画像を入力として与えますが,動作理解を目的とするため,numpy配列を作成しそれを入力画像にしたいと思います.
test_im1 = np.ones((10,10))
test_im2 = np.ones((10,10))*10
test_im3 = np.ones((10,10))*20
test_im = np.array([test_im1, test_im2, test_im3])[:,:,:,None]
1〜3行目:test_im1, 2, 3はすべて10×10の配列で,test_im1の値はすべて1, test_im2はすべて10,test_im3はすべて20です.
4行目:これらをすべて連結すると(3,10,10)のテンソルになりますが,それに対してチャネルの分を加えて(3,10,10,1)のテンソルにしたものをtest_imとします.
そして,フィルタリングします.
filtered_im = model1.predict(test_im)
結果であるfiltered_imのサイズを見てみます.
filtered_im.shape
結果は(3,10,10,1)で入力と同じサイズのテンソルになってます.それでは,1枚ずつ表示してみます.
print(filtered_im[0,:,:,0])
print(filtered_im[1,:,:,0])
print(filtered_im[2,:,:,0])
結果は,
最初は1番目の画像test_im0をフィルタリングした結果です.フィルタの係数はすべて1としたので,3×3の領域の画素値をすべて加算したものが答えです.
ここで,入力は10×10ですが,padding=’same’としてるので,10×10のtest_im0の周囲に1ピクセルの画素値0が入ることになります(つまり,12×12になる).それに対してフィルタリングを行うので,filtered_im[0,:,:,0]の4隅の値は4,へりの値は6,それ以外の値は9となります.
つまり,こういうことですね.
4隅(たとえばオレンジ色の場所)では9つのピクセル値を加算するので4,へり(たとえば緑色の場所)は9つのピクセル値を加算するので6,内部(たとえば青の場所)は9つのピクセルを加算すると9となります.
画像フィルタの具体的な計算方法はここでは述べませんが,下記が詳しいです.
2枚目,3枚目についても同様です.
これで,3枚の画像を入力として与えた時の結果が得られました.
一応,確認として,padding=’valid’にした場合を実行してみます.結果はこちら
padding=’valid’にすると,周囲に0のピクセルが入らないので,入力画像をそのままフィルタリングします.なので,出力結果は8×8になりサイズが小さくなります.
test_im0をフィルタリングした結果,各要素の値がすべて9なのは,以下の図をみればすぐわかると思います.今度は0のピクセルがないので,4隅であってもへりであっても9つのピクセルの値を加算すると9になります.
これで,3枚の画像を1つのテンソルにまとめて入力すると,それぞれの計算結果がテンソルの形で取得できることが分かりました.
チャネルを増やす
今度はチャネルを増やしてみます.コードはこちら
model1 = Sequential()
model1.add(Input((10,10,2)))
model1.add(Conv2D(1, (3,3), padding='valid'))
w = model1.layers[0].get_weights()
print(w[0].shape)
w1 = np.array([[-1,-1,-1],[-1,-1,-1],[-1,-1,-1]])
w2 = np.array([[1,1,1],[1,1,1],[1,1,1]])
ww = np.array([w1,w2])[:,:,:,None] #(2,3,3,1)のテンソル
ww = np.transpose(ww, (1,2,0,3)) #(3,3,2,1)のテンソル
w[0] = ww
w[1] = np.array([10]) #バイアスは1つのみ(全てのチャネルで共通らしい)
model1.layers[0].set_weights(w)
1〜3行目:モデルの定義.2行目で(10,10,2)としてるので,2チャネルの画像です.通常,カラー画像だとRGBの3チャネルですが,フィルタサイズと同じ数になると紛らわしいので2チャネルにしてます.フィルタ数は簡単のため1つにしてます.
5, 6行目:Conv2Dの重みを読み込み,その形状を出力してます.その結果,(3,3,2,1)となります.つまり,(フィルタの横幅,フィルタの縦幅,画像のチャネル数,フィルタの数)という形状です.つまり,フィルタの係数はチャネル数だけ必要というわけです.
7〜8行目:画像のチャネル数だけフィルタが必要となるので,チャネル1のためのフィルタ係数,チャネル2のためのフィルタ係数を設定します(それぞれw1, w2)
9行目:w1とw2を連結してnp.array([w1,w2])は(2,3,3)のテンソルになるので,これにフィルタ数を加えてwwとしている.結果,(2,3,3,1)のテンソル.
10行目:Conv2Dの重みのフォーマットに合わせて,添え字の順番を入れ替える.(3,3,2,1)のテンソルにする
11〜13番目:wwとバイアスをリストで連結して,フィルタ係数を設定する.ただし,バイアスは1つだけ(すべてのチャネルで共通らしい).今回はバイアスを10に設定.
次は,入力画像を作成します.
test_im1 = np.ones((10,10))
test_im2 = np.ones((10,10))*10
test_im = np.array([test_im1,test_im2])[:,:,:,None] # (2,10,10,1)の形
test_im = np.transpose(test_im, (3,1,2,0)) # (1,10,10,2)に変形
1, 2行目:10×10のすべての要素が1の配列(チャネル1),10×10のすべての要素が10の配列(チャネル2)をそれぞれ作成する.
3行目:2つのチャネルの画像を連結し,画像の数を表す部分をNoneで加えて(2,10,10,1)のテンソルにする
4行目:テンソルの要素の順番をConv2Dの入力形式に入れ替える.結果,(1,10,10,2)のテンソルになる.
そして,フィルタリング
filtered_im1 = model1.predict(test_im)
フィルタリングした結果の形状をみます.
filtered_im1.shape
すると,(1,8,8,1)のテンソルになります.これは,padding=’valid’にしているので,10×10の入力画像が,8×8になります.
しかし,画像数もチャネル数も1になってる点.フィルタは2つあるのに,結果は1つというのはどういう意味?
それで,filtered_im1を出力してみます.
print(filtered_im1)
結果,以下のようになりました.
入力画像のチャネル1はすべての要素が1,チャネル2は全ての要素が10です.
チャネル1のフィルタの係数はすべて-1なので,フィルタリングすると,8×8の画像になりすべての要素が-9となります.
チャネル2のフィルタ係数はすべて1なので,フィルタリングすると,8×8の画像になりすべての要素が90となります.
そして,全体のバイアスが10です.
つまり,出力結果は,チャネル1のフィルタリングした結果,チャネル2のフィルタリングした結果,バイアスの3つを加算した値になります.つまり,各要素の値は,90-9+10 = 91になります.
この辺の畳み込み演算の基本的なしくみは,下記の本に詳しいです.
これで,Conv2Dの使い方はよく分かりました!
こんな調子で少しずつTensorflow/Kerasの使い方を勉強していきたいと思います.