机器学习之集成算法

173 阅读9分钟
参与拿奖:本文已参与「新人创作礼」活动,一起开启掘金创作之路
1.相关概念
bagging:基于数据随机重抽样的分类器构建方法。
boosting aggression:自举汇聚法,从原始数据中选择S次得到S个新数据集,新数据集和原始数据集大小相等。
boosting是一种与bagging类似的技术,两者所使用的分类器类型一直,只是分类器训练方式不同。
boosting训练器是通过观察已有的分类器错分的那些数据来获取新的分类器,boosting分类的结果是基于所有分类器的加权求和结果。
bagging训练器是通过串行训练而获得,每个新分类器都是根据已经训练好的分类器的性能来进行训练,bagging的权重相等。
AdaBoost集成学习算法
优点:泛化错误率低,易编码,可以应用在大部分编码器上,无参数调整
缺点:对离群点敏感
适用数据类型:数值型和标称型

更多资料参看[1][2][3][4][5]

2.训练算法
训练算法:基于错误提升分类器的性能
“弱”分类器中“弱”是指分类器的性能比随机猜测要略好
具体步骤:首先在训练集上训练一个弱分类器并计算错误率,然后在同一数据集上再次训练弱分类器。在第二次训练中调整每个样本的权重,AdaBoost为每个分类器都定义了一个权重值alpha,alpha=1/2*(ln((1-e)/e)),其中e表示错误率。
计算出alpha后对权重进行更新,如果分类器分类正确就降低其权重,分类错误就提升其权重。

2.1 创建数据集

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

# 查看数据分布
import matplotlib.pyplot as plt
def showP(datMat,classLbels):
    xcord0 = []
    ycord0 = []
    xcord1 = []
    ycord1 = []
    markers =[]
    colors =[]
    for i in range(len(classLabels)):
        if classLabels[i]==1.0:
            xcord1.append(datMat[i,0]), ycord1.append(datMat[i,1])
        else:
            xcord0.append(datMat[i,0]), ycord0.append(datMat[i,1])
    fig = plt.figure()
    ax = fig.add_subplot(111)       
    ax.scatter(xcord0,ycord0, marker='s', s=90)
    plt.rcParams['font.sans-serif']=['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    ax.scatter(xcord1,ycord1, marker='o', s=50, c='red')
    plt.title('Decision Stump Test Data')
    plt.show()

    
dataMat,classLabels=loadSimpData()
showP(dataMat,classLabels)
    

image.png

2.2 构建单层决策树作为弱分类器

def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):#just classify the data
    # 将返回数组所有值设为1,然后将所有满足不等式要求的元素设置为-1
    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)
    # bestStump字典用于存放给定权重向量D时所得到的最佳决策树的相关信息
    # numSteps用于在特征所有可能值上进行遍历
    numSteps = 10.0; bestStump = {}; bestClasEst = mat(zeros((m,1)))
    minError = inf #init error sum, to +infinity
    # 遍历stumpClassify()函数所有可能的输入值,并找到最佳单层决策树
    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

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

image.png

2.3 完整AdaBoos实现

 对于每次迭代:
     利用buildStump()函数找到最佳决策单层树
     将最佳决策单层树加入到单层决策树数组
     计算alpha
     计算新的权重向量D
     跟新累计类别估计值
     如果错误率在要求范围内(含0)推出循环
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)
        # 如果错误率为0直接推出循环
        if errorRate == 0.0:
            break
    return weakClassArr


adaBoostTrainDS(dataMat,classLabels,9)

image.png

2.4 测试函数


# 测试算法:基于AdaBoost的分类
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)):
        classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'],\
                                 classifierArr[i]['thresh'],\
                                 classifierArr[i]['ineq'])#call stump classify
        aggClassEst += classifierArr[i]['alpha']*classEst
        # print(aggClassEst)
    return sign(aggClassEst)

