えんじにあのじゆうちょう

勉強したことを中心にアウトプットしていきます。

畳み込みの処理を理解する

はじめに

超個人的な話題ですが、もう少しでE資格の受験日です。
そこで一通りのアルゴリズムの実装を見直していこうと思います。
特にディープ系はあまり自信がないので、コアになりそうなところを中心にしっかりやっていこうと思います。

今回は畳み込みの実装を整理していこうと思います。

畳み込みニューラルネットワーク

前提

畳み込みニューラルネットワークは画像だけのものではありませんが、一旦記載が楽になるように画像的なデータを扱っている前提で書きます。

convolution

convolutionは単純にフィルタ(カーネルと言ったりもする)と入力値との要素積です。
講座とか受けると、だいたい全結合の次が畳み込みなので、勘違いしがちですが、dot積ではありません。注意。

例えば、入力からあるフィルターサイズと同じ大きさの領域(例えば3x3)を取った時、以下のようだったとして実際に計算してみます。
計算順序は、まずフィルタと画像の要素積をとって、バイアスを足します。(バイアスはチャンネルごとに1つです)
その後、それらの要素を全て足し合わせます。

np.random.seed(5)
X =  np.random.normal(loc=0, scale=1, size=(3,3))
w =  np.random.normal(loc=0, scale=1, size=(3,3))
b =  np.random.normal(loc=0, scale=1, size=(1,))
print("X=\n", X)
print("w=\n", w)
print("b=\n", b)
print("X * w + b =\n", np.multiply(X, w) + b)
print("sum(X * w + b) =\n", np.sum(np.multiply(X, w) + b))
X=
 [[ 0.44122749 -0.33087015  2.43077119]
 [-0.25209213  0.10960984  1.58248112]
 [-0.9092324  -0.59163666  0.18760323]]
w=
 [[-0.32986996 -1.19276461 -0.20487651]
 [-0.35882895  0.6034716  -1.66478853]
 [-0.70017904  1.15139101  1.85733101]]
b=
 [-1.51117956]
X * w + b =
 [[-1.65672725 -1.11652935 -2.00918748]
 [-1.4207216  -1.44503313 -4.14567597]
 [-0.87455409 -2.19238469 -1.16273827]]
sum(X * w + b) =
 -16.023551823779634

最後の-16....という値がこの畳込みで得られた値です。これを次々と繰り返して画像全体を畳み込みます。

Pooling

Poolingはある範囲の画素から最大値だったり、平均値だったりを取り出していくことです。

例えば、フィルタサイズを3x3としたときに、以下の様なデータが有ったとします。

X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

今回は最大値でPoolingする例です。以下のように書きます。

np.max(X)
9

ただ最大値を取っているだけですね。

これを畳み込み同様繰り返していき、出力を得ます。

padding

paddingはnp.padを用いて簡単に実装できます。
例えば以下のような1次元のnumpy配列があったとします。

X = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
array([1, 2, 3, 4, 5, 6, 7, 8, 9])

これの前後をpaddingするには次のようにします。
例えば下の例では前に1つ、後ろに2個、0をpaddingしています。

np.pad(X, [1, 2], 'constant')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0])

第2引数で前後にどれだけpaddingするかの数を書いてます。

では、2次元の場合はどうやるのでしょうか。
例えば次のような配列が合ったとします。

X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

これも先程と同様にpaddingしてみましょう。列方向の頭に1つ、お尻に2つ、行方向の頭に3つお尻に4ついれてみます。

