深層学習入門 ~畳み込みとプーリング編~

# 対象者
前回の記事は[こちら](https://qiita.com/kuroitu/items/a6725ac0139d8eeb1e19)
前回まででDNN(Deep Neural Network)は完成です。
(レイヤーマネージャの使い方も含めて別の記事でDNNで遊ぶ予定です)
ここでは画像認識へ向けてCNN(Convolutional Neural Network)を作成していきます。
ここで用いる`im2col`関数と`col2im`関数は、[こちら](https://qiita.com/kuroitu/items/35d7b5a4bde470f69570)と[こちら](https://qiita.com/kuroitu/items/7877c002e192955c7857)で紹介しています。
次回の記事は[こちら](https://qiita.com/kuroitu/items/d4716e754594a29b059d)


# 目次
- [畳み込み層](#畳み込み層)
    - [畳み込み層順伝播](#畳み込み層順伝播)
    - [畳み込み層逆伝播](#畳み込み層逆伝播)
    - [畳み込み層学習](#畳み込み層学習)
    - [畳み込み層実装](#畳み込み層実装)
- [プーリング層](#プーリング層)
    - [プーリング層順伝播](#プーリング層順伝播)
    - [プーリング層逆伝播](#プーリング層逆伝播)
    - [プーリング層学習](#プーリング層学習)
    - [プーリング層実装](#プーリング層実装)
- [おわりに](#おわりに)


# 畳み込み層
画像認識に多大な恩恵を与えるのが**畳み込み**という処理です。導入としては、画像などの位置関係が重要だと思われるデータに対して、単純にニューラルネットワークに1次元に平滑化して流すのはせっかくの位置関係という重要な情報を捨てるようなものなのでもったいない、みたいな感じです。
入力の次元を維持したまま、つまり位置関係などの重要な情報を維持したままニューラルネットワークにデータを流すのが畳み込み層の役割です。
![filter_image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/767a1722-6702-b730-f91a-1d3212244547.gif)
畳み込み層はこのフィルタが普通のレイヤでの重みに相当しています。あとはこのgifの通りに動作するコードを書けばいいわけですが、実はそのまま実装するととても実用に耐えない激重コードになってしまいます。
というのも、簡略化してこのgifの部分を実装すると

```python
Image = I_h×I_wの配列
Filter = F_h×F_wの配列
Output = O_h×O_wの配列
for h in range(O_h):
    h_lim = h + F_h
    for w in range(O_w):
        w_lim = w + F_w
        Output[h, w] = Image[h:h_lim, w:w_lim] * Filter
```

のようになり、二重ループでnumpy配列にアクセスして入力の該当箇所に要素積を施していき、その結果を出力に保存していく、みたいなことをすることになります。
しかもこのループ、ここでは二重ループですが実際の入力は4次元ですので四重ループになってしまいます。すると簡単にループ回数が急増することは想像に難くないでしょう。
numpyには`for`文でアクセスすると遅くなるという仕様があるため、できるだけループでのアクセスは避けたいものです。そこで活躍するのが`im2col`関数です。
先のgifは

```math
a = 1W + 2X + 5Y + 6Z \\
b = 2W + 3X + 6Y + 7Z \\
c = 3W + 4X + 7Y + 8Z \\
d = 5W + 6X + 9Y + 10Z \\
e = 6W + 7X + 10Y + 11Z \\
f = 7W + 8X + 11Y + 12Z \\
g = 9W + 10X + 13Y + 14Z \\
h = 10W + 11X + 14Y + 15Z \\
i = 11W + 12X + 15Y + 16Z
```

という感じとなっていますが、これを行列積で表すと

```math
\left(
  \begin{array}{c}
    a \\
    b \\
    c \\
    d \\
    e \\
    f \\
    g \\
    h \\
    i
  \end{array}
\right)^{\top}
=
\left(
  \begin{array}{cccc}
    W & X & Y & Z
  \end{array}
\right)
\left(
  \begin{array}{ccccccccc}
    1 & 2 & 3 & 5 & 6 & 7 & 9 & 10 & 11 \\
    2 & 3 & 4 & 6 & 7 & 8 & 10 & 11 & 12 \\
    5 & 6 & 7 & 9 & 10 & 11 & 13 & 14 & 15 \\
    6 & 7 & 8 & 10 & 11 & 12 & 14 & 15 & 16
  \end{array}
\right)
```
となります。`im2col`関数は入力画像やフィルタをこんな感じの行列に変換するための関数になります。詳しくは[こちら](https://qiita.com/kuroitu/items/35d7b5a4bde470f69570)で解説しています。
さて、この`im2col`関数を用いることで前述の問題はだいぶ解消されます。が、もちろん`im2col`関数を用いると元の入力などの形状とは異なるためこのままでは誤差逆伝播法での学習が進められません。ということで逆の動作を行う`col2im`関数を逆伝播時に噛ませる必要があります。詳しくは[こちら](https://qiita.com/kuroitu/items/7877c002e192955c7857)で解説しています。

ここまでで簡単に畳み込み層の概要を説明しましたので、その設計図を示しておきます。
![conv_layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/c9173e91-0b06-a524-0b8a-bb42bed4b84f.png)

## 畳み込み層順伝播
順伝播から見ていきましょう。関係のある部分は下図のカラー部分です。
![conv_layer_forward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d3bd6ec7-059c-ebb2-d9fb-26b9055dd11d.png)
動作的には

1. 入力画像を`im2col`関数に投げる
2. 返り値とフィルタ(を変形したもの)とを行列積計算する。
3. 2.の出力とバイアスとを足し算
4. 活性化関数を通す

基本動作は通常のニューラルネットワークの順伝播と同じです。違うのはその前に`im2col`関数を挟むことくらいですね。
詳しく見ていきましょう。
まず畳み込み演算は以下の図のようになります。
![conv_filtering.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/6d06ad5e-550c-2e59-7906-04989b5269a5.png)
バイアスは省いています。
入力はバッチサイズ$B$、チャンネル数$C$、画像サイズ$(I_h, I_w)$のテンソルです。
フィルタは各チャンネルごとに$M$枚存在しており、入力と同じチャンネル数を持ち、フィルタサイズ$(F_h, F_w)$のテンソルです。
入力の各チャンネルに対応するチャンネルのフィルタを全てのバッチデータに対してフィルタリングを行い、結果として$(B, M, O_h, O_w)$という形状を持つテンソルができます。この処理を具体的にどう行うかを見ていきます。
入力及びフィルタを以下の図の通りに処理します。
![input_im2col.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/f22aa967-6879-1cc1-41e2-d83ceeb956f3.png)
![filter_reshape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d550d4be-8531-1986-aaa9-b3352e1008bb.png)
これにより4次元テンソルを2次元に落とし込むことができたため、行列積を行うことができるようになります。
![convolution.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/f763ca02-5d32-440a-44ec-5ca51817db9e.png)
この出力にバイアス(形状は$(M, 1)$の2次元行列)を加えます。この時、`numpy`のブロードキャスト機能を用いて全ての列に同じ値を加算します。
その後この出力を変形、次元入れ替えすることで求める出力テンソルにします。
![output_reshape_transpose.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/1efdd55b-3bb5-375a-5ffa-d1ad3eedf0e4.png)
この出力テンソルを活性化関数に投げれば畳み込み層の順伝播の完了です。

## 畳み込み層逆伝播
続いて逆伝播です。関係している部分は下図のカラー部分です。
![conv_layer_backward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/44b305f7-1454-4872-6a86-9f303fbcaeea.png)
動作としては

1. 出力の勾配と活性化関数の微分の要素積をとる
2. 一つはバイアスの勾配として利用する
3. `im2col`関数を通した入力画像との行列積をとったものはフィルタの勾配として利用する
4. フィルタとの行列積をとったものは`col2im`関数を通して入力の勾配として利用する

という感じです。
詳しく見ていきましょう。
![grad_transpose_reshape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/8046589f-cfdc-9d20-90c9-0c6ca2bb8531.png)
伝播してきた勾配は$(B, M, O_h, O_w)$のテンソルです。まずは順伝播の時とは逆順にこの勾配を変形します。
![grad_w.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ec045776-474f-3306-11f3-256eba31cd94.png)
![grad_w_reshape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ed184163-7152-e473-914f-ffeb3e1e7aa8.png)
フィルタへの勾配は勾配と入力の行列積で計算されます。
得られた結果は2次元行列であるため、これを変形することでフィルタと同じ形状の4次元テンソルへ揃えます。
![grad_b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/85d6b66c-0d79-701f-c6e1-ca0223b1aa6d.png)
バイアスへの勾配について、順伝播の際に全ての列に同じ値を加算したことが鍵になります。同じ値をいくつかの要素に加算することが示すのは、以下の図のような形状のネットワークと同等である、ということです。
![broadcast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/864ce6f0-bcd2-14c8-144d-b5aef71feca2.png)
(数は適当です)
このため、$axis=1$つまり列方向からそれぞれ1つのバイアスに向けて逆伝播が行われるため、それらを足し合わせたものがバイアスへの勾配となります。
![grad_x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/fe802cc7-2017-5b8b-3efc-8788e9bbae59.png)
入力への勾配はフィルタと勾配の行列積で計算されます。
計算結果のテンソルを見てみるとわかるかと思いますが、形状が順伝播の時に入力テンソルを`im2col`関数に投げた結果と同じとなっています。そのため、逆の操作を行う`col2im`関数にこれを投げることで入力への勾配テンソルが形成されます。
![grad_x_col2im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ce5fa050-5c64-3435-a68c-7152348e0f5b.png)
これで畳み込み層の逆伝播は完了です。

## 畳み込み層学習
さて、フィルタの変形は実は毎回行う必要がありません。最初に1回行うだけで良いです。その理由は「フィルタは毎回同じ変形をするのでわざわざ繰り返し行う必要がない」ためです。
フィルタは最初に変形したあとそのまま、ということは逆伝播で計算したフィルタへの勾配も変形する必要はありません。
そんなこんなで、畳み込み層の学習は通常のレイヤと同じ形になります。
![update_filter_reshape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d3ae37eb-5525-70b1-a01b-cc0978758ff5.png)
![update_filter_backward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/02e08f54-4aab-8339-62f0-e3a886f19cd7.png)

## 畳み込み層実装
ということで実装します。ただし`BaseLayer`を継承させるために少しの工夫が必要です。

<details><summary>conv.py</summary><div>

```conv.py
import numpy as np


class ConvLayer(BaseLayer):
    def __init__(self, *, I_shape=None, F_shape=None,
                 stride=1, pad="same",
                 name="", wb_width=5e-2,
                 act="ReLU", opt="Adam",
                 act_dic={}, opt_dic={}, **kwds):
        self.name = name
        
        if I_shape is None:
            raise KeyError("Input shape is None.")
        if F_shape is None:
            raise KeyError("Filter shape is None.")
        
        if len(I_shape) == 2:
            C, I_h, I_w = 1, *I_shape
        else:
            C, I_h, I_w = I_shape
        self.I_shape = (C, I_h, I_w)
        
        if len(F_shape) == 2:
            M, F_h, F_w = 1, *F_shape
        else:
            M, F_h, F_w = F_shape
        self.F_shape = (M, C, F_h, F_w)
        
        if isinstance(stride, tuple):
            stride_ud, stride_lr = stride
        else:
            stride_ud = stride
            stride_lr = stride
        self.stride = (stride_ud, stride_lr)
        
        if isinstance(pad, tuple):
            pad_ud, pad_lr = pad
        elif isinstance(pad, int):
            pad_ud = pad
            pad_lr = pad
        elif pad == "same":
            pad_ud = 0.5*((I_h - 1)*stride_ud - I_h + F_h)
            pad_lr = 0.5*((I_w - 1)*stride_lr - I_w + F_w)
        self.pad = (pad_ud, pad_lr)
        
        O_h = get_O_shape(I_h, F_h, stride_ud, pad_ud)
        O_w = get_O_shape(I_w, F_w, stride_lr, pad_lr)
        self.O_shape = (M, O_h, O_w)
        
        self.n = np.prod(self.O_shape)
        
        # フィルタとバイアスを設定
        self.w = wb_width*np.random.randn(*self.F_shape).reshape(M, -1).T
        self.b = wb_width*np.random.randn(M)
        
        # 活性化関数(クラス)を取得
        self.act = get_act(act, **act_dic)

        # 最適化子(クラス)を取得
        self.opt = get_opt(opt, **opt_dic)
    
    
    def forward(self, x):
        B = x.shape[0]
        M, O_h, O_w = self.O_shape
        
        x, _, self.pad_state = im2col(x, self.F_shape,
                                      stride=self.stride,
                                      pad=self.pad)
        super().forward(x.T)
        return self.y.reshape(B, O_h, O_w, M).transpose(0, 3, 1, 2)
    
    
    def backward(self, grad):
        B = grad.shape[0]
        I_shape = B, *self.I_shape
        M, O_h, O_w = self.O_shape
        
        grad = grad.transpose(0, 2, 3, 1).reshape(-1, M)
        super().backward(grad)
        self.grad_x = col2im(self.grad_x.T, I_shape, self.O_shape,
                             stride=self.stride, pad=self.pad_state)
        return self.grad_x
```
</div></details>

どのあたりを工夫しているのか説明していきます。
工夫せずに、先ほどまでで説明した通りに実装すると以下のようになります。

<details><summary>工夫なしver.</summary><div>

```conv.py
import numpy as np


class ConvLayer(BaseLayer):
    def __init__(self, *, I_shape=None, F_shape=None,
                 stride=1, pad="same",
                 name="", wb_width=5e-2,
                 act="ReLU", opt="Adam",
                 act_dic={}, opt_dic={}, **kwds):
        self.name = name
        
        if I_shape is None:
            raise KeyError("Input shape is None.")
        if F_shape is None:
            raise KeyError("Filter shape is None.")
        
        if len(I_shape) == 2:
            C, I_h, I_w = 1, *I_shape
        else:
            C, I_h, I_w = I_shape
        self.I_shape = (C, I_h, I_w)
        
        if len(F_shape) == 2:
            M, F_h, F_w = 1, *F_shape
        else:
            M, F_h, F_w = F_shape
        self.F_shape = (M, C, F_h, F_w)
        
        _, O_shape, self.pad_state = im2col(np.zeros((1, *self.I_shape)), self.F_shape,
                                            stride=stride, pad=pad)
        self.O_shape = (M, *O_shape)
        self.stride = stride
        
        self.n = np.prod(self.O_shape)
        
        # フィルタとバイアスを設定
        self.w = wb_width*np.random.randn(*self.F_shape).reshape(M, -1)
        self.b = wb_width*np.random.randn(M, 1)
        
        # 活性化関数(クラス)を取得
        self.act = get_act(act, **act_dic)

        # 最適化子(クラス)を取得
        self.opt = get_opt(opt, **opt_dic)


    def forward(self, x):
        B = x.shape[0]
        M, O_h, O_w = self.O_shape

        self.x, _, self.pad_state = im2col(x, self.F_shape,
                                           stride=self.stride,
                                           pad=self.pad)
        
        self.u = self.w@self.x + self.b
        self.u = self.u.reshape(M, B, O_h, O_w).transpose(1, 0, 2, 3)
        self.y = self.act.forward(self.u)
        
        return self.y
    
    
    def backward(self, grad):
        B = grad.shape[0]
        I_shape = B, *self.I_shape
        _, O_h, O_w = self.O_shape
        
        dact = grad*self.act.backward(self.u, self.y)
        dact = dact.transpose(1, 0, 2, 3).reshape(M, -1)
        self.grad_w = dact@self.x.T
        self.grad_b = np.sum(dact, axis=1).reshape(M, 1)
        self.grad_x = self.w.T@dact
        self.grad_x = col2im(self.grad_x, I_shape, self.O_shape,
                             stride=self.stride, pad=self.pad_state)
        
        return self.grad_x
```
</div></details>

`BaseLayer`との差異を、コードを省略しながら細かく見ていきます。

|注目部分| `BaseLayer` | 形状 || `ConvLayer` | 形状 |
|:-:|:-:|:-:|:-:|:-:|:-:|
|w|randn(prev, n)|$(prev, n)$||randn(*F_shape).reshape(M, -1)|$(M, CF_hF_w)$|
|b|randn(n)|$(n, )$||randn(M, 1)|$(M, 1)$|
|||||||
|x|-|$(B, prev)$||im2col(x)|$(CF_hF_w, BO_hO_w)$|
|u|x@w + b|$(B, prev)@(prev, n)+(n)=(B, n)$||w@x + b|$(M, CF_hF_w)@(CF_hF_w, BO_hO_w)+(M, 1)=(M, BO_hO_w)$|
|u|-|-||u.reshape(M, B, O_h, O_w).transpose(1, 0, 2, 3)|$(B, M, O_h, O_w)$|
|y|act.forward(u)|$(B, n)$||act.forward(u)|$(B, M, O_h, O_w)$|
|||||||
|grad|-|$(B, n)$||-|$(B, M, O_h, O_w)$|
|dact|grad*act.backward(u, y)|$(B, n)$||grad*act.backward(u, y)|$(B, M, O_h, O_w)$|
|dact|-|-||dact.transpose(1, 0, 2, 3).reshape(M, -1)|$(M, BO_hO_w)$|
|grad_w|x.T@dact|$(prev, B)@(B, n)=(prev, n)$||dact@x.T|$(M, BO_hO_w)@(BO_hO_w, CF_hF_w)=(M, CF_hF_w)$|
|grad_b|sum(dact, axis=0)|$(n)$||sum(dact, axis=1).reshape(M, 1)|$(M, 1)$|
|grad_x|dact@w.T|$(B, n)@(n, prev)=(B, prev)$||w.T@dact|$(CF_hF_w, M)@(M, BO_hO_w)=(CF_hF_w, BO_hO_w)$|
|grad_x|-|-||col2im(grad_x)|$(B, C, I_h, I_w)$|

まずは順伝播を揃えましょう。
順伝播の最も異なる点は`u`の計算ですね。

```math
\boldsymbol{x}@\boldsymbol{w} + \boldsymbol{b} \quad \Leftrightarrow \quad \boldsymbol{w}@\boldsymbol{x} + \boldsymbol{b}
```

行列積は$\boldsymbol{w}@\boldsymbol{x} = \boldsymbol{x}^{\top}@\boldsymbol{w}^{\top}$とすることで順番を逆にすることができるため、

```math
\begin{align}
  \boldsymbol{x} &\leftarrow \textrm{im2col}(\boldsymbol{x})^{\top} = (BO_hO_w, CF_hF_w) \\
  \boldsymbol{w} &\leftarrow \boldsymbol{w}^{\top} = (CF_hF_w, M) \\
  \boldsymbol{b} & \leftarrow (M, )
\end{align}
```

のようにしておくことで順伝播の数式と揃えることが可能です。また、バイアスについても`numpy`のブロードキャスト機能を有効にするために$(M, 1)$とわざわざ2次元行列としていたのを1次元配列にすることができます。
このように順伝播を変更すると

```math
\boldsymbol{x}@\boldsymbol{w} + \boldsymbol{b} = (BO_hO_w, CF_hF_w)@(CF_hF_w, M) + (M) = (BO_hO_w, M)
```

と計算されるので、`BaseLayer`の`forward`で計算したあと、次の層への伝播を`self.y.reshape(B, O_h, O_w, M).transpose(0, 3, 1, 2)`とすることで$(B, M, O_h, O_w)$と変形することができます。
また、工夫した方のコードを見ると`return`文で上述の変形をして流していますが、これにより`u`と`y`の形状が$(BO_hO_w, M)$のままとなります。これはこのままで大丈夫です。

続いて逆伝播です。勾配`grad`は$(B, M, O_h, O_w)$となっており、このままでは`grad*act.backward(u, y)`の要素積計算ができません。

```math
\boldsymbol{grad} \otimes \textrm{act.backward}(\boldsymbol{u}, \boldsymbol{y}) = (B, M, O_h, O_w) \otimes (BO_hO_w, M)
```

ということで`grad`を変形して揃えましょう.
`grad.transpose(0, 2, 3, 1).reshape(-1, M)`とすればOKですね。
この後は`BaseLayer`の`backward`に投げれば

```math
\begin{array}[cccc]
   d\boldsymbol{dact} &= \boldsymbol{grad} \otimes \textrm{act.backward}(\boldsymbol{u}, \boldsymbol{y}) &= (BO_hO_w, M) & \\
  \boldsymbol{grad_w} &= \boldsymbol{x}^{\top}@\boldsymbol{dact} &= (CF_hF_w, BO_hO_w)@(BO_hO_w, M) &= (CF_hF_w, M)\\
  \boldsymbol{grad_b} &= \textrm{sum}(\boldsymbol{dact}, \textrm{axis}=0) &= (M, ) & \\
  \boldsymbol{grad_x} &= \boldsymbol{dact}@\boldsymbol{w}^{\top} &= (BO_hO_w, M)@(M, CF_hF_w) &= (BO_hO_w, CF_hF_w)
\end{array}
```

となるので

```math
\boldsymbol{grad_x} \leftarrow \textrm{col2im}(\boldsymbol{grad_x}^{\top}) = (B, C, I_h, I_w)
```

とすればOKです。
`BaseLayer`の`update`関数については前述の通り変更する必要がありません。
よって、これで畳み込み層の完成です。


# プーリング層
続いてプーリング層です。まずプーリング層とは、入力画像の中から重要だと思われる情報のみを抜き出してデータサイズを小さくするレイヤです。この場合の重要な情報というのは、大抵の場合は最大値だったり平均値だったりします。
![pooling.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/1f3e8bb1-1854-4b23-a06b-80d08eb6c704.gif)
また、これを実装する際には畳み込み層と同様に`im2col`関数や`col2im`関数を利用することで高速かつ効率的になります。
![pooling_act.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/411f7dd7-7fe5-6763-de7f-ae84d2c11b5e.png)
プーリング層の設計図は次のような感じとなります。
![pooling_layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/b9bf6745-ddb5-b8ec-b2d3-28493d7cfabd.png)

## プーリング層順伝播
順伝播を見てみます。関係しているのはカラー部分です。
![pooling_layer_forward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/4c7bcc43-8106-0252-3889-f6a14108efba.png)
動作としては

1. 入力画像を`im2col`関数に投げる
2. 返り値の形状を取得する
3. 返り値から最大値とそのインデックスを取得する
4. 出力画像の形状に再構成する

という感じです。逆伝播のためにいくつか保持しておくべきものがありますね。
詳しく見ていきます。目標とする動作は下図の通りです。
![pool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/bf45f93a-b4a7-2ed4-d04d-9dd3bd9c13b6.png)
まず、入力テンソルを`im2col`関数に投げて2次元行列に変換します。
![pool_im2col.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/04590f0f-8901-3115-be70-2e1efaae5fa7.png)
さらにこの2次元行列を変形します。
![pool_T_reshape.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/22eb02a7-5ee2-0e09-49af-fb1af5159e72.png)
このような縦長な行列に変形した後は列方向に和を取り、最後に変形と次元入れ替えを行えば出力の完成です。
![pool_sum_reshape_transpose.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/d76ae218-11b8-3eb4-fed9-365d188a3e08.png)
また、列和を取る前に最大値のインデックスを取得しておく必要があります。

## プーリング層逆伝播
続いて逆伝播です。関係しているのは数のカラー部分ですね。
![pooling_layer_backward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/2f4a6417-7746-8fdc-b91b-d3915695aa66.png)
動作としては

1. 出力画像の勾配を変形する
2. 入力画像を`im2col`関数に投げた時の返り値と同じ形状の空の行列を生成する
3. 生成した空の行列の、元の返り値の最大値があったインデックスに勾配情報を配置する
4. `col2im`関数に投げる

という感じです。ちょっと言葉だけでは動作が分かりにくいですね…以下のような感じです。
![pool_backward_transpose_reshape_push.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/79fa12c4-200f-2d8d-11ed-cab316d00aab.png)
![pool_backward_reshape_T.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/fd588201-1152-6083-77e4-79be7a1c2eef.png)
![pool_backward_col2im.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/2fac683e-d63a-47fc-1482-d4b13591d505.png)

## プーリング層学習
設計図にないのでお分かりかと思いますが、プーリング層に学習すべきパラメータは存在しません。ということで学習することもありません。

## プーリング層実装
プーリング層の説明は畳み込み層と比べて随分簡単でしたね。実装もそこまで複雑なことはしません。
<details><summary>pool.py</summary><div>

```pool.py
import numpy as np


class PoolingLayer(BaseLayer):
    def __init__(self, *, I_shape=None,
                 pool=1, pad=0,
                 name="", **kwds):
        self.name = name
        
        if I_shape is None:
            raise KeyError("Input shape is None.")
        
        if len(I_shape) == 2:
            C, I_h, I_w = 1, *I_shape
        else:
            C, I_h, I_w = I_shape
        self.I_shape = (C, I_h, I_w)
        
        _, O_shape, self.pad_state = im2col(np.zeros((1, *self.I_shape)), (pool, pool),
                                            stride=pool, pad=pad)
        self.O_shape = (C, *O_shape)
        
        self.n = np.prod(self.O_shape)
        
        self.pool = pool
        self.F_shape = (pool, pool)
    
    
    def forward(self, x):
        B = x.shape[0]
        C, O_h, O_w = self.O_shape
        
        self.x, _, self.pad_state = im2col(x, self.F_shape,
                                           stride=self.pool,
                                           pad=self.pad_state)
        
        self.x = self.x.T.reshape(B*O_h*O_w*C, -1)
        self.max_index = np.argmax(self.x, axis=1)
        self.y = np.max(self.x, axis=1).reshape(B, O_h, O_w, C).transpose(0, 3, 1, 2)
        
        return self.y
    
    
    def backward(self, grad):
        B = grad.shape[0]
        I_shape = B, *self.I_shape
        C, O_h, O_w = self.O_shape
        
        grad = grad.transpose(0, 2, 3, 1).reshape(-1, 1)
        self.grad_x = np.zeros((grad.size, self.pool*self.pool))
        self.grad_x[:, self.max_index] = grad
        self.grad_x = self.grad_x.reshape(B*O_h*O_w, C*self.pool*self.pool).T
        self.grad_x = col2im(self.grad_x, I_shape, self.O_shape,
                             stride=self.pool, pad=self.pad_state)
        
        return self.grad_x
    
    
    def update(self, **kwds):
        pass
```
</div></details>


# おわりに
CNNの実験コード組んでみたら上手く動かず、ずっと調査してました...結論から言うと畳み込み層もプーリング層も問題なく、活性化関数が問題だったんですけどね泣笑
[活性化関数一覧](https://qiita.com/kuroitu/items/73cd401afd463a78115a)の方の実装も変更してあります。
次回記事で実験コードを載せます。`LayerManager`クラスなども結構変更してたりします。

# 深層学習シリーズ
- [深層学習入門 ~基礎編~](https://qiita.com/kuroitu/items/221e8c477ffdd0774b6b)
- [深層学習入門 ~コーディング準備編~](https://qiita.com/kuroitu/items/884c62c48c2daa3def08)
- [深層学習入門 ~順伝播編~](https://qiita.com/kuroitu/items/d22c8750e34d5d75fb6c)
- [深層学習入門 ~逆伝播編~](https://qiita.com/kuroitu/items/ea6ed8f614e65ec44976)
- [深層学習入門 ~学習則編~](https://qiita.com/kuroitu/items/ab5ad4ac716ae7a04891)
- [深層学習入門 ~ローカライズと損失関数編~](https://qiita.com/kuroitu/items/a6725ac0139d8eeb1e19)
- [深層学習入門 ~関数近似編~](https://qiita.com/kuroitu/items/4a8badcf7d3139d9ec7b)
- [深層学習入門 ~畳み込みとプーリング編~](https://qiita.com/kuroitu/items/48bf6bc42e4dfee52cde)
- [深層学習入門 ~CNN実験編~](https://qiita.com/kuroitu/items/d4716e754594a29b059d)
- [深層学習外伝 ~GPUプログラミング編~](https://qiita.com/kuroitu/items/2618c598f46d6ab3889c)
- [深層学習入門 ~ドロップアウト編~](https://qiita.com/kuroitu/items/d1bed8c216950d87be74)
- [活性化関数一覧 (2020)](https://qiita.com/kuroitu/items/73cd401afd463a78115a)
- [勾配降下法一覧 (2020)](https://qiita.com/kuroitu/items/36a58b37690d570dc618)
- [見てわかる！最適化手法の比較 (2020)](https://qiita.com/kuroitu/items/6695e0c79e888543e150)
- [im2col徹底理解](https://qiita.com/kuroitu/items/35d7b5a4bde470f69570)
- [col2im徹底理解](https://qiita.com/kuroitu/items/7877c002e192955c7857)
- [numpy.pad関数完全理解](https://qiita.com/kuroitu/items/51f4c867c8a44de739ec)