3.4 矩陣與線性變換:幾何的代數化#
學習動機#
前兩節我們把點變成數對(3.1)、把位移變成向量(3.2)。還剩最後一塊拼圖:把「移動圖形」也變成代數。旋轉、縮放、剪下、鏡射——這些幾何動作如果能寫成一個小表格(矩陣)乘上座標就能完成,那麼「連續做十個變換」就只是十個矩陣相乘、「把整個 3D 場景投影到螢幕」就只是一連串矩陣運算。這正是線性代數的幾何起源,也是今天電腦圖學(GPU 的核心工作就是瘋狂地做矩陣乘法)、機器人學、物理學中座標變換的共同基礎。
歷史上,矩陣這個「表格」在 19 世紀中葉由凱萊(Arthur Cayley, 1821–1895)在 1858 年的《矩陣論備忘錄》(A Memoir on the Theory of Matrices)中正式定義——注意這比格拉斯曼的向量空間(1844)晚了十幾年,比笛卡兒晚了兩百多年。凱萊明說他的動機之一就是研究線性變換的複合:兩個變換接連施行,等價於一個怎樣的變換?答案「矩陣相乘」的乘法規則,正是從這個幾何問題逼出來的。換句話說,矩陣乘法那套看似任意的「列乘行」規則,其實是「變換的複合」這個幾何操作的必然產物。本節你將學會:
- 把旋轉、縮放、剪下、鏡射寫成 \(2\times 2\) 矩陣,並用 SymPy 驗證其幾何效果;
- 理解矩陣乘法 = 變換複合,並解釋為何乘法不可交換;
- 用行列式讀出「面積縮放倍率」;
- 用
eigenvects找出變換下的不動方向(特徵向量),理解旋轉為何「沒有實的不動方向」; - 把整個多邊形用矩陣變換,連接到電腦圖學的頂點變換管線。
1. 線性變換:只保留「直線」與「原點」的變換#
1.1 什麼樣的變換值得用矩陣?#
在 3.1 節我們知道直線可以寫成 \(ax + by + c = 0\) 。現在問:哪些座標變換 \(T: \mathbb{R}^2 \to \mathbb{R}^2\) 會把直線映射到直線?如果進一步要求原點不動( \(T(0,0) = (0,0)\) )且保持向量加法與純量伸縮:
這樣的 \(T\) 就叫線性變換。線性變換有個驚人的性質:它被兩個點完全決定——只要知道 \(T\) 把 \((1,0)\) 與 \((0,1)\) 送到哪裡, \(T\) 對整個平面的作用就全部確定了。因為任意向量 \((x, y) = x\,(1,0) + y\,(0,1)\) ,而線性變換「保持加法與伸縮」,所以
把 \(T(1,0)\) 與 \(T(0,1)\) 的像當成兩個直行排成一個 \(2\times 2\) 表格,這個表格就是矩陣:
這就是「幾何的代數化」的最後一步:**變換 \(T\) ≈ 矩陣 \(A\) **。凱萊正是沿著這條思路,把「變換的複合」變成「矩陣的乘法」。
1.2 SymPy 中的矩陣#
SymPy 的 Matrix 是精確符號矩陣:元素可以是整數、有理數、 \(\sqrt{3}\) 、 \(\cos(\pi/3)\) 、甚至未定符號。
from sympy import Matrix, Rational, sqrt
A = Matrix([[2, 0], [0, 3]]) # 一個表格
v = Matrix([1, 1]) # 一個直行向量
print(A * v)
# 預期輸出:Matrix([[2], [3]])
print(Matrix([[Rational(1, 2), -sqrt(3)/2], [sqrt(3)/2, Rational(1, 2)]]))
# 預期輸出:Matrix([[1/2, -sqrt(3)/2], [sqrt(3)/2, 1/2]])
注意 Matrix([1, 1]) 是直行向量( \(2\times 1\) ),這與 3.2 節的向量約定一致:變換作用是「矩陣在左、向量在右」。
2. 四種基本變換#
2.1 旋轉#
把向量 \((1,0)\) 逆時針旋轉 \(\theta\) 角,由 3.2 節的內積計算知它落在 \((\cos\theta, \sin\theta)\) ;把 \((0,1)\) 旋轉後落在 \((-\sin\theta, \cos\theta)\) 。因此旋轉矩陣為
驗證:把單位向量 \((1,0)\) 旋轉 \(60°\) ,應落在 \((\tfrac12, \tfrac{\sqrt3}{2})\) ;連轉三次共 \(180°\) ,應把 \((1,0)\) 變成 \((-1, 0)\) :
from sympy import Matrix, pi, cos, sin, simplify
R = Matrix([[cos(pi/3), -sin(pi/3)], [sin(pi/3), cos(pi/3)]])
p = Matrix([1, 0])
print(R * p)
# 預期輸出:Matrix([[1/2], [sqrt(3)/2]])
print(simplify(R * R * R))
# 預期輸出:Matrix([[-1, 0], [0, -1]]) (60° x 3 = 180°,一切變號)
print(R.det())
# 預期輸出:1 (旋轉不改變面積)
R * R * R 不需任何三角公式展開,SymPy 直接化簡成 \(-I\) 。這正是「複合 = 矩陣乘法」的第一個證據。
2.2 縮放#
沿 \(x\) 軸伸縮 \(a\) 倍、沿 \(y\) 軸伸縮 \(b\) 倍:
顯然 \(S\) 把單位正方形面積變成 \(|ab|\) 倍——這提示行列式 \(\det A = ab\) 的幾何意義(見第 4 節)。
from sympy import Matrix, Rational
S = Matrix([[2, 0], [0, Rational(1, 2)]])
print(S * Matrix([3, 4]))
# 預期輸出:Matrix([[6], [2]]) (x 變 2 倍,y 縮一半)
print(S.det())
# 預期輸出:1 (一伸一縮,面積不變)
2.3 剪下(shear)#
剪下變換「水平方向推擠」: \(y\) 越大的點被推得越遠:
它把正方形推成平行四邊形,但底邊不動、高度不變,所以面積不變—— \(\det K = 1\) 。剪下的乘方有漂亮的形式: \(K^5\) 相當於「剪五次」:
from sympy import Matrix, Rational
K = Matrix([[1, Rational(1, 2)], [0, 1]])
print(K.det())
# 預期輸出:1
print(K**5)
# 預期輸出:Matrix([[1, 5/2], [0, 1]]) (k 從 1/2 變 5/2)
2.4 鏡射(反射)#
對 \(x\) 軸鏡射把 \((x, y)\) 映成 \((x, -y)\) :
鏡射有兩個與旋轉截然不同的特徵: \(\det F = -1\) (把平面「翻面」,定向反轉),以及 \(F^2 = I\) (照兩次鏡子回到原樣):
from sympy import Matrix
F = Matrix([[1, 0], [0, -1]])
print(F.det(), F**2)
# 預期輸出:-1 Matrix([[1, 0], [0, 1]])
| 變換 | 矩陣 | 行列式 | 定向 |
|---|---|---|---|
| 旋轉 \(R_\theta\) | \(\begin{pmatrix}\cos\theta & -\sin\theta\\ \sin\theta & \cos\theta\end{pmatrix}\) | \(1\) | 保持 |
| 縮放 \(S\) | \(\begin{pmatrix}a & 0\\ 0 & b\end{pmatrix}\) | \(ab\) | 保持( \(ab>0\) ) |
| 剪下 \(K\) | \(\begin{pmatrix}1 & k\\ 0 & 1\end{pmatrix}\) | \(1\) | 保持 |
| 鏡射 \(F\) | \(\begin{pmatrix}1 & 0\\ 0 & -1\end{pmatrix}\) | \(-1\) | 反轉 |
3. 矩陣乘法 = 變換複合#
3.1 為什麼「列乘行」不是任意規則#
設先施行剪下 \(K\) 、再施行縮放 \(S\) ,合成變換是 \(T(\mathbf{v}) = S(K(\mathbf{v})) = (S\,K)\,\mathbf{v}\) 。矩陣 \(SK\) 的每個元素由「 \(S\) 的列」配「 \(K\) 的行」內積(3.2 節!)而來——矩陣乘法的規則就是內積。這解釋了兩件事:
- 為什麼複合順序寫反(先 \(K\) 後 \(S\) 寫成 \(SK\) ):因為作用從右往左;
- 為什麼矩陣乘法不可交換:先旋轉再縮放,與先縮放再旋轉,幾何結果不同!
from sympy import Matrix, pi, cos, sin
R = Matrix([[cos(pi/2), -sin(pi/2)], [sin(pi/2), cos(pi/2)]]) # 轉 90°
S = Matrix([[2, 0], [0, 1]]) # x 伸 2 倍
print(R * S)
# 預期輸出:Matrix([[0, -1], [2, 0]])
print(S * R)
# 預期輸出:Matrix([[0, -2], [1, 0]])
print((R * S).equals(S * R))
# 預期輸出:False (先轉再縮 ≠ 先縮再轉)
3.2 用矩陣搬動一個多邊形#
電腦圖學的日常:物件儲存為一串頂點,顯示前對每個頂點左乘變換矩陣。用 SymPy 模擬「把一個 \(2\times 1\) 矩形先剪下再縮放」:
from sympy import Matrix, Rational
from sympy.geometry import Point, Polygon
M = Matrix([[2, 1], [0, Rational(1, 2)]]) # M = S * K,先剪下後縮放
poly = Polygon(Point(0, 0), Point(2, 0), Point(2, 1), Point(0, 1))
pts = [Matrix([p.x, p.y]) for p in poly.vertices]
new_poly = Polygon(*[Point(*(M * v)) for v in pts])
print(new_poly.vertices)
# 頂點 (0,0) (4,0) (5,1/2) (1,1/2) 的一個平行四邊形
print(new_poly.area, poly.area, M.det())
# 預期輸出:1 1 1 (det M = 1,面積不變)
這個「頂點列表 × 矩陣」的迴圈,就是 GPU 每秒執行數十億次的基本操作(頂點著色器)在符號世界裡的樣子。
4. 行列式:面積縮放因子#
線性變換把單位正方形映成以 \(A\) 的兩個直行為邊的平行四邊形,其面積為
我們可以機器驗證這個定理對符號矩陣成立——變換一個面積為 2 的多邊形,比對面積與 \(|\det A|\) :
from sympy import Matrix, symbols, simplify, Abs
from sympy.geometry import Point, Polygon
a, b, c, d = symbols('a b c d', real=True)
A = Matrix([[a, b], [c, d]])
poly = Polygon(Point(0, 0), Point(2, 0), Point(2, 1), Point(0, 1))
pts = [Matrix([p.x, p.y]) for p in poly.vertices]
new_poly = Polygon(*[Point(*(A * v)) for v in pts])
lhs = simplify(new_poly.area)
rhs = simplify(Abs(A.det()) * poly.area)
print(simplify(lhs - rhs))
# 預期輸出:0 (對任意 a, b, c, d,面積比 = |det A| 恆成立)
一條對所有矩陣成立的幾何定理,靠符號計算一次驗證——這是 2.3 節「符號畢氏定理」精神的延伸。由此立刻得到兩個推論:
- \(\det A = 0 \iff\) 變換把平面壓扁成一直線(不可逆);
- \(\det A < 0 \iff\) 變換翻轉定向(含鏡射成分)。
5. 特徵向量:變換下的不動方向#
5.1 不動方向是什麼#
大多數向量被變換 \(A\) 作用後,方向都會改變。但有些「幸運」的方向,被 \(A\) 作用後只在同一直線上伸縮、不改變方向:
這樣的 \(\mathbf{v}\) 稱為特徵向量(eigenvector), \(\lambda\) 稱為對應的特徵值(eigenvalue)。「特徵」二字取得極好:這些方向是變換的骨架——抓住它們,就抓住了變換的本性。
5.2 三種變換、三種命運#
用 SymPy 的 eigenvects(回傳 (特徵值, 重數, 特徵向量列表) 三元組列表)檢查前面三種變換:
from sympy import Matrix, Rational, pi, cos, sin
K = Matrix([[1, Rational(1, 2)], [0, 1]]) # 剪下
print(K.eigenvects())
# 預期輸出:
# [(1, 2, [Matrix([
# [1],
# [0]])])]
# 唯一不動方向:x 軸(特徵值 1),幾何上剪下正是沿 x 軸推擠
S = Matrix([[2, 0], [0, Rational(1, 2)]]) # 縮放
print(S.eigenvects())
# 預期輸出:
# [(1/2, 1, [Matrix([
# [0],
# [1]])]), (2, 1, [Matrix([
# [1],
# [0]])])]
# 兩個座標軸各是不動方向,特徵值就是伸縮倍率
R = Matrix([[cos(pi/3), -sin(pi/3)], [sin(pi/3), cos(pi/3)]]) # 轉 60°
print(R.eigenvects())
# 預期輸出:
# [(1/2 - sqrt(3)*I/2, 1, [Matrix([
# [-sqrt(3)/3 + 2*sqrt(3)*(1/2 - sqrt(3)*I/2)/3],
# [ 1]])]),
# (1/2 + sqrt(3)*I/2, 1, [Matrix([
# [-sqrt(3)/3 + 2*sqrt(3)*(1/2 + sqrt(3)*I/2)/3],
# [ 1]])])]
# 特徵值是複數 e^{±i·π/3}!實平面上沒有不動方向——旋轉就是這個意思
剪下的不動方向是 \(x\) 軸、縮放的不動方向是兩軸,這些都能從圖上「看出來」。最深刻的是旋轉:平面上沒有任何一條直線轉 \(60°\) 後還是自己,SymPy 如實回報特徵值為複數 \(\tfrac12 \pm \tfrac{\sqrt3}{2}i = e^{\pm i\pi/3}\) 。特徵值的模長為 \(1\) (純旋轉不伸縮),輻角就是旋轉角——複數、旋轉、特徵值在此匯流。這也預告了一個更大的主題:要理解實平面上的旋轉,數學家被迫推廣數系;同樣的張力日後把線性代數推向了複向量空間與譜理論。
5.3 驗證特徵方程#
特徵值來自特徵方程 \(\det(A - \lambda I) = 0\) 。手動驗證剪下矩陣:
from sympy import Matrix, Rational, symbols, solve
lam = symbols('lambda')
K = Matrix([[1, Rational(1, 2)], [0, 1]])
char_eq = K - lam * Matrix.eye(2)
print(char_eq.det())
# 頂期輸出:lambda**2 - 2*lambda + 1
print(solve(char_eq.det(), lam))
# 預期輸出:[1] (特徵值 1,二重根——剪下的唯一「骨架方向」)
(注意:輸出中的 lambda 是 SymPy 顯示的希臘字母 \(\lambda\) 之名,與 Python 的 lambda 關鍵字無關。)
6. 來龍去脈:從凱萊到 GPU#
timeline
title 線性變換的歷史線
1637 : 笛卡兒《幾何學》:點 = 座標
1844 : 格拉斯曼《延量論》:向量空間
1858 : 凱萊《矩陣論備忘錄》:矩陣代數與乘法
1884 : Gibbs/Heaviside 推廣向量分析
1920s : 量子力學:狀態 = 向量、觀測 = 算符(特徵值)
1963 : Sketchpad(Sutherland):互動式電腦圖學
1990s- : GPU 管線:頂點變換 = 矩陣乘法硬體化三個值得記住的聯結:
- 解析幾何 → 微積分(3.1 節):曲線 = 方程,才能對曲線做運算;
- 向量空間 → 內積(3.2 節):幾何量(長度、角度、面積)進入代數;
- 矩陣 → 線性代數與圖學(本節):幾何動作變成資料,變換複合變成矩陣乘法,於是「變換」可以被編程、被硬體加速。第 5 章的射影變換(齊次座標)與第 10 章的電腦圖學,都是本節的直接續集。
7. 綜合練習:把一棟「房子」變換#
from sympy import Matrix, Rational, pi, cos, sin
from sympy.geometry import Point, Polygon
# 一個簡筆房子:方形屋身 + 三角屋頂
body = Polygon(Point(0, 0), Point(2, 0), Point(2, 1), Point(0, 1))
roof = Polygon(Point(0, 1), Point(2, 1), Point(1, Rational(3, 2)))
# 變換:先逆時針轉 90°,再對 x 軸鏡射
R = Matrix([[0, -1], [1, 0]])
F = Matrix([[1, 0], [0, -1]])
M = F * R # 先 R 後 F
def transform(poly, M):
return Polygon(*[Point(*(M * Matrix([p.x, p.y]))) for p in poly.vertices])
new_body = transform(body, M)
new_roof = transform(roof, M)
print(new_body.vertices)
# 預期輸出:(0,0) (0,-2) (-1,-2) (-1,0) 的四邊形
print(new_roof.vertices)
# 頂點 (0,-1) (0,-2) (-3/2,-1) 的三角形
print(new_body.area, new_roof.area, abs(M.det()))
# 預期輸出:2 1/2 1 (面積不變,因為 |det M| = 1)
旋轉加鏡射把房子搬到第三象限並翻面;由於 \(|\det M| = 1\) ,兩塊面積原封不動。若把 \(M\) 換成 \(\begin{pmatrix}2&0\\0&2\end{pmatrix}\) ,面積會變成 \(4\) 倍——讀者可自行驗證。
習題#
-
旋轉矩陣:寫出把平面逆時針旋轉 \(30°\) 的矩陣 \(R_{30°}\) ,用 SymPy 計算 \(R_{30°}^6\) 並解釋結果為何是 \(-I\) ;再計算 \(R_{30°}^{12}\) ,你得到了什麼?這說明旋轉矩陣構成怎樣的結構?
-
乘法不可交換:令 \(R\) 為轉 \(90°\) 的矩陣、 \(F\) 為對直線 \(y = x\) 鏡射的矩陣(提示: \(F = \begin{pmatrix}0&1\\1&0\end{pmatrix}\) )。計算 \(RF\) 與 \(FR\) ,說明兩者各是什麼變換(提示:都是鏡射,但對稱軸不同)。
-
行列式與面積:對 \(A = \begin{pmatrix}3 & 1 \\ 0 & 2\end{pmatrix}\) ,用第 4 節的多邊形方法驗證「面積縮放倍率 = \(|\det A|\) = 6」。再問: \(A\) 把單位圓 \(x^2 + y^2 = 1\) 映成什麼曲線?(提示:把 \(\begin{pmatrix}\cos t\\ \sin t\end{pmatrix}\) 代入 \(A\) ,消去參數 \(t\) 。)
-
特徵向量:求 \(A = \begin{pmatrix}2 & 1 \\ 1 & 2\end{pmatrix}\) 的特徵值與特徵向量(可用
eigenvects)。這個變換在幾何上是「沿對角線 \(y = x\) 伸縮、沿 \(y = -x\) 壓縮」,請用兩個特徵向量方向的伸縮倍率解釋。 -
矩陣的冪: \(K = \begin{pmatrix}1 & 1/3 \\ 0 & 1\end{pmatrix}\) 。計算 \(K^{10}\) ,並歸納出 \(K^n\) 的一般公式。再用
eigenvects說明:為什麼剪下矩陣「永遠只有一個不動方向、且特徵值永遠是 1」?(這類只有一個特徵方向的重根矩陣,是第 7 章仿射變換分類的關鍵例子。)