Repository: deepjavalibrary/djl
Pull request #1930 (2022-08-19 08:14:44)

Object Detection
This PR is mainly about yolov3 model in ObjectDetection.
1. add Yolov3 Model in basic model zoo and can be trained with a simple loss function for Pytorch engine. And corresponding test.
2. add Yolov3 Loss function, maybe still have some bugs to fix
3. add an example of yolov3Training: TrainPikachuWithYOLOV3
4. About ObjectDetectionDataSet, add a new function getClasses() because for most ObjectDetection Models and Translators, we need to know how many types of objects in the dataset.
5. Add ObjectDetection for DJL-Zero, and due to small bugs for yolov3, currently just train a SingleShotDetection Model.

TODO List:
1. Translator of Yolov3(actually I'm written a demo but it can't pass the code check so I stash it.
2. For COCODataSet, we should update synset.

warthecatalyst: emm,this is just a test pr for my mentor Zack to get a view on my current progress and make suggestions



At 2022-08-29 13:50:35, "Frank Liu" ***@***.***> wrote:

@frankfliu commented on this pull request.

In api/src/main/java/ai/djl/training/loss/SingleShotDetectionLoss.java:

> @@ -48,6 +48,8 @@ public SingleShotDetectionLoss() {
     @Override
     protected Pair<NDList, NDList> inputForComponent(
             int componentIndex, NDList labels, NDList predictions) {
+        System.out.println(labels.singletonOrThrow());  //print labels for test


Remove testing code

In api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java:

> @@ -0,0 +1,275 @@
+package ai.djl.training.loss;


Add license header

In api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java:

> +import ai.djl.modality.cv.output.Rectangle;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDArrays;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.index.NDIndex;
+import ai.djl.ndarray.types.DataType;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.Activation;
+import ai.djl.util.Pair;
+import ai.djl.util.PairList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class YOLOv3Loss extends Loss{

./gradlew formatJava


In api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java:

> +                boxBTrue.get(":,2:").expandDims(0).broadcast(A,B,2));
+        NDArray minXY = NDArrays.maximum(boxATrue.get(":,:2").expandDims(1).broadcast(A,B,2),
+                boxBTrue.get(":,:2").expandDims(0).broadcast(A,B,2));
+
+        NDArray inter = NDArrays.minimum(maxXY.sub(minXY),0);
+        inter = inter.get(":,:,0").mul(inter.get(":,:,1"));
+
+        //to calculate the area of prediction bbox and true bbox
+        NDArray areaA = boxATrue.get(":,2").sub(boxATrue.get(":,0")).mul(boxATrue.get(":,3").sub(boxATrue.get(":,1"))).expandDims( 1).broadcast(inter.getShape()),
+                areaB = boxBTrue.get(":,2").sub(boxBTrue.get(":,0")).mul(boxBTrue.get(":,3").sub(boxBTrue.get(":,1"))).expandDims( 0).broadcast(inter.getShape());
+
+        NDArray union = areaA.add(areaB).sub(inter);
+        return inter.div(union);
+    }
+
+    public ArrayList<PairList<Long,Rectangle>> getTargetFromCurrentLabel(NDArray labels){


Use List instead of ArrayList in function declaration

In api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java:

> +                    float interHeight = Math.min(trueBottom,predBottom)-Math.max(trueTop,predTop);
+                    float inter = interWidth*interHeight, union = wgt.get(i).getFloat()*inW*hgt.get(i).getFloat()*inH
+                            + anchors.get(j,0).getFloat()*anchors.get(j,1).getFloat()-inter;
+                    iou.set(new NDIndex(curIndex),inter/union);
+                }
+            }
+            return new NDList(iou,boxLossScale,groundTruth);
+        }
+
+
+
+        return null;
+    }
+
+    //calculate IOU is already defined in Rectangle
+    public NDArray calculateIOU(NDArray boxA,NDArray boxB){


javadoc

In examples/src/main/java/ai/djl/examples/training/TrainPikachu.java:

> @@ -87,7 +87,7 @@ public static TrainingResult runExample(String[] args) throws IOException, Trans
             try (Trainer trainer = model.newTrainer(config)) {
                 trainer.setMetrics(new Metrics());
 
-                Shape inputShape = new Shape(arguments.getBatchSize(), 3, 256, 256);
+                Shape inputShape = new Shape(1, 3, 256, 256);


Why?

—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>

Patch:
diff --git a/api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java b/api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java
new file mode 100644
index 000000000..462b86e28
--- /dev/null
+++ b/api/src/main/java/ai/djl/training/loss/YOLOv3Loss.java
@@ -0,0 +1,487 @@
+/*
+ * 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.NDArrays;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.index.NDIndex;
+import ai.djl.ndarray.types.DataType;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.Activation;
+
+/**
+ * {@code YOLOv3Loss} is an implementation of {@link Loss}. It is used to compute the loss while
+ * training a YOLOv3 model for object detection. It involves computing the targets given the
+ * generated anchors, labels and predictions, and then computing the sum of class predictions and
+ * bounding box predictions.
+ */
+public final class YOLOv3Loss extends Loss {
+    // TODO: currently not finished, still have some bugs inside and it can only be trained with
+    // PyTorch Engine
+    /*
+       PRESETANCHORS shapes come from the K-means clustering of COCO dataset, which image size is 416*416
+       it can be reshaped into any shape like 256*256, just multiply each value with 256/416
+    */
+    private static final float[] PRESETANCHORS = {
+        116, 90, 156, 198, 373, 326,
+        30, 61, 62, 45, 59, 119,
+        10, 13, 16, 30, 33, 23
+    };
+
+    private float[] anchors;
+    private int numClasses;
+    private int boxAttr;
+    private Shape inputShape;
+    private float ignoreThreshold;
+    private NDManager manager;
+    private static final float EPSILON = 1e-7f;
+
+    private YOLOv3Loss(Builder builder) {
+        super(builder.name);
+        this.anchors = builder.anchorsArray;
+        this.numClasses = builder.numClasses;
+        this.boxAttr = builder.numClasses + 5; // 5 for x,y,h,w,c
+        this.inputShape = builder.inputShape;
+        this.ignoreThreshold = builder.ignoreThreshold;
+    }
+
+    /**
+     * Gets the preset anchors of YoloV3.
+     *
+     * @return the preset anchors of YoloV3
+     */
+    public static float[] getPresetAnchors() {
+        return PRESETANCHORS.clone();
+    }
+
+    /**
+     * Make the value of given NDArray between tMin and tMax.
+     *
+     * @param tList the given NDArray
+     * @param tMin the min value
+     * @param tMax the max value
+     * @return a NDArray where values are set between tMin and tMax
+     */
+    public NDArray clipByTensor(NDArray tList, float tMin, float tMax) {
+        NDArray result = tList.gte(tMin).mul(tList).add(tList.lt(tMin).mul(tMin));
+        result = result.lte(tMax).mul(result).add(result.gt(tMax).mul(tMax));
+        return result;
+    }
+
+    /**
+     * Calculates the MSELoss between prediction and target.
+     *
+     * @param prediction the prediction array
+     * @param target the target array
+     * @return the MSELoss between prediction and target
+     */
+    public NDArray mseLoss(NDArray prediction, NDArray target) {
+        return prediction.sub(target).pow(2);
+    }
+
+    /**
+     * Calculates the BCELoss between prediction and target.
+     *
+     * @param prediction the prediction array
+     * @param target the target array
+     * @return the BCELoss between prediction and target
+     */
+    public NDArray bceLoss(NDArray prediction, NDArray target) {
+        prediction = clipByTensor(prediction, EPSILON, (float) (1.0 - EPSILON));
+        return prediction
+                .log()
+                .mul(target)
+                .add(prediction.mul(-1).add(1).log().mul(target.mul(-1).add(1)))
+                .mul(-1);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public NDArray evaluate(NDList labels, NDList predictions) {
+        manager = predictions.getManager();
+
+        /*
+        three outputs in total
+        NDArray out0 = predictions.get(0),  //Shape = (batchSize * 75 * 13 * 13) 75 = 3*(20+5)
+                out1 = predictions.get(1),  //Shape = (batchSize * 75 * 26 * 26)
+                out2 = predictions.get(2);  //Shape = (batchSize * 75 * 52 * 52)
+        */
+
+        NDArray[] lossComponents = new NDArray[3];
+        for (int i = 0; i < 3; i++) {
+            lossComponents[i] = evaluateOneOutput(i, predictions.get(i), labels.singletonOrThrow());
+        }
+
+        // calculate the final loss
+        return NDArrays.add(lossComponents);
+    }
+
+    /**
+     * Computes the Loss for one outputLayer.
+     *
+     * @param componentIndex which outputLayer does current input represent. the shape should be
+     *     (13*13,26*26,52*52)
+     * @param input one prediction layer of YOLOv3
+     * @param labels target labels. Must contain (offsetLabels, masks, classlabels)
+     * @return the total loss of a outputLayer
+     */
+    public NDArray evaluateOneOutput(int componentIndex, NDArray input, NDArray labels) {
+        int batchSize = (int) input.getShape().get(0);
+        int inW = (int) input.getShape().get(2);
+        int inH = (int) input.getShape().get(3);
+
+        NDArray prediction =
+                input.reshape(batchSize, 3, boxAttr, inW, inH)
+                        .transpose(1, 0, 3, 4, 2); // reshape into (3,batchSize,inW,inH,attrs)
+
+        // the prediction value of x,y,w,h which shape should be (3,batchSize,inW,inH)
+        NDArray x = Activation.sigmoid(prediction.get("...,0"));
+        NDArray y = Activation.sigmoid(prediction.get("...,1"));
+        NDArray w = prediction.get("...,2");
+        NDArray h = prediction.get("...,3");
+
+        // Confidence of whether there is an object and conditional probability of each class
+        // it should be reshaped into (batchSize,3)
+        NDArray conf = Activation.sigmoid(prediction.get("...,4")).transpose(1, 0, 2, 3);
+        NDArray predClass = Activation.sigmoid(prediction.get("...,5:")).transpose(1, 0, 2, 3, 4);
+
+        // get an NDList of groundTruth which contains boxLossScale and groundTruth
+        NDList truthList = getTarget(labels, inH, inW);
+
+        /*
+           boxLossScale shape should be: (batchSize,3,inW,inH)
+           groundTruth shape should be: (3,batchSize,inW,inH,boxAttr)
+        */
+        NDArray boxLossScale = truthList.get(0).transpose(1, 0, 2, 3);
+        NDArray groundTruth = truthList.get(1);
+
+        // iou shape should be: (batchSize,3 ,inW,inH)
+        NDArray iou =
+                calculateIOU(x, y, groundTruth.get("...,0:4"), componentIndex)
+                        .transpose(1, 0, 2, 3);
+
+        // get noObjMask and objMask
+        NDArray noObjMask =
+                NDArrays.where(
+                        iou.lte(ignoreThreshold), manager.ones(iou.getShape()), manager.create(0f));
+
+        NDArray objMask = iou.argMax(1).oneHot(3).transpose(0, 3, 1, 2);
+
+        objMask =
+                NDArrays.where(
+                        iou.gte(ignoreThreshold / 2),
+                        objMask,
+                        manager.zeros(objMask.getShape())); // to get rid of wrong ones
+        noObjMask = NDArrays.where(objMask.eq(1f), manager.zeros(noObjMask.getShape()), noObjMask);
+
+        NDArray xTrue = groundTruth.get("...,0");
+        NDArray yTrue = groundTruth.get("...,1");
+        NDArray wTrue = groundTruth.get("...,2");
+        NDArray hTrue = groundTruth.get("...,3");
+        NDArray classTrue = groundTruth.get("...,4:").transpose(1, 0, 2, 3, 4);
+
+        NDArray widths =
+                manager.create(
+                                new float[] {
+                                    anchors[componentIndex * 6],
+                                    anchors[componentIndex * 6 + 2],
+                                    anchors[componentIndex * 6 + 4]
+                                })
+                        .div(inputShape.get(0));
+
+        NDArray heights =
+                manager.create(
+                                new float[] {
+                                    anchors[componentIndex * 6 + 1],
+                                    anchors[componentIndex * 6 + 3],
+                                    anchors[componentIndex * 6 + 5]
+                                })
+                        .div(inputShape.get(1));
+
+        // three loss parts: box Loss, confidence Loss, and class Loss
+        NDArray boxLoss =
+                objMask.mul(boxLossScale)
+                        .mul(
+                                NDArrays.add(
+                                                xTrue.sub(x).pow(2),
+                                                yTrue.sub(y).pow(2),
+                                                wTrue.sub(
+                                                                w.exp()
+                                                                        .mul(
+                                                                                widths.broadcast(
+                                                                                                inH,
+                                                                                                inW,
+                                                                                                batchSize,
+                                                                                                3)
+                                                                                        .transpose(
+                                                                                                3,
+                                                                                                2,
+                                                                                                1,
+                                                                                                0)))
+                                                        .pow(2),
+                                                hTrue.sub(
+                                                                h.exp()
+                                                                        .mul(
+                                                                                heights.broadcast(
+                                                                                                inH,
+                                                                                                inW,
+                                                                                                batchSize,
+                                                                                                3)
+                                                                                        .transpose(
+                                                                                                3,
+                                                                                                2,
+                                                                                                1,
+                                                                                                0)))
+                                                        .pow(2))
+                                        .transpose(1, 0, 2, 3))
+                        .sum();
+
+        NDArray confLoss =
+                objMask.mul(
+                                conf.add(EPSILON)
+                                        .log()
+                                        .mul(-1)
+                                        .add(bceLoss(predClass, classTrue).sum(new int[] {4})))
+                        .sum();
+
+        NDArray noObjLoss = noObjMask.mul(conf.mul(-1).add(1 + EPSILON).log().mul(-1)).sum();
+
+        return boxLoss.add(confLoss).add(noObjLoss).div(batchSize);
+    }
+
+    /**
+     * Gets target NDArray for a given evaluator.
+     *
+     * @param labels the true labels
+     * @param inH the height of current layer
+     * @param inW the width of current layer
+     * @return an NDList of {boxLossScale and groundTruth}
+     */
+    public NDList getTarget(NDArray labels, int inH, int inW) {
+        int batchSize = (int) labels.size(0);
+
+        // the loss Scale of a box, used to punctuate small boxes
+        NDList boxLossComponents = new NDList();
+
+        // the groundTruth of a true object in pictures
+        NDList groundTruthComponents = new NDList();
+
+        // Shape of labels:(batchSize,objectNum,5)
+
+        for (int batch = 0; batch < batchSize; batch++) {
+            if (labels.get(batch).size(0) == 0) {
+                continue; // no object in current picture
+            }
+
+            NDArray boxLoss = manager.zeros(new Shape(inW, inH), DataType.FLOAT32);
+            NDArray groundTruth = manager.zeros(new Shape(inW, inH, boxAttr - 1), DataType.FLOAT32);
+
+            NDArray picture = labels.get(batch);
+            // the shape should be (objectNums,5)
+            NDArray xgt = picture.get("...,1").add(picture.get("...,3").div(2)).mul(inW);
+            // Center of x should be X value in labels and add half of the width and multiplies the
+            // input width to get which grid cell it's in
+            NDArray ygt = picture.get("...,2").add(picture.get("...,4").div(2)).mul(inH);
+            // Center of y is the same as well
+            NDArray wgt = picture.get("...,3");
+            // the width of the ground truth box
+            NDArray hgt = picture.get("...,4");
+            // the height of the ground truth box
+            // we should transform the presentation of true class, like
+            // [[0],[1],[2]]->[[1,0,0,...0],[0,1,0,...,0],[0,0,1,...,0]]
+            NDArray objectClass = picture.get("...,0");
+            objectClass = objectClass.oneHot(numClasses);
+
+            NDArray curLabel = labels.get(batch); // curLabel shape:(objectNum,5)
+            int objectNum = (int) curLabel.size(0);
+
+            for (int i = 0; i < objectNum; i++) {
+                // for each object, the middle of the object(x and y) should be in one grid cell of
+                // 13*13
+                // the tx and ty should indicate the grid cell and bx and by should indicate the
+                // movement from top-left of the grid cell
+                int tx = (int) xgt.get(i).getFloat();
+                int ty = (int) ygt.get(i).getFloat();
+                float bx = xgt.get(i).getFloat() - tx;
+                float by = ygt.get(i).getFloat() - ty;
+
+                String index = tx + "," + ty;
+                // set groundTruth
+                groundTruth.set(new NDIndex(index + ",0"), bx);
+                groundTruth.set(new NDIndex(index + ",1"), by);
+                groundTruth.set(new NDIndex(index + ",2"), wgt.getFloat(i));
+                groundTruth.set(new NDIndex(index + ",3"), hgt.getFloat(i));
+                groundTruth.set(new NDIndex(index + ",4:"), objectClass.get(i));
+
+                // set boxLoss
+                boxLoss.set(new NDIndex(index), 2 - wgt.getFloat(i) * hgt.getFloat(i));
+            }
+            boxLossComponents.add(boxLoss);
+            groundTruthComponents.add(groundTruth);
+        }
+
+        NDArray boxLossScale = NDArrays.stack(boxLossComponents).broadcast(3, batchSize, inW, inH);
+        NDArray groundTruth =
+                NDArrays.stack(groundTruthComponents)
+                        .broadcast(3, batchSize, inW, inH, boxAttr - 1);
+
+        return new NDList(boxLossScale, groundTruth);
+    }
+
+    /**
+     * Calculates the IOU between priori Anchors and groundTruth.
+     *
+     * @param predx the tx value of prediction
+     * @param predy the ty value of prediction
+     * @param groundTruth the groundTruth value of labels
+     * @param componentIndex the current component Index
+     * @return an NDArray of IOU
+     */
+    public NDArray calculateIOU(
+            NDArray predx, NDArray predy, NDArray groundTruth, int componentIndex) {
+        int inW = (int) predx.getShape().get(2);
+        int inH = (int) predx.getShape().get(3);
+        int strideW = (int) inputShape.get(0) / inW;
+        int strideH = (int) inputShape.get(1) / inH;
+
+        NDList iouComponent = new NDList();
+        // shape of predx, predy should all be (3,batchSize,inW,inH)
+        // shape of groundTruth should be (3,batchSize,inW,inH,4)
+        for (int i = 0; i < 3; i++) {
+            NDArray curPredx = predx.get(i);
+            NDArray curPredy = predy.get(i);
+            float width = anchors[componentIndex * 6 + 2 * i] / strideW;
+            float height = anchors[componentIndex * 6 + 2 * i + 1] / strideH;
+            NDArray predLeft = curPredx.sub(width / 2);
+            NDArray predRight = curPredx.add(width / 2);
+            NDArray predTop = curPredy.sub(height / 2);
+            NDArray predBottom = curPredy.add(height / 2);
+
+            NDArray truth = groundTruth.get(i);
+
+            NDArray trueLeft = truth.get("...,0").sub(truth.get("...,2").mul(inW).div(2));
+            NDArray trueRight = truth.get("...,0").add(truth.get("...,2").mul(inW).div(2));
+            NDArray trueTop = truth.get("...,1").sub(truth.get("...,3").mul(inH).div(2));
+            NDArray trueBottom = truth.get("...,1").add(truth.get("...,3").mul(inH).div(2));
+
+            NDArray left = NDArrays.maximum(predLeft, trueLeft);
+            NDArray right = NDArrays.minimum(predRight, trueRight);
+            NDArray top = NDArrays.maximum(predTop, trueTop);
+            NDArray bottom = NDArrays.minimum(predBottom, trueBottom);
+
+            NDArray inter = right.sub(left).mul(bottom.sub(top));
+            NDArray union =
+                    truth.get("...,2")
+                            .mul(inW)
+                            .mul(truth.get("...,3").mul(inH))
+                            .add(width * height)
+                            .sub(inter)
+                            .add(EPSILON); // should not be divided by zero
+
+            iouComponent.add(inter.div(union));
+        }
+
+        return NDArrays.stack(iouComponent);
+    }
+
+    /**
+     * Creates a new builder to build a {@link YOLOv3Loss}.
+     *
+     * @return a new builder;
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** The Builder to construct a {@link YOLOv3Loss} object. */
+    public static class Builder {
+        private String name = "YOLOv3Loss";
+        private float[] anchorsArray = PRESETANCHORS;
+        private int numClasses = 20;
+        private Shape inputShape = new Shape(419, 419);
+        private float ignoreThreshold = 0.5f;
+
+        /**
+         * Sets the loss name of YoloV3Loss.
+         *
+         * @param name the name of loss function
+         * @return this {@code Builder}
+         */
+        public Builder setName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        /**
+         * Sets the preset anchors for YoloV3.
+         *
+         * @param anchorsArray the anchors in float array
+         * @return this {@code Builder}
+         */
+        public Builder setAnchorsArray(float[] anchorsArray) {
+            if (anchorsArray.length != PRESETANCHORS.length) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "setAnchorsArray requires anchors of length %d, but was given"
+                                        + " filters of length %d instead",
+                                PRESETANCHORS.length, anchorsArray.length));
+            }
+            this.anchorsArray = anchorsArray;
+            return this;
+        }
+
+        /**
+         * Sets the number of total classes.
+         *
+         * @param numClasses the number of total classes
+         * @return this {@code Builder}
+         */
+        public Builder setNumClasses(int numClasses) {
+            this.numClasses = numClasses;
+            return this;
+        }
+
+        /**
+         * Sets the shape of the input picture.
+         *
+         * @param inputShape the shape of input picture.
+         * @return this {@code Builder}
+         */
+        public Builder setInputShape(Shape inputShape) {
+            this.inputShape = inputShape;
+            return this;
+        }
+
+        /**
+         * Sets the ignoreThreshold for iou to check if we think it detects a picture.
+         *
+         * @param ignoreThreshold the ignore threshold
+         * @return this {@code Builder}
+         */
+        public Builder optIgnoreThreshold(float ignoreThreshold) {
+            this.ignoreThreshold = ignoreThreshold;
+            return this;
+        }
+
+        /**
+         * Builds a {@link YOLOv3Loss} instance.
+         *
+         * @return a {@link YOLOv3Loss} instance.
+         */
+        public YOLOv3Loss build() {
+            return new YOLOv3Loss(this);
+        }
+    }
+}
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/cv/BananaDetection.java b/basicdataset/src/main/java/ai/djl/basicdataset/cv/BananaDetection.java
index a9555d353..b86f92078 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/cv/BananaDetection.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/cv/BananaDetection.java
@@ -88,6 +88,12 @@ public class BananaDetection extends ObjectDetectionDataset {
         return new PairList<>(Collections.singletonList(labels.get((int) index)));
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<String> getClasses() {
+        return Collections.singletonList("banana");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected long availableSize() {
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/cv/CocoDetection.java b/basicdataset/src/main/java/ai/djl/basicdataset/cv/CocoDetection.java
index e45abd33e..1f36b4a5e 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/cv/CocoDetection.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/cv/CocoDetection.java
@@ -45,14 +45,13 @@ import java.util.Optional;
  * <p>Each image might have different {@link ai.djl.ndarray.types.Shape}s.
  */
 public class CocoDetection extends ObjectDetectionDataset {
-
+    // TODO: Add synset logic for coco dataset
     private static final String ARTIFACT_ID = "coco";
     private static final String VERSION = "1.0";
 
     private Usage usage;
     private List<Path> imagePaths;
     private List<PairList<Long, Rectangle>> labels;
-
     private MRL mrl;
     private boolean prepared;
 
@@ -79,6 +78,13 @@ public class CocoDetection extends ObjectDetectionDataset {
         return labels.get(Math.toIntExact(index));
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<String> getClasses() {
+        throw new UnsupportedOperationException(
+                "getClasses() for CocoDetection has not been implemented yet.");
+    }
+
     /** {@inheritDoc} */
     @Override
     public void prepare(Progress progress) throws IOException {
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/cv/ObjectDetectionDataset.java b/basicdataset/src/main/java/ai/djl/basicdataset/cv/ObjectDetectionDataset.java
index c48dd669f..84c7eb423 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/cv/ObjectDetectionDataset.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/cv/ObjectDetectionDataset.java
@@ -20,13 +20,13 @@ import ai.djl.util.Pair;
 import ai.djl.util.PairList;
 
 import java.io.IOException;
+import java.util.List;
 
 /**
  * A helper to create {@link ai.djl.training.dataset.Dataset}s for {@link
  * ai.djl.Application.CV#OBJECT_DETECTION}.
  */
 public abstract class ObjectDetectionDataset extends ImageDataset {
-
     /**
      * Creates a new instance of {@link ObjectDetectionDataset} with the given necessary
      * configurations.
@@ -68,4 +68,11 @@ public abstract class ObjectDetectionDataset extends ImageDataset {
      * @throws IOException if the data could not be loaded
      */
     public abstract PairList<Long, Rectangle> getObjects(long index) throws IOException;
+
+    /**
+     * Returns the classes that detected objects in the dataset can be classified into.
+     *
+     * @return the classes that detected objects in the dataset can be classified into.
+     */
+    public abstract List<String> getClasses();
 }
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/cv/PikachuDetection.java b/basicdataset/src/main/java/ai/djl/basicdataset/cv/PikachuDetection.java
index 770858c75..dfdcb175c 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/cv/PikachuDetection.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/cv/PikachuDetection.java
@@ -127,6 +127,12 @@ public class PikachuDetection extends ObjectDetectionDataset {
         return new PairList<>(Collections.singletonList(labels.get((int) index)));
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<String> getClasses() {
+        return Collections.singletonList("pikachu");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected long availableSize() {
diff --git a/djl-zero/src/main/java/ai/djl/zero/cv/ObjectDetection.java b/djl-zero/src/main/java/ai/djl/zero/cv/ObjectDetection.java
new file mode 100644
index 000000000..a10727f65
--- /dev/null
+++ b/djl-zero/src/main/java/ai/djl/zero/cv/ObjectDetection.java
@@ -0,0 +1,137 @@
+/*
+ * 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.zero.cv;
+
+import ai.djl.Model;
+import ai.djl.basicdataset.cv.ObjectDetectionDataset;
+import ai.djl.basicmodelzoo.cv.object_detection.ssd.SingleShotDetection;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.modality.cv.transform.ToTensor;
+import ai.djl.modality.cv.translator.SingleShotDetectionTranslator;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.Block;
+import ai.djl.nn.SequentialBlock;
+import ai.djl.repository.zoo.ZooModel;
+import ai.djl.training.DefaultTrainingConfig;
+import ai.djl.training.EasyTrain;
+import ai.djl.training.Trainer;
+import ai.djl.training.TrainingConfig;
+import ai.djl.training.dataset.Dataset;
+import ai.djl.training.evaluator.BoundingBoxError;
+import ai.djl.training.evaluator.SingleShotDetectionAccuracy;
+import ai.djl.training.listener.TrainingListener;
+import ai.djl.training.loss.SingleShotDetectionLoss;
+import ai.djl.translate.TranslateException;
+import ai.djl.translate.Translator;
+import ai.djl.zero.Performance;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** ObjectDetection takes an image and extract one or more main subjects in the image. */
+public final class ObjectDetection {
+    private ObjectDetection() {}
+
+    /**
+     * Trains the recommended object detection model on a custom dataset. Currently, trains a
+     * SingleShotDetection Model.
+     *
+     * <p>In order to train on a custom dataset, you must create a custom {@link
+     * ObjectDetectionDataset} to load your data.
+     *
+     * @param dataset the data to train with
+     * @param performance to determine the desired model tradeoffs
+     * @return the model as a {@link ZooModel} with the {@link Translator} included
+     * @throws IOException if the dataset could not be loaded
+     * @throws TranslateException if the translator has errors
+     */
+    public static ZooModel<Image, DetectedObjects> train(
+            ObjectDetectionDataset dataset, Performance performance)
+            throws IOException, TranslateException {
+        List<String> classes = dataset.getClasses();
+        int channels = dataset.getImageChannels();
+        int width =
+                dataset.getImageWidth()
+                        .orElseThrow(
+                                () ->
+                                        new IllegalArgumentException(
+                                                "The dataset must have a fixed image width"));
+        int height =
+                dataset.getImageHeight()
+                        .orElseThrow(
+                                () ->
+                                        new IllegalArgumentException(
+                                                "The dataset must have a fixed image height"));
+
+        Shape imageShape = new Shape(channels, height, width);
+
+        Dataset[] splitDataset = dataset.randomSplit(8, 2);
+        Dataset trainDataset = splitDataset[0];
+        Dataset validateDataset = splitDataset[1];
+
+        Block block = getSsdTrainBlock(classes.size());
+        Model model = Model.newInstance("ObjectDetection");
+        model.setBlock(block);
+
+        TrainingConfig trainingConfig =
+                new DefaultTrainingConfig(new SingleShotDetectionLoss())
+                        .addEvaluator(new SingleShotDetectionAccuracy("classAccuracy"))
+                        .addEvaluator(new BoundingBoxError("boundingBoxError"))
+                        .addTrainingListeners(TrainingListener.Defaults.basic());
+
+        try (Trainer trainer = model.newTrainer(trainingConfig)) {
+            trainer.initialize(new Shape(1).addAll(imageShape));
+            EasyTrain.fit(trainer, 50, trainDataset, validateDataset);
+        }
+
+        Translator<Image, DetectedObjects> translator =
+                SingleShotDetectionTranslator.builder()
+                        .addTransform(new ToTensor())
+                        .optSynset(classes)
+                        .optThreshold(0.6f)
+                        .build();
+
+        return new ZooModel<>(model, translator);
+    }
+
+    private static Block getSsdTrainBlock(int numClasses) {
+        int[] numFilters = {16, 32, 64};
+        SequentialBlock baseBlock = new SequentialBlock();
+        for (int numFilter : numFilters) {
+            baseBlock.add(SingleShotDetection.getDownSamplingBlock(numFilter));
+        }
+
+        List<List<Float>> sizes = new ArrayList<>();
+        List<List<Float>> ratios = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            ratios.add(Arrays.asList(1f, 2f, 0.5f));
+        }
+        sizes.add(Arrays.asList(0.2f, 0.272f));
+        sizes.add(Arrays.asList(0.37f, 0.447f));
+        sizes.add(Arrays.asList(0.54f, 0.619f));
+        sizes.add(Arrays.asList(0.71f, 0.79f));
+        sizes.add(Arrays.asList(0.88f, 0.961f));
+
+        return SingleShotDetection.builder()
+                .setNumClasses(numClasses)
+                .setNumFeatures(3)
+                .optGlobalPool(true)
+                .setRatios(ratios)
+                .setSizes(sizes)
+                .setBaseNetwork(baseBlock)
+                .build();
+    }
+}
diff --git a/examples/src/main/java/ai/djl/examples/training/TrainPikachu.java b/examples/src/main/java/ai/djl/examples/training/TrainPikachu.java
index 1c11df399..534f58d85 100644
--- a/examples/src/main/java/ai/djl/examples/training/TrainPikachu.java
+++ b/examples/src/main/java/ai/djl/examples/training/TrainPikachu.java
@@ -134,7 +134,7 @@ public final class TrainPikachu {
                         .optUsage(usage)
                         .optLimit(arguments.getLimit())
                         .optPipeline(pipeline)
-                        .setSampling(arguments.getBatchSize(), true)
+                        .setSampling(1, true)
                         .build();
         pikachuDetection.prepare(new ProgressBar());
 
diff --git a/examples/src/main/java/ai/djl/examples/training/TrainPikachuWithYOLOV3.java b/examples/src/main/java/ai/djl/examples/training/TrainPikachuWithYOLOV3.java
new file mode 100644
index 000000000..adc72e78e
--- /dev/null
+++ b/examples/src/main/java/ai/djl/examples/training/TrainPikachuWithYOLOV3.java
@@ -0,0 +1,113 @@
+/*
+ * 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.examples.training;
+
+import ai.djl.MalformedModelException;
+import ai.djl.Model;
+import ai.djl.basicdataset.cv.PikachuDetection;
+import ai.djl.basicmodelzoo.cv.object_detection.yolo.YOLOV3;
+import ai.djl.engine.Engine;
+import ai.djl.examples.training.util.Arguments;
+import ai.djl.metric.Metrics;
+import ai.djl.modality.cv.transform.ToTensor;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.training.DefaultTrainingConfig;
+import ai.djl.training.EasyTrain;
+import ai.djl.training.Trainer;
+import ai.djl.training.TrainingResult;
+import ai.djl.training.dataset.Dataset;
+import ai.djl.training.dataset.RandomAccessDataset;
+import ai.djl.training.listener.SaveModelTrainingListener;
+import ai.djl.training.listener.TrainingListener;
+import ai.djl.training.loss.YOLOv3Loss;
+import ai.djl.training.util.ProgressBar;
+import ai.djl.translate.Pipeline;
+import ai.djl.translate.TranslateException;
+
+import java.io.IOException;
+
+public final class TrainPikachuWithYOLOV3 {
+    private TrainPikachuWithYOLOV3() {}
+
+    public static void main(String[] args)
+            throws IOException, TranslateException, MalformedModelException {
+        TrainPikachuWithYOLOV3.runExample(args);
+    }
+
+    public static TrainingResult runExample(String[] args) throws IOException, TranslateException {
+        Arguments arguments = new Arguments().parseArgs(args);
+        if (arguments == null) {
+            return null;
+        }
+
+        try (Model model = Model.newInstance("pikachu-yolov3")) {
+            model.setBlock(YOLOV3.builder().setNumClasses(1).build());
+            RandomAccessDataset trainingSet = getDataset(Dataset.Usage.TRAIN, arguments);
+            RandomAccessDataset validateSet = getDataset(Dataset.Usage.TEST, arguments);
+
+            DefaultTrainingConfig config = setupTrainingConfig(arguments);
+
+            try (Trainer trainer = model.newTrainer(config)) {
+                trainer.setMetrics(new Metrics());
+
+                Shape inputShape = new Shape(1, 3, 256, 256);
+                trainer.initialize(inputShape);
+
+                EasyTrain.fit(trainer, arguments.getEpoch(), trainingSet, validateSet);
+
+                return trainer.getTrainingResult();
+            }
+        }
+    }
+
+    private static RandomAccessDataset getDataset(Dataset.Usage usage, Arguments arguments)
+            throws IOException {
+        Pipeline pipeline = new Pipeline(new ToTensor());
+        PikachuDetection pikachuDetection =
+                PikachuDetection.builder()
+                        .optUsage(usage)
+                        .optLimit(arguments.getLimit())
+                        .optPipeline(pipeline)
+                        .setSampling(8, true)
+                        .build();
+        pikachuDetection.prepare(new ProgressBar());
+
+        return pikachuDetection;
+    }
+
+    private static DefaultTrainingConfig setupTrainingConfig(Arguments arguments) {
+        String outputDir = arguments.getOutputDir();
+        SaveModelTrainingListener listener = new SaveModelTrainingListener(outputDir);
+        listener.setSaveModelCallback(
+                trainer -> {
+                    TrainingResult result = trainer.getTrainingResult();
+                    Model model = trainer.getModel();
+                    model.setProperty("Loss", String.format("%.5f", result.getValidateLoss()));
+                });
+
+        float[] anchorsArray = YOLOv3Loss.getPresetAnchors();
+        for (int i = 0; i < anchorsArray.length; i++) {
+            anchorsArray[i] = anchorsArray[i] * 256 / 416; // reshaping into the
+        }
+
+        return new DefaultTrainingConfig(
+                        YOLOv3Loss.builder()
+                                .setNumClasses(1)
+                                .setInputShape(new Shape(256, 256))
+                                .setAnchorsArray(anchorsArray)
+                                .build())
+                .optDevices(Engine.getInstance().getDevices(arguments.getMaxGpus()))
+                .addTrainingListeners(TrainingListener.Defaults.basic())
+                .addTrainingListeners(listener);
+    }
+}
diff --git a/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/YOLOV3.java b/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/YOLOV3.java
new file mode 100644
index 000000000..1a329cf54
--- /dev/null
+++ b/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/YOLOV3.java
@@ -0,0 +1,533 @@
+/*
+ * 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.basicmodelzoo.cv.object_detection.yolo;
+
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.util.NDImageUtils;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDArrays;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.DataType;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.AbstractBlock;
+import ai.djl.nn.Activation;
+import ai.djl.nn.Block;
+import ai.djl.nn.Blocks;
+import ai.djl.nn.ParallelBlock;
+import ai.djl.nn.SequentialBlock;
+import ai.djl.nn.convolutional.Conv2d;
+import ai.djl.nn.core.Linear;
+import ai.djl.nn.norm.BatchNorm;
+import ai.djl.nn.pooling.Pool;
+import ai.djl.training.ParameterStore;
+import ai.djl.util.PairList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@code YOLOV3} contains a generic implementation of yolov3 adapted from
+ * https://github.com/bubbliiiing/yolo3-pytorch (Original author bubbliiiing). Yolov3 is a fast and
+ * accurate model for ObjectDetection tasks.
+ *
+ * @see <a href="https://arxiv.org/abs/1804.02767">for more information and knowledge about
+ *     yolov3</a>
+ */
+public final class YOLOV3 extends AbstractBlock {
+    private static final byte VERSION = 1;
+    private SequentialBlock darkNet53; // backBone of YOLOv3
+
+    private Block lastLayer0;
+    private Block layer0Output;
+
+    private Block lastLayer1Conv;
+    private Block lastLayer1UpSample;
+    private Block lastLayer1;
+    private Block layer1Output;
+
+    private Block lastLayer2Conv;
+    private Block lastLayer2UpSample;
+    private Block lastLayer2;
+    private Block layer2Output;
+
+    static final int[] REPEATS = {
+        1, 2, 8, 8, 4
+    }; // the repeat times of the darkNet53 residual units
+
+    static final int[] FILTERS = {
+        32, 64, 128, 256, 512, 1024
+    }; // the filters of darkNet53 residual units
+
+    private YOLOV3(Builder builder) {
+        super(VERSION);
+        darkNet53 = addChildBlock("darkNet53", darkNet53(builder, true));
+        lastLayer0 =
+                addChildBlock(
+                        "lastLayer0",
+                        makeLastLayers(
+                                FILTERS[4],
+                                FILTERS[5],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+        layer0Output =
+                addChildBlock(
+                        "layer0Output",
+                        makeOutputLayers(
+                                FILTERS[5],
+                                3 * (builder.numClasses + 5),
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+
+        lastLayer1Conv =
+                addChildBlock(
+                        "lastLayer1Conv",
+                        convolutionBlock(256, 1, builder.batchNormMomentum, builder.leakyAlpha));
+        lastLayer1UpSample = addChildBlock("lastLayer1UpSample", upSampleBlockNearest());
+        lastLayer1 =
+                addChildBlock(
+                        "lastLayer1",
+                        makeLastLayers(
+                                FILTERS[3],
+                                FILTERS[4],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+        layer1Output =
+                addChildBlock(
+                        "layer1Output",
+                        makeOutputLayers(
+                                FILTERS[4],
+                                3 * (builder.numClasses + 5),
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+
+        lastLayer2Conv =
+                addChildBlock(
+                        "lastLayer2Conv",
+                        convolutionBlock(128, 1, builder.batchNormMomentum, builder.leakyAlpha));
+        lastLayer2UpSample = addChildBlock("lastLayer2UpSample", upSampleBlockNearest());
+        lastLayer2 =
+                addChildBlock(
+                        "lastLayer2",
+                        makeLastLayers(
+                                FILTERS[2],
+                                FILTERS[3],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+        layer2Output =
+                addChildBlock(
+                        "layer2Output",
+                        makeOutputLayers(
+                                FILTERS[3],
+                                3 * (builder.numClasses + 5),
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+    }
+
+    /**
+     * Builds a {@link Block} that represents an upSampleLayer(the nearest mode) for yolov3.
+     *
+     * @return a {@link Block} that represent an upSampleLayer for yolov3
+     */
+    public static Block upSampleBlockNearest() {
+        // transpose + upSample + transpose
+        return new SequentialBlock()
+                .addSingleton(array -> array.transpose(0, 2, 3, 1))
+                .addSingleton(
+                        array ->
+                                NDImageUtils.resize(
+                                        array,
+                                        (int) (array.getShape().get(1) * 2),
+                                        (int) (array.getShape().get(2) * 2),
+                                        Image.Interpolation.NEAREST))
+                .addSingleton(array -> array.transpose(0, 3, 1, 2));
+    }
+
+    /**
+     * Builds a {@link Block} that represents a conv-bn-leakyRelu unit for darkNet53.
+     *
+     * @param filters the number of filters for conv
+     * @param kernel the kernel size for conv
+     * @param batchNormMomentum the momentum for batchNorm layer
+     * @param leakyAlpha the alpha for leakyRelu activation
+     * @return a {@link Block} that represents a conv-bn-leakyRelu unit for darkNet53
+     */
+    public static Block convolutionBlock(
+            int filters, int kernel, float batchNormMomentum, float leakyAlpha) {
+        int pad = 0;
+        if (kernel > 0) {
+            pad = (kernel - 1) >> 1;
+        }
+        return new SequentialBlock()
+                .add(
+                        Conv2d.builder()
+                                .setFilters(filters)
+                                .setKernelShape(new Shape(kernel, kernel))
+                                .optPadding(new Shape(pad, pad))
+                                .build())
+                .add(BatchNorm.builder().optEpsilon(2E-5f).optMomentum(batchNormMomentum).build())
+                .add(Activation.leakyReluBlock(leakyAlpha));
+    }
+
+    /**
+     * Builds a {@link Block} that represents the feature head in yolov3.
+     *
+     * @param filtersIn the number of input filters
+     * @param filtersOut the number of output filters
+     * @param batchNormMomentum the momentum of batchNorm layer
+     * @param leakyAlpha the alpha value for leakyRelu activation
+     * @return a {@link Block} that represents the feature head in yolov3.
+     */
+    public static Block makeLastLayers(
+            int filtersIn, int filtersOut, float batchNormMomentum, float leakyAlpha) {
+        return new SequentialBlock()
+                .add(convolutionBlock(filtersIn, 1, batchNormMomentum, leakyAlpha))
+                .add(convolutionBlock(filtersOut, 3, batchNormMomentum, leakyAlpha))
+                .add(convolutionBlock(filtersIn, 1, batchNormMomentum, leakyAlpha))
+                .add(convolutionBlock(filtersOut, 3, batchNormMomentum, leakyAlpha))
+                .add(convolutionBlock(filtersIn, 1, batchNormMomentum, leakyAlpha));
+    }
+
+    /**
+     * Builds a {@link Block} that represents the output layer of yolov3.
+     *
+     * @param filtersOut the number of output filters
+     * @param outClass the number of output classes
+     * @param batchNormMomentum the momentum for batchNorm layer
+     * @param leakyAlpha the alpha for leakyRelu activation
+     * @return a {@link Block} that represents the output layer of yolov3.
+     */
+    public static Block makeOutputLayers(
+            int filtersOut, int outClass, float batchNormMomentum, float leakyAlpha) {
+        return new SequentialBlock()
+                .add(convolutionBlock(filtersOut, 3, batchNormMomentum, leakyAlpha))
+                .add(Conv2d.builder().setFilters(outClass).setKernelShape(new Shape(1, 1)).build());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected NDList forwardInternal(
+            ParameterStore parameterStore,
+            NDList inputs,
+            boolean training,
+            PairList<String, Object> params) {
+        NDList xList = darkNet53.forward(parameterStore, inputs, training);
+        NDArray x0 = xList.get(7);
+        NDArray x1 = xList.get(6);
+        NDArray x2 = xList.get(5);
+
+        // the first feature layer
+        NDList out0Branch = lastLayer0.forward(parameterStore, new NDList(x0), training);
+        NDList out0 = layer0Output.forward(parameterStore, out0Branch, training);
+        NDList x1In = lastLayer1Conv.forward(parameterStore, out0Branch, training);
+        x1In = lastLayer1UpSample.forward(parameterStore, x1In, training);
+        x1In = new NDList(x1In.singletonOrThrow().concat(x1, 1));
+
+        NDList out1Branch = lastLayer1.forward(parameterStore, x1In, training);
+        NDList out1 = layer1Output.forward(parameterStore, out1Branch, training);
+
+        NDList x2In = lastLayer2Conv.forward(parameterStore, out1Branch, training);
+        x2In = lastLayer2UpSample.forward(parameterStore, x2In, training);
+        x2In = new NDList(x2In.singletonOrThrow().concat(x2, 1));
+
+        // the third feature layer
+        NDList out2 = lastLayer2.forward(parameterStore, x2In, training);
+        out2 = layer2Output.forward(parameterStore, out2, training);
+
+        // Outputs
+        return new NDList(
+                out0.singletonOrThrow(), out1.singletonOrThrow(), out2.singletonOrThrow());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Shape[] getOutputShapes(Shape[] inputShapes) {
+        Shape[] current = inputShapes;
+        Shape[] outputs = new Shape[3];
+        Shape[] darkNetOutputs = new Shape[8];
+        int index = 0;
+        for (String name : children.keys()) {
+            Block block = children.get(name);
+            if (name.contains("darkNet")) {
+                darkNetOutputs = block.getOutputShapes(current);
+                current = new Shape[] {darkNetOutputs[7]};
+            } else if (name.contains("lastLayer")) {
+                if ("05lastLayer1UpSample".equals(name)) {
+                    current = block.getOutputShapes(current);
+                    current =
+                            new Shape[] {
+                                new Shape(
+                                        current[0].get(0),
+                                        current[0].get(1) + darkNetOutputs[6].get(1),
+                                        current[0].get(2),
+                                        current[0].get(3))
+                            };
+                } else if ("09lastLayer2UpSample".equals(name)) {
+                    current = block.getOutputShapes(current);
+                    current =
+                            new Shape[] {
+                                new Shape(
+                                        current[0].get(0),
+                                        current[0].get(1) + darkNetOutputs[5].get(1),
+                                        current[0].get(2),
+                                        current[0].get(3))
+                            };
+                } else {
+                    current = block.getOutputShapes(current);
+                }
+            } else if (!name.contains("Output")) {
+                current = block.getOutputShapes(current);
+            } else { // name.contains("Output")
+                Shape[] output = block.getOutputShapes(current);
+                outputs[index++] = output[0];
+            }
+        }
+        return outputs;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void initializeChildBlocks(NDManager manager, DataType dataType, Shape... inputShapes) {
+        Shape[] current = inputShapes;
+        Shape[] darkNetOutputs = new Shape[8];
+        for (String name : children.keys()) {
+            Block block = children.get(name);
+            block.initialize(manager, dataType, current);
+            if (name.contains("darkNet")) {
+                darkNetOutputs = block.getOutputShapes(current);
+                current = new Shape[] {darkNetOutputs[7]};
+            } else if (name.contains("lastLayer")) {
+                if ("05lastLayer1UpSample".equals(name)) {
+                    current = block.getOutputShapes(current);
+                    current =
+                            new Shape[] {
+                                new Shape(
+                                        current[0].get(0),
+                                        current[0].get(1) + darkNetOutputs[6].get(1),
+                                        current[0].get(2),
+                                        current[0].get(3))
+                            };
+                } else if ("09lastLayer2UpSample".equals(name)) {
+                    current = block.getOutputShapes(current);
+                    current =
+                            new Shape[] {
+                                new Shape(
+                                        current[0].get(0),
+                                        current[0].get(1) + darkNetOutputs[5].get(1),
+                                        current[0].get(2),
+                                        current[0].get(3))
+                            };
+                } else {
+                    current = block.getOutputShapes(current);
+                }
+            } else if (!name.contains("Output")) {
+                current = block.getOutputShapes(current);
+            } else { // name.contains("Output")
+                Shape[] output = block.getOutputShapes(current);
+            }
+        }
+    }
+
+    /**
+     * Builds a {@link Block} that a basic residual block unit used in DarkNet53.
+     *
+     * @param filters the output filter of the Convolutional Layer
+     * @param batchNormMomentum the momentum used for computing batchNorm
+     * @param leakyAlpha the alpha used in LeakyRelu Function
+     * @return a basic residual block unit used in DarkNet53
+     */
+    public static Block basicBlock(int filters, float batchNormMomentum, float leakyAlpha) {
+        SequentialBlock block = new SequentialBlock();
+        block.add(Conv2d.builder().setFilters(filters / 2).setKernelShape(new Shape(1, 1)).build())
+                .add(BatchNorm.builder().optEpsilon(2E-5f).optMomentum(batchNormMomentum).build())
+                .add(Activation.leakyReluBlock(leakyAlpha))
+                .add(
+                        Conv2d.builder()
+                                .setFilters(filters)
+                                .setKernelShape(new Shape(3, 3))
+                                .optPadding(new Shape(1, 1))
+                                .build())
+                .add(BatchNorm.builder().optEpsilon(2E-5f).optMomentum(batchNormMomentum).build())
+                .add(Activation.leakyReluBlock(leakyAlpha));
+        return new ParallelBlock(
+                list ->
+                        new NDList(
+                                NDArrays.add(
+                                        list.get(0).singletonOrThrow(),
+                                        list.get(1).singletonOrThrow())),
+                Arrays.asList(block, Blocks.identityBlock()));
+    }
+
+    /**
+     * Creates repeated Residual Blocks used in DarkNet53.
+     *
+     * @param filters the output filters of the final Convolutional Layer
+     * @param repeats the repeat times of a residual unit
+     * @param batchNormMomentum the momentum used for computing batchNorm
+     * @param leakyAlpha the alpha used in LeakyRelu Function
+     * @return several repeats of a residual block
+     */
+    public static Block makeLayer(
+            int filters, int repeats, float batchNormMomentum, float leakyAlpha) {
+        List<Block> layer = new ArrayList<>();
+        SequentialBlock convolutionalLayer = new SequentialBlock();
+        convolutionalLayer
+                .add(
+                        Conv2d.builder()
+                                .setFilters(filters)
+                                .setKernelShape(new Shape(3, 3))
+                                .optStride(new Shape(2, 2))
+                                .optPadding(new Shape(1, 1))
+                                .build())
+                .add(BatchNorm.builder().optEpsilon(2E-5f).optMomentum(batchNormMomentum).build())
+                .add(Activation.leakyReluBlock(leakyAlpha));
+        for (int i = 0; i < repeats; i++) {
+            layer.add(basicBlock(filters, batchNormMomentum, leakyAlpha));
+        }
+        return new SequentialBlock().add(convolutionalLayer).addAll(layer);
+    }
+
+    private static SequentialBlock darkNet53(Builder builder, boolean setReturnIntermediate) {
+        SequentialBlock darkNet53 = new SequentialBlock();
+        darkNet53.setReturnIntermediate(setReturnIntermediate); // return interMediate results;
+        darkNet53
+                .add(
+                        Conv2d.builder()
+                                .setFilters(FILTERS[0])
+                                .optPadding(new Shape(1, 1))
+                                .setKernelShape(new Shape(3, 3))
+                                .build())
+                .add(
+                        BatchNorm.builder()
+                                .optEpsilon(2E-5f)
+                                .optMomentum(builder.batchNormMomentum)
+                                .build())
+                .add(Activation.leakyReluBlock(builder.leakyAlpha))
+                .add(
+                        makeLayer(
+                                FILTERS[1],
+                                REPEATS[0],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha))
+                .add(
+                        makeLayer(
+                                FILTERS[2],
+                                REPEATS[1],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha))
+                .add(
+                        makeLayer(
+                                FILTERS[3],
+                                REPEATS[2],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha))
+                .add(
+                        makeLayer(
+                                FILTERS[4],
+                                REPEATS[3],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha))
+                .add(
+                        makeLayer(
+                                FILTERS[5],
+                                REPEATS[4],
+                                builder.batchNormMomentum,
+                                builder.leakyAlpha));
+        return darkNet53;
+    }
+
+    /**
+     * Creates a builder to build a {@link YOLOV3}.
+     *
+     * @return a new builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** The Builder to construct a {@link YOLOV3} object. */
+    public static final class Builder {
+        int numClasses = 20;
+        float batchNormMomentum = 0.9f;
+        float leakyAlpha = 0.1f;
+        int darkNetOutSize = 10;
+
+        /**
+         * Sets the number of classes for yolov3.
+         *
+         * @param numClasses the number of classes
+         * @return @return this {@code Builder}
+         */
+        public Builder setNumClasses(int numClasses) {
+            this.numClasses = numClasses;
+            return this;
+        }
+
+        /**
+         * Sets the momentum for batchNorm layer.
+         *
+         * @param batchNormMomentum the momentum for batchNorm layer
+         * @return this {@code Builder}
+         */
+        public Builder optBatchNormMomentum(float batchNormMomentum) {
+            this.batchNormMomentum = batchNormMomentum;
+            return this;
+        }
+
+        /**
+         * Sets the alpha for leakyRelu activation.
+         *
+         * @param leakyAlpha the alpha for leakyRelu activation
+         * @return this {@code Builder}
+         */
+        public Builder optLeakyAlpha(float leakyAlpha) {
+            this.leakyAlpha = leakyAlpha;
+            return this;
+        }
+
+        /**
+         * Sets the out size of darkNet for testing.
+         *
+         * @param darkNetOutSize the out size of darkNet
+         * @return this {@code Builder}
+         */
+        public Builder optDarkNetOutSize(int darkNetOutSize) {
+            this.darkNetOutSize = darkNetOutSize;
+            return this;
+        }
+
+        /**
+         * Builds a {@link YOLOV3} block.
+         *
+         * @return a {@link YOLOV3} block
+         */
+        public Block build() {
+            return new YOLOV3(this);
+        }
+
+        /**
+         * Builds a {@link Block} that represents the backbone of yolov3, which is called DarkNet53.
+         * This can be used for testing and transfer learning.
+         *
+         * @return a {@link Block} that represents darkNet53
+         */
+        public Block buildDarkNet() {
+            Block block = darkNet53(this, false);
+            return new SequentialBlock()
+                    .add(block)
+                    .add(Pool.globalAvgPool2dBlock())
+                    .add(Linear.builder().setUnits(darkNetOutSize).build());
+        }
+    }
+}
diff --git a/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/package-info.java b/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/package-info.java
new file mode 100644
index 000000000..5b25239de
--- /dev/null
+++ b/model-zoo/src/main/java/ai/djl/basicmodelzoo/cv/object_detection/yolo/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Contains classes for the {@link ai.djl.Application.CV#OBJECT_DETECTION} models in the {@link
+ * ai.djl.basicmodelzoo.BasicModelZoo}.
+ */
+package ai.djl.basicmodelzoo.cv.object_detection.yolo;


Test patch:
diff --git a/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/YOLOv3Test.java b/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/YOLOv3Test.java
new file mode 100644
index 000000000..3eef4f6c6
--- /dev/null
+++ b/integration/src/main/java/ai/djl/integration/tests/model_zoo/object_detection/YOLOv3Test.java
@@ -0,0 +1,153 @@
+/*
+ * 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.integration.tests.model_zoo.object_detection;
+
+import ai.djl.Model;
+import ai.djl.basicmodelzoo.cv.object_detection.yolo.YOLOV3;
+import ai.djl.engine.Engine;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.Block;
+import ai.djl.nn.Parameter;
+import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
+import ai.djl.training.DefaultTrainingConfig;
+import ai.djl.training.EasyTrain;
+import ai.djl.training.Trainer;
+import ai.djl.training.TrainingConfig;
+import ai.djl.training.dataset.Batch;
+import ai.djl.training.initializer.Initializer;
+import ai.djl.training.loss.Loss;
+import ai.djl.training.loss.YOLOv3Loss;
+import ai.djl.translate.Batchifier;
+import ai.djl.util.PairList;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class YOLOv3Test {
+    @Test
+    public void testDarkNet() {
+        TrainingConfig config =
+                new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
+                        .optDevices(Engine.getInstance().getDevices(2))
+                        .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
+        Block darkNet = YOLOV3.builder().buildDarkNet();
+        try (Model model = Model.newInstance("darkNet")) {
+            model.setBlock(darkNet);
+            try (Trainer trainer = model.newTrainer(config)) {
+                int batchSize = 1;
+                Shape inputShape = new Shape(batchSize, 3, 224, 224);
+                trainer.initialize(inputShape);
+                NDManager manager = trainer.getManager();
+                NDArray input = manager.ones(inputShape);
+                NDArray label = manager.ones(new Shape(batchSize, 1));
+                Batch batch =
+                        new Batch(
+                                manager.newSubManager(),
+                                new NDList(input),
+                                new NDList(label),
+                                batchSize,
+                                Batchifier.STACK,
+                                Batchifier.STACK,
+                                0,
+                                0);
+                PairList<String, Parameter> parameters = darkNet.getParameters();
+                EasyTrain.trainBatch(trainer, batch);
+                trainer.step();
+
+                // expected kernelShapes
+                NDArray expectedAtIndex0 =
+                        manager.ones(new Shape(32, 3, 3, 3)); // 32*3*3*3 for first layer
+                NDArray expectedAtIndex6 =
+                        manager.ones(new Shape(64, 32, 3, 3)); // 64*32*3*3 for second layer
+                NDArray expectedAtIndex120 =
+                        manager.ones(new Shape(128, 256, 1, 1)); // 128*256*1*1  for 20th layer
+                NDArray expectedLinear =
+                        manager.ones(
+                                new Shape(
+                                        10,
+                                        1024)); // outSize = 10, linear layer size should be 10*1024
+
+                // test if the shape of the kernel sizes are right
+                Assertions.assertAlmostEquals(
+                        parameters.get(0).getValue().getArray(), expectedAtIndex0);
+                Assertions.assertAlmostEquals(
+                        parameters.get(6).getValue().getArray(), expectedAtIndex6);
+                Assertions.assertAlmostEquals(
+                        parameters.get(120).getValue().getArray(), expectedAtIndex120);
+                Assertions.assertAlmostEquals(
+                        parameters.get(312).getValue().getArray(), expectedLinear);
+            }
+        }
+    }
+
+    @Test
+    public void testYoloV3() {
+        TestRequirements.engine("PyTorch");
+
+        float[] anchorsArray = YOLOv3Loss.getPresetAnchors();
+        for (int i = 0; i < anchorsArray.length; i++) {
+            anchorsArray[i] = anchorsArray[i] * 256 / 416; // reshaping into the
+        }
+        TrainingConfig config =
+                new DefaultTrainingConfig(
+                                YOLOv3Loss.builder()
+                                        .setNumClasses(1)
+                                        .setInputShape(new Shape(256, 256))
+                                        .setAnchorsArray(anchorsArray)
+                                        .build())
+                        .optDevices(Engine.getInstance().getDevices(2))
+                        .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
+
+        Block yolov3 = YOLOV3.builder().setNumClasses(1).build();
+        try (Model model = Model.newInstance("yolov3")) {
+            model.setBlock(yolov3);
+            try (Trainer trainer = model.newTrainer(config)) {
+                int batchSize = 4;
+                Shape inputShape = new Shape(batchSize, 3, 256, 256);
+                trainer.initialize(inputShape);
+                NDManager manager = trainer.getManager();
+                NDArray input = manager.ones(inputShape);
+                NDArray label = manager.zeros(new Shape(batchSize, 1, 5));
+                Batch batch =
+                        new Batch(
+                                manager.newSubManager(),
+                                new NDList(input),
+                                new NDList(label),
+                                batchSize,
+                                Batchifier.STACK,
+                                Batchifier.STACK,
+                                0,
+                                0);
+                PairList<String, Parameter> parameters = yolov3.getParameters();
+                EasyTrain.trainBatch(trainer, batch);
+                trainer.step();
+
+                // expected shapes
+                Shape expectedAtIndex312 =
+                        new Shape(512, 1024, 1, 1); // the layer where output0 concat to 1
+                Shape expectedAtIndex330 =
+                        new Shape(1024, 512, 3, 3); // the layer where output1 concat to 2
+
+                // test if the shape of the kernel sizes are right
+                Assert.assertEquals(
+                        parameters.get(312).getValue().getArray().getShape(), expectedAtIndex312);
+                Assert.assertEquals(
+                        parameters.get(330).getValue().getArray().getShape(), expectedAtIndex330);
+            }
+        }
+    }
+}