Model Validation

Training error measures how closely a model fits observations it has already seen. Model validation instead estimates generalization: performance on independent data from the same target population. A low training error alone may only reveal overfitting.

SMILE separates orchestration in smile.validation from scores in smile.validation.metric. Splitters return a Bag record; validation runners fit models, score held-out rows, and collect timing, predictions, confusion matrices, and metrics.

A hard classifier returns only labels. A soft classifier also returns posterior probabilities, which enable AUC, log loss, and multiclass cross entropy. Metrics that do not apply to the classifier type or class count are represented by Double.NaN in ClassificationMetrics.

PackagePurpose
smile.validation Splits, hold-out evaluation, bootstrap, cross-validation, LOOCV, result records, and model-selection criteria
smile.validation.metric Classification, probabilistic classification, regression, and clustering scores
Bag.samples()
Training-row indices. Bootstrap bags may repeat an index because sampling is with replacement.
Bag.oob()
Held-out or out-of-bag row indices used for validation.

Classification Metrics

Classification metrics consume integer ground-truth and predicted labels. Import them from smile.validation.metric.*. Accuracy and error work directly for multiclass data; many rate metrics default to binary labels {0, 1}.

Accuracy and Error

Accuracy is the fraction correctly predicted; Error is the number of incorrect predictions. The Iris class column is named class and has three classes.

import smile.datasets.Iris
import smile.validation.metric.Accuracy
import smile.validation.metric.Error

var iris = new Iris()
int[] truth = iris.y()
int[] prediction = truth.clone()
prediction[0] = 1
prediction[50] = 2
prediction[100] = 0
System.out.printf("accuracy=%.2f, error=%d%n",
    Accuracy.of(truth, prediction), Error.of(truth, prediction))

accuracy=0.98, error=3
      

Precision, Recall, F-Score, and Rates

Precision asks how many predicted positives are correct; recall (the same quantity as sensitivity) asks how many actual positives were found. Specificity is the true-negative rate, fallout is the false-positive rate, and FDR is the false-discovery rate. F-score balances precision and recall; FScore.F1.score(...) is equivalent to FScore.of(truth, prediction, 1.0, null) for binary labels.

For multiclass precision, recall, and F-score, pass Averaging.Macro, Micro, or Weighted. Macro gives each class equal weight, micro pools decisions, and weighted averaging follows class support.

import smile.validation.metric.*

int[] truth =      {0, 0, 1, 1, 1, 0}
int[] prediction = {0, 1, 1, 1, 0, 0}
System.out.printf(
    "P=%.3f R=%.3f F1=%.3f sensitivity=%.3f specificity=%.3f%n" +
    "fallout=%.3f FDR=%.3f MCC=%.3f%n",
    Precision.of(truth, prediction),
    Recall.of(truth, prediction),
    FScore.F1.score(truth, prediction),
    Sensitivity.of(truth, prediction),
    Specificity.of(truth, prediction),
    Fallout.of(truth, prediction),
    FDR.of(truth, prediction),
    MatthewsCorrelation.of(truth, prediction))

P=0.667 R=0.667 F1=0.667
sensitivity=0.667 specificity=0.667 fallout=0.333 FDR=0.333 MCC=0.333
      

MCC and Confusion Matrix

Matthews correlation coefficient summarizes all four binary contingency counts and remains informative under imbalance. ConfusionMatrix.of exposes the complete class-by-class count matrix, with truth on rows and prediction on columns.

import smile.validation.metric.ConfusionMatrix

int[] truth =      {0, 0, 1, 1, 1, 0}
int[] prediction = {0, 1, 1, 1, 0, 0}
System.out.println(ConfusionMatrix.of(truth, prediction))

2 x 2 confusion matrix with rows [2, 1] and [1, 2]
      

ClassificationMetrics Fields

fitTime(), scoreTime()
Mean milliseconds spent fitting and scoring.
size(), error()
Validation row count and misclassification count.
accuracy()
Fraction of correct labels.
sensitivity(), specificity()
Binary true-positive and true-negative rates.
precision(), f1(), mcc()
Binary precision, F1, and Matthews correlation.
auc(), logloss()
Binary probability metrics; NaN for hard classifiers.
crossEntropy()
Multiclass probability loss; NaN when posterior probabilities are unavailable.

