機器學習實踐筆記--Logistic回歸

Logistic回歸

主要內容

  • Sigmoid函數和Logistic回歸分類器
  • 最優化理論初步
  • 梯度下降最優化算法
  • 數據中的缺失項處理

假設現在有一些數據點,我們用一條直線對這些點進行擬合(該線稱為最佳擬合直線),這個擬合過程就稱作回歸。利用Logistic回歸進行分類的主要思想是:根據現有數據對分類邊界線建立回歸公式。訓練分類器時的做法就是尋找最佳擬合參數,使用的是最優化算法。

Logistic回歸的一般過程

  1. 收集數據:采用任意方法收集數據。
  2. 準備數據:由于需要進行距離計算,因此要求數據類型為數值型。另外,結構化數據格式則最佳。
  3. 分析數據:采用任意方法對數據進行分析。
  4. 訓練算法: 大部分時間將用于訓練,訓練的目的是為了找到最佳的分類回歸系數。
  5. 測試算法:一旦訓練步驟完成,分類將會很快。
  6. 使用算法:首先,我們下需要一些輸入數據,并將其轉換成對應的數據結構化數值;接著,給予訓練好的回歸系數就可以對這些數值進行簡單的回歸計算,判定它們屬于哪個類別。

1.基于Logistic回歸和Sigmoid函數的分類

Logistic回歸
優點:計算代價不高,易于理解和實現。

缺點:容易欠擬合,分類精度可能不高。

使用數據類型:數值型和標稱型數據。

1.1 Sigmoid函數

在這里我們需要這樣一個函數,能接收所有輸入數據(特征向量)然后預測出類別。例如,有兩個類別,那么這個函數的輸出可以使0或1。
可能大家會想起單位階躍函數,又稱海維賽德階躍函數(Heaviside step function)。然而,階躍函數的問題在于:該函數在跳躍點上從0瞬間跳躍到1,這個瞬間跳躍過程有時很難處理。幸好,另一個函數也有類似的性質,且數學上更好處理,這就是Sigmoid函數。
sigmoid函數具體公式如下:
$ \sigma(z)= \frac{1}{1 + e^{-z}} $

Sigmoid函數是一種階躍函數(step function)。在數學中,如果實數域上的某個函數可以用半開區間上的指示函數的有限次線性組合來表示,那么這個函數就是階躍函數。而數學中指示函數(indicator function)是定義在某集合X上的函數,表示其中有哪些元素屬于某一子集A。

圖一 給出了Sigmoid函數在不同坐標尺度下的兩條曲線圖。當x為0時,Sigmoid函數值為0.5。隨著x的增大,對應的Sigmoid值將逼近于1;而隨著x的減小,Sigmoid值將逼近于0。如果橫坐標刻度足夠大(圖一),Sigmoid函數將看起來很像一個階躍函數。

圖一
圖一
兩種坐標尺度下的Sigmoid函數圖。上圖橫坐標為-5到5,這時的曲線變化較為平滑;下圖橫坐標的尺度足夠大,在x = 0點處Sigmoid函數看起來很像是階躍函數。因此,為了實現Logistic回歸分類器,我們可以在每個特征上乘以一個回歸系數,然后把所有的結果值相加,將這個總和帶入Sigmoid函數,進而得到一個范圍0~1之間的數值。最后,結果大于0.5的數據被歸為1類,小于0.5的即被歸為0類。所以Logistic回歸也可以被看成是一種概率估計。

確定了分類器的函數形式之后,現在的問題變成了:最佳回歸系數是多少?如何確定它們的大???

2.基于最優化方法的最佳回歸系數確定

Sigmoid函數的輸入記為z,由下面公式得出:
$$ z = w_0+w_1+w_2+···+w_n$$
如果使用向量的寫法,上述公式可以寫成 $ z = w^Tx $ 。它表示將兩個數值向量的對應元素相乘然后全部加起來即得到z值。 其中的向量x是分類器的輸入數據,向量w也就是我們要找到的最佳參數(參數),從而使得分類盡可能地精確。

2.1 梯度上升法

梯度上升法基于的思想是:要找到某函數的最大值,最好的方法沿著該函數的梯度方向探尋。如果梯度記為$\nabla$ ,則函數f(x,y)的梯度由下式表示:$$\nabla f(x,y) = \dbinom{\frac{ \partial f(x,y)}{ \partial x} }{ \frac{\partial f(x,y)}{\partial y}} $$

