从零实现多项式拟合:最小二乘法原理、Python源码与过拟合实战
2026/8/17 6:59:49 网站建设 项目流程

1. 项目概述:从数据点到趋势线

“建模算法入门笔记-多项式拟合(附源码)”这个标题,对于刚接触数据分析、机器学习或者数学建模的朋友来说,吸引力是巨大的。它直指一个核心痛点:面对一堆看似杂乱无章的数据点,如何用一条光滑的曲线来揭示其背后的规律?多项式拟合,就是解决这个问题的“第一把钥匙”。

简单来说,多项式拟合就是用一个多项式函数(比如 y = a + bx + cx²)去逼近一组给定的数据点。它不像插值那样要求曲线必须穿过每一个点,而是追求整体趋势上的“最佳”匹配,这在实际应用中更为常见,因为真实数据总是伴随着噪声和误差。这个项目的目的,就是带你亲手实现这个过程,从理解数学原理,到编写代码,再到解读结果,完成一次完整的、可复现的建模实践。

为什么从多项式拟合入门?因为它完美地串联了数学、编程和实际应用。你不需要高深的数学背景,只需要基础的代数和微积分知识;你也不需要复杂的编程框架,用Python的NumPy库几行代码就能实现核心计算。通过它,你能直观地理解“模型”、“参数”、“拟合”、“过拟合”这些建模领域最核心的概念。无论是预测明天的气温变化,分析用户活跃度的趋势,还是校准传感器的读数,多项式拟合都是一个可靠且直观的起点。接下来,我们就从最根本的数学原理开始拆解。

2. 核心数学原理:最小二乘法的来龙去脉

多项式拟合的灵魂在于“最小二乘法”。这个名字听起来有点唬人,但它的思想非常直观:我们想要找到一条多项式曲线,使得这条曲线到所有数据点的“垂直距离”的平方和最小。

2.1 问题形式化与目标函数

假设我们有一组N个数据点(x_i, y_i),其中i = 1, 2, ..., N。我们想用一个m次多项式来拟合它们:y = w_0 + w_1 * x + w_2 * x² + ... + w_m * x^m这里的w_0, w_1, ..., w_m就是我们要求解的未知系数(也叫权重或参数)。

对于每一个数据点x_i,用我们的多项式模型预测出的y值为:y_pred_i = w_0 + w_1 * x_i + w_2 * x_i² + ... + w_m * x_i^m那么,预测值与真实值之间的误差(残差)就是:e_i = y_pred_i - y_i

最小二乘法的目标,就是找到一组系数w,使得所有数据点的误差平方和最小。这个目标函数(也叫损失函数)可以写成:L(w) = Σ (y_pred_i - y_i)² = Σ (w_0 + w_1*x_i + ... + w_m*x_i^m - y_i)²其中求和符号Σ是对 i 从1到N求和。

注意:为什么用平方和而不是绝对值和?主要是数学上的便利。平方函数处处可导,这使得我们可以用求导这个强大的工具来找到最小值点。而绝对值函数在零点不可导,求解起来更复杂。此外,平方项对大的误差惩罚更重,这通常符合我们的直觉。

2.2 求解过程:从求导到正规方程

我们的目标是最小化L(w)。在微积分中,一个多元函数取极值的必要条件是它对各个自变量的偏导数等于零。因此,我们对每个系数w_j求偏导,并令其为零:∂L/∂w_j = 2 * Σ (w_0 + w_1*x_i + ... + w_m*x_i^m - y_i) * x_i^j = 0, 其中j = 0, 1, ..., m

这给出了一个包含 (m+1) 个方程的线性方程组,未知数就是 (m+1) 个系数w。这个方程组可以整理成一个非常整洁的矩阵形式,即正规方程(X^T * X) * w = X^T * y

我们来拆解一下这个公式:

  • X是一个N x (m+1)的设计矩阵(也叫范德蒙矩阵)。它的每一行对应一个数据点,每一列对应多项式的一个基函数(1, x, x², ...)。
X = [[1, x_1, x_1^2, ..., x_1^m], [1, x_2, x_2^2, ..., x_2^m], ..., [1, x_N, x_N^2, ..., x_N^m]]
  • y是一个N x 1的列向量,包含了所有数据点的真实y值:[y_1, y_2, ..., y_N]^T
  • w是一个(m+1) x 1的列向量,就是我们要求解的系数:[w_0, w_1, ..., w_m]^T
  • X^TX的转置矩阵。

