机器学习实战——7.利用AdaBoost元算法提高分类性能

目录

7.1 基于数据集多重抽样的分类器

7.1.1 bagging:基于数据随机重抽样的分类器构建方法

7.1.2 boosting

7.2 训练算法:基于错误提升分类器的性能

7.3基于单层决策树构建弱分类器

7.4 完整AdaBoosts算法的实现

7.5 测试算法:基于AdaBoosts的分类

7.6 示例:在一个难数据集上应用AdaBoost

7.7 非均衡分类问题

7.1.1 其它分类性能度量指标:正确率、召回率及ROC曲线

7.7.2 基于代价函数的分类器决策控制

7.7.3 处理非均衡问题的数据抽样方法

7.8 本章小结


元算法(meta-algorithm)是对其他算法进行组合的一种方式。AdaBoost是最流行的元算法。

7.1 基于数据集多重抽样的分类器

将不同分类器组合起来,组合结果被称为集成方法(ensemble method)或者元算法(meta-algorithm)。使用集成方法时会有多种形式:可以是不同算法的集成,也可以是同一算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。

AdaBoost:优点:泛化错误率低,易编码,可以应用在大部分分类器上,无参数调整。缺点:对离群点敏感。适用数据类型:数值型和标称型数据。

7.1.1 bagging:基于数据随机重抽样的分类器构建方法

自举汇聚法(bootstrap aggregating),也称为bagging方法,是在从原始数据集选择S次后得到S个新数据集的一种技术。新数据集和原始数据集的大小相等。每个数据集都是通过在原始数据集中随机选择一个样本来进行替换而得到的。这里的替换就意味着可以多次地选择同一个样本。这一性质就允许新数据集中可以有重复的值,而原始数据集的某些值在新集合中则不再出现。

在S个数据集建好之后,将某个学习算法分别作用于每个数据集就得到了S个分类器。当要对新数据进行分类时,就可以应用这S个分类器进行分类。与此同时,选择分类器投票结果中最多的类别作为最后的分类结果。

一些更先进的bagging方法,比如随机森林(random forest)详见:https://www.stat.berkeley.edu/~breiman/RandomForests/cc_home.htm

7.1.2 boosting

boosting是一种与bagging很类似的技术。不论是在boosting还是bagging当中,所使用的分类器的类型都是一致的,但是在前者中,不同的分类器是通过串行训练而获得的,每个新分类器都根据已训练出的分类器的性能来进行训练。boosting是通过集中关注被已有分类器错分的那些数据来获得新的分类器。

由于boosting分类的结果是基于所有分类器的加权求和结果的,因此boosting和bagging不太一样。bagging中的分类器权重是相等的,而boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度。boosting方法拥有多个版本,这里只关注其中一个最流行的版本AdaBoost。

AdaBoost的一般流程:

(1)收集数据:可以使用任意方法。

(2)准备数据:依赖于所使用的弱分类器类型,本章使用的是单层决策树,这种分类器可以处理任何数据类型。当然也可以使用任意分类器作为弱分类器。

(3)分析数据:可以使用任意方法。

(4)训练算法:AdaBoost的大部分时间都用在训练上,分类器将多次在同一数据集上训练弱分类器。

(5)测试算法:计算分类的错误率。

(6)使用算法:同SVM一样,AdaBoost预测两个类别中的一个。如果想把它应用到多个类别的场合,那么就要像多类SVM中的做法一样对AdaBoost进行修改。

7.2 训练算法:基于错误提升分类器的性能

能否使用弱分类器和多个实例来构建一个强分类器?这里的‘弱’意味着分类器的性能比随机猜测要略好,但是也不会好太多。这就是说,在二分类情况下弱分类器的错误率会高于50%,而“强”分类器的错误率将会低很多。AdaBoost算法即脱胎于上述理论问题。

AdaBoost是adaptive boosting(自适应boosting)的缩写,其运行过程如下:训练数据中的每个样本,并赋予其一个权重,这些权重构成了向量D。一开始,这些权重都初始化成相等值。首先在训练数据上训练出一个若分类器并计算该分类器的错误率,然后在同一数据集上再次训练弱分类器。在分类器的第二次训练当中,将会重新调整每个样本的权重,其中第一次分对的样本的权重将会降低,而第一次分错的样本的权重将会提高。为了从所有弱分类器中得到最终的分类结果,AdaBoost为每个分类器都分配了一个权重值alpha,这些alpha值是基于每个弱分类器的错误率进行计算的。其中,错误率的定义为:

\varepsilon=未正确分类的样本数目/所有样本数目

