机器学习实战笔记:AdaBoost

AdaBoost概述

AdaBoost是典型的Boosting算法,属于Boosting家族的一员。在说AdaBoost之前,先说说Boosting提升算法。

Boosting算法基于这样一种思想:如果有一个复杂的问题,如果将多个专家的判断进行适当的综合可能要比某一个单独的专家独自判断的结果要好,实际上就是“三个臭皮匠顶个诸葛亮”的道理。

将这个思想应用于机器学习领域,如果有一个问题,存在一个学习算法能够学习它,但它的正确率仅仅只比随即猜测要好,那么这个学习算法就叫做“弱学习算法”,那么我们能否将这种弱学习算法提升为正确率很高的强学习算法呢。答案是可以的,其中最具代表性的就是AdaBoost算法。

对于分类问题而言,如果有一个训练集,一般来说,找到弱学习算法要相对容易一些,然后通过反复学习得到一系列弱分类器,组合这些弱分类器得到一个强分类器。Boosting算法要涉及到两个部分,加法模型和前向分步算法。加法模型就是说强分类器由一系列弱分类器线性相加而成。一般组合形式如下:

                                                                     F_M(x;P)=\sum_{m=1}^n\beta_{m}h(x;a_m)

其中,h(x;a_m) 就是一个个的弱分类器,a_m是弱分类器学习到的最优参数,\beta _m就是弱学习在强分类器中所占比重,P是所有a_m\beta _m的组合。这些弱分类器线性相加组成强分类器。

前向分步就是说在训练过程中,下一轮迭代产生的分类器是在上一轮的基础上训练得来的。也就是可以写成这样的形式:

                                                                F_m (x)=F_{m-1}(x)+ \beta _mh_m (x;a_m)

由于采用的损失函数不同,Boosting算法也因此有了不同的类型,AdaBoost就是损失函数为指数损失的Boosting算法。

 大多数的提升方法都是改变训练数据的概率分布(训练数据的权值分布),针对不同的训练数据分布调用弱学习算法学习一系列弱分类器。这样的话,有两个问题需要回答:

1 在每一轮迭代中如何改变训练数据的权值或者概率分布

2 如何将弱分类器组合成一个强分类器

关于第一个问题,AdaBoost的做法是,提高那些被前一轮弱分类器错误分类样本的权值,而降低那些被正确分类样本的权值,这样一来,那些没有得到正确分类的数据,由于其权值的加大而受到后一轮分类器的更大关注,于是,分类问题就被一系列弱分类器分而治之。关于第二个问题,弱分类器的组合,AdaBoost采用加权多数表决的方法,即加大分类误差率小的弱分类器的权值,使其在表决中起较大作用,减小分类误差率大的弱分类器的权值,使其在表决中起较小的作用。

实例

为了加深理解,我们来举一个例子。有如下的训练样本,我们需要构建强分类器对其进行分类。x是特征,y是标签。

令权值分布D_1=(w_{1,1},w_{1,2},\cdots ,w_{1,10} )

并假设一开始的权值分布是均匀分布:w_{1,i}=0.1,i=1,2,…,10

现在开始训练第一个弱分类器。我们发现阈值取2.5时分类误差率最低,得到弱分类器为:

当然,也可以用别的弱分类器,只要误差率最低即可。这里为了方便,用了分段函数。得到了分类误差率e_1=0.3

第二步计算G_1 (x)在强分类器中的系数\alpha _{1}=\frac{1}{2} log\frac{ 1-e_1}{e_1}=0.4236,这个公式先放在这里,下面再做推导。

第三步更新样本的权值分布,用于下一轮迭代训练。由公式:

                                                     w_{2,i}=\frac{w_{1,i}}{z_1}exp(-\alpha _{1}y_1G_1(x_i)),i=1,2,\cdots ,n

得到新的权值分布,从各0.1变成了:

                  D2=(0.0715,0.0715,0.0715,0.0715,0.0715,0.0715,0.1666,0.1666,0.1666,0.0715)

可以看出,被分类正确的样本权值减小了,被错误分类的样本权值提高了。

第四步得到第一轮迭代的强分类器:

                                                           sign(F1(x))=sign(0.4236G1(x))

