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)