這是機器學習中最容易造成混淆的一個地方,但在數學上并不難,需要做的只是牢記這些符號的意義。這個梯度意味著要沿x的方向移動 $ \frac{\partial f(x,y) }{\partial x} $,沿y的方向移動 $ \frac{\partial f(x,y)}{\partial y} $。其中,函數$f(x,y)$必須要在待計算的點上有定義并且可微。

圖二
圖二 梯度上升算法到達每個點后都會重新估計移動的方向。從P0開始,計算完該點的梯度,函數就根據梯度移動到下一點P1。在P1點,梯度再次被重新計算,并沿新的梯度方向移動到P2。如此循環迭代,直到滿足停止條件。迭代的過程中,梯度算子總是保證我們能選取到最佳的移動方向。 可以看到,梯度算子總是指向函數值增長最快的方向。這里所說的是移動方向,而未提到移動量的大小。該量值稱為步長,記做$ \alpha $。用向量來表示的話,梯度上升算法的迭代公式如下:$$ w:=w+\alpha \nabla_{w}f(w) $$ 該公式將一直被迭代執行,直到到達某個停止條件為止,比如迭代次數到達某個指定值或算法到達某個可以允許的誤差范圍。

梯度下降算法

“梯度下降算法,它與這里的梯度上升算法是一樣的,只是公式中的加法需要變成減法。因此,對應的公式可以寫成:$$ w:=w+\alpha\nabla_{w}f(w) $$

梯度上升算法用來求函數的最大值,而梯度下降算法用來求函數的最小值。

基于上面的內容,我們來看一個Logistic回歸分類器的應用例子,從圖三可以看到我們采用的數據集。

圖三

圖三 下面將采用梯度上升法找到Logistic回歸分類器在此數據集上的最佳回歸系數。

2.2 訓練算法:使用梯度上升找到最佳參數

中有100個樣本點,每個點包含兩個數值型特征:X1和X2。在此數據集上,我們將通過使用梯度上升法找到最佳回歸系數,也就是擬合出Logistic回歸模型的最佳參數。

梯度上升法的偽代碼如下:

每個回歸系數初始化為1

重復R次:

計算整個數據集的梯度

使用alpha × gradient更新回歸系數的向量

返回回歸系數

下面是梯度上升算法的具體實現:

def loadDataSet():
    dataMat = []; labelMat = [];
    fr = open('testSet.txt')
    for line in fr.readlines():
        lineArr = line.strip().split()
        dataMat.append([1.0,float(lineArr[0]),float(lineArr[1])])
        labelMat.append(int(lineArr[2]))
    return dataMat,labelMat
