Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RustyML User Guide

RustyML provides machine learning models, neural networks, data utilities, and evaluation metrics for Rust. This book explains the main public APIs in RustyML 0.17.0. It assumes basic Rust and Cargo knowledge.

Start with Installation, then run Your first model. Read the other chapters when you need their APIs.

TaskChapter
Split data, scale features, or encode labelsPrepare data
Predict values or class labelsSupervised models
Find clusters or reduce feature countsUnsupervised models
Build and train a neural networkNeural networks
Select layers and tensor shapesNeural network layers
Measure model resultsEvaluate models
Save and load a classical modelModel files
Handle failed operationsErrors
Repeat randomized operationsRandom seeds

The examples use small arrays to show API use. Their scores do not measure performance on real datasets. For complete type and method details, use the API reference.

Installation

RustyML 0.17.0 requires Rust 1.89.0 or later. The examples use Rust edition 2024 and ndarray 0.17.2.

Create a project

  1. Create a Cargo project.

    cargo new rustyml-example
    
  2. Open the project directory.

    cd rustyml-example
    
  3. Add these dependencies to Cargo.toml.

    [dependencies]
    rustyml = "=0.17.0"
    ndarray = "=0.17.2"
    
  4. Replace src/main.rs with the program in Your first model.

  5. Run the program.

    cargo run
    

RustyML enables all public modules by default. Declare ndarray directly because the examples import its arrays and macros.

Select features

To select modules, disable default features and list the features you need. For example, this dependency supports classical models, data utilities, and evaluation metrics.

[dependencies]
rustyml = { version = "=0.17.0", default-features = false, features = ["machine_learning", "utils", "metrics"] }
ndarray = "=0.17.2"
FeaturePublic APIs
machine_learningRegression, classification, clustering, and dimensionality reduction
neural_networkModels, layers, losses, and optimizers
utilsData splits, scalers, and label conversion
metricsRegression, classification, and clustering scores
mathDistance functions and reduction functions
fullAll five modules above
show_progressTraining progress bars in supported operations

The default feature enables full. Each of machine_learning, neural_network, utils, and metrics also enables math. Cargo features add APIs to the build. The show_progress feature does not enable a model module.

Import APIs

The examples use module paths to identify each API. For shorter imports, use a category prelude.

use ndarray::array;
use rustyml::prelude::machine_learning::*;

fn main() -> Result<(), rustyml::error::Error> {
    let x = array![[1.0], [2.0]];
    let y = array![3.0, 5.0];
    let mut model = LinearRegression::new(true);
    model.fit(&x, &y)?;
    println!("prediction: {:.1}", model.predict(&array![[3.0]])?[0]);
    Ok(())
}

Output:

prediction: 7.0

Other category preludes are utils, metrics, and neural_network under rustyml::prelude. The root prelude exports APIs from every enabled category. With metrics enabled, its Average name refers to the metrics enum. Use rustyml::prelude::neural_network::Average for the merge layer.

Your first model

This program trains a linear regression model and evaluates it on test data. The data follows y = 2 * x + 1. Each row contains one sample with one feature.

Copy the complete program into src/main.rs. Use the dependencies from Installation.

use ndarray::array;
use rustyml::error::RustymlResult;
use rustyml::machine_learning::LinearRegression;
use rustyml::metrics::mean_absolute_error;
use rustyml::utils::train_test_split::train_test_split;

fn main() -> RustymlResult<()> {
    let x = array![[0.0], [1.0], [2.0], [3.0], [4.0], [5.0]];
    let y = array![1.0, 3.0, 5.0, 7.0, 9.0, 11.0];
    let (x_train, x_test, y_train, y_test) = train_test_split(x, y, Some(0.33), Some(42))?;

    let mut model = LinearRegression::new(true);
    model.fit(&x_train, &y_train)?;
    let predictions = model.predict(&x_test)?;
    println!("training rows: {}", x_train.nrows());
    println!("test rows: {}", x_test.nrows());
    println!(
        "test MAE: {:.6}",
        mean_absolute_error(&y_test, &predictions)
    );

    let query = array![[6.0]];
    println!("prediction for 6: {:.3}", model.predict(&query)?[0]);
    Ok(())
}

Run cargo run.

training rows: 4
test rows: 2
test MAE: 0.000000
prediction for 6: 13.000

Read the program

train_test_split divides features and targets into matching training and test arrays. The explicit seed fixes the split for repeat runs. The function consumes the original arrays.

LinearRegression::new(true) creates a model with an intercept. fit learns the relation from the training arrays. predict returns one value for each input row.

mean_absolute_error compares the predictions with the test targets. The test arrays contain samples that the model did not use during training. The final query uses the same feature count and feature order as the training data.

This dataset has an exact linear relation. Real datasets usually produce a nonzero error. Use Prepare data for feature scaling and label conversion. Use Supervised models to select another model.

Prepare data

Enable the utils feature for this chapter. Add ndarray = "=0.17.2" to your dependencies. The examples also work with the default RustyML features.

Choose array types

Use Array2<f64> for classical model features. Each row contains one sample. Each column contains one feature. A matrix with shape (100, 4) contains 100 samples and four features.

Use Array1<f64> for regression targets. Class label types depend on the receiving API. Keep target order aligned with feature rows.