正规方程的意义在于,它将一个复杂的优化问题,转化为了一个线性代数中的求解线性方程组的问题。只要(X^T * X)这个矩阵是可逆的(通常数据点足够多且x值不特殊时成立),我们就可以直接解出系数:w = (X^T * X)^(-1) * X^T * y

这里的^(-1)表示矩阵的逆。这个公式就是多项式拟合(以及更广泛的线性回归)的解析解。理解了这个推导过程,你就能明白代码背后每一步计算的意义,而不仅仅是调用一个黑箱函数。

3. 从零实现:手搓核心拟合代码

理解了原理,我们动手用Python实现它。我们将使用NumPy库来处理矩阵运算,这是科学计算的基础。即使你未来会使用sklearn等高级库,自己实现一遍也能让你对底层机制有坚实的掌控感。

3.1 环境准备与数据构造

首先,确保你的Python环境安装了NumPy和Matplotlib(用于绘图)。可以通过pip install numpy matplotlib来安装。

我们从一个简单的例子开始:假设真实的规律是一个二次函数y = 1 + 2*x + 0.5*x²,然后我们加上一些随机噪声来模拟真实观测数据。

import numpy as np import matplotlib.pyplot as plt # 设置随机种子,确保每次运行结果一致 np.random.seed(42) # 生成模拟数据 def generate_sample_data(num_points=20, noise_scale=0.5): """ 生成带噪声的二次多项式数据。 参数: num_points: 数据点数量 noise_scale: 噪声的标准差 返回: x: 自变量数组 y: 因变量数组(带噪声) """ x = np.linspace(-3, 3, num_points) # 在-3到3之间均匀生成点 # 真实函数:y = 1 + 2*x + 0.5*x^2 y_true = 1 + 2*x + 0.5*x**2 # 加入高斯噪声 noise = np.random.normal(0, noise_scale, size=x.shape) y_noisy = y_true + noise return x, y_noisy, y_true x_data, y_data, y_true = generate_sample_data() print(f"生成 {len(x_data)} 个数据点。") print(f"X样本: {x_data[:5]}...") print(f"Y样本(带噪声): {y_data[:5]}...")

这段代码生成了我们的“实验数据”。y_true是我们想逼近的真相(但现实中未知),y_data是我们实际拿到手的有噪声数据。我们的任务就是通过x_datay_data,反推出一个接近1 + 2*x + 0.5*x²的多项式。

3.2 核心拟合函数实现

接下来,我们根据正规方程w = (X^T * X)^(-1) * X^T * y来实现拟合函数。

def polynomial_fit(x, y, degree): """ 使用最小二乘法进行多项式拟合。 参数: x: 一维自变量数组 y: 一维因变量数组 degree: 多项式次数 返回: w: 拟合系数数组,从常数项到高次项 [w0, w1, ..., w_degree] """ # 1. 构建设计矩阵 X # 使用np.vander可以快速生成范德蒙矩阵,但需要注意列的顺序(高次项在前)。 # 我们手动构建以便更清晰地理解。 X = np.ones((len(x), degree + 1)) # 先创建一个全1的矩阵,第一列对应常数项 for i in range(1, degree + 1): X[:, i] = x ** i # 第i列是x的i次方 # 2. 计算 X^T * X 和 X^T * y XT = X.T # 转置 XTX = np.dot(XT, X) # 矩阵乘法 XTy = np.dot(XT, y) # 3. 求解线性方程组 (XTX) * w = XTy # 使用np.linalg.solve求解,比直接求逆数值上更稳定。 w = np.linalg.solve(XTX, XTy) return w def polynomial_predict(x, w): """ 使用拟合好的系数w,对新的x值进行预测。 参数: x: 一维自变量数组或标量 w: 多项式系数数组 [w0, w1, ..., w_degree] 返回: y_pred: 预测值 """ degree = len(w) - 1 y_pred = np.zeros_like(x, dtype=float) for i, coeff in enumerate(w): y_pred += coeff * (x ** i) # w_i * x^i return y_pred

