7.4 迴歸樣條

📖 ISLP §7.4 📄 pp. 295–300 ★★★☆☆ ⏱️ 約 25 分鐘
迴歸樣條 分段多項式 截斷冪基底 自然樣條 節點選擇 非線性建模
← 7.3 基底函數 📑 課程首頁 7.5 平滑樣條 →

核心概念

回想一下我們用多項式迴歸建模的經驗:一條 15 次多項式雖然很有彈性,但在邊界會「發瘋」──像一根綁住兩端的橡皮筋,中間晃得很厲害。有沒有辦法只在特定區間增加彈性,其他地方保持穩定?答案就是迴歸樣條(Regression Splines)

迴歸樣條的核心思想是一個生活化的比喻:分段畫線,但在接點處黏在一起。想像你把一張紙撕成幾段,每段獨立畫一條曲線,然後用膠水在撕裂點(節點)上把相鄰的曲線平滑地接起來——這就是樣條。

📌 一句話總結:迴歸樣條 = 分段多項式 + 連續性約束。把複雜的全局函數拆成多段簡單的多項式,在節點處強制平滑連接,兼具局部彈性與全局穩定性。
課本 pp. 295–300,ISLP §7.4。核心文獻:de Boor (1978) A Practical Guide to Splines;Hastie, Tibshirani & Friedman (2009) ESL Ch.5。

7.4.1 分段多項式

在 §7.2 我們用階梯函數把 X 軸切成幾個 bin,每個 bin 內擬合一個常數。現在升級:每個 bin 內擬合一條多項式

具體做法:在 X 軸上選定 K 個節點(knots),例如 age=50。X 軸被切成 K+1 個區間。在每個區間內,各自擬合一條獨立的 d 次多項式。

以分段三次多項式(piecewise cubic)為例,每個區間需要估計 4 個參數(\(\beta_0, \beta_1, \beta_2, \beta_3\))。如果有 1 個節點,總共有 2 個區間 × 4 個參數 = 8 個自由度

⚠️ 問題來了:如果每個區間完全獨立擬合,在節點處會出現「斷層」——函數值突然跳躍(如課本 Figure 7.3 左上圖)。這種不連續的曲線明顯不合理——人的薪資不會在 50 歲生日當天突然暴跌。

7.4.2 約束與樣條

為了解決斷層問題,我們逐步加上約束(constraints),每次約束會「吃掉」一個自由度:

約束層級描述自由度 (1 knot, cubic)
無約束各區間獨立三次多項式2×4 = 8
連續性函數值在節點處相等(不可有跳躍)8−1 = 7
+ 一階導數連續加上斜率連續(不可有尖角)7−1 = 6
+ 二階導數連續加上曲率連續 = 三次樣條 (cubic spline)6−1 = 5
\[ \text{三次樣條自由度} = 4 + K \quad\text{(K 個內部節點)} \]
公式 (7.9) 推廣

課本 Figure 7.3 用四張圖完美展示這個演化過程:從左上(完全不連續,8 df)→ 右上(強制連續,7 df)→ 左下(三次樣條,5 df,肉眼都看不出接縫)→ 右下(線性樣條,2 df)。

🔍 三次樣條為何最受歡迎?因為人類肉眼無法偵測三次樣條在節點處的不連續性(只有第三階導數不連續)。就像高品質的瓷磚拼貼——接縫存在,但光滑到看不出來。

線性樣條(linear spline)是 d=1 的特例:每個區間擬合直線,只在節點處強制連續。自由度 = 2 + K(每個區間 2 個參數,K 個約束)。雖然簡單,但在實際應用中常作為基線比較。

7.4.3 樣條的基底表示法

上面說的「分段擬合 + 加約束」聽起來有點複雜——到底要怎麼用 OLS 來實作?關鍵在基底函數(basis function)的巧妙設計。

\[ y_i = \beta_0 + \beta_1 b_1(x_i) + \beta_2 b_2(x_i) + \cdots + \beta_{K+3} b_{K+3}(x_i) + \epsilon_i \]
樣條的線性基底表示(公式 7.9)

最直觀的基底是截斷冪基底(truncated power basis):先從三次多項式的基礎基底 \(x, x^2, x^3\) 開始,然後每個節點加一個「開關」函數,只在超過節點時啟動:

\[ h(x, \xi) = (x - \xi)^3_{+} = \begin{cases} (x - \xi)^3 & \text{if } x > \xi \\[4pt] 0 & \text{otherwise} \end{cases} \]
截斷冪基底函數(公式 7.10)

