「ゼロから作るDeep Learning」 Convolution/Poolingレイヤの実装

# 「ゼロから作るDeep Learning」Convolution/Poolingレイヤの実装


Convolutionレイヤについて整理しておきます。2次元配列を複数チャネル持ったデータをあるバッチ数まとめて入力します。このデータ形状を、(バッチ数$N$, チャネル数$C$, 高さ$H$, 幅$W$)とします。Convolutionレイヤ自体は、パラメータとして3次元フィルタを複数個持ち、同じ数だけスカラーのバイアスを持ちます。フィルタの形状を(フィルタ数$FN$, チャネル数$C$, 高さ$FH$, 幅$FW$)とします。出力形状は(バッチ数$N$, チャネル数$FN$, 高さ$OH$, 幅$OW$)となります。ここで、パディング$P$、ストライド$S$としたとき、

$$ OH = \frac{H+2P-FH}{S}+1 $$

$$ OW = \frac{W+2P-FW}{S}+1 $$

です。注目する点は、

* フィルタは、入力データのチャネル数分をまとめて新たな一つの「画像」を作る
* 出力のチャネル数はフィルタ数に等しくなる
* 新しい「画像」のサイズが上述の（$OH$, $OW$）となる

です。

## 4次元配列

Convolutionレイヤへの入力データ$\mathbf{X}$は4次元配列です。データの形状を(バッチ数$N$, チャネル数$C$, 高さ$H$, 幅$W$)とします。

