多项式曲线拟合是数据分析中的关键统计方法,旨在通过构建数学模型来逼近给定数据点。本文基于 C++ 结合 Eigen 库,深入讲解二维平面上的多项式拟合实现。核心采用最小二乘法优化系数,涵盖范德蒙矩阵构建、正规方程推导、数值稳定性处理(如归一化、SVD 分解)、异常值剔除及模型诊断等工程实践。文章强调偏差 - 方差权衡与过拟合风险,提供从理论到代码落地的完整路径,适用于趋势预测与建模分析场景。
graph TD
A[原始数据点 xi, yi] --> B{是否需要预处理?}
B -->|是 | C[归一化/去噪]
B -->|否 | D[直接构造矩阵]
C --> D
D --> E[初始化空矩阵 X]
E --> F[遍历每个 xi]
F --> G[计算 1, xi, xi², ..., xin]
G --> H[填入矩阵第 i 行]
H --> I{是否所有点处理完毕?}
I -->|否 | F
I -->|是 | J[输出范德蒙矩阵 X]
这套流程图看着平淡无奇,实则是工业级系统和玩具代码的本质区别。真正的鲁棒性,从来不是靠运气维持的。
阶数选择的艺术:偏差 - 方差的永恒博弈
很多人以为,拟合效果好不好,全看阶数够不够高。错!
现实中更常见的情况是:阶数越高,训练误差越低,但预测能力反而下降。
这就是著名的'偏差 - 方差权衡'问题。
模型类型
偏差
方差
表现
低阶(如线性)
高
低
欠拟合,错过趋势
中阶(如 3~6 次)
适中
适中
最佳平衡区
高阶(>8 次)
低
高
过拟合,记住噪声
我们可以用 Python 快速验证这一点:
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
np.random.seed(42)
x = np.linspace(0, 3, 30)
y_true = 2 * x**2 - 3 * x + 1
y_noisy = y_true + np.random.normal(0, 0.5, size=x.shape)
train_errors = []
val_errors = []
degrees = range(1, 10)
for d in degrees:
poly = PolynomialFeatures(degree=d)
X_poly = poly.fit_transform(x.reshape(-1, 1))
idx_train = np.random.choice(len(x), size=20, replace=False)
idx_val = np.setdiff1d(np.arange(len(x)), idx_train)
reg = LinearRegression().fit(X_poly[idx_train], y_noisy[idx_train])
y_train_pred = reg.predict(X_poly[idx_train])
y_val_pred = reg.predict(X_poly[idx_val])
train_errors.append(mean_squared_error(y_noisy[idx_train], y_train_pred))
val_errors.append(mean_squared_error(y_noisy[idx_val], y_val_pred))
# 绘图观察 U 型曲线
运行之后你会发现:验证误差先降后升,形成一个漂亮的 U 型曲线。最低点对应的阶数,才是真正的'最优解'。
boolread_csv(const std::string& filename, std::vector<double>& x_vec, std::vector<double>& y_vec){
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + filename);
}
std::string line;
int line_num = 0;
while (std::getline(file, line)) {
++line_num;
std::istringstream ss(line);
std::string cell_x, cell_y;
if (!std::getline(ss, cell_x, ',')) continue;
if (!std::getline(ss, cell_y, ',')) {
throw std::invalid_argument("Incomplete data at line " + std::to_string(line_num));
}
try {
double x = std::stod(cell_x);
double y = std::stod(cell_y);
x_vec.push_back(x);
y_vec.push_back(y);
} catch (...) {
throw std::invalid_argument("Invalid number format at line " + std::to_string(line_num));
}
}
if (x_vec.empty()) {
throw std::length_error("No valid data found in file.");
}
returntrue;
}
Vector gaussian_elimination(Matrix A, Vector b){
int n = A.size();
for (int i = 0; i < n; ++i) A[i].push_back(b[i]);
for (int k = 0; k < n; ++k) {
int max_row = k;
for (int i = k + 1; i < n; ++i)
if (abs(A[i][k]) > abs(A[max_row][k])) max_row = i;
swap(A[k], A[max_row]);
for (int i = k + 1; i < n; ++i) {
double factor = A[i][k] / A[k][k];
for (int j = k; j <= n; ++j) A[i][j] -= factor * A[k][j];
}
}
Vector x(n);
for (int i = n - 1; i >= 0; --i) {
x[i] = A[i][n];
for (int j = i + 1; j < n; ++j) x[i] -= A[i][j] * x[j];
x[i] /= A[i][i];
}
return x;
}
set title "Polynomial Fit (degree $1)"
set xlabel "x"
set ylabel "y"
set grid
set terminal png size 800,600
set output 'fit_plot.png'
plot '$2' using 1:2 with points pt 7 ps 0.8 title "Data", \
'$2' using 1:3 with lines lw 2 title "Fitted Curve"
C++ 中调用:
std::system("gnuplot plot.gp");
从此告别手动画图,真正实现'一键出报告'🎯。
模型诊断:防止自我欺骗的关键一步
最后提醒一句:不要盲目相信你的拟合结果。
系统应具备基本的自检能力:
if (degree >= 8 && rmse_on_test_region > 2 * global_rmse) {
std::cout << "[Warning] Possible overfitting detected. "
<< "Consider reducing polynomial degree." << std::endl;
}
double r2 = fitter.compute_r_squared();
if (r2 < 0.8) {
std::cout << "[Suggestion] Low R² (" << r2 << "). Model may underfit. Try higher degree or check data noise." << std::endl;
}