机器学习实战之AdaBoost算法
一,引言
前面几章的介绍了几种分类算法,当然各有优缺。如果将这些不同的分类器组合起来,就构成了我们今天要介绍的集成方法或者说元算法。集成方法有多种形式:可以使多种算法的集成,也可以是一种算法在不同设置下的集成,还可以将数据集的不同部分分配不同的分类器,再将这些分类器进行集成。
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算法的优势所在。
优点:泛化错误率低,容易实现,可以应用在大部分分类器上,无参数调整
缺点:对离散数据点敏感