(Day 4) 正規化迴歸 (Regularization Regression)
延續昨日的多項式迴歸中,我們觀察到一個現象: 雖然二次特徵提升了模型的表現,但同時也引入過擬合 (Overfitting) 風險。這是因為當特徵數量暴增,模型就會變得過於「貪婪」,試圖將每個資料點都擬合得極好,結果反而喪失了在新資料上的泛化 (Generalization) 能力。
那怎麼辦? 就是在多項式迴歸的基礎上,限制模型的自由度,也就是今天要介紹的——正則化回歸 (Regularized Regression)。
這是一種透過在模型參數加上限制,以提升泛化能力 (該操作並非為了提高準確度),讓它在「解釋資料」與「控制複雜度」間取得平衡。最常見的三種正則化技術分別為:
- 套索回歸 (Lasso Regression): L1 Normalization
- 脊回歸 (Ridge Regression): L2 Normalization
- Elastic Net Regression: L1 + L2 Normalization
模型介紹
模型邏輯與核心概念
先回到 Day 2 的線性迴歸,線性迴歸如何找出最佳的迴歸線?
- 先設定損失函數 (Cost Function) 假設為 $MSE = \frac{1}{2n} \sum\limits_{i=1}^{n} (y_{i} - \hat{y}_{i})^{2}$。
- 再使用梯度下降 (Batch Gradient Descent) 來最小化損失函數。
而所謂的正規化迴歸就是在損失函數加上懲罰項,而前述那些不同的正規化迴歸名稱,就只是懲罰項的差異而已,以下是正規化迴歸的懲罰項:
- 套索迴歸: $\lambda \sum |\beta_i|$
- 脊迴歸: $\lambda \sum \beta_i^2$
- Elastic Net Regression: $\lambda_1 \sum |\beta_i| + \lambda_2 \sum \beta_i^2$
我們先來看看這幾種正規化的效果差異:
正規化迴歸 | 特性 |
---|---|
套索回歸 | 1. 適用高維度資料 (p » n) 2. 可以自動選擇重要特徵 |
脊回歸 | 1. 適合處理共線性問題 2. 解具封閉形式 (可解析求解) |
Elastic Net Regression | 1. 同時具備: 特徵選擇 (L1) + 稳定性 (L2),可在 L1 與 L2 間彈性調整,實務表現常優於純 Lasso 或 Ridge 2. 適合特徵高度相關、且數量多的資料集 |
一般來說,比較常用的會是脊回歸 (Ridge Regression),不會是 Elastic Net Regression,因為會考量模型的複雜度,Elastic Net Regression 很難去做超參數的實驗。
適用情境
- 訓練誤差低,但測試誤差高 (Overfitting)
- 權重變化劇烈,解釋困難
- 高維資料下,模型不穩定
限制條件
- 套索回歸
- 具備特徵選擇功能 (feature 可能會被壓縮為 0)
- 解不具封閉解 (需使用坐標下降等方法)
- 當特徵高度相關時,會隨機選擇其中一個而非平均分配
- 脊回歸
- 不具備特徵選擇功能 (feature 不會被壓縮為 0)
- Elastic Net Regression
- 超參數實驗複雜
模型實作
資料集介紹
將使用經典的 Boston Housing Dataset 為例。由於 scikit-learn 已移除該資料集,我們改採自 Carnegie Mellon University 所提供的公開版本。樣本內容如下:
欄位說明:
- CRIM: 每人平均犯罪率
- ZN: 區域住宅用地比例
- INDUS: 區域非零售商業用地比例
- CHAS: 查爾斯河虛擬變數 (1 = 河流旁, 0 = 其他)
- NOX: 一氧化氮濃度 (parts per 10 million)
- RM: 每個住宅的平均房間數
- AGE: 1940 年之前建造的自用住宅比例
- DIS: 到波士頓五個中心區域的加權距離
- RAD: 公路接近指數 (1 = 最接近, 24 = 最遠)
- TAX: 每 $10,000 的財產稅率
- PTRATIO: 學生與教師比例
- B: 1000(Bk - 0.63)^2, Bk = 區域黑人比例
- LSTAT: 區域人口中低收入者的比例
- MEDV: 自用住宅的中位數價格 (單位: $1000s)
程式實例
import requests
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error, r2_score
def get_boston_housing_data() -> pd.DataFrame:
url = "http://lib.stat.cmu.edu/datasets/boston" # Boston Housing Dataset from Carnegie Mellon University
raw_data = requests.get(url).text.splitlines()[22:] # 從第 23 行開始
# 每筆資料分為 2 行 → 共 506 筆 → 總共 1012 行
data = []
for i in range(0, len(raw_data), 2):
line_1 = list(map(float, raw_data[i].strip().split()))
line_2 = list(map(float, raw_data[i + 1].strip().split()))
data.append(line_1 + line_2)
column_names = [
"CRIM", # 每人平均犯罪率
"ZN", # 區域住宅用地比例
"INDUS", # 區域非零售商業用地比例
"CHAS", # 查爾斯河虛擬變數 (1 = 河流旁, 0 = 其他)
"NOX", # 一氧化氮濃度 (parts per 10 million)
"RM", # 每個住宅的平均房間數
"AGE", # 1940 年之前建造的自用住宅比例
"DIS", # 到波士頓五個中心區域的加權距離
"RAD", # 公路接近指數 (1 = 最接近, 24 = 最遠)
"TAX", # 每 $10,000 的財產稅率
"PTRATIO", # 學生與教師比例
"B", # 1000(Bk - 0.63)^2, Bk = 區域黑人比例
"LSTAT", # 區域人口中低收入者的比例
"MEDV", # 自用住宅的中位數價格 (單位: $1000s)
]
df = pd.DataFrame(data, columns=column_names)
return df
def main():
# ----- 讀取資料 -----
original_data = get_boston_housing_data() # 讀取資料
# ----- 資料前處理 -----
cleaned_data = original_data.copy()
## 因透過 print(cleaned_data.isnull().sum()) 檢查缺失值 (0 筆)
## 且 Pearson 相關係數檢查,相關係數 >= 0.8 (0 筆)
### correlation_matrix = cleaned_data.corr()
### target_corr = correlation_matrix['MEDV']
### high_corr_features = target_corr[target_corr > 0.8].drop('MEDV')
### print(f"與 target 高度正相關的欄位:", high_corr_features)
## 故不做資料前處理
# ----- 模型訓練 -----
## 資料分割
X = cleaned_data.drop(columns=["MEDV"])
y = cleaned_data["MEDV"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
## 建立模型
model = Pipeline([
("poly", PolynomialFeatures(degree=2, include_bias=False)),
("scaler", StandardScaler()),
("lr", Ridge())
])
model.fit(X_train, y_train)
## 預測
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)
## 評估
train_mse = mean_squared_error(y_train, y_train_pred)
train_r2 = r2_score(y_train, y_train_pred)
print(f"train data 均方誤差 (MSE): {train_mse:.4f}")
print(f"train data 決定係數 R²: {train_r2:.4f}")
test_mse = mean_squared_error(y_test, y_test_pred)
test_r2 = r2_score(y_test, y_test_pred)
print(f"test data 均方誤差 (MSE): {test_mse:.4f}")
print(f"test data 決定係數 R²: {test_r2:.4f}")
if __name__ == '__main__':
main()
執行結果
- train data 均方誤差 (MSE): 6.9778
- train data 決定係數 R²: 0.9197
- test data 均方誤差 (MSE): 11.2069
- test data 決定係數 R²: 0.8472
結果評估
先回顧昨天多項式迴歸的結果:
- train data 均方誤差 (MSE): 5.1315
- train data 決定係數 R²: 0.9409
- test data 均方誤差 (MSE): 14.2573
- test data 決定係數 R²: 0.8056
整體而言,Ridge 模型透過引入懲罰項,確實有效約束了參數大小,抑制模型過度依賴特定特徵,成功降低過擬合現象,這就是典型的 bias-variance tradeoff 成功案例: 增加 bias,降低 variance,整體泛化誤差下降。
下一步建議
如果接下來想要進一步優化模型效能與可解釋性,建議可從以下幾點推進:
- 自動化正則化強度 (alpha) 調整: 目前 Ridge() 採用預設 $\alpha=1$,並非最佳選擇,需要透過實驗尋找最適 $\alpha$ 值。
- 嘗試其他正則化方式: 使用 Lasso Regression 或 Elastic Net Regression 看是否有更好的效果。
- 特徵工程精緻化: 目前直接使用 PolynomialFeatures 擴充所有變數的二次項,可能引入過多冗餘項,也許可以搭配 SelectKBest 或 Recursive Feature Elimination (RFE) 精簡高維特徵空間。
- 引入模型穩定性驗證: 可已加入 k-fold cross-validation 評估模型在不同資料分割下的穩定性。
結語
這篇是迴歸模型系列的首尾,補上了最後一塊重要拼圖「正則化」。這不只是技術手段,更是一種建模哲學: 我們不再一味追求最小誤差,而是學會控制模型自由度,讓模型在複雜度與泛化能力間取得平衡。
正則化迴歸讓我們能在特徵維度增長、模型變得脆弱時,透過懲罰機制維持穩定與解釋性。這一策略並非萬靈丹,但卻是進入高維度問題、開始關注模型可解釋性與風險管理時不可或缺的工具。
至此,我們已經完成線性迴歸、多項式擴展與正則化等主流回歸方法。這些模型的共通點在於它們假設輸入與輸出之間存在線性或可線性擴展的關係。下一階段,我們將把焦點移到分類問題的建模。