からっぽのしょこ

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

7.4.2:im2colの可視化:2次元データ版【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事では、im2colの処理や影響をPythonで可視化します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

7.4.2 im2colの可視化:2次元データ版

 2次元データ(1データ・1チャンネルのピクセルデータ)に対する画像から行列への展開(image to column)を確認します。展開の処理は、im2col関数で行われます。
 畳み込み演算については「畳み込み演算の可視化:2次元データ版」、ストライドについては「ストライドの可視化:2次元データ版」を参照してください。

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

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


数式で確認

 まずは、展開の処理を数式で確認します。ここでは、Pythonのインデックスに合わせて、0から添字を割り当てます。

  (H_{\mathrm{I}}, W_{\mathrm{I}}) の行列を入力データ  \mathbf{X} (H_{\mathrm{F}}, W_{\mathrm{F}}) の行列をフィルター(重み)  \mathbf{W} とします。

 \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}
,\ 
\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}

 畳み込み演算では、 \mathbf{X} から  x_{h_{\mathrm{I}}, w_{\mathrm{I}}} を基準としてフィルターサイズ  (H_{\mathrm{F}}, W_{\mathrm{F}}) に取り出した要素を

 \displaystyle
\tilde{\mathbf{X}}_{(h_{\mathrm{I}}, w_{\mathrm{I}})}
    = \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}

として、フィルター  \mathbf{W} との要素ごとの積の和にバイアス  b を加えました。

 \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}}) の行列で出力データ  \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}


  \tilde{\mathbf{X}} の各行を1行に結合して横ベクトル

 \displaystyle
\tilde{\mathbf{x}}_{(h_{\mathrm{I}}, w_{\mathrm{I}})}^{\mathrm{col}}
    = \begin{bmatrix}
          x_{h_{\mathrm{I}}, w_{\mathrm{I}}} & \cdots & x_{h_{\mathrm{I}}, w_{\mathrm{I}}+W_{\mathrm{F}}-1} & 
          x_{h_{\mathrm{I}}+1, w_{\mathrm{I}}} & \cdots & x_{h_{\mathrm{I}}+1, w_{\mathrm{I}}+W_{\mathrm{F}}-1} & 
          \cdots & \cdots & 
          x_{h_{\mathrm{I}}+H_{\mathrm{F}}-1, w_{\mathrm{I}}} & \cdots & x_{h_{\mathrm{I}}+H_{\mathrm{F}}-1, w_{\mathrm{I}}+W_{\mathrm{F}}-1}
      \end{bmatrix}

とし、 \mathbf{W} の各列を1列に結合して縦ベクトル

 \displaystyle
\mathbf{w}^{\mathrm{col}}
    = \begin{bmatrix}
          w_{0,0} \\ \vdots \\ w_{H_{\mathrm{F}}-1,0} \\
          w_{0,1} \\ \vdots \\ w_{H_{\mathrm{F}}-1,1} \\
          \vdots  \\ \vdots \\
          w_{0,W_{\mathrm{F}}-1} \\ \vdots \\ w_{H_{\mathrm{F}}-1, W_{\mathrm{F}}-1}
      \end{bmatrix}

とすると、畳み込み演算の計算をベクトルの内積で表わせます。

 \displaystyle
\begin{aligned}
y_{h_{\mathrm{O}}, w_{\mathrm{O}}}
   &= \tilde{\mathbf{x}}^{\mathrm{col}}
      \cdot \mathbf{w}^{\mathrm{col}}
      + 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}


図で確認

 次は、入力データの展開の処理を図で確認します。

展開の処理

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

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

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

# ストライドのサイズを指定
S = 2

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

 入力サイズを  (H_{\mathrm{I}}, W_{\mathrm{I}})、フィルターサイズを  (H_{\mathrm{F}}, W_{\mathrm{F}})、ストライドサイズを  S を指定して、出力サイズを  (H_{\mathrm{O}}, W_{\mathrm{O}}) を計算します。

 展開における各要素の対応関係をアニメーションで確認します。

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

# 入力データを作成
X = np.arange(IH*IW).reshape((IH, IW))
print(X)
print(X.shape)
[[ 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 30 31 32 33 34]
 [35 36 37 38 39 40 41]]