ValueType or shapeUse
Feature matrixArray2<f64>, (samples, features)Classical models and split utilities
Regression targetsArray1<f64>, (samples,)Regression models and metrics
Split labelsArray1<A>Any cloneable label type
One-hot labelsArray2<f64>, (samples, classes)Categorical targets
Sparse encoding labelsArray1<i32>to_categorical input and to_sparse_categorical output

array! creates small arrays directly. Array2::from_shape_vec builds matrices from flat values in row order. Its shape must match the number of values. Handle its shape error before passing data to RustyML.

Use .view() to borrow an array without copying its elements. Many preprocessing functions accept owned arrays and views through generic array references.

Split functions take ownership of their arrays. Clone them only when you need the originals later.

Neural network layers use their own tensor shapes. Check each layer before reshaping data.

Split before fitting preprocessing

train_test_split(x, y, test_size, random_state) returns (x_train, x_test, y_train, y_test) inside a Result. Pass Some(0.33) to reserve about one third for testing. None selects the default test fraction, 0.3.

Pass Some(42) for a fixed random seed. None uses the global seed stream when set, or system entropy. See Random seeds. The split rounds the test count and keeps at least one sample on each side.

Use train_test_split_stratified for classification when both subsets need each class. It requires labels that implement Clone + Eq + Hash. Integer and string labels support these bounds. Floating-point labels do not implement Eq.

Each class must contain at least two samples. The function splits each class separately. Rounding can make its total test fraction differ from the requested fraction.

Fit StandardScaler on training features only. Transform training and held-out features with the same fitted scaler. This sequence prevents test information from changing the training transformation. Keep the scaler with the model for later predictions.

The scaler stores each feature’s population mean and variance. It subtracts the mean and divides by the standard deviation by default. Constant or near-constant features use a divisor of 1.0.

fit replaces stored statistics. fit_transform fits and transforms the same input. partial_fit updates statistics from additional training batches.

The stateless standardize function computes fresh statistics from every input. Use the stateful scaler when you need consistent transformations across datasets.

Run data_prepare.rs:

use ndarray::array;
use rustyml::utils::{StandardScaler, train_test_split, train_test_split_stratified};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![
        [1.0, 10.0],
        [2.0, 20.0],
        [3.0, 30.0],
        [4.0, 40.0],
        [5.0, 50.0],
        [6.0, 60.0]
    ];
    let y = array![0, 0, 0, 1, 1, 1];
    let (x_train, x_test, y_train, y_test) =
        train_test_split(x.clone(), y.clone(), Some(0.33), Some(42))?;
    println!("train: {:?}, labels: {}", x_train.dim(), y_train.len());
    println!("test: {:?}, labels: {}", x_test.dim(), y_test.len());
    let mut scaler = StandardScaler::new();
    scaler.fit(&x_train)?;
    let z_train = scaler.transform(&x_train)?;
    let z_test = scaler.transform(&x_test)?;
    println!("scaled: {:?}, {:?}", z_train.dim(), z_test.dim());
    let (_, _, strat_train, strat_test) = train_test_split_stratified(x, y, Some(0.33), Some(42))?;
    println!(
        "class 0 counts: train {}, test {}",
        strat_train.iter().filter(|&&v| v == 0).count(),
        strat_test.iter().filter(|&&v| v == 0).count()
    );
    Ok(())
}
train: (4, 2), labels: 4
test: (2, 2), labels: 2
scaled: (4, 2), (2, 2)
class 0 counts: train 2, test 1

Encode labels consistently

to_categorical accepts nonnegative i32 labels. Each label selects its corresponding column. Pass a fixed class count to preserve the same width across subsets. Without a class count, the function uses the largest label plus one.

to_categorical_with_mapping accepts a slice of hashable labels. It assigns indices in first-seen order. Save the returned mapping. Use those indices when encoding later data. Calling it independently on test labels can change column meanings. Decide how your application handles labels absent from the training mapping.

to_sparse_categorical selects each row’s largest value. It returns column indices as i32 labels. Ties select the lowest column index. Use your saved mapping to recover original string labels. This function also accepts finite score matrices.

Run data_labels.rs:

use ndarray::array;
use rustyml::utils::{to_categorical, to_categorical_with_mapping, to_sparse_categorical};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let labels = array![0_i32, 2, 1];
    let encoded = to_categorical(&labels, Some(3))?;
    println!("one-hot shape: {:?}", encoded.dim());
    println!("first row: {:?}", encoded.row(0).to_vec());
    println!("decoded: {:?}", to_sparse_categorical(&encoded)?.to_vec());
    let (encoded, mapping) = to_categorical_with_mapping(&["cat", "dog", "cat"], None)?;
    println!("cat: {}, dog: {}", mapping["cat"], mapping["dog"]);
    println!("mapped shape: {:?}", encoded.dim());
    Ok(())
}
one-hot shape: (3, 3)
first row: [1.0, 0.0, 0.0]
decoded: [0, 2, 1]
cat: 0, dog: 1
mapped shape: (3, 2)

Handle preprocessing errors

The examples return errors with ?. Production code can match rustyml::error::Error variants. Include a fallback arm because this enum is non-exhaustive.

