随机森林与集成算法
决策树:
使用决策树算法,我们从树根开始,基于可获得最大信息增益(information gain,IG)的特征来对数据进行划分,我们将在下一节详细介绍信息增益的概念。
通过迭代处理,在每个子节点上重复此划分过程,直到叶子节点。这意味着在每一个节点处,所有的样本都属于同一类别。
在实际应用中,这可能会导致生成一棵深度很大且拥有众多节点的树,这样容易产生过拟合问题,由此,我们一般通过对树进行“剪枝”来限定树的最大深度。
最大化信息增益——获知尽可能准确的结果
为了在可获得最大信息增益的特征处进行节点划分,需要定义一个可通过树学习算法进行优化的目标函数。
在此,目标函数能够在每次划分时实现对信息增益的最大化。
信息增益只不过是父节点的不纯度与所有子节点不纯度总和之差——子节点的不纯度越低,信息增益越大。
然而,出于简化和缩小组合搜索空间的考虑,大多数(包括scikit-learn)库中实现的都是二叉决策树。
这意味着每个父节点被划分为两个子节点:Dleft和Dright。
就目前来说,二叉决策树中常用的三个不纯度衡量标准或划分标准分别是:
基尼系数(Gini index,IG)、熵(entropy,IH),以及误分类率(classification error,IE)。
熵:
如果某一节点中所有的样本都属于同一类别,则其熵为0,当样本以相同的比例分属于不同的类时,熵的值最大。
对二类别分类来说,当p(i=1|t)=1或p(i=0|t)=0时,熵为0,如果类别均匀分布,即p(i=1|t)=0.5且p(i=1|t)=0.5时,熵为1。
在决策树中熵的准则就是使得互信息最大化。
基尼系数
可以理解为降低误分类可能性的标准
与熵类似,当所有类别是等比例分布时,基尼系数的值最大
# 在实践中,基尼系数与熵通常会生成非常类似的结果,并不值得花费大量时间使用不纯度标准评估树的好坏,而通常尝试使用不同的剪枝算法。
误分类率:
这是一个对于剪枝方法很有用的准则,但是不建议用于决策树的构建过程,因为它对节点中各类别样本数量的变动不敏感。
使用误分类率得到的信息增益都是相同,在使用基尼系数时,与划分A(IGG=0.125)相比,更倾向于使用B(IGG=0.16)的划分,
因为这样子节点处的类别纯度相对更高
熵的纯度系统最高
那么你到底应该使用基尼不纯度还是信息熵呢?其实,大多数情况下,它们并没有什么大的不同,产生的树都很相似。
基尼不纯度的计算速度略微快一些,所以它是个不错的默认选择。
它们的不同在于,基尼不纯度倾向于从树枝中分裂出最常见的类别,而信息熵则倾向于生产更平衡的树。
决策树可以通过将特征空间进行矩形划分的方式来构建复杂的决策边界。然而,必须注意:深度越大的决策树,决策边界也就越复杂,因而很容易产生过拟合现象。
scikit-learn一个吸引人的功能就是,它允许将训练后得到的决策树导出为.dot格式的文件,这使得我们可以使用GraphViz程序进行可视化处理。
通过观察GraphViz创建的图像,可以很好地回溯决策树在训练数据集上对各节点进行划分的过程。
from sklearn.tree import export_graphviz
export_graphviz(
tree_clf,
out_file=image_path("iris_tree.dot"),
feature_names=iris.feature_names[2:],
class_names=iris.target_names,
rounded=True,
filled=True
)
dot -Tpng iris_tree.dot -o iris_tree.png
决策树的众多特性之一就是, 它不需要太多的数据预处理, 尤其是不需要进行特征的缩放或者归一化。
估计分类概率:
决策树还可以估计某个实例属于特定类 k 的概率:首先遍历树来查找此实例的叶节点,然后它返回此节点中类 k 的训练实例的比例。
CART 训练算法:
Scikit-Learn 用分裂回归树(Classification And Regression Tree,简称 CART)算法训练决策树(也叫“增长树”)。
这种算法思想真的非常简单:首先使用单个特征 k 和阈值 (例如,“花瓣长度 ≤2.45cm ”)将训练集分成两个子集。
它如何选择 k 和 呢?它寻找到能够产生最纯粹的子集一对 ,然后通过子集大小加权计算。
当它成功的将训练集分成两部分之后, 它将会继续使用相同的递归式逻辑继续的分割子集,然后是子集的子集。
当达到预定的最大深度之后将会停止分裂(由 max_depth 超参数决定),或者是它找不到可以继续降低不纯度的分裂方法的时候。
几个其他超参数(之后介绍)控制了其他的停止生长条件。
CART 算法是一种贪婪算法:它贪婪地搜索最高级别的最佳分割方式,然后在每个深度重复该过程。
它不检查分割是否能够在几个级别中的全部分割可能中找到最佳方法。贪婪算法通常会产生一个相当好的解决方法,但它不保证这是全局中的最佳解决方案。
而不幸的是,寻找最优树是一个已知的NP完全问题:[1]需要的时间是O(exp(m)),所以即使是很小的训练集,也相当棘手。
这就是为什么我们必须接受一个“相当不错”的解。
计算复杂度:
进行预测需要从根到叶遍历决策树。通常来说,决策树大致平衡,因此遍历决策树需要经历大约O(log2(m))个节点。(注:log2是以2为底的对数。
等于log2(m)=log(m)/log(2)。)而每个节点只需要检查一个特征值,所以总体预测复杂度也只是O(log2(m)),与特征数量无关。
如此,即便是处理大型数据集,预测也很快。
但是,训练时在每一个节点,算法都需要在所有样本上比较所有特征(如果设置了max_features会少一些)。这导致训练的复杂度为O(n×m log(m))。
对于小型训练集(几千个实例以内),Scikit-Learn可以通过对数据预处理(设置presort=True)来加快训练,
但是对于较大训练集而言,可能会减慢训练的速度。
正则化超参数:
决策树几乎不对训练数据做任何假设(于此相反的是线性回归等模型,这类模型通常会假设数据是符合线性关系的)。
如果不添加约束,树结构模型通常将根据训练数据调整自己,使自身能够很好的拟合数据,而这种情况下大多数会导致模型过拟合。
这一类的模型通常会被称为非参数模型,这不是因为它没有任何参数(通常也有很多),而是因为在训练之前没有确定参数的具体数量,
所以模型结构可以根据数据的特性自由生长。
与此相反的是,像线性回归这样的参数模型有事先设定好的参数数量,所以自由度是受限的,这就减少了过拟合的风险(但是增加了欠拟合的风险)。
DecisionTreeClassifier 类还有一些其他的参数用于限制树模型的形状:
min_samples_split (节点在被分裂之前必须具有的最小样本数)
min_samples_leaf (叶节点必须具有的最小样本数)
min_weight_fraction_leaf (和 min_samples_leaf 相同,但表示为加权总数的一小部分实例)
max_leaf_nodes (叶节点的最大数量)
max_features (在每个节点被评估是否分裂的时候,具有的最大特征数量)。
增加 min_* hyperparameters 或者减少 max_* hyperparameters 会使模型正则化。
一些其他算法的工作原理是在没有任何约束条件下训练决策树模型,让模型自由生长,然后再对不需要的节点进行剪枝。
当一个节点的全部子节点都是叶节点时,如果它对纯度的提升不具有统计学意义,我们就认为这个分支是不必要的。
标准的假设检验,例如卡方检测,通常会被用于评估一个概率值 -- 即改进是否纯粹是偶然性的结果(也叫原假设)。
如果 p 值比给定的阈值更高(通常设定为 5%,也就是 95% 置信度,通过超参数设置),那么节点就被认为是非必要的,它的子节点会被删除。
这种剪枝方式将会一直进行,直到所有的非必要节点都被删光。
回归:
决策树也能够执行回归任务,让我们使用 Scikit-Learn 的 DecisionTreeRegressor 类构建一个回归树,
让我们用 max_depth = 2 在具有噪声的二次项数据集上进行训练。
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(max_depth=2)
tree_reg.fit(X, y)
这棵树看起来非常类似于你之前建立的分类树,它的主要区别在于,它不是预测每个节点中的样本所属的分类,而是预测一个具体的数值。
CART 算法的工作方式与之前处理分类模型基本一样,不同之处在于,现在不再以最小化不纯度的方式分割训练集,而是试图以最小化 MSE 的方式分割训练集。
和处理分类任务时一样,决策树在处理回归问题的时候也容易过拟合。
不稳定性:
决策树很喜欢设定正交化的决策边界,(所有边界都是和某一个轴相垂直的),这使得它对训练数据集的旋转很敏感。
解决这个难题的一种方式是使用 PCA 主成分分析(第八章),这样通常能使训练结果变得更好一些。
更加通俗的讲,决策时的主要问题是它对训练数据的微小变化非常敏感。
通过随机森林将弱分类器集成为强分类器:
由于具备好的分类能力、可扩展性、易用性等特点,随机森林(random forest)过去十年间在机器学习应用领域获得了广泛的关注。
直观上,随机森林可以视为多棵决策树的集成。
集成学习的基本理念就是将弱分类器集成为鲁棒性更强的模型,即一个能力更强的分类器,集成后具备更好的泛化误差,不易产生过拟合现象。
随机森林算法可以概括为四个简单的步骤:
1)使用bootstrap抽样方法随机选择n个样本用于训练(从训练集中随机可重复地选择n个样本)。
2)使用第1)步选定的样本构造一棵决策树。节点划分规则如下:
(1)不重复地随机选择d个特征;
(2)根据目标函数的要求,如最大化信息增益,使用选定的特征对节点进行划分。
3)重复上述过程1~2000次。
4)汇总每棵决策树的类标进行多数投票(majority vote)。
虽然随机森林没有决策树那样良好的可解释性,但其显著的优势在于不必担心超参值的选择。
我们通常不需要对随机森林进行剪枝,因为相对于单棵决策树来说,集成模型对噪声的鲁棒性更好。
在实践中,我们真正需要关心的参数是为构建随机森林所需的决策树数量(步骤3))。
通常情况下,决策树的数量越多,随机森林整体的分类表现就越好,但这同时也相应地增加了计算成本。
尽管在实践中不常见,但是随机森林中可优化的其他超参分别是:bootstrap抽样的数量(步骤1))以及在节点划分中使用的特征数量(步骤(2))
通过选择bootstrap抽样中样本数量n,我们可以控制随机森林的偏差与方差权衡的问题。
如果n的值较大,就降低了随机性,由此更可能导致随机森林的过拟合。反之,我们可以基于模型的性能,通过选择较小的n值来降低过拟合。
包括scikit-learn中RandomForestClassifier在内的大多数对随机森林的实现中,bootstrap抽样的数量一般与原始训练集中样本的数量相同,
因为这样在折中偏差与方差方面一般会有一个好的均衡结果。
而对于在每次节点划分中用到的特征数量m,我们选择一个比训练集中特征总量小的值。
在scikit-learn及其他程序实现中常用的默认值一般是开方,其中m是训练集中特征的总量。
forest = RandomForestClassifier(criterion='entropy',n_estimators=10,random_state=1,n_jobs=2)
或:
bag_clf = BaggingClassifier(DecisionTreeClassifier(splitter="random", max_leaf_nodes=16),n_estimators=500, max_samples=1.0, bootstrap=True,
n_jobs=-1)
集成学习——组合不同的模型:
构建一组分类器的集合,使得整体分类效果优于其中任意一个单独的分类器。
集成学习:
集成方法(ensemble method)的目标是:将不同的分类器组合成为一个元分类器,与包含于其中的单个分类器相比,元分类器具有更好的泛化性能。
例如:假设我们收集到了10位专家的预测结果,集成方法允许我们以一定策略将这10位专家的预测进行组合,与每位专家单独的预测相比,它具有更好的准确性和鲁棒性。
多数投票(majority voting)原则:
将大多数分类器预测的结果作为最终类标,也就是说,将得票率超过50%的结果作为类标。
严格来说,多数投票仅用于二类别分类情形。不过,很容易将多数投票原则推广到多类别分类,也称作简单多数票法(plurality voting)。
基于训练集,我们首先训练m个不同的成员分类器(C1,…,Cm)。在多数投票原则下,可集成不同的分类算法,如决策树、支持向量机、逻辑斯谛回归等。
此外,我们也可以使用相同的成员分类算法拟合不同的训练子集。这种方法典型的例子就是随机森林算法,它组合了不同的决策树分类器。
想要通过简单的多数投票原则对类标进行预测,我们要汇总所有分类器Cj的预测类标,并选出得票率最高的类标:
为了说明集成方法的效果为何好于单个成员分类器,我们借用下组合学中的概念。
假定每个分类器都是独立的,且出错率之间是不相关的。基于这些假设,我们可以将成员分类器集成后的出错概率简单地表示为二项分布的概率密度函数
在满足所有假设的条件下,集成后的出错率(0.034)远远小于单个分类器的出错率(0.25)。
请注意,在此演示示例中,当集成分类中分类器个数n为偶数时,若预测结果为五五分,我们则将其以错误对待,不过仅有一半的可能会出现这种情况。
为了比较成员分类器在不同出错率的情况下与集成分类器出错率的差异,我们用Python实现其概率密度函数:
def ensemble_error(n_classifier,error):
k_start = math.ceil(n_classifier/2.0)
probs = [comb(n_classifier,k)*error**k *(1-error)**(n_classifier-k)
for k in range(k_start,n_classifier+1)]
return sum(probs)
画图可知,当成员分类器出错率低于随机猜测时(ε<0.5),集成分类器的出错率要低于单个分类器。
硬投票分类器只是统计每个分类器的投票,然后挑选出得票最多的类别。
如果所有的分类器都能够预测类别的概率(例如他们有一个 predict_proba() 方法),那么你就可以让 sklearn 以最高的类概率来预测这个类,
平均在所有的分类器上。这种方式叫做软投票。他经常比硬投票表现的更好,因为它给予高自信的投票更大的权重。
你可以通过把 voting="hard" 设置为 voting="soft" 来保证分类器可以预测类别概率。然而这不是 SVC类的分类器默认的选项,
所以你需要把它的 probability hyperparameter 设置为 True (这会使 SVC 使用交叉验证去预测类别概率,其降低了训练速度,
但会添加 predict_proba() 方法)。
from sklearn.ensemble import VotingClassifier
voting_clf = VotingClassifier(estimators=[('lr', log_clf), ('rf', rnd_clf),('svc', svm_clf)],voting='hard')
实现一个简单的多数投票分类器:
基于多数投票原则,使用Python实现一个简单的集成分类器
集成算法允许我们使用单独的权重对不同分类算法进行组合。我们的目标是构建一个更加强大的元分类器,以在特定的数据集上平衡单个分类器的弱点。
为了使用Python代码实现加权多数投票,可以使用NumPy中的argmax和bincount函数:
np.argmax(np.bincount([0,0,1],weights=[0.2,0.2,0.6]))
通过predict_proba方法,scikit-learn中的分类器可以返回样本属于预测类标的概率。
如果集成分类器事先得到良好的修正,那么在多数投票中使用预测类别的概率来替代类标会非常有用。
为实现基于类别预测概率的加权多数投票,我们可以再次使用NumPy中的numPy.average和np.argmax方法:
ex = np.array([[0.9,0.1],[0.8,0.2],[0.4,0.6]])
p = np.average(ex,axis=0,weights=[0.2,0.2,0.6])
print(np.argmax(p))
实现一个MajorityVotingClassifier,以10折交叉验证作为评估标准,MajorityVotingClassifier的性能与单个成员分类器相比有着质的提高。
评估与调优集成分类器:
在本节,我们将在测试数据上计算MajorityVoteClassifier类的ROC曲线,以验证其在未知数据上的泛化性能。
请记住,训练数据并未应用于模型选择,使用它们的唯一目的就是对分类系统的泛化性能做出无偏差的估计。
for clf,label,clr,ls in zip(all_clf,clf_labels,colors,linestyles):
y_pred = clf.fit(X_train,y_train).predict_proba(X_test)[:,1] # 测出结果
fpr, tpr, thresholds = roc_curve(y_true=y_test,y_score=y_pred)
roc_auc = auc(x=fpr,y=tpr)
plt.plot(fpr, tpr,color=clr,linestyle=ls,label="%s (auc=%0.2f)" %(label,roc_auc))
因为只使用了分类样本中的两个特征,集成分类器的决策区域到底是什么样子可能会引起我们的兴趣。
由于逻辑斯谛回归和k-近邻流水线会自动对数据进行预处理,因此不必事先对训练特征进行标准化。
不过出于可视化考虑,也就是在相同的度量标准上实现决策区域,我们在此对训练集做了标准化处理。
也如我们所预期的那样,集成分类器的决策区域看起来像是各成员分类器决策区域的混合。
在学习集成分类的成员分类器调优之前,我们调用一下get_param方法,以便对如何访问GridSearch对象内的单个参数有个基本的认识:
得到get_params方法的返回值后,我们现在知道怎样去访问成员分类器的属性了。如"decisiontreeclassifier__max_depth"
出于演示的目的,先通过网格搜索来调整逻辑斯谛回归分类器的正则化系数C以及决策树的深度。
正如我们所见,当选择的正则化强度较小时(C=100.0),我们能够得到最佳的交叉验证结果,而决策树的深度似乎对性能没有任何影响,
这意味着使用单层决策树足以对数据进行划分。
请注意,在模型评估时,不止一次使用测试集并非一个好的做法,本节不打算讨论超参调优后的集成分类器泛化能力的评估。
将继续学习另外一种集成方法:bagging。
我们在本节实现的多数投票方法有时也称为堆叠(stacking)。
不过,堆叠算法更典型地应用于组合逻辑斯谛回归模型,以各独立分类器的输出作为输入,通过对这些输入结果的继承来预测最终的类标
bagging——通过bootstrap样本构建集成分类器:
# bagging是有放回的随机抽样,pasting是不放回的随机抽样,而boosting是循环训练预测器,每一次都对其前序做出一些改正。
bagging是一种与上一节实现的MajorityVoteClassifier关系紧密的集成学习技术
不过,此算法没有使用相同的训练集拟合集成分类器中的单个成员分类器。对每一个分类器都使用相同的训练算法,但是在不同的训练集上去训练它们。
由于原始训练集使用了boostrap抽样(有放回的随机抽样),这也就是bagging被称为boostrap aggregating的原因。
在每一轮的bagging循环中,它们都被可放回随机抽样。每个bootstrap抽样都被用于分类器Cj的训练,这就是一棵典型的未剪枝的决策树:
bagging还与我们在第3章中曾经介绍过的随机森林紧密相关。实际上,随机森林是bagging的一个特例,它使用了随机的特征子集去拟合单棵决策。
在实战中,分类任务会更加复杂,数据集维度会更高,使用单棵决策树很容易产生过拟合,这时bagging算法就可显示出其优势了。
最后,我们需注意bagging算法是降低模型方差的一种有效方法。
然而,bagging在降低模型偏差方面的作用不大,这也是我们选择未剪枝决策树等低偏差分类器作为集成算法成员分类器的原因。
聚合函数通常对分类是统计模式(例如硬投票分类器)或者对回归是平均。
通常情况下,集成的结果是有一个相似的偏差,但是对比与在原始训练集上的单一分类器来讲有更小的方差。
在sklearn中的 Bagging 和 Pasting:
sklearn 为 Bagging 和 Pasting 提供了一个简单的API: BaggingClassifier 类(或者对于回归可以是 BaggingRegressor 。
接下来的代码训练了一个 500 个决策树分类器的集成,每一个都是在数据集上有放回采样 100 个训练实例下进行训练
(这是 Bagging 的例子,如果你想尝试Pasting,就设置 bootstrap=False )。
n_jobs 参数告诉 sklearn 用于训练和预测所需要 CPU核的数量。(-1 代表着 sklearn 会使用所有空闲核):
from sklearn.ensemble import BaggingClassifier
rom sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)
Bootstrap 在每个预测器被训练的子集中引入了更多的分集,所以 Bagging 结束时的偏差比Pasting 更高,但这也意味着预测因子最终变得不相关,
从而减少了集合的方差。总体而言,Bagging 通常会导致更好的模型,这就解释了为什么它通常是首选的。
Out-of-Bag 评价:
对于 Bagging 来说,一些实例可能被一些分类器重复采样,但其他的有可能不会被采样。
BaggingClassifier为默认采样。BaggingClassifier 默认是有放回的采样 m 个实例( bootstrap=True ),其中 m 是训练集的大小,
这意味着平均下来只有63%的训练实例被每个分类器采样,剩下的37%个没有被采样的训练实例就叫做 Out-of-Bag 实例。
注意对于每一个的分类器它们的 37% 不是相同的。
因为在训练中分类器从来没有看到过 oob 实例,所以它可以在这些实例上进行评估,而不需要单独的验证集或交叉验证。
你可以拿出每一个分类器的 oob 来评估集成本身。
在 sklearn 中,你可以在训练后需要创建一个 BaggingClassifier 来自动评估时设置 oob_score=True 来自动评估。接下来的代码展示了这个操作。
评估结果通过变量 oob_score_ 来显示:
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,bootstrap=True, n_jobs=-1, oob_score=True)
bag_clf.fit(X_train, y_train)
bag_clf.oob_score_ # 0.93066666666666664,再与下面测试集上的结果比较一下
根据这个 obb 评估, BaggingClassifier 可以再测试集上达到93.1%的准确率,让我们修改一下:
from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
accuracy_score(y_test, y_pred) # 0.93600000000000005
我们在测试集上得到了 93.6% 的准确率,足够接近了!
对于每个训练实例 oob 决策函数也可通过 oob_decision_function_ 变量来展示。在这种情况下(当基决策器有 predict_proba() 时)
决策函数会对每个训练实例返回类别概率。例如,oob 评估预测第二个训练实例有 60.6% 的概率属于正类(39.4% 属于负类):
bag_clf.oob_decision_function_
array([[ 0., 1.], [ 0.60588235, 0.39411765],[ 1., 0. ],... [ 1. , 0. ],[ 0., 1.],[ 0.48958333, 0.51041667]])
随机贴片与随机子空间
BaggingClassifier 也支持采样特征。它被两个超参数 max_features 和 bootstrap_features 控制。
他们的工作方式和 max_samples 和 bootstrap 一样,但这是对于特征采样而不是实例采样。因此,每一个分类器都会被在随机的输入特征内进行训练。
当你在处理高维度输入下(例如图片)此方法尤其有效。对训练实例和特征的采样被叫做随机贴片。
保留了所有的训练实例(例如 bootstrap=False 和 max_samples=1.0 ),但是对特征采样( bootstrap_features=True 并且/或者
max_features 小于 1.0)叫做随机子空间。
采样特征导致更多的预测多样性,用高偏差换低方差。
随机森林:
通常用bagging(有时也可能是pasting)方法训练
极端随机树
如前所述,随机森林里单棵树的生长过程中,每个节点在分裂时仅考虑到了一个随机子集所包含的特征。
如果我们对每个特征使用随机阈值,而不是搜索得出的最佳阈值(如常规决策树),则可能让决策树生长得更加随机。
这种极端随机的决策树组成的森林,被称为极端随机树集成[4](或简称Extra-Trees)。
同样,它也是以更高的偏差换取了更低的方差。
极端随机树训练起来比常规随机森林要快很多,因为在每个节点上找到每个特征的最佳阈值是决策树生长中最耗时的任务之一。
使用Scikit-Learn的ExtraTreesClassifier可以创建一个极端随机树分类器。它的API与RandomForestClassifier相同。
同理,ExtraTreesRegressor与RandomForestRegressor的API也相同。
通常来说,很难预先知道一个RandomForestClassifier是否会比一个ExtraTreesClassifier更好或是更差。
唯一的方法是两种都尝试一遍,然后使用交叉验证(还需要使用网格搜索调整超参数)进行比较。
提升法:
提升法(Boosting,最初被称为假设提升)是指可以将几个弱学习器结合成一个强学习器的任意集成方法。
大多数提升法的总体思路是循环训练预测器,每一次都对其前序做出一些改正。
在boosting中,集成分类器包含多个非常简单的成员分类器,这些成员分类器性能仅稍好于随机猜测,常被称作弱学习机。
典型的弱学习机例子就是单层决策树。boosting主要针对难以区分的训练样本,也就是说,弱学习机通过在错误分类样本上的学习来提高集成分类的性能。
与bagging不同,在boosting的初始化阶段,算法使用无放回抽样从训练样本中随机抽取一个子集。
原始的boosting过程可总结为如下四个步骤:
1)从训练集D中以无放回抽样方式随机抽取一个训练子集d1,用于弱学习机C1的训练。
2)从训练集中以无放回抽样方式随机抽取第2个训练子集d2,并将C1中误分类样本的50%加入到训练集中,训练得到弱学习机C2。
3)从训练集D中抽取C1和C2分类结果不一致的样本生成训练样本集d3,以此训练第3个弱学习机C3。
4)通过多数投票组合三个弱学习机C1、C2和C3。
与bagging模型相比,boosting可同时降低偏差和方差。在实践中,boosting算法(如AdaBoost)也存在明显的高方差问题,也就是说,对训练数据有过拟合倾向。
AdaBoost:
在本节对集成方法的介绍中,我们将重点讨论boosting算法中一个常用例子:AdaBoost(Adaptive Boosting的简称)。
AdaBoost与这里讨论的原始boosting过程不同,它使用整个训练集来训练弱学习机,其中训练样本在每次迭代中都会重新被赋予一个权重,
在上一弱学习机错误的基础上进行学习进而构建一个更加强大的分类器。
新预测器对其前序进行纠正的办法之一,就是更多地关注前序拟合不足的训练实例。
从而使新的预测器不断地越来越专注于难缠的问题,这就是AdaBoost使用的技术。
过程:
1.首先需要训练一个基础分类器(比如决策树),用它对训练集进行预测。
2.然后计算该预测器的加权误差率rj,计算该预测器权重αj,更新实例权重,对错误分类的训练实例增加其相对权重w
# 实例权重w用来衡量决策树更新权重w时,该实例对权重w的更新系数大小,影响每个实例的贡献(所以只能单个训练?两个for循环)
# 乘以一个 实例权重 w 矩阵? 类似于批量训练的 预测结果最后平均出一个值来更新权重 w,平均的时候先乘以一个实例权重矩阵即可。
预测器的准确率越高,其权重就越高。如果它只是随机猜测,则其权重接近于零。
但是,如果大部分情况下它都是错的(也就是准确率比随机猜测还低),那么它的权重为负。
3.使用这个最新的权重对第二个分类器进行训练,然后再次对训练集进行预测,继续更新权重,并不断循环向前。
4.当到达所需数量的预测器,或得到完美的预测器时,算法停止。
5.预测时简单地计算所有预测器的预测结果,并使用预测器权重αj对它们进行加权。
算法步骤:
1.以等值方式为权重向量w赋值,总和为1
2.在m轮的boosting操作中,对第j轮做如下操作
训练一个加权的弱学习机
预测样本类标
计算权重错误率,正确为1,错误为0
计算相关系数αj
更新权重,计算正确会被降低权重,错误会增加
归一化权重,总为1
3.完成最终的预测。
学习率越低,波动会越小。
AdaBoost这种依序循环的学习技术跟梯度下降有一些异曲同工之处,差别只在于——不再是调整单个预测器的参数使成本函数最小化,
而是不断在集成中加入预测器,使模型越来越好。
一旦全部预测器训练完成,集成整体做出预测时就跟bagging或pasting方法一样了,除非预测器有不同的权重,因为它们总的准确率是基于加权后的训练集。
序列学习技术的一个重要的缺点就是:它不能被并行化(只能按步骤),因为每个分类器只能在之前的分类器已经被训练和评价后再进行训练。
因此,它不像Bagging和Pasting一样。
sklearn 通常使用 Adaboost 的多分类版本 SAMME(这就代表了 分段加建模使用多类指数损失函数)。
如果只有两类别,那么 SAMME 是与 Adaboost 相同的。如果分类器可以预测类别概率(例如如果它们有 predict_proba() ),
如果 sklearn 可以使用 SAMME 叫做 SAMME.R 的变量(R 代表“REAL”),这种依赖于类别概率的通常比依赖于分类器的更好。
from sklearn.ensemble import AdaBoostClassifier
tree = DecisionTreeClassifier(criterion="entropy",max_depth=1)
ada = AdaBoostClassifier(base_estimator=tree,n_estimators=500,learning_rate=0.1,random_state=0)
可以发现,与单层决策树相比,AdaBoost分类器能够些许提高分类性能,并且与上一节中训练的bagging分类器的准确率接近。
不过请注意:重复使用测试集进行模型选择不是一个好的方法。集成分类器的泛化性能可能会被过度优化。
如果你的AdaBoost集成过度拟合训练集,你可以试试减少估算器数量,或是提高基础估算器的正则化程度。
对集成技术做一个总结:毋庸置疑,与单独分类器相比,集成学习提高了计算复杂度。
但在实践中,我们需仔细权衡是否愿意为适度提高预测性能而付出更多的计算成本。
梯度提升:
另一个非常著名的提升算法是梯度提升。与 Adaboost 一样,梯度提升也是通过向集成中逐步增加分类器运行的,每一个分类器都修正之前的分类结果。
然而,它并不像 Adaboost 那样每一次迭代都更改实例的权重,而是让新的预测器针对前一个预测器的残差进行拟合。
我们来看一个简单的回归示例,使用决策树作为基础预测器(梯度提升当然也适用于回归任务),这被称为梯度树提升或者是梯度提升回归树(GBRT)。
tree_reg1 = DecisionTreeRegressor(max_depth=2)
tree_reg1.fit(X, y)
现在在第一个分类器的残差上训练第二个分类器:
y2 = y - tree_reg1.predict(X) # 预测结果偏差
tree_reg2 = DecisionTreeRegressor(max_depth=2)
tree_reg2.fit(X, y2)
随后在第二个分类器的残差上训练第三个分类器:
y3 = y2 - tree_reg1.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2)
tree_reg3.fit(X, y3)
现在我们有了一个包含三个回归器的集成。它可以通过集成所有树的预测来在一个新的实例上进行预测。
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3)) # 求和
可以使用 sklean 中的 GradientBoostingRegressor 来训练 GBRT 集成。
与 RandomForestClassifier 相似,它也有超参数去控制决策树的生长(例如 max_depth , min_samples_leaf 等等),
也有超参数去控制集成训练,例如基分类器的数量( n_estimators )。
接下来的代码创建了与之前相同的集成:
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0)
gbrt.fit(X, y)
超参数learning_rate对每棵树的贡献进行缩放。如果你将其设置为低值,比如0.1,则需要更多的树来拟合训练集,但是预测的泛化效果通常更好。
这是一种被称为收缩的正则化技术。
要找到树的最佳数量,可以使用早期停止法(参见第4章)。简单的实现方法就是使用staged_predict()方法:
它在训练的每个阶段(一棵树时,两棵树时,等等)都对集成的预测返回一个迭代器(用于测量每个训练阶段的验证误差,从而找到树的最优数量)。
以下代码训练了一个拥有120棵树的GBRT集成,然后测量每个训练阶段的验证误差,从而找到树的最优数量,最后使用最优树数重新训练了一个GBRT集成:
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120)
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred) for y_pred in gbrt.staged_predict(X_test)] # 预测器的数量
bst_n_estimators = np.argmin(errors)
gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators)
实际上,要实现早期停止法,不一定需要先训练大量的树,然后再回头找最优的数字,还可以真的提前停止训练。
设置warm_start=True,当fit()方法被调用时,Scikit-Learn会保留现有的树,从而允许增量训练。
以下代码会在验证误差连续5次迭代未改善时,直接停止训练:
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True)
for n_estimators in range(1, 120):
gbrt.n_estimators = n_estimators # 调整数量,继续fit
gbrt.fit(X_train, y_train) #
y_pred = gbrt.predict(X_val)
val_error = mean_squared_error(y_val, y_pred)
if val_error < min_val_error:
min_val_error = val_error
error_going_up = 0
else:
error_going_up += 1
if error_going_up == 5:
break # early stopping
GradientBoostingRegressor类还可以支持超参数subsample,指定用于训练每棵树的实例的比例。
例如,如果subsample=0.25,则每棵树用25%的随机选择的实例进行训练。现在你可以猜到,这也是用更高的偏差换取了更低的方差
# 模型降低了过拟合,可能会欠拟合,方差减小,偏差增大(过拟合至少训练集表现好,模式复杂,多个样本集来说偏差较小)
同时在相当大的程度上加速了训练过程。这种技术被称为随机梯度提升。
梯度提升也可以使用其他成本函数,通过超参数loss来控制(有关更多详细信息,请参阅Scikit-Learn的文档)。
XGBoost
XGBoost是2014年2月诞生的专注于梯度提升算法的机器学习函数库,此函数库因其优良的学习效果以及高效的训练速度而获得广泛的关注。
XGBoost实现的是一种通用的Tree Boosting算法,此算法的一个代表为梯度提升决策树(Gradient Boosting Decision Tree, GBDT),
又名MART(Multiple Additive Regression Tree)。
GBDT的核心在于后面的树拟合的是前面预测值的残差,这样可以一步步逼近真值。然而,之所以拟合残差可以逼近到真值,是因为使用了平方损失作为损失函数。
如果换成是其他损失函数,使用残差将不再能够保证逼近真值。
XGBoost的方法是,将损失函数做泰勒展开到第二阶,使用前两阶作为改进的残差。
可以证明,传统GBDT使用的残差是泰勒展开到一阶的结果,因此,GBDT是XGBoost的一个特例。
支持列抽样。
在决策树中,模型复杂度体现在树的深度上。XGBoost使用了一种替代指标,即叶子节点的个数。
此外,与许多其他机器学习模型一样,XGBoost也加入了L2正则项,来平滑各叶子节点的预测值。
速度快的原因:
1.连续型特征的处理,只枚举若干个分位点,例如将所有样本根据此特征进行排序,然后均分10份,两份之间断开的数值即为分位点,
枚举所有9个分位点后,使用降低损失最多的那个作为分叉点。
2. 利用数据稀疏性
数据稀疏有三个原因:缺失数据;某些特征本身就含有大量的0;对离散特征做了one-hot处理。
每次分叉时,都指定一条默认分支,如果样本的这个特征为0,就走这个默认分支。
3.数据的预排序和分块存储
分叉的时候为了判断分叉点,需要对每个特征进行排序。这个步骤是要重复多次的,
因此XGBoost在训练之前预先对数据进行每一列做了排序,并按列存储到内存中。
4.减少读写相关,提高Cache命中率
5.数据量大时,提高硬盘吞吐率
示例:
import xgboost as xgb
xlf = xgb.XGBClassifier() # 是xgboost的sklearn包,可以使用sklearn的utils如Grid Search等,利用函数参数设置模型参数
xlf.fit(X_train, y_train) # 内部调用原始xgboost的xgb.train(),原始利用param列表设置模型参数
preds = xlf.predict(X_test)
LightGBM:
目前已有的 GBDT 工具基本都是基于预排序的方法(pre-sorted)的决策树算法(如 xgboost),
而LightGBM是基于 Histogram 的决策树算法,带深度限制的Leaf-wise的叶子生长策略,直方图做差加速,直接支持类别特征(Categorical Feature),
Cache命中率优化,基于直方图的稀疏特征优化
1.Histogram 算法
直方图算法的基本思想是先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。
在遍历数据的时候,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,
然后根据直方图的离散值,遍历寻找最优的分割点。
减少了内存,直方图算法不仅不需要额外存储预排序的结果,而且可以只保存特征离散化后的值。
在计算上的代价也大幅降低,预排序算法每遍历一个特征值就需要计算一次分裂的增益,而直方图算法只需要计算k次
当然,Histogram 算法并不是完美的。由于特征被离散化后,找到的并不是很精确的分割点,所以会对结果产生影响。
但在不同的数据集上的结果表明,离散化的分割点对最终的精度影响并不是很大,甚至有时候会更好一点。
2.带深度限制的 Leaf-wise 的叶子生长策略
它抛弃了大多数 GBDT 工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise) 算法。
Level-wise 过一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。
但实际上 Level-wise 是一种低效的算法,因为它不加区分的对待同一层的叶子,带来了很多没必要的开销,因为实际上很多叶子的分裂增益较低,
没必要进行搜索和分裂。
Leaf-wise 则是一种更为高效的策略,每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。
因此同 Level-wise 相比,在分裂次数相同的情况下,Leaf-wise 可以降低更多的误差,得到更好的精度。
Leaf-wise 的缺点是可能会长出比较深的决策树,产生过拟合。
因此 LightGBM 在 Leaf-wise 之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。
3.直方图加速
一个叶子的直方图可以由它的父亲节点的直方图与它兄弟的直方图做差得到。
通常构造直方图,需要遍历该叶子上的所有数据,但直方图做差仅需遍历直方图的k个桶。
利用这个方法,LightGBM 可以在构造一个叶子的直方图后,可以用非常微小的代价得到它兄弟叶子的直方图,在速度上可以提升一倍。
4.直接支持类别特征
实际上大多数机器学习工具都无法直接支持类别特征,一般需要把类别特征,转化到多维的0/1 特征,降低了空间和时间的效率。
而类别特征的使用是在实践中很常用的。基于这个考虑,LightGBM 优化了对类别特征的支持,可以直接输入类别特征,不需要额外的0/1 展开。
并在决策树算法上增加了类别特征的决策规则。在 Expo 数据集上的实验,相比0/1 展开的方法,训练速度可以加速 8 倍,并且精度一致。
据我们所知,LightGBM 是第一个直接支持类别特征的 GBDT 工具。
超参:
1.核心参数
- boosting_type,默认gbdt,可选rf,dart,goss
gbdt, 传统的梯度提升决策树
rf, Random Forest (随机森林)
dart, Dropouts meet Multiple Additive Regression Trees
goss, Gradient-based One-Side Sampling (基于梯度的单侧采样)
- learning_rate 默认设置成0.1
- n_estimators
- num_leaves 调整 num_leaves 的取值时, 应该让其小于 2^(max_depth)
- n_jobs
- device cpu, gpu
2.控制参数:
- max_depth
- min_child_samples 默认20,一个叶子上数据的最小数量. 可以用来处理过拟合.对于大数据来说几百或几千
- min_child_weight,0.001,一个叶子上的最小 hessian 和. 类似于 min_data_in_leaf, 可以用来处理过拟合.
- colsample_bytree 1.0,加速训练,处理过拟合
- subsample 1.0,将在不进行重采样的情况下随机选择部分数据
- subsample_freq 0 bagging 的频率, 0 意味着禁用 bagging. k 意味着每 k 次迭代执行bagging
- early_stopping_round 如果一个验证集的度量在 early_stopping_round 循环中没有提升, 将停止训练
- reg_alpha L1 正则
- reg_lambda L2 正则
- min_split_gain 0,执行切分的最小增益
- subsample_for_bin:用来构建直方图的数据的数量,在设置更大的数据时, 会提供更好的培训效果, 但会增加数据加载时间。如果数据非常稀疏, 则将其设置为更大的值
3.IO参数
- max_bin 默认255,值越大,准确率越高,可能会过拟合
- save_binary 如果设置为 true LightGBM 则将数据集(包括验证数据)保存到二进制文件中。 可以加快数据加载速度。
- ignore_column ignore_column=0,1,2
- categorical_feature categorical_feature=0,1,2
示例:
import lightgbm as lgb
lf = lgb.LGBMClassifier() # 类似,封装了sklearn
lf.fit()