$$\mathbf X = \left[\begin{matrix}\left[\begin{matrix}x_{1111}&x_{1112}&x_{1113}\\\\x_{1121}&x_{1122}&x_{1123}\end{matrix}\right]&\left[\begin{matrix}x_{1211}&x_{1212}&x_{1213}\\\\x_{1221}&x_{1222}&x_{1223}\end{matrix}\right]\\\\ \left[\begin{matrix}x_{2111}&x_{2112}&x_{2113}\\\\x_{2121}&x_{2122}&x_{2123}\end{matrix}\right]&\left[\begin{matrix}x_{2211}&x_{2212}&x_{2213}\\\\x_{2221}&x_{2222}&x_{2223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{3111}&x_{3112}&x_{3113}\\\\x_{3121}&x_{3122}&x_{3123}\end{matrix}\right]&\left[\begin{matrix}x_{3211}&x_{3212}&x_{3213}\\\\x_{3221}&x_{3222}&x_{3223}\end{matrix}\right]\end{matrix}\right] $$

上記の例では、高さ2、幅3の画像が、チャネル数2（横方向）、バッチ数3（縦方向）で並んでいると解釈できます。

## im2colによる展開

「ゼロから作るDeep Learning」で導入されている`im2col`関数の動作を確認します。フィルタサイズが、$FH=2$、$FW=2$の場合に、`im2col`関数で$\mathbf{X}$を2次元配列に変換した結果$\hat{\mathbf X}$は以下のようになります。

$$\hat{\mathbf X}=\left[\begin{matrix}x_{1111}&x_{1112}&x_{1121}&x_{1122}&x_{1211}&x_{1212}&x_{1221}&x_{1222}\\\\x_{1112}&x_{1113}&x_{1122}&x_{1123}&x_{1212}&x_{1213}&x_{1222}&x_{1223}\\\\x_{2111}&x_{2112}&x_{2121}&x_{2122}&x_{2211}&x_{2212}&x_{2221}&x_{2222}\\\\x_{2112}&x_{2113}&x_{2122}&x_{2123}&x_{2212}&x_{2213}&x_{2222}&x_{2223}\\\\x_{3111}&x_{3112}&x_{3121}&x_{3122}&x_{3211}&x_{3212}&x_{3221}&x_{3222}\\\\x_{3112}&x_{3113}&x_{3122}&x_{3123}&x_{3212}&x_{3213}&x_{3222}&x_{3223}\end{matrix}\right]$$

$\hat{\mathbf X}$の形状を確認しておきます。列の数は、一つのフィルタの形状$(C,\ FH,\ FW)$の要素数$C\times FH\times FW$に等しくなります。つまり、ある一つのフィルタによって畳み込まれる要素が一行に並ぶ形になります。今回の場合は、$2\times 2\times 2=8$です。行方向はどうでしょうか。今は、チャネルあたりの元画像が$2\times 3$でフィルタが$2\times 2$です。出力画像のサイズの式：

$$ OH = \frac{H+2P-FH}{S}+1 $$

$$ OW = \frac{W+2P-FW}{S}+1 $$

を使うと、$OH=1$、$OW=2$で、出力される一つの画像の画素数が2となります。これが、バッチ数3個分繰り返されるので、行数は$2\times 3= 6$となります。このように、一つのフィルタでの畳み込みで、出力画素ひとつが生成されるので、行数は出力画像の総画素数になります。結果的に、入力の4次元配列$(N,\ C,\ H,\ W)$は、

$$(N\times OH\times OW,\ C\times FH\times FW)=(6,\ 8)$$

の2次元配列となりました。一つの行が一つのフィルタで変換され、それが出力される画像の総画素数分だけ行方向に並ぶ形状です。

次にフィルタ$\mathbf W$を見ていきます。フィルタはバッチ数とは関係なく、(フィルタ数$FN$, チャネル数$C$, 高さ$FH$, 幅$FW$)の4次元配列です。$FN=4$のとき、

$$\mathbf W = \left[\begin{matrix}\left[\begin{matrix}w_{1111}&w_{1112}\\\\w_{1121}&w_{1122}\end{matrix}\right]&\left[\begin{matrix}w_{1211}&w_{1212}\\\\w_{1221}&w_{1222}\end{matrix}\right]\\\\\left[\begin{matrix}w_{2111}&w_{2112}\\\\w_{2121}&w_{2122}\end{matrix}\right]&\left[\begin{matrix}w_{2211}&w_{2212}\\\\w_{2221}&w_{2222}\end{matrix}\right]\\\\\left[\begin{matrix}w_{3111}&w_{3112}\\\\w_{3121}&w_{3122}\end{matrix}\right]&\left[\begin{matrix}w_{3211}&w_{3212}\\\\w_{3221}&w_{3222}\end{matrix}\right]\\\\\left[\begin{matrix}w_{4111}&w_{4112}\\\\w_{4121}&w_{4122}\end{matrix}\right]&\left[\begin{matrix}w_{4211}&w_{4212}\\\w_{4221}&w_{4222}\end{matrix}\right]\end{matrix}\right]$$

です。なお、ここでのフィルタ数$FN$が次のレイヤでのチャネル数$C$になります。

## Convolutionレイヤの実装

### 順伝搬

 [「ゼロから作るDeep Learning」の実装](https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/common/layers.py)を確認します。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">def forward(self, x):
    FN, C, FH, FW = self.W.shape
    N, C, H, W = x.shape
    out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
    out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
    col = im2col(x, FH, FW, self.stride, self.pad)
    col_W = self.W.reshape(FN, -1).T
    out = np.dot(col, col_W) + self.b
    out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
    self.x = x
    self.col = col
    self.col_W = col_W
    return out</code></pre></div></div></div>


上記のフィルタ$\mathbf W$は以下のように変換されています。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    col_W = self.W.reshape(FN, -1).T</code></pre></div></div></div>


変換後のフィルタを$\hat{\mathbf W}$とすると、

$$\hat{\mathbf W}=\left[\begin{matrix}w_{1111}&w_{2111}&w_{3111}&w_{4111}\\\\w_{1112}&w_{2112}&w_{3112}&w_{4112}\\\\w_{1121}&w_{2121}&w_{3121}&w_{4121}\\\\w_{1122}&w_{2122}&w_{3122}&w_{4122}\\\\w_{1211}&w_{2211}&w_{3211}&w_{4211}\\\\w_{1212}&w_{2212}&w_{3212}&w_{4212}\\\\w_{1221}&w_{2221}&w_{3221}&w_{4221}\\\\w_{1222}&w_{2222}&w_{3222}&w_{4222}\end{matrix}\right]$$

です。一つの列が一つのフィルタを平坦化したものに対応し、フィルタ数分だけ列があります。すなわち形状は、$(C\times FH\times FW,\ FN)$となります。今回の場合は、$(8,\ 4)$です。

さて、先の2次元化した入力$\hat{\mathbf X}$と比べてみます。こちらは一つの行が一つのフィルタに対応していました。

$$\hat{\mathbf X}=\left[\begin{matrix}x_{1111}&x_{1112}&x_{1121}&x_{1122}&x_{1211}&x_{1212}&x_{1221}&x_{1222}\\\\x_{1112}&x_{1113}&x_{1122}&x_{1123}&x_{1212}&x_{1213}&x_{1222}&x_{1223}\\\\x_{2111}&x_{2112}&x_{2121}&x_{2122}&x_{2211}&x_{2212}&x_{2221}&x_{2222}\\\\x_{2112}&x_{2113}&x_{2122}&x_{2123}&x_{2212}&x_{2213}&x_{2222}&x_{2223}\\\\x_{3111}&x_{3112}&x_{3121}&x_{3122}&x_{3211}&x_{3212}&x_{3221}&x_{3222}\\\\x_{3112}&x_{3113}&x_{3122}&x_{3123}&x_{3212}&x_{3213}&x_{3222}&x_{3223}\end{matrix}\right]$$

順伝搬においては、2次元化したこれらの配列をAffineレイヤと同じように、

$$\hat{\mathbf Y} = \hat{\mathbf X}\cdot\hat{\mathbf W} + \mathbf B$$

とします。（$\mathbf B$はフィルタ数分のスカラー値を持ったバイアスで計算時にはブロードキャストされます。）形状を再確認しておきます。

|2次元化した配列  |行数  |列数  |
|---|---|---|
|$\hat{\mathbf X}$  |$N\times OH\times OW$  |$C\times FH\times FW$  |
|$\hat{\mathbf W}$  |$C\times FH\times FW$  |$FN$  |
|$\hat{\mathbf Y}$  |$N\times OH\times OW$  |$FN$  |

結果的に出力$\hat{\mathbf Y}$の形状は、上表のとおりになります。

次に、「ゼロから作るDeep Learning」の実装では、上述の計算結果を`out`として、


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)</code></pre></div></div></div>