dataMat,labelMat = loadDataSet()
print(dataMat)
print("----------------------------")
print(labelMat)
[[1.0, -0.017612, 14.053064], [1.0, -1.395634, 4.662541], [1.0, -0.752157, 6.53862], [1.0, -1.322371, 7.152853], [1.0, 0.423363, 11.054677], [1.0, 0.406704, 7.067335], [1.0, 0.667394, 12.741452], [1.0, -2.46015, 6.866805], [1.0, 0.569411, 9.548755], [1.0, -0.026632, 10.427743], [1.0, 0.850433, 6.920334], [1.0, 1.347183, 13.1755], [1.0, 1.176813, 3.16702], [1.0, -1.781871, 9.097953], [1.0, -0.566606, 5.749003], [1.0, 0.931635, 1.589505], [1.0, -0.024205, 6.151823], [1.0, -0.036453, 2.690988], [1.0, -0.196949, 0.444165], [1.0, 1.014459, 5.754399], [1.0, 1.985298, 3.230619], [1.0, -1.693453, -0.55754], [1.0, -0.576525, 11.778922], [1.0, -0.346811, -1.67873], [1.0, -2.124484, 2.672471], [1.0, 1.217916, 9.597015], [1.0, -0.733928, 9.098687], [1.0, -3.642001, -1.618087], [1.0, 0.315985, 3.523953], [1.0, 1.416614, 9.619232], [1.0, -0.386323, 3.989286], [1.0, 0.556921, 8.294984], [1.0, 1.224863, 11.58736], [1.0, -1.347803, -2.406051], [1.0, 1.196604, 4.951851], [1.0, 0.275221, 9.543647], [1.0, 0.470575, 9.332488], [1.0, -1.889567, 9.542662], [1.0, -1.527893, 12.150579], [1.0, -1.185247, 11.309318], [1.0, -0.445678, 3.297303], [1.0, 1.042222, 6.105155], [1.0, -0.618787, 10.320986], [1.0, 1.152083, 0.548467], [1.0, 0.828534, 2.676045], [1.0, -1.237728, 10.549033], [1.0, -0.683565, -2.166125], [1.0, 0.229456, 5.921938], [1.0, -0.959885, 11.555336], [1.0, 0.492911, 10.993324], [1.0, 0.184992, 8.721488], [1.0, -0.355715, 10.325976], [1.0, -0.397822, 8.058397], [1.0, 0.824839, 13.730343], [1.0, 1.507278, 5.027866], [1.0, 0.099671, 6.835839], [1.0, -0.344008, 10.717485], [1.0, 1.785928, 7.718645], [1.0, -0.918801, 11.560217], [1.0, -0.364009, 4.7473], [1.0, -0.841722, 4.119083], [1.0, 0.490426, 1.960539], [1.0, -0.007194, 9.075792], [1.0, 0.356107, 12.447863], [1.0, 0.342578, 12.281162], [1.0, -0.810823, -1.466018], [1.0, 2.530777, 6.476801], [1.0, 1.296683, 11.607559], [1.0, 0.475487, 12.040035], [1.0, -0.783277, 11.009725], [1.0, 0.074798, 11.02365], [1.0, -1.337472, 0.468339], [1.0, -0.102781, 13.763651], [1.0, -0.147324, 2.874846], [1.0, 0.518389, 9.887035], [1.0, 1.015399, 7.571882], [1.0, -1.658086, -0.027255], [1.0, 1.319944, 2.171228], [1.0, 2.056216, 5.019981], [1.0, -0.851633, 4.375691], [1.0, -1.510047, 6.061992], [1.0, -1.076637, -3.181888], [1.0, 1.821096, 10.28399], [1.0, 3.01015, 8.401766], [1.0, -1.099458, 1.688274], [1.0, -0.834872, -1.733869], [1.0, -0.846637, 3.849075], [1.0, 1.400102, 12.628781], [1.0, 1.752842, 5.468166], [1.0, 0.078557, 0.059736], [1.0, 0.089392, -0.7153], [1.0, 1.825662, 12.693808], [1.0, 0.197445, 9.744638], [1.0, 0.126117, 0.922311], [1.0, -0.679797, 1.22053], [1.0, 0.677983, 2.556666], [1.0, 0.761349, 10.693862], [1.0, -2.168791, 0.143632], [1.0, 1.38861, 9.341997], [1.0, 0.317029, 14.739025]]
----------------------------
[0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0]

函數loadDataSet(),它的主要功能是打開文本文件testSet.txt并逐行讀取。每行前兩個值分別是X1和X2,第三個值是數據對應的類別標簽。此外,為了方便計算,該函數還將X0的值設為1.0。

點擊 testSet.txt獲取文件。

sigmoid函數

def sigmoid(inx):
    return 1.0 / (1+np.exp(-inx))

gradAscent函數

def gradAscent(dataMatIn,classLabels):
    #?(以下兩行)轉換為numpy矩陣數據類型
    dataMatrix = np.mat(dataMatIn)
    labelMat = np.mat(classLabels).transpose()
    
    m,n = np.shape(dataMatrix)
    alpha = 0.001
    maxCycles = 500
    weights = np.ones((n,1))
    for k in range(maxCycles):
        # ?矩陣相乘
        h = sigmoid(dataMatrix*weights)
        error = (labelMat - h)
        weights = weights + alpha*dataMatrix.transpose()*error
        
    return weights

梯度上升算法的實際工作是在函數gradAscent()里完成的,該函數有兩個參數。第一個參數是dataMatIn,它是一個2維NumPy數組,每列分別代表每個不同的特征,每行則代表每個訓練樣本。我們現在采用的是100個樣本的簡單數據集,它包含了兩個特征X1和X2,再加上第0維特征X0,所以dataMath里存放的將是100×3的矩陣。在?處,我們獲得輸入數據并將它們轉換成NumPy矩陣。這是本書首次使用NumPy矩陣,如果你對矩陣數學不太熟悉,那么一些運算可能就會不易理解。比如,NumPy對2維數組和矩陣都提供一些操作支持,如果混淆了數據類型和對應的操作,執行結果將與預期截然不同。第二個參數是類別標簽,它是一個1×100的行向量。為了便于矩陣運算,需要將該行向量轉換為列向量,做法是將原向量轉置,再將它賦值給labelMat。接下來的代碼是得到矩陣大小,再設置一些梯度上升算法所需的參數。

