rml / Data and metrics

Data and metrics

rml/data gets examples into a training loop: datasets, a seeded loader, preprocessing fitted on the training rows and leak-free splits. rml/metrics scores what comes out: hard-label, regression, threshold-free ranking and probability metrics, each checked against scikit-learn's own documented examples.

On this page
  1. Datasets and the loader
  2. Why epoch e has its own generator
  3. Preprocessing
  4. Splits and cross-validation
  5. How the metrics read their inputs
  6. Classification metrics
  7. Worked example
  8. Regression metrics
  9. Ranking metrics: ROC and precision-recall
  10. Probability metrics and calibration
  11. Who uses it
  12. Tests

In short: a Dataset is a list of examples, each a fixed number of tensors, and a DataLoader walks it in batches by stacking each field along a new axis 0. Shuffling draws epoch e's order from Generator(seed, e), so any epoch can be replayed without running the ones before it. Scalers and the imputer work column by column on f32 [N, D] matrices and are fitted on the training split only; splits and k-folds are plain index lists, optionally stratified by class. The metrics copy their inputs into flat vectors of doubles, compute in double, and return a float. They follow scikit-learn's conventions: a zero denominator gives 0, micro-averaged scores equal accuracy, and average precision is the step-wise sum rather than an interpolated area. The thirteen training demos use the dataset and loader; nothing in the library or the examples calls preprocessing, splits or metrics yet.

The concepts are on /ml-models: data loading and batching, preprocessing, missing data, class imbalance, data splits, cross-validation, classification metrics, regression metrics and calibration.

Datasets and the loader

rml::data has three libraries, dataset, preprocessing and split, each depending only on rml/core and, for the two that shuffle, rml/random (BUILD.bazel). The header's usage comment is the whole loop (dataset.h L79–85): construct a loader once, then per epoch call StartEpoch(epoch) and Next(batch) until it returns false.

From tensors to batches A TensorDataset holds fields such as features [N, D] and labels [N] that share their first dimension. A split produces train and test index lists, and a Subset restricts the dataset to one of them. The DataLoader's StartEpoch(e) builds the order: a permutation drawn from Generator(seed, e) when shuffling, otherwise 0 to n minus 1. Each Next call takes the next batch_size indices of that order and calls GetBatch, which for a TensorDataset copies those rows of every field with GatherRows, giving a batch of [B, D] and [B]. A scaler is fitted on the training rows only and then applied to every split. TensorDataset x [N, D], y [N] Split / KFold train, test index lists Subset(base, train) maps i to indices[i] DataLoader::StartEpoch(e) order = Permutation(n, Generator(seed, e)), position = 0 shuffle = false order = 0, 1, …, n − 1 Next(batch): GetBatch(order[pos, pos + B)) drop_last stops before a short final batch GatherRows per field batch: [B, D], [B] Preprocessing sits beside this: Fit() a scaler or imputer on the training rows, then Transform() train, validation and test alike (preprocessing.h L9–10).
The path from tensors to a batch. Only the loader shuffles; splits shuffle once when they are made, with their own generator.
ItemType/SignatureMeaningSource
GatherRowsTensor(const Tensor& x, span<const int64_t> rows)Copies the chosen rows along axis 0 into a new contiguous [rows.size(), x.shape[1:]…] tensor with one memcpy per row, for any non-quantized dtype. A strided view is made contiguous first; an out-of-range row dies.dataset.cc L36–53
StackTensor(span<const Tensor>)Stacks equally shaped, same-dtype tensors along a new axis 0.L55–68
Datasetsize(), Get(index), virtual GetBatch(indices)The interface. The default GetBatch calls Get for each index and stacks each field; in-memory datasets override it.dataset.h L28–38, .cc L70–82
TensorDatasetexplicit TensorDataset(vector<Tensor> fields)Examples are the rows of tensors that share their first dimension. Get returns Select(0, index) views; GetBatch is one GatherRows per field.dataset.cc L84–105
SubsetSubset(const Dataset& base, vector<int64_t> indices)One side of a split: maps its own indices into the base's, which must outlive it, and forwards a whole batch to the base's GetBatch.L107–129
DataLoaderOptionsbatch_size = 32, shuffle = true, drop_last = false, seed = 0drop_last drops a final short batch so every batch has the same shape.dataset.h L71–77
DataLoader(const Dataset&, DataLoaderOptions = {}); num_batches(), StartEpoch(epoch), bool Next(vector<Tensor>&)The constructor calls StartEpoch(0). num_batches is n / B with drop_last, otherwise ⌈n / B⌉. Next fills the batch and advances, or returns false at the end of the epoch.dataset.cc L131–165

