[机器学习实战] AdaBoost集成算法及非均衡分类问题

利用AdaBoost元算法提高分类性能

元算法(meta-algorithm): 对其他算法进行组合决策。也叫集成方法(ensemble method)

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

集成算法的方式:

  1. 不同算法的集成
  2. 同一算法在不同设置下的集成
  3. 数据集不同部分分配给不同分类器之后的集成

AdaBoost:一种boosting实现方式
优点:
泛化错误率低、易编码、可以应用在大部分分类器上、无参数调整
缺点:
对离群点敏感

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

有放回地取样得到S个新数据集,将某个学习算法作用到每个数据集上就得到了S个分类器。当对新数据进行分类时,就可以应用这S个分类器进行分类。

boosting

  1. boosting是通过集中关注被已有分类器错分的那些数据来获得新的分类器。
  2. boosting分类的结果是基于所有分类器的加权求和得到的。
  3. AdaBoost是最流行的版本。

二、基于错误提升分类器的性能

AdaBoosting运行过程:

  1. 首次在训练数据上训练出一个弱分类器,并计算错误率;
  2. 在分类器的再次训练中,重新调整每个样本的权重D_i,分对的样本的权重将会降低,分错的样本的权重将会提高;
  3. 每个分类器都被分配一个权重值alpha_i = 0.5*ln(1/ε - 1) 其中ε = 错误分类的样本/总样本;
  4. 计算出alpha值之后,可以对权重向量D进行更新,以使得正确分类的样本的权重降低,错误分类的样本的权重升高;
  5. 正确分类的样本:D(t+1;i) = D(t;i)*e^-α / Sum(D);
  6. 错误分类的样本:D(t+1;i) = D(t;i)*e^α / Sum(D);
  7. 进入下一轮迭代,直到训练错误率为0或者弱分类器的数目达到用户指定值为止。

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

from numpy import *
import matplotlib.pyplot as pp
%matplotlib inline
def loadSimpleData():
    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
datMat, classLabels = loadSimpleData()
x0 = []; y0 = []
x1 = []; y1 = []
for i in range(len(datMat)):
    if classLabels[i] > 0:
        x0.append(array(datMat)[i][0])
        y0.append(array(datMat)[i][1])
    else:
        x1.append(array(datMat)[i][0])
        y1.append(array(datMat)[i][1])
pp.scatter(x0,y0,color='r')
pp.scatter(x1,y1,color='g')
<matplotlib.collections.PathCollection at 0x2c40e2b2cf8>

找不到一条于坐标轴平行的直线来分类,这是单层决策树无法处理的问题。

# 通过阈值比较对数据进行分类
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
    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
    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)
buildStump(datMat, classLabels, D)
({'dim': 0, 'ineq': 'lt', 'thresh': 1.3}, matrix([[ 0.2]]), array([[-1.],
        [ 1.],
        [-1.],
        [-1.],
        [ 1.]]))

四、完整AdaBoost算法的实现

  1. 利用buildStump()函数找到最佳的单层决策树
  2. 将最佳单层决策树加入到单层决策树数组
  3. 计算alpha
  4. 计算新的权重向量D
  5. 更新累计类别估计值
  6. 如果错误率等于0.0,则退出循环
# 基于单层决策树的AdaBoost训练过程
def adaBoostTrainDS(dataArr, classLabels, numIt=40):
    weakClassArr = []
    m = shape(dataArr)[0]
    D = mat (ones((m,1))/m) # 所有数据点Di之和为1
    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
        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
adaBoostTrainDS(datMat, classLabels, 9)
total error:  0.2 

total error:  0.2 

total error:  0.0 






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

五、测试算法:基于AdaBoost的分类

# AdaBoost分类函数
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 = loadSimpleData()
classifierArr = adaBoostTrainDS(datArr, labelArr, 30)
total error:  0.2 

total error:  0.2 

total error:  0.0 
adaClassify([0,0],classifierArr)
matrix([[-1.]])

六、在一个难数据集上应用AdaBoost

准备数据:确保类别标签是+1和-1

# 自适应数据加载函数
def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) #readline返回的是第一行
    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('data/horseColicTraining2.txt')
classifierArray = adaBoostTrainDS(datArr,labelArr,10)
total error:  0.284280936455 

total error:  0.284280936455 

total error:  0.247491638796 

total error:  0.247491638796 

total error:  0.254180602007 

total error:  0.240802675585 

total error:  0.240802675585 

total error:  0.220735785953 

total error:  0.247491638796 

