邏輯回歸是一種解決分類問題的機(jī)器學(xué)習(xí)算法。
邏輯回歸的思想是將樣本特征和樣本發(fā)生的概率聯(lián)系起來,概率是一個0到1之間的數(shù)。線性回歸中,通過回歸方程可以得到一個預(yù)測值
而在邏輯回歸中,通過回歸函數(shù)得到的應(yīng)該是一個概率值

p是一個概率,可以通過p去對應(yīng)正事件或負(fù)事件,例如

可以表示當(dāng)p>0.5時,得到的樣本預(yù)測值對應(yīng)正事件,反之對應(yīng)負(fù)事件。
邏輯回歸可以視為回歸算法也可以視為分類算法,但通常用于分類,######但只適用于二分類問題!
對于線性回歸

θ是θ0到θn的系數(shù)集合,Xb是在原有數(shù)據(jù)特征的基礎(chǔ)上增加X0=1的一列之后得到的矩陣。通過f(X)得到的結(jié)果是一個在-∞ -> +∞之間的一個值,但是在邏輯回歸中,我們需要的p是一個介于0和1之間的值,因此,我們需要對原有的回歸函數(shù)做做手腳。

這個函數(shù)稱為sigmoid函數(shù)。sigmoid函數(shù)表達(dá)式

t就是通過原有的回歸函數(shù)得到的值,經(jīng)過sigmoid函數(shù)的轉(zhuǎn)化變成一個介于0到1之間的值。反映到代碼中:
def sigmoid(t):
return 1 / (1 + np.exp(-t))
x = np.linspace(-10, 10, 500)
y = sigmoid(x)
plt.plot(x, y)
plt.show()
繪制出sigmoid函數(shù)對應(yīng)的曲線。

當(dāng)sigmoid函數(shù)值t為0時,函數(shù)值為0.5,t越大,函數(shù)值越趨近于1,t越小越趨近于0。
邏輯回歸的損失函數(shù)
通過之前的講解,當(dāng)?shù)玫降念A(yù)測概率p大于0.5時,得到的預(yù)測值為1,否則為0。損失函數(shù)反應(yīng)的是預(yù)測值和實際值之間的偏差情況,假設(shè)實際值為1,因為預(yù)測值根據(jù)預(yù)測概率得到,因此預(yù)測概率p越小,損失函數(shù)越大,因為p越小,低于0.5的概率就越高,以此得到的實際值為0的可能性月就越高,離實際的偏差也就越大。同樣的,如果實際值為0,那么p越大,損失函數(shù)值就越大,道理同上。

給出邏輯回歸的損失函數(shù)

下圖是y=-log(x)的圖像

因為p是一個0到1之間的值,因此橫軸一下的部分可以不要,得到樣本實際值y=1時的損失函數(shù)圖像

對于y=0的情況,繪出相應(yīng)的曲線

也是只取橫軸以上的部分,加上之前y=1的曲線,得到的最終曲線

因為y只能去0或者1兩個值,因此可以將損失函數(shù)歸并為

對于樣本集而言,含有m個樣本的樣本集,每個樣本可以通過上面的cost函數(shù)求得單個樣本的代價,求模型的代價函數(shù)則是通過樣本集中每個樣本的代價之和的均值得到。

將p代入代價函數(shù)J

這個函數(shù)沒有公式解,只能通過梯度下降的方式求得最優(yōu)解。
邏輯回歸損失函數(shù)的梯度推導(dǎo)
根據(jù)梯度下降公式
需要求出代價函數(shù)對每個θ的偏導(dǎo)。首先從sigmoid函數(shù)入手。

求出sigmoid函數(shù)的導(dǎo)數(shù)后,返回看代價函數(shù)。把代價函數(shù)拆開來看。

先求出log(σ(t))和log(1-σ(t))的導(dǎo)數(shù),接下來將整個代價函數(shù)拆分成兩部分,依次求yi * log(σ(t))和(1-yi) * log(1-σ(t))對于θj的偏導(dǎo)。

