Skip to content

Commit

Permalink
[CoreML] Solve CoreML frontend issue of image scaler and padding so t…
Browse files Browse the repository at this point in the history
…hat Mobilenet mlmodel can work correctly. (apache#3800)
  • Loading branch information
FrozenGene authored and wweic committed Sep 16, 2019
1 parent 9110aec commit 1b3e102
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 51 deletions.
10 changes: 8 additions & 2 deletions python/tvm/relay/frontend/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,15 @@ def new_const(self, value, shape=None, dtype="float32"):
def get_expr(self, name):
return self.exprs[name]

def set_expr(self, name, expr):
def set_expr(self, name, expr, force_override=False):
assert isinstance(expr, _expr.Expr)
if name not in self.exprs:
# if name exists, we should override the value
# otherwise, we can not get like x = func(x) work.
# One example is CoreML preprocess, which will override
# the same name of input.
# However, according to git log, Find keras frontend depends
# on this property, so we add one force_override to control it.
if name not in self.exprs or force_override:
self.exprs[name] = expr

def has_expr(self, name):
Expand Down
129 changes: 85 additions & 44 deletions python/tvm/relay/frontend/coreml.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# pylint: disable=invalid-name, import-self, unused-argument, unused-variable, inconsistent-return-statements
"""CoreML frontend."""
from __future__ import absolute_import as _abs
import math
import numpy as np
import tvm
from .. import analysis
Expand All @@ -26,11 +27,13 @@
from ... import nd as _nd
from ..._ffi import base as _base
from .common import ExprTable
from .common import infer_shape as _infer_shape

__all__ = ['from_coreml']


def _NeuralNetworkImageScaler(op, inexpr, etab):
# TODO: we need to support more colorspace, such as rgb.
# this changes the symbol
biases = np.array([op.blueBias, op.greenBias, op.redBias]).reshape([3, 1, 1])
bias = etab.new_const(biases)
Expand All @@ -47,11 +50,16 @@ def _NeuralNetworkMeanImage(op, inexpr, etab):

def _ConvolutionLayerParams(op, inexpr, etab):
"""Convolution layer params."""
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
tuple([op.outputChannels, op.kernelChannels] + list(op.kernelSize))))
if op.isDeconvolution:
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
tuple([op.kernelChannels, op.outputChannels] + list(op.kernelSize))))
else:
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
tuple([op.outputChannels, op.kernelChannels] + list(op.kernelSize))))
dilation = list(op.dilationFactor)
if not dilation:
dilation = [1, 1]
N, C, H, W = _infer_shape(inexpr)
params = {'channels':op.outputChannels,
'kernel_size':list(op.kernelSize),
'strides':list(op.stride),
Expand All @@ -60,30 +68,31 @@ def _ConvolutionLayerParams(op, inexpr, etab):

if op.WhichOneof('ConvolutionPaddingType') == 'valid':
valid = op.valid
padding = [b.startEdgeSize for b in valid.paddingAmounts.borderAmounts]
padding2 = [b.endEdgeSize for b in valid.paddingAmounts.borderAmounts]
for i, j in zip(padding, padding2):
assert i == j, "Asymmetry padding not supported"
if padding:
params['padding'] = padding
if valid.paddingAmounts.borderAmounts:
assert len(valid.paddingAmounts.borderAmounts) == 2
pad_t = valid.paddingAmounts.borderAmounts[0].startEdgeSize
pad_l = valid.paddingAmounts.borderAmounts[1].startEdgeSize
pad_b = valid.paddingAmounts.borderAmounts[0].endEdgeSize
pad_r = valid.paddingAmounts.borderAmounts[1].endEdgeSize
inexpr = _op.nn.pad(data=inexpr, pad_width=((0, 0),
(0, 0),
(pad_t, pad_b),
(pad_l, pad_r)))
elif op.WhichOneof('ConvolutionPaddingType') == 'same':
assert op.same.asymmetryMode == 0, "Only support BOTTOM_RIGHT_HEAVY mode, " \
"which is used by tf/caffe and so on"
kernel = params['kernel_size']
pad_h = kernel[0] - 1
pad_w = kernel[1] - 1
pad_t = pad_h // 2
pad_l = pad_w // 2
pad_b = pad_h - pad_t
pad_r = pad_w - pad_l
assert pad_t == pad_r and pad_l == pad_b, "Asymmetry padding not supported"
params['padding'] = [pad_t, pad_l]
strides = params['strides']
pad_t, pad_b = get_pad_value(H, kernel[0], strides[0])
pad_l, pad_r = get_pad_value(W, kernel[1], strides[1])
inexpr = _op.nn.pad(data=inexpr, pad_width=((0, 0),
(0, 0),
(pad_t, pad_b),
(pad_l, pad_r)))

else:
raise NotImplementedError("Valid/Same convolution padding implemented")

# consume padding layer
if etab.in_padding:
params['padding'] = [sum(x) for x in zip(params.get('padding', [0, 0]), etab.paddings)]
etab.clear_padding()

if op.isDeconvolution:
ret = _op.nn.conv2d_transpose(data=inexpr, weight=weights, **params)
else:
Expand Down Expand Up @@ -193,11 +202,13 @@ def _PoolingLayerParams(op, inexpr, etab):

if op.WhichOneof('PoolingPaddingType') == 'valid':
valid = op.valid
padding = [b.startEdgeSize for b in valid.paddingAmounts.borderAmounts]
padding2 = [b.endEdgeSize for b in valid.paddingAmounts.borderAmounts]
for i, j in zip(padding, padding2):
assert i == j
params['padding'] = padding
if valid.paddingAmounts.borderAmounts:
assert len(valid.paddingAmounts.borderAmounts) == 2
pad_t = valid.paddingAmounts.borderAmounts[0].startEdgeSize
pad_l = valid.paddingAmounts.borderAmounts[1].startEdgeSize
pad_b = valid.paddingAmounts.borderAmounts[0].endEdgeSize
pad_r = valid.paddingAmounts.borderAmounts[1].endEdgeSize
params['padding'] = [pad_t, pad_l, pad_b, pad_r]
elif op.WhichOneof('PoolingPaddingType') == 'includeLastPixel':
# I don't know if this is correct
valid = op.includeLastPixel
Expand All @@ -209,12 +220,6 @@ def _PoolingLayerParams(op, inexpr, etab):
op_name = op.WhichOneof('PoolingPaddingType')
raise tvm.error.OpAttributeUnImplemented(msg.format(op_name))

# consume padding layer
if etab.in_padding:
params['padding'] = [sum(x) for x in zip(
params.get('padding', [0, 0]), etab.paddings)]
etab.clear_padding()

if op.type == 0:
return _op.nn.max_pool2d(inexpr, **params)
if op.type == 1:
Expand Down Expand Up @@ -276,21 +281,24 @@ def _FlattenLayerParams(op, inexpr, etab):


def _PaddingLayerParams(op, inexpr, etab):
"""Hacking for padding layer params."""
"""Padding layer params."""
if op.WhichOneof('PaddingType') == 'constant':
constant = op.constant
if constant.value != 0:
raise tvm.error.OpAttributeUnImplemented(
'{} is not supported in operator Padding.'.format(constant.value))
padding = [b.startEdgeSize for b in op.paddingAmounts.borderAmounts]
padding2 = [b.endEdgeSize for b in op.paddingAmounts.borderAmounts]
for i, j in zip(padding, padding2):
assert i == j
etab.set_padding(padding)
pad_t = op.paddingAmounts.borderAmounts[0].startEdgeSize
pad_l = op.paddingAmounts.borderAmounts[1].startEdgeSize
pad_b = op.paddingAmounts.borderAmounts[0].endEdgeSize
pad_r = op.paddingAmounts.borderAmounts[1].endEdgeSize
return _op.nn.pad(data=inexpr, pad_width=((0, 0),
(0, 0),
(pad_t, pad_b),
(pad_l, pad_r)))

else:
raise tvm.error.OpNotImplemented(
'Non-constant padding is not supported in frontend CoreML.')
return inexpr


def _PermuteLayerParams(op, inexpr, etab):
Expand Down Expand Up @@ -372,6 +380,32 @@ def _MinLayerParams(op, inexpr, etab):
'MinLayerParams': _MinLayerParams,
}

