本文参考于《机器学习实战》和《机器学习》
在此之前一共介绍了五种分类算法,分别为KNN、决策树、朴素贝叶斯、逻辑回归、支持向量机,可以看到每一种算法都有各自的优缺点,以及适合的数据集。集成学习方法可以将不同分类算法构建的分类器组合在一起,更加高效准确的分类。
使用集成学习方法时可以有多种形式:可以是不同算法的集成,也可以是同一算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。
对于集成方法中分类器的组合也可以将集成分为“同质”和“异质”。例如“决策树集成”中所有的分类器都为决策树,同质集成中的个体分类器可以称作“基分类器”;而“异质”就是集成中包含不同分类算法,异质集成中的个体分类器常称为“组件分类器”。
集成学习通过将若干个弱学习器组合成一个强分类器,这个强分类器可以获得比单一学习器更加显著的准确率。弱分类器常指准确率略优于随机猜测的学习器,比如在二分类问题上准确率略高于50%的分类器,在同质集成学习中基分类器有时也可以直接称为弱分类器。
集成学习方法大致可分为Bagging和Boosting两种方法,这两种方法的思想都是将多种分类和回归算法通过某种方式组合在一起,从而得到一个强分类器。下面分别介绍这两种方法,比较两种方法的异同。
可以将Bagging方法的主要原理总结成以下几点:
抽样:在原始数据中有放回的抽取N次得到一个新的采样数据集。新数据集与原始数据集大小相等,由于是有放回抽样,所以允许采样数据集中有重复值。
训练:总共选取M个采样数据集,每一个数据集对应一种学习算法,从而可以得到M个分类器,利用这些分类器对数据进行分类。
票选:M个分类器会得到M个分类结果,所以最后通过投票将分类结果中最多的类别作为最终的分类结果。
随机森林就是Bagging方法的典型代表,它是由很多棵决策树与Bagging方法思想相结合而成。
Boosting方法与Bagging方法很类似,但前者主要是通过将权重与串行迭代结合的方式进行学习,Boosting更加关注每次分类结束后被分错的样本,在下次分类之前,Boosting会增大分错样本的权重,这样下一个分类器在训练时就会更加注重对分错样本的训练。
在决定最终的分类时,由于每一轮分类权重都会发生改变,所以Boosting方法通过加权求和的方式得到分类结果。Boosting方法的典型代表为Adaboost,它是由很多棵决策树与Boosting方法思想相结合而成,也被称作提升树,除此之外还有GBDT和XGBoost。下文会给出以AdaBoost的流程图,其原理就是Boosting的主要思想。
两种算法虽然思想上都是通过将若干个弱分类器组合成一个强分类器,让最终的分类准确率得到提升,但是二者也是有很大区别的,具体如下:
Bagging在原始数据集中以随机放回抽样的方式得到新采样集;而Boosting每一轮的训练集都为原始数据集,只是训练集中每个样本的权重在发生变化。
Bagging是采用并行的方式学习,每个分类器学习时互不影响;而Boosting是采用串行的方式学习,上一次训练结果会影响下一次训练集中样本的权重。
Bagging由于每个分类器权重都一致,所以在判定分类最终结果时采用投票式;而Boosting每个分类器都有相应的权重,所以在判定分类最终结果时采用加权求和的方式。
AdaBoost的全称为adaptive boosting(自适应boosting),可以将其运行过程归结为以下几点:
训练数据集中的每个样本都会被赋予一个相同的权重值,这些权重会构成一个向量,暂称为D。
分类器在训练数据集之后会计算出该分类器的错误率,即分错样本的概率,并且依据错误率更新分类器的权重。
在下一个分类器训练之前,会依据上一个分类器的权重调整每一个样本的权重,上一次分对的样本权重降低,分错的样本权重提高。
迭代次数是人为设置的,在达到迭代次数或者满足某个条件后,以加权求和的方式确定最终的分类结果。
1、样本权重
对于一个含有N个样本的数据集,在初始化训练数据的权重分布式,每个样本的权重都应该是相等的。
2、错误率
如果我们将基于不同权重 D ( M ) D(M) D(M)训练出的分类器称作 h M ( x ) h_M(x) hM(x),每个样本实际对应类别为 f ( x ) f(x) f(x),则错误率 ϵ \epsilon ϵ的定义如下: ϵ = 未 正 确 分 类 的 样 本 数 目 所 有 样 本 数 目 \epsilon=\frac{未正确分类的样本数目}{所有样本数目} ϵ=所有样本数目未正确分类的样本数目 ϵ M = P ( h M ( x ) ≠ f ( x ) ) \epsilon_M=P(h_M(x)\neq f(x)) ϵM=P(hM(x)=f(x))
3、分类器权重
除样本权重外,每一个分类器也会有一个权重 α M \alpha_M αM: α M = 1 2 l n ( 1 − ϵ M ϵ M ) \alpha_M=\frac{1}{2}ln(\frac{1-\epsilon_M}{\epsilon_M}) αM=21ln(ϵM1−ϵM)
可以看下面这张流程图:
左边是数据集,黑条的长度代表每个样本的权重,每一个分类器训练之后后,都会得到一个分类器权重 α \alpha α,最后所有分类器加权结果(三角形)会在圆形中求和,以得到最终的分类结果。
4、更新权重D
在下次训练之前,为了使正确分类的样本权重降低而错分样本的权重提高,需要对权重向量D更新:
在两个式子合并时,这里一般针对二分类问题,即类别标签为+1和-1。当分类器预测结果和真实类别相同时,二者乘积为+1,不同时,二者乘积为-1。其中 Z t Z_t Zt称作规范化因子,它的作用就是确保 D M + 1 D_{M+1} DM+1是一个概率分布。
5、最终分类器输出
可以利用每个弱分类器的线性组合达到加权求和的目的,并且通过符号函数确定最终的分类结果。 H ( x ) = s i g n ( ∑ m = 1 M α m h m ( x ) ) H(x)=sign(\sum_{m=1}^M\alpha_m h_m(x)) H(x)=sign(m=1∑Mαmhm(x))
可以看到AdaBoost的流程图中有一个“结合策略”,顾名思义,就是通过某种策略将所有的弱分类器的分类结果结合在一起,继而判断出最终的分类结果。结合策略可分为三类:平均法、投票法和学习法。
对于数值型的回归预测问题,最常见的结合策略是使用平均法,平均法又可以分为简单平均法和加权平均法。
假设有n个弱分类器 ( h 1 ( x ) , h 2 ( x ) , . . . , h n ( x ) ) (h_1(x),h_2(x),...,h_n(x)) (h1(x),h2(x),...,hn(x)),简单平均法公式如下: H ( x ) = 1 n ∑ i = 1 n h i ( x ) H(x)=\frac{1}{n}\sum_{i=1}^nh_i(x) H(x)=n1i=1∑nhi(x)
加权平均法公式如下: H ( x ) = ∑ i = 1 n w i h i ( x ) H(x)=\sum_{i=1}^nw_ih_i(x) H(x)=i=1∑nwihi(x)
其中 w i w_i wi是弱分类器 h i ( x ) h_i(x) hi(x)的权重,通常要求 w i ≥ 0 w_i\geq0 wi≥0, ∑ i = 1 n w i = 1 \sum_{i=1}^nw_i=1 ∑i=1nwi=1。可以看出简单平均法是加权平均法令 w i = 1 n w_i=\frac{1}{n} wi=n1的特例。
对分类的预测,通常使用的是投票法,投票法可以分为相对多数投票法、绝对多数投票法和加权投票法。
相对多数投票法就是常提及的少数服从多数,在预测结果中选择出现次数最多的类别作为最终的分类类别,如果不止一个类别获得最高票数,则在其中随机选取一个做最终类别。
绝对多数投票法就是在相对多数投票法的基础上进行改进,不光要求要获得最高票数,并且票数还要超过50%,否则拒绝预测。
加权投票法与加权平均法类似,即每个若分类器得到的分类票数要乘以一个权重,最终将各个类别的票数加权求和,最大值对应类别作为最终分类类别。
学习法相对于前两种方法较为复杂,但得到的效果可能会更优,学习法的代表是Stacking,其整体思想是在弱分类器之后再加上一个分类器,将弱分类器的输出当成输入再次训练。
我们将弱分类器称作初级分类器,将用于结合的分类器称作次级分类器,利用学习法结合策略时,先通过初级分类器对数据集训练,然后再通过次级分类器在训练一次,才会得到最终的预测结果。
单层决策树是一种简单的决策树,它仅仅基于单个特征来做决策,也就是得到一个信息增益之后会舍弃其他特征,所以单层决策树只有一次分裂过程,实际上也是一个树桩。
之所以用单层决策树来构建弱分类器,是因为在下面这份数据集中,单层决策树能更好的体现"弱"这个性质。
为什么这么说呢?在某个坐标轴上选择一个值,如果要利用垂直坐标轴并且经过该值的一条直线将两类样本分隔开,这显然是不可能的。但是可以通过多次划分,即通过使用多棵单层决策树构建出一个完全正确分类的分类器。
我们暂且将上图样本的分类规定为正类和负类,那么在划分数据集时候可以给定相应的阈值,如下图:
假如选取X1特征的阈值为1.4,可以规定小于这个阈值的样本为负类,相应大于这个阈值的样本为正类;但规定小于阈值的样本为正类,大于为负类也同样合理,所以对于一个特征有两种情况,这也是下面这个函数中唯一比较别扭的点。
def buildStump(dataMatrix, classLabels, D):
labelMat = np.mat(classLabels).T # 标签列表矩阵化并转置
m, n = np.shape(dataMatrix) # 获取输入数据矩阵行数和列数
numSteps = 10.0 # 阈值改变次数
bestStump = {} # 用于存树的空字典
bestClasEst = np.mat(np.zeros((m, 1)))
minError = float('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): # 阈值改变次数
# lt(less than)指小于该阈值,分类为-1
# gt(greater than)指大于该阈值,分类为-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 # 将分类正确的样本赋值为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
这部分代码中需要了解以下点:numSteps就是遍历某个特征所有取值的次数;bestStump这个字典用来存储最佳单层决策树的相关信息;minError为最小误差,暂定正无穷大,每次循环都会更新。
三层for循环是关键部分,首先第一层是遍历所有特征,并且设置了每次阈值改变的长度;第二层是以步长为基础,遍历特征上所有可取值;第三层则是小于和大于之间切换不等式。
在第三层循环内调用了一个函数,基于给定变量,会返回分类预测结果。首先创建了一个全为1的矩阵,如果预测结果和真实结果一致则对应位置调整为0,并且利用这个矩阵和权重矩阵的转置相乘可得到一个新的误差,接着会通过比较更新最小误差,并储存最佳单层决策树的相关信息,即特征、阈值和(lt or gt)。
第三层循环调用函数如下:
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):
# 创建一个与输入矩阵行数相同、列数为1的矩阵
retArray = np.ones((np.shape(dataMatrix)[0], 1))
# 如果小于阈值,分类为-1
if threshIneq == 'lt':
retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
# 如果大于阈值,分类为-1
else:
retArray[dataMatrix[:, dimen] > threshVal] = -1.0
return retArray
这个函数非常简单,就是之前提及两种情况,小于阈值将其分类为-1或者大于阈值时将其分类为-1都是合理情况,所以需要遍历考虑这两种情况。
可以看到对于第一个特征来说最小误差为0.2,也就是说垂直于x轴划分,最优情况下会分错一个样本,下面利用AdaBoost方法将多个单侧决策树结合在一起,看一下样本是否能全部分类正确。
前文已经给出了AdaBoost运行流程及所需公式,只需要设定弱分类器的个数或者称为迭代次数,每个弱分类器都实现一遍流程即可,代码部分如下:
def adaBoostTrainDS(dataArr, classLabels, numIt = 40):
weakClassArr = []
m = np.shape(dataArr)[0]
D = np.mat(np.ones((m, 1)) / m) # 初始化权重
aggClassEst = np.mat(np.zeros((m,1))) #初始化一个值为0的行向量
for i in range(numIt): # 在给定次数内迭代
bestStump, error, classEst = buildStump(dataArr, classLabels, D) # 构建单层决策树
alpha = float(0.5 * np.log((1.0 - error) / max(error, 1e-16))) # 计算弱分类器的权重
bestStump['alpha'] = alpha # 将弱分类器权重存入树中
weakClassArr.append(bestStump) # 将最佳单层决策树存入一个列表
# 更新样本权重
expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)
D = np.multiply(D, np.exp(expon))
D = D / D.sum()
# 计算AdaBoost的误差
aggClassEst += alpha * classEst
aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T, np.ones((m,1)))
errorRate = aggErrors.sum() / m
# 如果误差为0,则退出循环
if errorRate == 0.0:
break
return weakClassArr, aggClassEst
首先对所有样本初始化一个权重D,在设置的迭代次数内,得到存储最佳单层决策树相关信息的字典、最小误差、预测结果。在最小误差的基础上更新分类器权重alpha,依据公式当最小误差为0时,程序会发生报错,即除数为0,利用max方法即可防止这种情况发生。
然后依据alpha更新样本的权重,而expon中的classEst也就是上文公式中弱分类器预测结果 h M ( x ) h_M(x) hM(x),classLabels也就是真实结果 f ( x ) f(x) f(x)。
因为我们无法猜得一个数据集需要几棵单层决策树才能完全正确分类,如果迭代次数过多,则会发生过拟合的现象,分类反而不准了,所以上述代码最后这部分就是为了防止迭代次数过多,在误差为0时即使未达到迭代次数也退出迭代。
这里运用了上文公式的 H ( x ) = s i g n ( ∑ m = 1 M α m h m ( x ) ) H(x)=sign(\sum_{m=1}^M\alpha_m h_m(x)) H(x)=sign(m=1∑Mαmhm(x))
先计算每个弱分类器预测类别的估计值,累加后再利用符号函数进行分类。这里科普一下sign方法,当值大于0时返回1.0,当值小于0时返回-1.0,当值等于0时返回0,知道了sign方法,这部分代码就很容易理解。
最后AdaBoost方法通过三个最佳单层决策树实现了对数据集的完全正确分类,比如下面这种切分方式:
可能表达的比较抽象,但是在理解的时候你要抛开一个刀就能划分的思想,我们利用的是集成方法求解分类问题,这是三刀组合在一起才得到的完全正确分类结果。
算法模型已经建成了,所以需要对该模型进行测试,代码的思路比较简单,就是遍历所有训练得到的弱分类器,计算每个弱分类器预测类别的估计值,累加后再通过符号函数对类别进行判断,代码如下:
def adaClassify(datToClass,classifierArr):
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
return np.sign(aggClassEst)
本身我们的训练数据集就非常小,所以不会计算模型准确率,只是输入两个样本对其类别进行判断,这里样本选择[[5,5],[0,0]],运行截图如下:
可以看到,随着迭代次数的增加,类别的估计值的绝对值越来越大(离0越来越远),因为最后是利用符号函数对分类结果进行判断,所以这也代表着输入样本的分类结果越来越强。
AdaBoost的优点就是分类的准确率高,与其他复杂算法相比更容易编码,并且可以应用在大部分分类器上,没有什么参数需要调整。缺点就是对离群点敏感,因为离群点一般都是错误数据,往往应该被舍弃,对于这类样本分类器很容易分错,而分错的样本的下次迭代时权重会增加,分类器对这类样本会更加重视,就可能会导致一错再错。
关注公众号【奶糖猫】后台回复"AdaBoost"可获取源码供参考,感谢阅读。