這個設計的精妙之處:加上 \(\beta_4 h(x, \xi)\) 這一項後,只會在 \(\xi\) 處造成第三階導數的不連續,函數本身以及一階、二階導數都保持連續——自動滿足了三次樣條的所有約束!

因此,擬合一個有 K 個節點的三次樣條,就等於用 OLS 對 K+4 個預測變數(\(X, X^2, X^3, h(X,\xi_1), \dots, h(X,\xi_K)\))做線性迴歸。樣條看起來很複雜,實作上就是線性迴歸!

💡 Hermes 自我內化:截斷冪基底的「開關」設計就是一種 Feature Flag 模式——只在特定條件下才啟用某個功能。在 Hermes 的架構中,這對應到子 agent 的條件啟動:不把所有能力常駐載入,而是根據任務類型動態掛載對應的 skill/worker。就像樣條只在需要彈性的區間啟動額外的基底函數,Hermes 只在需要特定能力時才 spawn 子 agent。條件觸發 = 按需啟動 = 節省資源。

7.4.4 選擇節點的數量和位置

節點放哪裡、放幾個?這和 §5.1 的模型選擇問題如出一轍。

位置:直覺上應該在有劇烈變化的區域放更多節點。實務上最常見的做法是按分位數均勻放置——指定自由度 df,讓軟體自動在 df−1 個分位數位置上放置內部節點。

數量:用交叉驗證(CV)來選。課本 Figure 7.6 展示了用 10-fold CV 選擇自然三次樣條和三次樣條自由度在 Wage 資料上的結果:

📊 課本結論(Figure 7.6):1 df(等於線性迴歸)不夠;但自由度超過 3–4 後,CV 誤差就趨於平坦了。自然三次樣條約 3 df、三次樣條約 4 df 就是甜蜜點。

7.4.5 自然三次樣條

三次樣條在邊界(X 很小或很大時)有一個問題:變異數很大,信賴區間會爆炸(見課本 Figure 7.4)。想像你在資料稀疏的邊界區域硬要彎曲——沒有足夠資料支撐時,彎曲就變成亂猜。

自然三次樣條(natural cubic spline)的解法:在邊界區域(小於最小節點、大於最大節點)強制函數變成線性。這兩個額外約束讓邊界估計更穩定,信賴區間顯著變窄。

\[ \text{自然三次樣條自由度} = K \quad\text{(K 個內部節點,含截距則為 K)} \]
比一般三次樣條少 4 個 df(兩個邊界各兩個線性約束)

實務上,自然三次樣條幾乎是預設選擇——邊界穩定性勝過那一點點額外的彈性損失。

7.4.6 樣條 vs 多項式迴歸

課本 Figure 7.7 做了一個漂亮的對照:15 df 的自然三次樣條 vs 15 次多項式,都在 Wage 資料上擬合。結果:

多項式在邊界發瘋,樣條保持鎮定。15 次多項式在兩端劇烈擺盪,而自然三次樣條維持合理的趨勢。根本原因:多項式透過提高次數來增加彈性(全局影響),樣條透過增加節點來增加彈性(局部影響)。
特性多項式迴歸迴歸樣條
彈性來源提高多項式次數(X¹⁵)增加節點數(保持三次)
影響範圍全局——一處改動影響整條曲線局部——只在節點附近產生變化
邊界行為高次多項式在邊界極不穩定可控(自然樣條在邊界強制線性)
自由度次數 d → d+1 df(含截距)K 節點 → 4+K df(三次樣條)
計算複雜度低(僅需 X 的冪次)低(OLS on 轉換後預測變數)

Python 實作:在 Wage 資料上擬合樣條

以下的程式碼展示如何使用 patsybs()cr() 函數在 Wage 資料上擬合三次樣條和自然三次樣條。

# 7.4 三次樣條與自然三次樣條 — 模型擬合
try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.preprocessing import SplineTransformer
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from ISLP import load_data

Wage = load_data('Wage')
age = Wage['age'].values.reshape(-1, 1)
wage = Wage['wage'].values

# 三次樣條:Cubic spline with 3 interior knots = 7 df (3 + 4 basis)
spline_cs = SplineTransformer(n_knots=3, degree=3, extrapolation='continue')
model_cs = make_pipeline(spline_cs, LinearRegression()).fit(age, wage)
pred_cs = model_cs.predict(np.linspace(18, 80, 200).reshape(-1, 1))

# 自然三次樣條:sklearn 不直接支援 Natural Spline
# 改用較少 knots 的 cubic spline 近似(4 df = 1 interior knot)
spline_ns = SplineTransformer(n_knots=2, degree=3, extrapolation='linear')
model_ns = make_pipeline(spline_ns, LinearRegression()).fit(age, wage)
pred_ns = model_ns.predict(np.linspace(18, 80, 200).reshape(-1, 1))