Probabilistic Classification Metrics

AUC and log loss accept the probability of binary class 1. AUC measures ranking quality; log loss penalizes confident mistakes. Cross entropy generalizes log loss to a posterior-probability row for every multiclass sample. These scores require a soft classifier or explicitly supplied probabilities.

import smile.validation.metric.AUC
import smile.validation.metric.LogLoss
import smile.validation.metric.CrossEntropy

int[] binaryTruth = {0, 0, 1, 1}
double[] probability = {0.10, 0.40, 0.35, 0.80}
System.out.printf("AUC=%.2f, logloss=%.4f%n",
    AUC.of(binaryTruth, probability),
    LogLoss.of(binaryTruth, probability))

int[] truth = {0, 2, 1}
double[][] posterior = {
    {0.80, 0.10, 0.10},
    {0.05, 0.15, 0.80},
    {0.10, 0.70, 0.20}
}
System.out.printf("crossEntropy=%.4f%n",
    CrossEntropy.of(truth, posterior))

AUC=0.75, logloss=0.4723
crossEntropy=0.2677
      

Regression Metrics

Regression scores compare continuous predictions with true responses. MSE and RMSE emphasize large residuals, MAD is more robust, RSS is the unnormalized squared-error total, and R² reports improvement over predicting the response mean.

import smile.validation.metric.*

double[] truth = {3.0, -0.5, 2.0, 7.0}
double[] prediction = {2.5, 0.0, 2.0, 8.0}
System.out.printf("MSE=%.3f RMSE=%.3f MAD=%.3f RSS=%.3f R2=%.3f%n",
    MSE.of(truth, prediction), RMSE.of(truth, prediction),
    MAD.of(truth, prediction), RSS.of(truth, prediction),
    R2.of(truth, prediction))

MSE=0.375 RMSE=0.612 MAD=0.500 RSS=1.500 R2=0.949
      
fitTime(), scoreTime()
Mean milliseconds spent fitting and scoring.
size()
Number of validation rows.
rss()
Residual sum of squares.
mse(), rmse()
Mean squared error and its square root.
mad()
Mean absolute deviation of predictions from truth.
r2()
Coefficient of determination; higher is better.

Data Splitting

Splitting must happen before fitting preprocessors or models. Bag.split makes one random hold-out split; its DataFrame overload returns Tuple2<DataFrame, DataFrame>. Bag.stratify preserves class proportions.

import smile.datasets.Iris
import smile.validation.Bag

var iris = new Iris()
Bag index = Bag.split(iris.data().nrow(), 0.20)
var split = Bag.split(iris.data(), 0.20)
var stratified = Bag.stratify(iris.data(), "class", 0.20)
System.out.printf("indices=%d/%d, frames=%d/%d, stratified=%d/%d%n",
    index.samples().length, index.oob().length,
    split._1().nrow(), split._2().nrow(),
    stratified._1().nrow(), stratified._2().nrow())

indices=120/30, frames=120/30, stratified=120/30
      

Bootstrap.of samples training rows with replacement and puts never-selected rows in oob(). CrossValidation.of partitions rows into k folds; stratify preserves labels and nonoverlap keeps all rows from a group in one test fold. LOOCV.of returns the training indices for every possible single-row hold-out.

import smile.datasets.Iris
import smile.validation.*

var iris = new Iris()
Bag[] bootstrap = Bootstrap.of(iris.data().nrow(), 5)
Bag[] folds = CrossValidation.of(iris.data().nrow(), 10)
Bag[] stratified = CrossValidation.stratify(iris.y(), 10)
int[] groups = new int[150]
for (int i = 0; i < groups.length; i++) groups[i] = i / 10
Bag[] grouped = CrossValidation.nonoverlap(groups, 5)
int[][] loo = LOOCV.of(iris.data().nrow())
System.out.printf("%d bootstrap, %d folds, %d stratified, %d grouped, %d LOO%n",
    bootstrap.length, folds.length, stratified.length,
    grouped.length, loo.length)

5 bootstrap, 10 folds, 10 stratified, 5 grouped, 150 LOO
      

Hold-out Validation

When a dataset already supplies independent train and test partitions, pass both to ClassificationValidation.of. The result retains the model, labels, predictions, posterior probabilities when available, a confusion matrix, and metrics.

