前言
本文將介紹機器學習分類算法中的Logistic回歸分類算法並給出偽代碼,Python代碼實現。
(說明:從本文開始,將接觸到最優化算法相關的學習。旨在將這些最優化的算法用於訓練出一個非線性的函數,以用於分類。)
算法原理
首先要提到的概念是回歸。
對於回歸這個概念,在以后的文章會有系統而深入的學習。簡單的說,回歸就是用一條線對N多數據點進行一個擬合,這個擬合的過程就叫做回歸。
Logistic回歸分類算法就是對數據集建立回歸公式,以此進行分類。
而至於如何尋找最佳回歸系數,或者說是分類器的訓練,就需要使用到最優化算法了。
回歸分類器的形式
基本形式是用每個特征都乘以一個回歸系數,然后把所有的結果值相加。
這樣算出的很多結果都是連續的,不利於分類,故可以將結果再帶入到一個Sigmoid函數以得到一些比較離散的分類結果。
Sigmoid函數的輪廓如下:

這樣,計算的結果會是一個0-1的值。進而0.5以上歸為一類,以下歸為一類即可。(一般的邏輯回歸只能解決兩個分類的問題)
接下來的工作重點就轉移到了最佳回歸系數的確定了。
最佳回歸系數的確定
確定最佳回歸系數的過程,也就是對數據集進行訓練的過程。
求最佳回歸系數的步驟如下:
1. 列出分類函數:
(θ 指回歸系數,在實踐中往往會再對結果進行一個Sigmoid轉換)
2. 給出分類函數對應的錯誤估計函數:

(m為樣本個數)
只有當某個θ向量使上面的錯誤估計函數J(θ)取得最小值的時候,這個θ向量才是最佳回歸系數向量。
3. 采用梯度下降法或者最小二乘法求錯誤函數取得最小值的時候θ的取值:

為表述方便,上式僅為一個樣本的情況,實際中要綜合多個樣本的情況需要進行一個求和 (除非你使用后面會介紹的隨機梯度上升算法),具體請參考下面的代碼實現部分。
將步驟 2 中的錯誤函數加上負號,就可以把問題轉換為求極大值,梯度下降法轉換為梯度上升法。
更詳盡的推導部分,在以后專門分析回歸的文章中給出。
基於梯度上升法的最佳回歸參數擬合
梯度上升法求最大值的核心思想是將自變量沿着目標函數的梯度方向移動,一直移動到指定的次數或者說某個允許的誤差范圍。
基於梯度上升法的分類偽代碼:
1 每個回歸系數初始化為1 2 重復R次: 3 計算整個數據集的梯度 4 使用 alpha * gradient 更新回歸系數向量 5 返回回歸系數
下面給出完整的實現及測試代碼(用到的數據集文件共 4 列,前 3 列為特征,最后一列為分類結果):
1 #!/usr/bin/env python 2 # -*- coding:UTF-8 -*- 3 4 ''' 5 Created on 20**-**-** 6 7 @author: fangmeng 8 ''' 9 10 import numpy 11 12 #===================================== 13 # 輸入: 14 # 空 15 # 輸出: 16 # dataMat: 測試數據集 17 # labelMat: 測試分類標簽集 18 #===================================== 19 def loadDataSet(): 20 '創建測試數據集,分類標簽集並返回。' 21 22 # 測試數據集 23 dataMat = []; 24 # 測試分類標簽集 25 labelMat = [] 26 # 文本數據源 27 fr = open('/home/fangmeng/testSet.txt') 28 29 # 載入數據 30 for line in fr.readlines(): 31 lineArr = line.strip().split() 32 dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])]) 33 labelMat.append(int(lineArr[2])) 34 35 return dataMat,labelMat 36 37 38 #===================================== 39 # 輸入: 40 # inX: 目標轉換向量 41 # 輸出: 42 # 1.0/(1+numpy.exp(-inX)): 轉換結果 43 #===================================== 44 def sigmoid(inX): 45 'sigmoid轉換函數' 46 47 return 1.0/(1+numpy.exp(-inX)) 48 49 #===================================== 50 # 輸入: 51 # dataMatIn: 數據集 52 # classLabels: 分類標簽集 53 # 輸出: 54 # weights: 最佳擬合參數向量 55 #===================================== 56 def gradAscent(dataMatIn, classLabels): 57 '基於梯度上升法的logistic回歸分類器' 58 59 # 將數據集,分類標簽集存入矩陣類型。 60 dataMatrix = numpy.mat(dataMatIn) 61 labelMat = numpy.mat(classLabels).transpose() 62 63 # 上升步長度 64 alpha = 0.001 65 # 迭代次數 66 maxCycles = 500 67 # 初始化回歸參數向量 68 m,n = numpy.shape(dataMatrix) 69 weights = numpy.ones((n,1)) 70 71 # 對回歸系數進行maxCycles次梯度上升 72 for k in range(maxCycles): 73 h = sigmoid(dataMatrix*weights) 74 error = (labelMat - h) 75 weights = weights + alpha * dataMatrix.transpose()* error 76 77 return weights 78 79 def test(): 80 '測試' 81 82 dataArr, labelMat = loadDataSet() 83 print gradAscent(dataArr, labelMat) 84 85 if __name__ == '__main__': 86 test()
測試結果:

