Monthly Hacker's Blog

毎月のテーマに沿ったプログラミング記事を中心に書きます。

【保存版】chainerのconvolutionとdeconvolution周りを理解する


chainerのdeconvolutionがどういう演算をしているのか理解していなかったので、ソースコードとにらめっこしました。ちなみに、以下の可視化は非常に参考になりました。

github.com

この記事では次の関数について解説します。

  • ダウンサンプリング系
    • F.convolution_2d(L.Convolution2D)
    • F.max_pooling_2d(F.average_pooling_2d)
  • アップサンプリング系
    • F.deconvolution_2d(L.Deconvolution2D)
    • F.unpooling_2d
    • F.upsampling_2d
    • F.depth2space


基本的に入力xとフィルタ(重み)Wを用いて説明していきます。xとWの値は以下の通りです。

x = np.arange(25, dtype=np.float32).reshape(1, 1, 5, 5)
W = np.arange(4, dtype=np.float32).reshape(1, 1, 2, 2)

なお、以降ブログでは次のような行列表現も使用します。

$$
\begin{eqnarray}
x &=& \begin{bmatrix}
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
\end{bmatrix} \\
W &=& \begin{bmatrix}
0 & 1 \\\
2 & 3
\end{bmatrix}
\end{eqnarray}
$$

これは、入力・フィルタ・出力それぞれで特定のチャンネルにのみ考える場合、テンソルではなく行列と考えることができるためです。

ダウンサンプリング系

F.convolution_2d(L.Convolution2D)

  • x
    • 入力
  • W
    • フィルタ
  • b
    • バイアス
  • stride
    • ストライド
  • pad
    • パディングサイズ
  • use_cudnn
    • cuDNN使用のフラグ
  • cover_all
    • cover_allのフラグ(後述)
  • deterministic
    • cuDNNでの決定論的アルゴリズム使用のフラグ

L.Convolution2D内で呼び出されている関数がF.convolution_2dです。

cover_allというのは、ストライドが2以上のときに影響することがあります。具体例を見てみます。

$$
\begin{eqnarray}
x &=& \begin{bmatrix}
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
\end{bmatrix} \\
W &=& \begin{bmatrix}
0 & 1 \\\
2 & 3
\end{bmatrix}
\end{eqnarray}
$$

strideが2の場合、入力の5行(列)目が何の計算もされず、情報が切り捨てられてしまいます。これを避けるフラグがcover_allです。入力の全ての値で計算が行われるように自動で0パディングされます。この例では、pad=1と同じ意味です。ちなみに、この機能は現在(v1.21.0まで確認)average_pooling_2dにはありません。0パディングすると、平均の値が変わってしまうためだと思います。

F.max_pooling_2d(F.average_pooling_2d)

  • x
    • 入力
  • ksize
    • カーネルサイズ
  • stride
    • ストライド
  • pad
    • パディングサイズ
  • cover_all(F.max_pooling_2dのみ)
    • cover_allのフラグ(前述)
  • use_cudnn
    • cuDNN使用のフラグ

F.max_pooling_2dとF.average_pooling_2dはcover_allの有無以外は同じ引数です(v1.21.0現在)。cover_allがTrueの場合、0パディングが行われる場合があります。その場合、平均の値が変わってしまうのでcover_allがまだ実装されていないのだと思います。

基本的にF.convolution_2d(L.Convolution2D)と(ダウンサンプリングの観点では)同じ挙動です。

アップサンプリング系

F.deconvolution_2d(L.Deconvolution2D)

  • x
    • 入力
  • W
    • フィルタ
  • b
    • バイアス
  • stride
    • ストライド
  • pad
    • パディングサイズ
  • outsize
    • 出力サイズ
  • use_cudnn
    • cuDNN使用のフラグ
  • deterministic
    • cuDNNでの決定論的アルゴリズム使用のフラグ

この計算方法がかなりややこしいことになっていますが、こちらが基本です。

