このシリーズではE資格対策として、シラバスの内容を項目別にまとめています。

E資格まとめ

試験概要 ディープラーニングの理論を理解し、適切な手法を選択して実装する能力や知識を有しているかを認定する。 1.応用数学 (1)確率・統計 (2)情報理論 2.機…

全結合層と畳み込み層

全結合層(Affineレイヤ)と畳み込み層(Convolutionレイヤ)は、ニューラルネットワークの中で異なる役割を果たします。それぞれの特性と違いについて詳しく解説します。

全結合層(Affineレイヤ): 全結合層は、ニューラルネットワークの各層のすべてのニューロンが、前の層のすべてのニューロンと接続されている層です。これは、各入力特徴が出力にどのように影響するかを学習するために使用されます。全結合層は、入力と重みの内積を計算し、バイアスを加えることで動作します。ReLU(Rectified Linear Unit)は一般的な活性化関数で、非線形性を導入し、ネットワークがより複雑なパターンを学習するのを助けます。ReLUは、負の入力に対しては0を出力し、正の入力に対してはそのままの値を出力します。

畳み込み層(Convolutionレイヤ): 畳み込み層は、主に画像や音声などのグリッド状のデータを処理するために使用される特殊な種類の層です。畳み込み層は、小さな領域(フィルターまたはカーネルと呼ばれる)をスライドさせて入力全体に適用し、特徴マップを生成します。これにより、ネットワークは空間的な構造を学習し、位置に関係なく特徴を検出することができます。畳み込み層は、フィルターの重みとバイアスを学習します。

全結合層と畳み込み層の違い

全結合層と畳み込み層の主な違いは、全結合層がデータの空間的な構造を無視するのに対し、畳み込み層はその構造を利用することです。全結合層は入力をフラットなベクトルとして扱いますが、畳み込み層は入力の形状(例えば、画像の場合は高さ、幅、チャンネル)を保持します。これにより、畳み込み層は特に画像などの空間的な構造を持つデータに対して有効です。

また、畳み込み層はパラメータの共有(同じフィルターを入力の異なる部分に適用)を行うため、全結合層と比較してパラメータの数が大幅に少なくなります。これは計算効率を向上させ、過学習を防ぐ助けとなります。

さらに、畳み込み層は局所的な特徴を捉える能力があります。つまり、小さな領域内でのパターンや特徴を検出することができます。これは、画像のようなデータでは非常に有用です。なぜなら、画像の一部に存在する特徴(たとえば、エッジやテクスチャ)が、画像の他の部分でも同様に重要である可能性が高いからです。

一方、全結合層は、入力特徴全体を考慮して出力を生成します。これは、入力特徴間の相互作用や依存関係を捉えるのに有用です。しかし、このアプローチは、パラメータの数が多くなるため、計算コストが高くなり、過学習のリスクが増えます。

畳み込み演算

畳み込み演算(Convolution)は、一部分の情報を全体に適用する一種の数学的操作です。フィルタ(またはカーネル)と呼ばれる小さな行列が用いられ、このフィルタが信号上を移動(「スライド」)しながら、その各部分との要素ごとの積の和(ドット積)を計算します。

以下に4×4の画像と3×3のフィルタによる畳み込み演算の例を示します。

これらを用いて畳み込み演算を行うと、新しい2×2の出力画像が生成されます。各要素は元の画像の対応する部分とフィルタを要素ごとに掛け合わせ、その和を計算したものになります。

この例は非常に単純化されていますが、このような操作を用いて実際の畳み込みニューラルネットワークでは、画像から特徴を抽出します。また、通常の畳み込みニューラルネットワークでは、複数のフィルタを同時に使用し、それぞれの結果をスタックします。これにより、ネットワークは画像の異なる特徴(エッジ、色、テクスチャなど)を同時に学習することができます。

Pythonでの実装コードは以下のようになります。

import numpy as np

# 2次元配列 (4x4)
a = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
    [13, 14, 15, 16]
])