关键点解析

  1. 构建设计矩阵X:这是最关键的一步。矩阵的每一行对应一个样本,每一列对应一个特征(这里是x的0次方到m次方)。常数项(对应w_0)的那一列全是1。
  2. 使用np.linalg.solve:直接使用np.linalg.inv(XTX) @ XTy来计算w在数学上是等价的,但在数值计算中,直接求逆矩阵然后再乘,可能会因为矩阵条件数过大而引入较大的数值误差。np.linalg.solve是专门用于求解线性方程组的函数,它采用了更稳定的数值算法(如LU分解),是更专业的选择。
  3. 预测函数:实现了多项式求值。这里用了一个循环,清晰易懂。对于高性能需求,可以用NumPy的广播机制向量化实现:y_pred = np.polyval(w[::-1], x)(注意np.polyval的系数顺序是从高次到低次)。

3.3 拟合效果可视化与评估

现在,让我们用一次、二次和五次多项式来拟合数据,并直观地看看效果。

# 进行不同次数的拟合 degrees = [1, 2, 5] coefficients = {} predictions = {} # 生成用于绘制平滑曲线的密集点 x_plot = np.linspace(x_data.min() - 0.5, x_data.max() + 0.5, 200) plt.figure(figsize=(15, 5)) for idx, degree in enumerate(degrees): # 拟合 w = polynomial_fit(x_data, y_data, degree) coefficients[degree] = w # 预测 y_plot_pred = polynomial_predict(x_plot, w) predictions[degree] = y_plot_pred # 绘图 plt.subplot(1, 3, idx + 1) plt.scatter(x_data, y_data, color='blue', alpha=0.6, label='Noisy Data', s=30) plt.plot(x_plot, y_true, 'k--', label='True Function (y=1+2x+0.5x²)', linewidth=2) plt.plot(x_plot, y_plot_pred, 'r-', label=f'Fit (degree={degree})', linewidth=2) plt.title(f'Polynomial Fit (Degree {degree})') plt.xlabel('x') plt.ylabel('y') plt.legend() plt.grid(True, linestyle='--', alpha=0.5) # 在图上标注拟合出的方程(以二次为例) if degree == 2: eq_text = f'$y = {w[0]:.2f} + {w[1]:.2f}x + {w[2]:.2f}x^2$' plt.text(0.05, 0.95, eq_text, transform=plt.gca().transAxes, fontsize=10, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) plt.tight_layout() plt.show() # 打印拟合系数 print("拟合系数:") for degree, w in coefficients.items(): print(f" 次数 {degree}: {np.round(w, 4)}")

运行这段代码,你会得到三张并排的图。从图中你可以立刻观察到几个重要现象:

  • 一次拟合(直线):明显无法捕捉数据的弯曲趋势,误差很大。
  • 二次拟合:曲线形状与真实函数(黑色虚线)非常接近,拟合效果很好。打印出的系数[w0, w1, w2]也会接近[1, 2, 0.5]
  • 五次拟合:曲线变得非常“扭曲”,为了穿过每一个数据点而剧烈摆动,尤其是在数据区域的边缘。它完美地拟合了噪声,而不是潜在的趋势。

这引出了建模中一个至关重要的概念:过拟合。我们将在下一章深入探讨。

4. 关键挑战与进阶话题:欠拟合、过拟合与模型选择

拟合不是次数越高越好。上面的对比已经生动地展示了欠拟合和过拟合的问题。

4.1 理解欠拟合与过拟合

  • 欠拟合:模型过于简单(如一次多项式),无法捕捉数据中的基本结构和规律。表现为训练误差和测试误差都很大。就像用一把直尺去量一个弯曲的碗边,怎么量都不准。
  • 过拟合:模型过于复杂(如高次多项式),不仅学习了数据中的潜在规律,还“学习”了数据中的随机噪声。表现为在训练数据上误差很小,但在新的、未见过的数据上误差很大。模型失去了泛化能力。就像根据一张有折痕的地图,硬生生记下了折痕的形状,以为那也是道路的一部分。

4.2 量化评估:误差指标

我们需要定量的指标来衡量拟合好坏。最常用的包括:

  1. 均方误差MSE = (1/N) * Σ (y_pred_i - y_i)²。这就是我们最小化的目标函数。值越小越好。
  2. 均方根误差RMSE = sqrt(MSE)。它与原始y值有相同的量纲,更易于解释。
  3. R平方R² = 1 - (Σ(y_i - y_pred_i)² / Σ(y_i - y_mean)²)。它表示模型对数据波动的解释比例,范围在0到1之间(可能为负),越接近1越好。