變量alpha是向目標移動的步長,maxCycles是迭代次數。在for循環迭代完成后,將返回訓練好的回歸系數。需要強調的是,在?處的運算是矩陣運算。變量h不是一個數而是一個列向量,列向量的元素個數等于樣本個數,這里是100。對應地,運算dataMatrix * weights代表的不是一次乘積計算,事實上該運算包含了300次的乘積。

?中公式的前兩行,是在計算真實類別與預測類別的差值,接下來就是按照該差值的方向調整回歸系數

import numpy as np
weights = gradAscent(dataMat,labelMat)
print weights
[[ 4.12414349]
 [ 0.48007329]
 [-0.6168482 ]]

調用gradAscent函數,可以得到調整后的回歸系數。

畫出數據集和logistic回歸最佳擬合直線的函數

def plotBestFit(dataMat1,labelMat1,weights):
    import matplotlib.pyplot as plt  
    dataArr = np.array(dataMat1)
    n = np.shape(dataArr)[0]
    xcord1 = []; ycord1 = [];
    xcord2 = []; ycord2 = [];
    
    for i in range(n):
        if int(labelMat[i]==1):
            xcord1.append(dataArr[i,1])
            ycord1.append(dataArr[i,2])
        else:
            xcord2.append(dataArr[i,1])
            ycord2.append(dataArr[i,2])
            
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1,ycord1,s=30,c='red',marker='s')
    ax.scatter(xcord2,ycord2,s=30,c='green')
    
    x = np.arange(-3.0,3.0,0.1)
    # ?  最佳擬合直線
    y1 = (-weights[0]-weights[1]*x)/weights[2] 
    y = y1.reshape(60,1)
    ax.plot(x,y)
    
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()

?處設置了sigmoid函數為0。回憶5.2節,0是兩個類別(類別1和類別0)的分界處。因此,我們設定 $ 0 = w0x0+ w1x1 + w2x2 $,然后解出X2和X1的關系式(即分隔線的方程,注意X0=1)。

%matplotlib inline
plotBestFit(np.array(dataMat),np.array(labelMat),weights)
500次迭代擬合直線

圖四 梯度上升算法在500次迭代后得到的Logistic回歸最佳擬合直線
從這個分類結果相當不錯,從圖上看只錯分了兩到四個點。但是,盡管雷子簡單且數據集很小,這個方法卻需要大量的計算(300次乘法)。因此下一節將對該算法稍作改進,從而使它可以用在真實數據集上。

2.4 訓練算法:隨機梯度上升

梯度上升算法在每次更新回歸系數時都需要遍歷整個數據集,該方法在處理100個左右的數據集時尚可,但如果有數十億樣本和成千上萬的特征,那么該方案的計算復雜度就太高了。一種改進方法是一次僅用一個樣本點來更新回歸系數,該方法稱為隨機梯度上升算法。由于可以在新樣本到來時對分類器進行增量式更新,因而隨機梯度上升算法是一個在線學習算法。與“在線學習”相對應,一次處理所有數據被稱作是“批處理”。

隨機梯度上升算法可以寫成如下的偽代碼:

所有回歸系數初始化1

對數據集中每個樣本

   計算該樣本的梯度
   使用alpha x gradient 更新回歸系數值

返回回歸系數值

以下是隨機梯度上升算法實現代碼。

def stocGradAscent0(dataMatrix, classLabels):
    m,n = shape(dataMatrix)
    alpha = 0.01
    weights = ones(n)
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * dataMatrix[i]
    return weights 

可以看到,隨機梯度上升算法與梯度上升算法在代碼上很相似,但也有一些區別:第一,后者的變量h和誤差error都是向量,而前者則全是數值;第二,前者沒有矩陣的轉換過程,所有變量的數據類型都是Numpy數組。

下面驗證該方法的結果:

from numpy import *
dataArr,labelMat=loadDataSet()
weights=stocGradAscent0(array(dataArr),labelMat)
plotBestFit(dataArr,labelMat,weights)
image