np.pad(X, [(1, 2), (3, 4)], 'constant')
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 2, 3, 0, 0, 0, 0],
       [0, 0, 0, 4, 5, 6, 0, 0, 0, 0],
       [0, 0, 0, 7, 8, 9, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

2次元以上のときはこのようにpaddingの指定にリストを渡すのですが、paddingを追加したい軸の番号の要素に(前にpaddingする数, 後ろにpaddingする数)を指定してあげるのです。

畳み込みの場合、入力要素は(入力画像数, 高さ, 幅, チャネル数)になるのが一般的ですが、入力画像数とチャネル数の次元にはpaddingする必要はないので、指定は

np.pad(入力画像, [(0, 0), (p, p), (p, p) (0, 0)], 'constant')

になります。(pがpadding数です)

stride

strideはフィルタを適用するときに適用する範囲をどれだけ動かすかです。当たり前ですが1以上の整数になります。
ストライドとpaddingを実際に適用してみてみることにします。

データは一旦、5x5の配列を用意しました。
Xsは画像のサイズ(1辺)、fsはフィルタサイズ(1辺)です。

X =  np.random.normal(loc=0, scale=1, size=(5,5))
Xs = 5
fs = 3

paddingサイズを0 or 1, strideサイズを1 or 2として出力される画像の大きさを見てみます。

for p in (0, 1):
    for s in (1, 2):
        print("padding: {} / stride: {}".format(p, s))
        X_pad = np.pad(X, [(p, p), (p, p)], 'constant')
        r = []
        for i in range(int((Xs + 2 * p - fs) / s +1)):
            for j in range(int((Xs + 2 * p - fs) / s +1)):
                r.append(X_pad[i * s:i * s + fs, j* s:j * s + fs])
        print(len(r))
padding: 0 / stride: 1
9
padding: 0 / stride: 2
4
padding: 1 / stride: 1
25
padding: 1 / stride: 2
9

言うまでもないのですが、それぞれルートを取ると1辺あたりのサイズになるので、上から3x3, 2x2, 5x5, 3x3が出力サイズです。

まぁこれは以下のように式で表せます。そのとおりになってますね。
 O_w = \frac{X_w + 2p - f_w}{s} + 1
 O_h = \frac{X_h + 2p - f_h}{s} + 1

まとめて畳み込んでみる

まずはデータを用意します。
今回は画像Xは10枚あることとし、サイズは32x32、チャンネル数は3とします。
wはフィルタサイズ3x3, 入力チャンネル数は3, 出力チャネル数は32とします。
bは出力チャネル数に合わせれば良いので、32個あればいいのですが、計算が楽になるように以下の様に定義します。

Xsは画像のサイズ(1辺)、fsはフィルタサイズ(1辺)、outchsは出力チャネル数です。

np.random.seed(5)
X = np.random.normal(loc=0, scale=1, size=(10, 32,32, 3))
w = np.random.normal(loc=0, scale=1, size=(3, 3, 3, 32))
b = np.random.normal(loc=0, scale=1, size=(1, 1, 1, 32))

Xs = X.shape[1]
fs = w.shape[0]
outchs = w.shape[3]

前と同じようにpadding, strideを変えながら出力サイズを出力していきます。

for p in (0, 1, 2):
    for s in (1, 2, 3, 4, 5):
        print("padding: {} / stride: {}".format(p, s))
        X_pad = np.pad(X, [(0, 0), (p, p), (p, p), (0, 0)], 'constant')
        result = []
        for i in range(int((Xs + 2 * p - fs) / s +1)):
            cols = []
            for j in range(int((Xs + 2 * p - fs) / s +1)):
                chs = []
                for c in range(outchs):
                    chs.append(np.sum(np.multiply(X_pad[:, i * s:i * s + fs, j* s:j * s + fs,:], w[:,:,:,c]) + b[:,:,:,c]))
                cols.append(chs)
            result.append(cols)
        print("{}x{}, {}ch".format(len(result), len(result[0]), len(result[0][0])))
padding: 0 / stride: 1
30x30, 32ch
padding: 0 / stride: 2
15x15, 32ch
padding: 0 / stride: 3
10x10, 32ch
padding: 0 / stride: 4
8x8, 32ch
padding: 0 / stride: 5
6x6, 32ch
padding: 1 / stride: 1
32x32, 32ch
padding: 1 / stride: 2
16x16, 32ch
padding: 1 / stride: 3
11x11, 32ch
padding: 1 / stride: 4
8x8, 32ch
padding: 1 / stride: 5
7x7, 32ch
padding: 2 / stride: 1
34x34, 32ch
padding: 2 / stride: 2
17x17, 32ch
padding: 2 / stride: 3
12x12, 32ch
padding: 2 / stride: 4
9x9, 32ch
padding: 2 / stride: 5
7x7, 32ch

まぁ当たり前なんですが、同じstrideならpaddingが大きくなると出力は大きくなって、同じpaddingならstrideが大きくなると出力は小さくなります。
チャンネル数は今回出力数を32と決めたので、ちゃんと全て32です。

poolingも動きは同じなため、ここでは特に実装しません。

まとめ

今回は畳み込みに必要な要素と数式と実装をまとめてみました。
本にしても講座にしても、読んだり受けたりしたあとはわかった気になるのですが、やっぱり自分で実装し直さないと危ないですね。
他の項目についても、特に自分がふわっとしているところを中心に、色々と試していこうと思います。