# 畳み込みを行うカーネル (3x3)
kernel = np.array([
    [1, 0, -1],
    [0, 1, 0],
    [-1, 0, 1]
])

# 入力配列とカーネルのサイズ
n, m = a.shape
kn, km = kernel.shape

# 畳み込み結果を格納する配列 (n-kn+1)x(m-km+1)
result = np.zeros((n - kn + 1, m - km + 1))

# 畳み込み演算
for i in range(result.shape[0]):
    for j in range(result.shape[1]):
        result[i, j] = np.sum(a[i:i+kn, j:j+km] * kernel)

print(result)

実行結果:

[[ 6.  7.]
 [10. 11.]]

パディング

畳み込み演算の一部として行われるパディングとは、主に画像データなどのマトリクスに対し、境界部分に特定の値(通常は0)を追加して元のマトリクスを囲む操作のことを指します。この操作は特に、ディープラーニングや画像処理のコンボリューション(畳み込み)ニューラルネットワーク(CNN)において一般的です。

パディングの主なメリットは以下の通りです:

  1. 情報の損失の防止: 畳み込み操作では、入力のマトリクスの端部はカーネル(またはフィルタ)による探索の対象となる回数が中心部に比べて少なくなるため、情報が失われやすいです。パディングを行うことで、これらの端部の情報が次のレイヤーにもしっかりと伝播することを確保します。
  2. 出力サイズの制御: パディングなし(valid padding)の畳み込みでは、カーネルのサイズによっては出力のサイズが入力のサイズよりも小さくなってしまいます。これを避け、出力のサイズを入力のサイズと一致させたい場合(same padding)、パディングが有用です。
  3. 畳み込みの自由度の向上: パディングにより、カーネルを元のデータの任意の位置に配置して畳み込むことが可能になります。これにより、畳み込みの自由度が増し、より豊かな特徴抽出が可能となります。

パディングの種類

畳み込みネットワーク(CNN)におけるパディングの種類は、データの性質や問題設定により変わります。以下に具体的な例を交えて解説します:

  1. “Same” と “Valid” パディング:”Same”パディングは、出力のサイズを入力のサイズと同じに保つためのパディング方法です。例えば、入力が画像であり、その全体の特徴を保持しながら畳み込みを行いたい場合には、”Same”パディングが適しています。一方、”Valid”パディングはパディングを行わない方法で、出力は入力よりも小さくなります。これは情報のダウンサンプリングが必要な場合や、過度なパディングによる過学習を避けたい場合に適しています。
  2. パディングの量:パディングの量も重要な要素です。パディングが多すぎると、モデルがパディングされた部分(通常はゼロ)に過度に依存する可能性があり、これが過学習を引き起こす可能性があります。一方、パディングが少なすぎると、入力の端部の情報が失われてしまい、モデルの性能が低下する可能性があります。これは特に、端部の情報が重要な役割を果たす画像認識や自然言語処理などのタスクにおいて問題となります。
  3. パディングの種類:一般的にはゼロパディングが使われますが、場合によっては他の種類のパディングが有用な場合があります。例えば、鏡像パディング(入力の端部を反転したものをパディングとして使用する)は、画像の端部近くで高頻度の情報を保持することが必要な場合に有用です。もう1つのパディング方法は、エッジパディング(入力の端部の値をコピーしてパディングとして使用する)で、これは画像の端部の情報がそのまま重要な場合に適しています。

パディングの実装

PythonのNumPyライブラリを使って2次元配列の畳み込み演算を行うコードを以下に示します。ここでは、配列を0でパディングしてから3×3のカーネルで畳み込みを行います。

import numpy as np

# 入力の2次元配列
input_array = np.array([[1, 2, 3, 4],
                        [5, 6, 7, 8],
                        [9, 10, 11, 12],
                        [13, 14, 15, 16]])

# カーネル
kernel = np.array([[0, 1, 0],
                   [1, -4, 1],
                   [0, 1, 0]])

# パディング
padded_array = np.pad(input_array, pad_width=1, mode='constant', constant_values=0)

# 畳み込み後の結果を格納する2次元配列
output_array = np.zeros_like(input_array)