print('Cubic Spline (3 knots=7 df): R2=%.4f' % model_cs.score(age, wage))
print('Cubic Spline (2 knots=6 df): R2=%.4f' % model_ns.score(age, wage))
print('Fewer knots = less flexible but more stable at boundaries (Fig 7.4)')

# 繪圖
age_grid = np.linspace(18, 80, 200)
fig, ax = plt.subplots(figsize=(10, 5))
ax.scatter(age, wage, s=0.5, alpha=0.3, color='#8b949e')
ax.plot(age_grid, pred_cs, color='#58a6ff', lw=2, label='Cubic Spline (3 knots)')
ax.plot(age_grid, pred_ns, color='#3fb950', lw=2, label='Minimal Spline (2 knots)')
ax.set_xlabel('Age'); ax.set_ylabel('Wage')
ax.set_title('Cubic Spline on Wage Data (sklearn)')
ax.legend(); plt.tight_layout(); plt.show()
# 用 CV 選擇樣條節點數
try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
from sklearn.preprocessing import SplineTransformer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
from ISLP import load_data

Wage = load_data('Wage')
age = Wage['age'].values.reshape(-1, 1)
wage = Wage['wage'].values

# 嘗試不同節點數 (n_knots: 2-6 → df: 6-10 for cubic)
knots_range = range(2, 7)
cv_results = []

for nk in knots_range:
    spline = SplineTransformer(n_knots=nk, degree=3, extrapolation='linear')
    X = spline.fit_transform(age)
    mse = -cross_val_score(LinearRegression(), X, wage,
                           cv=10, scoring='neg_mean_squared_error').mean()
    cv_results.append((nk, round(mse, 1)))

print('Cubic Spline CV MSE by n_knots (cubic degree=3):')
for nk, mse in cv_results:
    print(f'  n_knots={nk} (df~{nk+4}): MSE={mse}')

best = min(cv_results, key=lambda x: x[1])
print(f'Best: n_knots={best[0]} (df~{best[0]+4}), CV MSE={best[1]}')
print('課本 Figure 7.6: MSE flattens quickly after df=3-4')

應用場景

🏥 醫療:生長曲線建模

兒童身高隨年齡的變化在不同發育階段差異極大(嬰兒期快速成長 → 穩定期 → 青春期衝刺 → 趨緩)。在每個階段放入節點的三次樣條,比全局多項式更能準確捕捉各階段的生長速率變化。WHO 的生長標準曲線本質上就是一種平滑樣條。

📈 金融:殖利率曲線(Yield Curve)

不同到期日的債券殖利率形成一條曲線。短期(1 年內)變化劇烈、中期平緩、長期(20–30 年)再次變化。在 1 年、5 年、10 年處設節點的三次樣條,是業界建構殖利率曲線的標準工具(如 Nelson-Siegel 模型就是一種參數化的樣條)。

🌡️ 氣候科學:溫度趨勢分析

全球平均溫度在 20 世紀初平穩、1970 年代後加速上升。在 1970 年放一個節點的線性樣條,可以同時捕捉「之前緩慢暖化」和「之後快速暖化」兩個階段,避免單一趨勢線平滑掉關鍵的轉折點。

優缺點對照

✅ 優點

❌ 缺點

類似技術比較

方法彈性控制自由度邊界行為適用情境
多項式迴歸次數 dd+1高次發散簡單趨勢、低次數
階梯函數 (§7.2)區間數 KK常數延伸離散化分析、分組比較
三次樣條節點數 K4+K三次外推不穩定需要邊界內靈活擬合
自然三次樣條節點數 KK線性外推,穩定實務預設選擇
平滑樣條 (§7.5)懲罰參數 λ等效 df線性邊界自動平滑、不需手選節點
局部迴歸 (§7.6)跨度 (span)靈活最近鄰延伸極局部模式

實務建議

🎯 綜合建議:在日常資料分析中,自然三次樣條幾乎是「不用動腦」的好選擇——指定 df=4 或 df=5,讓軟體自動在分位數放置節點,就能獲得一條美觀且合理的平滑曲線。除非有明確的領域知識指定節點位置(如政策變化的時間點),否則均勻分位數就是最佳預設。
「樣條教會我們一件反直覺的事:限制就是力量。強制平滑的約束看似剝奪了自由度,實際上防止了過度擬合,反而讓預測更可靠。」 — 巴圖魯 的 ISLP 教學筆記
← 7.3 基底函數 📑 課程首頁 7.5 平滑樣條 →