Repository: deepjavalibrary/djl (java)

Issue:
`Criteria.builder()` confusing behaviour related to ordering
## Description
If I call `.optModelPath` before `.setTypes` my `optModelPath` is removed, while if I call `.setTypes` first the `optModelPath` is still "active".

This bug caused me having an incredible amount of confusion!


### Expected Behavior
Shouldn't matter which order we apply building steps.

### Error Message
Model not found, or even worse when I tried to also remove Application that it loads ANOTHER model from a DIFFERENT path. That was incredibly confusing too, now my model required the wrong input rank!

## How to Reproduce?
```kotlin
return Criteria.builder()
            .optEngine(engine.djlEngineName())
            .optDevice(device)
            .setTypes(String::class.java, String::class.java)
            .optModelPath(file.path)
            .optTranslator(translator)
            .optApplication(Application.NLP.SENTIMENT_ANALYSIS)
            .optProgress(ProgressBar())
            .build()
```
vs
```kotlin
return Criteria.builder()
            .optEngine(engine.djlEngineName())
            .optDevice(device)
            .optModelPath(file.path)
            .setTypes(String::class.java, String::class.java)
            .optTranslator(translator)
            .optApplication(Application.NLP.SENTIMENT_ANALYSIS)
            .optProgress(ProgressBar())
            .build()
```

### Steps to reproduce

1. See code update, I think the issue is attached to `return new Builder<>(inputClass, outputClass, this);`

## Environment Info

Windows 11, WSL Ubuntu

```
FAILS
```


Pull request:
[api] Fixes bug in Criteria
Fixes #1961

Change-Id: Ic3b1ca69ddbfffc030a9f301ca129d4ee7fedd2b

## Description ##

Brief description of what this PR is about

- If this change is a backward incompatible change, why must this change be made?
- Interesting edge cases to note here


