7.3 基底函數(Basis Functions)

📖 ISLP §7.3 📄 pp. 301–302 ★★☆☆☆ ⏱️ 約 20 分鐘
基底函數 非線性建模 線性模型框架 Ch7
← 7.2 階梯函數 📑 課程首頁 7.4 迴歸樣條 →

為什麼需要基底函數?

前兩節我們學了兩種「超越線性」的方法:§7.1 多項式迴歸用 x, x², x³, … 來擬合曲線;§7.2 階梯函數把資料切成分段常數區間。這兩種方法看起來很不一樣,但它們其實共享同一個數學骨架——這就是本節要介紹的 基底函數(basis functions) 框架。

想想看:你想用積木蓋房子。有些積木是長方形的(多項式項)、有些是階梯狀的(分段常數)、還有些是波浪狀的(傅立葉級數)。不管積木的形狀怎麼變,蓋房子的邏輯是一樣的:把積木疊起來,調整每塊積木的高度。基底函數就是這些「積木」——你選好形狀,交給線性迴歸去算出最佳高度。

📚 James, Witten, Hastie, Tibshirani (2023) ISLP §7.3, p.301:
「多項式迴歸與分段常數迴歸都是基底函數方法的特例。核心想法是預先選定一組函數 b₁(X), b₂(X), …, bK(X),然後用這些函數的線性組合來建模,而非直接對 X 建模。」

基底函數的數學框架

基底函數方法的通用模型是:

$$y_i = \beta_0 + \beta_1 b_1(x_i) + \beta_2 b_2(x_i) + \cdots + \beta_K b_K(x_i) + \epsilon_i \tag{7.7}$$

這裡的 bⱼ(·) 是「預先選定」的函數(不是從資料中學出來的),而 βⱼ 是我們要用最小平方法估計的係數。重點來了:

  1. 基底函數是固定的——你選好 b₁, b₂, …, bK 之後就不再調整
  2. 模型對 β 是線性的——雖然 bⱼ(x) 可以是非線性的,但 y 對 β 是線性關係
  3. 第三章的所有推論工具都適用——標準誤、F 檢定、信賴區間,全部可以直接拿來用

這個框架的美妙之處在於:你在做非線性建模,但用的是線性模型的推論引擎。這就是為什麼基底函數方法既靈活又可靠。

📚 歷史脈絡:基底函數(basis functions)的概念源自函數空間理論——就像向量空間有基底向量,函數空間也有基底函數。這個想法最早可追溯到 Fourier(1807)用正弦波來表示任意函數、以及後來的 wavelet 理論(Daubechies, 1992)。ISLP 課本將這個數學工具帶入統計學習的脈絡:選好基底 → 線性擬合 → 得到非線性模型。

已知方法的基底函數表示

特例一:多項式迴歸(§7.1)

把 x 的冪次當作基底:

$$b_j(x_i) = x_i^j, \quad j = 1, 2, \ldots, K$$

這就得到了我們在 §7.1 中的多項式模型:y = β₀ + β₁x + β₂x² + … + βdxᵈ + ε。

特例二:分段常數(階梯函數)(§7.2)

用指示函數(indicator function)當基底:

$$b_j(x_i) = I(c_j \leq x_i < c_{j+1}), \quad j = 1, 2, \ldots, K$$

這表示當 xᵢ 落在第 j 個區間時,bⱼ(xᵢ) = 1;否則為 0。我們在 §7.2 中用它來做分段常數擬合。

圖解:同一框架,不同基底

以下程式碼展示同一組 Wage 資料,用三種基底函數(多項式、分段常數、以及自然樣條作為預告)擬合的結果:

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 ISLP import load_data
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import SplineTransformer

# Load Wage data
wage = load_data('Wage')
X = wage[['age']].values
y = wage['wage'].values

# Sort for plotting
sort_idx = np.argsort(X.ravel())
X_sorted = X[sort_idx]

fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))

# --- Polynomial basis: b_j(x) = x^j ---
poly_deg = 4
X_poly = np.column_stack([X.ravel()**j for j in range(1, poly_deg + 1)])
model_poly = LinearRegression().fit(X_poly, y)
y_poly_pred = model_poly.predict(X_poly[sort_idx])

axes[0].scatter(X.ravel(), y, alpha=0.25, s=8, color='#8b949e')
axes[0].plot(X_sorted, y_poly_pred, color='#58a6ff', lw=2)
axes[0].set_title('Polynomial (d=4)', fontsize=11, color='#f0f6fc')
axes[0].set_xlabel('Age'); axes[0].set_ylabel('Wage (k$)')

# --- Step function basis ---
n_bins = 6
bins = np.linspace(X.min(), X.max(), n_bins + 1)
X_binned = np.zeros((len(X), n_bins))
for j in range(n_bins):
    X_binned[:, j] = ((X.ravel() >= bins[j]) & (X.ravel() < bins[j+1])).astype(float)
X_binned[-1, -1] = 1.0
model_step = LinearRegression().fit(X_binned, y)
y_step_pred = model_step.predict(X_binned[sort_idx])