def evaluate_fit(y_true, y_pred): """计算并打印多种评估指标。""" mse = np.mean((y_true - y_pred) ** 2) rmse = np.sqrt(mse) ss_res = np.sum((y_true - y_pred) ** 2) ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 print(f" 均方误差: {mse:.4f}") print(f" 均方根误差: {rmse:.4f}") print(f" R平方: {r_squared:.4f}") return mse, rmse, r_squared # 在训练数据上评估不同模型 print("在训练数据上的评估:") for degree in degrees: y_pred_train = polynomial_predict(x_data, coefficients[degree]) print(f"次数 {degree}:") evaluate_fit(y_data, y_pred_train)

你会发现,五次多项式的MSE和R²在训练集上可能是最好的,但这是一种“虚假繁荣”。

4.3 模型选择的实战方法:交叉验证

为了真实评估模型的泛化能力,我们必须使用模型未见过的数据。标准做法是划分训练集和测试集,或者使用更稳健的K折交叉验证

from sklearn.model_selection import train_test_split, KFold # 注意:这里仅为了演示交叉验证思想,实际我们仍使用自己的拟合函数。 # 方法1:简单划分 x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=42) print(f"训练集大小: {len(x_train)}, 测试集大小: {len(x_test)}") best_degree = None best_rmse_test = float('inf') results = {} for degree in range(1, 8): # 尝试1到7次 # 在训练集上拟合 w_train = polynomial_fit(x_train, y_train, degree) # 在测试集上预测和评估 y_pred_test = polynomial_predict(x_test, w_train) _, rmse_test, _ = evaluate_fit(y_test, y_pred_test) # 在训练集上也评估一下,用于对比 y_pred_train = polynomial_predict(x_train, w_train) _, rmse_train, _ = evaluate_fit(y_train, y_pred_train) results[degree] = {'train_rmse': rmse_train, 'test_rmse': rmse_test} print(f"次数 {degree}: 训练集RMSE={rmse_train:.4f}, 测试集RMSE={rmse_test:.4f}") if rmse_test < best_rmse_test: best_rmse_test = rmse_test best_degree = degree print(f"\n根据测试集表现,最佳多项式次数为: {best_degree}") # 绘制误差随次数变化的曲线 degrees_tried = list(results.keys()) train_errors = [results[d]['train_rmse'] for d in degrees_tried] test_errors = [results[d]['test_rmse'] for d in degrees_tried] plt.figure(figsize=(8,5)) plt.plot(degrees_tried, train_errors, 'bo-', label='Training RMSE', linewidth=2, markersize=8) plt.plot(degrees_tried, test_errors, 'rs-', label='Test RMSE', linewidth=2, markersize=8) plt.xlabel('Polynomial Degree') plt.ylabel('RMSE') plt.title('Model Complexity vs. Error (Train/Test Split)') plt.legend() plt.grid(True) plt.xticks(degrees_tried) plt.show()

观察绘制的误差曲线,你会看到一个典型模式:随着模型复杂度(次数)增加,训练误差持续下降(模型越来越强,总能更好地拟合训练数据),但测试误差会先下降后上升。那个测试误差最低点对应的模型复杂度,通常就是最佳选择。这直观地展示了偏差-方差权衡。

实操心得:在实际项目中,数据往往更宝贵。如果数据量很少,简单划分训练/测试集可能不稳定。这时K折交叉验证是更可靠的选择。其思想是将数据分成K份,轮流用其中K-1份训练,1份验证,重复K次后取平均误差作为模型性能的估计。这能更充分地利用数据,评估结果也更稳健。

5. 源码解析与工程化扩展

我们实现了一个基础版本。但在实际工程或更复杂的研究中,还需要考虑更多因素。让我们深入源码,看看有哪些可以优化和扩展的地方。

5.1 数值稳定性与正则化

当多项式次数较高时,设计矩阵X的列(即x, x², x³...)之间可能产生严重的多重共线性,导致(X^T * X)矩阵接近奇异(不可逆),求解系数时数值误差会急剧放大。这就是高次多项式拟合不稳定的根源之一。

解决这个问题有两大策略:

  1. 特征缩放:在构建设计矩阵前,对x值进行标准化(减去均值,除以标准差)。这样x, x², ...的尺度不会相差太大,能改善矩阵条件数。
  2. 正则化:在损失函数中加入对系数大小的惩罚项,强制模型变得简单。最常见的是岭回归,其损失函数为:L(w) = Σ(y_pred - y)² + α * Σ w_j²。其中α是正则化强度。这相当于在正规方程中给(X^T * X)矩阵的对角线元素加了一个小的常数:(X^T * X + α * I) * w = X^T * y。这能稳定求解过程,并有效抑制过拟合。
