跳轉到

6.3 最小平方法#

本節目標#

  • 理解超定方程組( \(m > n\) ):通常沒有精確解,只能找「最近的解」
  • 建立「最近 = 投影到行空間」的幾何直覺,並推導正規方程 \(A^T A\hat{\mathbf{x}} = A^T\mathbf{b}\)
  • 用 np.linalg.lstsq 實作,完成直線擬合的完整例子(含殘差圖)
  • 比較 normal equation / QR / lstsq 三條路
  • 實驗:用同一套方法擬合二次曲線

超定方程組:無解是常態#

三個資料點想共用一條直線 \(y = c_0 + c_1 x\) :

\(x\) \(y\)
0 1
1 2
2 2

代進方程式得到三條式、卻只有兩個未知數:

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

三條式、兩個未知數:這是 \(m > n\) 的超定方程組。畫圖看:三個點不共線, 任何直線都不可能同時穿過—— \(\mathbf{b}\) 根本不在 \(A\) 的行空間裡,精確解不存在。 (這時 np.linalg.solve 會直接報錯,因為它只收方陣。)

np.linalg.solve 罷工了。我們需要一個更謙遜的問題:

沒有完美解,那誤差最小的解是哪一個?


最近的解:把 \(\mathbf{b}\) 投影到行空間#

\(A\mathbf{x}\) 只能落在 \(A\) 的行空間(各行的所有線性組合,第 2 章的「能到達的區域」)。 \(\mathbf{b}\) 在行空間外面,但離它最近的點一定在裡面——把 \(\mathbf{b}\) 垂直投影過去:

\[ A\hat{\mathbf{x}} = \text{proj}_{\text{Col}(A)}\, \mathbf{b} \]

幾何圖像:行空間是一張平面(這裡是 \(\mathbb{R}^3\) 中的一張斜面), \(\mathbf{b}\) 是平面外的箭頭, \(\hat{\mathbf{b}}\) 是它的影子,而 \(\mathbf{b} - \hat{\mathbf{b}}\) (誤差向量 \(\mathbf{e}\) )垂直於整張平面。

「垂直於整張平面」等價於「垂直於每一行」:

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

整理一下,就是大名鼎鼎的正規方程:

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

注意 \(A^T A\) 是 \(n \times n\) 方陣(行空間獨立時可逆), 所以可以用 np.linalg.solve 解——我們把一個無解的 \(m \times n\) 問題, 轉成了一個有解的 \(n \times n\) 問題。這就是最小平方法的全部魔法。


np.linalg.lstsq:一行搞定#

NumPy 把整套流程(其實底層用更穩的 SVD)封裝成一個函式:

import numpy as np

A = np.array([[1., 0.],
              [1., 1.],
              [1., 2.]])
b = np.array([1., 2., 2.])

xhat, residuals, rank, sv = np.linalg.lstsq(A, b, rcond=None)
print(xhat)      # [1.  0.5]  →  y = 1 + 0.5x
print(residuals) # 平方誤差總和 = 0.5

# 驗證正規方程
print(np.linalg.solve(A.T @ A, A.T @ b))   # 一樣的答案

完整例子:直線擬合 + 殘差圖#

import numpy as np
import matplotlib.pyplot as plt
from linalg_viz import plot_fit_line

np.random.seed(0)

# 1. 產生帶雜訊的資料:真實關係是 y = 2x + 1
x = np.linspace(0, 10, 30)
y = 2 * x + 1 + np.random.randn(30) * 2.5

# 2. 建矩陣:每列 [1, x_i],設計矩陣
A = np.column_stack([np.ones_like(x), x])

# 3. 最小平方解
xhat = np.linalg.lstsq(A, y, rcond=None)[0]
c0, c1 = xhat
print(f"擬合結果: y = {c0:.2f} + {c1:.2f} x")   # 應接近 1 和 2

# 4. 畫資料點、擬合線、殘差
plot_fit_line(x, y, xhat, title="y = 2x + 1 + noise")

# 5. 殘差:資料減去擬合值,應該像「沒有規律的雜訊」
residual = y - A @ xhat
plt.figure()
plt.stem(x, residual)
plt.axhline(0, color='k', lw=0.5)
plt.title("殘差:垂直小線段就是 e 的各分量")
plt.show()

print(f"平方誤差總和: {residual @ residual:.2f}")

