Repository: deepjavalibrary/djl (java)

Issue:
Dynamic learning parameter - time period
## Description
Dynamically modified learning parameter over time. We want learning parameter to change dynamically when network is learning:
- constant learning parameter at the start then **cycle** mode. We want to start with constant learning parameter, then after N epoch start **cycle** - like saw, from min. to max. definition of learning parameter (up and down)
- it should be configurable - we want to start with static learning parameter and then **cycle**, we want to start with **cycle** at the beginning,.. 
- any combination with constant and **cycle**, for example 0-10 epoch constant learning parameter, then **cycle**(whatever type) (11-20), then again constant parameter (21-30),..

**Cycle** (learning parameter alternation) mode should have multiple types, with configurable time interval and min., max. learning parameter value, for example:
- like saw wave (up and down /\ /\ /\ )
- like first half of saw wave (just up / / / )
- like second half of saw wave (just down \ \ \ )
- we can go any combination (/\ - / / - \ \ )

### Will this change the current api? How?
Yes, implement learning parameter which dynamically changes over iteration/epoch

### Who will benefit from this feature?
Everybody, this will enable network to learn more quickly.

## References
Here is dynamic learning parameter change implementation, kind of saw wave over time:
https://github.com/eclipse/deeplearning4j/blob/b5f0ec072f3fd0da566e32f82c0e43ca36553f39/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/schedule/CycleSchedule.java
We want to extend this implementation - see above.


Pull request description:
[training] Add Cyclical Tracker
## Description ##