def polynomial_fit_ridge(x, y, degree, alpha=0.1): """使用岭回归(L2正则化)进行多项式拟合。""" # 构建设计矩阵 X = np.ones((len(x), degree + 1)) for i in range(1, degree + 1): X[:, i] = x ** i XT = X.T XTX = np.dot(XT, X) # 添加正则化项:在单位矩阵上乘以alpha reg_matrix = alpha * np.eye(degree + 1) # 注意:通常不对常数项w0进行强正则化,这里简单处理 w = np.linalg.solve(XTX + reg_matrix, np.dot(XT, y)) return w # 对比高次多项式下普通拟合与岭回归拟合 high_degree = 10 w_normal = polynomial_fit(x_data, y_data, high_degree) w_ridge = polynomial_fit_ridge(x_data, y_data, high_degree, alpha=1.0) print(f"普通拟合({high_degree}次)系数绝对值之和: {np.sum(np.abs(w_normal)):.2f}") print(f"岭回归拟合({high_degree}次)系数绝对值之和: {np.sum(np.abs(w_ridge)):.2f}") # 通常岭回归的系数绝对值之和会更小,模型更“平滑”。

5.2 使用专业库:NumPy.polyfit 与 Scikit-learn

在实际开发中,我们很少从头手写。掌握如何使用成熟、优化的库是必备技能。

NumPy.polyfit:

# 使用numpy的polyfit函数,一行代码搞定拟合 # 注意:np.polyfit返回的系数是从高次到低次 degree = 2 coefficients_np = np.polyfit(x_data, y_data, degree) print(f"np.polyfit 拟合系数(高次到低次): {coefficients_np}") # 预测可以使用np.poly1d生成一个多项式函数 poly_func = np.poly1d(coefficients_np) y_pred_np = poly_func(x_data)

np.polyfit内部也是通过求解正规方程(或使用更稳定的奇异值分解)实现的,并且做了大量优化,比自己写的版本更快、更稳定。

Scikit-learn: Scikit-learn提供了统一的机器学习接口,功能更强大,尤其适合集成到机器学习流水线中。

from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression, Ridge from sklearn.pipeline import make_pipeline degree = 2 # 方法1:使用Pipeline组合多项式特征生成和线性回归 model = make_pipeline(PolynomialFeatures(degree), LinearRegression()) # 注意:sklearn的输入X需要是二维数组 model.fit(x_data.reshape(-1, 1), y_data) # 从管道中提取系数稍微麻烦一点,但预测很方便 y_pred_sklearn = model.predict(x_plot.reshape(-1, 1)) # 方法2:使用岭回归 model_ridge = make_pipeline(PolynomialFeatures(degree), Ridge(alpha=1.0)) model_ridge.fit(x_data.reshape(-1, 1), y_data)

使用sklearn的好处是能轻松进行模型对比、超参数调优(如用GridSearchCV搜索最佳的degreealpha)以及交叉验证。

5.3 源码的模块化与封装

对于一个可复用的项目,我们应该将代码组织得更好。例如,可以创建一个PolynomialFitter类:

