#はじめに
CNNモデルの大きさの比較には総パラメータ数が用いられることが多いが、計算量とパラメータ数は必ずしも比例関係に無い。そこで、計算量を推定するコードを作成し、各種モデルの比較を行うことにした。計算に使ったコードは記事の最後に掲載。
##動機
DenseNetはパラメータ数が比較的少ないが、その割に訓練に時間がかかることが気になった。同様の疑問を持った人が記事を書いていて、そこではパラメータ数に対して計算量が多いからだろう、という感じのことが指摘されていた。本当にそうなのか確かめるため、その他のモデルと比較してみた。
環境
TensorFlow : 2.4.0
Keras : 2.4.0
#計算方法
モデル中の掛け算の数の合計で計算量とする。論文によって'MAdds'や'FLOPS'など呼称に違いがあるが、ここでは'FLOPS'と表記する。
計算量の対象としては、基本的にConv2D系とDenseのみに限定する。Pooling系は無視しているので、若干実際よりも小さい推定になっているはずだが、ほぼ無視してよい程度だろう。
基本的に、こちらの記事を参考にしてあるので、計算の根拠はそちらの記事で確認してもらいたい。
Xceptionで使用されるSeparableConv2Dは、Depthwise2Dと1x1のConv2Dの合計として算出した。SeparableConv2Dについては、こちらの記事参照。
MobileNetやEfficientNetの論文では各種モデルの計算量が示されているが、今回使用したコードでの算出でもほぼ同じ数値が出ているので、計算法に関してはそれなりに信頼できるものと思われる。
モデルはtf.kerasで提供されるモデルを使用する。
大抵のモデルはImageNetにあわせて(224x224x3)をデフォルトの入力とするが、一部モデルではデフォルトではこのサイズになっていない。入力サイズによって計算量が変わってしまうため、参考として(224x224x3)での計算量も載せた。
同一モデルでも複数提供されているものもあるが(ResNet50とResNet101など)、その場合はパラメータ数が最も大きいものと最も小さいもので、二つを対象とした。
#計算結果
以下の表が計算結果。
Model Name | Input Shape | Params(M) | MFLOPS | FLOPS/Params |
---|---|---|---|---|
VGG16 | (224, 224) | 138.4 | 15,470.3 | 111.8 |
VGG19 | (224, 224) | 143.7 | 19,632.1 | 136.6 |
Xception | (299, 299) | 22.9 | 8,357.4 | 364.8 |
ResNet50v2 | (224, 224) | 25.6 | 3,482.3 | 136.0 |
Resnet152v2 | (224, 224) | 60.4 | 10,906.7 | 180.6 |
InceptionV3 | (299, 299) | 23.9 | 5,713.2 | 239.5 |
InceptionResNetV2 | (299, 299) | 55.9 | 13,155.8 | 235.5 |
NASNetMobile | (224, 224) | 5.3 | 583.6 | 105.8 |
NASNetLarge | (331, 331) | 88.9 | 23,783.3 | 267.4 |
DenseNet121 | (224, 224) | 8.1 | 2,834.2 | 351.5 |
DenseNet201 | (224, 224) | 20.2 | 4,291.4 | 212.0 |
MobileNet | (224, 224) | 4.3 | 568.7 | 133.7 |
MobileNetV2 | (224, 224) | 3.5 | 300.8 | 85.0 |
EfficientNetB0 | (224, 224) | 5.3 | 388.1 | 72.8 |
EfficientNetB7 | (600, 600) | 66.7 | 37,868.8 | 568.1 |
以下、一部モデルで入力を224x224にあわせたもの。
Model Name | Input Shape | Params(M) | MFLOPS | FLOPS/Params |
---|---|---|---|---|
Xception | (224, 224) | 22.9 | 4,552.9 | 198.7 |
InceptionV3 | (224, 224) | 23.9 | 2,837.9 | 119.0 |
InceptionResNetV2 | (224, 224) | 55.9 | 6,469.0 | 115.8 |
NASNetLarge | (224, 224) | 88.9 | 10,283.8 | 115.6 |
EfficientNetB7 | (224, 224) | 66.7 | 5,186.8 | 77.8 |
総パラメータ数には計算対象外のものも入っているが、無視できる程度のはずなのでそのまま使用。(実際各種論文で示された数値との乖離はほとんどない)
筆者の関心は「パラメータ数と計算量の関係」にあるので、'計算量をパラメータ数で割ったもの'も載せてある。
実は、tf.kerasではMobileNetV3も提供されているのだが、論文に示された計算量と乖離がやや大きかったので掲載していない。これは記事執筆時のTensorFlow 2.4.0でのモデル側の実装に問題があるためと判断する。(自作のMobileNetV3のモデルでは論文と乖離が出ない)
##所感
- 224x224で合わせれば、多くのモデルでは計算量はパラメータ数の100~200倍程度になっている。
- VGG系はパラメータ数のわりに計算量はさほど大きくない。
- DenseNetはパラメータ数に対して計算量が大きめにでた。特にDenseNet121は比率では突出している。
- MobileNetV2とその派生といえるEfficientNetはパラメータ当たりの計算量が比較的小さい。
まとめ
各モデルの計算量の違いを示した。
DenseNetの計算量が多いことが示されたので、訓練時間が長いことはとりあえず納得することにする。面白い発想のモデルだと思うのだが、個人的には使うことを躊躇する。
MobileNet系(EfficientNet含む)は評判通り計算コストが小さい。今後もさらなる節約が可能なのか期待したい。
計算量を調べると実装の間違いもわかるので、そういう用途にも使える。(間違ったコードでもそれなりに動いてしまうのがDeepLearningの面白いところだが)
コード
import tensorflow as tf
def calc_FLOPS(model, verbose=0):
total_FLOPS = 0
for layer in model.layers:
input_shape = layer.input_shape[1:]
output_shape = layer.output_shape[1:]
if (isinstance(layer, tf.keras.layers.DepthwiseConv2D)):
kernel_size = layer.get_config()['kernel_size']
FLOPS = output_shape[2] * (output_shape[0]*output_shape[1]) * (kernel_size[0]*kernel_size[1])
elif (isinstance(layer, tf.keras.layers.SeparableConv2D)):
kernel_size = layer.get_config()['kernel_size']
FLOPS = input_shape[2] * (output_shape[0]*output_shape[1]) * (kernel_size[0]*kernel_size[1])
FLOPS += (input_shape[2]*output_shape[2]) * (output_shape[0]*output_shape[1])
elif (isinstance(layer,tf.keras.layers.Conv2D)):
kernel_size = layer.get_config()['kernel_size']
FLOPS = (input_shape[2]*output_shape[2]) * (output_shape[0]*output_shape[1]) * (kernel_size[0]*kernel_size[1])
elif (isinstance(layer, tf.keras.layers.Dense)):
FLOPS = (input_shape[0]*output_shape[0])
elif (isinstance(layer, tf.keras.layers.Multiply)):
FLOPS = output_shape[0]*output_shape[1]*output_shape[2]
else:
FLOPS = 0
if verbose==1:
if FLOPS!=0:
print( f'{layer.name: <32} : {FLOPS: >16,}')
total_FLOPS += FLOPS
total_params = model.count_params()
input_shape = model.layers[0].input_shape[0][1:-1]
result_str = f'| {model.name: <20} '
result_str += f'| {str(input_shape): ^11} '
result_str += f'| {total_params/(1000*1000): >9,.1f} '
result_str += f'| {total_FLOPS/(1000*1000): >9,.1f} '
result_str += f'| {total_FLOPS/total_params: >12,.1f} |'
print(result_str)
return
print(f"| {'Model Name': ^20} | {'Input Shape': ^11} | {'Params(M)': >9} | {'MFLOPS': ^9} | {'FLOPS/Params': ^12} |")
kwargs ={ 'weights':None}
calc_FLOPS( tf.keras.applications.VGG16(**kwargs) )
calc_FLOPS( tf.keras.applications.VGG19(**kwargs) )
calc_FLOPS( tf.keras.applications.Xception(**kwargs) )
calc_FLOPS( tf.keras.applications.ResNet50V2(**kwargs) )
calc_FLOPS( tf.keras.applications.ResNet152V2(**kwargs) )
calc_FLOPS( tf.keras.applications.InceptionV3(**kwargs) )
calc_FLOPS( tf.keras.applications.InceptionResNetV2(**kwargs) )
calc_FLOPS( tf.keras.applications.NASNetMobile(**kwargs) )
calc_FLOPS( tf.keras.applications.NASNetLarge(**kwargs) )
calc_FLOPS( tf.keras.applications.DenseNet121(**kwargs) )
calc_FLOPS( tf.keras.applications.DenseNet201(**kwargs) )
calc_FLOPS( tf.keras.applications.MobileNet(**kwargs) )
calc_FLOPS( tf.keras.applications.MobileNetV2(**kwargs) )
# calc_FLOPS( tf.keras.applications.MobileNetV3Small(input_shape=(224,224,3), **kwargs) )
# calc_FLOPS( tf.keras.applications.MobileNetV3Large(input_shape=(224,224,3), **kwargs))
calc_FLOPS( tf.keras.applications.EfficientNetB0(**kwargs) )
calc_FLOPS( tf.keras.applications.EfficientNetB7(**kwargs) )
kwargs ={ 'weights':None, 'input_shape':(224,224,3)}
print(f"| {'Model Name': ^20} | {'Input Shape': ^11} | {'Params(M)': >9} | {'MFLOPS': ^9} | {'FLOPS/Params': ^12} |")
calc_FLOPS( tf.keras.applications.Xception(**kwargs) )
calc_FLOPS( tf.keras.applications.InceptionV3(**kwargs) )
calc_FLOPS( tf.keras.applications.InceptionResNetV2(**kwargs) )
calc_FLOPS( tf.keras.applications.NASNetLarge(**kwargs) )
calc_FLOPS( tf.keras.applications.EfficientNetB7(**kwargs) )