求出log(σ(t))和log(1-σ(t))的導(dǎo)數(shù)后代入代價函數(shù),就可以求出每個θj的導(dǎo)數(shù)公式。再將每個θj的導(dǎo)數(shù)代入梯度。

X0是添加的值為1的一列,因此對θ0的導(dǎo)數(shù)項可以省略X0,最終可以得到矩陣相乘的表達(dá)式結(jié)果。這也就是將在代碼中實現(xiàn)的方式。
根據(jù)上述推導(dǎo),模擬一次邏輯回歸的批量梯度下降。
import numpy as np
from sklearn.metrics import mean_squared_error
import sklearn.datasets as dataset
from sklearn.model_selection import train_test_split
class Logistic_Regression:
def __init__(self):
'''
初始化Logistic Regression模型
'''
self.coef_ = None
self.interept_ = None
self._theta = None
def _sigmoid(self, t):
'''
sigmoid函數(shù)
:param t:
:return:
'''
return 1 / (1 + np.exp(-t))
def fit(self, X_train, y_train, eta=0.01, n_iters=1e4, epsilon=0.0001):
'''
梯度下降
:param X_train:
:param y_train:
:param eta:
:param n_iters:
:return:
'''
assert X_train.shape[0] == y_train.shape[0], 'the size of X_train must equals the size of y_train'
def J(theta, X_b, y):
'''
計算代價
:param theta:
:param X_b:
:param y:
:return:
'''
y_hat = self._sigmoid(X_b.dot(theta))
try:
return - np.sum(y * np.log(y_hat) + (1 - y) * np.log(1 - y_hat)) / len(y)
except:
return float('inf')
def dJ(theta, X_b, y):
'''
計算梯度
:param theta:
:param X_b:
:param y:
:return:
'''
return X_b.T.dot(self._sigmoid(X_b.dot(theta)) - y) / len(X_b)
def gradient_descent(X_b, y, theta_init, eta=eta, n_iters=n_iters, epsilon=epsilon):
'''
梯度下降
:param X_b:
:param y:
:param theta_init:
:param eta:
:param n_iters:
:param epsilon:
:return:
'''
theta = theta_init
cur_iters = 0
while cur_iters < n_iters:
gradient = dJ(theta, X_b, y)
last_theta = theta
theta = theta - gradient * eta
if abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon:
break
cur_iters = cur_iters + 1
return theta
X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
theta_init = np.zeros(X_b.shape[1])
self._theta = gradient_descent(X_b, y_train, theta_init, eta, n_iters, epsilon)
self.interept_ = self._theta[0]
self.coef_ = self._theta[1:]
return self
def predict_probability(self, X_test):
'''
預(yù)測概率函數(shù)
:param X_test:
:return:
'''
assert self.coef_ is not None, 'coef can not be None'
assert X_test.shape[1] == len(self.coef_), 'the size of X_test must equals the size of coef'
X_b = np.hstack([np.ones((len(X_test), 1)), X_test])
return self._sigmoid(X_b.dot(self._theta))
def predict(self, X_test):
'''
預(yù)測函數(shù)
:param X_test:
:return:
'''
assert self.coef_ is not None, 'coef can not be None'
assert X_test.shape[1] == len(self.coef_), 'the size of X_test must equals the size of coef'
prob = self.predict_probability(X_test)
return np.array(prob >= 0.5, dtype='int')
def mse(self, X_test, y_test):
'''
測試預(yù)測準(zhǔn)確度
:param X_test:
:param y_test:
:return:
'''
y_predict = self.predict(X_test)
return mean_squared_error(y_predict, y_test)
data = dataset.load_iris()
X = data.data
y = data.target
X = X[y < 2, :2]
y = y[y<2]
X_train, X_test, y_train, y_test = train_test_split(X, y)
logistics = Logistic_Regression()
logistics.fit(X_train, y_train)
mse = logistics.mse(X_test, y_test)
print(mse)
定義一個Logistic_Regression類,通過coef_和interept_記錄特征的系數(shù)和回歸曲線的截距。fit()函數(shù)封裝了梯度下降相關(guān)的所有函數(shù),包括代價函數(shù)J、求梯度的函數(shù)dJ以及梯度下降函數(shù)gradient_descent(),gradient_descent()函數(shù)的執(zhí)行過程和總結(jié)線性回歸時的批量梯度下降基本一樣,不過是代價函數(shù)和求梯度的方式有所變化而已。之后可以調(diào)用predict()方法進(jìn)行測試預(yù)測并使用mse函數(shù)求預(yù)測值與實際值之間的均方誤差檢驗?zāi)P皖A(yù)測效果。得到輸出
mse is 0.0
the coef of 2 features is [ 1.19406112 -2.03850496]
the intercept is -0.21296952322024035
可見預(yù)測準(zhǔn)確度100%正確,相應(yīng)的特征系數(shù)θ和截距θ0也能打印得到。
決策邊界
邏輯回歸本質(zhì)上是一種二分類問題,決策邊界就是將樣本劃分為不同類別的一條邊界。

