在这篇文章中,我们一起学习了AdaBoost算法的原理,今天我们在python3的环境下,根据原理写一段代码来实现AdaBoost算法。
01 构造单层决策树
- 逻辑:
- 遍历数据集的每个特征:
- 遍历特征的每个步长:
- 遍历步长的每个阈值对比方式(less than/greater than):
- 计算每次迭代的weightedError
- 遍历步长的每个阈值对比方式(less than/greater than):
- 遍历特征的每个步长:
- 认为weightedError最小的点(特征,阈值,方式)是最佳决策点,以此构建一棵决策树桩(stump)
- 遍历数据集的每个特征:
根据这样的逻辑,写出如下单层决策树生成函数。
#通过阈值对数据分类+1 -1
#dimen为dataMat的列索引值,即特征位置;threshIneq为阈值对比方式,大于或小于
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
retArray=ones((shape(dataMatrix)[0],1))#注意,有两个()
#阈值的模式,将小于某一阈值的特征归类为-1
if threshIneq=='lt':#less than
retArray[dataMatrix[:,dimen]<=threshVal]=-1.0
#将大于某一阈值的特征归类为-1
else:#greater than
retArray[dataMatrix[:,dimen]>threshVal]=-1.0
return retArray
#单层决策树生成函数
#D为各样本权重,shape=(m,1);label为样本标签,shape=(1,m)
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']:
threshVal=rangeMin+float(j)*stepSize
#选定阈值后,调用阈值过滤函数分类预测
predictedVals=\
stumpClassify(dataMatrix,i,threshVal,inequal)
#初始化错误向量
errArr=mat(ones((m,1)))
#将错误向量中分类正确项置0
errArr[predictedVals==labelMat]=0
#计算"加权"的错误率
weigthedError=D.T*errArr
#print ("分割特征为第{0}个,分割阈值为{1},分割方式为{2},weight error为{3}"\
# .format(i+1,threshVal,threshIneq,weightedErr))
if weigthedError<minError:
minError=weigthedError
bestClasEst=predictedVals.copy()
bestStump['dim']=i
bestStump['thresh']=threshVal
bestStump['ineq']=inequal
#返回最佳单层决策树相关信息的字典,最小错误率,决策树预测输出结果
return bestStump,minError,bestClasEst
测试一下
data=mat([[ 1. , 2.1],[ 2. , 1.1],[ 1.3, 1. ],[ 1. , 1. ],[ 2. , 1. ]])
label=[1.0,1.0,-1.0,-1.0,1.0];D=mat(ones((5,1))/5)
buildStump(array(data),array(label),D)
得到如下结果,表示最佳分类特征为第1个特征,分割阈值为1.3,分割方式为"将<阈值的样本归为-1类":
02 利用AdaBoost提升单层决策树效果
- 逻辑
- 样本初始权重D=1/m
- 进入循环:(设置循环次数上限or错误率e=0为止)
- 将D带入buildStump,训练最佳决策树,得到本次迭代的最佳分类bestClass
- 计算汇集的分类结果aggClass(即,将每次最佳决策树分类结果bestClass*alpha相加)
- 根据当前的汇集分类错误率e,计算alpha,并更新样本权值D
- 直到达到循环次数上限或错误率e=0为止
- 输出弱分类器集合(bestStump的集合)
根据这样的逻辑,给出如下函数。
#label为样本标签,shape=(1,m)
def adaBoostTrainDS(dataArr,classLabels,numIt=40):
weakClassArr=[];m=shape(dataArr)[0]
D=mat(ones((m,1))/m) #初始化样本权重
aggClassEst=mat(zeros((m,1)))
for i in range(numIt):
bestStump,error,classEst=buildStump(dataArr,classLabels,D)
alpha = float(0.5*log((1.0-error)/max(error,1e-16))) #max(error,1e-16)防止0误差的计算溢出
bestStump["alpha"]=alpha
weakClassArr.append(bestStump)
#print ("D:",D.T,"\n","predClass:",classEst.T)
#为下一次迭代更新D(很关键,矩阵运算容易写错!)
expon=multiply(-1*alpha*mat(classLabels).T,classEst) #shape(5,1)*shape(5,1)对应元素相乘,得到各样本的-alpha*yi*G(xi)
D=multiply(D,exp(expon)) #shape(5,1)*shape(5,1)对应元素相乘,
D=D/D.sum() #得到各样本更新后的wi
#计算汇集的分类结果aggClassEst(即,将每次最佳决策树分类结果bestClass*alpha相加)
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,errorRate,aggClassEst
测试一下
data=mat([[ 1. , 2.1],[ 2. , 1.1],[ 1.3, 1. ],[ 1. , 1. ],[ 2. , 1. ]])
label=[1.0,1.0,-1.0,-1.0,1.0];D=mat(ones((5,1))/5)
adaBoostTrainDS(array(data),array(label))
输出结果如下,当叠加到第三个弱分类器时,分类错误率降为了0:
03 使用adaboost+单层决策树模型分类数据
刚才我们写好了模型,可以利用训练集结合我们的代码来训练若干个弱分类器了,当得到弱分类器集合之后,我们就可以利用这些弱分类器对测试集数据进行分类了。
分类原理是,将测试集特征带入各个弱分类器中,得到一系列预测值,然后将这些 预测值 X 对应弱分类器的alpha,再求和,就可以得到这些弱分类器对测试集样本的累加预测值了
def adaClassify(datToClass,classifierArr):
dataMatrix=mat(datToClass)
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'])
#对各个分类器的预测结果进行加权累加
aggClassEst+=classifierArr[i]['alpha']*classEst
#print('aggClassEst',aggClassEst)
#通过sign函数根据结果大于或小于0预测出+1或-1
return sign(aggClassEst)
04 使用自定义AdaBoost预测疝病马是否存活
- 步骤:
- 读取数据、处理数据
- 使用adaBoostTrainDS训练弱分类器
- 使用adaClassify预测疝病马是否存活
def horseColic(numWeakClassifier):
#读取训练集、测试集
dataArr,labelArr=loadDataSet(r'D:\DM\python\data\MLiA_SourceCode\machinelearninginaction\Ch07\horseColicTraining2.txt')
dataTest,labelTest=loadDataSet(r'D:\DM\python\data\MLiA_SourceCode\machinelearninginaction\Ch07\horseColicTest2.txt')
#训练弱分类器
classifierArr,trainErrorRate=adaBoostTrainDS(dataArr,labelArr,numWeakClassifier)
#用训练好的弱分类器分类测试样本
predictLabels=adaClassify(dataTest,classifierArr)
#计算分类准确率
errArr=mat(ones((len(labelTest),1)))
errArr[predictLabels==mat(labelTest).T]=0
testErrorRate=errArr.sum()/len(labelTest)
return numWeakClassifier,trainErrorRate,testErrorRate
测试一下,下面设置了训练不同个数的弱分类器
for i in [1,10,100,500,1000]:
numWeakClassifier,trainErrorRate,testErrorRate=horseColic(i)
print("=".center(70,"="))
print("弱分类器个数为{0}时,训练集分类错误率为{1}%,测试集分类错误率为{2}%"\
.format(i,round(trainErrorRate*100,2),round(testErrorRate*100,2)))
print("=".center(70,"="))
我们来看看效果,
可以看到,并不是弱分类器越多越好,太多容易过拟合,使得测试集准确率较低,太少容易欠拟合,使得训练集准确率较低,因此adaboost的弱分类器个数是一个需要斟酌的参数
ROC曲线
最后,我们来看一下本次预测的ROC曲线,看看我们训练的这个分类器是不是一个“好医生”。
可以看到,本次训练的模型,AUC达到了0.895,图中表现为红色曲线,可以说这个模型是一个比较好的医生了,诊断结果还是比较靠谱的,只要选取合适的阈值,如A点,可以实现较低的漏诊率(1-y轴)和误诊率(x轴)。
05 总结
本文根据AdaBoost原理,给出基于python3的自定义函数实现,然后用一组疝病马的数据测试了算法。
06 参考
- 《机器学习实战》 Peter Harrington Chapter7
- 《统计学习方法》 李航 Chapter8