[完]機器學習實戰 第七章 利用AdaBoost元算法提高分類性能

【參考書籍】機器學習實戰(Machine Learning in Action)

AdaBoost,一種元算法(meta-algorithm)或者集成方法(ensemble method),是對其他算法進行組合的一種方式。有人認爲AdaBoost是最好的監督學習的方法。使用集成算法時,可是不同算法的集成,也可是同一算法在不同設置下的集成,還可是數據集不同部分分配不同分類器之後的集成。

優點:泛化錯誤率低,易編碼,可應用在大部分分類器上,無參數調整。
缺點:對離羣點敏感。
適用數據類型:數值型和標稱型數據。

自舉匯聚法bootstrap aggregating),也稱爲bagging方法,是在從原始數據集選擇S次後得到S個數據集的一種技術。新數據集和原數據集的大小相等。每個數據集都是通過在原始數據集中隨機選擇一個樣本來進行替換而得到的。可以多次地選擇同一樣本,允許新數據集中可以有重複的值,而原始數據集的某些值在新集合中則不再出現。

在S個數據集建好後,將某個機器學習算法分別作用於某個數據集就得到S個分類器。對新數據分類時,可應用這S個分類器進行分類。選擇分類器投票結果中最多的類別作爲最後的分類結果。一種更先進的bagging方法,隨機森林(random forest)

Boosting與bagging類似,都使用多個分類器。但Boosting中,不同分類器是通過串行訓練得到的,每個新分類器都根據已訓練出的分類器的性能進行訓練。Boosting是通過集中關注被已有分類器錯分的那些數據獲得新的分類器。Boosting分類結果基於所有分類器的加權求和結果,而bagging中的分類器權重相等的。

最流行的Boosting方法是AdaBoost,AdaBoot的大部分時間都用於訓練上,分類器將多次在同一數據集上訓練弱分類器

AdaBoost(Adaptive Boosting,自適應Boosting)的運行過程如下:爲訓練數據中的每個樣本賦予一個權重,這些權重構成了向量D。初始時,權重相等。首先在訓練數據上訓練出一個弱分類器並計算該分類器的錯誤率,然後再同一數據集上再次訓練弱分類器。在分類器的第二次訓練中,重新調整每個樣本的權重,降低第一次分對的樣本的權重,提高第一次錯分樣本的權重。爲從所有弱分類器中得到最終的分類器結果,AdaBoost爲每個分類器都分配一個權重值alpha,這些alpha是基於每個弱分類器的錯誤率計算的,其中,錯誤率的定義是:

ϵ=

alpha的計算公式如下:

α=12ln(1ϵϵ)

得到alpha值後,對權重向量D進行更新。若某個樣本被正確分類,則該樣本的權重更改爲:

D(t+1)i=DtieαSum(D)

若樣本被錯分,則該樣本的權重更改爲:

D(t+1)i=DtieαSum(D)

計算出D後,AdaBoost開始下一輪迭代,AdaBoost會不斷地重複訓練和調整權重,直到訓練錯誤率爲0,或者弱分類器的數目達到了用戶指定值爲止。

單層決策樹decision stump,也稱爲決策樹樁),僅基於單個特徵來做決策。

測試錯誤率在達到了一個最小值之後,又開始上升,這種現象稱爲過擬合overfitting,也稱過學習)。有文獻稱,對於表現好的數據集,AdaBoost的測試錯誤率就會達到一個穩定值,並不會隨着分類器的增多而上升。

AdaBoostSVM監督機器學習中最強大的兩種方法,兩者擁有不少相似之處,可將弱分類器想象成SVM中的一個核函數,也可按照最大化某個最小間隔的方式重寫AdaBoost算法,它們的不同在於其所定義的間隔計算方式有所不同,在高維空間下,這兩者間的差異會更加明顯。

非均衡分類問題

前述的所有分類問題中,都假設所有類別的分類代價都一樣。但大多數情況下,不同類別的分類代價是不一樣的。分類器性能度量方法,非均衡問題。

分類性能度量指標:正確率召回率ROC曲線錯誤率:是在所有測試樣例中錯分的樣例比例。這樣度量錯誤掩蓋了樣例如何被錯分的事實。混淆矩陣(confusion matrix)幫助人們瞭解分類中的錯誤。