total error:  0.230769230769 
for DS in classifierArray:print(DS)
{'dim': 9, 'thresh': 3.0, 'ineq': 'gt', 'alpha': 0.4616623792657674}
{'dim': 17, 'thresh': 52.5, 'ineq': 'gt', 'alpha': 0.31248245042467104}
{'dim': 3, 'thresh': 55.199999999999996, 'ineq': 'gt', 'alpha': 0.2868097320169577}
{'dim': 18, 'thresh': 62.300000000000004, 'ineq': 'lt', 'alpha': 0.23297004638939514}
{'dim': 10, 'thresh': 0.0, 'ineq': 'lt', 'alpha': 0.19803846151213736}
{'dim': 5, 'thresh': 2.0, 'ineq': 'gt', 'alpha': 0.18847887349020642}
{'dim': 12, 'thresh': 1.2, 'ineq': 'lt', 'alpha': 0.1522736899747682}
{'dim': 7, 'thresh': 1.2, 'ineq': 'gt', 'alpha': 0.15510870821690495}
{'dim': 5, 'thresh': 0.0, 'ineq': 'lt', 'alpha': 0.1353619735335938}
{'dim': 4, 'thresh': 28.799999999999997, 'ineq': 'lt', 'alpha': 0.12521587326132094}
testArr,testLabelArr = loadDataSet('data/horseColicTest2.txt')
prediction10 = adaClassify(testArr,classifierArray)
errArr = mat(ones((67,1)))
errRate = (errArr[prediction10 != mat(testLabelArr).T].sum())/(len(testLabelArr))
errRate
0.23880597014925373

弱分类器的数目从1-10000,训练错误率将越来越低,但测试错误率下降到21%左右后就逐渐升高,因为出现了过拟合的现象。

七、非均衡分类问题---分类器的普遍问题

在大多数情况下不同类别额度分类代价并不相等

1. 正确率、召回率及ROC曲线

正确率和召回率

  1. 错误率掩盖了样例如何被分错的事实。
  2. 通过混淆矩阵(confusion matrix)计算正确率(precision, TP/(TP+FP))和召回率(recall, TP/(TP+FN))
  3. 我们很容易构造一个高正确率或高召回率的分类器,但很难保证两者同时成立

ROC曲线
可以通过调节分类器的阈值处理非均衡分类代价的方法。

# ROC曲线的绘制及AUC计算函数
def plotROC(predStrengths, classLabels):
    import matplotlib.pyplot as pp
    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 = pp.figure()
    fig.clf()
    ax = pp.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]
        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--')
    pp.xlabel('False Positive Rate')
    pp.ylabel('True Positive Rate')
    pp.title('ROC curve for AdaBoost Horse Colic Detection System')
    ax.axis([0,1,0,1])
    pp.show()
    print('the Area Under the Curve is: ',ySum*xStep)
# 改动返回值
def adaBoostTrainDS(dataArr, classLabels, numIt=40):
    weakClassArr = []
    m = shape(dataArr)[0]
    D = mat (ones((m,1))/m) # 所有数据点Di之和为1
    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
        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, aggClassEst
datArr,labelArr = loadDataSet('data/horseColicTraining2.txt')
classifierArray,aggClassEst = adaBoostTrainDS(datArr,labelArr,10)
plotROC(aggClassEst.T,labelArr)

the Area Under the Curve is:  0.8582969635063604
  1. 横轴是假阳率(FP/(FP+TN)),纵轴是真阳率(TP/(TP+FN))
  2. ROC曲线给出的是当阈值变化时假阳率和真阳率的变化情况
  3. 左下角↙的点对应的是将所有样例判为反例的情况,右上角↗的点对应的是将所有样例判为正例的情况
  4. 虚线给出的是随机猜测的结果曲线
  5. 在理想情况下,最佳的分类器应该尽可能地处于左上角↖,这就意味这分类器在假阳率很低的同时获得了很高的真阳率
  6. ROC曲线不仅可以比较分类器,还可以基于成本效益(cost-versus-benefit)分析来做出决策
  7. 对不同的ROC曲线进行比较的一个指标是曲线下的面积AUC(area unser the curve),AUC给出的是平均性能值,完美的AUC=1.0

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

除了调节分类器的阈值之外,还可以用代价敏感的学习(cost-sensitive learning)。
均衡代价的情况下:
cost = TP*0+FN*1+FP*1+TN*0
非均衡代价的情况下:
cost = TP*(-5)+FN*1+FP*(50)+TN*0

如何引入代价信息?

  1. AdaBoost可以基于代价函数来调整错误权重向量D
  2. 朴素贝叶斯中,可以选择具有最小期望代价而不是最大概率的类别作为最后结果
  3. SVM中,可以在代价函数中对不同的类别选择不同的参数C

3. 数据抽样方法

欠抽样(undersampling):删除样例
过抽样(oversampling):复制样例
一种平衡的做法就是正例过抽样,对反例欠抽样(如果正例过少,反例过多)

posted @ 2017-06-29 20:30  戴戴Day  阅读(1639)  评论(0编辑  收藏  举报