https://github.com/vdumoulin/conv_arithmetic/blob/master/gif/padding_strides_transposed.gif?raw=true

では、具体例で見てみましょう。

$$
\begin{eqnarray}
x &=& \begin{bmatrix}
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
\end{bmatrix} \\
W &=& \begin{bmatrix}
0 & 1 \\\
2 & 3
\end{bmatrix}
\end{eqnarray}
$$

このxとWで、ストライド1の例を考えてみます。ストライド1の場合、通常の畳み込みと同じだと思っていましたが、実は違います。

F.convolution_2d(x, W)

$$
\begin{bmatrix}
29 & 35 & 41 & 47 \\
59 & 65 & 71 & 77 \\
89 & 95 & 101 & 107 \\
119 & 125 & 131 & 137
\end{bmatrix}
$$

F.deconvolution_2d(x, W)

$$
\begin{bmatrix}
0 & 0 & 1 & 2 & 3 & 4 \\
0 & 7 & 13 & 19 &25 & 21 \\
10 & 37 & 43 & 49 & 55 & 41 \\
20 & 67 & 73 & 79 & 85 & 61 \\
30 & 97 & 103 & 109 & 115 & 81 \\
40 & 102 & 107 & 112 & 117 & 72
\end{bmatrix}
$$

ストライド1の場合で比較してもF.convolution_2dとF.deconvolution_2dは違う結果を出力します。この原因は畳み込み演算の違いによります。ニューラルネットワークの畳み込みと一般的な畳み込みは、演算方法が違うという話を聞いたことがある人もいるのではないでしょうか。本記事ではここを詳しく説明してしまうと長くなりすぎるので、気になる方は検索してみてください。

Wの縦横を逆順にすると、F.convolution_2dとF.deconvolution_2dの畳み込み演算が一致します。

W

$$
\begin{bmatrix}
0 & 1 \\\
2 & 3
\end{bmatrix}
$$

W[:, :, ::-1, ::-1]

$$
\begin{bmatrix}
3 & 2 \\\
1 & 0
\end{bmatrix}
$$

F.convolution_2d(x, W)

$$
\begin{bmatrix}
29 & 35 & 41 & 47 \\
59 & 65 & 71 & 77 \\
89 & 95 & 101 & 107 \\
119 & 125 & 131 & 137
\end{bmatrix}
$$

F.deconvolution_2d(x, W[:, :, ::-1, ::-1])

$$
\begin{bmatrix}
0 & 3 & 8 & 13 & 18 & 8 \\
15 & 29 & 35 & 41 & 47 & 18 \\
35 & 59 & 65 & 71 & 77 & 28 \\
55 & 89 & 95 & 101 & 107 & 38 \\
75 & 119 & 125 & 131 & 137 & 48 \\
20 & 21 & 22 & 23 & 24 & 0
\end{bmatrix}
$$

F.convolution_2dの結果がF.deconvolution_2dの一部に現れています。ただ行数、列数が異なります。これはF.deconvolution_2dではストライド数に関わらずパディングサイズ1で0パディングが適用されるからです。ちなみにこれは引数のpadとは異なるので注意してください。試しに、次のコードでパディングしてからF.convolution_2dしてみます。

x_padded = np.zeros((1, 1, 7, 7), dtype=np.float32)
x_padded[:, :, 1:-1, 1:-1] = x
F.convolution_2d(x_padded, W)

$$
\begin{bmatrix}
0 & 3 & 8 & 13 & 18 & 8 \\
15 & 29 & 35 & 41 & 47 & 18 \\
35 & 59 & 65 & 71 & 77 & 28 \\
55 & 89 & 95 & 101 & 107 & 38 \\
75 & 119 & 125 & 131 & 137 & 48 \\
20 & 21 & 22 & 23 & 24 & 0
\end{bmatrix}
$$

F.deconvolution_2d(x, W[:, :, ::-1, ::-1])

