简介这是一份围绕投影追踪Projection Pursuit实现的机器学习资源基于Jerome Friedman与Werner Stuetzle的回归算法思想提供与Scikit-Learn兼容的多元投影追踪回归、降维估计器以及面向单变量分类的配套分类器适合想深入理解投影寻踪原理、或需要可扩展回归/分类组件的Python开发者使用。压缩包共23个文件以12个Python源码文件为核心辅以2个YAML配置、2个RST文档、1个TEX数学公式说明及Markdown介绍等整体仅31KB结构精简。项目同时示范了Travis CI、Coveralls、Sphinx、PyTest、PyPI与GitHub Pages部署以及如何通过Scikit-Learn estimator checks遵循PEP 8规范是学习算法实现与开源工程化的不错范例。目前已有149人学习适合中级以上机器学习开发者参考。1. 投影追踪是什么一个被低估的非线性回归和分类方法做高维数据建模时我经常遇到一种尴尬特征几十个线性回归解释不错但验证集方差特别大换成树模型精度上去了业务方又问“哪个特征起作用”。投影追踪回归和分类就是在这两者之间找平衡点。这份资源把多元投影追踪回归和单变量分类封装成了带 scikit-learn 接口的 Python 模块下载后不用改核心算法直接 import 就能用。它的核心思路不是把每个特征拎出来拟合而是先找数据方差最大的方向再在该方向上做单变量函数逼近从而隐含地学习特征交互项。适合做工业回归预测、风险评分、特征诊断的从业者也适合想从零理解这个算法的学生。2. 多元投影追踪回归与单变量分类从原理到 scikit-learn 接口的落地2.1 从多元回归到投影追踪为什么研究方向比逐特征拟合更有效传统多元回归假设 y 是特征的线性加权和可一旦真实关系藏在特征组合里比如 y g₁(x₁x₂) g₂(x₃·x₄)线性就拟合不了。投影追踪回归的核心循环分三步先找一个单位方向 v让投影向量 z Xv 与 y 的相关性最强再对 (z, y) 做一元回归得到光滑函数 g并计算残差 r y - g(z)把 r 当作新的 y继续找下一个方向直到残差变化低于阈值。最终模型写出来是y ≈ g₁(X·v₁) g₂(X·v₂) … g_K(X·v_K) ε这里 K 是 n_components也就是投影分量的个数。这个结构与梯度提升很像但每一轮搜索的不是原始特征而是特征的一个线性组合方向。所以两个特征联合影响 y 时投影方向 v 会同时给它们非零系数g 再拟合出它们的联合效应。这种设计天然绕开了“手工构造交互项”的繁琐流程。方向搜索不是一步到位的。我一般用梯度上升法迭代更新 v每一步让拟合优度增大一点然后立刻做归一化防止向量长度发散。tol 参数在这里很关键如果设到 1e-2 可能提前停止K 还没加够设到 1e-7 又太慢。我实际跑下来1e-4 是个比较稳的默认值。而且特征必须标准化否则方向向量会被大数值特征绑架这一点我在后面避坑章节还会提。分类版本的单变量思想更直接。它不拟合连续残差而是在每个投影方向上拟合类概率函数。具体说每轮搜索一个方向让投影后的类间均值差最大然后对该方向的投影值做一维逻辑回归得到 P(y1 | z)。多个方向合并后概率输出通过加权投票组合。这就是“单变量分类”的来历原始特征再多真正参与决策的是一串一维投影函数而不是高维密度估计。2.2 这份资源里有什么回归器和分类器的接口设计这份资源的核心是一个纯 Python 包名为 ppron。拆开目录后关键文件如下表文件路径作用ppron/init.py导出回归器和分类器两个类ppron/ppr.py投影追踪回归器实现包含方向搜索和光滑函数拟合ppron/ppc.py投影追踪分类器实现包含二分类概率输出ppron/smooth.py一元光滑器支持局部线性平滑和 RBF 平滑examples/regression_demo.py回归示例加载糖尿病数据集并输出 MSEexamples/classification_demo.py分类示例用人工数据演示 AUC 计算tests/test_ppr.py回归器单元测试tests/test_ppc.py分类器单元测试docs/conf.py文档构建配置pyproject.toml、tox.ini依赖声明和测试环境配置回归器构造函数如下class ProjectionPursuitRegressor: def __init__(self, n_components2, smooth_span0.5, smooth_modelocal_linear, max_iter50, tol1e-5): pass参数含义要分清。n_components 控制模型最终保留几个投影方向太大会把残差噪声也拟合进来smooth_span 是一元光滑函数的窗口比例取值在 0 到 1 之间越大曲线越平对异常值越钝感smooth_mode 有两个选项local_linear 是局部线性回归rbf 是径向基函数后者更能拟合剧烈波动但计算代价高max_iter 是每个方向搜索时的最大迭代次数tol 是目标函数提升量阈值。分类器接口多了一个 penalty 参数class ProjectionPursuitClassifier: def __init__(self, n_components3, smooth_span0.5, smooth_modelocal_linear, max_iter200, tol1e-5, penalty1e-3): passpenalty 控制投影系数的 L2 正则强度防止类别分离时系数爆炸。两个类都实现了 fit、predict、predict_proba并且能无缝进入 scikit-learn 的 Pipeline 和 GridSearchCV。这一点比很多开源实现都贴心因为不用手工管理交叉验证过程。平滑器中还有一处边界处理如果测试集投影值落在训练集范围之外光滑函数会自动裁剪到训练时的最小/最大值。这个细节看起来小实际能避免外推产生的极端输出后面排查部分我会结合具体现象说明。3. 跑通回归与分类示例环境安装、训练评估与参数调整3.1 准备环境与安装依赖先用虚拟环境隔离依赖避免污染系统 Python。我习惯用 venv也可以换成 conda。在项目根目录执行python -m venv ppron-env source ppron-env/bin/activate pip install numpy scikit-learn pytestnumpy 负责多维数组运算scikit-learn 提供数据集和评估工具pytest 用于跑测试。这三者是运行示例的最低依赖。安装完先跑一遍测试确认资源没有被改动破坏pytest tests/ -v全部通过后再进示例。如果后续想做可视化还需要单独安装 matplotlib核心模块不依赖它。3.2 回归示例加载糖尿病数据集并拟合投影追踪模型回归示例用 scikit-learn 的糖尿病数据集它有 10 个特征目标变量是连续值。直接跑原始特征不是不行但为了投影方向稳定我会把标准化放进 Pipelinefrom ppron import ProjectionPursuitRegressor from sklearn.datasets import load_diabetes from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline X, y load_diabetes(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) pipeline Pipeline(steps[ (scaler, StandardScaler()), (ppr, ProjectionPursuitRegressor(n_components3, smooth_span0.4)), ]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) mse mean_squared_error(y_test, y_pred) print(Test MSE:, mse)代码说明了三件事。第一ProjectionPursuitRegressor 能直接放进 Pipelinescaler 在训练时学习均值和方差预测时自动复用避免数据泄漏。第二n_components 设成 3对应糖尿病数据集中我认为比较主要的三个交互方向smooth_span 设成 0.4让光滑函数在灵活和平滑之间取中间值。第三pipeline.fit 会调用 ppr.fit 并保存投影方向向量pipeline.predict 则依次做标准化、投影、累加。如果这里跑出的 MSE 比随机森林高不用意外。投影追踪的优势是可解释性和对中等维度数据的稳定表现不是跟树模型比拼非线性极限。我一般用它做诊断先看投影方向权重再决定要不要换更重的模型。smooth_span 也有讲究0.4 在大多数连续回归任务中表现均衡如果残差图出现系统性弯曲可以尝试降到 0.2 或升到 0.6。3.3 分类示例用人造月牙数据做二分类并输出概率分类示例使用 scikit-learn 自带的 make_moons 数据集。这是一个标准二分类 bench线性模型只能勉强分离投影追踪应该能学到弯曲的决策边界。示例代码from ppron import ProjectionPursuitClassifier from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, roc_auc_score X, y make_moons(n_samples600, noise0.15, random_state42) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) clf ProjectionPursuitClassifier( n_components4, smooth_span0.5, max_iter200, penalty1e-2 ) clf.fit(X_train, y_train) y_prob clf.predict_proba(X_test)[:, 1] y_pred clf.predict(X_test) print(Accuracy:, accuracy_score(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_prob))n_components 设成 4因为月牙数据的边界至少需要两个方向交叉才能表达。max_iter 从默认 50 提高到 200是因为分类目标函数比回归更不平滑梯度迭代需要更多轮数才能落到稳定点。penalty 设为 1e-2用来抑制投影系数爆炸。predict_proba 是分类器的核心价值。很多自己实现的投影追踪分类器只给类别标签不给概率有了概率就能做排序、调阈值、算 AUC。实际跑完后AUC 应该在 0.9 以上。如果 AUC 低于 0.5说明方向搜索陷入局部最优这时要降低 n_components 或者切换 smooth_mode 为 rbf。我遇到过这种翻车把 n_components 从 6 降到 3 后AUC 从 0.47 跳回 0.91根源就是后几个方向在拟合噪声。4. 投影追踪避坑指南四个常见问题和排查思路4.1 特征尺度不统一导致投影方向漂移现象训练时用原始特征跑测试集分数波动大切换随机种子后结果差异明显。查看投影方向权重发现数值范围大的特征总是占据主导位置。原因投影追踪搜索方向时目标函数高度依赖投影方差。如果特征没有标准化量级大的特征天然贡献更多方差算法会优先选择它弱信号特征就被忽略了。解决把标准化放进 Pipeline并且保证 fit 和 predict 走同一套 scaler。错误做法是训练时手动 transform测试时又用另一套 scaler。正确写法from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) reg ProjectionPursuitRegressor(n_components3, smooth_span0.4) reg.fit(X_train_scaled, y_train) y_pred reg.predict(X_test_scaled)我在业务环境里习惯把 scaler 和 reg 一起包进 Pipeline这样线上推理少传一个参数也避免预测时漏掉标准化。4.2 组件数 K 从小调大后测试集误差反弹现象n_components 从 2 调到 5训练集 MSE 持续下降但验证集 MSE 在第 4 个分量附近开始上升。原因投影追踪是加和模型每次增加一个方向模型复杂度就加一层。当数据量不够或方向本身是噪声时新分量只是在拟合残差的随机波动。这和决策树不剪枝的道理一样不是越深越好。解决用交叉验证选 K不要凭感觉。写个小循环from sklearn.model_selection import cross_val_score for k in range(2, 7): reg ProjectionPursuitRegressor(n_componentsk, smooth_span0.4) scores cross_val_score(reg, X_train_scaled, y_train, cv5, scoringneg_mean_squared_error) print(k, -scores.mean())输出中均方误差最小的 K 就是合适值。如果相邻两个 K 的误差差小于 2%选更小的 K 更稳因为模型更简单在真实场景里泛化更好。我一般还会把 K1 的结果也打出来作为基线看增加分量到底带来多少提升。4.3 分类概率总是 0.5 或训练不收敛现象分类器 max_iter 跑满后predict_proba 非常接近 0.5predict 结果几乎全是多数类。把 smooth_span 调小也没有改善。原因分类器的方向搜索是交替优化先找投影方向再对投影值拟合概率响应。如果 max_iter 太小或者初始方向设置不好算法可能还没跳出平缓区域就停了。n_components 太大时后几个方向完全被残差主导概率输出被拉向中性。解决先把 n_components 降到 1 或 2确认方向能产生区分度再逐步增加同时把 max_iter 提高到 300 以上。如果数据类别不平衡需要设置 class_weight 参数给少数类更高权重。一个实用诊断是打印每个分量的投影方向系数如果某方向所有权重都接近零说明该方向没有有效信号直接删掉。4.4 文档和 CI 配置里的依赖缺失导致测试失败现象本地 pytest 全绿推到集成环境却报 ImportError原因是缺少文档或覆盖率相关工具。原因资源里的运行时依赖只声明了核心包但测试、文档构建、覆盖率工具属于开发依赖没有单独写进 dev 依赖文件。CI 环境是全新环境不会提前装这些工具。解决维护 requirements.txt 和 requirements-dev.txt 两个文件安装时都装。pip install -r requirements.txt pip install -r requirements-dev.txtrequirements-dev.txt 里至少包含 pytest、覆盖率工具和文档生成工具。文档生成工具的版本兼容性也是个坑版本太新可能导致 API 文档构建失败。我建议在 dev 依赖里锁一个大版本区间避免无预警升级破坏构建。CI 配置中安装依赖的命令统一走这两条不要散装安装。5. 进阶验证投影方向可视化和残差图解读5.1 可视化投影方向权重投影追踪最吸引人的地方是每个分量都有对应的投影方向向量。训练好的回归器会把方向存在 proj_dirs_ 属性里形状是 (n_components, n_features)。取出来画柱状图import matplotlib.pyplot as plt proj_dirs pipeline.named_steps[ppr].proj_dirs_ fig, axes plt.subplots(1, proj_dirs.shape[0], figsize(12, 4)) for k, ax in enumerate(axes): ax.bar(range(proj_dirs.shape[1]), proj_dirs[k, :]) ax.set_title(fComponent {k1}) plt.tight_layout() plt.show()柱子的高低代表该特征在这个投影方向上的贡献。如果某个方向的权重集中在两三个特征上那它就是专门刻画这几个特征交互关系的。业务解释时我通常更关注第二和第三个方向因为它们捕捉了第一方向剩下的残差结构往往对应更细腻的业务规则。5.2 用残差图验证拟合效果回归模型的泛化能力单看 MSE 不够还要看残差结构。训练结束后把预测值和真实值画成散点再画一条 yx 参考线y_pred_train pipeline.predict(X_train) residual y_train - y_pred_train plt.scatter(y_pred_train, residual, alpha0.5) plt.axhline(0, colorred, linestyle--) plt.xlabel(Predicted value) plt.ylabel(Residual) plt.show()理想状态是残差点围绕虚线随机分布。如果出现弓形或喇叭形说明某些区间的非线性没被投影函数捕获。这时候不要急着调大 smooth_span因为平滑度加大只会让曲线更平正确做法是增加 n_components让模型多拟合一个方向。如果增加后残差图没有改善那就该回头检查特征工程看是不是漏了关键组合。做完这两个可视化我对模型的信任度会提高很多。从那以后每次用这个资源我都会强制走一遍流程先标准化再交叉验证选 K训练后必画残差图和投影方向图。如果残差图有结构就回去查特征工程而不是继续调参。这套习惯帮我避免了不少把噪声当信号的翻车时刻。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?