
1. 项目概述从“拟合”到“逼近”的工程思维在工程和科学计算的无数场景里我们常常会面对一堆看似杂乱无章的数据点。比如你手头有一组实验测得的不同温度下某材料的电阻值或者记录了某个APP在不同用户量下的服务器响应时间。这些数据点散落在坐标图上我们迫切地想知道它们背后隐藏的规律——那条能“代表”它们整体趋势的曲线。这个寻找规律的过程就是“拟合”。而“最小二乘法”正是解决这类拟合问题最经典、最强大也是工程实践中最常用的数学工具之一。它不追求曲线完美穿过每一个点这在有误差的实测数据中既不现实也无必要而是追求一个整体上的“最优妥协”让所有数据点到拟合曲线的垂直距离即误差的平方和最小。这个“平方和最小”的思想就是“最小二乘”的核心。我当年在西电做这个计算方法大作业时最初也以为这只是一个数学公式的编程实现。但真正动手后才发现它远不止于此。从理解问题背景、选择拟合模型是线性、多项式还是指数到处理可能出现的“病态”方程组再到评估拟合效果的好坏每一步都充满了工程权衡的智慧。这个作业的精髓不在于你调用了某个库里的polyfit函数而在于你是否亲手实现了从构建法方程到求解的完整流程并理解了每一步背后的“为什么”。今天我就结合当年的经验和后续的工程实践把这个“最小二乘法”大作业拆解透彻让你不仅能交出一份漂亮的代码更能掌握一种解决实际问题的核心思路。2. 核心原理与数学模型拆解2.1 问题定义与损失函数构建我们先抛开数学符号用最直白的话说清楚最小二乘法要干什么。假设我们有n个观测数据点(x_i, y_i), i1,2,...,n。我们认为y和x之间存在某种函数关系y f(x, β)其中β是一组待确定的参数。例如对于最简单的线性拟合f(x, β) β_0 β_1 * x这里β_0和β_1就是待求的截距和斜率。由于测量误差、模型简化等原因模型预测值f(x_i, β)和实际观测值y_i之间必然存在偏差我们称之为残差r_i y_i - f(x_i, β)。最小二乘法的目标非常直观找到一组参数β使得所有残差的平方和S(β)达到最小。这个S(β)就是我们的损失函数S(β) Σ (y_i - f(x_i, β))^2 Σ r_i^2为什么是“平方和”而不是简单的“绝对值和”这里有两个关键考量一是数学上便于处理平方函数处处可导便于我们使用求导这一强大工具来寻找极值点二是它对大的误差给予更重的惩罚这通常更符合我们对“大误差更不可接受”的直观认知。当然也有绝对值和最小L1范数的方法但那属于另一类“稳健回归”的范畴计算上更复杂一些。2.2 线性最小二乘与法方程推导当拟合模型f(x, β)是关于待定参数β的线性函数时我们称之为线性最小二乘问题。这是最常见、也最重要的情况。除了上面的一次线性模型多项式拟合y β_0 β_1*x β_2*x^2 ... β_m*x^m也属于线性最小二乘因为它是关于参数β_0, β_1, ..., β_m线性的。我们的目标是最小化S(β)。对于线性模型我们可以将其写成一个漂亮的矩阵形式。令设计矩阵X为n x (m1)的矩阵其第i行是[1, x_i, x_i^2, ..., x_i^m]参数向量β [β_0, β_1, ..., β_m]^T观测值向量y [y_1, y_2, ..., y_n]^T。则模型可以写成y ≈ Xβ残差向量r y - Xβ损失函数S(β) ||r||^2 (y - Xβ)^T (y - Xβ)。为了求S(β)的最小值我们对其求关于β的梯度并令其为零向量。经过推导这是作业中必须自己推导一遍的关键步骤我们得到著名的法方程(X^T X) β X^T y这是一个关于β的线性方程组。解这个方程组我们就得到了使得平方和最小的参数估计值β_hat (X^T X)^{-1} X^T y。注意这里隐藏了一个重要的前提——矩阵X^T X必须是可逆的。这在大多数情况下成立但如果你的数据点x值选取不当例如所有点几乎在一条垂直线上做线性拟合或者多项式阶数m过高接近数据点数量n就可能导致X列线性相关使得X^T X奇异或病态。这是实操中第一个要警惕的坑。2.3 非线性最小二乘简介如果模型f(x, β)关于参数β是非线性的例如指数衰减模型y β_0 * exp(-β_1 * x)问题就变成了非线性最小二乘。此时无法直接推导出封闭解必须采用迭代数值方法如高斯-牛顿法、列文伯格-马夸尔特法等。这些方法的核心思想是在当前参数估计值附近将非线性模型局部线性化然后求解一个线性最小二乘子问题来更新参数如此迭代直至收敛。在大作业中通常会先聚焦于线性最小二乘但了解非线性情况的存在和基本思路能让你对方法的适用范围有更完整的认识。3. 算法实现的关键步骤与代码解析理解了原理我们来看如何用代码实现。这里我以多项式拟合为例展示从零实现的核心流程。虽然Python的NumPy有现成的np.polyfit但自己实现一遍才能真正吃透。3.1 数据准备与预处理任何数据分析的第一步都是看数据。你需要加载或生成数据并快速可视化用散点图观察其大致趋势这是选择拟合模型阶数的基础。import numpy as np import matplotlib.pyplot as plt # 示例生成带噪声的二次多项式数据 np.random.seed(42) x np.linspace(0, 10, 20) y_true 2.5 1.3 * x - 0.2 * x**2 # 真实模型y 2.5 1.3x - 0.2x^2 noise np.random.normal(0, 1.5, x.shape) # 加入高斯噪声 y_obs y_true noise # 可视化原始数据 plt.figure(figsize(10, 6)) plt.scatter(x, y_obs, labelObserved Data, colorblue, alpha0.6) plt.plot(x, y_true, labelTrue Model, colorred, linestyle--) plt.xlabel(X) plt.ylabel(Y) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.title(Raw Data Visualization) plt.show()这一步至关重要。通过看图你能直观判断用线性拟合是否足够还是需要二次或更高次项。同时检查数据中是否有明显的异常点离群点传统的普通最小二乘法对异常点非常敏感一个异常点就可能把整个拟合线“拉偏”。3.2 构建设计矩阵与法方程求解假设我们通过观察决定尝试用m阶多项式进行拟合。接下来就是构建设计矩阵X。def build_design_matrix(x, degree): 构建多项式拟合的设计矩阵。 参数: x: 一维数组自变量数据。 degree: 多项式阶数。 返回: X: 设计矩阵形状为 (len(x), degree1)。 n len(x) X np.ones((n, degree 1)) # 第一列全为1对应常数项β0 for i in range(1, degree 1): X[:, i] x ** i return X def solve_least_squares(X, y): 通过解法方程 (X^T X) β X^T y 求解最小二乘参数。 参数: X: 设计矩阵。 y: 观测值向量。 返回: beta: 拟合参数向量。 # 计算 X^T X 和 X^T y XTX np.dot(X.T, X) XTy np.dot(X.T, y) # 解法方程。使用np.linalg.solve而不是直接求逆数值上更稳定。 try: beta np.linalg.solve(XTX, XTy) except np.linalg.LinAlgError: print(警告X^T X 矩阵奇异或病态尝试使用伪逆。) # 当矩阵奇异时使用伪逆Moore-Penrose pseudoinverse作为后备方案 beta np.linalg.pinv(XTX) XTy return beta # 假设我们选择2阶多项式拟合 degree 2 X_design build_design_matrix(x, degree) beta_estimated solve_least_squares(X_design, y_obs) print(fEstimated parameters (β0, β1, β2): {beta_estimated})实操心得直接计算(X^T X)^{-1} X^T y在理论推导中很清晰但在数值计算中应尽量避免显式求逆矩阵inv(X^T X)。因为求逆运算不仅计算量大而且在X^T X病态条件数大时会放大舍入误差导致结果极不稳定。np.linalg.solve是求解线性方程组的专用函数它采用LU分解等更稳定的数值算法是更优的选择。只有当方程组可能奇异时才将伪逆作为兜底方案。3.3 模型评估与效果可视化得到参数后我们需要评估拟合效果。最直接的指标是残差平方和和决定系数 R²。def evaluate_fit(X, beta, y_obs): 评估拟合效果。 参数: X: 设计矩阵。 beta: 拟合参数。 y_obs: 观测值。 返回: y_pred: 预测值。 residuals: 残差。 r_squared: 决定系数 R²。 y_pred np.dot(X, beta) residuals y_obs - y_pred ss_res np.sum(residuals**2) # 残差平方和 ss_tot np.sum((y_obs - np.mean(y_obs))**2) # 总平方和 r_squared 1 - (ss_res / ss_tot) if ss_tot ! 0 else 0 return y_pred, residuals, r_squared, ss_res y_pred, residuals, r_squared, ss_res evaluate_fit(X_design, beta_estimated, y_obs) print(fResidual Sum of Squares (RSS): {ss_res:.4f}) print(fCoefficient of Determination (R²): {r_squared:.4f})R² 越接近1说明模型对数据的解释能力越强。但要注意盲目增加多项式阶数总能提高 R²甚至达到1即完美穿过所有点但这会导致过拟合——模型不仅拟合了趋势还拟合了噪声在新数据上表现会很差。最后将拟合曲线画出来与原始数据对比这是最直观的检验。# 生成平滑曲线用于绘制拟合结果 x_smooth np.linspace(x.min(), x.max(), 300) X_smooth build_design_matrix(x_smooth, degree) y_smooth np.dot(X_smooth, beta_estimated) plt.figure(figsize(12, 5)) # 子图1拟合曲线与数据 plt.subplot(1, 2, 1) plt.scatter(x, y_obs, labelObserved Data, alpha0.6) plt.plot(x_smooth, y_smooth, labelfFitted Curve (degree{degree}), colorgreen, linewidth2) plt.plot(x, y_true, labelTrue Model, colorred, linestyle--, alpha0.8) plt.xlabel(X) plt.ylabel(Y) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.title(fPolynomial Fit (R²{r_squared:.3f})) # 子图2残差分析图 plt.subplot(1, 2, 2) plt.scatter(y_pred, residuals, alpha0.6) plt.axhline(y0, colorr, linestyle--) plt.xlabel(Predicted Y) plt.ylabel(Residuals) plt.grid(True, linestyle--, alpha0.5) plt.title(Residual Plot) plt.tight_layout() plt.show()残差图是诊断模型好坏的重要工具。一个好的拟合其残差应该随机、均匀地分布在0线上下没有明显的模式如曲线、漏斗形。如果残差图显示出规律性说明模型可能遗漏了某个重要的解释变量或函数形式。4. 工程实践中的进阶问题与解决方案课堂作业的数据往往是“干净”的但真实世界的数据要复杂得多。实现基础算法只是第一步要让最小二乘法在工程中真正可靠必须处理以下几个进阶问题。4.1 病态问题与正则化当设计矩阵X的列之间存在近似线性关系时例如多项式拟合中高阶项与低阶项高度相关X^T X的条件数会变得非常大成为病态矩阵。此时微小的数据扰动会导致解β发生巨大变化结果极不稳定。解决方案中心化与标准化对于多项式拟合先将自变量x中心化减去均值甚至标准化再除以标准差可以显著降低设计矩阵的列间相关性改善条件数。这是最推荐的首选预处理步骤。x_mean np.mean(x) x_centered x - x_mean # 然后用 x_centered 去构建设计矩阵使用正交多项式构建一组关于数据点正交的多项式基函数这样设计矩阵的列就是正交的X^T X是对角阵从根本上避免了病态。这是数值计算的标准做法之一。正则化岭回归当问题不可避免病态时可以引入正则化项。最常用的是Tikhonov正则化岭回归它将损失函数修改为S(β) ||y - Xβ||^2 λ||β||^2其中λ 0是正则化参数。对应的法方程变为(X^T X λI) β X^T y。增加的对角元λI像给矩阵加了一个“稳定器”强制改善其条件数得到的解β虽然是有偏的但方差大大减小稳定性显著提升。λ的选择需要借助交叉验证等方法。4.2 模型选择与过拟合防范如何确定多项式的最佳阶数m阶数太低欠拟合无法捕捉数据特征阶数太高过拟合模型泛化能力差。解决方案可视化判断画出不同阶数的拟合曲线观察其是否开始“疯狂摆动”去贴合噪声点。信息准则使用如赤池信息准则或贝叶斯信息准则。它们在衡量模型拟合优度的同时加入了对于参数数量的惩罚倾向于选择简洁且拟合良好的模型。计算AIC 2k n * log(RSS/n)其中k是参数个数n是样本数选择AIC最小的模型。交叉验证将数据分为训练集和验证集或使用K折交叉验证。用训练集拟合不同阶数的模型然后在验证集上计算误差如均方误差。选择在验证集上误差最小的模型阶数。这是工程上最可靠的方法。def k_fold_cross_validation(x, y, max_degree, k5): 使用K折交叉验证选择最佳多项式阶数。 n len(x) indices np.arange(n) np.random.shuffle(indices) fold_size n // k mse_cv np.zeros(max_degree 1) # 存储每个阶数的平均MSE for degree in range(max_degree 1): mse_list [] for fold in range(k): # 划分训练集和验证集 val_idx indices[fold*fold_size : (fold1)*fold_size] train_idx np.setdiff1d(indices, val_idx) x_train, y_train x[train_idx], y[train_idx] x_val, y_val x[val_idx], y[val_idx] # 训练模型 X_train build_design_matrix(x_train, degree) beta solve_least_squares(X_train, y_train) # 验证模型 X_val build_design_matrix(x_val, degree) y_val_pred X_val beta mse np.mean((y_val - y_val_pred)**2) mse_list.append(mse) mse_cv[degree] np.mean(mse_list) print(fDegree {degree}: Average CV MSE {mse_cv[degree]:.4f}) best_degree np.argmin(mse_cv) print(f\nBest polynomial degree based on {k}-fold CV: {best_degree}) return best_degree, mse_cv4.3 加权最小二乘与异方差性在普通最小二乘中我们默认所有数据点的误差方差是相同的同方差性。但现实中误差方差可能随x变化异方差性。例如测量仪器的精度可能随量程变化。此时普通最小二乘虽然仍是无偏估计但不再是“最优”的方差不是最小。解决方案使用加权最小二乘。为每个数据点赋予一个权重w_i通常取为误差方差估计值的倒数。损失函数变为S(β) Σ w_i * (y_i - f(x_i, β))^2。对应的法方程为(X^T W X) β X^T W y其中W是对角权重矩阵。在实践中权重往往需要通过残差分析来迭代估计。5. 从作业到实战常见陷阱与调试技巧即使理论清晰代码写起来还是会遇到各种“坑”。下面是我总结的几个典型问题及排查思路。5.1 数值不稳定与溢出问题问题现象当多项式阶数较高如15阶以上或x值较大时计算x^i可能导致数值溢出inf或者X^T X的条件数极大求解结果出现NaN或巨大异常值。排查与解决中心化/标准化如前所述这是必须做的。将x映射到[-1, 1]或零均值附近能极大缓解数值问题。检查条件数求解前计算np.linalg.cond(X.T X)。如果条件数大于1e10就要高度警惕病态问题。使用QR分解或SVD求解解法方程(X^T X) β X^T y是“正规方程法”数值稳定性较差。更稳健的方法是直接对设计矩阵X进行QR分解或奇异值分解。QR分解将X分解为正交矩阵Q和上三角矩阵R则原问题min ||y - Xβ||等价于min ||Q^T y - R β||由于R是上三角阵可以通过回代法稳定求解。SVD分解将X分解为U Σ V^T则解为β V Σ^{-1} U^T y。SVD能自动处理秩亏的情况是最稳定的方法。NumPy中可以用np.linalg.lstsq它默认使用的就是SVD。# 使用NumPy的稳健求解器 (基于SVD) beta_svd, residuals_rank, rank, s np.linalg.lstsq(X_design, y_obs, rcondNone) print(fSolution via SVD (np.linalg.lstsq): {beta_svd})踩坑实录我曾用正规方程法拟合一个20阶的多项式结果完全错误。改用SVD后问题立刻解决。自此之后对于任何可能病态的问题我的首选都是np.linalg.lstsq或显式调用SVD/QR。5.2 拟合曲线“端点狂舞”问题问题现象在多项式拟合尤其是高次拟合时拟合曲线在数据范围的两端端点外可能会出现剧烈震荡或飞升这与龙格现象有关。原因与解决多项式在区间端点附近本身就不稳定。工程上严禁使用拟合模型对训练数据范围之外的点进行预测。如果你需要外推必须非常谨慎并辅以其他领域知识进行合理性判断。对于区间内的拟合可以考虑使用样条插值/拟合它将全局拟合分解为分段低阶多项式拟合能有效避免高阶多项式带来的全局震荡。5.3 结果与现成库函数对不上问题现象自己实现的结果与np.polyfit或sklearn的LinearRegression结果在小数点后几位有细微差别。排查步骤检查是否中心化np.polyfit在拟合高阶多项式时内部会先对x进行缩放以提高数值稳定性。确保你自己的实现也做了同样的预处理。检查求解方法确认对方程组的求解方法是否一致正规方程、QR、SVD。检查截距项sklearn.linear_model.LinearRegression默认包含截距项。如果你自己构建的设计矩阵第一列不是全1或者用了fit_interceptFalse结果就会不同。精度问题浮点数计算本身就有微小的舍入误差不同计算顺序可能导致最后几位不同只要相对误差在1e-10量级以内通常可以认为是数值计算误差无需担心。5.4 大作业报告撰写要点如果你需要为这个大作业撰写报告或论文除了展示代码和结果以下几点能显著提升质量理论部分清晰推导法方程说明最小二乘的原理。实验设计用可控的合成数据如指定参数的真模型可控噪声进行测试验证你实现的正确性。再找一组真实数据或课程提供的标准数据进行应用。对比分析比较不同多项式阶数的拟合效果画图、列R²、残差图。分析欠拟合和过拟合现象。进阶探索尝试实现一种改进方法如岭回归并展示其对病态问题的改善效果。或者对比正规方程法、QR分解、SVD分解在数值稳定性上的差异。结论与思考总结最小二乘法的优缺点讨论其在工程应用中的局限性如对异常值敏感、线性假设等并简要提及可能的扩展方向如稳健回归、非线性最小二乘。实现最小二乘法就像掌握了一把瑞士军刀。它基础但绝不简单。从理解其“误差平方和最小”的朴素思想到应对数值稳定的各种技巧再到防范过拟合的模型选择策略每一步都体现了数学理论与工程实践的紧密结合。这个西电的大作业真正有价值的不是那几行求解方程组的代码而是在实现过程中你被迫去思考、去调试、去优化的完整问题解决链条。当你下次再面对一堆散乱的数据时希望你能自信地拿起最小二乘这把工具并且清楚地知道它的刀刃在哪里它的局限又在何处。