OperationInvalid inputResult
Either splitEmpty samplesError::EmptyInput
Either splitDifferent feature and label countsError::DimensionMismatch
Either splitTest fraction at or outside (0, 1)Error::InvalidParameter
Plain splitFewer than two samplesError::InvalidInput for one sample
Stratified splitAny class has fewer than two samplesError::InvalidInput
Scaler fitEmpty matrix or non-finite valuesError::EmptyInput or Error::NonFinite
Scaler transformScaler has no fitted stateError::NotFitted
Scaler transformDifferent feature countError::DimensionMismatch
Integer encodingNegative labelError::InvalidInput
Either one-hot encoderClass count is too smallError::InvalidParameter
Sparse decodingNon-finite matrix valueError::NonFinite

Check dataset values before training. Split utilities do not perform full feature validation.

Supervised learning

Use regression to predict numeric values. Use classification to predict labels.

Enable the machine_learning feature. These examples also require ndarray = "0.17.2".

Prepare inputs

Store features in an Array2<f64> with shape (samples, features). Each row contains one sample.

Store targets in an Array1 with one target per sample. Keep feature order consistent during training and prediction.

Use separate training and test samples to measure performance. Fit preprocessing only on training samples.

Choose a model

Use linear regression for numeric targets. Start with the default LeastSquaresSolver::Normal solver.

Use logistic regression for binary targets and positive-class probabilities. Use KNN for labels with multiple classes and distance-based decisions.

Compare models on held-out samples. Scale features before KNN or gradient-based training when their ranges differ.

Train and predict

LinearRegression::new(true) enables an intercept. Its targets and predictions contain f64 values.

KNN::<i32>::new(k) creates a classifier with integer labels. Choose k between one and the training sample count.

The example uses one neighbor and avoids distance ties.

use ndarray::array;
use rustyml::prelude::machine_learning::{KNN, LinearRegression};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![[0.0], [1.0], [2.0], [3.0]];
    let y = array![1.0, 3.0, 5.0, 7.0];
    let mut regression = LinearRegression::new(true);
    regression.fit(&x, &y)?;
    let predictions = regression.predict(&array![[4.0], [5.0]])?;
    println!("regression: {:.1}, {:.1}", predictions[0], predictions[1]);

    let labels = array![0_i32, 0, 1, 1];
    let mut classifier = KNN::<i32>::new(1)?;
    classifier.fit(&x, &labels)?;
    let predictions = classifier.predict(&array![[0.1], [2.9]])?;
    println!("classes: {:?}", predictions.to_vec());
    Ok(())
}

Output:

regression: 9.0, 11.0
classes: [0, 1]

Configure logistic regression and penalties

Logistic regression requires Array1<f64> targets containing only 0.0 and 1.0.

LogisticRegression::new(fit_intercept, learning_rate, max_iterations, tolerance) configures training. Use positive, finite learning rates and tolerances.

Set max_iterations above zero. Increase it when training needs more iterations, and reduce the learning rate if training becomes unstable.

predict_proba(&x) returns one positive-class probability per row. predict(&x) returns 1.0 when that probability is at least 0.5.

Both methods return Result<Array1<f64>, Error>. Logistic regression uses the same fit(&x, &y) input shapes as linear regression.

Add with_regularization(RegularizationType::L2(alpha)) to either regression model. Use finite, nonnegative penalty strengths.

Linear regression’s default solver supports L2. Select LeastSquaresSolver::GradientDescent for L1, with learning_rate, max_iter, and tol settings.

The example configures logistic L2 regularization and linear L1 regularization.

use ndarray::array;
use rustyml::prelude::machine_learning::{
    LeastSquaresSolver, LinearRegression, LogisticRegression, RegularizationType,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![[-2.0], [-1.0], [1.0], [2.0]];
    let y = array![0.0, 0.0, 1.0, 1.0];
    let mut classifier = LogisticRegression::new(true, 0.1, 10_000, 1e-8)?
        .with_regularization(RegularizationType::L2(0.1))?;
    classifier.fit(&x, &y)?;
    let queries = array![[-1.5], [1.5]];
    let probabilities = classifier.predict_proba(&queries)?;
    println!(
        "positive probabilities: {:.3}, {:.3}",
        probabilities[0], probabilities[1]
    );
    println!(
        "binary labels: {:?}",
        classifier.predict(&queries)?.to_vec()
    );

    let targets = array![-3.0, -1.0, 3.0, 5.0];
    let mut regression = LinearRegression::new(true)
        .with_solver(LeastSquaresSolver::GradientDescent {
            learning_rate: 0.05,
            max_iter: 10_000,
            tol: 1e-8,
        })?
        .with_regularization(RegularizationType::L1(0.01))?;
    regression.fit(&x, &targets)?;
    println!("L1 predictions: {}", regression.predict(&queries)?.len());
    Ok(())
}

Output:

positive probabilities: 0.101, 0.899
binary labels: [0.0, 1.0]
L1 predictions: 2

Method contracts

The signatures below use owned arrays for clarity. These methods also accept compatible ndarray views.

ModelCallSuccessful result
Linear regressionfit(&mut self, x: &Array2<f64>, y: &Array1<f64>)&mut Self
Linear regressionpredict(&self, x: &Array2<f64>)Array1<f64>
KNN classificationfit(&mut self, x: &Array2<f64>, y: &Array1<T>)&mut Self
KNN classificationpredict(&self, x: &Array2<f64>)Array1<T>

Each call returns Result<_, Error>. Predictions contain one value per input row.

KNN labels must implement Clone, Hash, and Eq. Integer labels satisfy these requirements.

Handle errors

Call fit before predict. Unfitted predictions return Error::NotFitted.