alpha的计算公式如下:\alpha =\frac{1}{2}ln\left ( \frac{1-\varepsilon }{\varepsilon } \right )

AdaBoost算法流程如下图所示:

计算出alpha值之后,可以对权重向量D进行更新,以使得那些正确分类的样本的权重降低而错分样本的权重升高。D的计算方法如下。

如果某个样本被正确分类,那么该样本的权重更改为:D_{i}^{\left ( l+1 \right )}=\frac{D_{i}^{l}e^{-\alpha }}{Sum\left (D \right )}

而如果某个样本被错分,那么该样本的权重更改为: D_{i}^{\left ( l+1 \right )}=\frac{D_{i}^{l}e^{\alpha }}{Sum\left (D \right )}

在计算出D之后,AdaBoost又开始进入下一轮迭代。AdaBoost算法会不断地重复训练和调整权重的过程,直到训练错误率为0或者弱分类器的数目达到用户的指定值为止。

7.3基于单层决策树构建弱分类器

单层决策树(decision stump,也称决策树桩)是一种简单的决策树。接下来要构建一个单层决策树,而它仅基于单个特征来做决策。由于这棵树只有一次分裂过程,因此它实际就是一个树桩。

在构建AdaBoost的代码时,我们将首先通过一个简单数据集来确保在算法实现上一切就绪。然后,建立一个叫adaboost.py的新文件并加入如下代码:

def loadSimpData():
    datMat=([[1.0,2.1],
             [2.0,1.1],
             [1.3,1.0],
             [1.0,1.0],
             [2.0,1.0]])
    classLabels=[1.0,1.0,-1.0,-1.0,1.0]
    return datMat,classLabels

下图给出了上述数据集的示意图。如果想要试着从某个坐标轴选择一个值(即选择一条与坐标轴平行的直线)来将所有的圆形点和方形点分开,这显然是不可能的。这就是单层决策树难以处理的一个著名问题。通过使用多棵单层决策树,我们就可以构建出一个能够对该数据集完全正确分类的分类器。

有了数据,接下来就可以通过构建多个函数来建立单层决策树。

第一个函数将用于测试是否有某个值小于或大于我们正在测试的阈值。第二个函数则更加复杂一些,它会在一个加权数据集中循环,并找到具有最低错误率的单层决策树。

这个程序的伪代码看起来大致如下:

将最小错误率minError设为+∞
对数据集中的每一个特征(第一层循环):
    对每个步长(第二层循环):
        对每个不等号(第三层循环):
            建立一棵单层决策树并利用加权数据集对它进行测试
            如果错误率低于minError,则将当前单层决策树设为最佳单层决策树
返回最佳单层决策树

 单层决策树生成函数:

#单层决策树生成函数
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
    retArray=ones((shape(dataMatrix)[0],1))
    if threshIneq=='lt':
        retArray[dataMatrix[:,dimen]<=threshVal]=-1
    else:
        retArray[dataMatrix[:,dimen]>threshVal]=-1
    return retArray

