2.8 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)となる

です。

2.8.1 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(縦方向)で並んでいると解釈できます。

2.8.2 im2colによる展開

「ゼロから作るDeep Learning」で導入されているim2col関数の動作を確認します。フィルタサイズが、FH=2FW=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=1OW=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になります。

2.8.3 Convolutionレイヤの実装

2.8.3.1 順伝搬

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

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

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

    col_W = self.W.reshape(FN, -1).T

変換後のフィルタを\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として、

    out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

としています。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)

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

2.8.3.2 逆伝搬

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

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

フィルタの形状が、(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]

それを、

    dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)

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

(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

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

    self.db = np.sum(dout, axis=0)
    self.dW = np.dot(self.col.T, dout)
    dcol = np.dot(dout, self.col_W.T)

\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次元配列に戻しています:

    self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

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

    dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

さて、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レイヤの実装の確認ができました。

2.8.4 Poolingレイヤの実装

2.8.4.1 順伝搬

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

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

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=2PW=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で最大値を取った後、reshapetransposeによって、

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

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

2.8.4.2 逆伝搬

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

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

前述のとおり、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}}と同じ形状のゼロ配列です。

doutflattenされてベクトルになった後、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レイヤの実装の確認ができました。

2.8.5 Ivoryライブラリでの実装

実際にIvoryライブラリでの実装を確認します。まずは孤立したレイヤを作成する例を示します。

conv = Convolution((2, 6, 6, 3, 3, 3))  # (C, H, W, FN, FH, FW)
print(conv.x)  # (C, H, W)
print(conv.W)  # (FN, C, FH, FW)
print(conv.b)  # (FN,)
print(conv.y)  # (FN, OH, OW)

[65] 2019-06-12 20:01:24 (31.3ms) python3 (1.09s)

<Input('Convolution.1.x', (2, 6, 6)) at 0x1aec976cd68>
<Weight('Convolution.1.W', (3, 2, 3, 3)) at 0x1aec976cdd8>
<Weight('Convolution.1.b', (3,)) at 0x1aec80794e0>
<Output('Convolution.1.y', (3, 4, 4)) at 0x1aec976cda0>
pool = Pooling((3, 4, 4, 2, 2))  # (C, H, W, PH, PW)
print(pool.x)  # (C, H, W)
print(pool.y)  # (C, OH, OW)

[66] 2019-06-12 20:01:24 (13.0ms) python3 (1.11s)

<Input('Pooling.1.x', (3, 4, 4)) at 0x1aec976ccc0>
<Output('Pooling.1.y', (3, 2, 2)) at 0x1aec976cc50>

「ゼロから作るDeep Learing」のSimpleConvNetを再現します。ここではAffineレイヤを接続するためにFlattenレイヤを間に入れます。

from ivory.core.model import sequential

net = [
    ("input", 1, 10, 10),
    ("convolution", 10, 3, 3, "relu"),
    ("pooling", 2, 2, "flatten"),
    ("affine", 10, "relu"),
    ("affine", 10, "softmax_cross_entropy"),
]
model = sequential(net)
model.layers

[67] 2019-06-12 20:01:24 (17.0ms) python3 (1.12s)

[<Convolution('Convolution.2', (1, 10, 10, 10, 3, 3)) at 0x1aec97788d0>,
 <Relu('Relu.1', (10, 8, 8)) at 0x1aec8b42278>,
 <Pooling('Pooling.2', (10, 8, 8, 2, 2)) at 0x1aec9767c50>,
 <Flatten('Flatten.1', (10, 4, 4, 160)) at 0x1aec97675f8>,
 <Affine('Affine.1', (160, 10)) at 0x1aec9767588>,
 <Relu('Relu.2', (10,)) at 0x1aec8020668>,
 <Affine('Affine.2', (10, 10)) at 0x1aec898c2e8>,
 <SoftmaxCrossEntropy('SoftmaxCrossEntropy.1', (10,)) at 0x1aec5bad048>]

例題に合わせるため重みの標準偏差を0.01に設定します。

for v in model.weight_variables:
    if v.parameters[0].name == "W":
        v.data = v.init(std=0.01)
        print(v.parameters[0].name, v.data.std())

[68] 2019-06-12 20:01:25 (23.9ms) python3 (1.15s)

