一,引言
前面幾章的介紹了幾種分類算法,當然各有優缺。如果將這些不同的分類器組合起來,就構成了我們今天要介紹的集成方法或者說元算法。集成方法有多種形式:可以使多種算法的集成,也可以是一種算法在不同設置下的集成,還可以將數據集的不同部分分配不同的分類器,再將這些分類器進行集成。
adaBoost分類器就是一種元算法分類器,adaBoost分類器利用同一種基分類器(弱分類器),基於分類器的錯誤率分配不同的權重參數,最后累加加權的預測結果作為輸出。
1 bagging方法
在介紹adaBoost之前,我們首先大致介紹一種基於數據隨機重抽樣的分類器構建方法,即bagging(bootstrap aggregating)方法,其是從原始數據集選擇s次后得到s個新數據集的一種技術。需要說明的是,新數據集和原數據集的大小相等。每個數據集都是通過在原始數據集上先后隨機選擇一個樣本來進行替換得到的新的數據集(即先隨機選擇一個樣本,然后隨機選擇另外一個樣本替換之前的樣本),並且這里的替換可以多次選擇同一樣本,也就是說某些樣本可能多次出現,而另外有一些樣本在新集合中不再出現。
s個數據集准備好之后,將某個學習算法分別作用於每個數據集就得到s個分類器。當要對新的數據進行分類時,就應用這s個分類器進行分類,最后根據多數表決的原則確定出最后的分類結果。
2 boosting方法
boosting方法就是我們本文要講到的分類算法,其與上面提到的bagging很類似,都是采用同一種基分類器的組合方法。而與bagging不同的是,boosting是集中關注分類器錯分的那些數據來獲得新的分類器
此外,bagging中分類器權重相等,而boosting中分類器的權值並不相等,分類器的錯誤率越低,那么其對應的權重也就越大,越容易對預測結果產生影響。boosting有許多版本,而今天要介紹的是比較流行的AdaBoost。
二,AdaBoost
AdaBoost的一般流程如下所示:
(1)收集數據
(2)准備數據:依賴於所用的基分類器的類型,這里的是單層決策樹,即樹樁,該類型決策樹可以處理任何類型的數據。
(3)分析數據
(4)訓練算法:利用提供的數據集訓練分類器
(5)測試算法:利用提供的測試數據集計算分類的錯誤率
(6)使用算法:算法的相關推廣,滿足實際的需要
接下來,具體闡述adaBoost分類算法
1 訓練算法:基於錯誤提升分類器的性能
上面所述的基分類器,或者說弱分類器,意味着分類器的性能不會太好,可能要比隨機猜測要好一些,一般而言,在二類分類情況下,弱分類器的分類錯誤率達到甚至超過50%,顯然也只是比隨機猜測略好。但是,強分類器的分類錯誤率相對而言就要小很多,adaBoost算法就是易於這些弱分類器的組合最終來完成分類預測的。
adaBoost的運行過程:訓練數據的每一個樣本,並賦予其一個權重,這些權值構成權重向量D,維度等於數據集樣本個數。開始時,這些權重都是相等的,首先在訓練數據集上訓練出一個弱分類器並計算該分類器的錯誤率,然后在同一數據集上再次訓練弱分類器,但是在第二次訓練時,將會根據分類器的錯誤率,對數據集中樣本的各個權重進行調整,分類正確的樣本的權重降低,而分類錯的樣本權重則上升,但這些權重的總和保持不變為1.
並且,最終的分類器會基於這些訓練的弱分類器的分類錯誤率,分配不同的決定系數alpha,錯誤率低的分類器獲得更高的決定系數,從而在對數據進行預測時起關鍵作用。alpha的計算根據錯誤率得來:
alpha=0.5*ln(1-ε/max(ε,1e-16))
其中,ε=為正確分類的樣本數目/樣本總數,max(ε,1e-16)是為了防止錯誤率為而造成分母為0的情況發生
計算出alpha之后,就可以對權重向量進行更新了,使得分類錯誤的樣本獲得更高的權重,而分類正確的樣本獲得更低的權重。D的計算公式如下:
如果某個樣本被正確分類,那么權重更新為:
D(m+1,i)=D(m,i)*exp(-alpha)/sum(D)
如果某個樣本被錯誤分類,那么權重更新為:
D(m+1,i)=D(m,i)*exp(alpha)/sum(D)
其中,m為迭代的次數,即訓練的第m個分類器,i為權重向量的第i個分量,i<=數據集樣本數量
當我們更新完各個樣本的權重之后,就可以進行下一次的迭代訓練。adaBoost算法會不斷重復訓練和調整權重,直至達到迭代次數,或者訓練錯誤率為0。
2 基於單層決策樹構建弱分類器
單層決策樹是一種簡單的決策樹,也稱為決策樹樁。單層決策樹可以看做是由一個根節點直接連接兩個葉結點的簡單決策樹,比如x>v或x<v,就可以看做是一個簡單決策樹。
為了更好的演示adaBoost的訓練過程,我們首先建立一個簡單的數據集,並將其轉為我們想要的數據格式,代碼如下:
#獲取數據集 def loadSimpData(): dataMat=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 dataMat,classLabels
接下來,我們就要通過上述數據集來尋找最佳的單層決策樹,最佳單層決策樹是具有最低分類錯誤率的單層決策樹,偽代碼如下:
#構建單層分類器 #單層分類器是基於最小加權分類錯誤率的樹樁 #偽代碼 #將最小錯誤率minError設為+∞ #對數據集中的每個特征(第一層特征): #對每個步長(第二層特征): #對每個不等號(第三層特征): #建立一顆單層決策樹並利用加權數據集對它進行測試 #如果錯誤率低於minError,則將當前單層決策樹設為最佳單層決策樹 #返回最佳單層決策樹
接下來看單層決策樹的生成函數代碼:
#單層決策樹的閾值過濾函數 def stumpClassify(dataMatrix,dimen,threshVal,threshIneq): #對數據集每一列的各個特征進行閾值過濾 retArray=ones((shape(dataMatrix)[0],1)) #閾值的模式,將小於某一閾值的特征歸類為-1 if threshIneq=='lt': retArray[dataMatrix[:,dimen]<=threshVal]=-1.0 #將大於某一閾值的特征歸類為-1 else: retArray[dataMatrix[:,dimen]>threshVal]=-1.0 def buildStump(dataArr,classLabels,D): #將數據集和標簽列表轉為矩陣形式 dataMatrix=mat(dataArr);labelMat=mat(classLabels).T m,n=shape(dataMatrix) #步長或區間總數 最優決策樹信息 最優單層決策樹預測結果 numSteps=10.0;bestStump={};bestClasEst=mat(zeros((m,1))) #最小錯誤率初始化為+∞ minError=inf #遍歷每一列的特征值 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 inequal in ['lt','gt']: #閾值計算公式:最小值+j(-1<=j<=numSteps+1)*步長 threshVal=(rangeMin+float(j)*stepSize) #選定閾值后,調用閾值過濾函數分類預測 predictedVals=\ stumpClassify(dataMatrix,i,threshVal,'inequal') #初始化錯誤向量 errArr=mat(ones((m,1))) #將錯誤向量中分類正確項置0 errArr[predictedVals==labelMat]=0 #計算"加權"的錯誤率 weigthedError=D.T*errArr #打印相關信息,可省略 #print("split: dim %d, thresh %.2f,thresh inequal:\ # %s, the weighted error is %.3f", # %(i,threshVal,inequal,weigthedError)) #如果當前錯誤率小於當前最小錯誤率,將當前錯誤率作為最小錯誤率 #存儲相關信息 if weigthedError<minError: minError=weigthedError bestClasEst=predictedVals.copy() bestStump['dim']=i bestStump['thresh']='threshVal' bestStump['ineq']=inequal #返回最佳單層決策樹相關信息的字典,最小錯誤率,決策樹預測輸出結果 return bestStump,minError,bestClasEst
需要說明的是,上面的代碼包含兩個函數,第一個函數是分類器的閾值過濾函數,即設定某一閾值,凡是超過該閾值的結果被歸為一類,小於閾值的結果都被分為另外一類,這里的兩類依然同SVM一樣,采用+1和-1作為類別。
第二個函數,就是建立單層決策樹的具體代碼,基於樣本值的各個特征及特征值的大小,設定合適的步長,獲得不同的閾值,然后以此閾值作為根結點,對數據集樣本進行分類,並計算錯誤率,需要指出的是,這里的錯誤率計算是基於樣本權重的,所有分錯的樣本乘以其對應的權重,然后進行累加得到分類器的錯誤率。錯誤率得到之后,根據錯誤率的大小,跟當前存儲的最小錯誤率的分類器進行比較,選擇出錯誤率最小的特征訓練出來的分類器,作為最佳單層決策樹輸出,並通過字典類型保存其相關重要的信息。
迭代的過程如下所示:
好了,弱分類器有了,那么我們接下來就可以來討論adaBoost的具體訓練過程了
3 完整AdaBoost算法實現
上面已經構建好了基於加權輸入值進行決策的單層分類器,那么就已經有了實現一個完整AdaBoost算法所需要的所有信息了。下面先看一下整個AdaBoost的偽代碼實現:
#完整AdaBoost算法實現 #算法實現偽代碼 #對每次迭代: #利用buildStump()函數找到最佳的單層決策樹 #將最佳單層決策樹加入到單層決策樹數組 #計算alpha #計算新的權重向量D #更新累計類別估計值 #如果錯誤率為等於0.0,退出循環
再來看看具體的實現代碼吧:
#adaBoost算法 #@dataArr:數據矩陣 #@classLabels:標簽向量 #@numIt:迭代次數 def adaBoostTrainDS(dataArr,classLabels,numIt=40): #弱分類器相關信息列表 weakClassArr=[] #獲取數據集行數 m=shape(dataArr)[0] #初始化權重向量的每一項值相等 D=mat(ones((m,1))/m) #累計估計值向量 aggClassEst=mat((m,1)) #循環迭代次數 for i in range(numIt): #根據當前數據集,標簽及權重建立最佳單層決策樹 bestStump,error,classEst=buildStump(dataArr,classLabels,D) #打印權重向量 print("D:",D.T) #求單層決策樹的系數alpha alpha=float(0.5*log((1.0-error)/(max(error,1e-16)))) #存儲決策樹的系數alpha到字典 bestStump['alpha']=alpha #將該決策樹存入列表 weakClassArr.append(bestStump) #打印決策樹的預測結果 print("classEst:",classEst.T) #預測正確為exp(-alpha),預測錯誤為exp(alpha) #即增大分類錯誤樣本的權重,減少分類正確的數據點權重 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") #錯誤率為0.0退出循環 if errorRate==0.0:break #返回弱分類器的組合列表 return weakClassArr
對於上面的代碼,需要說明的有一下幾點:
(1)上面的輸入除了數據集和標簽之外,還有用戶自己指定的迭代次數,用戶可以根據自己的成本需要和實際情況,設定合適的迭代次數,構建出需要的弱分類器數量。
(2)權重向量D包含了當前單層決策樹分類器下,各個數據集樣本的權重,一開始它們的值都相等。但是,經過分類器分類之后,會根據分類的權重加權錯誤率對這些權重進行修改,修改的方向為,提高分類錯誤樣本的權重,減少分類正確的樣本的權重。
(3)分類器系數alpha,是另外一個非常重要的參數,它在最終的分類器組合決策分類結果的過程中,起到了非常重要的作用,如果某個弱分類器的分類錯誤率更低,那么根據錯誤率計算出來的分類器系數將更高,這樣,這些分類錯誤率更低的分類器在最終的分類決策中,會起到更加重要的作用。
(4)上述代碼的訓練過程是以達到迭代的用戶指定的迭代次數或者訓練錯誤率達到要求而跳出循環。而最終的分類器決策結果,會通過sign函數,將結果指定為+1或者-1
下面是訓練的過程:
4 測試算法
那么有了訓練好的分類器,是不是要測試一下呢,畢竟訓練錯誤率針對的是已知的數據,我們需要在分類器未知的數據上進行測試,看看分類效果。上面的訓練代碼會幫我們保存每個弱分類器的重要信息,比如分類器系數,分類器的最優特征,特征閾值等。有了這些重要的信息,我們拿到之后,就可以對測試數據進行預測分類了
#測試adaBoost,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(dataMat,classifierArr[i]['dim'],\ classifierArr[i]['thresh'], classifierArr[i]['ineq']) #對各個分類器的預測結果進行加權累加 aggClassEst+=classifierArr[i]['alpha']*classEst print('aggClassEst',aggClassEst) #通過sign函數根據結果大於或小於0預測出+1或-1 return sign(aggClassEst)
看一個測試樣例
從結果看來,不拿發現,隨着迭代次數的增加,分類結果是逐漸越強的。
三,實例:難數據集上應用adaBoost
第四章的logistic回歸時用到了預測馬疝病是否死亡的數據集。這里,我們再次利用該存在30%數據缺失的數據集來進行adaBoost算法測試,比較其與logistic回歸分類器的分類錯誤率。
首先,從文件中加載數據集,轉變成我們想要的數據格式,先看下面自適應數據加載函數代碼:
#自適應加載數據 def loadDataSet(filename): #創建數據集矩陣,標簽向量 dataMat=[];labelMat=[] #獲取特征數目(包括最后一類標簽) #readline():讀取文件的一行 #readlines:讀取整個文件所有行 numFeat=len(open(filename).readline().split('\t')) #打開文件 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
與之前的加載數據代碼不同的是,該函數可以自動檢測出數據樣本的特征數目。好了,來看最終的測試代碼函數:
#訓練和測試分類器 def classify(): #利用訓練集訓練分類器 datArr,labelArr=loadDataSet('horseColicTraining2.txt') #得到訓練好的分類器 classifierArray=adaBoostTrainDS(datArr,labelArr,10) #利用測試集測試分類器的分類效果 testArr,testLabelArr=loadDataSet('horseClicTest2.txt') prediction=adaClassify(testArr,classifierArray) #輸出錯誤率 num=shape(mat(labelArr))[1] errArr=mat(ones((num,1))) error=errArr[prediction!=mat(testLabelArr).T].sum() print("the errorRate is: %.2f",errorRate=float(error)/float((num)))
基於上面的adaBoost分類器訓練和測試代碼,得到了下面的不同弱分類器數目情況下的AdaBoost測試和分類錯誤率。
分類器數目 | 訓練錯誤率(%) | 測試錯誤率(%) |
1 | 0.28 | 0.27 |
10 | 0.23 | 0.24 |
50 | 0.19 | 0.21 |
100 | 0.19 | 0.22 |
500 | 0.16 | 0.25 |
1000 | 0.14 | 0.31 |
10000 | 0.11 | 0.33 |
觀察商標的數據我們發現:
(1)隨着分類器數目的增加,adaBoost分類器的訓練錯誤率不斷的減少,而測試錯誤率則是經歷先減少到最小值,再逐漸增大的過程。顯然,這就是所說的過擬合。因此,對於這種情況,我們應該采取相應的措施,比如采取交叉驗證的方法,在訓練分類器時,設定一個驗證集合,不斷測試驗證集的分類錯誤率,當發現訓練集錯誤率減少的同時,驗證集的錯誤率較之上一次結果上升了,就停止訓練。或者其他比較實用的模擬退火方法,基因遺傳方法等。
(2)前面的第四章的logistic回歸分類器對該數據集的分類錯誤率是35%,顯然adaBoost分類器取得了更好的分類效果。
(3)有文獻表明,對於表現好的數據集,AdaBoost的測試誤差率會隨着迭代次數的增加而逐漸穩定在某一個值附近,而不會出現上表中的先減小后上升的情況。顯然,這里用到的數據集不能稱為"表現好"的數據集,比較該數據集存在30%的數據缺失。在第四章的logistic回歸中,我們講這些確實的數據設置為0,顯然這在logistic回歸算法中是合適,這樣不會對分類結果造成影響。但是,在adaBoost算法中依然這樣設置,其合理性還有待證明,所以,有必要可以將這些缺失的數據值由0變成該特征相類似的數據,或者該特征數據的平均值,再來進行adaBoost算法訓練,看看得到的結果會不會有所提升?
四,總結
adaBoost是boosting方法中最流行的一種算法。它是以弱分類器作為基礎分類器,輸入數據之后,通過加權向量進行加權,;在每一輪的迭代過程中都會基於弱分類器的加權錯誤率,更新權重向量,從而進行下一次迭代。並且會在每一輪迭代中計算出該弱分類器的系數,該系數的大小將決定該弱分類器在最終預測分類中的重要程度。顯然,這兩點的結合是adaBoost算法的優勢所在。
優點:泛化錯誤率低,容易實現,可以應用在大部分分類器上,無參數調整
缺點:對離散數據點敏感