文章目录
一、随机森林在巨量数据上的增量学习
1. 普通学习 vs 增量学习
1.1 普通学习
2. 增量学习
2. 增量学习在 Kaggle 数据上的应用
2.1 实际应用
二、Bagging 方法 6 大面试热点问题
Q1:为什么 Bagging 算法的效果比单个评估器更好?
Q2:为什么 Bagging 可以降低方差?
Q3:Bagging 有效的基本条件有哪些?Bagging 的效果总是强于弱评估器吗?
Q4:Bagging 方法可以集成决策树之外的算法吗?
Q5:怎样增强 Bagging 中弱评估器的独立性?
Q6:除了随机森林,你还知道其他 Bagging 算法吗?
- 在开始学习之前,先导入我们需要的库。
import numpy as np import pandas as pd import sklearn import matplotlib as mlp import seaborn as sns import re, pip, conda import matplotlib.pyplot as plt from sklearn.ensemble import RandomForestRegressor as RFR from sklearn.tree import DecisionTreeRegressor as DTR from sklearn.model_selection import cross_validate, KFold
一、随机森林在巨量数据上的增量学习
集成学习是工业领域中应用最广泛的机器学习算法。实际工业环境下的数据量往往十分巨大,一个训练好的集成算法的复杂程度与训练数据量高度相关。
因此企业在应用机器学习时通常会提供强大的计算资源作为支持,也因此当代的大部分集成算法都是支持 GPU 运算的(相对的,如果你发现一个算法在任何机器学习库中,都没有接入 GPU 运算的选项,这可能说明该算法在工业应用中基本不会被使用)。
sklearn 作为早期开源的机器学习算法库,难以预料到如今人工智能技术走进千家万户的应用状况,因此并未开放接入 GPU 进行运算的接口,即 sklearn 中的所有算法都不支持接入更多计算资源。
因此当我们想要使用随机森林在巨量数据上进行运算时,很可能会遭遇计算资源短缺的情况。幸运的是,我们有两种方式解决这个问题:
(1) 使用其他可以接入 GPU 的机器学习算法库实现随机森林,比如 xgboost。
(2) 继续使用 sklearn 进行训练,但使用增量学习(incremental learning)。
增量学习是机器学习中非常常见的方法,在有监督和无监督学习当中都普遍存在。增量学习允许算法不断接入新数据来拓展当前的模型,即允许巨量数据被分成若干个子集,分别输入模型进行训练。
1. 普通学习 vs 增量学习
1.1 普通学习
- 通常来说,当一个模型经过一次训练之后,如果再使用新数据对模型进行训练,原始数据训练出的模型会被替代掉。举个例子,我们原本的数据集 X 与 y 是 kaggle 房价数据集,结构为:
X.shape #(1460, 80) y.shape #(1460,)
- 现在,我们导入 sklearn 中非常常用的另一个数据集,加利福尼亚房价数据集:
from sklearn.datasets import fetch_california_housing from sklearn.metrics import mean_squared_error X_fc = fetch_california_housing().data y_fc = fetch_california_housing().target X_fc.shape #可以看到,加利福尼亚房价数据集的特征量为8 #(20640, 8)
- 建模,并在 X_,y_ 基础上进行训练:
model = RFR(n_estimators=3, warm_start=False) #不支持增量学习的 model1 = model.fit(X_fc,y_fc) #RMSE (mean_squared_error(y_fc,model1.predict(X_fc)))**0.5 #0.30123985583215596 #使用.estimators_查看森林中所有树的情况,可以看到每一棵树的随机数种子 model1.estimators_ #[DecisionTreeRegressor(max_features='auto', random_state=1785210460), # DecisionTreeRegressor(max_features='auto', random_state=121562514), # DecisionTreeRegressor(max_features='auto', random_state=1271073231)]
- 此时,如果让 model1 继续在 kaggle 房价数据集 X,y 上进行训练:
model1 = model1.fit(X.iloc[:,:8],y) #注意,X有80个特征,X_fc只有8个特征,输入同一个模型的数据必须结构一致 model1.estimators_ #你发现了吗?model1中原始的树消失了,新的树替代了原始的树 #[DecisionTreeRegressor(max_features='auto', random_state=349555903), # DecisionTreeRegressor(max_features='auto', random_state=1253222501), # DecisionTreeRegressor(max_features='auto', random_state=2145441582)]
- 再让 model1 对加利福尼亚房价数据集进行训练,会发生什么呢?别忘了 model1 之前训练过加利福尼亚房价数据集:
再让 model1 对加利福尼亚房价数据集进行训练,会发生什么呢?别忘了 model1 之前训练过加利福尼亚房价数据集: #RMSE#RMSE
RMSE 异常巨大,模型现在已经不具备任何预测 y_fc 的能力了。非常明显,model1 中原始的树消失了,基于 kaggle 数据集训练的树覆盖了原始的树,因此 model1 不再对本来见过的加利福尼亚房价数据报有记忆。
sklearn 的这一覆盖规则是交叉验证可以进行的基础,正因为每次训练都不会受到上次训练的影响,我们才可以使用模型进行交叉验证,否则就会存在数据泄露的情况。
但在增量学习中,原始数据训练的树不会被替代掉,模型会一致记得之前训练过的数据,我们来看看详细情况:
2. 增量学习
- 我们还是可以使用 X,y 以及 X_fc,y_fc 作为例子,这一次,我们让 warm_start 参数取值为 True,允许随机森林进行增量学习:
model = RFR(n_estimators=3, warm_start=True) #支持增量学习 model2 = model.fit(X_fc,y_fc) (mean_squared_error(y_fc,model2.predict(X_fc)))**0.5 #0.30099931130927154 model2.estimators_ #[DecisionTreeRegressor(max_features='auto', random_state=338470642), # DecisionTreeRegressor(max_features='auto', random_state=1545812511), # DecisionTreeRegressor(max_features='auto', random_state=740599321)]
- 让 X 和 y 在 model2 上继续进行训练:
model2 = model2.fit(X.iloc[:,:8],y) (mean_squared_error(y_fc,model2.predict(X_fc)))**0.5 #0.30099931130927154
- 即便已经对 X 和 y 进行了训练,但是 model2 中对加利福尼亚房价数据集的记忆还在,因此在对 X_fc 与 y_fc 进行预测时,依然能够取得不错的分数。
model2.estimators_ #在增量学习当中,树没有发生变化 #[DecisionTreeRegressor(max_features='auto', random_state=338470642), # DecisionTreeRegressor(max_features='auto', random_state=1545812511), # DecisionTreeRegressor(max_features='auto', random_state=740599321)] model2
所以在增量学习当中,已经训练过的结果会被保留。对于随机森林这样的 Bagging 模型来说,这意味着之前的数据训练出的树会被保留,新数据会训练出新的树,新旧树互不影响。对于逻辑回归、神经网络这样不断迭代以求解权重 w ww 的算法来说,新数据训练时 w ww 的起点是之前的数据训练完毕之后的 w ww。
不过,这里存在一个问题:虽然原来的树没有变化,但增量学习看起来并没有增加新的树——事实上,对于随机森林而言,我们需要手动增加新的树:
model2.estimators_ #属性,反映训练完毕的模型的一些特点、一些客观存在的性质 #调用模型的参数,可以通过这种方式修改模型的参数,而不需要重新实例化模型 model2.n_estimators += 2 #增加2棵树,用于增量学习 model2 #RandomForestRegressor(n_estimators=5, warm_start=True) model2.fit(X.iloc[:,:8],y) #RandomForestRegressor(n_estimators=5, warm_start=True) model2.estimators_ #原来的树还是没有变化,新增的树是基于新输入的数据进行训练的 #[DecisionTreeRegressor(max_features='auto', random_state=338470642), # DecisionTreeRegressor(max_features='auto', random_state=1545812511), # DecisionTreeRegressor(max_features='auto', random_state=740599321), # DecisionTreeRegressor(max_features='auto', random_state=1633155700), # DecisionTreeRegressor(max_features='auto', random_state=623929223)]
2. 增量学习在 Kaggle 数据上的应用
2.1 实际应用
现在我们使用一个 385MB 的 csv 文件作为例子,进行巨量数据的导入和训练(当然,在实际中 csv 文件往往是 5G 以上,基本不可能使用 excel 打开进行简单分析或观察)。
该数据是来自 Kaggle 的五大人格心理特质回归数据集。五大人格心理特质是心理学当中常见的人格分类法,也称为 FFM 模型或 OCEAN 模型。这种人格分类法是通过给与被调查者一些描述性格方面的句子,让被调查者选择自己符合的项目,例如:
(1) 考试前我总是提前准备好一切,尽全力避免出错。
(2) 考试前我会花几天时间准备。
(3) 我在考试前临时抱佛脚。
(4) 我考试前从不准备。
(5) 我不在意考试,甚至不记得考试的时间。
你从中选择最像你的选项,和最不像你的选项,选择结果最终被用于性格分类。该数据集通过收集 100w 人群在大约 80 个问题当中的选项,得出最终性格分数和分类。
训练集大约有一百万样本,测试集则有 2w 样本,更详细的状况可以查看:https://www.kaggle.com/tunguz/big-five-personality-test。
在面对大型数据时,我们采用循环模式分批读取巨大 csv 或数据库文件中的内容,并将数据分批进行预处理、再增量学习到一个模型当中。
现在,我们在干净的数据上来看看增量学习具体的步骤:
(1) 定义训练和测试数据地址
trainpath = r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\Big data\bigdata_train.csv" testpath = r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\Big data\bigdata_test.csv"
(2) 设法找出 csv 中的总数据量
我们决定使用增量学习时,数据应该是巨大到不可能直接打开查看、不可能直接训练、甚至不可能直接导入的(比如,超过 20 个 G)。但如果我们需要对数据进行循环导入,就必须知道真实的数据量大概有多少,因此我们可以从以下途径获得无法打开的 csv 中的数据量:
如果是比赛数据集,一般可以在比赛页面找到相应的说明。
如果是数据库数据集,则可以在数据库中进行统计。
如果无法找到相应的说明,可以使用 deque 库导入 csv 文件的最后几行,查看索引。
如果数据没有索引,就只能够靠 pandas 尝试找出大致的数据范围了。
#使用deque与StringIO辅助,导入csv文件最后的n行 from collections import deque #deque:双向队列 from io import StringIO with open(trainpath, 'r') as data: q = deque(data, 5) q pd.read_csv(StringIO(''.join(q)), header=None)
可以看到最后一行的索引是 995033,因此训练集中有 99w 条数据。
如果数据没有索引,则使用 pandas 中的 skiprows 与 nrows 进行尝试。
skiprows 表示本次导入跳过前 skiprows 行。
nrows 表示本次导入只导入 nrows 行。
例如,当 skiprows=1000, nrows=1000 时,pandas 会导入 1001~2000 行。
但是当 skiprows 超出数据量时,就会报空值错误 EmptyDataError。
for i in range(0,10**7,100000): df = pd.read_csv(trainpath,skiprows=i, nrows=1) print(i)
- 可以看到 90w 顺利导入了,但是 100w 报错了,所以数据量在 90-100w 之间。如果我们想,我们可以继续精确数据量的具体范围,但通常来说我们只要确认 10w 以内的区域就可以了。
- (3) 确认数据量后,准备循环范围
[*range(0,10**6,50000)] looprange = range(0,10**6,50000)
- (4) 建立增量学习使用的模型,定义测试集
reg = RFR(n_estimators=10 ,random_state=1412 ,warm_start=True ,verbose=True #增量学习的过程总是很漫长的,你可以选择展示学习过程 ,n_jobs=-1 #调用你全部的资源进行训练 ) #定义测试集 test = pd.read_csv(testpath,header="infer",index_col=0) Xtest = test.iloc[:,:-1] Ytest = test.iloc[:,-1] Xtest.head()
- (5) 开始循环导入与增量学习
- 当 skiprows 与 nrows 之和超出数据量的时候,会发生什么?
trainsubset = pd.read_csv(trainpath, header=None, index_col=0 , skiprows=950000 , nrows=50000) trainsubset.tail(5) #会导出全部剩下的数据,即便不足200w trainsubset.shape #(45035, 110) for line in looprange: if line == 0: #首次读取时,保留列名,并且不增加树的数量 header = "infer" newtree = 0 else: #非首次读取时,不要列名,每次增加10棵树 header = None newtree = 10 trainsubset = pd.read_csv(trainpath, header = header, index_col=0, skiprows=line, nrows=50000) Xtrain = trainsubset.iloc[:,:-1] Ytrain = trainsubset.iloc[:,-1] reg.n_estimators += newtree reg = reg.fit(Xtrain,Ytrain) print("DONE",line+50000) #当训练集的数据量小于50000时,打断循环 if Xtrain.shape[0] < 50000: break
- 现在,全部的 99w 数据都已经训练完毕了,我们可以在测试集上进行测试:
reg.score(Xtest,Ytest) #R2 99%,这可能与测试集上的数据太少有关 #0.9903482355083931
当使用增量学习时,如果需要调参,我们则需要将增量学习循环打包成一个评估器或函数,以便在调参过程中不断调用,这个过程所需的计算量是异常大的,不过至少我们拥有了在 CPU 上训练巨大数据的方法。
二、Bagging 方法 6 大面试热点问题
- 本节,我们总结关于随机森林和 bagging 的几个重点面试问题,深入地探讨一下 Bagging 算法背后相关的原理,这对于我们理解后续模型融合中的 Voting 与 Averaging 方法有重要的意义。本节一共包括6个问题:
Q1:为什么 Bagging 算法的效果比单个评估器更好?
该问题其实是在考察 Bagging 方法降低模型泛化误差的基本原理。
泛化误差是模型在未知数据集上的误差,更低的泛化误差是所有机器学习/深度学习建模的根本目标。在机器学习当中,泛化误差一般被认为由偏差、方差和噪音构成。
其中偏差是预测值与真实值之间的差异,衡量模型的精度。方差是模型在不同数据集上输出的结果的方差,衡量模型稳定性。噪音是数据收集过程当中不可避免的、与数据真实分布无关的信息。
当算法是回归算法、且模型衡量指标是 MSE 时,模型的泛化误差可以有如下定义:
Bagging 的基本思想是借助弱评估器之间的独立性来降低方差,从而降低整体的泛化误差。这个思想可以被推广到任意并行使用弱分类器的算法或融合方式上,极大程度地左右了并行融合方式的实际使用结果。
其中,降低方差指的是 Bagging 算法输出结果的方差一定小于弱评估器输出结果的方差,因此在相同数据上,随机森林往往比单棵决策树更加稳定,也因此随机森林的泛化能力往往比单棵决策树更强。
Q2:为什么 Bagging 可以降低方差?
r = np.array([-1,-1,-1, 1, 1, 1, 1]) #-1,1 (r == 1).sum() #4 (r == -1).sum() #3
按少数服从多数结果,随机森林的输出应该是 1。这个过程可以很容易使用函数来替代,只要我们对所有树的结果的均值套上 sigmoid 函数,再以 0.5 为阈值就可以。
r = np.array([-1,-1,-1, 1, 1, 1, 1]) r.mean() #0.14285714285714285 def sigmoid(z): return 1/(1+np.e**(-z)) sigmoid(r.mean()) #0.5356536708339716
Q3:Bagging 有效的基本条件有哪些?Bagging 的效果总是强于弱评估器吗?
在之前的学习当中,我们已经或多或少地涉及到了这个问题——Bagging 当然不总是有效的,Bagging 能够提升模型效果的条件有以下三个:
(1) 弱评估器的偏差较低,特别地来说,弱分类器的准确率至少要达到 50% 以上。
(2) 弱评估器之间相关性弱,最好相互独立。
(3) 弱评估器是方差较高、不稳定的评估器。
第一个条件非常容易解释。Bagging 集成算法是对基评估器的预测结果进行平均或用多数表决原则来决定集成评估器的结果。在分类的例子中,假设我们建立了 25 棵树,对任何一个样本而言,平均或多数表决原则下,当且仅当有 13 棵以上的树判断错误的时候,随机森林才会判断错误。
假设单独一棵决策树在样本 i 上的分类准确率在0.8上下浮动,那一棵树判断错误的概率大约就有 0.2(ε),那随机森林判断错误的概率(有 13 棵及以上的树都判断错误的概率)是:
import numpy as np from scipy.special import comb np.array([comb(25,i)*(0.2**i)*((1-0.2)**(25-i)) for i in range(13,26)]).sum() #0.00036904803455582827
可见,判断错误的几率非常小,这让随机森林的表现比单棵决策树好很多。基于上述式子,我们可以绘制出以弱分类器的误差率 ε 为横坐标、随机森林的误差率为纵坐标的图像。大家可以自己运行一下这段代码,看看图像呈什么样的分布。
import numpy as np x = np.linspace(0,1,20) y = [] for epsilon in np.linspace(0,1,20): E = np.array([comb(25,i)*(epsilon**i)*((1-epsilon)**(25-i)) for i in range(13,26)]).sum() y.append(E) plt.plot(x,y,"o-") plt.plot(x,x,"--",color="red") plt.xlabel("individual estimator's error") plt.ylabel("RandomForest's error") plt.grid() plt.show()
可以从图像上看出,当基分类器的误差率小于 0.5,即准确率大于 0.5 时,集成的效果是比弱分类器要好的。相反,当基分类器的误差率大于 0.5,袋装的集成算法就失效了。所以在使用随机森林之前,一定要检查,用来组成随机森林的分类树们是否都有至少 50% 的预测正确率。
对于第二个条件,我们在证明 Bagging 降低方差的数学过程中已经申明了很多次,唯有弱评估器之间相互独立、弱评估器输出的结果相互独立时,方差计算公式的前提假设才能被满足,Bagging 才能享受降低方差的福利。
然而在现实中,森林中的弱评估器很难完全相互独立,因为所有弱评估器都是在相同的数据上进行训练的、因此构建出的树结构也大同小异。幸运的是,我们能够衡量弱评估器之间相关性。以随机森林回归为例,假设任意弱评估器之间的相关系数为 ρ ρρ,则随机森林输出结果的方差等于
这个公式是根据比奈梅定义与协方差相关的公式推导出来的,这暗示随机森林输出结果的方差与森林中弱评估器之间的相关性是负相关的,弱评估器之间的相关性越强,随机森林输出的结果的方差就越大,Bagging 方法通过降低方差而获得的泛化能力就越小。
因此在使用随机森林时,我们需要让弱评估器之间尽量相互独立,我们也可以通过这一点来提升随机森林的水平。
同样,因为 Bagging 是作用于方差的集成手段,所以 Bagging 方法擅长处理方差大、偏差低的模型,而不擅长处理方差小、偏差大的模型,这能够解释 Bagging 有效的剩下两个原因。
Q4:Bagging 方法可以集成决策树之外的算法吗?
强大又复杂的算法如决策树、支持向量机等,往往学习能力较强,倾向于表现为偏差低、方差高,这些算法就比较适合于 Bagging。而线性回归、逻辑回归、KNN 等复杂度较低的算法,学习能力较弱但表现稳定,因此倾向于表现为偏差高,方差低,就不太适合被用于 Bagging。
这也解答了另一个常见面试问题:Bagging除了能用于决策树,还能用于其他弱评估器吗?现在答案已经很清晰了。
在 sklearn 当中,我们可以使用以下类来轻松实现 Bagging:
class sklearn.ensemble.BaggingRegressor(base_estimator=None, n_estimators=10, *, max_samples=1.0, max_features=1.0, bootstrap=True, bootstrap_features=False, oob_score=False, warm_start=False, n_jobs=None, random_state=None, verbose=0) class sklearn.ensemble.BaggingClassifier(base_estimator=None, n_estimators=10, *, max_samples=1.0, max_features=1.0, bootstrap=True, bootstrap_features=False, oob_score=False, warm_start=False, n_jobs=None, random_state=None, verbose=0)
不难发现,这两个类的参数与随机森林大同小异,只不过因为弱分类器不再局限于树模型,因此不再具有对树模型进行剪枝的一系列参数。只要你对随机森林有深入的掌握,便可以轻松使用这两个类。、需要注意的是,这两个类只能够接受 sklearn 中的评估器作为弱评估器。
Q5:怎样增强 Bagging 中弱评估器的独立性?
正如之前所说,在实际使用数据进行训练时,我们很难让 Bagging 中的弱评估器完全相互独立,主要是因为:
(1) 训练的数据一致。
(2) 弱评估器构建的规则一致。
导致最终建立的弱评估器都大同小异,Bagging 的效力无法完整发挥出来。为了弱评估器构建规则一致的问题,我们有了 Averaging 和 Voting 这样的模型融合方法:基本来看,就是使用 Bagging 的逻辑来融合数个不同算法的结果。
而当我们不使用模型融合时,我们可以使用随机性来削弱弱分类器之间的联系、增强独立性、提升随机森林的效果。
在随机森林中,天生就存在有放回随机抽取样本建树的机制,因此才会有 bootstrap、max_samples 等参数,才会有袋外数据、袋外评估指标 oob_score 等属性,意在使用不同的数据建立弱评估器。
除了有放回随机抽样之外,还可以使用 max_features 随机抽样特征进行分枝,加大弱评估器之间的区别。
正因为存在不同的随机的方式,Bagging 集成方法下才有了多种不同的算法。
Q6:除了随机森林,你还知道其他 Bagging 算法吗?
Bagging 方法的原理简单,因此 Bagging 算法之间的不同主要体现在随机性的不同上。
在 sklearn 当中,除了随机森林之外还提供另一个 bagging 算法:极端随机树。极端随机树是一种比随机森林更随机、对方差降低更多的算法,我们可以通过以下两个类来实现它:
class sklearn.ensemble.ExtraTreesClassifier(n_estimators=100, *, criterion='gini', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features='auto', max_leaf_nodes=None, min_impurity_decrease=0.0, bootstrap=False, oob_score=False, n_jobs=None, random_state=None, verbose=0, warm_start=False, class_weight=None, ccp_alpha=0.0, max_samples=None) class sklearn.ensemble.ExtraTreesRegressor(n_estimators=100, *, criterion='squared_error', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features='auto', max_leaf_nodes=None, min_impurity_decrease=0.0, bootstrap=False, oob_score=False, n_jobs=None, random_state=None, verbose=0, warm_start=False, ccp_alpha=0.0, max_samples=None)
与随机森林一样,极端随机树在建树时会随机挑选特征,但不同的是,随机森林会将随机挑选出的特征上每个节点都进行完整、精致的不纯度计算,然后挑选出最优节点,而极端随机树则会随机选择数个节点进行不纯度计算,然后选出这些节点中不纯度下降最多的节点。
这样生长出的树比随机森林中的树更不容易过拟合,同时独立性更强,因此极端随机树可以更大程度地降低方差。