讀圖要點: - 藍點是資料,直線是 \(A\hat{\mathbf{x}}\) ——資料點在行空間的影子。 - 每個點到直線的垂直小線段就是殘差 \(\mathbf{e} = \mathbf{b} - A\hat{\mathbf{x}}\) ; 最小平方法的保證是:這些殘差的平方和是所有直線中最小的。 - 殘差若呈現規律(例如彎曲),代表直線模型不夠——見下面的實驗。


三條路比較:normal equation / QR / lstsq#

同樣的問題有三種解法,各有定位:

方法 寫法 優點 缺點
正規方程 solve(A.T @ A, A.T @ b) 最直觀、好推導 \(A^T A\) 相乘會放大誤差;條件數平方倍變差
QR solve(R, Q.T @ b) 穩定,沿用 6.2 的兩步走 自己要湊兩行程式
lstsq np.linalg.lstsq(A, b) 最穩(底層 SVD)、可處理相依行 稍慢,回傳值較多
import numpy as np

# 三條路給出一致的答案
x1 = np.linalg.solve(A.T @ A, A.T @ b)          # normal equation
Q, R = np.linalg.qr(A)                          # QR
x2 = np.linalg.solve(R, Q.T @ b)
x3 = np.linalg.lstsq(A, b, rcond=None)[0]       # lstsq

print(np.allclose(x1, x2), np.allclose(x2, x3))

經驗法則:手算/教學用正規方程,寫程式用 lstsq(或 QR)。 下一節你會看到,若 \(A\) 是方陣且對稱,正規方程反而翻身——因為 \(A^T A = A^2\) 的性質變好了。


實驗:擬合二次曲線#

直線不行,那就升級模型。二次曲線 \(y = c_0 + c_1 x + c_2 x^2\) 的設計矩陣只是每列多一格:

\[ A = \begin{bmatrix} 1 & x_1 & x_1^2 \\ \vdots & \vdots & \vdots \end{bmatrix} \]

這是「多項式迴歸」,但解法完全一樣——對未知數 \(c\) 而言仍是線性的:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
x = np.linspace(-3, 3, 40)
y = 0.8 * x**2 - 1.5 * x + 2 + np.random.randn(40) * 2.0

# 一次 vs 二次模型
A1 = np.column_stack([np.ones_like(x), x])
A2 = np.column_stack([np.ones_like(x), x, x**2])

c1 = np.linalg.lstsq(A1, y, rcond=None)[0]
c2 = np.linalg.lstsq(A2, y, rcond=None)[0]

xs = np.linspace(-3, 3, 200)
plt.scatter(x, y, label="資料")
plt.plot(xs, np.column_stack([np.ones_like(xs), xs]) @ c1, label="直線")
plt.plot(xs, np.column_stack([np.ones_like(xs), xs, xs**2]) @ c2, label="二次")
plt.legend(); plt.show()

r1 = y - A1 @ c1
r2 = y - A2 @ c2
print(f"直線平方誤差: {r1 @ r1:.1f}")
print(f"二次平方誤差: {r2 @ r2:.1f}")   # 必然 <= 直線(行空間更大)

一個深刻的觀察:二次模型的行空間包含直線模型的行空間(多了 \(x^2\) 這一行的組合), 所以誤差只會更小。但行空間太大也危險——模型開始「背答案」而不是「學規律」, 這就是機器學習說的過擬合。最小平方法是你未來學迴歸的第一塊基石。


小結與練習#

本節重點 1. 超定方程組 \(m > n\) 通常無解;改問「哪個 \(A\mathbf{x}\) 離 \(\mathbf{b}\) 最近」。 2. 答案是投影: \(\hat{\mathbf{x}}\) 讓誤差 \(\mathbf{b} - A\hat{\mathbf{x}}\) 垂直於行空間,得到 \(A^T A\hat{\mathbf{x}} = A^T\mathbf{b}\) 。 3. np.linalg.lstsq 是工業級解法;殘差圖是檢查模型適切性的眼睛。 4. 換設計矩陣就能擬合任意線性模型(多項式、多特徵)——同一招走天下。

練習 1. 用本節開頭的三點資料,手算 \(A^T A\) 、 \(A^T\mathbf{b}\) 並解出 \(\hat{\mathbf{x}}\) ,與 lstsq 比對。 2. 把直線擬合的雜訊強度調大 5 倍,觀察擬合線與殘差圖的變化。 3. 續做二次曲線實驗:加入 \(x^3, x^4, \dots\) 項到 9 次多項式,畫出擬合線——親眼看看過擬合長什麼樣子。