Use finite feature values and matching target counts. Prediction inputs must have the training feature count.

Handle EmptyInput, DimensionMismatch, NonFinite, and invalid parameter errors. Numerical training failures can also return errors.

Propagate failures with ?, as the example does. See Model files to save fitted models.

Unsupervised learning

Use clustering to group samples. Use PCA to reduce the number of features.

Enable the machine_learning feature. These examples also require ndarray = "0.17.2".

Prepare inputs

Pass an Array2<f64> with shape (samples, features). Training does not require targets.

Scale features when their units or ranges differ. Apply the same fitted preprocessing to new samples.

Cluster and reduce features

KMeans::new(n_clusters, max_iterations, tolerance) configures clustering. Set a random seed with with_random_state(seed) for repeatable initialization.

KMeans cluster numbers identify groups. They do not indicate rank or meaning.

DBSCAN::new(eps, min_samples) configures neighborhood radius and minimum density. DBSCAN uses -1 for noise.

DBSCAN prediction uses the nearest fitted core point within eps. Otherwise, prediction returns -1.

PCA::new(n_components) selects the output width. Fit PCA before transforming new samples.

use ndarray::array;
use rustyml::prelude::machine_learning::{DBSCAN, KMeans, PCA};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![[0.0, 0.0], [0.1, 0.1], [5.0, 5.0], [5.1, 5.1]];
    let mut kmeans = KMeans::new(2, 100, 1e-6)?.with_random_state(42);
    let labels = kmeans.fit_predict(&x)?;
    println!("same first pair: {}", labels[0] == labels[1]);
    println!("different groups: {}", labels[0] != labels[2]);
    let queries = array![[0.05, 0.05], [5.05, 5.05]];
    println!("KMeans predictions: {}", kmeans.predict(&queries)?.len());

    let mut dbscan = DBSCAN::new(0.3, 2)?;
    let labels = dbscan.fit_predict(&x)?;
    println!("DBSCAN labels: {:?}", labels.to_vec());
    let noise = dbscan.predict(&array![[20.0, 20.0]])?;
    println!("DBSCAN noise: {}", noise[0]);

    let mut pca = PCA::new(1)?;
    let reduced = pca.fit_transform(&x)?;
    println!("PCA shape: {:?}", reduced.dim());
    println!("PCA query shape: {:?}", pca.transform(&queries)?.dim());
    Ok(())
}

Output:

same first pair: true
different groups: true
KMeans predictions: 2
DBSCAN labels: [0, 0, 1, 1]
DBSCAN noise: -1
PCA shape: (4, 1)
PCA query shape: (2, 1)

Method contracts

The signatures below use owned arrays for clarity. These methods also accept compatible ndarray views.

ModelsCallSuccessful result
KMeans, DBSCAN, PCAfit(&mut self, x: &Array2<f64>)&mut Self
KMeans, DBSCANfit_predict(&mut self, x: &Array2<f64>)Array1<isize>
KMeans, DBSCANpredict(&self, x: &Array2<f64>)Array1<isize>
PCAfit_transform(&mut self, x: &Array2<f64>)Array2<f64>
PCAtransform(&self, x: &Array2<f64>)Array2<f64>

Each call returns Result<_, Error>. Cluster output contains one label per input row.

PCA output has shape (input_samples, n_components). PCA requires at least two training samples.

Choose n_components from one through min(samples, features). Choose no more KMeans clusters than training samples.

Handle errors

Use nonempty, finite inputs and valid constructor parameters. New inputs must have the fitted feature count.

Prediction and transformation require a fitted model. Otherwise, they return Error::NotFitted.

Training can report invalid inputs, numerical failures, or convergence failures. Propagate errors with ? and inspect their messages.

See Model files to save the fitted clustering model or PCA projection.

Neural networks

Use SequentialBuilder when each layer takes the previous layer output. Import the neural network prelude with use rustyml::prelude::neural_network::*. Enable the neural_network Cargo feature. Use ndarray = "0.17.2" for your input arrays.

Prepare tensors

Tensor means ndarray::ArrayD<f32>. Convert arrays with .into_dyn() and store values as f32. Axis zero contains samples. For tabular data, use [samples, features]. Use [samples, outputs] for regression targets. Training inputs and targets must contain the same number of samples.

Scale features consistently before training and prediction. Keep evaluation samples separate from training samples.

Build and train

  1. Create layers and add them in order.
  2. Call build(&Shape::known(x.shape())) to validate shapes and initialize weights.
  3. Call compile(optimizer, loss) before training or evaluation.
  4. Call fit, fit_with_batches, or train_batch.
  5. Call predict to produce output tensors.

fit(&x, &y, epochs) uses the complete dataset for one update per epoch. fit_with_batches(&x, &y, epochs, batch_size) shuffles samples each epoch and updates each batch. Use a positive batch size that does not exceed the sample count. train_batch(&x, &y) performs one update without splitting or shuffling.

Both fitting methods return History. history.loss() contains one loss per epoch, measured during training. evaluate(&x, &y) measures the current model loss without updating weights. predict(&x) and evaluate use inference behavior for dropout and normalization. predict works without compile. The batch size can change after building when the other required dimensions remain compatible.

Choose outputs and loss

