目录
1 集成算法简介
前面我们已经学了几种分类算法,但是往往单个分类器的效果并不是太好,这时就希望能够‘集思广益’,利用多个‘弱分类器’组成一个更大强大的分类器,这样分类效果就会更加可信、准确。
集成算法主要有以下三类:
1.Bagging
自举汇聚法(bootstrap aggregating),也称为bagging方法,是在从原始数据集选择S次后得到S个新数据集的一种技术。新数据集和原数据集的大小相等。每个数据集都是通过在原始数据集中随机选择一个样本来进行替换而得到的。这里的替换就意味着可以多次地选择同一样本。这一性质就允许新数据集中可以有重复的值,而原始数据集的某些值在新集合中则不再出现。 在S个数据集建好之后,将某个学习算法分别作用于每个数据集就得到了S个分类器。当我们要对新数据进行分类时,就可以应用这S个分类器进行分类。与此同时,选择分类器投票结果中多的类别作为后的分类结果。
2.boosting
boosting是一种与bagging很类似的技术。不论是在boosting还是bagging当中,所使用的多个分类器的类型都是一致的。但是在前者当中,不同的分类器是通过串行训练而获得的,每个新分类器都根据已训练出的分类器的性能来进行训练。boosting是通过集中关注被已有分类器错分的那些数据来获得新的分类器。
由于boosting分类的结果是基于所有分类器的加权求和结果的,因此boosting与bagging不太一 样。bagging中的分类器权重是相等的,而boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度。
3.Stacking
Stacking训练一个模型用于组合(combine)其他各个基模型。具体方法是把数据分成两部分,用其中一部分训练几个基模型,用另一部分数据测试这几个基模型,把基模型的输出作为输入,训练组合模型B。注意,它不是把模型的结果组织起来,而把模型组织起来。理论上,Stacking可以组织任何模型,实际中常使用单层logistic回归作为模型。
2 AdaBoost算法原理
AdaBoost是adaptive boosting(自适应boosting)的缩写,其运行过程如下:训练数据中的每个样本,并赋予其一个权重,这些权重构成了向量D。一开始,这些权重都初始化成相等值。首先在训练数据上训练出一个弱分类器并计算该分类器的错误率,然后在同一数据集上再次训练弱分类器。在分类器的第二次训练当中,将会重新调整每个样本的权重,其中第一次分对的样本的权重将会降低,而第一次分错的样本的权重将会提高。为了从所有弱分类器中得到终的分类结果,AdaBoost为每个分类器都分配了一个权重值alpha,这些alpha值是基于每个弱分类器的错误率进行计算的。其中,错误率ε的定义为:
而alpha的计算公式如下:
下面是AdaBoost算法的示意图:
左边是数据集,其中直方图的不同宽度表示每个样例上的不同权重。在经过一个分类器之后,加权的预测结果会通过三角形中的 alpha值进行加权。每个三角形中输出的加权结果在圆形中求和,从而得到终的输出结果。
3 AdaBoost算法流程
补充李航统计学习方法书中的算法流程:
假定输入一个二分类的数据集:
输入弱分类算法,输入迭代次数k,输出为-1和+1。
具体步骤如下:
1)初始化样本集权重为
即初始化权重是相同的,每个都是1/样本个数。
2) 对于m=1,2,…M(即迭代k次):
1.使用具有权重Dk的样本集来训练数据,得到基本分类器:
2.计算Gm(x)在训练数据集熵的分类误差率
即误差就是分类不正确的概率,就是分类不正确的样本权重和。这个公式不太好懂,我们再看一个:
这个公式可以看出,误差就是未分类正确的样本的权重之和。
3.根据当前弱分类器的误差,计算该分类器的权重:
这里的对数是自然对数。可以看出当该弱分类器的准确率(1-前面的误差)大于0.5,那么这个权重就是正值,且误差越小,权重越大,否则该权重为负值。所以分类误差越小的弱分类器在最终的分类器中权重越大。
4.更新训练集的权重分布
其中当分类正确时,真实结果和预测结果相同,成绩就是1,即y(Gm(x))=1;相反分类错误时,y(Gm(x))=-1。
所以也可以写成:
其中Zm是规范化因子,确保所有的数据权重总和为1:
5.构建基本分类器的线性组合
得到最终分类器:
线性组合f(x)实现了M个基本分类器的加权表决。系数α代表分类器的重要性,注意这里的α加和并不是1。f(x)的符号决定了x的分类,sign()函数实现了,如果f(x)大于0,分类结果为1,如果f(x)小于0.分类结果为-1。f(x)的绝对值表示分类的确信度。
4 Adaboost代码实现
4.1 构建弱分类器
本书中基于单层决策树构建弱分类器。单层决策树(decision stump,也称决策树桩)是一种简单的决策树。单层决策树也就是使用一个特征进行分类,值看一个特征,就决定分类结果。
先自己构建一个数据集:
import numpy as np
def loadSimpData ():
datMat = np.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
数据集是一个二维向量,还有classLabels是二分类结果。
我们看一下数据集的分布:
如果想要试着从某个坐标轴上选择一个值(即选择一条与坐标轴平行的直线)来将所有的圆形点和方形点分开,这显然是不可能的。这就是单层决策树难以处理的一个著名问题。通过使用多棵单层决策树,我们就可以构建出一个能够对该数据集完全正确分类的分类器。
有了数据,接下来就可以通过构建多个函数来建立单层决策树。 书中的单层决策树是非常简单的,只是为了举例,实际中并不太会用到。那么怎么建立呢?首先,我们需要遍历所有特征,看看哪个特征分类效果好;然后对于每个特征,如下图,在特征范围内划分不同的阈值,用来判断大于或小于某个阈值就分类为1,看哪个阈值分类效果好;然后就是决定,到底是大于这个阈值分类为1效果好,还是小于这个阈值分类效果好。所以要建立这个单层决策树,一共需要三层循环。
程序的伪代码如下:
构建决策树的函数如下:
def buildStump(dataArr,classLabels,D):
'''
构建单层决策树
dataArr:输入数据集
classLabels:数据集标签
D:数据集权重
'''
dataMatrix = np.mat(dataArr)
labelMat = np.mat(classLabels).T
m,n = dataMatrix.shape
numSteps = 10.0 # 要划分数据集为几份
bestStump = {} # 用来存储最佳单层决策树的信息
bestClasEst = np.mat(np.zeros((m,1))) # 最好的单层决策树的分类结果,都初始化为1
minError = np.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']: # 第三层循环:遍历大于阈值为1还是小于阈值为1
threshVal = (rangeMin + float(j) * stepSize) # 阈值
predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)#封装一个能够预测分类标签的函数,返回每个数据的分类结果
errArr = np.mat(np.ones((m,1))) # 初始化每个数据的误差为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 stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
'''
用于数据分类
dataMatrix:数据集
dimen:哪个特征
threshVal:阈值
threshIneq:大于还是小于为1
'''
retArray = np.ones((dataMatrix.shape[0],1)) # 初始化结果列表都为1
if threshIneq == 'lt': # 如果是大于
retArray[dataMatrix[:,dimen] <= threshVal] = -1.0 # 把小于阈值的分类结果改为-1
else:# 如果是小于
retArray[dataMatrix[:,dimen] > threshVal] = -1.0 # 把大于阈值的分类结果改为-1
return retArray
下面用我们构建的数据集测试一下:
# test1
dataMat, classLabels = loadSimpData ()
D = np.mat(np.ones((5,1))/5)
bestStump,minError,bestClasEst = buildStump(dataMat,classLabels,D)
print(bestStump)
print(minError)
# 结果如下
{'dim': 0, 'thresh': 1.3, 'ineq': 'lt'}
[[0.2]]
可以看出,最后构建的最佳单层决策树是使用第1个特征,阈值为1.3,大于阈值为+1。最后的总误差为0.2。
4.2 完整的AdaBoost算法实现
在上一节,我们构建了一个基于加权输入值进行决策的分类器。现在,我们拥有了实现一个完整AdaBoost算法所需要的所有信息。依据第三节的算法就成,我们需要不停的迭代重复那个流程,知道满足停止要求。
伪代码如下:
代码如下:
AdaBoost算法的输入参数包括数据集、类别标签以及迭代次数numIt,其中numIt是在整个 AdaBoost算法中唯一需要用户指定的参数。 我们假定迭代次数设为9,如果算法在第三次迭代之后错误率为0,那么就会退出迭代过程, 因此,此时就不需要执行所有的9次迭代过程。
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
'''
Adaboost算法流程
dataArr:数据集
classLabels:标签
numIt:迭代次数
'''
weakClassArr = [] # 存储每个分类器的信息
m = dataArr.shape[0] #获取数据集行数
D = np.mat(np.ones((m,1))/m) #初始化初始权重都为1/m
aggClassEst = np.mat(np.zeros((m,1))) # 累计估计值向量,也就是对应的第三节的第5步的f(x)的结果
for i in range(numIt):
bestStump,error,classEst = buildStump(dataArr,classLabels,D)#根据当前数据集,标签及权重建立最佳单层决策树
#计算当前模型的α,max(error,1e-16)的作用是防止错误率为0时程序报错
alpha = float(0.5*np.log((1.0-error)/max(error,1e-16)))
bestStump['alpha'] = alpha # 存储决策树的系数alpha到字典
weakClassArr.append(bestStump) #将该决策树存入列表
print("classEst: ",classEst.T) # 打印决策树的预测结果
expon = np.multiply(-1*alpha*np.mat(classLabels).T,classEst) #计算第三节的第4步e的指数项
D = np.multiply(D,np.exp(expon)) #更新权值向量
D = D/D.sum() # 使更新的样本的权重和为1,D.sum()就是第三节的第4步的Zm
#计算所有若分类器的错误率,如果错误率为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
return weakClassArr,aggClassEst
使用构造的数据集测试:
#test2
dataMat, classLabels = loadSimpData ()
weakClassArr,aggClassEst = adaBoostTrainDS(dataMat,classLabels,numIt=9)
print(weakClassArr)
print(aggClassEst)
# 结果如下:
classEst: [[-1. 1. -1. -1. 1.]]
aggClassEst: [[-0.69314718 0.69314718 -0.69314718 -0.69314718 0.69314718]]
total error: 0.2
classEst: [[ 1. 1. -1. -1. -1.]]
aggClassEst: [[ 0.27980789 1.66610226 -1.66610226 -1.66610226 -0.27980789]]
total error: 0.2
classEst: [[1. 1. 1. 1. 1.]]
aggClassEst: [[ 1.17568763 2.56198199 -0.77022252 -0.77022252 0.61607184]]
total error: 0.0
[{'dim': 0, 'thresh': 1.3, 'ineq': 'lt', 'alpha': 0.6931471805599453}, {'dim': 1, 'thresh': 1.0, 'ineq': 'lt', 'alpha': 0.9729550745276565}, {'dim': 0, 'thresh': 0.9, 'ineq': 'lt', 'alpha': 0.8958797346140273}]
[[ 1.17568763]
[ 2.56198199]
[-0.77022252]
[-0.77022252]
[ 0.61607184]]
从结果看出,我们迭代了三次,总误差就为0了,就停止迭代了。最后的分类
4.3 测试算法
一旦拥有了多个弱分类器以及其对应的alpha值,进行测试就变得相当容易了。现在,需要做的就只是将弱分类器的训练过程从程序中抽出来,然后应用到某个具体的实例上去。每个弱分类器的结果以其对应的alpha值作为权重。所有这些弱分类器的结果加权求和就得到了后的结果。
def adaClassify(datToClass,classifierArr):
dataMatrix =np.mat(datToClass) #构建数据向量或矩阵
m = dataMatrix.shape[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)
我们注释掉AdaBoost算法流程的打印,测试结果:
# test3
dataMat, classLabels = loadSimpData ()
weakClassArr,aggClassEst = adaBoostTrainDS(dataMat,classLabels,numIt=30)
#进行分类
adaClassify([0,0],weakClassArr)
# 结果如下:
# 分类器迭代三次的输出
[[-0.69314718]]
[[-1.66610226]]
[[-2.56198199]]
# 最后的分类结果
[[-1.]]
# 进行分类
adaClassify([[5,5],[0,0]],weakClassArr)
# 结果如下:
# 分类器迭代三次的输出
[[ 0.69314718]
[-0.69314718]]
[[ 1.66610226]
[-1.66610226]]
[[ 2.56198199]
[-2.56198199]]
# 最后的分类结果
[[ 1.]
[-1.]]
4.4 示例:在一个难数据集上应用AdaBoost
在第4章给出的马疝病数据集上应用AdaBoost分类器。在第4章,我们曾经利用Logistic回归来预测患有疝病的马是否能够存活。而在本节,我们则想要知道如果利用多个单层决策树和AdaBoost能不能预测得更准。
下面自定义一个函数加载数据集,。在这里,并不必指定每个文件中的特征数目,该函数能够自动检测出特征的数目。 同时,该函数也假定后一个特征是类别标签。是比较通用的一种方法。代码如下:
def loadDataSet(fileName):
numFeat = len(open(fileName).readline().split('\t')) #获取特征数目
datalist = [] # 数据列表
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]))
datalist.append(lineArr) # 加到所有的特征数据列表
dataMat = np.mat(datalist) # 把数据转为矩阵
labelMat.append(float(curLine[-1])) # 最后一行标签数据加到标签列表
return dataMat,labelMat
下面加载我们的马疝病数据集并进行预测:
# test4
datArr, labelArr = loadDataSet('horseColicTraining2.txt')
classifierArray = adaBoostTrainDS(datArr, labelArr, 10) # 迭代10次
# 打印总的错误率变化
total error: 0.2842809364548495
total error: 0.2842809364548495
total error: 0.24749163879598662
total error: 0.24749163879598662
total error: 0.25418060200668896
total error: 0.2408026755852843
total error: 0.2408026755852843
total error: 0.22073578595317725
total error: 0.24749163879598662
total error: 0.23076923076923078
然就看下测试集的表现:
testArr, testLabelArr = loadDataSet('horseColicTest2.txt') # 共67个测试样本
pridiction10 = adaClassify(testArr,classifierArray)
errArr = np.mat(np.ones((67,1))) # 初始化所有分类都不正确
print(errArr[pridiction10 != np.mat(testLabelArr).T].sum()) # 得到一共分错的样本数量
要得到错误率,只需将上述错分样例的个数除以67即可。
上面我们使用的弱分类器数量为10,将弱分类器的数目设定为1到10 000之间的几个不同数字,并运行上述过程。这时,得到的结果就会如下:
观察表中的测试错误率一栏,就会发现测试错误率在达到了一个小值之后又开始上升了。这类现象称之为过拟合(overfitting,也称过学习)。有文献声称,对于表现好的数据集,AdaBoost的测试错误率就会达到一个稳定值,并不会随着分类器的增多而上升。或许在本例子中的数据集也称不上“表现好”。该数据集一开始有30%的缺失值,对于Logistic回归而言,这些缺失值的假设就是有效的,而对于决策树却可能并不合适。如果回到数据集,将所有的0值替换成其他值,或者给定类别的平均值,那么能否得到更好的性能?