邏輯迴歸算法梳理(從理論到示例)

邏輯迴歸算法的名字裏雖然帶有“迴歸”二字,但實際上邏輯迴歸算法是用來解決分類問題的算法。本博客將從二分類入手,介紹邏輯迴歸算法的預測函數、損失函數(成本函數)和梯度下降算法公式,然後由二分類延伸到多分類的問題,接下來介紹正則化,即通過數學的手段來解決模型過擬合問題,最後用一個乳腺癌檢測的實例及其模型性能優化來結束全文。

1 邏輯迴歸算法的原理

假設有一場球賽,我們有兩支球隊的所有出場球員信息、歷史交鋒成績、比賽時間、主客場、裁判和天氣等信息,根據這些信息預測球隊的輸贏。假設比賽結果記爲y{y},贏球標記爲1,輸球標記爲0,這就是一個典型的二元分類問題,可以用邏輯迴歸算法來解決。
從這個例子裏可以看出,邏輯迴歸算法的輸出y{0,1}{y \in \{0,1}\}是個離散值,這是與線性迴歸算法的最大區別。

1.1 預測函數

需要找出一個預測函數模型,使其值輸出在[0,1]{[0,1]}之間。然後選擇一個基準值,如0.5{0.5},如果算出來的預測值大於0.5{0.5},就認爲其預測值爲1,反之則其預測值爲0.
我們選擇g(z)=11+ez{g(z)= \frac{1}{1+e^{-z}}}來作爲預測函數,。函數g(z){g(z)}稱爲Sigmoid{Sigmoid}函數,也稱爲Logistic{Logistic}函數。圖像如下:
sigmoid
z=0{z=0}時,g(z)=0.5{g(z)=0.5}
z>0{z>0}時,g(z)>0.5{g(z)>0.5},當z{z}越來越大時,g(z){g(z)}無限接近於1{1}
z<0{z<0}時,g(z)<0.5{g(z)<0.5},當z{z}越來越小時,g(z){g(z)}無限接近於0{0}
這正是我們想要的針對二元分類算法的預測函數。
問題來了,怎樣把輸入特徵和預測函數結合起來呢?
結合線性迴歸函數的預測函數hθ(x)=θTx{h_{\theta}(x)={\theta}^Tx},假設令z(x)=θTx{z(x)={\theta}^Tx},則邏輯迴歸算法的預測函數如下:
hθ(x)=g(z)=g(θTx)=11+eθTx{h_{\theta}(x)=g(z)=g({\theta}^Tx)=\frac{1}{1+e^{-{\theta}^Tx}}}。下面解讀預測函數。
hθ(x){h_{\theta}(x)}表示在輸入值爲x{x},參數爲θ{\theta}的前提下y=1{y=1}的概率。用概率論的公式可以寫成:hθ(x)=P(y=1x,θ){h_{\theta}(x)=P(y=1|x,\theta)},即在輸入x{x}及參數θ{\theta}條件下y=1{y=1}的概率。由條件概率公式可以推導出
P(y=1x,θ)+P(y=0x,θ)=1{P(y=1|x,\theta)+P(y=0|x,\theta)=1}
對二分類來說,這是一個非黑即白的世界。

1.2 判定邊界

邏輯迴歸算法的預測函數由以下兩個公式給出:
hθ(x)=g(θTx){h_{\theta}(x)=g({\theta}^Tx)}g(z)=11+ez{g(z)= \frac{1}{1+e^{-z}}}
假定y=1{y=1}的判定條件是hθ(x)0.5{h_{\theta}(x)\geq0.5}y=0{y=0}的判定條件是hθ(x)0.5{h_{\theta}(x)\leq 0.5},所以θTx=0{\theta^Tx=0}就是我們的判定邊界。
假定有兩個變量x1x2{x_1,x_2},其邏輯迴歸預測函數是hθ(x)=g(θ0+θ1x1+θ2x2){h_{\theta}(x)=g({\theta}_0+{\theta}_1{x_1}+{\theta}_2{x_2})}。假設給定參數θ=[3 1 1]T{\theta=[-3\ 1 \ 1]^T},那麼得到判定邊界爲3+x1+x2=0{-3+x_1+x_2=0}
如果預測函數是多項式hθ(x)=g(θ0+θ1x1+θ2x2+θ3x12+θ4x22){h_{\theta}(x)=g({\theta}_0+{\theta}_1{x_1}+{\theta}_2{x_2}+{\theta}_3{x_1}^2+{\theta}_4{x_2}^2)},給定θ=[1 0 0 1 1]T{\theta=[-1\ 0 \ 0 \ 1 \ 1]^T},則得到判定邊界函數爲x12+x22=1{{x_1}^2+{x_2}^2=1}。這是二階多項式的情況,更一般的多階多項式可以表達出更復雜的判定邊界。