def buildStump(dataArr,classLabels,D):
    dataMatrix=mat(dataArr); labelMat=mat(classLabels).T
    m,n=shape(dataMatrix)
    numSteps=10;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)))
                errArr[predictedVals==labelMat]=0
                weightedError=D.T*errArr
                print('split:dim %d,thresh %.2f, thresh inequal:%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
D=mat(ones((5,1))/5)
datMat,classLabels=loadSimpData()
buildStump(datMat,classLabels,D)

上述程序包含两个函数。第一个函数stumpClassify()是通过阈值比较对数据进行分类的。所有在阈值一边的数据会分到类别-1,而在另一边的数据会分到类别+1.该函数可以通过数组过滤来实现,首先将返回数组的全部元素设置为1,然后将所有不满足不等式要求的元素设置为-1.可以基于数据集中的任一元素进行比较,同时也可以将不等号在大于、小于之间切换。

第二个函数buildStump()将会遍历stumpClassify()函数所有的可能输入值,并找到数据集上最佳的单层决策树。这里的“最佳”是基于数据的权重向量D来定义的。在确保输入数据符合矩阵格式之后,整个函数就开始执行了。然后,函数将构建一个称为bestStump的空字典,这个字典用于存储给定权重向量D时所得到的最佳单层决策树的相关信息。变量numSteps用于在特征的所有可能值上进行遍历。而变量minError则在一开始就初始化成正无穷大,之后用于寻找可能的最小错误率。

三层嵌套的for循环是程序最主要的部分。第一层for循环在数据集的所有特征上遍历。考虑到数值型的特征,可以通过计算最小值和最大值来了解应该需要多大的步长。然后,第二层for循环再在这些值上遍历。甚至将阈值设置为整个取值范围之外也是可以的。因此,在取值范围之外还应该有两个额外的步骤。最后一个for循环则是在大于与小于之间切换不等式。

在嵌套的三层for循环之内,在数据集及三个循环变量上调用stumpClassify()函数。基于这些循环变量,该函数将会返回分类预测结果。接下来构建一个列向量errArr,如果predictedVals()中的值不等于labelMat中的真正类别标签值,那么errArr的相应位置为1.将错误向量errArr和权重向量D的相应元素相乘并求和,就得到了数值weightedError。这就是AdaBoosts和分类器交互的地方。这里,基于权重向量D而不是其他错误计算指标来评价分类器的。如果需要使用其他分类器的话,需要考虑D上最佳分类器所定义的计算过程。

程序接下来输出所有的值。虽然这一行后面可以注释掉,但是它对理解函数的运行还是很有帮助的。最后,将当前的错误率与已有的最小错误率进行对比,如果当前的值较小,那么就在词典bestStump中保存该单层决策树。字典、错误率和类别估计值都会返回给AdaBoosts算法。

为了解实际运行过程,输入以下命令:

D=mat(ones((5,1))/5)
datMat,classLabels=loadSimpData()
buildStump(datMat,classLabels,D)

 split:dim 0,thresh 0.90, thresh inequal:lt,                the weighted error is 0.400
split:dim 0,thresh 0.90, thresh inequal:gt,                the weighted error is 0.600
split:dim 0,thresh 1.00, thresh inequal:lt,                the weighted error is 0.400
split:dim 0,thresh 1.00, thresh inequal:gt,                the weighted error is 0.600
split:dim 0,thresh 1.10, thresh inequal:lt,                the weighted error is 0.400
split:dim 0,thresh 1.10, thresh inequal:gt,                the weighted error is 0.600
split:dim 0,thresh 1.20, thresh inequal:lt,                the weighted error is 0.400
split:dim 0,thresh 1.20, thresh inequal:gt,                the weighted error is 0.600
split:dim 0,thresh 1.30, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.30, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.40, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.40, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.50, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.50, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.60, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.60, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.70, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.70, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.80, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.80, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 1.90, thresh inequal:lt,                the weighted error is 0.200
split:dim 0,thresh 1.90, thresh inequal:gt,                the weighted error is 0.800
split:dim 0,thresh 2.00, thresh inequal:lt,                the weighted error is 0.600
split:dim 0,thresh 2.00, thresh inequal:gt,                the weighted error is 0.400
split:dim 1,thresh 0.89, thresh inequal:lt,                the weighted error is 0.400
split:dim 1,thresh 0.89, thresh inequal:gt,                the weighted error is 0.600
split:dim 1,thresh 1.00, thresh inequal:lt,                the weighted error is 0.200
split:dim 1,thresh 1.00, thresh inequal:gt,                the weighted error is 0.800

......

{'ineq': 'lt', 'dim': 0, 'thresh': 1.3} [[0.2]] [[-1.]
 [ 1.]
 [-1.]
 [-1.]
 [ 1.]]

buildStump在所有可能的值上遍历的同时,也可以看到输出的结果,并且最后会看到返回的字典。(思考:该词典是否对应了最小可能的加权错误率?是否存在其他的设置也能得到相同的错误率?)

上述单层决策树的生成函数是决策树的一个简化版本。它就是所谓的若学习器,即若分类算法。但目前为止,已经构建了单层决策树,并生成了程序,做好了过渡到完整AdaBoosts算法的准备。下一节,将使用多个若分类器来构建AdaBoosts代码。

7.4 完整AdaBoosts算法的实现

上一节,构建了一个基于加权输入值进行决策的分类器。现在,拥有了实现一个完整AdaBoosts算法所需要的所有信息。

整个实现的伪代码如下:

对每次迭代:
    利用buildStump()函数找到最佳的单层决策树
    将最佳单层决策树加入到单层决策树组
    计算alpha
    计算新的权重向量D
    更新累计类别估计值
    如果错误率等于0.0,则退出循环

将下述代码加入到adaboost.py程序中去:

#基于单层决策树的AdaBoosts训练过程
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
    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)))
        bestStump['alpha']=alpha
        weakClassArr.append(bestStump)
        print('classEst:',classEst.T)
        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')
        if errorRate==0.0:
            break
    return weakClassArr

运行代码:

if __name__ == '__main__':
    datMat,classLabels=loadSimpData()
    adaBoostTrainDS(datMat,classLabels,9)

