動態

詳情 返回 返回

彩筆運維勇闖機器學習--邏輯迴歸 - 動態 詳情

前言

從本節開始,我們的機器學習之旅進入了下一個篇章。之前討論的是迴歸算法,迴歸算法主要用於預測數據。而本節討論的是分類問題,簡而言之就是按照規則將數據分類

而要討論的邏輯迴歸,雖然名字叫做迴歸,它要解決的是分類問題

開始探索

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}")

腳本!啓動:

watermarked-logistic_regression_1_1

報告解讀

單特徵影響結果,這明顯是一個線性模型,所以出現了熟悉的係數與截距,還有一個新的參數:決策邊界,這意味着9.1就是分類閾值,>=9.1的結果分類為1,<9.1為0

帶入到情景當中,每天9根煙以上,要被老婆打,否則不打

深入理解邏輯迴歸

與線性迴歸比較

那位大哥説了,怎麼和線性迴歸這麼相似,但是最後又有一點不同

  • 邏輯迴歸是將線性迴歸的輸出,再通過函數映射成概率值(0~1之間),再進行分類
  • 線性迴歸的損失函數是MSE,而邏輯迴歸的損失函數則是平均交叉熵
  • 線性迴歸的迴歸係數算法可以用最小二乘法或者梯度算法(之前沒有介紹過),邏輯迴歸只能用梯度算法
  • 還有很多不同,包括但不限:評估模型、使用場景、目標函數等都不一樣

總之,邏輯迴歸雖然也有“迴歸”2字,但是主要還是更適合分類問題

數學模型

邏輯迴歸通過將線性迴歸的輸出映射到概率值(0到1之間),利用Sigmoid函數(或稱邏輯函數)實現分類

\[\hat{y} = \sigma(z) = \frac{1}{1 + e^{-z}} \quad , z = \mathbf{w}^\top \mathbf{x} + b \]

w 是權重向量,b是偏置項,X 是輸入特徵向量

\[z \to \infty,\sigma(z) \to 1 \]

\[z \to -\infty,\sigma(z) \to 0 \]

通過該函數,把線性方程的值域從\((-\infty,+\infty)\),修改為概率的值域\([0,1]\)

損失函數

與線性迴歸的mse不同,邏輯迴歸使用的損失函數為平均交叉熵

\[\mathcal{L} = - \frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log(\hat{y}^{(i)}) + (1 - y^{(i)}) \log(1 - \hat{y}^{(i)}) \right] \]

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}")

watermarked-logistic_regression_1_5

  • 值接近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=3
    
    from 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

多特徵下的邏輯迴歸

決策邊界

先來討論一下決策邊界,決策邊界是先推導出迴歸係數與截距之後,再帶入模型

\[\hat{y} = \sigma(z) = \frac{1}{1 + e^{-z}} \quad , z = \mathbf{w}^\top \mathbf{x} + b \]

如果是單特徵:

\[\hat{y} = \sigma(w_1x_1+b) = \frac{1}{1 + e^{-(w_1x_1+b)}} \quad \]

取分類閾值為0.5,為什麼要取0.5,大部分情況,二分類中01的可能性是均等的,通常任務>0.5為1,反之<0.5則為0。但是遇到所謂的分類不平衡的情況,就要變化了,這個後面再討論,這裏先姑且取0.5

\[\frac{1}{1 + e^{-(w_1x_1+b)}} = 0.5 \quad \]

\[e^{-(w_1x_1+b)} = 1 \quad \]

\[-(w_1x_1+b) = 0 \quad \]

\[x_1 = -\frac{b}{w_1} \quad \]

可以看到單特徵的決策邊界是一個點,這就非常容易區分01

如果是2個特徵:

\[\hat{y} = \sigma(w_1x_1+w_2x_2+b) = \frac{1}{1 + e^{-(w_1x_1+w_2x_2+b)}} \quad \]

同理\(\hat{y}=0.5\)

\[\frac{1}{1 + e^{-(w_1x_1+w_2x_2+b)}} = 0.5 \quad \]

\[x_2=-\frac{w_1x_1+b}{w_2} \]

可以看到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}")

watermarked-logistic_regression_1_9

決策邊界:$$ 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()

watermarked-logistic_regression_1_11

在邊界以上的是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)

watermarked-logistic_regression_1_12

  • 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)

watermarked-logistic_regression_1_13

情況並沒有好轉,模型依然無法區分少數類

權重調整

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)

watermarked-logistic_regression_1_14

情況有所好轉

  • 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))

watermarked-logistic_regression_1_15

  • 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))

watermarked-logistic_regression_1_16

與過採樣大同小異,效果還不如過採樣

正則化

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))

watermarked-logistic_regression_1_17

代價敏感學習

這其實也是其中調整的一種,只不過針對於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(錯誤預測時懲罰更嚴重)

watermarked-logistic_regression_1_18

這是一種犧牲準確率為代價,儘量不要漏掉任何一個少數類,所以表現就是少數類1的precision很低,但是recall是非常高的。這就是所謂的寧可錯殺一千,也絕不放過一個

小結

在邏輯迴歸中,針對類別不平衡的問題,往往有兩種決策

  • 一種是寧可誤報,也不能漏報。先把少數類找出來,再對少數類進行進一步的校驗。比如預測入侵篩查、代碼漏洞檢測等
  • 另外一種則是需要更關注多數類,有少數類被誤報,也是可以接受。比如垃圾郵件分類、推薦系統的準確率等

聯繫我

  • 聯繫我,做深入的交流

至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...

Add a new 評論

Some HTML is okay.