W 0.009962479
W 0.010202464
W 0.010482968

ランダムデータで評価してみます。

import numpy as np

x = np.random.rand(100).reshape((1, 1, 10, 10))
t = np.array([1])

model.set_data(x, t)
model.forward()
model.backward()

for v in model.grad_variables:
    print(v.parameters[0].name, model.gradient_error(v))

[69] 2019-06-12 20:01:25 (928ms) python3 (2.08s)

x 4.65996037693533e-12
W 5.970507979976521e-07
b 4.4278590312536723e-10
W 8.194454141895044e-11
b 4.3282836403521485e-09
W 1.5676114057263746e-10
b 1.7663007221835335e-07

最後に、実装コードを記載します。

File 2.2 layers/convolution.py

from ivory.common.context import np
from ivory.common.util import col2im, im2col
from ivory.core.layer import Layer

class Convolution(Layer):
    """Convolution((C, H, W, FN, FH, FW)).

    * Input: (N, C, H, W)
    * Filter : (FN, C, FH, FW)
    * Output:  (N, FN, OH, OW)
    """

    input_ndim = 3

    def init(self, stride=1, padding=0):
        C, W, H, FN, FH, FW = self.shape
        self.W = self.add_weight((FN, C, FH, FW)).randn()
        self.b = self.add_weight((FN,)).zeros()
        self.stride = self.add_state(stride)
        self.padding = self.add_state(padding)
        OH = 1 + int((H - FH + 2 * padding) / stride)
        OW = 1 + int((W - FW + 2 * padding) / stride)
        self.y.shape = FN, OH, OW

    def forward(self):
        FN, C, FH, FW = self.W.shape
        FN, OH, OW = self.y.shape
        self.x_2d = im2col(self.x.d, FH, FW, self.stride.d, self.padding.d)
        self.W_2d = self.W.d.reshape(FN, -1).T
        y_2d = self.x_2d @ self.W_2d + self.b.d
        N = self.x.d.shape[0]
        self.y.d = y_2d.reshape(N, OH, OW, -1).transpose(0, 3, 1, 2)

    def backward(self):
        FN, C, FH, FW = self.W.shape
        dy_2d = self.y.g.transpose(0, 2, 3, 1).reshape(-1, FN)
        self.b.g = np.sum(dy_2d, axis=0)
        dW_2d = self.x_2d.T @ dy_2d
        self.W.g = dW_2d.transpose(1, 0).reshape(FN, C, FH, FW)
        dx_2d = dy_2d @ self.W_2d.T
        self.x.g = col2im(dx_2d, self.x.d.shape, FH, FW, self.stride.d, self.padding.d)


class Pooling(Layer):
    """Pooling((C, H, W, PH, PW)).

    * Input: (N, C, H, W)
    * Output:  (N, C, OH, OW)
    """

    input_ndim = 3

    def init(self, stride=0, padding=0):
        C, H, W, PH, PW = self.shape
        stride = stride or PH
        self.stride = self.add_state(stride)
        self.padding = self.add_state(padding)
        OH = 1 + int((H - PH + 2 * padding) / stride)
        OW = 1 + int((W - PW + 2 * padding) / stride)
        self.y.shape = C, OH, OW

    def forward(self):
        PH, PW = self.shape[3:5]
        C, OH, OW = self.y.shape
        x_2d = im2col(self.x.d, PH, PW, self.stride.d, self.padding.d)
        pool_size = self.shape[3] * self.shape[4]
        x_2d = x_2d.reshape(-1, pool_size)
        self.arg_max = np.argmax(x_2d, axis=1)
        N = self.x.d.shape[0]
        self.y.d = np.max(x_2d, axis=1).reshape(N, OH, OW, C).transpose(0, 3, 1, 2)

    def backward(self):
        PH, PW = self.shape[3:5]
        dy = self.y.g.transpose(0, 2, 3, 1)
        pool_size = self.shape[3] * self.shape[4]
        dmax = np.zeros((dy.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dy.flatten()
        dmax = dmax.reshape(dy.shape + (pool_size,))
        dx_2d = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        self.x.g = col2im(dx_2d, self.x.d.shape, PH, PW, self.stride.d, self.padding.d)