浅尝AdaBoost算法

元算法是对其他算法进行组合的一种方式。

使用集成方法时,会有多种形式:可以是不同算法的集成;也可以是同一算法在不同设置下的集成;还可以是数据集不同部分分配给不同分类器之后的集成。


bagging,自举汇聚法(bootstrap aggregating),其算法过程如下:

  1. 从原始样本集中抽取训练集。每轮从原始样本集中使用Bootstraping的方法抽取n个训练样本(在训练集中,有些样本可能被多次抽取到,而有些样本可能一次都没有被抽中)。共进行k轮抽取,得到k个训练集。(k个训练集之间是相互独立的)

  2. 每次使用一个训练集得到一个模型,k个训练集共得到k个模型。(注:这里并没有具体的分类算法或回归方法,我们可以根据具体问题采用不同的分类或回归方法,如决策树、感知器等)

  3. 对分类问题:将上步得到的k个模型采用投票的方式得到分类结果;对回归问题,计算上述模型的均值作为最后的结果。(所有模型的重要性相同)。

boosting(提升)是通过集中关注被已有分类器错分的那些数据来获得新的分类器。bagging中的分类器权重是相等的,而boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度。

AdaBoost

AdaBoost是adaptive boosting(自适应boosting)的缩写。其运行过程如下:

  1. 为每个训练数据赋予相等的初始权重,这些权重构成向量D
  2. 训练出一个弱分类器并计算该分类器的错误率
  3. 重新调整每个样本的权重,上一次分对的样本的权重降低,分错的样本的权重提高,然后在同一数据集上再次训练弱分类器

为了从所有弱分类器中得到最终的分类结果,AdaBoost为每个分类器分配一个权重值alpha,这些alpha值是基于弱分类器的错误率进行计算的。其中,错误率\varepsilon的定义为:
\varepsilon = \frac{未正确分类的样本数}{所有样本数}
而alpha的计算公式如下:
\alpha = \frac{1}{2}ln\lgroup\frac{1 - \varepsilon}{\varepsilon}\rgroup

计算出alpha值之后,对权重向量D进行更新。
如果某个样本被正确分类,那么该样本的权重更改为:
D^{(t+1)}_i = \frac{D^{(t)}_ie^{-\alpha}}{Sum(D)}
如果某个样本被错分,那么该样本的权重更改为:
D^{(t+1)}_i = \frac{D^{(t)}_ie^{\alpha}}{Sum(D)}

AdaBoost算法会不断重复训练和调整权重的过程,直到训练错误率为0或者弱分类器数目达到指定值。


上图为Adaboost算法示意图。最左边为数据集,不同宽度表示权重不同。各个弱分类器预测出的结果通过三角形中的alpha值进行加权,最后汇总求和,通过sign函数输出结果。

基于单层决策树构建分类器

单层决策树(decision stump),仅基于单个特征做决策,只有一次分裂过程,实际上就是个树桩。

# 创建简单数据集
def loadSimpData():
    datMat = np.matrix([[ 1. ,  2.1],
        [ 1.5 ,  1.5],
        [ 1.3,  1. ],
        [ 1. ,  1. ],
        [ 2. ,  1. ]])
    classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
    return datMat,classLabels

# 数据可视化
datMat, classLabels = loadSimpData()

xcord0 = []
ycord0 = []
xcord1 = []
ycord1 = []
markers =[]
colors =[]

for i in range(len(classLabels)):
    if classLabels[i]==1.0:
        xcord1.append(datMat[i,0]), ycord1.append(datMat[i,1])
    else:
        xcord0.append(datMat[i,0]), ycord0.append(datMat[i,1])
fig = plt.figure()
ax = fig.add_subplot(111)       
ax.scatter(xcord0,ycord0, marker='s', s=90)
ax.scatter(xcord1,ycord1, marker='o', s=50, c='red')
plt.title('decision stump test data')
plt.show()

观察上图,可以发现如果要找一条与坐标轴平行的直线来分开两类数据是不可能的。这就是单层决策树难以处理的一个问题。

def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
    '''
    dataMatrix:数据集
    dimen:用于决策的特征
    threshVal:阈值
    threshIneq:比较符号(小于等于或大于)
    '''
    retArray = np.ones((np.shape(dataMatrix)[0],1))
    if threshIneq == 'le':
        retArray[dataMatrix[:,dimen] <= threshVal] = -1.0
    else:
        retArray[dataMatrix[:,dimen] > threshVal] = -1.0
    return retArray

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 ['le', 'gt']:        # 第三层:循环切换不等式
                threshVal = (rangeMin + float(j) * stepSize)
                predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)
                errArr = np.mat(np.ones((m,1)))
                errArr[predictedVals == labelMat] = 0
                weightedError = D.T*errArr      # 计算误差
                if weightedError < minError:
                    minError = weightedError
                    bestClasEst = predictedVals.copy()
                    bestStump['dim'] = I
                    bestStump['thresh'] = threshVal
                    bestStump['ineq'] = inequal
    return bestStump,minError,bestClasEst

试一下,观察结果。

D = np.mat(np.ones((5,1))/5)
buildStump(datMat, classLabels, D)

output:
({'dim': 0, 'thresh': 1.3, 'ineq': 'le'}, matrix([[0.2]]), array([[-1.],
        [ 1.],
        [-1.],
        [-1.],
        [ 1.]]))

上面生成的单层决策树就是一个弱分类器。接下来将使用多个弱分类器来构建AdaBoost。

完整AdaBoost算法

伪代码:

