邏輯迴歸模型 Logistic Regression 詳細推導 (含 Numpy 與PyTorch 實現)
內容概括
邏輯迴歸模型 (Logistic Regression, LR) 是一個二分類模型, 它假設數據服從 Bernoulli 分佈(也稱 0 - 1 分佈), 採用 Sigmoid 函數 σ(x) 將線性迴歸 Y=Xθ 的結果約束在 (0,1) 區間內, 以表示樣本屬於某一類的概率. 之後通過最大似然函數的方法, 對目標函數採用梯度下降法實現對模型參數 θ 的更新. 本質上, LR 模型仍然是一個線性模型.
下面的內容主要是對 LR 進行推導, 按照兩種思路:
- 使用代數法進行推導
- 採用矩陣法進行推導
前者較爲繁瑣, 而後者非常簡潔! 完成推導之後, 再分別使用 Numpy 或者 PyTorch 實現 LR 模型.
LR 模型介紹
符號說明
在介紹 LR 模型之前, 先對本文用到的符號進行說明;
- xi∈Rn×1 表示第 i 個樣本
- yi∈{0,1} 表示第 i 個樣本對應的標籤
- X∈Rm×n 爲由 m 個樣本組成的矩陣
- Y∈Rm×1 爲 X 對應的標籤組成的矩陣
- θ∈Rn×1 爲 LR 模型的權重參數
- E∈Rm×1 爲全 1 向量, 即 [1,1,…,1]T
Sigmoid 函數
Sigmoid 函數定義爲:
σ(x)=1+exp(−x)1
其函數圖像如下:
當 x→+∞ 時, σ(x)→1; 而當 x→−∞ 時, σ(x)→0. 由於 σ(x) 的取值範圍在 (0,1) 區間內, 因此可以用來表示概率的大小.
另外一個關於 Sigmoid 函數的有用性質是, 對其求導的結果可以用它的輸出值來表示, 即:
σ′(x)=σ(x)(1−σ(x))
具體推導過程如下:
σ′(x)=(1+exp(−x)1)′=−(1+exp(−x))21⋅exp(−x)⋅(−x)′=1+exp(−x)1⋅1+exp(−x)exp(−x)=σ(x)(1−σ(x))
LR 模型
對樣本 x∈Rn×1, 設其類別爲 y, 線性迴歸模型的參數設爲 θ∈Rn×1, 使用 Sigmoid 函數將線性模型的結果 xTθ 進行轉換, 便得到了二元邏輯迴歸的一般形式:
hθ(x)=1+exp(−xTθ)1
可以用 hθ(x) 表示分類的概率, 如果 hθ(x)>0.5, 那麼可以認爲樣本 x 的類別爲 y=1; 若 hθ(x)<0.5, 則認爲樣本 x 的類別爲 y=0. 如果 hθ(x) 剛好等於 0.5, 即此時 x 剛好等於 0, 模型無法判斷樣本的具體類別, 但具體實現時, 一般將等於 0.5 的情況加入到前面兩種情況之一中.
將二元邏輯迴歸寫成矩陣的形式:
hθ(X)=1+exp(−Xθ)1
其中 X=[x1,x2,…,xm]T∈Rm×n 爲樣本的輸入特徵矩陣, 參數 θ∈Rn×1, 那麼輸出結果 hθ(X)∈Rm×1.
LR 模型的優化目標
似然函數與損失函數
對於輸入樣本 (x,y), 利用 LR 模型可以得到它屬於某一類的概率分別爲:
P(y=1∣x,θ)P(y=0∣x,θ)=hθ(x)=1−hθ(x)
將兩個式子合併爲一個式子, 可以表示如下:
P(y∣x,θ)=hθ(x)y(1−hθ(x))(1−y)
對於樣本集 T={(xi,yi)}i=1m, 其似然函數可以表示爲:
J(θ)=i=1∏mhθ(xi)yi(1−hθ(xi))(1−yi)
如果採用似然函數最大化進行優化, 求的是最大值, 如果取負, 相當於優化方向是進行最小化目標, 之所以取負, 是因爲一般機器學習中我們的優化目標是最小化損失函數, 通常採用梯度下降法來求解, 原因是梯度的負方向函數值下降最快. 不取負也是可以的, 但是在更新模型參數的時候, 需要做簡單的修改. 這裏按照慣例來.
其對數似然函數(代數形式)取負爲:
L(θ)=−i=1∑m[yiloghθ(xi)+(1−yi)log(1−hθ(xi))]=−i=1∑m[yilog1−hθ(xi)hθ(xi)+log(1−hθ(xi))]=−i=1∑m[yilogexp(−xiTθ)1+log(1−hθ(xi))]=−i=1∑m[yilogexp(−xiTθ)1+log(1−1+exp(−xiTθ)1)]=−i=1∑m[yilogexp(−xiTθ)1+log(1+exp(−xiTθ)exp(−xiTθ))]=−i=1∑m[yilogexp(−xiTθ)1+log(1+exp(xiTθ)1)]=−i=1∑m[yi(xiTθ)−log(1+exp(xiTθ))]
如果用矩陣來表示 L(θ), 那麼結果爲:
L(θ)=−(YTXθ−ETlog(E+exp(Xθ)))
其中 E∈Rm×1 爲全 1 向量, 即 [1,1,…,1]T.
模型參數更新 – 代數法求梯度
這一小節使用代數法求二元邏輯迴歸的梯度, 相對繁瑣; 而使用矩陣法求解在形式上更爲簡潔, 但是理解上有一定的門檻.
前面得到了損失函數爲:
L(θ)=−i=1∑m[yi(xiTθ)−log(1+exp(xiTθ))]
那麼 ∂θj∂L(θ) 的結果爲:
∂θj∂L(θ)=−∂θj∂i=1∑m[yi(xiTθ)−log(1+exp(xiTθ))]=−i=1∑m[yixij−1+exp(xiTθ)1⋅exp(xiTθ)⋅xij]=−i=1∑m[yi−1+exp(−xiTθ)1]xij=−i=1∑m[yi−hθ(xi)]xij=i=1∑m[hθ(xi)−yi]xij
那麼 ∂θ∂L(θ)=[∂θ1∂L(θ),∂θ2∂L(θ),…,∂θn∂L(θ)]T=XT(hθ(X)−Y)
其中 ∂θ∂L(θ)∈Rn×1, XT∈n×m, (hθ(X)−Y)∈Rm×1
模型參數更新 – 矩陣法求梯度
這裏採用矩陣法來求梯度, 根據前面得到的損失函數的矩陣形式爲:
L(θ)=−(YTXθ−ETlog(E+exp(Xθ)))
其中 E∈Rm×1 爲全 1 向量.
在求解之前, 需要了解關於矩陣導數與微分以及跡的關係, 詳情可以參考: 矩陣求導術(上)
其中需要用到的是:
- df=i=1∑mj=1∑n∂Xij∂fdXij=tr(dXdfTdX)
- 若 x 爲向量, 那麼 df=dxdfTdx
- 逐元素函數: dσ(X)=σ′(X)⊙dX, 其中 σ(X)=[σ(Xij)] 是逐元素標量函數運算, σ′(X)=[σ′(Xij)] 是逐元素求導數.
- 矩陣乘法/逐元素乘法交換: tr(AT(B⊙C))=tr((A⊙B)TC), 其中 A,B,C 尺寸相同, 兩邊都等於 ij∑AijBijCij
因此推導如下:
dL(θ)=−(YTXθ−ETlog(E+exp(Xθ)))=−(dYTXθ+YTdXθ+YTXdθ−dETlog(E+exp(Xθ))−ETdlog(E+exp(Xθ)))=−(YTXdθ−ETdlog(E+exp(Xθ)))=−(YTXdθ−ET(E+exp(Xθ)1⊙d(E+exp(Xθ))))=−(YTXdθ−(E⊙E+exp(Xθ)1)Td(E+exp(Xθ)))=−(YTXdθ−(E⊙E+exp(Xθ)1)T(exp(Xθ)⊙d(Xθ)))=−(YTXdθ−(E⊙E+exp(Xθ)1⊙exp(Xθ))Td(Xθ))=−(YTXdθ−hθ(X)Td(Xθ))=−(YTXdθ−hθ(X)T(dXθ+Xdθ))=−(YTXdθ−hθ(X)TXdθ)=−([YT−hθ(X)T]Xdθ)=[hθ(X)−Y]TXdθ
由矩陣求導公式 2, 即若 x 爲向量, 那麼 df=dxdfTdx, 那麼可以得到:
∂θ∂L(θ)=XT(hθ(X)−Y)
(吐槽: 打完這些公式也太累了吧… 另外我前面說矩陣法求更簡潔, 是在沒有打這些公式的情況下說的, 現在弄完這些公式, 感覺也不簡潔 … 🤣🤣🤣)
參數更新
使用梯度下降法對 θ 的更新公式爲:
θ=θ−α∂θ∂L(θ)=θ−αXT(hθ(X)−Y)
LR Numpy 代碼實現
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from collections import Counter
def sigmoid(x):
x = np.array(x)
return 1. / (1. + np.exp(-x))
def L(w, b, X, y):
dot = np.dot(X, w) + b
return np.mean(y * dot - np.log(1 + np.exp(dot)), axis=0)
def dL(w, b, X, y):
dot = np.dot(X, w) + b
distance = y - sigmoid(dot)
distance = distance.reshape(-1, 1)
return np.mean(distance * X, axis=0), np.mean(distance, axis=0)
def sgd(w, b, X, y, epoch, lr):
for i in range(epoch):
dw, db = dL(w, b, X, y)
w += lr * dw
b += lr * db
return w, b
def predict(w, b, X_test):
return sigmoid(np.dot(X_test, w) + b) >= 0.5
def plot_surface(X, y, w, b):
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))
X_test = np.c_[xx.ravel(), yy.ravel()]
Z = predict(w, b, X_test)
Z = Z.reshape(xx.shape)
fig, ax = plt.subplots()
counter = Counter(y)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
ax.set_xlabel(feature_names[0])
ax.set_ylabel(feature_names[1])
ax.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu)
for label in counter.keys():
ax.scatter(X[y==label, 0], X[y==label, 1])
plt.show()
iris = load_iris()
X = iris.data[:100, :2]
y = iris.target[:100]
feature_names = iris.feature_names[2:]
np.random.seed(123)
n = X.shape[1]
w = np.random.randn(n)
b = np.random.randn(1)
print('initial: w: {}, b: {}, L: {}'.format(w, b, L(w, b, X, y)))
w, b = sgd(w, b, X, y, 10000, 0.001)
print('final: w: {}, b: {}, L: {}'.format(w, b, L(w, b, X, y)))
plot_surface(X, y, w, b)
效果:
LR 的 PyTorch 實現
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
iris = load_iris()
X = iris.data[:100, :2]
y = iris.target[:100]
X = torch.tensor(X).float()
y = torch.tensor(y).float()
feature_names = iris.feature_names[2:]
class LR(nn.Module):
def __init__(self, in_features):
super(LR, self).__init__()
self.linear = nn.Linear(in_features, 1, bias=True)
def sigmoid(self, x):
return 1. / (1 + torch.exp(-x))
def predict(self, x):
return (self(x) > 0.5).int()
def forward(self, x):
x = self.linear(x)
x = self.sigmoid(x)
return x
model = LR(X.size(1))
criterion = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
epoch = 100
batch_size = 10
N = X.size(0)
for i in range(epoch):
order = torch.randperm(N)
X = X[order]
y = y[order]
for n in range(N // batch_size):
input = X[n * batch_size : (n + 1) * batch_size]
label = y[n * batch_size : (n + 1) * batch_size]
optimizer.zero_grad()
output = model(input)
loss = criterion(output, label)
loss.backward()
optimizer.step()
if n % 100 == 0:
print(loss)
def plot_surface(X, y, w, b):
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))
X_test = np.c_[xx.ravel(), yy.ravel()]
Z = predict(w, b, X_test)
Z = Z.reshape(xx.shape)
fig, ax = plt.subplots()
counter = Counter(y)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
ax.set_xlabel(feature_names[0])
ax.set_ylabel(feature_names[1])
ax.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu)
for label in counter.keys():
ax.scatter(X[y==label, 0], X[y==label, 1])
plt.show()
def sigmoid(x):
x = np.array(x)
return 1. / (1. + np.exp(-x))
def predict(w, b, X_test):
return sigmoid(np.dot(X_test, w) + b) >= 0.5
X = torch.tensor(X).float()
y = torch.tensor(y).float()
x = X.numpy()
t = y.numpy()
w = model.linear.weight.data.numpy().transpose()
b = model.linear.bias.data.numpy()[0]
plot_surface(x, t, w, b)
效果:
參考文獻
- 邏輯迴歸原理小結: 劉建平Pinard 的博客, 大佬, 博文讓人受益匪淺
- 矩陣求導術(上): 感覺這是機器學習裏的內功祕籍啊, 分爲上下兩卷, 我今天研讀了上卷, 那種心情怎麼來描述呢, 大徹大悟 ? No, 還沒到出家的地步 😂😂😂. 總之, 彷彿腦袋中有個燈泡💡突然亮起來了的感覺.