docs: @Override
      public void eval(INDArray labels, INDArray predictions, INDArray mask, List<? extends Serializable> recordMetaData) {
  
          Triple<INDArray, INDArray, INDArray> p = BaseEvaluation.reshapeAndExtractNotMasked(labels, predictions, mask, axis);
          if (p == null) {
              //All values masked out; no-op
              return;
          }
          INDArray labels2d = p.getFirst();
          INDArray predictions2d = p.getSecond();
  
          if (labels2d.rank() == 3 && predictions2d.rank() == 3) {
              //Assume time series input -> reshape to 2d
              evalTimeSeries(labels2d, predictions2d);
          }
          if (labels2d.rank() > 2 || predictions2d.rank() > 2 || labels2d.size(1) != predictions2d.size(1)
                  || labels2d.size(1) > 2) {
              throw new IllegalArgumentException("Invalid input data shape: labels shape = "
                      + Arrays.toString(labels2d.shape()) + ", predictions shape = "
                      + Arrays.toString(predictions2d.shape()) + "; require rank 2 array with size(1) == 1 or 2");
          }
  
          if (labels2d.dataType() != predictions2d.dataType())
              labels2d = labels2d.castTo(predictions2d.dataType());
  
          //Check for NaNs in predictions - without this, evaulation could silently be intepreted as class 0 prediction due to argmax
          long count = Nd4j.getExecutioner().execAndReturn(new MatchCondition(predictions2d, Conditions.isNan())).getFinalResult().longValue();
          org.nd4j.base.Preconditions.checkState(count == 0, "Cannot perform evaluation with NaN(s) present:" +
                  " %s NaN(s) present in predictions INDArray", count);
  
          double step = 1.0 / thresholdSteps;
          boolean singleOutput = labels2d.size(1) == 1;
  
          if (isExact) {
              //Exact approach: simply add them to the storage for later computation/use
  
              if (probAndLabel == null) {
                  //Do initial allocation
                  val initialSize = Math.max(labels2d.size(0), exactAllocBlockSize);
                  probAndLabel = Nd4j.create(DataType.DOUBLE, new long[]{initialSize, 2}, 'c'); //First col: probability of class 1. Second col: "is class 1"
              }
  
              //Allocate a larger array if necessary
              if (exampleCount + labels2d.size(0) >= probAndLabel.size(0)) {
                  val newSize = probAndLabel.size(0) + Math.max(exactAllocBlockSize, labels2d.size(0));
                  INDArray newProbAndLabel = Nd4j.create(DataType.DOUBLE, new long[]{newSize, 2}, 'c');
                  if (exampleCount > 0) {
                      //If statement to handle edge case: no examples, but we need to re-allocate right away
                      newProbAndLabel.get(interval(0, exampleCount), all()).assign(
                              probAndLabel.get(interval(0, exampleCount), all()));
                  }
                  probAndLabel = newProbAndLabel;
              }
  
              //put values
              INDArray probClass1;
              INDArray labelClass1;
              if (singleOutput) {
                  probClass1 = predictions2d;
                  labelClass1 = labels2d;
              } else {
                  probClass1 = predictions2d.getColumn(1,true);
                  labelClass1 = labels2d.getColumn(1,true);
              }
              val currMinibatchSize = labels2d.size(0);
              probAndLabel.get(interval(exampleCount, exampleCount + currMinibatchSize),
                      NDArrayIndex.point(0)).assign(probClass1);
  
              probAndLabel.get(interval(exampleCount, exampleCount + currMinibatchSize),
                      NDArrayIndex.point(1)).assign(labelClass1);
  
              int countClass1CurrMinibatch = labelClass1.sumNumber().intValue();
              countActualPositive += countClass1CurrMinibatch;
              countActualNegative += labels2d.size(0) - countClass1CurrMinibatch;
          } else {
              //Thresholded approach
              INDArray positivePredictedClassColumn;
              INDArray positiveActualClassColumn;
              INDArray negativeActualClassColumn;
  
              if (singleOutput) {
                  //Single binary variable case
                  positiveActualClassColumn = labels2d;
                  negativeActualClassColumn = labels2d.rsub(1.0); //1.0 - label
                  positivePredictedClassColumn = predictions2d;
              } else {
                  //Standard case - 2 output variables (probability distribution)
                  positiveActualClassColumn = labels2d.getColumn(1,true);
                  negativeActualClassColumn = labels2d.getColumn(0,true);
                  positivePredictedClassColumn = predictions2d.getColumn(1,true);
              }
  
              //Increment global counts - actual positive/negative observed
              countActualPositive += positiveActualClassColumn.sumNumber().intValue();
              countActualNegative += negativeActualClassColumn.sumNumber().intValue();
  
              //Here: calculate true positive rate (TPR) vs. false positive rate (FPR) at different threshold
  
              INDArray ppc = null;
              INDArray itp = null;
              INDArray ifp = null;
              for (int i = 0; i <= thresholdSteps; i++) {
                  double currThreshold = i * step;
  
                  //Work out true/false positives - do this by replacing probabilities (predictions) with 1 or 0 based on threshold
                  Condition condGeq = Conditions.greaterThanOrEqual(currThreshold);
                  Condition condLeq = Conditions.lessThanOrEqual(currThreshold);
  
                  if (ppc == null) {
                      ppc = positivePredictedClassColumn.dup(positiveActualClassColumn.ordering());
                  } else {
                      ppc.assign(positivePredictedClassColumn);
                  }
                  Op op = new CompareAndSet(ppc, 1.0, condGeq);
                  INDArray predictedClass1 = Nd4j.getExecutioner().exec(op);
                  op = new CompareAndSet(predictedClass1, 0.0, condLeq);
                  predictedClass1 = Nd4j.getExecutioner().exec(op);
  
  
                  //True positives: occur when positive predicted class and actual positive actual class...
                  //False positive occurs when positive predicted class, but negative actual class
                  INDArray isTruePositive; // = predictedClass1.mul(positiveActualClassColumn); //If predicted == 1 and actual == 1 at this threshold: 1x1 = 1. 0 otherwise
                  INDArray isFalsePositive; // = predictedClass1.mul(negativeActualClassColumn); //If predicted == 1 and actual == 0 at this threshold: 1x1 = 1. 0 otherwise
                  if (i == 0) {
                      isTruePositive = predictedClass1.mul(positiveActualClassColumn);
                      isFalsePositive = predictedClass1.mul(negativeActualClassColumn);
                      itp = isTruePositive;
                      ifp = isFalsePositive;
                  } else {
                      isTruePositive = Nd4j.getExecutioner().exec(new OldMulOp(predictedClass1, positiveActualClassColumn, itp));
                      isFalsePositive = Nd4j.getExecutioner().exec(new OldMulOp(predictedClass1, negativeActualClassColumn, ifp));
                  }
  
                  //Counts for this batch:
                  int truePositiveCount = isTruePositive.sumNumber().intValue();
                  int falsePositiveCount = isFalsePositive.sumNumber().intValue();
  
                  //Increment counts for this thold
                  CountsForThreshold thresholdCounts = counts.get(currThreshold);
                  thresholdCounts.incrementTruePositive(truePositiveCount);
                  thresholdCounts.incrementFalsePositive(falsePositiveCount);
              }
          }
  
          exampleCount += labels2d.size(0);
          auc = null;
          auprc = null;
          rocCurve = null;
          prCurve = null;
      }