擬合結果展示
可使用matplotlib畫決策邊界,用作分析擬合結果是否達到預期。
繪制及測試部分代碼如下所示:
1 #====================================== 2 # 輸入: 3 # weights: 回歸系數向量 4 # 輸出: 5 # 圖形化的決策邊界演示 6 #====================================== 7 def plotBestFit(weights): 8 '決策邊界演示' 9 10 import matplotlib.pyplot as plt 11 # 獲取數據集 分類標簽集 12 dataMat,labelMat=loadDataSet() 13 dataArr = numpy.array(dataMat) 14 15 # 兩種分類下的兩種特征列表 16 xcord1 = []; ycord1 = [] 17 xcord2 = []; ycord2 = [] 18 for i in range(numpy.shape(dataArr)[0]): 19 if int(labelMat[i])== 1: 20 xcord1.append(dataArr[i,1]); ycord1.append(dataArr[i,2]) 21 else: 22 xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2]) 23 24 fig = plt.figure() 25 ax = fig.add_subplot(111) 26 ax.scatter(xcord1, ycord1, s=30, c='red', marker='s') 27 ax.scatter(xcord2, ycord2, s=30, c='green') 28 29 # 繪制決策邊界 30 x = numpy.arange(-3.0, 3.0, 0.1) 31 y = (-weights[0]-weights[1]*x)/weights[2] 32 ax.plot(x, y) 33 34 plt.xlabel('X1'); plt.ylabel('X2'); 35 plt.show() 36 37 def test(): 38 '測試' 39 40 dataArr, labelMat = loadDataSet() 41 weights = gradAscent(dataArr, labelMat) 42 plotBestFit(weights.getA())
測試結果:

更好的求最值方法 - 隨機梯度上升
Logistic回歸的本質其實就是求擬合參數,而求擬合參數最核心的就是求錯誤估計函數的最小值。
仔細分析上面的代碼,會發現該分類的效率做多的耗費也是在求最值上面。由於每次都要用所有數據來計算梯度,導致數據集非常大的時候,該算法很不給力。
實踐表明,每次僅僅用一個樣本點來更新回歸系數,當所有樣本算完,也能達到相似的效果(僅僅是相似,或者說接近)。
由於可以在每個樣本到達的時候對分類器進行增量式更新,因此隨機梯度上升算法其實是一個在線學習算法。
基於隨機梯度上升的分類偽代碼:
1 所有回歸系數初始化為1 2 對數據集中的每個樣本: 3 計算該樣本的梯度 4 使用 alpha * gradient 更新回歸系數值 5 返回回歸系數值
請對比上面的基本梯度上升算法進行理解學習。
優化之后的分類算法函數如下:
1 #===================================== 2 # 輸入: 3 # dataMatIn: 數據集(注意是向量類型) 4 # classLabels: 分類標簽集 5 # 輸出: 6 # weights: 最佳擬合參數向量 7 #===================================== 8 def stocGradAscent0(dataMatrix, classLabels): 9 '基於隨機梯度上升法的logistic回歸分類器' 10 11 m,n = numpy.shape(dataMatrix) 12 # 上升步長度 13 alpha = 0.01 14 # 初始化回歸參數向量 15 weights = numpy.ones(n) 16 17 # 對回歸系數進行樣本數量次數的梯度上升,每次上升僅用一個樣本。 18 for i in range(m): 19 h = sigmoid(sum(dataMatrix[i]*weights)) 20 error = classLabels[i] - h 21 weights = weights + alpha * error * dataMatrix[i] 22 return weights
你也許會疑惑 "不是說隨機梯度上升算法嗎?隨機呢?",不用急,很快就會提到這個。
分析優化后的代碼可以看到兩個區別:一個是當前分類結果變量h和誤差變量都是數值類型(之前為向量類型),二是無矩陣轉換過程,數據集為numpy向量的數組類型。
測試結果:

對比優化前的算法結果,可以看出分類錯誤率更高了。
優化后的效果反而更差了?這樣說有點不公平,因為優化后的算法只是迭代了100次,而優化前的足足有500次。
那么接下來可以進一步優化,理論依據為:增加迭代計算的次數,當達到一個接近收斂或者已經收斂的狀態時,停止迭代。
那么如何做到這點呢?
第一是要動態的選定步長,第二,就是每次隨機的選定樣本,這就是為什么叫做隨機梯度上升算法了。
最終修改后的分類器如下:
1 #===================================== 2 # 輸入: 3 # dataMatIn: 數據集(注意是向量類型) 4 # classLabels: 分類標簽集 5 # 輸出: 6 # weights: 最佳擬合參數向量 7 #===================================== 8 def stocGradAscent1(dataMatrix, classLabels, numIter=150): 9 '基於隨機梯度上升法的logistic回歸分類器' 10 11 m,n = numpy.shape(dataMatrix) 12 weights = numpy.ones(n) 13 14 # 迭代次數控制 15 for j in range(numIter): 16 # 樣本選擇集 17 dataIndex = range(m) 18 # 隨機選取樣本遍歷一輪 19 for i in range(m): 20 # 動態修正步長 21 alpha = 4/(1.0+j+i)+0.0001 22 # 隨機選取變量進行梯度上升 23 randIndex = int(numpy.random.uniform(0,len(dataIndex))) 24 h = sigmoid(sum(dataMatrix[randIndex]*weights)) 25 error = classLabels[randIndex] - h 26 weights = weights + alpha * error * dataMatrix[randIndex] 27 # 從選擇集中刪除已經使用過的樣本 28 del(dataIndex[randIndex]) 29 return weights
運行結果:

這次,和最原始的基於梯度上升法分類器的結果就差不多了。但是迭代次數大大的減少了。
網上也有一些非常嚴格的證明隨機梯度上升算法的收斂速度更快(相比基礎梯度上升算法)的證明,有興趣的讀者可以查找相關論文。
小結
1. 邏輯回歸的計算代價不高,是很常用的分類算法。集中基於隨機梯度上升的邏輯回歸分類器能夠支持在線學習。
2. 但邏輯回歸算法缺點很明顯 - 一般只能解決兩個類的分類問題。
3. 另外邏輯回歸容易欠擬合,導致分類的精度不高。