TaskLast activationLossTarget shape
RegressionActivation::LinearMeanSquaredError::new() or MeanAbsoluteError::new()Same as predictions
Binary classificationActivation::SigmoidBinaryCrossEntropy::new()[samples, 1], values 0 or 1
Class probabilitiesActivation::Softmax { axis: -1 }CategoricalCrossEntropy::new(false)[samples, classes], one-hot values
Class logitsActivation::LinearSparseCategoricalCrossEntropy::new(true)[samples, 1], class indices

Sparse labels still use f32, such as 0.0 and 2.0. Use whole class indices from zero through classes - 1. For categorical losses, true means raw logits and false means probabilities. Do not apply softmax before a loss configured for logits.

Start with SGD for a simple learning rate and optional momentum. SGD::new(0.05, 0.0, false, 0.0) disables momentum, Nesterov updates, and weight decay. Adam::new(0.001, 0.9, 0.999, 1e-8, 0.0) provides adaptive updates. AdamW provides decoupled weight decay. RMSprop and AdaGrad provide other adaptive update choices. Use set_learning_rate to change the compiled optimizer learning rate.

Train, save, and restore

This example learns y = 2x + 1 from four samples. It checks training progress and verifies exact predictions after loading. The source also lives in guide/examples/nn_regression.rs.

use ndarray::array;
use rustyml::prelude::neural_network::*;

fn build_model() -> Result<Sequential, rustyml::error::Error> {
    SequentialBuilder::new_with_seed(42)
        .add(Dense::new(1, Activation::Linear)?.with_random_state(7))
        .build(&Shape::known(&[4, 1]))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![[-1.0_f32], [0.0], [1.0], [2.0]].into_dyn();
    let y = array![[-1.0_f32], [1.0], [3.0], [5.0]].into_dyn();
    let mut model = build_model()?;
    model.compile(SGD::new(0.05, 0.0, false, 0.0)?, MeanSquaredError::new());
    let initial_loss = model.evaluate(&x, &y)?;
    let history = model.fit_with_batches(&x, &y, 200, 2)?;
    let final_loss = model.evaluate(&x, &y)?;
    assert!(final_loss < initial_loss);
    assert!(final_loss < 0.0001);
    println!("epochs: {}", history.loss().len());
    println!("loss decreased: {}", final_loss < initial_loss);

    let path = std::env::temp_dir().join(format!("rustyml-nn-{}.bin", std::process::id()));
    model.save_to_path(&path)?;
    let mut restored = build_model()?;
    restored.load_from_path(&path)?;
    let prediction = restored.predict(&x)?;
    assert_eq!(model.predict(&x)?, prediction);
    println!("prediction shape: {:?}", prediction.shape());
    println!("restored predictions match: true");
    std::fs::remove_file(path)?;
    Ok(())
}

Expected output:

epochs: 200
loss decreased: true
prediction shape: [4, 1]
restored predictions match: true

save_to_path writes trainable arrays and non-trainable arrays, including normalization statistics. It does not save the architecture, layer configuration, optimizer, or loss. Rebuild the same architecture before calling load_from_path. The strict loader checks layer types, array names, array shapes, and compatible build shapes. The checkpoint permits a different batch size. Keep the same layer configuration because the checkpoint does not restore it.

Call compile after loading before training or evaluation. Training starts with new optimizer state, so loading does not resume optimizer history. Prediction needs no compilation, as the example shows. Use load_partial_from_path only when you intend to load matching arrays and inspect its report.

Repeat a training run

Set each randomized layer seed with with_random_state before building. Set the batch shuffle seed with SequentialBuilder::new_with_seed or model.set_seed. The builder seed controls shuffling, not layer initialization. Full-batch fit does not shuffle.

Alternatively, call rustyml::random::set_global_seed before creating randomized components. Reset that seed before repeating the same operation sequence on the same thread. Random operations without local seeds share that thread-local stream. Keep input data, preprocessing, operation order, and training settings fixed. Floating-point results can vary across hardware and execution environments.

Continue with layer shapes and common stacks.

Neural network layers

Choose layers by the shape of each sample and the output your task needs. All tensors use f32 values and place the batch axis first. The neural network prelude exports the layers below.

Common shape rules

Layer familyInputOutput
Dense(units)[batch, ..., features], rank at least two[batch, ..., units]
Conv1D[batch, length, channels][batch, output_length, filters]
Conv2D[batch, height, width, channels][batch, output_height, output_width, filters]
Conv3D[batch, depth, height, width, channels]Same axis order with new spatial sizes and filters
Local poolingConvolution axis orderReduced spatial axes, unchanged channels
Flatten[batch, ...][batch, product_of_remaining_axes]
Recurrent, default[batch, time, features][batch, units]
Recurrent, return sequences[batch, time, features][batch, time, units]

These rows describe shape behavior, not constructor syntax. Image and convolution tensors place channels last. For grayscale images, keep the final channel dimension equal to one. A single image still needs a batch axis.

Dense layers

Create a dense layer with Dense::new(units, activation)?. Use Activation::ReLU for a simple hidden layer. Choose the final activation to match your loss. See output and loss choices.

Dense replaces only the last axis and preserves all leading axes. For example, [8, 12, 6] becomes [8, 12, 4] with four units. Add Flatten::new() first when you need one vector per image. Without flattening, Dense processes each leading position separately.

Convolution and pooling