表1 一個二類問題的混淆矩陣,其中的輸出採用了不同的類別標籤
預測結果
+1-1

真實結果
+1真正例(TP,True Positive,真陽)僞反例(FN,False Negative,假陰)
-1僞正例(FP,False Positive,假陽)真反例(TN,True Negative,真陰)


將一個正例判爲正例,產生一個真正例(True Positive,TP,真陽)。將一個反例判爲反例,產生一個真反例(True Negative,TN,真陰)。例外兩種情況稱爲僞反例(False Negative,FN,假陰),和僞正例(False Positive,FP,假陽)。正確率(Precision)=TP/(TP+FP),給出的是預測爲正例的樣本中真正正例的比例。召回率(Recall)=TP/(TP+FN),給出的是預測爲正例的真實正例佔所有真實正例的比例。在召回率很大的分類器中,真正判錯的正例的數目並不多。很難保證一個分類器同時具有高正確率和高召回率。

另一個度量分類中的非均衡性的工具是ROC曲線(ROC Curve),ROC代表接受者操作特徵(receiver operating characteristic),在圖1中的ROC曲線中,橫軸爲僞正例的比例(假陽率=FP/(TP+TN)),縱軸爲真正例的比例(真陽率=TP/(TP+FN))。ROC曲線不但用於比較分類器,還可基於成本效益分析(cost-versus-benefit)來做出決策。理想中的分類器應該儘可能處於左上角,在假陽率很低的同時獲取很高的真陽率。


利用10個單層決策樹的AdaBoost馬疝病檢測系統的ROC曲線
圖1 利用10個單層決策樹的AdaBoost馬疝病檢測系統的ROC曲線

對不同的ROC曲線進行比較的一個指標是曲線下的面積(Area Unser the Curve,AUC),它給出了分類器的平均性能值。爲了畫出ROC曲線,分類器必須提供每個樣例被判爲陽性或者隱形的可信程度值。樸素貝葉斯提供一個可能性,Logistic迴歸中輸入到Sigmoid函數中的是一個數值。在AdaBoost和SVM中,都會計算出一個數值輸入到sign()函數中。這些值可用於衡量給定分類器的預測強度。

基於代價函數的分類器決策控制,除調節分類器的閾值外,代價敏感的學習(cost-sensitive learning)也可用於處理非均衡分類。引入代價信息的方法,在AdaBoost中,可基於代價函數來調整錯誤權重向量D;在樸素貝葉斯中,可選擇具有最小期望代價而不是最大概率的類別作爲最後的結果;在SVM中,可在代價函數中對於不同的類別選擇不同的參數C。這些做法會給較小類更多的權重,即在訓練時,小類當中只允許更少的錯誤。

處理非均衡問題的數據抽樣方法,就是對分類器的訓練數據進行改造。可通過欠抽樣(undersampling)過抽樣(oversampling)來實現。過抽樣意味着複製樣例,或者加入與已有樣例相似的點(加入已有數據點的插值點,可能導致過擬合問題)。欠抽樣意味着刪除樣例,此方法的缺點在於確定哪些樣例需要進行剔除,在選擇剔除的樣例中可能攜帶了剩餘樣例中並不包含的有價值信息。一種解決方法,選擇那些離決策邊界較遠的樣例進行刪除。

小結

多個分類器組合可能會進一步凸顯單個分類器的不足,如過擬合問題。若多個分類器間差別顯著,可能會緩解這一問題。差別可以是算法本身或者應用於算法上的數據的不同。

針對錯誤的調節能力是AdaBoost的長處,AdaBoost函數可以應用於任意能夠處理加權數據的分類器。AdaBoost算法十分強大,它能夠快速處理其他分類器難以處理的數據集。

使用的函數

函數 功能
arr.argsort() 返回的是數組值從小到大的索引值
mat1.tolist() 將矩陣轉換成列表,一般進行迭代循環時,需要使用for i in mat1.tolist()[0]:

程序代碼

# coding=utf-8

from numpy import *

def loadSimpData() :
    datMat = matrix([[1. , 2.1],
        [2. , 1.1],
        [1.3, 1. ],
        [1. , 1. ],
        [2. , 1. ]])
    classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
    return datMat, classLabels

