6.13. 线性代数

摄像头上的线性代数都是小矩阵运算:将 IMU 采样融合到世界坐标系的 3x3 旋转、用于校正镜头的标定矩阵、卡尔曼滤波器的状态协方差更新、其正规方程可化为微型线性求解的多项式拟合。numpy.linalgscipy.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

dot() 的结果始终是 float 类型。cross() 是两个三维向量的叉积,trace() 是方阵主对角线之和。

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

对于对称正定矩阵 Acholesky() 返回一个下三角矩阵 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)

注意事项:

  • 特征值返回时没有特定的顺序。当顺序重要时,请应用 sort()(并通过 argsort() 对特征向量施加相同的置换)。

  • 特征向量只在相差一个非零标量的意义下唯一,因此单个特征向量的符号并非唯一确定。两次正确的运行可能产生符号相反的向量;这是无害的。

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 --- 线性代数例程