🧠 向所有學習者致敬!
“學習不是裝滿一桶水,而是點燃一把火。” —— 葉芝
我的博客主頁: https://lizheng.blog.csdn.net
🌐 歡迎點擊加入AI人工智能社區!
🚀 讓我們一起努力,共創AI未來! 🚀
嘿,朋友們!今天咱們要來搞個大事情——自己動手實現邏輯回歸!為啥要這么干呢?因為有時候,用現成的工具就像吃別人嚼過的糖,沒勁!自己動手,那才叫真本事,不僅能搞懂背后的原理,還能在朋友面前炫耀一番:“看我這代碼,多牛!”所以,咱們這就開始,一起踏上這個充滿挑戰和樂趣的旅程吧!🚀
邏輯回歸
邏輯回歸是一種算法,能幫咱們預測一個變量的兩個類別。現實生活里頭,這玩意兒能干不少事兒,比如:
- 檢測垃圾郵件
- 預測客戶會不會流失
- 預測客戶會不會拖欠貸款
這些用例有個共同點:它們的結果都只有兩種。當然啦,邏輯回歸也有能預測多個結果的類型,不過這篇文章就只講二分類的邏輯。
邏輯回歸的假設(二分類)
你會發現邏輯回歸模型的假設和線性回歸模型很像,因為邏輯回歸也是線性的。
- 二元因變量:目標變量必須只有兩種可能的結果。
- 沒有多重共線性:自變量之間應該沒啥相關性,不然模型的效果會大打折扣。
- 自變量與對數幾率的線性關系:對數幾率是另一種表示概率的方式,形式上就是 log ? ( p / ( 1 ? p ) ) \log(p/(1-p)) log(p/(1?p))。
還有一些對其他模型也很關鍵的假設,比如沒有異常值(如果有,記得處理一下),樣本量要足夠大。雖然沒有明確的樣本量閾值,但記住:樣本越多越好。
搭建我們的邏輯回歸對象
咱們先從創建初始化函數和對象里要用到的屬性開始。你會看到初始化函數里有三個屬性:學習率(learning_rate)、迭代次數(iterations)和測試集比例(test_size)。
在用梯度下降找到模型最優權重的時候,fit
方法會用到學習率和迭代次數這兩個屬性。等咱們講到這一步的時候,還會詳細說說。咱們還加了一個訓練集和測試集劃分的方法,能將特征和目標變量的數據分成訓練集和測試集。
自己動手搭建這些對象的時候,我喜歡把能用的功能都塞進去,這樣后面寫代碼的時候就能更省事兒。
大部分屬性一看就懂,除了 min_
和 max_
。咱們會在這些屬性里存歸一化縮放器的信息。對數據進行歸一化處理,能讓咱們在模型擬合的時候更快地找到解決方案。
import numpy as np
import pandas as pdclass DIYLogisticRegression:def __init__(self, learning_rate=0.01, iterations=1000, test_size=0.2):# 學習率,用于控制梯度下降的步長self.learning_rate = learning_rate# 迭代次數,決定了訓練過程的循環次數self.iterations = iterations# 測試集占總數據集的比例self.test_size = test_size# 權重,初始值為 None,稍后會初始化self.W = None# 偏置項,初始值為 None,稍后會初始化self.b = None# 訓練集特征數據self.X_train = None# 測試集特征數據self.X_test = None# 訓練集目標數據self.y_train = None# 測試集目標數據self.y_test = None# 用于歸一化處理的最小值self.min_ = None# 用于歸一化處理的最大值self.max_ = None
訓練集/測試集劃分與歸一化
要正確地訓練機器學習模型,必須用一個數據集來訓練,另一個數據集來驗證。機器學習生命周期里的另一個關鍵步驟是數據清洗,這可能包括歸一化處理。咱們會創建一個方法,把特征和目標數據分成訓練集和測試集,同時填充相應的屬性。這個方法還會用最大最小值歸一化方法對訓練數據進行縮放,并把同樣的邏輯應用到測試數據上。這就是 min_
和 max_
屬性的用武之地啦。
注意,這些最小值和最大值是從訓練集中得到的,然后應用到測試集上。為啥要這么做呢?假設咱們訓練好了一個模型,把它放到生產環境中,那咱們根本不知道新數據的上下限是多少,所以只能依賴從訓練集中提取出來的邏輯。
def train_test_split_scale(self, X, y):## 訓練集和測試集劃分n_samples = X.shape[0]test_size = int(n_samples * self.test_size)indices = np.arange(n_samples)np.random.shuffle(indices)test_indices = indices[:test_size]train_indices = indices[test_size:]self.X_train, self.X_test = X.iloc[train_indices], X.iloc[test_indices]self.y_train, self.y_test = y.iloc[train_indices], y.iloc[test_indices]# 最大最小值歸一化self.min_ = self.X_train.min()self.max_ = self.X_train.max()self.X_train = (self.X_train - self.min_) / (self.max_ - self.min_)self.X_test = (self.X_test - self.min_) / (self.max_ - self.min_)
Sigmoid 函數
文章前面提到,咱們要搭建的是一個預測二元結果的邏輯回歸模型,背后的原理其實是線性模型:
圖由作者提供
和線性回歸一樣,咱們把線性模型設為等于 (z)。這個變量會被代入 Sigmoid 函數。Sigmoid 函數是邏輯回歸模型做出預測之前的最后一步。這個函數的關鍵特性是,它的輸出值會被限制在 0 和 1 之間。現在知道為啥邏輯回歸要用它了吧?有了 Sigmoid 函數的輸出,通常情況下,如果值大于或等于 0.5,咱們就預測二元變量的結果為 1,否則就是 0。當然啦,這個 0.5 的決策邊界也可以根據具體需求調整。咱們這就把這個函數作為對象的一個方法加進去。
圖由作者提供
def sigmoid(self, z):# Sigmoid 函數,將輸入值映射到 0 和 1 之間return 1 / (1 + np.exp(-z))
最大似然估計和二元交叉熵損失
要理解邏輯回歸,有一個最基本的概念必須得搞清楚,那就是怎么找到特征數據的權重 (W)。這個是通過最大似然估計(MLE)來實現的。MLE 的過程就是找到最能契合咱們數據的權重。一開始我琢磨這個概念的時候,總是在想,找到最優權重是不是就能得到最高的預測準確率呢?其實不是這么回事。最大似然估計函數(咱們要最大化這個函數)的目標是找到最能契合數據內在模式的權重。舉個例子,假設咱們有一條觀測數據,它的特征并沒有明顯顯示出目標變量是 1 還是 0 的模式。那找到的最優權重和偏置就應該預測這個結果接近 0.5。再假設咱們有另一條觀測數據,它的特征明顯和預測結果為 1 的模式強相關,那咱們找到的權重和偏置就應該預測這個觀測的結果接近 1。
所以,知道最大化 MLE 函數能找到最優權重,但它是怎么做到的呢?這就要通過梯度下降這個迭代過程來實現。梯度下降更適合最小化問題,所以咱們會用 MLE 函數的負值,也就是二元交叉熵損失。
在梯度下降的每一步迭代中,咱們都要計算這個損失函數。接下來的部分,咱們會詳細講講怎么做到這一點。
先看看計算這個損失函數的方法。你會注意到,咱們在這個損失函數里加了一個額外的步驟,用到了 np.clip()
函數。這個損失函數很容易得到極小值。
一旦出現這種情況,Python 可能會直接四舍五入到 0,那就會出現對數為 0 的情況,這是未定義的。np.clip()
函數會設置一個極小值下限,這樣咱們就不會得到 0 了。
圖由作者提供
def compute_loss(self, y, y_hat):# 計算二元交叉熵損失epsilon = 1e-10y_hat = np.clip(y_hat, epsilon, 1 - epsilon)return -np.mean(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat))
找到最優權重和偏置
這一步就是咱們要把模型擬合到數據上了。正如前面提到的,這是一個叫梯度下降的迭代過程。一開始,咱們會隨機初始化權重和偏置的值,然后用它們通過線性函數和 Sigmoid 激活函數來預測每一個 (y) 值。這些預測值就叫 (y_{\text{hat}})。有了 (y_{\text{hat}}) 之后,咱們就能通過二元交叉熵損失函數對權重和偏置進行調整,朝著更優的解靠近。具體來說,咱們要對二元交叉熵損失函數分別對權重和偏置求偏導數。
圖由作者提供
圖由作者提供
接下來,咱們把這兩個導數值乘以學習率 (\alpha),然后從當前的權重和偏置中減去。這個過程會不斷重復,直到損失函數的值不再下降。
圖由作者提供
在咱們的 fit
方法里,每迭代 100 次就輸出一次損失函數的值,看看它是不是正確收斂了。如果學習率太高,損失值可能會一直越過最小值,那咱們就永遠也“撞”不到最優權重和偏置了。反過來,如果學習率太小,那可能要經過很多次迭代才能“撞”到最小值。
def fit(self):n_samples, n_features = self.X_train.shape# 隨機初始化權重self.W = np.random.randn(n_features) * 0.01# 初始化偏置為 0self.b = 0for i in range(self.iterations):linear_model = np.dot(self.X_train, self.W) + self.by_hat = self.sigmoid(linear_model)dW = (1 / n_samples) * np.dot(self.X_train.T, (y_hat - self.y_train))db = (1 / n_samples) * np.sum(y_hat - self.y_train)self.W -= self.learning_rate * dWself.b -= self.learning_rate * dbif i % 100 == 0:loss = self.compute_loss(self.y_train, y_hat)print(f"Iteration {i}, Loss: {loss:.4f}")
預測和評估
重頭戲已經搞定了!接下來只要再加幾個方法就行啦。一切都已經準備就緒,咱們可以開始添加預測和評估方法了。首先,咱們會創建一個方法,用擬合過程中找到的最優權重,代入線性函數,再通過 Sigmoid 方法,預測結果的概率。這個方法最終會輸出一個介于 0 和 1 之間的值,也就是咱們對預測結果的概率估計。
接下來,我會再加一個預測方法,它會根據一個閾值,把概率轉換成 1 或者 0。默認的閾值就設為 0.5 吧。
最后,我還會加一個綜合評估方法,打印出混淆矩陣、精確率、召回率,還有咱們模型的整體準確率。注意,這些都是基于咱們的測試集來評估模型性能的。
def predict_proba(self, X):"""預測給定輸入 X 的概率"""linear_model = np.dot(X, self.W) + self.breturn self.sigmoid(linear_model)def predict(self, X, threshold=0.5):"""根據閾值預測類別標簽(0 或 1)"""probabilities = self.predict_proba(X)return (probabilities >= threshold).astype(int)def evaluate(self):"""使用精確率、召回率、準確率和混淆矩陣評估模型"""y_pred = self.predict(self.X_test)tp = np.sum((self.y_test == 1) & (y_pred == 1))fp = np.sum((self.y_test == 0) & (y_pred == 1))fn = np.sum((self.y_test == 1) & (y_pred == 0))tn = np.sum((self.y_test == 0) & (y_pred == 0))precision = tp / (tp + fp) if (tp + fp) > 0 else 0recall = tp / (tp + fn) if (tp + fn) > 0 else 0accuracy = (tp + tn) / (tp + tn + fp + fn)print("混淆矩陣:")print(f"TP: {tp}, FP: {fp}")print(f"FN: {fn}, TN: {tn}\n")print(f"精確率(預測為正的樣本中實際為正的比例): {precision:.4f}")print(f"召回率(所有實際為正的樣本中預測正確的比例): {recall:.4f}")print(f"準確率(所有樣本預測正確的比例): {accuracy:.4f}")
測試我們的對象
咱們來看看這個邏輯回歸對象在實際中的表現吧。為了測試它,咱們用的是 Kaggle 上的 心臟病發作預測數據集。這個數據集有 13 列,其中 12 列是特征,最后一列是目標變量 DEATH_EVENT
。幸好,咱們的對象已經具備了處理數據、訓練模型和評估模型的所有功能。
df = pd.read_csv("heart_failure_clinical_records_dataset.csv") # 加載數據集
X = df.drop('DEATH_EVENT', axis=1)
y = df['DEATH_EVENT']model = DIYLogisticRegression(learning_rate=0.1, iterations=1000, test_size=0.25)
model.train_test_split_scale(X, y)
model.fit()
圖由作者提供
model.evaluate()
還不錯嘛,咱們的模型準確率達到了大約 80%。而且損失函數也收斂得很好,每次迭代的損失值都在下降。咱們來好好聊聊數據和模型吧。這個模型能幫咱們告訴患者他們是否有心臟病發作的風險,所以可別小瞧了它的表現哦。我列出了三個評估指標:精確率、召回率和準確率。你覺得哪個最重要呢?要是咱們告訴某人他有心臟病發作的風險,但實際上他沒有,這算不算壞事呢?我覺得不算,但如果咱們沒告訴某人他有風險,而他其實有,那可就太糟糕了。所以,咱們應該盡量提高召回率,它能告訴我們所有實際為正的樣本中有多少被正確預測了。
和 Sklearn 的邏輯回歸對象對比
咱們成功地從零搭建了一個邏輯回歸對象,現在來看看它和 sklearn 的邏輯回歸類比起來怎么樣吧。
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_scoreX = df.drop('DEATH_EVENT', axis=1)
y = df['DEATH_EVENT']# 訓練集和測試集劃分
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)# 最大最小值歸一化
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)# 初始化并訓練邏輯回歸模型
model = LogisticRegression()
model.fit(X_train, y_train)# 在測試數據上進行預測
y_pred = model.predict(X_test)# 計算評估指標
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
cm = confusion_matrix(y_test, y_pred)# 顯示結果
print("混淆矩陣:")
print(f"TP: {cm[1,1]}, FP: {cm[0,1]}")
print(f"FN: {cm[1,0]}, TN: {cm[0,0]}")
print(f"精確率: {precision:.4f}")
print(f"召回率: {recall:.4f}")
print(f"準確率: {accuracy:.4f}")
總的來說,評估指標都很接近!感謝您閱讀這篇文章!希望您通過這篇文章對邏輯回歸模型有了更深入的理解。