由于不同的分类器具有不同的性质,对数据的分类方法也不同。因此,我们可以认为使用多个分类器对数据进行分类,并通过某种方式来得出最终结果的分类方法要比使用单个分类器的性能要好:比如在单分类器常遇到的过拟合问题,使用差别显著的多个分类器,就能够有效缓解这个问题。
本章介绍了bagging和boosting两种集成方法。二者的区别在于前者给予每个样例和预测函数相同的权重,且有放回的从原始集中抽取训练集;而后者则使用相同的训练集,根据前一轮的训练结果,给予错误率高的样例和正确率高的预测函数更高的权重。
使用这两种方法,将不同的单分类器分类算法构成框架时,会产生不同的分类方法,本章介绍的为adaboost算法。它的算法原理用一张图就可以简单表示:
当构建好分类器后,我们就可以开始训练了,算法的思想是,每轮迭代时首先找到最佳的单层决策树(错误率最小),将其加入决策树组。计算各个样例和决策树的错误率,修改其权重。计算总体错误率(每个决策树权重*分类结果输入sign函数作为总体预测结果),若其为0或达到最大迭代次数,则结束。代码如下:
import numpy as np
def stumpClassify(dataMatrix,dimen,threshVal,threshIneq): #单个决策树分类
retArray = np.ones((np.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 = np.mat(dataArr)
labelMat = np.mat(classLabels).T
m,n = np.shape(dataMatrix)
numSteps = 10.0
bestStump = {}
bestClassEst = np.mat(np.zeros((m,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 ['lt','gt']:
threshVal = (rangeMin + float(j) * stepSize)
predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)
errArr = np.mat(np.ones((m,1)))
errArr[predictedVals == labelMat] = 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
def adaBoostTrainDS(dataArr,classLabels,numIt = 10000): # adaboost方法提升分类器性能,numit为迭代次数
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)
print("D:",D.T)
alpha = float(0.5 * np.log((1.0 - error)/max(error,1e-16)))
bestStump['alpha'] = alpha
weakClassArr.append(bestStump)
print("classEst:",classEst.T)
expon = np.multiply(-1 * alpha * np.mat(classLabels).T,classEst)
D = np.multiply(D,np.exp(expon))
D = D/D.sum()
aggClassEst += alpha * classEst
print("aggClassEst:",aggClassEst.T)
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
def adaClassify(datToClass,classifierArr): #adaboost分类函数
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)
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
if __name__ == '__main__':
dataArr,LabelArr = loadDataSet('horseColicTraining2.txt')
weakClassArr,aggClassEst = adaBoostTrainDS(dataArr,LabelArr)
testArr,testLabelArr = loadDataSet('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))
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))
最终结果如下:
有些时候我们对于不同分类的结果,其代价是不同的:例如将癌症患者分类为正常人与将正常人分类为癌症患者。那么如何在这种条件下度量分类器的性能呢?这个问题称为非均衡分类问题。该问题的一个解决方法为定义一个混淆矩阵:
并定义以下几个概念:
精度:Accuracy
即模型预测正确的个数/样本的总个数
(tp+tn)/样例数
正确率:Positive predictive value(PPV,Precision)
即阳性预测值,在模型预测为正类的样本中,真正的正样本所占的比例
tp/(tp+fp)
伪发现率:False discovery rate(FDR)
也是错误发现率,表示在模型预测为正类的样本中,真正的负类的样本所占的比例
fp/(tp+fp)
错误遗漏率:False omission rate(FOR)
表示在模型预测为负类的样本中,真正的正类所占的比例。即评价模型"遗漏"掉的正类的多少
fn/(fn+tn)
阴性预测值:Negative predictive value(NPV)
在模型预测为负类的样本中,真正为负类的样本所占的比例
tn/(fn+tn)
召回率:True positive rate(TPR,Recall)
真正类率,表示的是,模型预测为正类的样本的数量,占总的正类样本数量的比值
tp/(tp+fn)
假正率:False positive rate(FPR),Fall-out
表示的是模型预测为正类的样本中,占模型负类样本数量的比值
fp/(fp+tn)
假负类率:缺失率,模型预测为负类的样本中,是正类的数量,占真实正类样本的比值
fn/(fn+tn)
使用以上指标,我们就可以解决这个问题了。
另一个思路是使用ROC曲线。
先解释ROC,图中的横坐标是伪正例的比例(假阳率=FP/(FP+TN)),而纵坐标是真正例的比例(真阳率=TP/(TP+FN))。ROC曲线给出的是当阈值变化时假阳率和真阳率的变化情况。左下角的点所对应的将所有样例判为反例的情况,而右上角的点对应的则是将所有样例判为正例的情况。虚线给出的是随机猜测的结果曲线。
ROC曲线不但可以用于比较分类器,还可以基于成本效益(cost-versus-benefit)分析来做出决策。由于在不同的阈值下,不用的分类器的表现情况是可能各不相同,因此以某种方式将它们组合起来或许更有意义。如果只是简单地观察分类器的错误率,那么我们就难以得到这种更深入的洞察效果了。
在理想的情况下,最佳的分类器应该尽可能地处于左上角,这就意味着分类器在假阳率很低的同时获得了很高的真阳率。例如在垃圾邮件的过滤中,就相当于过滤了所有的垃圾邮件,但没有将任何合法邮件误识别为垃圾邮件而放入垃圾邮件额文件夹中。
对不同的ROC曲线进行比较的一个指标是曲线下的面积(Area Unser the Curve,AUC)。AUC给出的是分类器的平均性能值,当然它并不能完全代替对整条曲线的观察。一个完美分类器的ACU为1.0,而随机猜测的AUC则为0.5。
(https://cuijiahua.com/blog/2017/11/ml_10_adaboost.html)
ROC代码如下:
def plotROC(predStrengths,classLabels):
cur = (1.0,1.0)
ySum = 0.0
numPosClas = np.sum(np.array(classLabels)== 1.0)
yStep = 1/float(numPosClas)
xStep = 1/float(len(classLabels)-numPosClas)
sortedIndicies = predStrengths.argsort()
fig = plt.figure()
fig.clf()
ax = plt.subplot(111)
for index in sortedIndicies.tolist()[0]:
if classLabels[index] == 1.0:
delX = 0
delY = yStep
else:
delX = xStep
delY = 0
ySum += cur[1]
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.title('ROC曲线')
plt.xlabel("假阳率")
plt.ylabel("真阳率")
ax.axis([0,1,0,1])
print("AUC为",ySum*xStep)
plt.show()