class PolynomialFitter: """多项式拟合器,支持普通最小二乘和岭回归。""" def __init__(self, degree, alpha=0.0, fit_intercept=True): self.degree = degree self.alpha = alpha # 正则化强度,0表示普通最小二乘 self.fit_intercept = fit_intercept self.coef_ = None # 系数,约定与sklearn一致,用下划线结尾表示估计量 self.intercept_ = 0.0 def fit(self, x, y): """拟合模型。""" # 特征工程:生成多项式特征 X = self._create_polynomial_features(x) # 求解系数 if self.alpha == 0: # 普通最小二乘 self.coef_ = np.linalg.lstsq(X, y, rcond=None)[0] else: # 岭回归 XT = X.T XTX = np.dot(XT, X) reg_matrix = self.alpha * np.eye(X.shape[1]) # 不对截距项正则化?这里简单处理,实际可调整 self.coef_ = np.linalg.solve(XTX + reg_matrix, np.dot(XT, y)) if self.fit_intercept: self.intercept_ = self.coef_[0] self.coef_ = self.coef_[1:] else: self.intercept_ = 0.0 return self def predict(self, x): """使用拟合模型进行预测。""" X = self._create_polynomial_features(x) if self.fit_intercept: # 如果拟合了截距,预测时加上 return np.dot(X, np.concatenate([[self.intercept_], self.coef_])) else: return np.dot(X, self.coef_) def _create_polynomial_features(self, x): """创建多项式特征矩阵。""" n_samples = len(x) if self.fit_intercept: X = np.ones((n_samples, self.degree + 1)) start_col = 1 else: X = np.ones((n_samples, self.degree)) start_col = 0 for i in range(start_col, X.shape[1]): X[:, i] = x ** (i if self.fit_intercept else i+1) return X def score(self, x, y): """计算R平方分数。""" y_pred = self.predict(x) ss_res = np.sum((y - y_pred) ** 2) ss_tot = np.sum((y - np.mean(y)) ** 2) return 1 - (ss_res / ss_tot) # 使用示例 fitter = PolynomialFitter(degree=2, alpha=0.1) fitter.fit(x_data, y_data) print(f"拟合系数: {fitter.coef_}") print(f"截距: {fitter.intercept_}") print(f"R²分数: {fitter.score(x_data, y_data):.4f}")

这样的封装将数据准备、模型训练、预测和评估集成在一起,接口清晰,易于测试和集成到更大的系统中。

6. 常见问题与实战排坑指南

在实际应用多项式拟合时,你会遇到各种各样的问题。下面是我总结的一些典型“坑”及其解决方法。

6.1 数值问题与求解失败

问题现象:当多项式次数较高或数据范围很大时,程序可能报错LinAlgError: Singular matrix(奇异矩阵错误),或者拟合出的系数巨大无比(如1e+15),预测结果完全错误。

根本原因:如前所述,高次幂会导致设计矩阵X的列向量近似线性相关,(X^T * X)矩阵的条件数极大,求逆或求解方程时数值误差爆炸。

解决方案

  1. 优先使用np.linalg.lstsqnp.linalg.solve:它们比直接求逆更稳定。
  2. 进行特征缩放:将x值标准化到[0, 1]或均值为0、方差为1的分布。这能极大改善条件数。
    x_mean, x_std = x_data.mean(), x_data.std() x_scaled = (x_data - x_mean) / x_std # 用x_scaled去拟合,得到系数w_scaled # 预测时,也需要先将新x值缩放,再用w_scaled预测,最后无需反缩放(因为y的尺度不变)。
  3. 使用正则化(岭回归):这是对付过拟合和数值不稳定性的标准方法。即使是很小的alpha(如1e-5)也能起到稳定作用。
  4. 使用更稳定的算法:如奇异值分解np.linalg.lstsq默认就使用了SVD。你也可以显式调用np.linalg.pinv(求伪逆)来求解:w = np.linalg.pinv(X) @ y。SVD能处理秩亏的矩阵,是数值计算中最稳健的方法之一,但计算量稍大。

6.2 如何确定最佳多项式次数?

这是一个模型选择问题,没有放之四海而皆准的答案,但有以下系统性的方法:

  1. 可视化观察法:像我们之前做的那样,画出不同次数下的拟合曲线,观察其是否平滑、是否过度扭曲。这对于低维数据(1-2个特征)非常直观有效。
  2. 训练/验证/测试集法
    • 将数据分为三部分:训练集(用于训练模型)、验证集(用于选择超参数,如degree)、测试集(用于最终评估)。
    • 在训练集上用不同degree训练模型,在验证集上计算误差(如RMSE)。
    • 选择验证集误差最小的degree作为最佳模型。
    • 最后,用测试集评估一次这个最佳模型的泛化性能。注意,测试集在整个调参过程中只能使用这唯一一次,否则会“数据泄露”,导致评估过于乐观。
  3. 交叉验证法:当数据量不大时,K折交叉验证是更可靠的选择。可以用sklearn.model_selection.cross_val_score方便地实现。
  4. 信息准则法:如AICBIC。它们在衡量模型拟合优度的同时,加入了对于模型复杂度的惩罚。AIC = 2k - 2ln(L),其中k是参数个数,L是模型似然函数的最大值。AIC/BIC越小越好。对于线性回归,在误差服从正态分布的假设下,AIC的计算可以简化为与对数MSE和参数个数相关。Scikit-learn的线性模型在statsmodels库中有更详细的统计输出包含AIC/BIC。