以此类推,经过第二轮……第N轮,迭代多次直至得到最终的强分类器。迭代范围可以自己定义,比如限定收敛阈值,分类误差率小于某一个值就停止迭代,比如限定迭代次数,迭代1000次停止。这里数据简单,在第3轮迭代时,得到强分类器:

                                     sign(F3(x))=sign(0.4236G1(x)+0.6496G2(x)+0.7514G3(x))
的分类误差率为0,结束迭代。

F(x)=sign(F3(x))就是最终的强分类器。

算法流程

总结一下,得到AdaBoost的算法流程:

  • 输入:训练数据集T={(x1,y1),(x2,y2),(xN,yN)},其中,x_i\in X\sqsubseteq Rny_i\in Y=-1,1,迭代次数M
  • 1. 初始化训练样本的权值分布:D1=(w_{1,1}},w_{1,2},\cdots ,w_{1,i}),w_{1,i}=1/N,i=1,2,\cdots ,N,D1=(w_{1,1},w_{1,2},\cdots ,w_{1,i}),w_{1,i}=1/N,i=1,2,\cdots ,N。
  • 2. m=1,2,\cdots ,M次迭代
  •   (a) 使用具有权值分布D_m的训练数据集进行学习,得到弱分类器G_m(x)
  •   (b) 计算G_m(x)在训练数据集上的分类误差率:

                                                               e_m=\sum_{i=1}^Nw_{m,i} I(G_m (x_i )≠y_i )

  •   (c) 计算G_m(x)在强分类器中所占的权重:

                                                                     \alpha _m=\frac{1}{2}log \frac{1-e_m}{e_m}

  •   (d) 更新训练数据集的权值分布(这里,z_m是归一化因子,为了使样本的概率分布和为1):

                                                w_{m+1,i}=\frac{w_{m,i}}{z_m}exp(-\alpha _{i}y_iG_m(x_i)),i=1,2,\cdots ,10

                                                                     z_m=\sum_{i=1}^Nw_{m,i}exp⁡(-α_m y_i G_m (x_i ))

  • 3.    得到最终分类器:

                                                            F(x)=sign(\sum_{1}^{n}\alpha _m G_m (x))

公式推导

现在我们来搞清楚上述公式是怎么来的。

假设已经经过m−1轮迭代,得到F_{m-1} (x),根据前向分步,我们可以得到:

                                                                   F_m (x)=F_{m-1} (x)+\alpha _m G_m (x)

我们已经知道AdaBoost是采用指数损失,由此可以得到损失函数:

                                   Loss=\sum_{i=1}^Nexp(-y_iF_m(x_i))=\sum_{i=1}^Nexp(-y_i(F_{m-1}(x_i)+\alpha _m G_m (x_i )))

这时候,F_{m-1}(x)是已知的,可以作为常量移到前面去:

                                                          Loss=\sum_{i=1}^N\widetilde{w_{m,i}} exp(-y_i\alpha _m G_m (x_i )))


其中,\widetilde{w_{m,i}}=exp(-y_i(F_{m-1}(x_i))),敲黑板!这个就是每轮迭代的样本权重!依赖于前一轮的迭代重分配。

是不是觉得还不够像?那就再化简一下:

                                 \widetilde{w_{m,i}}=exp(-y_i(F_{m-2}(x_i)+\alpha _{m-1} G_{m-1} (x_i )))=\widetilde{w_{m-1,i}} exp(-y_i\alpha _{m-1} G_{m-1} (x_i ))

现在够像了吧?ok,我们继续化简Loss:                    

公式变形之后,炒鸡激动!\frac{\sum_{y_i\neq G_m(x_i)}\widetilde{w_{m,i}}}{\sum_{i=1}^N\widetilde{w_{m,i}}}这个不就是分类误差率e_m吗???!重写一下,

                                                        Loss=\sum_{i=1}^N\widetilde{w_{m,i}}(1-e_m)exp(-\alpha_m)+e_mexp(\alpha_m)

Ok,这样我们就得到了化简之后的损失函数。接下来就是求导了。

\alpha _m求偏导,令\frac{\partial Loss}{\partial \alpha _m}=0得到:

                                                                        \alpha _m=\frac{1}{2}log \frac{1-e_m}{e_m}

 

代码实战

