6.13. 线性代数¶
摄像头上的线性代数都是小矩阵运算:将 IMU 采样融合到世界坐标系的 3x3 旋转、用于校正镜头的标定矩阵、卡尔曼滤波器的状态协方差更新、其正规方程可化为微型线性求解的多项式拟合。numpy.linalg 和 scipy.linalg 子模块正好覆盖了这种规模。
这些函数分布在两个模块中。矩阵乘积运算位于 numpy 顶层;分解和矩阵求逆位于 numpy.linalg 之下;专门的线性方程组求解器位于 scipy.linalg 之下。例如,一个 2x2 矩阵乘法:
from ulab import numpy as np
A = np.array([[1, 2], [3, 4]], dtype=np.float)
B = np.array([[5, 6], [7, 8]], dtype=np.float)
np.dot(A, B)
# array([[19.0, 22.0],
# [43.0, 50.0]])
6.13.1. 有哪些可用¶
dot()—— 矩阵或向量乘积。cross()—— 三维向量叉积。trace()—— 对角线之和。inv()—— 矩阵求逆。det()—— 行列式。cholesky()—— Cholesky 分解(输入须为对称正定矩阵)。eig()—— 实对称矩阵的特征值与特征向量。norm()—— 向量或矩阵的 2-范数。qr()—— QR 分解,可选mode='reduced'(默认)或mode='complete'。solve_triangular()—— 当A为三角矩阵时求解A @ x = b。cho_solve()—— 给定A的 Cholesky 因子,求解A @ x = b。
6.13.2. dot、cross、trace¶
dot() 是在摄像头上书写矩阵乘法的方式。桌面版 numpy 为同一任务提供的 @ 运算符在 ndarray 上并未实现,因此所有矩阵-向量、矩阵-矩阵以及向量点积都要通过 dot() 调用完成:
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.dot(a, b) # 32.0 (scalar product)
np.cross(a, b) # array([-3.0, 6.0, -3.0])
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
np.dot(m, a) # matrix-vector product
np.dot(m, m) # matrix-matrix product
np.trace(m) # 1 + 5 + 9 = 15.0
6.13.3. inv 与 det¶
m = np.array([[1, 2, 3, 4],
[4, 5, 6, 4],
[7, 9, 9, 4],
[3, 4, 5, 6]])
print(np.linalg.inv(m))
print(np.linalg.det(m))
逆矩阵通过高斯-约当消元法计算,因此当矩阵奇异时(消元过程中某个对角元素变为零),inv() 会抛出 ValueError。其 RAM 开销大约是输入大小的两倍。
行列式复用了同样的消元过程——运行时间与求逆基本相同。
当应用程序的目标是求解线性方程组时,不要先求逆再相乘——而应优先使用下面专门的求解器。两者在数值上都更快、表现也更好。
6.13.4. cholesky¶
对于对称正定矩阵 A,cholesky() 返回一个下三角矩阵 L,使得 A = L @ L.T:
a = np.array([[25, 15, -5],
[15, 18, 0],
[-5, 0, 11]])
L = np.linalg.cholesky(a)
如果输入不是正定的或不是对称的,则会抛出 ValueError。
Cholesky 因子的计算量是 LU 分解的一半,对于任何已知矩阵为对称正定的问题(协方差更新、最小二乘拟合的正规方程),它都是正确的出发点。
6.13.5. eig¶
eig() 仅适用于实对称矩阵。非对称矩阵会抛出 ValueError。它返回一个二元组 (eigenvalues, eigenvectors):
a = np.array([[1, 2, 1, 4],
[2, 5, 3, 5],
[1, 3, 6, 1],
[4, 5, 1, 7]], dtype=np.uint8)
x, y = np.linalg.eig(a)
注意事项:
6.13.6. norm¶
向量或矩阵的欧几里得(Frobenius)范数:
v = np.array([1, 2, 3, 4, 5])
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
np.linalg.norm(v) # 7.416...
np.linalg.norm(m) # 16.881...
可选的 axis= 关键字会沿单个轴求范数,而不是对整个数组求范数。
6.13.7. qr¶
qr() 将一个矩形矩阵 A(形状为 (M, N))分解为一个正交矩阵 Q 和一个上三角矩阵 R,使得 A == Q @ R:
A = np.arange(6).reshape((3, 2))
q, r = np.linalg.qr(A)
# mode='reduced' (default): q is (3, 2), r is (2, 2)
q, r = np.linalg.qr(A, mode='complete')
# q is (3, 3), r is (3, 2)
该分解通过连续的 Givens 旋转实现。对于矩阵非对称的最小二乘问题,这是正确的选择。
6.13.8. 求解方程组¶
ulab.scipy.linalg 之下的两个专用求解器都比 np.dot(np.linalg.inv(A), b) 更快、更准确:
solve_triangular(a, b, lower=False)()—— 在假定a为三角矩阵的前提下求解a @ x = b:A = np.array([[3, 0, 0, 0], [2, 1, 0, 0], [1, 0, 1, 0], [1, 2, 1, 8]]) b = np.array([4, 2, 4, 2]) x = sp.linalg.solve_triangular(A, b, lower=True)
cho_solve(L, b)()—— 给定一个 Cholesky 因子L,求解A @ x = b,其中A = L @ L.T:L = np.linalg.cholesky(A) x = sp.linalg.cho_solve(L, b)
只要 A 的结构允许,就应该使用这些求解器而非求逆——它们既省去了消元工作,又免去了显式求逆。
6.13.9. 求解小型线性方程组¶
A = np.array([[3, 0, 1, 1],
[0, 1, 0, 2],
[1, 0, 1, 1],
[1, 2, 1, 8]])
b = np.array([4, 2, 4, 2])
x = np.dot(np.linalg.inv(A), b)
print(x)
print(np.dot(A, x)) # should equal b
同样的问题可以通过分解 A 并调用相应的求解器来表达——当 A 具有合适的结构时,这样做更快也更准确。
完整的参数级参考请参见 numpy.linalg --- 线性代数例程 和 scipy.linalg --- 线性代数例程。