# 1.6. 错误处理
## 1.6.1. 一种错误类型
RustyML只有一种错误类型`rustyml::error::Error`。RustyML里每一个可能失败的操作都返回`Result<T, rustyml::error::Error>`,它还有一个别名:
```rust,ignore
pub type RustymlResult<T> = std::result::Result<T, Error>;
```
`Error`不在`prelude`里,错误相关的内容要单独引入`use rustyml::error::Error;`以及其他内容。 以下是`Error`的变体:
| 变体 | 触发场景 | `Display` 信息(`{}` / `to_string()`) |
|-----------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------|
| `EmptyInput(String)` | 需要数据的地方传入了空数组、空向量或空数据集 | `input is empty: <what>` |
| `DimensionMismatch { expected, found }` | 两个标量计数对不上 | `dimension mismatch: expected <e>, found <f>` |
| `ShapeMismatch { expected, found }` | 两个张量的形状对不上(梯度与它流入的那个激活值) | `shape mismatch: expected [..], found [..]` |
| `NonFinite(String)` | 数据中或计算产出的某个值为 `NaN` / `inf` | `non-finite value (NaN or infinity) encountered in <where>` |
| `InvalidParameter { name, reason }` | 用户传入的超参数超出取值范围 | ``invalid parameter `<name>`: <reason>`` |
| `InvalidInput(String)` | 没有更具体变体可用时的校验失败(rank 不对、样本太少) | `invalid input: <msg>` |
| `NotFitted(&'static str)` | 在`fit`之前调用了需要已训练模型的方法 | ``model `<name>` has not been fitted; call `fit` before this operation`` |
| `NotConverged(String)` | 迭代算法始终未达到收敛条件 | `failed to converge: <msg>` |
| `Computation { context, source }` | 数值崩溃、不变量被破坏,或包装了一个外部错误 | `computation failed: <context>` |
| `NeuralNetwork(NnError)` | 神经网络特有的失败 | 透明转发自 `NnError` |
| `Tree(TreeError)` | 决策树特有的失败 | 透明转发自 `TreeError` |
| `Io(IoError)` | 文件系统或(反)序列化失败 | 透明转发自 `IoError` |
需要注意的是,`DimensionMismatch`比较的是标量计数,比如特征数、向量长度。而`ShapeMismatch`针对的问题是整个张量的形状不一致,主要出现在神经网络代码里。
`Error`标注了`#[non_exhaustive]`,这要求在对错误进行`match`时**必须**带一个通配`_ =>`(或 `Err(e) =>`)分支。
## 1.6.2. 子错误
`Error`的三个变体各自包装了一个更小的枚举。只与神经网络相关的问题(层状态、权重形状、编译)和只与树相关的问题(分类还是回归)都待在各自的枚举里。
**`NnError`**(位于 `rustyml::neural_network::NnError`)包含:
- `ForwardPassNotRun(&'static str)`
- `WeightShape { name, expected, found }`
- `NotCompiled(&'static str)`
- `EmptyModel`。
代码例:
```rust
use rustyml::neural_network::sequential::Sequential;
use rustyml::neural_network::layers::Dense;
use rustyml::neural_network::layers::activation::ReLU;
use rustyml::neural_network::NnError;
use rustyml::error::Error;
use ndarray::Array;
fn main() {
let mut model = Sequential::new();
model.add(Dense::new(4, 2, ReLU::new()).unwrap());
let x = Array::ones((3, 4)).into_dyn();
let y = Array::ones((3, 2)).into_dyn();
// 没有调用 compile(),所以还没配置优化器和损失函数
match model.fit(&x, &y, 1) {
Ok(_) => unreachable!("training should not have started"),
Err(Error::NeuralNetwork(NnError::NotCompiled(missing))) => {
println!("compile the model first: `{missing}` is not specified");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
**`TreeError`**(位于`rustyml::machine_learning::TreeError`)有以下两个变体:
- `NotClassificationTree`
- `CorruptStructure(&'static str)`
代码例:
```rust
use rustyml::machine_learning::{Algorithm, DecisionTree, TreeError};
use rustyml::error::Error;
use ndarray::array;
fn main() {
// 回归树(is_classifier = false)没有各类别的概率
let tree = DecisionTree::new(Algorithm::CART, false).unwrap();
let x = array![[1.0, 2.0]];
match tree.predict_proba(&x) {
Err(Error::Tree(TreeError::NotClassificationTree)) => {
println!("predict_proba is classification-only");
}
other => println!("unexpected: {other:?}"),
}
}
```
**`IoError`**(位于 `rustyml::error::IoError`)有四个变体:
- `Std(std::io::Error)` 对应文件系统失败
- `Serialization(postcard::Error)` 对应二进制格式(RustyML用[postcard](https://docs.rs/postcard)序列化)
- `ModelStructureMismatch(String)` 对应加载的神经网络文件与目标架构对不上的情况(层数不同、某个位置的层类型不同,或某个权重的形状放不进目标层)
- `UnsupportedModelFormat(String)` 对应这个文件根本不是RustyML模型文件,或者它的磁盘格式版本不是当前构建写出的那个版本
代码例:
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::error::{Error, IoError};
fn main() {
match LinearRegression::load_from_path("model_that_does_not_exist.bin") {
Ok(_) => unreachable!("the file should not exist"),
Err(Error::Io(IoError::Std(io_err))) => {
// io_err 是底层的 std::io::Error(这里的 kind 是 NotFound)。
println!("filesystem error: {io_err}");
}
Err(Error::Io(IoError::Serialization(e))) => {
println!("the file exists but is not a valid model: {e}");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
序列化格式与版本控制详见[7.2. 深入模型持久化](../Chapter-07/7.2._深入模型持久化.md)。
## 1.6.3. 匹配具体的变体
最日常的失败是在`fit`之前就调用`predict`,这会导致返回`Error::NotFitted`,并把自己的名字作为`&'static str`带上:
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::error::Error;
use ndarray::array;
fn main() {
// 已构造,但从未训练
let model = LinearRegression::new(true);
let x = array![[1.0, 2.0], [3.0, 4.0]];
match model.predict(&x) {
Ok(preds) => println!("{preds:?}"),
Err(Error::NotFitted(name)) => {
println!("`{name}` was not fitted; call fit() first");
}
Err(Error::DimensionMismatch { expected, found }) => {
println!("wrong feature count: model wants {expected}, got {found}");
}
// `Error`是`#[non_exhaustive]`,所以通配分支是强制的
Err(e) => println!("other error: {e}"),
}
}
```
`DimensionMismatch` 分支放在这里是为了展示写法,这次调用实际触发的是 `NotFitted`。但如果给一个已训练的模型进列数不对的矩阵,走的就是第二个分支了,此时`expected`是`fit`时看到的特征数,`found`是传进`predict`的那个。
## 1.6.4. 用`?`传播
RustyML整个库只使用一种错误类型,所以一整个管线上的错误都可以作为`Error`返回,除了`Result`和`?`之外什么都不需要:
```rust
use rustyml::machine_learning::{LinearRegression, RegularizationType};
use rustyml::error::RustymlResult;
use ndarray::{array, Array1, Array2};
fn train_and_predict(x: &Array2<f64>, y: &Array1<f64>) -> RustymlResult<Array1<f64>> {
// 下面每个 ? 都会从一次可能失败的调用中抬出一个 rustyml::error::Error
let mut model = LinearRegression::new(true)
.with_regularization(RegularizationType::L2(0.01))?; // 可能是 InvalidParameter
model.fit(x, y)?; // 可能是 EmptyInput / DimensionMismatch / NonFinite
let preds = model.predict(x)?; // 可能是 NotFitted / DimensionMismatch
Ok(preds)
}
fn main() {
let x = array![[1.0], [2.0], [3.0]];
let y = Array1::from_vec(vec![2.0, 4.0, 6.0]);
match train_and_predict(&x, &y) {
Ok(preds) => println!("got {} predictions", preds.len()),
Err(e) => eprintln!("pipeline failed: {e}"),
}
}
```
当你确实需要汇报外部错误(来自标准库或别的 crate),但是又想使用进这套错误处理体系、同时保留它的成因链时,就用`Context`扩展trait(需要把这个trait导入到作用域)。它为任何满足`Send + Sync + 'static`且实现了`std::error::Error`的`Result<T, E>`都做了实现,因此能和`?`配合。`context`会立即取用信息,`with_context`接收一个只在错误路径上运行的闭包,只要构造信息会带来分配(凡是用到 `format!` 的),就优先用闭包形式,这样成功路径就不用执行闭包:
```rust
use rustyml::error::{Context, Error, RustymlResult};
fn parse_threshold(raw: &str) -> RustymlResult<f64> {
// 一个标准库的 ParseFloatError,连同我们的 context 一起包装成 Error::Computation,
// 它的 source() 链得以保留,供之后向下转型使用。
let value: f64 = raw
.parse()
.with_context(|| format!("parsing threshold from {raw:?}"))?;
Ok(value)
}
fn main() {
match parse_threshold("not-a-number") {
Ok(v) => println!("threshold = {v}"),
Err(Error::Computation { context, source }) => {
println!("{context}");
if let Some(cause) = source {
println!(" caused by: {cause}");
}
}
Err(e) => println!("unexpected: {e}"),
}
}
```
外部错误会成为`Error::Computation`的`source`,可以经由标准的`std::error::Error::source()`链拿到,并向下转型回原本的具体类型不丢失任何信息。
## 1.6.5. 及早校验
RustyML错误处理设计是任何接收超参数的入口都会及早校验并返回`Result`,而不是在遇到非法输入时panic。
```rust
use rustyml::machine_learning::LinearRegression;
use rustyml::machine_learning::linear_model::LeastSquaresSolver;
use rustyml::error::Error;
fn main() {
// learning_rate必须为正且有限
// 0.0会返回错误
match LinearRegression::new(true).with_solver(LeastSquaresSolver::GradientDescent {
learning_rate: 0.0,
max_iter: 1000,
tol: 1e-6,
}) {
Ok(_) => unreachable!("a zero learning rate must not be accepted"),
Err(Error::InvalidParameter { name, reason }) => {
// bad parameter `learning_rate`: must be positive and finite, got 0
println!("bad parameter `{name}`: {reason}");
}
Err(e) => println!("unexpected: {e}"),
}
}
```
有些地方还是会直接panic:
- `metrics`与`math`模块的函数在遇到错误时直接 panic,而不返回`Result`,这是为了保持模块的轻量化。
- RustyML之外的ndarray操作是返回`Result`还是直接panic是ndarray决定的,RustyML无法干涉。