《机器学习实战》7.1利用AdaBoost元算法提高分类性能
搜索微信公众号:‘AI-ming3526’或者’计算机视觉这件小事’ 获取更多人工智能、机器学习干货
csdn:https://blog.csdn.net/baidu_31657889/
github:https://github.com/aimi-cn/AILearners
本文出现的所有代码,均可在github上下载,不妨来个Star把谢谢~:Github代码地址
一、引言
前面的文章已经介绍了五种不同的分类器,它们各有优缺点。我们可以很自然地将不同的分类器组合起来,而这种组合结果则被成为集成方法(ensemble method)或者元算法(meta-algorithm)。使用集成方法时会有多种形式:可以是不同算法的集成,也可以是同一种算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。
二、集成方法
我们知道集成方法就是把不同的分类器组合起来。集成方法(ensemble method)就是通过组合多个学习器来完成学习任务。基分类器一般采用的是弱可学习(weakly learnable)分类器,通过集成方法,组合成一个强可学习(strongly learnable)分类器。弱分类器中的’弱’意味着分类器的性能比随机猜测要略好,但是也不会好太多。这就是说,在二分类的情况下弱分类器的错误率会高于50%,而’强’分类器的错误率将会低很多。
集成方法主要包括Bagging和Boosting两种方法,Bagging和Boosting都是将已有的分类或回归算法通过一定方式组合起来,形成一个性能更加强大的分类器,更准确的说这是一种分类算法的组装方法,即将弱分类器组装成强分类器的方法。
2.1、集成方法之bagging和boosting
自举汇聚法(bootstrap aggregating),也称为bagging方法。Bagging对训练数据采用自举采样(boostrap sampling),即有放回地采样数据,主要思想:
- 从原始样本集中抽取训练集。每轮从原始样本集中使用Bootstraping的方法抽取n个训练样本(在训练集中,有些样本可能被多次抽取到,而有些样本可能一次都没有被抽中)。共进行k轮抽取,得到k个训练集。(k个训练集之间是相互独立的)
- 每次使用一个训练集得到一个模型,k个训练集共得到k个模型。(注:这里并没有具体的分类算法或回归方法,我们可以根据具体问题采用不同的分类或回归方法,如决策树、感知器等)
- 对分类问题:将上步得到的k个模型采用投票的方式得到分类结果;对回归问题,计算上述模型的均值作为最后的结果。(所有模型的重要性相同)
Boosting是一种与Bagging很类似的技术。Boosting的思路则是采用重赋权(re-weighting)法迭代地训练基分类器,主要思想:
- 每一轮的训练数据样本赋予一个权重,并且每一轮样本的权值分布依赖上一轮的分类结果。
- 基分类器之间采用序列式的线性加权方式进行组合。
目前 bagging 方法最流行的版本是: 随机森林(random forest)
通俗解释理解可以是:
选男友:美女选择择偶对象的时候,会问几个闺蜜的建议,最后选择一个综合得分最高的一个作为男朋友
目前 boosting 方法最流行的版本是: AdaBoost
通俗解释理解可以是:
追女友:3个帅哥追同一个美女,第1个帅哥失败->(传授经验:姓名、家庭情况) 第2个帅哥失败->(传授经验:兴趣爱好、性格特点) 第3个帅哥成功
2.2 、bagging和boosting两者区别
样本选择上:
- Bagging:训练集是在原始集中有放回选取的,从原始集中选出的各轮训练集之间是独立的。
- Boosting:每一轮的训练集不变,只是训练集中每个样例在分类器中的权重发生变化。而权值是根据上一轮的分类结果进行调整。
样例权重:
- Bagging:使用均匀取样,每个样例的权重相等。
- Boosting:根据错误率不断调整样例的权值,错误率越大则权重越大。
预测函数:
- Bagging:所有预测函数的权重相等。
- Boosting:每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重。
并行计算:
- Bagging:各个预测函数可以并行生成。
- Boosting:各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果。
总结下来:这两种方法都是把若干个分类器整合为一个分类器的方法,只是整合的方式不一样,最终得到不一样的效果,将不同的分类算法套入到此类算法框架中一定程度上会提高了原单一分类器的分类效果,但是也增大了计算量。
下面是将决策树与这些算法框架进行结合所得到的新的算法:
- Bagging + 决策树 = 随机森林
- AdaBoost + 决策树 = 提升树
- Gradient Boosting + 决策树 = GBDT
集成方法众多,本文主要关注Boosting方法中的一种最流行的版本,即AdaBoost。
三、AdaBoost算法
AdaBoost算法是基于Boosting思想的机器学习算法,AdaBoost是adaptive boosting(自适应boosting)的缩写,其运行过程如下:
1、计算样本权重
训练数据中的每个样本,赋予其权重,即样本权重,用向量D表示,这些权重都初始化成相等值。假设有n个样本的训练集,设定每个样本的权重都是相等的,即1/n。
2、计算错误率
利用第一个弱学习算法h1对其进行学习,学习完成后进行错误率ε的统计:
3、计算弱学习算法权重
弱学习算法也有一个权重,用向量α表示,利用错误率计算权重α:
4、更新样本权重
在第一次学习完成后,需要重新调整样本的权重,以使得在第一分类中被错分的样本的权重,在接下来的学习中可以重点对其进行学习,使正确分类样本的权重降低而分类错误的样本权重升高:
其中,ht(xi) = yi表示对第i个样本训练正确,不等于则表示分类错误。Zt是一个归一化因子:
这个公式我们可以继续化简,将两个公式进行合并,化简如下:
AdaBoost算法的流程图如下:
四、基于单层决策树构建弱分类器
建立AdaBoost算法之前,我们必须先建立弱分类器,并保存样本的权重。弱分类器使用单层决策树(decision stump),也称决策树桩,它是一种简单的决策树,通过给定的阈值,进行分类。
1、数据集可视化
为了训练单层决策树,我们需要创建一个训练集,编写代码如下:
import numpy as np
import matplotlib.pyplot as plt
'''
@description: 创建单层决策树的数据集
@param: None
@return: dataMat - 数据矩阵
classLabels - 数据标签
'''
def loadSimpData():
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
'''
@description: 数据可视化
@param: dataMat - 数据矩阵
labelMat - 数据标签
@return: None
'''
def showDataSet(dataMat,labelMat):
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__":
dataArr,classLabels = loadSimpData()
showDataSet(dataArr,classLabels)
代码运行结果如下:
可以看到,如果想要试着从某个坐标轴上选择一个值(即选择一条与坐标轴平行的直线)来将所有的蓝色圆点和橘色圆点分开,这显然是不可能的。这就是单层决策树难以处理的一个著名问题。通过使用多颗单层决策树,我们可以构建出一个能够对该数据集完全正确分类的分类器。
2、构建单层决策树
我们设置一个分类阈值,比如我横向切分,如下图所示:
蓝横线上边的是一个类别,蓝横线下边是一个类别。显然,此时有一个蓝点分类错误,计算此时的分类误差,误差为1/5 = 0.2。这个横线与坐标轴的y轴的交点,就是我们设置的阈值,通过不断改变阈值的大小,找到使单层决策树的分类误差最小的阈值。同理,竖线也是如此,找到最佳分类的阈值,就找到了最佳单层决策树。
程序的伪代码看起来大致如下:
将最小的错误率minError设置为正无穷大
对数据集的每一个特征(第一层循环):
对每个步长(第二层循环):
对每个不等号(第三个循环):
建立一颗单层决策树并利用加权数据及对它进行测试
如果错误率低语minError,则将当前层决策树设为最佳单层决策树
返回最佳单层决策树
编写代码如下:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
'''
@description: 创建单层决策树的数据集
@param: None
@return: dataMat - 数据矩阵
classLabels - 数据标签
'''
def loadSimpData():
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
'''
@description: 单层决策树分类函数
@param: dataMatrix - 数据矩阵
dimen - 第dimen列,也就是第几个特征
threshVal - 阈值
threshIneq - 标志 "lt" "gt"
这里lt表示less than,表示分类方式,对于小于阈值的样本点赋值为-1,
gt表示greater than,也是表示分类方式,对于大于阈值的样本点赋值为-1。
@return:
'''
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
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
'''
@description: 找到数据集上最佳的单层决策树
@param: dataArr - 数据矩阵
classLabels - 数据标签
D - 样本权重
@return: bestStump - 最佳单层决策树信息
minError - 最小误差
bestClasEst - 最佳的分类结果
'''
def buildStump(dataArr,classLabels,D):
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']:
#计算阈值
threshVal = (rangeMin + float(j) * stepSize)
#计算分类结果
predictedVals = stumpClassify(dataMatrix, i, threshVal, inequal)
#初始化误差矩阵
errArr = np.mat(np.ones((m,1)))
#分类正确的,赋值为0
errArr[predictedVals == labelMat] = 0
#计算误差
weightedError = D.T * errArr
#找到误差最小的分类方式-
if weightedError < minError:
minError = weightedError
bestClasEst = predictedVals.copy()
#第一行的特征
bestStump['dim'] = i
#阈值
bestStump['thresh'] = threshVal
#标志 "lt" "gt"
bestStump['ineq'] = inequal
return bestStump, minError, bestClasEst
if __name__ == "__main__":
dataArr,classLabels = loadSimpData()
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算法是如何实现的。
五、完整的AdaBoost算法实现
根据之前介绍的AdaBoost算法实现过程,使用AdaBoost算法提升分类器性能,整个实现的伪代码如下:
使用buildStump()函数找到最佳的单层决策树
将最佳单层决策树加入到单层决策树数组
计算alpha
计算新的权重向量D
更新累计类别估计值
如果错误率等于0,则退出循环
在AdaBoost01.py下编写代码如下:
'''
@description: 使用AdaBoost算法提升弱分类器性能
@param: dataArr - 数据矩阵
classLabels - 数据标签
numIt - 最大迭代次数
@return: weakClassArr - 训练好的分类器
aggClassEst - 类别估计累计值
'''
def adaBoostTrainDS(dataArr, classLabels, numIt = 40):
weakClassArr = []
#初始化权重
m = np.shape(dataArr)[0]
D = np.mat(np.ones((m, 1)) / m)
aggClassEst = np.mat(np.zeros((m,1)))
for i in range(numIt):
#构建单层决策树
bestStump, error, classEst = buildStump(dataArr, classLabels, D)
#计算弱学习算法权重alpha,使error不等于0,因为分母不能为0
alpha = float(0.5 * np.log((1.0 - error) / max(error, 1e-16)))
#存储弱学习算法权重和单层决策树
bestStump['alpha'] = alpha
weakClassArr.append(bestStump)
#计算e的指数项
expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)
D = np.multiply(D, np.exp(expon))
#根据样本权重公式,更新样本权重
D = D / D.sum()
#计算AdaBoost误差,当误差为0的时候,退出循环
#计算类别估计累计值
aggClassEst += alpha * classEst
#计算误差
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
if __name__ == "__main__":
dataArr,classLabels = loadSimpData()
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值,进行测试就变得想当容易了。编写代码如下:
'''
@description: 分类函数
@param: datToClass - 待分类样例
classifierArr - 训练好的分类器
@return: 分类结果
'''
def adaClassify(datToClass,classifierArr):
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__":
dataArr,classLabels = loadSimpData()
weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, classLabels)
print(adaClassify([[0,0],[5,5]], weakClassArr))
运行结果如下图所示:
代码很简单,在之前代码的基础上,添加adaClassify()函数,该函数遍历所有训练得到的弱分类器,利用单层决策树,输出的类别估计值乘以该单层决策树的分类器权重alpha,然后累加到aggClassEst上,最后通过sign函数最终的结果。可以看到,分类没有问题,(5,5)属于正类,(0,0)属于负类。
六、在一个难数据集上应用AdaBoost
在《机器学习实战》5.1Logistic回归项目案例:预测病马死亡率 文章中,我们使用Logistic回归方法训练马疝病数据集,预测病马死亡率。当时的训练结果如下图所示:
可以看到错误率还是蛮高的,现在我们使用AdaBoost算法,训练出一个更强的分类器,这里的数据集有所变化,之前的标签是0和1,现在将标签改为+1和-1,其他数据不变。
新的更改过的数据集下载地址:
下载地址
1、使用自己的算法来进行训练测试
使用自己的用Python写的AbaBoost算法进行训练,添加loadDataSet函数用于加载数据集。编写代码horse_adaboost.py如下:
if __name__ == "__main__":
dataArr, LabelArr = loadDataSet('C:/Users/Administrator/Desktop/blog/github/AILearners/data/ml/jqxxsz/7.AdaBoost/horseColicTraining2.txt')
weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, LabelArr)
testArr, testLabelArr = loadDataSet('C:/Users/Administrator/Desktop/blog/github/AILearners/data/ml/jqxxsz/7.AdaBoost/horseColicTest2.txt')
print(weakClassArr)
predictions = adaClassify(dataArr, weakClassArr)
errArr = np.mat(np.ones((len(dataArr), 1)))
print('训练集的错误率:%.3f%%' % float(errArr[predictions != np.mat(LabelArr).T].sum() / len(dataArr) * 100)).decode('utf-8').encode('gb2312')
predictions = adaClassify(testArr, weakClassArr)
errArr = np.mat(np.ones((len(testArr), 1)))
print('测试集的错误率:%.3f%%' % float(errArr[predictions != np.mat(testLabelArr).T].sum() / len(testArr) * 100)).decode('utf-8').encode('gb2312')
代码运行结果如下:
这里输出了AdaBoost算法训练好的分类器的组合,我们只迭代了40次,也就是训练了40个弱分类器。最终,训练集的错误率为19.732%,测试集的错误率为19.403%,可以看到相对于Sklearn的罗辑回归方法,错误率降低了很多。这个仅仅是我们训练40个弱分类器的结果,如果训练更多弱分类器,效果会更好。但是当弱分类器数量过多的时候,你会发现训练集错误率降低很多,但是测试集错误率提升了很多,这种现象就是过拟合(overfitting)。分类器对训练集的拟合效果好,但是缺失了普适性,只对训练集的分类效果好,这是我们不希望看到的。
2、使用Sklearn的AdaBoost
官方英文文档手册:地址
sklearn.ensemble模块提供了很多集成方法,AdaBoost、Bagging、随机森林等。本文使用的是AdaBoostClassifier。
非常简单的代码:
if __name__ == '__main__':
dataArr, classLabels = loadDataSet('C:/Users/Administrator/Desktop/blog/github/AILearners/data/ml/jqxxsz/7.AdaBoost/horseColicTraining2.txt')
testArr, testLabelArr = loadDataSet('C:/Users/Administrator/Desktop/blog/github/AILearners/data/ml/jqxxsz/7.AdaBoost/horseColicTest2.txt')
bdt = AdaBoostClassifier(DecisionTreeClassifier(max_depth = 2), algorithm = "SAMME", n_estimators = 10)
bdt.fit(dataArr, classLabels)
predictions = bdt.predict(dataArr)
errArr = np.mat(np.ones((len(dataArr), 1)))
print('训练集的错误率:%.3f%%' % float(errArr[predictions != classLabels].sum() / len(dataArr) * 100)).decode('utf-8').encode('gb2312')
predictions = bdt.predict(testArr)
errArr = np.mat(np.ones((len(testArr), 1)))
print('测试集的错误率:%.3f%%' % float(errArr[predictions != testLabelArr].sum() / len(testArr) * 100)).decode('utf-8').encode('gb2312')
运行结果如下所示:
我们使用DecisionTreeClassifier作为使用的弱分类器,使用AdaBoost算法训练分类器。可以看到训练集的错误率为16.054%,测试集的错误率为:17.910%。更改n_estimators参数,你会发现跟我们自己写的代码,更改迭代次数的效果是一样的。n_enstimators参数过大,会导致过拟合。
总结:
AdaBoost的优缺点:
- 优点:泛化错误率低,易编码,可以应用在大部分分类器上,无参数调整。
- 缺点:对离群点敏感。
AIMI-CN AI学习交流群【1015286623】 获取更多AI资料
扫码加群:
分享技术,乐享生活:我们的公众号计算机视觉这件小事每周推送“AI”系列资讯类文章,欢迎您的关注!