# 是否有某個值小於或者大於測試的閾值
# threshVal:閾值(threshold)
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq) :
    retArray = ones((shape(dataMatrix)[0], 1))
    if threshIneq == 'lt' :
        retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
    else : 
        retArray[dataMatrix[:, dimen] > threshVal] = 1.0
    return retArray

# 在一個加權數據集中循環,並找到具有最低錯誤率的單層決策樹
# 遍歷stumpClassify()函數所有的可能輸入值,找到數據集上最佳的單層決策樹
# 這裏的“最佳”是基於數據的權重向量D來定義的
def buildStump(dataArr, classLabels, D) :
    dataMatrix = mat(dataArr)
    labelMat = mat(classLabels).T
    m,n = shape(dataMatrix)
    # numSteps用於在特徵的所有可能值上進行遍歷
    numSteps = 10.0
    # bestStump用於存儲給定權重向量D時所得到的最佳單層決策樹的相關信息
    bestStump = {}
    bestClassEst = mat(zeros((m,1)))
    # minError則在一開始就初始化爲正無窮大,之後用於尋找可能的最小錯誤率
    minError = inf
    # 第一層for循環在數據集的所有特徵上遍歷
    for i in range(n) :
        # 特徵爲數值型,可通過計算最小值和最大值來了解應該需要多大的步長
        rangeMin = dataMatrix[:, i].min()
        rangeMax = dataMatrix[:, i].max()
        stepSize = (rangeMax - rangeMin)/numSteps
        # 第二層在步長上進行遍歷,甚至將閾值設置爲整個取值範圍之外也是可以的,
        # 在取值之外還應該有兩個額外的步驟
        for j in range(-1, int(numSteps) + 1) :
            # 最後一個for循環在大於和小於之間切換不等式
            for inequal in ['lt', 'gt'] :
                threshVal = rangeMin + float(j) * stepSize
                # 在數據集和三個循環變量上調用stumpClassify()函數,其中threshVal與j相關
                predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)
                # 構建errArr列向量,默認所有元素都爲1,如果預測的結果和labelMat中標籤值相等,則置爲0
                errArr = mat(ones((m, 1)))
                errArr[predictedVals == labelMat] = 0
                # 將errArr向量和權重向量D的相應元素相乘並求和,得到數值weightedError,這就是AdaBoost和分類器交互的地方
                # 即,計算加權錯誤率
                weightedError = D.T*errArr
                print "split: dim %d, thresh %.2f, thresh ineqal: %s, the weighted error is %.3f" %\
                    (i, threshVal, inequal, weightedError)
                # 將當前的錯誤率與已有的最小錯誤率進行對比,如果當前的值較小,那麼就在詞典bestStump中保存該單層決策樹
                if weightedError < minError : 
                    minError = weightedError
                    bestClassEst = predictedVals.copy()
                    bestStump['dim'] = i
                    bestStump['thresh'] = threshVal
                    bestStump['ineq'] = inequal
    # 返回字典,錯誤率和類別估計值
    return bestStump, minError, bestClassEst

# 基於單層決策樹的AdaBoost訓練過程
# 輸入參數:數據集、類別標籤、迭代次數
def adaBoostTrainDS(dataArr, classLabels, numIt=40) :
    # 存儲得到單層決策樹
    weakClassArr = []
    # 數據點的數目
    m = shape(dataArr)[0]
    # 列向量D,包含每個數據點的權重,開始時每個數據點的權重相等,後續迭代會增加錯分數據的權重,
    # 降低正確分類數據的權重。D是一個概率分佈向量,所有元素之和爲1.0
    D = mat(ones((m,1))/m)
    # 列向量aggClassEst,記錄每個數據點的類別估計累計值
    aggClassEst = mat(zeros((m,1)))
    for i in range(numIt) :
        # 具有最小錯誤率的單層決策樹、最小的錯誤率,估計的類別向量
        bestStump, error, classEst = buildStump(dataArr, classLabels, D)
        print "D: ", D.T
        # alpha告訴總分類器本次單層決策樹輸出結果的權重,其中max(error, 1e-16)確保沒有錯誤時,不會發生除零溢出
        alpha = float(0.5*log((1.0-error)/max(error, 1e-16)))
        bestStump['alpha'] = alpha
        weakClassArr.append(bestStump)
        print "classEst: ", classEst.T 
        # 計算下一次迭代中的新的權重向量D
        expon = multiply(-1*alpha*mat(classLabels).T, classEst)
        D = multiply(D, exp(expon))
        D = D/D.sum()
        # 錯誤率累加計算
        aggClassEst += alpha*classEst
        print "aggClassEst: ", aggClassEst.T 
        aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T, ones((m,1)))
        errorRate = aggErrors.sum()/m

        print "total error: ", errorRate, "\n"
        if errorRate == 0.0 : break
    # 繪製ROC曲線時,需要返回aggClassEst
    return weakClassArr, aggClassEst

