跳轉到

6.4 最小平方法與線性變換:當方程組無解,找最好的近似解#

本節目標#

  • 理解線性變換的矩陣表示,學會寫出旋轉與鏡射矩陣
  • 理解複合變換對應矩陣乘法(並體會順序的重要性)
  • 推導超定方程組的最小平方法:normal equation \(A^T A \mathbf{x} = A^T \mathbf{b}\)
  • 用符號推導 + 數值驗證完成最小平方直線擬合
  • 學會 Gram-Schmidt 正交化

線性變換的矩陣表示#

一個線性變換完全由它對基底向量的作用決定。把變換後的 \(\begin{pmatrix} 1 \\ 0 \end{pmatrix}\) 與 \(\begin{pmatrix} 0 \\ 1 \end{pmatrix}\) 當作矩陣的兩行,就得到變換的矩陣。

旋轉矩陣(逆時針轉 \(\theta\) 角):

\[ R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix} \]

鏡射矩陣(沿與 \(x\) 軸夾 \(\theta\) 角的直線鏡射):

\[ M(\theta) = \begin{pmatrix} \cos 2\theta & \sin 2\theta \\ \sin 2\theta & -\cos 2\theta \end{pmatrix} \]

用 SymPy 驗證:

from sympy import Matrix, cos, sin, pi, simplify

theta = pi / 6   # 30 度
R = Matrix([[cos(theta), -sin(theta)],
            [sin(theta),  cos(theta)]])
print(simplify(R * Matrix([1, 0])))   # Matrix([[sqrt(3)/2], [1/2]]):正確!
print(simplify(R.T * R))              # 單位矩陣:旋轉不改變長度

M = Matrix([[cos(2*theta), sin(2*theta)],
            [sin(2*theta), -cos(2*theta)]])
print(simplify(M * Matrix([1, 0])))   # 鏡射 (1,0):結果在 60 度方向

旋轉與鏡射矩陣都滿足 \(A^T A = I\) ,這類矩陣稱為正交矩陣——它們是純粹的「剛體運動」,不拉伸也不壓縮。

複合變換 = 矩陣乘法#

先做變換 \(B\) 、再做變換 \(A\) ,合成變換就是 \(AB\) :

from sympy import Matrix, pi, cos, sin, simplify

theta = pi / 2
R = Matrix([[cos(theta), -sin(theta)],
            [sin(theta),  cos(theta)]])   # 逆時針轉 90 度
F = Matrix([[1, 0],
            [0, -1]])                     # 對 x 軸鏡射

# 先鏡射再旋轉
print(simplify(R * F))
# Matrix([[0, 1], [1, 0]]):沿直線 y=x 鏡射

# 先旋轉再鏡射
print(simplify(F * R))
# Matrix([[0, -1], [-1, 0]],不同!

兩個順序結果不同——這正是 6.1 節「矩陣乘法不交換」的幾何具象:先翻轉再旋轉 ≠ 先旋轉再翻轉。

超定方程組與最小平方法#

實驗數據常常「方程太多、未知數太少」。例如用直線 \(y = c_0 + c_1 x\) 擬合 4 個資料點,得到 4 條方程卻只有 2 個未知數:

\[ A\mathbf{x} = \mathbf{b}, \quad A \text{ 是 } 4 \times 2,\text{通常無精確解} \]

無解時退而求其次:找使誤差平方和 \(\|A\mathbf{x} - \mathbf{b}\|^2\) 最小的 \(\mathbf{x}\) 。幾何上, \(A\mathbf{x}\) 張成的空間裡離 \(\mathbf{b}\) 最近的點是 \(\mathbf{b}\) 的正交投影,由此推得正規方程(normal equation):

\[ A^T A \mathbf{x} = A^T \mathbf{b} \]

(推導中用到 \((AB)^T = B^T A^T\) ,正是 6.1 節的轉置恆等式。)當 \(A\) 的行線性獨立( \(\operatorname{rank}(A) = n\) )時, \(A^T A\) 可逆,解為

\[ \mathbf{x} = (A^T A)^{-1} A^T \mathbf{b} \]

例:最小平方擬合直線(符號推導)#

