把全书知识串起来的第一个实战项目:用 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.0857136.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 点云与刚体变换。
练习题
- 用上面的代码拟合给定数据,打印斜率和截距。
- 自己造一批数据(y = 3x + 2 加随机噪声),拟合看能不能还原。
- 把拟合从直线改成二次曲线(扩展设计矩阵)。
- 计算并打印拟合的残差。
- 说说最小二乘和机器学习线性回归的关系。