🐸

CNNの畳み込みとプーリングを解説

2024/02/05に公開

今回はCNNの畳み込みとプーリングについて解説します。

CNN

CNNは畳み込み機能を持ったニューラルネットワークです。
通常のニューラルネットに畳み込みとプーリングが追加されたものです。

畳み込みとプーリング

1. 畳み込みとは?

畳み込みは、「カーネルと同じ様な形状のデータを検知する」操作です。

具体的には、入力に対してカーネルを内積(各要素を掛けて足す)する操作です
畳み込みによって入力からカーネルと似た特徴が抽出されたものを特徴マップと言います。

1.1 感覚的な理解

感覚的に、畳み込みとはどの様な操作でしょうか。
上記では、「カーネルと同じ様な形状のデータを検知する」と説明しましたが、これをコードで視覚化してみます。

・畳み込み

from scipy.signal import correlate
import numpy as np

# 入力。二次元の画像(シンプルな例として5x5配列)
image = np.array([
    [1, 0, 0, 0, 0],
    [1, 0, 0, 0, 0],
    [1, 0, 1, 0, -1],
    [1, 0, 1, 0, -1],
    [1, 0, 1, 0, -1]
])

# カーネル(フィルタ)(3x3配列)
# これと同じような形状を抽出する
kernel = np.array([
    [1, 0, -1],
    [1, 0, -1],
    [1, 0, -1]
])

# 二次元畳み込みの計算 (ストライド1,パディング0)=カーネルが入力の左上から1ずつ動いて内積をとる
conv_result = correlate(image, kernel, 'valid')

print("2D Convolution result:")
print(conv_result)

・出力(特徴マップ)

2D Convolution result:
[[2 0 2]
 [1 0 4]
 [0 0 6]]

出力行列の右下が大きくなっていることがわかります。これは、入力の右下の部分がカーネルと同じ形状であるため、強く検出されたのです。

具体的な畳み込み動作の説明は少し煩雑になります。(行列同士の内積)
まずカーネル(3×3)と入力行列の左上部分(3×3)を重ねて、各行列の重なった要素をそれぞれ掛け算し、その結果を全て足しています。
この計算結果が、出力行列の左上に入ります。そして、カーネルを重ねる場所を一つ右にずらして同じ計算をする、さらにずらして同じ計算…という風に計算を繰り返し、出力行列を求めます。

これによりカーネルに似た特徴が検出されます。これを畳み込みと言います。

1.2 使い方

では畳み込みの使い方について、3つ説明します。
畳み込みは

  1. 判別器
  2. 特徴抽出
  3. 物体検出
    として使用できます。

1.2.1 判別器

まずは判別器についてです。

例えば次の様なカーネルを考えます。
・縦線カーネル

kernel = np.array([
    [0, 1, 0],
    [0, 1, 0],
    [0, 1, 0]
])

これは、縦の線を検知するカーネルと考えられます。
これに対する入力として、次のような「0」と「1」の入力画像を考えます。
・「0」「1」の入力画像

# おおまかに0の形
image0 = np.array([
    [0, 1, 1, 1, 0],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [0, 1, 1, 1, 0]
])
# おおまかに1の形
image1 = np.array([
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0]
])

これに対して畳み込みを行った結果、
・出力(特徴マップ)

# 特徴マップ
# 入力0
# 上と下に縦線らしきものがあったよ
[[1 1 1]
 [0 0 0]
 [1 1 1]]
# 入力1
# 真ん中に縦線があったよ
[[0 3 0]
 [0 3 0]
 [0 3 0]]

のように、出力から「0」と「1」を判別できます。
この時点で、我々は既に01判別器を作ることに成功しました。(簡単そうに見えますが、画像が0か1かを機械的に判別するのは意外に難しいのです。)

簡易判別器コード例
from scipy.signal import correlate
import numpy as np

# 二次元の画像(シンプルな例として5x5配列)
image = np.array([
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0]
])

# カーネル(フィルタ)(3x3配列)
kernel = np.array([
    [0, 1, 0],
    [0, 1, 0],
    [0, 1, 0]
])

# 二次元畳み込みの計算
conv_result = correlate(image, kernel, 'valid')

map_1 = np.array([
    [0, 3, 0],
    [0, 3, 0],
    [0, 3, 0]
])


if np.array_equal(conv_result, map_1):
    print("入力画像は1です")
else:
    print("入力画像は0です")

1.2.2 特徴抽出

続いて特徴抽出です。

前述した通り、CNNではカーネルによって形状を検出することができます。これにより、画像に何が含まれるかを知ることができます。

カーネルを変更することで、「横線」「カーブ」「円」など様々なものを検出できます。

  • 横線やカーブ検出用のカーネル