1.3 損失函數(成本函數)

我們不能使用線性迴歸模型的損失函數來推導邏輯迴歸的損失函數,因爲那樣的損失函數太複雜,最終很可能會導致無法通過迭代找到損失函數值最小的點。爲了容易地求出損失函數的最小值,我們分成y=1{y=1}y=0{y=0}兩種情況分別考慮其預測函數值與真實值的誤差。我們先考慮最簡單的情況,即計算某一個樣本(x,y){(x,y)}的預測值與真實值的誤差,損失函數如下:
Cost(hθ(x),y)=\{log(hθ(x)), if y=1log(1hθ(x)), if y=0\}{Cost(h_{\theta}(x), y)={-log(h_{\theta}(x)), \ if \ y=1 \brace -log(1-h_{\theta}(x)), \ if \ y=0}},其中hθ(x){h_{\theta}(x)}表示預測爲1{1}的概率。
回顧成本/損失的定義:預測值與真實值的差異。差異越大,損失越大,模型受到的“懲罰”也越嚴重。當y=1{y=1}時,隨着hθ(x){h_{\theta}(x)}的值(預測爲1的概率)越來越大,預測值越來越接近真實值,其成本/損失越來越小。當y=0{y=0}時,隨着hθ(x){h_{\theta}(x)}的值(預測爲1的概率)越來越大,預測值越來越偏離真實值,其成本/損失越來越大。

符合上述規律的函數模型有很多,爲什麼我們要選擇自然對數函數作爲損失函數呢?
邏輯迴歸模型的預測函數是sigmoid{sigmoid}函數,而sigmoid{sigmoid}函數裏有e{e}n{n}次方運算,自然對數剛好是其逆運算,比如log(en)=n{log(e^n)=n},最終會推導出形式優美的邏輯迴歸模型參數的迭代函數,而不需要去涉及對數運算和指數函數運算。這就是我們選擇自然對數函數作爲損失函數的原因。

下面將損失函數統一寫法。分開表述的成本計算公式始終不方便,合併成一個公式豈不完美。然後就有了下面的公式:
Cost(hθ(x),y)=ylog(hθ(x))(1y)log(1hθ(x)){Cost(h_{\theta}(x), y)=-y*{log(h_{\theta}(x))-(1-y)*log(1-h_{\theta}(x))}}
由於y{0,1}{y \in \{0,1}\}是離散值,當y=1{y=1}時,1y=0{1-y=0},上式的後半部分爲0{0};當y=0{y=0}時,上式的前半部分爲0{0}。因此上式與分開表達的成本計算公式是等價的。

介紹到這裏,損失函數就要隆重登場了。根據一個樣本的成本計算公式,很容易寫出所有樣本的成本/損失平均值,即損失函數:
J(θ)=1m[i=1m y(i)log(hθ(x(i)))+(1y(i))log(1hθ(x(i)))]{J(\theta)=- \frac{1}{m} \lbrack \sum_{i=1}^{m} \ y^{(i)} * log(h_\theta(x^{(i)}))+(1-y^{(i)}) * log(1-h_\theta(x^{(i)}))\rbrack}

1.4 梯度下降算法

