前言
從本節開始,我們的機器學習之旅進入了下一個篇章。之前討論的是迴歸算法,迴歸算法主要用於預測數據。而本節討論的是分類問題,簡而言之就是按照規則將數據分類
而要討論的邏輯迴歸,雖然名字叫做迴歸,它要解決的是分類問題
開始探索
scikit-learn
還是老規矩,先來個例子,再討論原理
假設以下場景:一位老哥想要測試他老婆對於抽煙忍耐度,他進行了以下測試
| 星期一 | 星期二 | 星期三 | 星期四 | 星期五 | 星期六 | 星期日 | |
|---|---|---|---|---|---|---|---|
| 抽煙(單位:根) | 6 | 18 | 14 | 13 | 5 | 10 | 8 |
| 是否被老婆打 | 否 | 是 | 是 | 是 | 否 | 是 | 否 |
將以上情形帶入模型
from sklearn.linear_model import LogisticRegression
import numpy as np
X = np.array([6, 18, 14, 13, 5, 10, 8]).reshape(-1, 1)
y = np.array([0, 1, 1, 1, 0, 1, 0])
model = LogisticRegression()
model.fit(X, y)
print(f"係數: {model.coef_[0][0]:.4f}")
print(f"截距: {model.intercept_[0]:.4f}")
decision_boundary = -model.intercept_[0] / model.coef_[0][0]
print(f"決策邊界: {decision_boundary:.2f}")
腳本!啓動:

報告解讀
單特徵影響結果,這明顯是一個線性模型,所以出現了熟悉的係數與截距,還有一個新的參數:決策邊界,這意味着9.1就是分類閾值,>=9.1的結果分類為1,<9.1為0
帶入到情景當中,每天9根煙以上,要被老婆打,否則不打
深入理解邏輯迴歸
與線性迴歸比較
那位大哥説了,怎麼和線性迴歸這麼相似,但是最後又有一點不同
- 邏輯迴歸是將線性迴歸的輸出,再通過函數映射成概率值(0~1之間),再進行分類
- 線性迴歸的損失函數是MSE,而邏輯迴歸的損失函數則是平均交叉熵
- 線性迴歸的迴歸係數算法可以用最小二乘法或者梯度算法(之前沒有介紹過),邏輯迴歸只能用梯度算法
- 還有很多不同,包括但不限:評估模型、使用場景、目標函數等都不一樣
總之,邏輯迴歸雖然也有“迴歸”2字,但是主要還是更適合分類問題
數學模型
邏輯迴歸通過將線性迴歸的輸出映射到概率值(0到1之間),利用Sigmoid函數(或稱邏輯函數)實現分類
w 是權重向量,b是偏置項,X 是輸入特徵向量
通過該函數,把線性方程的值域從\((-\infty,+\infty)\),修改為概率的值域\([0,1]\)
損失函數
與線性迴歸的mse不同,邏輯迴歸使用的損失函數為平均交叉熵
from sklearn.metrics import log_loss
y_proba = model.predict_proba(X)[:, 1]
loss_sklearn = log_loss(y, y_proba)
print('=='*20)
print(f"損失函數(Log Loss): {loss_sklearn:.4f}")