としています。`reshape`によって


$$(N\times OH\times OW,\ FN)\rightarrow(N,\ OH,\ OW,\ FN)$$

となり、続く`transpose`によって軸の順番が変わり、

$$(N,\ OH,\ OW,\ FN)\rightarrow(N,\ FN,\ OH,\ OW)$$

となります。結果的にConvolutionレイヤを通過することによって、

$$(N,\ C,\ H,\ W)\rightarrow(N,\ FN,\ OH,\ OW)$$

というふうに、「チャネル数」、「画像サイズ」が変換されます。当然、バッチ数には変化がありません。


### 逆伝搬

逆伝搬についても見ていきます。 「ゼロから作るDeep Learning」の実装を確認しておきます。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">def backward(self, dout):
    FN, C, FH, FW = self.W.shape
    dout = dout.transpose(0,2,3,1).reshape(-1, FN)
    self.db = np.sum(dout, axis=0)
    self.dW = np.dot(self.col.T, dout)
    self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
    dcol = np.dot(dout, self.col_W.T)
    dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
    return dx</code></pre></div></div></div>


フィルタの形状が、$(FN,\ C,\ FH,\ FW)$だったのを思い出しましょう。

前述のとおり、Convolutionレイヤを通過することによって、$(N,\ FN,\ OH,\ OW)$が伝搬されているので、逆伝搬における勾配も同じ形状です。Convolutionレイヤの出力の勾配$\partial L/\partial \mathbf{Y}$を$\mathbf G$とします。

$$\frac{\partial L}{\partial \mathbf{Y}} = \mathbf G=\left[\begin{matrix}\left[\begin{matrix}g_{1111}&g_{1112}\end{matrix}\right]&\left[\begin{matrix}g_{1211}&g_{1212}\end{matrix}\right]&\left[\begin{matrix}g_{1311}&g_{1312}\end{matrix}\right]&\left[\begin{matrix}g_{1411}&g_{1412}\end{matrix}\right]\\\\\left[\begin{matrix}g_{2111}&g_{2112}\end{matrix}\right]&\left[\begin{matrix}g_{2211}&g_{2212}\end{matrix}\right]&\left[\begin{matrix}g_{2311}&g_{2312}\end{matrix}\right]&\left[\begin{matrix}g_{2411}&g_{2412}\end{matrix}\right]\\\\\left[\begin{matrix}g_{3111}&g_{3112}\end{matrix}\right]&\left[\begin{matrix}g_{3211}&g_{3212}\end{matrix}\right]&\left[\begin{matrix}g_{3311}&g_{3312}\end{matrix}\right]&\left[\begin{matrix}g_{3411}&g_{3412}\end{matrix}\right]\end{matrix}\right]$$

それを、


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)</code></pre></div></div></div>


とすることによって、形状変換します。