classifierArr=adaBoostTrainDS(dataMat,classLabels,30)
# print(adaClassify([[5,5],[0,0]],classifierArr))

2.5 一个完整案例

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

dataMat,classLabels=loadDataSet(r'horseColicTraining2.txt')
# print(shape(dataMat))
testMat,testLabels=loadDataSet(r'horseColicTest2.txt')
# print(shape(testMat))
print("分类器数目    训练错误率    测试错误率")
for i in [1,10,50,100,500]:
    # 获取分类器
    classifierArr=adaBoostTrainDS(dataMat,classLabels,i)
    # 使用训练好的分类器进行训练
    trainR=adaClassify(dataMat,classifierArr)
    prediction=adaClassify(testMat,classifierArr)
    # 预测错误结果
    errorArr=mat(ones((67,1)))
    errorArr[prediction!=mat(testLabels).T].sum()
    # 训练错误结果
    errorArr1=mat(ones((299,1)))
    errorArr1[trainR!=mat(classLabels).T].sum()
    a=i
    b=errorArr1[trainR!=mat(classLabels).T].sum()/299
    c=errorArr[prediction!=mat(testLabels).T].sum()/67
    print("%d             %.2f             %.2f"%(a,b,c),end="\n")

### 从错误率中可以看出训练错误率一直在下降,但是测试错误率先下降后上升,这就是过拟合现象

image.png

上图表明训练集上,分类器数目越多分类效果越好,但是测试集上效果先变好,后变差,出现了过拟合的现象,由此可见分类器并不是越多越好。

3.使用ROC曲线展示效果
非均衡分类问题:在大多数情况下不同类别的分类代价不同
分类度量指标:正确率、召回率、ROC曲线
可以通过混淆矩阵观察分类中的错误情况
当某个类别重要性高于其他类别时,可以利用上述指标来进行抉择
ROC曲线不但可以用于比较分类,还可以基于成本效益分析来做出决策
对于不同ROC曲线的一个比较指标就是曲线下面积,AUC,其代表分类器的平均性能值

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)
        # 如果错误率为0直接推出循环
        if errorRate == 0.0:
            break
    return weakClassArr,aggClassEst

dataArr,labelArr=loadDataSet(r'horseColicTraining2.txt')
classifierArr,aggClassEst=adaBoostTrainDS(dataMat,classLabels,10)
    

# 两个参数分别为分类器预测强度、类标签
def plotROC(predStrengths, classLabels):
    import matplotlib.pyplot as plt
    cur = (1.0,1.0) #cursor
    # ySum用于计算AUC值
    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)

plotROC(aggClassEst.T,classLabels)

image.png

4.小结

# 基于代价函数的分类器决策控制
除了调节分类器的阈值之外,还有一些其他用于处理非均衡分类代价的方法
代价敏感学习:在AdaBoost中可以基于代价函数来调整错误权重向量D
在SVM中可以在代价函数中对于不同类别选择不同的参数C
在朴素贝叶斯中,可以选择具有最小期望代价而不是最大概率的类别作为最后结果


处理非均衡问题的数据抽样方法
对分类器的训练数据进行改造,可以通过欠抽样或者过抽样来实现
欠抽样:意味着删除样例
过抽样:意味着复制样例,复制已有样例或者加入已有样例相似的点,一种方法是加入已有数据点的插值点,可能会导致过拟合


集成方法通过组合多个分类器的分类结果,获取比单分类更好的结果
bagging是通过随机抽样的替换方式,得到了与原始数据规模一样的数据集
boosting在bagging基础上,在数据集上顺序运用了多个不同的分类器
随机森林也是一种集成学习方法
AdaBoost以弱学习器作为基分类器,并且输入数据,使其通过权重向量进行加权,第一次迭代时,所有数据都等权
在后续迭代中,前次迭代中错分的数据的权重会增大,针对错误进行调节
5.参考资料

[1] 机器学习实战

[2] 书籍源码

[3] jupyter版本

[4] 本节代码

[5] 相关资料