rustyml 0.14.0

A high-performance machine learning & deep learning library in pure Rust, offering ML algorithms and neural network support
Documentation
# 1.3. Working with ndarray

Every matrix type in RustyML comes from [`ndarray`](https://docs.rs/ndarray/0.17/ndarray/). If you want the more advanced parts of `ndarray` — broadcasting rules, the various iterators, linear-algebra helpers — read the [official `ndarray` documentation](https://docs.rs/ndarray/0.17/ndarray/).

RustyML 0.14 depends on `ndarray` 0.17. Your own `Cargo.toml` needs to use the same `ndarray` version:

```toml
[dependencies]
ndarray = "0.17"
rustyml = { version = "0.14", features = ["full"] }
```

## 1.3.1. f64 versus f32

In RustyML the elements of a classical machine-learning matrix are **`f64`**, while the neural-network stack's tensor elements are **`f32`**.

The classical estimators (see [`machine_learning`](../Chapter-02/2.0._Classical_Machine_Learning.md)) take a two-dimensional **`Array2<f64>`** feature matrix laid out as samples by features (one row per sample), plus a one-dimensional **`Array1<_>`** target vector. The target's element type varies by model:

| Model | Target (`y`) | `predict` output |
| --- | --- | --- |
| `LinearRegression` | `Array1<f64>` | `Array1<f64>` |
| `LogisticRegression` | `Array1<f64>` (0.0 / 1.0) | `Array1<f64>` (0.0 / 1.0) |
| `DecisionTree` | `Array1<f64>` | `Array1<f64>` |
| `SVC`, `LinearSVC` | `Array1<f64>` | `Array1<f64>` |
| `LDA` | `Array1<i32>` | `Array1<i32>` |
| `KNN<T>` | `Array1<T>` (`T: Clone + Hash + Eq`) | `Array1<T>` |
| `KMeans`, `MeanShift`, `DBSCAN` | *(unsupervised)* | `Array1<isize>` (cluster ids, `-1` is noise) |
| `IsolationForest` | *(unsupervised)* | `Array1<i32>` (`-1` outlier / `+1` inlier; scores come from `score_samples`) |
| `PCA`, `KernelPCA` | *(unsupervised)* | `Array2<f64>` (via `transform`) |

The neural-network stack uses [`Tensor`](https://docs.rs/rustyml/latest/rustyml/neural_network/type.Tensor.html), which is a type alias:

```rust,ignore
pub type Tensor = ArrayD<f32>;
```

`Tensor` differs from the classical machine-learning matrices in two ways:

- The element type is **`f32`**: half the precision, half the memory, and the width every deep-learning framework standardizes on, because neural networks are compute-bound and the extra throughput is worth a few bits of precision.
- The dimensionality is **dynamic**: `ArrayD` (equivalently `Array<f32, IxDyn>`) carries its rank at run time rather than encoding it in the type. A wrong rank surfaces at run time as an `Err`; see [1.3.7]#137-shape-mismatch-errors.

## 1.3.2. Building arrays

Here are the common ways to build the `ndarray` matrix types:

```rust
use ndarray::{array, Array, Array1, Array2};

fn main() {
    // `array!`: the shape is inferred from the nesting — this is a 3x2 `Array2<f64>`
    let m = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
    assert_eq!(m.shape(), &[3, 2]);

    // `from_shape_vec`: (shape, flat row-major Vec)
    // The length must equal the product of the shape, or you get an Err
    let x = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
    assert_eq!(x.shape(), &[2, 3]);

    // `from_shape_fn`: give it a shape, plus a closure that produces each element
    let g = Array2::from_shape_fn((3, 2), |(i, j)| (i * 2 + j) as f64);
    assert_eq!(g[[2, 1]], 5.0);

    // `from_vec`: turn a `Vec<T>` into an `Array1<T>`
    let y = Array1::from_vec(vec![2.0, 4.0, 6.0]);
    // `from_iter`: fill an `Array1<T>` from an iterator
    let labels = Array1::from_iter(0..3i32);

    // Constant / all-zero / all-one fills, with the element type spelled out
    let zeros = Array2::<f64>::zeros((2, 2));
    let ones = Array2::<f64>::ones((2, 2));
    let filled = Array::from_elem((2, 2), 7.0f64);

    println!("{} {} {} {}", y.len(), labels.len(), zeros.sum(), ones.sum());
    println!("{}", filled.sum());
}
```

Most data arrives as nested `Vec`s, and that is when you reach for `from_shape_vec`. It reads the `Vec` in **row-major** order. Its return value is wrapped in a `Result`, and if you get an `Err`, the shape is almost certainly wrong.

Often you want to initialize a matrix **randomly**. The [`ndarray-rand` crate](https://crates.io/crates/ndarray-rand) provides `Array::random` for that, and you add it to your `Cargo.toml`:

```toml
[dependencies]
ndarray-rand = "0.16"
ndarray = "0.17"
```

Example:

```rust
use ndarray::Array;
use ndarray_rand::RandomExt; // the extension trait that gives ndarray's array types
                             // random construction — bring it into scope for `Array::random`
use ndarray_rand::rand_distr::Uniform; // samples uniformly between the given bounds

fn main() {
    let w = Array::random((4, 3), Uniform::new(-1.0, 1.0).unwrap());
    println!("{:?}", w.shape());
}
```

When you use RustyML, do not seed `ndarray-rand`'s generator by hand. RustyML has its own randomness management (`set_global_seed`, a per-model `with_random_state`, and `Sequential::new_with_seed`), so control randomness through RustyML's API instead; see [7.1 Reproducibility and Random Seeds](../Chapter-07/7.1._Reproducibility_and_Random_Seeds.md).

## 1.3.3. Inputs to the classical estimators

Here is the shape a typical *supervised* classical `fit` and `predict` signature takes:

```rust,ignore
pub fn fit<S1, S2>(&mut self, x: &ArrayBase<S1, Ix2>, y: &ArrayBase<S2, Ix1>) -> Result<&mut Self, Error>
where
    S1: Data<Elem = f64>,
    S2: Data<Elem = f64>,
```

Two things depart from it, both visible in the table in [1.3.1](#131-f64-versus-f32): `LDA`'s `y` is `Data<Elem = i32>` rather than `f64`, and the unsupervised estimators (`KMeans`, `DBSCAN`, `MeanShift`, `PCA`, `KernelPCA`) take the feature matrix alone, with no `y` argument at all.

`ndarray` has two kinds of borrow:

- a reference to an owned array (`&Array`)
- a view (`ArrayView`), which is what `.view()` and `.slice(..)` produce
- Both are zero-copy, and RustyML accepts either. The parameter is a *reference*, so write `&array` or `&array.view()`; a bare `array.view()` does not type-check.

You also have to keep the feature matrix two-dimensional (`Ix2`) and the target vector one-dimensional (`Ix1`).

```rust
use rustyml::machine_learning::LinearRegression;
use ndarray::{s, Array1, Array2};

fn main() {
    let x = Array2::from_shape_vec((4, 1), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
    let y = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0]);

    let mut model = LinearRegression::new(true);

    // A reference to the owned array
    model.fit(&x, &y).unwrap();

    // A reference to a view
    model.fit(&x.view(), &y.view()).unwrap();

    // A reference to a slice: train on the first three rows only
    // This subview is a borrow into `x`
    let x_head = x.slice(s![0..3, ..]);
    let y_head = y.slice(s![0..3]);
    model.fit(&x_head, &y_head).unwrap();
}
```

What `.view()` and `.slice(..)` really buy you is the ability to feed a **subset** of an array — a range of rows, a strided window, a single column reshaped — straight into `fit` or `predict` without first materializing that subset as a new owned array. To train on rows 0..800 of a 1000-row matrix, `x.slice(s![0..800, ..])` is all it takes.

## 1.3.4. Inputs to the neural-network stack

The neural layers want `ArrayD<f32>`, but you almost never construct one directly. The usual path is to build a fixed-dimension array and then convert it to the dynamic dimension `IxDyn`. That conversion is a thin wrapper; it does not touch the data:

```rust
use rustyml::neural_network::sequential::Sequential;
use rustyml::neural_network::layers::{Activation, Dense};
use rustyml::neural_network::optimizers::Adam;
use rustyml::neural_network::losses::MeanSquaredError;
use ndarray::Array2;

fn main() {
    // 4 samples, 2 features: build an `Array2<f32>` first, then convert to `ArrayD`
    let x = Array2::from_shape_vec(
        (4, 2),
        vec![0.0f32, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
    )
    .unwrap()
    .into_dyn(); // convert to a dynamic-dimension tensor
    let y = Array2::from_shape_vec((4, 1), vec![0.0f32, 1.0, 1.0, 0.0])
        .unwrap()
        .into_dyn(); // convert to a dynamic-dimension tensor

    let mut model = Sequential::new();
    model
        .add(Dense::new(2, 4, Activation::ReLU).unwrap())
        .add(Dense::new(4, 1, Activation::Linear).unwrap());
    model.compile(Adam::new(0.001, 0.9, 0.999, 1e-8, 0.0).unwrap(), MeanSquaredError::new());

    model.fit(&x, &y, 5).unwrap();
    let preds = model.predict(&x).unwrap();
    println!("prediction tensor shape: {:?}", preds.shape());
}
```

Output:

```
prediction tensor shape: [4, 1]
```

## 1.3.5. Input shapes

RustyML's input format is **one row per sample, one column per feature**.

The tensor shapes for neural-network layers match Keras's default `(batch, height, width, channels)`. The channel axis is always the trailing one and the batch axis is always the leading one, with the spatial axes in between — so a Keras user should feel at home. Here are the exact layouts:

| Layer | Input tensor | Weight tensor | Bias | Output tensor |
| --- | --- | --- | --- | --- |
| `Conv1D` | `[batch, length, Cin]` | `[k, Cin, filters]` | `[filters]` | `[batch, out_len, filters]` |
| `Conv2D` | `[batch, height, width, Cin]` | `[kh, kw, Cin, filters]` | `[filters]` | `[batch, out_h, out_w, filters]` |
| `Conv3D` | `[batch, depth, height, width, Cin]` | `[kd, kh, kw, Cin, filters]` | `[filters]` | `[batch, out_d, out_h, out_w, filters]` |
| `DepthwiseConv2D` | `[batch, height, width, C]` | `[kh, kw, C, dm]` | `[C·dm]` | `[batch, out_h, out_w, C·dm]` |
| `SeparableConv2D` | `[batch, height, width, Cin]` | depthwise `[kh, kw, Cin, dm]`, pointwise `[1, 1, Cin·dm, filters]` | `[filters]` | `[batch, out_h, out_w, filters]` |
| `MaxPooling{1,2,3}D`, `AveragePooling{1,2,3}D` | `[batch, spatial…, C]` ||| `[batch, pooled…, C]` |
| `GlobalMaxPooling{1,2,3}D`, `GlobalAveragePooling{1,2,3}D` | `[batch, spatial…, C]` ||| `[batch, C]` |
| `SpatialDropout{1,2,3}D` | `[batch, spatial…, C]` ||| same shape as the input |
| `BatchNormalization`, `GroupNormalization`, `InstanceNormalization` | `[batch, spatial…, C]` | `gamma`/`beta` `[C]` || same shape as the input |

For example:

- A batch of 8 single-channel 28×28 images is `[8, 28, 28, 1]`
- A bank of 3×3 filters producing 16 feature maps has weight shape `[3, 3, 1, 16]`

Here is a code example:

```rust
use rustyml::neural_network::layers::{Activation, Conv1D};
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    // filters=2, kernel=3, input_shape=[batch, length, channels], stride=1
    let mut conv = Conv1D::new(2, 3, vec![1, 6, 1], 1, Activation::Linear).unwrap();

    // Channels last: [batch=1, length=6, channels=1]
    let input = Array::from_shape_vec((1, 6, 1), vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0])
        .unwrap()
        .into_dyn();

    let output = conv.forward(&input).unwrap();
    println!("conv output shape: {:?}", output.shape()); // [1, 4, 2]
}
```

The constructor's `input_shape` argument records the `[batch, length, channels]` the layer is configured for. Passing a tensor whose rank or channel count disagrees is rejected at `forward` time. For the per-layer detail, see [3.5 Convolutional Layers](../Chapter-03/3.5._Convolutional_Layers.md) and [3.6 Pooling Layers](../Chapter-03/3.6._Pooling_Layers.md).

## 1.3.6. Practical advice

Real data rarely arrives as an `Array2`. The most common shape is a `Vec<Vec<f64>>`, one inner `Vec` per parsed CSV row. The conversion is to flatten it into a single row-major `Vec<T>` and hand it to `from_shape_vec` along with the `(rows, cols)` you counted:

```rust
use ndarray::Array2;

fn main() {
    // As if parsed from CSV: 3 rows, 2 columns
    let rows: Vec<Vec<f64>> = vec![
        vec![1.0, 2.0],
        vec![3.0, 4.0],
        vec![5.0, 6.0],
    ];
    let n_rows = rows.len();
    let n_cols = rows[0].len();

    // `flatten()` flattens the nested vectors in row-major order
    let flat: Vec<f64> = rows.into_iter().flatten().collect();
    let x = Array2::from_shape_vec((n_rows, n_cols), flat).unwrap();

    assert_eq!(x.shape(), &[3, 2]);
    println!("{:?}", x.row(0)); // [1.0, 2.0]
}
```

If your rows are ragged, `flatten` still flattens the nested vectors, but the length will not equal `n_rows * n_cols` and `from_shape_vec` returns an `Err`.

Other tools you will use:

- The [`s!`]https://docs.rs/ndarray/0.17/ndarray/macro.s.html macro expresses ranges and strides
- `concatenate` joins arrays along an existing axis
- `outer_iter` (equivalently `axis_iter(Axis(0))`) walks each row as a one-dimensional view, which is how you iterate sample by sample

Code example:

```rust
use ndarray::{array, concatenate, s, Axis};

fn main() {
    let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];

    // Slice: all rows, first two columns (drop a feature)
    let first_two_cols = x.slice(s![.., 0..2]);
    assert_eq!(first_two_cols.shape(), &[3, 2]);

    // Slice: a single row as a submatrix
    let middle = x.slice(s![1..2, ..]);
    assert_eq!(middle.shape(), &[1, 3]);

    // Concatenate two matrices along axis 0 (append rows)
    let more = array![[10.0, 11.0, 12.0]];
    let stacked = concatenate(Axis(0), &[x.view(), more.view()]).unwrap();
    assert_eq!(stacked.shape(), &[4, 3]);

    // Iterate each row as an `ArrayView1`
    // The sample-by-sample idiom
    for (i, row) in x.outer_iter().enumerate() {
        println!("row {i} sums to {}", row.sum());
    }
}
```

Turning raw labels into the `Array1` a model wants is usually a `map` over your source data collected into an array. When a model or a loss expects **one-hot** targets rather than integer class ids, use RustyML's [`to_categorical`](../Chapter-04/4.3._Label_Encoding.md) function, which turns an `Array1<i32>` of class ids into an `Array2<f64>` one-hot matrix:

```rust
use rustyml::utils::to_categorical;
use ndarray::Array1;

fn main() {
    // Map string labels to integer class ids, then into an `Array1<i32>`
    let raw = ["cat", "dog", "cat", "bird"];
    let ids: Array1<i32> = raw
        .iter()
        .map(|s| match *s {
            "cat" => 0,
            "dog" => 1,
            _ => 2,
        })
        .collect::<Vec<_>>()
        .into();

    // One-hot encode: 4 samples, 3 classes -> a (4, 3) `Array2<f64>`
    let onehot = to_categorical(&ids, None).unwrap();
    assert_eq!(onehot.shape(), &[4, 3]);
    println!("{:?}", onehot.row(1)); // [0.0, 1.0, 0.0]
}
```

Label encoding, its inverse, and the mapping-preserving variants are covered in full in [4.3 Label Encoding](../Chapter-04/4.3._Label_Encoding.md). Preprocessing your feature matrix with `standardize` and `normalize` is in [4.2 Standardization and Normalization](../Chapter-04/4.2._Standardization_and_Normalization.md).

## 1.3.7. Shape-mismatch errors

Two different error systems catch shape problems. Errors that happen while you are *building* an array come from `ndarray` — `from_shape_vec` returns `ndarray`'s `ShapeError` when the buffer length disagrees with the shape. Errors that happen while **RustyML** runs come back as RustyML's own [`Error`](../Chapter-01/1.6._Error_Handling.md) enum, which subdivides them further:

- `Error::DimensionMismatch { expected, found }`: two scalar counts disagree, such as the feature count at `predict` time versus at `fit` time, or `x.nrows()` versus `y.len()`
- `Error::ShapeMismatch { expected, found }`: two whole tensor shapes disagree, such as a gradient in the neural-network stack not matching the activation it flows into
- `Error::InvalidInput(_)`: the input's rank is wrong, such as handing a 2-D tensor to a `Conv1D` that expects 3-D

The one you will hit most often is a feature-count mismatch: you train on `p` features and then call `predict` with a matrix that has a different number of columns. RustyML validates this and returns `DimensionMismatch`:

```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::error::Error;
use ndarray::{Array1, Array2};

fn main() {
    // Train on 2 features
    let x = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 2.0, 3.0, 3.0, 4.0]).unwrap();
    let y = Array1::from_vec(vec![6.0, 9.0, 12.0]);
    let mut model = LinearRegression::new(true);
    model.fit(&x, &y).unwrap();

    // Predict with 3 features
    // The model returns an error
    let bad = Array2::from_shape_vec((1, 3), vec![4.0, 5.0, 6.0]).unwrap();
    match model.predict(&bad) {
        Err(Error::DimensionMismatch { expected, found }) => {
            println!("expected {expected} features, got {found}");
        }
        other => println!("unexpected: {other:?}"),
    }
}
```

Because `Error` is marked `#[non_exhaustive]`, a `match` over it always needs a trailing `_ =>` arm, which keeps your code compiling when new error variants are added. For the full error-handling story, see [1.6 Error Handling](../Chapter-01/1.6._Error_Handling.md).