跳到主要内容

多项式回归

文档定位:面向数学建模竞赛(国赛 / 美赛)备赛学习者的算法笔记。本文先讲清原理与指标,再给出完整可独立运行的 Python 代码(第六节),用合成数据演示「手写实现 vs sklearn 对照」「阶数选择」与「4 张诊断图」。运行代码后建议通读第七节,对照打印输出理解过拟合的典型表现。


一、算法含义

1.1 一句话理解

散点图上,如果数据点整体不是直线关系(例如倒 U 型、S 型、振荡型),硬用一条直线拟合会系统性错过重要结构。多项式回归的思路非常简单:把直线"掰弯"——用 xx 的幂次构造出新的特征,把一元非线性问题转化为我们已经熟练掌握的多元线性回归问题

1.2 数学模型

y=β0+β1x+β2x2++βdxd+ε,εN(0,σ2)y = \beta_0 + \beta_1 x + \beta_2 x^2 + \cdots + \beta_d x^d + \varepsilon, \qquad \varepsilon \sim N(0, \sigma^2)

其中:

  • xx:自变量(本文只讨论单特征情形);
  • yy:因变量(观测值);
  • dd:多项式阶数(degree),是唯一需要"调"的超参数;
  • β0\beta_0:截距项;β1,,βd\beta_1, \dots, \beta_d:各幂次项的系数;
  • ε\varepsilon:随机误差(噪声),通常假设服从均值为 0、方差为 σ2\sigma^2 的正态分布。

d=1d = 1 时,上式退化为简单线性回归 y=β0+β1xy = \beta_0 + \beta_1 x,所以线性回归是多项式回归的特例

1.3 核心思想:对参数仍是线性模型

虽然 xx 出现在平方、立方项里,但模型真正的未知数是 β\beta,且每个 β\beta 都只出现一次方。定义新特征:

z1=x,z2=x2,,zd=xdz_1 = x,\quad z_2 = x^2,\quad \dots,\quad z_d = x^d

则模型改写为:

y=β0+β1z1+β2z2++βdzd+εy = \beta_0 + \beta_1 z_1 + \beta_2 z_2 + \cdots + \beta_d z_d + \varepsilon

这正是我们熟悉的多元线性回归形式。由此得到三个重要结论:

  1. 求解方法不变:最小二乘、正规方程、梯度下降等线性回归的全部工具可以原封不动地使用;
  2. 一句话总结(可以直接写进竞赛论文):多项式回归对自变量 xx 是非线性的,但对参数 β\boldsymbol{\beta} 仍是线性模型
  3. 本质是特征工程:把一维 xx 通过映射 ϕ(x)=[1,x,x2,,xd]\phi(x) = [1, x, x^2, \dots, x^d] 扩成 (d+1)(d+1) 维特征向量,再用线性模型拟合。理解这一点后,sklearn 中 PolynomialFeatures + LinearRegression 的实现就一目了然了。

1.4 范德蒙矩阵表示

nn 个样本展开成矩阵形式,得到范德蒙矩阵(Vandermonde matrix) VRn×(d+1)V \in \mathbb{R}^{n \times (d+1)}