Why epoch e has its own generator

StartEpoch builds a fresh random::Generator(seed, epoch) and draws random::Permutation(size, gen) from it (dataset.cc L143–153). Philox is counter-based, so the epoch number picks an independent stream instead of continuing one shared stream: epoch 1's order is the same whether or not epoch 0 ran first, and training resumed from a checkpoint replays exactly (dataset.h L7–11). The test ShuffleIsAPermutationFixedPerEpoch checks exactly that with two loaders. How the streams work is on Random numbers.

Each batch is a fresh copy: GatherRows allocates its output, so a batch never aliases the dataset and the model can do anything with it. The training loop that consumes the batch is on Losses and optimizers.

Preprocessing

Every transformer checks for an f32 rank-2 matrix and, after Fit, that the column count matches (preprocessing.cc L18–27); calling Transform before Fit dies with "call Fit() first". The scalers are written with the broadcasting ops from rml/core, so a [D] statistic applies to every row.

ItemType/SignatureMeaningSource
StandardScalerFit(x), Transform(x), InverseTransform(z); mean(), stddev()(x − mean) / std per column, with the population standard deviation (divide by N). A constant column gets std 1, so it maps to 0 instead of dividing by zero.preprocessing.cc L36–51
MinMaxScalerexplicit MinMaxScaler(float lo = 0, float hi = 1)lo + (x − min) / (max − min) · (hi − lo) per column; a constant column's range becomes 1, so it maps to lo. lo < hi is checked.L53–72
MeanImputerFit(x), Transform(x); fill()Each column's mean over its non-NaN training values, summed in double (0 for a column with none). Transform clones the input and replaces every NaN with its column's fill.L74–106
OneHotTensor(const Tensor& labels, int64_t num_classes)i32 labels of any shape [...] to f32 [..., num_classes] with a single 1 per row.L108–123
BalancedClassWeightsTensor(const Tensor& labels, int64_t num_classes)Per-class loss weights n / (num_classes · countc) for 1-D i32 labels, so each class contributes equally in total; a class that never appears gets 0.L125–146

For example, labels {0, 0, 0, 1} with two classes give weights 4 / (2 · 3) = 0.667 and 4 / (2 · 1) = 2: three class-0 rows and one class-1 row then each total 2.

Splits and cross-validation

A Split is two index lists, train and test (split.h L19–22). The header's rule is to split before fitting anything, scalers included, since statistics over the test rows leak them into training (L8–9). The indices feed a Subset or GatherRows.

FunctionSignatureWhat it doesSource
TrainTestSplitSplit(int64_t n, double test_fraction, Generator&)Permutes 0..n−1 and holds out the first round(n · test_fraction). The fraction must be in [0, 1].split.cc L62–71
StratifiedSplitSplit(const Tensor& labels, double test_fraction, Generator&)Groups the 1-D i32 labels by class (in label order), shuffles each group, and holds out round(size · fraction) of each, so rare classes appear on both sides.L24–41, L73–83
KFoldvector<Split>(int64_t n, int k, Generator* gen = nullptr)Cuts the order into k contiguous chunks, the first n % k one larger; fold i tests on chunk i and trains on the rest. A null generator keeps 0..n−1. Needs 2 ≤ k ≤ n.L85–102
StratifiedKFoldvector<Split>(const Tensor& labels, int k, Generator&)Deals every shuffled class round-robin into the k chunks, continuing where the previous class stopped, so fold sizes stay within one of each other and each fold holds about 1/k of every class.L104–118

