集成学习是一大类模型融合策略和方法的统称,其中包含多种集成学习的思想。
Boosting
Boosting方法训练基分类器时采用串行的方式,各个基分类器之间有依赖。
它的基本思路是将基分类器层层叠加,每一层在训练的时候,对前一层基分类器分错的样本,给予更高的权重。测试时,根据各层分类器的结果的加权得到最终结果。
Boosting的过程很类似于人类学习的过程 (如图) ,我们学习新知识的过程往往是迭代式的,第一遍学习的时候,我们会记住一部分知识,但往往也会犯一些错误,对于这些错误,我们的印象会很深。第二遍学习的时候,就会针对犯过错误的知识加强学习,以减少类似的错误发生。不断循环往复,直到犯错误的次数减少到很低的程度。
Boosting算法的实现:
import util
import numpy as np
import sys
import random
PRINT = True
random.seed(42)
np.random.seed(42)
def small_classify(y):
classifier, data = y
return classifier.classify(data)
class AdaBoostClassifier:
"""
AdaBoost classifier.
Note that the variable 'datum' in this code refers to a counter of features
(not to a raw samples.Datum).
"""
def __init__( self, legalLabels, max_iterations, weak_classifier, boosting_iterations):
self.legalLabels = legalLabels
self.boosting_iterations = boosting_iterations
self.classifiers = [weak_classifier(legalLabels, max_iterations) for _ in range(self.boosting_iterations)]
self.alphas = [0]*self.boosting_iterations
def train( self, trainingData, trainingLabels):
"""
The training loop trains weak learners with weights sequentially.
The self.classifiers are updated in each iteration and also the self.alphas
"""
self.features = trainingData[0].keys()
size_sampled_data = int(len(trainingData))
sampled_weights = [(1.0/size_sampled_data)] * size_sampled_data
for i in range(self.boosting_iterations):
self.classifiers[i].train(trainingData, trainingLabels, sampled_weights)
error = 0
for j in range(len(trainingData)):
if self.classifiers[i].classify([trainingData[j]])[0] != trainingLabels[j]:
error = error + sampled_weights[j]
for j in range(len(trainingData)):
if self.classifiers[i].classify([trainingData[j]])[0] == trainingLabels[j]:
sampled_weights[j] = sampled_weights[j] * (error/ (1 - error))
# Normalize the sampled_weight list
sum_of_weights = sum(sampled_weights)
for p in range(len(sampled_weights)):
sampled_weights[p] = sampled_weights[p] / sum_of_weights
self.alphas[i] = np.log((1 - error) / error)
#print "training loop: ", i
def classify( self, data):
"""
Classifies each datum as the label that most closely matches the prototype vector
for that label. This is done by taking a polling over the weak classifiers already trained.
See the assignment description for details.
Recall that a datum is a util.counter.
The function should return a list of labels where each label should be one of legaLabels.
"""
prediction = []
for i in range(len(data)):
guesses = []
for j in range(self.boosting_iterations):
guesses.append(self.classifiers[j].classify([data[i]])[0] * self.alphas[j])
prediction.append(int(np.sign(sum(guesses))))
return prediction
Bagging
Bagging与Boosting的串行训练方式不同,Bagging方法在训练过程中,各基分类器之间无强依赖,可以进行并行训练。其中很著名的算法之一是基于决策树基分类器的随机森林 (Random Forest) 。为了让基分类器之间互相独立,将训练集分为若干子集 (当训练样本数量较少时,子集之间可能有交叠)。Bagging方法更像是一个集体决策的过程,每个个体都进行单独学习,学习的内容可以相同,也可以不同,也可以部分重叠。但由于个体之间存在差异性,最终做出的判断不会完全一致。在最终做决策时,每个个体单独作出判断,再通过投票的方式做出最后的集体决策 (如图)。
我们再从消除基分类器的偏差和方差的角度来理解Boosting和Bagging方法的差异。基分类器,有时又被称为弱分类器,因为基分类器的错误率要大于集成分类器。基分类器的错误,是偏差和方差两种错误之和。偏差主要是由于分类器的表达能力有限导致的系统性错误,表现在训练误差不收敛。方差是由于分类器对于样本分布过于敏感,导致在训练样本数较少时,产生过拟合。
Boosting方法是通过逐步聚焦于基分类器分错的样本,减小集成分类器的偏差。Bagging方法则是采取分而治之的策略,通过对训练样本多次采样,并分别训练出多个不同模型,然后做综合,来减小集成分类器的方差。假设所有基分类器出错的概率是独立的,在某个测试样本上,用简单多数投票方法来集成结果,超过半数基分类器出错的概率会随着基分类器的数量增加而下降。
下图是Bagging算法的示意图,Model 1、Model 2、Model 3都是用训练集的个子集训练出来的,单独来看,它们的决策边界都很曲折,有过拟合的倾向。集成之后的模型(红线所示) 的决策边界就比各个独立的模型平滑了,这是由于集成的加权投票方法,减小了方差。
Bagging算法的实现:
import util
import numpy as np
import sys
import random
PRINT = True
random.seed(42)
np.random.seed(42)
class BaggingClassifier:
"""
Bagging classifier.
Note that the variable 'datum' in this code refers to a counter of features
(not to a raw samples.Datum).
"""
def __init__( self, legalLabels, max_iterations, weak_classifier, ratio, num_classifiers):
self.ratio = ratio
self.num_classifiers = num_classifiers
self.classifiers = [weak_classifier(legalLabels, max_iterations) for _ in range(self.num_classifiers)]
def train( self, trainingData, trainingLabels):
"""
The training loop samples from the data "num_classifiers" time. Size of each sample is
specified by "ratio". So len(sample)/len(trainingData) should equal ratio.
"""
self.features = trainingData[0].keys()
sample_size = int(self.ratio * len(trainingData))
for i in range(self.num_classifiers):
random_integers = np.random.randint(len(trainingData), size = sample_size)
sampled_data = []
sampled_labels = []
for choosen_element in random_integers:
sampled_data.append(trainingData[choosen_element])
sampled_labels.append(trainingLabels[choosen_element])
self.classifiers[i].train(sampled_data, sampled_labels, None)
def classify( self, data):
"""
Classifies each datum as the label that most closely matches the prototype vector
for that label. This is done by taking a polling over the weak classifiers already trained.
See the assignment description for details.
Recall that a datum is a util.counter.
The function should return a list of labels where each label should be one of legaLabels.
"""
prediction = []
for i in range(len(data)):
guesses = []
for j in range(self.num_classifiers):
guesses.append(self.classifiers[j].classify([data[i]])[0])
prediction.append(int(np.sign(sum(guesses))))
return prediction
集成学习的基本步骤
集成学习一般可分为以下3个步骤。
(1)找到误差互相独立的基分类器。
(2)训练基分类器。
(3)合并基分类器的结果。
合并基分类器的方法有voting和stacking两种。前者是用投票的方式,将获得最多选票的结果作为最终的结果。后者是用串行的方式,把前一个基分类器的结果输出到下一个分类器,将所有基分类器的输出结果相加(或者用更复杂的算法
融合,比如把各基分类器的输出作为特征,使用逻辑回归作为融合模型进行最后的结果预测)作为最终的输出。
以Adaboost为例,其基分类器的训练和合并的基本步骤如下。
(1)确定基分类器:这里可以选取ID3决策树作为基分类器。事实上,任何分类模型都可以作为基分类器,但树形模型由于结构简单且较易产生随机性所以比较常用。
(2)训练基分类器:假设训练集为{xi,yi},i=1,…,N,{x_i,y_i},i=1,…,N,其中yi∈{−1,1}y_iboldsymbol{in}{−1,1},并且有T个基分类器,则可以按照如下过程来训练基分类器。
1.初始化采分布
Dl(i)=1/ND_{mathrm{l}}(i)={1/N}
2.令t=1,2,…,Tt=1,2,…,T循环:
- 从训练集中,按照DtD_t分布,采样出子集St={xi,yi},i=1,…,Nt;S_{t}={x_{i},y_{i}},i=1,ldots,N_{t};
- 用StS_{t}训练出基分类器hth_t;
- 计算hth_t的错误率:
Et=∑i=1NtI[ht(xi)≠yi]Dt(xi)Ntmathcal{E}_t=frac{sum_{i=1}^{N_t}I[h_t(x_i)neq y_i]D_t(x_i)}{N_t}
其中I[]I[]为判别函数;
- 计算基分类器hth_t的权重:
at=log(1−Et)Eta_{t}=log{frac{(1-{mathcal{E}}_{t})}{{mathcal{E}}_{t}}}
- 设置下一次采样
Di+1={Di(i)或者Di(i)(1−εi)εi,hi(xi)≠yi;Di(i)εi(1−εi),pi(xi)=yi.D_{i+1}=begin{cases}D_i(i)text{或者}dfrac{D_i(i)(1-varepsilon_i)}{varepsilon_i},h_i(x_i)neq y_i;\ \ dfrac{D_i(i)varepsilon_i}{(1-varepsilon_i)},p_i(x_i)=y_i.end{cases}
并将它归一化为一个概率分布函数。
(3)合并基分类器:给定一个未知样本zz,输出分类结果为加权投票的结果Sign(∑t=1Tht(z)at)Sign(sumlimits_{t=1}^T h_t(z)a_t)。
从Adaboost的例子中我们可以明显地看到Boosting的思想,对分类正确的样本降低了权重,对分类错误的样本升高或者保持权重不变。 在最后进行模型融合的过程中,也根据错误率对基分类器进行加权融合。错误率低的分类器拥有更大的“话语权”。
另一个非常流行的模型是梯度提升决策树,其核心思想是,每一棵树学的是之前所有树结论和的残差,这个残差就是一个加预测值后能得真实值的累加量。
我们以一个视频网站的用户画像为例,为了将广告定向投放给指定年龄的用户,视频网站需要对每个用户的年龄做出预测。在这个问题中,每个样本是一个已知性别/年龄的用户,而特征则包括这个人访问的时长、时段、观看的视频的类型等。
例如用户A的真实年龄是25岁,但第一棵决策树的预测年龄是22岁,差了3
岁,即残差为3。那么在第二棵树里我们把A的年龄设为3岁去学习,如果第二棵树
能把A分到3岁的叶子节点,那两棵树的结果相加就可以得到A的真实年龄;如果第二棵树的结论是5岁,则A仍然存在−2岁的残差,第三棵树里A的年龄就变成−2岁,继续学。这里使用残差继续学习,就是GBDT中Gradient Boosted所表达的意思。
基分类器
基分类器的选择是集成学习主要步骤中的第一步,也是非常重要的一步。
最常用的基分类器是决策树,主要有以下3个方面的原因。
(1)决策树可以较为方便地将样本的权重整合到训练过程中,而不需要使用过采样的方法来调整样本权重。
(2)决策树的表达能力和泛化能力,可以通过调节树的层数来做折中。
(3)数据样本的扰动对于决策树的影响较大,因此不同子样本集合生成的决策树基分类器随机性较大,这样的“不稳定学习器”更适合作为基分类器。此外,在决策树节点分裂的时候,随机地选择一个特征子集,从中找出最优分裂属性,很好地引入了随机性。除了决策树外,神经网络模型也适合作为基分类器,主要由于神经网络模型也比较“不稳定”,而且还可以通过调整神经元数量、连接方式、网络层数、初始权值等方式引入随机性。
随机森林中的基分类器即为决策树。
这是随机森林属于Bagging类的集成学习。Bagging的主要好处是集成后的分类器的方差,比基分类器的方差小。Bagging所采用的基分类器,最好是本身对样本分布较为敏感的(即所谓不稳定的分类器),这样Bagging才能有用武之地。线性分类器或者K-近邻都是较为稳定的分类器,本身方差就不大,所以以它们为基分类器使用Bagging并不能在原有基分类器的基础上获得更好的表现,甚至可能因为Bagging的采样,而导致他们在训练中更难收敛,从而增大了集成分类器的偏差。
“本文正在参加 人工智能创作者扶持计划 ”