からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

7.2.2:畳み込み演算の可視化:2次元データ版【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning 1』の攻略ノートです。『ゼロつくシリーズ』の補助となるように解説を加えます。本と一緒に読んでください。
 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事では、畳み込み演算の処理や影響をPythonで可視化します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

7.2.2 畳み込み演算の可視化:2次元データ版

 2次元データ(1データ・1チャンネルのピクセルデータ)に対する畳み込み演算(convolution operation)を確認します。畳み込みの処理は、畳み込み層(convolution layer)で行われます。
 パディングについては「7.2.3:パディングの可視化:2次元データ版【ゼロつく1のノート(実装)】 - からっぽのしょこ」、ストライドについては「ストライドの可視化:2次元データ版」を参照してください。

 利用するライブラリを読み込みます。

# ライブラリを読込
import numpy as np
import matplotlib.pyplot as plt


数式で確認

 まずは、畳み込み演算の処理を数式で確認します。ここでは、Pythonのインデックスに合わせて、0から添字を割り当てます。(添字用の記号が乱立して可読性が犠牲になっていますが、良案を思い付きませんでした。頑張って読んでください。)

 入力データの高さを  H_{\mathrm{I}}、横幅を  W_{\mathrm{I}} として、 (H_{\mathrm{I}}, W_{\mathrm{I}}) の行列を入力データ  \mathbf{X} とします。

 \displaystyle
\mathbf{X}
    = \begin{bmatrix}
          x_{0,0} & x_{0,1} & \cdots & x_{0,W_{\mathrm{I}}-1} \\
          x_{1,0} & x_{1,1} & \cdots & x_{1,W_{\mathrm{I}}-1} \\
          \vdots  & \vdots  & \ddots & \vdots \\
          x_{H_{\mathrm{I}}-1,0} & x_{H_{\mathrm{I}}-1,1} & \cdots & x_{H_{\mathrm{I}}-1, W_{\mathrm{I}}-1}
      \end{bmatrix}

 フィルターの高さを  H_{\mathrm{F}}、横幅を  W_{\mathrm{F}} として、 (H_{\mathrm{F}}, W_{\mathrm{F}}) の行列をフィルター(重み)  \mathbf{W} とします。

 \displaystyle
\mathbf{W}
    = \begin{bmatrix}
          w_{0,0} & w_{0,1} & \cdots & w_{0,W_{\mathrm{F}}-1} \\
          w_{1,0} & w_{1,1} & \cdots & w_{1,W_{\mathrm{F}}-1} \\
          \vdots  & \vdots  & \ddots & \vdots \\
          w_{H_{\mathrm{F}}-1,0} & w_{H_{\mathrm{F}}-1,1} & \cdots & w_{H_{\mathrm{F}}-1, W_{\mathrm{F}}-1}
      \end{bmatrix}

 入力サイズがフィルターサイズ以上(  H_{\mathrm{I}} \geq W_{\mathrm{F}} H_{\mathrm{I}} \geq W_{\mathrm{F}} である)必要があります。

  \mathbf{X} からフィルターサイズ  (H_{\mathrm{F}}, W_{\mathrm{F}}) に取り出した要素を

 \displaystyle
\tilde{\mathbf{X}}
    = \begin{bmatrix}
          x_{h_{\mathrm{I}}, w_{\mathrm{I}}}   & x_{h_{\mathrm{I}}, w_{\mathrm{I}}+1}   & \cdots & x_{h_{\mathrm{I}}, w_{\mathrm{I}}+W_{\mathrm{F}}-1} \\
          x_{h_{\mathrm{I}}+1, w_{\mathrm{I}}} & x_{h_{\mathrm{I}}+1, w_{\mathrm{I}}+1} & \cdots & x_{h_{\mathrm{I}}+1, w_{\mathrm{I}}+W_{\mathrm{F}}-1} \\
          \vdots & \vdots & \ddots & \vdots \\
          x_{h_{\mathrm{I}}+H_{\mathrm{F}}-1, w_{\mathrm{I}}} & x_{h_{\mathrm{I}}+H_{\mathrm{F}}-1, w_{\mathrm{I}}+1} & \cdots & x_{h_{\mathrm{I}}+H_{\mathrm{F}}-1, w_{\mathrm{I}}+W_{\mathrm{F}}-1}
      \end{bmatrix}

として、入力データ  \tilde{\mathbf{X}} とフィルター  \mathbf{W} の要素ごとの積の和にバイアス  b を加えた値が、出力データの1つの要素  y_{h_{\mathrm{O}}, w_{\mathrm{O}}} に対応します。

 \displaystyle
\begin{aligned}
y_{h_{\mathrm{O}}, w_{\mathrm{O}}}
   &= \tilde{\mathbf{X}}
      \circledast \mathbf{W}
      + b