Both k-fold variants finish in FoldsFromChunks, which records each index's fold, sorts each test chunk, and lists the remaining indices in increasing order as that fold's training set (split.cc L43–58). The two single splits keep their shuffled order.

How the metrics read their inputs

rml::metrics has four public libraries and one private helper, inputs, which every metric goes through (inputs.cc L12–34): LabelVector checks for a rank-1 i32 tensor, ValueVector takes every element of an f32 tensor of any shape as a double, and BinaryLabelVector also checks the labels are 0 or 1. So all arithmetic is in double, inputs can be strided views, and the result is converted to float once at the end. Nothing here is differentiable; for training objectives see the losses.

Classification metrics

Everything starts from the confusion matrix C, where C[i][j] counts true class i predicted as j (classification.h L7–13). Precision for class c is C[c][c] over column c's sum, recall is C[c][c] over row c's sum, and any ratio with a zero denominator is 0, as in scikit-learn (SafeRatio, classification.cc L17–19).

ItemType/SignatureMeaningSource
ConfusionMatrixTensor(y_true, y_pred, int num_classes)i32 [num_classes, num_classes], rows true, columns predicted; an out-of-range label dies.classification.cc L34–47
Accuracyfloat(y_true, y_pred)The fraction of matches; needs at least one example.L49–57
BalancedAccuracyfloat(y_true, y_pred, int num_classes)The mean recall over the classes that appear in y_true, robust to imbalance.L59–68
ClassScores, PerClassScores{precision, recall, f1, support}; vector<ClassScores>(const Tensor& confusion)Per-class scores from a square i32 confusion matrix; support is the number of true examples of the class..h L30–38, .cc L70–89
AveragekBinary, kMicro, kMacro, kWeightedThe positive class alone; pooled counts; the unweighted mean of per-class scores; the support-weighted mean..h L40–45
PrecisionRecallF1ClassScores(y_true, y_pred, num_classes, Average = kMacro, int positive_class = 1)Micro returns accuracy for all three, since every false positive for one class is a false negative for another. Macro and weighted F1 average the per-class F1 values, as scikit-learn does, rather than taking the F1 of the averaged precision and recall..cc L91–122
FBetafloat(precision, recall, beta)(1 + β²) P R / (β² P + R); β > 1 weights recall more.L124–128
MatthewsCorrelationfloat(y_true, y_pred, int num_classes)Gorodkin's RK: (c·s − Σ tkpk) / √((s² − Σ tk²)(s² − Σ pk²)) with true counts t, predicted counts p, c correct of s. +1 perfect, 0 chance, −1 inverse; 0 when a side is constant.L130–158

Worked example

The tests use ten labels over three classes (classification_test.cc L18–20): y_true = {0,0,0,1,1,1,1,2,2,2}, y_pred = {0,1,0,1,1,2,1,2,0,2}.

true \ predicted012recall
02102/3
10313/4
21022/3
precision2/33/42/3
  • Accuracy is 7/10 = 0.7, and so is every micro-averaged score.
  • Macro precision, recall and F1 are (2/3 + 3/4 + 2/3) / 3 = 0.6944, scikit-learn's value; balanced accuracy is the same mean of recalls.
  • Weighted recall is (3 · 2/3 + 4 · 3/4 + 3 · 2/3) / 10 = 0.7.
  • For MCC, t = p = (3, 4, 3), so Σ tkpk = Σ tk² = 34, and RK = (7 · 10 − 34) / (100 − 34) = 36/66 ≈ 0.545 (L83–90).

Regression metrics

Inputs are f32 tensors of equal shape, of any rank, compared element by element; at least one value is needed (regression.cc L19–23).

