Skip to content

Commit b93e367

Browse files
jiafatomWenbing Li
authored andcommitted
Convert Keras Separable Convolution to Onnx (#201)
* Separable Convolution of keras #197
1 parent 92f5406 commit b93e367

File tree

3 files changed

+94
-7
lines changed

3 files changed

+94
-7
lines changed

onnxmltools/convert/keras/operator_converters/Conv.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import numpy
77
import keras
88
from distutils.version import StrictVersion
9-
from keras.layers import Conv1D, Conv2D, Conv3D, Conv2DTranspose, Conv3DTranspose
9+
from keras.layers import Conv1D, Conv2D, Conv3D, Conv2DTranspose, Conv3DTranspose, SeparableConv2D
1010
if StrictVersion(keras.__version__) >= StrictVersion('2.1.5'):
1111
from keras.layers import DepthwiseConv2D
12+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
13+
from keras.layers import SeparableConv1D
1214
from ....proto import onnx_proto
1315
from ...common._apply_operation import apply_identity, apply_transpose
1416
from ...common._registration import register_converter
@@ -32,10 +34,42 @@ def _calc_explicit_padding(input_size, output_shape, output_padding, kernel_shap
3234
return pads
3335

3436

37+
def process_separable_conv_2nd(scope, operator, container, convolution_input_names, n_dims,
38+
weight_perm_axes, parameters, auto_pad):
39+
attrs = {'name': operator.full_name + '1'}
40+
41+
weight_tensor_name = scope.get_unique_variable_name('W')
42+
weight_params = parameters[1].transpose(weight_perm_axes)
43+
container.add_initializer(weight_tensor_name, onnx_proto.TensorProto.FLOAT,
44+
weight_params.shape, weight_params.flatten())
45+
convolution_input_names.append(weight_tensor_name)
46+
47+
if len(parameters) == 3:
48+
bias_tensor_name = scope.get_unique_variable_name('B')
49+
container.add_initializer(bias_tensor_name, onnx_proto.TensorProto.FLOAT,
50+
parameters[2].shape, parameters[2].flatten())
51+
convolution_input_names.append(bias_tensor_name)
52+
53+
all_ones = numpy.ones(n_dims, numpy.int8)
54+
attrs['dilations'] = all_ones
55+
attrs['strides'] = all_ones
56+
attrs['kernel_shape'] = all_ones
57+
attrs['group'] = 1
58+
attrs['auto_pad'] = auto_pad
59+
60+
intermediate_output_name = scope.get_unique_variable_name('convolution_output')
61+
container.add_node('Conv', convolution_input_names,
62+
intermediate_output_name, **attrs)
63+
return intermediate_output_name
64+
65+
3566
def convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, input_perm_axes,
3667
output_perm_axes, weight_perm_axes):
3768
op = operator.raw_operator
3869

70+
is_separable_conv = isinstance(op, SeparableConv2D) or \
71+
(StrictVersion(keras.__version__) >= StrictVersion('2.1.3') and isinstance(op, SeparableConv1D))
72+
3973
channels_first = n_dims > 1 and op.data_format == 'channels_first'
4074

4175
# Unless channels_first is the Keras data format, the inputs and weights in Keras v.s. ONNX
@@ -48,10 +82,15 @@ def convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, in
4882

4983
op_type = 'ConvTranspose' if is_transpose else 'Conv'
5084
convolution_input_names = [adjusted_input_name]
51-
attrs = {'name': operator.full_name}
52-
5385
parameters = op.get_weights()
54-
assert (len(parameters) == 2 if op.use_bias else 1)
86+
87+
if is_separable_conv:
88+
attrs = {'name': operator.full_name + '0'}
89+
assert (len(parameters) == 3 if op.use_bias else 2)
90+
else:
91+
attrs = {'name': operator.full_name}
92+
assert (len(parameters) == 2 if op.use_bias else 1)
93+
5594
weight_params = parameters[0]
5695

5796
input_channels, output_channels = weight_params.shape[-2:]
@@ -68,6 +107,11 @@ def convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, in
68107
new_shape = shape[:2] + (1, shape[2] * shape[3])
69108
weight_params = numpy.reshape(weight_params, new_shape)
70109
weight_params = weight_params.transpose(weight_perm_axes)
110+
elif is_separable_conv:
111+
group = weight_params.shape[-2]
112+
shape = weight_params.shape
113+
new_shape = shape[:-2] + (1, shape[-2] * shape[-1])
114+
weight_params = numpy.reshape(weight_params, new_shape).transpose(weight_perm_axes)
71115
else:
72116
weight_params = weight_params.transpose(weight_perm_axes)
73117
group = 1
@@ -77,7 +121,7 @@ def convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, in
77121
weight_params.shape, weight_params.flatten())
78122
convolution_input_names.append(weight_tensor_name)
79123

80-
if len(parameters) == 2:
124+
if len(parameters) == 2 and not is_separable_conv:
81125
bias_tensor_name = scope.get_unique_variable_name('B')
82126
container.add_initializer(bias_tensor_name, onnx_proto.TensorProto.FLOAT,
83127
parameters[1].shape, parameters[1].flatten())
@@ -114,6 +158,10 @@ def convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, in
114158
container.add_node(op_type, convolution_input_names,
115159
intermediate_output_name, **attrs)
116160

161+
if is_separable_conv:
162+
intermediate_output_name = process_separable_conv_2nd(scope, operator, container, [intermediate_output_name], n_dims,
163+
weight_perm_axes, parameters, attrs['auto_pad'])
164+
117165
# The construction of convolution is done. Now, we create an activation operator to apply the activation specified
118166
# in this Keras layer.
119167
apply_activation_function = _activation_map[op.activation]
@@ -166,10 +214,23 @@ def convert_keras_conv_transpose_3d(scope, operator, container):
166214
convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, input_perm, output_perm, weight_perm)
167215

