轉載:http://www.hankcs.com/ml/adaboost.html
本文是《統計學習方法》第8章提升方法的筆記,整合了《機器學習實戰》中的提升樹Python代碼,並添加了註解和PR值計算代碼。《方法》重理論,但不易理解,《實戰》重實踐,但缺乏理論基礎,特別是AdaBoost算法的解釋、提升樹與加法模型的關係等。兩相結合,應該能獲得較爲全面的知識。
提升方法AdaBoost算法
提升方法的思路是綜合多個分類器,得到更準確的分類結果。
AdaBoost算法的歸類
《統計學習方法》稱AdaBoost是提升算法的代表,所謂提升算法,指的是一種常用的統計學習方法,應用廣泛且有效。在分類問題中,它通過改變訓練樣本的權重,學習多個分類器,並將這些分類器進行線性組合,提髙分類的性能。
《機器學習實戰》稱AdaBoost是最流行的元算法,所謂元算法,指的是“學習算法的算法”。
AdaBoost算法的基本思想
多輪訓練,多個分類器
每輪訓練增加錯誤分類樣本的權值,降低正確分類樣本的權值
降低錯誤率高的分類器的權值,增加正確率高的分類器的權值
AdaBoost算法
給定一個二類分類的訓練數據集
其中,每個樣本點由實例與標記組成。實例,標記,是實例空間,是標記集合。AdaBoost利用以下算法,從訓練數據中學習一系列弱分類器或基本分類器,並將這些弱分類器線性組合成爲一個強分類器。
AdaBoost算法
輸入:訓練數據集:弱學習算法;
輸出:最終分類器
(1)初始化訓練數據的權值分佈
每個w的下標由兩部分構成,前一個數表示當前迭代次數,與D的下標保持一致,後一個數表示第幾個權值,與位置保持一致。初始情況下,每個權值都是均等的。
(2)對(這裏的M原著未做解釋,其實是表示訓練的迭代次數,是由用戶指定的。每輪迭代產生一個分類器,最終就有M個分類器):
(a)使用具有權值分佈的訓練數據集學習,得到基本分類器
(b)計算在訓練數據集上的分類誤差率
分類誤差率這個名字可能產生誤解,這裏其實是個加權和。
(c)計算的係數
這裏的對數是自然對數。表示在最終分類器中的重要性。由式可知,當時,,並且隨着的減小而增大,所以分類誤差率越小的基本分類器在最終分類器中的作用越大。
爲什麼一定要用這個式子呢?這與前向分步算法的推導有關,在後面的章節會介紹。
(d)更新訓練數據集的權值分佈
y只有正負一兩種取值,所以上式可以寫作:
這裏,是規範化因子
它使成爲一個概率分佈。
由此可知,被基本分類器誤分類樣本的權值得以擴大,而被正確分類樣本的權值卻得以縮小。兩相比較,誤分類樣本的權值被放大倍。因此,誤分類樣本在下一輪學習中起更大的作用。不改變所給的訓練數據,而不斷改變訓練數據權值的分佈,使得訓練數據在基本分類器的學習中起不同的作用,這是AdaBoost的一個特點。
(3)構建基本分類器的線性組合
得到最終分類器
AdaBoost算法的解釋
AdaBoost算法還有另一個解釋,即可以認爲AdaBoost算法是模型爲加法模型、損失函數爲指數函數、學習算法爲前向分步算法時的二類分類學習方法。
爲什麼還要學習前向分步算法呢?直接給我AdaBoost的代碼不就好了嗎?因爲只有理解了前向分步算法,才能理解AdaBoost爲什麼能跟決策樹組合起來。
前向分步算法
考慮加法模型(additive model)
其中,爲基函數,爲基函數的參數,爲基函數的係數。顯然,是一個加法模型。
在給定訓練數據及損失函數的條件下,學習加法模型成爲經驗風險極小化即損失函數極小化問題:
通常這是一個複雜的優化問題。前向分步算法(forward stage wise algorithm)求解這一優化問題的想法是:因爲學習的是加法模型,如果能夠從前向後,每一步只學習一個基函數及其係數,逐步逼近優化目標函數式(L應該是loss的縮寫,表示一個損失函數,輸入正確答案yi和模型預測值,輸出損失值),那麼就可以簡化優化的複雜度。具體地,每步只需優化如下損失函數:
也就是說,原來有M個分類器,現在只專注優化一個。
給定訓練數據集。損失函數和基函數的集合,學習加法模型的前向分步算法如下:
算法(前向分步算法)
輸入:訓練數據集,損失函數,基函數集;
輸出:加法模型
(1)初始化
(2)對
(a)極小化損失函數
得到參數
(b)更新
(3)得到加法模型
這樣,前向分步算法將同時求解從m=1到M所有參數的優化問題簡化爲逐次求解各個的優化問題。
前向分步算法與AdaBoost
由前向分步算法可以推導出AdaBoost,用定理敘述這一關係。
定理 AdaBoost算法是前向分歩加法算法的特例。這時,模型是由基本分類器組成的加法模型,損失函數是指數函數。
證明 前向分步算法學習的是加法模型,當基函數爲基本分類器時,該加法模型等價於AdaBoost的最終分類器
由基本分類器及其係數組成,m=1,2,…,M。前向分步算法逐一學習基函數,這一過程與AdaBoost算法逐一學習基本分類器的過程一致。下面證明前向分步算法的損失函數是指數損失函數(exponential loss function)
時,其學習的具體操作等價於AdaBoost算法學習的具體操作。
假設經過m-1輪迭代前向分步算法已經得到:
在第m輪迭代得到和。
目標是使前向分步算法得到的使在訓練數據集T上的指數損失最小,即
上式可以表示爲
其中,(指數中的加法可以拿出來做乘法)。因爲既不依賴α也不依賴於G,所以與最小化無關。但依賴於,隨着每一輪迭代而發生改變。
現證使式達到最小的就是AdaBoost算法所得到的。
求解式可分兩步:
首先,求。對任意a>0,使式最小的由下式得到:
其中,。
此分類器即爲AdaBoost算法的基本分類器,因爲它是使第m輪加權訓練數據分類誤差率最小的基本分類器。
之後,求。中
這個轉換很簡單,當y和G一致時,指數爲負,反之爲正,第二個等號也是利用這個原理,只不過換成了用指示變量I表述。
將已求得的代入式,對α求導並使導數爲0,即得到使式最小的a。
其中,是分類誤差率:
這裏的與AdaBoost算法第2(c)步的完全一致。
最後來看每一輪樣本權值的更新。由
以及,可得
這與AdaBoost算法第2(d)步的樣本權值的更新,只相差規範化因子,因而等價。
提升樹
提升樹是以分類樹或迴歸樹爲基本分類器的提升方法。提升樹被認爲是統計學習中性能最好的方法之一。
提升方法實際採用加法模型(即基函數的線性組合)與前向分步算法。以決策樹爲基函數的提升方法稱爲提升樹(boosting tree)。對分類問題決策樹是二叉分類樹,對迴歸問題決策樹是二叉迴歸樹。在原著例題中看到的基本分類器,可以看作是由一個根結點直接連接兩個葉結點的簡單決策樹,即所謂的決策樹樁(decision stump)。提升樹模型可以表示爲決策樹的加法模型:
其中,表示決策樹;爲決策樹的參數;M爲樹的個數。
提升樹算法
提升樹算法採用前向分步算法。首先確定初始提升樹/e(x)=0,第m歩的模型是
其中,爲當前模型,通過經驗風險極小化確定下一棵決策樹的參數
由於樹的線性組合可以很好地擬合訓練數據,即使數據中的輸入與輸出之間的關係很複雜也是如此,所以提升樹是一個髙功能的學習算法。
不同問題有大同小異的提升樹學習算法,其主要區別在於使用的損失函數不同。包括用平方誤差損失函數的迴歸問題,用指數損失函數的分類問題,以及用一般損失函數的一般決策問題。
對於二類分類問題,提升樹算法只需將AdaBoost算法中的基本分類器限制爲二類分類樹即可,可以說這時的提升樹算法是AdaBoost算法的特殊情況,接下來通過《機器學習實戰》中的代碼學習其應用。
提升樹的Python實現
AdaBoost+決策樹=提升樹,來看看具體用Python怎麼實現。
測試數據
老規矩,寫代碼前先看數據,有什麼樣的數據寫什麼樣的代碼。
考慮到學習代碼的最佳方式是單步,而單步的時候,測試數據越簡單越好。所以這裏先上一份簡單的測試數據:
- def loadSimpData():
- """
- 加載簡單數據集
- :return:
- """
- 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
數據很簡單,就是一個二維平面上的5個點。
寫一段可視化代碼:
- def plotData(datMat, classLabels):
- xcord0 = []
- ycord0 = []
- xcord1 = []
- ycord1 = []
- for i in range(len(classLabels)):
- if classLabels[i]==1.0:
- xcord1.append(datMat[i,0]), ycord1.append(datMat[i,1])
- else:
- xcord0.append(datMat[i,0]), ycord0.append(datMat[i,1])
- fig = plt.figure()
- ax = fig.add_subplot(111)
- ax.scatter(xcord0,ycord0, marker='s', s=90)
- ax.scatter(xcord1,ycord1, marker='o', s=50, c='red')
- plt.title('decision stump test data')
- plt.show()
畫出來是這種效果:
單層決策樹分類
單層決策樹就是一個樹樁,只能利用一個維度的特徵進行分類。
- def stumpClassify(dataMatrix, dimen, threshVal, threshIneq): # just classify the data
- """
- 用只有一層的樹樁決策樹對數據進行分類
- :param dataMatrix: 數據
- :param dimen: 特徵的下標
- :param threshVal: 閾值
- :param threshIneq: 大於或小於
- :return: 分類結果
- """
- retArray = ones((shape(dataMatrix)[0], 1))
- if threshIneq == 'lt':
- retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
- else:
- retArray[dataMatrix[:, dimen] > threshVal] = -1.0
- return retArray
dimen決定了這個樹樁能用的唯一特徵。
構建決策樹樁
上個函數到底取什麼參數纔好呢?給定訓練數據集的權重向量,利用該向量計算錯誤率加權和,取最低的參數作爲訓練結果。
- def buildStump(dataArr, classLabels, D):
- """
- 構建決策樹(一個樹樁)
- :param dataArr: 數據特徵矩陣
- :param classLabels: 標籤向量
- :param D: 訓練數據的權重向量
- :return: 最佳決策樹,最小的錯誤率加權和,最優預測結果
- """
- 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']: # 遍歷大於和小於
- threshVal = (rangeMin + float(j) * stepSize)
- predictedVals = stumpClassify(dataMatrix, i, threshVal,
- inequal) # 使用參數 i, j, lessThan 調用樹樁決策樹分類
- errArr = mat(ones((m, 1)))
- errArr[predictedVals == labelMat] = 0 # 預測正確的樣本對應的錯誤率爲0,否則爲1
- weightedError = D.T * errArr # 計算錯誤率加權和
- # print "split: dim %d, thresh %.2f, thresh ineqal: %s, the weighted error is %.3f" % (i, threshVal, inequal, weightedError)
- if weightedError < minError: # 記錄最優樹樁決策樹分類器
- minError = weightedError
- bestClasEst = predictedVals.copy()
- bestStump['dim'] = i
- bestStump['thresh'] = threshVal
- bestStump['ineq'] = inequal
- return bestStump, minError, bestClasEst
上面的這一句
- weightedError = D.T * errArr # 計算錯誤率加權和
其實對應着這個公式:
AdaBoost訓練
有了上面的基礎,就不難看懂完整的訓練代碼了:
- def adaBoostTrainDS(dataArr, classLabels, numIt=40):
- """
- 基於單層決策樹的ada訓練
- :param dataArr: 樣本特徵矩陣
- :param classLabels: 樣本分類向量
- :param numIt: 迭代次數
- :return: 一系列弱分類器及其權重,樣本分類結果
- """
- weakClassArr = []
- m = shape(dataArr)[0]
- D = mat(ones((m, 1)) / m) # 將每個樣本的權重初始化爲均等
- aggClassEst = mat(zeros((m, 1)))
- for i in range(numIt):
- bestStump, error, classEst = buildStump(dataArr, classLabels, D) # 構建樹樁決策樹,這是一個若分類器,只能利用一個維度做決策
- # print "D:",D.T
- alpha = float(
- 0.5 * log((1.0 - error) / max(error, 1e-16))) # 計算 alpha, 防止發生除零錯誤
- bestStump['alpha'] = alpha
- weakClassArr.append(bestStump) # 保存樹樁決策樹
- # print "classEst: ",classEst.T
- expon = multiply(-1 * alpha * mat(classLabels).T, classEst) # 每個樣本對應的指數,當預測值等於y的時候,恰好爲-alpha,否則爲alpha
- D = multiply(D, exp(expon)) # 計算下一個迭代的D向量
- D = D / D.sum() # 歸一化
- # 計算所有分類器的誤差,如果爲0則終止訓練
- aggClassEst += alpha * classEst
- # print "aggClassEst: ",aggClassEst.T
- aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T, ones((m, 1))) # aggClassEst每個元素的符號代表分類結果,如果與y不等則表示錯誤
- errorRate = aggErrors.sum() / m
- print "total error: ", errorRate
- if errorRate == 0.0: break
- return weakClassArr, aggClassEst
其中alpha的計算:
- alpha = float(
- 0.5 * log((1.0 - error) / max(error, 1e-16))) # 計算 alpha, 防止發生除零錯誤
對應着
之後記錄下這個alpha和對應的決策樹樁,然後算法更新訓練數據的權重:
- expon = multiply(-1 * alpha * mat(classLabels).T, classEst) # 每個樣本對應的指數,當預測值等於y的時候,恰好爲-alpha,否則爲alpha
- D = multiply(D, exp(expon)) # 計算下一個迭代的D向量
- D = D / D.sum() # 歸一化
對應着
更新完畢後就可以利用這個權值分佈訓練下一個決策樹樁了,當然,作爲一個節省時間的策略,可以檢查一下當前錯誤率是否滿足要求,如果滿足,則終止訓練。
提升樹分類
分類很簡單,就是一個多數表決的過程:
- 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)
調用方法
利用上述簡單數據集進行測試:
- datArr, labelArr = loadSimpData()
- classifierArr, aggClassEst = adaBoostTrainDS(datArr, labelArr, 30)
- print adaClassify([0, 0], classifierArr)
輸出
- total error: 0.2
- total error: 0.2
- total error: 0.0
- [[-0.69314718]]
- [[-1.66610226]]
- [[-2.56198199]]
- [[-1.]]
可見前向分步訓練過程中的誤差率是逐漸降低的:
- total error: 0.2
- total error: 0.2
- total error: 0.0
分類過程中,隨着加法模型的不斷疊加,對於(0,0)這個點,其累加結果是“負”得越來越厲害的,最終取符號輸出-1類別。對應訓練數據:
的確應該屬於-1。
複雜數據集
《機器學習實戰》還給出了一個馬疝病數據集:horseColicTraining2.txt
加載代碼:
- def loadDataSet(fileName):
- """
- 從文件中加載以製表符分割的數據集(浮點數格式)
- :param fileName:
- :return:
- """
- 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
訓練代碼:
- datArr, labelArr = loadDataSet("horseColicTraining2.txt")
- classifierArr, aggClassEst = adaBoostTrainDS(datArr, labelArr, 10)
我們可以直接將aggClassEst打印出來觀察對訓練集的預測結果,但這樣太抽象,而且我們希望計算準確率和召回率,特別是在測試集上的準確率和召回率。
準確率與召回率
對於二分類問題:
準確率的計算方法爲
- TP / (TP + FP)
召回率的計算方法爲
- TP / (TP + FN)
寫一段代碼計算在不同的數據集下的準確率與召回率:
- def evaluate(aggClassEst, classLabels):
- """
- 計算準確率與召回率
- :param aggClassEst:
- :param classLabels:
- :return: P, R
- """
- TP = 0.
- FP = 0.
- TN = 0.
- FN = 0.
- for i in range(len(classLabels)):
- if classLabels[i] == 1.0:
- if (sign(aggClassEst[i]) == classLabels[i]):
- TP += 1.0
- else:
- FP += 1.0
- else:
- if (sign(aggClassEst[i]) == classLabels[i]):
- TN += 1.0
- else:
- FN += 1.0
- return TP / (TP + FP), TP / (TP + FN)
- def train_test(datArr, labelArr, datArrTest, labelArrTest, num):
- classifierArr, aggClassEst = adaBoostTrainDS(datArr, labelArr, num)
- prTrain = evaluate(aggClassEst, labelArr)
- aggClassEst = adaClassify(datArrTest, classifierArr)
- prTest = evaluate(aggClassEst, labelArrTest)
- return prTrain, prTest
- datArr, labelArr = loadDataSet("horseColicTraining2.txt")
- datArrTest, labelArrTest = loadDataSet("horseColicTest2.txt")
- prTrain, prTest = train_test(datArr, labelArr, datArrTest, labelArrTest, 10)
- print prTrain, prTest
輸出
- (0.8595505617977528, 0.7766497461928934) (0.7872340425531915, 0.8604651162790697)
事實上,通過調整分類器的數量(train_test的最後一個參數),可以得到不同的性能。《機器學習實戰》驗證了1到10000個分類器的錯誤率:
上圖說明分類器並不是越多越好,50是個最好的值,過了這個值,模型發生過擬合,性能越來越低。
ROC曲線
給定分類器在某個數據集上的輸出,我們就能計算出假陽率與真陽率,這兩個值決定一個座標點。以假陽率作爲x軸,真陽率作爲y軸,制定座標系。
完美的分類器應該是左上角那個點,分類器越接近左上角,就越完美。這樣我們就可以較爲直觀地比較兩個不同的分類器了。
在決策函數中,我們使用的是sign來二值化預測強度,以得到最終分類。也就是說,我們取的閾值是0。分類強度離閾值越遠,分類的可信度越高。如果我們改變閾值,我們就能得到同一個分類器在座標系中不同的點,將它們按照閾值大小順序連成線,就能得到一條ROC曲線。完美的分類器的ROC曲線應該是正方形的左上角對應的兩條邊,而隨機猜測的ROC曲線則是連接左下角與右上角的一條直線。ROC曲線下的面積(AUC)反映了分類器的平均性能。
繪製代碼:
- def plotROC(predStrengths, classLabels):
- import matplotlib.pyplot as plt
- cur = (1.0, 1.0) # 中間變量,初始狀態爲右上角
- ySum = 0.0 # variable to calculate AUC
- numPosClas = sum(array(classLabels) == 1.0) # TP
- yStep = 1 / float(numPosClas)
- xStep = 1 / float(len(classLabels) - numPosClas)
- sortedIndicies = predStrengths.argsort() # 按元素值排序後的下標,逆序
- fig = plt.figure()
- fig.clf()
- ax = plt.subplot(111)
- # 遍歷所有的值
- for index in sortedIndicies.tolist()[0]:
- if classLabels[index] == 1.0: # 預測正確
- delX = 0 # 真陽率不變
- delY = yStep # 假陽率減小
- else:
- delX = xStep
- delY = 0
- ySum += cur[1] # ROC面積的一個小長條
- # 從 cur 到 (cur[0]-delX,cur[1]-delY) 畫一條線
- 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--') # 隨機猜測的ROC線
- 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()
- print "the Area Under the Curve is: ", ySum * xStep
- plotROC(aggClassEst.T, labelArr)
效果:
隨機猜測的ROC曲線是一條y=x直線,這是因爲對真陽率和假陽率相等,意味着分類器猜對和猜錯的比率相等,說明該分類器就是在隨機猜。