FunctionDefinitionSource
MeanSquaredError, RootMeanSquaredErrormean (y − ŷ)², and its square root.regression.cc L47–56
MeanAbsoluteErrormean |y − ŷ|, robust to outliers.L58–63
MeanAbsolutePercentageErrormean |y − ŷ| / max(|y|, ε), with ε the double machine epsilon, as a fraction rather than a percentage.L65–73
MedianAbsoluteErrorThe median of |y − ŷ|, averaging the middle two for an even count.L75–83
R2Score1 − SSres / SStot: 1 perfect, 0 no better than predicting the mean, negative when worse. For a constant y_true it returns 1 for a perfect prediction and 0 otherwise, as scikit-learn does.L38–43, L85–95
ExplainedVariance1 − Var(y − ŷ) / Var(y): R² without penalizing a constant bias.L97–102

With scikit-learn's documentation example y = {3, −0.5, 2, 7}, ŷ = {2.5, 0, 2, 8}, the errors are 0.5, 0.5, 0 and 1: MSE 0.375, MAE 0.5, median absolute error 0.5, R² 0.9486 and explained variance 0.9572 (regression_test.cc L12–26). Shifting every prediction by one, {4, 0.5, 3, 8}, leaves explained variance at exactly 1.

Ranking metrics: ROC and precision-recall

A score can be cut at any threshold t, predicting positive when score ≥ t. Every curve here comes from one sweep (ranking.cc L25–56): stable-sort the scores in decreasing order, start with a point at threshold +∞ where nothing is positive, then walk down counting true and false positives. Tied scores move together, so a point is emitted only after the last of a tie, which is why ties count half in the ROC area.

The ROC sweep for the scikit-learn example Labels 0, 0, 1, 1 with scores 0.1, 0.4, 0.35 and 0.8. Sorted by decreasing score the labels are 1 at 0.8, 0 at 0.4, 1 at 0.35 and 0 at 0.1. The sweep gives five points (FPR, TPR): (0, 0) at threshold infinity, (0, 0.5) at 0.8, (0.5, 0.5) at 0.4, (0.5, 1) at 0.35 and (1, 1) at 0.1. The staircase encloses an area of 0.75 under it, above the dashed chance diagonal, which is the ROC AUC. 0 0.5 1 false positive rate 0 0.5 1 true positive rate AUC 0.75 threshold label TP, FP FPR, TPR +∞ — 0, 0 0, 0 0.8 1 1, 0 0, 0.5 0.4 0 1, 1 0.5, 0.5 0.35 1 2, 1 0.5, 1 0.1 0 2, 2 1, 1 Precision at each point is TP / (TP + FP): 1, 1, 0.5, 2/3, 0.5, so average precision is 0.5 · 1 + 0.5 · 2/3 = 0.8333.
scikit-learn's documentation example, used by ranking_test.cc (L24–37, L71–85). Each row of the sweep is one point of both curves.
FunctionReturnsMeaningSource
RocCurveRoc{fpr, tpr, thresholds}TP and FP counts divided by the totals; a rate is NaN when its class is absent.ranking.cc L68–80
RocAucfloatThe trapezoid area under the ROC curve, which equals the probability that a random positive outranks a random negative, ties counting half. NaN unless both classes are present.L82–88
PrecisionRecallCurvePrecisionRecall{precision, recall, thresholds}Starts at (recall 0, precision 1), then TP / (TP + FP) and TP / positives at each threshold.L90–102
AveragePrecisionfloatΣ (Rn − Rn−1) Pn: each threshold's precision weighted by the recall it adds. This is scikit-learn's average_precision_score; it does not interpolate, so it is not optimistic. NaN without positives.L104–112
PrAucfloatThe trapezoid area under the precision-recall curve. It interpolates linearly between points and can overstate performance; the header says to prefer AveragePrecision. For the example it is 0.5 + 0.25 · (0.5 + 2/3) ≈ 0.792..h L45–48, .cc L114–118

The precision-recall curve suits rare positives better than ROC, because precision ignores the many easy negatives (ranking.h L7–16). A test computes the ROC AUC as the fraction of winning positive-negative pairs on random data and checks the two agree to 10−6 (MatchesPairwiseRanking).

Probability metrics and calibration

These take i32 labels and probs that are either rank 1, the probability of class 1 for binary labels, or [n, num_classes] rows (calibration.h L14–16). A private Rows helper expands rank-1 input to rows [1 − p, p], so one code path serves both (calibration.cc L16–48).