執行完畢后將得到 圖五 所示的最佳擬合直線圖,該圖與圖四有一些相似之處??梢钥吹?,擬合出來的直線效果還不錯,但并不像圖四那樣完美。這里的分類器錯分了三分之一的樣本。

直接比較程序清單3和程序清單1的代碼結果是不公平的,后者的結果是在整個數據集上迭代了500次才得到的。一個判斷優化算法優劣的可靠方法是看它是否收斂,也就是說參數是否達到了穩定值,是否還會不斷地變化?對此,我們在程序清單5-3中隨機梯度上升算法上做了些修改,使其在整個數據集上運行200次。最終繪制的三個回歸系數的變化情況如圖六所示。

image

圖六 運行隨機梯度上升算法,在數據集的一次遍歷中回歸系數與迭代次數的關系圖?;貧w系數經過大量迭代才能達到穩定值,并且仍然有局部的波動現象。

圖六展示了隨機梯度上升算法在200次迭代過程中回歸系數的變化情況。其中的系數2,也就是圖5-5中的X2只經過了50次迭代就達到了穩定值,但系數1和0則需要更多次的迭代。另外值得注意的是,在大的波動停止后,還有一些小的周期性波動。不難理解,產生這種現象的原因是存在一些不能正確分類的樣本點(數據集并非線性可分),在每次迭代時會引發系數的劇烈改變。我們期望算法能避免來回波動,從而收斂到某個值。另外,收斂速度也需要加快。

對于圖六存在的問題,可以通過修改程序stocGradAscent0 的隨機梯度上升算法來解決,代碼如下:

def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    import numpy as np
    m,n = np.shape(dataMatrix)
    weights = np.ones(n)
    for j in range(numIter):         
        dataIndex = range(m)
        for i in range(m):
            #?  alpha每次迭代時需要調整
            alpha = 4/(1.0+j+i)+0.01
            #?  隨機選取更新 
            randIndex = int(np.random.uniform(0,len(dataIndex)))
            h = sigmoid(sum(dataMatrix[randIndex]*weights))
            error = classLabels[randIndex] - h
            weights = weights + alpha * error * dataMatrix[randIndex]
            del(dataIndex[randIndex])
    return weights

stocGradAscent1 與 stocGradAscent0 類似,但增加了兩處代碼來進行改進。

第一處改進在?處。一方面,alpha在每次迭代的時候都會調整,這會緩解圖六上的數據波動或者高頻波動。另外。雖然alpha會隨著迭代次數不斷減小,但永遠不會減小到0,這是因為?中還存在一個常數項。必須這樣做的原因是為了保證多次迭代之后新數據仍然具有一定得影響。如果要處理的問題是動態變化的,那么可以適當加大上述常數項,來確保新的值獲得更大的回歸系數。另一點值得注意的是,在降低alpha的函數中,alpha每次減少** 1/(j+i)** ,其中j時迭代次數,i是樣本點的下標。這樣當j<<max(i)時,alpha就不是嚴格下降的。避免參數的嚴格下降也常見于模擬退火算法等其他優化算法。

第二個改進的地方在?處,這里通過隨機選取樣本來更新回歸系數。這種方法將減少周期性的波動(如圖五中的波動)。這種方法每次隨機從列表中選出一個值,然后從列表中刪掉該值(再進行下一次迭代)。

此外,改進算法還增加了一個迭代次數作為第3個參數。如果該參數沒有給定的話,算法將默認迭代150次。如果給定,那么算法將按照新的參數值進行迭代。

圖七 顯示了每次迭代時各個回歸系數的變化情況。
image

該方法比采用固定alpha的方法收斂速度更快。

比較圖七和圖六可以看到兩點不同。第一點是,圖七中的系數沒有像圖六里那樣出現周期性的波動,這歸功于stocGradAscent1()里的樣本隨機選擇機制;第二點是,圖七的水平軸比圖六短了很多,這是由于stocGradAscent1()可以收斂得更快。這次我們僅僅對數據集做了20次遍歷,而之前的方法是500次。

看一下在同一個數據集上的分類效果:

%matplotlib inline
dataMat,labelMat = loadDataSet()
weights = stocGradAscent1(np.array(dataMat),np.array(labelMat))
plotBestFit(np.array(dataMat),np.array(labelMat),weights)
png

