本文是机器学习中集成学习的第二篇,主要讲boosting算法家族中非常知名的AdaBoost算法,AdaBoost算法是第一个实用的boosting算法,非常值得学习和研究。本文第一部分对AdaBoost进行介绍,包括AdaBoost的历史,AdaBoost算法到底是怎样的一个算法,使用图例进一步理解AdaBoost;第二部分使用python来实现AdaBoost算法并进行预测;第三部分是对AdaBoost的进一步学习和研究,主要是学习斯坦福大学大学的几位大牛Friedman等人在2000年发表的论文“Additive logistic regression: A statistical view of boosting”,深入理解AdaBoost。小伙伴们如果有什么想法,欢迎留言~

一. AdaBoost介绍

我们在机器学习(八)-集成学习(Ensemble learning)中介绍了集成学习的应用场景,之后介绍了集成学习获得一组成员模型(或基学习器)的学习算法,以及对成员模型(或基学习器)决策结果的组合方法,其中就提到了Boosting。没错,Boosting是机器学习中集成学习家族中的一员,也属于监督学习,同时Boosting本身也是一族机器学习算法,其思想是通过组合许多相对较弱和不准确的规则来创建一个高度精确的预测规则[1]。而本文主要讲的AdaBoost是第一个实用的boosting算法,直到目前仍然是广泛使用和值得研究的算法之一。

1.1 AdaBoost诞生记

科学技术的发展是无数大牛努力的结果,我们来大致了解下AdaBoost的发展史吧~

  • 1984年 Valiant 提出了PAC(probably approximately correct) 学习框架[2] [3],从而使计算机科学诞生了一个新的分支,即计算学习理论(computational learning theory),主要研究通过数学推理和论证更好地解答机器学习中如:“机器学习的学习是什么意思?”,“为什么看起来简单的hypothesis可以很好地模拟现实问题?”等哲学问题;
  • 随后,Kearns 和 Valiant (1988, 1989) [4] [5]首先提出了 PAC学习模型中强可学习(strongly learnable)和弱可学习(weakly learnable)的等价性问题,即任意给定仅比随机猜测略好的弱可学习算法,能否将其提升为强可学习算法 ? 如果二者等价,那么只需找到一个比随机猜测略好的弱可学习算法就可以将其提升为强学习算法 ,而不必寻找很难获得的强可学习算法;
  • Robert Schapire在1990年的一篇论文 [6]中对Kearns 和 Valiant 的问题给出了肯定的回答,这在机器学习和统计学方面产生了重大影响,这就是最初的 Boosting算法;
  • 一年后,Freund [7]提出了一种效率更高的Boosting算法,即“boost by majority”,尽管在某种意义上是最优的,但也存在一些实用方面的缺陷;
  • Drucker、Schapire和Simard[8]对这些早期的boosting算法进行了首次实验,但仍然存在实用方面的缺陷;
  • 1995年 , Freund 和 schapire [9]提出了AdaBoost (Adaptive Boosting)算法,与以前的Boosting算法相比具有很强的实用优势。之后,Freund和 schapire进一步提出了改变 Boosting投票权重的AdaBoost . M1,AdaBoost . M2等算法,在机器学习领域受到了极大的关注。

1.2 AdaBoost算法流程

AdaBoost(Ada为Adaptive的简写)是一个具有里程碑意义的算法,适应性(adaptive)指后续的分类器为更好地支持被先前分类器分类错误的样本实例而进行调整。考虑一个二分类问题,输出编码为\(Y \in \{-1, 1\}\),给定特征向量\(X\),分类器\(G(X)\)预测出\(\{-1,1\}\)其中之一。现有\(N\)个训练样本,每个训练样本表示为\((x_i,y_i), i=1,2,...N\),那么训练集的误差率为:

\[\overline{err} = \frac{1}{N} \sum_{i=1}^{N}I(y_i \neq G(x_i)) \]

AdaBoost算法的每一次迭代都会为训练样本\((x_i,y_i), i=1,2,...N\) 赋予新的权重\(w_1, w_2,...,w_N\)。最初,训练样本权重相同,均为\(w_i=1/N\),所以第一次迭代相当于在数据原有的样子上训练分类器,而在接下来的迭代\(m=2,3,..,M\)中,训练集中的每一个样本权重都可能被修改,之后在已修改权重的训练样本上训练下一个分类器。当迭代次数为\(m\)时,那些在上一次迭代中被分类器\(G_{m-1}(x)\)分类错误的样本的权重会被增加,而分类正确的样本权重会减小,这样随着迭代的进行,难以正确分类的样本会被赋予越来越大的权重,迫使接下来的分类器越来越关注这些在之前错误分类的样本,如下图所示:
image-20190910104235029

最终生成一系列基学习器\(G_m(x),m=1,2,...,M\),再将这些基学习器进行加权组合,得到最终分类器\(G(x)\)

\[G(x) = sign \bigg( \sum \limits_{m=1}^{M}\alpha_mG_m(x) \bigg) \]

其中\(\alpha_m\)是根据\(G_m(x)\)的错误率计算出的,表示\(G_m(x)\)的权重,准确度越高权重越大,对整体预测的影响也越大。我们来看一下Adaboost.M1的算法流程:

输入: 训练数据集$ 𝑇={(𝑥_1,𝑦_1),(𝑥_2,𝑦_2),⋯(𝑥_𝑁,𝑦_𝑁)}\(,\)𝑦∈{−1,+1}$,训练轮数M

  1. 初始化样本权重分布:\(w_i=\frac{1}{N}\)\(i=1,2,...N\)
  2. for m=1 to M:

(a)使用带有权重分布的训练集学习得到基学习器\(G_m(x)\)

\[G_m(x)=arg \ \min \limits_{G(x)} \sum \limits_{i=1}^{N} w_i^{(m)}1\{y_i \neq G(x_i) \} \]

(b)计算\(G_m(x)\)在训练集上的误差率:

\[err_m = \frac{\sum \limits_{i=1}^{N} w_i^{(m)}1\{y_i \neq G(x_i) \}}{\sum \limits_{i=1}^{N} w_i^{(m)}} \]

(c)计算\(G_m(x)\)的系数:\(\alpha_m = \frac{1}{2} ln \frac{1 - err_{m}}{err_m}\)

(d)更新样本权重:\(w_i^{(m+1)} = \frac{w_i^{(m)} \cdot e^{-y_i \alpha_m G_m(x_i)}}{Z^{m}}\)\(i=1,2,...N\)

其中 \(Z^{(m)}\)是规范化因子,\(Z^{(m)} = \sum \limits_{i=1}^{N}w_i^{(m)} \cdot e^{-y_i \alpha_m G_m(x_i)}\)以确保所有的\(w_i^{(m + 1 )}\)构成一个分布。

  1. 输出最终模型:\(G(x) = sign \bigg( \sum \limits_{m=1}^{M}\alpha_mG_m(x) \bigg)\)

算法在2(a)行对加权训练样本引入分类器\(G_m(x)\); 2(b)行计算加权误差率;2(c)行计算分类器\(G_m(x)\)的权重\(\alpha_m\); 2(d)行用2(c)行中计算的\(\alpha_m\)更新每个样本的权重,用因子\(exp(\alpha_m)\)放大分类器\(G_m(x)\)错误分类的样本权重,增加它们在下一个迭代中引入分类器\(G_{m+1}(x)\)时的影响力。

1.3 图解AdaBoost

看算法介绍之后,有的小伙伴仍然感到懵圈,没关系,我们在第二节会用python实现算法去解决一个简单的分类问题,对于算法运行过程,我们可以先睹为快,以便更快地理解Adaboost算法。如下图左上所示,有两个特征dim_0,dim_1,训练数据为5个点: [1., 2.1],[1.5, 1.6],[1.3, 1.],[1., 1.],[2., 1.],类别标签为[1.0,1.0,-1.0,-1.0,1.0](1.0 为蓝色点,-1.0为橙色点)。初始时,所有点权重一样(点大小表示权重大小)。这里我们选用的弱分类器是单层决策树(decision stump),它是一种简单的决策树,仅有两个结点,通过给定的阈值,进行分类。

如上图右上所示,首先我们在dim_0选了一个相对较好的弱分类器\(G_1(x)\),dim_0 <= 1.3为-1.0,即橙色,dim_0 > 1.3为1.0,即蓝色,根据权重的计算方法,算出$\alpha_1=0.693 $。此时,我们可以看到点[1., 2.1]被错误分类为橙色,因此增加权重;

如上图右下所示,首先我们在dim_1继续选了一个弱分类器\(G_2(x)\),dim_1 <= 1.0为-1.0,即橙色,dim_0 > 1.0为1.0,即蓝色,根据权重的计算方法,算出$\alpha_2=0.973 $。此时,我们可以看到点[2.0, 1.0]被错误分类为橙色,因此增加权重;

如上图左下所示,首先我们在dim_0继续选了一个弱分类器\(G_3(x)\),能够将之前分错的两个点都能正确分类,dim_0 <= 0.9为-1.0,即橙色,dim_0 > 0.9为1.0,即蓝色,根据权重的计算方法,算出$\alpha_3=0.896 $。

此时我们获得了\(M=3\)个弱分类器:
$G_1(x),\alpha_1=0.693 \( \)G_2(x),\alpha_2=0.973 \( \)G_3(x),\alpha_3=0.896 $

组合成最终的分类器如下:

$G_x = sign \bigg( \sum_{m=1}^{M}\alpha_mG_m(x)\bigg) = sign \bigg( 0.693*G_1(x) + 0.973 * G_2(x) + 0.896 * G_3(x) \bigg) $

那么使用上面的分类器来预测一下三个绿色的点所属类别吧,如下图所示:

image-20190902090947437

点1:0.693 * ( -1) + 0.973 * (-1) + 0.896 * (-1) = -2.562毫无疑问为橙色点;点2:0.693 * (-1) + 0.973* (1) + 0.896* (1) = 1.176 为蓝色点;点3:0.693 * (1) + 0.973 * (1) + 0.896 * (1) = 2.562为蓝色点;

同时,多个弱分类器形成决策边界,如上图中橙色粗线所示。

二. python实现AdaBoost

上一节中我们了解了Adaboost算法的流程,本小节我们使用python来实现并进行预测。上一节最后提到我们选用的基学习器是单层决策树(decision stump),它是仅有两个结点的简单决策树,通过给定的阈值,进行分类。

2.1 数据集可视化

import numpy as np
import matplotlib.pyplot as plt