实操心得:在工业界,可视化+交叉验证是最常用的组合。先画图看个大概,再用交叉验证确定一个稳健的最优范围。记住一个原则:简单且有效的模型通常是更好的选择。如果二次和五次的交叉验证误差相差无几,果断选择二次模型。

6.3 拟合结果“跑飞”了怎么办?

有时你会发现,拟合的曲线在数据范围之外的地方(外推)会以不可思议的速度飞向正负无穷。这在高次多项式中尤其常见。

原因:多项式函数在|x|很大时,其值由最高次项主导。如果高次项系数不为零,x^m会变得极其巨大。

应对策略

  1. 认清局限性:多项式拟合(以及大多数基于统计的模型)主要适用于内插,即在训练数据覆盖的范围内进行预测。对于外推,其可靠性很差。这是模型本身的特性,不是bug。
  2. 限制使用范围:在应用中明确说明模型的有效区间。如果必须做外推,考虑使用其他模型,如考虑饱和增长的逻辑函数,或有物理意义的指数衰减/增长模型
  3. 使用约束拟合:如果你有先验知识(比如知道当x很大时,y应该趋近于一个常数),可以寻找支持约束的拟合方法,但这超出了普通最小二乘的范围,可能涉及非线性优化。

6.4 类别变量或复杂关系怎么办?

多项式拟合只能处理单个连续变量x和y之间的非线性关系。如果你的问题更复杂:

  • 多个特征:你需要的是多元线性回归,其原理完全相同,只是设计矩阵X的列变成了各个特征(以及它们的多项式项、交互项)。这可以通过PolynomialFeatures轻松扩展到多维。
  • 非多项式关系:如果数据看起来像指数增长、周期性变化等,可以尝试通过对y或x做变换(如取对数log(y),或使用sin(x),cos(x)作为特征),将其转化为线性问题来处理。这称为“线性化”。
  • 完全无法线性化:则需要转向更强大的非线性模型,如决策树神经网络等。

7. 项目总结与源码获取

走完这一趟,你应该已经对多项式拟合有了从理论到实践的全方位理解。我们从最小二乘法的数学原理出发,亲手推导了正规方程,并用Python从零实现了拟合过程。通过可视化对比,我们深刻认识了欠拟合与过拟合这一对核心矛盾,并学习了通过训练测试集划分和交叉验证来选择模型复杂度的方法。最后,我们还探讨了数值稳定性、正则化、工程化封装以及实际应用中常见的坑和解决方案。

这个项目提供的不仅仅是一个拟合函数,更是一个完整的建模思维框架:问题定义 -> 数学建模 -> 算法实现 -> 评估验证 -> 分析改进。掌握了这个框架,你再去学习更复杂的算法,如逻辑回归、支持向量机,甚至深度学习,都会发现它们的内核是相通的。

关于源码:本文中的所有代码块都是可独立运行的。我建议你千万不要直接复制粘贴,而是打开你的代码编辑器(如VSCode、PyCharm或Jupyter Notebook),亲手逐行敲入这些代码。在敲代码的过程中,尝试去修改参数(比如数据点的数量num_points、噪声大小noise_scale、多项式次数degree、正则化强度alpha),观察图形和输出结果如何变化。这是将知识内化的最快途径。

你可以将本章各节的代码块整合到一个或多个Python文件中,形成一个完整的“多项式拟合实验工具包”。例如,可以创建三个文件:

  1. core.py:存放polynomial_fit,polynomial_fit_ridge,PolynomialFitter等核心函数和类。
  2. utils.py:存放数据生成generate_sample_data、评估evaluate_fit、可视化绘图函数。
  3. demo.ipynb:一个Jupyter Notebook,用于交互式地运行和展示所有示例,就像本文所做的那样。

我个人在最初学习时,曾因为没做特征缩放,用一个7次多项式去拟合范围在0到100的数据,结果系数大到溢出,程序直接报错。也曾经盲目追求在训练集上的高R²,用一个15次多项式去拟合只有10个数据点的问题,结果模型完全失去了预测能力。这些教训让我深刻理解到,在建模中,对数据的理解和敬畏,与对数学和代码的掌握同等重要。希望这份笔记和源码,能成为你建模算法之旅的一块坚实垫脚石。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询