跳轉到

6.1 Gram-Schmidt 正交化#

本節目標#

  • 理解為什麼「斜的基座標」難算,而「正交基座標」好算
  • 學會 Gram-Schmidt 正交化演算法:逐個減去投影,把歪的基扳正
  • 手算 \(2 \times 2\) 與 \(3 \times 3\) 的完整例子,並畫圖觀察每一步
  • 用 np.linalg.qr 驗證手算結果,最後正規化成 orthonormal 基

動機:斜基座標有多難算?#

想像你在測量一棟建築,卻只有兩支不垂直的捲尺,一支指向東北方,一支指向北方偏 30 度。 要描述一個點,你得知道「沿著這兩支尺各走多遠」,而且兩支尺會互相干擾—— 你在東北方向的移動也會稍微改變北方的讀數。

在線性代數裡,這就是「斜基座標難算」:

\[ \mathbf{b} = c_1 \mathbf{v}_1 + c_2 \mathbf{v}_2 \]

若 \(\mathbf{v}_1\) 與 \(\mathbf{v}_2\) 不垂直,求係數 \(c_1, c_2\) 得解聯立方程組。 但若基是正交的(彼此垂直),情況奇蹟般地簡單:

\[ c_1 = \frac{\mathbf{b} \cdot \mathbf{v}_1}{\mathbf{v}_1 \cdot \mathbf{v}_1}, \qquad c_2 = \frac{\mathbf{b} \cdot \mathbf{v}_2}{\mathbf{v}_2 \cdot \mathbf{v}_2} \]

只要做內積,連方程組都不用解。原因很幾何: \(\mathbf{v}_2\) 垂直於 \(\mathbf{v}_1\) , 所以 \(\mathbf{v}_2\) 在 \(\mathbf{v}_1\) 上的投影是零,兩個座標互不干擾。

正交基好算,所以整個線性代數都在追求它:QR 分解、最小平方法、 對稱矩陣的特徵分解(6.4 節),背後都是同一件事——把歪基扳正。


Gram-Schmidt 演算法:一個個扳正#

問題:給定一組線性獨立的基 \(\mathbf{v}_1, \dots, \mathbf{v}_n\) ,製造一組張成同樣空間的正交基 \(\mathbf{u}_1, \dots, \mathbf{u}_n\) 。

核心想法極其簡單:每一個新向量,減掉它在「已處理好的向量」上的所有投影。 剩下的部分,正是與所有已處理向量垂直的成分:

\[ \mathbf{u}_k = \mathbf{v}_k - \sum_{i < k} \text{proj}_{\mathbf{u}_i}(\mathbf{v}_k), \qquad \text{proj}_{\mathbf{u}}(\mathbf{v}) = \frac{\mathbf{v} \cdot \mathbf{u}}{\mathbf{u} \cdot \mathbf{u}} \, \mathbf{u} \]

投影的幾何意義(第 2 章學過):把 \(\mathbf{v}_k\) 影子落在 \(\mathbf{u}_i\) 方向的那一段「剪掉」。 剪掉之後,殘餘的部分自然垂直於 \(\mathbf{u}_i\) 。用程式描述:

import numpy as np

def gram_schmidt(V):
    """V 的每一「行」是一個基向量,回傳同樣張成空間的正交基"""
    V = V.astype(float)
    U = np.zeros_like(V)
    for k in range(V.shape[1]):
        u = V[:, k].copy()
        for i in range(k):
            u -= (U[:, i] @ V[:, k]) / (U[:, i] @ U[:, i]) * U[:, i]
        U[:, k] = u
    return U

手算例子一: \(2 \times 2\)#

取 \(\mathbf{v}_1 = (3, 1)\) 、 \(\mathbf{v}_2 = (1, 2)\) 。

第一步: \(\mathbf{u}_1 = \mathbf{v}_1 = (3, 1)\) 。

第二步:計算 \(\mathbf{v}_2\) 在 \(\mathbf{u}_1\) 上的投影:

\[ \mathbf{v}_2 \cdot \mathbf{u}_1 = 1 \cdot 3 + 2 \cdot 1 = 5, \qquad \mathbf{u}_1 \cdot \mathbf{u}_1 = 3^2 + 1^2 = 10 \]
\[ \mathbf{u}_2 = \mathbf{v}_2 - \frac{5}{10}(3, 1) = (1, 2) - (1.5, 0.5) = (-0.5, \; 1.5) \]

驗證: \(\mathbf{u}_1 \cdot \mathbf{u}_2 = 3(-0.5) + 1(1.5) = 0\) 。垂直了! (可用 plot_vectors 畫出 \(\mathbf{v}_1, \mathbf{v}_2, \mathbf{u}_1, \mathbf{u}_2\) 四支箭頭。)

畫圖時你會看到: \(\mathbf{u}_2\) 正是 \(\mathbf{v}_2\) 往 \(\mathbf{u}_1\) 的影子被剪掉後剩下的部分, 兩支箭頭呈直角。