"""
Author: 与谁同坐
Desc: adaboost算法
"""
def loadSimpData():
    """
        创建训练数据集
        Parameters:
            无
        Returns:
            dataMat - 训练数据矩阵
            classLabels - 数据标签
    """
  
    dataMat = np.matrix([[1., 2.1],
                         [1.5, 1.6],
                         [1.3, 1.],
                         [1., 1.],
                         [2., 1.]])
    classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
    return dataMat, classLabels


def showDataSet(dataMat, labelMat):
    """
        训练数据可视化
        Parameters:
            dataMat - 训练数据矩阵
            labelMat - 数据标签
        Returns:
            无
    """
    data_plus = []                              #正样本
    data_minus = []                             #负样本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])

    data_plus_np = np.array(data_plus)          #转换为numpy矩阵
    data_minus_np = np.array(data_minus)        #转换为numpy矩阵

    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1]) #正样本散点图
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) #负样本散点图
    plt.show()

if __name__ == '__main__':
    # 1. 创建数据集,并进行可视化
    dataArr,classLabels = loadSimpData()
    showDataSet(dataArr, classLabels)

运行结果如下:

训练数据集有两个特征(两个坐标轴),由于我们选用的基学习器是stump,如果想要尝试从某个坐标轴(特征)上选择一个值(即选择一条与坐标轴平行的直线)来将所有的蓝色圆点和橘色圆点分开,显然是不可能的。但通过使用多棵stump,我们可以构建出一个对该数据集正确分类的分类器。

2.2 构建stump

我们开始尝试设置分类阈值,比如我横向切分,如下图所示:

蓝横线上边的是一个类别,蓝横线下边是一个类别。显然,此时有一个蓝点分类错误,计算此时的分类误差率为1/5 = 0.2。这个横线与坐标轴的y轴的交点,就是我们设置的阈值,通过不断改变阈值的大小,找到使stump分类误差最小的阈值。同理,竖线也是如此,找到最佳分类的阈值,就找到了最佳基学习器\(G_m( x)\),编写代码如下:

def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
    """
    单层决策树分类函数
    Parameters:
        dataMatrix - 数据矩阵
        dimen - 第dimen列,也就是第几个特征
        threshVal - 阈值
        threshIneq - 标志
    Returns:
        retArray - 分类结果
    """
    retArray = np.ones((np.shape(dataMatrix)[0], 1))  # 初始化retArray为1
    if threshIneq == 'lt':
        retArray[dataMatrix[:, dimen] <= threshVal] = -1.0  # 如果小于阈值,则赋值为-1
    else:
        retArray[dataMatrix[:, dimen] > threshVal] = -1.0  # 如果大于阈值,则赋值为-1
    return retArray


