Kerasでの2D畳み込み処理時間比較

#概要
[Depthwise (separable) convolutionとか色々な畳込みの処理時間を比較してみる](https://qiita.com/yu4u/items/cf3f81e32fe613747f76)の記事の中に
>というわけで、depthwise convolutionは現状の実装ではGPUでは高速化に寄与しなさそう。
>(中略)
>元の論文では、separable convolutionとpointwise convolutionは計算量的にpointwise convolutionがボトルネックとしていたが、現状圧倒的にseparable convolutionのほうがボトルネックとなっている。この辺り、どのくらいまで実装で高速化できるのだろうか。

という処理時間の観点でdepthwise convolutionの役立たず説があったので(一年以上前の記事ですが…)、自分の環境でも畳込みの時間測定をしてdepthwise convolutionの性能をみてみました。

#環境
OS:Windows10
GPU:GeForce 1070Ti
TensorFlow:1.10.0
Keras:2.2.2

#条件
CNNを行うサイズをそれぞれ[200x200x16, 100x100x32, 50x50x64, 25x25x128, 12x12x256, 6x6x512]とした32層のCNNを構築し、バッチサイズを200としてランダムな入力を50回forwardした時間を計測しました。
積み重ねる畳み込み層はkernel_size=(1,1)～(5,5)のConv2D, DepthwiseConv2D, SeparableConv2Dとしました。
また、stride=1のMaxPooling2DとAveragePooling2DもDepthwiseの畳み込みに近いので同様に測定しました。

```python:
    inputs = Input(shape=(image_size, image_size, 1))
    x = Conv2D(filter_num, (1,1), padding='same', use_bias=False)(inputs)
    for i in range(layer_num):
        x = Conv2D(filter_num, (3,3), padding='same')(x)
```

#結果
KerasのGPU環境にて上記条件で実行にかかった処理時間[s]を測定してみました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/299928/9645543b-72d9-2e85-5d0e-0502e0aad308.png)

#考察
- **$H×W×C$** (縦×横×チャンネル)において縦横を1/2倍にしてチャンネルを2倍にしたサイズでの畳み込み演算の方が大体の場合において演算時間は短縮される。
- 本来はサイズが減れば演算時間も1/2倍ずつになってほしいところだが、何らかのゲタをはいているのかConv2Dにおいて演算時間は1/2倍になっていない。
- また、例外として**$6×6×512$**のConv2dの(3x3)で、この場合は却って演算時間は増大する。
- 一般にkernel_sizeが大きいほど処理時間は増える。しかし、(2x2)と(3x3)にほとんど差がない場合もある。
- DepthwiseConv2Dの(3x3)はチャンネル数が小さい時、通常のConv2Dと大差ないが、チャンネル数が大きい時、通常のConv2Dより有利になる。
- SeparableConv2Dの(3x3)は理論的にはDepthwiseConv2Dの(3x3)+Conv2dの(1x1)の足し合わせだが、測定した処理時間は二つの足し合わせよりも短くなっている。（ゲタをはいているのか？）
- SeparableConv2Dの(3x3)はチャンネル数64以上で通常のConv2dの(3x3)より早くなる。
- MaxPooling2DのほうがAveragePooling2Dより早い。
- MaxPooling2DやAveragePooling2Dの演算時間の傾向はDepthwiseConv2Dに近い。

#結論
KerasのGPU環境にて畳み込みの速度比較を行ったが、自分の環境ではDepthwiseConv2Dの処理速度の有利性は確認できた。特にSeparableConv2Dはチャンネル数が多い状況では通常のConv2dより高速化（倍くらい）するので有利である。
逆に入力層近くではSeparableConv2Dを使う必要性はないかも。

#コード
```python:
from keras import Input
from keras.models import Model
from keras.layers.convolutional import Conv2D, DepthwiseConv2D, SeparableConv2D,  MaxPooling2D, AveragePooling2D
import numpy as np
import time

data_num = 10000
batch_num = 200
layer_num = 32
image_size = 6
filter_num = 512
loop_num = data_num//batch_num

device_mode = "/gpu:0"
# device_mode = "/cpu:0"

timings = []
input = np.random.randn(batch_num, image_size, image_size, 1)

for j in range(-2, 24):
    inputs = Input(shape=(image_size, image_size, 1))
    x = Conv2D(filter_num, (1,1), padding='same', use_bias=False)(inputs)

    for i in range(layer_num):
        if j == -2:
            pass
        if j == -1:
            pass
        if j == 0:
            pass
        if j == 1:
            x = Conv2D(filter_num, (1,1), padding='same')(x)
        if j == 2:
            x = Conv2D(filter_num, (2,2), padding='same')(x)
        if j == 3:
            x = Conv2D(filter_num, (3,3), padding='same')(x)
        if j == 4:
            x = Conv2D(filter_num, (4,4), padding='same')(x)
        if j == 5:
            x = Conv2D(filter_num, (5,5), padding='same')(x)
        if j == 6:
            x = DepthwiseConv2D((1,1), padding='same')(x)
        if j == 7:
            x = DepthwiseConv2D((2,2), padding='same')(x)
        if j == 8:
            x = DepthwiseConv2D((3,3), padding='same')(x)
        if j == 9:
            x = DepthwiseConv2D((4,4), padding='same')(x)
        if j == 10:
            x = DepthwiseConv2D((5,5), padding='same')(x)
        if j == 11:
            x = SeparableConv2D(filter_num, (1,1), padding='same')(x)
        if j == 12:
            x = SeparableConv2D(filter_num, (2,2), padding='same')(x)
        if j == 13:
            x = SeparableConv2D(filter_num, (3,3), padding='same')(x)
        if j == 14:
            x = SeparableConv2D(filter_num, (4,4), padding='same')(x)
        if j == 15:
            x = SeparableConv2D(filter_num, (5,5), padding='same')(x)
        if j == 16:
            x = MaxPooling2D((2, 2), strides = (1,1), padding='same')(x)
        if j == 17:
            x = MaxPooling2D((3, 3), strides = (1,1), padding='same')(x)
        if j == 18:
            x = MaxPooling2D((4, 4), strides = (1,1), padding='same')(x)
        if j == 19:
            x = MaxPooling2D((5, 5), strides = (1,1), padding='same')(x)
        if j == 20:
            x = AveragePooling2D((2, 2), strides = (1,1), padding='same')(x)
        if j == 21:
            x = AveragePooling2D((3, 3), strides = (1,1), padding='same')(x)
        if j == 22:
            x = AveragePooling2D((4, 4), strides = (1,1), padding='same')(x)
        if j == 23:
            x = AveragePooling2D((5, 5), strides = (1,1), padding='same')(x)
        

    model = Model(inputs, x)
    #model.summary()

    start = time.time()
    for i in range(loop_num):
        out = model.predict(input, batch_size=batch_num)

    elapsed_time = time.time() - start

    print('j = ', j, ',time = ', elapsed_time)
    if j > 0:
        print('diff_time = ', elapsed_time - timings[2])
    timings.append(elapsed_time)
```