资讯动态

【RustyML入门】2.2. 逻辑回归

发布时间:2026/8/5 11:18:48 来源:尧图企业网站定制
2.2. 逻辑回归2.2.1. 模型到底在优化什么每个样本先得到一个线性得分z w * x拟合截距时再加上偏置。sigmoidsigmoid(z) 1 / (1 e^-z)把这个得分映射到(0, 1)RustyML 把它读作正类的概率。训练最小化这些概率与标签之间的平均二元交叉熵优化器就是朴素的全批量梯度下降每次迭代在整个训练集上计算梯度(1/n) * X^T * (sigmoid(X * w) - y)然后沿反方向走一步步长为learning_rate。这个选择在实践中有两个后果。其一没有随机采样也没有随机初始化权重从精确的零开始更新是确定的。相同数据和超参数上跑两次得到的权重逐位相同测试套件验证了这一点。其二全批量梯度下降是只有一个全局步长的一阶方法对特征尺度和learning_rate的敏感程度远高于 scikit-learn 默认的拟牛顿求解器lbfgs、liblinear。这是它和那些求解器之间最大的行为差异也决定了 2.2.6 里的实用建议。RustyML 用数值稳定的 log-sum-exp 形式max(z, 0) - z * y ln(1 e^-|z|)计算损失而不是对 sigmoid 取对数因此幅度很大的 logit 不会让损失计算本身溢出。权重仍可能溢出2.2.6 节把它作为一种真实的故障模式来讨论。每次迭代的 logit、梯度和损失只要规模越过尺寸门限就交由 RustyML 的并行 GEMV 与确定性归约原语来算因此在同一台机器上结果可复现。详见 7.3. 性能调优与并行。2.2.2. 构造模型有两个构造器。LogisticRegression::default()给出一个合理的起点LogisticRegression::new(...)让你设定每一个超参数并在一开始就逐个校验。userustyml::machine_learning::LogisticRegression;fnmain(){// 默认值fit_intercept true, lr 0.01, max_iter 100, tol 1e-4, 无惩罚let_aLogisticRegression::default();// 显式指定new(fit_intercept, learning_rate, max_iterations, tolerance)let_bLogisticRegression::new(true,0.1,1000,1e-6).unwrap();}new返回ResultSelf, Error。遇到非法超参数会立刻用Error::InvalidParameter拒绝而不是拖到 fit 时才失败。参数类型默认值约束fit_interceptbooltrue无learning_ratef640.01严格为正且有限max_iterationsusize100至少为 1tolerancef641e-4严格为正且有限默认值刻意保守learning_rate 0.01加上仅100次迭代除了平凡、尺度良好的数据几乎收敛不了。把default()当作冒烟测试而不是生产配置。大多数真实拟合都需要更大的learning_rate先标准化数据和数千量级的max_iterations。每个存储的值都有 getter。其中 2 个是模型拟合后的诊断量Getter返回get_fit_intercept()boolget_learning_rate()f64get_max_iterations()usizeget_tolerance()f64get_regularization_type()OptionRegularizationTypeget_actual_iterations()Optionusize实际跑过的迭代次数fit 之前为Noneget_weights()OptionArray1f64fit 之前为Noneget_actual_iterations()就是收敛检查。相邻两次迭代的损失变化小于tolerance或者达到max_iterations时训练停止。如果返回的次数等于max_iterations就认定模型没有收敛到该容差。这时应调大max_iterations、调大learning_rate或标准化输入而不是相信这个边界值。2.2.3. 标签约定严格的 0 与 1fit接收特征矩阵x行为样本列为特征和目标向量y两者都必须是同一存储类型的f64数组。标签的取值域是精确的y的每个元素必须是0.0或1.0。RustyML 会拒绝其他任何值例如0.5、2.0或-1.0抛出Error::InvalidInput。这与SVC和LinearSVC的标签约定一致它们的预测结果同样是0.0和1.0。如果你的标签是别的编码先映射到{0, 1}。字符串或类别标签先用 4.3. 标签编码 编码。刻意挑好哪一类是正类也就是那个1因为这个选择定义了精确率、召回率和概率输出的含义。usendarray::array;userustyml::machine_learning::LogisticRegression;fnmain(){// 逻辑 AND编码为 {0.0, 1.0}letx_trainarray![[0.0,0.0],[0.0,1.0],[1.0,0.0],[1.0,1.0]];lety_trainarray![0.0,0.0,0.0,1.0];letmutmodelLogisticRegression::new(true,0.5,500,1e-7).unwrap();model.fit(x_train,y_train).unwrap();letpredsmodel.predict(x_train).unwrap();// Array1f64取值在 {0.0, 1.0}println!(predictions: {:?},preds);println!(iterations: {:?},model.get_actual_iterations());}fit_intercept为 true 时模型会把偏置作为权重的第 0 项前置get_weights()于是返回n_features 1个值。fit_intercept为 false 时它正好返回n_features个值。查看权重时这个下标很要紧因为正则化器对截距有特殊处理见下文。2.2.4. 预测硬标签还是概率RustyML 有 3 个预测入口硬标签与概率之间的区别正是最容易搞错的地方。predict(x) - ResultArray1f64, Error返回硬类别标签0.0或1.0做法是把正类概率以0.5为阈值二值化。predict_proba(x) - ResultArray1f64, Error返回(0, 1)之间的原始正类概率每个样本一个值即 sigmoid 的输出。fit_predict(mut self, x, y)先跑fit再在同一个x上跑predict方便快速核验训练集。predict完全等价于predict_proba再套一个固定的 0.5切分模型上没有阈值参数。这个固定切分对均衡问题没问题但 0.5 这条边界是建模选择不是规则。当假阳性和假阴性的代价不同、或类别不均衡时改调predict_proba自己定阈值。usendarray::array;userustyml::machine_learning::LogisticRegression;fnmain(){letx_trainarray![[-3.0],[-2.0],[-1.0],[1.0],[2.0],[3.0]];lety_trainarray![0.0,0.0,0.0,1.0,1.0,1.0];letmutmodelLogisticRegression::new(true,0.3,500,1e-7).unwrap();model.fit(x_train,y_train).unwrap();letx_testarray![[-0.5],[0.5]];letprobamodel.predict_proba(x_test).unwrap();// 正类概率letdefaultmodel.predict(x_test).unwrap();// 0.5 阈值 - {0.0, 1.0} 的 Array1f64// 更严格的工作点只有 p 0.8 才判为正类letstrict:Veci32proba.iter().map(|p|ifp0.8{1}else{0}).collect();println!(proba: {:?},proba);println!(default: {:?},default);println!(strict: {:?},strict);}这 3 个方法都会用训练时的特征数不含隐式的偏置列校验输入。对未拟合的模型调用返回Error::NotFitted。列数不对返回Error::DimensionMismatch。出现NaN或无穷元素返回Error::NonFinite。传入特征时不要手动加偏置列模型内部会自行增删这一列以匹配它训练时的方式。2.2.5. 正则化默认不带惩罚这一点与 scikit-learn默认 L2不同。用 builder 方法with_regularization加惩罚它会消费并返回模型因此可以直接接在new后面链式调用usendarray::array;userustyml::machine_learning::{LogisticRegression,RegularizationType};fnmain(){letxarray![[-4.0,-3.0],[-3.0,-4.0],[-2.0,-1.0],[2.0,1.0],[3.0,4.0],[4.0,3.0],];letyarray![0.0,0.0,0.0,1.0,1.0,1.0];letmutplainLogisticRegression::new(true,0.1,2000,1e-8).unwrap();plain.fit(x,y).unwrap();letmutridgeLogisticRegression::new(true,0.1,2000,1e-8).unwrap().with_regularization(RegularizationType::L2(5.0)).unwrap();ridge.fit(x,y).unwrap();// 特征权重的 L2 范数跳过下标 0 处那个不受惩罚的偏置letfeature_norm|m:LogisticRegression|{m.get_weights().unwrap().iter().skip(1).map(|w|w*w).sum::f64()};println!(no penalty: {:.4},feature_norm(plain));println!(L2(5.0): {:.4},feature_norm(ridge));}每个变体里的f64是惩罚强度alpha必须非负且有限alpha 0也接受等价于无惩罚。RegularizationType::L2(alpha)ridge在损失上加alpha * 0.5 * ||w||^2把权重平滑地朝零收缩。RegularizationType::L1(alpha)lasso加alpha * ||w||_1会把个别权重压到精确的零给出一个对特征选择很有用的稀疏模型。“精确的零”是字面意思。RustyML不是把 L1 以alpha * sign(w)的形式折进梯度里的次梯度步只能逼近零那种写法不管跑多久都给不出稀疏性。取而代之的是优化器先走完常规的梯度步再施加一个邻近步。每个特征权重按learning_rate * alpha做软阈值于是数据撑不起来的权重会落到0.0并停在那儿。这个方法叫 ISTA正是它让 L1 成为一个真正的特征选择器。用w.iter().skip(1).filter(|v| **v 0.0).count()就能数出这些零。有 3 个实现细节会改变你挑alpha的方式截距从不受惩罚。模型拟合截距时惩罚梯度从特征下标 1 算起偏置因而可以自由移动。这是标准且正确的选择正则化器不应妨碍模型平移决策边界的能力。比较权重范数时应跳过下标 0就像示例里那样。惩罚不除以样本数。数据项是平均对数损失但 RustyML 把惩罚以绝对值alpha * R(w)加进去。所以无论数据有多少行alpha的含义都固定不变把数据集复制一份正则化后的最优解不变测试套件核验了这个不变性。在这里alpha越大意味着正则化越强方向与 scikit-learn 的倒数C相反。alpha可以从 scikit-learn 的 SGD 估计器 1:1 搬过来从别的估计器则需要换算。上面「均值数据项加不作除法的惩罚项」这个目标恰好和SGDClassifier的一致所以在那边调好的alpha原样搬过来即可。若来自LogisticRegression(Cc)就用alpha 1 / (c * n)其中n是训练样本数。完整的换算表挂在RegularizationType上与LinearRegression共用。2.2.6. 标准化、类别不平衡与可分数据有 3 种故障模式常见到值得在这里点名。标准化你的特征。优化器是只有一个全局learning_rate的全批量梯度下降尺度天差地别的特征因而收敛速率也天差地别。对取值在[0, 1]的特征恰到好处的那一步对取值在[0, 10000]的特征就太小了。于是训练爬行损失在max_iterations之内就停在了够不着tolerance的地方。先用 4.2. 标准化与归一化 里的工具做中心化和缩放。这一步对收敛速度和数值稳定性的收益最大。它还能让你用上真正管用的learning_rate比如0.1到1.0而不是那个保守的默认值。没有类别加权。模型没有class_weight参数predict内部阈值固定在 0.5。在不均衡数据上它可能只学会预测多数类还照样报出一个不低的准确率。用 2 道防线来应对自己在从精确率-召回率或 ROC 分析选出的工作点上给predict_proba定阈值再用不会被不均衡骗到的指标来评估例如 5.2. 分类指标 里的balanced_accuracy、mcc或roc_auc而不是原始准确率。完全可分的数据会让无正则化的最大似然估计发散。当一个超平面把两类干净地分开时似然靠把权重范数推向无穷来最大化因为概率会饱和到 0 和 1。无正则化的梯度下降因而跑得越久、权重就越大。实践中max_iterations和tolerance会把训练截断分类结果保持正确。权重以及由此得到的概率仍然会变得数量级任意、校准很差。在幅度很大的可分输入上使用较大的learning_rate还可能让整个权重更新完全溢出。RustyML 会在循环内用Error::NonFinite守卫抓住它而不是悄悄返回NaN。哪怕只加一点 L2 惩罚也能给最优解定界、让概率保持有意义并消除溢出风险。这正是生产中默认保留惩罚的主要理由。2.2.7. 用多项式特征拟合非线性边界决策边界在你给它的特征空间里是线性的非线性问题因而需要更丰富的空间。generate_polynomial_features(x, degree)把每一行展开成直到degree次的所有单项式。2 个特征、2 次时这会给出 5 列[x1, x2, x1^2, x1*x2, x2^2]不含常数列截距已经提供了。在展开后的数据上拟合并在同样的展开上预测。usendarray::array;userustyml::machine_learning::{LogisticRegression,generate_polynomial_features};fnmain(){// 内圈 类别 0外圈 类别 1在 (x1, x2) 中不是线性可分的letxarray![[1.0,0.0],[0.0,1.0],[-1.0,0.0],[0.0,-1.0],[5.0,0.0],[0.0,5.0],[-5.0,0.0],[0.0,-5.0],];letyarray![0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0];// 2 个特征2 次 - [x1, x2, x1^2, x1*x2, x2^2]letx_polygenerate_polynomial_features(x,2);assert_eq!(x_poly.ncols(),5);letmutmodelLogisticRegression::new(true,0.01,3000,1e-7).unwrap();model.fit(x_poly,y).unwrap();// x1^2 x2^2 这一项让两个圆环变得线性可分letpredsmodel.predict(x_poly).unwrap();println!({:?},preds);}列数随特征数和次数组合式增长3 个特征、3 次就已经是 19 列。这个工具只适合少数几个特征、低次数的场合。当展开变大时改用像 SVC 这样的核方法。2.2.8. 带评估的完整示例这个示例在一个小的双特征数据集上拟合再用第 5 章的分类指标评估。predict已经把硬标签{0.0, 1.0}以Array1f64的形式返回这正是ConfusionMatrix::new所要求的。它自己不做任何阈值化所以会拒绝一个概率向量而不是悄悄把它二值化。roc_auc则反过来它要的是一个布尔真值向量配上连续的predict_proba得分因为排序才是它的重点。usendarray::{array,Array1};userustyml::machine_learning::LogisticRegression;userustyml::metrics::{ConfusionMatrix,accuracy,roc_auc};fnmain(){letx_trainarray![[-2.0,-1.5],[-1.5,-2.0],[-1.0,-0.5],[-2.5,-1.0],[-0.5,-1.0],[2.0,1.5],[1.5,2.0],[1.0,0.5],[2.5,1.0],[0.5,1.0],];lety_trainarray![0.0,0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0];letmutmodelLogisticRegression::new(true,0.5,500,1e-7).unwrap();model.fit(x_train,y_train).unwrap();// 已经是硬标签 {0.0, 1.0}正是 ConfusionMatrix::new 所要求的letpredsmodel.predict(x_train).unwrap();letcmConfusionMatrix::new(y_train,preds);println!({},cm.summary());println!(accuracy: {:.3},accuracy(y_train,preds));// ROC AUC 对概率排序所以它需要原始得分而不是 0/1 标签lettruth:Array1booly_train.mapv(|v|v0.5);letscoresmodel.predict_proba(x_train).unwrap();println!(ROC AUC: {:.3},roc_auc(truth,scores));}ConfusionMatrix::summary()会在一张表里打印计数连同准确率、平衡准确率、精确率、召回率、特异度、F1 和 MCC。它给出了核验二分类器最快的办法。把predict_proba而非阈值化后的标签喂给roc_auc正是这一点让 AUC 成为与阈值无关的排序质量度量。它回答的是模型把正样本排在负样本之上的能力有多好与你之后把切分点定在哪里无关。上面的输出取决于数据这里给出应当预期的形态Confusion Matrix: --------------------------------------------------------- | ... | Predicted Positive | Predicted Negative | ... Performance Metrics: - Accuracy: 0.0..1.0 - ... accuracy: 0.0..1.0 ROC AUC: 0.0..1.02.2.9. 持久化与可复现性训练好的模型通过save_to_path和load_from_path序列化为紧凑的 postcard 二进制携带权重、超参数和迭代次数。往返是逐字节精确的所以加载后模型的预测与原模型的预测分毫不差。usendarray::array;userustyml::machine_learning::LogisticRegression;fnmain(){letxarray![[-2.0,1.0],[-1.0,-1.0],[1.0,1.0],[2.0,-1.0]];letyarray![0.0,0.0,1.0,1.0];letmutmodelLogisticRegression::new(true,0.3,500,1e-7).unwrap();model.fit(x,y).unwrap();letpathlogreg_model.bin;model.save_to_path(path).unwrap();letloadedLogisticRegression::load_from_path(path).unwrap();assert_eq!(model.predict(x).unwrap(),loaded.predict(x).unwrap());std::fs::remove_file(path).unwrap();println!(round-trip OK);}拟合过程里没有任何随机性可复现性因而是白来的不像本章那些基于采样的模型这里没有种子要设。相同数据和超参数上的 2 次拟合会产生相同的权重这正是持久化往返能做到精确的原因。哪里的种子确实要紧见 7.1. 可复现性与随机种子序列化格式及其跨版本的限制见 7.2. 深入模型持久化。如果你用show_progressfeature 构建fit还会渲染一个带实时损失的进度条方便你盯着上面描述的不收敛和发散行为。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价