摘要
本文主要介绍组合相似的分类器来提高分类器性能、AdaBoost算法以及应用于处理非均衡问题分类问题。
目录
一、元算法
1.1 元算法简介
元算法就是对其他算法进行组合的一种方式。也称为集成算法(ensemble method),例如bagging方法和boosting方法。它可以是不同算法的集成;也可以是同一算法在不同设置下的集成;还可以是数据集不同部分分配给不同分类器之后的集成。
1.2 bagging方法
自举汇聚法(bootstrap aggregating),也称为bagging方法,是在从原始数据集选择S次后得到S个数据集的一种技术。新数据集和原数据集的大小相等。每个数据集都是通过在原始数据集中随机选择一个样本来进行替换而得到的。可以多次地选择同一样本,允许新数据集中可以有重复的值,而原始数据集的某些值在新集合中则不再出现。
在S个数据集建好后,将某个机器学习算法分别作用于某个数据集就得到S个分类器。对新数据分类时,可应用这S个分类器进行分类。选择分类器投票结果中最多的类别作为最后的分类结果。一种更先进的bagging方法,随机森林(random forest)
1.3 boosting方法
Boosting与bagging类似,都使用多个分类器。但Boosting中,不同分类器是通过串行训练得到的,每个新分类器都根据已训练出的分类器的性能进行训练。Boosting是通过集中关注被已有分类器错分的那些数据获得新的分类器。Boosting分类结果基于所有分类器的加权求和结果,而bagging中的分类器权重是相等的。
最流行的Boosting方法是AdaBoost,AdaBoot的大部分时间都用于训练上,分类器将多次在同一数据集上训练弱分类器。
二、AdaBoost元算法
2.1 AdaBoost简介
AdaBoost,一种元算法(meta-algorithm)或者集成方法(ensemble method),是对其他算法进行组合的一种方式。有人认为AdaBoost是最好的监督学习的方法。使用集成算法时,可是不同算法的集成,也可是同一算法在不同设置下的集成,还可是数据集不同部分分配不同分类器之后的集成。
2.2 AdaBoost步骤
AdaBoost(Adaptive Boosting,自适应Boosting)的运行过程如下:为训练数据中的每个样本赋予一个权重,这些权重构成了向量
D
D
。初始时,权重相等。首先在训练数据上训练出一个弱分类器并计算该分类器的错误率,然后再同一数据集上再次训练弱分类器。在分类器的第二次训练中,重新调整每个样本的权重,降低第一次分对的样本的权重,提高第一次错分样本的权重。为从所有弱分类器中得到最终的分类器结果,AdaBoost为每个分类器都分配一个权重值,这些
α
α
是基于每个弱分类器的错误率计算的,其中,错误率的定义是:
α
α
的计算公式如下:
AdaBoost算法的流程图

