rustyml 0.15.0

A high-performance machine learning & deep learning library in pure Rust, offering ML algorithms and neural network support
Documentation
# 1.3. 使用ndarray准备数据

Rust的所有矩阵类型全部使用[`ndarray`](https://docs.rs/ndarray/0.17/ndarray/)提供的类型。你如果你想要更加详细地了解ndarray的进阶用法,比如广播规则、各类迭代器、线性代数辅助函数,可以去看[官方ndarray文档](https://docs.rs/ndarray/0.17/ndarray/)。

RustyML 0.15依赖ndarray 0.17。在你自己的`Cargo.toml`里需要使用相同的ndarry版本:

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

## 1.3.1. f64与f32

RustyML中经典机器学习矩阵元素类型是**`f64`**,神经网络栈张量元素用**`f32`**。

经典机器学习估计器(详见[`machine_learning`](../Chapter-02/2.0._经典机器学习.md))接受一个二维的**`Array2<f64>`**特征矩阵,按样本x特征排布(每行一个样本),还需要接收一个一维的**`Array1<_>`**目标向量。目标的元素类型随模型而变:

| 模型                            | 目标向量`y`                           | `predict` 输出                                              |
|---------------------------------|---------------------------------------|-------------------------------------------------------------|
| `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` | *(无监督)*                          | `Array1<isize>`(簇id,`-1`表示噪声)                       |
| `IsolationForest`               | *(无监督)*                          | `Array1<i32>``-1`离群 / `+1`正常;分数走`score_samples`|
| `PCA``KernelPCA`              | *(无监督)*                          | `Array2<f64>`(经由`transform`|

神经网络栈使用[`Tensor`](https://docs.rs/rustyml/latest/rustyml/neural_network/type.Tensor.html),它是一个类型别名:

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

`Tensor`相比经典机器学习的矩阵有两处不同:
- 元素类型是**`f32`**:精度减半、内存减半,也是每个深度学习框架都统一采用的位宽,因为神经网络是计算密集型任务,值得用部分进度换取更大的吞吐量。
- 维度是**动态**的:`ArrayD`(等价于 `Array<f32, IxDyn>`)在运行时携带自己的阶(rank),而非把它编进类型里。阶不对会在运行时以`Err`报错,详见[1.3.7]#137-形状不匹配错误
## 1.3.2. 构造数组

以下是一些常见的构造`ndarray`矩阵类型的方式:

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

fn main() {
    // `array!`: 形状由嵌套推断而来,比如这是一个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`: (形状, 行主序扁平 Vec)
    // 长度必须等于形状各维之积,否则返回 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`: 通过参数指定形状,并且可以通过闭包来制造每个元素的值
    let g = Array2::from_shape_fn((3, 2), |(i, j)| (i * 2 + j) as f64);
    assert_eq!(g[[2, 1]], 5.0);

    // `from_vec`: 把`Vec<T>`转化成`Array1<T>`
    let y = Array1::from_vec(vec![2.0, 4.0, 6.0]);
    // `from_iter`: 使用迭代器的值来填充`Array1<T>`
    let labels = Array1::from_iter(0..3i32);

    // 常量 / 全0 / 全1填充,需要指定元素类型
    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());
}
```

大部分的数据会以`Vec`嵌套的形式出现,这时候就应该使用`from_shape_vec`。它按**行主序**读取这个`Vec`。`from_shape_vec`的返回值被`Result`包裹,如果返回值是`Err`,大概率是形状没写对。

很多时候我们希望**随机**初始化矩阵内容。这时候可以使用[`ndarray-rand` crate](https://crates.io/crates/ndarray-rand)提供了`Array::random`,你需要在`Cargo.toml`里面把它加入依赖项:

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

代码例:

```rust
use ndarray::Array;
use ndarray_rand::RandomExt; // 给ndarray的矩阵类型使用的用于构建随机元素生成的扩展trait
                             // 把它引入作用域才有`Array::random`可以用
use ndarray_rand::rand_distr::Uniform; // 在指定的上限和下限之间均匀采样以生成随机数

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

搭配RustyML使用时不要手动去给`ndarray-rand`的随机数生成器播种。RustyML有自己的随机性管理系统(`set_global_seed`、每个模型上的`with_random_state`,以及`Sequential::new_with_seed`),你可以通过RustyML提供的API来控制随机性,详见[7.1 可复现性与随机种子](../Chapter-07/7.1._可复现性与随机种子.md)。

## 1.3.3. 经典机器学习估计器的输入

下面是典型的*有监督*经典估计器`fit`和`predict`的签名长相:

```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>,
```

有两处与它不同,在[1.3.1](#131-f64与f32)的表格里都能看到:`LDA`的`y`是`Data<Elem = i32>`而不是`f64`;无监督的估计器(`KMeans`、`DBSCAN`、`MeanShift`、`PCA`、`KernelPCA`)只接收特征矩阵,根本没有`y`这个参数。

ndarray中有两种引用类型:
- 拥有型数组的引用(`&Array`)
- 视图(`ArrayView`)
- 这两种写法都是零拷贝的,RustyML两种都接受。但参数类型是*引用*,所以要写成`&array``&array.view()`,直接传`array.view()`是通不过类型检查的。

同时还得保证特征矩阵是二维(`Ix2`)的,目标向量是一维的(`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);

    // 拥有型数组的引用
    model.fit(&x, &y).unwrap();

    // 视图的引用
    model.fit(&x.view(), &y.view()).unwrap();

    //切片的引用:只在前三行上训练
    // 这个子视图是对`x`的借用
    let x_head = x.slice(s![0..3, ..]);
    let y_head = y.slice(s![0..3]);
    model.fit(&x_head, &y_head).unwrap();
}
```

`.view()` 和 `.slice(..)` 真正的价值在它们让你能把数组的一个**子集**(若干行、跨步窗口、reshape过的单列)直接喂给`fit`或`predict`,而不必先把这个子集实例化成一个新的拥有型数组。假如说在一个1000行矩阵的0到800行上训练时就可以使用`x.slice(s![0..800, ..])`。

## 1.3.4. 神经网络栈的输入

神经网络层要的是`ArrayD<f32>`,但你几乎从不直接构造它。最常见的做法是先建一个固定维度的数组,然后转为动态维度`IxDyn`。这个转换只是一层轻量包装,不碰底层数据:

```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个样本,2个特征,先建成`Array2<f32>`,再转为`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(); // 转为动态维度张量
    let y = Array2::from_shape_vec((4, 1), vec![0.0f32, 1.0, 1.0, 0.0])
        .unwrap()
        .into_dyn(); // 转为动态维度张量

    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());
}
```

输出:

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

## 1.3.5. 输入形状

RustyML的输入格式为**一行是一个样本,一列是一个特征**。

神经网络层的张量形状跟Keras默认的`(batch, height, width, channels)`一致。通道轴永远是最后一根,batch轴永远是最前一根,空间轴夹在中间。对于Keras的用户来说应该很好上手。下面是精确的布局:

|| 输入张量                             | 权重张量                                                           | 偏置        | 输出 张量                               |
|---------------------------------------------------------------------|--------------------------------------|--------------------------------------------------------------------|-------------|-----------------------------------------|
| `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]` |
| `Conv1DTranspose`                                                   | `[batch, length, Cin]`               | `[k, filters, Cin]`                                                | `[filters]` | `[batch, out_len, filters]`             |
| `Conv2DTranspose`                                                   | `[batch, height, width, Cin]`        | `[kh, kw, filters, Cin]`                                           | `[filters]` | `[batch, out_h, out_w, filters]`        |
| `Conv3DTranspose`                                                   | `[batch, depth, height, width, Cin]` | `[kd, kh, kw, filters, Cin]`                                       | `[filters]` | `[batch, out_d, out_h, out_w, filters]` |
| `DepthwiseConv1D`                                                   | `[batch, length, C]`                 | `[k, C, dm]`                                                       | `[C·dm]`    | `[batch, out_len, C·dm]`                |
| `DepthwiseConv2D`                                                   | `[batch, height, width, C]`          | `[kh, kw, C, dm]`                                                  | `[C·dm]`    | `[batch, out_h, out_w, C·dm]`           |
| `SeparableConv1D`                                                   | `[batch, length, Cin]`               | depthwise `[k, Cin, dm]`、pointwise `[1, Cin·dm, filters]`         | `[filters]` | `[batch, out_len, filters]`             |
| `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]`               ||| 与输入同形                              |
| `BatchNormalization``GroupNormalization``InstanceNormalization` | `[batch, spatial…, C]`               | `gamma`/`beta` `[C]`                                               || 与输入同形                              |
| `PReLU`                                                             | `[batch, d1…]`                       | `alpha [d1…]`,每个共享轴上为 `1`                                  || 与输入同形                              |

例子:
- 一批8张单通道28×28的图像是`[8, 28, 28, 1]`
- 一组产生16张特征图、kernel为3×3的滤波器,权重形状是`[3, 3, 1, 16]`

下面是一个代码例:

```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();

    // 通道在后:[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]
}
```

构造器的`input_shape`参数记录了这一层针对的`[batch, length, channels]`。传入一个阶或通道数对不上的张量,会在`forward`时被拒。逐层的细节见[3.5 卷积层](../Chapter-03/3.5._卷积层.md)与[3.6 池化层](../Chapter-03/3.6._池化层.md)。

## 1.3.6. 实战建议

真实数据很少直接以`Array2`的形式到来。最常见的形态是 `Vec<Vec<f64>>`(每个内层`Vec`对应解析出来的一行CSV)。转换的办法是把它压平成一个行主序`Vec<T>`,再数出`(rows, cols)`一起交给`from_shape_vec`:

```rust
use ndarray::Array2;

