1.6. 错误处理
1.6.1. 一种错误类型
RustyML只有一种错误类型rustyml::error::Error。RustyML里每一个可能失败的操作都返回Result<T, rustyml::error::Error>,它还有一个别名:
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。
代码例:
userustyml::neural_network::sequential::Sequential;userustyml::neural_network::layers::Dense;userustyml::neural_network::layers::activation::ReLU;userustyml::neural_network::NnError;userustyml::error::Error;usendarray::Array;fnmain(){letmutmodel=Sequential::new();model.add(Dense::new(4,2,ReLU::new()).unwrap());letx=Array::ones((3,4)).into_dyn();lety=Array::ones((3,2)).into_dyn();// 没有调用 compile(),所以还没配置优化器和损失函数matchmodel.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)有以下两个变体:
NotClassificationTreeCorruptStructure(&'static str)
代码例:
userustyml::machine_learning::{Algorithm,DecisionTree,TreeError};userustyml::error::Error;usendarray::array;fnmain(){// 回归树(is_classifier = false)没有各类别的概率lettree=DecisionTree::new(Algorithm::CART,false).unwrap();letx=array![[1.0,2.0]];matchtree.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序列化)ModelStructureMismatch(String)对应加载的神经网络文件与目标架构对不上的情况(层数不同、某个位置的层类型不同,或某个权重的形状放不进目标层)UnsupportedModelFormat(String)对应这个文件根本不是RustyML模型文件,或者它的磁盘格式版本不是当前构建写出的那个版本
代码例:
userustyml::machine_learning::LinearRegression;userustyml::error::{Error,IoError};fnmain(){matchLinearRegression::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. 深入模型持久化。
1.6.3. 匹配具体的变体
最日常的失败是在fit之前就调用predict,这会导致返回Error::NotFitted,并把自己的名字作为&'static str带上:
userustyml::machine_learning::LinearRegression;userustyml::error::Error;usendarray::array;fnmain(){// 已构造,但从未训练letmodel=LinearRegression::new(true);letx=array![[1.0,2.0],[3.0,4.0]];matchmodel.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和?之外什么都不需要:
userustyml::machine_learning::{LinearRegression,RegularizationType};userustyml::error::RustymlResult;usendarray::{array,Array1,Array2};fntrain_and_predict(x:&Array2<f64>,y:&Array1<f64>)->RustymlResult<Array1<f64>>{// 下面每个 ? 都会从一次可能失败的调用中抬出一个 rustyml::error::Errorletmutmodel=LinearRegression::new(true).with_regularization(RegularizationType::L2(0.01))?;// 可能是 InvalidParametermodel.fit(x,y)?;// 可能是 EmptyInput / DimensionMismatch / NonFiniteletpreds=model.predict(x)?;// 可能是 NotFitted / DimensionMismatchOk(preds)}fnmain(){letx=array![[1.0],[2.0],[3.0]];lety=Array1::from_vec(vec![2.0,4.0,6.0]);matchtrain_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!的),就优先用闭包形式,这样成功路径就不用执行闭包:
userustyml::error::{Context,Error,RustymlResult};fnparse_threshold(raw:&str)->RustymlResult<f64>{// 一个标准库的 ParseFloatError,连同我们的 context 一起包装成 Error::Computation,// 它的 source() 链得以保留,供之后向下转型使用。letvalue:f64=raw.parse().with_context(||format!("parsing threshold from {raw:?}"))?;Ok(value)}fnmain(){matchparse_threshold("not-a-number"){Ok(v)=>println!("threshold = {v}"),Err(Error::Computation{context,source})=>{println!("{context}");ifletSome(cause)=source{println!(" caused by: {cause}");}}Err(e)=>println!("unexpected: {e}"),}}外部错误会成为Error::Computation的source,可以经由标准的std::error::Error::source()链拿到,并向下转型回原本的具体类型不丢失任何信息。
1.6.5. 及早校验
RustyML错误处理设计是任何接收超参数的入口都会及早校验并返回Result,而不是在遇到非法输入时panic。
userustyml::machine_learning::LinearRegression;userustyml::machine_learning::linear_model::LeastSquaresSolver;userustyml::error::Error;fnmain(){// learning_rate必须为正且有限// 0.0会返回错误matchLinearRegression::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 0println!("bad parameter `{name}`: {reason}");}Err(e)=>println!("unexpected: {e}"),}}有些地方还是会直接panic:
metrics与math模块的函数在遇到错误时直接 panic,而不返回Result,这是为了保持模块的轻量化。- RustyML之外的ndarray操作是返回
Result还是直接panic是ndarray决定的,RustyML无法干涉。