ItemType/SignatureMeaningSource
LogLossfloat(y_true, probs, double eps = 1e-15)mean −log p(true class), with p clamped to [eps, 1 − eps] so a confident mistake costs a large but finite amount.calibration.cc L52–60
BrierScorefloat(y_true, probs)The mean squared distance between the probability row and the one-hot label, summed over classes. For rank-1 input the expansion counts each error twice, so the total is halved, giving mean (p − y)², scikit-learn's brier_score_loss.L62–75
ReliabilityBin{lower, upper, confidence, accuracy, count}Confidences in (lower, upper]; the first bin also takes 0. Empty bins report 0 confidence and accuracy.calibration.h L34–40
ReliabilityDiagramvector<ReliabilityBin>(y_true, probs, int num_bins = 15)Top-label reliability: each prediction is its most likely class (the first on a tie), with that probability as its confidence, placed in bin ⌈conf · B⌉ − 1 clamped to [0, B − 1].calibration.cc L77–111
ExpectedCalibrationErrorfloat(y_true, probs, int num_bins = 15)Σb (nb / n) |accuracyb − confidenceb| over the reliability bins (Guo et al., 2017).L113–122

In the test with four binary rows and four bins (calibration_test.cc L46–62), the top-label confidences are 0.9 (right), 0.7 (right), 0.65 (wrong) and 0.8 (wrong). Bin (0.5, 0.75] holds 0.7 and 0.65: confidence 0.675, accuracy 0.5. Bin (0.75, 1] holds 0.9 and 0.8: confidence 0.85, accuracy 0.5. ECE = 0.5 · 0.175 + 0.5 · 0.35 = 0.2625.

Who uses it

TensorDataset and DataLoader are used by thirteen training demos in examples/: cnn, diffusion, diffusion_unet, dpo, encoder, finetune, flow_matching, mlp, rag, rnn, ssm, vae and vit. At the pin no library code and no example includes preprocessing.h, split.h or any rml/metrics header; their tests are their only callers. The classical models are checked against scikit-learn fixtures with their own assertions rather than through these metrics.

Tests

The metric tests use scikit-learn's documented examples, with the reference values written into the test comments, rather than generated fixtures.

FileTestsWhat they pin down
dataset_test.cc8GatherRows on f32 and i32 tensors and on a transposed view; Stack's leading axis; Get and GetBatch agreeing; Subset remapping; sequential batches covering every example, drop_last keeping batches full, and the shuffle being a permutation that is fixed per epoch.
preprocessing_test.cc6Zero mean and unit variance after StandardScaler, its constant column mapping to 0 and its inverse round-tripping; MinMaxScaler mapping to its range; transforming before Fit or with other columns dies; NaNs filled with training means; one-hot keeping leading axes; balanced weights inversely proportional to counts.
split_test.cc7The held-out fraction and the same split from the same seed; stratified class proportions; k folds partitioning the indices, shuffled with a generator; every stratified fold seeing every class; k = 1 dies.
classification_test.cc8The worked example above: confusion matrix, accuracy and balanced accuracy, per-class and macro, weighted and micro scores, binary scores and zero division, F-beta, MCC including ±1 and a constant prediction; out-of-range labels die.
regression_test.cc4scikit-learn's values for every metric; R² baselines (1, 0 for the mean, negative, and the constant-target rule); explained variance ignoring a bias; matrices accepted and mismatched shapes rejected.
ranking_test.cc6ROC points and AUC 0.75; ties counting half; AUC equal to pairwise ranking on random data; NaN with one class; the precision-recall curve, average precision 0.8333 and PR-AUC; a perfect ranking scoring 1.
calibration_test.cc5Binary and multiclass log loss agreeing and certain mistakes clipped; Brier 0.0375 (scikit-learn's example) and a multiclass value; the reliability bins and ECE above; labels drawn at their stated probability (n = 20000) giving ECE near 0, while an always-99%-sure model is far off.