利用AdaBoost(adaptive boosting)元算法提高分类性能
当做重要决定时,大家可能都会考虑吸取多个专家而不只是一个人的意见。机器学习处理问题时又何尝不是如此?这就是元算法(meta-algorithm)背后的思路。元算法是对其他算法进行组合的一种方式。
前面我们介绍了五种不同的分类算法:KNN、决策树、贝叶斯、Logistic回归以及SVM,我们可以将不同的分类器组合起来,而这种组合结果则被称为集成方法(ensemble method)或者元算法。
使用集成方法时会有多种形式:可以是不同算法的集成,也可以是同一算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。
baggingL自举汇聚法(bootstrap aggregating),是在从原始数据集选择S次后得到S个新数据集的一种技术(放回取样)。新数据集和原始数据集的大小相等。每个数据集都是通过在原始数据集中随机选择一个样本来进行替换而得到的。
在S个数据集建好之后,将某个学习算法分别作用于每个数据集就得到了S个分类器。当我们要对新数据进行分类时,就可以应用这S个分类器进行分类。与此同时,选择分类器投票结果中最多的类别作为最后的分类结果。也有一些更先进的bagging方法,比如随机森林(random forest)
弱分类器:在二分的情况下错误率高于50%
AdaBoost的运行流程如下:训练-数据中的每个样本,并赋予其一个权重,这些权重构成了向量D。一开始,这些权重都初始化为相等值。首先在训练数据上训练出一个弱分类器并计算该分类器的错误率,然后在同一数据集上再次训练弱分类器。在分类器的第二次训练当中,将会重新调整每个样本的权重,其中第一次分对的样本的权重将会降低,而第一次分错的样本的权重将会提高。为了从所有弱分类器中得到最终的分类结果,AdaBoost为每个分类器都分配了一个权重值alpha,这些alpha值是基于每个弱分类器的错误率进行计算的。
错误率η=未正确分类的样本数目/所有样本数目
alpha=2^(-1)exp((1-η)/η)
计算出alpha值之后,可以对权重向量D进行更新,以使得那些正确分类的样本的权重降低而错分样本的权重升高。
D = np.mat(np.ones((m,1))/m) #向量D,包含每个数据点的权重。增加错分数据的权重,降低正确分类的权重,D是一个概率分布向量,因此其所有的元素之和为1.0
单层决策树(decision stump,也称决策树桩)是一种简单的决策树。它仅基于单个特征来做决策。
遍历stumpClassify()函数所有可能输入值,并找到数据集上最佳的单层决策树
伪代码如下:
将最小错误率minError设为+∞
对数据集中的每一个特征(第一层循环)
对每个步长(第二层循环)
对每个不等号(第三层循环)
建立一颗单层决策树并利用加权数据集对它进行测试
如果错误率低于MinError,则将当前单层决策树设为最佳单层决策树
返回最佳单层决策树
import numpy as np
#基于单层决策树构建弱分类器
def loadSimData():
"""用于测试AdaBoost构建函数的简单数据。这不可能仅仅通过在某个坐标轴上选择某个阈值来将数据点分开。AdaBoost需要将多个单层决策树组合起来才能对该数据集进行正确分类"""
dataMat = np.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
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
"""单层决策树分类函数
通过阈值比较对数据进行分类.该函数通过数组过滤来实现。首先将返回数组的全部元素设置为1,然后将不满足阈值条件的值设为-1"""
retArray = np.ones((np.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):
"""单层决策树生成函数
遍历stumpClassify()函数所有可能输入值,并找到数据集上最佳的单层决策树
伪代码如下:
将最小错误率minError设为+∞
对数据集中的每一个特征(第一层循环)
对每个步长(第二层循环)
对每个不等号(第三层循环)
建立一颗单层决策树并利用加权数据集对它进行测试
如果错误率低于MinError,则将当前单层决策树设为最佳单层决策树
返回最佳单层决策树"""
dataMatrix = np.mat(dataArr)
labelMat = np.mat(classLabels).T
m,n = np.shape(dataMatrix)
print(n)
numSteps = 10.0 #用于在特征的所有可能值上进行遍历
bestStump = {} #存储给定权重向量D时所得到的最佳单层决策树的相关信息
bestClassEst = np.mat(np.zeros((m,1)))
minError = np.inf
for i in range(n):#n为特征数,在数据集的所有特征上遍历
rangeMin = dataMatrix[:,i].min() #第i列的最小值
rangeMax = dataMatrix[:,i].max()
stepSize = (rangeMax-rangeMin)/numSteps #步长,数据变化范围被分成了10个步长的大小
"""生成numSteps+2个分类器"""
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 = np.mat(np.ones((m,1))) #错误向量
errArr[predictedVals == labelMat] = 0 #如果预测值=实际值,则errArr = 0
weightedError = D.T * errArr #计算加权错误率,这就是AdaBoost和分类器交互的地方
print("split:dim %d,thresh %.2f,thresh ineqal:%s,the weighted error is %.3f" % (i,threshVal,inequal,weightedError))
if weightedError < minError:
minError = weightedError
bestClassEst = predictedVals.copy()
bestStump['dim'] = i
bestStump['thresh'] = threshVal
bestStump['ineq'] = inequal
return bestStump,minError,bestClassEst
if __name__ == '__main__':
D = np.mat(np.ones((5,1))/5)
dataArr,labelArr = loadSimData()
bestStump,minError,bestClassEst = buildStump(dataArr,labelArr,D)
整个实现的伪代码如下:
对每次迭代:
利用buildStump()函数找到最佳的单层决策树
将最佳单层决策树加入到单层决策树组
计算alpha
计算新的权重向量D
更新累计类别估计值
如果错误率等于0.0,则退出循环
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
"""单层决策树分类函数
通过阈值比较对数据进行分类.该函数通过数组过滤来实现。首先将返回数组的全部元素设置为1,然后将不满足阈值条件的值设为-1"""
retArray = np.ones((np.shape(dataMatrix)[0],1))
if threshIneq == 'lt':
retArray[dataMatrix[:,dimen] <= threshVal] = -1.0
else:
retArray[dataMatrix[:,dimen] > threshVal] = -1.0
return retArray
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
"""返回弱分类器数组
dataArr:训练数据集
classLabels:标签
numIt=40:迭代次数,默认为40次
return weakClassArr,aggClassEst:分类器数组,估计值"""
weakClassArr = [] #存储与数据个数相同的弱分类器
m = np.shape(dataArr)[0]
D = np.mat(np.ones((m,1))/m) #向量D,包含每个数据点的权重。增加错分数据的权重,降低正确分类的权重,D是一个概率分布向量,因此其所有的元素之和为1.0
aggClassEst = np.mat(np.zeros((m,1))) #记录每个数据点的类别估计累计值
for i in range(numIt):
bestStump,error,classEst = buildStump(dataArr,classLabels,D)
print("D:",D.T)
alpha = float(0.5*np.log((1.0 - error)/max(error,1e-16))) #max(error,1e-16)确保不会发生除零溢出,alpha各分类器所占的权重
bestStump['alpha'] = alpha
weakClassArr.append(bestStump)
print("classEst:",classEst.T)
expon = np.multiply(-1*alpha*np.mat(classLabels).T,classEst)
#为下一次迭代计算D
D = np.multiply(D,exp(expon))
print(type(D)) #
D = np.mat(D/D.sum()) #数组类型改变,需要转换回去
print(type(D)) #
#错误率累加计算
aggClassEst += alpha*classEst
print("aggClassEst:",aggClassEst.T)
aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T,np.ones((m,1))) #若预测值和实际值不匹配,设置权重
errorRate = aggErrors.sum()/m
print("total error:",errorRate,"\n")
if errorRate == 0.0:
break
return weakClassArr,aggClassEst
def adaClassify(dataToClass,classifierArr):
"""利用训练出的多个弱分类器进行分类
dataToClass:需要进行分类的数据
classifierArr:包含多个分类器的数组"""
dataMatrix = np.mat(dataToClass)
m = np.shape(dataToClass)[0]
aggClassEst = np.mat(np.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 #每个弱分类器有一个权重alpha
print(aggClassEst)
return np.sign(aggClassEst)
if __name__ == '__main__':
dataArr ,labelArr = loadDataSet('./M_07_AdaBoost/horseColicTraining2.txt')
classifiers = adaBoostTrainDS(dataArr,labelArr,10)
testDataArr,testLabelArr = loadDataSet('./M_07_AdaBoost/horseColicTest2.txt')
m = np.shape(np.mat(testDataArr))[0]
predict = adaClassify(testDataArr,classifiers)
errArr = np.mat(np.ones((m,1)))
errorRate = errArr[predict != np.mat(testLabelArr).T].sum()/m
print("the test errorRate is :",errorRate)
每个弱分类器的结果以其对应的alpha值作为权重,所有这些弱分类器的结果加权求和就得到了最后的结果
过程
1.收集数据
2.准备数据:确保类别标签是+1和-1而非1和0
3.分析数据
4.训练算法:在数据上,利用adaBoostTrainDS()函数训练出一系列的分类器
5.测试算法:我们有两个数据集。在不采用随机抽样的方法下,我们就会对adaBoost和logistic回归的结果进行完全对等的比较
过拟合(overfitting,也称过学习)
测试错误率在达到了一个最小值之后又开始上升了。这种现象称为过拟合。
对于表现好的数据,adaBoost的测试错误率就会达到一个稳定值,并不会随着分类器的增多而上升。
SVM与AdaBoost
这两者之间拥有不少相似之处。我们可以把弱分类器想象成SVM中的一个核函数,也可以按照最大化某个最小间隔的方式重写AdaBoost算法。而他们的不同就在于其所定义的间隔计算方式有所不同,因此导致的结果也不同。特别是在高维空间下,这两者之间的差异就会变得更加明显。
迄今为止,已经介绍了六个算法:
KNN、决策树、贝叶斯、logistic回归、SVM-SMO、AdaBoost
这些算法中,我们此前皆假定所有类别的分类代价是一样的。也就是说,我们没有考虑错分分类带来的损失,如果错分垃圾邮件,则合法邮件再也找不到;如果分类结果确认不是癌症,那么要不要换一个医生再确认一下?
显然,不同类别的分类代价并不相等。
1.错误率指的是再所有测试样例中错分的样例比例。实际上,这样的度量错误掩盖了样例如何被分错的事实。
2.混淆矩阵:更好的了解分类中的错误。二分类中有:TP(ture positive),FN(判错的正例),FP,TN.标签组成二维矩阵。
3.在分类中,当某个类别的重要性高于其他类别时,我们就可以利用上述定义来定义出多个比错误率更好的新指标。
1)正确率precision:TP/(TP+FP):给出的是预测为正例的样本中的真正正例的比例。
2)召回率recall:TP/(TP+FN):给出的是预测为正例的真实正例占所有真实正例的比例。
3)ROC曲线:用于度量分类中的非均衡性的工具,ROC代表接收者操作特征。
理想情况下,最佳的分类器应该尽可能地处于左上角,这意味着分类器在假阳率很低的同时获得了很高的真阳率
a.ROC曲线给出的是当阈值变化时假阳率(FP/(FP+TN))和真阳率(TP/(TP+FN))的变化情况。
b.ROC曲线不但可以用于比较分类器,还可以基于成本效益(cost-versus-benefit)分析来做出决策。由于在不同的阈值下,不同的分类器的表现情况可能各不相同,因此以某种方式将它们组合起来或许会更有意义。
c.对不同的ROC曲线进行比较的一个指标是曲线下的面积(AUC).AUC给出的是分类器的平均性能值。一个完美分类器的AUC为1.0
d.为了创建ROC曲线,首先要将分类样例按照其预测强度排序。先从排名最低的样例开始,所有排名更低的样例都被判为反例,而所有排名更高的样例都被判为正例。该情况的对应点为<1.0,1.0>.然后,将其移到排名次低的样例中去,如果该样例属于正例,那么对真阳率进行修改,如果该样例属于反例,那么对假阴率进行修改。
除了调节分类器的阈值之外,我们还有一些其他可以用于处理非均衡分类代价的方法,其中的一种称为代价敏感的学习(cost-sensitive learning).我们可以基于该代价矩阵计算其总代价:TPC11+FNC12+FPC21+TNC22.给予不同的分类器不同的代价矩阵(两个循环),计算出不同分类器的总代价,然后选择代价最小的分类器。
在分类算法中,有很多方法可以用来引入代价信息。
在AdaBoost中,可以基于代价函数来调整错误权重向量D;
在朴素贝叶斯中,可以选择具有最小期望代价而不是最大概率的类别作为最后的结果;
在SVM中,可以在代价函数中对于不同的类别选择不同的参数C(惩罚系数)。
上述做法就会给较小类更多的权重,即在训练时,小类当中只允许更少的错误。
对分类器的训练数据进行改造。这可以通过欠抽样(undersampling)或者过抽样(oversampling)来实现。
过抽样意味着复制样例,而欠抽样意味着删除样例。抽样过程则可以通过随机方式或者某个预定方式来实现。
import matplotlib.pyplot as plt
import numpy as np
def plotROC(predStrengths,classLabels):
"""ROC曲线的绘制及AUC计算函数
predStrengths:代表的是分类器的预测强度,是一个Numpy数组或者一个行向量组成的矩阵。在分类器和训练函数将这些数值应用到sign()函数之前,它们就已经产生了。
"""
cur = (1.0,1.0) #该元组保留的是绘制光标的位置
ySum = 0.0 #计算AUC的值
numPosClas = sum(np.array(classLabels) == 1.0) #标签为1的正例数据个数
yStep = 1/float(numPosClas) #在y坐标轴上的步进数目
xStep = 1/float(len(classLabels) - numPosClas)
sortedIndicies = predStrengths.argsort() #获取排好序的索引,这些索引是按照最小到最大的顺序排列的,因此我们需要从<1.0,1.0>开始绘制
print(sortedIndicies)
fig = plt.figure()
fig.clf()
ax = plt.subplot(111)
for index in sortedIndicies.tolist()[0]: #tolist()转换为列表
"""遍历表时,每得到一个标签为1.0的类,则要沿着y轴的方向下降一个步长,即不断降低真阳率,x轴降低真阴率"""
if classLabels[index] == 1.0:
delX = 0
delY = yStep
else:
delX =xStep
delY = 0
ySum += cur[1]
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)