AdaBoost实现
前两篇文章针对AdaBoost的伪代码实现步骤进行了讨论,也对关键步骤的更新方法进行了推导,脑海里已经基本有了整体框架,有了基本框架,代码的含义就清晰易懂了,下面看看AdaBoost是如何串行生成一系列基学习器的.
导入数据
from numpy import * import matplotlib.pyplot as plt def loadSimpData():#二维数据点,二分类 datMat = [[ 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
几个简单的二维数据点,对应的标签值属于二分类{-1,1}
数据可视化
def show_dataSet(dataMat,classLabels): for i in range(len(dataMat)): if classLabels[i] == 1.0: plt.scatter(dataMat[i][0],dataMat[i][1],c='r',marker = 'o') else: plt.scatter(dataMat[i][0],dataMat[i][1],c='b',marker = 'o') plt.show()
简单画图看一下几个数据点的分布情况:
根据维度与阈值调整标签
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):#just classify the data retArray = ones((shape(dataMatrix)[0],1))#自定义一个5x1的全部为1的列表作为初始标签 if threshIneq == 'lt':#根据维度和阈值,调整标签值 retArray[dataMatrix[:,dimen] < threshVal] = -1.0 else: retArray[dataMatrix[:,dimen] > threshVal] = -1.0 return retArray
retArray[dataMatrix[:,dimen] <= threshVal] = -1.0 改为了 retArray[dataMatrix[:,dimen] < threshVal] = -1.0与书中不同,这样修改是为了使界限更加清晰,应为有等号时分割界限会和样本点重合,可视化效果不是太好.
选择最优划分方式
def buildStump(dataArr,classLabels,D): dataMatrix = mat(dataArr); labelMat = mat(classLabels).T#转换数据为矩阵形式 m,n = shape(dataMatrix)#提取维度 numSteps = 10.0; bestStump = {}; bestClasEst = mat(zeros((m,1)))#初始化最优划分选择 minError = 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,gt为不同的标签适应 threshVal = (rangeMin + float(j) * stepSize) predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)#根据阈值修改标签 errArr = mat(ones((m,1))) errArr[predictedVals == labelMat] = 0 weightedError = D.T*errArr #计算错误率 print ("划分维度: %d, 阈值: %.2f, 标签方法: %s, 错误率 %.3f" % (i, threshVal, inequal, weightedError)) if weightedError < minError:#添加最低错误率时对应的维度,阈值,划分方式 minError = weightedError bestClasEst = predictedVals.copy() bestStump['划分维度:'] = i bestStump['阈值:'] = threshVal bestStump['标签方法:'] = inequal # print('bestStump is ',bestStump) return bestStump,minError,bestClasEst
这一步是针对单个学习器而言,通过最大值与最小值构造阈值变化步长,针对不同的维度选择,阈值选择,标签划分方式,分别计算对应划分方式的预测错误率,选择当前错误率最低的划分方式,依次放到beststum中,用于之后构建最佳分类边界.
AdaBoost串行实现
def adaBoostTrainDS(dataArr,classLabels,numIt=40): weakClassArr = [] m = shape(dataArr)[0] D = mat(ones((m,1))/m) #初始化权值,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)))#学习器重要性度量α计算 bestStump['学习器权数α:'] = alpha weakClassArr.append(bestStump) #添加最优划分 print ("classEst: ",classEst.T) expon = multiply(-1*alpha*mat(classLabels).T,classEst) #规范化分布计算,分母zm D = multiply(D,exp(expon)) D = D/D.sum()#更新权数D #根据预测分类与权数,累加样本预测值 aggClassEst += alpha*classEst print ("aggClassEst: ",aggClassEst.T) aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T,ones((m,1))) errorRate = aggErrors.sum()/m#计算总分类器预测错误率 print ("total error: ",errorRate) if errorRate == 0.0: break return weakClassArr,aggClassEst
这一步主要参考伪代码很容易理解,参考之前的推导,这里思路很清晰,1)初始化权值分布 -> 2)迭代生成基学习器,更新α,权值D,计算总体错误率 -> 3)直到总体错误率为0时,停止迭代过程,每一步更新的代码对照之前的公式也很容易读懂,至此,Adaboost的生成过程就结束了.
分类边界可视化
def show_classifier_region(classfierarray): for i in range(len(dataMat)): if classLabels[i] == 1.0: plt.scatter(dataMat[i][0],dataMat[i][1],c='r',marker = 'o') else: plt.scatter(dataMat[i][0],dataMat[i][1],c='b',marker = 'o') x = arange(0.5,2.3,0.01) y = arange(0.5,2.3,0.01) for i in range(3): if int(classfierarray[i]['划分维度:']) == 0: for j in y: plt.scatter(classfierarray[i]['阈值:'],j,c='k',alpha = 0.5,s=1) elif int(classfierarray[i]['划分维度:']) == 1: for j in x: plt.scatter(j,classfierarray[i]['阈值:'],c='k',alpha = 0.5,s=1) plt.show()
针对每次最优划分方式,在图中画出对应的分类边界.
[{'划分维度:': 0, '阈值:': 1.3999999999999999, '标签方法:': 'lt', '学习器权数α:': 0.6931471805599453}, {'划分维度:': 0, '阈值:': 0.90000000000000002, '标签方法:': 'lt', '学习器权数α:': 0.5493061443340548}, {'划分维度:': 1, '阈值:': 1.1100000000000001, '标签方法:': 'lt', '学习器权数α:': 0.8047189562170503}]
由于数据点比较少,所以只需要三个基学习器,就可以达到100%精度,上面三条记录就是每个学习器的划分参数
主函数
if __name__ == "__main__": dataMat,classLabels = loadSimpData() D = mat(ones((5,1))/5) print(buildStump(dataMat,classLabels,D)) weakClassArr,aggClassEst = adaBoostTrainDS(dataMat,classLabels,9) print([i for i in weakClassArr]) show_dataSet(dataMat,classLabels) show_classifier_region(weakClassArr)
将上述函数依次添加到AdaBoost.py中,运行主函数即可得到上述结果,这里只显示了一部分,可以看到最终分类器的total error为0.0,从而达到停止条件,如果只想出结果,不需要这些步骤,可以相应的去掉print语句即可.
根据最优选择图解AdaBoost
1)第一条划分界限
{'划分维度:': 0, '阈值:': 1.3999999999999999, '标签方法:': 'lt', '学习器权数α:': 0.6931471805599453}
维度为0,对应x轴,阈值为1.3999,所以分类边界为x = 1.3999,标签方法为lt,即小于阈值的数据判定为-1,大与阈值的判定为+1,如图所示.
2)第二条划分界限
{'划分维度:': 0, '阈值:': 0.90000000000000002, '标签方法:': 'lt', '学习器权数α:': 0.5493061443340548}
维度为0,对应x轴,阈值为0.9,所以分类边界为x = 0.9,标签方法仍为lt,即小于阈值的数据判定为-1,大与阈值的判定为+1,如图所示.这个时候0.9-1.39之间的数据点分类还不正确,因此还需继续迭代.
3)第三条划分界限
{'划分维度:': 1, '阈值:': 1.1100000000000001, '标签方法:': 'lt', '学习器权数α:': 0.8047189562170503}
维度更改为1,对应y轴,阈值为1.11,所以分类边界为y=1.11,标签方法仍为lt,即小于阈值的数据判定为-1,大与阈值的判定为+1,所以对应边界上面部分判断为正,下面判断为负,对于最右边的两个红点,根据投票法,两正一负为正,左上角的红点,两正一负为正,左下的两个蓝点,两负一正为负,从而全部样本预测正确,结束学习器生成过程,完成迭代.
4)总分类边界
根据三条划分线以及对应的标签规则,我们得到了最终的分类边界,如果存在更多的数据点,则可能需要生成更多的基学习器,而分类边界也对应更多的直线.
总结:
针对更复杂的数据点,AdaBoost将生成更多的学习器,分类边界也更加复杂,但是总体的泛化错误率会随之降低,AdaBoost的大致过程就是这样了,总的来说,集成学习就是人多力量大,虽然一个学习器的效果不佳,但是大家的结果组合起来考虑,结果往往有所不同,不过在建模同时,也应该考虑模型是否存在过拟合的情形,像本文的例子,只有五个样例点,建模预测时很容易出现泛化错误率太高的情况。接下来将介绍集成学习中另一个主要成员,Bagging,它和Boosting不同,Bagging可以同时并行生成多个学习器,最终汇总结果.