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.
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
GatherRows | Tensor(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 |
Stack | Tensor(span<const Tensor>) | Stacks equally shaped, same-dtype tensors along a new axis 0. | L55–68 |
Dataset | size(), 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 |
TensorDataset | explicit 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 |
Subset | Subset(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 |
DataLoaderOptions | batch_size = 32, shuffle = true, drop_last = false, seed = 0 | drop_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.
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
StandardScaler | Fit(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 |
MinMaxScaler | explicit 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 |
MeanImputer | Fit(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 |
OneHot | Tensor(const Tensor& labels, int64_t num_classes) | i32 labels of any shape [...] to f32 [..., num_classes] with a single 1 per row. | L108–123 |
BalancedClassWeights | Tensor(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.
| Function | Signature | What it does | Source |
|---|---|---|---|
TrainTestSplit | Split(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 |
StratifiedSplit | Split(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 |
KFold | vector<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 |
StratifiedKFold | vector<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).
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
ConfusionMatrix | Tensor(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 |
Accuracy | float(y_true, y_pred) | The fraction of matches; needs at least one example. | L49–57 |
BalancedAccuracy | float(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 |
Average | kBinary, kMicro, kMacro, kWeighted | The positive class alone; pooled counts; the unweighted mean of per-class scores; the support-weighted mean. | .h L40–45 |
PrecisionRecallF1 | ClassScores(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 |
FBeta | float(precision, recall, beta) | (1 + β²) P R / (β² P + R); β > 1 weights recall more. | L124–128 |
MatthewsCorrelation | float(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 \ predicted | 0 | 1 | 2 | recall |
|---|---|---|---|---|
| 0 | 2 | 1 | 0 | 2/3 |
| 1 | 0 | 3 | 1 | 3/4 |
| 2 | 1 | 0 | 2 | 2/3 |
| precision | 2/3 | 3/4 | 2/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).
| Function | Definition | Source |
|---|---|---|
MeanSquaredError, RootMeanSquaredError | mean (y − ŷ)², and its square root. | regression.cc L47–56 |
MeanAbsoluteError | mean |y − ŷ|, robust to outliers. | L58–63 |
MeanAbsolutePercentageError | mean |y − ŷ| / max(|y|, ε), with ε the double machine epsilon, as a fraction rather than a percentage. | L65–73 |
MedianAbsoluteError | The median of |y − ŷ|, averaging the middle two for an even count. | L75–83 |
R2Score | 1 − 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 |
ExplainedVariance | 1 − 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.
ranking_test.cc
(L24–37, L71–85).
Each row of the sweep is one point of both curves.| Function | Returns | Meaning | Source |
|---|---|---|---|
RocCurve | Roc{fpr, tpr, thresholds} | TP and FP counts divided by the totals; a rate is NaN when its class is absent. | ranking.cc L68–80 |
RocAuc | float | The 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 |
PrecisionRecallCurve | PrecisionRecall{precision, recall, thresholds} | Starts at (recall 0, precision 1), then TP / (TP + FP) and TP / positives at each threshold. | L90–102 |
AveragePrecision | float | Σ (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 |
PrAuc | float | The 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).
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
LogLoss | float(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 |
BrierScore | float(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 |
ReliabilityDiagram | vector<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 |
ExpectedCalibrationError | float(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.
| File | Tests | What they pin down |
|---|---|---|
dataset_test.cc | 8 | GatherRows 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.cc | 6 | Zero 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.cc | 7 | The 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.cc | 8 | The 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.cc | 4 | scikit-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.cc | 6 | ROC 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.cc | 5 | Binary 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. |