在上一课中,我们使用 scikit learn 的实现计算了糖尿病数据集的最小二乘线性回归。 今天,我们将看看如何编写自己的实现。
起步
from sklearn import datasets, linear_model, metricsfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import PolynomialFeaturesimport math, scipy, numpy as npfrom scipy import linalgnp.set_printoptions(precision=6)data = datasets.load_diabetes()feature_names=['age', 'sex', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']trn,test,y_trn,y_test = train_test_split(data.data, data.target, test_size=0.2)trn.shape, test.shape# ((353, 10), (89, 10))def regr_metrics(act, pred):return (math.sqrt(metrics.mean_squared_error(act, pred)),metrics.mean_absolute_error(act, pred))
sklearn 如何实现它
sklearn 是如何做到这一点的? 通过检查源代码,你可以看到在密集的情况下,它调用scipy.linalg.lstqr,它调用 LAPACK 方法:
选项是
'gelsd','gelsy','gelss'。默认值'gelsd'是个好的选择,但是,'gelsy'在许多问题上更快一些。'gelss'由于历史原因而使用。它通常更慢但是使用更少内存。
Scipy 稀疏最小二乘
我们不会详细介绍稀疏版本的最小二乘法。如果你有兴趣,请参考以下信息:
Scipy 稀疏最小二乘使用称为 Golub 和 Kahan 双对角化的迭代方法。
Scipy 稀疏最小二乘源代码:预处理是减少迭代次数的另一种方法。如果有可能有效地求解相关系统M*x = b,其中M以某种有用的方式近似A(例如,M-A具有低秩或其元素相对于A的元素较小),则 LSQR 可以在系统A*M(inverse)*z = b更快地收敛。之后可以通过求解M * x = z来恢复x。
如果A是对称的,则不应使用 LSQR!替代方案是对称共轭梯度法(cg)和/或 SYMMLQ。 SYMMLQ 是对称 cg 的一种实现,适用于任何对称的A,并且比 LSQR 更快收敛。如果A是正定的,则存在对称 cg 的其他实现,每次迭代需要的工作量比 SYMMLQ 少一些(但需要相同的迭代次数)。
linalg.lstqr
sklearn 实现为我们添加一个常数项(因为对于我们正在学习的直线,y截距可能不是 0)。 我们现在需要手工完成:
trn_int = np.c_[trn, np.ones(trn.shape[0])]test_int = np.c_[test, np.ones(test.shape[0])]
由于linalg.lstsq允许我们指定,我们想要使用哪个 LAPACK 例程,让我们尝试它们并进行一些时间比较:
%timeit coef, _,_,_ = linalg.lstsq(trn_int, y_trn, lapack_driver="gelsd")# 290 µs ± 9.24 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)%timeit coef, _,_,_ = linalg.lstsq(trn_int, y_trn, lapack_driver="gelsy")# 140 µs ± 91.7 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)%timeit coef, _,_,_ = linalg.lstsq(trn_int, y_trn, lapack_driver="gelss")# 199 µs ± 228 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
朴素解法
回想一下,我们想找到
,来最小化:

另一种思考方式是,我们对向量b最接近A的子空间(称为A的范围)的地方感兴趣。 这是b在A上的投影。由于
必须垂直于A的子空间,我们可以看到:

使用了
因为要相乘A和
的每列。
这让我们得到正规方程:

def ls_naive(A, b):return np.linalg.inv(A.T @ A) @ A.T @ b%timeit coeffs_naive = ls_naive(trn_int, y_trn)# 45.8 µs ± 4.65 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)coeffs_naive = ls_naive(trn_int, y_trn)regr_metrics(y_test, test_int @ coeffs_naive)# (57.94102134545707, 48.053565198516438)
正规方程(Cholesky)
正规方程:

如果A具有满秩,则伪逆
是正方形,埃尔米特正定矩阵。 解决这种系统的标准方法是 Cholesky 分解,它找到上三角形R,满足
。
以下步骤基于 Trefethen 的算法 11.1:
A = trn_intb = y_trnAtA = A.T @ AAtb = A.T @ b
警告:对于Cholesky,Numpy 和 Scipy 默认为不同的上/下三角。
R = scipy.linalg.cholesky(AtA)np.set_printoptions(suppress=True, precision=4)R'''array([[ 0.9124, 0.1438, 0.1511, 0.3002, 0.2228, 0.188 ,-0.051 , 0.1746, 0.22 , 0.2768, -0.2583],[ 0. , 0.8832, 0.0507, 0.1826, -0.0251, 0.0928,-0.3842, 0.2999, 0.0911, 0.15 , 0.4393],[ 0. , 0. , 0.8672, 0.2845, 0.2096, 0.2153,-0.2695, 0.3181, 0.3387, 0.2894, -0.005 ],[ 0. , 0. , 0. , 0.7678, 0.0762, -0.0077,0.0383, 0.0014, 0.165 , 0.166 , 0.0234],[ 0. , 0. , 0. , 0. , 0.8288, 0.7381,0.1145, 0.4067, 0.3494, 0.158 , -0.2826],[ 0. , 0. , 0. , 0. , 0. , 0.3735,-0.3891, 0.2492, -0.3245, -0.0323, -0.1137],[ 0. , 0. , 0. , 0. , 0. , 0. ,0.6406, -0.511 , -0.5234, -0.172 , -0.9392],[ 0. , 0. , 0. , 0. , 0. , 0. ,0. , 0.2887, -0.0267, -0.0062, 0.0643],[ 0. , 0. , 0. , 0. , 0. , 0. ,0. , 0. , 0.2823, 0.0636, 0.9355],[ 0. , 0. , 0. , 0. , 0. , 0. ,0. , 0. , 0. , 0.7238, 0.0202],[ 0. , 0. , 0. , 0. , 0. , 0. ,0. , 0. , 0. , 0. , 18.7319]])'''
检查我们的分解:
np.linalg.norm(AtA - R.T @ R)# 4.5140158187158533e-16

w = scipy.linalg.solve_triangular(R, Atb, lower=False, trans='T')
检查我们的结果是否符合预期总是好的:(以防我们输入错误的参数,函数没有返回我们想要的东西,或者有时文档甚至过时)。
np.linalg.norm(R.T @ w - Atb)# 1.1368683772161603e-13coeffs_chol = scipy.linalg.solve_triangular(R, w, lower=False)np.linalg.norm(R @ coeffs_chol - w)# 6.861429794408013e-14def ls_chol(A, b):R = scipy.linalg.cholesky(A.T @ A)w = scipy.linalg.solve_triangular(R, A.T @ b, trans='T')return scipy.linalg.solve_triangular(R, w)%timeit coeffs_chol = ls_chol(trn_int, y_trn)# 111 µs ± 272 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)coeffs_chol = ls_chol(trn_int, y_trn)regr_metrics(y_test, test_int @ coeffs_chol)# (57.9410213454571, 48.053565198516438)
QR 分解

def ls_qr(A,b):Q, R = scipy.linalg.qr(A, mode='economic')return scipy.linalg.solve_triangular(R, Q.T @ b)%timeit coeffs_qr = ls_qr(trn_int, y_trn)# 205 µs ± 264 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)coeffs_qr = ls_qr(trn_int, y_trn)regr_metrics(y_test, test_int @ coeffs_qr)# (57.94102134545711, 48.053565198516452)
SVD

SVD 给出伪逆。
def ls_svd(A,b):m, n = A.shapeU, sigma, Vh = scipy.linalg.svd(A, full_matrices=False)w = (U.T @ b)/ sigmareturn Vh.T @ w%timeit coeffs_svd = ls_svd(trn_int, y_trn)# 1.11 ms ± 320 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)%timeit coeffs_svd = ls_svd(trn_int, y_trn)# 266 µs ± 8.49 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)coeffs_svd = ls_svd(trn_int, y_trn)regr_metrics(y_test, test_int @ coeffs_svd)# (57.941021345457244, 48.053565198516687)
最小二乘回归的随机 Sketching 技巧
线性 Sketching(Woodruff)
- 抽取
r×n随机矩阵S,r << n - 计算
S A和S b - 找到回归
SA x = Sb的精确解x
时间比较
import timeitimport pandas as pddef scipylstq(A, b):return scipy.linalg.lstsq(A,b)[0]row_names = ['Normal Eqns- Naive','Normal Eqns- Cholesky','QR Factorization','SVD','Scipy lstsq']name2func = {'Normal Eqns- Naive': 'ls_naive','Normal Eqns- Cholesky': 'ls_chol','QR Factorization': 'ls_qr','SVD': 'ls_svd','Scipy lstsq': 'scipylstq'}m_array = np.array([100, 1000, 10000])n_array = np.array([20, 100, 1000])index = pd.MultiIndex.from_product([m_array, n_array], names=['# rows', '# cols'])pd.options.display.float_format = '{:,.6f}'.formatdf = pd.DataFrame(index=row_names, columns=index)df_error = pd.DataFrame(index=row_names, columns=index)# %%prunfor m in m_array:for n in n_array:if m >= n:x = np.random.uniform(-10,10,n)A = np.random.uniform(-40,40,[m,n]) # removed np.asfortranarrayb = np.matmul(A, x) + np.random.normal(0,2,m)for name in row_names:fcn = name2func[name]t = timeit.timeit(fcn + '(A,b)', number=5, globals=globals())df.set_value(name, (m,n), t)coeffs = locals()[fcn](abd82db15d4a3d91b8a4e4e2c71f025f)reg_met = regr_metrics(b, A @ coeffs)df_error.set_value(name, (m,n), reg_met[0])df
| # rows | 100 | 1000 | 10000 | ||||||
|---|---|---|---|---|---|---|---|---|---|
| # cols | 20 | 100 | 1000 | 20 | 100 | 1000 | 20 | 100 | 1000 |
| Normal Eqns- Naive | 0.001276 | 0.003634 | NaN | 0.000960 | 0.005172 | 0.293126 | 0.002226 | 0.021248 | 1.164655 |
| Normal Eqns- Cholesky | 0.001660 | 0.003958 | NaN | 0.001665 | 0.004007 | 0.093696 | 0.001928 | 0.010456 | 0.399464 |
| QR Factorization | 0.002174 | 0.006486 | NaN | 0.004235 | 0.017773 | 0.213232 | 0.019229 | 0.116122 | 2.208129 |
| SVD | 0.003880 | 0.021737 | NaN | 0.004672 | 0.026950 | 1.280490 | 0.018138 | 0.130652 | 3.433003 |
| Scipy lstsq | 0.004338 | 0.020198 | NaN | 0.004320 | 0.021199 | 1.083804 | 0.012200 | 0.088467 | 2.134780 |
df_error
| # rows | 100 | 1000 | 10000 | ||||||
|---|---|---|---|---|---|---|---|---|---|
| # cols | 20 | 100 | 1000 | 20 | 100 | 1000 | 20 | 100 | 1000 |
| Normal Eqns- Naive | 1.702742 | 0.000000 | NaN | 1.970767 | 1.904873 | 0.000000 | 1.978383 | 1.980449 | 1.884440 |
| Normal Eqns- Cholesky | 1.702742 | 0.000000 | NaN | 1.970767 | 1.904873 | 0.000000 | 1.978383 | 1.980449 | 1.884440 |
| QR Factorization | 1.702742 | 0.000000 | NaN | 1.970767 | 1.904873 | 0.000000 | 1.978383 | 1.980449 | 1.884440 |
| SVD | 1.702742 | 0.000000 | NaN | 1.970767 | 1.904873 | 0.000000 | 1.978383 | 1.980449 | 1.884440 |
| Scipy lstsq | 1.702742 | 0.000000 | NaN | 1.970767 | 1.904873 | 0.000000 | 1.978383 | 1.980449 | 1.884440 |
store = pd.HDFStore('least_squares_results.h5')store['df'] = df'''C:\Users\rache\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py:2881: PerformanceWarning:your performance may suffer as PyTables will pickle object types that it cannotmap directly to c-types [inferred_type->floating,key->block0_values] [items->[(100, 20), (100, 100), (100, 1000), (1000, 20), (1000, 100), (1000, 1000), (5000, 20), (5000, 100), (5000, 1000)]]exec(code_obj, self.user_global_ns, self.user_ns)'''
注解
我用魔术指令%prun来测量我的代码。
替代方案:最小绝对偏差(L1 回归)
- 异常值的敏感度低于最小二乘法。
- 没有闭式解,但可以通过线性规划解决。
条件作用和稳定性
条件数
条件数是一个指标,衡量输入的小变化导致输出变化的程度。
问题:为什么我们在数值线性代数中,关心输入的小变化的相关行为?
相对条件数由下式定义:

其中
是无穷小。
根据 Trefethen(第 91 页),如果κ很小(例如 1, 10, 10^2),问题是良态的,如果κ很大(例如10^6, 10^16),那么问题是病态的。
条件作用:数学问题的扰动行为(例如最小二乘)
稳定性:用于在计算机上解决该问题的算法的扰动行为(例如,最小二乘算法,householder,回代,高斯消除)
条件作用的例子
计算非对称矩阵的特征值的问题通常是病态的。
A = [[1, 1000], [0, 1]]B = [[1, 1000], [0.001, 1]]wA, vrA = scipy.linalg.eig(A)wB, vrB = scipy.linalg.eig(B)wA, wB'''(array([ 1.+0.j, 1.+0.j]),array([ 2.00000000e+00+0.j, -2.22044605e-16+0.j]))'''
矩阵的条件数
乘积
经常出现,它有自己的名字:A的条件数。注意,通常我们谈论问题的条件作用,而不是矩阵。
A的条件数涉及:
- 给定
Ax = b中的A和x,计算b - 给定
Ax = b中的A和b,计算x
未交待清楚的事情
完整和简化分解
SVD
来自 Trefethen 的图:
对于所有矩阵,QR 分解都存在
与 SVD 一样,有 QR 分解的完整版和简化版。
矩阵的逆是不稳定的
from scipy.linalg import hilbertn = 5hilbert(n)'''array([[ 1. , 0.5 , 0.3333, 0.25 , 0.2 ],[ 0.5 , 0.3333, 0.25 , 0.2 , 0.1667],[ 0.3333, 0.25 , 0.2 , 0.1667, 0.1429],[ 0.25 , 0.2 , 0.1667, 0.1429, 0.125 ],[ 0.2 , 0.1667, 0.1429, 0.125 , 0.1111]])'''n = 14A = hilbert(n)x = np.random.uniform(-10,10,n)b = A @ xA_inv = np.linalg.inv(A)np.linalg.norm(np.eye(n) - A @ A_inv)# 5.0516495470543212np.linalg.cond(A)# 2.2271635826494112e+17A @ A_inv'''array([[ 1. , 0. , -0.0001, 0.0005, -0.0006, 0.0105, -0.0243,0.1862, -0.6351, 2.2005, -0.8729, 0.8925, -0.0032, -0.0106],[ 0. , 1. , -0. , 0. , 0.0035, 0.0097, -0.0408,0.0773, -0.0524, 1.6926, -0.7776, -0.111 , -0.0403, -0.0184],[ 0. , 0. , 1. , 0.0002, 0.0017, 0.0127, -0.0273,0. , 0. , 1.4688, -0.5312, 0.2812, 0.0117, 0.0264],[ 0. , 0. , -0. , 1.0005, 0.0013, 0.0098, -0.0225,0.1555, -0.0168, 1.1571, -0.9656, -0.0391, 0.018 , -0.0259],[-0. , 0. , -0. , 0.0007, 1.0001, 0.0154, 0.011 ,-0.2319, 0.5651, -0.2017, 0.2933, -0.6565, 0.2835, -0.0482],[ 0. , -0. , 0. , -0.0004, 0.0059, 0.9945, -0.0078,-0.0018, -0.0066, 1.1839, -0.9919, 0.2144, -0.1866, 0.0187],[-0. , 0. , -0. , 0.0009, -0.002 , 0.0266, 0.974 ,-0.146 , 0.1883, -0.2966, 0.4267, -0.8857, 0.2265, -0.0453],[ 0. , 0. , -0. , 0.0002, 0.0009, 0.0197, -0.0435,1.1372, -0.0692, 0.7691, -1.233 , 0.1159, -0.1766, -0.0033],[ 0. , 0. , -0. , 0.0002, 0. , -0.0018, -0.0136,0.1332, 0.945 , 0.3652, -0.2478, -0.1682, 0.0756, -0.0212],[ 0. , -0. , -0. , 0.0003, 0.0038, -0.0007, 0.0318,-0.0738, 0.2245, 1.2023, -0.2623, -0.2783, 0.0486, -0.0093],[-0. , 0. , -0. , 0.0004, -0.0006, 0.013 , -0.0415,0.0292, -0.0371, 0.169 , 1.0715, -0.09 , 0.1668, -0.0197],[ 0. , -0. , 0. , 0. , 0.0016, 0.0062, -0.0504,0.1476, -0.2341, 0.8454, -0.7907, 1.4812, -0.15 , 0.0186],[ 0. , -0. , 0. , -0.0001, 0.0022, 0.0034, -0.0296,0.0944, -0.1833, 0.6901, -0.6526, 0.2556, 0.8563, 0.0128],[ 0. , 0. , 0. , -0.0001, 0.0018, -0.0041, -0.0057,-0.0374, -0.165 , 0.3968, -0.2264, -0.1538, -0.0076, 1.005 ]])'''row_names = ['Normal Eqns- Naive','QR Factorization','SVD','Scipy lstsq']name2func = {'Normal Eqns- Naive': 'ls_naive','QR Factorization': 'ls_qr','SVD': 'ls_svd','Scipy lstsq': 'scipylstq'}pd.options.display.float_format = '{:,.9f}'.formatdf = pd.DataFrame(index=row_names, columns=['Time', 'Error'])for name in row_names:fcn = name2func[name]t = timeit.timeit(fcn + '(A,b)', number=5, globals=globals())coeffs = locals()[fcn](abd82db15d4a3d91b8a4e4e2c71f025f)df.set_value(name, 'Time', t)df.set_value(name, 'Error', regr_metrics(b, A @ coeffs)[0])
SVD 在这里最好
不要重新运行。
df
| Time | Error | |
|---|---|---|
| Normal Eqns- Naive | 0.001334339 | 3.598901966 |
| QR Factorization | 0.002166139 | 0.000000000 |
| SVD | 0.001556937 | 0.000000000 |
| Scipy lstsq | 0.001871590 | 0.000000000 |
即使A是稀疏的,
通常是密集的。对于大型矩阵,
放不进内存。
运行时间
- 矩阵求逆:

- 矩阵乘法:

- Cholesky:

- QR,Gram Schmidt:
,
(Trefethen 第 8 章) - QR,Householder:2
(Trefethen 第 10 章) - 求解三角形系统:

为什么 Cholesky 较快:

QR 最优的一个案例
m=100n=15t=np.linspace(0, 1, m)# 范德蒙矩阵A=np.stack([t**i for i in range(n)], 1)b=np.exp(np.sin(4*t))# 这将使解决方案标准化为 1b /= 2006.787453080206from matplotlib import pyplot as plt%matplotlib inlineplt.plot(t, b)# [<matplotlib.lines.Line2D at 0x7fdfc1fa7eb8>]

检查我们得到了 1:
1 - ls_qr(A, b)[14]# 1.4137685733217609e-07
不好的条件数:
kappa = np.linalg.cond(A); kappa# 5.827807196683593e+17row_names = ['Normal Eqns- Naive','QR Factorization','SVD','Scipy lstsq']name2func = {'Normal Eqns- Naive': 'ls_naive','QR Factorization': 'ls_qr','SVD': 'ls_svd','Scipy lstsq': 'scipylstq'}pd.options.display.float_format = '{:,.9f}'.formatdf = pd.DataFrame(index=row_names, columns=['Time', 'Error'])for name in row_names:fcn = name2func[name]t = timeit.timeit(fcn + '(A,b)', number=5, globals=globals())coeffs = locals()[fcn](abd82db15d4a3d91b8a4e4e2c71f025f)df.set_value(name, 'Time', t)df.set_value(name, 'Error', np.abs(1 - coeffs[-1]))df
| Time | Error | |
|---|---|---|
| Normal Eqns- Naive | 0.001565099 | 1.357066025 |
| QR Factorization | 0.002632104 | 0.000000116 |
| SVD | 0.003503785 | 0.000000116 |
| Scipy lstsq | 0.002763502 | 0.000000116 |
通过正规方程求解最小二乘的解决方案通常是不稳定的,尽管对于小条件数的问题是稳定的。
低秩
m = 100n = 10x = np.random.uniform(-10,10,n)A2 = np.random.uniform(-40,40, [m, int(n/2)]) # removed np.asfortranarrayA = np.hstack([A2, A2])A.shape, A2.shape# ((100, 10), (100, 5))b = A @ x + np.random.normal(0,1,m)row_names = ['Normal Eqns- Naive','QR Factorization','SVD','Scipy lstsq']name2func = {'Normal Eqns- Naive': 'ls_naive','QR Factorization': 'ls_qr','SVD': 'ls_svd','Scipy lstsq': 'scipylstq'}pd.options.display.float_format = '{:,.9f}'.formatdf = pd.DataFrame(index=row_names, columns=['Time', 'Error'])for name in row_names:fcn = name2func[name]t = timeit.timeit(fcn + '(A,b)', number=5, globals=globals())coeffs = locals()[fcn](abd82db15d4a3d91b8a4e4e2c71f025f)df.set_value(name, 'Time', t)df.set_value(name, 'Error', regr_metrics(b, A @ coeffs)[0])df
| Time | Error | |
|---|---|---|
| Normal Eqns- Naive | 0.001227640 | 300.658979382 |
| QR Factorization | 0.002315920 | 0.876019803 |
| SVD | 0.001745647 | 1.584746056 |
| Scipy lstsq | 0.002067989 | 0.804750398 |
比较
比较我们的结果和上面:
df
| # rows | 100 | 1000 | 10000 | ||||||
|---|---|---|---|---|---|---|---|---|---|
| # cols | 20 | 100 | 1000 | 20 | 100 | 1000 | 20 | 100 | 1000 |
| Normal Eqns- Naive | 0.001276 | 0.003634 | NaN | 0.000960 | 0.005172 | 0.293126 | 0.002226 | 0.021248 | 1.164655 |
| Normal Eqns- Cholesky | 0.001660 | 0.003958 | NaN | 0.001665 | 0.004007 | 0.093696 | 0.001928 | 0.010456 | 0.399464 |
| QR Factorization | 0.002174 | 0.006486 | NaN | 0.004235 | 0.017773 | 0.213232 | 0.019229 | 0.116122 | 2.208129 |
| SVD | 0.003880 | 0.021737 | NaN | 0.004672 | 0.026950 | 1.280490 | 0.018138 | 0.130652 | 3.433003 |
| Scipy lstsq | 0.004338 | 0.020198 | NaN | 0.004320 | 0.021199 | 1.083804 | 0.012200 | 0.088467 | 2.134780 |
来自 Trefethen(第 84 页):
正规方程式/ Cholesky 在生效时速度最快。 Cholesky 只能用于对称正定矩阵。 此外,对于具有高条件数或具有低秩的矩阵,正规方程/ Cholesky 是不稳定的。
数值分析师推荐通过 QR 进行线性回归,作为多年的标准方法。 它自然,优雅,适合“日常使用”。