Add Cyclical Tracker according to cyclical learning rate policy, as detailed in the paper [Cyclical Learning Rates for Training Neural Networks](https://arxiv.org/abs/1506.01186).

close #37.


Patch:
diff --git a/api/src/main/java/ai/djl/training/tracker/CyclicalTracker.java b/api/src/main/java/ai/djl/training/tracker/CyclicalTracker.java
new file mode 100644
index 000000000..0495c5d8f
--- /dev/null
+++ b/api/src/main/java/ai/djl/training/tracker/CyclicalTracker.java
@@ -0,0 +1,280 @@
+/*
+ * 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.tracker;
+
+import ai.djl.util.Preconditions;
+
+/**
+ * {@code CyclicalTracker} is an implementation of {@link Tracker} which is a policy of learning
+ * rate adjustment that increases the learning rate off a base value in a cyclical nature, as
+ * detailed in the paper <a href="https://arxiv.org/abs/1506.01186">Cyclical Learning Rates for
+ * Training Neural Networks</a>.
+ */
+public class CyclicalTracker implements Tracker {
+
+    private float baseValue;
+    private float maxValue;
+    private int stepSizeUp;
+    private int stepSizeDown;
+    private int totalSize;
+    private float stepRatio;
+    private ScaleFunction scaleFunction;
+    private boolean scaleModeCycle;
+
+    /**
+     * Creates a new instance of {@code CyclicalTracker}.
+     *
+     * @param builder the builder to create a new instance of {@code CyclicalTracker}
+     */
+    public CyclicalTracker(Builder builder) {
+        this.baseValue = builder.baseValue;
+        this.maxValue = builder.maxValue;
+        this.stepSizeUp = builder.stepSizeUp;
+        this.stepSizeDown = builder.stepSizeDown > 0 ? builder.stepSizeDown : builder.stepSizeUp;
+        this.totalSize = this.stepSizeUp + this.stepSizeDown;
+        this.stepRatio = (float) this.stepSizeUp / this.totalSize;
+        if (builder.scaleFunction != null) {
+            this.scaleFunction = builder.scaleFunction;
+            this.scaleModeCycle = builder.scaleModeCycle;
+        } else {
+            switch (builder.mode) {
+                case TRIANGULAR:
+                    this.scaleFunction = new TriangularScaleFunction();
+                    this.scaleModeCycle = true;
+                    break;
+                case TRIANGULAR2:
+                    this.scaleFunction = new Triangular2ScaleFunction();
+                    this.scaleModeCycle = true;
+                    break;
+                case EXP_RANGE:
+                    this.scaleFunction = new ExpRangeScaleFunction(builder.gamma);
+                    this.scaleModeCycle = false;
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unsupported Cyclical mode.");
+            }
+        }
+    }
+
+    /**
+     * Creates a new builder.
+     *
+     * @return a new builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public float getNewValue(int numUpdate) {
+        int cycle = (int) Math.floor(1 + (float) numUpdate / this.totalSize);
+        float x = 1 + (float) numUpdate / this.totalSize - cycle;
+        float scaleFactor;
+        float res;
+        if (x < this.stepRatio) {
+            scaleFactor = x / this.stepRatio;
+        } else {
+            scaleFactor = (x - 1) / (this.stepRatio - 1);
+        }
+        float baseHeight = (this.maxValue - this.baseValue) * scaleFactor;
+        if (this.scaleModeCycle) {
+            res = this.baseValue + baseHeight * this.scaleFunction.func(cycle);
+        } else {
+            res = this.baseValue + baseHeight * this.scaleFunction.func(numUpdate);
+        }
+        return res;
+    }
+
+    /** The Builder to construct an {@link CyclicalTracker} object. */
+    public static final class Builder {
+
+        private float baseValue = 0.001f;
+        private float maxValue = 0.006f;
+        private int stepSizeUp = 2000;
+        private int stepSizeDown;
+        private CyclicalMode mode = CyclicalMode.TRIANGULAR;
+        private ScaleFunction scaleFunction;
+        private boolean scaleModeCycle = true;
+        private float gamma = 1f;
+
+        private Builder() {}
+
+        /**
+         * Sets the initial value. Default: 0.001.
+         *
+         * @param baseValue the initial value
+         * @return this {@code Builder}
+         */
+        public Builder optBaseValue(float baseValue) {
+            this.baseValue = baseValue;
+            return this;
+        }
+
+        /**
+         * Sets the initial value. Default: 0.006.
+         *
+         * @param maxValue the max value
+         * @return this {@code Builder}
+         */
+        public Builder optMaxValue(float maxValue) {
+            this.maxValue = maxValue;
+            return this;
+        }
+
+        /**
+         * Sets the number of iterations in up half of cycle. Default: 2000.
+         *
+         * @param stepSizeUp number of iterations in up half of cycle
+         * @return this {@code Builder}
+         */
+        public Builder optStepSizeUp(int stepSizeUp) {
+            this.stepSizeUp = stepSizeUp;
+            return this;
+        }
+
+        /**
+         * Sets the number of iterations in up half of cycle. If {@code stepSizeDown} equals 0, it
+         * is set to {@code stepSizeUp}. Default: 0.
+         *
+         * @param stepSizeDown number of iterations in the down half of cycle
+         * @return this {@code Builder}
+         */
+        public Builder optStepSizeDown(int stepSizeDown) {
+            this.stepSizeDown = stepSizeDown;
+            return this;
+        }
+
+        /**
+         * Sets the cyclical mode. Can be {@code CyclicalMode.TRIANGULAR}, {@code
+         * CyclicalMode.TRIANGULAR2} or {@code CyclicalMode.EXP_RANGE}. Values correspond to
+         * policies detailed above.
+         *
+         * @param mode cyclical mode
+         * @return this {@code Builder}
+         */
+        public Builder optMode(CyclicalMode mode) {
+            this.mode = mode;
+            return this;
+        }
+
+        /**
+         * Constant in {@code CyclicalMode.EXP_RANGE} scaling function. Default: 1.
+         *
+         * @param gamma constant in 'exp_range' scaling function: gamma^cycleIterations
+         * @return this {@code Builder}
+         */
+        public Builder optGamma(float gamma) {
+            this.gamma = gamma;
+            return this;
+        }
+
+        /**
+         * Custom scaling function. If set, {@code mode} will be ignored. Default: null.
+         *
+         * @param scaleFunction Custom scaling function
+         * @return this {@code Builder}
+         */
+        public Builder optScaleFunction(ScaleFunction scaleFunction) {
+            this.scaleFunction = scaleFunction;
+            return this;
+        }
+
+        /**
+         * Defines whether scaling function is evaluated on cycle number or update number. Default:
+         * true.
+         *
+         * @param scaleModeCycle if true then scaling function is evaluated on cycle number, else on
+         *     update number
+         * @return this {@code Builder}
+         */
+        public Builder optScaleModeCycle(boolean scaleModeCycle) {
+            this.scaleModeCycle = scaleModeCycle;
+            return this;
+        }
+
+        /**
+         * Builds a {@link CyclicalTracker} block.
+         *
+         * @return the {@link CyclicalTracker} block
+         */
+        public CyclicalTracker build() {
+            Preconditions.checkArgument(baseValue > 0, "baseValue has to be positive!");
+            Preconditions.checkArgument(maxValue > 0, "maxValue has to be positive!");
+            Preconditions.checkArgument(
+                    baseValue <= maxValue, "baseValue has to lower than maxValue!");
+            Preconditions.checkArgument(stepSizeUp >= 1, "stepSizeUp has to be positive!");
+            Preconditions.checkArgument(stepSizeDown >= 0, "stepSizeUp cannot be negative!");
+            Preconditions.checkArgument(
+                    gamma >= 0f && gamma <= 1f, "gamma has to be between 0 and 1!");
+            return new CyclicalTracker(this);
+        }
+    }
+
+    /**
+     * {@code CyclicalTracker} provides three predefined cyclical modes and can be selected by this
+     * enum.
+     *
+     * <ul>
+     *   <li>TRIANGULAR: A basic triangular cycle without amplitude scaling.
+     *   <li>TRIANGULAR2: A basic triangular cycle that scales initial amplitude by half each cycle.
+     *   <li>EXP_RANGE: A cycle that scales initial amplitude by (gamma<sup>cycleIterations</sup>)
+     *       at each cycle iteration.
+     * </ul>
+     */
+    public enum CyclicalMode {
+        TRIANGULAR,
+        TRIANGULAR2,
+        EXP_RANGE
+    }
+
+    /** {@code ScaleFunction} is an interface to implement a custom scale function. */
+    public interface ScaleFunction {
+        /**
+         * Custom scaling policy. Return value has to satisfy 0&lt;=func(steps)&lt;=1 for all
+         * steps&gt;1.
+         *
+         * @param steps current cycles if {@code scaleModeCycle} is true; input update number if it
+         *     is false
+         * @return scale ratio
+         */
+        float func(int steps);
+    }
+
+    private static class TriangularScaleFunction implements ScaleFunction {
+        @Override
+        public float func(int steps) {
+            return 1f;
+        }
+    }
+
+    private static class Triangular2ScaleFunction implements ScaleFunction {
+        @Override
+        public float func(int steps) {
+            return (float) (1 / (Math.pow(2f, steps - 1)));
+        }
+    }
+
+    private static class ExpRangeScaleFunction implements ScaleFunction {
+        float gamma;
+
+        ExpRangeScaleFunction(float gamma) {
+            this.gamma = gamma;
+        }
+
+        @Override
+        public float func(int steps) {
+            return (float) Math.pow(this.gamma, steps);
+        }
+    }
+}
diff --git a/api/src/main/java/ai/djl/training/tracker/Tracker.java b/api/src/main/java/ai/djl/training/tracker/Tracker.java
index a07c7b3da..913765330 100644
--- a/api/src/main/java/ai/djl/training/tracker/Tracker.java
+++ b/api/src/main/java/ai/djl/training/tracker/Tracker.java
@@ -70,6 +70,16 @@ public interface Tracker {
         return CosineTracker.builder();
     }
 
+    /**
+     * Returns a new instance of {@link ai.djl.training.tracker.CyclicalTracker.Builder} that can
+     * build an {@link CyclicalTracker}.
+     *
+     * @return the {@link CyclicalTracker} {@link ai.djl.training.tracker.CyclicalTracker.Builder}
+     */
+    static CyclicalTracker.Builder cyclical() {
+        return CyclicalTracker.builder();
+    }
+
     /**
      * Returns a new instance of {@link Tracker} with a fixed value.
      *
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java
new file mode 100644
index 000000000..bb27640ca
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java
@@ -0,0 +1,268 @@
+/*
+ * 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.basicdataset.tabular;
+
+import ai.djl.Application.Tabular;
+import ai.djl.basicdataset.BasicDatasets;
+import ai.djl.basicdataset.utils.DynamicBuffer;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.repository.Artifact;
+import ai.djl.repository.MRL;
+import ai.djl.repository.Repository;
+import ai.djl.training.dataset.Record;
+import ai.djl.util.Progress;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVRecord;
+
+/**
+ * Daily Delhi climate dataset from <a
+ * href="https://www.kaggle.com/datasets/sumanthvrao/daily-climate-time-series-data">https://www.kaggle.com/datasets/sumanthvrao/daily-climate-time-series-data</a>.
+ *
+ * <p>The Dataset is fully dedicated for the developers who want to train the model on Weather
+ * Forecasting for Indian climate. This dataset provides data from 1st January 2013 to 24th April
+ * 2017 in the city of Delhi, India. The 4 parameters here are meantemp, humidity, wind_speed,
+ * meanpressure.
+ */
+public class DailyDelhiClimate extends CsvDataset {
+
+    private static final String ARTIFACT_ID = "daily-delhi-climate";
+    private static final String VERSION = "3.0";
+
+    private Usage usage;
+    private MRL mrl;
+    private boolean prepared;
+
+    DailyDelhiClimate(Builder builder) {
+        super(builder);
+        usage = builder.usage;
+        mrl = builder.getMrl();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Record get(NDManager manager, long index) {
+        CSVRecord csvRecord = csvRecords.get(Math.toIntExact(index));
+        NDList data = toNDList(manager, csvRecord, features);
+        NDList label = new NDList();
+
+        return new Record(data, label);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void prepare(Progress progress) throws IOException {
+        if (prepared) {
+            return;
+        }
+
+        Artifact artifact = mrl.getDefaultArtifact();
+        mrl.prepare(artifact, progress);
+
+        Path root = mrl.getRepository().getResourceDirectory(artifact);
+        Path csvFile;
+        switch (usage) {
+            case TRAIN:
+                csvFile = root.resolve("DailyDelhiClimateTrain.csv");
+                break;
+            case TEST:
+                csvFile = root.resolve("DailyDelhiClimateTest.csv");
+                break;
+            case VALIDATION:
+            default:
+                throw new UnsupportedOperationException("Validation data not available.");
+        }
+
+        csvUrl = csvFile.toUri().toURL();
+        super.prepare(progress);
+        prepared = true;
+    }
+
+    /**
+     * Creates a builder to build a {@link DailyDelhiClimate}.
+     *
+     * @return a new builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** A builder to construct a {@link DailyDelhiClimate}. */
+    public static final class Builder extends CsvBuilder<Builder> {
+
+        Repository repository;
+        String groupId;
+        String artifactId;
+        Usage usage;
+        List<String> featureArray =
+                new ArrayList<>(
+                        Arrays.asList(
+                                "date", "meantemp", "humidity", "wind_speed", "meanpressure"));
+
+        /** Constructs a new builder. */
+        Builder() {
+            repository = BasicDatasets.REPOSITORY;
+            groupId = BasicDatasets.GROUP_ID;
+            artifactId = ARTIFACT_ID;
+            usage = Usage.TRAIN;
+            csvFormat =
+                    CSVFormat.DEFAULT
+                            .builder()
+                            .setHeader()
+                            .setSkipHeaderRecord(true)
+                            .setIgnoreHeaderCase(true)
+                            .setTrim(true)
+                            .build();
+        }
+
+        /**
+         * Returns this {code Builder} object.
+         *
+         * @return this {@code BaseBuilder}
+         */
+        @Override
+        public Builder self() {
+            return this;
+        }
+
+        /**
+         * Sets the optional usage.
+         *
+         * @param usage the new usage
+         * @return this builder
+         */
+        public Builder optUsage(Usage usage) {
+            this.usage = usage;
+            return self();
+        }
+
+        /**
+         * Sets the optional repository.
+         *
+         * @param repository the repository
+         * @return this builder
+         */
+        public Builder optRepository(Repository repository) {
+            this.repository = repository;
+            return self();
+        }
+
+        /**
+         * Sets optional groupId.
+         *
+         * @param groupId the groupId}
+         * @return this builder
+         */
+        public Builder optGroupId(String groupId) {
+            this.groupId = groupId;
+            return self();
+        }
+
+        /**
+         * Sets the optional artifactId.
+         *
+         * @param artifactId the artifactId
+         * @return this builder
+         */
+        public Builder optArtifactId(String artifactId) {
+            if (artifactId.contains(":")) {
+                String[] tokens = artifactId.split(":");
+                groupId = tokens[0];
+                this.artifactId = tokens[1];
+            } else {
+                this.artifactId = artifactId;
+            }
+            return self();
+        }
+
+        /**
+         * Adds a feature to the features set.
+         *
+         * @param name the name of the feature
+         * @return this builder
+         */
+        public Builder addFeature(String name) {
+            if ("date".equals(name)) {
+                return addDateFeature(name);
+            } else {
+                return addNumericFeature(name);
+            }
+        }
+
+        /**
+         * Add a new feature of date type. Since the dataset is a time series dataset, the date is
+         * an important feature in String form, and we can't just treat it as a simple categorical
+         * feature.
+         *
+         * @param name the name of the feature
+         * @return this builder
+         */
+        private Builder addDateFeature(String name) {
+            return addFeature(new Feature(name, new DateFeaturizer()));
+        }
+
+        /**
+         * Returns the available features of this dataset.
+         *
+         * @return a list of feature names
+         */
+        public List<String> getAvailableFeatures() {
+            return featureArray;
+        }
+
+        /**
+         * Builds the new {@link DailyDelhiClimate}.
+         *
+         * @return the new {@link DailyDelhiClimate}
+         */
+        @Override
+        public DailyDelhiClimate build() {
+            if (features.isEmpty()) {
+                for (String name : featureArray) {
+                    addFeature(name);
+                }
+            }
+            return new DailyDelhiClimate(this);
+        }
+
+        MRL getMrl() {
+            return repository.dataset(Tabular.ANY, groupId, artifactId, VERSION);
+        }
+    }
+
+    /** A featurizer implemented for feature of date type. */
+    public static final class DateFeaturizer implements Featurizer {
+
+        /**
+         * Featurize the feature of date type to epoch day (the number of days passed since
+         * 1970-01-01) and put it into float buffer, so that it can be used for future training in a
+         * simple way.
+         *
+         * @param buf the float buffer to be filled
+         * @param input the date string in the format {@code yyyy-MM-dd}
+         */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            LocalDate ld = LocalDate.parse(input, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+            long day = ld.toEpochDay();
+            buf.put(day);
+        }
+    }
+}
diff --git a/engines/tensorflow/tensorflow-engine/README.md b/engines/tensorflow/tensorflow-engine/README.md
index 3ab09d45a..ac084cc5c 100644
--- a/engines/tensorflow/tensorflow-engine/README.md
+++ b/engines/tensorflow/tensorflow-engine/README.md
@@ -46,14 +46,14 @@ You can choose a native library based on your platform if you don't have network
 ### macOS
 For macOS, you can use the following library:
 
-- ai.djl.tensorflow:tensorflow-native-cpu:2.4.1:osx-x86_64
+- ai.djl.tensorflow:tensorflow-native-cpu:2.7.0:osx-x86_64
 
 ```xml
 <dependency>
     <groupId>ai.djl.tensorflow</groupId>
     <artifactId>tensorflow-native-cpu</artifactId>
     <classifier>osx-x86_64</classifier>
-    <version>2.4.1</version>
+    <version>2.7.0</version>
     <scope>runtime</scope>
 </dependency>
 ```
@@ -64,21 +64,21 @@ installed on your GPU machine, you can use one of the following library:
 
 #### Linux GPU
 
-- ai.djl.tensorflow:tensorflow-native-cu110:2.4.1:linux-x86_64 - CUDA 11.0
+- ai.djl.tensorflow:tensorflow-native-cu110:2.7.0:linux-x86_64 - CUDA 11.3
 
 ```xml
 <dependency>
     <groupId>ai.djl.tensorflow</groupId>
-    <artifactId>tensorflow-native-cu110</artifactId>
+    <artifactId>tensorflow-native-cu113</artifactId>
     <classifier>linux-x86_64</classifier>
-    <version>2.4.1</version>
+    <version>2.7.0</version>
     <scope>runtime</scope>
 </dependency>
 ```
 
 ### Linux CPU
 
-- ai.djl.tensorflow:tensorflow-native-cpu:2.4.1:linux-x86_64
+- ai.djl.tensorflow:tensorflow-native-cpu:2.7.0:linux-x86_64
 
 ```xml
 <dependency>
@@ -86,7 +86,7 @@ installed on your GPU machine, you can use one of the following library:
     <artifactId>tensorflow-native-cpu</artifactId>
     <classifier>linux-x86_64</classifier>
     <scope>runtime</scope>
-    <version>2.4.1</version>
+    <version>2.7.0</version>
 </dependency>
 ```
 
@@ -96,21 +96,21 @@ For the Windows platform, you can choose between CPU and GPU.
 
 #### Windows GPU
 
-- ai.djl.tensorflow:tensorflow-native-cu110:2.4.1:win-x86_64
+- ai.djl.tensorflow:tensorflow-native-cu113:2.7.0:win-x86_64
 
 ```xml
 <dependency>
     <groupId>ai.djl.tensorflow</groupId>
-    <artifactId>tensorflow-native-cu110</artifactId>
+    <artifactId>tensorflow-native-cu113</artifactId>
     <classifier>win-x86_64</classifier>
-    <version>2.4.1</version>
+    <version>2.7.0</version>
     <scope>runtime</scope>
 </dependency>
 ```
 
 ### Windows CPU
 
-- ai.djl.tensorflow:tensorflow-native-cpu:2.4.1:win-x86_64
+- ai.djl.tensorflow:tensorflow-native-cpu:2.7.0:win-x86_64
 
 ```xml
 <dependency>
@@ -118,6 +118,6 @@ For the Windows platform, you can choose between CPU and GPU.
     <artifactId>tensorflow-native-cpu</artifactId>
     <classifier>win-x86_64</classifier>
     <scope>runtime</scope>
-    <version>2.4.1</version>
+    <version>2.7.0</version>
 </dependency>
 ```
diff --git a/tools/gradle/check.gradle b/tools/gradle/check.gradle
index b953da7e7..08e0d945a 100644
--- a/tools/gradle/check.gradle
+++ b/tools/gradle/check.gradle
@@ -54,7 +54,7 @@ tasks.withType(Checkstyle) {
 
 apply plugin: 'jacoco'
 jacoco {
-    toolVersion = "0.8.5"
+    toolVersion = "0.8.7"
 }
 jacocoTestReport {
     reports {


Test patch:
diff --git a/api/src/test/java/ai/djl/training/LearningRateTest.java b/api/src/test/java/ai/djl/training/LearningRateTest.java
index b2c812b88..791b33c1c 100644
--- a/api/src/test/java/ai/djl/training/LearningRateTest.java
+++ b/api/src/test/java/ai/djl/training/LearningRateTest.java
@@ -14,6 +14,7 @@
 package ai.djl.training;
 
 import ai.djl.training.tracker.CosineTracker;
+import ai.djl.training.tracker.CyclicalTracker;
 import ai.djl.training.tracker.FactorTracker;
 import ai.djl.training.tracker.MultiFactorTracker;
 import ai.djl.training.tracker.Tracker;
@@ -70,4 +71,84 @@ public class LearningRateTest {
         Assert.assertEquals(cosineTracker.getNewValue(200), finalValue);
         Assert.assertEquals(cosineTracker.getNewValue(300), finalValue);
     }
+
+    @Test
+    public void testCyclicalTracker() {
+        float baseValue = 0.01f;
+        float maxValue = 0.1f;
+        double epsilon = 1e-3;
+        CyclicalTracker cyclicalTrackerTriangular =
+                Tracker.cyclical()
+                        .optBaseValue(baseValue)
+                        .optMaxValue(maxValue)
+                        .optMode(CyclicalTracker.CyclicalMode.TRIANGULAR)
+                        .optStepSizeUp(100)
+                        .build();
+        Assert.assertEquals(cyclicalTrackerTriangular.getNewValue(0), baseValue);
+        Assert.assertEquals(cyclicalTrackerTriangular.getNewValue(10), 0.019f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular.getNewValue(50), 0.055f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular.getNewValue(100), 0.1f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular.getNewValue(380), 0.028f, epsilon);
+
+        CyclicalTracker cyclicalTrackerUnbalance =
+                Tracker.cyclical()
+                        .optBaseValue(baseValue)
+                        .optMaxValue(maxValue)
+                        .optMode(CyclicalTracker.CyclicalMode.TRIANGULAR)
+                        .optStepSizeUp(100)
+                        .optStepSizeDown(50)
+                        .build();
+        Assert.assertEquals(cyclicalTrackerUnbalance.getNewValue(0), baseValue);
+        Assert.assertEquals(cyclicalTrackerUnbalance.getNewValue(10), 0.019f, epsilon);
+        Assert.assertEquals(cyclicalTrackerUnbalance.getNewValue(50), 0.055f, epsilon);
+        Assert.assertEquals(cyclicalTrackerUnbalance.getNewValue(130), 0.046f, epsilon);
+        Assert.assertEquals(cyclicalTrackerUnbalance.getNewValue(380), 0.082f, epsilon);
+
+        CyclicalTracker cyclicalTrackerTriangular2 =
+                Tracker.cyclical()
+                        .optBaseValue(baseValue)
+                        .optMaxValue(maxValue)
+                        .optMode(CyclicalTracker.CyclicalMode.TRIANGULAR2)
+                        .optStepSizeUp(100)
+                        .build();
+        Assert.assertEquals(cyclicalTrackerTriangular2.getNewValue(0), baseValue);
+        Assert.assertEquals(cyclicalTrackerTriangular2.getNewValue(50), 0.055f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular2.getNewValue(100), 0.1f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular2.getNewValue(380), 0.019f, epsilon);
+        Assert.assertEquals(cyclicalTrackerTriangular2.getNewValue(460), 0.0235f, epsilon);
+
+        CyclicalTracker cyclicalTrackerExpRange =
+                Tracker.cyclical()
+                        .optBaseValue(baseValue)
+                        .optMaxValue(maxValue)
+                        .optMode(CyclicalTracker.CyclicalMode.EXP_RANGE)
+                        .optGamma(0.999f)
+                        .optStepSizeUp(100)
+                        .build();
+        Assert.assertEquals(cyclicalTrackerExpRange.getNewValue(0), baseValue);
+        Assert.assertEquals(cyclicalTrackerExpRange.getNewValue(50), 0.0528f, epsilon);
+        Assert.assertEquals(cyclicalTrackerExpRange.getNewValue(380), 0.0223f, epsilon);
+
+        CyclicalTracker cyclicalTrackerCustom =
+                Tracker.cyclical()
+                        .optBaseValue(baseValue)
+                        .optMaxValue(maxValue)
+                        .optScaleFunction(new CustomScaleFunction())
+                        .optScaleModeCycle(true)
+                        .optStepSizeUp(100)
+                        .build();
+        Assert.assertEquals(cyclicalTrackerCustom.getNewValue(0), baseValue);
+        Assert.assertEquals(cyclicalTrackerCustom.getNewValue(50), 0.055f, epsilon);
+        Assert.assertEquals(cyclicalTrackerCustom.getNewValue(500), 0.04f, epsilon);
+    }
+
+    private static class CustomScaleFunction implements CyclicalTracker.ScaleFunction {
+        @Override
+        public float func(int steps) {
+            if (steps == 0) {
+                return 1;
+            }
+            return 1f / ((float) steps);
+        }
+    }
 }
diff --git a/basicdataset/src/test/java/ai/djl/basicdataset/DailyDelhiClimateTest.java b/basicdataset/src/test/java/ai/djl/basicdataset/DailyDelhiClimateTest.java
new file mode 100644
index 000000000..4a7fd5b0d
--- /dev/null
+++ b/basicdataset/src/test/java/ai/djl/basicdataset/DailyDelhiClimateTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.basicdataset;
+
+import ai.djl.Model;
+import ai.djl.basicdataset.tabular.DailyDelhiClimate;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.nn.Blocks;
+import ai.djl.nn.Parameter;
+import ai.djl.training.DefaultTrainingConfig;
+import ai.djl.training.Trainer;
+import ai.djl.training.TrainingConfig;
+import ai.djl.training.dataset.Batch;
+import ai.djl.training.dataset.Dataset;
+import ai.djl.training.dataset.Record;
+import ai.djl.training.initializer.Initializer;
+import ai.djl.training.loss.Loss;
+import ai.djl.translate.TranslateException;
+import java.io.IOException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class DailyDelhiClimateTest {
+
+    @Test
+    public void testDailyDelhiClimateRemote() throws IOException, TranslateException {
+        TrainingConfig config =
+                new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
+                        .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
+
+        try (Model model = Model.newInstance("model")) {
+            model.setBlock(Blocks.identityBlock());
+
+            NDManager manager = model.getNDManager();
+            // path of directory
+            DailyDelhiClimate dailyDelhiClimate =
+                    DailyDelhiClimate.builder()
+                            .optUsage(Dataset.Usage.TEST)
+                            .addFeature("humidity")
+                            .addFeature("wind_speed")
+                            .setSampling(32, true)
+                            .build();
+
+            dailyDelhiClimate.prepare();
+
+            long size = dailyDelhiClimate.size();
+            Assert.assertEquals(size, 114);
+
+            Record record = dailyDelhiClimate.get(manager, 0);
+            NDList data = record.getData();
+            NDList labels = record.getLabels();
+            Assert.assertEquals(data.head().toFloatArray(), new float[] {85.86957f, 2.7434783f});
+            Assert.assertEquals(labels.size(), 0);
+
+            try (Trainer trainer = model.newTrainer(config)) {
+                Batch batch = trainer.iterateDataset(dailyDelhiClimate).iterator().next();
+                Assert.assertEquals(batch.getData().size(), 1);
+                Assert.assertEquals(batch.getLabels().size(), 0);
+                batch.close();
+            }
+        }
+    }
+}
diff --git a/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/daily-delhi-climate/metadata.json b/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/daily-delhi-climate/metadata.json
new file mode 100644
index 000000000..59b9ff5c2
--- /dev/null
+++ b/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/daily-delhi-climate/metadata.json
@@ -0,0 +1,34 @@
+{
+  "metadataVersion": "0.2",
+  "resourceType": "dataset",
+  "application": "tabular",
+  "groupId": "ai.djl.basicdataset",
+  "artifactId": "daily-delhi-climate",
+  "name": "daily-delhi-climate",
+  "description": "This dataset provides data from 1st January 2013 to 24th April 2017 in the city of Delhi, India. The 4 parameters here are meantemp, humidity, wind_speed, meanpressure.",
+  "website": "https://www.kaggle.com/datasets/sumanthvrao/daily-climate-time-series-data",
+  "licenses": {
+    "license": {
+      "name": "CC0 1.0 Universal",
+      "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode"
+    }
+  },
+  "artifacts": [
+    {
+      "version": "3.0",
+      "snapshot": false,
+      "files": {
+        "train": {
+          "uri": "3.0/DailyDelhiClimateTrain.csv",
+          "sha1Hash": "7e2b6ebf11015280bfd9308acf49567eca262b94",
+          "size": 78199
+        },
+        "test": {
+          "uri": "3.0/DailyDelhiClimateTest.csv",
+          "sha1Hash": "ca462410900d18037059fde452b4e084ef620d6d",
+          "size": 7100
+        }
+      }
+    }
+  ]
+}
diff --git a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
index 4941ea52a..99304bb88 100644
--- a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
+++ b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
@@ -17,7 +17,6 @@ import ai.djl.MalformedModelException;
 import ai.djl.ModelException;
 import ai.djl.inference.Predictor;
 import ai.djl.modality.Classifications;
-import ai.djl.modality.cv.BufferedImageFactory;
 import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
 import ai.djl.modality.cv.output.BoundingBox;
@@ -38,17 +37,15 @@ public class OCRTest {
 
     @Test
     public void testOCR() throws IOException, ModelException, TranslateException {
-        String url = "https://resources.djl.ai/images/flight_ticket.jpg";
-        Image img = ImageFactory.getInstance().fromUrl(url);
-        DetectedObjects objects = detectWords(img);
-        if (ImageFactory.getInstance() instanceof BufferedImageFactory) {
-            objects = detectWords(img);
-        }
-        List<DetectedObjects.DetectedObject> boxes = objects.items();
-
         // load Character model
+        Predictor<Image, DetectedObjects> detector = getWordDetector();
         Predictor<Image, String> recognizer = getRecognizer();
         Predictor<Image, Classifications> rotator = getRotateClassifer();
+
+        String url = "https://resources.djl.ai/images/flight_ticket.jpg";
+        Image img = ImageFactory.getInstance().fromUrl(url);
+        DetectedObjects objects = detector.predict(img);
+        List<DetectedObjects.DetectedObject> boxes = objects.items();
         for (DetectedObjects.DetectedObject box : boxes) {
             Image subImg = getSubImage(img, box.getBoundingBox());
             if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) {
@@ -63,18 +60,15 @@ public class OCRTest {
         }
     }
 
-    private static DetectedObjects detectWords(Image img)
-            throws ModelException, IOException, TranslateException {
+    private static Predictor<Image, DetectedObjects> getWordDetector()
+            throws ModelNotFoundException, MalformedModelException, IOException {
         Criteria<Image, DetectedObjects> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, DetectedObjects.class)
                         .optArtifactId("ai.djl.paddlepaddle:word_detection")
                         .optFilter("flavor", "mobile")
                         .build();
-        try (ZooModel<Image, DetectedObjects> model = criteria.loadModel();
-                Predictor<Image, DetectedObjects> predictor = model.newPredictor()) {
-            return predictor.predict(img);
-        }
+        return criteria.loadModel().newPredictor();
     }
 
     private static Predictor<Image, String> getRecognizer()
diff --git a/testing/src/main/java/ai/djl/testing/CoverageUtils.java b/testing/src/main/java/ai/djl/testing/CoverageUtils.java
index 6b0eb5869..31053bab4 100644
--- a/testing/src/main/java/ai/djl/testing/CoverageUtils.java
+++ b/testing/src/main/java/ai/djl/testing/CoverageUtils.java
@@ -101,7 +101,12 @@ public final class CoverageUtils {
     private static List<Class<?>> getClasses(Class<?> clazz)
             throws IOException, ReflectiveOperationException, URISyntaxException {
         ClassLoader appClassLoader = ClassLoaderUtils.getContextClassLoader();
-        Field field = appClassLoader.getClass().getDeclaredField("ucp");
+        Field field;
+        try {
+            field = appClassLoader.getClass().getDeclaredField("ucp");
+        } catch (NoSuchFieldException e) {
+            field = appClassLoader.getClass().getSuperclass().getDeclaredField("ucp");
+        }
         field.setAccessible(true);
         Object ucp = field.get(appClassLoader);
         Method method = ucp.getClass().getDeclaredMethod("getURLs");
@@ -131,7 +136,7 @@ public final class CoverageUtils {
 
                 try {
                     classList.add(Class.forName(className, true, cl));
-                } catch (Error ignore) {
+                } catch (Throwable ignore) {
                     // ignore
                 }
             }
@@ -146,7 +151,7 @@ public final class CoverageUtils {
                         fileName = fileName.replace('/', '.');
                         try {
                             classList.add(Class.forName(fileName, true, cl));
-                        } catch (Error ignore) {
+                        } catch (Throwable ignore) {
                             // ignore
                         }
                     }