第七章 集成学习
1.集成分类器
集成学习顾名思义就是集合成果进行学习,也被称为元算法,它将其他原始算法进行组合,经过组合后的算法会比单纯的一种算法效果要好得多。这对“弱学习器”(用于构建强学习器的基本预测模型)来说尤为明显,使用集成方式时会有很多形式,比如不同算法的集成、同种算法不同设置的集成、数据集的部分分给不同分类器的集成等,集成学习示意图如下:
集成学习大致可以分成两类,一种是不存在强依赖关系可并行生成的方法,如bagging,另一种是个体学习器间存在强依赖关系而必须串行的序列化,比如boosting
bagging
自举汇聚法(bootstrap aggregating),通过将多个弱学习器(通常是决策树)组合成一个强学习器,以提高整体预测性能,Bagging的工作机制可以概括为以下步骤:
- 自助采样:从原始训练数据集中使用有放回地进行随机采样,生成多个大小相同的训练子集。这意味着某些样本可能在一个子集中出现多次,而其他样本可能被完全排除
- 弱学习器训练:对于每个训练子集,使用相同的弱学习算法(例如决策树)训练一个独立的弱学习器
- 弱学习器预测:对于每个训练得到的弱学习器,使用测试数据集进行预测
- 预测集成:根据任务类型,对弱学习器的预测结果进行组合。对于分类问题,可以使用投票的方式,选择预测结果中出现最频繁的类别作为最终预测结果。对于回归问题,可以对弱学习器的预测结果进行平均,得到最终的预测结果
boosting
与bagging类似,不同之处是提升(Boosting)通过串行训练而获得,每个新分类器根据已有的分类器性能来训练。bagging中的分类器权重相等,boosting则是根据上一轮迭代的成功度来分配权重。它通过将多个弱学习器(通常是决策树)组合成一个强学习器,以提高整体预测性能,Boosting的工作机制可以概括为以下步骤:
- 初始化权重:将训练数据集中的每个样本赋予一个初始权重,使它们等权重或按照均匀分布进行赋值
- 弱学习器训练:使用当前样本权重下的训练数据,训练一个弱学习器(例如决策树)。弱学习器被设计为在某种程度上优化预测任务
- 计算误差:使用弱学习器对训练数据进行预测,并计算预测错误的样本权重之和。错误率越高的样本,在后续的训练中将被赋予更高的权重
- 更新样本权重:根据误差率调整样本的权重,提高错误样本的权重,降低正确样本的权重。这样,弱学习器在下一轮训练中将更关注先前错误分类的样本
- 弱学习器组合:重复步骤2-4,迭代训练出一系列的弱学习器。每个弱学习器的训练过程都会根据样本的权重进行调整,以侧重于先前被错误分类的样本
- 预测集成:将所有弱学习器的预测结果进行组合,可以采用加权投票或加权求和的方式。组合后的结果形成最终的强学习器,用于进行预测
2.AdaBoost
作为boosting中的优秀范例,AdaBoost(Adaptive Boosting)通过迭代训练一系列弱学习器和多个实例构建强分类器,它的运行过程如下:
首先初始化样本权重,将训练数据集中的每个样本赋予一个初始权重 α \alpha α,使它们等权重或按照均匀分布进行赋值,形成权重向量 D D D。然后再迭代训练弱学习器,即使用当前样本权重下的训练数据,训练一个弱学习器(例如决策树),之后使用弱学习器对训练数据进行预测,并计算预测结果与实际标签不一致的样本权重之和。通过这个误差率,计算弱学习器的权重,误差率较低的学习器会被赋予更高的权重,其中误差率 ε \varepsilon ε定义为 ε = 未分类正确的样本数 所有样本数 \varepsilon=\frac{未分类正确的样本数}{所有样本数} ε=所有样本数未分类正确的样本数之后根据弱学习器的权重和预测结果更新样本权重向量,也就是将错误分类的样本的权重增加,而正确分类的样本的权重减小。权重 α \alpha α计算公式为 α = 1 2 ln ( 1 − ε ε ) \alpha=\frac{1}{2}\ln\left(\frac{1-\varepsilon}{\varepsilon}\right) α=21ln(ε1−ε)计算被正确和错误分类的更新权重向量 D D D公式分别为 D i t + 1 = D i t e − α S u m ( D ) D_i^{t+1}=\frac{D_i^{t}e^{-\alpha}}{Sum(D)} Dit+1=Sum(D)Dite−α D i t + 1 = D i t e α S u m ( D ) D_i^{t+1}=\frac{D_i^{t}e^{\alpha}}{Sum(D)} Dit+1=Sum(D)Diteα在计算出权重向量 D D D之后重复迭代直到失误率为0或弱分类器数量达到指定值为止
3.基于单层决策树的弱分类器
现在构造一个单层决策树(decision stump),由于只有一次分裂,因此只是个树桩,现在构造一个简单的数据集
def loadSimpData():
datMat = matrix([[ 1. , 2.1],
[ 2. , 1.1],
[ 1.3, 1. ],
[ 1. , 1. ],
[ 1.5, 1.6]])
classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
return datMat,classLabels
可视化后的结果如下
接下来构单层决策树,其中stumpClassify
函数接受输入数据矩阵dataMatrix
,维度dimen
,阈值threshVal
和阈值不等式threshIneq
,根据阈值将样本进行分类,并返回分类结果。
buildStump
函数接受输入数据集dataArr
、类别标签classLabels
和样本权重 D,通过迭代寻找最佳的单层决策树(最佳的弱学习器),并返回最佳决策树的相关信息(维度、阈值和不等式)、最小错误率和最佳分类结果
在 buildStump
函数中,通过遍历所有特征维度和可能的阈值,对每个维度和阈值组合进行分类,并计算加权错误率。选择加权错误率最小的分类结果作为最佳决策树的划分,并将相关信息保存到bestStump
中
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
# 将返回结果初始化为全部为1的数组
retArray = ones((shape(dataMatrix)[0], 1))
if threshIneq == 'lt':
# 根据阈值进行分类,小于等于阈值的样本标签设为-1.0
retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
else:
# 根据阈值进行分类,大于阈值的样本标签设为-1.0
retArray[dataMatrix[:, dimen] > threshVal] = -1.0
return retArray
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) # 调用stumpClassify进行分类
errArr = mat(ones((m, 1)))
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
结果如下,显示了最佳决策树的相关信息(维度、阈值和不等式)、最小错误率和最佳分类结果,其中buildStump
的样本权重
D
=
m
a
t
(
o
n
e
s
(
(
5
,
1
)
)
/
5
)
D=mat(ones((5,1))/5)
D=mat(ones((5,1))/5)这就是一个单层决策树,即所谓的弱学习器,下面使用多个弱分类器构建AdaBoost
4.完整AdaBoost实现
在已经构建了一个弱分类器后,进行完整的AdaBoost实现
def adaBoostTrainDS(dataArr, classLabels, numIt=40):
weakClassArr = [] # 存储弱学习器的列表
m = shape(dataArr)[0]
D = mat(ones((m, 1)) / m) # 初始化权重D为均等分布
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))) # 计算alpha值,避免除零错误
bestStump['alpha'] = alpha
weakClassArr.append(bestStump) # 将弱学习器的信息存储到列表中
print("classEst: ", classEst.T)
expon = multiply(-1 * alpha * mat(classLabels).T, classEst) # 计算指数项用于更新权重D
D = multiply(D, exp(expon)) # 更新权重D
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 # 如果累计错误率为0,提前结束训练
return weakClassArr
adaBoostTrainDS
函数首先初始化弱学习器列表(weakClassArr)、样本数量(m)、权重向量(D),以及累计类别估计值(aggClassEst),再迭代训练指定次数(numIt),在当前样本权重(D)下,使用"buildStump"函数构建最佳弱学习器(bestStump)。计算当前弱学习器的权重(alpha),其中alpha值表示该弱学习器在最终分类器中的重要性。将alpha值存储在bestStump字典中,并将bestStump添加到弱学习器列表(weakClassArr)中。更新样本权重(D)以便下一轮迭代。累计当前弱学习器的类别估计值,并计算累计错误率。如果累计错误率为0,提前结束训练。最后返回弱学习器列表(weakClassArr)
结果发现只用了一次迭代失误率就为0了,观察得到的classifierArray
数组,其中包含了分类的信息,分类器构建完毕。之后使用下列代码将弱分类器进行抽离并加权求和
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)
return sign(aggClassEst) # 根据累计类别估计值进行分类,返回分类结果
adaClassify
函数首先将待分类数据转换为矩阵形式,并初始化累计类别估计值为零向量。然后,通过遍历弱学习器列表,依次对待分类数据使用弱学习器进行分类,并将每个弱学习器的类别估计值乘以其权重alpha后累加到累计类别估计值中。在每次累加后,打印出累计类别估计值。最后,函数根据累计类别估计值的正负号进行分类
实现效果如上,在第一次迭代中,初始样本权重D的值为[0.2 0.2 0.2 0.2 0.2],弱学习器的类别估计值为[1. 1. -1. -1. 1.],累计类别估计值为[18.42068074 18.42068074 -18.42068074 -18.42068074 18.42068074]。累计错误率为0.0,即分类结果完全正确。在第二次迭代中,根据弱学习器的权重alpha和类别估计值进行累加计算,得到累计类别估计值为[-18.42068074]。最后,根据累计类别估计值的正负号进行分类,得到分类结果为-1
5.示例
之前曾用逻辑回归来预测患病马的存活率,这里使用多个单层决策树和AdaBoost进行预测
大致流程
- 收集数据:提供的文本文件
- 准备数据:确保分类的结果是1和-1
- 分析数据:检查数据的完整性
- 训练算法:利用
adaBoostTrainDS
函数训练分类器 - 测试算法并使用算法
实操
首先加载数据集
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
先是计算训练集上的失误率,再用训练集进行预测之后发现错误率只有20%,比之前的30%错误率要好得多
6.非均衡分类问题
之前的分类都是基于数据集类别分类代价一样,而实际情况往往不同,例如,在医学诊断中,将一个健康的病人误分类为患有疾病可能会造成更严重的后果,它的代价也更大
分类性能度量指标
在之前的算法中,仅使用了错误率这一个单一指标来衡量成功度,但比如对于不同的代价的分类来说,仅使用错误率就不够了。而混淆矩阵就是专用于机器学习的适用工具。混淆矩阵是一种展示分类器性能的表格形式,它将实际类别和预测类别进行对比。混淆矩阵包括四个重要的指标:真正例(True Positive, TP)、真反例(True Negative, TN)、假正例(False Positive, FP)和假反例(False Negative, FN)。这些指标反映了分类器在正确分类和错误分类方面的表现
由混淆矩阵就可以得到更加优秀的指标:
- 正确率:正确率衡量了分类器预测为正例的样本中,实际为正例的比例。正确率越高,表示分类器将负例误判为正例的能力越低,它等于 T P / ( T P + F P ) TP/(TP+FP) TP/(TP+FP)
- 召回率:召回率衡量了分类器正确预测为正例的样本占实际正例样本的比例。召回率越高,表示分类器将正例正确预测的能力越强,它等于 T P / ( T P + F N ) TP/(TP+FN) TP/(TP+FN)
- ROC曲线:ROC曲线是一种用于可视化分类器性能的工具,它绘制了分类器在不同阈值下的正确率和召回率之间的关系。ROC曲线可以帮助我们判断分类器在不同阈值下的表现,并选择最佳的阈值来平衡正确率和召回率
- ROC曲线的横轴表示假正例率(FPR),即被错误地预测为正例的负例样本占所有负例样本的比例
- ROC曲线的纵轴表示真正例率(TPR),即被正确地预测为正例的正例样本占所有正例样本的比例
- AUC是ROC曲线下的面积,AUC值越大,表示分类器性能越好
上图中的实线是 ROC 曲线,表示分类器在不同阈值下的真正例率(True Positive Rate, TPR)和假正例率(False Positive Rate, FPR)之间的关系。ROC 曲线越靠近左上角,表示分类器的性能越好。虚线是基准线,也称为随机猜测线或对角线。它是一条从原点 (0,0) 到 (1,1) 的对角线,代表了完全随机猜测的情况。在这种情况下,TPR 和 FPR 的比例是相等的,分类器无法区分正例和负例。通过将实线与虚线进行对比,可以更好地评估分类器的性能。如果 ROC 曲线位于虚线的上方,表示分类器的性能优于随机猜测;如果 ROC 曲线位于虚线下方,则分类器的性能不如随机猜测。此外,ROC 曲线下的面积(AUC)也可以作为评估分类器性能的指标,AUC 值越大,表示分类器的性能越好
以下是一个生成ROC曲线的代码
def plotROC(predStrengths, classLabels):
import matplotlib.pyplot as plt
cur = (1.0,1.0)
ySum = 0.0
numPosClas = sum(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]
#draw line from cur to (cur[0]-delX,cur[1]-delY)
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.xlabel('False positive rate'); plt.ylabel('True positive rate')
plt.title('ROC curve for AdaBoost horse colic detection system')
ax.axis([0,1,0,1])
plt.show()
print("the Area Under the Curve is: ",ySum*xStep)
其他方法
除了调节分类器阈值,还有其他方法处理非均衡分类的代价。
如代价敏感的学习:代价敏感的学习通过引入代价矩阵(Cost Matrix)或代价权重(Cost Weight)来解决这个问题。代价矩阵是一个二维矩阵,其中行表示真实类别,列表示预测类别。矩阵中的每个元素表示将真实类别分为预测类别所造成的代价。代价权重是为每个类别分配的一个代价值,用于衡量将该类别错误分类的代价
或通过欠抽样和过抽样来实现:欠抽样是通过减少多数类别的样本数量来平衡数据集。这意味着从多数类别中删除一些样本,使得多数类别的样本数量与少数类别相近。欠抽样的目标是通过减少多数类别的样本来提高分类器对少数类别的识别能力。过抽样是通过增加少数类别的样本数量来平衡数据集。这可以通过复制少数类别的样本、生成合成样本或从多数类别中生成类似样本的方式实现。过抽样的目标是增加少数类别的样本以提高其在训练中的重要性,从而使分类器更好地识别少数类别