# 基於AdaBoost的分類
# 一個或者多個待分類樣例datToClass
# classifierArr:多個弱分類器組成的數組
def adaClassify(datToClass, classifierArr) :
    dataMatrix = mat(datToClass)
    m = shape(dataMatrix)[0]
    aggClassEst = mat(zeros((m,1)))
    for i in range(len(classifierArr)) :
        classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'], classifierArr[i]['thresh'], classifierArr[i]['ineq'])
        aggClassEst += classifierArr[i]['alpha'] * classEst
        print aggClassEst
    return sign(aggClassEst)

# 自適應數據加載函數,該函數能夠自動檢測出特徵的數目,假定最後一個特徵是類別標籤
def loadDataSet(fileName) :
    numFeat = len(open(fileName).readline().split('\t'))
    dataMat = []; labelMat = []
    fr = open(fileName)
    for line in fr.readlines():
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat - 1) :
            lineArr.append(float(curLine[i]))
        dataMat.append(lineArr)
        labelMat.append(float(curLine[-1]))
    return dataMat, labelMat

# predStrengths: 一個Numpy數組或者一個行向量組成的矩陣,該參數代表的是分類器的預測強度,
# 在分類器和訓練函數將這些數值應用到sign()函數之前,它們就已經產生
# classLabels: 類別標籤
def plotROC(predStrengths, classLabels) :
    import matplotlib.pyplot as plt
    # cur保留的是繪製光標的位置
    cur = (1.0, 1.0)
    # ySum則用於計算AUC的值
    ySum = 0.0
    # 通過數組過濾方式計算正例的數目,並賦給numPosClas,接着在x軸和y軸的0.0到1.0區間上繪點
    numPosClas = sum(array(classLabels)==1.0)
    # 在y軸上的步長
    yStep = 1/float(numPosClas)
    # 在x軸上的步長
    xStep = 1/float(len(classLabels) - numPosClas)
    # 獲取排好序的索引sortedIndicies,這些索引從小到大排序。需要從<1, 1>開始繪,一直到<0,0>
    sortedIndicies = predStrengths.argsort()
    fig = plt.figure()
    fig.clf()
    ax = plt.subplot(111)
    # 在所有排序值上進行循環。這些值在一個NumPy數組或矩陣中進行排序,
    # python則需要一個表來進行迭代循環,因此需要調用tolist()方法
    for index in sortedIndicies.tolist()[0] :
        # 每得到一個標籤爲1.0的類,則要沿着y軸的方向下降一個步長,即降低真陽率
        if classLabels[index] == 1.0 :
            delX = 0; delY = yStep
        # 對於每個其他的標籤,則是x軸方向上倒退一個步長(假陰率方向),
        # 代碼只關注1這個類別標籤,採用1/0標籤還是+1/-1標籤就無所謂了
        else :
            delX = xStep; delY = 0
            # 所有高度的和ySum隨着x軸的每次移動而漸次增加
            ySum += cur[1]
        # 一旦決定了在x軸還是y軸方向上進行移動,就可在當前點和新點之間畫出一條線段 
        ax.plot([cur[0], cur[0]-delX], [cur[1], cur[1]-delY], c='b')
        cur = (cur[0]-delX, cur[1]-delY)
    ax.plot([0,1], [0,1], 'b--')
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('ROC curve for AdaBoost Horse Colic Detection System')
    ax.axis([0,1,0,1])
    plt.show()
    # 計算AUC需要對多個小矩形的面積進行累加,這些小矩形的寬度都是xStep,
    # 因此可對所有矩形的高度進行累加,然後再乘以xStep得到其總面積
    print "the Area Under the Curve is: ", ySum*xStep