用符號完整推導:對兩點 \((0, 0)\) 、 \((1, 1)\) 擬合 \(y = c_0 + c_1 x\) 太容易(剛好精確),我們故意用三個點 \((0,0)\) 、 \((1,1)\) 、 \((2,1)\) 製造矛盾:

\[ \begin{cases} c_0 = 0 \\ c_0 + c_1 = 1 \\ c_0 + 2c_1 = 1 \end{cases} \quad\Rightarrow\quad A = \begin{pmatrix} 1 & 0 \\ 1 & 1 \\ 1 & 2 \end{pmatrix}, \ \mathbf{b} = \begin{pmatrix} 0 \\ 1 \\ 1 \end{pmatrix} \]

先用 SymPy 做符號推導:

from sympy import Matrix, symbols, simplify

c0, c1 = symbols('c0 c1')
A = Matrix([[1, 0],
            [1, 1],
            [1, 2]])
b = Matrix([0, 1, 1])

# normal equation: A^T A c = A^T b
ATA = A.T * A
ATb = A.T * b
print(ATA)   # Matrix([[3, 3], [3, 5]])
print(ATb)   # Matrix([[2], [3]])

c = ATA.inv() * ATb
print(simplify(c))   # Matrix([[1/4], [3/8]]) → y = 1/4 + (3/8)x

手算核對: \(\det(A^T A) = 15 - 9 = 6\) , \((A^T A)^{-1} = \frac{1}{6}\begin{pmatrix} 5 & -3 \\ -3 & 3 \end{pmatrix}\) ,所以

\[ \begin{pmatrix} c_0 \\ c_1 \end{pmatrix} = \frac{1}{6}\begin{pmatrix} 5 \cdot 2 - 3 \cdot 3 \\ -3 \cdot 2 + 3 \cdot 3 \end{pmatrix} = \begin{pmatrix} 1/4 \\ 3/8 \end{pmatrix} \]

擬合直線: \(y = \dfrac{1}{4} + \dfrac{3}{8}x\) 。再數值驗證誤差確實最小:

import matplotlib.pyplot as plt
import numpy as np

xs = np.array([0, 1, 2], dtype=float)
ys = np.array([0, 1, 1], dtype=float)
c0v, c1v = 0.25, 0.375

plt.plot(xs, ys, 'ro', label='資料點')
t = np.linspace(-0.5, 2.5, 100)
plt.plot(t, c0v + c1v * t, 'b-', label=f"擬合線 y = {c0v} + {c1v}x")
for xi, yi in zip(xs, ys):
    plt.plot([xi, xi], [yi, c0v + c1v*xi], 'g--')   # 誤差線
plt.legend()
plt.grid(True)
plt.show()

# 誤差平方和
err = sum((c0v + c1v*x - y)**2 for x, y in zip(xs, ys))
print(err)   # 0.125

# 對照:用 numpy 的最小平方法
coef = np.polyfit(xs, ys, 1)
print(coef)   # [0.375 0.25]:斜率與截距完全一致!

也可以直接讓 SymPy 解 normal equation(不必手算反矩陣):

print(A.T * A .solve(A.T * b))   # Matrix([[1/4], [3/8]])

Gram-Schmidt 正交化#

最小平方法的進階版(QR 分解)需要正交化:把一組線性獨立向量轉成互相垂直的向量組。Gram-Schmidt 過程:保留第一個向量方向,之後每個向量減掉它在先前向量上的投影分量。SymPy 內建 GramSchmidt:

from sympy import Matrix, GramSchmidt

v1 = Matrix([1, 1, 0])
v2 = Matrix([1, 0, 1])
v3 = Matrix([0, 1, 1])

ortho = GramSchmidt([v1, v2, v3])
for v in ortho:
    print(v.T)
# Matrix([[1, 1, 0]])
# Matrix([[1/2, -1/2, 1]])
# Matrix([[-2/3, 2/3, 2/3]])

# 驗證兩兩垂直:內積皆為 0
print(ortho[0].dot(ortho[1]))   # 0
print(ortho[0].dot(ortho[2]))   # 0
print(ortho[1].dot(ortho[2]))   # 0