运行结果:

D: [[0.2 0.2 0.2 0.2 0.2]]
classEst: [[-1.  1. -1. -1.  1.]]
aggClassEst: [[-0.69314718  0.69314718 -0.69314718 -0.69314718  0.69314718]]
total error: 0.2 

D: [[0.5   0.125 0.125 0.125 0.125]]
classEst: [[ 1.  1. -1. -1. -1.]]
aggClassEst: [[ 0.27980789  1.66610226 -1.66610226 -1.66610226 -0.27980789]]
total error: 0.2 

D: [[0.28571429 0.07142857 0.07142857 0.07142857 0.5       ]]
classEst: [[1. 1. 1. 1. 1.]]
aggClassEst: [[ 1.17568763  2.56198199 -0.77022252 -0.77022252  0.61607184]]
total error: 0.0 

AdaBoost算法的输入参数包括数据集、类别标签以及迭代次数numIt,其中numIt是在整个AdaBoosts算法中唯一需要用户指定的参数。

假定迭代次数设为9,如果算法在第三次迭代之后错误率为0,那么就会退出迭代过程,因此,此时就不需要执行所有的9次迭代过程。每次迭代的中间结果都会通过print语句进行输出。

函数名称尾部的DS代表的就是单层决策树(decision stump),它是AdaBoosts中最流行的弱分类器,当然并非唯一可用的弱分类器。上述函数确实是建立于单层决策树之上的,但是可以很容易对此进行修改以引入其他基分类器。实际上,任意分类器都可以作为基分类器。上述算法会输出一个单层决策树的数组,因此首先需要建立一个新的python表来对其进行存储。然后,得到数据集中的数据点的数目m,并建立一个列向量D。

向量D非常重要,它包含了每个数据点的权重。一开始,这些权重都赋予了相等的值。在后续的迭代中,AdaBoosts算法会在增加错分数据的权重的同时,降低正确分类数据的权重。D是一个概率分布向量,因此其所有元素之和为1.0.为了满足此要求,一开始的所有元素都会被初始化成1/m。同时,程序还会建立另一个列向量aggClassEst,记录每个数据点的类别估计累计值。

AdaBoosts算法的核心在于for循环,该循环运行numIt次或者知道错误率为0为止。循环中的第一件事就是利用前面介绍的buildStump()函数建立一个单层决策树。该函数的输入为权重向量D,返回的则是利用D而得到的具有最小错误率的单层决策树,同时返回的还有最小的错误率以及估计的类别向量。

接下来,需要计算的则是alpha值。该值会告诉总分类器本次单层决策树输出结果的权重。其中的语句max(error,1e-26)用于确保在没有错误时不会发生除零溢出。而后,alpha值加入到bestStump()字典中,该字典又添加到列表中。该字典包括了分类所需要的所有信息。

接下来的三行则用于计算下一次迭代中的新权重向量D。在训练错误率为0时,就要提前结束for循环。此时程序是通过aggClassEst变量保持一个运行时的类别估计值来实现的。该值只是一个浮点数,为了得到二值分类结果还需要调用sign()函数。如果总错误率为0,则由break语句中止for循环。

接下来,观察一下中间的运行结果。数据的类别标签为[1.0,1.0,-1.0,-1.0,1.0]。在第一轮迭代中,D中的所有值都相等。于是,只有第一个数据点被错分了。因此在第二轮迭代中,D向量给第一个数据点0.5的权重。这就可以通过变量aggClassEst的符号来了解总的类别。第二次迭代之后,发现第一个数据点已经正确分类了,但此时最后一个数据点却是错分了。D向量中的最后一个元素变成0.5,而D向量中的其他值都变得非常小。最后,第三次迭代之后aggClassEst所有值的符号和真实类别标签都完全吻合,那么训练错误率为0,程序就此退出。

观察classifierArray的值:

classifierArray=adaBoostTrainDS(datMat,classLabels,9)
print(classifierArray)

结果:

[{'dim': 0, 'thresh': 1.3, 'ineq': 'lt', 'alpha': 0.6931471805599453}, {'dim': 1, 'thresh': 1.0, 'ineq': 'lt', 'alpha': 0.9729550745276565}, {'dim': 0, 'thresh': 0.9, 'ineq': 'lt', 'alpha': 0.8958797346140273}]

该数组包含三个字典,其中包含了分类所需要的所有信息。此时,一个分类器已经构建成功,并且随时都可以把错误率训练到0.

7.5 测试算法:基于AdaBoosts的分类

