最小二乘法C++线性回归求解器可在50行内实现:用std::vector存数据,单遍计算n、sum_x、sum_y、sum_xx、sum_xy;检查分母denom避免除零和浮点误差;过滤NaN/Inf;中心化x提升数值稳定性;计算R²并处理ss_tot=0的边界情况。

用最小二乘法手写 C++ 线性回归求解器
直接上核心结论:不需要第三方库,用 std::vector 存数据、手动推导并实现最小二乘闭式解(即 y = a * x + b 中的斜率 a 和截距 b),50 行内可跑通。关键不是“能不能”,而是“怎么避免浮点误差和除零”。
公式本身很简单:a = (n*sum_xy - sum_x*sum_y) / (n*sum_xx - sum_x*sum_x)b = (sum_y - a*sum_x) / n
但实际写的时候,n*sum_xx - sum_x*sum_x 可能接近 0(所有 x 几乎相等),必须检查。
- 先用
std::vector<double></double>分别存x和y数据点,确保长度一致且 ≥2 - 遍历一次计算五个累加量:
n、sum_x、sum_y、sum_xx、sum_xy(不要分开多次遍历,减少 cache miss) - 计算分母
denom = n * sum_xx - sum_x * sum_x,如果std::abs(denom) ,说明 <code>x几乎无变化,此时无法拟合斜率,返回a = 0.0,b = mean(y) - 否则按公式算出
a和b,注意用double运算,别隐式转成int
为什么不用 Eigen 或 Armadillo?
因为真实场景中,你可能只在嵌入式设备、竞赛环境或遗留系统里跑单次小规模回归——连 #include <eigen3/Eigen/Dense> 都不被允许。Eigen 编译慢、模板膨胀大,Armadillo 依赖 BLAS。而手写最小二乘,逻辑清晰、无依赖、体积可控。
但要注意:
– 如果后续要扩展到多元线性回归(多个特征),手写矩阵求逆就容易出数值问题,那时才该切到 Eigen 的 householderQr().solve()
– 单变量下,手写比调用 std::valarray 或 Boost.Accumulators 更轻量、更可控
常见错误:输入数据没做基本校验
用户常把空向量、单个点、含 NaN 或 inf 的数据直接喂进去,结果得到 nan 或崩溃。这不是算法问题,是接口契约没守好。
- 构造函数或求解函数入口处,先检查
x.size() == y.size()且x.size() >= 2 - 用
std::isfinite(x[i]) && std::isfinite(y[i])过滤非法值,或抛出std::invalid_argument - 避免用
std::accumulate直接累加——它不检查NaN;自己写循环,在累加前判断 - 若业务允许,可加一个布尔参数
drop_invalid = true,默认丢弃异常点,而不是中断
如何验证结果是否合理?
算完 a 和 b 后,别急着返回。至少做两件事:
立即学习“C++免费学习笔记(深入)”;
- 用原始数据算一次残差平方和:
ss_res = Σ(y_i - (a*x_i + b))²,如果太大(比如比var(y)还大),说明拟合失败或数据本身无相关性 - 打印或返回
r_squared = 1.0 - ss_res / ss_tot(其中ss_tot = Σ(y_i - mean_y)²),值在 [0,1] 外就一定有问题 - 特别注意:当所有
y相等时,ss_tot = 0,此时r_squared无定义,应单独返回std::numeric_limits<double>::quiet_NaN()</double>
最易被忽略的是:没有对输入做去量纲处理。虽然单变量线性回归对缩放不敏感(a 会自动调整),但如果 x 是时间戳(如 1672531200),而 y 是毫秒级延迟(如 0.012),中间计算 sum_xx 可能损失精度。稳妥做法是传入前做中心化:x_i -= mean_x,最后再把 b 平移回去。


