# 畳み込み演算
for i in range(output_array.shape[0]):
    for j in range(output_array.shape[1]):
        output_array[i, j] = np.sum(padded_array[i:i+3, j:j+3] * kernel)

print(output_array)

実行結果:

[[  3   2   1  -5]
 [ -4   0   0  -9]
 [ -8   0   0 -13]
 [-29 -18 -19 -37]]

ストライド

畳み込み演算では、入力データ(一般的には画像やそれに類似する2次元または3次元データ)に対してフィルタまたはカーネルを適用します。このフィルタを入力データ上で移動させて各位置での畳み込みを計算しますが、この移動する際のステップ幅がストライドです。

たとえば、ストライドが1の場合、フィルタは入力データ上で1ピクセルごとに移動します。ストライドが2の場合は、2ピクセルごとに移動します。

ストライドを大きくすると、出力データ(畳み込み後の特徴マップ)のサイズは小さくなります。これは、大きいストライドではフィルタが入力データの一部しかカバーしないためです。一方で、ストライドを小さくすると、出力データのサイズは大きくなります。

また、ストライドを変更することは、モデルの容量(パラメータの数)を変更せずに、計算量を減らす一方で、特徴マップの解像度を低下させる効果があります。これは、特に大規模なディープラーニングモデルでの計算効率を上げるために使用されるテクニックです。

出力サイズの計算方法

出力サイズの計算方法は以下の通りです。ここでは、入力サイズを(I_w, I_h)、フィルターサイズを(F_w, F_h)、パディングをP、ストライドをSとします。

出力の幅および高さ(出力サイズ)は次の式を用いて計算できます:

O_w = (I_w – F_w + 2P) / S + 1

O_h = (I_h – F_h + 2P) / S + 1

ここで、

  • I_wとI_hは入力画像の幅と高さです。
  • F_wとF_hはフィルタ(またはカーネル)の幅と高さです。
  • Pはパディングの量で、入力画像の周りに追加される0のレイヤーの数です。
  • Sはストライドの量で、フィルタが入力画像を通過する際のステップの大きさです。

この計算は、入力画像が正方形である場合、またはフィルタが正方形である場合に最もよく使用されます。非正方形の画像またはフィルタについては、幅と高さで個別に計算を行います。

ただし、上記の式は出力サイズが整数になることを前提としています。出力サイズが整数でない場合(つまり、フィルタが入力に完全にフィットしない場合)、通常は出力サイズを下に丸めるか、入力やフィルタのパラメータを調整して整数になるようにします。

ストライドの実装

import numpy as np

# 入力の2次元配列を定義します。
input_array = np.array([[0, 1, 2, 0, 1, 2, 0],
                        [2, 0, 1, 2, 0, 1, 2],
                        [1, 2, 0, 1, 2, 0, 1],
                        [0, 1, 2, 0, 1, 2, 0],
                        [2, 0, 1, 2, 0, 1, 2],
                        [1, 2, 0, 1, 2, 0, 1],
                        [0, 1, 2, 0, 1, 2, 0]])

# 3x3のカーネルを定義します。
kernel = np.array([[0, 1, 2],
                   [2, 0, 1],
                   [1, 2, 0]])

# ストライドの値を定義します。
stride = 2

# 出力のサイズを計算します。
output_size = (input_array.shape[0] - kernel.shape[0]) // stride + 1

# 出力の2次元配列を初期化します。
output_array = np.zeros((output_size, output_size))

# 畳み込み演算を実行します。
for i in range(0, input_array.shape[0] - kernel.shape[0] + 1, stride):
    for j in range(0, input_array.shape[1] - kernel.shape[1] + 1, stride):
        output_array[i // stride][j // stride] = np.sum(input_array[i:i+kernel.shape[0], j:j+kernel.shape[1]] * kernel)

print(output_array)

実行結果:

[[15.  6.  6.]
 [ 6. 15.  6.]
 [ 6.  6. 15.]]

まとめ

最後までご覧いただきありがとうございました。