☰
第 36 章 · 综合项目一:最小二乘拟合
2026/10/9 15:15:13 网站建设 项目流程

把全书知识串起来的第一个实战项目:用 Eigen 做最小二乘直线拟合。这是数据科学、机器学习的入门基础,也是 Eigen 的典型应用。

36.1 问题:拟合一条直线

假设你有一批实验数据点 (x, y):

(0, 1.1), (1, 3.0), (2, 5.2), (3, 6.9), (4, 9.1), (5, 11.0)

它们大致落在一条直线上,但有点噪声。你想找一条直线y = ax + b最好地"拟合"这些点。

最小二乘的思想:找 a、b,使得所有点到直线的误差平方和最小。

36.2 数学原理:正规方程

把问题写成矩阵形式。对每个点 (xᵢ, yᵢ),有:

a·xᵢ + b = yᵢ

n 个点就是 n 个方程,写成矩阵:

[ x₁ 1 ] [ y₁ ] [ x₂ 1 ] [ a ] [ y₂ ] [ ... ] [ b ] = [ ...] [ xₙ 1 ] [ yₙ ]

即A·c = y,其中:

  • A 是 n×2 矩阵(第一列是 x,第二列全 1)
  • c = [a; b] 是待求参数
  • y 是观测值

这是"方程比未知数多"的超定方程,一般没有精确解,但有最小二乘解:

c = (AᵀA)⁻¹ Aᵀ y ← 正规方程

或者直接用 QR 分解求解(更稳定)。

36.3 用 Eigen 实现

#include<Eigen/Dense>#include<iostream>#include<vector>intmain(){// 原始数据std::vector<double>xs={0,1,2,3,4,5};std::vector<double>ys={1.1,3.0,5.2,6.9,9.1,11.0};intn=xs.size();Eigen::MatrixXdA(n,2);// 设计矩阵Eigen::VectorXdy(n);for(inti=0;i<n;i++){A(i,0)=xs[i];// 第一列是 xA(i,1)=1.0;// 第二列是 1(对应截距 b)y(i)=ys[i];// 观测值}// 最小二乘求解(QR 分解,稳健通用)Eigen::VectorXd c=A.colPivHouseholderQr().solve(y);doublea=c(0);// 斜率doubleb=c(1);// 截距std::cout<<"拟合直线:y = "<<a<<" * x + "<<b<<std::endl;return0;}

运行结果接近y = 1.98571 * x + 1.08571(真实关系是 y = 2x + 1 加噪声),拟合非常准。

编译运行:把上面代码存成ch36.cpp,然后(路径写法见附录 C.3):

g++-std=c++17-O2-I"你的Eigen目录"ch36.cpp-och36.exe ./ch36.exe

实际输出:

拟合直线:y = 1.98571 * x + 1.08571

36.4 评估拟合质量

用残差(真实值与预测值的差)评估。在ch36.cpp的std::cout << "拟合直线..."后面加两行:

doubleerror=(A*c-y).norm();// 残差范数std::cout<<"残差 = "<<error<<std::endl;

重新编译运行,输出变成:

拟合直线:y = 1.98571 * x + 1.08571 残差 = 0.226779

残差越小,拟合越好。这里 6 个点的总偏差约 0.23,相对 y 本身 1~11 的量级来说很小,说明直线确实"贴"住了这批数据。

36.5 拓展:多项式拟合

直线拟合是"线性拟合"。想拟合曲线(如二次 y = ax² + bx + c),只需扩展设计矩阵——加一列 x²。下面是独立的一份完整程序(存成ch36_poly.cpp,注意变量名和 36.3 的c无关):

#include<Eigen/Dense>#include<iostream>#include<vector>intmain(){std::vector<double>xs={0,1,2,3,4,5};std::vector<double>ys={1.1,3.0,5.2,6.9,9.1,11.0};Eigen::Index n=xs.size();Eigen::MatrixXdA(n,3);// 三列:x²、x、1Eigen::VectorXdy(n);for(Eigen::Index i=0;i<n;i++){A(i,0)=xs[i]*xs[i];// x²A(i,1)=xs[i];// xA(i,2)=1.0;// 常数项y(i)=ys[i];}Eigen::Vector3d c=A.colPivHouseholderQr().solve(y);// c = [a, b, c]std::cout<<"二次拟合:y = "<<c(0)<<" * x^2 + "<<c(1)<<" * x + "<<c(2)<<std::endl;std::cout<<"残差 = "<<(A*c-y).norm()<<std::endl;return0;}

实际输出:

二次拟合:y = 0 * x^2 + 1.98571 * x + 1.08571 残差 = 0.226779

这个结果很有意思:二次项系数是0,残差和直线拟合一模一样(0.226779)。原因是这批数据本来就是直线,你给它更高次的模型,它也只会把多余的那项压成 0。

把ys改成{0, 1, 4, 9, 16, 25}(正好是 y = x²)再跑一次,实测输出:

二次拟合:y = 1 * x^2 + -5.21706e-15 * x + 7.14981e-15 残差 = 1.39037e-14

二次项变成 1,一次项和常数项是 1e-15 量级(也就是 0),残差 1e-14(也就是 0)——模型正确识别出了真实关系。

思路完全一样:把"特征"塞进设计矩阵的列,就能拟合任意线性模型。这正是机器学习的核心思路——特征 + 最小二乘。

36.6 为什么这是机器学习基础

最小二乘拟合 = 机器学习里线性回归的 C++ 实现:

  • 设计矩阵 A = 特征矩阵 X
  • 待求参数 c = 模型权重 w
  • 观测值 y = 标签

学会这个,你就理解了线性回归、岭回归、甚至神经网络最后一层的数学本质。

36.7 小结

  • 最小二乘:让误差平方和最小的拟合。
  • 写成矩阵 Ax = y,用 QR 分解求解。
  • 残差范数评估拟合质量。
  • 扩展设计矩阵就能做多项式拟合 = 线性回归。

下一章,第二个实战项目:3D 点云与刚体变换。


练习题

  1. 用上面的代码拟合给定数据,打印斜率和截距。
  2. 自己造一批数据(y = 3x + 2 加随机噪声),拟合看能不能还原。
  3. 把拟合从直线改成二次曲线(扩展设计矩阵)。
  4. 计算并打印拟合的残差。
  5. 说说最小二乘和机器学习线性回归的关系。

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

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

立即咨询