\\
   &= \left\{
          \sum_{h_{\mathrm{F}}=0}^{H_{\mathrm{F}}-1} \sum_{w_{\mathrm{F}}=0}^{W_{\mathrm{F}}-1}
                  x_{h_{\mathrm{I}}+h_{\mathrm{F}}, w_{\mathrm{I}}+w_{\mathrm{F}}}
                  w_{h_{\mathrm{F}}, w_{\mathrm{F}}}
          \right\}
          + b
\end{aligned}

 要素ごとの積和を  \mathbf{A} \circledast \mathbf{B}、入力データのインデックスを  h_{\mathrm{I}}, w_{\mathrm{I}}、出力データのインデックスを  h_{\mathrm{O}}, w_{\mathrm{O}}、フィルターのインデックスを  h_{\mathrm{F}}, w_{\mathrm{F}} で表しています。
 取り出し方は、フィルターとストライドのサイズによって決まります。後ほど図と合わせて確認します。
 出力データの高さを  H_{\mathrm{O}}、横幅を  W_{\mathrm{O}} として、 (H_{\mathrm{O}}, W_{\mathrm{O}}) の行列で出力データ  \mathbf{Y} を表します。

 \displaystyle
\mathbf{Y}
    = \begin{bmatrix}
          y_{0,0} & y_{0,1} & \cdots & y_{0,W_{\mathrm{O}}-1} \\
          y_{1,0} & y_{1,1} & \cdots & y_{1,W_{\mathrm{O}}-1} \\
          \vdots  & \vdots  & \ddots & \vdots \\
          y_{H_{\mathrm{O}}-1,0} & y_{H_{\mathrm{O}}-1,1} & \cdots & y_{H_{\mathrm{O}}-1, W_{\mathrm{O}}-1}
      \end{bmatrix}

 出力サイズは、次の式で計算できます。

 \displaystyle
\begin{aligned}
H_{\mathrm{O}}
   &= H_{\mathrm{I}} - H_{\mathrm{F}}
      + 1
\\
W_{\mathrm{O}}
   &= W_{\mathrm{I}} - W_{\mathrm{F}}
      + 1
\end{aligned}

 この記事では、パディングの幅が  P = 0 (パディングしない)、ストライドの幅が  S = 1 (1要素ずつスライドする)の場合のみを扱います。

図で確認

 次は、畳み込み演算の処理を図で確認します。

畳み込み演算の処理

 入力データとフィルター(重み)の形状を設定して、出力データの形状を計算します。

# 入力データのサイズを指定
IH = 5
IW = 6

# フィルターのサイズを指定
FH = 3
FW = 3

# 出力データのサイズを計算
OH = IH - FH + 1
OW = IW - FW + 1
print(OH, OW)
3 4

 入力サイズ  (H_{\mathrm{I}}, W_{\mathrm{I}})、フィルターサイズ  (H_{\mathrm{F}}, W_{\mathrm{F}}) を指定して、出力サイズ  (H_{\mathrm{O}}, W_{\mathrm{O}}) を計算します。
 ここでは、パディングサイズを  P = 0 (またはパディング後の入力データを  \mathbf{X} )、ストライドサイズを  S = 1 とします。

 畳み込み演算における各要素の対応関係をアニメーションで確認します。

 各行列(配列)について、計算に用いる要素を塗りつぶしで示しています。
 入力データに対するフィルターのスライドと出力データの要素が対応しているのを確認できます。

 入力データ  \mathbf{X} の要素をフィルター  \mathbf{W} と同じ形状に取り出して、出力データ  \mathbf{Y} の要素  y_{h_{\mathrm{O}}, w_{\mathrm{O}}} を計算します。取り出し方(対応する入力データの範囲)は、フィルターサイズ  (H_{\mathrm{F}}, W_{\mathrm{F}}) とストライドサイズ  S によって決まります。
  y_{h_{\mathrm{O}}, w_{\mathrm{O}}} に対応する入力データの範囲について、最小のインデックスは、次の式で計算できます。

 \displaystyle
\begin{aligned}
h_{\mathrm{I}}
   &= S h_{\mathrm{O}}
\\
w_{\mathrm{I}}
   &= S w_{\mathrm{O}}
\end{aligned}

  x_{S h_{\mathrm{O}}, S h_{\mathrm{O}}} は、 S 間隔で、 x_{0,0} から縦に  h_{\mathrm{O}} 回、横に  w_{\mathrm{O}} 回スライドした要素を表します。
 また、最大のインデックスは、次の式で計算できます。

 \displaystyle
\begin{aligned}
h_{\mathrm{I}}
   &= S h_{\mathrm{O}}
      + H_{\mathrm{F}}
