4. 数据预处理
原始数据几乎从不以模型想要的形态出现。特征列的量纲可能天差地别。类别标签可能是字符串,也可能是不连续的整数。而且,在拟合任何模型之前,你还需要一份真正独立的留出集。本章介绍rustyml::utils里的预处理工具:划分、标准化、归一化和标签编码。它们都藏在utilsfeature 之后,直接操作你的模型本来就要吃的 ndarray 数组。
如果你从 scikit-learn 过来,先记住这个模块的形状。它大部分是普通函数,而不是带状态的fit/transform估计器。每次调用只根据传进去的那个数组计算统计量,再返回一个全新的数组。例外是那几个 scaler:StandardScaler、MinMaxScaler、MaxAbsScaler、RobustScaler、Normalizer。它们确实把fit/transform契约带过了训练/测试的边界。每一个都会存下从训练矩阵学到的东西,好让之后的每一批数据都走同一个映射。不管走哪条路,顺序的纪律都归你。先划分数据,再拟合缩放。这样一来,从测试行推出来的任何东西就都不会漏进训练。每个函数都返回Result<_, Error>,并先校验输入。每个函数都会拒绝空数据集、形状不匹配,或者(在适用的检查项上)非有限值。还没读过错误处理的话,先去读一下。
按流水线顺序读这几节。先划分(4.1)。再缩放训练特征(4.2)。如果模型或损失函数需要 one-hot 目标,最后编码标签(4.3)。下面这段示例跑的正是这条顺序:
usendarray::{Array1,array};userustyml::utils::train_test_split::train_test_split;userustyml::utils::standardize::{StandardizationAxis,standardize};userustyml::utils::label_encoding::to_categorical;fnmain(){// 六个样本,两个量纲差异极大的特征,两个类别。letx=array![[1.0,20.0],[2.0,21.0],[3.0,19.0],[4.0,22.0],[5.0,18.0],[6.0,23.0],];lety:Array1<i32>=array![0,1,0,1,0,1];// 1. 先划分,缩放就永远看不到测试行。let(x_train,_x_test,y_train,_y_test)=train_test_split(x,y,Some(0.34),Some(42)).unwrap();// 2. 把每个特征列标准化到零均值、单位方差。letx_train=standardize(&x_train,StandardizationAxis::Column).unwrap();// 3. 为分类损失把整数标签做 one-hot 编码。lety_train=to_categorical(&y_train,None).unwrap();println!("x_train shape: {:?}",x_train.shape());println!("y_train shape: {:?}",y_train.shape());}4.1. 训练集与测试集划分
训练集与测试集划分把你的特征矩阵和标签切成互不相交的训练子集与测试子集。这样一来,评估数字才能如实反映未见过的数据。train_test_split做的是普通随机划分。train_test_split_stratified对每个类别单独划分,让两侧保持相同的标签比例。只要某个类别稀有,就选分层版本,因为普通划分可能把这个类别整个漏到某一侧。test_size默认取0.3。标签数组对元素类型是泛型的。传入random_state种子能让洗牌结果可复现(参见可复现性与随机种子)。
4.2. 标准化与归一化
标准化与归一化是两种常被混为一谈的不同缩放方式。standardize计算 z-score。它先减去均值,再除以标准差,让每个特征最终变成零均值、单位方差。normalize则按 L1、L2、Max 或自定义 Lp 阶数,把每一行或每一列缩放到单位范数。
这两个操作各自还有一种带状态的形式。带状态的形式会记住自己的训练统计量,好让测试划分或线上的单个样本,按模型训练时所用的数字来缩放。这两个带状态的形式分别是StandardScaler和Normalizer。与它们同族的还有MinMaxScaler(有界区间)、MaxAbsScaler(只看幅值,保住结构性的零)和RobustScaler(中位数与四分位距,让离群点决定不了尺度)。
列标准化是任何基于距离或梯度的模型之前的家常选择。KNN、SVM、PCA和神经网络,在特征量纲不一致时统统会失常。按行的单位范数归一化,则适合只看方向的向量,比如用余弦相似度比较的 TF-IDF 行。两个操作都会处理退化的那一路,而不是去除以一个趋近于零的尺度。standardize把除数强制设为 1.0,常量特征居中后变成全 0。normalize把接近零的条带原样保留。
4.3. 标签编码
标签编码在数据集自带的整数标签和分类损失所需的 one-hot 矩阵之间来回转换。to_categorical把一列非负i32标签变成 one-hot 矩阵。to_categorical_with_mapping对任意可哈希的标签(字符串也算)做同样的事,还会返回标签到索引的映射,方便你之后解码。to_sparse_categorical做逆向操作。它把 one-hot 或 softmax 概率矩阵按行取 argmax,还原成i32标签。用to_sparse_categorical就能把网络的 softmax 输出转回预测类别。如果你从 Keras 过来,这三件套对应的就是它的to_categorical。one-hot 目标恰好是分类交叉熵要吃的东西。