和線性迴歸類似,我們使用梯度下降算法來求解邏輯迴歸模型參數。根據梯度下降算法的定義,可以得出:
θj=θjαJ(θ)θj{\theta_j = \theta_j - \alpha \frac{\partial J(\theta)}{\partial \theta_j}},這裏的關鍵同樣是求解損失函數的偏導數。最終推導出來的梯度下降算法公式爲:
θj=θjα1mi=1m(hθ(x(i))y(i))xj(i){\theta_j = \theta_j - \alpha \frac{1}{m} \sum_{i=1}^{m} (h_\theta(x^{(i)})-y^{(i)})x_j^{(i)}}
這個公式的形式和線性迴歸算法的參數迭代公式是一樣。當然,由於這裏hθ(x)=11+eθTx{h_{\theta}(x)=\frac{1}{1+e^{-{\theta}^Tx}}},而線性迴歸算法裏hθ(x)=θTx{h_{\theta}(x)={\theta}^Tx}。所以,兩者的形式是一樣,但是數值計算方法則完全不同。
至此,我們把邏輯迴歸算法的相關原理解釋清楚了。

2 多元分類

邏輯迴歸模型可以解決二分類問題,即y={0,1}{y=\{0,1\}},能否用來解決多元分類問題呢?答案是肯定的。針對多元分類問題,y={0,1,2,...,n}{y=\{0,1,2,...,n \}},總共有n+1{n+1}個類別。其解決思路是,首先將問題轉化爲二分類問題,即y=0{y=0}是一個類別,y={1,2,...,n}{y=\{ 1,2,...,n\}}作爲另一個類別,然後計算這兩個類別的概率;接着,把y=1{y=1}作爲一個類別,把y={0,2,...,n}{y=\{ 0,2,...,n\}}作爲另一個類別,再計算這兩個類別的概率。由此推廣開來,總共需要n+1{n+1}個預測函數(分類器)。預測出來的概率最高的那個類別,就是樣本所屬的類別。

3 正則化

回顧線性迴歸算法梳理,過擬合是指模型很好地擬合了訓練樣本,但對新樣本預測地準確性很差,因爲模型太複雜了。解決辦法是減少輸入特徵的個數,或者獲取更多的訓練樣本。正則化也是用來解決模型過擬合問題的一個方法。

  • 保留所有的特徵,減小特徵的權重θj{\theta_j}的值。確保所有的特徵對預測值都要少量的貢獻。
  • 當每個特徵xi{x_i}對預測值y{y}都有少量貢獻時,這樣的模型可以良好地工作,這就是正則化地目的,可以用它解決特徵過多時地過擬合問題。

3.1 線性迴歸模型正則化

我們先來看線性迴歸模型的損失函數是如何正則化的:
J(θ)=12m[i=1m(hθ(x(i))y(i))2]+λj=1nθj2{J(\theta)=\frac{1}{2m} \lbrack \sum_{i=1}^{m}(h_{\theta}(x^{(i)})-y^{(i)})^2 \rbrack+\lambda \sum_{j=1}^{n} \theta_j^2}
公式前半部分是線性迴歸模型的損失函數,後半部分爲加入的正則化項。其中λ{\lambda}有兩個目的,既要維持對訓練樣本的擬合,又要避免對訓練樣本的過擬合。如果λ{\lambda}值太大,則能確保不出現過擬合,但可能會導致欠擬合。

從數學角度來看,損失函數增加了一個正則項後,損失函數不再唯一地由預測值與真實值的誤差所決定了,還和參數θ{\theta}的大小有關。有了這個限制之後,要實現損失函數最小的目的,θ{\theta}就不能隨便取值了。比如某個比較大的θ{\theta}值可能會讓預測值與真實值的誤差(hθ(x(i))y(i))2{(h_{\theta}(x^{(i)})-y^{(i)})^2}值很小,但會導致θj2{\theta_j^2}很大,最終結果就是損失函數太大。這樣,通過調節參數λ{\lambda},就可以控制正則項的權重,從而避免線性迴歸算法過擬合。

梯度下降的時候爲什麼要對θj2{\theta_j^2}進行收縮呢?因爲加入正則項的損失函數和θj2{\theta_j^2}成正比,所以迭代的時候需要不斷地努力縮小θj2{\theta_j^2}的值。

3.2 邏輯迴歸模型正則化

