1. 概述
1.1 集成學習
目前存在各種各樣的機器學習算法,例如SVM、決策樹、感知機等等。但是實際應用中,或者說在打比賽時,成績較好的隊伍幾乎都用了集成學習(ensemble learning)的方法。集成學習的思想,簡單來講,就是“三個臭皮匠頂個諸葛亮”。集成學習通過結合多個學習器(例如同種算法但是參數不同,或者不同算法),一般會獲得比任意單個學習器都要好的性能,尤其是在這些學習器都是"弱學習器"的時候提升效果會很明顯。
弱學習器指的是性能不太好的學習器,比如一個准確率略微超過50%的二分類器。
下面看看西瓜書對此做的一個簡單理論分析。
考慮一個二分類問題、真實函數
以及
個相互獨立且犯錯概率均為
的個體學習器(或者稱基學習器)
。我們用簡單的投票進行集成學習,即分類結果取半數以上的基學習器的結果:
由Hoeffding不等式知,集成學習后的犯錯(即過半數基學習器犯錯)概率滿足
式指出,當犯錯概率獨立的基學習器個數
很大時,集成后的犯錯概率接近0,這也很符合直觀想法: 大多數人同時犯錯的概率是比較低的。
就如上面加粗字體強調的,以上推論全部建立在基學習器犯錯相互獨立的情況下,但實際中這些學習器不可能相互獨立,而如何讓基學習器變得“相對獨立一些”,也即增加這些基學習器的多樣性,正是集成學習需要考慮的主要問題。
按照每個基學習器之間是否存在依賴關系可以將集成學習分為兩類:
- 基學習器之間存在強依賴關系,一系列基學習器需要串行生成,代表算法是Boosting;
- 基學習器之間不存在強依賴關系,一系列基學習器可並行生成,代表算法是Bagging和隨機森林。
Boosting系列算法里最著名算法主要有AdaBoost和提升樹(Boosting tree)系列算法,本文只介紹最具代表性的AdaBoost。提升樹、Bagging以及隨機森林不在本文介紹范圍內,有時間了再另外介紹。
1.2 Boosting
Boosting指的是一類集成方法,其主要思想就是將弱的基學習器提升(boost)為強學習器。具體步驟如下:
- 先用每個樣本權重相等的訓練集訓練一個初始的基學習器;
- 根據上輪得到的學習器對訓練集的預測表現情況調整訓練集中的樣本權重(例如提高被錯分類的樣本的權重使之在下輪訓練中得到更多的關注), 然后據此訓練一個新的基學習器;
- 重復2直到得到
個基學習器,最終的集成結果是
個基學習器的組合。
由此看出,Boosting算法是一個串行的過程。
Boosting算法簇中最著名的就是AdaBoost,下文將會詳細介紹。
2. AdaBoost原理
2.1 基本思想
對於1.2節所述的Boosting算法步驟,需要回答兩個問題:
- 如何調整每一輪的訓練集中的樣本權重?
- 如何將得到的
個學習器組合成最終的學習器?
AdaBoost(Adaptive Boosting, 自適應增強)算法采取的方法是:
- 提高上一輪被錯誤分類的樣本的權值,降低被正確分類的樣本的權值;
- 線性加權求和。誤差率小的基學習器擁有較大的權值,誤差率大的基學習器擁有較小的權值。
下面先給出AdaBoost算法具體實現步驟,至於算法解釋(為什么要這樣做)將在下一大節闡述。
2.2 算法步驟
考慮如下形式的二分類(標准AdaBoost算法只適用於二分類任務)訓練數據集:其中
是一個含有
個元素的列向量, 即
;
是標量,
。
Adaboost算法具體步驟如下:
- 初始化樣本的權重
- 對
,重復以下操作得到
個基學習器:
(1) 按照樣本權重分布訓練數據得到第
個基學習器:
(2) 計算在加權訓練數據集上的分類誤差率:
上式中是指示函數,考慮更加周全的AdaBoost算法在這一步還應該判斷是否滿足基本條件(例如生成的基學習器是否比隨機猜測好), 如果不滿足,則當前基學習器被拋棄,學習過程提前終止。
(3) 計算的系數(即最終集成使用的的基學習器的權重):
(4) 更新訓練樣本的權重,其中是規范化因子,目的是為了使
的所有元素和為1。
- 構建最終的分類器線性組合
得到最終的分類器為
由式知,當基學習器
的誤差率
時,
,並且
隨着
的減小而增大,即分類誤差率越小的基學習器在最終集成時占比也越大。即AdaBoost能夠適應各個弱分類器的訓練誤差率,這也是它的名稱中"適應性(Adaptive)"的由來。
由式知, 被基學習器
誤分類的樣本權值得以擴大,而被正確分類的樣本的權值被得以縮小。
需要注意的是式中所有的
的和並不為1(因為沒有做一個softmax操作),
的符號決定了所預測的類,其絕對值代表了分類的確信度。
3. AdaBoost算法解釋
有沒有想過為什么AdaBoost算法長上面這個樣子,例如為什么要用式
那樣計算?本節將探討這個問題。
3.1 前向分步算法
在解釋AdaBoost算法之前,先來看看前向分步算法。就以AdaBoost算法的最終模型表達式為例:
可以看到這是一個“加性模型(additive model)”。我們希望這個模型在訓練集上的經驗誤差最小,即
通常這是一個復雜的優化問題。前向分步算法求解這一優化問題的思想就是: 因為最終模型是一個加性模型,如果能從前往后,每一步只學習一個基學習器及其權重
, 不斷迭代得到最終的模型,那么就可以簡化問題復雜度。具體的,當我們經過
輪迭代得到了最優模型
時,因為
所以此輪優化目標就為
求解上式即可得到第
個基分類器
及其權重
。
這樣,前向分步算法就通過不斷迭代求得了從到
的所有基分類器及其權重,問題得到了解決。
3.2 AdaBoost算法證明
上一小結介紹的前向分步算法逐一學習基學習器,這一過程也即AdaBoost算法逐一學習基學習器的過程。本節就證明前向分步算法的損失函數是指數損失函數(exponential loss function)時,AdaBoost學習的具體步驟就如2.2節所示。
指數損失函數即,指數損失函數是分類任務原本0/1損失函數的一致(consistent)替代損失函數(損失函數的上界,優化指數損失函數,等價於優化AdaBoost的損失函數)。由於指數損失函數有更好的數學性質,例如處處可微,所以我們用它替代0/1損失作為優化目標。
將指數損失函數代入式,優化目標就為
因為
與優化變量
和
無關,如果令
這個其實就是2.2節中歸一化之前的權重
,那么式
等價於
我們分兩步來求解式所示的優化問題的最優解
和
:
-
對任意的
, 求
:
上式將指數函數換成指示函數是因為前面說的指數損失函數和0/1損失函數是一致等價的。
式子所示的優化問題其實就是AdaBoost算法的基學習器的學習過程,即2.2節的步驟2(1),得到的
是使第
輪加權訓練數據分類誤差最小的基分類器。
-
求解
:
將式子中的目標函數展開
注:為了簡潔,上式子中的
被略去了
,
被略去了下標
,下同;將上式對
求導並令導數為0,即
解得
其中,
是分類誤差率:
如果式子
中的
歸一化成和為1的話那么式
也就和2.2節式
一模一樣了,進一步地也有上面的
也就是2.2節的
。
最后來看看每一輪樣本權值的更新,由和
可得
如果將上式進行歸一化成和為1的話就和與2.2節中
完全相同了。
如果某個樣本被正確分類,那么ωm+1,i=ωm,iexp(-αm)/sum(ωm,iexp(-αm)),錯誤則為ωm+1,i=ωm,iexp(αm)/sum(ωm,iexp(αm))。
由此可見,2.2節所述的AdaBoost算法步驟是可以經過嚴密推導得來的。總結一下,本節推導有如下關鍵點:
- AdaBoost算法是一個加性模型,將其簡化成前向分步算法求解;
- 將0/1損失函數用數學性質更好的指數損失函數替代(這里可以替換的原因是,(1/N)∑i=1...Nexp(yiƒ(xi)是損失函數(1/N)∑i=1...NI(yi!=G(xi))的上界,當G(xi)≠yi時,yi*f(xi)<0,因而exp(-yi*f(xi))≥1,可以用指數損失函數來代替原來的函數,便於運算。
一個例子
(下面的例子與解法來源於李航的《統計學習方法》)
例 給定如下表所示訓練數據。假設個體學習器由x(輸入)和y(輸出)產生,其閾值v(判定正反例的分界線)使該分類器在訓練數據集上分類誤差率最低。(y=1為正例,y=-1為反例)
第一個個體學習器:
我們首先認為(i=1,2,…,10)的權重是一樣的,即每一個數據同等重要。(權重是用來計算誤差的)
(a)在權值分布為
的訓練數據上,閾值v取2.5(紅線)時分類誤差率最低(此時x=6,7,8的數據被錯分為反例,誤差為它們的權重之和
=0.1+0.1+0.1=0.3,誤差率小於
才有意義),故個體學習器為
(b)根據誤差計算系數
=0.4236(公式:
,可以發現只有當
<
時,
>0,這樣個體學習器才是有意義的)
(c)更新訓練數據的權值分布(公式:,
,
是為了保證每次權值總和為1)
(通過指數損失函數調整權重,分類正確的降低權重(
和
同號則
,
),分類錯誤的增加權重):
(權重之和始終為1)
可以看到x=6,7,8的數據的權重變大了,而其他數據的權重降低了,這是希望能把之前經常分類錯誤(經常分類錯誤會出現權重不斷變大)的數據能在下一個個體學習器分類正確(記住:權重是用來計算誤差的,為了降低誤差,選擇閾值時會傾向把權重大的分類正確)
集成學習器(第一次集成,只有一個個體學習器)在訓練數據集上有3個誤分類點
第二個個體學習器:
(a)在權值分布為的訓練數據上,閾值v取8.5時分類誤差率最低(此時x=3,4,5的數據被錯分為正例,誤差為它們的權重之和
=0.07143+0.07143+0.07143=0.2143,誤差率降低了!),故個體學習器為
(b)根據誤差計算系數
(c)更新訓練數據的權值分布(在的基礎上調整
,分類正確的降低權重,分類錯誤的增加權重):
對比可以看到x=3,4,5的數據的權重變大了,而其他權重降低了。
,
(注意:x<2.5時,也<8.5)
分類器在訓練數據集上有3個誤分類點
第三個個體學習器:
(a)在權值分布為的訓練數據上,閾值v取5.5時分類誤差率最低(
=0.1820,誤差率又降低了!x=0,1,2,9被分類錯誤),故個體學習器為
(b)根據誤差計算系數
(c)更新訓練數據的權值分布:
(自己算一算吧)
最終結果:
分類器在訓練數據集上有0個誤分類點(amazing!)
代碼如下所示:
#!/usr/bin/env python # -*- coding: utf-8 -*- from numpy import * # 模擬創建數據集 def loadSimpData(): datMat = mat([[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 # 通過閾值比較對數據進行分類 def stumpClassify(dataMatrix, dimen, threshVal, threshIneq): """ Function: 通過閾值比較對數據進行分類 Input: dataMatrix:數據集 dimen:數據集列數 threshVal:閾值 threshIneq:比較方式:lt,gt Output: retArray:分類結果 """ #新建一個數組用於存放分類結果,初始化都為1 retArray = ones((shape(dataMatrix)[0],1)) #lt:小於,gt;大於;根據閾值進行分類,並將分類結果存儲到retArray if threshIneq == 'lt': retArray[dataMatrix[:, dimen] <= threshVal] = -1.0 else: retArray[dataMatrix[:, dimen] > threshVal] = -1.0 #返回分類結果 return retArray # 找到最低錯誤率的單層決策樹 def buildStump(dataArr, classLabels, D): """ Function: 找到最低錯誤率的單層決策樹 Input: dataArr:數據集 classLabels:數據標簽 D:權重向量 Output: bestStump:分類結果 minError:最小錯誤率 bestClasEst:最佳單層決策樹 """ #初始化數據集和數據標簽 dataMatrix = mat(dataArr); labelMat = mat(classLabels).T #獲取行列值 m,n = shape(dataMatrix) #初始化步數,用於在特征的所有可能值上進行遍歷 numSteps = 10.0 #初始化字典,用於存儲給定權重向量D時所得到的最佳單層決策樹的相關信息 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']: #設定閾值 threshVal = (rangeMin + float(j) * stepSize) #通過閾值比較對數據進行分類 predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal) #初始化錯誤計數向量 errArr = mat(ones((m,1))) #如果預測結果和標簽相同,則相應位置0 errArr[predictedVals == labelMat] = 0 #計算權值誤差,這就是AdaBoost和分類器交互的地方 weightedError = D.T * errArr #打印輸出所有的值 #print("split: dim %d, thresh %.2f, thresh ineqal: %s, the weighted error is %.3f" % (i, threshVal, inequal, weightedError)) #如果錯誤率低於minError,則將當前單層決策樹設為最佳單層決策樹,更新各項值 if weightedError < minError: minError = weightedError bestClasEst = predictedVals.copy() bestStump['dim'] = i bestStump['thresh'] = threshVal bestStump['ineq'] = inequal #返回最佳單層決策樹,最小錯誤率,類別估計值 return bestStump, minError, bestClasEst # 找到最低錯誤率的單層決策樹 def adaBoostTrainDS(dataArr, classLabels, numIt = 40): """ Function: 找到最低錯誤率的單層決策樹 Input: dataArr:數據集 classLabels:數據標簽 numIt:迭代次數 Output: weakClassArr:單層決策樹列表 aggClassEst:類別估計值 """ #初始化列表,用來存放單層決策樹的信息 weakClassArr = [] #獲取數據集行數 m = shape(dataArr)[0] #初始化向量D每個值均為1/m,D包含每個數據點的權重 D = mat(ones((m,1))/m) #初始化列向量,記錄每個數據點的類別估計累計值 aggClassEst = mat(zeros((m,1))) #開始迭代 for i in range(numIt): #利用buildStump()函數找到最佳的單層決策樹 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))) #保存alpha的值 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) #計算錯誤率,aggClassEst本身是浮點數,需要通過sign來得到二分類結果 aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T, ones((m,1))) errorRate = aggErrors.sum() / m # print("total error: ", errorRate) #如果總錯誤率為0則跳出循環 if errorRate == 0.0: break #返回單層決策樹列表和累計錯誤率 return weakClassArr #return weakClassArr, aggClassEst # AdaBoost分類函數 def adaClassify(datToClass, classifierArr): """ Function: AdaBoost分類函數 Input: datToClass:待分類樣例 classifierArr:多個弱分類器組成的數組 Output: sign(aggClassEst):分類結果 """ #初始化數據集 dataMatrix = mat(datToClass) #獲得待分類樣例個數 m = shape(dataMatrix)[0] #構建一個初始化為0的列向量,記錄每個數據點的類別估計累計值 aggClassEst = mat(zeros((m,1))) #遍歷每個弱分類器 for i in range(len(classifierArr)): #基於stumpClassify得到類別估計值 classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'], classifierArr[i]['thresh'], classifierArr[i]['ineq']) #累加類別估計值 aggClassEst += classifierArr[i]['alpha']*classEst #打印aggClassEst,以便我們了解其變化情況 #print(aggClassEst) #返回分類結果,aggClassEst大於0則返回+1,否則返回-1 return sign(aggClassEst) datMat, classLabels = loadSimpData() classifierArr = adaBoostTrainDS(datMat, classLabels, 30) print(classifierArr) print(adaClassify([0,0], classifierArr)) print(adaClassify([[5,5],[0,0]], classifierArr))