$$
\begin{bmatrix}
0 & 3 & 8 & 13 & 18 & 8 \\
15 & 29 & 35 & 41 & 47 & 18 \\
35 & 59 & 65 & 71 & 77 & 28 \\
55 & 89 & 95 & 101 & 107 & 38 \\
75 & 119 & 125 & 131 & 137 & 48 \\
20 & 21 & 22 & 23 & 24 & 0
\end{bmatrix}
$$

これはストライド2以上でも同じです。ストライド3で具体例を見てみます。まずパディングのコードはこちら。

x_padded = np.zeros((1, 1, 15, 15), dtype=np.float32)
x_padded[:, :, 1:-1:3, 1:-1:3] = x
x_padded

$$
\begin{bmatrix}
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 2 & 0 & 0 & 3 & 0 & 0 & 4 & 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 & 5 & 0 & 0 & 6 & 0 & 0 & 7 & 0 & 0 & 8 & 0 & 0 & 9 & 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 & 10 & 0 & 0 & 11 & 0 & 0 & 12 & 0 & 0 & 13 & 0 & 0 & 14 & 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 & 15 & 0 & 0 & 16 & 0 & 0 & 17 & 0 & 0 & 18 & 0 & 0 & 19 & 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 & 20 & 0 & 0 & 21 & 0 & 0 & 22 & 0 & 0 & 23 & 0 & 0 & 24 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix}
$$

F.convolution_2d(x_padded, W)

$$
\begin{bmatrix}
0 & 0 & 0 & 0 & 1 & 0 & 0 & 2 & 0 & 0 & 3 & 0 & 0 & 4 \\
0 & 0 & 0 & 2 & 3 & 0 & 4 & 6 & 0 & 6 & 9 & 0 & 8 & 12 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 5 & 0 & 0 & 6 & 0 & 0 & 7 & 0 & 0 & 8 & 0 & 0 & 9 \\
10 & 15 & 0 & 12 & 18 & 0 & 14 & 21 & 0 & 16 & 24 & 0 & 18 & 27 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 10 & 0 & 0 & 11 & 0 & 0 & 12 & 0 & 0 & 13 & 0 & 0 & 14 \\
20 & 30 & 0 & 22 & 33 & 0 & 24 & 36 & 0 & 26 & 39 & 0 & 28 & 42 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 15 & 0 & 0 & 16 & 0 & 0 & 17 & 0 & 0 & 18 & 0 & 0 & 19 \\
30 & 45 & 0 & 32 & 48 & 0 & 34 & 51 & 0 & 36 & 54 & 0 & 38 & 57 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 20 & 0 & 0 & 21 & 0 & 0 & 22 & 0 & 0 & 23 & 0 & 0 & 24 \\
40 & 60 & 0 & 42 & 63 & 0 & 44 & 66 & 0 & 46 & 69 & 0 & 48 & 72
\end{bmatrix}
$$

F.deconvolution_2d(x, W[:, :, ::-1, ::-1], stride=3)

$$
\begin{bmatrix}
0 & 0 & 0 & 0 & 1 & 0 & 0 & 2 & 0 & 0 & 3 & 0 & 0 & 4 \\
0 & 0 & 0 & 2 & 3 & 0 & 4 & 6 & 0 & 6 & 9 & 0 & 8 & 12 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 5 & 0 & 0 & 6 & 0 & 0 & 7 & 0 & 0 & 8 & 0 & 0 & 9 \\
10 & 15 & 0 & 12 & 18 & 0 & 14 & 21 & 0 & 16 & 24 & 0 & 18 & 27 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 10 & 0 & 0 & 11 & 0 & 0 & 12 & 0 & 0 & 13 & 0 & 0 & 14 \\
20 & 30 & 0 & 22 & 33 & 0 & 24 & 36 & 0 & 26 & 39 & 0 & 28 & 42 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 15 & 0 & 0 & 16 & 0 & 0 & 17 & 0 & 0 & 18 & 0 & 0 & 19 \\
30 & 45 & 0 & 32 & 48 & 0 & 34 & 51 & 0 & 36 & 54 & 0 & 38 & 57 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 20 & 0 & 0 & 21 & 0 & 0 & 22 & 0 & 0 & 23 & 0 & 0 & 24 \\
40 & 60 & 0 & 42 & 63 & 0 & 44 & 66 & 0 & 46 & 69 & 0 & 48 & 72
\end{bmatrix}
$$