使用相同的思路,我們對邏輯迴歸模型的損失函數進行正則化,其方法也是在原來的損失函數的基礎上加上正則項:
J(θ)=1m[i=1m y(i)log(hθ(x(i)))+(1y(i))log(1hθ(x(i)))]+λ2mj=1nθj2{J(\theta)=- \frac{1}{m} \lbrack \sum_{i=1}^{m} \ y^{(i)} * log(h_\theta(x^{(i)}))+(1-y^{(i)}) * log(1-h_\theta(x^{(i)}))\rbrack + \frac{\lambda}{2m} \sum_{j=1}^{n}\theta_j^2}
相應地,正則化後的參數迭代公式爲:
θj=θjαJ(θ)θj{\theta_j=\theta_j-\alpha \frac{\partial J(\theta)}{\theta_j}}
θj=θjα[1mi=1m(hθ(x(i))xj(i))+λmθj]{\theta_j=\theta_j-\alpha \lbrack \frac{1}{m} \sum_{i=1}^{m}(h_\theta(x^{(i)})-x_j^{(i)}) + \frac{\lambda}{m} \theta_j \rbrack}
需要注意的是,上式中j1{j \geq 1},因爲θ0{\theta_0}沒有參與正則化。另外要留意,邏輯迴歸和線性迴歸的參數迭代算法看起來形式是一樣的,但其實它們的算法不一樣,因爲兩個式子的預測函數hθ(x){h_\theta(x)}不一樣。針對線性迴歸hθ(x)=θTx{h_\theta(x) = \theta^Tx},而針對邏輯迴歸hθ(x)=11+eθTx{h_\theta(x) = \frac{1}{1+e^{-\theta^Tx}}}

4 實例:乳腺癌檢測

使用邏輯迴歸算法解決乳腺癌檢測問題,我們需要先採集腫瘤病竈造影圖片,然後對圖片進行分析,從圖片中提取特徵,再根據特徵來訓練模型。最終使用模型來檢測新採集到的腫瘤病竈造影,以便判斷腫瘤是良性還是惡性的。這是一個典型的二分類問題。

4.1 數據採集和特徵提取

在工程應用中,數據採集和特徵提取工作往往決定着項目的成敗。爲了簡單起見,作爲示例,我們直接加載scikit-learn自帶的一個乳腺癌數據集。這個數據集是已經採集後的數據:

# 載入數據
from sklearn.datasets import load_breast_cancer

cancer = load_breast_cancer()
X = cancer.data
y = cancer.target
print('data type: {0}; no.positive: {1}; no. negative: {2}'.format(X.shape, y[y==1].shape, y[y==0].shape))
print(cancer.data[0])

"""輸出:
data type: (569, 30); no.positive: (357,); no. negative: (212,)
[1.799e+01 1.038e+01 1.228e+02 1.001e+03 1.184e-01 2.776e-01 3.001e-01
 1.471e-01 2.419e-01 7.871e-02 1.095e+00 9.053e-01 8.589e+00 1.534e+02
 6.399e-03 4.904e-02 5.373e-02 1.587e-02 3.003e-02 6.193e-03 2.538e+01
 1.733e+01 1.846e+02 2.019e+03 1.622e-01 6.656e-01 7.119e-01 2.654e-01
 4.601e-01 1.189e-01]
 
 我們可以看到,數據集中共有569個樣本,每個樣本30個特徵,其中357個陽性(y=1)樣本,212個陰性樣本。

"""

print(cancer.feature_names) # 查看特徵名稱

"""輸出:
['mean radius' 'mean texture' 'mean perimeter' 'mean area'
 'mean smoothness' 'mean compactness' 'mean concavity'
 'mean concave points' 'mean symmetry' 'mean fractal dimension'
 'radius error' 'texture error' 'perimeter error' 'area error'
 'smoothness error' 'compactness error' 'concavity error'
 'concave points error' 'symmetry error' 'fractal dimension error'
 'worst radius' 'worst texture' 'worst perimeter' 'worst area'
 'worst smoothness' 'worst compactness' 'worst concavity'
 'worst concave points' 'worst symmetry' 'worst fractal dimension']
 
"""