- 值接近0,預測概率接近真實
- 值越大,預測概率錯誤或不確定
- 趨於\(+\infty\),極端錯誤(比如預測為1但是0)
模型評估
-
準確率:顧名思義,分類的準確率
from sklearn.metrics import accuracy_score y_pred = model.predict(X) accuracy = accuracy_score(y, y_pred) print('=='*20) print(f"準確率:{accuracy:.2f}")![watermarked-logistic_regression_1_1]()
-
混淆矩陣:對於一個二分類(二元問題,最後的結果可以用0、1來分類)問題,混淆矩陣是一個 2×2 的矩陣,包含以下四個關鍵指標
- 真正例(TP):模型正確預測為正例的樣本數。比如例子中的“捱打”
- 假負例(FN):模型錯誤預測為正例的樣本數(誤報)。例子中錯誤判斷為“捱打”
- 假正例(FP):模型錯誤預測為負例的樣本數(漏報)。例子中錯誤判斷為“沒有捱打”
- 真負例(TN):模型正確預測為負例的樣本數。比如例子中的“沒有捱打”
[[3 1] # TN=3, FP=1 [1 3]] # FN=1, TP=3from sklearn.metrics import confusion_matrix print('=='*20) print('混淆矩陣:') y_pred = model.predict(X) cm = confusion_matrix(y, y_pred) print(cm)![watermarked-logistic_regression_1_3]()
從混淆矩陣中產生了一系列評估指標:
- 準確率(accuracy):模型預測正確的比例 \(\frac{TP+TN}{TP+TN+FP+FN}\)
- 精確率(precision):預測為正例的樣本中,真實為正例的比例 \(\frac{TP}{TP+FP}\)
- 召回率(recall):真實為正例的樣本中,被正確預測的比例 \(\frac{TP}{TP+FN}\)
- 特異度(specificity):真實為負例的樣本中,被正確預測的比例 \(\frac{TN}{TN+FP}\)
- F1分數:精確率和召回率的調和平均數 \(2⋅\frac{精確率\times召回率}{精確率+召回率}\)
![watermarked-logistic_regression_1_4]()
或者直接使用
classification_report:from sklearn.metrics import classification_report print('=='*20) y_pred = model.predict(X) print("Logistic Regression 分類報告:\n", classification_report(y, y_pred))![watermarked-logistic_regression_1_10]()
-
ROC-AUC
- ROC(受試者工作特徵)曲線與AUC(曲線下面積),在類別不平衡的場景中廣泛使用。所謂類別不平衡,就是在樣本中類別數量差異較大的情況,比如在100w日誌當中,99.9%都是正常的,只有0.1%的日誌是異常的
from sklearn.metrics import roc_curve, roc_auc_score y_proba = model.predict_proba(X)[:, 1] auc_score = roc_auc_score(y, y_proba) print('=='*20) print(f"AUC = {auc_score:.4f}")![watermarked-logistic_regression_1_6]()
- AUC越接近1,表示分類模型泛化能力越好,如果在0.5左右,代表着跟猜的一樣差
import matplotlib.pyplot as plt fpr, tpr, thresholds = roc_curve(y, y_proba) plt.figure(figsize=(6, 5)) plt.plot(fpr, tpr, color='blue', label=f'ROC curve (AUC = {auc_score:.4f})') plt.plot([0, 1], [0, 1], color='gray', linestyle='--') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('ROC Curve') plt.legend() plt.grid(True) plt.tight_layout() plt.show()![watermarked-logistic_regression_1_7]()
直接丟gpt看下吧
![watermarked-logistic_regression_1_8]()
多特徵下的邏輯迴歸
決策邊界
先來討論一下決策邊界,決策邊界是先推導出迴歸係數與截距之後,再帶入模型
如果是單特徵:
取分類閾值為0.5,為什麼要取0.5,大部分情況,二分類中0和1的可能性是均等的,通常任務>0.5為1,反之<0.5則為0。但是遇到所謂的分類不平衡的情況,就要變化了,這個後面再討論,這裏先姑且取0.5
可以看到單特徵的決策邊界是一個點,這就非常容易區分0和1了
如果是2個特徵:
同理\(\hat{y}=0.5\)
可以看到2個特徵的決策邊界是y=x的直線
同理3個特徵是一個面,>3個特徵就已經不能畫出來了
2個特徵
繼續剛才的問題,比如除了抽煙被打,再加上喝酒,2個特徵
| 星期一 | 星期二 | 星期三 | 星期四 | 星期五 | 星期六 | 星期日 | |
|---|---|---|---|---|---|---|---|
| 抽煙(單位:根) | 6 | 18 | 14 | 13 | 5 | 10 | 8 |
| 喝酒(單位:兩) | 8 | 1 | 2 | 4 | 3 | 3 | 0 |
| 是否被老婆打 | 是 | 否 | 否 | 是 | 否 | 是 | 是 |
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
import numpy as np
X = np.array([
[6,8],
[18,1],
[14,2],
[13,4],
[5,3],
[10,3],
[8,0],
])
y = np.array([1, 0, 0, 1, 0, 1, 1])
model = LogisticRegression()
model.fit(X, y)
coef = model.coef_[0]
intercept = model.intercept_[0]
print(f"係數: {coef}")
print(f"截距: {intercept}")

決策邊界:$$ y=\frac{0.127x-0.94}{0.26} $$
import matplotlib.pyplot as plt
x_vals = np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 100)
decision_boundary = -(coef[0] * x_vals + intercept) / coef[1]
plt.figure(figsize=(8, 6))
colors = ['red' if label == 0 else 'blue' for label in y]
plt.scatter(X[:, 0], X[:, 1], c=colors, s=80, edgecolor='k')
plt.plot(x_vals, decision_boundary, 'k--', label='Decision Boundary')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

在邊界以上的是1,邊界以下的0
類別不平衡
比如以下代碼,1000個樣本中,只有14個1,986個0,屬於嚴重的類別不平衡
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=1000, n_features=5,
weights=[0.99], flip_y=0.01,
class_sep=0.5, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
model = LogisticRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
c_report = classification_report(y_test, y_pred, zero_division=0)
print("Logistic Regression 分類報告:\n", c_report)

