docs: @Override
      public void eval(INDArray labels, INDArray predictions, INDArray mask, final 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();
          INDArray maskArray = p.getThird();
          Preconditions.checkState(maskArray == null, "Per-output masking for Evaluation is not supported");
  
          //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 NaNs present in predictions:" +
                  " %s NaNs present in predictions INDArray", count);
  
          // Add the number of rows to numRowCounter
          numRowCounter += labels2d.size(0);
  
          if(labels2d.dataType() != predictions2d.dataType())
              labels2d = labels2d.castTo(predictions2d.dataType());
  
          // If confusion is null, then Evaluation was instantiated without providing the classes -> infer # classes from
          if (confusion == null) {
              int nClasses = labels2d.columns();
              if (nClasses == 1)
                  nClasses = 2; //Binary (single output variable) case
              if(labelsList == null || labelsList.isEmpty()) {
                  labelsList = new ArrayList<>(nClasses);
                  for (int i = 0; i < nClasses; i++)
                      labelsList.add(String.valueOf(i));
              }
              createConfusion(nClasses);
          }
  
          // Length of real labels must be same as length of predicted labels
          if (!Arrays.equals(labels2d.shape(),predictions2d.shape())) {
              throw new IllegalArgumentException("Unable to evaluate. Predictions and labels arrays are not same shape." +
                      " Predictions shape: " + Arrays.toString(predictions2d.shape()) + ", Labels shape: " + Arrays.toString(labels2d.shape()));
          }
  
          // For each row get the most probable label (column) from prediction and assign as guessMax
          // For each row get the column of the true label and assign as currMax
  
          final int nCols = labels2d.columns();
          final int nRows = labels2d.rows();
  
          if (nCols == 1) {
              INDArray binaryGuesses = predictions2d.gt(binaryDecisionThreshold == null ? 0.5 : binaryDecisionThreshold).castTo(predictions.dataType());
  
              INDArray notLabel = labels2d.rsub(1.0); //Invert entries (assuming 1 and 0)
              INDArray notGuess = binaryGuesses.rsub(1.0);
              //tp: predicted = 1, actual = 1
              int tp = labels2d.mul(binaryGuesses).castTo(DataType.INT).sumNumber().intValue();
              //fp: predicted = 1, actual = 0
              int fp = notLabel.mul(binaryGuesses).castTo(DataType.INT).sumNumber().intValue();
              //fn: predicted = 0, actual = 1
              int fn = notGuess.mul(labels2d).castTo(DataType.INT).sumNumber().intValue();
              int tn = nRows - tp - fp - fn;
  
              confusion().add(1, 1, tp);
              confusion().add(1, 0, fn);
              confusion().add(0, 1, fp);
              confusion().add(0, 0, tn);
  
              truePositives.incrementCount(1, tp);
              falsePositives.incrementCount(1, fp);
              falseNegatives.incrementCount(1, fn);
              trueNegatives.incrementCount(1, tn);
  
              truePositives.incrementCount(0, tn);
              falsePositives.incrementCount(0, fn);
              falseNegatives.incrementCount(0, fp);
              trueNegatives.incrementCount(0, tp);
  
              if (recordMetaData != null) {
                  for (int i = 0; i < binaryGuesses.size(0); i++) {
                      if (i >= recordMetaData.size())
                          break;
                      int actual = labels2d.getDouble(0) == 0.0 ? 0 : 1;
                      int predicted = binaryGuesses.getDouble(0) == 0.0 ? 0 : 1;
                      addToMetaConfusionMatrix(actual, predicted, recordMetaData.get(i));
                  }
              }
  
          } else {
              INDArray guessIndex;
              if (binaryDecisionThreshold != null) {
                  if (nCols != 2) {
                      throw new IllegalStateException("Binary decision threshold is set, but number of columns for "
                                      + "predictions is " + nCols
                                      + ". Binary decision threshold can only be used for binary " + "prediction cases");
                  }
  
                  INDArray pClass1 = predictions2d.getColumn(1);
                  guessIndex = pClass1.gt(binaryDecisionThreshold);
              } else if (costArray != null) {
                  //With a cost array: do argmax(cost * probability) instead of just argmax(probability)
                  guessIndex = Nd4j.argMax(predictions2d.mulRowVector(costArray.castTo(predictions2d.dataType())), 1);
              } else {
                  //Standard case: argmax
                  guessIndex = Nd4j.argMax(predictions2d, 1);
              }
              INDArray realOutcomeIndex = Nd4j.argMax(labels2d, 1);
              val nExamples = guessIndex.length();
  
              for (int i = 0; i < nExamples; i++) {
                  int actual = (int) realOutcomeIndex.getDouble(i);
                  int predicted = (int) guessIndex.getDouble(i);
                  confusion().add(actual, predicted);
  
                  if (recordMetaData != null && recordMetaData.size() > i) {
                      Object m = recordMetaData.get(i);
                      addToMetaConfusionMatrix(actual, predicted, m);
                  }
  
                  // instead of looping through each label for confusion
                  // matrix, instead infer those values by determining if true/false negative/positive,
                  // then just add across matrix
  
                  // if actual == predicted, then it's a true positive, assign true negative to every other label
                  if (actual == predicted) {
                      truePositives.incrementCount(actual, 1);
                      for (int col = 0; col < nCols; col++) {
                          if (col == actual) {
                              continue;
                          }
                          trueNegatives.incrementCount(col, 1); // all cols prior
                      }
                  } else {
                      falsePositives.incrementCount(predicted, 1);
                      falseNegatives.incrementCount(actual, 1);
  
                      // first determine intervals for adding true negatives
                      int lesserIndex, greaterIndex;
                      if (actual < predicted) {
                          lesserIndex = actual;
                          greaterIndex = predicted;
                      } else {
                          lesserIndex = predicted;
                          greaterIndex = actual;
                      }
  
                      // now loop through intervals
                      for (int col = 0; col < lesserIndex; col++) {
                          trueNegatives.incrementCount(col, 1); // all cols prior
                      }
                      for (int col = lesserIndex + 1; col < greaterIndex; col++) {
                          trueNegatives.incrementCount(col, 1); // all cols after
                      }
                      for (int col = greaterIndex + 1; col < nCols; col++) {
                          trueNegatives.incrementCount(col, 1); // all cols after
                      }
                  }
              }
          }
  
          if (nCols > 1 && topN > 1) {
              //Calculate top N accuracy
              //TODO: this could be more efficient
              INDArray realOutcomeIndex = Nd4j.argMax(labels2d, 1);
              val nExamples = realOutcomeIndex.length();
              for (int i = 0; i < nExamples; i++) {
                  int labelIdx = (int) realOutcomeIndex.getDouble(i);
                  double prob = predictions2d.getDouble(i, labelIdx);
                  INDArray row = predictions2d.getRow(i);
                  int countGreaterThan = (int) Nd4j.getExecutioner()
                                  .exec(new MatchCondition(row, Conditions.greaterThan(prob)))
                                  .getDouble(0);
                  if (countGreaterThan < topN) {
                      //For example, for top 3 accuracy: can have at most 2 other probabilities larger
                      topNCorrectCount++;
                  }
                  topNTotalCount++;
              }
          }
      }