10 · 数值计算与稳定性
理论上成立的公式,在浮点数计算机上可能算出错得离谱的结果。本节讲为什么代码要这么写。
理论上成立的公式,在浮点数计算机上可能算出错得离谱的结果。本节讲「为什么代码要这么写」,是从理论走向实践的必修补。
一、浮点误差
计算机用有限位数表示实数,每步运算都有微小舍入误差。单步无所谓,但成千上万步累积、或者遇到「坏」的矩阵时,误差会被放大到头重脚轻。
二、条件数:误差被放大多少倍
第 6 节定义过:。它是「输入相对误差」被放大到「输出相对误差」的最大倍数:
- :良态(well-conditioned),结果可靠。
- 很大(如 ):病态(ill-conditioned),输入动一点点,解就面目全非。
三、为什么不要显式求逆
这是初学者最常犯的错。inv(A) @ b 比 solve(A, b) 又慢又不准:
- 求逆要解 n 个方程系统,计算量更大;
- 逆矩阵本身引入额外舍入误差,再乘一次又放大一次。
原则:能用 solve / 分解就不要求逆。 理论里写 ,代码里写 np.linalg.solve(A, b)。
四、正则化:给病态问题上保险
当 病态或不可逆(特征多重共线性),加一个 :
这就是岭回归(Ridge)。几何上,加 把所有特征值抬高了 λ,最小特征值不再贴近 0,条件数下降,解变得稳定。L2 正则化控制过拟合的本质,就是这个数值稳定性的改善。
五、代码:看条件数怎么毁掉结果
import numpy as np
# 构造一个病态矩阵(希尔伯特矩阵)
n = 8
H = np.array([[1.0/(i+j+1) for j in range(n)] for i in range(n)])
print("条件数 =", f"{np.linalg.cond(H):.1e}") # 极大,高度病态
x_true = np.ones(n)
b = H @ x_true
x_solve = np.linalg.solve(H, b)
print("solve 误差 =", np.linalg.norm(x_solve - x_true))
# 加正则化(岭回归)后变稳
lam = 1e-3
x_ridge = np.linalg.solve(H.T@H + lam*np.eye(n), H.T@b)
print("岭回归误差 =", np.linalg.norm(x_ridge - x_true))
# 错误示范:显式求逆(不要学)
x_bad = np.linalg.inv(H) @ b
print("显式求逆误差 =", np.linalg.norm(x_bad - x_true))
六、工程速记(抓住这几条就够用)
- 解方程用
solve/分解,不要inv。 - 最小二乘用
lstsq或 QR,不要手搭正规方程(条件数会被平方)。 - 担心病态就看
np.linalg.cond;偏大就加正则化。 - 对称正定用 Cholesky;要主方向用 SVD/特征分解。
七、动手想一想
- 为什么“手搭正规方程 ”比 QR 数值差?
- 深度网络的权重初始化为什么偏爱正交矩阵?
答案:(1) 的条件数是 的平方,原本稍差的矩阵会变得更坏;QR 直接处理 ,不平方条件数。(2) 正交矩阵条件数为 1,反复相乘不放大也不衰减信号,缓解梯度爆炸/消失。