- precision:模型在識別少數類
1上完全失敗,雖然多數類0的準確率是99%,但是毫無意義,從未正確預測為1 - recall:所有真正為
0的樣本都被找到了(100%);一個1類都沒找到 - f1-score:類別
1的 F1 是 0,説明模型對少數類的預測能力完全崩潰 - support:類別
0有 296 個樣本,類別1只有 4 個樣本 - accuracy:0.99,模型總共預測對了 296 個,錯了 4 個
- macro avg:每個類的指標的“簡單平均”,不考慮樣本數權重
- weighted avg:各類指標的“加權平均”,考慮樣本量
有位彥祖説了,你這分類只分了1次訓練集和測試集,如果帶上交叉驗證,多分幾次類,讓其更有機會學習到少數類,情況能不能有所改善?
from sklearn.model_selection import cross_val_predict
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(model, X, y, cv=cv)
c_report = classification_report(y, y_pred, zero_division=0)
print("Logistic Regression(交叉驗證)分類報告:\n", c_report)

情況並沒有好轉,模型依然無法區分少數類
權重調整
model = LogisticRegression(class_weight='balanced')
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
c_report = classification_report(y_test, y_pred, zero_division=0)
print("Logistic Regression 加權 分類報告:\n", c_report)

情況有所好轉
1的recall從0-->0.5,2 個正類樣本中至少預測中了 1 個1的Precision從0-->0.01,模型預測為正類的樣本大多數是錯的,這是 class_weight 造成的:寧願錯也要猜一猜正類0的recall從1-->0.7,同樣是class_weight造成的,把一部分原本是負類的樣本錯判為正類了- accuracy從99%-->70%,模型開始嘗試預測少數類,雖然整體正確率下降,但變得更願意去預測少數類了
過採樣
增加少數類樣本,複製或生成新樣本,通過 SMOTE(Synthetic Minority Over-sampling Technique)進行過採樣
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
model = Pipeline([
('smote', SMOTE(random_state=0)),
('logreg', LogisticRegression(solver='lbfgs', max_iter=1000))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(model, X, y, cv=cv)
print("SMOTE + LogisticRegression 分類報告:\n")
print(classification_report(y, y_pred, zero_division=0))

- recall提升到了0.64,模型識別了少數類的概率提升了
- Precision=0.04,精確率依舊不佳
- accuracy=0.75,由於少數類的識別概率提升,所以整體的準確率有所提升
欠採樣
減少多數類樣本(隨機刪除或聚類),通過RandomUnderSampler進行欠採樣
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('undersample', RandomUnderSampler(random_state=0)),
('logreg', LogisticRegression(solver='lbfgs', max_iter=1000))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("欠採樣 + LogisticRegression 分類報告:\n")
print(classification_report(y, y_pred, zero_division=0))

與過採樣大同小異,效果還不如過採樣
正則化
lasso與Ridge在這裏依然可以使用
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('smote', SMOTE(random_state=0)),
('lasso', LogisticRegression(penalty='l1', solver='liblinear', max_iter=1000, random_state=0))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("SMOTE + Lasso Logistic Regression(L1)分類報告:\n")
print(classification_report(y, y_pred, zero_division=0))

代價敏感學習
這其實也是其中調整的一種,只不過針對於class_weight這個超參數,進行了更精細化得調整
from imblearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_predict
pipeline = Pipeline([
('smote', SMOTE(random_state=0)),
('lasso', LogisticRegression(class_weight={0: 1, 1: 50}))
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
y_pred = cross_val_predict(pipeline, X, y, cv=cv)
print("class_weight {0:1, 1:50} 分類報告:\n")
print(classification_report(y, y_pred, zero_division=0))
class_weight={0: 1, 1: 50} 的含義:
- 類別 0(多數類)的權重為 1(標準懲罰)
- 類別 1(少數類)的權重為 50(錯誤預測時懲罰更嚴重)

這是一種犧牲準確率為代價,儘量不要漏掉任何一個少數類,所以表現就是少數類1的precision很低,但是recall是非常高的。這就是所謂的寧可錯殺一千,也絕不放過一個
小結
在邏輯迴歸中,針對類別不平衡的問題,往往有兩種決策
- 一種是寧可誤報,也不能漏報。先把少數類找出來,再對少數類進行進一步的校驗。比如預測入侵篩查、代碼漏洞檢測等
- 另外一種則是需要更關注多數類,有少數類被誤報,也是可以接受。比如垃圾郵件分類、推薦系統的準確率等
聯繫我
- 聯繫我,做深入的交流
![]()
至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...






