一. AdaBoost介绍
我们在机器学习(八)-集成学习(Ensemble learning)中介绍了集成学习的应用场景,之后介绍了集成学习获得一组成员模型的学习算法,以及对成员模型决策结果的组合方法,其中就提到了Boosting。没错,Boosting是机器学习中集成学习家族中的一员,也属于监督学习,同时Boosting本身也是一族机器学习算法,其思想是通过组合许多相对较弱和不准确的规则来创建一个高度精确的预测规则[1]。
AdaBoost发展史
科学技术的发展是无数大牛努力的结果,我们来大致了解下AdaBoost的发展史吧~
1984年 Valiant 提出了PAC(probably approximately correct) learning[2][3],从而使计算机科学诞生了一个新的分支,即计算学习理论(computational learning theory),主要研究通过数学推理和论证更好地解答机器学习中的哲学问题,比如:“机器学习的学习是什么意思”,“为什么看起来简单的hypothesis可以很好地模拟现实问题”等。
随后,Kearns 和 Valiant (1988, 1989) [4][5]首次提出了 PAC学习模型中弱学习算法和强学习算法的等价性问题,即任意给定仅比随机猜测略好的弱学习算法 ,是否可以将其提升为强学习算法 ? 如果二者等价 ,那么只需找到一个比随机猜测略好的弱学习算法就可以将其提升为强学习算法 ,而不必寻找很难获得的强学习算法。
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等算法 ,在机器学习领域受到了极大的关注。
AdaBoost(Ada为Adaptive的简写)是一个具有里程碑意义的算法,适应性(adaptive)是指后续的弱学习器为了支持先前分类器分类错误的实例而进行调整。考虑一个二分类问题,输出编码为,给定特征向量,分类器预测出其中之一。现有个训练样本,每个训练样本表示为,那么训练集的误差率为:
AdaBoost算法的每一次迭代都会为训练样本赋予新的权重。最开始的训练样本权重相同,均为,所以第一次迭代相当于在数据原有的样子上训练分类器,而在接下来的迭代中,训练集中的每一个样本权重都可能被修改,而分类算法会在已修改权重的训练样本上训练下一个分类器。当迭代次数为时,那些在上一次迭代中被分类器分类错误的样本的权重会被增加,而分类正确的样本权重会减小,这样随着迭代的进行,难以正确分类的样本会被赋予越来越大的权重,迫使接下来的分类器越来越关注这些在之前错误分类的样本,如下图所示:
算法生成一系列弱分类器之后,将这些弱分类器的进行加权组合,得到最终分类器:
其中是根据的错误率计算出的,表示的权重,准确度越高权重越大,对整体预测的影响也越大。我们来看一下Adaboost.M1的算法伪代码:
算法在2(a)行对加权训练样本引入分类器; 2(b)行计算加权误差率;2(c)行计算分类器的权重; 2(d)行用2(c)行中计算的更新每个样本的权重。 用因子放大分类器错误分类的样本权重,增加它们在下一个迭代中引入分类器时的影响力。
看算法介绍之后,有的小伙伴仍然感到懵圈,没关系,我们在第二节会用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选了一个相对较好的弱分类器,dim_0 <= 1.3为-1.0,即橙色,dim_0 > 1.3为1.0,即蓝色,根据权重的计算方法,算出。此时,我们可以看到点[1., 2.1]被错误分类为橙色,因此增加权重;
如上图右下所示,首先我们在dim_1继续选了一个弱分类器,dim_1 <= 1.0为-1.0,即橙色,dim_0 > 1.0为1.0,即蓝色,根据权重的计算方法,算出。此时,我们可以看到点[2.0, 1.0]被错误分类为橙色,因此增加权重;
如上图左下所示,首先我们在dim_0继续选了一个弱分类器,能够将之前分错的两个点都能正确分类,dim_0 <= 0.9为-1.0,即橙色,dim_0 > 0.9为1.0,即蓝色,根据权重的计算方法,算出。
此时我们获得了个弱分类器:
那么使用上面三个弱分类器预测一下三个绿色的点所属类别吧,如下图所示:
点1:(-0.693) + (-0.973) + (-0.896) = -2.562毫无疑问为橙色点;
点2:(-0.693) + 0.973 + 0.896 = 1.176 为蓝色点;
点3:0.693 + 0.973 + 0.896 = 2.562毫无疑问为蓝色点;
二. 使用python实现Adaboost算法
上一节中我们了解了Adaboost算法的原理,本小节我们使用python来实现Adaboost算法,并进行预测。上一节最后提到我们选用的弱分类器是单层决策树(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)
运行结果如下:
训练数据集有两个特征(两个坐标轴),由于我们选用的若分类器是单层决策树,可以看到,如果想要试着从某个坐标轴(特征)上选择一个值(即选择一条与坐标轴平行的直线)来将所有的蓝色圆点和橘色圆点分开,这显然是不可能的。这就是单层决策树难以处理的一个著名问题。但通过使用多颗单层决策树,我们可以构建出一个能够对该数据集完全正确分类的分类器。
2.2 构建单层决策树
我们设置一个分类阈值,比如我横向切分,如下图所示:
蓝横线上边的是一个类别,蓝横线下边是一个类别。显然,此时有一个蓝点分类错误,计算此时的分类误差,误差为1/5 = 0.2。这个横线与坐标轴的y轴的交点,就是我们设置的阈值,通过不断改变阈值的大小,找到使单层决策树的分类误差最小的阈值。同理,竖线也是如此,找到最佳分类的阈值,就找到了最佳单层决策树,编写代码如下:
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. 构建单层决策树
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算法是如何实现的。
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,程序终止运行。
最后训练结果包含了三个弱分类器,其中包含了分类所需要的所有信息。一共迭代了3次,所以训练了3个弱分类器构成一个使用AdaBoost算法优化过的分类器,分类器的错误率为0。
一旦拥有了多个弱分类器以及其对应的alpha值,进行测试就变得想当容易了。
2.4 使用AdaBoost进行预测
给定两个测试样本,使用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))
预测结果如图所示:
代码很简单,在之前代码的基础上,添加adaClassify()函数,该函数遍历所有训练得到的弱分类器,利用单层决策树,输出的类别估计值乘以该单层决策树的分类器权重alpha,然后累加到aggClassEst上,最后通过sign函数最终的结果。可以看到,分类没有问题,(5,5)属于正类,(0,0)属于负类。
三. Adaboost算法的理解
Over the years, a great variety of attempts have been made to “explain” AdaBoost as a learning algorithm, that is, to understand why it works, how it works, and when it works (or fails).
对Adaboost算法的多种解释,从统计学的角度或者说损失最小化的角度来解释Adaboost
http://rob.schapire.net/papers/explaining-adaboost.pdf
Adaboost.M1算法在Friedman等人的论文中称为“Discrete AdaBoost”(2000,Additive Logistic Regression: A Statistical View Of Boosting),因为成员分类器的输出是离散的类别标签。如果成员分类器返回实数的预测值(比如返回[-1,1]之间的概率),AdaBoost也可以做相应的调整,见“Real AdaBoost”(2000,Additive Logistic Regression: A Statistical View Of Boosting)。
3.1 Adaboost算法的性能比较
即使是一个非常简单的若分类器,Adaboost也能能显著提升性能,如下图所示:
假设有二分类问题,其中特征是独立的标准正太分布,目标的真实分布如下:
注,这里,若,记,自由度为,则称为分布(chi-square distribution)的上分位数。
训练集中2000个样本,每个类别约1000个,1000个测试样本。这里的弱分类器是单层决策树(decision stump)。如果仅使用这个弱分类器进行训练,在测试集上的误差率为45.8%,而随机猜测的误差率是50%。但是,随着迭代次数的增加,误差率逐渐减小,在迭代次数为400时,误差率达到5.8%。比一棵244个节点的决策树(误差率为24.7%)性能要好。
3.2 Forward Stagewise Additive Modeling
3.3 指数损失函数
参考资料:
[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] Michael J. Kearns and Umesh V. Vazirani. An Introduction to Computational Learning Theory. MIT Press, 1994.
[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.
Claude Sammut, Geoffrey I. Webb (Eds.),Encyclopedia of Machine Learning, Springer Press 2010, Boosting
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
周志华. (2016). "机器学习",清华大学出版社,171-196
Joseph Rocca, Ensemble methods: bagging, boosting and stacking,Understanding the key concepts of ensemble learning.
Trevor Hastie, Robert Tibshirani, Jerome Friedman(2009). The Elements of Statistical Learning: Data Mining, Inference, and Prediction (2nd Ed.), Springer, New York, 337-388
Just Chillin’ , Machine Learning (6) Boosting: AdaBoost and Forward Stagewise Additive Modeling
Peter Harrington(2012), Machine Learning in Action, Manning, New York, 129-150
Jack Cui, 机器学习实战教程(十):提升分类器性能利器-AdaBoost
AdaBoost的具体流程:先对每个样本赋予相同的初始权重,每一轮学习器训练过后都会根据其表现对每个样本的权重进行调整,增加分类错误样本实例的权重,使先前分类错误的样本在后续得到更多关注,按这样的过程重复训练出个学习器,最后进行加权组合得到最终分类器,就可以用于预测了。