加參數 orthonormal=True 可以連長度也正規化(變成單位向量):

unit = GramSchmidt([v1, v2, v3], orthonormal=True)
for v in unit:
    print(v.dot(v))   # 1, 1, 1:每個都是單位向量

動手實驗:擬合自己造的資料點#

把流程打包成函式,擬合你自己造的資料:

import matplotlib.pyplot as plt
import numpy as np
from sympy import Matrix, Rational

def least_squares_line(points):
    """points: [(x, y), ...],回傳 (c0, c1) 與誤差平方和(精確分數)"""
    A = Matrix([[1, Rational(x)] for x, _ in points])
    b = Matrix([Rational(y) for _, y in points])
    c = (A.T * A).solve(A.T * b)
    err = sum((c[0] + c[1]*Rational(x) - Rational(y))**2 for x, y in points)
    return c, err

# 自己造資料:故意不平共線
points = [(0, 1), (1, 2), (2, 2), (3, 4)]
(c0, c1), err = least_squares_line(points)
print(f"y = {c0} + {c1}x,誤差平方和 = {err}")
# y = 4/5 + 4x/5,誤差平方和 = 1/5

畫圖看看結果(SymPy 分數轉成 float 來畫):

xs = [p[0] for p in points]
ys = [p[1] for p in points]
plt.plot(xs, ys, 'ro', label='資料點')
t = np.linspace(min(xs) - 0.5, max(xs) + 0.5, 100)
plt.plot(t, float(c0) + float(c1) * t, 'b-', label='最小平方擬合線')
plt.legend()
plt.grid(True)
plt.show()

延伸挑戰:

  1. 把資料點換成自己的生日數字或隨手亂數,重新擬合,觀察擬合線如何「折衷」於各點之間。
  2. 把某個點的 \(y\) 值大幅調動(離群值 outlier),觀察擬合線被拉動多少——這是最小平方法「對離群值敏感」的實驗證據。
  3. 改用二次多項式 \(y = c_0 + c_1 x + c_2 x^2\) 擬合( \(A\) 的每一列變成 \([1, x, x^2]\) ),比較誤差平方和是否下降。

小結與練習#

本節重點:

  • 線性變換由矩陣編碼:旋轉矩陣 \(R(\theta)\) 、鏡射矩陣 \(M(\theta)\) ;複合變換對應矩陣乘法,且順序不可交換
  • 超定方程組無精確解時,最小平方法解 normal equation \(A^T A\mathbf{x} = A^T \mathbf{b}\) ,即找 \(\mathbf{b}\) 到 \(A\) 的行空間的正交投影
  • 擬合直線的完整流程:建 \(A, \mathbf{b}\) → 解 normal equation → 畫圖驗證誤差
  • GramSchmidt() 把線性獨立向量組轉成正交(或正規正交)向量組

練習:

  1. 寫出逆時針旋轉 \(45°\) 的矩陣,驗證 \(R^T R = I\) 且 \(\det(R) = 1\) ;再對鏡射矩陣驗證 \(\det(M) = -1\) (鏡射翻轉了面積方向!)。
  2. 用最小平方法擬合點 \((0, 0)\) 、 \((1, 2)\) 、 \((2, 4)\) 、 \((3, 5)\) ,先手算 \(A^T A\) 與 \(A^T \mathbf{b}\) ,再對照 SymPy 結果。
  3. 證明(並用 SymPy 驗證)若 \(\mathbf{x}\) 滿足 normal equation,則殘差 \(A\mathbf{x} - \mathbf{b}\) 與 \(A\) 的每個行向量正交,即 \(A^T(A\mathbf{x} - \mathbf{b}) = \mathbf{0}\) 。
  4. 用 GramSchmidt 正交化 \(\begin{pmatrix} 1 \\ 0 \end{pmatrix}\) 、 \(\begin{pmatrix} 1 \\ 1 \end{pmatrix}\) ,並畫圖確認結果互相垂直。
  5. 對實驗中的資料點加上離群值(例如把 \((3, 4)\) 改成 \((3, 10)\) ),比較擬合線的變化,並思考:如果想要更抗離群值的擬合,誤差該用什麼方式度量?