对每次迭代:
    利用buildStump()函数找到最佳的单层决策树
    将最佳单层决策树加入到单层决策树数组
    计算alpha
    计算新的权重向量D
    更新累计类别估计值
    如果错误率等于0,则退出循环

python代码实现:

def adaBoostTrainDS(dataArr, classLabels, maxIter=40):
    '''
    dataArr:特征矩阵
    classLabels:标签矩阵
    maxIter=40:最大迭代次数,默认值为40
    返回:弱分类器数组,类别估计累计值
    '''
    weakClassArr = []
    m = dataArr.shape[0]
    D = np.mat(np.ones((m,1))/m)                                       # 初始化权重值
    aggClassEst = np.mat(np.zeros((m,1)))
    for i in range(maxIter):
        bestStump,error,classEst = buildStump(dataArr,classLabels,D)   # 构建单层决策树
        alpha = float(0.5*np.log((1.0-error)/max(error,1e-16)))        # 计算alpha
        bestStump['alpha'] = alpha  
        weakClassArr.append(bestStump)                                 # 存储单层决策树
        expon = np.multiply(-1*alpha*np.mat(classLabels).T,classEst)   # 计算e的指数项
        D = np.multiply(D,np.exp(expon))
        D = D/D.sum()
        # 计算全部分类器的累计误差,如果为0则终止循环
        aggClassEst += alpha*classEst
        aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T,np.ones((m,1)))
        errorRate = aggErrors.sum()/m
        if errorRate == 0.0: break
    return weakClassArr,aggClassEst

执行函数,观察结果。

DSArr, aggClass = adaBoostTrainDS(datMat, classLabels)

output:
D: [[0.2 0.2 0.2 0.2 0.2]]
D: [[0.5   0.125 0.125 0.125 0.125]]
D: [[0.28571429 0.07142857 0.07142857 0.07142857 0.5       ]]

可以观察到在迭代过程中,被错分的样本权重会增加,正确分类的样本权重会降低。
观察分类器数组:

DSArr

ooutput:
[{'dim': 0, 'thresh': 1.3, 'ineq': 'le', 'alpha': 0.6931471805599453},
 {'dim': 1, 'thresh': 1.0, 'ineq': 'le', 'alpha': 0.9729550745276565},
 {'dim': 0, 'thresh': 0.9, 'ineq': 'le', 'alpha': 0.8958797346140273}]

只有三个弱分类器,也就是说迭代三次就停止了。
接下来测试下算法。

基于AdaBoost的分类

# AdaBoost分类函数
def adaClassify(inX, classifierArr):
    '''
    inX:待分类数据
    classifierArr:训练好的弱分类器数组
    '''
    dataMatrix = np.mat(inX)
    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
    return np.sign(aggClassEst)

观察实际运行效果。

print(adaClassify([0,0], DSArr))
print(adaClassify([2,2], DSArr))

output:
[[-1.]]
[[1.]]

示例:在病马数据集上应用AdaBoost

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 np.matrix(dataMat), np.matrix(labelMat)

def calErrRate(maxC = 40):
    trainMat, trainLabelMat = loadDataSet('horseColicTraining2.txt')
    weakClassArr,aggClassEst = adaBoostTrainDS(trainMat, trainLabelMat, maxC)
    trainPred = adaClassify(trainMat, weakClassArr)
    errArr = np.mat(np.ones((trainMat.shape[0], 1)))
    trainErrRate = errArr[trainPred != trainLabelMat.T].sum() / trainMat.shape[0]

    testMat, testLabelMat = loadDataSet('horseColicTest2.txt')
    testPred = adaClassify(testMat, weakClassArr)
    errArr = np.mat(np.ones((testMat.shape[0], 1)))
    testErrRate = errArr[testPred != testLabelMat.T].sum() / testMat.shape[0]
    return trainErrRate, testErrRate

loadDataSet()函数加载数据集,并以矩阵形式返回。calErrRate()计算训练错误率和测试错误率。
直接执行calErrRate()函数会得到如下结果。

(0.19732441471571907, 0.19402985074626866)

也就是说弱分类器在40个的情况下,训练错误率在0.20左右,测试错误率在0.19左右。
接下来看一下不同分类器数目的情况。

import pandas as pd

Cycles = [1, 10, 50, 100, 500, 1000, 10000]
trainErrRate = []
testErrRate = []
for maxC in Cycles:
    a, b = calErrRate(maxC)
    trainErrRate.append(round(a, 2))
    testErrRate.append(round(b, 2))

df = pd.DataFrame({'分类器数目':Cycles, '训练错误率':trainErrRate, '测试错误率':testErrRate})
df
分类器数目 训练错误率 测试错误率
1 0.28 0.27
10 0.23 0.24
50 0.19 0.21
100 0.19 0.22
500 0.16 0.25
1000 0.14 0.31
10000 0.11 0.33

观察发现,测试错误率在达到一个最小值后又开始上升。这种现象称之为过拟合

小结

集成方法通过组合多个分类器的分类结果,获得了比简单的单分类器更好的分类结果。

多个分类器组合可能会进一步凸显出单分类器的不足,比如过拟合问题。如果分类器之间差别显著,那么可能会缓解这一问题。分类器之间的差别可以是算法本身或者应用于算法上的数据不同。

bagging是通过随机抽样的替换方式,得到了与原始数据集规模一样的数据集。boosting在数据集上顺序应用了多个不同的分类器。

本文以单层决策树作为弱学习器构建AdaBoost分类器。实际上,AdaBoost可以应用于任意分类器,只要该分类器能够处理加权数据即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容