一旦拥有了多个弱分类器以及其对应的alpha值,进行测试就变得相当容易了。在adaBoostsTrainDS()中,已经写完了大部分的代码。现在,需要做的就是将若分类器的训练过程从程序中抽出来,然后应用到某个具体的实例中去。每个弱分类器的结果及其对应的alpha值作为权重。所有这些弱分类器的结果加权求和就得到了最后的结果。将下列代码添加到adaboosts.py中,就可以利用它基于adaboostsTrainDS()中的弱分类器对数据进行分类。

#AdaBoost分类函数
def adaClassify(datToClass,classifierArr):
    dataMatrix=mat(datToClass)
    m=shape(dataMatrix)[0]
    aggClassEst=mat(zeros((m,1)))
    for i in range(len(classifierArray)):
        classEst=stumpClassify(dataMatrix,classifierArr[i]['dim'],\
                               classifierArr[i]['thresh'],\
                               classifierArr[i]['ineq'])
        aggClassEst+=classifierArr[i]['alpha']*classEst
        print(aggClassEst)
    return sign(aggClassEst)

上述的adaClassify()函数就是利用训练出的多个弱分类器进行分类的函数。该函数的输入是由一个或者多个待分类样例datToClass以及多个弱分类器组成的数组classifierArr。函数adaClassify()首先将datToClass转换成了一个numpy矩阵,并且得到datToClass中的待分类样例的个数m。然后构建一个0列向量aggClassEst,这个列向量与adaBoostTrainDS()中的含义一样。

接下来,遍历classifierArr中的所有弱分类器,并基于stumpClassify()对每个分类器得到一个类别的估计值。在前面构建单层决策树时,已经用到了stumpClassify()函数,在那里,在所有可能的树桩值上进行迭代来得到具有最小加权错误率的单层决策树。在这里,只是简单地应用了单层决策树。输出的类别估计值乘上该单层决策树的alpha权重然后累加到aggClassEst上,就完成了这一过程。上述程序中加入了一条print语句,以便了解aggClassEst每次迭代后的变化结果。最后,程序返回aggClassEst的符号,即如果aggClassEst大于0则但会+1,小于0则返回-1.

测试代码:

if __name__ == '__main__':
    datMat,classLabels=loadSimpData()
    classifierArray=adaBoostTrainDS(datMat,classLabels,30)
    matrix=adaClassify([0,0],classifierArray)
    print(matrix)

运行结果:

[[-0.69314718]]
       [[-1.66610226]]
       [[-2.56198199]]
       [[-1.]]

可以发现,随着迭代的进行,数据点[0,0]的分类结果越来越强,也可以在其他点上进行分类:

matrix=adaClassify([[5,5],[0,0]],classifierArray)

结果:

[[ 0.69314718]
         [-0.69314718]]
        [[ 1.66610226]
         [-1.66610226]]
        [[ 2.56198199]
         [-2.56198199]]
        [[ 1.]
         [-1.]]

这两个点的分类结果也会随着迭代的进行而越来越强。

7.6 示例:在一个难数据集上应用AdaBoost

本节将在马疝病数据集上应用AdaBoost分类器。

示例:在一个难数据集上的AdaBoost应用

(1)收集数据:提供的文本文件。

(2)准备数据:确保类别标签是+1和-1而非1和0.

(3)分析数据:手工检查数据。

(4)训练算法:在数据上,利用adaBoostTrainDS()函数训练出一系列的分类器。

(5)测试算法:我们拥有两个数据集。在不采用随机抽样的方法下,我们就会对AdaBoost和Logistic回归的结果进行完全对等的比较。

(6)使用算法:观察该例子上的错误率,不过,也可以构建一个Web网站,让训马师输入马的症状然后预测马是否会死去。

在使用上述程序中的代码之前,必须要有向文件中加载数据的方法。一个常见的loadDataset()的程序如下所示:

#自适应数据加载函数
def loadDataSet(fileName):
    numFeat=len(open(fileName).readline().split(' '))
    dataMat=[];labelMat=[]
    fr=open(fileName)
    for line in fr.readlines():
        lineArr=[]
        curLine=line.strip().split(' ')
        for i in range(numFeat-1):
            lineArr.append(float(curLine[i]))
        dataMat.append(lineArr)
        if float(curLine[-1])==1:
            labelMat.append(1)
        else:
            labelMat.append(-1)
    return dataMat,labelMat

将上述代码添加到adaboost.py文件中并且将其保存之后,输入如下命令来使用上述函数:

datArr,labelArr=loadDataSet(r'E:\2019\python code\machine_learning\AdaBoosts/horseColicTraining.txt')
classifierArray = adaBoostTrainDS(datArr, labelArr, 10)

