This is an automated email from the ASF dual-hosted git repository.
reminisce pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-mxnet.git
The following commit(s) were added to refs/heads/master by this push:
new 89cb4ad Numpy add numpy op roll (#15902)
89cb4ad is described below
commit 89cb4ad30459b16b89b0e2ec4a9d8f76ee497dbd
Author: Shi, guangyong <[email protected]>
AuthorDate: Sun Sep 22 11:37:40 2019 +0800
Numpy add numpy op roll (#15902)
* add numpy op roll
updata roll
address comments
fix backward and docs
* fix docs and test fils
---
python/mxnet/_numpy_op_doc.py | 67 ++++++++++++++
src/operator/numpy/np_matrix_op-inl.h | 157 +++++++++++++++++++++++++++++++++
src/operator/numpy/np_matrix_op.cc | 76 ++++++++++++++++
src/operator/numpy/np_matrix_op.cu | 3 +
tests/python/unittest/test_numpy_op.py | 66 +++++++++++++-
5 files changed, 365 insertions(+), 4 deletions(-)
diff --git a/python/mxnet/_numpy_op_doc.py b/python/mxnet/_numpy_op_doc.py
index ae21580..a83447f 100644
--- a/python/mxnet/_numpy_op_doc.py
+++ b/python/mxnet/_numpy_op_doc.py
@@ -516,3 +516,70 @@ def _np__linalg_svd(a):
array(0.)
"""
pass
+
+
+def _np_roll(a, shift, axis=None):
+ """
+ roll(a, shift, axis=None):
+
+ Roll array elements along a given axis.
+
+ Elements that roll beyond the last position are re-introduced at
+ the first.
+
+ Parameters
+ ----------
+ a : ndarray
+ Input array.
+ shift : int or tuple of ints
+ The number of places by which elements are shifted. If a tuple,
+ then `axis` must be a tuple of the same size, and each of the
+ given axes is shifted by the corresponding number. If an int
+ while `axis` is a tuple of ints, then the same value is used for
+ all given axes.
+ axis : int or tuple of ints, optional
+ Axis or axes along which elements are shifted. By default, the
+ array is flattened before shifting, after which the original
+ shape is restored.
+
+ Returns
+ -------
+ res : ndarray
+ Output array, with the same shape as `a`.
+
+ Notes
+ -----
+ Supports rolling over multiple dimensions simultaneously.
+
+ Examples
+ --------
+ >>> x = np.arange(10)
+ >>> np.roll(x, 2)
+ array([8., 9., 0., 1., 2., 3., 4., 5., 6., 7.])
+ >>> np.roll(x, -2)
+ array([2., 3., 4., 5., 6., 7., 8., 9., 0., 1.])
+
+ >>> x2 = np.reshape(x, (2,5))
+ >>> x2
+ array([[0., 1., 2., 3., 4.],
+ [5., 6., 7., 8., 9.]])
+ >>> np.roll(x2, 1)
+ array([[9., 0., 1., 2., 3.],
+ [4., 5., 6., 7., 8.]])
+ >>> np.roll(x2, -1)
+ array([[1., 2., 3., 4., 5.],
+ [6., 7., 8., 9., 0.]])
+ >>> np.roll(x2, 1, axis=0)
+ array([[5., 6., 7., 8., 9.],
+ [0., 1., 2., 3., 4.]])
+ >>> np.roll(x2, -1, axis=0)
+ array([[5., 6., 7., 8., 9.],
+ [0., 1., 2., 3., 4.]])
+ >>> np.roll(x2, 1, axis=1)
+ array([[4., 0., 1., 2., 3.],
+ [9., 5., 6., 7., 8.]])
+ >>> np.roll(x2, -1, axis=1)
+ array([[1., 2., 3., 4., 0.],
+ [6., 7., 8., 9., 5.]])
+ """
+ pass
diff --git a/src/operator/numpy/np_matrix_op-inl.h
b/src/operator/numpy/np_matrix_op-inl.h
index fe2fadd..631196e 100644
--- a/src/operator/numpy/np_matrix_op-inl.h
+++ b/src/operator/numpy/np_matrix_op-inl.h
@@ -26,6 +26,7 @@
#define MXNET_OPERATOR_NUMPY_NP_MATRIX_OP_INL_H_
#include <vector>
+#include <algorithm>
#include "../tensor/matrix_op-inl.h"
#include "../nn/concat-inl.h"
@@ -140,6 +141,162 @@ void NumpyVstackBackward(const nnvm::NodeAttrs& attrs,
});
}
+struct NumpyRollParam : public dmlc::Parameter<NumpyRollParam> {
+ dmlc::optional<mxnet::TShape> shift;
+ dmlc::optional<mxnet::TShape> axis;
+ DMLC_DECLARE_PARAMETER(NumpyRollParam) {
+ DMLC_DECLARE_FIELD(shift)
+ .set_default(dmlc::optional<mxnet::TShape>())
+ .describe("The number of places by which elements are shifted. If a tuple,"
+ "then axis must be a tuple of the same size, and each of the
given axes is shifted"
+ "by the corresponding number. If an int while axis is a tuple of
ints, "
+ "then the same value is used for all given axes.");
+ DMLC_DECLARE_FIELD(axis)
+ .set_default(dmlc::optional<mxnet::TShape>())
+ .describe("Axis or axes along which elements are shifted. By default, the
array is flattened"
+ "before shifting, after which the original shape is restored.");
+ }
+};
+
+template<int req>
+struct RollAxisNone_forward {
+ template<typename DType>
+ MSHADOW_XINLINE static void Map(int i, DType* out_data, const DType* in_data,
+ const int size, const int shift) {
+ int new_index = i - shift < 0 ? i - shift + size : i - shift;
+ KERNEL_ASSIGN(out_data[i], req, in_data[new_index]);
+ }
+};
+
+template<int req>
+struct RollAxis_forward {
+ template<typename DType>
+ MSHADOW_XINLINE static void Map(int i, DType* out_data, const DType* in_data,
+ const size_t* new_index) {
+ KERNEL_ASSIGN(out_data[i], req, in_data[new_index[i]]);
+ }
+};
+
+inline void RollDfs(const std::vector<std::vector<size_t>>& new_axes,
+ const std::vector<size_t>& value,
+ std::vector<size_t>* new_index,
+ int index, int ndim, int mid) {
+ for (int a : new_axes[index]) {
+ if (index == ndim - 1) {
+ std::vector<size_t>& out = (*new_index);
+ out.push_back(mid + a);
+ } else {
+ mid += a * value[ndim - 1 - index];
+ RollDfs(new_axes, value, new_index, index + 1, ndim, mid);
+ mid -= a * value[ndim - 1 - index];
+ }
+ }
+}
+
+template<typename xpu>
+void NumpyRollCompute(const nnvm::NodeAttrs& attrs,
+ const OpContext& ctx,
+ const std::vector<TBlob>& inputs,
+ const std::vector<OpReqType>& req,
+ const std::vector<TBlob>& outputs) {
+ using namespace mxnet_op;
+ CHECK_EQ(inputs.size(), 1U);
+ CHECK_EQ(outputs.size(), 1U);
+ CHECK_EQ(req.size(), 1U);
+ if (inputs[0].Size() == 0U) return;
+ const NumpyRollParam& param = nnvm::get<NumpyRollParam>(attrs.parsed);
+ const index_t ndim(inputs[0].shape_.ndim());
+ Stream<xpu> *s = ctx.get_stream<xpu>();
+ std::vector<int> shifts(ndim, 0);
+ index_t input_size = inputs[0].Size();
+ if (!param.axis.has_value()) {
+ int shift = param.shift.value()[0];
+ shift = shift % input_size;
+ if (shift < 0) {
+ shift += inputs[0].shape_.Size();
+ }
+ MSHADOW_TYPE_SWITCH(outputs[0].type_flag_, DType, {
+ MXNET_ASSIGN_REQ_SWITCH(req[0], req_type, {
+ Kernel<RollAxisNone_forward<req_type>, xpu>::Launch(
+ s, outputs[0].Size(), outputs[0].dptr<DType>(),
inputs[0].dptr<DType>(),
+ inputs[0].Size(), shift);
+ });
+ });
+ } else {
+ mxnet::TShape axes(param.axis.value());
+ for (int i = 0; i < axes.ndim(); ++i) {
+ if (axes[i] < 0) {
+ axes[i] += ndim;
+ }
+ }
+ for (int i = 0; i < axes.ndim(); ++i) {
+ CHECK_LT(axes[i], ndim)
+ << "axis " << axes[i]
+ << " Exceeds input dimensions " << inputs[0].shape_;
+ CHECK_GE(axes[0], 0)
+ << "Reduction axis " << param.axis.value()
+ << " Exceeds input dimensions " << inputs[0].shape_;
+ }
+ if (param.shift.value().ndim() == 1) {
+ for (int i = 0; i < axes.ndim(); ++i) {
+ shifts[axes[i]] = param.shift.value()[0];
+ }
+ } else {
+ if (param.shift.value().ndim() != axes.ndim()) {
+ LOG(FATAL) << "shift and `axis` must be a tuple of the same size,";
+ }
+ for (int i = 0; i < axes.ndim(); ++i) {
+ shifts[axes[i]] = param.shift.value()[i];
+ }
+ }
+ // keep shift in a legal range
+ for (int i = 0; i < ndim; ++i) {
+ int trans_shift = shifts[i] % inputs[0].shape_[i];
+ if (trans_shift < 0) {
+ trans_shift = shifts[i] + inputs[0].shape_[i];
+ }
+ shifts[i] = trans_shift;
+ }
+ // the result of new axis after shift.
+ std::vector<std::vector<size_t>> new_axes;
+ std::vector<size_t> new_index;
+ std::vector<size_t> temp;
+ std::vector<size_t> value(ndim, 0);
+ int mid_val = 1;
+ for (int i = 0; i < ndim; ++i) {
+ if (shifts[i] != 0) {
+ for (int j = 0; j < inputs[0].shape_[i]; ++j) {
+ int new_axis = (j + inputs[0].shape_[i] - shifts[i]) %
inputs[0].shape_[i];
+ temp.push_back(new_axis);
+ }
+ } else {
+ for (int j = 0; j < inputs[0].shape_[i]; ++j) {
+ temp.push_back(j);
+ }
+ }
+ new_axes.push_back(temp);
+ temp.clear();
+ value[i] = mid_val;
+ mid_val *= inputs[0].shape_[ndim - 1 - i];
+ }
+ RollDfs(new_axes, value, &new_index, 0, ndim, 0);
+ size_t workspace_size = new_index.size() * sizeof(size_t);
+ Tensor<xpu, 1, char> workspace =
+ ctx.requested[0].get_space_typed<xpu, 1, char>(Shape1(workspace_size),
s);
+ Tensor<cpu, 1, size_t> index_cpu_tensor(new_index.data(),
Shape1(new_index.size()));
+ Tensor<xpu, 1, size_t> index_xpu_tensor(
+ reinterpret_cast<size_t*>(workspace.dptr_), Shape1(new_index.size()));
+ mshadow::Copy(index_xpu_tensor, index_cpu_tensor, s);
+ MSHADOW_TYPE_SWITCH(outputs[0].type_flag_, DType, {
+ MXNET_ASSIGN_REQ_SWITCH(req[0], req_type, {
+ Kernel<RollAxis_forward<req_type>, xpu>::Launch(
+ s, outputs[0].Size(), outputs[0].dptr<DType>(),
inputs[0].dptr<DType>(),
+ index_xpu_tensor.dptr_);
+ });
+ });
+ }
+}
+
} // namespace op
} // namespace mxnet
diff --git a/src/operator/numpy/np_matrix_op.cc
b/src/operator/numpy/np_matrix_op.cc
index 5509f34..45e4c3f 100644
--- a/src/operator/numpy/np_matrix_op.cc
+++ b/src/operator/numpy/np_matrix_op.cc
@@ -31,6 +31,7 @@ namespace mxnet {
namespace op {
DMLC_REGISTER_PARAMETER(NumpyTransposeParam);
+DMLC_REGISTER_PARAMETER(NumpyRollParam);
bool NumpyTransposeShape(const nnvm::NodeAttrs& attrs,
mxnet::ShapeVector *in_attrs,
@@ -489,5 +490,80 @@ NNVM_REGISTER_OP(_backward_np_vstack)
.set_attr<nnvm::TIsBackward>("TIsBackward", true)
.set_attr<FCompute>("FCompute<cpu>", NumpyVstackBackward<cpu>);
+inline bool NumpyRollShape(const nnvm::NodeAttrs& attrs,
+ mxnet::ShapeVector *in_attrs,
+ mxnet::ShapeVector *out_attrs) {
+ using namespace mshadow;
+ const NumpyRollParam& param = nnvm::get<NumpyRollParam>(attrs.parsed);
+
+ if (!param.shift.has_value()) {
+ LOG(FATAL) << "roll missing 1 required positional argument: 'shift'.";
+ }
+ if (param.shift.value().ndim() > 1 &&
+ param.axis.has_value() &&
+ param.axis.value().ndim() != param.shift.value().ndim()) {
+ LOG(FATAL) << "shift and `axis` must be a tuple of the same size.";
+ }
+ if (!param.axis.has_value() && param.shift.has_value() &&
param.shift.value().ndim() > 1) {
+ LOG(FATAL) << "shift must be an int.";
+ }
+ if (param.axis.has_value()) {
+ mxnet::TShape axes(param.axis.value());
+ const index_t ndim = (*in_attrs)[0].ndim();
+ for (index_t i = 0; i < axes.ndim(); i++) {
+ if (axes[i] < 0) {
+ axes[i] += ndim;
+ }
+ }
+ std::sort(axes.begin(), axes.end());
+ for (index_t i = 1; i < axes.ndim(); i++) {
+ CHECK_LT(axes[i - 1], axes[i])
+ << "axes have duplicates " << axes;
+ }
+ CHECK_LT(axes[axes.ndim() - 1], ndim)
+ << "axis " << axes[axes.ndim() - 1]
+ << " Exceeds input dimensions " << (*in_attrs)[0];
+ CHECK_GE(axes[0], 0)
+ << "Reduction axis " << param.axis.value()
+ << " Exceeds input dimensions " << (*in_attrs)[0];
+ }
+ return ElemwiseShape<1, 1>(attrs, in_attrs, out_attrs);
+}
+
+NNVM_REGISTER_OP(_np_roll)
+.set_num_inputs(1)
+.set_num_outputs(1)
+.set_attr_parser(ParamParser<NumpyRollParam>)
+.set_attr<nnvm::FListInputNames>("FListInputNames",
+ [](const NodeAttrs& attrs) {
+ return std::vector<std::string>{"data"};
+})
+.set_attr<mxnet::FInferShape>("FInferShape", NumpyRollShape)
+.set_attr<nnvm::FInferType>("FInferType", ElemwiseType<1, 1>)
+.set_attr<mxnet::FCompute>("FCompute<cpu>", NumpyRollCompute<cpu>)
+.set_attr<nnvm::FGradient>("FGradient",
+ [](const nnvm::NodePtr& n, const std::vector<nnvm::NodeEntry>& ograds) {
+ const NumpyRollParam& param = nnvm::get<NumpyRollParam>(n->attrs.parsed);
+ if (!param.shift.has_value()) {
+ LOG(FATAL) << "roll missing 1 required positional argument: 'shift'.";
+ }
+ mxnet::TShape shifts(param.shift.value());
+ for (int i = 0; i < shifts.ndim(); ++i) {
+ shifts[i] = -shifts[i];
+ }
+ std::ostringstream os1;
+ os1 << dmlc::optional<mxnet::TShape>(shifts);
+ std::ostringstream os2;
+ os2 << param.axis;
+ return MakeNonlossGradNode("_np_roll", n, ograds, {},
+ {{"shift", os1.str()}, {"axis", os2.str()}});
+})
+.set_attr<FResourceRequest>("FResourceRequest",
+ [](const NodeAttrs& n) {
+ return std::vector<ResourceRequest>{ResourceRequest::kTempSpace};
+})
+.add_argument("data", "NDArray-or-Symbol", "Input ndarray")
+.add_arguments(NumpyRollParam::__FIELDS__());
+
} // namespace op
} // namespace mxnet
diff --git a/src/operator/numpy/np_matrix_op.cu
b/src/operator/numpy/np_matrix_op.cu
index b017ad6..ba49d32 100644
--- a/src/operator/numpy/np_matrix_op.cu
+++ b/src/operator/numpy/np_matrix_op.cu
@@ -53,5 +53,8 @@ NNVM_REGISTER_OP(_npi_vstack)
NNVM_REGISTER_OP(_backward_np_vstack)
.set_attr<FCompute>("FCompute<gpu>", NumpyVstackBackward<gpu>);
+NNVM_REGISTER_OP(_np_roll)
+.set_attr<FCompute>("FCompute<gpu>", NumpyRollCompute<gpu>);
+
} // namespace op
} // namespace mxnet
diff --git a/tests/python/unittest/test_numpy_op.py
b/tests/python/unittest/test_numpy_op.py
index 87c7ab2..42d9043 100644
--- a/tests/python/unittest/test_numpy_op.py
+++ b/tests/python/unittest/test_numpy_op.py
@@ -1883,7 +1883,7 @@ def test_np_copysign():
super(TestCopysign, self).__init__()
def hybrid_forward(self, F, a1, a2):
- return F.np.copysign(a1, a2)
+ return F.np.copysign(a1, a2)
def get_grad(a1, a2):
sign = _np.logical_or(_np.logical_and(a1 < 0, a2 < 0),
@@ -1892,7 +1892,7 @@ def test_np_copysign():
sign = sign.reshape(-1, *a1.shape)
sign = _np.sum(sign, axis=0)
return sign, _np.zeros_like(a2)
-
+
def get_grad_left(a1, a2):
sign = _np.logical_or(_np.logical_and(a1 < 0, a2 < 0),
_np.logical_and(a1 >= 0, a2 >= 0))
@@ -2081,10 +2081,10 @@ def test_np_vstack():
class TestVstack(HybridBlock):
def __init__(self):
super(TestVstack, self).__init__()
-
+
def hybrid_forward(self, F, a, *args):
return F.np.vstack([a] + list(args))
-
+
def g(data):
return _np.ones_like(data)
@@ -2131,6 +2131,64 @@ def test_np_vstack():
assert_almost_equal(mx_out.asnumpy(), expected_np, rtol=rtol,
atol=atol)
+@with_seed()
+@use_np
+def test_np_roll():
+ class TestRoll(HybridBlock):
+ def __init__(self, shift=None, axis=None):
+ super(TestRoll, self).__init__()
+ self._shift = shift
+ self._axis = axis
+
+ def hybrid_forward(self, F, x):
+ return F.np.roll(x, shift=self._shift, axis=self._axis)
+
+ dtypes = ['int32', 'int64', 'float16', 'float32', 'float64']
+ configs = [
+ ((), (3,), None),
+ ((1,), (-3,), None),
+ ((20,), (-3,), None),
+ ((3,), (2,), 0),
+ ((2, 3, 4), (12,), (1,)),
+ ((2, 3, 4), (10, -10), (0, 1)),
+ ((2, 3, 4, 5), (0, 1), (-1, 2)),
+ ((2, 3, 0, 1), (0, 1), (-1, 2)),
+ ((2, 3, 4, 5), 10, (0, 2)),
+ ]
+ i_dtype = {"float32" : _np.float32,
+ "float64" : _np.float64
+ }
+ for dtype in dtypes:
+ for config in configs:
+ for hybridize in [False, True]:
+ shape, shift, axis = config[0], config[1], config[2]
+ x = rand_ndarray(shape=shape, dtype=dtype).as_np_ndarray()
+ net = TestRoll(shift=shift, axis=axis)
+ np_out = _np.roll(x.asnumpy(), shift=shift, axis=axis)
+ if hybridize:
+ net.hybridize()
+ x.attach_grad()
+ with mx.autograd.record():
+ mx_out = net(x)
+ assert mx_out.shape == np_out.shape
+ mx_out.backward()
+ assert same(mx_out.asnumpy(), np_out)
+ assert same(x.grad.shape, x.shape)
+ assert same(x.grad.asnumpy(), _np.ones(shape))
+
+ # test imperativen
+ np_out = _np.roll(x.asnumpy(), shift=shift, axis=axis)
+ mx_out = np.roll(x, shift=shift, axis=axis)
+ assert same(mx_out.asnumpy(), np_out)
+
+ # test numeric
+ if dtype in ['float32', 'float64'] and len(shape)> 0 and
_np.prod(shape) > 0:
+ x_sym = mx.sym.Variable("x").as_np_ndarray()
+ mx_sym = mx.sym.np.roll(x_sym, shift=shift,
axis=axis).as_nd_ndarray()
+ check_numeric_gradient(mx_sym, [x.as_nd_ndarray()],
+ numeric_eps=1e-3, rtol=1e-3,
atol=1e-5, dtype=i_dtype[dtype])
+
+
if __name__ == '__main__':
import nose
nose.runmodule()