# SAME padding: https://www.tensorflow.org/api_guides/python/nn
def get_pad_value(data, kernel, stride):
"""Get the pad tuple of value for SAME padding
Parameters
----------
data:
1D input data
kernel:
1D input kernel
stride:
1D input stride
Returns
-------
pad tuple of value
"""

out = int(math.ceil(float(data) / float(stride)))
pad = max(0, (out - 1) * stride + kernel - data)
pad_before = pad // 2
pad_after = pad - pad_before
return pad_before, pad_after


def coreml_op_to_relay(op, inname, outname, etab):
"""Convert coreml layer to a Relay expression and update the expression table.
Expand Down Expand Up @@ -399,9 +433,7 @@ def coreml_op_to_relay(op, inname, outname, etab):
insym = [etab.get_expr(i) for i in inname]
ret = _convert_map[classname](op, insym, etab)
if outname:
etab.set_expr(outname, ret)
if classname != 'PaddingLayerParams':
assert not etab.in_padding, "Previous padding not consumed by conv/pool"
etab.set_expr(outname, ret, force_override=True)


def from_coreml(model, shape=None):
Expand Down Expand Up @@ -442,10 +474,19 @@ def from_coreml(model, shape=None):
for pp in cc.preprocessing:
whichpp = pp.WhichOneof('preprocessor')
ppmethod = getattr(pp, whichpp)
# the NeuralNetworkImageScalar doesn't seem to have a featureName?
if whichpp == 'scaler':
# Be careful we maybe only preprocess one input when we have multi inputs
# which is stored in pp.featureName. See unit testing verify_image_scaler
# in test_forward.py for CoreML.
for i in spec.description.input:
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
# we have multi inputs
if len(spec.description.input) > 1:
assert pp.featureName != ''
if i.name == pp.featureName:
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
else:
assert pp.featureName == ''
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
else:
coreml_op_to_relay(ppmethod, pp.featureName, pp.featureName, etab)

Expand Down
74 changes: 73 additions & 1 deletion tests/python/frontend/coreml/test_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import topi.testing
from tvm import relay
from tvm.relay.testing.config import ctx_list
from topi.testing import conv2d_nchw_python

import coremltools as cm
import model_zoo
Expand Down Expand Up @@ -95,7 +96,10 @@ def run_tvm_graph(coreml_model, target, ctx, input_data, input_name, output_shap
tvm_output_list.append(tvm_output.asnumpy())
return tvm_output_list
else:
tvm_output = m.get_output(0, tvm.nd.empty((output_shape), output_dtype))
if not output_shape:
tvm_output = m.get_output(0)
else:
tvm_output = m.get_output(0, tvm.nd.empty((output_shape), output_dtype))
return tvm_output.asnumpy()

def verify_AddLayerParams(input_dim, alpha=2):
Expand Down Expand Up @@ -330,6 +334,72 @@ def test_forward_min():
verify_min((1, 3, 20, 20))
verify_min((20, 20))

def verify_image_scaler(input_dim, blue_bias=0.0, green_bias=0.0, red_bias=0.0, image_scale=1.0):
dtype = 'float32'
a_np = np.random.uniform(size=input_dim).astype(dtype)
# make sure it is valid image format CHW.
assert len(a_np.shape) == 3 and a_np.shape[0] == 3
b_np = np.zeros(a_np.shape, dtype=dtype)
b_np[0, :, :] = image_scale * a_np[0, :, :] + blue_bias
b_np[1, :, :] = image_scale * a_np[1, :, :] + green_bias
b_np[2, :, :] = image_scale * a_np[2, :, :] + red_bias
b_np = np.add(a_np, b_np)
inputs = [('input1', datatypes.Array(*input_dim)),
('input2', datatypes.Array(*input_dim))]
output = [('output', datatypes.Array(*b_np.shape))]
builder = NeuralNetworkBuilder(inputs, output)
builder.set_pre_processing_parameters(image_input_names=['input1'],
is_bgr=True,
blue_bias=blue_bias,
green_bias=green_bias,
red_bias=red_bias,
image_scale=image_scale)
# add one add layer to make CoreML model format valid
# add layer has been tested before.
builder.add_elementwise(name='add', input_names=['input1', 'input2'],
output_name='output', alpha=0, mode='ADD')
model = cm.models.MLModel(builder.spec)
for target, ctx in ctx_list():
out = run_tvm_graph(model, target, ctx, [a_np, a_np],
['input1', 'input2'], b_np.shape, dtype)
tvm.testing.assert_allclose(out, b_np, rtol=1e-5)

def test_forward_image_scaler():
verify_image_scaler((3, 224, 224), image_scale=0.17)
verify_image_scaler((3, 224, 224),
blue_bias=-1.7669800519943237,
green_bias=-1.985260009765625,
red_bias=-2.102560043334961,
image_scale=0.379)

def verify_convolution(input_dim, filter, padding):
dtype = 'float32'
N, C, H, W = input_dim
OC, _, KH, KW = filter
a_np = np.random.uniform(size=input_dim).astype(dtype)
w_np = np.random.uniform(size=(OC, C, KH, KW)).astype(dtype)
w_np_cm = np.transpose(w_np, axes=(2, 3, 1, 0))
b_np = conv2d_nchw_python(a_np, w_np, [1, 1], padding)
inputs = [('input1', datatypes.Array(C, H, W))]
output = [('output', datatypes.Array(*b_np.shape))]
builder = NeuralNetworkBuilder(inputs, output)
builder.add_convolution(name='conv', kernel_channels=3, output_channels=OC,
height=KH, width=KW, stride_height=1, stride_width=1,
border_mode=padding.lower(), groups=1,
W=w_np_cm, b=None, has_bias=False,
is_deconv=False,
input_name='input1',
output_name='output')
model = cm.models.MLModel(builder.spec)
for target, ctx in ctx_list():
out = run_tvm_graph(model, target, ctx, [a_np],
['input1'], output_shape=None)
tvm.testing.assert_allclose(out, b_np, rtol=1e-5)

def test_forward_convolution():
verify_convolution((1, 3, 224, 224), filter=(32, 3, 3, 3), padding='VALID')
verify_convolution((1, 3, 224, 224), filter=(32, 3, 3, 3), padding='SAME')

if __name__ == '__main__':
test_forward_AddLayerParams()
test_forward_ConcatLayerParams()
Expand All @@ -342,3 +412,5 @@ def test_forward_min():
test_forward_min()
test_mobilenet_checkonly()
test_resnet50_checkonly()
test_forward_image_scaler()
test_forward_convolution()
12 changes: 8 additions & 4 deletions tutorials/frontend/from_coreml.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
Compile CoreML Models
=====================
**Author**: `Joshua Z. Zhang <https://zhreshold.github.io/>`_, \
`Kazutaka Morita <https://github.com/kazum>`_
`Kazutaka Morita <https://github.com/kazum>`_, \
`Zhao Wu <https://github.com/FrozenGene>`_
This article is an introductory tutorial to deploy CoreML models with Relay.
Expand Down Expand Up @@ -58,13 +59,15 @@
img_url = 'https://github.com/dmlc/mxnet.js/blob/master/data/cat.png?raw=true'
img_path = download_testdata(img_url, 'cat.png', module='data')
img = Image.open(img_path).resize((224, 224))
x = np.transpose(img, (2, 0, 1))[np.newaxis, :]
# Mobilenet.mlmodel's input is BGR format
img_bgr = np.array(img)[:,:,::-1]
x = np.transpose(img_bgr, (2, 0, 1))[np.newaxis, :]

######################################################################
# Compile the model on Relay
# ---------------------------
# We should be familiar with the process right now.
target = 'cuda'
target = 'llvm'
shape_dict = {'image': x.shape}

# Parse CoreML model and convert into Relay computation graph
Expand All @@ -80,7 +83,7 @@
# -------------------
# The process is no different from other example
from tvm.contrib import graph_runtime
ctx = tvm.gpu(0)
ctx = tvm.cpu(0)
dtype = 'float32'
m = graph_runtime.create(graph, lib, ctx)
# set inputs
Expand All @@ -104,4 +107,5 @@
synset_path = download_testdata(synset_url, synset_name, module='data')
with open(synset_path) as f:
synset = eval(f.read())
# You should see the following result: Top-1 id 282 class name tiger cat
print('Top-1 id', top1, 'class name', synset[top1])

0 comments on commit 1b3e102

Please sign in to comment.