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\) 角):
鏡射矩陣(沿與 \(x\) 軸夾 \(\theta\) 角的直線鏡射):
用 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}\|^2\) 最小的 \(\mathbf{x}\) 。幾何上, \(A\mathbf{x}\) 張成的空間裡離 \(\mathbf{b}\) 最近的點是 \(\mathbf{b}\) 的正交投影,由此推得正規方程(normal equation):
(推導中用到 \((AB)^T = B^T A^T\) ,正是 6.1 節的轉置恆等式。)當 \(A\) 的行線性獨立( \(\operatorname{rank}(A) = n\) )時, \(A^T A\) 可逆,解為
例:最小平方擬合直線(符號推導)#
用符號完整推導:對兩點 \((0, 0)\) 、 \((1, 1)\) 擬合 \(y = c_0 + c_1 x\) 太容易(剛好精確),我們故意用三個點 \((0,0)\) 、 \((1,1)\) 、 \((2,1)\) 製造矛盾:
先用 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}\) ,所以
擬合直線: \(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()
延伸挑戰:
- 把資料點換成自己的生日數字或隨手亂數,重新擬合,觀察擬合線如何「折衷」於各點之間。
- 把某個點的 \(y\) 值大幅調動(離群值 outlier),觀察擬合線被拉動多少——這是最小平方法「對離群值敏感」的實驗證據。
- 改用二次多項式 \(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()把線性獨立向量組轉成正交(或正規正交)向量組
練習:
- 寫出逆時針旋轉 \(45°\) 的矩陣,驗證 \(R^T R = I\) 且 \(\det(R) = 1\) ;再對鏡射矩陣驗證 \(\det(M) = -1\) (鏡射翻轉了面積方向!)。
- 用最小平方法擬合點 \((0, 0)\) 、 \((1, 2)\) 、 \((2, 4)\) 、 \((3, 5)\) ,先手算 \(A^T A\) 與 \(A^T \mathbf{b}\) ,再對照 SymPy 結果。
- 證明(並用 SymPy 驗證)若 \(\mathbf{x}\) 滿足 normal equation,則殘差 \(A\mathbf{x} - \mathbf{b}\) 與 \(A\) 的每個行向量正交,即 \(A^T(A\mathbf{x} - \mathbf{b}) = \mathbf{0}\) 。
- 用
GramSchmidt正交化 \(\begin{pmatrix} 1 \\ 0 \end{pmatrix}\) 、 \(\begin{pmatrix} 1 \\ 1 \end{pmatrix}\) ,並畫圖確認結果互相垂直。 - 對實驗中的資料點加上離群值(例如把 \((3, 4)\) 改成 \((3, 10)\) ),比較擬合線的變化,並思考:如果想要更抗離群值的擬合,誤差該用什麼方式度量?