def buildStump(dataArr, classLabels, D):
    """
    找到数据集上最佳的单层决策树
    Parameters:
        dataArr - 数据矩阵
        classLabels - 数据标签
        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                # 计算权重误差,上一阶段被增大权重的样本如果再次被分错,weightedError会更大,这个弱分类器也不会被选中

                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__':
    # 1. 创建数据集,并进行可视化
    dataArr,classLabels = loadSimpData()
    # showDataSet(dataArr, classLabels)

    # 2. 构建stump
    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)

代码运行结果如下:

经过遍历,我们找到训练好的最佳stump的最小分类误差率为0.2,就是对于该数据集,无论用什么样的stump,分类误差率最小就是0.2。接下来,使用AdaBoost算法提升整体性能,将分类误差率缩小到0,看下AdaBoost算法是如何实现的。

2.3 使用AdaBoost提升整体分类器性能

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

def adaBoostTrainDS(dataArr, classLabels, numIt=40):
    """
        adaboost算法
        Parameters:
            dataArr - 数据矩阵
            classLabels - 数据标签
            numIt - 迭代次数
        Returns:
            weakClassArr - 弱分类器集合
            aggClassEst - 分类结果
        """
    weakClassArr = []
    m = np.shape(dataArr)[0]         # 样本数量m
    D = np.mat(np.ones((m, 1)) / 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,同时使error不等于0,因为分母不能为0,这里要说明一下 alpha是否需要乘1/2,测试对结果无影响,且后面在第四节我们会讨论这个常量是怎么来的
        # alpha = float(np.log((1.0 - error) / max(error, 1e-16)))
        alpha = float(0.5 * np.log((1.0 - error) / max(error, 1e-16)))
        bestStump['alpha'] = alpha          # 存储弱学习算法权重
        weakClassArr.append(bestStump)      # 存储单层决策树
        print("classEst: ", classEst.T)

        # 计算e的指数项,分类正确权重减小 expon=-alpha,分类错误权重增加 expon=alpha
        expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)

        # 根据样本权重公式,更新样本权重
        D = np.multiply(D, np.exp(expon))
        D = D / D.sum()                     # 归一化(normalization)

        # 计算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__':
    # 1. 创建数据集,并进行可视化
    dataArr,classLabels = loadSimpData()
    # showDataSet(dataArr, classLabels)

    # 2. 构建单层决策树
    # 初始化样本权重
    # 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)

    # 3. 使用adaboost算法提升整体性能,使分类误差降低到0
    weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
    print(weakClassArr)
    print(aggClassEst)

代码运行如下:

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

一旦拥有了多个基学习器以及其对应的权重\(\alpha\) 值,进行测试就变得想当容易了。

2.4 使用AdaBoost进行预测

给定两个测试样本\([0,0],[5,5],[1.1, 1.5]\),使用AdaBoost进行预测的代码如下,

def adaClassify(datToClass,classifierArr):
    """
    使用AdaBoost进行预测
    Parameters:
        datToClass - 待预测样本
        classifierArr - 训练好的分类器
    Returns:
        分类结果
    """
    dataMatrix = np.mat(datToClass)
    m = np.shape(dataMatrix)[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
        print(aggClassEst)
    return np.sign(aggClassEst)


if __name__ == '__main__':
    # 1. 创建数据集,并进行可视化
    dataArr,classLabels = loadSimpData()
    # showDataSet(dataArr, classLabels)

    # 2. 构建单层决策树
    # 初始化样本权重
    # 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)

    # 3. 使用adaboost算法提升整体性能,使分类误差降低到0
    # weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
    # print(weakClassArr)
    # print(aggClassEst)

    # 4. 使用adaboost算法提升整体性能,并进行预测
    weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
    print("weakClassArr : ", weakClassArr)
    print(adaClassify([[0,0],[5,5],[1.1, 1.5]], weakClassArr))

预测结果如图所示,可以看到,分类没有问题。

三. 理解AdaBoost

Freund and Schapire[9] 提出的AdaBoost算法是第一个实用的boosting算法,直到目前仍然是广泛使用和值得研究的算法之一。在这之后,人们做了大量的尝试来“explain” AdaBoost这一学习算法,去理解它的工作原理,什么条件下表现良好以及什么条件下表现欠佳,详细可见[1]。斯坦福的几位大牛Friedman等人在2000年发表的论文[10]中从统计学的“加法模型(additive modeling)+指数损失(exponential loss)”角度来解释Adaboost。

前面两节我们讨论了AdaBoost的原理和实现,但我们是否对一些问题仍有疑惑,比如基学习器的计算方法为什么是\(G_m(x)=arg \ \min \limits_{G(x)} \sum \limits_{i=1}^{N} w_i^{(m)}1\{y_i \neq G(x_i) \}\)\(G_m(x)\)的系数\(\alpha_m\)为什么这样计算等问题,接下来我们将从统计学的角度讨论几个问题,主要参考论文[Friedman et al., 2000] [10]。

3.1 基学习器\(G_m(x)\)从何而来?

由前两节可以知道,AdaBoost最后得到的分类器是一系列的基学习器的线性组合,此即加法模型:

\[f(𝑥)=\sum \limits_{𝑚=1}^{𝑀}𝛼_𝑚𝐺_𝑚(𝑥) \]

在第m步,我们的目标是最小化一个指定的损失函数\(𝐿(𝑦,𝑓(𝑥))\),即 :

\[\min \limits_{(𝛼_𝑚,𝐺_𝑚)} \sum \limits_{i=1}^{N}𝐿(𝑦_𝑖, \sum \limits_{𝑚=1}^{𝑀}𝛼_𝑚𝐺_𝑚(𝑥_𝑖))$ \qquad (1.0) \]

其中 \((𝑥_1,𝑦_1),(𝑥_2,𝑦_2),⋯(𝑥_𝑁,𝑦_𝑁)\)为训练数据集。

这是个复杂的优化问题,前向分步算法(forward stagewise algorithm)求解这一优化问题的想法是:因为学习的是加法模型,如果能够从前向后,每一步只学习一个基函数及其系数,逐步逼近优化目标函数式(1.0),即假设在第m次迭代中,前m-1次的系数\(\alpha\) 和基学习器\(𝐺(𝑥)\)都是固定的,则\(𝑓_𝑚(𝑥)=𝑓_{𝑚−1}(𝑥)+𝛼_𝑚𝐺_𝑚(𝑥)\),这样在第m步我们只需就当前的\(𝛼_𝑚\)\(𝐺_𝑚(𝑥)\)最小化损失函数,即

\[\min \limits_{(𝛼_𝑚,𝐺_𝑚)} \sum \limits_{i=1}^{N}𝐿(𝑦_𝑖, f_{m-1} + 𝛼_𝑚𝐺_𝑚(𝑥_𝑖)) \qquad (1.1) \]

如果损失函数选用平方差损失,形式如下:

\[L(y, f(x)) = (y - f(x))^2 \]

则式(1.1)为:

\[\begin{align*} 𝐿(𝑦_𝑖, f_{m-1} + 𝛼_𝑚𝐺_𝑚(𝑥_𝑖)) &= (y_i - f_{m-1} - 𝛼_𝑚𝐺_𝑚(𝑥_𝑖))^2 \\ &= (rim - 𝛼_𝑚𝐺_𝑚(𝑥_𝑖))^2 \end{align*} \]

其中\(rim = y_i - f_{m-1}\)是当前模型拟合数据的残差(residual)。

AdaBoost采用的损失函数为指数损失,形式如下:

\[L(y, f(x)) = e^{-yf(x)} \qquad (1.2) \]

结合上文,我们现在的目标是在指数函数最小的情况下求得\(𝛼_𝑚\)\(𝐺_𝑚(𝑥)\),即:

\[(\alpha_m, G_m(x)) = arg \ \min \limits_{(\alpha, G)} \sum \limits_{i=1}^{N} e^{- y_i f_m(x_i)} = arg \ \min \limits_{(\alpha, G)} \sum \limits_{i=1}^{N} e^{- y_i (f_{m-1}(x_i) + \alpha G(x_i))} \qquad (1.3) \]

第m步训练添加分类器\(G_m\),并计算相应的系数\(\alpha_m\)。设 \(w_i^{(m)} = e^{- y_i f_{m-1} (x_i)}\), 由于\(w_i^{(m)}\)不依赖于\(𝛼_𝑚\)\(𝐺_𝑚(𝑥)\),所以可认为其是第m步训练之前赋予每个样本的权重,然而\(w_i^{(m)}\)依赖于\(𝑓_{𝑚−1}(𝑥_𝑖)\),所以每一轮迭代会改变。

于是式(1.3)变为:

\[\begin{align*} \sum \limits_{i=1}^{N} w_i^{(m)} e^{- y_i \alpha G(x_i)} &= e^{- \alpha} \sum \limits_{y_i = G(x_i)} w_i^{(m)} + e^{\alpha} \sum \limits_{y_i \neq G(x_i)} w_i^{(m)} \qquad (1.4) \\ &= (e^{\alpha} - e^{-\alpha}) \sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))+ e^{-\alpha} \sum \limits_{i=1}^{N} w_i^{(m)} \qquad (1.5) \end{align*} \]

刚刚我们说到,\(w_i^{(m)}\)不依赖于\(𝛼_𝑚\)\(𝐺_𝑚(𝑥)\),对于求解式(1.3),我们分为两步:首先,对任意\(\alpha>0\),由式(1.5)可以看出\(G_m(x)\)必须满足最小化带权重误差才能使损失最小化,否则对于相同的\(\alpha\),损失总是可以更小,由此推导出基学习器\(𝐺_𝑚(𝑥)\):

\[G_m(x) = arg \ \min \limits_{G} \sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i)) \]

3.2 分类器\(G_m(x)\)的系数$\alpha_m $ 从何而来

3.1节中我们已经求解了\(G_m(x)\),现在我们开始求解$\alpha_m $:

\[\alpha_m = arg \ \min \limits_{\alpha} (e^{\alpha} - e^{-\alpha}) \sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))+ e^{-\alpha} \sum \limits_{i=1}^{N} w_i^{(m)} \]

为了得到最小化上式的\(\alpha\),我们对\(\alpha\)求导并让其等于0,得到:

\[(e^{\alpha} + e^{-\alpha}) \sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))- e^{-\alpha} \sum \limits_{i=1}^{N} w_i^{(m)} = 0 \\ 两边同乘 e^{\alpha},得到:\quad (e^{2\alpha} + 1) \sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))= \sum \limits_{i=1}^{N} w_i^{(m)} \\ e^{2\alpha} = \frac{\sum \limits_{i=1}^{N} w_i^{(m)}}{\sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))} - 1 \]

设 $err_m = \frac{\sum \limits_{i=1}^{N} w_i^{(m)} 𝕀(y_i \neq G(x_i))}{\sum \limits_{i=1}^{N} w_i^{(m)}} $(和AdaBoost.M1算法中计算的带权误差率相同),因此我们有:

\[e^{2\alpha} = \frac{1}{err_m} - 1 = \frac{1 - err_m}{err_m} \\ 则, \alpha = \frac{1}{2} log \frac{1 - err_m}{err_m} \]

可以看出, \(err_m\)越小,最后得到的\(\alpha_m\)就越大,表明在最后的线性组合中,准确率越高的基学习器会被赋予较高的权重。

3.3 样本权重\(w\)从何而来?

\[\begin{align*} w_i^{(m+1)} &= e^{- y_i f_{m} (x_i)} \\ &= e^{- y_i (f_{m-1} (x_i) + \alpha_m G_m(x_i))} \\ &= e^{- y_i f_{m-1} (x_i)} e^{-y_i \alpha_m G_m(x_i))} \\ &= w_i^{(m)} e^{-y_i \alpha_m G_m(x_i))} \end{align*} \]

可以看到对于\(\alpha_m>0\),若\(𝑦_𝑖=𝐺_𝑚(𝑥_𝑖)\),则\(w_i^{(m+1)} = w_i^{(m)} e^{-\alpha_m}\),表明前一轮被正确分类样本的权值会减小; 若\(𝑦_𝑖 \neq 𝐺_𝑚(𝑥_𝑖)\),则\(w_i^{(m+1)} = w_i^{(m)} e^{\alpha_m}\),表明前一轮误分类样本的权值会增大。

3.4 指数损失 (Exponential Loss)

由于AdaBoost使用指数损失,将指数损失表示为期望的形式:

\[E(e^{-yf(x)}|x) = P(y=1|x)e^{-f(x)} + P(y=-1|x)e^{f(x)} \]

由于是最小化指数损失,则将上式求导并令其为0:

\[\frac{\partial E(e^{-yf(x)}|x)}{\partial f(x)} = - P(y=1|x)e^{-f(x)} + P(y=-1|x)e^{f(x)} = 0 \]

我们可以得到:

\[f(x) = \frac{1}{2} log \frac{P(y=1|x)}{P(y=-1|x)} \qquad 或者写成 \\ P(y=1|x) = \frac{1}{1 + e^{- 2 f(x) }}\quad , \quad P(y=-1|x) = \frac{e^{- f(x)}}{e^{- f(x)} + e^{ f(x) }} \]

与logistic regression只差系数\(\frac{1}{2}\),因此每一轮最小化指数损失其实就是在训练一个logistic regression模型。

因此,有

\[\begin{align*} sign(f(x)) &= sign \big( \frac{1}{2} log \frac{P(y=1|x)}{P(y=-1|x)} \big) \\ &= \begin{cases} 1 & P(y=1|x) > P(y=-1|x) \\ -1 & P(y=-1|x) > P(y=1|x) \end{cases} \\ &= arg \ \max \limits_{y \in \{-1, 1\}} P(y|x) \end{align*} \]

这意味着\(𝑠𝑖𝑔𝑛(𝑓(𝑥))\)达到了贝叶斯最优错误率,即对于每个样本𝑥都选择后验概率最大的类别。若指数损失最小化,则分类错误率也将最小化。这说明指数损失函数是分类任务原本0-1损失函数的一致性替代函数。由于这个替代函数是单调连续可微函数,因此用它代替0-1损失函数作为优化目标。

3.5 Adaboost算法的性能比较

即使基学习器是一个非常简单的弱分类器,如stump,Adaboost也能能显著提升性能[13],如下图所示:

提升(boosting)单层决策树(stump)的预测误差(test error
rate)与迭代次数的关系,同时也展示了单层决策树本身(Single stump)和一棵有244个节点的树(244 Node Tree)的预测误差

假设有二分类问题,其中特征\(X_1,...,X_{10}\)是独立的标准正态分布,目标\(Y\)的真实分布如下:

\[y = \begin{cases} 1 & \mbox{if $\sum_{j=1}^{10} X_j^2 > \chi_{10}^2 (0.5)$ } \\ -1 & \mbox{otherwise} \end{cases} \]

,这里$ \chi_{10}^2 (0.5) = 9.34\(,若\)X \sim \chi_n^2\(,记\)P(X>c) = \alpha\(,自由度为\)n\(,则\)c = \chi_n^2 ( \alpha )\(称为\)\chi_n^2\(分布(chi-square distribution)的上\)\alpha$分位数。

训练集中2000个样本,每个类别约1000个,1000个测试样本。这里的基学习器是stump,如果仅使用这个弱分类器进行训练,在测试集上的误差率为45.8%,而随机猜测的误差率是50%。但是,随着迭代次数的增加,误差率逐渐减小,在迭代次数为400时,误差率达到5.8%。比一棵244个节点的决策树(误差率为24.7%)性能要好。

3.6 Real AdaBoost

论文[Friedman et al., 2000] [10]中的Discrete AdaBoost即是“AdaBoost.M1”,因为基学习器\(G_m(x)\)的输出是离散的类别标签{-1,1}。如果将基学习器的输出改为一个类别概率,则产生了Real AdaBoost。

Real AdaBoost的推导与Discrete AdaBoost类似,仍最小化指数损失:

\[\begin{align*} E(e^{-yf_m(x)}|x) &= E(e^{-y(f_{m-1}(x)+G(x))}|x) \\ &= E(w \cdot e^{-yG(x)}|x) \\ &= e^{-G(x)}P_w(y=1|x) + e^{G(x)}P_w(y=-1|x) \\ \end{align*} \]

其中\(𝑃_𝑤(𝑦=1|𝑥)\)是样本权重为𝑤下y=1的概率,对𝐺(𝑥)求导,得到:

\[\begin{align*} -e^{-G(x)}P_w(y=1|x) + e^{G(x)}P_w(y=-1|x)=0 \end{align*} \]

则得到\(G(x)\):

\[G(x) = \frac{1}{2} log \frac{P_w(y=1|x)}{P_w(y=-1|x)} \]

由此Real AdaBoost的算法流程如下:

  1. 初始化权重分布:\(w_i^{(1)} = \frac{1}{N}\)\(i=1,2...N\)
  2. for m=1 to M:

(a)使用带有权重分布的训练基学习器,得到类别概率\(P_m(x) = P_w(y=1|x) \in [0,1]\)

(b)\(G_m(x) = \frac{1}{2} log \frac{P_m(x)}{1-P_m(x)} \in R\)

(c)更新权重分布:\(w_i^{(m+1)} = \frac{w_i^{(m)} \cdot e^{-y_i G_m(x_i)}}{Z^{m}}\)\(i=1,2,...N\)

  1. 输出最终模型:\(G(x) = sign \bigg( \sum \limits_{m=1}^{M}G_m(x) \bigg)\)

3.7 从模拟实验看基学习器\(G (x)\)的选择

我们在上面的讨论中基本都使用了stump(两叶子结点决策树)作为基学习器,小伙伴们会不会有疑问,能不能用更复杂的决策树来作为基学习器?可以,使用stump还是更复杂决策树作为基学习器要看情况~论文[Friedman et al., 2000] [10]第6部分做了两个实验向我们展示对这个问题的研究。

我们先来看第一个实验:样本有10个特征,是从10-维标准正态分布\(x\sim N^{10}(0, I)\)随机采集而来,相对原点的平方半径定义为:

\[r^2 = \sum \limits_{j=1}^{10}x_{j}^2 \]

\(r^2\)进行阈值处理作为分隔连续类别的决策边界,共划分成K个类别,这些类别的决策边界(K-1个)构成不同半径的嵌套的10-维同心球体,每个类别\(C_k(1 \leq k \leq K)\)是样本的一个子集:

\[C_k = \{x_i | t_{k-1} \leq r_i^2 \lt t_k \} \quad t_0=0, t_K=\infty \]

\(\{t_k\}_1^{K-1}\)的选择使每个类别的样本数量大致相同,训练集样本总量\(N = K \cdot 1000\),因此每个类别约有1000个训练样本,2-维举例如下:

image-20190910103622466

再独立采集10000个样本作为测试集来评估训练集误差率。将10个这样的独立抽取的训练-测试集组合的平均结果用于最终误差率的估计。实验结果如下图所示:

注:LogitBoost和Gentle AdaBoost是AdaBoost两个变体,这里暂不讨论,只关注Discrete AdaBoost和Real AdaBoost。Discrete AdaBoost简称DAB(黑色实线),Real AdaBoost简称RAB(红色点线),图中显示的是boosting迭代次数和对应的错误率。

image-20190906140045552

左上:使用stump作为基分类器,有两个类别(K=2),DAB在这里表现出相当差的性能,在所有迭代中大约是其他算法错误率的两倍。

左下和右下:同样使用stump作为基分类器,但分别是3个类别(K=3)和5个类别(K=5),从测试错误率可以看出问题变得越来越复杂,但同样的是DAB表现出较差的性能。

右上:使用8-叶子结点的决策树作为基分类器,解决二分类(K=2)问题,和左上图做对比,所有算法开始时8-叶子结点决策树的误差率随着迭代次数增加下降的比stump快,这块主要因为一颗8-叶子结点决策树相当于4个stump,但当迭代次数超过100之后误差率下降的非常缓慢。DAB的性能提升了很多,基本与其他算法相当。最终在迭代次数为800时使用8-叶子结点决策树作为基学习器的误差率(0.072)比使用stump作为基学习器误差率(0.054)要高33%。

使用复杂4倍的决策树作为基学习器性能反而变差了,为什么呢?我们先来看下分隔两个类别的贝叶斯决策边界(Bayes decision boundary)是集合:

\[\big\{ x : log \frac{P(y=1|x)}{P(y=-1|x)} = 0 \big\} \qquad 或简写为 \ \{x:B(x)=0 \} \]

为了近似这个集合,尽可能接近地估计logit \(B(x)\)􏴹或\(B(x)\)􏴹的任何单调变换就足够了。我们已经知道,boosting最终会生成一个加法(additive)逻辑模型,其成员函数用基学习器来表示。当选用stump作为基学习器时,假设第m个stump选择在第\(j\)个特征划分,分裂点是\(t_m\),成员函数有如下形式:

\[\begin{align*} f_m(x) &= c_m^L1_{[x_j \leq t_m]} + c_m^R1_{[x_j \gt t_m]} \\ &=f_m(x_j) \end{align*} \]

其中\(c_m^L\)\(c_m^R\) 表示左右两个叶子结点响应的加权平均值,所以通过提升stump产生的模型可以表示成特征的加法形式:

\[F(x) = \sum \limits_{j=1}^{p}g_j(x_j) \]

其中,\(g_j(x_j)\)是所有stump中涉及\(x_j\)(如果stump中不存在\(x_j\)则为0)的加和,p为特征数量。

上面实验说明对于上面数据样本的最佳决策边界在原始特征上也是加法的(addtive),因此,决策树中stump是最理想的,不需要复杂的树,除非复杂的树的所有分裂点都是在一个变量上,这样也能生成原始特征的加法模型(additive model),不过因为boosting用的是贪婪分步策略(greedy stagewise strategy),每颗树都尽可能包含更多重要的变量,因此这种情况不大可能出现。由于决策树的自身特点,包含多个变量就会产生交叉影响(interaction effects)的模型,这种非加法(non addtitive)的模型不太适合上例中加法(additive)的决策边界,因此会像导致上图右上中误差率的增加。

上面的讨论还表明,如果在预测变量中分隔两个类别的决策边界本质上是非加法的,那么使用stump不如使用更复杂的树。 m-叶子节点的树可以产生最大的交叉阶数(interaction order)为\(min􏴸(m-1,􏴴p)\)􏴹的基函数,其中p是特征的数量。这些高阶基函数往往能更准确地估计高阶交叉影响的决策边界\(\{x:B(x)=0 \}\)。 我们来看下一个例子。 有两个类􏴸(K=2)􏴹和5000个训练样本,这些训练样本与之前的例子一样是从10维标准正态分布中采集而来,如前面的例子所示。 使用对数概率将类标签随机分配给每个样本:

\[log \big( \frac{P(y=1|x)}{P(y=-1|x)} \big)=10 \sum \limits_{j=1}^{6}x_j \big( 1+\sum \limits_{l=1}^{6}(-1)^lx_l \big) \]

每个类别的样本数量大致相等,贝叶斯错误率为0.046。 该问题的决策边界是前六个预测变量的复杂函数,同时涉及所有这些变量在相等强度的二阶交叉作用,2-维举例如下:

image-20190910103547033

与前一个例子相同,使用10,000个观测值的测试集来估计每个训练集的误差率,最终估计值是10次重复的平均值。实验结果如下图所示:

image-20190908174328604

左上:总的来说,所有boosting算法中没有一个能够很好地解决这个问题,最好的错误率为0.35;

右上:boosting算法使用了4-叶子结点的决策树,性能有了很大的提升;

左下:图中显示了使用8-叶子结点的决策树作为基学习器,性能得到进一步提升。

虽然实验范围有限,但从这些模拟研究中也可以得出一些结论:决策边界\(B(x)\)􏴹如果可以通过原始特征的加法函数来近似逼近,使用stump有时可以优于使用更复杂的决策树;当需要更高阶的相互作用时,stump表现出差的性能,可以尝试更复杂的决策树。 总的来说,boosting算法的性能取决于决策边界的性质,基学习器的复杂性和迭代的数量等几方面问题。

3.8 Discrete AdaBoost 与Real AdaBoost性能

论文[Friedman et al., 2000] [10]的第7节通过一些真实数据集来比较不同boosting算法之间的性能差异,数据集如下:

image-20190909130702464

较大数据集能够使性能差异表现得更明显,如下图所示,同样我们暂时只关注Discrete AdaBoost 与Real AdaBoost, 对于Satimage数据集,8-叶子节点树模型略微比stump更准确。 而对于Letter数据则毋庸置疑,使用stump作为基学习器显然是不够的。 同样是8-叶子节点树的情况下,几种boosting算法之间没有明显的区别。 对于stump,Real AdaBoost明显优于Discrete AdaBoost。 这与上节模拟研究的结果一致。

image-20190909132105783

此外,博客[16]融合了Weight Trimming和Early stopping做实验比较了Discrete AdaBoost 与Real AdaBoost,也值得参考和学习。

3.9 Weight Trimming

[Friedman et al., 2000] 第9节讲到Weight trimming,主要目的是在不牺牲精确度的情况下减少计算量,提高训练速度。在AdaBoost的每一轮基学习器训练过程中,只有小部分样本的权重较大,因而能产生较大的影响,而其他大部分权重小的样本则对训练影响甚微。Weight trimming的思想是每一轮迭代中删除那些低权重的样本,只用高权重样本进行训练。具体是设定一个阈值 (比如90%或99%),再将所有样本按权重排序,计算权重的累积和,累积和大于阈值的权重 (样本) 被舍弃,不会用于训练。注意每个基学习器虽然只用一部分样本进行训练,但会使用全部样本进行预测,因此每一轮训练完成后所有样本的权重依然会被重新计算,这意味着之前被舍弃的样本在之后的迭代中如果权重增加,可能会重新用于训练。

3.10 Early stopping

massquantity的博客[16]中还提到Early stopping:将数据集划分为训练集和测试集,在训练过程中不断检查在测试集上的表现,如果测试集上的准确率下降到一定阈值之下,则停止训练,选用当前的迭代次数M,这同样是防止过拟合的手段。

参考资料:

[1] Robert E. Schapire, explaining-adaboost

[2] L. G. Valiant. A theory of the learnable. Communications of the ACM, 27(11):1134–1142, November 1984.

[3] j2kun, probably-approximately-correct-a-formal-theory-of-learning, January, 2014

[4] Michael Kearns(1988); Thoughts on Hypothesis Boosting, Unpublished manuscript (Machine Learning class project, December 1988)

[5] Michael Kearns,Leslie Valiant(1989). Crytographic limitations on learning Boolean formulae and finite automata. Symposium on Theory of Computing. 21. ACM. pp. 433–444. doi:10.1145/73007.73049. ISBN 978-0897913072.

[6] Schapire, Robert E. (1990). "The Strength of Weak Learnability" (PDF). Machine Learning. 5 (2): 197–227. CiteSeerX 10.1.1.20.723. doi:10.1007/bf00116037. Archived from the original (PDF) on 2012-10-10. Retrieved 2012-08-23.

[7] Yoav Freund. Boosting a weak learning algorithm by majority. Information and Computation,121(2):256–285, 1995.

[8] Harris Drucker, Robert Schapire, and Patrice Simard. Boosting performance in neural networks. International Journal of Pattern Recognition and Artificial Intelligence, 7(4):705–719, 1993.

[9] Yoav Freund and Robert E. Schapire. A decision-theoretic generalization of on-line learning and an application to boosting. Journal of Computer and System Sciences, 55(1):119–139, August 1997.

[10]Jerome Friedman, Trevor Hastie and Robert Tibshirani. (2000). "Additive logistic regression: A statistical view of boosting (with discussion)". Annals of Statistics, 28(2):337-407

[11] 周志华. (2016). "机器学习",清华大学出版社,171-196

[12] Joseph Rocca, Ensemble methods: bagging, boosting and stacking,Understanding the key concepts of ensemble learning.

[13] Trevor Hastie, Robert Tibshirani, Jerome Friedman(2009). The Elements of Statistical Learning: Data Mining, Inference, and Prediction (2nd Ed.), Springer, New York, 337-388

[14] Just Chillin’ , Machine Learning (6) Boosting: AdaBoost and Forward Stagewise Additive Modeling

[15] Jack Cui, 机器学习实战教程(十):提升分类器性能利器-AdaBoost

[16] massquantity,集成学习之Boosting —— AdaBoost原理集成学习之Boosting —— AdaBoost实现