L1 Operator
Summary: Adds the L1 Distance operator to distance_op.
Reviewed By: bwasti
Differential Revision: D5007719
fbshipit-source-id: fd547c6645cf5f87305e9ebfd95ed918779c1d2a
diff --git a/caffe2/operators/distance_op.cc b/caffe2/operators/distance_op.cc
index 0ef1aae..db4cb53 100644
--- a/caffe2/operators/distance_op.cc
+++ b/caffe2/operators/distance_op.cc
@@ -31,6 +31,71 @@
}
template <>
+bool L1DistanceOp<float, CPUContext>::RunOnDevice() {
+ auto& X = Input(0);
+ auto& Y = Input(1);
+ auto* distance = Output(0);
+ CAFFE_ENFORCE_EQ(X.ndim(), Y.ndim());
+ for (int i = 0; i < X.ndim(); ++i) {
+ CAFFE_ENFORCE_EQ(X.dim32(i), Y.dim32(i));
+ }
+ distance->Resize(1);
+ const float* X_data = X.data<float>();
+ const float* Y_data = Y.data<float>();
+
+ *(distance->mutable_data<float>()) =
+ (ConstEigenVectorMap<float>(X_data, X.size()).array() -
+ ConstEigenVectorMap<float>(Y_data, Y.size()).array())
+ .abs()
+ .sum();
+ // L1(x, y) = sum(|x-y|)
+ return true;
+}
+
+template <>
+bool L1DistanceGradientOp<float, CPUContext>::RunOnDevice() {
+ auto& X = Input(0);
+ auto& Y = Input(1);
+ auto& dDistance = Input(2);
+ auto* dX = Output(0);
+ auto* dY = Output(1);
+ CAFFE_ENFORCE_EQ(X.ndim(), Y.ndim());
+ for (int i = 0; i < X.ndim(); ++i) {
+ CAFFE_ENFORCE_EQ(X.dim32(i), Y.dim32(i));
+ }
+ int N = X.ndim() > 0 ? X.dim32(0) : 1;
+ int D = N > 0 ? X.size() / N : 0;
+ CAFFE_ENFORCE(X.ndim() == Y.ndim());
+ for (int i = 0; i < X.ndim(); ++i) {
+ CAFFE_ENFORCE(X.dim32(i) == Y.dim32(i));
+ }
+ CAFFE_ENFORCE(dDistance.ndim() == 1);
+ CAFFE_ENFORCE(dDistance.dim32(0) == 1);
+ dX->ResizeLike(X);
+ dY->ResizeLike(Y);
+
+ for (int i = 0; i < N; ++i) {
+ auto offset = i * D;
+ for (int j = 0; j < D; ++j) {
+ const float temp =
+ (X.data<float>())[offset + j] - (Y.data<float>())[offset + j];
+ const float kEps = 1e-12;
+ if (temp < -kEps) {
+ dX->mutable_data<float>()[offset + j] = -(dDistance.data<float>())[0];
+ dY->mutable_data<float>()[offset + j] = (dDistance.data<float>())[0];
+ } else if (temp > kEps) {
+ dX->mutable_data<float>()[offset + j] = (dDistance.data<float>())[0];
+ dY->mutable_data<float>()[offset + j] = -(dDistance.data<float>())[0];
+ } else {
+ dX->mutable_data<float>()[offset + j] = 0;
+ dY->mutable_data<float>()[offset + j] = 0;
+ }
+ }
+ }
+ return true;
+}
+
+template <>
bool DotProductOp<float, CPUContext>::RunOnDevice() {
auto& X = Input(X_IN);
auto& Y = Input(Y_IN);
@@ -229,6 +294,38 @@
};
REGISTER_GRADIENT(SquaredL2Distance, GetSquaredL2DistanceGradient);
+// L1
+REGISTER_CPU_OPERATOR(L1Distance, L1DistanceOp<float, CPUContext>);
+REGISTER_CPU_OPERATOR(
+ L1DistanceGradient,
+ L1DistanceGradientOp<float, CPUContext>);
+
+OPERATOR_SCHEMA(L1Distance)
+ .NumInputs(2)
+ .NumOutputs(1)
+ .IdenticalTypeAndShapeOfInput(0)
+ .SetDoc(R"DOC(
+ Given two input float tensors X, Y, and produces one output float tensor
+ of the L1 difference between X and Y, computed as L1(x,y) = sum over |x-y|
+ )DOC")
+ .Input(0, "X", "1D input tensor")
+ .Output(0, "Y", "1D input tensor");
+
+OPERATOR_SCHEMA(L1DistanceGradient).NumInputs(3).NumOutputs(2);
+
+class GetL1DistanceGradient : public GradientMakerBase {
+ using GradientMakerBase::GradientMakerBase;
+ vector<OperatorDef> GetGradientDefs() override {
+ return SingleGradientDef(
+ "L1DistanceGradient",
+ "",
+ vector<string>{I(0), I(1), GO(0)},
+ vector<string>{GI(0), GI(1)});
+ }
+};
+
+REGISTER_GRADIENT(L1Distance, GetL1DistanceGradient);
+
// Dot Product
REGISTER_CPU_OPERATOR(DotProduct, DotProductOp<float, CPUContext>);
REGISTER_CPU_OPERATOR(
diff --git a/caffe2/operators/distance_op.h b/caffe2/operators/distance_op.h
index 965152c..9d38fb3 100644
--- a/caffe2/operators/distance_op.h
+++ b/caffe2/operators/distance_op.h
@@ -72,6 +72,32 @@
};
template <typename T, class Context>
+class L1DistanceOp : public Operator<Context> {
+ public:
+ L1DistanceOp(const OperatorDef& def, Workspace* ws)
+ : Operator<Context>(def, ws) {}
+ USE_OPERATOR_CONTEXT_FUNCTIONS;
+
+ bool RunOnDevice() override;
+
+ protected:
+ // Input: X, Y; Output: Distance
+};
+
+template <typename T, class Context>
+class L1DistanceGradientOp : public Operator<Context> {
+ public:
+ L1DistanceGradientOp(const OperatorDef& def, Workspace* ws)
+ : Operator<Context>(def, ws) {}
+ USE_OPERATOR_CONTEXT_FUNCTIONS;
+
+ bool RunOnDevice() override;
+
+ protected:
+ // Input: X, Y, dDistance; Output: dX, dY
+};
+
+template <typename T, class Context>
class DotProductOp : public Operator<Context> {
public:
DotProductOp(const OperatorDef& def, Workspace* ws)
diff --git a/caffe2/python/operator_test/distance_op_test.py b/caffe2/python/operator_test/distance_op_test.py
index 89b9247..dcfed87 100644
--- a/caffe2/python/operator_test/distance_op_test.py
+++ b/caffe2/python/operator_test/distance_op_test.py
@@ -12,6 +12,35 @@
class DistanceTest(hu.HypothesisTestCase):
@given(inputs=hu.tensors(n=2,
min_dim=1,
+ max_dim=4,
+ dtype=np.float32),
+ **hu.gcs_cpu_only)
+ def test_L1_distance(self, inputs, gc, dc):
+ X, Y = inputs
+ # avoid kinks by moving away from 0
+ X += 0.02 * np.sign(X - Y)
+ X[(X - Y) == 0.0] += 0.02
+
+ self.ws.create_blob("X").feed(X)
+ self.ws.create_blob("Y").feed(Y)
+ op = core.CreateOperator(
+ 'L1Distance',
+ ['X', 'Y'],
+ ['l1_dist'],
+ )
+ self.ws.run(op)
+
+ np.testing.assert_allclose(self.ws.blobs[("l1_dist")].fetch(),
+ np.linalg.norm((X - Y).flatten(), ord=1),
+ rtol=1e-4, atol=1e-4)
+
+ # Gradient check wrt X
+ self.assertGradientChecks(gc, op, [X, Y], 0, [0], stepsize=1e-2, threshold=1e-2)
+ # Gradient check wrt Y
+ self.assertGradientChecks(gc, op, [X, Y], 1, [0], stepsize=1e-2, threshold=1e-2)
+
+ @given(inputs=hu.tensors(n=2,
+ min_dim=1,
max_dim=2,
dtype=np.float32),
**hu.gcs)