小さな窓を、画像の上で滑らせる
前のレッスンでは、画像を1列に並べると、隣り合うマスの関係が見えにくくなることを確かめました。このレッスンでは、隣り合うマスをまとめて見る 計算、畳み込み(たたみこみ)を学びます。CNNの「C」(Convolution)は、この畳み込みのことです。
直感:虫めがねで、少しずつ見ていく
大きな絵を、3×3マスだけが見える小さな窓(虫めがねのようなもの)を通して見る場面を想像してください。窓を左上に置いて中をよく見たら、右に1マスずらして、また見る。右端まで来たら、1マス下の左端に戻って、また見る。これを繰り返すと、絵の全体を、小さな範囲ずつ見て回れます。
窓の中で「何を探すか」を決めるのが、フィルター です。フィルターは、窓と同じ3×3の数の表です。
窓の中で行う計算は、第4章の内積と同じ
窓を1か所に置いたら、次の計算をします。
- 窓の中の9マスと、フィルターの9マスを、同じ位置同士で掛ける
- 9個の積を、全部足す
「同じ位置同士を掛けて、全部足す」は、第4章で学んだ 内積 そのものです。9個の数の並び(窓の中)と、9個の数の並び(フィルター)の内積を計算していると考えられます。
足した結果の1つの数を、出力の表の、窓の位置に対応するマスに書き込みます。窓を滑らせながらこれを繰り返すと、出力の表ができあがります。この「窓を滑らせながら、フィルターとの内積を並べていく」計算が、畳み込みです。
現実のどこで使われているか
- スマートフォンの顔認識や、写真の中の人や物を見つける機能
- 自動運転の車が、カメラの映像から歩行者や標識を見つける仕組み
- 画像を「ぼかす」「くっきりさせる」写真の加工アプリ(これは、決まったフィルターで畳み込みをしています)
どれも、画像の上でフィルターを滑らせて、小さな範囲の模様を調べることから始まります。
1か所ずつ計算してみる
5×5マスの画像に、3×3のフィルターで畳み込みをします。フィルターは、四すみと真ん中が1、残りが0の「×」の形です。
下の図の「次の位置」ボタンで、窓(点線の枠)を1マスずつ動かせます。各位置で、窓の中の9マスとフィルターを同じ位置同士で掛けて足した結果が、出力の太い枠のマスに入ります。
位置 1 / 9
出力の0行目・0列目: 1×1 + 2×0 + 0×1 + 0×0 + 1×1 + 3×0 + 2×1 + 0×0 + 1×1 = 5
画像の値| 0行目 | 1 | 2 | 0 | 1 | 0 |
|---|
| 1行目 | 0 | 1 | 3 | 1 | 2 |
|---|
| 2行目 | 2 | 0 | 1 | 0 | 1 |
|---|
| 3行目 | 1 | 3 | 0 | 2 | 0 |
|---|
| 4行目 | 0 | 1 | 2 | 1 | 1 |
|---|
フィルターの値| 0行目 | 1 | 0 | 1 |
|---|
| 1行目 | 0 | 1 | 0 |
|---|
| 2行目 | 1 | 0 | 1 |
|---|
5×5の画像の上を、3×3のフィルターが1マスずつ動く。窓が左上にあるとき、出力の0行目・0列目は 1×1 + 2×0 + 0×1 + 0×0 + 1×1 + 3×0 + 2×1 + 0×0 + 1×1 = 5。全部の位置を計算すると、出力は上の行から 5, 6, 3 / 4, 8, 5 / 8, 2, 7 になる
左上の位置を、手で確かめる
窓が左上にあるとき、窓の中は次の9マスです。
フィルターが1のマス(四すみと真ん中)だけが、足し算に残ります。1+0+1+2+1=5 で、出力の左上は5になります。
式で書くと
出力の i 行目・j 列目の値は、次のように書けます。
出力[i,j]=a=0∑2b=0∑2画像[i+a, j+b]×フィルター[a,b]
∑ が2つ並んでいるのは、「a を0から2まで、その中で b を0から2まで変えながら、全部足す」という意味です。a がフィルターの中の行、b が列を表すので、3×3の9マス分を足していることになります。i と j は窓の左上の位置で、窓を滑らせると変わります。
出力は、画像より小さくなる
3×3の窓は、5×5の画像の中で、横に3通り(左端が0列目・1列目・2列目)、縦に3通りの位置に置けます。そのため、出力は3×3です。
一般に、一辺が N マスの画像に、一辺が k マスのフィルターを使うと、出力の一辺は N−k+1 マスになります。5×5の画像なら 5−3+1=3、手書き数字の8×8の画像なら 8−3+1=6 です。
パディングとストライド
実際のCNNでは、窓の動かし方を、次の2つの設定で変えることがあります。
- パディング: 画像のまわりを、0のマスで囲んでから畳み込みをします。まわりを1マスずつ囲むと、5×5の画像は7×7になり、3×3のフィルターでの出力は 7−3+1=5 で、元の画像と同じ5×5になります。画像の端のマスも、窓の真ん中で見られるようになります
- ストライド: 窓を1マスずつではなく、何マスかおきに動かします。ストライドが2なら、窓を2マスずつ動かすので、出力は縦も横もおよそ半分になります
まわりを囲むマスの数を P、窓を動かすマスの数を S とすると、出力の一辺は次の式で求められます。
⌊SN+2P−k⌋+1
⌊ ⌋ は、小数点以下を切り捨てるという記号です。この章では、ずっと P=0(囲まない)、S=1(1マスずつ)なので、この式は N−k+1 と同じになります。
なお、フィルターは カーネル とも呼ばれます。どちらも同じもの(窓に掛ける数の表)を指します。
NumPyで畳み込みを書く
窓を滑らせる動きは、第2章の for 文を2重にすると書けます。for 文の中に、もう1つ for 文を書く形です。外側の for 文で窓の行 i を、内側の for 文で窓の列 j を動かします。
2重の for 文では、外側が1回進むごとに、内側が最初から最後まで回ります。5×5の画像なら、i = 0 のときに j = 0, 1, 2、次に i = 1 のときに j = 0, 1, 2……と進み、全部で 3×3=9 回、窓の中を計算します。窓を「右へ、右端まで来たら次の行の左端へ」と動かす順番と同じです。
import numpy as np
def convolve(image, filt):
h, w = image.shape # 画像の行の数・列の数
k = filt.shape[0] # フィルターの一辺
out = np.zeros((h - k + 1, w - k + 1)) # 出力の表(最初は全部0)
for i in range(h - k + 1):
for j in range(w - k + 1):
window = image[i:i+k, j:j+k] # 窓の中(k行×k列)を切り出す
out[i, j] = np.sum(window * filt) # 同じ位置同士を掛けて、全部足す
return out
image = np.array([[1, 2, 0, 1, 0],
[0, 1, 3, 1, 2],
[2, 0, 1, 0, 1],
[1, 3, 0, 2, 0],
[0, 1, 2, 1, 1]])
filt = np.array([[1, 0, 1],
[0, 1, 0],
[1, 0, 1]])
convolve(image, filt) # → [[5., 6., 3.], [4., 8., 5.], [8., 2., 7.]](np.zeros で作ったので小数で表示される)
image[i:i+k, j:j+k] は、i 行目から k 行分、j 列目から k 列分を切り出します。第3章の scores[:3](先頭から3つ)は、始まりを省略した書き方でした。[始まり:終わり] と書くと、「始まり」の番号から「終わり」の 1つ手前 までを取り出します。例えば i = 1、k = 3 なら 1:4 で、1・2・3行目の3行分です。カンマの前が行、後ろが列の範囲です
window * filt は、同じ形の配列同士の * なので、同じ位置同士の掛け算 になります。np.sum で、その9個を全部足します
機械学習でどう使われるか
このレッスンでは、フィルターの数を人間が決めました。CNNでは、フィルターの9個の数が、学習で決まる重み です。第6章のニューロンの重みと同じように、正解との差(損失)が小さくなる向きに、誤差逆伝播で少しずつ調整されます。人間が「どんな模様を探すか」を決めなくても、データから、見分けるのに役立つフィルターが見つかります。
よくある誤解
- 「畳み込みは、画像を小さくするための処理」: 出力が小さくなるのは、窓を画像の外に出さないことによる副作用です。目的は、小さな範囲の模様を調べることです(実際のCNNでは、画像のまわりを0で囲んで、出力を同じ大きさにすることもよくあります)
- 「数学の授業で習う畳み込みと、まったく同じ計算」: 数学で「畳み込み」と呼ぶ計算では、フィルターを上下左右に反転させてから重ねます。CNNでは、反転させずにそのまま重ねるのが一般的です。フィルターの数は学習で決まるので、反転させる計算で学習しても、反転した形のフィルターが見つかるだけで、見分ける力は変わりません
振り返り
- 畳み込みは、画像の上で窓を滑らせながら、窓の中とフィルターの内積(同じ位置同士を掛けて足す)を並べていく計算
- 一辺 N の画像に一辺 k のフィルターを使うと、出力の一辺は N−k+1
- CNNでは、フィルターの数は学習で決まる重み