1 & x_1 & x_1^2 & \cdots & x_1^d \\ 1 & x_2 & x_2^2 & \cdots & x_2^d \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_n & x_n^2 & \cdots & x_n^d \end{bmatrix}, \qquad \mathbf{y} = V\boldsymbol{\beta} + \boldsymbol{\varepsilon}$$ 最小二乘的目标是最小化残差平方和 $\|\mathbf{y} - V\boldsymbol{\beta}\|_2^2$,其解析解由**正规方程(normal equation)**给出: $$\hat{\boldsymbol{\beta}} = (V^\top V)^{-1} V^\top \mathbf{y}$$ > **数值稳定性提示**:当阶数较高时,$V$ 的各列($x, x^2, \dots, x^d$)量纲差异巨大,$V^\top V$ 的条件数会爆炸(例如 $x \in [0, 2\pi]$、$d=9$ 时 $x^{18}$ 可达 $10^{14}$ 量级),直接求解数值上不可信。实践中**先对非截距列做标准化(z-score)再求解**,相当于换了一组数值上更稳定的基,预测结果不变。第六节代码中有条件数的对比演示。 ### 1.5 与线性回归的关系 | 对比项 | 线性回归 | 多项式回归 | |---|---|---| | 模型形式 | $y = \beta_0 + \beta_1 x$ | $y = \beta_0 + \beta_1 x + \cdots + \beta_d x^d$ | | 设计矩阵 | $X = [\mathbf{1}, \mathbf{x}]$ | $V = [\mathbf{1}, \mathbf{x}, \mathbf{x}^2, \dots, \mathbf{x}^d]$ | | 损失函数 | 残差平方和 | 完全相同 | | 求解方式 | 正规方程 / 梯度下降 | 完全相同 | | sklearn 实现 | `LinearRegression` | `PolynomialFeatures` + `LinearRegression` 的 Pipeline | 一句话:**多项式回归 = 线性回归在多项式基上的特例**,唯一的差别是"换了一组特征"。 ### 1.6 欠拟合与过拟合 阶数 $d$ 直接决定模型复杂度,选得不好会出现两类经典问题: - **欠拟合(underfitting,高偏差)**:$d$ 太小,模型表达能力不足,无法捕捉数据的弯曲趋势。表现是**训练误差和测试误差都很高**。例如用 $d=1$ 的直线去拟合 $\sin(x)$ 曲线。 - **过拟合(overfitting,高方差)**:$d$ 太大,模型开始"背下"噪声与个别数据点的随机波动。表现是**训练误差极低、测试误差反而升高**,拟合曲线剧烈抖动,系数的绝对值变得巨大。例如 $n=100$ 的数据用 $d=9$ 拟合。 阶数选择的本质是**偏差—方差权衡(bias-variance trade-off)**:$d$ 增大 → 偏差减小、方差增大,总测试误差呈 U 型,最优阶数在 U 型底部(见第三节、第四节的图②)。 ### 1.7 优缺点 | 优点 | 缺点 | |---|---| | 形式简单、参数可解释(各系数含义可讨论) | 全局假设强:一个多项式管住整个区间,某一段的弯曲会污染整条曲线 | | 有解析解(正规方程),计算极快,适合小样本 | 外推极其危险:远离数据区间的 $x$ 由最高次项主导,两端迅速爆炸 | | 任何光滑函数在小范围内都可用泰勒展开近似,多项式基是自然选择 | 高阶时数值不稳定,对离群点敏感 | | 与线性回归共用全部理论(R²、检验、置信区间) | 对周期性、突变性、分段变化的数据拟合效果差 | --- ## 二、何时使用(适用场景与条件) ### 2.1 适用场景 - **单特征**且散点图呈现明显的非线性趋势(弯曲、单峰、振荡等); - **数据量有限**(几十到几百个样本):多项式回归参数少、有解析解,小样本下比神经网络等复杂模型稳定得多; - 需要一个**简单、可解释**的模型(例如作为竞赛论文的基准模型); - 只关心**观测区间内部**的拟合与插值质量,外推需求很弱。 ### 2.2 竞赛典型场景 1. **曲线拟合**:例如温度—销量、广告投入—销售额、浓度—产率等单变量关系建模; 2. **趋势外推(务必谨慎)**:时间序列的短期外推可以尝试(如预测下一期数据),但只能外推很短的距离,且必须在论文中说明风险; 3. **基准模型(baseline)**:先跑线性回归与多项式回归,作为更复杂模型(随机森林、神经网络)的性能参照,这也是论文"模型对比"章节的标准写法; 4. **辅助分析**:残差诊断、边际效应分析(导数 $dy/dx = \beta_1 + 2\beta_2 x + \cdots$ 有明确的解释)。 ### 2.3 使用前提 - 只有一个自变量(多元情形交叉项数量爆炸,见 2.4); - 变量关系**光滑连续**,无明显突变点; - 噪声近似同方差,无强离群点; - 数据量相对阶数足够:经验法则 $n \ge 10d$(更稳妥 $n \ge 15d$),竞赛中阶数一般不超过 3~5; - $x$ 的取值区间不能太窄(区间太窄时高次项彼此高度相关,参数估计不稳定)。 ### 2.4 不适用情形 - **高维数据**:$p$ 个特征构造 $d$ 阶多项式时,特征数约为组合数 $C(p+d, d)$,呈指数爆炸。$p=10, d=3$ 时特征数已达 286 个,参数数量远超样本量,必然过拟合; - **强外推需求**:需要预测远超观测区间的未来值时,多项式两端由最高次项主导,误差呈幂次放大(外推灾难,见 7.2); - **周期数据**:$\sin/\cos$ 型周期数据用傅里叶基(三角函数回归)远优于多项式; - **分段/突变数据**:有明显拐点、平台、跳变的数据应改用样条回归或分段模型; - **机理已知**:若从学科机理可导出具体函数形式(如指数增长、Logistic 饱和),应优先使用对应的非线性回归模型。 ### 2.5 与其他方法的选择 | 方法 | 特点 | 适用场景 | 主要局限 | |---|---|---|---| | **多项式回归** | 全局单一函数,解析解,可解释 | 单特征、小样本、光滑趋势、需要简单模型 | 高阶外推失控;全局假设强 | | **样条回归** | 分段低阶多项式,节点处光滑拼接 | 变化复杂的曲线;避免 Runge 现象 | 需选节点数目/位置;解释性稍弱 | | **非线性回归** | 指定机理函数(如指数、Logistic)后迭代估计参数 | 机理已知、需参数有物理含义 | 需先验知识;初始值敏感 | | **局部加权回归(LOWESS)** | 非参数,逐点用邻域数据加权拟合 | 不关心函数形式、只求曲线贴合 | 无法给出全局公式,外推困难 | 竞赛中的决策链:**先画散点图 → 线性回归打底 → 多项式回归(试 d=2,3,4)→ 需要更灵活时上样条 → 有明确机理时上非线性回归**。把这条决策链写进论文,比"直接甩出最复杂模型"更有说服力。 --- ## 三、算法指标 > 本节所有指标在第六节代码中都会计算并打印,符号定义见第五节。约定:$\hat{y}_i$ 为第 $i$ 个样本的预测值,$\bar{y}$ 为样本均值,$n$ 为样本量,$p = d + 1$ 为参数个数(含截距)。 ### 3.1 均方误差(MSE) - **中文名**:均方误差 - **公式**:$$\mathrm{MSE} = \frac{1}{n}\sum_{i=1}^{n}\left(y_i - \hat{y}_i\right)^2$$ - **取值范围**:$[0, +\infty)$,无量纲(量纲是 $y$ 的平方) - **解读**:越小越好。是模型拟合损失函数本身,也是交叉验证、AIC/BIC 的"原材料"。缺点是不直观(单位平方)且受离群点影响大。 ### 3.2 均方根误差(RMSE) - **中文名**:均方根误差 - **公式**:$$\mathrm{RMSE} = \sqrt{\mathrm{MSE}} = \sqrt{\frac{1}{n}\sum_{i=1}^{n}\left(y_i - \hat{y}_i\right)^2}$$ - **取值范围**:$[0, +\infty)$,与 $y$ 同量纲 - **解读**:越小越好,竞赛最常用。因为与 $y$ 同单位,可以直接和数据的量级、噪声水平 $\sigma$ 比较。例如生成数据噪声 $\sigma = 0.25$,若测试 RMSE 约 0.25,说明模型已逼近"不可再降"的噪声地板;若测试 RMSE 明显大于训练 RMSE,就是过拟合信号。 ### 3.3 决定系数(R²) - **中文名**:决定系数(拟合优度) - **公式**:$$R^2 = 1 - \frac{\sum_{i=1}^{n}(y_i - \hat{y}_i)^2}{\sum_{i=1}^{n}(y_i - \bar{y})^2} = 1 - \frac{SS_{res}}{SS_{tot}}$$ - **取值范围**:训练集上 $R^2 \in [0, 1]$;**测试集上可以为负**(负值意味着模型还不如直接预测均值 $\bar{y}$) - **解读**:表示因变量方差中被模型解释的比例,越接近 1 越好。**关键警告**:训练 $R^2$ 随阶数 $d$ 单调不减(模型越复杂必然贴得越近),因此**绝不能单看训练 R² 选阶数**——它一定会选出最大的 $d$ 并导致过拟合。 ### 3.4 调整 R²(Adjusted R²) - **中文名**:调整(校正)决定系数 - **公式**:$$\bar{R}^2 = 1 - \frac{(1 - R^2)(n - 1)}{n - p}, \qquad p = d + 1$$ - **取值范围**:$\bar{R}^2 \le R^2$(参数越多惩罚越重),可以小于 0 - **解读**:在 $R^2$ 基础上惩罚参数个数,只有新增参数带来的拟合提升足够大时 $\bar{R}^2$ 才上升。随 $d$ 增大一般先升后降,峰值可作为选阶参考。注意它是**基于训练集**计算的,本质是"带惩罚的训练指标"。 ### 3.5 K 折交叉验证 MSE(CV-MSE) - **中文名**:K 折交叉验证均方误差 - **公式**:将数据分成 $K$ 折,第 $k$ 折作验证集、其余 $K-1$ 折训练,得到验证误差 $\mathrm{MSE}_k$,取平均: $$\mathrm{CV\text{-}MSE} = \frac{1}{K}\sum_{k=1}^{K}\mathrm{MSE}_k$$ - **取值范围**:$[0, +\infty)$,与 MSE 同量纲 - **解读**:**阶数选择的第一依据**。随 $d$ 增大,CV-MSE 通常呈 U 型:先降(偏差主导,欠拟合被改善)后升(方差主导,过拟合出现),U 型最低点即推荐阶数。竞赛中常用 $K=5$ 或 $K=10$;样本少时用留一交叉验证(LOOCV,$K=n$)。 ### 3.6 AIC 与 BIC(信息准则选阶) - **中文名**:赤池信息准则 / 贝叶斯信息准则 - **公式**(线性模型、高斯误差情形): $$\mathrm{AIC} = n\ln\!\left(\frac{RSS}{n}\right) + 2p, \qquad \mathrm{BIC} = n\ln\!\left(\frac{RSS}{n}\right) + p\ln n$$ 其中 $RSS = \sum_{i=1}^{n}(y_i - \hat{y}_i)^2$ 为残差平方和,$p = d + 1$ 为参数个数(部分教材把 $\sigma^2$ 也计作参数,即 $p = d + 2$;只要同一数据集上口径一致,**相对比较结论不变**)。 - **取值范围**:整个实数轴,无上下界;**只有相对大小有意义**,越小越好 - **解读**:第一项度量拟合优度,第二项是复杂度罚项。由于 $n > 7$ 时 $\ln n > 2$,BIC 的惩罚比 AIC 更重,倾向选择更简单的模型(更小的 $d$)。两者最低点对应的阶数可作为 CV 的交叉验证参考。 ### 3.7 训练误差与测试误差的背离:过拟合诊断 理论上有(偏差—方差分解):测试误差 $\approx \sigma^2 + \text{偏差}^2 + \text{方差}$。当 $d$ 增大时偏差项减小、方差项增大,二者此消彼长。实战中只需对比训练/测试两条误差曲线: | 现象 | 诊断 | 对策 | |---|---|---| | 训练、测试误差都很高且接近 | 欠拟合(偏差大) | 提高阶数 / 换更灵活的模型 | | 训练误差很低、测试误差高,二者背离大 | 过拟合(方差大) | 降低阶数 / 加正则化 / 增加样本 | | 两者都低且接近 | 拟合恰当 | 保持 | 定量参考:若测试 RMSE / 训练 RMSE 之比超过 2~3 倍,就应怀疑过拟合(第六节代码第 8 部分会打印该比值)。 ### 3.8 指标汇总表 | 指标(中文名) | 符号 | 公式 | 取值范围 | 解读要点 | |---|---|---|---|---| | 均方误差 | MSE | $\frac{1}{n}\sum(y_i-\hat{y}_i)^2$ | $[0, +\infty)$ | 损失函数本身,越小越好 | | 均方根误差 | RMSE | $\sqrt{\mathrm{MSE}}$ | $[0, +\infty)$ | 与 $y$ 同量纲,竞赛首选,可与噪声水平比较 | | 决定系数 | $R^2$ | $1 - SS_{res}/SS_{tot}$ | $(-\infty, 1]$ | 方差解释比例;训练 $R^2$ 单调不减,不能单独用来选阶 | | 调整 R² | $\bar{R}^2$ | $1 - \frac{(1-R^2)(n-1)}{n-p}$ | $\le R^2$ | 惩罚参数个数,峰值可参考选阶 | | K 折 CV 均方误差 | CV-MSE | $\frac{1}{K}\sum_k \mathrm{MSE}_k$ | $[0, +\infty)$ | 阶数选择第一依据,U 型最低点即最优阶数 | | 赤池信息准则 | AIC | $n\ln(RSS/n) + 2p$ | 实数,越小越好 | 拟合优度 + 轻度复杂度惩罚 | | 贝叶斯信息准则 | BIC | $n\ln(RSS/n) + p\ln n$ | 实数,越小越好 | 惩罚比 AIC 重,倾向更简单模型 | --- ## 四、可视化图表 > 第六节代码会生成以下 4 张图并保存到 `figures/` 目录(文件名前缀 `poly_`),这是竞赛论文插图的标配组合。 | # | 图名 | 用途 | 关键解读点 | |---|---|---|---| | ① | 不同阶数拟合曲线对比(`poly_degree_comparison.png`) | 直观展示欠拟合 / 合适 / 过拟合三种状态 | $d=1$ 直线贯穿弯曲数据(欠拟合);$d=3$ 贴合并保持光滑(合适);$d=9$ 曲线剧烈抖动、逐点穿越噪声(过拟合) | | ② | 阶数—训练/测试误差曲线(`poly_error_vs_degree.png`) | 用误差曲线定位最优阶数 | 训练误差随 $d$ 单调下降;测试误差呈 U 型,最低点即最优阶数;U 型右侧两线背离(阴影区)即过拟合区 | | ③ | 学习曲线(`poly_learning_curve.png`) | 判断"加数据"能否解决问题 | 合适阶数:两线随样本量收敛到低值;过拟合阶数:训练误差始终很低,CV 误差居高不下,两线间距大且不收敛——此时增加数据有效但缓慢 | | ④ | 残差图(`poly_residual_plot.png`) | 检验模型假设(独立性、同方差、正态性) | 残差应围绕 0 随机散布、无弯曲/喇叭形趋势;直方图近似钟形则噪声正态假设成立;残差仍有正弦形状说明模型漏掉了结构 | 四张图的使用顺序:**先看 ① 定性 → 再看 ② 选阶 → 用 ③ 判断数据量的作用 → 最后用 ④ 检验假设**。完整写进论文时,① 和 ② 是"模型选择"章节的核心证据,④ 是"模型检验"章节的证据。 --- ## 五、符号说明 | 符号 | 含义 | 示例/单位 | |---|---|---| | $x$ | 自变量(单特征) | 如时间(天)、浓度(mol/L) | | $y$ | 因变量(观测值) | 如销量(件)、温度(℃) | | $\hat{y}$ | 模型预测值 | 与 $y$ 同单位 | | $\bar{y}$ | 样本均值 | 与 $y$ 同单位 | | $n$ | 样本量 | 本文示例 $n = 100$ | | $d$ | 多项式阶数(超参数) | 正整数,如 $d = 3$ | | $p$ | 参数个数(含截距) | $p = d + 1$ | | $\beta_j$ | 第 $j$ 次项系数 | 如 $\beta_2$ 为二次项系数 | | $\hat{\boldsymbol{\beta}}$ | 系数的最小二乘估计向量 | 长度 $d+1$ | | $\varepsilon$ | 随机误差(噪声) | $\varepsilon \sim N(0, \sigma^2)$ | | $\sigma^2$ | 噪声方差 | 示例中 $\sigma^2 = 0.0625$ | | $V$ | 范德蒙矩阵 | $n \times (d+1)$ 矩阵 | | $RSS$ | 残差平方和 | $RSS = \sum (y_i - \hat{y}_i)^2$,与 $y^2$ 同单位 | | MSE | 均方误差 | 与 $y^2$ 同单位 | | RMSE | 均方根误差 | 与 $y$ 同单位 | | $R^2$ | 决定系数 | 无量纲,$(-\infty, 1]$ | | $\bar{R}^2$ | 调整 R² | 无量纲 | | CV-MSE | K 折交叉验证均方误差 | 与 $y^2$ 同单位 | | AIC / BIC | 赤池 / 贝叶斯信息准则 | 无量纲,仅相对大小有意义 | | $K$ | 交叉验证折数 | 如 $K = 5$ | --- ## 六、可运行程序(完整代码) > **代码说明**:以下所有代码块**按顺序拼接即为一个完整脚本**,可直接保存为 `.py` 文件运行。数据由 `np.random.seed(42)` 合成生成(真实函数 $y = \sin(x)$,$x \in [0, 2\pi]$,$n = 100$,高斯噪声 $\sigma = 0.25$),无任何外部文件依赖。 > > **代码结构**:① 导入与全局设置 → ② 生成合成数据 → ③ 手写实现(范德蒙矩阵 + 正规方程)→ ④ sklearn 对照实现 → ⑤ 指标辅助函数 → ⑥ 划分训练/测试集 → ⑦ 5 折交叉验证选阶 → ⑧ 打印全部指标 → ⑨ 过拟合诊断 → ⑩ 手写与 sklearn 一致性对照 → ⑪⑫⑬⑭ 绘制并保存 4 张图。 > > **运行结果**:控制台打印各阶数的 CV 误差、全部指标表、诊断信息;`figures/` 目录下生成 4 张 PNG 图。 ```python # ============================================================================= # 多项式回归完整示例(可独立运行,无外部文件依赖) # 环境:Python 3.12;依赖库:numpy / scikit-learn / matplotlib / pandas # 数据:np.random.seed(42) 合成数据,真实函数 y = sin(x),x ∈ [0, 2π],n = 100 # 内容:① 手写实现(范德蒙矩阵 + 正规方程,先标准化保证数值稳定) # ② sklearn(StandardScaler + PolynomialFeatures + LinearRegression)对照 # ③ 阶数选择:5 折交叉验证 CV-MSE + AIC / BIC + 调整 R² # ④ 4 张诊断图(保存至 figures/ 目录,文件名前缀 poly_,并 plt.show()) # ============================================================================= import os import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import KFold, train_test_split, learning_curve from sklearn.preprocessing import PolynomialFeatures, StandardScaler from sklearn.pipeline import make_pipeline from sklearn.linear_model import LinearRegression # ========== 全局设置:中文字体(按序尝试,保证标题/标签不乱码) ========== plt.rcParams["font.sans-serif"] = ["PingFang SC", "Arial Unicode MS", "SimHei"] plt.rcParams["axes.unicode_minus"] = False # ========== 输出目录:4 张图统一保存到 figures/ 子目录 ========== os.makedirs("figures", exist_ok=True) ``` ```python # ========== 1. 生成合成数据(固定随机种子,结果可复现) ========== np.random.seed(42) n = 100 # 样本量 x = np.linspace(0.0, 2.0 * np.pi, n) # 自变量:在 [0, 2π] 上等距取点 y_true = np.sin(x) # 真实函数(无噪声,仅用于对照) noise = np.random.normal(loc=0.0, scale=0.25, size=n) # 高斯噪声,σ = 0.25 y = y_true + noise # 观测值 = 真实函数 + 噪声 X = x.reshape(-1, 1) # 统一转成 (n, 1) 的列向量格式 print("=" * 78) print(f"数据概览:n = {n},x ∈ [0, 2π],真实函数 y = sin(x),噪声 σ = 0.25") print("=" * 78) ``` ```python # ========== 2. 手写实现:范德蒙矩阵 + 正规方程 ========== class HandPolyReg: """手写多项式回归(最小二乘解析解)。 思路:把 [x, x², ..., x^d] 当作新特征 → 多元线性回归 → 正规方程 (VᵀV)β = Vᵀy。 数值稳定性:高阶时 x^k 各列量纲差异巨大,VᵀV 条件数爆炸; 因此拟合前对非截距列做 z-score 标准化(本质是换一组基,预测结果不变)。 """ def __init__(self, degree: int, standardize: bool = True): self.degree = degree # 多项式阶数 d self.standardize = standardize # 是否对非截距列做标准化 def _design_matrix(self, x): """构造范德蒙矩阵 V = [1, x, x², ..., x^d],形状 (n, d+1)。""" x = np.asarray(x).ravel() return np.column_stack([x ** k for k in range(self.degree + 1)]) def fit(self, x, y): V = self._design_matrix(x) # 记录非截距列的均值/标准差(predict 时必须用同样的变换) self.col_mean_ = V[:, 1:].mean(axis=0) self.col_std_ = V[:, 1:].std(axis=0) if self.standardize: V[:, 1:] = (V[:, 1:] - self.col_mean_) / self.col_std_ # 正规方程:β̂ = (VᵀV)⁻¹ Vᵀy(用 solve 求解线性方程组,比显式求逆更稳定) self.beta_ = np.linalg.solve(V.T @ V, V.T @ y) return self def predict(self, x): V = self._design_matrix(x) if self.standardize: V[:, 1:] = (V[:, 1:] - self.col_mean_) / self.col_std_ return V @ self.beta_ # ========== 3. sklearn 对照实现:标准化 + 多项式特征 + 线性回归 ========== def make_sklearn_poly(degree: int): """等价流水线:先标准化 x,再构造多项式特征,最后线性回归。 注意:先标准化再乘方 与 对 x^k 各列分别标准化 张成的是同一个函数空间 (多项式空间的基变换),因此预测结果与手写实现一致。""" return make_pipeline( StandardScaler(), PolynomialFeatures(degree=degree, include_bias=False), LinearRegression(), ) ``` ```python # ========== 4. 指标计算辅助函数(第三节公式的直接翻译) ========== def r2_score(y_true_, y_pred_): """决定系数 R² = 1 - SS_res / SS_tot""" ss_res = np.sum((y_true_ - y_pred_) ** 2) ss_tot = np.sum((y_true_ - np.mean(y_true_)) ** 2) return 1.0 - ss_res / ss_tot def rmse(y_true_, y_pred_): """均方根误差 RMSE = sqrt( MSE )""" return float(np.sqrt(np.mean((y_true_ - y_pred_) ** 2))) # ========== 5. 划分训练集 / 测试集(固定随机种子,保证各阶数可比) ========== X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) n_train, n_test = len(y_train), len(y_test) print(f"训练集 n_train = {n_train},测试集 n_test = {n_test}") ``` ```python # ========== 6. 阶数选择:5 折交叉验证(手写循环,d = 1 ~ 12) ========== max_degree = 12 kf = KFold(n_splits=5, shuffle=True, random_state=42) cv_mse = {} # 阶数 -> 5 折 CV 平均 MSE print("\n" + "=" * 78) print("【阶数选择】各阶数的 5 折交叉验证 MSE(在全部 100 个样本上评估)") print("=" * 78) for d in range(1, max_degree + 1): fold_mse = [] for tr_idx, va_idx in kf.split(X): model = HandPolyReg(d).fit(X[tr_idx], y[tr_idx]) # 手写模型 fold_mse.append(np.mean((model.predict(X[va_idx]) - y[va_idx]) ** 2)) cv_mse[d] = float(np.mean(fold_mse)) print(f"阶数 d = {d:2d} -> 5折CV-MSE = {cv_mse[d]:.5f}") best_d = min(cv_mse, key=cv_mse.get) # CV-MSE 最小的阶数即最优阶数 print(f"\n>>> 交叉验证选择的最优阶数:d* = {best_d}" f"(CV-MSE 最小 = {cv_mse[best_d]:.5f})") ``` ```python # ========== 7. 打印第三节提到的全部指标(R² / RMSE / AIC / BIC / 调整R²) ========== print("\n" + "=" * 78) print("【全部指标】各阶数在训练集 / 测试集上的指标(手写实现,标准化求解)") print("=" * 78) rows = [] for d in range(1, max_degree + 1): m = HandPolyReg(d).fit(X_train, y_train) y_tr = m.predict(X_train) # 训练集预测 y_te = m.predict(X_test) # 测试集预测 mse_tr = np.mean((y_train - y_tr) ** 2) mse_te = np.mean((y_test - y_te) ** 2) rss_tr = np.sum((y_train - y_tr) ** 2) # 训练集残差平方和 p = d + 1 # 参数个数(含截距) aic = n_train * np.log(rss_tr / n_train) + 2 * p # AIC bic = n_train * np.log(rss_tr / n_train) + p * np.log(n_train) # BIC adj_r2 = 1 - (1 - r2_score(y_train, y_tr)) * (n_train - 1) / (n_train - p) rows.append([d, cv_mse[d], mse_tr, rmse(y_train, y_tr), r2_score(y_train, y_tr), mse_te, rmse(y_test, y_te), r2_score(y_test, y_te), aic, bic, adj_r2]) df_metrics = pd.DataFrame(rows, columns=[ "阶数d", "5折CV-MSE", "训练MSE", "训练RMSE", "训练R²", "测试MSE", "测试RMSE", "测试R²", "AIC", "BIC", "调整R²"]) print(df_metrics.round(4).to_string(index=False)) idx_best = best_d - 1 # DataFrame 从 0 开始,阶数 1 对应第 0 行 print(f"\n>>> 最优阶数 d* = {best_d}:" f"训练R² = {df_metrics.loc[idx_best, '训练R²']:.4f}," f"测试R² = {df_metrics.loc[idx_best, '测试R²']:.4f}," f"测试RMSE = {df_metrics.loc[idx_best, '测试RMSE']:.4f}(噪声水平 σ = 0.25)") ``` ```python # ========== 8. 过拟合诊断:d = 9(高次多项式)的典型表现 ========== m9 = HandPolyReg(9).fit(X_train, y_train) m_best = HandPolyReg(best_d).fit(X_train, y_train) y_tr9, y_te9 = m9.predict(X_train), m9.predict(X_test) print("\n" + "=" * 78) print("【过拟合诊断】d = 9 与最优阶数的对比") print("=" * 78) print(f"d = {best_d} 训练R² = {r2_score(y_train, m_best.predict(X_train)):.4f}," f"测试R² = {r2_score(y_test, m_best.predict(X_test)):.4f}") print(f"d = 9 训练R² = {r2_score(y_train, y_tr9):.4f}," f"测试R² = {r2_score(y_test, y_te9):.4f}") print(f"d = 9 训练RMSE = {rmse(y_train, y_tr9):.4f}," f"测试RMSE = {rmse(y_test, y_te9):.4f}") print(f"d = 9 测试RMSE / 训练RMSE = " f"{rmse(y_test, y_te9) / rmse(y_train, y_tr9):.2f} 倍(背离越大过拟合越严重)") print(f"d = 9 系数绝对值最大 |β| = {np.max(np.abs(m9.beta_)):.2f}" f"(标准化基下;系数巨大是过拟合的典型信号)") ``` ```python # ========== 9. 手写实现 vs sklearn Pipeline:结果一致性对照 ========== print("\n" + "=" * 78) print("【一致性对照】手写实现(范德蒙矩阵+正规方程) vs sklearn Pipeline") print("=" * 78) for d in (best_d, 9): hand = HandPolyReg(d).fit(X_train, y_train) sk = make_sklearn_poly(d).fit(X_train, y_train) pred_h, pred_s = hand.predict(X_test), sk.predict(X_test) max_diff = float(np.max(np.abs(pred_h - pred_s))) print(f"阶数 d = {d:2d}:测试集预测最大绝对差 = {max_diff:.3e};" f"测试R²(手写 {r2_score(y_test, pred_h):.6f} / " f"sklearn {r2_score(y_test, pred_s):.6f})") # 数值稳定性说明:d = 9 时不标准化 vs 标准化 的系数矩阵条件数对比 V9 = np.column_stack([X_train.ravel() ** k for k in range(10)]) V9s = V9.copy() V9s[:, 1:] = (V9s[:, 1:] - V9s[:, 1:].mean(axis=0)) / V9s[:, 1:].std(axis=0) print("d = 9 时正规方程系数矩阵 VᵀV 的条件数:") print(f" 不标准化:cond(VᵀV) = {np.linalg.cond(V9.T @ V9):.3e}" f"(接近机器精度倒数 1e16,求解不可信)") print(f" 标准化后:cond(VᵀV) = {np.linalg.cond(V9s.T @ V9s):.3e}(大幅改善)") ``` ```python # ========== 10. 图①:不同阶数拟合曲线对比(欠拟合 / 合适 / 过拟合) ========== fig, ax = plt.subplots(figsize=(9, 5.5)) ax.scatter(x, y, s=25, alpha=0.6, color="#898781", label="观测数据(含噪声)") ax.plot(x, y_true, "--", color="#0b0b0b", lw=2, label="真实函数 y = sin(x)") for d, c, lb in [(1, "#2a78d6", "d=1 欠拟合"), (3, "#1baf7a", "d=3 拟合良好"), (9, "#e34948", "d=9 过拟合")]: m = HandPolyReg(d).fit(X, y) # 用全部数据拟合,便于直观对比 ax.plot(x, m.predict(X), color=c, lw=2, label=lb) ax.set_xlabel("x") ax.set_ylabel("y") ax.set_title("图① 不同阶数多项式拟合对比(同一份数据)") ax.legend() ax.grid(alpha=0.3, color="#e1e0d9") fig.tight_layout() fig.savefig("figures/poly_degree_comparison.png", dpi=200) plt.show() # ========== 11. 图②:阶数—训练/测试误差曲线(U 型 + 背离) ========== d_list = np.arange(1, max_degree + 1) train_rmse_list, test_rmse_list = [], [] for d in d_list: m = HandPolyReg(d).fit(X_train, y_train) train_rmse_list.append(rmse(y_train, m.predict(X_train))) test_rmse_list.append(rmse(y_test, m.predict(X_test))) fig, ax = plt.subplots(figsize=(9, 5.5)) ax.plot(d_list, train_rmse_list, "o-", color="#2a78d6", label="训练 RMSE") ax.plot(d_list, test_rmse_list, "s-", color="#e34948", label="测试 RMSE") ax.axvline(best_d, color="#1baf7a", ls="--", lw=1.5, label=f"CV 最优阶数 d*={best_d}") ax.fill_between(d_list, train_rmse_list, test_rmse_list, color="#eb6834", alpha=0.12, label="训练—测试误差背离(过拟合区)") ax.set_xlabel("多项式阶数 d") ax.set_ylabel("RMSE") ax.set_title("图② 阶数与误差:测试误差呈 U 型,高阶时与训练误差背离") ax.set_xticks(d_list) ax.legend() ax.grid(alpha=0.3, color="#e1e0d9") fig.tight_layout() fig.savefig("figures/poly_error_vs_degree.png", dpi=200) plt.show() ``` ```python # ========== 12. 图③:学习曲线(训练误差与 CV 误差随样本量的变化) ========== fig, axes = plt.subplots(1, 2, figsize=(13, 5)) train_sizes_frac = np.linspace(0.2, 1.0, 7) # 训练集比例:20% ~ 100% for ax, d, title in zip(axes, [best_d, 9], [f"阶数 d={best_d}(合适)", "阶数 d=9(过拟合)"]): model = make_sklearn_poly(d) # 用 sklearn 流水线 sizes, tr_scores, va_scores = learning_curve( model, X, y, train_sizes=train_sizes_frac, cv=5, scoring="neg_mean_squared_error", random_state=42) tr_mse = -tr_scores.mean(axis=1) # 取负号还原成 MSE va_mse = -va_scores.mean(axis=1) ax.plot(sizes, tr_mse, "o-", color="#2a78d6", label="训练 MSE") ax.plot(sizes, va_mse, "s-", color="#e34948", label="5 折 CV MSE") ax.set_xlabel("训练样本量") ax.set_ylabel("MSE") ax.set_title(f"图③ 学习曲线:{title}") ax.legend() ax.grid(alpha=0.3, color="#e1e0d9") fig.tight_layout() fig.savefig("figures/poly_learning_curve.png", dpi=200) plt.show() # ========== 13. 图④:最优阶数模型的残差图 ========== best_model = HandPolyReg(best_d).fit(X_train, y_train) y_fit = best_model.predict(X_test) resid = y_test - y_fit # 测试集残差 fig, axes = plt.subplots(1, 2, figsize=(13, 5)) axes[0].axhline(0, color="#0b0b0b", lw=1) axes[0].axhline(2 * resid.std(), color="#eb6834", ls="--", lw=1, label="±2σ 参考线") axes[0].axhline(-2 * resid.std(), color="#eb6834", ls="--", lw=1) axes[0].scatter(y_fit, resid, s=30, alpha=0.6, color="#1baf7a", label="测试集残差") axes[0].set_xlabel("拟合值 ŷ") axes[0].set_ylabel("残差 y − ŷ") axes[0].set_title(f"图④ 残差图(d*={best_d}):残差应围绕 0 随机散布") axes[0].legend() axes[0].grid(alpha=0.3, color="#e1e0d9") axes[1].hist(resid, bins=12, color="#2a78d6", alpha=0.8, edgecolor="white") axes[1].set_xlabel("残差") axes[1].set_ylabel("频数") axes[1].set_title("残差直方图(近似钟形 → 噪声正态假设合理)") axes[1].grid(alpha=0.3, color="#e1e0d9") fig.tight_layout() fig.savefig("figures/poly_residual_plot.png", dpi=200) plt.show() print("\n" + "=" * 78) print("全部完成!4 张图已保存至 figures/ 目录:") print(" figures/poly_degree_comparison.png (图① 不同阶数拟合对比)") print(" figures/poly_error_vs_degree.png (图② 阶数—误差曲线)") print(" figures/poly_learning_curve.png (图③ 学习曲线)") print(" figures/poly_residual_plot.png (图④ 残差图)") print("=" * 78) ``` --- ## 七、结果解读与注意事项 ### 7.1 运行输出解读(以本文合成数据为例) 运行第六节代码(`seed=42`,$n=100$,$\sigma=0.25$),典型输出与解读如下: 1. **交叉验证选阶**:5 折 CV-MSE 在 $d$ 较小时快速下降($d=1$ 时为 0.2150,欠拟合严重),在 $d=5$ 达到最低(0.0540),此后随 $d$ 增大缓慢回升($d=12$ 时达 0.1249)——整体呈 U 型,故 $d^* = 5$。两个细节值得注意:① $d=2$ 的 CV-MSE(0.2343)反而略高于 $d=1$,因为 $\sin x$ 是奇函数,二次项几乎帮不上忙;② $d=3\sim 9$ 的 CV-MSE 差异不悬殊(0.054~0.061),小样本下中等阶数表现接近是常见现象。另外,AIC 与 BIC 的最低点同样落在 $d=5$,与 CV 结论一致;而调整 R² 到 $d=10$ 仍在缓慢上升(0.9153),说明它对复杂度的惩罚偏弱——这正是本文主张「CV 为主、AIC/BIC 为辅、调整 R² 仅供参考」的原因。 2. **指标表**:$d^*=5$ 时训练 R² = 0.9189、测试 R² = 0.8150、测试 RMSE = 0.2604,与噪声水平 $\sigma=0.25$ 接近,说明模型已逼近"噪声地板",继续加复杂度没有意义。注意训练 R² 随阶数单调上升($d=12$ 时达 0.9276),但测试 R² 在 $d=5$ 之后总体走低($d=7$ 时已明显下降)——这是"不能只看训练 R² 选阶"的直接证据。 3. **过拟合诊断($d=9$)**:训练 R² = 0.9245(高于 $d^*=5$ 的 0.9189),但测试 R² = 0.7868 反而更低;测试 RMSE / 训练 RMSE = 1.38 倍($d^*=5$ 时该比值约 1.24),训练误差与测试误差出现背离;同时 $|\beta|$ 的最大值高达 7994.7——系数巨大是过拟合的经典信号。图① 中 $d=9$ 曲线在数据点间剧烈抖动,图② 右侧两线明显分离。 4. **学习曲线**:$d^*=5$ 的两条曲线随样本量增大迅速靠拢到低值(模型合适);$d=9$ 的训练 MSE 始终贴地、CV MSE 居高不下,两线间距几乎不随样本量缩小——说明这个模型方差过大,主要问题不是数据少。 5. **残差图**:$d^*=5$ 的残差围绕 0 随机散布、无弯曲或喇叭形结构,直方图近似钟形,说明同方差与正态性假设基本成立。若残差仍呈正弦形,则说明模型漏掉了系统性结构(此时应提高阶数或换模型)。 6. **一致性对照**:手写实现与 sklearn Pipeline 的预测最大绝对差为 $3.5\times 10^{-12}$($d=5$)和 $1.9\times 10^{-5}$($d=9$,条件数更高所致),两套实现的测试 R² 完全一致,说明二者等价;条件数对比显示标准化把 $V^\top V$ 的条件数从 $3.3\times 10^{18}$ 降到 $4.2\times 10^{12}$(下降 6 个数量级),直观验证了"先标准化更稳"的结论——且阶数越高,标准化的收益越大。 ### 7.2 常见坑 1. **高次多项式外推灾难**:多项式在数据区间外由最高次项 $x^d$ 主导,预测值会以幂次速度冲向 $+\infty$ 或 $-\infty$。哪怕 $d=2$,把 $x$ 外推 2 倍,误差也按 $(2x)^2$ 放大。**结论:多项式回归原则上只用于插值,不用于外推**;确需外推时,只能小步外推并在论文中写明风险。 2. **Runge 现象**:对于光滑但变化丰富的函数(如 $f(x) = 1/(1+25x^2)$),在等距节点上提高阶数,两端反而出现剧烈振荡、误差发散。多项式回归同样受影响——**不是阶数越高越好**,等距数据下 $d$ 一般不超过 5,更灵活的需求交给样条。 3. **特征标准化**:$x$ 的量纲若很大(如年份 2000~2026),$x^d$ 会迅速爆炸导致正规方程病态。拟合前务必对特征标准化(本文手写代码对范德蒙列标准化,sklearn 代码在 Pipeline 中先 `StandardScaler`)。 4. **阶数不能只看 R²**:训练 R² 随阶数单调上升,永远"越大越好",直接按训练 R² 选阶必然选出最大 $d$ 而严重过拟合。正确的选阶依据顺序是:**交叉验证 CV-MSE(主)→ AIC/BIC(辅)→ 调整 R²(参考)**,测试集 R²/RMSE 只用于最终报告,不能参与选阶。 5. **噪声地板效应**:测试 RMSE 不可能低于噪声水平 $\sigma$。若模型 RMSE 已接近数据噪声估计值,再优化空间有限,应在论文中说明,避免被评审质疑"为什么不再提升精度"。 6. **样本量与阶数匹配**:$n < d+1$ 时正规方程无唯一解(矩阵不满秩),$n$ 刚超过 $d+1$ 时估计极不稳定。经验法则 $n \ge 10d$。 ### 7.3 竞赛论文写作建议(话术模板) - **模型引入**:「考虑到自变量 $x$ 与因变量 $y$ 之间呈现非线性关系(见图①),本文采用多项式回归模型 $y = \beta_0 + \beta_1 x + \cdots + \beta_d x^d + \varepsilon$。该模型对参数 $\boldsymbol{\beta}$ 仍是线性的,可由最小二乘法得到解析解,计算高效且可解释性强。」 - **阶数选择**:「为避免过拟合,本文采用 5 折交叉验证对阶数进行选择:以 CV-MSE 最小为准则,同时参考 AIC、BIC 与调整 R²,最终确定阶数 $d^* = 5$(各阶 CV-MSE 见图②,呈 U 型)。」 - **模型检验**:「最优模型的测试集 $R^2$ 为 0.82,RMSE 为 0.26,与数据噪声水平($\sigma = 0.25$)接近,表明拟合已达到噪声地板;残差图显示残差围绕零线随机分布,说明同方差与正态性假设成立。」 - **过拟合讨论**:「对比实验表明,当阶数升至 $d=9$ 时,训练 R² 接近 1 而测试 RMSE 显著上升,训练误差与测试误差出现明显背离,且系数绝对值急剧增大——这是过拟合的典型特征,进一步验证了交叉验证选阶的必要性。」 - **局限性声明**:「多项式回归为全局模型,外推能力有限:当 $x$ 超出观测区间时,预测值受最高次项支配而迅速失真。因此本文仅将多项式回归用于观测区间内的拟合与短期预测,长期预测另采用(其他模型)。」 --- ## 八、延伸阅读 1. **样条回归(Spline Regression)**:把定义域切成若干段,每段用低阶多项式拟合,并在节点处约束光滑拼接。灵活性远高于全局多项式,且避免了 Runge 现象;代价是需要选择节点数目与位置。sklearn 中可用 `SplineTransformer` + `LinearRegression`,scipy 中可用 `scipy.interpolate.UnivariateSpline`。适合"拟合质量优先、不追求全局公式"的场景。 2. **局部加权回归(Locally Weighted Regression, LOWESS)**:非参数方法,对每个待预测点用其邻域样本做加权最小二乘(权重随距离衰减)。没有全局函数形式,拟合极度灵活;缺点是难以写出解析式、外推能力弱、计算量随样本增长。`statsmodels` 的 `lowess` 是标准实现(本文代码未使用该库)。 3. **正交多项式(Orthogonal Polynomials)**:把基函数 $\{1, x, \dots, x^d\}$ 换成正交基(如 Chebyshev、Legendre 多项式),使设计矩阵列之间近似正交,从根本上改善条件数,且各阶系数可独立估计——"加一阶不影响前几阶的系数"。适合高阶、等距数据的稳定拟合。 4. **Ridge 正则化多项式(岭回归)**:在最小二乘目标上加入 $\lambda\|\boldsymbol{\beta}\|^2$ 惩罚项,抑制高阶项系数爆炸,从而在保留高阶灵活性的同时控制过拟合。这是处理"必须用高阶但怕过拟合"的标准手段,与多项式特征结合即 `PolynomialFeatures + Ridge`。选 $\lambda$ 同样用交叉验证。 5. **进一步阅读方向**:偏差—方差分解的数学推导(理解 U 型误差曲线的理论根源);LOOCV 与广义交叉验证 GCV(小样本下的高效 CV);多元多项式与交互项($x_1 x_2$ 等交叉项及其指数爆炸问题);Lasso 回归用于稀疏选阶(同时做特征选择与拟合)。 --- > **总结**:多项式回归 = 线性回归 + 多项式基变换(范德蒙矩阵)。它是竞赛中最常用的"简单非线性基线",核心纪律只有一条——**阶数用交叉验证选,绝不用训练 R² 选;插值放心用,外推要谨慎**。配合第六节代码亲手运行一遍、看懂四张图,这一节的内容就可以直接写进论文了。