axes[1].scatter(X.ravel(), y, alpha=0.25, s=8, color='#8b949e')
axes[1].plot(X_sorted, y_step_pred, color='#d2991d', lw=2)
for b in bins[1:-1]:
    axes[1].axvline(b, color='#484f58', ls='--', lw=0.8)
axes[1].set_title('Step Function (6 bins)', fontsize=11, color='#f0f6fc')
axes[1].set_xlabel('Age')

# --- Natural cubic spline basis (preview §7.4) ---
n_knots = 4
spline = SplineTransformer(n_knots=n_knots, degree=3, extrapolation='linear')
X_spline = spline.fit_transform(X)
model_spline = LinearRegression().fit(X_spline, y)
y_spline_pred = model_spline.predict(X_spline[sort_idx])

axes[2].scatter(X.ravel(), y, alpha=0.25, s=8, color='#8b949e')
axes[2].plot(X_sorted, y_spline_pred, color='#3fb950', lw=2)
axes[2].set_title('Natural Cubic Spline (4 knots)', fontsize=11, color='#f0f6fc')
axes[2].set_xlabel('Age')

plt.tight_layout()
plt.savefig('/tmp/basis_demo.png', dpi=120, bbox_inches='tight',
            facecolor='#0d1117', edgecolor='none')
plt.show()

from math import comb
print('Polynomial R2:    %.4f' % model_poly.score(X_poly, y))
print('Step Function R2: %.4f' % model_step.score(X_binned, y))
print('Spline R2:        %.4f' % model_spline.score(X_spline, y))
print('All three are linear in beta — same OLS engine, different basis.')
📚 關鍵洞察 (ISLP §7.3, p.302):
因為模型 (7.7) 對 β 是線性的,我們可以用最小平方法估計未知係數。重要的是,第三章討論的所有線性模型推論工具——係數估計的標準誤、模型整體顯著性的 F 統計量——在這個框架下全部適用。基底函數方法讓你在保有非線性靈活度的同時,仍能享有古典線性迴歸的推論保證。

基底函數的實際應用

🏥 流行病學:年齡分組分析

課本 §7.2 的 Figure 7.2 展示了階梯函數在生物統計學中的經典應用:以 5 年為一組分析疾病發生率。用指示函數當基底,每個年齡組就是一個 bⱼ(X)。這種方法在 cohort study 中極其常見——雖然粗糙,但解釋性強:「40-44 歲族群的心血管疾病風險是 20-24 歲族群的 3.2 倍」。

📈 計量經濟學:Fourier 基底捕捉季節性

課文提到,除了多項式和分段常數之外,傅立葉級數(sine/cosine)和 wavelet 也可以當基底函數。在時間序列分析中,用 sine/cosine 基底來建模季節效應是標準做法——無需手動標記「一月、二月…」,基底函數自動捕捉週期性波動。

🤖 AI Agent 架構設計:基底函數的模組化思維

基底函數框架給我們的啟發遠超出統計:把複雜問題拆成「固定的變換模組 + 線性組合」,是軟體架構中反覆出現的模式。就像 Hermes Agent 系統中的 skill 機制——每個 skill 是一個「基底函數」(固定的能力模組),Agent 根據任務需求動態組合這些模組(相當於學習 β 係數)。這解釋了為什麼 skill 架構既靈活(能處理多樣任務)又可控(每個 skill 的行為是確定的):它本質上是一個基底函數方法!

基底函數方法的優缺點

✅ 優點

⚠️ 缺點

各種基底函數類型比較

基底類型 bⱼ(x) 形式 全域/局部 平滑度 典型應用
多項式 全域 平滑 簡單的非線性趨勢(§7.1)
分段常數 I(cⱼ ≤ x < cⱼ₊₁) 局部 不連續 類別化分析、流行病學(§7.2)
迴歸樣條 截斷冪基底 局部 平滑(可微) 靈活曲線擬合(§7.4)
自然樣條 邊界線性約束的樣條基底 局部 平滑 + 邊界穩定 邊界行為重要的場景(§7.4)
傅立葉級數 sin(ωⱼx), cos(ωⱼx) 全域 平滑(週期性) 時間序列季節性、信號處理
Wavelet ψⱼₖ(x),多尺度局部基底 局部 可調 影像壓縮、突變點偵測
基底函數的威力不在於它多複雜,而在於它把看似不相干的方法——多項式、階梯、樣條、傅立葉——全部收斂到同一個線性模型框架。選對基底,就是選對看世界的透鏡。

下一步:迴歸樣條

基底函數框架給了我們一個統一的語言來描述非線性模型。但問題來了:如何選擇既靈活又穩定的基底? 多項式在邊界不穩定、分段常數缺乏平滑性。§7.4 要介紹的 迴歸樣條(regression splines) 正是解決這個問題的答案——它把區間分段,每段用低階多項式,並在節點處強制平滑連接。這是基底函數框架中最實用的變體。

← 7.2 階梯函數 7.3 基底函數 7.4 迴歸樣條 →