$$(N,\ FN,\ OH,\ OW)\rightarrow(N,\ OH,\ OW,\ FN)\rightarrow(N\times OH\times OW,\ FN)$$

実際に計算すると、





$$\frac{\partial L}{\partial \hat{\mathbf{Y}}} = \hat{\mathbf G}=\left[\begin{matrix}g_{1111}&g_{1211}&g_{1311}&g_{1411}\\\\g_{1112}&g_{1212}&g_{1312}&g_{1412}\\\\g_{2111}&g_{2211}&g_{2311}&g_{2411}\\\\g_{2112}&g_{2212}&g_{2312}&g_{2412}\\\\g_{3111}&g_{3211}&g_{3311}&g_{3411}\\\\g_{3112}&g_{3212}&g_{3312}&g_{3412}\end{matrix}\right]$$

となり、順伝搬における

$$\hat{\mathbf Y}=\hat{\mathbf X}\cdot\hat{\mathbf W} + \mathbf B$$

と同じ形状になります。次に、パラメータと入力の勾配を求めます。該当部分を再掲します。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    self.db = np.sum(dout, axis=0)
    self.dW = np.dot(self.col.T, dout)
    dcol = np.dot(dout, self.col_W.T)</code></pre></div></div></div>


$\partial L/\partial\mathbf{B}$はAffineレイヤと同じように軸0で和を取ります。$\partial L/\partial\hat{\mathbf{W}}$と$\partial L/\partial\hat{\mathbf{X}}$はAffineレイヤと同じように、

$$\frac{\partial L}{\partial\hat{\mathbf{W}}} = \hat{\mathbf X}^\mathrm{T}\cdot\frac{\partial L}{\partial\hat{\mathbf{Y}}}$$

$$\frac{\partial L}{\partial\hat{\mathbf{X}}} = \frac{\partial L}{\partial\hat{\mathbf{Y}}}\cdot\hat{\mathbf W}^\mathrm{T} $$

です。右辺の形状を確認しておきます。

|2次元化した配列  |行数  |列数  |
|---|---|---|
|$\hat{\mathbf X}$  |$N\times OH\times OW$  |$C\times FH\times FW$  |
|$\hat{\mathbf W}$  |$C\times FH\times FW$  |$FN$  |
|$\partial L/\partial \hat{\mathbf Y}$  |$N\times OH\times OW$  |$FN$  |

上式の内積の結果は、

$$ (C\times FH\times FW,\ N\times OH\times OW)\cdot(N\times OH\times OW,\ FN) \rightarrow(C\times FH\times FW,\ FN)$$

$$ (N\times OH\times OW,\ FN)\cdot(FN,\ C\times FH\times FW) \rightarrow(N\times OH\times OW,\ C\times FH\times F)$$

となり、確かに、$\hat{\mathbf W}$と$\hat{\mathbf X}$の形状に一致します。後は、前のレイヤに逆伝搬できるように、$\mathbf W$と$\mathbf X$の形状に戻すだけです。

|4次元配列  |形状   |
|---|---|
|$\mathbf X$  |$(N,\ C,\ H,\ W)$|
|$\mathbf W$  |$(FN,\ C,\ FH,\ FW)$|

$\partial L/\hat{\mathbf{W}}\rightarrow\partial L/\mathbf{W}$については簡単です。形状の順番が違うだけなので、転置した後、`reshape`で4次元配列に戻しています:


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)</code></pre></div></div></div>


$\partial L/\hat{\mathbf{X}}\rightarrow\partial L/\mathbf{X}$については、`im2col`関数の逆変換、`col2im`関数が用意されています。逆伝搬の関数の中で、該当する部分は以下です。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">    dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)</code></pre></div></div></div>

さて、2次元化された入力$\hat{\mathbf{X}}$が、

$$\hat{\mathbf X}=\left[\begin{matrix}x_{1111}&x_{1112}&x_{1121}&x_{1122}&x_{1211}&x_{1212}&x_{1221}&x_{1222}\\\\x_{1112}&x_{1113}&x_{1122}&x_{1123}&x_{1212}&x_{1213}&x_{1222}&x_{1223}\\\\x_{2111}&x_{2112}&x_{2121}&x_{2122}&x_{2211}&x_{2212}&x_{2221}&x_{2222}\\\\x_{2112}&x_{2113}&x_{2122}&x_{2123}&x_{2212}&x_{2213}&x_{2222}&x_{2223}\\\\x_{3111}&x_{3112}&x_{3121}&x_{3122}&x_{3211}&x_{3212}&x_{3221}&x_{3222}\\\\x_{3112}&x_{3113}&x_{3122}&x_{3123}&x_{3212}&x_{3213}&x_{3222}&x_{3223}\end{matrix}\right]$$