Conv2D::new(filters, kernel_size, strides, activation)? configures a two-dimensional convolution. Its default padding is PaddingType::Valid. Call .with_padding(PaddingType::Same) to request padding that preserves spatial size when the stride equals one. Use .with_dilation_rate(...) when you need a larger effective kernel.

For each spatial axis, let effective_kernel = dilation * (kernel - 1) + 1. With valid padding, the output size is floor((input - effective_kernel) / stride) + 1. The effective kernel must fit the input for valid padding. With same padding, the output size is ceil(input / stride). Apply these rules separately to each spatial axis.

MaxPooling2D::new((2, 2)) keeps the largest value in each window. AveragePooling2D uses window averages. Local pooling preserves the channel count. Pool strides default to the pool size, with valid padding. Override them using with_strides(...)? and with_padding(...). Global pooling reduces all spatial positions to one value per channel.

The image example below transforms [2, 8, 8, 1] through these shapes:

  1. Valid convolution produces [2, 6, 6, 4].
  2. Pooling produces [2, 3, 3, 4].
  3. Flatten produces [2, 36].
  4. Dense produces [2, 3].

Recurrent layers

Use SimpleRNN, LSTM, or GRU for ordered sequences. Each constructor takes units and an activation, such as LSTM::new(4, Activation::Tanh)?. Each expects [batch, time, features] input. The default output contains the final hidden state, shaped [batch, units]. Call .with_return_sequences(true) to return one hidden state per timestep.

Enable sequence output before stacking another recurrent layer. A following Dense layer then maps every timestep separately. For one prediction per sequence, keep the final recurrent layer default output. Call .with_go_backwards(true) to process timesteps in reverse order.

Run image and sequence stacks

Prediction does not require an optimizer or loss. This example uses zero inputs to verify both stack shapes. The source also lives in guide/examples/nn_shapes.rs.

use ndarray::Array;
use rustyml::prelude::neural_network::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let images = Array::<f32, _>::zeros((2, 8, 8, 1)).into_dyn();
    let cnn = SequentialBuilder::new()
        .add(Conv2D::new(4, (3, 3), (1, 1), Activation::ReLU)?.with_random_state(11))
        .add(MaxPooling2D::new((2, 2)))
        .add(Flatten::new())
        .add(Dense::new(3, Activation::Linear)?.with_random_state(12))
        .build(&Shape::known(images.shape()))?;
    println!("image predictions: {:?}", cnn.predict(&images)?.shape());

    let sequence = Array::<f32, _>::zeros((2, 5, 3)).into_dyn();
    let rnn = SequentialBuilder::new()
        .add(
            LSTM::new(4, Activation::Tanh)?
                .with_random_state(13)
                .with_return_sequences(true),
        )
        .add(Dense::new(2, Activation::Linear)?.with_random_state(14))
        .build(&Shape::known(sequence.shape()))?;
    println!(
        "sequence predictions: {:?}",
        rnn.predict(&sequence)?.shape()
    );
    Ok(())
}

Expected output:

image predictions: [2, 3]
sequence predictions: [2, 5, 2]

Fix shape errors

Compare every layer output with the next layer input requirements. Use model.summary() to inspect the complete stack. Build with the actual input tensor shape before training. Check channel order when convolution rejects image input. Check with_return_sequences when recurrent layers reject a preceding layer output. Check target dimensions against the selected loss before fitting.

Evaluate results

Enable the metrics feature for this chapter. Add ndarray = "=0.17.2" to your dependencies. The examples also work with the default RustyML features.

Evaluate predictions on data excluded from fitting. Reuse preprocessing statistics learned from the training set. Compare models on the same samples and target order. Pass true values first and predicted values second to regression and hard-label metrics. These metrics return scores directly. They do not return Result.

Select a regression metric

Regression metrics accept one-dimensional f64 arrays or views.

FunctionInterpretationPreferred direction
mean_squared_errorMean squared residualLower
root_mean_squared_errorSquare root of MSE, in target unitsLower
mean_absolute_errorMean absolute residual, in target unitsLower
median_absolute_errorMedian absolute residualLower
r2_scoreImprovement over the true target mean baselineHigher
explained_variance_scoreExplained target varianceHigher
mean_absolute_percentage_errorMean absolute relative error as a fractionLower

MSE gives large residuals more weight. MAE is easier to interpret in target units.

A perfect R2 score is 1.0. Negative R2 scores indicate worse predictions than the mean baseline. For constant targets, r2_score returns 1.0 for exact predictions and 0.0 otherwise.

MAPE can become very large near zero targets. Its output is a fraction, so multiply by 100 for percentages.

Select a classification metric

accuracy and binary ConfusionMatrix accept f64 labels. ConfusionMatrix::new uses 0.0 for negative labels and 1.0 for positive labels. Use new_with_labels for another explicit negative and positive label pair. get_counts returns (true_positives, false_positives, true_negatives, false_negatives).

Precision measures the fraction of predicted positives that are correct. Recall measures the fraction of actual positives that the model finds. F1 combines precision and recall through their harmonic mean.

MulticlassConfusionMatrix accepts usize labels. Its matrix rows represent true classes, and columns represent predicted classes. labels() gives the sorted labels corresponding to matrix indices. Average::Macro weights each class equally. Average::Weighted weights each class by its true support. Average::Micro combines counts across classes before computing the score.