sigmoid函數(shù)將回歸函數(shù)得到的值求得一個介于0和1之間的概率,當(dāng)p>0.5時,預(yù)測值為1,p<0.5時,預(yù)測值為0.那么p=0.5就是一個臨界值,此時e的系數(shù)就是0,也就是說

因此將上面的式子稱為決策邊界。
以二維特征矩陣為例,含有兩個特征的決策邊界為:

以此推得

于是我們定義求解X2的函數(shù)
def X2(logistic, X1):
return (-logistic.intercept_ - logistic.coef_[0] * X1) / logistic.coef_[1]
logistic是一個邏輯回歸的實例,intercept_為回歸曲線的截距也就是θ0,coef_記錄從θ1起的所有特征系數(shù)。
descision_boundary = X2(logistic, X_train[:][0])
還是以鳶尾花數(shù)據(jù)集為例,只取前兩個特征,依據(jù)數(shù)據(jù)的特點,在區(qū)間4到8之間繪制決策邊界。
data = dataset.load_iris()
X = data.data
y = data.target
X = X[y < 2, :2]
y = y[y < 2]
logistic = Logistic_Regression()
logistic.fit(X_train, y_train)
X1_plot = np.linspace(4, 8, 1000)
X2_plot = X2(logistic, X1_plot)
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='red')
plt.scatter(X[y == 1, 0], X[y == 1, 1], color='blue')
plt.plot(X1_plot, X2_plot)
plt.show()
得到?jīng)Q策邊界
注意決策邊界并非邏輯回歸專有,很多其他機(jī)器學(xué)習(xí)模型也有決策邊界的概念,如KNN等。
邏輯回歸與多項式回歸
之前模擬的邏輯回歸都是基于一次項的邏輯回歸,在實際環(huán)境中,一次項并不能很好的適應(yīng)數(shù)據(jù),因此,多項式回歸和邏輯回歸相結(jié)合,可以得到更好的邏輯回歸模型。
構(gòu)造虛擬數(shù)據(jù)
#模擬測試用例
np.random.seed(666)
X = np.random.normal(0, 1, size=(200, 2))
y = np.array(X[:, 0]**2 + X[:, 1]**2 < 1.5, dtype=int)
plt.scatter(X[y == 0, 0], X[y == 0, 1])
plt.scatter(X[y == 1, 0], X[y == 1, 1])
plt.show()
根據(jù)表達(dá)式,這個曲線應(yīng)該是一個圓,將圓內(nèi)和圓外的點分為不同類別。打印圖像。