建立AdaBoost算法之前,我们必须先建立弱分类器,并保存样本的权重。弱分类器使用单层决策树(decision stump),也称决策树桩,它是一种简单的决策树,通过给定的阈值,进行分类。

1、数据集可视化

为了训练单层决策树,我们需要创建一个训练集,编写代码如下:

import numpy as np
import matplotlib.pyplot as plt

def loadSimpleData():
    dataMat = np.matrix([[1,2.3],
                         [1.5,1.7],
                         [1.9,2],
                         [2.8,1.1],
                         [2.9,2.]])
    classLables = [-1,1,1,-1,1]
    return dataMat,classLables


def showDataSet(dataMat,classLables):
    
    data_plus = []
    data_minus = []
    
    for i in range(0,len(classLables)):
        if classLables[i]>0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
            
    data_plus_arr = np.array(data_plus)
    data_minus_arr = np.array(data_minus)
    
    plt.scatter(np.transpose(data_plus_arr)[0],np.transpose(data_plus_arr)[1])
    plt.scatter(np.transpose(data_minus_arr)[0],np.transpose(data_minus_arr)[1])
    plt.axhline(1.6)
    plt.show()

if __name__ == '__main__':
    dataArr,classLabels = loadSimpleData()
    showDataSet(dataArr,classLabels)

代码运行结果如下:

2、构建单层决策树

我们设置一个分类阈值,比如我横向切分,如下图所示:

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

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 25 10:41:15 2019

@author: jacksong1996
"""
import numpy as np
import matplotlib.pyplot as plt

def loadSimpleData():
    dataMat = np.matrix([[1,2.3],
                         [1.5,1.7],
                         [1.9,2],
                         [2.8,1.1],
                         [2.9,2.]])
    classLables = [-1,1,1,-1,1]
    return dataMat,classLables
    
def stumpClassify(dataMatrix,dimen,thresholdVal,threshIneq):
    """
    Parameters:
        dataMattrix - 数据集矩阵
        dimen - 特征的索引
        thresholdVal - 阈值
        threshIneq - 标志
    returns:
        retArray - 分类结果
    """
    retArray = np.ones((np.shape(dataMatrix)[0],1))
    if threshIneq == 'lt':
        retArray[dataMatrix[:,dimen] <= thresholdVal] = -1.0
    else:
        retArray[dataMatrix[:,dimen] > thresholdVal] = 1.0
    return retArray


def buildStump(dataArr,classLabel,D):
    """
    Parameter:
        dataArr - 数据矩阵
        classLabel - 数据标签
        D - 样本权重
    returns:
        bestStump - 最佳单层决策树信息
        minError - 最小误差
        bestClasEst - 最佳的分类结果
    """
    dataMatrix = np.mat(dataArr); labelMat = np.mat(classLabels).T
    m,n = np.shape(dataMatrix)
    numSteps = 10.0; bestStump = {}; bestClasEst = np.mat(np.zeros((m,1)))
    minError = float('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']:                                          #大于和小于的情况,均遍历。lt:less than,gt:greater than
                threshVal = (rangeMin + float(j) * stepSize)                     #计算阈值
                predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)#计算分类结果
                errArr = np.mat(np.ones((m,1)))                                 #初始化误差矩阵
                errArr[predictedVals == labelMat] = 0                             #分类正确的,赋值为0
                weightedError = D.T * errArr                                      #计算误差
                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

if __name__ == '__main__':
    dataArr,classLabels = loadSimpleData()
    D = np.mat(np.ones((5, 1)) / 5)
    bestStump,minError,bestClasEst = buildStump(dataArr,classLabels,D)
    print('bestStump:\n', bestStump)
    print('minError:\n', minError)
    print('bestClasEst:\n', bestClasEst)    
    

结果如下图所示:

代码不难理解,就是通过遍历,改变不同的阈值,计算最终的分类误差,找到分类误差最小的分类方式,即为我们要找的最佳单层决策树。这里lt表示less than,表示分类方式,对于小于阈值的样本点赋值为-1,gt表示greater than,也是表示分类方式,对于大于阈值的样本点赋值为-1。经过遍历,我们找到,训练好的最佳单层决策树的最小分类误差为0.2,就是对于该数据集,无论用什么样的单层决策树,分类误差最小就是0.2。这就是我们训练好的弱分类器。接下来,使用AdaBoost算法提升分类器性能,将分类误差缩短到0,看下AdaBoost算法是如何实现的。

3、 使用AdaBoost提升分类器性能

根据之前介绍的AdaBoost算法实现过程,使用AdaBoost算法提升分类器性能,编写代码如下:

# -*- coding: utf-8 -*-
"""
Created on Thu Apr 25 10:41:15 2019