在命令行中執行:

>>> import ml.adaboost as adaboost
>>> datMat, classLabels=adaboost.loadSimpData()
# 構建單層決策樹
>>> from numpy import *
>>> D=mat(ones(5,1)/5)
>>> adaboost.buildStump(datMat, classLabels, D)
split: dim 0, thresh 0.90, thresh ineqal: lt, the weighted error is 0.400
split: dim 0, thresh 0.90, thresh ineqal: gt, the weighted error is 0.400
split: dim 0, thresh 1.00, thresh ineqal: lt, the weighted error is 0.400
......
split: dim 0, thresh 1.90, thresh ineqal: lt, the weighted error is 0.200
split: dim 0, thresh 1.90, thresh ineqal: gt, the weighted error is 0.400
......
split: dim 1, thresh 2.10, thresh ineqal: lt, the weighted error is 0.600
split: dim 1, thresh 2.10, thresh ineqal: gt, the weighted error is 0.400
({'dim': 0, 'ineq': 'lt', 'thresh': 1.3}, matrix([[ 0.2]]), array([[-1.],
       [ 1.],
       [-1.],
       [-1.],
       [ 1.]]))
# 完整AdaBoost算法的實現
>>> reload(adaboost)
<module 'ml.adaboost' from 'C:\Python27\ml\adaboost.py'>
>>> classifierArray = adaboost.adaBoostTrainDS(datMat, classLabels, 9)
D:  [[ 0.2  0.2  0.2  0.2  0.2]]
classEst:  [[-1.  1. -1. -1.  1.]]
aggClassEst:  [[-0.69314718  0.69314718 -0.69314718 -0.69314718  0.69314718]]
total error:  0.2

D:  [[ 0.5    0.125  0.125  0.125  0.125]]
classEst:  [[ 1.  1. -1. -1. -1.]]
aggClassEst:  [[ 0.27980789  1.66610226 -1.66610226 -1.66610226 -0.27980789]]
total error:  0.2

D:  [[ 0.28571429  0.07142857  0.07142857  0.07142857  0.5       ]]
classEst:  [[ 1.  1.  1.  1.  1.]]
aggClassEst:  [[ 1.17568763  2.56198199 -0.77022252 -0.77022252  0.61607184]]
total error:  0.0

>>> reload(adaboost)
<module 'ml.adaboost' from 'C:\Python27\ml\adaboost.py'>
>>> datArr, labelArr = adaboost.loadSimpData()
>>> classifierArr = adaboost.adaBoostTrainDS(datArr, labelArr, 30)
>>> adaboost.adaClassify([[5,5],[0,0]],classifierArr)
[[ 0.69314718]
 [-0.69314718]]
[[ 1.66610226]
 [-1.66610226]]
[[ 2.56198199]
 [-2.56198199]]
matrix([[ 1.],
        [-1.]])
# 自適應數據加載
>>> reload(adaboost)
>>> dataArr, labelArr = adaboost.loadDataSet('c:\python26\ml\\horseColicTraining2.txt')
>>> classifierArray = adaboost.adaBoostTrainDS(dataArr, labelArr, 10)
>>> testArr, testLabelArr = adaboost.loadDataSet('c:\python27\ml\\horseColicTest2.txt')
>>> prediction10 = adaboost.adaClassify(testArr, classifierArray)
# 獲取被錯誤分類的元素個數
>>> errArr = mat(ones((67,1)))
>>> errArr[prediction10!=mat(testLabelArr).T].sum()
# 繪製ROC
>>> reload(adaboost)
>>> dataArr, labelArr = adaboost.loadDataSet('c:\python27\ml\\horseColicTraining2.txt')
>>> classifierArray, aggClassEst = adaboost.adaBoostTrainDS(dataArr, labelArr, 10)
>>> adaboost.plotROC(aggClassEst.T, labelArr)
the Area Under the Curve is:  0.858296963506
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章