(Day 6) 邏輯迴歸 (多項式 + 正規化)
在上一篇中,我們深入介紹了邏輯迴歸的模型邏輯、損失函數與分類行為。這篇則要進一步延伸這個經典模型,回答一個關鍵問題: 邏輯迴歸能否結合多項式特徵與正規化機制,來對抗非線性與過擬合問題?
在實務中,這樣的需求非常常見,但你可能很少看到「多項式邏輯迴歸」或「正規化邏輯迴歸」這樣的說法。雖然命名不常見,但本質上邏輯迴歸完全可以與這兩個技巧結合使用,而且這種搭配在複雜資料下是極具威力的實務技巧。
為什麼邏輯迴歸可以搭配多項式與正規化?
邏輯迴歸其實是線性模型
邏輯迴歸雖然應用在分類任務f,但本質仍是一種「線性模型」:
$$ \hat{y} = \sigma(\beta_0 + \mathbf{x}^\top \boldsymbol{\beta}) $$
這表示它只能建構一條線性的 decision boundary。當你的資料本身具有非線性邊界時,例如 XOR 類型的資料,這條邏輯迴歸線就顯得力不從心。
解法之一,就是在原始特徵上做多項式擴展 (Polynomial Feature Expansion)——也就是增加特徵空間的非線性組合,例如 $x_1^2$、$x_1 \cdot x_2$ 等,來幫助模型在更高維度中建立線性可分的邊界。
這與之前我們在線性迴歸所談的邏輯迴歸原理一樣,只是這次應用在分類問題中。
邏輯迴歸也容易過擬合
一旦你使用多項式特徵,特徵數暴增,就可能發生過擬合,這時就需要正規化 (Regularization) 機制來抑制模型複雜度。
與 Linear Regression 一樣,邏輯迴歸可以透過 L1 或 L2 懲罰項達到正規化的目的:
- L2 (Ridge): 抑制權重值變得太大
- L1 (Lasso): 推動部分權重變為 0,具有特徵選擇效果
- Elastic Net: L1 + L2 混合調整
值得注意的是,在 PyTorch 中,optimizer 的 weight_decay 只對應 L2,若要做 L1,則需自行加上額外的懲罰項。
模型實作
這個案例也一樣,使用 PyTorch 來實現,透過這段程式碼來窺探 Logistic Regression 的細節。但是還是要再次聲明一下,不論是機器學習演算法,還是說什麼排序的那些算法,你自己寫的打概率打不過這種主流套件做出來的方法,因為這些方法可能經過 10 幾年以上的迭代,不斷地維護與優化產生的,所以如果是學習的話可以自己做,但是正式要使用的話還是建議直接用這些現成的方法,表現往往更加優秀。
回到正題,如前面所說的,因為 Logistic Regression 是先使用 Linear Regression 方程式預測出一個結果,再藉由 Sigmoid 函數逕行轉換。以下是用單層神經網絡,來模擬 Logistic Regression 的演算法,所以只定義 output layer 為線性輸出層 + Sigmoid function。
這段程式碼跟昨天的沒有太大的區別,所以只挑重點說明,上半部就是 PolynomialFeatures 處理的標準寫法,只是說這個案例還有對特徵做 Rescaling,所以會把 PolynomialFeatures 輸出的結果再放到 StandardScaler 去處理。
poly = PolynomialFeatures(degree=2, include_bias=False)
X_train_poly = poly.fit_transform(X_train)
scaler = StandardScaler()
X_train_scaler = scaler.fit_transform(X_train_poly)
這段比較特別的地方是,增加 weight_decay 參數,這個參數就是 SGD 用來實作 L2 Regularization 的機制,至於傳入的數值就是只正規化的 $\lambda$,這個值沒有固定標準,它是個超參數,必須根據實驗調整,這邊就只是用 0.1 做示範。
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.1)
完整程式碼
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import make_classification
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
# 資料產生與前處理
X, y = make_classification(n_samples=100000, n_features=5, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
poly = PolynomialFeatures(degree=2, include_bias=False)
X_train_poly = poly.fit_transform(X_train)
scaler = StandardScaler()
X_train_scaler = scaler.fit_transform(X_train_poly)
# 轉換為 tensor
X_train_tensor = torch.tensor(X_train_scaler, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train.reshape(-1, 1), dtype=torch.float32)
# 模型定義
class LogisticRegressionModel(nn.Module):
def __init__(self, input_dim):
super(LogisticRegressionModel, self).__init__()
self.linear = nn.Linear(input_dim, 1)
self.sigmoid = nn.Sigmoid() # 1 個輸出 (sigmoid 之前)
def forward(self, X):
return self.sigmoid(self.linear(X))
model = LogisticRegressionModel(input_dim=X_train_poly.shape[1])
# 設定 criterion 與 optimizer
criterion = nn.BCELoss()
# weight_decay 是用來實作 L2 Regularization 的機制
# PyTorch 的 optim.SGD、optim.Adam 等 optimizer 本身沒有原生支援 L1 regularization 的核心原因在於以下三點:
## L1 不是 differentiable everywhere (在零點不可微)
## PyTorch 的 weight_decay 設計僅對應 L2 (即 Ridge)
## L1 實作需要手動處理梯度
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.1)
# 訓練
n_epochs = 1000
for epoch in range(n_epochs):
model.train() # 告訴模型進入: 訓練模式
optimizer.zero_grad() # 梯度歸零,預設梯度是累加的 (accumulated),必須在每一回合開始前清空上一輪的梯度,否則梯度會疊加,導致錯誤的參數更新
y_pred = model(X_train_tensor) # 前向傳播 (forward pass)
loss = criterion(y_pred, y_train_tensor) # 使用定義好的損失函數來計算預測值和真實標籤之間的損失
loss.backward() # 反向傳播 (Backward Pass),這些梯度會儲存在每個參數的 .grad 屬性中
optimizer.step() # 根據梯度更新模型參數
if (epoch+1) % 100 == 0:
print(f"Epoch {epoch+1}/{n_epochs}, Loss: {loss.item():.4f}")
# 預測
X_test_poly = poly.fit_transform(X_test)
X_test_scaler = scaler.transform(X_test_poly)
X_test_tensor = torch.tensor(X_test_scaler, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test.reshape(-1, 1), dtype=torch.float32)
model.eval()
with torch.no_grad(): # 停用自動微分 (autograd),不會追蹤張量的計算圖,減少記憶體消耗與加速推論,是測試與部署時的必備寫法,訓練需要追蹤梯度,測試不需要,這行就是明確告訴 PyTorch「這是測試」
# 預測機率
y_train_prob = model(X_train_tensor)
y_test_prob = model(X_test_tensor)
threshold = 0.5
y_train_pred = (y_train_prob > threshold).int().numpy()
y_test_pred = (y_test_prob > threshold).int().numpy()
# 評估
print(f"\n[Train Accuracy] {accuracy_score(y_train, y_train_pred):.2f}")
print(classification_report(y_train, y_train_pred))
print(f"\n[Test Accuracy] {accuracy_score(y_test, y_test_pred):.2f}")
print(classification_report(y_test, y_test_pred))
執行結果
[Train Accuracy] 0.87
precision recall f1-score support
0 0.86 0.87 0.87 40060
1 0.87 0.86 0.86 39940
accuracy 0.87 80000
macro avg 0.87 0.87 0.87 80000
weighted avg 0.87 0.87 0.87 80000
[Test Accuracy] 0.86
precision recall f1-score support
0 0.86 0.87 0.86 9954
1 0.87 0.86 0.86 10046
accuracy 0.86 20000
macro avg 0.86 0.86 0.86 20000
weighted avg 0.86 0.86 0.86 20000
結果評估
這篇就不做結果分析,因為這個案例當初沒有設計好,就直接使用 make_classification() 生成的資料,實際跑起來就會發現跟昨天的結果沒有太大的區別,但這並不表示擴充特徵與正規化沒有價值,反而應該這樣解釋:
- 本資料集本身就不難,線性模型已可擬合大部分樣本
- 多項式與正規化提升了模型的抗干擾性與穩定性
- 若資料更複雜、特徵更多 (如 100+)、樣本更雜,差異才會被放大
結語
Logistic Regression 是一個極為經典但常被低估的模型。很多人在初學時學過它,卻未曾真正理解它的潛力與可擴充性。
這篇的結論非常明確:
- 邏輯迴歸可以處理非線性問題,只要你做了特徵轉換 (如 Polynomial Features)。
- 邏輯迴歸也可以加入正規化項,對抗高維資料的過擬合風險。
- PyTorch 實作只需幾行調整,就能把這些進階能力加進來,落實在真實模型訓練中。
或許在這個範例中,我們沒有看到明顯壓倒性的效能差距,但請記住:
- 特徵擴展與正規化的真正價值,往往不是提升當下效能,而是「讓模型更穩定、可靠且可泛化」,這對實務來說,甚至比準確率還更關鍵。
這也呼應了一個核心理念: 模型不是越複雜越好,而是越穩定、越能抵抗資料波動才是真本事。