CNNを二値化する取り組みは制限のきついハードウエアでのCNNの実行が主な目的です
二値化することにより、下記のようなメリットが得られます。
-ハードウエア化を簡単にする
-コンパクトかつ高速化を行う
CNTKでBNNをトレーニングして実行する部分については、下記の記事を参照ください。BNNはMicrosoft Embedded Learning Libraryに付属しているモジュールを使用しています。
https://qiita.com/yasukit/items/a5507e131583d2fca490
コードやモデルは以下のサイトにまとめています。
https://github.com/yasukit1414/binaryconv
実際に、CNTKを使って、CNNとBNNを比較してみました。
BNNの作成と学習
scaled_input = C.element_times(C.constant(0.00390625), feature_var)
z = C.layers.Convolution((3, 3), 32, pad=True, activation=C.relu)(scaled_input)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = BinaryConvolution((3,3), 128, channels=32, pad=True)(z)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = BinaryConvolution((3,3), 128, channels=128, pad=True)(z)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = BinaryConvolution((1,1), num_classes, channels=128, pad=True)(z)
z = C.layers.AveragePooling((z.shape[1], z.shape[2]))(z)
z = C.reshape(z, (num_classes,))
SP = C.parameter(shape=z.shape, init=0.001)
z = C.element_times(z, SP)
下記がBNNの各オペレーションになります。
ElementTimes : 32x32x3 -> 34x34x3 | input padding 0 output padding 1
Convolution (ReLU) : 34x34x3 -> 32x32x32 | input padding 1 output padding 0
MaxPooling : 32x32x32 -> 15x15x32 | input padding 0 output padding 0
BatchNormalization : 15x15x32 -> 17x17x32 | input padding 0 output padding 1
BinaryConvolution : 17x17x32 -> 15x15x128 | input padding 1 output padding 0
Plus : 15x15x128 -> 15x15x128 | input padding 0 output padding 0
PReLU : 15x15x128 -> 15x15x128 | input padding 0 output padding 0
MaxPooling : 15x15x128 -> 7x7x128 | input padding 0 output padding 0
BatchNormalization : 7x7x128 -> 9x9x128 | input padding 0 output padding 1
BinaryConvolution : 9x9x128 -> 7x7x128 | input padding 1 output padding 0
Plus : 7x7x128 -> 7x7x128 | input padding 0 output padding 0
PReLU : 7x7x128 -> 7x7x128 | input padding 0 output padding 0
MaxPooling : 7x7x128 -> 3x3x128 | input padding 0 output padding 0
BatchNormalization : 3x3x128 -> 3x3x128 | input padding 0 output padding 0
BinaryConvolution : 3x3x128 -> 3x3x10 | input padding 0 output padding 0
Plus : 3x3x10 -> 3x3x10 | input padding 0 output padding 0
PReLU : 3x3x10 -> 3x3x10 | input padding 0 output padding 0
AveragePooling : 3x3x10 -> 1x1x10 | input padding 0 output padding 0
ElementTimes : 1x1x10 -> 1x1x10 | input padding 0 output padding 0
Softmax : 1x1x10 -> 1x1x10 | input padding 0 output padding 0
CNNの作成と学習
基本のCNNにBatch Normalizationを入れた形になっています。BNNと構成を合わせるように作成しています。
scaled_input = C.element_times(C.constant(0.00390625), feature_var)
# first layer is ok to be full precision
z = C.layers.Convolution((3, 3), 32, pad=True, activation=C.relu)(scaled_input)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = C.layers.Convolution((3,3), 128, pad=True)(z)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = C.layers.Convolution((3,3), 128, pad=True)(z)
z = C.layers.MaxPooling((3,3), strides=(2,2))(z)
z = C.layers.BatchNormalization(map_rank=1)(z)
z = C.layers.Convolution((1,1), num_classes, pad=True)(z)
z = C.layers.AveragePooling((z.shape[1], z.shape[2]))(z)
z = C.reshape(z, (num_classes,))
SP = C.parameter(shape=z.shape, init=0.001)
z = C.element_times(z, SP)
実際のオペレーションの中身になります。
ElementTimes : 32x32x3 -> 34x34x3 | input padding 0 output padding 1
Convolution (ReLU) : 34x34x3 -> 32x32x32 | input padding 1 output padding 0
MaxPooling : 32x32x32 -> 15x15x32 | input padding 0 output padding 0
BatchNormalization : 15x15x32 -> 17x17x32 | input padding 0 output padding 1
Convolution : 17x17x32 -> 15x15x128 | input padding 1 output padding 0
MaxPooling : 15x15x128 -> 7x7x128 | input padding 0 output padding 0
BatchNormalization : 7x7x128 -> 9x9x128 | input padding 0 output padding 1
Convolution : 9x9x128 -> 7x7x128 | input padding 1 output padding 0
MaxPooling : 7x7x128 -> 3x3x128 | input padding 0 output padding 0
BatchNormalization : 3x3x128 -> 3x3x128 | input padding 0 output padding 0
Convolution : 3x3x128 -> 3x3x10 | input padding 0 output padding 0
AveragePooling : 3x3x10 -> 1x1x10 | input padding 0 output padding 0
ElementTimes : 1x1x10 -> 1x1x10 | input padding 0 output padding 0
Softmax : 1x1x10 -> 1x1x10 | input padding 0 output padding 0
結果
GPUが搭載されていれば、通常のConvlutionの方がGPUの実行効率が高く、計算速度も高い結果になります。またmetrixも5%程度通常のConvotluionの方が有利になります。
Binary Convolutionはやはりハードウエアが制限された環境で、うまくチューニングして使用するか、FPGA等で直接ハードウエア化を行なわなければ、通常のGPU付きの環境ではあまりメリットがないです。
CPUの環境であれば、2-3割程度実行速度が上がります。Batch Normalizationを減らすと認識率は下がりますが実行速度は上がります。最終はそのトレードオフではないかと思います。