爲了強調特徵提取工作的重要性,這裏介紹一下這些特徵值的物理含義,我們也可以思考一下,如果讓我們來提取特徵,會怎麼做?
這個數據集總共從病症造影圖片中提取了以下10個關鍵屬性。

  • radius:半徑,即病症中心點離邊界的平均距離。
  • texture:紋理,灰度值的標準偏差。
  • perimeter:周長,即病竈的大小。
  • area:面積,也是反映病症大小的一個指標。
  • smoothness:平滑度,即半徑的變化幅度。
  • compactness:密實度,周長的平方除以面積的商,再減1。
  • concavity:凹度,凹陷部分輪廓的嚴重程度。
  • concave points:凹點,凹陷輪廓的數量。
  • symmetry:對稱性。
  • fractal dimension:分形維度。

從這些指標裏可以看出,有些指標屬於“複合”指標,即由其他指標經過運算得到的。比如密實度。
不要小看這種運算構建出來的新特徵,這是事物內在邏輯關係的體現。
劃重點哈:提取特徵時,不妨從事物的內在邏輯關係入手,分析已有特徵之間的關係,從而構造出新的特徵。

4.2 模型訓練

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

# 把數據集分爲訓練集和測試集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
"""
(455, 30) (455,)
(114, 30) (114,)

"""

# 使用LogisticRegression模型來訓練,並計算訓練集的評分數據和測試集的評分數據。
# 模型訓練
model = LogisticRegression()
model.fit(X_train, y_train)

train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print('train score: {train_score: .6f}; test score: {test_score: .6f}'.format(train_score=train_score, test_score=test_score))
"""
train score:  0.958242; test score:  0.947368
"""
# 樣本預測
import numpy as np #導入numpy,因爲下面要用到它(np。equal()函數)
y_pred = model.predict(X_test)
print('matches: {0}/{1}'.format(np.equal(y_pred, y_test).shape[0], y_test.shape[0]))

"""
matches: 114/114
總共114個測試樣本,全部預測正確。這裏有個疑問,爲什麼全部預測正確,而test_score卻只有0.973684,而不是1呢?
答案是:scikit-learn不是使用這個測試集數據來計算分數,因爲這個數據不能完全反映誤差情況,而是使用預測概率數據來計算模型評分。

"""

針對二分類問題,LogisticRegression{LogisticRegression}模型會針對每個樣本輸出兩個概率,即爲0的概率和爲1的概率,哪個概率高就預測爲哪個類別。
我們可以找出針對測試數據集,模型預測的“自信度”低於90%的樣本。怎麼找出這些樣本呢?
我們先計算出測試集裏的每個樣本的預測概率數據,針對每個樣本,它會有兩個數據,一是預測其爲陽性的概率,另外一個是預測其爲陰性的概率;
接着找出預測爲陰性的概率大於0.1的樣本,然後在結果集裏找出預測爲陽性的概率也大於0.1的樣本,這樣就找出了模型預測“自信度”低於90%的樣本。
這是因爲所有類別的預測概率加起來一定是100%。
我們看一下概率數據:

# 預測概率:找出預測概率低於90%的樣本
y_pred_proba = model.predict_proba(X_test) # 計算每個測試樣本的預測概率
# 打印第一個樣本的數據,以便我們瞭解數據形式
print('sample of predict probability: {0}'.format(y_pred_proba[0]))

"""輸出:
sample of predict probability: [0.00588337 0.99411663]

"""

# 找出第一列,即預測爲陰性的概率大於0.1的樣本,保存在result裏
y_pred_proba_0 = y_pred_proba[:, 0] > 0.1
result = y_pred_proba[y_pred_proba_0]

# 在result結果集裏,找到第二列,即預測爲陽性的概率大於0.1的樣本
y_pred_proba_1 = result[:, 1] > 0.1
print(result[y_pred_proba_1])

