1. 概述
1.1 集成学习
目前存在各种各样的机器学习算法,例如SVM、决策树、感知机等等。但是实际应用中,或者说在打比赛时,成绩较好的队伍几乎都用了集成学习(ensemble learning)的方法。集成学习的思想,简单来讲,就是“三个臭皮匠顶个诸葛亮”。集成学习通过结合多个学习器(例如同种算法但是参数不同,或者不同算法),一般会获得比任意单个学习器都要好的性能,尤其是在这些学习器都是"弱学习器"的时候提升效果会很明显。
弱学习器指的是性能不太好的学习器,比如一个准确率略微超过50%的二分类器。
下面看看西瓜书对此做的一个简单理论分析。
考虑一个二分类问题、真实函数
以及
个相互独立且犯错概率均为
的个体学习器(或者称基学习器)
。我们用简单的投票进行集成学习,即分类结果取半数以上的基学习器的结果:
由Hoeffding不等式知,集成学习后的犯错(即过半数基学习器犯错)概率满足
式指出,当犯错概率独立的基学习器个数
很大时,集成后的犯错概率接近0,这也很符合直观想法: 大多数人同时犯错的概率是比较低的。
就如上面加粗字体强调的,以上推论全部建立在基学习器犯错相互独立的情况下,但实际中这些学习器不可能相互独立,而如何让基学习器变得“相对独立一些”,也即增加这些基学习器的多样性,正是集成学习需要考虑的主要问题。
按照每个基学习器之间是否存在依赖关系可以将集成学习分为两类:
- 基学习器之间存在强依赖关系,一系列基学习器需要串行生成,代表算法是Boosting;
- 基学习器之间不存在强依赖关系,一系列基学习器可并行生成,代表算法是Bagging和随机森林。
Boosting系列算法里最著名算法主要有AdaBoost和提升树(Boosting tree)系列算法,本文只介绍最具代表性的AdaBoost。提升树、Bagging以及随机森林不在本文介绍范围内,有时间了再另外介绍。
1.2 Boosting
Boosting指的是一类集成方法,其主要思想就是将弱的基学习器提升(boost)为强学习器。具体步骤如下:
- 先用每个样本权重相等的训练集训练一个初始的基学习器;
- 根据上轮得到的学习器对训练集的预测表现情况调整训练集中的样本权重(例如提高被错分类的样本的权重使之在下轮训练中得到更多的关注), 然后据此训练一个新的基学习器;
- 重复2直到得到
个基学习器,最终的集成结果是
个基学习器的组合。
由此看出,Boosting算法是一个串行的过程。
Boosting算法簇中最著名的就是AdaBoost,下文将会详细介绍。
2. AdaBoost原理
2.1 基本思想
对于1.2节所述的Boosting算法步骤,需要回答两个问题:
- 如何调整每一轮的训练集中的样本权重?
- 如何将得到的
个学习器组合成最终的学习器?
AdaBoost(Adaptive Boosting, 自适应增强)算法采取的方法是:
- 提高上一轮被错误分类的样本的权值,降低被正确分类的样本的权值;
- 线性加权求和。误差率小的基学习器拥有较大的权值,误差率大的基学习器拥有较小的权值。
下面先给出AdaBoost算法具体实现步骤,至于算法解释(为什么要这样做)将在下一大节阐述。
2.2 算法步骤
考虑如下形式的二分类(标准AdaBoost算法只适用于二分类任务)训练数据集:其中
是一个含有
个元素的列向量, 即
;
是标量,
。
Adaboost算法具体步骤如下:
- 初始化样本的权重
- 对
,重复以下操作得到
个基学习器:
(1) 按照样本权重分布训练数据得到第
个基学习器:
(2) 计算在加权训练数据集上的分类误差率:
上式中是指示函数,考虑更加周全的AdaBoost算法在这一步还应该判断是否满足基本条件(例如生成的基学习器是否比随机猜测好), 如果不满足,则当前基学习器被抛弃,学习过程提前终止。
(3) 计算的系数(即最终集成使用的的基学习器的权重):
(4) 更新训练样本的权重,其中是规范化因子,目的是为了使
的所有元素和为1。
- 构建最终的分类器线性组合
得到最终的分类器为
由式知,当基学习器
的误差率
时,
,并且
随着
的减小而增大,即分类误差率越小的基学习器在最终集成时占比也越大。即AdaBoost能够适应各个弱分类器的训练误差率,这也是它的名称中"适应性(Adaptive)"的由来。
由式知, 被基学习器
误分类的样本权值得以扩大,而被正确分类的样本的权值被得以缩小。
需要注意的是式中所有的
的和并不为1(因为没有做一个softmax操作),
的符号决定了所预测的类,其绝对值代表了分类的确信度。
3. AdaBoost算法解释
有没有想过为什么AdaBoost算法长上面这个样子,例如为什么要用式
那样计算?本节将探讨这个问题。
3.1 前向分步算法
在解释AdaBoost算法之前,先来看看前向分步算法。就以AdaBoost算法的最终模型表达式为例:
可以看到这是一个“加性模型(additive model)”。我们希望这个模型在训练集上的经验误差最小,即
通常这是一个复杂的优化问题。前向分步算法求解这一优化问题的思想就是: 因为最终模型是一个加性模型,如果能从前往后,每一步只学习一个基学习器及其权重
, 不断迭代得到最终的模型,那么就可以简化问题复杂度。具体的,当我们经过
轮迭代得到了最优模型
时,因为
所以此轮优化目标就为
求解上式即可得到第
个基分类器
及其权重
。
这样,前向分步算法就通过不断迭代求得了从到
的所有基分类器及其权重,问题得到了解决。
3.2 AdaBoost算法证明
上一小结介绍的前向分步算法逐一学习基学习器,这一过程也即AdaBoost算法逐一学习基学习器的过程。本节就证明前向分步算法的损失函数是指数损失函数(exponential loss function)时,AdaBoost学习的具体步骤就如2.2节所示。
指数损失函数即,指数损失函数是分类任务原本0/1损失函数的一致(consistent)替代损失函数(损失函数的上界,优化指数损失函数,等价于优化AdaBoost的损失函数)。由于指数损失函数有更好的数学性质,例如处处可微,所以我们用它替代0/1损失作为优化目标。
将指数损失函数代入式,优化目标就为
因为
与优化变量
和
无关,如果令
这个其实就是2.2节中归一化之前的权重
,那么式
等价于
我们分两步来求解式所示的优化问题的最优解
和
:
-
对任意的
, 求
:
上式将指数函数换成指示函数是因为前面说的指数损失函数和0/1损失函数是一致等价的。
式子所示的优化问题其实就是AdaBoost算法的基学习器的学习过程,即2.2节的步骤2(1),得到的
是使第
轮加权训练数据分类误差最小的基分类器。
-
求解
:
将式子中的目标函数展开
注:为了简洁,上式子中的
被略去了
,
被略去了下标
,下同;将上式对
求导并令导数为0,即
解得
其中,
是分类误差率:
如果式子
中的
归一化成和为1的话那么式
也就和2.2节式
一模一样了,进一步地也有上面的
也就是2.2节的
。
最后来看看每一轮样本权值的更新,由和
可得
如果将上式进行归一化成和为1的话就和与2.2节中
完全相同了。
如果某个样本被正确分类,那么ωm+1,i=ωm,iexp(-αm)/sum(ωm,iexp(-αm)),错误则为ωm+1,i=ωm,iexp(αm)/sum(ωm,iexp(αm))。
由此可见,2.2节所述的AdaBoost算法步骤是可以经过严密推导得来的。总结一下,本节推导有如下关键点:
- AdaBoost算法是一个加性模型,将其简化成前向分步算法求解;
- 将0/1损失函数用数学性质更好的指数损失函数替代(这里可以替换的原因是,(1/N)∑i=1...Nexp(yiƒ(xi)是损失函数(1/N)∑i=1...NI(yi!=G(xi))的上界,当G(xi)≠yi时,yi*f(xi)<0,因而exp(-yi*f(xi))≥1,可以用指数损失函数来代替原来的函数,便于运算。
一个例子
(下面的例子与解法来源于李航的《统计学习方法》)
例 给定如下表所示训练数据。假设个体学习器由x(输入)和y(输出)产生,其阈值v(判定正反例的分界线)使该分类器在训练数据集上分类误差率最低。(y=1为正例,y=-1为反例)
第一个个体学习器:
我们首先认为(i=1,2,…,10)的权重是一样的,即每一个数据同等重要。(权重是用来计算误差的)
(a)在权值分布为
的训练数据上,阈值v取2.5(红线)时分类误差率最低(此时x=6,7,8的数据被错分为反例,误差为它们的权重之和
=0.1+0.1+0.1=0.3,误差率小于
才有意义),故个体学习器为
(b)根据误差计算系数
=0.4236(公式:
,可以发现只有当
<
时,
>0,这样个体学习器才是有意义的)
(c)更新训练数据的权值分布(公式:,
,
是为了保证每次权值总和为1)
(通过指数损失函数调整权重,分类正确的降低权重(
和
同号则
,
),分类错误的增加权重):
(权重之和始终为1)
可以看到x=6,7,8的数据的权重变大了,而其他数据的权重降低了,这是希望能把之前经常分类错误(经常分类错误会出现权重不断变大)的数据能在下一个个体学习器分类正确(记住:权重是用来计算误差的,为了降低误差,选择阈值时会倾向把权重大的分类正确)
集成学习器(第一次集成,只有一个个体学习器)在训练数据集上有3个误分类点
第二个个体学习器:
(a)在权值分布为的训练数据上,阈值v取8.5时分类误差率最低(此时x=3,4,5的数据被错分为正例,误差为它们的权重之和
=0.07143+0.07143+0.07143=0.2143,误差率降低了!),故个体学习器为
(b)根据误差计算系数
(c)更新训练数据的权值分布(在的基础上调整
,分类正确的降低权重,分类错误的增加权重):
对比可以看到x=3,4,5的数据的权重变大了,而其他权重降低了。
,
(注意:x<2.5时,也<8.5)
分类器在训练数据集上有3个误分类点
第三个个体学习器:
(a)在权值分布为的训练数据上,阈值v取5.5时分类误差率最低(
=0.1820,误差率又降低了!x=0,1,2,9被分类错误),故个体学习器为
(b)根据误差计算系数
(c)更新训练数据的权值分布:
(自己算一算吧)
最终结果:
分类器在训练数据集上有0个误分类点(amazing!)
代码如下所示:
#!/usr/bin/env python # -*- coding: utf-8 -*- from numpy import * # 模拟创建数据集 def loadSimpData(): datMat = mat([[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 # 通过阈值比较对数据进行分类 def stumpClassify(dataMatrix, dimen, threshVal, threshIneq): """ Function: 通过阈值比较对数据进行分类 Input: dataMatrix:数据集 dimen:数据集列数 threshVal:阈值 threshIneq:比较方式:lt,gt Output: retArray:分类结果 """ #新建一个数组用于存放分类结果,初始化都为1 retArray = ones((shape(dataMatrix)[0],1)) #lt:小于,gt;大于;根据阈值进行分类,并将分类结果存储到retArray if threshIneq == 'lt': retArray[dataMatrix[:, dimen] <= threshVal] = -1.0 else: retArray[dataMatrix[:, dimen] > threshVal] = -1.0 #返回分类结果 return retArray # 找到最低错误率的单层决策树 def buildStump(dataArr, classLabels, D): """ Function: 找到最低错误率的单层决策树 Input: dataArr:数据集 classLabels:数据标签 D:权重向量 Output: bestStump:分类结果 minError:最小错误率 bestClasEst:最佳单层决策树 """ #初始化数据集和数据标签 dataMatrix = mat(dataArr); labelMat = mat(classLabels).T #获取行列值 m,n = shape(dataMatrix) #初始化步数,用于在特征的所有可能值上进行遍历 numSteps = 10.0 #初始化字典,用于存储给定权重向量D时所得到的最佳单层决策树的相关信息 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))) #如果预测结果和标签相同,则相应位置0 errArr[predictedVals == labelMat] = 0 #计算权值误差,这就是AdaBoost和分类器交互的地方 weightedError = D.T * errArr #打印输出所有的值 #print("split: dim %d, thresh %.2f, thresh ineqal: %s, the weighted error is %.3f" % (i, threshVal, inequal, weightedError)) #如果错误率低于minError,则将当前单层决策树设为最佳单层决策树,更新各项值 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): """ Function: 找到最低错误率的单层决策树 Input: dataArr:数据集 classLabels:数据标签 numIt:迭代次数 Output: weakClassArr:单层决策树列表 aggClassEst:类别估计值 """ #初始化列表,用来存放单层决策树的信息 weakClassArr = [] #获取数据集行数 m = shape(dataArr)[0] #初始化向量D每个值均为1/m,D包含每个数据点的权重 D = mat(ones((m,1))/m) #初始化列向量,记录每个数据点的类别估计累计值 aggClassEst = mat(zeros((m,1))) #开始迭代 for i in range(numIt): #利用buildStump()函数找到最佳的单层决策树 bestStump, error, classEst = buildStump(dataArr, classLabels, D) #print("D: ", D.T) #根据公式计算alpha的值,max(error, 1e-16)用来确保在没有错误时不会发生除零溢出 alpha = float(0.5 * log((1.0 - error) / max(error, 1e-16))) #保存alpha的值 bestStump['alpha'] = alpha #填入数据到列表 weakClassArr.append(bestStump) #print("classEst: ", classEst.T) #为下一次迭代计算D expon = multiply(-1 * alpha * mat(classLabels).T, classEst) D = multiply(D, exp(expon)) D = D / D.sum() #累加类别估计值 aggClassEst += alpha * classEst #print("aggClassEst: ", aggClassEst.T) #计算错误率,aggClassEst本身是浮点数,需要通过sign来得到二分类结果 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 #return weakClassArr, aggClassEst # AdaBoost分类函数 def adaClassify(datToClass, classifierArr): """ Function: AdaBoost分类函数 Input: datToClass:待分类样例 classifierArr:多个弱分类器组成的数组 Output: sign(aggClassEst):分类结果 """ #初始化数据集 dataMatrix = mat(datToClass) #获得待分类样例个数 m = shape(dataMatrix)[0] #构建一个初始化为0的列向量,记录每个数据点的类别估计累计值 aggClassEst = mat(zeros((m,1))) #遍历每个弱分类器 for i in range(len(classifierArr)): #基于stumpClassify得到类别估计值 classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'], classifierArr[i]['thresh'], classifierArr[i]['ineq']) #累加类别估计值 aggClassEst += classifierArr[i]['alpha']*classEst #打印aggClassEst,以便我们了解其变化情况 #print(aggClassEst) #返回分类结果,aggClassEst大于0则返回+1,否则返回-1 return sign(aggClassEst) datMat, classLabels = loadSimpData() classifierArr = adaBoostTrainDS(datMat, classLabels, 30) print(classifierArr) print(adaClassify([0,0], classifierArr)) print(adaClassify([[5,5],[0,0]], classifierArr))