(Day 6) 邏輯迴歸 (多項式 + 正規化)

(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 實作只需幾行調整,就能把這些進階能力加進來,落實在真實模型訓練中。

或許在這個範例中,我們沒有看到明顯壓倒性的效能差距,但請記住:

  • 特徵擴展與正規化的真正價值,往往不是提升當下效能,而是「讓模型更穩定、可靠且可泛化」,這對實務來說,甚至比準確率還更關鍵。

這也呼應了一個核心理念: 模型不是越複雜越好,而是越穩定、越能抵抗資料波動才是真本事。

備註