Machine Learning in Action 读书笔记
第7章 利用AdaBoost元算法提高分类性能
文章目录
一、本章专业词汇
- 元算法(meta-algorithm):元算法是对其他算法进行组合的一种方式,这种组合结果也被称为集成方法(ensemble method)
- 单层决策树(decision stump):一个单节点的决策树
- :
二、基于数据集多重抽样的分类器
前几章已经学习了五种分类算法:k-近邻、决策树、朴素贝叶斯、logistic回归、支持向量机。将不同的分类器组合起来,这种组合结果被称为集成方法或元算法。使用集成方法时会有多种形式:
- 不同算法的集成
- 同一算法在不同设置下的集成
- 数据集不同部分分配给不同分类器之后的集成
三、AdaBoost算法
- 优点:泛化错误率低,易编码,可以应用在大部分分类器上,无参数调整
- 缺点:对离群点敏感
- 适用数据类型:数值型和标称型数据
四、两种集成方法
1.bagging
自举汇聚法(bootstrap aggregating),也称为bagging方法,是从原始数据集选择S次后得到S个新数据集的一种技术,新数据集和原数据集的大小相等。在S个数据集建立好之后,将某个学习算法分别作用于每个数据集就得到了S个分类器。当我们要对新数据进行分类时,就可以应用这S个分类器进行分类,选择分类器投票结果中最多的类别作为最后的分类结果。
2.boosting
boosting是与bagging类似的集成分类器方法。boosting是通过集中关注被已有分类器错分的那些数据来获得新的分类器。boosting分类的结果是基于所有分类器的加权求和结果。因此,boosting和bagging不太一样,bagging中的分类器权重是相等的,而boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度。
boosting方法拥有很多版本,AdaBoost是其中最流行的一个版本。
五、AdaBoost
1.AdaBoost的一般流程
- 收集数据:可以使用任意方法
- 准备数据:依赖于所使用的弱分类器类型,本章使用的是单层决策树,这种分类器可以处理任何数据类型。也可以使用任意分类器作为弱分类器。作为弱分类器,简单分类器的效果更好。
- 分析数据:可以使用任意方法
- 训练算法:AdaBoost的大部分时间都用在训练上,分类器将多次在同一数据集上训练弱分类器。
- 测试算法:计算分类的错误率
- 使用算法:同SVM一样,AdaBoost预测两个类别中的一个。如果想把它应用到多个类别的场合,那么就要像多类SVM中的做法一样对AdaBoost进行修改。
2.AdaBoost的运行过程
- 训练数据中的每个样本,并赋予其一个权重,这些权重构成了向量D(一开始这些权重都初始化成相等值,和为1)
- 在训练数据上训练出一个弱分类器并计算该分类器的错误率
- 然后在同一数据集上再次训练弱分类器,在分类器的第二次训练中,将会重新调整每个样本的权重,调整规则为
- 上一次分对的样本权重会降低
- 上一次分错的样本的权重会提高
为了从所有弱分类器中得到最终的分类结果,AdaBoost为每个分类器都分配了一个权重值alpha,alpha基于每个弱分类器的错误率进行计算,其中,错误率定义为:
ε
=
未
正
确
分
类
的
样
本
数
目
所
有
样
本
数
目
ε=\frac{未正确分类的样本数目}{所有样本数目}
ε=所有样本数目未正确分类的样本数目
alpha的计算公式:
α
=
1
2
l
n
(
1
−
ε
ε
)
α=\frac{1}{2}ln(\frac{1-ε}{ε})
α=21ln(ε1−ε)
计算出alpha值后,可以对样本的权重向量D进行更新:
如果某个样本被正确分类,那么该样本的权重更改为:
D
i
(
t
+
1
)
=
D
i
(
t
)
e
−
α
S
u
m
(
D
)
D_i^{(t+1)}=\frac{D_i^{(t)}e^{-α}}{Sum(D)}
Di(t+1)=Sum(D)Di(t)e−α
如果某个样本被错分,那么该样本的权重更改为:
D
i
(
t
+
1
)
=
D
i
(
t
)
e
α
S
u
m
(
D
)
D_i^{(t+1)}=\frac{D_i^{(t)}e^{α}}{Sum(D)}
Di(t+1)=Sum(D)Di(t)eα
AdaBoost算法会不断重复训练和调整权重,直到:
- 训练错误率为0
- 分类器的数目达到用户的指定值为止
3.基于单层决策树构建弱分类器
找到最低错误率的决策树的伪代码如下:
将最小错误率minError设为+∞
对数据集中的每一个特征(第一层循环):
对每个步长(第二层循环):
对每个不等号(第三层循环):
建立一颗单层决策树并利用加权数据集对它进行测试
如果错误率低于minError,则将当前单层决策树设为最佳单层决策树
返回最佳单层决策树
'''单层决策树生成函数'''
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
retArray = ones((shape(dataMatrix)[0], 1))
if threshIneq == 'lt':
retArray[dataMatrix[:, dimen] <= threshVal] = -1.0 # 通过阈值进行分类
else:
retArray[dataMatrix[:,dimen] > threshVal] = -1.0
return retArray
def buildStump(dataArr, classLabels, D):
dataMatrix = mat(dataArr)
labelMat = mat(classLabels).T
m, n = shape(dataMatrix) # 5*2
numSteps = 10.0
bestStump = {} # 储存最好单层决策树信息
bestClasEst = mat(zeros((m, 1))) # 初始化样本类别估计值
minError = inf # 最小错误率初始化为正无穷大,方便后面比较
for i in range(n): # 第一层for循环,在数据集所有特征上遍历
# print('=============:',i)
rangeMin = dataMatrix[:, i].min()
rangeMax = dataMatrix[:, i].max()
stepSize = (rangeMax - rangeMin)/numSteps # 计算需要多大的步长
for j in range(-1, int(numSteps)+1):
# print(j)
for inequal in ['lt', 'gt']:
# print(inequal)
threshVal = (rangeMin + float(j) * stepSize)
predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)#预测的标签
errArr = mat(ones((m, 1))) # 预测错误,该值加1(表现为将其初始化为1,如果预测正确,则将预测正确样本置为0)
errArr[predictedVals == labelMat] = 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 # 字典,错误率,类别估计值
''' 上面函数的三层for循环输出部分值
=============: 0
-1
lt
split: dim 0, thresh 0.90, thresh ineqal: lt, the weighted error is: 0.400
gt
split: dim 0, thresh 0.90, thresh ineqal: gt, the weighted error is: 0.600
0
lt
split: dim 0, thresh 1.00, thresh ineqal: lt, the weighted error is: 0.400
gt
split: dim 0, thresh 1.00, thresh ineqal: gt, the weighted error is: 0.600
'''
4.完整AdaBoost算法实现
伪代码:
对每次迭代:
利用buildStump()函数找到最佳的单层决策树
将最佳单层决策树加入到单层决策树数组
计算alpha
计算新的权重向量D
更新累计类别估计值
如果错误率等于0.0,则退出循环
'''基于单层决策树的AdaBoost训练过程'''
def adaBoostTrainDS(dataArr, classLabels, numIt=40): # numIt为迭代次数
weakClassArr = [] # 建立一个新的python表来存储输出的单层决策树
m = shape(dataArr)[0]
D = mat(ones((m,1))/m) # 初始化D向量,D向量包含了每个数据点的权重,初始都相同,后续AdaBoost算法会增加错分数据的权重,降低正确分类数据的权重;D是一个概率分布向量,所有元素和为1
aggClassEst = mat(zeros((m,1))) # 用于记录每个数据点的类别估计累计值
for i in range(numIt):
bestStump, error, classEst = buildStump(dataArr,classLabels,D) # 构建单层决策树
# print("D:", D.T)
alpha = float(0.5*log((1.0-error)/max(error,1e-16))) #max(error,1e-16)用于确保在没有错误时不会发生除零溢出;alpha值会告诉总分类器本次单层决策树输出结果的权重
bestStump['alpha'] = alpha #将alpha值加入到字典中
weakClassArr.append(bestStump) #将该字典添加到列表中
# print("classEst: ", classEst.T)
expon = multiply(-1*alpha*mat(classLabels).T,classEst) #exponent for D calc, getting messy
D = multiply(D,exp(expon)) #计算下一次迭代中的新权重向量D
D = D/D.sum()
#calc training error of all classifiers, if this is 0 quit for loop early (use break)
aggClassEst += alpha*classEst
# print("aggClassEst: ", aggClassEst.T)
aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T,ones((m,1))) # sign(x),if x >0:print 1,elif x <0:print -1,else:print 0
errorRate = aggErrors.sum()/m
print("total error: ", errorRate)
if errorRate == 0.0:
break
return weakClassArr,aggClassEst # 返回弱分类器数组 和 输出的用于分类的值
'''上面函数有print时候的输出
D: [[0.2 0.2 0.2 0.2 0.2]]
classEst: [[-1. 1. -1. -1. 1.]]
aggClassEst: [[-0.69314718 0.69314718 -0.69314718 -0.69314718 0.69314718]]
total error: 0.2
D: [[0.5 0.125 0.125 0.125 0.125]]
classEst: [[ 1. 1. -1. -1. -1.]]
aggClassEst: [[ 0.27980789 1.66610226 -1.66610226 -1.66610226 -0.27980789]]
total error: 0.2
D: [[0.28571429 0.07142857 0.07142857 0.07142857 0.5 ]]
classEst: [[1. 1. 1. 1. 1.]]
aggClassEst: [[ 1.17568763 2.56198199 -0.77022252 -0.77022252 0.61607184]]
total error: 0.0
'''
5.基于AdaBoost进行分类
将弱分类器的训练过程从程序中抽出来,然后应用到某个具体的实例上去,每个弱分类器的结果以其对应的alpha值作为权重,将所有这些弱分类器的结果加权求和就得到了最后的结果。
'''AdaBoost分类函数:利用多个弱分类器进行分类的函数'''
def adaClassify(datToClass,classifierArr): #datToClass:一个或多个待分类样例;classifierArr:多个弱分类器组成的数组
dataMatrix = mat(datToClass)#将分类样例转换为numpy矩阵形式
m = shape(dataMatrix)[0] # 获得待分类样例个数
aggClassEst = mat(zeros((m,1))) # 用于存放分类值
for i in range(len(classifierArr)):
classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'], \
classifierArr[i]['thresh'], \
classifierArr[i]['ineq']) #call stump classify
aggClassEst += classifierArr[i]['alpha']*classEst # 该单层决策树的alpha值 * 输出的类别估计值
# print(aggClassEst)
return sign(aggClassEst)
六、非均衡分类问题
大多数情况下,不同类别的分类代价并不相等,度量分类器性能的方法有:
- 错误率:所有测试样例中错分的样例比例
- 正确率:预测为正例样本中的真正正例的比例
- 召回率:预测为正例的真实正例占所有真实正例的比例
- ROC曲线:ROC(receiver operating characteristic)代表接收者操作特征,是用于度量分类中的非均衡性的工具
如下为AdaBoost马疝病检测系统的ROC曲线:
其中使用10个弱分类器,曲线下面积(the Area Under the Curve,AUC)为: 0.8582969635063604
使用50个弱分类器,AUC为: 0.8953941870182941
AUC表示分类器的平均性能值。上图中,(0,0)点表示所有样例判为反例的情况;(1,1)点表示所有样例判为正例的情况;虚线为随机猜测的结果曲线,AUC为0.5。
为了创建ROC曲线,首先要将分类样例按照其预测强度排序,从低到高。所有排名更低的样例都被判为反例,所有排名更高的样例都被判为正例。如果该样例属于正例,那么对真阳率进行修改,如果该样例属于反例,那么对假阴例进行修改。
'''ROC(接收者操作特征)曲线的绘制及AUC(曲线下面积)计算函数'''
def plotROC(predStrengths, classLabels): # predStrengths为分类器的预测强度数组
cur = (1.0,1.0) #该元组保留的是绘制光标的位置
ySum = 0.0 #用于计算曲线下面积的变量
numPosClas = sum(array(classLabels)==1.0) # 通过数组过滤的方式计算正例的数目
yStep = 1/float(numPosClas) # 在y坐标轴上的步进数目(步长)
xStep = 1/float(len(classLabels)-numPosClas) # 1/ (所有样例个数 - 正样例个数)(用于获得x轴的步长)
sortedIndicies = predStrengths.argsort()#获取排好序的索引,从最小到最大的顺序排列的,因此要从(1,1)开始画到(0,0)
fig = plt.figure()
fig.clf()
ax = plt.subplot(111)
#loop through all the values, drawing a line segment at each point
for index in sortedIndicies.tolist()[0]:
if classLabels[index] == 1.0: #在遍历表时,每得到一个标签为1.0的类,则要沿着y轴的方向下降一个步长,即不断降低真阳率
delX = 0; delY = yStep;
else: # 对于其他类别标签,则是在x轴方向上倒退一个步长(假阴率方向)
delX = xStep; delY = 0;
ySum += cur[1] # 为了计算曲线下面积,每次移动横坐标步长相同,这里累加纵坐标长度
#draw line from cur to (cur[0]-delX,cur[1]-delY)
ax.plot([cur[0],cur[0]-delX],[cur[1],cur[1]-delY], c='b')
cur = (cur[0]-delX,cur[1]-delY) # 当前点
ax.plot([0,1],[0,1],'b--') # 画虚线
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve for AdaBoost horse colic detection system')
ax.axis([0,1,0,1])
plt.show()
print("the Area Under the Curve is: ", ySum * xStep)
除了调节分类器的阈值之外,还能基于代价函数处理非均衡分类问题,直到了代价值,就可以选择付出代价最小的分类器。
还有处理非均衡问题的数据抽样方法:
- 欠抽样(undersampling):删除样例
- 过抽样(oversampling):复制样例
还可以采用过抽样和欠抽样混合的方式来调节数据集中正例和反例数目。
七、全部代码
from numpy import *
import matplotlib.pyplot as plt
def loadSimpData():
datMat = 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
'''单层决策树生成函数'''
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
retArray = ones((shape(dataMatrix)[0], 1))
if threshIneq == 'lt':
retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
else:
retArray[dataMatrix[:,dimen] > threshVal] = -1.0
return retArray
def buildStump(dataArr, classLabels, D):
dataMatrix = mat(dataArr)
labelMat = mat(classLabels).T
m, n = shape(dataMatrix) # 5*2
numSteps = 10.0
bestStump = {} # 储存最好单层决策树信息
bestClasEst = mat(zeros((m, 1))) # 初始化样本类别估计值
minError = inf # 最小错误率初始化为正无穷大,方便后面比较
for i in range(n): # 第一层for循环,在数据集所有特征上遍历
# print('=============:',i)
rangeMin = dataMatrix[:, i].min()
rangeMax = dataMatrix[:, i].max()
stepSize = (rangeMax - rangeMin)/numSteps # 计算需要多大的步长
for j in range(-1, int(numSteps)+1):
# print(j)
for inequal in ['lt', 'gt']:
# print(inequal)
threshVal = (rangeMin + float(j) * stepSize)
predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)#预测的标签
errArr = mat(ones((m, 1))) # 预测错误,该值加1(表现为将其初始化为1,如果预测正确,则将预测正确样本置为0)
errArr[predictedVals == labelMat] = 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 # 字典,错误率,类别估计值
''' 上面函数的三层for循环输出部分值
=============: 0
-1
lt
split: dim 0, thresh 0.90, thresh ineqal: lt, the weighted error is: 0.400
gt
split: dim 0, thresh 0.90, thresh ineqal: gt, the weighted error is: 0.600
0
lt
split: dim 0, thresh 1.00, thresh ineqal: lt, the weighted error is: 0.400
gt
split: dim 0, thresh 1.00, thresh ineqal: gt, the weighted error is: 0.600
'''
'''基于单层决策树的AdaBoost训练过程'''
def adaBoostTrainDS(dataArr, classLabels, numIt=40): # numIt为迭代次数
weakClassArr = [] # 建立一个新的python表来存储输出的单层决策树
m = shape(dataArr)[0]
D = mat(ones((m,1))/m) # 初始化D向量,D向量包含了每个数据点的权重,初始都相同,后续AdaBoost算法会增加错分数据的权重,降低正确分类数据的权重;D是一个概率分布向量,所有元素和为1
aggClassEst = mat(zeros((m,1))) # 用于记录每个数据点的类别估计累计值
for i in range(numIt):
bestStump, error, classEst = buildStump(dataArr,classLabels,D) # 构建单层决策树
# print("D:", D.T)
alpha = float(0.5*log((1.0-error)/max(error,1e-16))) #max(error,1e-16)用于确保在没有错误时不会发生除零溢出;alpha值会告诉总分类器本次单层决策树输出结果的权重
bestStump['alpha'] = alpha #将alpha值加入到字典中
weakClassArr.append(bestStump) #将该字典添加到列表中
# print("classEst: ", classEst.T)
expon = multiply(-1*alpha*mat(classLabels).T,classEst) #exponent for D calc, getting messy
D = multiply(D,exp(expon)) #计算下一次迭代中的新权重向量D
D = D/D.sum()
#calc training error of all classifiers, if this is 0 quit for loop early (use break)
aggClassEst += alpha*classEst
# print("aggClassEst: ", aggClassEst.T)
aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T,ones((m,1))) # sign(x),if x >0:print 1,elif x <0:print -1,else:print 0
errorRate = aggErrors.sum()/m
print("total error: ", errorRate)
if errorRate == 0.0:
break
return weakClassArr,aggClassEst # 返回弱分类器数组 和 输出的用于分类的值
'''上面函数有print时候的输出
D: [[0.2 0.2 0.2 0.2 0.2]]
classEst: [[-1. 1. -1. -1. 1.]]
aggClassEst: [[-0.69314718 0.69314718 -0.69314718 -0.69314718 0.69314718]]
total error: 0.2
D: [[0.5 0.125 0.125 0.125 0.125]]
classEst: [[ 1. 1. -1. -1. -1.]]
aggClassEst: [[ 0.27980789 1.66610226 -1.66610226 -1.66610226 -0.27980789]]
total error: 0.2
D: [[0.28571429 0.07142857 0.07142857 0.07142857 0.5 ]]
classEst: [[1. 1. 1. 1. 1.]]
aggClassEst: [[ 1.17568763 2.56198199 -0.77022252 -0.77022252 0.61607184]]
total error: 0.0
'''
'''AdaBoost分类函数:利用多个弱分类器进行分类的函数'''
def adaClassify(datToClass,classifierArr): #datToClass:一个或多个待分类样例;classifierArr:多个弱分类器组成的数组
dataMatrix = mat(datToClass)#将分类样例转换为numpy矩阵形式
m = shape(dataMatrix)[0] # 获得待分类样例个数
aggClassEst = mat(zeros((m,1))) # 用于
for i in range(len(classifierArr)):
classEst = stumpClassify(dataMatrix, classifierArr[i]['dim'], \
classifierArr[i]['thresh'], \
classifierArr[i]['ineq']) #call stump classify
aggClassEst += classifierArr[i]['alpha']*classEst # 该单层决策树的alpha值 * 输出的类别估计值
# print(aggClassEst)
return sign(aggClassEst)
'''新的加载函数'''
def loadDataSet(fileName):
numFeat = len(open(fileName).readline().split('\t')) # 自动检测样本特征数目
dataMat = []; 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]))
dataMat.append(lineArr)
labelMat.append(float(curLine[-1]))
return dataMat,labelMat
'''ROC(接收者操作特征)曲线的绘制及AUC(曲线下面积)计算函数'''
def plotROC(predStrengths, classLabels): # predStrengths为分类器的预测强度数组
cur = (1.0,1.0) #该元组保留的是绘制光标的位置
ySum = 0.0 #用于计算曲线下面积的变量
numPosClas = sum(array(classLabels)==1.0) # 通过数组过滤的方式计算正例的数目
yStep = 1/float(numPosClas) # 在y坐标轴上的步进数目(步长)
xStep = 1/float(len(classLabels)-numPosClas) # 1/ (所有样例个数 - 正样例个数)(用于获得x轴的步长)
sortedIndicies = predStrengths.argsort()#获取排好序的索引,从最小到最大的顺序排列的,因此要从(1,1)开始画到(0,0)
fig = plt.figure()
fig.clf()
ax = plt.subplot(111)
#loop through all the values, drawing a line segment at each point
for index in sortedIndicies.tolist()[0]:
if classLabels[index] == 1.0: #在遍历表时,每得到一个标签为1.0的类,则要沿着y轴的方向下降一个步长,即不断降低真阳率
delX = 0; delY = yStep;
else: # 对于其他类别标签,则是在x轴方向上倒退一个步长(假阴率方向)
delX = xStep; delY = 0;
ySum += cur[1] # 为了计算曲线下面积,每次移动横坐标步长相同,这里累加纵坐标长度
#draw line from cur to (cur[0]-delX,cur[1]-delY)
ax.plot([cur[0],cur[0]-delX],[cur[1],cur[1]-delY], c='b')
cur = (cur[0]-delX,cur[1]-delY) # 当前点
ax.plot([0,1],[0,1],'b--') # 画虚线
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve for AdaBoost horse colic detection system')
ax.axis([0,1,0,1])
plt.show()
print("the Area Under the Curve is: ", ySum * xStep)
if __name__ == '__main__':
datMat, classLabels = loadSimpData()
# D = mat(ones((5,1))/5) # 首先分配相同的样本权重
# buildStump(datMat, classLabels, D)
classifierArr, aggClassEst = adaBoostTrainDS(datMat, classLabels, 30)
# print(classifierArr) #[{'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}]
testClass = adaClassify([0, 0], classifierArr)
print('testClass:', testClass) #testClass: [[-1.]]
# 调用新的加载函数
datArr, labelArr = loadDataSet('horseColicTraining2.txt')
# print(datArr)
# print(labelArr)
classifierArray, aggClassEst = adaBoostTrainDS(datArr, labelArr, 10)
# print(classifierArray)
# print(aggClassEst)
prediction10 = adaClassify(datArr, classifierArr)
# print(prediction10) #输出预测的分类结果
errArr = mat(ones((299, 1)))
errNum = errArr[prediction10 != mat(labelArr).T].sum()
print(errNum) # 最终299个样例中,有135个预测结果不匹配
plotROC(aggClassEst.T, labelArr)