fn main() {
    // 假设从CSV解析而来:3行 2列
    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()`按行主序展平嵌套向量
    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]
}
```

如果你的行长短不齐`flatten`照样能展平嵌套向量,但它的长度不会等于 `n_rows * n_cols`,`from_shape_vec`会返回`Err`。

其他的使用工具:
- [`s!`]https://docs.rs/ndarray/0.17/ndarray/macro.s.html宏可以用来表达区间和跨步
- `concatenate`沿已有的轴把数组接起来
- `outer_iter`(等价于 `axis_iter(Axis(0))`)把每一行当作一维视图逐个走过,这正是逐样本迭代的方式

代码例:

```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]];

    // 切片: 所有行,前两列(丢掉一个特征)
    let first_two_cols = x.slice(s![.., 0..2]);
    assert_eq!(first_two_cols.shape(), &[3, 2]);

    // 切片: 取单行作为子矩阵
    let middle = x.slice(s![1..2, ..]);
    assert_eq!(middle.shape(), &[1, 3]);

    // 沿0轴拼接两个矩阵(追加行)
    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]);

    // 以`ArrayView1`迭代每一行
    // 逐样本的惯用写法
    for (i, row) in x.outer_iter().enumerate() {
        println!("row {i} sums to {}", row.sum());
    }
}
```

把原始标签变成模型想要的`Array1`,通常就是对源数据`map`一下再`collect`成数组。当某个模型或损失函数要的是**one-hot**目标、而不是整数类别id时,用RustyML提供的[`to_categorical`](../Chapter-04/4.3._标签编码.md)函数,它把一个装着类别id的`Array1<i32>`变成`Array2<f64>`的one-hot矩阵:

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

fn main() {
    // 把字符串标签映射为整数类别id,再转成`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编码:4个样本,3个类别 -> (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]
}
```

标签编码、它的逆操作,以及保留映射关系的各种变体,在[4.3 标签编码](../Chapter-04/4.3._标签编码.md)里有完整介绍。用`standardize`和`normalize`预处理特征矩阵则见[4.2 标准化与归一化](../Chapter-04/4.2._标准化与归一化.md)。

## 1.3.7. 形状不匹配错误

有两套不同的错误系统在抓形状问题。发生在你**构造**数组阶段的错误来自ndarray,比如当缓冲区长度和形状对不上时,`from_shape_vec`返回ndarray的`ShapeError`。发生在**RustyML**运行期间的错误会返回RustyML自己的[`Error`](../Chapter-01/1.6._错误处理.md)枚举,它还会对错误进行进一步细分:
- `Error::DimensionMismatch { expected, found }`:两个标量不一致,比如`predict`时的特征数与`fit`时不一致,又比如`x.nrows()``y.len()`不一致
- `Error::ShapeMismatch { expected, found }`:两个完整的张量形状对不上,比如神经网络栈里某个梯度和它要流入的激活形状不符
- `Error::InvalidInput(_)`:输入的阶是错的,比如把一个二维张量递给期望三维的 `Conv1D`

最常撞上的错误是特征数不匹配,比如用`p`个特征训练,随后拿一个列数不同的矩阵去调`predict`。RustyML会校验这一点并返回`DimensionMismatch`:

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

fn main() {
    // 用2个特征训练
    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();

    // 用3个特征预测
    // 模型会返回错误
    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:?}"),
    }
}
```

因为`Error`标了`#[non_exhaustive]`,对它做`match`时必须要带`_ =>`这种分支,这样在新增错误变体时代码依然能编译,详细的错误处理信息见[1.6 错误处理](../Chapter-01/1.6._错误处理.md)。