结果:

total error: 0.14 

datArr,labelArr=loadDataSet(r'E:\2019\python code\machine_learning\AdaBoosts/horseColicTraining.txt')
 classifierArray = adaBoostTrainDS(datArr, labelArr, 10)
 testArr,testLabelArr=loadDataSet(r'E:\2019\python code\machine_learning\AdaBoosts/horseColicTest.txt')
predictiion10=adaClassify(testArr,classifierArray)
errArr=mat(ones((68,1)))
print(errArr[predictiion10!=mat(testLabelArr).T].sum())

结果:

12.0

要得到错误率,只需将上述错分样例的个数除以68即可。

很多人认为,AdaBoosts和SVM是监督机器学习中最强大的两种方法。实际上,这两者之间拥有不少相似之处。可以把弱分类器想象成SVM中的一个核函数,也可以按照最大化某个最小间隔的方式重写AdaBoost算法。而它们的不同就在于其所定义的间隔计算方式有所不同,因此,导致的结果也不同。特别是在高维空间下,这两者的差异就会更加明显。

7.7 非均衡分类问题

在结束分类这个主题之前,还必须讨论一个问题。在之前所有分类介绍中,都假设所有类别的分类代价是一样的。在logistic回归分类中,构建了一个用于检测患疝病的马匹是否存活的系统。在那里,构建了分类器,但是并没有对分类后的情形加以讨论。加入某人给我们牵来一匹马,他希望我们预测这匹马是否生存。我们说马会死,那么他们就可能对马实施安乐死,而不是通过给马喂药来延缓其不可避免的死亡过程。我们的预测可能是错误的,马本来是可以继续活着的。毕竟,我们的分类器只有80%的精确度(accuracy)。如果我们预测错误,那么我们将会错杀一个如此昂贵的动物,更不要说人对马还存在情感上的依恋。

如何过滤垃圾邮件呢?如果收件箱中会出现某些垃圾邮件,但合法邮件永远不会扔进垃圾邮件夹中,那么人们是否会满意呢?癌症检测又如何呢?只要患病的人不会得不到治疗,那么再找一个医生来看看会不会更好呢(即情愿误判也不漏判)?

还可以举出很多这样的例子,在大多数情况下不同类别的分类代价并不相等。本节,将会考察一种新的分类器性能度量方法,并通过图像技术来对在上述非均衡问题下不同分类器的性能进行可视化处理。然后,考察这两种分类器的变换算法,它们能够将不同决策的代价考虑在内。

7.1.1 其它分类性能度量指标:正确率、召回率及ROC曲线

错误率指的是在所有测试样例中错分的样例比例。实际上,这样的度量错误掩盖了样例如何被分错的事实。在机器学习中,有一个普遍适用的称为混淆矩阵(confusion matrix)的工具,它可以帮助人们更好地了解分类中的错误。有这样一个关于在房子周围可能发现的动物类型的预测,这个预测的三类问题的混淆矩阵如下表所示:

利用混淆矩阵就可以更好地理解分类中的错误了。 如果矩阵中的非对角元素均为0,就会得到一个完美的分类器。

接下来,考虑另外一个混淆矩阵,这次的矩阵只针对一个简单的二类问题。下表中给出了该混淆矩阵,在这个二类问题中,如果将一个正例判为正例,那么就可以认为产生了一个真正例(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),它最早在二战期间由电气工程师构建雷达系统时使用过。下图给出了一条ROC曲线的例子。

 

 上图是利用10个单层决策树的AdaBoost马疝病检测系统的ROC曲线,上图中有一条虚线一条实线。图中的横轴是伪正例的比例(假阳率=FP/(FP+TN)),而纵轴是真正例的比例(真阳率=TP/(TP+FN))。ROC曲线给出的是当阈值变化时假阳率和真阳率的变化情况。左下角的点对应的是将所有样例判为反例的情况,而右上角的点对应的则是将所有样例判为正例的情况。虚线给出的是随机猜测的结果曲线。

ROC曲线不但可以用于比较分类器,还可以基于成本效益(cost-versus-benefit)分析来做出决策。由于在不同的阈值下,不同的分类器的表现情况可能各不相同,因此以某种方式将它们组合起来或许会更有意义。如果只是简单地观察分类器的错误率,那么我们就难以得到这种更深入的洞察效果了。

在理想的情况下,最佳的分类器应该尽可能地处于左上角,这就意味着分类器在假阳率很低的同时获得了很高的真阳率。例如在垃圾邮件的过滤中,这就相当于过滤了所有的垃圾邮件,但没有将任何合法邮件误实为垃圾邮件而放入垃圾邮件的文件夹中。