使用之前定義的邏輯回歸模型進(jìn)行訓(xùn)練
logistic = Logistic_Regression()
X_train, X_test, y_train, y_test = train_test_split(X, y)
logistic.fit(X_train, y_train)
mse = logistic.score(X_test, y_test)
print('theta[0] is %s, theta[1] is %s, interceptor is %s' % (logistic.coef_[0], logistic.coef_[1], logistic.intercept_))
print('regression is %s * X1 + %s * X2 + %s' % (logistic.coef_[0], logistic.coef_[1], logistic.intercept_))
print('the mse is ', mse)
得到輸出結(jié)果
theta[0] is 0.0004486610126586672, theta[1] is 0.0001887681532517242, interceptor is 0.0007333333333333333
regression is 0.0004486610126586672 * X1 + 0.0001887681532517242 * X2 + 0.0007333333333333333
the mse is 0.42
可以看到截距和兩個特征分別對應(yīng)的系數(shù),這種情況下的均方誤差為0.42。
打印決策邊界的圖像
def X2(logistic, X1):
return (-logistic.intercept_ - logistic.coef_[0] * X1) / logistic.coef_[1]
descision_boundary = X2(logistic, X_train[:][0])
X1_plot = np.linspace(-4, 4, 1000)
X2_plot = X2(logistic, X1_plot)
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='red')
plt.scatter(X[y == 1, 0], X[y == 1, 1], color='blue')
plt.plot(X1_plot, X2_plot)
plt.show()
得到圖像

可以看出,這個決策邊界并不能很好的劃分不同類別。因此,需要使用多項式邏輯回歸進(jìn)行更細(xì)致的劃分。借助先前講過的sklearn中的PipeLine進(jìn)行多項式邏輯回歸。
def polynomialLogisticRegression(degree):
'''
多項式邏輯回歸
:param degree:
:return:
'''
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std', StandardScaler()),
('Logistic_Regression', Logistic_Regression())
])
poly_log_reg = polynomialLogisticRegression(degree=2)
poly_log_reg.fit(X_train, y_train)
print('the mse of polynomial logistic regression is ', poly_log_reg.score(X_test, y_test))
得到輸出結(jié)果
the mse of polynomial logistic regression is 0.1
明顯的再使用多項式回歸后,誤差減小。
邏輯回歸中使用正則化
在使用多項式邏輯回歸的時候容易出現(xiàn)多項式項數(shù)過高引起過擬合,因此需要進(jìn)行正則化。結(jié)合之前講過的L1正則和L2正則,本節(jié)給出sklearn中將多項式邏輯回歸和模型正則化一起使用的方法。

邏輯回歸的正則化表達(dá)式有所不同,將正則化系數(shù)乘以代價函數(shù)再加上正則化項。