# 横線
horizontal_kernel = np.array([
    [0, 0, 0],
    [1, 1, 1],
    [0, 0, 0]
])

# カーブ
curve_kernel = np.array([
    [1, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
    [0, 0, 0, 1]
])

これらのカーネルで畳み込みをすることにより、横線やカーブといった「特徴が、入力のどこに存在するのか」という情報が特徴マップとして得られます。
これが特徴抽出です。

1.2.3 物体検出

ここまでの説明では、線なんか検出できても嬉しくないのでは?と考える方も多いと思います。

今回は1度の特徴抽出のみを行いましたが、畳み込みは繰り返し行うことで効力を発揮します。実際には何枚ものカーネルを使用して畳み込みを行い、その出力(特徴マップ)に対して再度繰り返して畳み込みを行います。

-- 例 ----
入力画像
↓畳み込み「線」「円」「カーブ」を検出
特徴マップ
↓畳み込み「耳」「顔」を検出
特徴マップ
↓畳み込み「犬」「猫」を検出
特徴マップ
↓全結合
これは犬です!
---------

最初の畳み込みで「線」「カーブ」「円」「色」のような低次元の情報をいくつも抽出し、さらにこれらをまとめて畳み込むことで、高次の「耳」や「顔」、さらに「犬」や「猫」のような特徴を検査できるようになります。
つまり畳み込みを重ねることで、高度な物体の検出を行うことができます。

このように、畳み込みという操作は特徴を抽出する操作であり、これを重ねることで汎用的な検査機を作ることができるのです。

-- まとめ ----
畳み込みについて複数の使い方を見て来ましたが、その本質はパターン検出であり、これを応用して物体の検出を行っています。

2. プーリングとは?

次はプーリングについて解説します。

プーリングも畳み込みと同じように、入力に重ねる形で処理が行われます。ここでは、代表的次の二つについて説明します。
・maxプーリング
・averageプーリング

2.1 maxプーリング

maxプーリングも畳み込みに似た動作であり、フィルタと重なっている範囲での最大値を取ります。
2×2のフィルタでmaxプーリングする時の動作を確認してみましょう。

# Maxプーリング。4分割した領域でそれぞれ最大値を取る
# ストライド2: フィルタがプーリングを行った後2つ横に移動する
# 入力
Input Matrix:
[[ 1, 2, 3, 4]
 [ 5, 6, 7, 8]
 [ 9, 10, 11, 12]
 [13, 14, 15, 16]]
 # 出力
Output Matrix after 2x2 Max Pooling:
[[ 6,  8]
 [14, 16]]
maxプーリング関数
import numpy as np

def max_pooling_2x2(input_matrix):
    # 入力行列の形状を取得
    rows, cols = input_matrix.shape
    # プーリング後の形状で出力行列を作成
    output_matrix = np.zeros((rows//2, cols//2))

    for i in range(0, rows, 2):
        for j in range(0, cols, 2):
            # 2x2領域の最大値を見つける
            max_value = np.max(input_matrix[i:i+2, j:j+2])
            # 出力行列に最大値をセット
            output_matrix[i//2, j//2] = max_value

    return output_matrix

# 入力行列(例)
input_matrix = np.array([[1, 2, 3, 4],
                         [5, 6, 7, 8],
                         [9, 10, 11, 12],
                         [13, 14, 15, 16]])

# maxプーリングを適用
output_matrix = max_pooling_2x2(input_matrix)

print("Input Matrix:")
print(input_matrix)

print("Output Matrix after 2x2 Max Pooling:")
print(output_matrix)

畳み込みの「積和処理」を、「最大値を取る処理」に入れ替えたもの、と解釈することもできます。
要するに、入力行列の各部分から最大値を抜き出しているのです。

2.2 averageプーリング

averageプーリングは、maxプーリングの「最大値を取る処理」を「平均を取る処理」に置き換えたものです。

# 入力行列
Input Matrix:
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]
 [13 14 15 16]]
# 出力
Output Matrix after 2x2 Average Pooling:
[[ 3.5  5.5]
 [11.5 13.5]]
averageプーリング関数
import numpy as np

def average_pooling_2x2(input_matrix):
    # 入力行列の形状を取得
    rows, cols = input_matrix.shape
    # プーリング後の形状で出力行列を作成
    output_matrix = np.zeros((rows//2, cols//2))

    for i in range(0, rows, 2):
        for j in range(0, cols, 2):
            # 2x2領域の平均値を計算
            average_value = np.mean(input_matrix[i:i+2, j:j+2])
            # 出力行列に平均値をセット
            output_matrix[i//2, j//2] = average_value

    return output_matrix

# 入力行列(例)
input_matrix = np.array([[1, 2, 3, 4],
                         [5, 6, 7, 8],
                         [9, 10, 11, 12],
                         [13, 14, 15, 16]])

# Averageプーリングを適用
output_matrix = average_pooling_2x2(input_matrix)

print("Input Matrix:")
print(input_matrix)

print("Output Matrix after 2x2 Average Pooling:")
print(output_matrix)

それぞれの部分の平均をとっていることがわかります。

2.3 使い方

使い方について説明します。
maxプーリングもaverageプーリングも基本の使い方は同じです。

プーリングの主な役割は、「データ誤差に対する特徴抽出の安定性を向上させること」です。
動作を確認してみましょう。

# 右にズレた画像
# input_matrix_1
[[1, 0, 1, 0],
 [1, 0, 1, 0],
 [1, 0, 1, 0],
 [1, 0, 1, 0]])
# input_matrix_2
[[0, 1, 0, 1],
 [0, 1, 0, 1],
 [0, 1, 0, 1],
 [0, 1, 0, 1]]

# 出力は同じ
# Output Matrix_1
[[1. 1.]
 [1. 1.]]
# Output Matrix_2
[[1. 1.]
 [1. 1.]]
誤差に対するmaxプーリング
import numpy as np

def max_pooling_2x2(input_matrix):
    # 入力行列の形状を取得
    rows, cols = input_matrix.shape
    # プーリング後の形状で出力行列を作成
    output_matrix = np.zeros((rows//2, cols//2))

    for i in range(0, rows, 2):
        for j in range(0, cols, 2):
            # 2x2領域の最大値を見つける
            max_value = np.max(input_matrix[i:i+2, j:j+2])
            # 出力行列に最大値をセット
            output_matrix[i//2, j//2] = max_value

    return output_matrix

# 入力行列(例)
input_matrix_1 = np.array([[1, 0, 1, 0],
                           [1, 0, 1, 0],
                           [1, 0, 1, 0],
                           [1, 0, 1, 0]])

input_matrix_2 = np.array([[0, 1, 0, 1],
                           [0, 1, 0, 1],
                           [0, 1, 0, 1],
                           [0, 1, 0, 1]])

# Maxプーリングを適用
output_matrix_1 = max_pooling_2x2(input_matrix_1)
output_matrix_2 = max_pooling_2x2(input_matrix_2)



print("Output Matrix_1")
print(output_matrix_1)

print("Output Matrix_2")
print(output_matrix_2)

畳み込みによる検出の後にプーリングを行うことで、ズレた画像も同じものを示すと判断できます。
例えば、「1」と書いてある画像が横に数ミリズレただけで、別の画像であると判断するのは期待する動作ではないでしょう。

プーリングを使うことで入力の誤差に強くなります。
これがプーリングの嬉しい点です。

max,averageプーリングの使い分け

max,averageプーリングぞれぞれの簡単な特徴を次に示します。

  • maxプーリング
    特徴の抽出: 目立った特徴を抽出できる
    局所的頑健性: 小さな部分の誤差に対して強い
    ・向いているタスク
    物体の形状やエッジの認識

  • averageプーリング
    情報の平均化: 滑らかな特徴マップを生成するので、情報の見逃しが少ない
    背景情報の保存: 大域的な情報を保持することができる
    ・向いているタスク
    テクスチャ認識、セグメンテーション

どちらにも利点と欠点があります。
最終的には両方を試し、性能の良いプーリング方式を選ぶようにしましょう。

CNNでの応用

今回、カーネルの値は固定でしたが、CNNでは学習の過程でカーネル内部の重みが更新されます(5×5等の形状は変化しません)。これによって、必要な情報を拾い上げるカーネルが作成され、画像を認識できるようになるのです。

※プーリングには学習されるパラメータ(重み)はありません。

まとめ

今回は、CNNの特徴である畳み込みとプーリングについて解説しました。
時間がある時に、これらを使用して簡易的なCNNを作成しつつ解説したいと考えています。

それでは、読んでいただきありがとうございました。

補足

・物体検出の補足
「線」「円」「色」を検出した特徴マップをまとめてさらに畳み込むことで、線がここ、円がここ、この色、、、→これは「耳」の可能性が高い!となり、次は「耳」がここ、「鼻」がここ、「顔の輪郭」がここ、、、→これは「犬」の可能性が高い!となります。

得られた特徴マップをまとめて畳み込むということが非常に重要なのです。「線」「円」「色」の検出結果を全て使用して「耳」や「鼻」や「輪郭」の検出を行い、その結果を全て使用してさらに「犬」などの検出を行うのです。

畳み込みを重ねることで高次の検出ができるというのは、前の畳み込みの出力をまとめて畳み込むことで、複数の特徴から判断を行うことができているのです。

Discussion