Patch:
diff --git a/api/src/main/java/ai/djl/repository/zoo/Criteria.java b/api/src/main/java/ai/djl/repository/zoo/Criteria.java
index c8e528d3a..0c7965240 100644
--- a/api/src/main/java/ai/djl/repository/zoo/Criteria.java
+++ b/api/src/main/java/ai/djl/repository/zoo/Criteria.java
@@ -406,12 +406,13 @@ public class Criteria<I, O> {
         Block block;
         String modelName;
         Progress progress;
-        private Translator<I, O> translator;
+        Translator<I, O> translator;
 
         Builder() {
             application = Application.UNDEFINED;
         }
 
+        @SuppressWarnings("unchecked")
         private Builder(Class<I> inputClass, Class<O> outputClass, Builder<?, ?> parent) {
             this.inputClass = inputClass;
             this.outputClass = outputClass;
@@ -419,12 +420,16 @@ public class Criteria<I, O> {
             engine = parent.engine;
             device = parent.device;
             groupId = parent.groupId;
+            artifactId = parent.artifactId;
+            modelZoo = parent.modelZoo;
             filters = parent.filters;
             arguments = parent.arguments;
             options = parent.options;
+            factory = parent.factory;
             block = parent.block;
             modelName = parent.modelName;
             progress = parent.progress;
+            translator = (Translator<I, O>) parent.translator;
         }
 
         /**
diff --git a/api/src/main/java/ai/djl/training/evaluator/Coverage.java b/api/src/main/java/ai/djl/training/evaluator/Coverage.java
new file mode 100644
index 000000000..2ee61c09e
--- /dev/null
+++ b/api/src/main/java/ai/djl/training/evaluator/Coverage.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package ai.djl.training.evaluator;
+
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.util.Pair;
+
+/**
+ * Coverage for a Regression problem: it measures the percent of predictions greater than the actual
+ * target, to determine whether the predictor is over-forecasting or under-forecasting. e.g. 0.50 if
+ * we predict near the median of the distribution.
+ *
+ * <pre>
+ *  def coverage(target, forecast):
+ *     return (np.mean((target &lt; forecast)))
+ * </pre>
+ *
+ * <a href="https://bibinmjose.github.io/2021/03/08/errorblog.html">...</a>
+ */
+public class Coverage extends AbstractAccuracy {
+
+    /**
+     * Creates an evaluator that measures the percent of predictions greater than the actual target.
+     */
+    public Coverage() {
+        this("Coverage", 1);
+    }
+
+    /**
+     * Creates an evaluator that measures the percent of predictions greater than the actual target.
+     *
+     * @param name the name of the evaluator, default is "Coverage"
+     * @param axis the axis along which to count the correct prediction, default is 1
+     */
+    public Coverage(String name, int axis) {
+        super(name, axis);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected Pair<Long, NDArray> accuracyHelper(NDList labels, NDList predictions) {
+        NDArray labl = labels.head();
+        NDArray pred = predictions.head();
+        return new Pair<>(labl.size(), labl.lt(pred));
+    }
+}
diff --git a/api/src/main/java/ai/djl/training/loss/Loss.java b/api/src/main/java/ai/djl/training/loss/Loss.java
index 014f47fef..a661a3e9a 100644
--- a/api/src/main/java/ai/djl/training/loss/Loss.java
+++ b/api/src/main/java/ai/djl/training/loss/Loss.java
@@ -69,7 +69,7 @@ public abstract class Loss extends Evaluator {
     }
 
     /**
-     * Returns a new instance of {@link L1Loss} with given weight and batch axis.
+     * Returns a new instance of {@link L1Loss} with given weight.
      *
      * @param name the name of the loss
      * @param weight the weight to apply on loss value, default 1
@@ -79,6 +79,27 @@ public abstract class Loss extends Evaluator {
         return new L1Loss(name, weight);
     }
 
+    /**
+     * Returns a new instance of {@link QuantileL1Loss} with given quantile.
+     *
+     * @param quantile the quantile position of the data to focus on
+     * @return a new instance of {@link QuantileL1Loss}
+     */
+    public static QuantileL1Loss quantileL1Loss(float quantile) {
+        return new QuantileL1Loss(quantile);
+    }
+
+    /**
+     * Returns a new instance of {@link QuantileL1Loss} with given quantile.
+     *
+     * @param name the name of the loss
+     * @param quantile the quantile position of the data to focus on
+     * @return a new instance of {@link QuantileL1Loss}
+     */
+    public static QuantileL1Loss quantileL1Loss(String name, float quantile) {
+        return new QuantileL1Loss(name, quantile);
+    }
+
     /**
      * Returns a new instance of {@link L2Loss} with default weight and batch axis.
      *
diff --git a/api/src/main/java/ai/djl/training/loss/QuantileL1Loss.java b/api/src/main/java/ai/djl/training/loss/QuantileL1Loss.java
new file mode 100644
index 000000000..d4bb2a987
--- /dev/null
+++ b/api/src/main/java/ai/djl/training/loss/QuantileL1Loss.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.training.loss;
+
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.types.DataType;
+
+/**
+ * {@code QuantileL1Loss} calculates the Weighted Quantile Loss between labels and predictions. It
+ * is useful for regression problems where you wish to estimate a particular quantile. For example,
+ * to target the P90, instantiate {@code new QuantileL1Loss("P90", 0.90)}. Basically, what this loss
+ * function does is to focus on a centain persentile of the data. Eg. q=0.5 is the original default
+ * case of regression, meaning the best-fit line lies in the center. When q=0.9, the best-fit line
+ * will lie above the center; and, if \partial forecast / \partial w are the same, then exactly 0.9
+ * of total data points will lie below the best-fit line.
+ *
+ * <pre>
+ *  def quantile_loss(target, forecast, q):
+ *      return 2 * np.sum(np.abs((forecast - target) * ((target &lt;= forecast) - q)))
+ * </pre>
+ *
+ * <p>Reference: <a href="https://bibinmjose.github.io/2021/03/08/errorblog.html">...</a>
+ */
+public class QuantileL1Loss extends Loss {
+
+    private Number quantile;
+
+    /**
+     * Computes QuantileL1Loss for regression problem.
+     *
+     * @param quantile the quantile position of the data to focus on
+     */
+    public QuantileL1Loss(float quantile) {
+        this("QuantileL1Loss", quantile);
+    }
+
+    /**
+     * Computes QuantileL1Loss for regression problem.
+     *
+     * @param name the name of the loss function, default "QuantileL1Loss"
+     * @param quantile the quantile position of the data to focus on
+     */
+    public QuantileL1Loss(String name, float quantile) {
+        super(name);
+        this.quantile = quantile;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public NDArray evaluate(NDList labels, NDList predictions) {
+        NDArray pred = predictions.singletonOrThrow();
+        NDArray labelReshaped = labels.singletonOrThrow().reshape(pred.getShape());
+        NDArray loss =
+                pred.sub(labelReshaped)
+                        .mul(labelReshaped.lte(pred).toType(DataType.FLOAT32, false).sub(quantile))
+                        .abs()
+                        .mul(2);
+        return loss.mean();
+    }
+}
diff --git a/bom/build.gradle b/bom/build.gradle
index c7119f07e..b37d1b6b8 100644
--- a/bom/build.gradle
+++ b/bom/build.gradle
@@ -45,6 +45,7 @@ dependencies {
         api "ai.djl.tensorflow:tensorflow-model-zoo:${version}"
         api "ai.djl.tensorrt:tensorrt:${version}"
         api "ai.djl.tflite:tflite-engine:${version}"
+        api "ai.djl.timeseries:timeseries:${version}"
     }
 }
 
diff --git a/extensions/timeseries/build.gradle b/extensions/timeseries/build.gradle
index 228874f29..f4aea7010 100644
--- a/extensions/timeseries/build.gradle
+++ b/extensions/timeseries/build.gradle
@@ -1,4 +1,4 @@
-group 'ai.dj.timeseries'
+group 'ai.djl.timeseries'
 
 dependencies {
     api project(":api")


Test patch:
diff --git a/integration/src/main/java/ai/djl/integration/tests/training/EvaluatorTest.java b/integration/src/main/java/ai/djl/integration/tests/training/EvaluatorTest.java
index 597ebb54c..0135f9cfb 100644
--- a/integration/src/main/java/ai/djl/integration/tests/training/EvaluatorTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/training/EvaluatorTest.java
@@ -19,6 +19,7 @@ import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.training.evaluator.AbstractAccuracy;
 import ai.djl.training.evaluator.Accuracy;
+import ai.djl.training.evaluator.Coverage;
 import ai.djl.training.evaluator.TopKAccuracy;
 
 import org.testng.Assert;
@@ -58,6 +59,25 @@ public class EvaluatorTest {
         }
     }
 
+    @Test
+    public void testCoverage() {
+        try (NDManager manager = NDManager.newBaseManager()) {
+
+            NDArray predictions = manager.create(new float[] {0.3f, 0.7f, 0.0f}, new Shape(3));
+            NDArray labels = manager.create(new float[] {0.5f, 0.5f, 0.5f}, new Shape(3));
+
+            Coverage coverage = new Coverage();
+            coverage.addAccumulator("");
+            coverage.updateAccumulator("", new NDList(labels), new NDList(predictions));
+            float accuracy = coverage.getAccumulator("");
+            float expectedAccuracy = 1.f / 3;
+            Assert.assertEquals(
+                    accuracy,
+                    expectedAccuracy,
+                    "Wrong accuracy, expected: " + expectedAccuracy + ", actual: " + accuracy);
+        }
+    }
+
     @Test
     public void testTopKAccuracy() {
         try (NDManager manager = NDManager.newBaseManager()) {
diff --git a/integration/src/main/java/ai/djl/integration/tests/training/LossTest.java b/integration/src/main/java/ai/djl/integration/tests/training/LossTest.java
index 3989c90e2..1fb74cccc 100644
--- a/integration/src/main/java/ai/djl/integration/tests/training/LossTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/training/LossTest.java
@@ -34,6 +34,19 @@ public class LossTest {
         }
     }
 
+    @Test
+    public void quantileL1LossTest() {
+        try (NDManager manager = NDManager.newBaseManager()) {
+            NDArray pred = manager.create(new float[] {1, 2, 3, 4, 5});
+            NDArray label = manager.ones(new Shape(5));
+            Assert.assertEquals(
+                    Loss.quantileL1Loss(0.9f)
+                            .evaluate(new NDList(label), new NDList(pred))
+                            .getFloat(),
+                    0.4000001f);
+        }
+    }
+
     @Test
     public void l2LossTest() {
         try (NDManager manager = NDManager.newBaseManager()) {