import smile.classification.DecisionTree
import smile.datasets.ImageSegmentation
import smile.validation.ClassificationValidation

var segment = new ImageSegmentation()
var validation = ClassificationValidation.of(
    segment.formula(), segment.train(), segment.test(), DecisionTree::fit)
System.out.println(validation.metrics().accuracy())
System.out.println(validation.confusion())

test accuracy, followed by the 7 × 7 confusion matrix
      

Out-of-Bag Estimates

Out-of-bag evaluation is also an ensemble property. For example, a fitted RandomForest.metrics() after fit reports OOB ClassificationMetrics computed while growing the forest. That estimate is produced by the ensemble itself; it is not a separate smile.validation runner.

Cross-Validation Runners

The runners build every model and return ClassificationValidations. Use avg() and std() for aggregate records and rounds() for individual folds. Stratification is usually preferred for imbalanced classification.

import smile.classification.DecisionTree
import smile.datasets.Iris
import smile.validation.CrossValidation

var iris = new Iris()
var cv = CrossValidation.classification(
    10, iris.formula(), iris.data(), DecisionTree::fit)
var stratified = CrossValidation.stratify(
    10, iris.formula(), iris.data(), DecisionTree::fit)
var repeated = CrossValidation.classification(
    3, 5, iris.formula(), iris.data(), DecisionTree::fit)
System.out.printf("cv=%.3f ± %.3f, stratified=%.3f, repeated rounds=%d%n",
    cv.avg().accuracy(), cv.std().accuracy(),
    stratified.avg().accuracy(), repeated.rounds().size())

10-fold mean ± standard deviation; repeated rounds=15
      

Leave-One-Out Cross-Validation

LOOCV trains once per row and combines all left-out predictions into one metrics record. It can reduce split-size bias but is computationally expensive. LDA.fit is an array API, so the runnable Iris demo uses x() and y() rather than a Formula overload.

import smile.classification.LDA
import smile.datasets.Iris
import smile.validation.LOOCV

var iris = new Iris()
var metrics = LOOCV.classification(iris.x(), iris.y(), LDA::fit)
System.out.println(metrics.accuracy())

Iris leave-one-out accuracy
      

Bootstrap Validation

Bootstrap validation repeatedly samples a training bag with replacement and scores the observations absent from that bag. More rounds stabilize the estimate at the cost of more fits.

import smile.classification.DecisionTree
import smile.datasets.Iris
import smile.validation.Bootstrap

var iris = new Iris()
var result = Bootstrap.classification(
    100, iris.formula(), iris.data(), DecisionTree::fit)
System.out.printf("accuracy=%.3f ± %.3f%n",
    result.avg().accuracy(), result.std().accuracy())

100-bootstrap mean accuracy ± standard deviation
      

Regression Cross-Validation

Regression runners mirror classification runners and return RegressionValidations. This example predicts Abalone rings with a regression tree and reports held-out RMSE.

import smile.datasets.Abalone
import smile.regression.RegressionTree
import smile.validation.CrossValidation

var abalone = new Abalone()
var cv = CrossValidation.regression(
    10, abalone.formula(), abalone.train(), RegressionTree::fit)
System.out.printf("RMSE=%.4f ± %.4f%n",
    cv.avg().rmse(), cv.std().rmse())

10-fold RMSE mean ± standard deviation
      

AIC and BIC

ModelSelection.AIC(logL, k) and ModelSelection.BIC(logL, k, n) balance maximized log-likelihood against parameter count. Lower is preferred among models fit to the same response data. BIC's k log(n) penalty grows more strongly with sample size than AIC's 2k penalty.

import smile.validation.ModelSelection

double logL = -120.0
int parameters = 8
int samples = 500
System.out.printf("AIC=%.2f, BIC=%.2f%n",
    ModelSelection.AIC(logL, parameters),
    ModelSelection.BIC(logL, parameters, samples))

AIC=256.00, BIC=289.72
      

For systematic hyperparameter search around a validation objective, continue to Hyperparameter Optimization.

Validation Pitfalls

Clustering Metrics

Rand index, adjusted Rand index, mutual information, and normalized or adjusted mutual information are covered with clustering workflows on the Clustering page.