"""
[[0.22451742 0.77548258]
 [0.10006953 0.89993047]
 [0.10750749 0.89249251]
 [0.7861653  0.2138347 ]
 [0.16501894 0.83498106]
 [0.16245788 0.83754212]
 [0.40120013 0.59879987]
 [0.54515479 0.45484521]
 [0.80401218 0.19598782]
 [0.34730441 0.65269559]
 [0.27727652 0.72272348]
 [0.27517093 0.72482907]
 [0.65010429 0.34989571]
 [0.23931504 0.76068496]
 [0.31196411 0.68803589]
 [0.75666569 0.24333431]
 [0.65202448 0.34797552]
 [0.43434309 0.56565691]
 [0.40553876 0.59446124]
 [0.40382138 0.59617862]]

"""

我們使用model.predict_proba(){model.predict\_proba()}來計算概率,同時找出那些預測“自信度”低於90%的樣本。可以看出,最沒有把握的樣本是[0.54515479 0.45484521],即只有45.48%的概率是陽性。大家在自己實現這個例子的時候,輸出結果可能會有差別,因爲訓練集和測試集是隨機分配的。

模型優化

我們使用LogisticRegression{LogisticRegression}模型的默認參數訓練出來的模型,準確性看起來還是挺高的。問題是,有沒有優化空間?如果有,往哪個方向優化呢?
我們先嚐試增加多項式特徵。首先我們使用Pipeline{Pipeline}來增加多項式特徵:

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline

# 增加多項式預處理
def polynomial_model(degree=1, **kwarg):
    polynomial_features = PolynomialFeatures(degree=degree, include_bias=False)
    logistic_regression = LogisticRegression(**kwarg)
    pipeline = Pipeline([("polynomial_features", polynomial_features), ("logistic_regression", logistic_regression)])
    return pipeline

# 接着,增加二項多項式特徵,創建並訓練模型
import time
model = polynomial_model(degree=2, penalty='l1') #我們使用L1範數作爲正則項(參數penalty='l1')

start = time.clock()
model.fit(X_train, y_train)

train_score = model.score(X_train, y_train)
cv_score = model.score(X_test, y_test)
print('elaspe: {0:.6f}; train_score: {1: 0.6f}; cv_score: {2: .6f}'.format(time.clock()-start, train_score, cv_score))

"""輸出:
elaspe: 0.369242; train_score:  0.993407; cv_score:  0.964912

"""

可以看到訓練集評分和測試集評分都增加了。
爲什麼使用L1{L1}範數作爲正則項呢?因爲它可以實現參數的稀疏化,即自動幫助我們選擇出那些對模型有關聯的特徵。
我們可以觀察一下有多少個特徵沒有被丟棄,即其對應的模型參數θj{\theta_j}非0

logistic_regression = model.named_steps['logistic_regression']
print('model parameters shape: {0}; count of non-zero element: {1}'.format(
    logistic_regression.coef_.shape, np.count_nonzero(logistic_regression.coef_)))

"""輸出:
model parameters shape: (1, 495); count of non-zero element: 113

"""

邏輯迴歸模型的coef_{coef\_}屬性裏保存的就是模型參數。從輸出結果可以看到,
增加二階多項式特徵後,輸入特徵由原來的30個增加到了495個,最終大多數特徵都被丟棄,只保留了94個有效特徵。

到此,總算把邏輯迴歸算法的思路整理出來了,當初線性迴歸和邏輯迴歸這兩個“孿生兄弟”把我折磨得欲生欲死,飄飄欲仙。

一點小擴展

我們的預測函數是寫成向量形式的:
hθ=g(z)=g(θTx)=11+eθTx{h_\theta=g(z)=g(\theta^Tx)=\frac{1}{1+e^{-\theta^Tx}}}
這個預測函數一次只計算一個樣本的預測值,我們要怎樣一次性計算出所有樣本的預測值呢?
h=g(Xθ){h=g(X\theta)}
上述公式即可達到目的。其中g(x){g(x)}sigmoid{sigmoid}函數。X{X}mn{m * n}的矩陣,即數據集的矩陣表達。
損失函數也有對應的矩陣形式:
J(θ)=1m(yTlog(h)(1y)Tlog(1h)){J(\theta)=\frac{1}{m}(-y^T*log(h)-(1-y)^T*log(1-h))},其中,y{y}爲目標值向量,h{h}爲一次性計算出來的所有樣本的預測值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章