@author: jacksong1996
"""
import numpy as np
import matplotlib.pyplot as plt

def loadSimpleData():
    dataMat = np.matrix([[1,2.3],
                         [1.5,1.7],
                         [1.9,2],
                         [2.8,1.1],
                         [2.9,2.]])
    classLables = [-1,1,1,-1,1]
    return dataMat,classLables


def showDataSet(dataMat,classLables):
    
    data_plus = []
    data_minus = []
    
    for i in range(0,len(classLables)):
        if classLables[i]>0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
            
    data_plus_arr = np.array(data_plus)
    data_minus_arr = np.array(data_minus)
    
    plt.scatter(np.transpose(data_plus_arr)[0],np.transpose(data_plus_arr)[1])
    plt.scatter(np.transpose(data_minus_arr)[0],np.transpose(data_minus_arr)[1])
    plt.axhline(1.6)
    plt.show()
    
def stumpClassify(dataMatrix,dimen,thresholdVal,threshIneq):
    """
    Parameters:
        dataMattrix - 数据集矩阵
        dimen - 特征的索引
        thresholdVal - 阈值
        threshIneq - 标志
    returns:
        retArray - 分类结果
    """
    retArray = np.ones((np.shape(dataMatrix)[0],1))
    if threshIneq == 'lt':
        retArray[dataMatrix[:,dimen] <= thresholdVal] = -1.0
    else:
        retArray[dataMatrix[:,dimen] > thresholdVal] = 1.0
    return retArray


def buildStump(dataArr,classLabel,D):
    """
    Parameter:
        dataArr - 数据矩阵
        classLabel - 数据标签
        D - 样本权重
    returns:
        bestStump - 最佳单层决策树信息
        minError - 最小误差
        bestClasEst - 最佳的分类结果
    """
    dataMatrix = np.mat(dataArr); labelMat = np.mat(classLabels).T
    m,n = np.shape(dataMatrix)
    numSteps = 10.0; bestStump = {}; bestClasEst = np.mat(np.zeros((m,1)))
    minError = float('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']:                                          #大于和小于的情况,均遍历。lt:less than,gt:greater than
                threshVal = (rangeMin + float(j) * stepSize)                     #计算阈值
                predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)#计算分类结果
                errArr = np.mat(np.ones((m,1)))                                 #初始化误差矩阵
                errArr[predictedVals == labelMat] = 0                             #分类正确的,赋值为0
                weightedError = D.T * errArr                                      #计算误差
                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

def adaBoostTrainDS(dataArr, classLabels, numIt = 40):
    weakClassArr = []
    m = np.shape(dataArr)[0]
    D = np.mat(np.ones((m, 1)) / m)                                            #初始化权重
    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)))         #计算弱学习算法权重alpha,使error不等于0,因为分母不能为0
        bestStump['alpha'] = alpha                                          #存储弱学习算法权重
        weakClassArr.append(bestStump)                                      #存储单层决策树
        print("classEst: ", classEst.T)
        expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)     #计算e的指数项
        D = np.multiply(D, np.exp(expon))                                      
        D = D / D.sum()                                                        #根据样本权重公式,更新样本权重
        #计算AdaBoost误差,当误差为0的时候,退出循环
        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)
        if errorRate == 0.0: break                                             #误差为0,退出循环
    return weakClassArr, aggClassEst

if __name__ == '__main__':
    dataArr,classLabels = loadSimpleData()
    weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
    print(weakClassArr)
    print(aggClassEst)
    

运行结果如下所示:

 

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

最后训练结果包含了三个弱分类器,其中包含了分类所需要的所有信息。一共迭代了3次,所以训练了3个弱分类器构成一个使用AdaBoost算法优化过的分类器,分类器的错误率为0

总结

AdaBoost的优缺点:

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

ps:参考文章

 https://www.cnblogs.com/ScorpioLu/p/8295990.html

https://cuijiahua.com/blog/2017/11/ml_10_adaboost.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值