对不同的ROC曲线进行比较的一个指标是曲线下的面积(Area Unser the Curve,AUC)。AUC给出的是分类器的平均性能值,当然它并不能完全代替对整条曲线的观察。一个完美分类器的AUC为1.0,而随机猜测的AUC则为0.5。

为了画出ROC曲线,分类器必须提供每个样例被判为阳性或者阴性的可信程度值。尽管大多数分类器都能做到这一点,但是通常情况下,这些值会在最后输出离散分类标签之前被清除。朴素贝叶斯能够提供一个可能性,而在Logistic回归中输入到Sigmoid函数中的是一个数值。在AdaBoost和SVM中,都会计算出一个数值然后输入到sign()函数中。所有的这些值都可以用于衡量给定分类器的预测强度。为了创建ROC曲线,首先要将分类样例按照其预测强度排序。先从排名最低的样例开始,所有排名更低的样例都被判为反例,而所有排名更高的样例都被判为正例。该情况的对应点为<1.0,1.0>。然后,将其移到排名次低的样例中去,如果该样例属于正例,那么对真阳率进行修改;如果该样例属于反例,那么对假阴率进行修改。

在adaboost.py文件加入如下代码:

#ROC曲线的绘制及AUC计算函数
def plotROC(predStrengths,classLabels):
    cur=(1.0,1.0)
    ySum=0.0
    numPosClas=sum(array(classLabels)==1.0)
    yStep=1/float(numPosClas)
    xStep=1/float(len(classLabels)-numPosClas)
    sortedIndicies=predStrengths.argsort()
    fig=plt.figure()
    ax=plt.subplot(111)
    for index in sortedIndicies.tolist()[0]:
        if classLabels[index]==1.0:
            delX=0;delY=yStep;
        else:
            delX=xStep;delY=0;
        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)

上述程序中的函数有两个输入参数,第一个参数是一个numpy数组或者一个行向量组成的矩阵。该参数代表的则是分类器的预测强度。在分类器和训练函数将这些数值应用到sign()函数之前,它们就已经产生了。尽管很快就可以看到该函数的实际执行效果,但是我们还是要先讨论一下这段代码。函数的第二个输入参数是先前使用过的classLabels。我们首先构建一个浮点数二元组,并将它初始化为(1.0,1.0)。该元组保留的是绘制光标的位置,变量ySum则用于计算AUC的值。接下来,通过数组过滤方式计算正例的数目,并将该值赋给numPosClass。该值先是确定了在y坐标轴上的步进数目,接着在x轴和y轴的0.0到1.0区间上绘点,因此y轴上的步长是1.0/numPosCla。类似地,就可以得到x轴的步长了。

接下来,我们得到了排序索引,但是这些索引是按照最小到最大的排序排列的,因此需要从点<1.0,1.0>开始绘,一直到<0,0>。跟着的三行代码则是用于构建画笔,并在所有排序值上进行循环。这些值在一个numpy数组或者矩阵中进行排序,python则需要一个表来进行迭代循环,因此需要调用tolist()方法。当遍历表时,每得到一个标签为1.0的类,则要沿着y轴的方向下降一个步长,即不断降低真阳率。类似地,对于每个其它类别的标签,则是在x轴方向上倒退了一个步长(假阴率方向)。上述代码只关注1这个类别标签,因此就无所谓是采用1/0标签还是+1/-1标签。

为了计算AUC,需要对多个小矩形的面积进行累加。这些小矩形的宽度时xStep,因此先对所有矩形的高度进行累加,然后再乘以xStep得到其总面积。所有高度的和(ySum)随着x轴的每次移动而渐次增加。一旦决定了是在x轴还是y轴方向上进行移动的,就可以在当前点和新点之间画出一条线段。然后,当前点cur更新了。最后就会得到一个像样的绘图并将AUC打印到终端输出。

为了解实际运行效果,需要将adaboostTrainDS()的最后一行代码替换成:

    return weakClassArr,aggClassEst

 以得到aggClassEst的值。然后,输入以下命令:

 datArr,labelArr=loadDataSet(r'E:\2019\python code\machine_learning\AdaBoosts/horseColicTraining.txt')
classifierArray,aggClassEst = adaBoostTrainDS(datArr, labelArr, 10)
plotROC(aggClassEst.T,labelArr)

结果:

the area under the curve is: 0.9349716066133961

这是在10个弱分类器下,AdaBoost算法性能的结果。 