C越大,與損失函數(shù)的乘積也就越大,這樣在回歸過程中若想達(dá)到達(dá)到極小值點,就需要將代價函數(shù)盡可能小。C越小,乘積越小,若想到達(dá)極小值點時模型約精確,就需要注意正則化項的值不能太小,否則求出來的模型準(zhǔn)確度不高。
先模擬數(shù)據(jù)
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
#模擬200個樣本,每個樣本有2個特征
np.random.seed(666)
X = np.random.normal(0, 1, size=(2000, 2))
y = np.array(X[:, 0]**2 + X[:, 1] < 1.5, dtype='int')
#添加噪音,強(qiáng)制樣本20個點值為1
for _ in range(20):
y[np.random.randint(200)] = 1
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='red')
plt.scatter(X[y == 1, 0], X[y == 1, 1], color='blue')
plt.show()
模擬2000個樣本,每個樣本含有兩個特征。y = X1^2 + 1.5 * X2,并隨機(jī)模擬200個噪聲數(shù)據(jù)。
logistic = LogisticRegression()
logistic.fit(X_train, y_train)
score = logistic.score(X_test, y_test)
print(logistic.coef_)
print(logistic.intercept_)
print('C = 1, penalty = l2 score is ', score)
logistic2 = LogisticRegression(C=0.1)
logistic2.fit(X_train, y_train)
score = logistic2.score(X_test, y_test)
print(logistic2.coef_)
print(logistic2.intercept_)
print('C = 0.1, penalty = l2 score is ', score)
logistic3 = LogisticRegression(C=0.1, penalty='l1')
logistic3.fit(X_train, y_train)
score = logistic3.score(X_test, y_test)
print(logistic3.coef_)
print(logistic3.intercept_)
print('C = 1, penalty = l1 score is ', score)
sklearn的邏輯回歸中,默認(rèn)的C=1且使用L2正則,通過修改C和penalty參數(shù)進(jìn)行調(diào)參得到不同結(jié)果。
得到輸出
[[ 0.12528696 -1.18653217]]
[1.06721009]
C = 1, penalty = l2 score is 0.804
[[ 0.11725372 -1.11016952]]
[1.0096957]
C = 0.1, penalty = l2 score is 0.808
[[ 0.08271496 -1.11618329]]
[1.00965503]
C = 1, penalty = l1 score is 0.808
通過不同參數(shù)得到的特征系數(shù)不同且預(yù)測的準(zhǔn)確度也不同。但都在0.8左右,因為使用的是一次項表達(dá)式,而模型是2次項,因此使用多項式邏輯回歸準(zhǔn)確度應(yīng)該會好些。
def polynomialLogisiticRegression(degree, C=1.0, penalty='l2'):
'''
多項式邏輯回歸
:param degree:
:param C:
:param penalty:
:return:
'''
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std', StandardScaler()),
('reg', LogisticRegression(C=C, penalty=penalty)),
])
poly_reg = polynomialLogisiticRegression(degree=2)
poly_reg.fit(X_train, y_train)
print('the score of polynomial logisitic regression with degree=2, C=1, penalty=l2 is ', poly_reg.score(X, y))
poly_reg = polynomialLogisiticRegression(degree=20)
poly_reg.fit(X_train, y_train)
print('the score of polynomial logisitic regression with degree=20, C=1, penalty=l2 is ', poly_reg.score(X, y))
poly_reg = polynomialLogisiticRegression(degree=20, C=0.1)
poly_reg.fit(X_train, y_train)
print('the score of polynomial logisitic regression with degree=20, C=0.1, penalty=l2 is ', poly_reg.score(X, y))
poly_reg = polynomialLogisiticRegression(degree=20, C=0.1, penalty='l1')
poly_reg.fit(X_train, y_train)
print('the score of polynomial logisitic regression with degree=20, C=0.1, penalty=l1 is ', poly_reg.score(X, y))
使用PipeLine封裝多項式回歸和邏輯回歸,通過將項數(shù)為2和20進(jìn)行測試,很明顯,20對于訓(xùn)練數(shù)據(jù)而言是過擬合的,因此,在項數(shù)20的基礎(chǔ)上進(jìn)行L1正則和L2正則。
得到輸出結(jié)果
the score of polynomial logisitic regression with degree=2, C=1, penalty=l2 is 0.991
the score of polynomial logisitic regression with degree=20, C=1, penalty=l2 is 0.9905
the score of polynomial logisitic regression with degree=20, C=0.1, penalty=l2 is 0.9765
the score of polynomial logisitic regression with degree=20, C=0.1, penalty=l1 is 0.9935
可以對比出當(dāng)項數(shù)過高時,模型的準(zhǔn)確度略微降低,當(dāng)使用L1正則并設(shè)置參數(shù)C=0.1后模型的準(zhǔn)確度提升,但整體上高于之前一次項回歸??梢钥闯稣齽t化和多項式回歸的效果。
OvR與OvO
邏輯回歸本身只能解決二分類問題,但通過一些手段可以解決多分類問題。常見的方式有OvR和OvO兩種。