從結果圖中可以看出,分割線達到了與GradientAscent()差不多的效果,但是所使用的計算量更少。默認迭代次數是150,可以通過stocGradAscent()的第3個參數來對此進行修改。

3 示例:從疝氣病癥預測病馬的死亡率

下面將使用Logistic回歸來預測患有疝病的馬的存活問題。這里的數據1包含368個樣本和28個特征。我并非育馬專家,從一些文獻中了解到,疝病是描述馬胃腸痛的術語。然而,這種病不一定源自馬的胃腸問題,其他問題也可能引發馬疝病。該數據集中包含了醫院檢測馬疝病的一些指標,有的指標比較主觀,有的指標難以測量,例如馬的疼痛級別。

數據集來自2010年1月11日的UCI機器學習數據庫(http://archive.ics.uci.edu/ml/datasets/Horse+Colic)。該數據最早由加拿大安大略省圭爾夫大學計算機系的Mary McLeish和Matt Cecile收集。

示例:使用Logistic回歸估計馬疝病的死亡率

  1. 收集數據:給定數據文件。
  2. 準備數據:用Python解析文本文件并填充缺失值。
  3. 分析數據:可視化并觀察數據。
  4. 訓練算法:使用優化算法,找到最佳的系數。
  5. 測試算法:為了量化回歸的效果,需要觀察錯誤率。根據錯誤率決定是否回退到訓練階段,通過改變迭代的次數和步長等參數來得到更好的回歸系數。
  6. 使用算法:實現一個簡單的命令行程序來收集馬的癥狀并輸出預測結果。

另外需要說明的是,除了部分指標主觀和難以測量之外,該數據集還存在一個問題,數據集中有30%的數據值是缺失的。下面將首先介紹如何處理數據集中的數據缺失問題,然后再利用Logistic回歸和隨機梯度上升算法來預測病馬的生死。

3.1 準備數據:處理數據中的缺失值

數據中的缺失值是個非常棘手的問題,有很多文獻都致力于解決這個問題。那么,數據缺失究竟帶來了什么問題?假設有100個樣本和20個特征,這些數據都是機器收集回來的。若機器上的某個傳感器損壞導致一個特征無效時該怎么辦?此時是否要扔掉整個數據?這種情況下,另外19個特征怎么辦?它們是否還可用?答案是肯定的。因為有時候數據相當昂貴,扔掉和重新獲取都是不可取的,所以必須采用一些方法來解決這個問題。

下面是一些可選的做法:

  • 使用可用特征的均值來填補缺失值;
  • 使用特殊值來填補缺失值,如-1;
  • 忽略有缺失值的樣本;
  • 使用相似樣本的均值填補缺失值;
  • 使用另外的機器學習算法預測缺失值。

在數據預處理階段需要做兩件事:第一,所有的缺失值必須用一個實數來替換,因為我們使用的NumPy數據類型不允許包含缺失值。這里選擇實數0來替換所有缺失值,恰好能適用于Logistic回歸。原因在于,我們需要的是一個在更新時不會影響系數的值。回歸系數的更新公式如下:
$$ weights = weights + alpha * error * dataMatrix[randIndex] $$

如果dataMatrix的某特征對應值為0,那么該特征的系數將不做更新,即:
$$ weights = weights $$

另外,由于sigmoid(0)=0.5,即它對結果的預測不具有任何傾向性,因此上述做法也不會對誤差項造成任何影響?;谏鲜鲈?,將缺失值用0代替既可以保留現有數據,也不需要對優化算法進行修改。此外,該數據集中的特征取值一般不為0,因此在某種意義上說它也滿足“特殊值”這個要求。

預處理中做的第二件事是,如果在測試數據集中發現了一條數據的類別標簽已經缺失,那么我們的簡單做法是將該條數據丟棄。這是因為類別標簽與特征不同,很難確定采用某個合適的值來替換。采用Logistic回歸進行分類時這種做法是合理的,而如果采用類似kNN的方法就可能不太可行。

原始的數據集經過預處理之后保存成兩個文件:horseColicTest.txthorseColicTraining.txt?,F在我們有一個“干凈”可用的數據集和一個不錯的優化算法,下面將把這些部分融合在一起訓練出一個分類器,然后利用該分類器來預測病馬的生死問題。

3.2 測試算法:用Logistic回歸進行分類

前面介紹了優化算法,但目前為止還沒有在分類上做任何實際嘗試。使用Logistic回歸方法進行分類并不需要做很多工作,所需做的只是把測試集上每個特征向量乘以最優化方法得來的回歸系數,再將該乘積結果求和,最后輸入到Sigmoid函數中即可。如果對應的Sigmoid值大于0.5就預測類別標簽為1,否則為0。

Logistic回歸分類函數:

def classifyVector(inX,weights):
    prob = sigmoid(sum(inX*weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0

函數classifyVector(),它以回歸系數和特征向量作為輸入來計算對應的Sigmoid值。如果Sigmoid值大于0.5函數返回1,否則返回0。

def colicTest():
    frTrain = open('horseColicTraining.txt')
    frTest = open('horseColicTest.txt')
    trainingSet = []; trainingLabels = [];
    for line in frTrain.readlines():
        currLine = line.strip().split('\t')
        lineArr = []
        for i in range(21):
            lineArr.append(float(currLine[i]))
        trainingSet.append(lineArr)
        trainingLabels.append(float(currLine[21]))

    trainWeights = stocGradAscent1(np.array(trainingSet),np.array(trainingLabels),500)
    
    errorCount = 0; numTestVec = 0.0
    for line in frTest.readlines():
        numTestVec += 1.0
        currLine = line.strip().split('\t')
        lineArr = []
        for i in range(21):
            lineArr.append(float(currLine[i]))
        if int(classifyVector(np.array(lineArr),trainWeights)) != int(currLine[21]):
            errorCount += 1
        
    errorRate = (float(errorCount / numTestVec))
    print "錯誤率 = %f" %errorRate
    return errorRate

函數colicTest(),是用于打開測試集和訓練集,并對數據進行格式化處理的函數。該函數首先導入訓練集,同前面一樣,數據的最后一列仍然是類別標簽。數據最初有三個類別標簽,分別代表馬的三種情況:“仍存活”、“已經死亡”和“已經安樂死”。這里為了方便,將“已經死亡”和“已經安樂死”合并成“未能存活”這個標簽 。數據導入之后,便可以使用函數stocGradAscent1()來計算回歸系數向量。這里可以自由設定迭代的次數,例如在訓練集上使用500次迭代,實驗結果表明這比默認迭代150次的效果更好。在系數計算完成之后,導入測試集并計算分類錯誤率。整體看來,colicTest()具有完全獨立的功能,多次運行得到的結果可能稍有不同,這是因為其中有隨機的成分在里面。如果在stocGradAscent1()函數中回歸系數已經完全收斂,那么結果才將是確定的。

def multiTest():
    numTests = 10;errorSum = 0.0
    for k in range(numTests):
        errorSum += colicTest()
    print " %d次平均錯誤率是:%f" % (numTests,errorSum/float(numTests))

最后一個函數是multiTest(),其功能是調用函數colicTest()10次并求結果的平均值。調用multiTest()函數,可以看到打印的10次錯誤率:

multiTest()
/usr/local/lib/python2.7/site-packages/ipykernel_launcher.py:2: RuntimeWarning: overflow encountered in exp
  


錯誤率 = 0.358209
錯誤率 = 0.313433
錯誤率 = 0.462687
錯誤率 = 0.402985
錯誤率 = 0.358209
錯誤率 = 0.402985
錯誤率 = 0.313433
錯誤率 = 0.373134
錯誤率 = 0.373134
錯誤率 = 0.402985
 10次平均錯誤率是:0.376119

從上面的結果可以看到,10次迭代之后的平均錯誤率為37%。事實上,這個結果并不差,因為數據集有30%的數據已經缺失。當然,如果調整colicTest()中的迭代次數和stochGradAscent1()中的步長,平均錯誤率可以降到20%左右。

小結

Logistic回歸的目的是尋找一個非線性函數Sigmoid的最佳擬合參數,求解過程可以由最優化算法來完成。在最優化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以簡化為隨機梯度上升算法。
隨機梯度上升算法與梯度上升算法的效果相當,但占用更少的計算資源。此外,隨機梯度上升是一個在線算法,它可以在新數據到來時就完成參數更新,而不需要重新讀取整個數據集來進行批處理運算
機器學習的一個重要問題就是如何處理缺失數據。這個問題沒有標準答案,取決于實際應用中的需求。現有一些解決方案,每種方案都各有優缺點。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容