からっぽのしょこ

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

7.2.3:ストライドの可視化:2次元データ版【ゼロつく1のノート(実装)】

はじめに

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

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

 この記事では、フィルター(ウィンドウ)のスライドをPythonで可視化します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

7.2.3 ストライドの可視化:2次元データ版

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

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

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


数式で確認

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

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

 \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} からフィルターサイズ  (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}}
   &= \frac{
          H_{\mathrm{I}} - H_{\mathrm{F}}
      }{
          S
      }
      + 1
\\
W_{\mathrm{O}}
   &= \frac{
          W_{\mathrm{I}} - W_{\mathrm{F}}
      }{
          S
      }
      + 1
\end{aligned}


図で確認

 次は、ストライドの処理を図で確認します。

ストライドの処理

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

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

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

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

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

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

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

 各行列(配列)について、計算に用いる要素を塗りつぶしで示しています。ただし、バイアスは省略しています。
 出力データの要素に対応する入力データの範囲が縦横に  S 間隔でスライドするを確認できます。

 入力データ  \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)
[[-0.76 -0.21  1.6 ]
 [-0.06  1.27  0.55]
 [ 0.54 -0.18 -0.66]]
(3, 3)

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

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

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

# 出力データを計算
for Oh in range(OH):
    for Ow in range(OW):
        
        # 入力データのインデックスを計算
        Ih = S * Oh
        Iw = S * Ow
        
        # フィルター範囲を抽出
        tmp_X = X[Ih:(Ih+FH), Iw:(Iw+FW)]
        
        # 入力データと重みの積和 + バイアスを計算
        Y[Oh, Ow] = np.sum(tmp_X * W) + b
print(Y.round(1))
print(Y.shape)
[[13.3 17.4]
 [38.2 42.4]]
(2, 2)

 出力データ  \mathbf{Y} を計算します。出力データの要素  y_{h,w} ごとに、対応する入力データの要素を取り出して計算します。

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

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

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

# 入力データのインデックスを計算
Ih = S * Oh
Iw = S * Ow

# マスク用の配列を作成
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  True  True  True  True]
 [ True  True  True  True  True  True]
 [ True  True False False False  True]
 [ True  True False False False  True]
 [ True  True False False False  True]]
[[-- -- -- -- -- --]
 [-- -- -- -- -- --]
 [-- -- 14 15 16 --]
 [-- -- 20 21 22 --]
 [-- -- 26 27 28 --]]

 出力データの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 False]]
[[-- --]
 [-- 42.37]]

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

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

# グラデーションの中心の調整用の範囲を設定
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$'+f', stride size: {S}', 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組みを網掛けで示しています。

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

 入力データやフィルター、ストライドのサイズによっては使われない要素が出る場合があります。パディングによってサイズを調整できます。

ストライドサイズと出力サイズの関係

 ストライドによる出力データへの影響をアニメーションで確認します。

 入力データの基準となる(範囲内の最小インデックスの)要素のみを塗りつぶしで示しています。
 縦横にストライド  S 間隔で並んでおり、 S が大きくなるほど出力サイズが小さくなるのを確認できます。

 この記事では、畳み込み演算におけるストライドを確認しました。次の記事では、プーリングを確認します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 数式の表記をドウシヨカナ~、図の表現はドチラニシヨカナ~、次の記事はドレニシヨカナ~。

 先日投稿されたAMEFURASSHIの新曲のライブ映像をどうぞ。

 鈴木萌花さんのこの金髪ショートヘアが最高で、この髪型にしたいまである。

【次節の内容】

www.anarchive-beta.com