(6, 7)

 入力データ  \mathbf{X} を作成します。

 展開した入力データを作成します。

# 展開データを初期化
X_col = np.tile(np.nan, reps=(OH*OW, FH*FW))
print(X_col)
print(X_col.shape)

# 展開データの列番号を初期化
j = 0

# 入力データを展開
for Fh in range(FH):
    Ih_max = Fh + S*OH
    for Fw in range(FW):
        Iw_max = Fw + S*OW
        
        # ストライドの間隔で値を格納
        X_col[:, j] = X[Fh:Ih_max:S, Fw:Iw_max:S].flatten()
        
        # 展開データの列番号をカウント
        j += 1
        
        # 途中経過を表示
        print(f'count: {j}, h = {Fh}, w = {Fw}')
        print(X_col)

・出力(クリックで展開)

[[nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]
 [nan nan nan nan nan nan]]
(9, 6)
count: 1, h = 0, w = 0
[[ 0. nan nan nan nan nan]
 [ 2. nan nan nan nan nan]
 [ 4. nan nan nan nan nan]
 [14. nan nan nan nan nan]
 [16. nan nan nan nan nan]
 [18. nan nan nan nan nan]
 [28. nan nan nan nan nan]
 [30. nan nan nan nan nan]
 [32. nan nan nan nan nan]]
count: 2, h = 0, w = 1
[[ 0.  1. nan nan nan nan]
 [ 2.  3. nan nan nan nan]
 [ 4.  5. nan nan nan nan]
 [14. 15. nan nan nan nan]
 [16. 17. nan nan nan nan]
 [18. 19. nan nan nan nan]
 [28. 29. nan nan nan nan]
 [30. 31. nan nan nan nan]
 [32. 33. nan nan nan nan]]
count: 3, h = 0, w = 2
[[ 0.  1.  2. nan nan nan]
 [ 2.  3.  4. nan nan nan]
 [ 4.  5.  6. nan nan nan]
 [14. 15. 16. nan nan nan]
 [16. 17. 18. nan nan nan]
 [18. 19. 20. nan nan nan]
 [28. 29. 30. nan nan nan]
 [30. 31. 32. nan nan nan]
 [32. 33. 34. nan nan nan]]
count: 4, h = 1, w = 0
[[ 0.  1.  2.  7. nan nan]
 [ 2.  3.  4.  9. nan nan]
 [ 4.  5.  6. 11. nan nan]
 [14. 15. 16. 21. nan nan]
 [16. 17. 18. 23. nan nan]
 [18. 19. 20. 25. nan nan]
 [28. 29. 30. 35. nan nan]
 [30. 31. 32. 37. nan nan]
 [32. 33. 34. 39. nan nan]]
count: 5, h = 1, w = 1
[[ 0.  1.  2.  7.  8. nan]
 [ 2.  3.  4.  9. 10. nan]
 [ 4.  5.  6. 11. 12. nan]
 [14. 15. 16. 21. 22. nan]
 [16. 17. 18. 23. 24. nan]
 [18. 19. 20. 25. 26. nan]
 [28. 29. 30. 35. 36. nan]
 [30. 31. 32. 37. 38. nan]
 [32. 33. 34. 39. 40. nan]]
count: 6, h = 1, w = 2
[[ 0.  1.  2.  7.  8.  9.]
 [ 2.  3.  4.  9. 10. 11.]
 [ 4.  5.  6. 11. 12. 13.]
 [14. 15. 16. 21. 22. 23.]
 [16. 17. 18. 23. 24. 25.]
 [18. 19. 20. 25. 26. 27.]
 [28. 29. 30. 35. 36. 37.]
 [30. 31. 32. 37. 38. 39.]
 [32. 33. 34. 39. 40. 41.]]

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

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

 展開データの行を指定して、入出力データのインデックスを作成します。

# 展開データの行番号を指定
i = 1

# 出力データのインデックスを計算
Oh, Ow = np.unravel_index(indices=i, shape=(OH, OW))
print(Oh, Ow)

# 入力データのインデックスを計算
Ih = S * Oh
Iw = S * Ow
print(Ih, Iw)
0 1
0 2

 展開後の入力データの行番号を  i として指定して、対応する出力データのインデックス計算し、さらに入力データのインデックスを計算します。

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

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