①OvR的思路
OvR(One vs Rest)的思路是將所有類別分為兩個類,當(dāng)前類別是一類,其他類別合并視為一個類。
那么有K個類別的數(shù)據(jù)樣本就會被分為K個由兩個類組成的新樣本集合,這樣就將多分類問題轉(zhuǎn)化為二分類問題,然后對每個數(shù)據(jù)樣本進(jìn)行模型訓(xùn)練,得到模型使用樣本進(jìn)行驗證,選擇分類得分最高的作為最終的樣本類別。這里的得分最高指的就是樣本屬于某類概率最高。
sklearn提供了OvR的實現(xiàn),可以在分類器中進(jìn)行實現(xiàn),也可以使用OvR類,以分類器作為參數(shù)進(jìn)行實現(xiàn)。
import numpy as np
import sklearn.datasets as dataset
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from sklearn.multiclass import OneVsOneClassifier
#以鳶尾花數(shù)據(jù)為例
iris = dataset.load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
#打印LogisticRegression實例信息所知,默認(rèn)采用OvR方式
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
print(log_reg)
score = log_reg.score(X_test, y_test)
print('with multi class OvR, the score is ', score)
#使用sklearn的OvR分類器
ovr = OneVsRestClassifier(log_reg)
ovr.fit(X_train, y_train)
score = ovr.score(X_test, y_test)
print('use sklearn OneVsRestClassifier, the score is ', score)
得到輸出
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False)
with multi class OvR, the score is 0.9473684210526315
use sklearn OneVsRestClassifier, the score is 0.9473684210526315
LogisticRegression默認(rèn)使用OvR的方式進(jìn)行分類,可以通過設(shè)置multiclass參數(shù)調(diào)整。在sklearn的multiclass包下也對OvR和OvO進(jìn)行封裝,可以將分類器對象作為參數(shù)傳入,對象可以是sklearn自帶的,也可以是自定義的,只要滿足函數(shù)規(guī)范即含有fit、predict、score等函數(shù)即可實現(xiàn)分類。
使用鳶尾花數(shù)據(jù)集,解決三分類問題,OvR模式下將生成三個分類模型,分類的準(zhǔn)確度為94%。
②OvO的思路
OvO即One vs One,不再將出當(dāng)前類別外的其他類別視為一個大類,而是將類別拆分,每兩個訓(xùn)練一個模型。例如有四個類別的樣本將會被分為如下6個OvO類別。
多分類問題轉(zhuǎn)化為兩個兩個類別進(jìn)行分類的問題,一個含有N個類別的樣本將被分為C(N, 2)個分類問題。當(dāng)對一個未知樣本進(jìn)行分類時,用這C(N, 2)個模型進(jìn)行分類,最后得票最多的類別即為該未知樣本的類別。
因為OvO產(chǎn)生的模型數(shù)量高于OvR,因此,一般情況下,OvO的分類準(zhǔn)確度好于OvR,但訓(xùn)練更多模型的系統(tǒng)和時間開銷要大,因此OvR更加高效。
#使用OvO進(jìn)行分類
log_reg2 = LogisticRegression(multi_class='multinomial', solver='newton-cg')
log_reg2.fit(X_train, y_train)
score = log_reg2.score(X_test, y_test)
print('with multi class OvO, the score is ', score)
#使用sklearn的OvO分類器
ovo = OneVsOneClassifier(log_reg)
ovo.fit(X_train, y_train)
score = ovo.score(X_test, y_test)
print('use sklearn OneVsOneClassifier, the score is ', score)
如果是使用LogisticRegression進(jìn)行OvO訓(xùn)練,除了設(shè)置multi_class參數(shù)為multinomial外,還需要指定solver參數(shù),否則無效。若使用OneVsOneClassifier,使用方式和OneVsRestClassifier沒有什么差別。
得到輸出結(jié)果
with multi class OvO, the score is 1.0
use sklearn OneVsOneClassifier, the score is 1.0
分類結(jié)果100%正確,可見在當(dāng)前樣本下,OvO確實好于OvR。