图1 流程图
得到
α
α
值后,对权重向量
D
D
进行更新。若某个样本被正确分类,则该样本的权重更改为:
若样本被错分,则该样本的权重更改为:
计算出 D D 后,AdaBoost开始下一轮迭代,AdaBoost会不断地重复训练和调整权重,直到训练错误率为0,或者弱分类器的数目达到了用户指定值为止。
2.3 基于单层决策树构建弱分类器
单层决策树(decision stump):基于单个特征来做决策。只有一次分裂过程。通过使用多颗单层决策树,就可以构建出一个能够对该数据集完全正确分类的分类器。
伪代码:
将最小错误率minError设为正无穷
对数据集中的每一个特征
对每个步长
对每个不等号
建立一颗单层决策树并利用加权数据集对它进行测试
如果错误率低于min Error,则将当前单层决策树设为最佳单层决策树
返回最佳单层决策树
2.4 完整AdaBoost算法的实现
伪代码
对每次迭代:
利用buildStump()函数找到最佳的单层决策树
将最佳单层决策树加入到单层决策树数组
计算
计算新的权重向量D
更新累计类别估计值
如果错误率等于0.0,则退出循环测试错误率在达到了一个最小值之后,又开始上升,这种现象称为过拟合(overfitting,也称过学习)。有文献称,对于表现好的数据集,AdaBoost的测试错误率就会达到一个稳定值,并不会随着分类器的增多而上升。
AdaBoost和SVM是监督机器学习中最强大的两种方法,两者拥有不少相似之处,可将弱分类器想象成SVM中的一个核函数,也可按照最大化某个最小间隔的方式重写AdaBoost算法,它们的不同在于其所定义的间隔计算方式有所不同,在高维空间下,这两者间的差异会更加明显。
三、非均衡分类问题
前述的所有分类问题中,都假设所有类别的分类代价都一样。但大多数情况下,不同类别的分类代价是不一样的。分类器性能度量方法,非均衡问题。
分类性能度量指标:正确率、召回率及ROC曲线。错误率:是在所有测试样例中错分的样例比例。这样度量错误掩盖了样例如何被错分的事实。混淆矩阵(confusion matrix)帮助人们了解分类中的错误。
下图是一个二类问题的混淆矩阵,其中的输出采用了不同的类别标签。
![]()
图2
将一个正例判为正例,产生一个真正例(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)来做出决策。理想中的分类器应该尽可能处于左上角,在假阳率很低的同时获取很高的真阳率。
![]()
图3
对不同的ROC曲线进行比较的一个指标是曲线下的面积(Area Unser the Curve,AUC),它给出了分类器的平均性能值。为了画出ROC曲线,分类器必须提供每个样例被判为阳性或者隐形的可信程度值。朴素贝叶斯提供一个可能性,Logistic回归中输入到Sigmoid函数中的是一个数值。在AdaBoost和SVM中,都会计算出一个数值输入到sign()函数中。这些值可用于衡量给定分类器的预测强度。
基于代价函数的分类器决策控制,除调节分类器的阈值外,代价敏感的学习(cost-sensitive learning)也可用于处理非均衡分类。引入代价信息的方法,在AdaBoost中,可基于代价函数来调整错误权重向量D;在朴素贝叶斯中,可选择具有最小期望代价而不是最大概率的类别作为最后的结果;在SVM中,可在代价函数中对于不同的类别选择不同的参数C。这些做法会给较小类更多的权重,即在训练时,小类当中只允许更少的错误。
处理非均衡问题的数据抽样方法,就是对分类器的训练数据进行改造。可通过欠抽样(undersampling)和过抽样(oversampling)来实现。过抽样意味着复制样例,或者加入与已有样例相似的点(加入已有数据点的插值点,可能导致过拟合问题)。欠抽样意味着删除样例,此方法的缺点在于确定哪些样例需要进行剔除,在选择剔除的样例中可能携带了剩余样例中并不包含的有价值信息。一种解决方法,选择那些离决策边界较远的样例进行删除。
四、测试
用Python代码实现AdaBoost算法并进行测试,使用数据集是UCI中汽车评价数据集(样本1729个,6种属性,4种标签),设置迭代次数为30。测试结果的错误率为: 0.2997。这结果优于使用决策树测试的结果(准确率为 0.3340)。其ROC曲线为:
![]()
图4
五、小结
多个分类器组合可能会进一步凸显单个分类器的不足,如过拟合问题。若多个分类器间差别显著,可能会缓解这一问题。差别可以是算法本身或者应用于算法上的数据的不同。
针对错误的调节能力是AdaBoost的长处,AdaBoost函数可以应用于任意能够处理加权数据的分类器。AdaBoost算法十分强大,它能够快速处理其他分类器难以处理的数据集。
优点:泛化错误率低,易编码,可应用在大部分分类器上,无参数调整。
缺点:对离群点敏感。
适用数据类型:数值型和标称型数据。六、参考文献
[1]周志华.机器学习[M].北京:清华大学出版社,2016.
[2]Peter Harrington.机器学习实战[M].北京:人民邮电出版社,2013.
[3]韩家炜等.数据挖掘概念与技术[M].北京:机械工业出版社,2012.七、附录
《机器学习实战》的代码,其代码的资源网址为:
https://www.manning.com/books/machine-learning-in-action其中,adaboost.py文件为:
""" Created on Sat May 19 20:26:31 2018 @author: Diky """ """ :: :;J7, :, ::;7: ,ivYi, , ;LLLFS: :iv7Yi :7ri;j5PL ,:ivYLvr ,ivrrirrY2X, :;r@Wwz.7r: :ivu@kexianli. :iL7::,:::iiirii:ii;::::,,irvF7rvvLujL7ur ri::,:,::i:iiiiiii:i:irrv177JX7rYXqZEkvv17 ;i:, , ::::iirrririi:i:::iiir2XXvii;L8OGJr71i :,, ,,: ,::ir@mingyi.irii:i:::j1jri7ZBOS7ivv, ,::, ::rv77iiiriii:iii:i::,rvLq@huhao.Li ,, ,, ,:ir7ir::,:::i;ir:::i:i::rSGGYri712: ::: ,v7r:: ::rrv77:, ,, ,:i7rrii:::::, ir7ri7Lri , 2OBBOi,iiir;r:: ,irriiii::,, ,iv7Luur: ,, i78MBBi,:,:::,:, :7FSL: ,iriii:::i::,,:rLqXv:: : iuMMP: :,:::,:ii;2GY7OBB0viiii:i:iii:i:::iJqL;:: , ::::i ,,,,, ::LuBBu BBBBBErii:i:i:i:i:i:i:r77ii , : , ,,:::rruBZ1MBBqi, :,,,:::,::::::iiriri: , ,,,,::::i: @arqiao. ,:,, ,:::ii;i7: :, rjujLYLi ,,:::::,:::::::::,, ,:i,:,,,,,::i:iii :: BBBBBBBBB0, ,,::: , ,:::::: , ,,,, ,,::::::: i, , ,8BMMBBBBBBi ,,:,, ,,, , , , , , :,::ii::i:: : iZMOMOMBBM2::::::::::,,,, ,,,,,,:,,,::::i:irr:i:::, i ,,:;u0MBMOG1L:::i:::::: ,,,::, ,,, ::::::i:i:iirii:i:i: : ,iuUuuXUkFu7i:iii:i:::, :,:,: ::::::::i:i:::::iirr7iiri:: : :rk@Yizero.i:::::, ,:ii:::::::i:::::i::,::::iirrriiiri::, : 5BMBBBBBBSr:,::rv2kuii:::iii::,:i:,, , ,,:,:i@petermu., , :r50EZ8MBBBBGOBBBZP7::::i::,:::::,: :,:,::i;rrririiii:: :jujYY7LS0ujJL7r::,::i::,::::::::::::::iirirrrrrrr:ii: ,: :@kevensun.:,:,,,::::i:i:::::,,::::::iir;ii;7v77;ii;i, ,,, ,,:,::::::i:iiiii:i::::,, ::::iiiir@xingjief.r;7:i, , , ,,,:,,::::::::iiiiiiiiii:,:,:::::::::iiir;ri7vL77rrirri:: :,, , ::::::::i:::i:::i:i::,,,,,:,::i:i:::iir;@Secbone.ii::: """ 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 def loadDataSet(fileName): #general function to parse tab -delimited floats numFeat = len(open(fileName).readline().split('\t')) #get number of fields 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 def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):#just classify the data retArray = ones((shape(dataMatrix)[0],1)) if threshIneq == 'lt': retArray[dataMatrix[:,dimen] <= threshVal] = -1.0 else: retArray[dataMatrix[:,dimen] > threshVal] = -1.0 return retArray 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 #init error sum, to +infinity for i in range(n):#loop over all dimensions rangeMin = dataMatrix[:,i].min(); rangeMax = dataMatrix[:,i].max(); stepSize = (rangeMax-rangeMin)/numSteps for j in range(-1,int(numSteps)+1):#loop over all range in current dimension for inequal in ['lt', 'gt']: #go over less than and greater than threshVal = (rangeMin + float(j) * stepSize) predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)#call stump classify with i, j, lessThan errArr = mat(ones((m,1))) errArr[predictedVals == labelMat] = 0 weightedError = D.T*errArr #calc total error multiplied by D #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 def adaBoostTrainDS(dataArr,classLabels,numIt=40): weakClassArr = [] m = shape(dataArr)[0] D = mat(ones((m,1))/m) #init D to all equal aggClassEst = mat(zeros((m,1))) for i in range(numIt): bestStump,error,classEst = buildStump(dataArr,classLabels,D)#build Stump #print "D:",D.T alpha = float(0.5*log((1.0-error)/max(error,1e-16)))#calc alpha, throw in max(error,eps) to account for error=0 bestStump['alpha'] = alpha weakClassArr.append(bestStump) #store Stump Params in Array #print "classEst: ",classEst.T expon = multiply(-1*alpha*mat(classLabels).T,classEst) #exponent for D calc, getting messy D = multiply(D,exp(expon)) #Calc New D for next iteration D = D/D.sum() #calc training error of all classifiers, if this is 0 quit for loop early (use break) 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) if errorRate == 0.0: break return weakClassArr,aggClassEst def adaClassify(datToClass,classifierArr): dataMatrix = mat(datToClass)#do stuff similar to last aggClassEst in adaBoostTrainDS m = shape(dataMatrix)[0] aggClassEst = mat(zeros((m,1))) for i in range(len(classifierArr[0])): classEst = stumpClassify(dataMatrix, classifierArr[0][i]['dim'],classifierArr[0][i]['thresh'],classifierArr[0][i]['ineq']) aggClassEst += classifierArr[0][i]['alpha']*classEst #print(aggClassEst) return sign(aggClassEst) def plotROC(predStrengths, classLabels): import matplotlib.pyplot as plt cur = (1.0,1.0) #cursor ySum = 0.0 #variable to calculate AUC numPosClas = sum(array(classLabels)==1.0) yStep = 1/float(numPosClas); xStep = 1/float(len(classLabels)-numPosClas) sortedIndicies = predStrengths.argsort()#get sorted index, it's reverse fig = plt.figure() fig.clf() ax = plt.subplot(111) #loop through all the values, drawing a line segment at each point for index in sortedIndicies.tolist()[0]: if classLabels[index] == 1.0: delX = 0; delY = yStep; else: delX = xStep; delY = 0; ySum += cur[1] #draw line from cur to (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--') 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)
测试main.py文件为:
# -*- coding: utf-8 -*- """ Created on Sun Jun 3 17:46:12 2018 @author: Diky """ from adaboost import * datArr,labelArr=loadDataSet ('horseColicTraining2.txt') classifierArray = adaBoostTrainDS(datArr,labelArr,numIt=10) testArr,testLabelArr=loadDataSet ('horseColicTest2.txt') prediction_numIt=adaClassify(testArr,classifierArray ) errArr=mat( ones((67,1) )) errArr[prediction_numIt!=mat( testLabelArr).T].sum() #datArr,labelArr=loadDataSet ('horseColicTraining2.txt') """ datArr,labelArr=loadDataSet ('car_data_num.txt') classifierArray,aggClassEst = adaBoostTrainDS(datArr,labelArr,numIt=30) plotROC(aggClassEst.T,labelArr) print(classifierArray) """