# 入力データをマスク
X_mask = np.ma.masked_array(X, X_bool)
print(X_mask)
[[ True  True False False False  True  True]
 [ True  True False False False  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]]
[[-- -- 2 3 4 -- --]
 [-- -- 9 10 11 -- --]
 [-- -- -- -- -- -- --]
 [-- -- -- -- -- -- --]
 [-- -- -- -- -- -- --]
 [-- -- -- -- -- -- --]]

 入力データの対応しない要素をマスクします。

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

# マスク用の配列を作成
X_col_bool = np.tile(True, reps=X_col.shape)
X_col_bool[i, :] = False
print(X_bool)

# 展開データをマスク
X_col_mask = np.ma.masked_array(X_col, X_col_bool)
print(X_col_mask)
[[ True  True False False False  True  True]
 [ True  True False False False  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True]]
[[-- -- -- -- -- --]
 [2.0 3.0 4.0 9.0 10.0 11.0]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- -- -- -- --]]

 展開後のデータの指定した行以外の要素をマスクします。

 入力データの展開のグラフを作成します。

# マスクデータの配色用の最小・最大値を設定
X_min = X.min()
X_max = X.max()

# 画像から行列への展開を作図
fig, axes = plt.subplots(nrows=1, ncols=2, constrained_layout=True, 
                         figsize=(IW+FH*FW, max(IH, OH*OW)), width_ratios=[IW, FH*FW], 
                         facecolor='white', dpi=100)
fig.suptitle('image to column', fontsize=20)

# 入力データを描画
ax = axes[0]
ax.pcolor(X, edgecolor='gray', vmin=X_min, vmax=X_max) # 入力データ
ax.pcolor(X_mask, hatch='x', edgecolor='orange', linewidth=1.5, 
          vmin=X_min, vmax=X_max) # 展開の範囲
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$'+f', filter size: ({FH}, {FW}), stride size: {S}', loc='left')
ax.set_aspect('equal', adjustable='box')

# 展開データを描画
ax = axes[1]
ax.pcolor(X_col, edgecolor='gray', vmin=X_min, vmax=X_max) # 展開した入力データ
ax.pcolor(X_col_mask, hatch='x', edgecolor='orange', linewidth=1.5, 
          vmin=X_min, vmax=X_max) # 展開の範囲
for h in range(OH*OW):
    for w in range(FH*FW):
        ax.text(x=w+0.5, y=h+0.5, s=f'{X_col[h, w]:.1f}', 
                size=15, ha='center', va='center') # 要素ラベル
ax.set_xticks(ticks=np.arange(FH*FW)+0.5, labels=np.arange(FH*FW))
ax.set_yticks(ticks=np.arange(OH*OW)+0.5, labels=np.arange(OH*OW))
ax.invert_yaxis() # 軸の反転
ax.set_xlabel('width: $H_{fil} W_{fil}$')
ax.set_ylabel('height: $H_{out} W_{out}$')
ax.set_title('input data: $X^{col}$', loc='left')
ax.set_aspect('equal', adjustable='box')

plt.show()

im2colによる展開前後のデータ

 展開前後の対応する要素の1組みを網掛けで示しています。

 展開をアニメーションで確認します。

 フィルターの範囲の要素が展開後のデータの行に対応しているのを確認できます。

 実装上の展開の処理をアニメーションで確認します。

 フィルターの要素に対応する全ての要素を取り出して列ごとに格納します。

 ここまでの記事では、2次元のデータの場合を確認しました。次からの記事では、複数チャンネルの場合を確認していきます。

参考文献

おわりに


 先ほど公開されたJuice=Juiceのリーダーの植村あかりさんのソロ曲のMVをどうぞ♪

 そして投稿日は、植村あかりさんの卒コン(1=LINE)の日でした。
 オリジナルメンバーとしては末っ子だったあーりーがリーダーとして華々しく卒業していく様をリアタイできて感無量です。目に見えてあーりーを慕う後輩メンバー達がいる様子も含めて今のJuice=Juiceが大好きです。

【次節の内容】

つづく