7.7.2 基于代价函数的分类器决策控制

除了调节分类器的阈值之外,还有一些其他可以用于处理非均匀分类代价问题的方法,其中的一种称为代价敏感的学习(cost-sensitive learning)。下面第一张表给出的是目前为止分类器的代价矩阵(代价不是0就是1)。可以基于该代价矩阵计算其总代价:TP*0+FN*1+FP*1+TN*0。基于第二张表中代价矩阵的分类代价的计算公式为:TP*(-5)+FN*1+FP*50+TN*0。采用第二张表作为代价矩阵时,两种分类错误的代价是不一样的。类似地,这两种正确分类所得到的收益也不一样。如果在构建分类器时,知道了这些代价值,那么就可以选择付出最小代价的分类器。

在分类算法中,我们有很多方法可以用来引入代价信息。在AdaBoost中,可以基于代价函数来调整错误权重向量D。在朴素贝叶斯中,可以选择具有最小期望代价而不是最大概率的类别作为最后的结果。在svm中,可以在代价函数中对于不同的类别选择不同的参数c。上述做法就会给较小类更多的权重,即在训练时,小类当中只允许更小的错误。

7.7.3 处理非均衡问题的数据抽样方法

另外一种针对非均衡问题调节分类器的方法,就是对分类器的训练数据进行改造。这可以通过欠抽样(undersampling)或者过抽样(oversampling)来实现。过抽样意味着复制样例,而欠抽样意味着剔除样例。不管采用哪种方式,数据都会从原始形式改造成新形式。抽样过程则可以通过随机方式或者某个预定方式来实现。

通常也存在某个罕见的类别需要我们来识别,比如在信用卡欺诈中。如前所述,正例类别属于罕见类别。我们希望对于这种罕见类别能尽可能保留更多的信息,因此,我们应该保留正例类别中的所有样例,而对反例类别进行欠抽样或者样例删除处理。这种方法的一个缺点就在于要确定哪些样例需要进行剔除。但是,在选择剔除的样例中可能携带了剩余样例中并不包含的有价值信息。

上述问题的一种解决方法,就是选择那些离决策边界较远的样例进行删除。假定我们有一个数据集,其中有50例信用卡欺诈交易和5000例合法交易。如果我们想要对合法交易样例进行欠抽样处理,使得这两类数据比较均衡的话,那么我们就需要去掉4950个样例,而这些样例中可能包含很多有价值的信息。这看上去有些极端,因此有一种替代的策略就是使用反例类别的欠抽样和正例类别的过抽样相混合的方法。

要对正例类别进行过抽样,可以复制已有样例或者加入与已有样例相似的点。一种方法是加入已有数据点的插值点,但是这种做法可能会导致过拟合的问题。

7.8 本章小结

集成方法通过组合多个分类器的分类结果,获得了比简单分类器更好的分类结果。有一些利用不同分类器的集成方法,但是本章只介绍了利用同一类分类器的集成方法。

多个分类器组合可能会进一步凸显出单分类器的不足,比如过拟合问题。如果分类器之间差异显著,那么多个分类器组合就可能会缓解这一问题。分类器之间的差异可以是算法本身或者是应用于算法上的数据的不同。

本章介绍的两种集成方法是bagging和boosting。在bagging中,是通过随机抽样的替换方法,得到了与原始数据集规模一样的数据集。而boosting在bagging的思路上更近了一步,它在数据集上顺序应用了多个不同的分类器。另一个成功的集成方法就是随机森林。

本章介绍了boosting方法中最流行的一个称为AdaBoost的算法。AdaBoost以弱学习器作为基学习器,并且输入数据,使其通过权重向量进行加权。在第一次迭代当中,所有数据都等权重。但是在后续的迭代当中,前次迭代中分错的数据的权重会增大。这种针对错误的调节能力正是AdaBoost的长处。

本章以单层决策树作为弱学习器构建了Adaboost分类器。实际上,AdaBoost函数可以应用于任意分类器,只要该分类器能够处理加权数据即可。AdaBoost算法十分强大,它能够快速处理其他分类器难以处理的数据集。

非均衡分类问题是指在分类器训练时正例数目和反例数目不相等(相差很大)。该问题在正例和反例的代价不同时也存在。本章不仅考察了一种不同分类器的评价方法——ROC曲线,还介绍了正确率和召回率这两种在类别重要性不同时,度量分类器性能的指标。

本章介绍了通过过抽样和欠抽样方法来调节数据集中的正例和反例数目。另外一种可能更好的非均衡问题的处理方法,就是在训练分类器时将错误的代价考虑在内。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值