168216

217+
def convert_keras_separable_conv1d(scope, operator, container):
218+
is_transpose, n_dims, input_perm, output_perm, weight_perm = get_converter_config(1, False)
219+
convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, input_perm, output_perm, weight_perm)
220+
221+
222+
def convert_keras_separable_conv2d(scope, operator, container):
223+
is_transpose, n_dims, input_perm, output_perm, weight_perm = get_converter_config(2, False)
224+
convert_keras_conv_core(scope, operator, container, is_transpose, n_dims, input_perm, output_perm, weight_perm)
225+
226+
169227
register_converter(Conv1D, convert_keras_conv1d)
170228
register_converter(Conv2D, convert_keras_conv2d)
171229
register_converter(Conv3D, convert_keras_conv3d)
172230
register_converter(Conv2DTranspose, convert_keras_conv_transpose_2d)
173231
register_converter(Conv3DTranspose, convert_keras_conv_transpose_3d)
174232
if StrictVersion(keras.__version__) >= StrictVersion('2.1.5'):
175233
register_converter(DepthwiseConv2D, convert_keras_depthwise_conv_2d)
234+
register_converter(SeparableConv2D, convert_keras_separable_conv2d)
235+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
236+
register_converter(SeparableConv1D, convert_keras_separable_conv1d)

onnxmltools/convert/keras/shape_calculators/Conv.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import keras
88
from distutils.version import StrictVersion
99
import numbers
10-
from keras.layers import Conv1D, Conv2D, Conv3D, Conv2DTranspose, Conv3DTranspose, RepeatVector
10+
from keras.layers import Conv1D, Conv2D, Conv3D, Conv2DTranspose, Conv3DTranspose, RepeatVector, SeparableConv2D
1111
if StrictVersion(keras.__version__) >= StrictVersion('2.1.5'):
1212
from keras.layers import DepthwiseConv2D
13+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
14+
from keras.layers import SeparableConv1D
1315
from ...common._registration import register_shape_calculator
1416

1517

@@ -36,5 +38,8 @@ def calculate_keras_depthwise_conv_output_shapes(operator):
3638
register_shape_calculator(Conv3D, calculate_keras_conv_output_shapes)
3739
register_shape_calculator(Conv2DTranspose, calculate_keras_conv_output_shapes)
3840
register_shape_calculator(Conv3DTranspose, calculate_keras_conv_output_shapes)
41+
register_shape_calculator(SeparableConv2D, calculate_keras_conv_output_shapes)
3942
if StrictVersion(keras.__version__) >= StrictVersion('2.1.5'):
4043
register_shape_calculator(DepthwiseConv2D, calculate_keras_depthwise_conv_output_shapes)
44+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
45+
register_shape_calculator(SeparableConv1D, calculate_keras_conv_output_shapes)

tests/end2end/test_single_operator_with_cntk_backend.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
import onnxmltools
1010
import coremltools
1111
import numpy as np
12+
import keras
1213
from distutils.version import StrictVersion
1314
from onnxmltools.proto import onnx
1415
from onnxmltools.utils.tests_dl_helper import evaluate_deep_model, create_tensor,\
1516
find_inference_engine, rt_onnxruntime, rt_caffe2, rt_cntk
1617
from keras.models import Sequential, Model
1718
from keras.layers import Input, Dense, Conv2D, MaxPooling2D, AveragePooling2D, Conv2DTranspose, \
1819
Dot, Embedding, BatchNormalization, GRU, Activation, PReLU, LeakyReLU, ThresholdedReLU, Maximum, \
19-
Add, Average, Multiply, Concatenate, UpSampling2D, Flatten, RepeatVector, Reshape, Dropout
20+
Add, Average, Multiply, Concatenate, UpSampling2D, Flatten, RepeatVector, Reshape, Dropout, SeparableConv2D
21+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
22+
from keras.layers import SeparableConv1D
2023
from keras.initializers import RandomUniform
2124

2225

@@ -422,6 +425,24 @@ def test_recursive_and_shared_model(self):
422425
# coremltools can't convert this kind of model.
423426
self._test_one_to_one_operator_keras(model, [x, 2 * x])
424427

428+
if StrictVersion(keras.__version__) >= StrictVersion('2.1.3'):
429+
@unittest.skipIf(find_inference_engine() == rt_cntk, 'Skip because CNTK is not able to evaluate this model')
430+
def test_separable_convolution(self):
431+
N, C, H, W = 2, 3, 5, 5
432+
x = np.random.rand(N, H, W, C).astype(np.float32, copy=False)
433+
model = Sequential()
434+
model.add(SeparableConv2D(filters=10, kernel_size=(1, 2), strides=(1, 1), padding='valid', input_shape=(H, W, C),
435+
data_format='channels_last', depth_multiplier=4))
436+
model.add(MaxPooling2D((2, 2), strides=(2, 2), data_format='channels_last'))
437+
model.compile(optimizer='sgd', loss='mse')
438+
self._test_one_to_one_operator_keras(model, x)
439+
440+
x = np.random.rand(N, H, C).astype(np.float32, copy=False)
441+
model = Sequential()
442+
model.add(SeparableConv1D(filters=10, kernel_size=2, strides=1, padding='valid', input_shape=(H, C),
443+
data_format='channels_last'))
444+
model.compile(optimizer='sgd', loss='mse')
445+
self._test_one_to_one_operator_keras(model, x)
425446

426447
if __name__ == "__main__":
427448
unittest.main()

0 commit comments

Comments
 (0)