手算例子二: \(3 \times 3\)#

取

\[ \mathbf{v}_1 = (1, 1, 0), \quad \mathbf{v}_2 = (1, 0, 1), \quad \mathbf{v}_3 = (0, 1, 1) \]

第一步: \(\mathbf{u}_1 = (1, 1, 0)\) 。

第二步:

\[ \mathbf{u}_2 = \mathbf{v}_2 - \frac{\mathbf{v}_2 \cdot \mathbf{u}_1}{\mathbf{u}_1 \cdot \mathbf{u}_1} \mathbf{u}_1 = (1, 0, 1) - \frac{1}{2}(1, 1, 0) = \left( \tfrac{1}{2}, -\tfrac{1}{2}, 1 \right) \]

第三步:要扣掉 \(\mathbf{v}_3\) 在 \(\mathbf{u}_1\) 和 \(\mathbf{u}_2\) 兩個方向的投影:

\[ \frac{\mathbf{v}_3 \cdot \mathbf{u}_1}{\mathbf{u}_1 \cdot \mathbf{u}_1} = \frac{1}{2}, \qquad \frac{\mathbf{v}_3 \cdot \mathbf{u}_2}{\mathbf{u}_2 \cdot \mathbf{u}_2} = \frac{1/2}{3/2} = \frac{1}{3} \]
\[ \mathbf{u}_3 = (0, 1, 1) - \tfrac{1}{2}(1, 1, 0) - \tfrac{1}{3}\left( \tfrac{1}{2}, -\tfrac{1}{2}, 1 \right) = \left( -\tfrac{2}{3}, \; \tfrac{2}{3}, \; \tfrac{2}{3} \right) \]
import numpy as np
from linalg_viz import plot_vectors

V = np.array([[1., 1., 0.],
              [1., 0., 1.],
              [0., 1., 1.]])   # 每行一個基向量

U = gram_schmidt(V)   # 用前面定義的函式
print(U)
print(U.T @ U)        # 應為對角矩陣:非對角全是 0

# 3D 圖:灰色是原向量,彩色是正交化後的結果
plot_vectors([V[:, 0], V[:, 1], V[:, 2]], labels=['v1', 'v2', 'v3'], color='gray')
plot_vectors([U[:, 0], U[:, 1], U[:, 2]], labels=['u1', 'u2', 'u3'])

注意 \(\mathbf{u}_3\) 有三個方向要同時垂直,所以它扣掉了兩個投影。 向量越多,要剪掉的影子越多——但演算法不變。


用 np.linalg.qr 驗證手算結果#

Gram-Schmidt 太重要了,NumPy 直接內建:np.linalg.qr 把矩陣 \(A\) 分解成 \(A = QR\) ,其中 \(Q\) 的行就是 orthonormal 基(下一節細講 \(R\) )。

import numpy as np

V = np.array([[1., 1., 0.],
              [1., 0., 1.],
              [0., 1., 1.]])

Q, R = np.linalg.qr(V)
print(Q)

# 手算結果(未正規化)的方向,應與 Q 的行一致
U = np.array([[1, 1/2, -2/3],
              [1, -1/2, 2/3],
              [0, 1, 2/3]])
Un = U / np.linalg.norm(U, axis=0)   # 逐行正規化
print(np.abs(np.abs(Q.T @ Un) - np.eye(3)).max())   # ~0:只差正負號

小提醒:qr 給出的行可能與手算差一個正負號(演算法內部選了方向), 檢查時比對「方向的絕對值」即可。

最後補上正規化,把正交基升級成 orthonormal 基(單位長 + 互相垂直)—— 其實上面驗證時的 Un 就是它,滿足 \(E^T E = I\) 。


小結與練習#

本節重點 1. 正交基的座標用內積就能算出,互不干擾——這是它好算的根本原因。 2. Gram-Schmidt:每個新向量減去它在先前向量上的投影,殘餘部分自動垂直。 3. np.linalg.qr 是 Gram-Schmidt 的工業級版本,直接把 \(A\) 拆成 orthonormal 基 \(Q\) 與上三角 \(R\) 。 4. 再除以長度,就得到 orthonormal 基 \(E\) ,滿足 \(E^T E = I\) 。

練習 1. 對 \(\mathbf{v}_1 = (2, 1)\) 、 \(\mathbf{v}_2 = (1, 3)\) 手算 Gram-Schmidt,再用 np.linalg.qr 驗證。 2. 用本節的 gram_schmidt 函式處理三個向量 \((1,0,0), (1,1,0), (1,1,1)\) ,畫圖觀察 \(\mathbf{u}_3\) 與 \(\mathbf{v}_3\) 差多少。 3. 把 \(\mathbf{v}_2\) 換成 \(\mathbf{v}_1\) 的倍數(線性相依),觀察演算法發生什麼事——這正是「線性獨立」假設的用意。