ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多项式回归实战指南:从PolynomialFeatures到过拟合规避

多项式回归实战指南:从PolynomialFeatures到过拟合规避 1. 从线性到曲线的第一步为什么需要多项式回归很多人在用 sklearn 做完线性回归之后会有一个共同的感觉明明训练集和测试集的分都还可以但把拟合结果画出来一看总觉得哪里不对劲。最常见的情况是数据明显呈弯曲趋势比如销售额随广告投入的增长逐渐放缓、温度随时间呈现周期性变化、房价随面积增大出现边际递减而你的模型却是一条笔直的直线硬生生穿过去。这个场景我见过太多次了——线画出来人麻了。线性回归的核心假设是特征和目标之间存在线性关系也就是 y w1x1 w2x2 ... b 这种形式。但现实世界里的关系往往不是一条直线能描述的。这时候很多新手第一反应是换一个更复杂的模型比如支持向量回归或者随机森林。但先别急着上重型武器有一个性价比极高的中间方案常常被忽略——多项式回归。多项式回归的思路特别朴素既然直线不够那就把原来的特征变成更高次幂的形式让模型有能力拟合曲线。而好消息是它用到的核心工具仍然是我们熟悉的线性回归只不过在喂给模型之前先用 PolynomialFeatures 对特征做了一次非线性变换。我在实际教学和项目里反复推荐这个方案的原因有三个。第一多项式回归保留了线性回归的可解释性每个新特征的系数都有明确的数学含义。第二它的实现成本极低sklearn 帮我们把特征构建和模型训练封装得明明白白。第三它天然是理解更复杂模型的最佳跳板——你搞懂了多项式回归后面学样条回归、广义加性模型都会顺畅很多。这篇博文是系列的第三篇我假设你已经会用 sklearn 做基本的线性回归也大致了解训练集测试集划分、均方误差这些概念。如果没有这些基础也不慌每一步我都会拆开讲清楚你跟着操作就能跑通。内容核心就三块PolynomialFeatures 到底是怎么工作的、degree 参数该选多少、以及多项式回归最常见的几个坑怎么绕过。2. 多项式回归的整体思路与方案选型2.1 多项式回归背后的数学逻辑先来说说多项式回归到底在做什么。假设我们只有一个特征 x线性回归的模型形式是y w0 w1x多项式回归做的事情是把 x 扩展成 x、x²、x³……直到你指定的最高次幂然后模型变成y w0 w1x w2x² w3x³ ... wnx^n注意一个关键细节这个等式关于权重 w 仍然是线性的。这就是为什么我们仍然可以用线性回归的求解方法最小二乘法来拟合这个模型。换句话说多项式回归并没有发明新的学习算法它只是通过特征工程把数据映射到了更高维的空间然后在那个空间里继续做线性拟合。我打个比方这就像你手里有一张弯曲的纸线性回归非要拿直尺去量量出来自然是歪的多项式回归不换尺子而是先把纸掰直了再量。这就是特征变换的意义所在。在实际代码层面sklearn 帮我们把掰直这一步封装成了 PolynomialFeatures 类。你只需要指定 degree最高次数它就会自动生成所有需要的组合特征。再说说为什么这个方案值得学。从模型能力角度看多项式回归能拟合相当广泛的非线性关系抛物线、三次曲线甚至更复杂的起伏形态都能覆盖。从工程角度看它是纯 sklearn 生态内的操作不需要额外引入任何依赖库管线构建和模型评估都跟线性回归保持一致。从学习角度看它让你第一次直观感受到特征工程 简单模型组合拳的威力也让你理解为什么同一个算法配不同特征会得到完全不同的效果。2.2 为什么不用其他非线性模型来替代有人可能会问既然数据是弯曲的我直接用决策树或者 KNN 回归不也能拟合吗何必绕弯子做特征变换确实树模型不需要特征缩放、天然处理非线性、也不用操心特征组合的爆炸问题在很多场景下甚至表现得更好。但多项式回归有几棵树给不了的东西第一可解释性。树模型的预测是分段常数你很难用一个简洁的数学表达式对外描述模型行为多项式回归则能给出完整的系数表达式一眼看清每个特征次幂的影响方向和强度。第二外推能力。树模型对训练数据范围之外的新样本表现极差因为它只会用叶子节点的均值去预测多项式回归在一个相对温和的次幂设定下能在区间外给出合理趋势。第三稳定性。树模型对数据微调和参数设置非常敏感同样的数据换一个 random_state 可能结构就完全变了多项式回归则是一个确定性的拟合过程结果稳定可复现。当然我也不是说多项式回归在所有场景都更优。如果数据波动极其复杂、样本量巨大、特征维度本来就很高那确实应该用更强力的模型。选择模型从来不是找最好的而是找最合适的——在当前这个学习阶段多项式回归能给你最大的理解回报和可控性。2.3 这套方案的核心架构拆解在 sklearn 中落地多项式回归核心就三个步骤构造多项式特征、训练线性回归模型、评估预测效果。为了操作更规范我强烈建议用 Pipeline 把前两步串联起来这样训练和预测阶段的特征变换逻辑天然保持一致不会出现训练时做了变换预测时忘了做这种低级错误。Pipeline 的具体写法后面实操部分会给出这里先说架构逻辑。PolynomialFeatures 负责把输入从 [x] 变成 [1, x, x², ..., x^n]LinearRegression 负责在新的特征空间里做线性拟合。整个管线对外看就是输入原始特征直接输出预测结果内部细节全部封装好。有一点需要特别注意PolynomialFeatures 有一个参数叫 include_bias默认值是 True它会自动生成一列全 1 的特征对应线性回归里的截距项。这时候如果 LinearRegression 默认的 fit_interceptTrue 也生效相当于截距被重复计算了一次。虽然线性回归在数值求解上通常会强行处理掉这个冗余不会让程序报错但会导致截距项的解释意义变糊。实际使用中我建议要么设 include_biasFalse 让回归器自己处理截距要么设 fit_interceptFalse 让多项式特征里的偏置列来承担截距角色。我个人习惯保留 include_biasFalse让 LinearRegression 统一管理截距。3. 多项式回归的核心细节与实操要点3.1 生成多项式特征的本质维度扩增PolynomialFeatures 的核心作用可以概括成四个字维度扩增。它用代数方法把原始特征组合成更高维的特征矩阵让线性模型能在增强后的空间中表达非线性关系。暴露出这个过程的细节很有价值我建议初学者千万别把它当黑盒务必亲手打印看一下变换前后的数据长什么样。假设你有一个特征 x取值为 [1, 2, 3, 4, 5]设置 degree2PolynomialFeatures 会输出三列第一列是偏置项全 1如果 include_biasTrue第二列是 x 本身第三列是 x 的平方。当有多个特征时它还会生成特征间的交叉项。比如两个特征 x1 和 x2degree2 时会生成 [1, x1, x2, x1², x1x2, x2²]。你仔细看连交叉项都帮你生成了这就是组合特征的含义。把维度扩增的逻辑想明白很多问题就迎刃而解了。比如为什么 degree 越高模型越容易过拟合——因为特征维度爆炸式增长你有 3 个原始特征、degree5 时生成的维度数已经是个不小的数字了超过样本数就容易出现过拟合。再比如为什么多项式回归前要做数据标准化——因为 x 和 x^5 之间的数值尺度差距非常大梯度下降类方法在这种数据上收敛会很慢而线性回归用的是最小二乘闭式解虽然不受收敛速度影响但数值稳定性也可能因为尺度悬殊出问题。从这个角度看PolynomialFeatures 不只是个简单的工具函数它是教你理解特征工程思维的最佳教材。3.2 degree 参数到底怎么选degree 参数是多项式回归里最重要的超参数。它直接决定了模型的复杂度上限也直接决定了过拟合的风险大小。这个参数没有固定答案完全取决于数据本身的形状和样本量但我可以分享一套通用的选择和调参策略。最低限度degree1 就是普通线性回归这不用多说。degree2 可以拟合抛物线类的单一弯曲关系比如边际效益递减的增长曲线、开口向上或向下的 U 形关系这类情况在实际业务中占比很大。degree3 可以拟合带拐点的 S 形趋势或者先升后降再升的波浪形态。degree 再往上就要非常谨慎了5 次、6 次以上的多项式很容易在数据边界处剧烈震荡训练集分数高得吓人测试集却一塌糊涂。选 degree 的最靠谱方法是用交叉验证做扫描。把 1 到 5 或 1 到 6 的 degree 轮流试一遍每次都用交叉验证评估模型的泛化能力最终选交叉验证分数最高、同时模型复杂度还算合理的那个值。我在项目里一般习惯把交叉验证分数逼近天花板作为参考线一旦发现继续增加 degree 对验证集分数没有实质提升就果断停在当前值。另外一定要警惕完美拟合。如果你发现训练集的 R2 已经飙到 0.99而测试集只有 0.7 甚至更低那就是教科书级的过拟合信号。此时正确做法不是继续调复杂度而是降低 degree 或增加正则化。sklearn 里可以用 PolynomialFeatures 配合 Ridge 回归给高次项系数施加 L2 惩罚这能有效抑制过拟合后面我会详细说。3.3 数据预处理标准化到底做不做多项式回归里标准化这个问题经常被初学者忽略。线性回归用最小二乘法求解时不需要梯度下降所以很多人想当然地以为不需要特征缩放。但多项式特征生成之后情况就不同了。举个例子假设特征 x 的范围是 0 到 100degree3 之后x 的三次方最大就是 100 万和 x 自身的 100 放在一起数值尺度差了 1 万倍。最小二乘法虽然能通过矩阵求解得到结果但这种极端尺度差异会导致数值计算精度下降尤其是特征维度高、样本量大的时候矩阵求逆的条件数会变得很差最终拟合结果出现微小但可感知的误差。更实际的问题是如果你后续想比较不同次幂特征的重要性或者换成 Ridge/Lasso 这类带正则化的线性模型标准化就是必需品。Ridge 的 L2 惩罚会对所有特征施加同等力度的权重收缩如果特征尺度不同惩罚实际起到的效果就完全失衡。我的建议非常简单只要用了 PolynomialFeatures就顺手加一个 StandardScaler。Pipeline 里写成 PolynomialFeatures - StandardScaler - LinearRegression。这个组合几乎不会带来任何副作用但能避免掉一大批潜在的数值问题。这是一种性价比极高的习惯。3.4 交叉验证和评估指标的选择多项式回归的评估不能只看训练集分数。因为模型的拟合能力很强训练集分数高是必然的关键要看测试集或验证集的表现。我推荐的做法有两种。第一种是最直接的 hold-out 验证把数据切成训练集和测试集训练完模型后在测试集上算 R2 和均方误差。适用于你只是想快速验证效果的场景。第二种是 K-Fold 交叉验证把数据分成 K 份轮流拿其中一份做验证、其余做训练最终取 K 次验证分数的平均值。适用于你想认真选 degree 的场景结论比单次划分可靠很多。sklearn 里做交叉验证有现成的工具比如 cross_val_score。你甚至可以把 Pipeline 对象直接传进去它会自动完成特征生成、缩放、训练、验证的完整流程不用你手动管理每一步的状态。评估指标这块回归任务我建议至少看两个R2 决定系数评估模型的解释力均方误差评估预测偏差的实际数值。R2 接近 1 且测试集不比训练集差太多说明拟合状态健康均方误差能帮你直观理解预测偏差在真实数据尺度上到底有多大。4. 实操过程与核心环节实现4.1 快速准备环境和数据开始写代码之前先把环境准备好。多项式回归用到的主要是 sklearn、numpy 和 matplotlib 这三个库。如果你还没有 sklearn安装命令很简单pip install scikit-learn之前有朋友问要不要单独安装 PolynomialFeatures其实不需要它是 sklearn.preprocessing 模块的一部分装上 scikit-learn 就自带了。为了演示多项式回归的效果我造一组带噪声的非线性数据这样拟合出来的结果真实感更强。我这里用 numpy 生成模拟数据横坐标 x 从 0 到 10 均匀取 100 个点真实的 y 由 sin 函数加上一个线性趋势构成再叠加一些高斯噪声。这样造出来的数据既有弯曲形态又不失规律性最适合用来展示多项式回归的威力。import numpy as np import matplotlib.pyplot as plt from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import r2_score, mean_squared_error np.random.seed(42) x np.linspace(0, 10, 100).reshape(-1, 1) y np.sin(x).ravel() 0.5 * x np.random.normal(0, 0.3, x.shape[0])这里 reshape(-1, 1) 非常重要。sklearn 的特征输入要求是二维数组形状为 (样本数, 特征数)如果直接用一维数组喂进去会直接报错。折腾 Excel 的时候你可能不需要关心这个问题但进了 sklearn 的生态就必须养成看形状的习惯。我见过太多新手在这个地方卡半天其实只要记住特征永远是二维的这一句话就够了。4.2 从基础线性回归开始对比做多项式回归之前我建议先拟合一个基础的线性回归模型作为对照组。这样你能直观看到线性回归的预测是一条直线在弯曲的数据面前确实力不从心。X_train, X_test, y_train, y_test train_test_split(x, y, test_size0.2, random_state42) lr LinearRegression() lr.fit(X_train, y_train) y_pred_linear lr.predict(X_test) print(Linear Regression R2:, r2_score(y_test, y_pred_linear)) print(Linear Regression RMSE:, mean_squared_error(y_test, y_pred_linear) ** 0.5)运行这个代码你大概率会看到线性回归的 R2 在 0.7 上下RMSE 在 0.7 以上的水平。单看数字可能觉得还行但一旦把预测线画出来就能发现问题直直的一条线穿过弯曲的数据点云上半截偏下、下半截偏上系统性的偏差非常明显。这就是所谓欠拟合的状态——模型没有能力捕捉数据中的非线性结构。4.3 用 Pipeline 构建多项式回归接下来进入正题用 PolynomialFeatures 配合 Pipeline 构建多项式回归。我直接推荐 Pipeline 的写法原因就一句话它能保证特征变换逻辑在训练和预测阶段完全一致。手动的烦琐写法是训练时先 fit_transform预测时要用同一个已学好的转化器去 transform一旦中间哪个环节漏了维度对不上还可能报错更隐蔽的情况是维度刚好对上了但变换逻辑不同模型产出结果完全错误。# 创建多项式回归管线 def create_poly_regression(degree): return Pipeline([ (poly, PolynomialFeatures(degreedegree, include_biasFalse)), (scaler, StandardScaler()), (linear, LinearRegression()) ]) # 用 degree2 试试 pipe create_poly_regression(degree2) pipe.fit(X_train, y_train) y_pred_poly2 pipe.predict(X_test) print(Polynomial Degree2 R2:, r2_score(y_test, y_pred_poly2)) print(Polynomial Degree2 RMSE:, mean_squared_error(y_test, y_pred_poly2) ** 0.5)你会在输出里看到degree2 的 R2 已经比线性回归高出一大截RMSE 也明显下降。这就是多项式特征带来的效果——只加了一个平方项模型从只能画直线变成了可以画抛物线。但注意degree2 只适用于数据呈现单峰或单谷形态的场景而我们的数据是 sin 加线性趋势形状更复杂所以 degree2 还不够。接着把 degree 往上调一调看看 3、4、5 的效果for degree in [3, 4, 5]: pipe create_poly_regression(degree) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) r2 r2_score(y_test, y_pred) rmse mean_squared_error(y_test, y_pred) ** 0.5 print(fDegree{degree}: R2{r2:.4f}, RMSE{rmse:.4f})这时候你就会看到经典的曲线degree 从 3 涨到 4R2 可能还在提升从 4 涨到 5R2 可能开始停滞甚至下降。这就是过拟合开始出现的信号。模型在训练集上表现越来越好但对测试集的泛化能力反而变差了。画图辅助判断这一步建议不要省。数据拟合问题的最终裁判永远是你的眼睛——指标只是参考图形才是直观。x_plot np.linspace(0, 10, 200).reshape(-1, 1) y_plot_true np.sin(x_plot).ravel() 0.5 * x_plot.ravel() plt.figure(figsize(12, 5)) plt.scatter(X_train, y_train, labelTraining Data, alpha0.6) plt.plot(x_plot, y_plot_true, k--, labelTrue Curve, linewidth2) for degree in [1, 3, 5, 7]: pipe create_poly_regression(degree) pipe.fit(X_train, y_train) y_plot_pred pipe.predict(x_plot) plt.plot(x_plot, y_plot_pred, labelfDegree{degree}) plt.xlabel(x) plt.ylabel(y) plt.title(Polynomial Regression with Different Degrees) plt.legend() plt.show()图一画出来教材上讲的过拟合瞬间就立体了。degree1 是一条直线贴不到数据上degree3 已经能比较贴合真曲线了degree7 在训练数据范围内看起来无比平滑几乎穿过每一个点但在数据范围的两端开始疯狂上下震荡拉出离谱的曲线。这就是高次多项式在边界处放飞自我的典型表现。4.4 用交叉验证选择最优 degree手动一个个试 degree 不是不行但效率低且结论不稳定。更规范的做法是结合交叉验证自动扫描。我把 1 到 8 的 degree 全部扫一遍每次做 5 折交叉验证看平均分数。from sklearn.model_selection import cross_val_score for degree in range(1, 9): pipe create_poly_regression(degree) scores cross_val_score(pipe, X_train, y_train, cv5, scoringr2) avg_r2 scores.mean() std_r2 scores.std() print(fDegree{degree}: CV R2{avg_r2:.4f} ± {std_r2:.4f})输出的表格会非常直观地告诉你哪一个 degree 在交叉验证视角下最稳。在我的模拟数据上通常 degree3 或 4 的交叉验证分数最高再往上分数要么停滞要么下降。选择那个分数不再明显提升的 degree 作为最终参数是最平衡、最不容易翻车的方式。如果你觉得只盯 R2 不够可以用负均方误差作为 scoring 参数再验证一轮结论一般是一致的。4.5 过拟合的兜底方案Pipeline 搭配 Ridge即使交叉验证选好了 degree某些场景下你可能仍然想用更高 degree 的模型但又不希望它震荡得那么厉害。这时候正则是你最好的朋友。多项式回归 Ridge 正则的组合是我在实际项目里最常用的高次低险配置。Ridge 回归在损失函数中加入了 L2 惩罚项迫使模型权重尽量小。高次幂特征对应的权重被压缩之后曲线在边界处的剧烈震荡就会大幅缓解。from sklearn.linear_model import Ridge def create_ridge_poly_regression(degree, alpha): return Pipeline([ (poly, PolynomialFeatures(degreedegree, include_biasFalse)), (scaler, StandardScaler()), (ridge, Ridge(alphaalpha)) ]) # 用 degree7 正则化试试 pipe_ridge create_ridge_poly_regression(degree7, alpha1.0) pipe_ridge.fit(X_train, y_train) y_pred_ridge pipe_ridge.predict(X_test) R2_ridge r2_score(y_test, y_pred_ridge) print(fRidge Degree7 R2:, R2_ridge)注意用了 Ridge 就必须先做标准化原因在上一节里说过——L2 惩罚对各维度一视同仁如果不把不同量纲的特征拉到同一尺度惩罚力度会被数值大的维度主导模型就学歪了。Pipeline 里的 StandardScaler 在这里不是锦上添花而是必需品。alpha 参数控制正则化的强度alpha 越大权重被压缩得越狠曲线越平滑alpha 太小则跟普通线性回归差不多。实际调参同样可以交给交叉验证用 GridSearchCV 在 alpha 和 degree 的组合空间里搜索最优解。5. 常见问题与排查技巧实录5.1 预测时报维度错误训练和预测的特征变换不一致这是多项式回归新手遇到最多的错误训练时对 X_train 做了 fit_transform得到多项式特征后训练模型但预测时直接拿原始 X_test 塞给模型。sklearn 会直接报错提示你特征数量不匹配。为什么会犯这个错因为很多人手动管理特征变换流程时只记得训练阶段要做变换忘了预测阶段同样要做。解决办法有两个一个是用我前面推荐的 Pipeline 写法让管线自动管理全流程另一个是手动做变换时务必把 PolynomialFeatures 实例存储下来预测时调用同一个实例的 transform 方法而不是重新 new 一个。poly PolynomialFeatures(degree3, include_biasFalse) X_train_poly poly.fit_transform(X_train) model LinearRegression() model.fit(X_train_poly, y_train) # 正确写法用同一个 poly 实例做 transform X_test_poly poly.transform(X_test) y_pred model.predict(X_test_poly)5.2 用了高 degree 但训练集分数反而没提升有时候你会发现 degree 从 4 加到 6训练集 R2 竟然纹丝不动甚至略微下降。这种现象一般有两个原因。第一数据本身的非线性关系并不需要那么高的自由度加再多次幂也只是增加了冗余特征模型用不到。第二在特征尺度差异极大的情况下最小二乘的数值求解遇到病态矩阵导致高阶系数没有被准确估计出来。针对第二个原因解法就是我在前面反复强调的标准化。加了 StandardScaler 之后再试试大概率会看到训练集分数恢复正常变化趋势。如果你的数据本身分布比较极端某些特征取值特别大或特别小这个坑会尤其明显。5.3 测试集 R2 是负的模型连均值都不如模型在测试集上的 R2 为负意思是预测结果比直接猜目标变量的均值还要差。这在多项式回归中不算罕见尤其是 degree 设得过高、数据量比较小的时候。模型在训练集上拟合得天花乱坠到了新数据上完全失效原因就是过拟合到把噪声也当规律记下来了。遇到这种情况先别急着调数据。第一步降低 degree看验证分数是否回升第二步加正则化用 Ridge 压制高阶权重第三步增加训练样本量让模型有更多信息去区分信号和噪声。这三步按顺序来大多数负 R2 的情况都能救回来。5.4 特征名字和系数对应关系怎么查调试模型时我经常会去看每个多项式特征的系数大小判断哪些特征对预测贡献最大。但 PolynomialFeatures 自动生成的组合特征名字在变换之后就变成数字序号了想知道序号跟原始特征的对应关系可以通过 get_feature_names_out 方法查看。poly PolynomialFeatures(degree2, include_biasFalse) X_poly poly.fit_transform(x) feature_names poly.get_feature_names_out([x1, x2]) print(feature_names) model LinearRegression() model.fit(X_poly, y) for name, coef in zip(feature_names, model.coef_): print(f{name}: {coef:.4f})这个方法启动项目时多看一眼能让你随时保持对模型行为的感知不至于等模型上线了还不知道哪个特征在驱动预测结果。承接上一节说的如果你在 Pipeline 里做变换可以先用 fit 把 Pipeline 跑一遍再通过 named_steps 找到内部的 PolynomialFeatures 对象来查看特征名字。6. 从多项式回归到更广阔的特征工程世界看到这里你应该已经感受到了多项式回归的魅力它用最朴素的特征变换手法撬动了线性模型的表达能力让我们能优雅地处理非线性数据。但我想说的是PolynomialFeatures 只是特征工程世界的入门第一课。沿着这个思路往外看你会发现周围还有大量类似的工具样条变换能把数据划分成多个分段分别拟合、径向基函数可以把特征映射到高维空间、分箱操作能让模型在每个区间内拥有独立的表达。它们共同的思路都是改造特征而不是更换模型——这个思想贯穿整个机器学习实践远比某一个具体工具重要。即使你后续走向深度学习、树模型大行其道的领域特征工程的思维方式依然有它的用武之地。理解特征决定模型性能上限模型只是逼近这个上限这句话会让你的建模水平上一个台阶。多项式回归就是理解这句话的最佳起点。我在实际项目中使用这个工具的经验是时刻保持对数据形态的敏感永远先画图再建模永远用交叉验证去选参数而不是凭感觉拍脑袋。这不是什么高超的技巧都是最基础的习惯但这些习惯能让你在绝大多数建模任务里少走很多弯路。希望这篇实操总结对你有用也欢迎你在自己的数据上多试试不同 degree 的效果——亲手对比过欠拟合和过拟合的差别比看十篇教程都更深刻。
返回列表