ストライドは理解できたと思います。続いて、パディングを説明します。これもまた非常にややこしくなっています。これは、F.convolution_2dとstrideおよびpadを揃えると、それぞれの入力と出力が同じサイズになるように定められています。ドキュメントに出力サイズを求める式が書いてあります。

http://docs.chainer.org/en/latest/reference/functions.html#chainer.functions.convolution_2d
http://docs.chainer.org/en/latest/reference/functions.html#chainer.functions.deconvolution_2d

それぞれ、出力サイズは次のように計算できます。簡略化のため、次のように文字を置き換えています。添字は畳み込みを表すcと逆畳み込みを表すdを用います。

  • o
    • 出力サイズ
  • i
    • 入力サイズ
  • p
    • パディングサイズ
  • k
    • カーネルサイズ
  • s
    • ストライド
F.convolution_2d

$$
o_c = (i_c + 2p_c - k_c) / s_c + 1
$$

F.deconvolution_2d

$$
o_d = s_d(i_d - 1) + k_d - 2p_d
$$

ここで、次の制約を考えます。

$$
\begin{eqnarray}
p_c &=& p_d &=& p \\
k_c &=& k_d &=& k \\
s_c &=& s_d &=& s \\
o_c &=& i_d &=& h
\end{eqnarray}
$$

この制約を出力サイズを計算する式に代入し整理すると、次のように簡潔になります。

$$
i_c = o_d
$$

畳み込みの入力サイズと逆畳み込みの出力サイズが一致しました。つまり畳み込み→逆畳み込みと(poolingなしで)演算をする際、パディングサイズ、カーネルサイズ、ストライドを合わせると入力サイズと出力サイズが同じになると言えます。逆畳み込みのpadという引数は、この性質を満たすように逆算的に決められているようです。そのため、パディングサイズを大きくすると出力サイズが小さくなるという、直感に反する結果が得られます。

最後にoutsizeです。例えばAutoEncoderやGANなどで、出力サイズを特定サイズにしたいけれど計算が面倒なときに使えます。出力結果に0パディングをして出力サイズを調整してくれます。ただし、調整できる範囲は決まっているようです。

F.unpooling_2d

  • x
    • 入力
  • ksize
    • カーネルサイズ
  • stride
    • ストライド
  • pad
    • パディングサイズ
  • outsize
    • 出力サイズ
  • cover_all
    • cover_allのフラグ

1つの値をカーネルサイズ分に増やすことでアップサンプリングする関数です。引数はF.deconvolution_2dと同じように解釈します。すなわち、F.max_pooling_2dやF.average_pooling_2dと引数を合わせると、サイズが変わらないように定められています。

F.upsampling_2d

  • x
    • 入力
  • indexes
    • 最大プーリングの際の、最大値のインデックス
  • kseze
    • カーネルサイズ
  • stride
    • ストライド
  • pad
    • パディングサイズ
  • outsize
    • 出力サイズ
  • cover_all
    • cover_allのフラグ

ほとんどF.unpooling_2dと同じです。違いは、最大プーリングの際の最大値の位置を用いたプーリングをすることです。この関数はドキュメントが充実しているので、参考にしてみてください。

F.depth2space

  • X
    • 入力
  • r
    • 倍率

この論文で使われているアップサンプリング手法です。論文から引用した図を見れば一目瞭然です。

f:id:d-higurashi:20170303161614p:plain

最後に

意外と理解していない引数が多く、なかなか良い勉強になりました。誤りや不足やど、加筆修正すべきところは多々あると思いますので、コメントでご指摘ください。また、今後もダウンサンプリング/アップサンプリング関数が増えると思うので、その際にも加筆していく予定です。