This is an automated email from the ASF dual-hosted git repository. haoj pushed a commit to branch numpy in repository https://gitbox.apache.org/repos/asf/incubator-mxnet.git
commit 11cb34ae2961890de2e53f6454543b57f25a1ef3 Author: reminisce <wujun....@gmail.com> AuthorDate: Tue Jun 4 22:55:10 2019 -0700 [WIP][numpy] Fix for D2L Chapters 2/3/4 (#15139) * Fix * Fix linear regression gluon * More fix * Fix pylint * Fix for chapter 4 * Add np.add mul div mod pow sub and shuffle * Fix model selection, underfitting, overfitting * Fix weight decay * Fix dropout * Fix * Fix chapter 4 --- python/mxnet/gluon/data/dataloader.py | 20 +- python/mxnet/gluon/data/vision/transforms.py | 6 +- python/mxnet/gluon/loss.py | 26 +- python/mxnet/gluon/nn/activations.py | 5 +- python/mxnet/gluon/nn/basic_layers.py | 13 +- python/mxnet/gluon/utils.py | 50 ++-- python/mxnet/ndarray/numpy/_op.py | 199 ++++++++++++++- python/mxnet/ndarray/register.py | 8 +- python/mxnet/numpy/multiarray.py | 326 ++++++++++++++++++------- python/mxnet/numpy_extension/__init__.py | 5 +- python/mxnet/optimizer/optimizer.py | 10 +- python/mxnet/symbol/numpy/_symbol.py | 194 ++++++++------- python/mxnet/symbol/register.py | 8 +- python/mxnet/symbol/symbol.py | 4 + python/mxnet/util.py | 38 ++- src/operator/nn/activation.cc | 1 + src/operator/nn/batch_norm.cc | 1 + src/operator/nn/convolution.cc | 1 + src/operator/nn/fully_connected.cc | 1 + src/operator/nn/pooling.cc | 3 +- src/operator/random/shuffle_op.cc | 1 + src/operator/tensor/elemwise_unary_op_basic.cc | 1 + src/operator/tensor/matrix_op.cc | 1 + tests/python/unittest/test_numpy_gluon.py | 6 +- 24 files changed, 696 insertions(+), 232 deletions(-) diff --git a/python/mxnet/gluon/data/dataloader.py b/python/mxnet/gluon/data/dataloader.py index 65fd7d8..7e8110c 100644 --- a/python/mxnet/gluon/data/dataloader.py +++ b/python/mxnet/gluon/data/dataloader.py @@ -18,6 +18,7 @@ # coding: utf-8 # pylint: disable=ungrouped-imports """Dataset generator.""" +from __future__ import absolute_import __all__ = ['DataLoader'] import pickle @@ -37,6 +38,8 @@ except ImportError: from . import sampler as _sampler from ... import nd, context +from ...util import is_np_array +from ... import numpy as _mx_np #pylint: disable=reimported if sys.platform == 'darwin' or sys.platform == 'win32': def rebuild_ndarray(*args): @@ -127,13 +130,14 @@ class SimpleQueue(multiprocessing.queues.SimpleQueue): def default_batchify_fn(data): """Collate data into batch.""" if isinstance(data[0], nd.NDArray): - return nd.stack(*data) + return _mx_np.stack(data) if is_np_array() else nd.stack(*data) elif isinstance(data[0], tuple): data = zip(*data) return [default_batchify_fn(i) for i in data] else: data = np.asarray(data) - return nd.array(data, dtype=data.dtype) + array_fn = _mx_np.array if is_np_array() else nd.array + return array_fn(data, dtype=data.dtype) def default_mp_batchify_fn(data): @@ -141,20 +145,26 @@ def default_mp_batchify_fn(data): if isinstance(data[0], nd.NDArray): out = nd.empty((len(data),) + data[0].shape, dtype=data[0].dtype, ctx=context.Context('cpu_shared', 0)) - return nd.stack(*data, out=out) + if is_np_array(): + out = out.as_np_ndarray() + return _mx_np.stack(data, out=out) + else: + return nd.stack(*data, out=out) elif isinstance(data[0], tuple): data = zip(*data) return [default_mp_batchify_fn(i) for i in data] else: data = np.asarray(data) - return nd.array(data, dtype=data.dtype, + array_fn = _mx_np.array if is_np_array() else nd.array + return array_fn(data, dtype=data.dtype, ctx=context.Context('cpu_shared', 0)) def _as_in_context(data, ctx): """Move data into new context.""" if isinstance(data, nd.NDArray): - return data.as_in_context(ctx) + out = data.as_in_context(ctx) + return out.as_np_ndarray() if is_np_array() else out elif isinstance(data, (list, tuple)): return [_as_in_context(d, ctx) for d in data] return data diff --git a/python/mxnet/gluon/data/vision/transforms.py b/python/mxnet/gluon/data/vision/transforms.py index 955f2b2..0e90c17 100644 --- a/python/mxnet/gluon/data/vision/transforms.py +++ b/python/mxnet/gluon/data/vision/transforms.py @@ -23,6 +23,7 @@ from ...block import Block, HybridBlock from ...nn import Sequential, HybridSequential from .... import image from ....base import numeric_types +from ....util import is_np_array class Compose(Sequential): @@ -134,7 +135,10 @@ class ToTensor(HybridBlock): super(ToTensor, self).__init__() def hybrid_forward(self, F, x): - return F.image.to_tensor(x) + if is_np_array(): + x = x.as_classic_ndarray() + out = F.image.to_tensor(x) + return out.as_np_ndarray() if is_np_array() else out class Normalize(HybridBlock): diff --git a/python/mxnet/gluon/loss.py b/python/mxnet/gluon/loss.py index e6d4c5b..8cf41a2 100644 --- a/python/mxnet/gluon/loss.py +++ b/python/mxnet/gluon/loss.py @@ -29,6 +29,7 @@ import numpy as np from .. import ndarray from ..base import numeric_types from .block import HybridBlock +from .utils import _to_classic_arrays, _to_np_arrays def _apply_weighting(F, loss, weight=None, sample_weight=None): @@ -135,10 +136,14 @@ class L2Loss(Loss): super(L2Loss, self).__init__(weight, batch_axis, **kwargs) def hybrid_forward(self, F, pred, label, sample_weight=None): + # TODO(junwu): This is a temp solution to reuse legacy ops for np.ndarray. + # We should rewrite this with np/npx ops. + pred, label, sample_weight = _to_classic_arrays(pred, label, sample_weight) label = _reshape_like(F, label, pred) loss = F.square(label - pred) loss = _apply_weighting(F, loss, self._weight / 2, sample_weight) - return F.mean(loss, axis=self._batch_axis, exclude=True) + out = F.mean(loss, axis=self._batch_axis, exclude=True) + return _to_np_arrays(out) class L1Loss(Loss): @@ -174,10 +179,14 @@ class L1Loss(Loss): super(L1Loss, self).__init__(weight, batch_axis, **kwargs) def hybrid_forward(self, F, pred, label, sample_weight=None): + # TODO(junwu): This is a temp solution to reuse legacy ops for np.ndarray. + # We should rewrite this with np/npx ops. + pred, label, sample_weight = _to_classic_arrays(pred, label, sample_weight) label = _reshape_like(F, label, pred) loss = F.abs(label - pred) loss = _apply_weighting(F, loss, self._weight, sample_weight) - return F.mean(loss, axis=self._batch_axis, exclude=True) + out = F.mean(loss, axis=self._batch_axis, exclude=True) + return _to_np_arrays(out) class SigmoidBinaryCrossEntropyLoss(Loss): @@ -243,6 +252,10 @@ class SigmoidBinaryCrossEntropyLoss(Loss): self._from_sigmoid = from_sigmoid def hybrid_forward(self, F, pred, label, sample_weight=None, pos_weight=None): + # TODO(junwu): This is a temp solution to reuse legacy ops for np.ndarray. + # We should rewrite this with np/npx ops. + pred, label, sample_weight, pos_weight =\ + _to_classic_arrays(pred, label, sample_weight, pos_weight) label = _reshape_like(F, label, pred) if not self._from_sigmoid: if pos_weight is None: @@ -264,7 +277,8 @@ class SigmoidBinaryCrossEntropyLoss(Loss): loss = -(F.broadcast_mul(F.log(pred + eps) * label, pos_weight) + F.log(1. - pred + eps) * (1. - label)) loss = _apply_weighting(F, loss, self._weight, sample_weight) - return F.mean(loss, axis=self._batch_axis, exclude=True) + out = F.mean(loss, axis=self._batch_axis, exclude=True) + return _to_np_arrays(out) SigmoidBCELoss = SigmoidBinaryCrossEntropyLoss @@ -341,6 +355,9 @@ class SoftmaxCrossEntropyLoss(Loss): self._from_logits = from_logits def hybrid_forward(self, F, pred, label, sample_weight=None): + # TODO(junwu): This is a temp solution to reuse legacy ops for np.ndarray. + # We should rewrite this with np/npx ops. + pred, label = _to_classic_arrays(pred, label) if not self._from_logits: pred = F.log_softmax(pred, self._axis) if self._sparse_label: @@ -349,7 +366,8 @@ class SoftmaxCrossEntropyLoss(Loss): label = _reshape_like(F, label, pred) loss = -F.sum(pred * label, axis=self._axis, keepdims=True) loss = _apply_weighting(F, loss, self._weight, sample_weight) - return F.mean(loss, axis=self._batch_axis, exclude=True) + out = F.mean(loss, axis=self._batch_axis, exclude=True) + return _to_np_arrays(out) SoftmaxCELoss = SoftmaxCrossEntropyLoss diff --git a/python/mxnet/gluon/nn/activations.py b/python/mxnet/gluon/nn/activations.py index 8c51b0a..04a8227 100644 --- a/python/mxnet/gluon/nn/activations.py +++ b/python/mxnet/gluon/nn/activations.py @@ -22,6 +22,7 @@ __all__ = ['Activation', 'LeakyReLU', 'PReLU', 'ELU', 'SELU', 'Swish', 'GELU'] from ... import initializer from ..block import HybridBlock +from ..utils import _to_classic_arrays, _to_np_arrays class Activation(HybridBlock): @@ -48,7 +49,9 @@ class Activation(HybridBlock): return self._act_type def hybrid_forward(self, F, x): - return F.Activation(x, act_type=self._act_type, name='fwd') + x = _to_classic_arrays(x) + out = F.Activation(x, act_type=self._act_type, name='fwd') + return _to_np_arrays(out) def __repr__(self): s = '{name}({_act_type})' diff --git a/python/mxnet/gluon/nn/basic_layers.py b/python/mxnet/gluon/nn/basic_layers.py index 3d6976c..654e3ef 100644 --- a/python/mxnet/gluon/nn/basic_layers.py +++ b/python/mxnet/gluon/nn/basic_layers.py @@ -25,7 +25,7 @@ import numpy as np from .activations import Activation from ..block import Block, HybridBlock -from ..utils import _indent +from ..utils import _indent, _to_classic_arrays, _to_np_arrays from ... import nd, sym @@ -217,11 +217,14 @@ class Dense(HybridBlock): self.act = None def hybrid_forward(self, F, x, weight, bias=None): + # TODO(junwu): This is a temp solution to reuse legacy ops for np.ndarray. + # We should rewrite this with np/npx ops. + x, weight, bias = _to_classic_arrays(x, weight, bias) act = F.FullyConnected(x, weight, bias, no_bias=bias is None, num_hidden=self._units, flatten=self._flatten, name='fwd') if self.act is not None: act = self.act(act) - return act + return _to_np_arrays(act) def __repr__(self): s = '{name}({layout}, {act})' @@ -262,10 +265,12 @@ class Dropout(HybridBlock): self._axes = axes def hybrid_forward(self, F, x): + x = _to_classic_arrays(x) if self._rate > 0: - return F.Dropout(x, p=self._rate, axes=self._axes, name='fwd', cudnn_off=False) + out = F.Dropout(x, p=self._rate, axes=self._axes, name='fwd', cudnn_off=False) else: - return F.identity(x) + out = F.identity(x) + return _to_np_arrays(out) def __repr__(self): s = '{name}(p = {_rate}, axes={_axes})' diff --git a/python/mxnet/gluon/utils.py b/python/mxnet/gluon/utils.py index 4ef4905..19f5c1a 100644 --- a/python/mxnet/gluon/utils.py +++ b/python/mxnet/gluon/utils.py @@ -38,7 +38,7 @@ except ImportError: import numpy as np from .. import ndarray -from ..util import is_np_shape +from ..util import is_np_shape, is_np_array def split_data(data, num_slice, batch_axis=0, even_split=True): @@ -112,12 +112,18 @@ def split_and_load(data, ctx_list, batch_axis=0, even_split=True): list of NDArray Each corresponds to a context in `ctx_list`. """ + # TODO(junwu): temp solution for supporting np.ndarray + # rewrite this using np ops if not isinstance(data, ndarray.NDArray): data = ndarray.array(data, ctx=ctx_list[0]) if len(ctx_list) == 1: + if is_np_array(): + data = data.as_np_ndarray() return [data.as_in_context(ctx_list[0])] slices = split_data(data, len(ctx_list), batch_axis, even_split) + if is_np_array(): + slices = [i.as_np_ndarray() for i in slices] return [i.as_in_context(ctx) for i, ctx in zip(slices, ctx_list)] @@ -415,6 +421,7 @@ class HookHandle(object): def __exit__(self, ptype, value, trace): self.detach() + def shape_is_known(shape): """Check whether a shape is completely known with or without np semantics. @@ -432,6 +439,7 @@ def shape_is_known(shape): "received {}".format(unknown_dim_size, dim_size) return True + def _check_same_symbol_type(symbols): """Check whether all the symbols in the list are of the same type. Raise type error if the types are different. Return the class of @@ -458,23 +466,33 @@ def _check_same_symbol_type(symbols): def _check_all_np_ndarrays(out): """Check if ndarrays in out are all np.ndarray""" from ..numpy import ndarray as np_ndarray + from ..symbol.numpy import _Symbol as np_symbol assert isinstance(out, (list, tuple)) for array in out: - if not isinstance(array, np_ndarray): - raise TypeError('Expected np.ndarray type in output, while received type ' + if not isinstance(array, (np_ndarray, np_symbol)): + raise TypeError('Expected np.ndarray or np._Symbol type in output, while received type ' '{}'.format(str(type(array)))) -def shape_is_known(shape): - """Check whether a shape is completely known w/ or w/o np semantics.""" - if shape is None: - return False - unknown_dim_size = -1 if is_np_shape() else 0 - if len(shape) == 0: - return unknown_dim_size == -1 - for dim_size in shape: - if dim_size == unknown_dim_size: - return False - assert dim_size > unknown_dim_size, "shape dimension size cannot be less than {}, while " \ - "received {}".format(unknown_dim_size, dim_size) - return True +def _to_classic_arrays(*args): + """Convert arrays to classic arrays. This is used in a Gluon layer for converting + inputs of np arrays to classic arrays so that the layer built with legacy ops can still + be used in np_array semantics.""" + num_inputs = len(args) + assert num_inputs != 0 + if not is_np_array(): + return args[0] if num_inputs == 1 else args + in_arrs = [arr if arr is None else arr.as_classic_ndarray() for arr in args] + return in_arrs[0] if num_inputs == 1 else in_arrs + + +def _to_np_arrays(*args): + """Convert arrays to np arrays. This is used in a Gluon layer for converting + outputs of classic arrays to np arrays so that the layer built with legacy ops can still + be used in np_array semantics.""" + num_outputs = len(args) + assert num_outputs != 0 + if not is_np_array(): + return args[0] if num_outputs == 1 else args + out = [arr.as_np_ndarray() for arr in args] + return out[0] if num_outputs == 1 else out diff --git a/python/mxnet/ndarray/numpy/_op.py b/python/mxnet/ndarray/numpy/_op.py index 6c83e1f..f3f4d74 100644 --- a/python/mxnet/ndarray/numpy/_op.py +++ b/python/mxnet/ndarray/numpy/_op.py @@ -24,7 +24,9 @@ from ...util import _sanity_check_params, set_module from ...context import current_context from . import _internal as _npi -__all__ = ['zeros', 'ones', 'maximum', 'minimum', 'stack', 'concatenate', 'arange', 'argmax'] +__all__ = ['zeros', 'ones', 'maximum', 'minimum', 'stack', 'arange', 'argmax', + 'add', 'subtract', 'multiply', 'divide', 'mod', 'power', 'concatenate', + 'clip'] @set_module('mxnet.ndarray.numpy') @@ -51,7 +53,7 @@ def zeros(shape, dtype=_np.float32, **kwargs): Array of zeros with the given shape, dtype, and ctx. """ _sanity_check_params('zeros', ['order'], kwargs) - ctx = kwargs.get('ctx', current_context()) + ctx = kwargs.pop('ctx', current_context()) if ctx is None: ctx = current_context() dtype = _np.float32 if dtype is None else dtype @@ -82,7 +84,7 @@ def ones(shape, dtype=None, **kwargs): Array of zeros with the given shape, dtype, and ctx. """ _sanity_check_params('zeros', ['order'], kwargs) - ctx = kwargs.get('ctx', current_context()) + ctx = kwargs.pop('ctx', current_context()) if ctx is None: ctx = current_context() dtype = _np.float32 if dtype is None else dtype @@ -302,3 +304,194 @@ def concatenate(seq, axis=0, out=None): The concatenated array. """ return _npi.concatenate(*seq, dim=axis, out=out) + + +@set_module('mxnet.ndarray.numpy') +def add(x1, x2, out=None): + """Add arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be added. If x1.shape != x2.shape, they must be broadcastable to + a common shape (which may be the shape of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + add : ndarray or scalar + The sum of x1 and x2, element-wise. This is a scalar if both x1 and x2 are scalars. + """ + return _ufunc_helper(x1, x2, _npi.add, _np.add, _npi.add_scalar, None, out) + + +@set_module('mxnet.ndarray.numpy') +def subtract(x1, x2, out=None): + """Subtract arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be subtracted from each other. If x1.shape != x2.shape, + they must be broadcastable to a common shape (which may be the shape + of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + subtract : ndarray or scalar + The difference of x1 and x2, element-wise. This is a scalar if both x1 and x2 are scalars. + """ + return _ufunc_helper(x1, x2, _npi.subtract, _np.subtract, _npi.subtract_scalar, + _npi.rsubtract_scalar, out) + + +@set_module('mxnet.ndarray.numpy') +def multiply(x1, x2, out=None): + """Multiply arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be multiplied. If x1.shape != x2.shape, they must be broadcastable to + a common shape (which may be the shape of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + The multiplication of x1 and x2, element-wise. This is a scalar if both x1 and x2 + are scalars. + """ + return _ufunc_helper(x1, x2, _npi.multiply, _np.multiply, _npi.multiply_scalar, None, out) + + +@set_module('mxnet.ndarray.numpy') +def divide(x1, x2, out=None): + """Returns a true division of the inputs, element-wise. + + Parameters + ---------- + x1 : ndarray or scalar + Dividend array. + + x2 : ndarray or scalar + Divisor array. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + This is a scalar if both x1 and x2 are scalars. + """ + return _ufunc_helper(x1, x2, _npi.true_divide, _np.divide, _npi.true_divide_scalar, + _npi.rtrue_divide_scalar, out) + + +@set_module('mxnet.ndarray.numpy') +def mod(x1, x2, out=None): + """Return element-wise remainder of division. + + Parameters + ---------- + x1 : ndarray or scalar + Dividend array. + + x2 : ndarray or scalar + Divisor array. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + This is a scalar if both x1 and x2 are scalars. + """ + return _ufunc_helper(x1, x2, _npi.mod, _np.mod, _npi.mod_scalar, _npi.rmod_scalar, out) + + +@set_module('mxnet.ndarray.numpy') +def power(x1, x2, out=None): + """First array elements raised to powers from second array, element-wise. + + Parameters + ---------- + x1 : ndarray or scalar + The bases. + + x2 : ndarray or scalar + The exponent. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + The bases in x1 raised to the exponents in x2. + This is a scalar if both x1 and x2 are scalars. + """ + return _ufunc_helper(x1, x2, _npi.power, _np.power, _npi.power_scalar, _npi.rpower_scalar, out) + + +@set_module('mxnet.ndarray.numpy') +def clip(a, a_min, a_max, out=None): + """Clip (limit) the values in an array. + + Given an interval, values outside the interval are clipped to + the interval edges. For example, if an interval of ``[0, 1]`` + is specified, values smaller than 0 become 0, and values larger + than 1 become 1. + + Parameters + ---------- + a : ndarray + Array containing elements to clip. + a_min : scalar or `None` + Minimum value. If `None`, clipping is not performed on lower + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + a_max : scalar or `None` + Maximum value. If `None`, clipping is not performed on upper + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + out : ndarray, optional + The results will be placed in this array. It may be the input + array for in-place clipping. `out` must be of the right shape + to hold the output. + + Returns + ------- + clipped_array : ndarray + An array with the elements of `a`, but where values + < `a_min` are replaced with `a_min`, and those > `a_max` + with `a_max`. + """ + if a_min is None and a_max is None: + raise ValueError('array_clip: must set either max or min') + if a_min is None: + a_min = float('-inf') + if a_max is None: + a_max = float('inf') + return _npi.clip(a, a_min, a_max, out=out) diff --git a/python/mxnet/ndarray/register.py b/python/mxnet/ndarray/register.py index c2225bb..cde1145 100644 --- a/python/mxnet/ndarray/register.py +++ b/python/mxnet/ndarray/register.py @@ -221,7 +221,13 @@ def %s(%s):"""%(func_name, ', '.join(signature))) vals.append(%s)"""%(name, name, name)) # dtype if dtype_name is not None: - code.append(""" + if is_np_op: + code.append(""" + if %s is not _Null and %s is not None: + keys.append('%s') + vals.append(_np.dtype(%s).name)"""%(dtype_name, dtype_name, dtype_name, dtype_name)) + else: + code.append(""" if %s is not _Null: keys.append('%s') vals.append(_np.dtype(%s).name)"""%(dtype_name, dtype_name, dtype_name)) diff --git a/python/mxnet/numpy/multiarray.py b/python/mxnet/numpy/multiarray.py index 6b3dcde..2f0cdbc 100644 --- a/python/mxnet/numpy/multiarray.py +++ b/python/mxnet/numpy/multiarray.py @@ -37,8 +37,9 @@ from ..context import current_context from ..ndarray import numpy as _mx_nd_np from ..ndarray.numpy import _internal as _npi -__all__ = ['ndarray', 'empty', 'array', 'zeros', 'ones', 'maximum', 'minimum', 'stack', - 'concatenate', 'arange', 'argmax'] +__all__ = ['ndarray', 'empty', 'array', 'zeros', 'ones', 'maximum', 'minimum', 'stack', 'arange', + 'argmax', 'add', 'subtract', 'multiply', 'divide', 'mod', 'power', 'concatenate', + 'clip'] # This function is copied from ndarray.py since pylint @@ -152,67 +153,40 @@ class ndarray(NDArray): def __add__(self, other): """x.__add__(y) <=> x + y""" - if isinstance(other, ndarray): - return _npi.add(self, other) - elif isinstance(other, numeric_types): - return _npi.add_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return add(self, other) def __iadd__(self, other): """x.__iadd__(y) <=> x += y""" if not self.writable: raise ValueError('trying to add to a readonly ndarray') - if isinstance(other, ndarray): - return _npi.add(self, other, out=self) - elif isinstance(other, numeric_types): - return _npi.add_scalar(self, float(other), out=self) - else: - raise TypeError('type {} is not supported'.format(str(type(other)))) + return add(self, other, out=self) def __sub__(self, other): """x.__sub__(y) <=> x - y""" - if isinstance(other, ndarray): - return _npi.subtract(self, other) - elif isinstance(other, numeric_types): - return _npi.subtract_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return subtract(self, other) def __isub__(self, other): """x.__isub__(y) <=> x -= y""" if not self.writable: raise ValueError('trying to subtract from a readonly ndarray') - if isinstance(other, ndarray): - return _npi.subtract(self, other, out=self) - elif isinstance(other, numeric_types): - return _npi.subtract_scalar(self, float(other), out=self) - else: - raise TypeError('type {} is not supported'.format(str(type(other)))) + return subtract(self, other, out=self) def __rsub__(self, other): """x.__rsub__(y) <=> y - x""" - if isinstance(other, ndarray): - return _npi.subtract(other, self) - elif isinstance(other, numeric_types): - return _npi.rsubtract_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return subtract(other, self) def __mul__(self, other): """x.__mul__(y) <=> x * y""" - if isinstance(other, ndarray): - return _npi.multiply(self, other) - elif isinstance(other, numeric_types): - return _npi.multiply_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return multiply(self, other) def __neg__(self): return self.__mul__(-1.0) def __imul__(self, other): - raise NotImplementedError + """x.__imul__(y) <=> x *= y""" + if not self.writable: + raise ValueError('trying to add to a readonly ndarray') + return multiply(self, other, out=self) def __rmul__(self, other): """x.__rmul__(y) <=> y * x""" @@ -233,67 +207,42 @@ class ndarray(NDArray): ' been encountered.') def __idiv__(self, other): - raise NotImplementedError + raise AttributeError('ndarray.__idiv__ is replaced by __irtruediv__. If you are using' + ' Python2, please use the statement from __future__ import division' + ' to change the / operator to mean true division throughout the' + ' module. If you are using Python3, this error should not have' + ' been encountered.') def __truediv__(self, other): """x.__truediv__(y) <=> x / y""" - if isinstance(other, ndarray): - return _npi.true_divide(self, other) - elif isinstance(other, numeric_types): - return _npi.true_divide_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as divisor".format(str(type(other)))) + return divide(self, other) def __rtruediv__(self, other): """x.__rtruediv__(y) <=> y / x""" - if isinstance(other, ndarray): - return _npi.true_divide(other, self) - elif isinstance(other, numeric_types): - return _npi.rtrue_divide_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as dividend".format(str(type(other)))) + return divide(other, self) def __itruediv__(self, other): - raise NotImplementedError + return divide(self, other, out=self) def __mod__(self, other): """x.__mod__(y) <=> x % y""" - if isinstance(other, ndarray): - return _npi.mod(self, other) - elif isinstance(other, numeric_types): - return _npi.mod_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return mod(self, other) def __rmod__(self, other): """x.__rmod__(y) <=> y % x""" - if isinstance(other, ndarray): - return _npi.mod(other, self) - elif isinstance(other, numeric_types): - return _npi.rmod_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return mod(other, self) def __imod__(self, other): - raise NotImplementedError + """x.__imod__(y) <=> x %= y""" + return mod(self, other, out=self) def __pow__(self, other): """x.__pow__(y) <=> x ** y""" - if isinstance(other, ndarray): - return _npi.power(self, other) - elif isinstance(other, numeric_types): - return _npi.power_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return power(self, other) def __rpow__(self, other): """x.__rpow__(y) <=> y ** x""" - if isinstance(other, ndarray): - return _npi.power(other, self) - elif isinstance(other, numeric_types): - return _npi.rpower_scalar(self, float(other)) - else: - raise TypeError("ndarray does not support type {} as operand".format(str(type(other)))) + return power(other, self) def __eq__(self, other): """x.__eq__(y) <=> x == y""" @@ -370,6 +319,18 @@ class ndarray(NDArray): else: raise ValueError("The truth value of an ndarray with multiple elements is ambiguous.") + def __float__(self): + num_elements = self.size + if num_elements != 1: + raise TypeError('only size-1 arrays can be converted to Python scalars') + return float(self.item()) + + def __int__(self): + num_elements = self.size + if num_elements != 1: + raise TypeError('only size-1 arrays can be converted to Python scalars') + return int(self.item()) + def __len__(self): """Number of elements along the first axis.""" return self.shape[0] @@ -557,7 +518,10 @@ class ndarray(NDArray): return self._as_classic_ndarray().copyto(other).as_np_ndarray() def asscalar(self): - raise AttributeError('mxnet.numpy.ndarray object has no attribute as_scalar') + raise AttributeError('mxnet.numpy.ndarray object has no attribute asscalar') + + def argmax(self, axis=None, out=None): # pylint: disable=arguments-differ + return _mx_nd_np.argmax(self, axis, out) def as_in_context(self, context): return super(ndarray, self).as_in_context(context).as_np_ndarray() @@ -722,14 +686,6 @@ class ndarray(NDArray): """ raise NotImplementedError - def argmax(self, *args, **kwargs): - """Convenience fluent method for :py:func:`argmax`. - - The arguments are the same as for :py:func:`argmax`, with - this array as data. - """ - raise NotImplementedError - def argmax_channel(self, *args, **kwargs): """Convenience fluent method for :py:func:`argmax_channel`. @@ -746,13 +702,11 @@ class ndarray(NDArray): """ raise NotImplementedError - def clip(self, *args, **kwargs): - """Convenience fluent method for :py:func:`clip`. - - The arguments are the same as for :py:func:`clip`, with - this array as data. + def clip(self, min=None, max=None, out=None): # pylint: disable=arguments-differ + """Return an array whose values are limited to [min, max]. + One of max or min must be given. """ - raise NotImplementedError + return clip(self, min, max, out=out) def abs(self, *args, **kwargs): """Convenience fluent method for :py:func:`abs`. @@ -882,13 +836,13 @@ class ndarray(NDArray): """ raise AttributeError('mxnet.numpy.ndarray object has no attribute nanprod') - def mean(self, *args, **kwargs): + def mean(self, axis=None, dtype=None, out=None, keepdims=False): # pylint: disable=arguments-differ """Convenience fluent method for :py:func:`mean`. The arguments are the same as for :py:func:`mean`, with this array as data. """ - raise NotImplementedError + return _mx_nd_np.mean(self, axis=axis, dtype=dtype, keepdims=keepdims, out=out) def max(self, *args, **kwargs): """Convenience fluent method for :py:func:`max`. @@ -1511,3 +1465,185 @@ def concatenate(seq, axis=0, out=None): The concatenated array. """ return _mx_nd_np.concatenate(seq, axis=axis, out=out) + + +@set_module('mxnet.numpy') +def add(x1, x2, out=None): + """Add arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be added. If x1.shape != x2.shape, they must be broadcastable to + a common shape (which may be the shape of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + add : ndarray or scalar + The sum of x1 and x2, element-wise. This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.add(x1, x2, out) + + +@set_module('mxnet.numpy') +def subtract(x1, x2, out=None): + """Subtract arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be subtracted from each other. If x1.shape != x2.shape, + they must be broadcastable to a common shape (which may be the shape + of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + subtract : ndarray or scalar + The difference of x1 and x2, element-wise. This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.subtract(x1, x2, out) + + +@set_module('mxnet.numpy') +def multiply(x1, x2, out=None): + """Multiply arguments element-wise. + + Parameters + ---------- + x1, x2 : ndarrays or scalar values + The arrays to be multiplied. If x1.shape != x2.shape, they must be broadcastable to + a common shape (which may be the shape of one or the other). + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + The difference of x1 and x2, element-wise. This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.multiply(x1, x2, out) + + +@set_module('mxnet.numpy') +def divide(x1, x2, out=None): + """Returns a true division of the inputs, element-wise. + + Parameters + ---------- + x1 : ndarray or scalar + Dividend array. + + x2 : ndarray or scalar + Divisor array. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.divide(x1, x2, out=out) + + +@set_module('mxnet.numpy') +def mod(x1, x2, out=None): + """Return element-wise remainder of division. + + Parameters + ---------- + x1 : ndarray or scalar + Dividend array. + + x2 : ndarray or scalar + Divisor array. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.mod(x1, x2, out=out) + + +@set_module('mxnet.numpy') +def power(x1, x2, out=None): + """First array elements raised to powers from second array, element-wise. + + Parameters + ---------- + x1 : ndarray or scalar + The bases. + + x2 : ndarray or scalar + The exponent. + + out : ndarray + A location into which the result is stored. If provided, it must have a shape + that the inputs broadcast to. If not provided or None, a freshly-allocated array + is returned. + + Returns + ------- + out : ndarray or scalar + The bases in x1 raised to the exponents in x2. + This is a scalar if both x1 and x2 are scalars. + """ + return _mx_nd_np.power(x1, x2, out=out) + + +@set_module('mxnet.numpy') +def clip(a, a_min, a_max, out=None): + """Clip (limit) the values in an array. + + Given an interval, values outside the interval are clipped to + the interval edges. For example, if an interval of ``[0, 1]`` + is specified, values smaller than 0 become 0, and values larger + than 1 become 1. + + Parameters + ---------- + a : ndarray + Array containing elements to clip. + a_min : scalar or `None` + Minimum value. If `None`, clipping is not performed on lower + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + a_max : scalar or `None` + Maximum value. If `None`, clipping is not performed on upper + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + out : ndarray, optional + The results will be placed in this array. It may be the input + array for in-place clipping. `out` must be of the right shape + to hold the output. + + Returns + ------- + clipped_array : ndarray + An array with the elements of `a`, but where values + < `a_min` are replaced with `a_min`, and those > `a_max` + with `a_max`. + """ + return _mx_nd_np.clip(a, a_min, a_max, out=out) diff --git a/python/mxnet/numpy_extension/__init__.py b/python/mxnet/numpy_extension/__init__.py index 0c89a88..6419c57 100644 --- a/python/mxnet/numpy_extension/__init__.py +++ b/python/mxnet/numpy_extension/__init__.py @@ -24,8 +24,9 @@ from . import _op from . import _register from ._op import * # pylint: disable=wildcard-import from ..context import * # pylint: disable=wildcard-import -from ..util import use_np_shape, np_shape, is_np_shape -from ..util import use_np_array, np_array, is_np_array, use_np +from ..util import use_np_shape, np_shape, is_np_shape, set_np_shape +from ..util import use_np_array, np_array, is_np_array, set_np_array +from ..util import set_np, use_np from .. import autograd __all__ = [] diff --git a/python/mxnet/optimizer/optimizer.py b/python/mxnet/optimizer/optimizer.py index 5b433ee..5ab256c 100644 --- a/python/mxnet/optimizer/optimizer.py +++ b/python/mxnet/optimizer/optimizer.py @@ -34,6 +34,7 @@ from ..ndarray import (sgd_update, sgd_mom_update, adam_update, rmsprop_update, multi_mp_sgd_mom_update) from ..ndarray import sparse from ..random import normal +from ..util import is_np_array __all__ = [ 'AdaDelta', 'AdaGrad', 'Adam', 'Adamax', 'DCASGD', 'FTML', 'Ftrl', 'LBSGD', @@ -95,7 +96,7 @@ class Optimizer(object): def __init__(self, rescale_grad=1., param_idx2name=None, wd=0., clip_gradient=None, learning_rate=0.01, lr_scheduler=None, sym=None, begin_num_update=0, - multi_precision=False, param_dict=None, allow_np=False): + multi_precision=False, param_dict=None): self.rescale_grad = rescale_grad self.lr = learning_rate self.lr_scheduler = lr_scheduler @@ -120,7 +121,7 @@ class Optimizer(object): self.idx2name = param_idx2name.copy() self.sym_info = (sym.attr_dict(), sym.list_arguments()) if sym is not None else () self.param_dict = param_dict if param_dict else {} - self.allow_np = allow_np + self.allow_np_array = is_np_array() self.set_lr_mult({}) self.set_wd_mult({}) @@ -1648,6 +1649,9 @@ create = Optimizer.create_optimizer # pylint: disable=invalid-name def _as_classic(a, allow_np): + # TODO(junwu): This is a temp solution for allowing converting + # np.ndarray to mx.nd.NDArray to be fed into the optimizer since + # users may have custom optimizers implemented using mx.nd.NDArray ops. from ..numpy import ndarray as np_ndarray if isinstance(a, (tuple, list)): if any(isinstance(x, np_ndarray) for x in a): @@ -1675,7 +1679,7 @@ class Updater(object): def __call__(self, index, grad, weight): """Updates weight given gradient and index.""" - allow_np = self.optimizer.allow_np + allow_np = self.optimizer.allow_np_array if not isinstance(index, (list, tuple)): indices = [index] grads = [_as_classic(grad, allow_np)] diff --git a/python/mxnet/symbol/numpy/_symbol.py b/python/mxnet/symbol/numpy/_symbol.py index 7a55547..72f9eca 100644 --- a/python/mxnet/symbol/numpy/_symbol.py +++ b/python/mxnet/symbol/numpy/_symbol.py @@ -29,7 +29,8 @@ from ..symbol import Symbol from .._internal import _set_np_symbol_class from . import _internal as _npi -__all__ = ['zeros', 'ones', 'maximum', 'minimum', 'stack', 'concatenate', 'arange', 'argmax'] +__all__ = ['zeros', 'ones', 'maximum', 'minimum', 'stack', 'concatenate', 'arange', 'argmax', + 'clip', 'add', 'subtract', 'multiply', 'divide', 'mod', 'power'] @set_module('mxnet.symbol.numpy') @@ -45,53 +46,23 @@ class _Symbol(Symbol): def __add__(self, other): """x.__add__(y) <=> x + y""" - if isinstance(other, _Symbol): - return _npi.add(self, other) - elif isinstance(other, numeric_types): - return _npi.add_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand" - .format(str(type(other)))) + return add(self, other) def __sub__(self, other): """x.__sub__(y) <=> x - y""" - if isinstance(other, _Symbol): - return _npi.subtract(self, other) - elif isinstance(other, numeric_types): - return _npi.subtract_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand" - .format(str(type(other)))) + return subtract(self, other) def __rsub__(self, other): """x.__rsub__(y) <=> y - x""" - if isinstance(other, _Symbol): - return _npi.subtract(other, self) - elif isinstance(other, numeric_types): - return _npi.rsubtract_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand" - .format(str(type(other)))) + return subtract(other, self) def __mul__(self, other): """x.__mul__(y) <=> x * y""" - if isinstance(other, _Symbol): - return _npi.multiply(self, other) - elif isinstance(other, numeric_types): - return _npi.multiply_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand" - .format(str(type(other)))) + return multiply(self, other) def __rmul__(self, other): """x.__rmul__(y) <=> y * x""" - if isinstance(other, _Symbol): - return _npi.multiply(self, other) - elif isinstance(other, numeric_types): - return _npi.multiply_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand" - .format(str(type(other)))) + return multiply(other, self) def __div__(self, other): raise AttributeError('_Symbol.__div__ is replaced by __truediv__. If you are using' @@ -109,63 +80,32 @@ class _Symbol(Symbol): def __mod__(self, other): """x.__mod__(y) <=> x % y""" - if isinstance(other, _Symbol): - return _npi.mod(self, other) - elif isinstance(other, numeric_types): - return _npi.mod_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand".format(str(type(other)))) + return mod(self, other) def __rmod__(self, other): """x.__rmod__(y) <=> y % x""" - if isinstance(other, _Symbol): - return _npi.mod(other, self) - elif isinstance(other, numeric_types): - return _npi.rmod_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand".format(str(type(other)))) + return mod(other, self) def __idiv__(self, other): raise NotImplementedError def __truediv__(self, other): """x.__truediv__(y) <=> x / y""" - if isinstance(other, _Symbol): - return _npi.true_divide(self, other) - elif isinstance(other, numeric_types): - return _npi.true_divide_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as divisor".format(str(type(other)))) + return divide(self, other) def __rtruediv__(self, other): """x.__rtruediv__(y) <=> y / x""" - if isinstance(other, _Symbol): - return _npi.true_divide(other, self) - elif isinstance(other, numeric_types): - return _npi.rtrue_divide_scalar(self, float(other)).as_np_ndarray() - else: - raise TypeError("_Symbol does not support type {} as dividend".format(str(type(other)))) + return divide(other, self) def __itruediv__(self, other): raise NotImplementedError def __pow__(self, other): """x.__pow__(y) <=> x ** y""" - if isinstance(other, _Symbol): - return _npi.power(self, other) - elif isinstance(other, numeric_types): - return _npi.power_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand".format(str(type(other)))) + return power(self, other) def __rpow__(self, other): - """x.__rpow__(y) <=> y ** x""" - if isinstance(other, _Symbol): - return _npi.power(other, self) - elif isinstance(other, numeric_types): - return _npi.rpower_scalar(self, float(other)) - else: - raise TypeError("_Symbol does not support type {} as operand".format(str(type(other)))) + return power(other, self) def __neg__(self): """x.__neg__() <=> - x""" @@ -243,6 +183,10 @@ class _Symbol(Symbol): check_call(_LIB.MXShallowCopySymbol(self.handle, ctypes.byref(hdl))) return Symbol(handle=hdl) + def as_np_ndarray(self): + """For the convenience of conversion between legacy and np symbols.""" + return self + @property # pylint: disable= invalid-name, undefined-variable def T(self): @@ -262,6 +206,9 @@ class _Symbol(Symbol): .format(str(order))) return _mx_np_op.reshape(self, newshape=shape, order=order) + def argmax(self, axis=None, out=None): # pylint: disable=arguments-differ + return _mx_np_op.argmax(self, axis, out) + def reshape_like(self, *args, **kwargs): """Convenience fluent method for :py:func:`reshape_like`. @@ -406,14 +353,6 @@ class _Symbol(Symbol): """ raise NotImplementedError - def argmax(self, *args, **kwargs): - """Convenience fluent method for :py:func:`argmax`. - - The arguments are the same as for :py:func:`argmax`, with - this array as data. - """ - raise NotImplementedError - def argmax_channel(self, *args, **kwargs): """Convenience fluent method for :py:func:`argmax_channel`. @@ -430,13 +369,11 @@ class _Symbol(Symbol): """ raise NotImplementedError - def clip(self, *args, **kwargs): - """Convenience fluent method for :py:func:`clip`. - - The arguments are the same as for :py:func:`clip`, with - this array as data. + def clip(self, min=None, max=None, out=None): # pylint: disable=arguments-differ + """Return an array whose values are limited to [min, max]. + One of max or min must be given. """ - raise NotImplementedError + return clip(self, min, max, out=out) def abs(self, *args, **kwargs): """Convenience fluent method for :py:func:`abs`. @@ -566,13 +503,13 @@ class _Symbol(Symbol): """ raise AttributeError('_Symbol object has no attribute nanprod') - def mean(self, *args, **kwargs): + def mean(self, axis=None, dtype=None, out=None, keepdims=False): # pylint: disable=arguments-differ """Convenience fluent method for :py:func:`mean`. The arguments are the same as for :py:func:`mean`, with this array as data. """ - raise NotImplementedError + return _mx_np_op.mean(self, axis=axis, dtype=dtype, keepdims=keepdims, out=out) def max(self, *args, **kwargs): """Convenience fluent method for :py:func:`max`. @@ -1031,11 +968,44 @@ def minimum(x1, x2, out=None): @set_module('mxnet.symbol.numpy') +def add(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.add, _np.add, _npi.add_scalar, None, out) + + +@set_module('mxnet.symbol.numpy') +def subtract(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.subtract, _np.subtract, _npi.subtract_scalar, + _npi.rsubtract_scalar, out) + + +@set_module('mxnet.symbol.numpy') +def multiply(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.multiply, _np.multiply, _npi.multiply_scalar, None, out) + + +@set_module('mxnet.symbol.numpy') +def divide(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.true_divide, _np.divide, _npi.true_divide_scalar, + _npi.rtrue_divide_scalar, out) + + +@set_module('mxnet.symbol.numpy') +def mod(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.mod, _np.mod, _npi.mod_scalar, _npi.rmod_scalar, out) + + +@set_module('mxnet.symbol.numpy') +def power(x1, x2, out=None): + return _ufunc_helper(x1, x2, _npi.power, _np.power, _npi.power_scalar, _npi.rpower_scalar, out) + + +@set_module('mxnet.symbol.numpy') def stack(arrays, axis=0, out=None): """Join a sequence of arrays along a new axis. - The axis parameter specifies the index of the new axis in the dimensions of the result. - For example, if `axis=0` it will be the first dimension and if `axis=-1` it will be the last dimension. + The axis parameter specifies the index of the new axis in the dimensions of the result. + For example, if `axis=0` it will be the first dimension and if `axis=-1` it will be the last + dimension. Parameters ---------- @@ -1161,4 +1131,46 @@ def argmax(a, axis=None, out=None): return _npi.argmax(a, axis=axis, keepdims=False, out=out) +@set_module('mxnet.symbol.numpy') +def clip(a, a_min, a_max, out=None): + """Clip (limit) the values in an array. + + Given an interval, values outside the interval are clipped to + the interval edges. For example, if an interval of ``[0, 1]`` + is specified, values smaller than 0 become 0, and values larger + than 1 become 1. + + Parameters + ---------- + a : _Symbol + Array containing elements to clip. + a_min : scalar or `None` + Minimum value. If `None`, clipping is not performed on lower + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + a_max : scalar or `None` + Maximum value. If `None`, clipping is not performed on upper + interval edge. Not more than one of `a_min` and `a_max` may be + `None`. + out : _Symbol, optional + The results will be placed in this array. It may be the input + array for in-place clipping. `out` must be of the right shape + to hold the output. + + Returns + ------- + clipped_array : _Symbol + An array with the elements of `a`, but where values + < `a_min` are replaced with `a_min`, and those > `a_max` + with `a_max`. + """ + if a_min is None and a_max is None: + raise ValueError('array_clip: must set either max or min') + if a_min is None: + a_min = float('-inf') + if a_max is None: + a_max = float('inf') + return _npi.clip(a, a_min, a_max, out=out) + + _set_np_symbol_class(_Symbol) diff --git a/python/mxnet/symbol/register.py b/python/mxnet/symbol/register.py index a835e2e..2bf3fbd 100644 --- a/python/mxnet/symbol/register.py +++ b/python/mxnet/symbol/register.py @@ -227,7 +227,13 @@ def %s(%s):"""%(func_name, ', '.join(signature))) _vals.append(%s)"""%(name, name, name)) # dtype if dtype_name is not None: - code.append(""" + if is_np_op: + code.append(""" + if %s is not _Null and %s is not None: + _keys.append('%s') + _vals.append(_np.dtype(%s).name)"""%(dtype_name, dtype_name, dtype_name, dtype_name)) + else: + code.append(""" if %s is not _Null: _keys.append('%s') _vals.append(_np.dtype(%s).name)"""%(dtype_name, dtype_name, dtype_name)) diff --git a/python/mxnet/symbol/symbol.py b/python/mxnet/symbol/symbol.py index 96397f6..87893c4 100644 --- a/python/mxnet/symbol/symbol.py +++ b/python/mxnet/symbol/symbol.py @@ -68,6 +68,10 @@ class Symbol(SymbolBase): check_call(_LIB.MXShallowCopySymbol(self.handle, ctypes.byref(hdl))) return _Symbol(hdl) + def as_classic_ndarray(self): + """Returns self. For the convenience of conversion between legacy and np symbols.""" + return self + def __repr__(self): """Gets a string representation of the symbol.""" name = self.name diff --git a/python/mxnet/util.py b/python/mxnet/util.py index 60c35bd..013a717 100644 --- a/python/mxnet/util.py +++ b/python/mxnet/util.py @@ -334,7 +334,7 @@ class _NumpyArrayScope(object): """ _current = threading.local() - def __init__(self, is_np_array): #pylint: disable=redefined-outer-name + def __init__(self, is_np_array): # pylint: disable=redefined-outer-name self._old_scope = None self._is_np_array = is_np_array @@ -545,3 +545,39 @@ def use_np(func): A function or class wrapped in the Numpy-shape and NumPy-array scope. """ return use_np_array(use_np_shape(func)) + + +def set_np_array(active): + """Turns on/off NumPy array semantics for the current thread in which `mxnet.numpy.ndarray` + is expected to be created, instead of the legacy `mx.nd.NDArray`. + + Parameters + --------- + active : bool + A boolean value indicating whether the NumPy-array semantics should be turned on or off. + + Returns + ------- + A bool value indicating the previous state of NumPy array semantics. + """ + cur_state = is_np_array() + _NumpyArrayScope._current.value = _NumpyArrayScope(active) + return cur_state + + +def set_np(shape=True, array=True): + """A convenience function for setting NumPy shape and array semantics at the same time. + + Parameters + ---------- + shape : bool + A boolean value indicating whether the NumPy-shape semantics should be turned on or off. + array : bool + A boolean value indicating whether the NumPy-array semantics should be turned on or off. + + Returns + ------- + A tuple with elements indicating the previous states of shape and array + semantics, respectively. + """ + return set_np_shape(shape), set_np_array(array) diff --git a/src/operator/nn/activation.cc b/src/operator/nn/activation.cc index 5b6cece..3d668c8 100644 --- a/src/operator/nn/activation.cc +++ b/src/operator/nn/activation.cc @@ -154,6 +154,7 @@ inline static bool BackwardActStorageType(const nnvm::NodeAttrs& attrs, MXNET_OPERATOR_REGISTER_UNARY(Activation) +.add_alias("_npx_Activation") .describe(R"code(Applies an activation function element-wise to the input. The following activation functions are supported: diff --git a/src/operator/nn/batch_norm.cc b/src/operator/nn/batch_norm.cc index 2564609..030f589 100644 --- a/src/operator/nn/batch_norm.cc +++ b/src/operator/nn/batch_norm.cc @@ -520,6 +520,7 @@ std::vector<nnvm::NodeEntry> BatchNormGrad(const nnvm::NodePtr& n, } NNVM_REGISTER_OP(BatchNorm) +.add_alias("_npx_BatchNorm") .describe(R"code(Batch normalization. Normalizes a data batch by mean and variance, and applies a scale ``gamma`` as diff --git a/src/operator/nn/convolution.cc b/src/operator/nn/convolution.cc index 536e9a7..6ab388a 100644 --- a/src/operator/nn/convolution.cc +++ b/src/operator/nn/convolution.cc @@ -397,6 +397,7 @@ struct ConvolutionGrad { }; NNVM_REGISTER_OP(Convolution) +.add_alias("_npx_Convolution") .describe(R"code(Compute *N*-D convolution on *(N+2)*-D input. In the 2-D convolution, given input data with shape *(batch_size, diff --git a/src/operator/nn/fully_connected.cc b/src/operator/nn/fully_connected.cc index 27f6595..9f30ed2 100644 --- a/src/operator/nn/fully_connected.cc +++ b/src/operator/nn/fully_connected.cc @@ -244,6 +244,7 @@ DMLC_REGISTER_PARAMETER(FullyConnectedParam); NNVM_REGISTER_OP(FullyConnected) MXNET_ADD_SPARSE_OP_ALIAS(FullyConnected) +.add_alias("_npx_FullyConnected") .describe(R"code(Applies a linear transformation: :math:`Y = XW^T + b`. If ``flatten`` is set to be true, then the shapes are: diff --git a/src/operator/nn/pooling.cc b/src/operator/nn/pooling.cc index 41a486e..0df5827 100644 --- a/src/operator/nn/pooling.cc +++ b/src/operator/nn/pooling.cc @@ -364,7 +364,8 @@ inline static bool BackwardPoolingStorageType(const nnvm::NodeAttrs &attrs, DMLC_REGISTER_PARAMETER(PoolingParam); NNVM_REGISTER_OP(Pooling) - .describe(R"code(Performs pooling on the input. +.add_alias("_npx_Pooling") +.describe(R"code(Performs pooling on the input. The shapes for 1-D pooling are diff --git a/src/operator/random/shuffle_op.cc b/src/operator/random/shuffle_op.cc index 7031571..86797c1 100644 --- a/src/operator/random/shuffle_op.cc +++ b/src/operator/random/shuffle_op.cc @@ -122,6 +122,7 @@ void ShuffleForwardCPU(const nnvm::NodeAttrs& attrs, NNVM_REGISTER_OP(_shuffle) .add_alias("shuffle") +.add_alias("_np__random_shuffle") .describe(R"code(Randomly shuffle the elements. This shuffles the array along the first axis. diff --git a/src/operator/tensor/elemwise_unary_op_basic.cc b/src/operator/tensor/elemwise_unary_op_basic.cc index 6da384d..4594b48 100644 --- a/src/operator/tensor/elemwise_unary_op_basic.cc +++ b/src/operator/tensor/elemwise_unary_op_basic.cc @@ -1289,6 +1289,7 @@ MXNET_OPERATOR_REGISTER_BINARY_WITH_SPARSE_CPU_DR(_backward_expm1, unary_bwd<msh // gamma MXNET_OPERATOR_REGISTER_UNARY_WITH_SPARSE_DR(gamma, cpu, mshadow_op::gamma) MXNET_ADD_SPARSE_OP_ALIAS(gamma) +.add_alias("_npx_gamma") .describe(R"code(Returns the gamma function (extension of the factorial function \ to the reals), computed element-wise on the input array. diff --git a/src/operator/tensor/matrix_op.cc b/src/operator/tensor/matrix_op.cc index e78050a..0f059e2 100644 --- a/src/operator/tensor/matrix_op.cc +++ b/src/operator/tensor/matrix_op.cc @@ -696,6 +696,7 @@ NNVM_REGISTER_OP(_backward_slice_like) NNVM_REGISTER_OP(clip) MXNET_ADD_SPARSE_OP_ALIAS(clip) +.add_alias("_npi_clip") .describe(R"code(Clips (limits) the values in an array. Given an interval, values outside the interval are clipped to the interval edges. diff --git a/tests/python/unittest/test_numpy_gluon.py b/tests/python/unittest/test_numpy_gluon.py index 0fcb874..b4db7bf 100644 --- a/tests/python/unittest/test_numpy_gluon.py +++ b/tests/python/unittest/test_numpy_gluon.py @@ -18,6 +18,7 @@ # pylint: skip-file from __future__ import absolute_import from __future__ import division + import mxnet as mx from mxnet import gluon, autograd, np, npx @@ -61,8 +62,8 @@ def test_create_np_param(): check_block_params(x.as_np_ndarray(), TestBlock2, True, np.ndarray) +@npx.use_np def test_optimizer_with_np_ndarrays(): - @npx.use_np class LinearRegression(gluon.HybridBlock): def __init__(self, num_input_dim=0, num_hidden_dim=100, num_output_dim=10): super(LinearRegression, self).__init__() @@ -78,7 +79,6 @@ def test_optimizer_with_np_ndarrays(): y_pred = h_relu.dot(w2) # equivalent to F.np.dot(h_relu, w2) return y_pred - @npx.use_np class TotalLoss(gluon.HybridBlock): def hybrid_forward(self, F, pred, label): return ((pred - label) ** 2).sum() # equivalent to F.np.sum(F.np.square(pred - label)) @@ -97,7 +97,7 @@ def test_optimizer_with_np_ndarrays(): trainer = gluon.Trainer(regressor.collect_params(), 'sgd', - {'learning_rate': 1e-3, 'momentum': 0.9, 'allow_np': True}) + {'learning_rate': 1e-3, 'momentum': 0.9}) for t in range(5): with autograd.record():