roc_auc accepts boolean true labels and f64 positive-class scores. Pass probabilities or ranking scores rather than thresholded class predictions. A perfect ROC AUC is 1.0. Both positive and negative samples must exist. Use average_precision or precision_recall_curve to inspect positive-class ranking. roc_curve returns (fpr, tpr, thresholds) arrays.

log_loss accepts usize labels and a (samples, classes) probability matrix. Each label indexes a probability column. Supply finite, nonnegative probabilities with positive row sums. The function normalizes each row and clips the selected probability before taking its logarithm. Lower log loss is better. top_k_accuracy checks whether the true class appears among the top k columns.

Run metrics_scores.rs:

use ndarray::array;
use rustyml::metrics::{
    Average, ConfusionMatrix, MulticlassConfusionMatrix, accuracy, mean_absolute_error,
    mean_squared_error, r2_score, roc_auc, root_mean_squared_error,
};

fn main() {
    let actual = array![1.0, 2.0, 3.0];
    let predicted = array![1.0, 2.0, 4.0];
    println!("MSE: {:.3}", mean_squared_error(&actual, &predicted));
    println!("RMSE: {:.3}", root_mean_squared_error(&actual, &predicted));
    println!("MAE: {:.3}", mean_absolute_error(&actual, &predicted));
    println!("R2: {:.3}", r2_score(&actual, &predicted));
    let actual = array![0.0, 0.0, 1.0, 1.0];
    let predicted = array![0.0, 1.0, 1.0, 1.0];
    let matrix = ConfusionMatrix::new(&actual, &predicted);
    println!("accuracy: {:.3}", accuracy(&actual, &predicted));
    println!("TP, FP, TN, FN: {:?}", matrix.get_counts());
    println!(
        "precision: {:.3}, recall: {:.3}, F1: {:.3}",
        matrix.precision(),
        matrix.recall(),
        matrix.f1_score()
    );
    let labels = array![false, false, true, true];
    let scores = array![0.1, 0.4, 0.35, 0.8];
    println!("ROC AUC: {:.3}", roc_auc(&labels, &scores));
    let actual = array![0_usize, 1, 2, 2];
    let predicted = array![0_usize, 2, 2, 2];
    let matrix = MulticlassConfusionMatrix::new(&actual, &predicted);
    println!("macro F1: {:.3}", matrix.f1(Average::Macro));
}
MSE: 0.333
RMSE: 0.577
MAE: 0.333
R2: 0.500
accuracy: 0.750
TP, FP, TN, FN: (2, 1, 1, 0)
precision: 0.667, recall: 1.000, F1: 0.800
ROC AUC: 0.750
macro F1: 0.600

Evaluate clusters

External clustering metrics compare reference labels with predicted cluster labels.

They use one-dimensional isize arrays. Cluster numbers need not match reference class numbers. adjusted_rand_index and normalized_mutual_info both return 1.0 for equivalent partitions.

ARI adjusts pair agreement for chance. NMI measures shared label information without chance adjustment.

adjusted_mutual_info adds chance adjustment to mutual information. Homogeneity, completeness, V-measure, and Fowlkes-Mallows provide other external comparisons.

Internal metrics use features and cluster assignments without reference labels. silhouette_score accepts an explicit DistanceCalculationMetric.

Larger silhouette values indicate better separation. Values near zero indicate overlapping clusters. Negative values suggest that samples may fit another cluster better.

davies_bouldin_score prefers lower values. calinski_harabasz_score prefers higher values. These internal metrics require between two and samples - 1 distinct clusters.

Negative labels count as ordinary cluster identifiers. Filter noise samples yourself when that matches your evaluation policy.

Run metrics_clusters.rs:

use ndarray::array;
use rustyml::math::distance::DistanceCalculationMetric;
use rustyml::metrics::{adjusted_rand_index, normalized_mutual_info, silhouette_score};

fn main() {
    let x = array![[0.0], [1.0], [10.0], [11.0]];
    let reference = array![0_isize, 0, 1, 1];
    let clusters = array![7_isize, 7, 3, 3];
    println!("ARI: {:.3}", adjusted_rand_index(&reference, &clusters));
    println!("NMI: {:.3}", normalized_mutual_info(&reference, &clusters));
    println!(
        "silhouette: {:.3}",
        silhouette_score(&x, &clusters, DistanceCalculationMetric::Euclidean)
    );
}
ARI: 1.000
NMI: 1.000
silhouette: 0.900

Validate before scoring

Metric input checks panic on invalid inputs. Handle validation before calling metrics in a data processing service.

Regression and paired label metrics require equal nonzero lengths. Internal clustering metrics require one label for every feature row and a valid cluster count.

Binary confusion matrices reject labels outside the selected label pair. ROC metrics reject NaN scores. ROC AUC requires both label values.

Log loss validates class indices. Validate probability values and row sums yourself.

Regression arithmetic can propagate non-finite values. Do not assume every metric rejects all non-finite inputs.

Keep prediction and target shapes equal. Preserve probability column meanings when encoding labels. Choose metrics before comparing models to avoid selecting only favorable results.

Classical model files

Save a fitted classical model, then restore it with its concrete model type.

This chapter covers LinearRegression, KNN, KMeans, DBSCAN, and PCA model files.

Enable the machine_learning feature. The example also requires ndarray = "0.17.2".

Save and restore

save_to_path(&self, path: &str) -> Result<(), Error> writes the model state.

Model::load_from_path(path: &str) -> Result<Model, Error> returns a restored model.