だったことを思い出しましょう。勾配も同じ形状なので、簡単のためにこのまま`im2col`関数に渡してみます。ここで、$x$は入力データではなく、勾配であると読み替えます。すると、4次元に戻った勾配$\partial L/\partial \mathbf{X}$が、

$$\frac{\partial L}{\partial \mathbf{X}} = \left[\begin{matrix}\left[\begin{matrix}x_{1111} & 2 x_{1112} & x_{1113}\\\\x_{1121} & 2 x_{1122} & x_{1123}\end{matrix}\right]&\left[\begin{matrix}x_{1211} & 2 x_{1212} & x_{1213}\\\\x_{1221} & 2 x_{1222} & x_{1223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{2111} & 2 x_{2112} & x_{2113}\\\\x_{2121} & 2 x_{2122} & x_{2123}\end{matrix}\right]&\left[\begin{matrix}x_{2211} & 2 x_{2212} & x_{2213}\\\\x_{2221} & 2 x_{2222} & x_{2223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{3111} & 2 x_{3112} & x_{3113}\\\\x_{3121} & 2 x_{3122} & x_{3123}\end{matrix}\right]&\left[\begin{matrix}x_{3211} & 2 x_{3212} & x_{3213}\\\\x_{3221} & 2 x_{3222} & x_{3223}\end{matrix}\right]\end{matrix}\right]$$

と計算されます。入力データをもう一度確認します。

$$\mathbf{X} = \left[\begin{matrix}\left[\begin{matrix}x_{1111}&x_{1112}&x_{1113}\\\\x_{1121}&x_{1122}&x_{1123}\end{matrix}\right]&\left[\begin{matrix}x_{1211}&x_{1212}&x_{1213}\\\\x_{1221}&x_{1222}&x_{1223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{2111}&x_{2112}&x_{2113}\\\\x_{2121}&x_{2122}&x_{2123}\end{matrix}\right]&\left[\begin{matrix}x_{2211}&x_{2212}&x_{2213}\\\\x_{2221}&x_{2222}&x_{2223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{3111}&x_{3112}&x_{3113}\\\\x_{3121}&x_{3122}&x_{3123}\end{matrix}\right]&\left[\begin{matrix}x_{3211}&x_{3212}&x_{3213}\\\\x_{3221}&x_{3222}&x_{3223}\end{matrix}\right]\end{matrix}\right]$$

添え字の場所が一致していることが確認できます。また、定数倍になっているのは、文字通り定数倍するのではなく、その要素が出力に伝搬される経路数を表しています。経路ごとに異なった勾配が、その要素の位置で和算されます。畳み込みされる頻度の高い画像中央ほど経路数が多いことが確認できます。

以上で、Convolutionレイヤの実装の確認ができました。

## Poolingレイヤの実装

### 順伝搬

「ゼロから作るDeep Learning」の実装を確認します。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">def forward(self, x):
    N, C, H, W = x.shape
    out_h = int(1 + (H - self.pool_h) / self.stride)
    out_w = int(1 + (W - self.pool_w) / self.stride)
    col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
    col = col.reshape(-1, self.pool_h * self.pool_w)
    arg_max = np.argmax(col, axis=1)
    out = np.max(col, axis=1)
    out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
    self.x = x
    self.arg_max = arg_max
    return out</code></pre></div></div></div>


Convolutionレイヤと同じ入力を考えます。

$$\mathbf X=\left[\begin{matrix}\left[\begin{matrix}x_{1111}&x_{1112}&x_{1113}\\\\x_{1121}&x_{1122}&x_{1123}\end{matrix}\right]&\left[\begin{matrix}x_{1211}&x_{1212}&x_{1213}\\\\x_{1221}&x_{1222}&x_{1223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{2111}&x_{2112}&x_{2113}\\\\x_{2121}&x_{2122}&x_{2123}\end{matrix}\right]&\left[\begin{matrix}x_{2211}&x_{2212}&x_{2213}\\\\x_{2221}&x_{2222}&x_{2223}\end{matrix}\right]\\\\\left[\begin{matrix}x_{3111}&x_{3112}&x_{3113}\\\\x_{3121}&x_{3122}&x_{3123}\end{matrix}\right]&\left[\begin{matrix}x_{3211}&x_{3212}&x_{3213}\\\\x_{3221}&x_{3222}&x_{3223}\end{matrix}\right]\end{matrix}\right]$$





`im2col`関数を適用した後、`reshape`します。ここで、$PH=2$、$PW=2$とします。

$$\hat{\mathbf X}=\left[\begin{matrix}x_{1111}&x_{1112}&x_{1121}&x_{1122}\\\\x_{1211}&x_{1212}&x_{1221}&x_{1222}\\\\x_{1112}&x_{1113}&x_{1122}&x_{1123}\\\\x_{1212}&x_{1213}&x_{1222}&x_{1223}\\\\x_{2111}&x_{2112}&x_{2121}&x_{2122}\\\\x_{2211}&x_{2212}&x_{2221}&x_{2222}\\\\x_{2112}&x_{2113}&x_{2122}&x_{2123}\\\\x_{2212}&x_{2213}&x_{2222}&x_{2223}\\\\x_{3111}&x_{3112}&x_{3121}&x_{3122}\\\\x_{3211}&x_{3212}&x_{3221}&x_{3222}\\\\x_{3112}&x_{3113}&x_{3122}&x_{3123}\\\\x_{3212}&x_{3213}&x_{3222}&x_{3223}\end{matrix}\right]$$

配列の形状は以下のようになります。

|2次元化した配列  |行数  |列数  |
|---|---|---|
|$\hat{\mathbf X}$  |$N\times OH\times OW\times C$  |$PH\times PW$  |

Convolutionレイヤの場合と比べて、`reshape`の結果、チャネル$C$が列から行に移っています。後は軸1で最大値を取った後、`reshape`と`transpose`によって、

$$N\times OH\times OW\times C\rightarrow(N,\ OH,\ OW,\ C) \rightarrow(N,\ C,\ OH,\ OW)$$

となります。このように、Poolingレイヤを通過することで、バッチ数とチャネル数はそのままで、画像のサイズが$OH\times OW$に変更になりました。

### 逆伝搬

「ゼロから作るDeep Learning」の実装を確認します。


<div class="pheasant-fenced-code"><div class="cell embed source"><div class="code"><pre><code class="python">def backward(self, dout):
    dout = dout.transpose(0, 2, 3, 1)
    pool_size = self.pool_h * self.pool_w
    dmax = np.zeros((dout.size, pool_size))
    dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
    dmax = dmax.reshape(dout.shape + (pool_size,))
    dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
    dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
    return dx</code></pre></div></div></div>


前述のとおり、Poolingレイヤを通過することによって、$(N,\ C,\ OH,\ OW)$が伝搬されているので、逆伝搬における勾配も同じ形状です。Poolingレイヤの出力の勾配$\partial L/\partial \mathbf{Y}$を$\mathbf G$とします。


$$\frac{\partial L}{\partial \mathbf{Y}} = \mathbf G=\left[\begin{matrix}\left[\begin{matrix}g_{1111}&g_{1112}\end{matrix}\right]&\left[\begin{matrix}g_{1211}&g_{1212}\end{matrix}\right]\\\\\left[\begin{matrix}g_{2111}&g_{2112}\end{matrix}\right]&\left[\begin{matrix}g_{2211}&g_{2212}\end{matrix}\right]\\\\\left[\begin{matrix}g_{3111}&g_{3112}\end{matrix}\right]&\left[\begin{matrix}g_{3211}&g_{3212}\end{matrix}\right]\end{matrix}\right]$$

`transpose`によって、中間状態`dout`は、

$$(N,\ C,\ OH,\ OW) \rightarrow (N,\ OH,\ OW,\ C)$$

となります。また、`dmax`は、$\hat{\mathbf{X}}$と同じ形状のゼロ配列です。

`dout`は`flatten`されてベクトルになった後、`dmax`に代入されますが、このとき、元々の入力が最大だった列へのみ代入します。


$$\mathrm{dout'}=\left[\begin{matrix}g_{1111}\\\\g_{1211}\\\\g_{1112}\\\\g_{1212}\\\\g_{2111}\\\\g_{2211}\\\\g_{2112}\\\\g_{2212}\\\\g_{3111}\\\\g_{3211}\\\\g_{3112}\\\\g_{3212}\end{matrix}\right],\ \ \mathrm{dmax}=\left[\begin{matrix}0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\\\\0 & 0 & 0 & 0\end{matrix}\right] \ \rightarrow \left[\begin{matrix}g_{1111}&g_{1111}&g_{1111}&g_{1111}\\\\g_{1211}&g_{1211}&g_{1211}&g_{1211}\\\\g_{1112}&g_{1112}&g_{1112}&g_{1112}\\\\g_{1212}&g_{1212}&g_{1212}&g_{1212}\\\\g_{2111}&g_{2111}&g_{2111}&g_{2111}\\\\g_{2211}&g_{2211}&g_{2211}&g_{2211}\\\\g_{2112}&g_{2112}&g_{2112}&g_{2112}\\\\g_{2212}&g_{2212}&g_{2212}&g_{2212}\\\\g_{3111}&g_{3111}&g_{3111}&g_{3111}\\\\g_{3211}&g_{3211}&g_{3211}&g_{3211}\\\\g_{3112}&g_{3112}&g_{3112}&g_{3112}\\\\g_{3212}&g_{3212}&g_{3212}&g_{3212}\end{matrix}\right] $$

上の例では、仮想的に`dmax`のすべての列に勾配を代入しています。本来であれば、非ゼロの要素は各行につき一つです。形状を確認しておきます。

|2次元化した配列  |行数  |列数  |
|---|---|---|
|$\mathrm{dmax}$  |$N\times OH\times OW\times C$  |$PH\times PW$  |

つぎに、2回の`reshape`によって、

$$(N\times OH\times OW\times C,\ PH\times PW)\rightarrow (N,\ OH,\ OW,\ C,\ PH\times PW)\ $$


$$(N,\ OH,\ OW,\ C,\ PH\times PW)\rightarrow (N\times OH\times OW,\ C\times PH\times PW)$$

と変化します。実際に確認してみます。

$$\mathrm{dcol} = \left[\begin{matrix}g_{1111}&g_{1111}&g_{1111}&g_{1111}&g_{1211}&g_{1211}&g_{1211}&g_{1211}\\\\g_{1112}&g_{1112}&g_{1112}&g_{1112}&g_{1212}&g_{1212}&g_{1212}&g_{1212}\\\\g_{2111}&g_{2111}&g_{2111}&g_{2111}&g_{2211}&g_{2211}&g_{2211}&g_{2211}\\\\g_{2112}&g_{2112}&g_{2112}&g_{2112}&g_{2212}&g_{2212}&g_{2212}&g_{2212}\\\\g_{3111}&g_{3111}&g_{3111}&g_{3111}&g_{3211}&g_{3211}&g_{3211}&g_{3211}\\\\g_{3112}&g_{3112}&g_{3112}&g_{3112}&g_{3212}&g_{3212}&g_{3212}&g_{3212}\end{matrix}\right] $$

最終的に、`col2im`関数を適用します。

$$\frac{\partial L}{\partial \mathbf{X}} = \left[\begin{matrix}\left[\begin{matrix}g_{1111} & g_{1111} + g_{1112} & g_{1112}\\\\g_{1111} & g_{1111} + g_{1112} & g_{1112}\end{matrix}\right]&\left[\begin{matrix}g_{1211} & g_{1211} + g_{1212} & g_{1212}\\\\g_{1211} & g_{1211} + g_{1212} & g_{1212}\end{matrix}\right]\\\\\left[\begin{matrix}g_{2111} & g_{2111} + g_{2112} & g_{2112}\\\\g_{2111} & g_{2111} + g_{2112} & g_{2112}\end{matrix}\right]&\left[\begin{matrix}g_{2211} & g_{2211} + g_{2212} & g_{2212}\\\\g_{2211} & g_{2211} + g_{2212} & g_{2212}\end{matrix}\right]\\\\\left[\begin{matrix}g_{3111} & g_{3111} + g_{3112} & g_{3112}\\\\g_{3111} & g_{3111} + g_{3112} & g_{3112}\end{matrix}\right]&\left[\begin{matrix}g_{3211} & g_{3211} + g_{3212} & g_{3212}\\\\g_{3211} & g_{3211} + g_{3212} & g_{3212}\end{matrix}\right]\end{matrix}\right]$$

ここで同じ添え字の勾配が$PH\times PW$回出現しますが、実際にゼロではないのは一つです。そして、その位置は、Poolingされる範囲で最大の要素がある位置になります。

以上でPoolingレイヤの実装の確認ができました。