\\
w_{\mathrm{I}}
   &= S w_{\mathrm{O}}
      + W_{\mathrm{F}}
\end{aligned}

  x_{h_{\mathrm{I}}+H_{\mathrm{F}}, w_{\mathrm{I}}+W_{\mathrm{F}}} は、 x_{h_{\mathrm{I}}, w_{\mathrm{I}}} から縦にフィルターの高さ  H_{\mathrm{F}}、横にフィルターの横幅  W_{\mathrm{F}} 個移動した要素を表します。

 入力データとフィルター、バイアスを作成します。

# 入力データを作成
X = np.arange(IH*IW).reshape((IH, IW))
print(X)
print(X.shape)

# 重みを作成
W = np.random.randn(FH, FW)
print(W.round(2))
print(W.shape)

# バイアスを指定
b = 2.5
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]
 [18 19 20 21 22 23]
 [24 25 26 27 28 29]]
(5, 6)
[[-1.18  2.   -0.86]
 [ 0.4   0.76  0.99]
 [-1.07  0.33 -0.91]]
(3, 3)

 入力データ  \mathbf{X}、フィルター(重み)  \mathbf{W}、バイアス  b を作成します。

 出力データを作成します。

# 出力データを初期化
Y = np.zeros((OH, OW))

# 出力データを計算
for h in range(OH):
    for w in range(OW):
        
        # フィルター範囲を抽出
        tmp_X = X[h:(h+FH), w:(w+FW)]
        
        # 入力データと重みの積和 + バイアスを計算
        Y[h, w] = np.sum(tmp_X * W) + b
print(Y.round(1))
print(Y.shape)
[[-2.9 -2.4 -1.9 -1.5]
 [-0.   0.4  0.9  1.4]
 [ 2.8  3.2  3.7  4.2]]
(3, 4)

 出力データ  \mathbf{Y} を計算します。出力データの要素  y_{h,w} ごとに、対応する入力データの要素を取り出して計算します。ただしこのコードは、ストライドが1の場合のみ処理できます。

・作図コード(クリックで展開)

 出力データの要素を指定して、入力データにおけるフィルターの範囲の描画用の配列を作成します。

# 出力データのインデックスを指定
Oh = 1
Ow = 2

# マスク用の配列を作成
X_bool = np.tile(True, reps=X.shape)
X_bool[Oh:(Oh+FH), Ow:(Ow+FW)] = False
print(X_bool)

# 入力データをマスク
X_mask = np.ma.masked_array(X, X_bool)
print(X_mask)
[[ True  True  True  True  True  True]
 [ True  True False False False  True]
 [ True  True False False False  True]
 [ True  True False False False  True]
 [ True  True  True  True  True  True]]
[[-- -- -- -- -- --]
 [-- -- 8 9 10 --]
 [-- -- 14 15 16 --]
 [-- -- 20 21 22 --]
 [-- -- -- -- -- --]]

 出力データの1つの要素のインデックス  h_{\mathrm{O}}, w_{\mathrm{O}} を指定して、入力データの対応しない要素をマスクします。

 出力データにおけるフィルターの範囲の描画用の配列を作成します。

# マスク用の配列を作成
Y_bool = np.tile(True, reps=Y.shape)
Y_bool[Oh, Ow] = False
print(Y_bool)

# 出力データをマスク
Y_mask = np.ma.masked_array(Y, Y_bool)
print(Y_mask.round(2))
[[ True  True  True  True]
 [ True  True False  True]
 [ True  True  True  True]]
[[-- -- -- --]
 [-- -- 0.89 --]
 [-- -- -- --]]

 指定したインデックス以外の要素をマスクします。

 畳み込み演算のグラフを作成します。

# グラデーションの中心の調整用の範囲を設定
XY_max = np.ceil(max(X.max(), abs(X.min()), Y.max(), abs(Y.min())))
W_max  = np.ceil(max(W.max(), abs(W.min())))

# グラフサイズの調整値を指定
r = 1.0

# 畳み込み演算を作図
fig, axes = plt.subplots(nrows=1, ncols=4, constrained_layout=True, 
                         figsize=((IW+FW+1+OW)*r, IH*r), width_ratios=[IW, FW, 1, OW], 
                         facecolor='white', dpi=100)
fig.suptitle('convolution', fontsize=20)

# 入力データを描画
ax = axes[0]
ax.pcolor(X, cmap='jet', vmin=-XY_max, vmax=XY_max, edgecolor='gray') # 入力データ
ax.pcolor(X_mask, cmap='jet', vmin=-XY_max, vmax=XY_max, 
          hatch='x', edgecolor='green', linewidth=1.5) # フィルターの範囲
for h in range(IH):
    for w in range(IW):
        ax.text(x=w+0.5, y=h+0.5, s=f'{X[h, w]:.1f}', 
                size=15, ha='center', va='center') # 要素ラベル