Saving replaces an existing file. Choose a separate path when you need to retain an earlier model.

The example checks an unfitted model, saves a trained model, and restores its predictions. It removes the temporary file afterward.

use ndarray::array;
use rustyml::error::Error;
use rustyml::prelude::machine_learning::LinearRegression;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x = array![[0.0], [1.0], [2.0]];
    let y = array![1.0, 3.0, 5.0];
    let mut model = LinearRegression::new(true);
    assert!(matches!(model.predict(&x), Err(Error::NotFitted(_))));
    model.fit(&x, &y)?;

    let path = std::env::temp_dir().join(format!("rustyml-guide-{}.bin", std::process::id()));
    let filename = path.to_str().ok_or("temporary path is not UTF-8")?;
    model.save_to_path(filename)?;
    let restored = LinearRegression::load_from_path(filename)?;
    let predictions = restored.predict(&array![[3.0]])?;
    println!("restored prediction: {:.1}", predictions[0]);
    std::fs::remove_file(path)?;
    Ok(())
}

Output:

restored prediction: 7.0

Keep compatible inputs

A restored model still expects the original feature count and feature order. Preserve label meanings and preprocessing settings separately.

Saving an unfitted model does not train it. Its predictions still require training.

Load the same concrete type that created the file. For KNN, also preserve the label type, such as KNN<i32>.

KNN persistence requires labels that implement serde serialization and deserialization. Integer labels satisfy these requirements.

Keep the crate version with each saved model. Check compatibility before you load files from another version.

Handle file errors

File access and serialization failures return Error::Io. Propagate the error or report it with the file path.

A successful load restores stored state. It does not validate your application’s feature names or preprocessing choices.

Retain training data and configuration when you need to retrain a model.

Errors

Most model and data utility methods return Result<T, rustyml::error::Error>. Use ? to return an error to the caller. Use match when the caller can correct a specific problem.

Handle a model state error

This program handles prediction before training. It then trains the model and repeats the prediction.

use ndarray::array;
use rustyml::error::{Error, RustymlResult};
use rustyml::machine_learning::LinearRegression;

fn main() -> RustymlResult<()> {
    let x = array![[1.0], [2.0]];
    let mut model = LinearRegression::new(true);
    match model.predict(&x) {
        Err(Error::NotFitted(_)) => println!("fit the model before prediction"),
        Err(error) => return Err(error),
        Ok(_) => println!("prediction succeeded"),
    }
    model.fit(&x, &array![3.0, 5.0])?;
    println!("prediction: {:.1}", model.predict(&array![[3.0]])?[0]);
    Ok(())
}

Output:

fit the model before prediction
prediction: 7.0

Error is non-exhaustive. Include a fallback arm when you match its variants.

Correct common errors

ErrorAction
EmptyInputSupply a nonempty array.
DimensionMismatchCheck sample counts and feature counts.
ShapeMismatchCheck the complete tensor shape.
InvalidParameterCheck the constructor or method parameters.
InvalidInputCheck array ranks, label values, and other input requirements.
NonFiniteCheck for NaN, infinity, or numerical overflow.
NotFittedCall fit before prediction or transformation.
NotConvergedCheck data scaling and the selected solver settings.
NeuralNetwork(NnError::NotCompiled(_))Call compile before training or evaluation.
IoCheck the file path, permissions, and file format.

NnError belongs to rustyml::neural_network. The neural network feature enables the NeuralNetwork error variant. Each method documents the errors that it can return.

Metric input checks

Metrics return scores directly. Invalid metric inputs can cause a panic. Check array lengths, label values, and metric requirements before you call them. See Evaluate models for common input requirements.

Random seeds

A seed controls random choices in an operation. Use explicit local seeds when an API accepts them. For example, train_test_split accepts Some(42) as its last argument.

Use the global seed

set_global_seed sets a seed stream for the current thread. Operations without local seeds can use this stream. Reset the seed before you repeat the same sequence of randomized operations.

use ndarray::array;
use rustyml::error::RustymlResult;
use rustyml::utils::train_test_split::train_test_split;
use rustyml::{clear_global_seed, set_global_seed};

fn main() -> RustymlResult<()> {
    let x = array![[0.0], [1.0], [2.0], [3.0], [4.0], [5.0]];
    let y = array![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
    set_global_seed(42);
    let first = train_test_split(x.clone(), y.clone(), Some(0.33), None)?;
    set_global_seed(42);
    let second = train_test_split(x, y, Some(0.33), None)?;
    println!("same test rows: {}", first.1 == second.1);
    clear_global_seed();
    Ok(())
}

Output:

same test rows: true

An explicit local seed takes priority over the global seed. It does not advance the global seed stream. Without either seed, operations that need randomness can use system entropy.

The global seed does not affect generators that already exist. Other threads have separate seed streams. A different operation order can assign different seeds to unseeded operations. Call clear_global_seed when subsequent operations must not use the stream.

Neural networks and solvers

Use SequentialBuilder::new_with_seed(42) before you build a neural network. It controls the batch shuffle seed. Set randomized layer seeds with with_random_state before building. Use model.set_seed(42) to control later randomized training operations. It does not reset existing weights.

Some solvers expose their own seed settings. For example, SVDSolver::Randomized accepts an explicit seed. The global seed does not replace this setting.

Seeds help repeat experiments with the same data, settings, and operation order. Do not expect identical floating-point results across all platforms or library versions.