ax.set_xticks(ticks=np.arange(IW)+0.5, labels=np.arange(IW))
ax.set_yticks(ticks=np.arange(IH)+0.5, labels=np.arange(IH))
ax.invert_yaxis() # 軸の反転
ax.set_xlabel('width: $W_{in}$')
ax.set_ylabel('height: $H_{in}$')
ax.set_title('input data: $X$', loc='left')
ax.set_aspect('equal', adjustable='box')

# 重みを描画
ax = axes[1]
ax.pcolor(W, cmap='bwr', vmin=-W_max, vmax=W_max, edgecolor='gray') # フィルター
for h in range(FH):
    for w in range(FW):
        ax.text(x=w+0.5, y=h+0.5, s=f'{W[h, w]:.2f}', 
                size=15, ha='center', va='center') # 要素ラベル
ax.set_xticks(ticks=np.arange(FW)+0.5, labels=np.arange(FW))
ax.set_yticks(ticks=np.arange(FH)+0.5, labels=np.arange(FH))
ax.invert_yaxis() # 軸の反転
ax.set_xlabel('width: $W_{fil}$')
ax.set_ylabel('height: $H_{fil}$')
ax.set_title('filter: $W$', loc='left')
ax.set_aspect('equal', adjustable='box')

# バイアスを描画
ax = axes[2]
ax.pcolor([[b]], cmap='jet', vmin=-XY_max, vmax=XY_max) # バイアス
ax.text(x=0.5, y=0.5, s=f'{b:.1f}', 
        size=15, ha='center', va='center') # 要素ラベル
ax.set_xticks(ticks=[0.5], labels=[0])
ax.set_yticks(ticks=[0.5], labels=[0])
ax.set_xlabel('width: 1')
ax.set_ylabel('height: 1')
ax.set_title('bias: $b$', loc='left')
ax.set_aspect('equal', adjustable='box')

# 出力データを描画
ax = axes[3]
ax.pcolor(Y, cmap='jet', vmin=-XY_max, vmax=XY_max, edgecolor='gray') # 出力データ
ax.pcolor(Y_mask, cmap='jet', vmin=-XY_max, vmax=XY_max, 
          hatch='x', edgecolor='green', linewidth=1.5) # フィルターの範囲
for w in range(OW):
    for h in range(OH):
        ax.text(x=w+0.5, y=h+0.5, s=f'{Y[h, w]:.1f}', 
                size=15, ha='center', va='center') # 要素ラベル
ax.set_xticks(ticks=np.arange(OW)+0.5, labels=np.arange(OW))
ax.set_yticks(ticks=np.arange(OH)+0.5, labels=np.arange(OH))
ax.invert_yaxis() # 軸の反転
ax.set_xlabel('width: $W_{out}$')
ax.set_ylabel('height: $H_{out}$')
ax.set_title('output data: $Y$', loc='left')
ax.set_aspect('equal', adjustable='box')

plt.show()

畳み込み演算の入出力

 入出力データの対応する要素の1組みを網掛けで示しています。

 畳み込み演算をアニメーションで確認します。

 入力データの要素(ピクセル)ごとに重み付けしてから和をとっています。また、バイアスは全ての要素に等しく影響します。

 この記事では、畳み込み演算を確認しました。次の記事では、パディングを確認します。

参考文献

おわりに

 春にはゼロつくも5巻が発売され半分くらい読んだ(まだ手は動かさず読んだだけだけど)のですが、気付けばn年ぶりに1巻の新記事を書いていました。
 7章はたぶん一度も加筆修正できておらず、im2colについても初稿を書いた時点でもっと詳しく踏み込みたかったのですが、手を付けられていませんでした。先日、誤記のコメントをいただきまして、その修正のための確認も兼ねて、7章の全面加筆修正を行うことにしました。
 n年が経つ間にmatplotlib力が上がっており、想定以上に分かりやすく図解できたのではと自画自賛しています。図を作るのが楽しくて、解説を書くのに詰まったら次の図を作り始め、記事の執筆が滞りがちなんですが。

 もうじきNumPyのバージョンが2.0になるとのことで、動作確認も兼ねて1巻の加筆修正をしたいなー、でもそんな余裕ないよなー。

 2024年6月6日は、鈴木愛理さんのソロデビュー6周年の日です。

 もう6年も経つのかぁ。グループ活動時代はギリギリ知らないのですが、ソロでの初武道館公演が私にとって初遠征、初武道館現場でした。懐かしいなぁ。
 もうアイドル界隈の外にも完全に売れましたね。これからも歌も演技も続けるとのことで、よきかなよきかな。おめでとうございます!

【次節の内容】

www.anarchive-beta.com