bagging框架
bagging
1 bagging介绍
-
Bagging的全称是Bootstrap Aggregating,其思想是通过将许多相互独立的学习器的结果进行结合,从而提高整体学习器的泛化能力
-
bagging框架流程:首先,它从原始数据集中使用有放回的随机采样方式抽取多个子集,这些子集的大小与原始数据集相同。然后,它使用每个子集独立地训练一个学习器。最后,当需要进行预测时,Bagging算法将所有学习器的预测结果进行结合,以得出最终的预测结果。
###1 bagging代码
-
bagging框架-代码
class Bagging: def __init__(self, base_learner, n_learners): self.learners = [base_learner for _ in range(n_learners)] def fit(self, X, y): for learner in self.learners: examples = np.random.choice(np.arange(len(X)), int(len(X)), replace=True) learner.fit(X.iloc[examples, :], y.iloc[examples]) def predict(self, X): preds = [learner.predict(X) for learner in self.learners] return np.array(preds).mean(axis=0)
2 随机森林分类
-
sklearn.ensemble.RandomForestClassifier
sklearn.ensemble.RandomForestClassifier (n_estimators=’10’ , 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 , min_impurity_split=None , bootstrap=True , oob_score=False , n_jobs=None , random_state=None , verbose=0 , warm_start=False , class_weight=None )
2.1 参数
-
参数
参数 含义 criterion 不纯度的衡量指标,有gini系数和信息熵entropy(实际用的是信息增益)两种选择 max_depth 树的最大深度,超过最大深度的树会被剪掉 min_samples_leaf 一个节点在分支后的每个子节点都必须包含至少min_samples_leaf个训练样本,否则分支就不会发生 min_samples_split 一个节点必须哟啊包含至少min_samples_split个训练样本,这个节点才允许被分支,否则分支就不会发生 max_features 限制分支时考虑的特征个数,超过限制个数的特征会被舍弃,默认值为总特征个数开平方取整。 min_impurity_decrease 限制信息增益的大小,信息增益小于设定数值的分支不会发生 单个决策树的准确率越高,随机森林的准确率也会越高,因为装袋法是依赖于平均值或少数服从多数的原则来决定集成结果的。
-
n_estimators(随机森林与决策树交叉验证对比、n_estimators学习曲线)这是森林中树木的数量
,即基评估器的数量。这个参数对随机森林模型的精确性影响是单调的,n_estimators越 大,模型的效果往往越好。但是相应的,任何模型都有决策边界,n_estimators达到一定的程度之后,随机森林的 精确性往往不在上升或开始波动
,并且,n_estimators越大,需要的计算量和内存也越大,训练的时间也会越来越 长。对于这个参数,我们是渴望在训练难度和模型效果之间取得平衡。
n_estimators的默认值在现有版本的sklearn中是10,但是在0.22版本中,这个默认值被修正为 100。这个修正显示出了使用者的调参倾向:要更大的n_estimators。
2.2代码分解
-
导入需要的包、数据集
"""导入需要的包、数据集""" import pandas as pd from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_wine from sklearn.model_selection import train_test_split from sklearn.model_selection import cross_val_score from matplotlib import pyplot as plt from tqdm import notebook plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.rcParams["font.family"] = 'Arial Unicode MS' %matplotlib inline wine = load_wine() data = pd.DataFrame(wine.data,columns=wine.feature_names) data['y'] = wine.target data.head()
-
决策树、随机森林建模
"""决策树、随机森林建模""" X_train,X_test,y_train,y_test = train_test_split(wine.data ,wine.target ,test_size=.3 ) # 单颗决策树 clf = DecisionTreeClassifier(random_state=0) #随机森林 rfc = RandomForestClassifier(random_state=0) clf = clf.fit(X_train,y_train) rfc = rfc.fit(X_train,y_train) score_clf = clf.score(X_test,y_test) score_rfc = rfc.score(X_test,y_test) print(f'Single Tree: {score_clf}') print(f'Random Forest: {score_rfc}') """输出""" Single Tree: 0.9074074074074074 Random Forest: 0.9814814814814815
-
画出随机森林和决策树在一组十折交叉验证下的效果对比
"""画出随机森林和决策树在一组十折交叉验证下的效果对比""" # 单颗决策树、交叉验证 clf = DecisionTreeClassifier() clf_s = cross_val_score(clf,wine.data,wine.target,cv=10) # 随机森林、交叉验证 rfc = RandomForestClassifier(n_estimators=25) rfc_s = cross_val_score(rfc,wine.data,wine.target,cv=10) plt.figure(figsize=(12,8)) plt.plot(range(1,11),rfc_s,label = "RandomForest") plt.plot(range(1,11),clf_s,label = "Decision Tree") plt.xticks(rotation='-45') plt.ylabel('精度(整体准确率)') plt.title('随机森林和决策树在一组十折交叉验证下的效果对比') plt.legend() plt.show() #====================一种更加有趣也更简单的写法===================# """ label = "RandomForest" for model in [RandomForestClassifier(n_estimators=25),DecisionTreeClassifier()]: score = cross_val_score(model,wine.data,wine.target,cv=10) print("{}:".format(label)),print(score.mean()) plt.plot(range(1,11),score,label = label) plt.legend() label = "DecisionTree" """
可以看出,单棵决策树的效果是不如(小于等于)随机森林的
-
画出随机森林和决策树在十组十折交叉验证下的效果对比
"""画出随机森林和决策树在十组十折交叉验证下的效果对比""" rcf_1 = [] # 随机森林十组十折交叉验证均值记录 clf_1 = [] # 单颗决策树十组十折交叉验证均值记录 for i in range(10): rcf = RandomForestClassifier(n_estimators=25) rcf_s = cross_val_score(rcf,wine.data,wine.target,cv=10).mean() rcf_1.append(rcf_s) clf = DecisionTreeClassifier() clf_s = cross_val_score(clf,wine.data,wine.target,cv=10).mean() clf_1.append(clf_s) plt.figure(figsize=(12,8)) plt.plot([f"第{i}组" for i in range(1,11)],rcf_1,label='Random Forest') plt.plot([f"第{i}组" for i in range(1,11)],clf_1,label='Decision Tree') plt.ylabel('10折交叉验证精度平均值') plt.title('随机森林和决策树在十组十折交叉验证下的效果对比') plt.legend() plt.show()
-
n_estimators
学习曲线"""n_estimators学习曲线""" superpa = [] # 记录不同基分类器数量下,随机森林交叉验证平均值 for i in notebook.tqdm(range(1,201)): rfc = RandomForestClassifier(n_estimators=i,n_jobs=-1) rfc_s = cross_val_score(rfc,wine.data,wine.target,cv=10).mean() superpa.append(rfc_s) print(f"随机森林交叉验证精度最大值:{max(superpa)}") print(f'随机森林精度对大值对应的基分类器数量:',superpa.index(max(superpa))) plt.figure(figsize=(12,8)) plt.plot(range(1,201),superpa) plt.xlabel('基分类器数量') plt.ylabel('10折交叉验证精度平均值') plt.title('n_estimators学习曲线') plt.show()
可见随着基分类器数量增加,随机森林准确度上升,当到达一定数量后趋于平缓(例子中是约15左右)。
2.3 random_state(随机森林中控制一群树)
-
随机森林的本质是一种装袋集成算法(bagging),装袋集成算法是对基评估器的预测结果进行平均或用多数表决 原则来决定集成评估器的结果。在刚才的红酒例子中,我们建立了25棵树,对任何一个样本而言,平均或多数表决 原则下,当且仅当有13棵以上的树判断错误的时候,随机森林才会判断错误。单独一棵决策树对红酒数据集的分类 准确率在0.85上下浮动,假设一棵树判断错误的可能性为0.2( ε \varepsilon ε),那13棵树以上都判断错误的可能性是:
e r a n d o m f o r e s t = ∑ i = 13 25 C 25 i ε i ( 1 − ε ) 25 − i = 0.000369 e_{{random_forest }}=\sum_{i=13}^{25} C_{25}^{i} \varepsilon^{i}(1-\varepsilon)^{25-i}=0.000369 erandomforest=i=13∑25C25iεi(1−ε)25−i=0.000369
其中,i是判断错误的次数,也是判错的树的数量,ε是一棵树判断错误的概率,(1-ε)是判断正确的概率,共判对 25-i次。采用组合,是因为25棵树中,有任意i棵都判断错误。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()
可见,判断错误的几率非常小,这让随机森林在红酒数据集上的表现远远好于单棵决策树。
那现在就有一个问题了:我们说袋装法服从多数表决原则或对基分类器结果求平均,这即是说,我们默认森林中的 每棵树应该是不同的,并且会返回不同的结果。设想一下,如果随机森林里所有的树的判断结果都一致(全判断对 或全判断错),那随机森林无论应用何种集成原则来求结果,都应该无法比单棵决策树取得更好的效果才对。但我 们使用了一样的类DecisionTreeClassifier,一样的参数,一样的训练集和测试集,为什么随机森林里的众多树会有 不同的判断结果呢?
问到这个问题,可能就会想到了:sklearn中的分类树DecisionTreeClassifier自带随机性,所以随机森 林中的树天生就都是不一样的。我们在讲解分类树时曾提到,决策树从最重要的特征中随机选择出一个特征来进行 分枝,因此每次生成的决策树都不一样,这个功能由参数random_state控制。
随机森林中其实也有random_state,用法和分类树中相似,只不过在分类树中,一个random_state只控制生成一 棵树,而随机森林中的random_state控制的是生成森林的模式,而非让一个森林中只有一棵树。
from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_wine wine = load_wine() """random_state = 2""" rfc = RandomForestClassifier(n_estimators=10,random_state=2) rfc = rfc.fit(wine.data, wine.target) #随机森林的重要属性之一:estimators,查看森林中树的状况 rfc.estimators_[0].random_state for i in range(len(rfc.estimators_)): print(rfc.estimators_[i].random_state) """输出""" 1872583848 794921487 111352301 1853453896 213298710 1922988331 1869695442 2081981515 1805465960 1376693511 """random_state = 3""" rfc = RandomForestClassifier(n_estimators=10,random_state=3) rfc = rfc.fit(wine.data, wine.target) #随机森林的重要属性之一:estimators,查看森林中树的状况 rfc.estimators_[0].random_state for i in range(len(rfc.estimators_)): print(rfc.estimators_[i].random_state) """输出""" 218175338 303761048 893988089 1460070019 1249426360 521102280 46504192 297689877 1687694333 1877166739
我们可以观察到,当random_state固定时,随机森林中生成是一组固定的树,但每棵树依然是不一致的,这是 用”随机挑选特征进行分枝“的方法得到的随机性。并且我们可以证明,
当这种随机性越大的时候,袋装法的效果一 般会越来越好
。用袋装法集成时,基分类器应当是相互独立的,是不相同的。
但这种做法的局限性是很强的,当我们需要成千上万棵树的时候,数据不一定能够提供成千上万的特征来让我们构 筑尽量多尽量不同的树。因此,除了random_state。我们还需要其他的随机性。
2.4 bootstrap、oob_score
-
有放回抽样、bootstrap
-
要让基分类器尽量都不一样,一种很容易理解的方法是使用不同的训练集来进行训练,而袋装法正是通过有放回的 随机抽样技术来形成不同的训练数据,
bootstrap
就是用来控制抽样技术的参数。 -
bootstrap
参数默认True,代表采用这种又放回的随机抽样技术,通常这个参数不会被我们设置为False。 -
然而有放回抽样也会有自己的问题。由于是有放回,一些样本可能在同一个自助集中出现多次,而其他一些却可能 被忽略,一般来说,自助集大约平均会包含63%的原始数据。因为每一个样本被抽到某个自助集中的概率为:
1 − ( 1 − 1 n ) n 1-(1-\frac{1}{n })^n \\ 1−(1−n1)nlim n → ∞ [ 1 − ( 1 − 1 n ) n ] = 1 − 1 e \lim_{n\rightarrow \infty}\left[1-(1-\frac{1}{n })^n\right] \quad = \quad 1-\frac{1}{e} n→∞lim[1−(1−n1)n]=1−e1
当n足够大时,这个概率收敛于1-(1/e),约等于0.632。
2.5 袋外数据、oob、our of bag data
-
bootstrap
会使得约37%的训练集数据被浪费掉,没有参与建模,这些数据被称为袋外 数据(out of bag data,简写为oob)。 -
除了我们最开始就划分好的测试集之外,这些数据也可 以被用来作为集成算法的测试集。也就是说,在使用随机森林时,我们可以不划分测试集和训练集,只需要用袋外 数据来测试我们的模型即可。
-
当然,这也不是绝对的,当n和n_estimators都不够大的时候,很可能就没有数据掉 落在袋外,自然也就无法使用oob数据来测试模型了。
-
如果希望用袋外数据来测试,则需要在实例化时就将oob_score这个参数调整为True,训练完毕之后,我们可以用 随机森林的另一个重要属性:oob_score_来查看我们的在袋外数据上测试的结果。
2.6 实例
-
代码
from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_wine wine = load_wine() # 无需划分训练集和测试集 rfc = RandomForestClassifier(n_estimators=25,oob_score=True) rfc = rfc.fit(wine.data,wine.target) # 重要属性oob_score_ print('训练集精度:',rfc.score(wine.data,wine.target)) print('测试集(袋外数据)精度',rfc.oob_score_) """输出""" 训练集精度: 1.0 测试集(袋外数据)精度 0.9662921348314607
2.7 其他重要属性和接口
-
属性
.estimators_
.oob_score
.feature_importances
-
常用接口
apply
fit
predict
score
-
除此之外,还需要注 意随机森林的
predict_proba
接口,这个接口返回每个测试样本对应的被分到每一类标签的概率,标签有几个分类 就返回几个概率。如果是二分类问题,则predict_proba返回的数值大于0.5的,被分为1,小于0.5的,被分为0。 -
注意
- 传统的随机森林是利用袋装法中的规则,平均或少数服从多数来决定集成的结果
- sklearn中的随机森林是平均 每个样本对应的predict_proba返回的概率,得到一个·
平均概率
·,从而决定测试样本的分类
3 Bagging的必要条件
-
必要条件
-
要求基评估器要尽量独立,
且不稳定
(下图为稳定和不稳定基学习器的误差区别) -
基分类器的判断准确率至少要超过随机分类器。
-
-
随机森林准确率公式(假设使用25个基分类器)
e r a n d o m _ f o r e s t = ∑ i = 13 25 C 25 i ε i ( 1 − ε ) 25 − i e_{{random\_forest }}=\sum_{i=13}^{25} C_{25}^{i} \varepsilon^{i}(1-\varepsilon)^{25-i} erandom_forest=i=13∑25C25iεi(1−ε)25−i
基于上面的公式,用下面的代码画出基分类器的误差率 ε \varepsilon ε 和随机森林的误差率之间的图像。import numpy as np from matplotlib import pyplot as plt from scipy.special import comb %matplotlib inline x = np.linspace(0,1,20) # 创建一个基分类器的误差率列表,从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.figure(figsize=(12,8)) plt.plot(x,y,"o-",label="when estimators are different") plt.plot(x,x,"--",color="red",label="if all estimators are same") plt.xlabel("individual estimator's error") plt.ylabel("RandomForest's error") plt.legend() plt.show()
可以从图像上看出,当基分类器的误差率小于0.5,即准确率大于0.5时,集成的效果是比基分类器要好的。相反, 当基分类器的误差率大于0.5,袋装的集成算法就失效了。所以在使用随机森林之前,一定要检查,用来组成随机 森林的分类树们是否都有至少50%的预测正确率。
4 随机森林回归
-
sklearn.ensemble.RandomForestRegressor
sklearn.ensemble.RandomForestRegressor (n_estimators=’warn’ , criterion=’mse’ , 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 , min_impurity_split=None , bootstrap=True , oob_score=False , n_jobs=None , random_state=None , verbose=0 , warm_start=False )
4.1 criterion
-
回归树衡量分枝质量的指标,支持的标准有三种:
-
输入
mse
使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为 特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失 -
输入
friedman_mse
使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差 -
输入
mae
使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值来最小化L1损失
M S E = 1 N ∑ i = 1 N ( f i − y i ) 2 M S E=\frac{1}{N} \sum_{i=1}^{N}\left(f_{i}-y_{i}\right)^{2} MSE=N1i=1∑N(fi−yi)2
其中N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi是样本点i实际的数值标签。所以MSE的本质, 其实是样本真实数据与回归结果的差异。在回归树中,MSE不只是我们的分枝质量衡量指标,也是我们最常用的衡 量回归树回归质量的指标,当我们在使用交叉验证,或者其他方式获取回归树的结果时,我们往往选择均方误差作 为我们的评估(在分类树中这个指标是score代表的预测准确率)。在回归中,我们追求的是,MSE越小越好。 然而,回归树的接口score返回的是R平方,并不是MSE。R平方被定义如下:
R 2 = 1 − u v u = ∑ i = 1 N ( f i − y i ) 2 v = ∑ i = 1 N ( y i − y ^ ) 2 \begin{array}{c} R^{2}=1-\frac{u}{v} \\ u=\sum_{i=1}^{N}\left(f_{i}-y_{i}\right)^{2} \quad v=\sum_{i=1}^{N}\left(y_{i}-\hat{y}\right)^{2} \end{array} R2=1−vuu=∑i=1N(fi−yi)2v=∑i=1N(yi−y^)2
其中u是残差平方和(MSE * N),v是总平方和,N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi 是样本点i实际的数值标签。y帽是真实数值标签的平均数。R平方可以为正为负(如果模型的残差平方和远远大于 模型的总平方和,模型非常糟糕,R平方就会为负),而均方误差永远为正。 值得一提的是,虽然均方误差永远为正,但是sklearn当中使用均方误差作为评判标准时,却是计算”负均方误 差“(neg_mean_squared_error)。这是因为sklearn在计算模型评估指标的时候,会考虑指标本身的性质,均 方误差本身是一种误差,所以被sklearn划分为模型的一种损失(loss),因此在sklearn当中,都以负数表示。真正的 均方误差MSE的数值,其实就是neg_mean_squared_error去掉负号的数字。 -
4.2 其他重要属性和接口
-
最重要的属性和接口,都与随机森林的分类器相一致,还是apply, fit, predict和score最为核心。
-
注意
: 随 机森林回归并没有predict_proba这个接口,因为对于回归来说,并不存在一个样本要被分到某个类别的概率问 题,因此没有predict_proba这个接口。 -
随机森林回归用法
from sklearn.datasets import load_boston from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestRegressor import sklearn boston = load_boston() regressor = RandomForestRegressor(n_estimators=100,random_state=0) cross_val_score(regressor, boston.data, boston.target, cv=10 ,scoring = "neg_mean_squared_error") """输出""" array([-10.60400153, -5.34859049, -5.00482902, -21.30948927, -12.21354202, -18.50599124, -6.89427068, -93.92849386, -29.91458572, -15.1764633 ])
返回十次交叉验证的结果,注意在这里,如果不填写
scoring = "neg_mean_squared_error"
,交叉验证默认的模型 衡量指标是R方,因此交叉验证的结果可能有正也可能有负。而如果写上scoring,则衡量标准是负MSE,交叉验 证的结果只可能为负。
5 随机森林实例
5.1 用随机森林回归填补缺失值
- 我们从现实中收集的数据,几乎不可能是完美无缺的,往往都会有一些缺失值。面对缺失值,很多人选择的方式是 直接将含有缺失值的样本删除,这是一种有效的方法,但是有时候填补缺失值会比直接丢弃样本效果更好,即便我 们其实并不知道缺失值的真实样貌。在
sklearn
中,我们可以使用sklearn.impute.SimpleImputer
(0.20版本以上)来轻松地将均 值,中值,或者其他最常用的数值填补到数据中,在这个案例中,我们将使用均值,0,和随机森林回归来填补缺 失值,并验证四种状况下的拟合状况,找出对使用的数据集来说最佳的缺失值填补方法。
5.1.1代码分解
-
导入所需要的库
"""导入需要的库""" import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_boston from sklearn.impute import SimpleImputer from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score from tqdm import tqdm
-
以波士顿房价数据集为例子,导入完整的数据集并探索
datasets = load_boston() datasets.data.shape #总共506*13=6578个数据 X_full,y_full = datasets.data,datasets.target # 完整数据集 n_samples = X_full.shape[0] # 样本量 n_features = X_full.shape[1] # 特征量
-
为完整数据集放入缺失值
"""为完整数据集放入缺失值""" #首先确定我们希望放入的缺失数据的比例,在这里我们假设是50%,那总共就要有3289个数据缺失 rng = np.random.RandomState(0) missing_rate = 0.5 n_missing_samples = int(np.floor(n_samples * n_features * missing_rate)) # 3289 #np.floor向下取整,返回.0格式的浮点数,所以需要将其转换成整数 # 创建0到n_features(这里是13)之间,长度为n_missing_samples(这里是3289)的数组,用来当做列索引 missing_features = rng.randint(0,n_features,n_missing_samples) # 创建0到n_samples(这里是506)之间,长度为n_missing_samples(这里是3289)的数组,用来当做行索引 missing_samples = rng.randint(0,n_samples,n_missing_samples) #我们现在采样了3289个数据,远远超过我们的样本量506,所以我们使用随机抽取的函数randint。但如果我们需要的数据量小于我们的样本量506,那我们可以采用np.random.choice来抽样,choice会随机抽取不重复的随机数,因此可以帮助我们让数据更加分散,确保数据不会集中在一些行中 X_missing = X_full.copy() y_missing = y_full.copy() # 将随机抽取的行列,置位Nan X_missing[missing_samples,missing_features] = np.nan #转换成DataFrame是为了后续方便各种操作,numpy对矩阵的运算速度快到拯救人生,但是在索引等功能上却不如pandas来得好用 X_missing = pd.DataFrame(X_missing)
-
使用0和均值填补缺失值
"""使用0和均值填补缺失值""" # 使用均值进行填充 imp_mean = SimpleImputer(missing_values=np.nan,strategy='mean') X_missing_mean = imp_mean.fit_transform(X_missing) # 使用0进行填充 imp_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0) X_missing_0 = imp_0.fit_transform(X_missing)
-
使用随机森林填补缺失值
任何回归都是从特征矩阵中学习,然后求解连续型标签y的过程,之所以能够实现这个过程,是因为回归算法认为,特征 矩阵和标签之前存在着某种联系。
实际上,标签和特征是可以相互转换的,比如说,在一个“用地区,环境,附近学校数 量”预测“房价”的问题中,我们既可以用“地区”,“环境”,“附近学校数量”的数据来预测“房价”,也可以反过来, 用“环境”,“附近学校数量”和“房价”来预测“地区”。
而回归填补缺失值,正是利用了这种思想。 对于一个有n个特征的数据来说,其中特征T有缺失值,我们就把特征T当作标签,其他的n-1个特征和原本的标签组成新 的特征矩阵。那对于T来说,它没有缺失的部分,就是我们的Y_test,这部分数据既有标签也有特征,而它缺失的部 分,只有特征没有标签,就是我们需要预测的部分。 特征T不缺失的值对应的其他n-1个特征 + 本来的标签:X_train 特征T不缺失的值:Y_train 特征T缺失的值对应的其他n-1个特征 + 本来的标签:X_test 特征T缺失的值:未知,我们需要预测的Y_test 这种做法,对于某一个特征大量缺失,其他特征却很完整的情况,非常适用。
那如果数据中除了特征T之外,其他特征也有缺失值怎么办?
答案是
遍历所有的特征,从缺失最少的开始进行填补
(因为填补缺失最少的特征所需要的准确信息最少)。 填补一个特征时,先将其他特征的缺失值用0代替,每完成一次回归预测,就将预测值放到原本的特征矩阵中,再继续填 补下一个特征。每一次填补完毕,有缺失值的特征会减少一个,所以每次循环后,需要用0来填补的特征就越来越少。当 进行到最后一个特征时(这个特征应该是所有特征中缺失值最多的),已经没有任何的其他特征需要用0来进行填补了, 而我们已经使用回归为其他特征填补了大量有效信息,可以用来填补缺失最多的特征。 遍历所有的特征后,数据就完整,不再有缺失值了。# 使用均值进行填充 imp_mean = SimpleImputer(missing_values=np.nan,strategy='mean') X_missing_mean = imp_mean.fit_transform(X_missing) # 使用0进行填充 imp_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0) X_missing_0 = imp_0.fit_transform(X_missing)
-
使用随机森林填补缺失值
"""使用随机森林填补缺失值""" X_missing_reg = X_missing.copy() # 按缺失值的多少进行排序,返回排序后的特征的索引 sortindex = np.argsort(X_missing_reg.isnull().sum(axis=0)).values for i in sortindex:#从缺失值最少的那个特征进行填充 # 构建新特征矩阵和新标签 df = X_missing_reg fillc = df.iloc[:,i] # 第i列是当前需要填充的列 df = pd.concat([df.iloc[:,df.columns !=i], pd.DataFrame(y_full)] , axis=1) # 在新特征矩阵中,对含有缺失值的列,进行0的填补,否则会有很多的nan值存在 df_0 = SimpleImputer(missing_values=np.nan , strategy='constant' ,fill_value=0).fit_transform(df) # 找出训练集和测试集 y_train = fillc[fillc.notnull()] y_test = fillc[fillc.isnull()] X_train = df_0[y_train.index, :] X_test = df_0[y_test.index, :] # 用随机森林回归来填补缺失值 rfc = RandomForestRegressor(n_estimators=100) rfc = rfc.fit(X_train, y_train) y_predict = rfc.predict(X_test) #将填补好的特征返回到我们的原始的特征矩阵中 X_missing_reg.loc[X_missing_reg.iloc[:,i].isnull(),i] = y_predict
-
对填补号的数据进行建模
"""对填补号的数据进行建模""" # 对所有数据进行建模,取得MSE结果 X = [X_full,X_missing_mean,X_missing_0,X_missing_reg] mse = [] std = [] for x in tqdm(X): estimator = RandomForestRegressor(random_state=0,n_estimators=100) scores = cross_val_score(estimator ,x ,y_full ,scoring='neg_mean_squared_error' ,cv=10).mean() mse.append(scores * -1)
-
用所得的结果画出条形图
"""用所得的结果画出条形图""" x_labels = ['Full data', 'Zero Imputation', 'Mean Imputation', 'Regressor Imputation'] colors = ['r', 'g', 'b', 'orange'] plt.figure(figsize=(12, 8),dpi=500) ax = plt.subplot(111) for i in np.arange(len(mse)): ax.barh(i, mse[i],color=colors[i], alpha=0.6, align='center') ax.set_title('Imputation Techniques with Boston Data') ax.set_xlim(left=np.min(mse) * 0.9, right=np.max(mse) * 1.1) ax.set_yticks(np.arange(len(mse))) ax.set_xlabel('MSE') ax.set_yticklabels(x_labels) plt.show()
5.1.2 全部代码
-
随机森林回归填补缺失值
"""导入需要的库""" import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_boston from sklearn.impute import SimpleImputer from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score from tqdm import tqdm """以波士顿房价数据集为例子,导入完整的数据集并探索""" datasets = load_boston() datasets.data.shape #总共506*13=6578个数据 X_full,y_full = datasets.data,datasets.target # 完整数据集 n_samples = X_full.shape[0] # 样本量 n_features = X_full.shape[1] # 特征量 """为完整数据集放入缺失值""" #首先确定我们希望放入的缺失数据的比例,在这里我们假设是50%,那总共就要有3289个数据缺失 rng = np.random.RandomState(0) missing_rate = 0.5 n_missing_samples = int(np.floor(n_samples * n_features * missing_rate)) # 3289 #np.floor向下取整,返回.0格式的浮点数,所以需要将其转换成整数 # 创建0到n_features(这里是13)之间,长度为n_missing_samples(这里是3289)的数组,用来当做列索引 missing_features = rng.randint(0,n_features,n_missing_samples) # 创建0到n_samples(这里是506)之间,长度为n_missing_samples(这里是3289)的数组,用来当做行索引 missing_samples = rng.randint(0,n_samples,n_missing_samples) #我们现在采样了3289个数据,远远超过我们的样本量506,所以我们使用随机抽取的函数randint。但如果我们需要的数据量小于我们的样本量506,那我们可以采用np.random.choice来抽样,choice会随机抽取不重复的随机数,因此可以帮助我们让数据更加分散,确保数据不会集中在一些行中 X_missing = X_full.copy() y_missing = y_full.copy() # 将随机抽取的行列,置位Nan X_missing[missing_samples,missing_features] = np.nan #转换成DataFrame是为了后续方便各种操作,numpy对矩阵的运算速度快到拯救人生,但是在索引等功能上却不如pandas来得好用 X_missing = pd.DataFrame(X_missing) """使用0和均值填补缺失值""" # 使用均值进行填充 imp_mean = SimpleImputer(missing_values=np.nan,strategy='mean') X_missing_mean = imp_mean.fit_transform(X_missing) # 使用0进行填充 imp_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0) X_missing_0 = imp_0.fit_transform(X_missing) """使用随机森林填补缺失值""" X_missing_reg = X_missing.copy() # 按缺失值的多少进行排序,返回排序后的特征的索引 sortindex = np.argsort(X_missing_reg.isnull().sum(axis=0)).values for i in sortindex:#从缺失值最少的那个特征进行填充 # 构建新特征矩阵和新标签 df = X_missing_reg fillc = df.iloc[:,i] # 第i列是当前需要填充的列 df = pd.concat([df.iloc[:,df.columns !=i], pd.DataFrame(y_full)] , axis=1) # 在新特征矩阵中,对含有缺失值的列,进行0的填补,否则会有很多的nan值存在 df_0 = SimpleImputer(missing_values=np.nan , strategy='constant' ,fill_value=0).fit_transform(df) # 找出训练集和测试集 y_train = fillc[fillc.notnull()] y_test = fillc[fillc.isnull()] X_train = df_0[y_train.index, :] X_test = df_0[y_test.index, :] # 用随机森林回归来填补缺失值 rfc = RandomForestRegressor(n_estimators=100) rfc = rfc.fit(X_train, y_train) y_predict = rfc.predict(X_test) #将填补好的特征返回到我们的原始的特征矩阵中 X_missing_reg.loc[X_missing_reg.iloc[:,i].isnull(),i] = y_predict """对填补号的数据进行建模""" # 对所有数据进行建模,取得MSE结果 X = [X_full,X_missing_mean,X_missing_0,X_missing_reg] mse = [] std = [] for x in tqdm(X): estimator = RandomForestRegressor(random_state=0,n_estimators=100) scores = cross_val_score(estimator ,x ,y_full ,scoring='neg_mean_squared_error' ,cv=10).mean() mse.append(scores * -1) """用所得的结果画出条形图""" x_labels = ['Full data', 'Zero Imputation', 'Mean Imputation', 'Regressor Imputation'] colors = ['r', 'g', 'b', 'orange'] plt.figure(figsize=(12, 8),dpi=500) ax = plt.subplot(111) for i in np.arange(len(mse)): ax.barh(i, mse[i],color=colors[i], alpha=0.6, align='center') ax.set_title('Imputation Techniques with Boston Data') ax.set_xlim(left=np.min(mse) * 0.9, right=np.max(mse) * 1.1) ax.set_yticks(np.arange(len(mse))) ax.set_xlabel('MSE') ax.set_yticklabels(x_labels) plt.show()
5.2 随机森林在乳腺癌数据上的调参
- 在乳腺癌数据上进行一次随 机森林的调参。乳腺癌数据是sklearn自带的分类数据之一。
5.2.1 代码分解
-
导入需要的库
from sklearn.datasets import load_breast_cancer from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV from sklearn.model_selection import cross_val_score from matplotlib import pyplot as plt import pandas as pd import numpy as np from tqdm import notebook plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.rcParams["font.family"] = 'Arial Unicode MS'
-
导入数据集,探索数据
data = load_breast_cancer() print('x shape:',data.data.shape) print('y shape:',data.target.shape) # 乳腺癌数据集有569条记录,30个特征,维度不算太高,但是样本量非常烧,过拟合情况可能存在 """输出""" x shape: (569, 30) y shape: (569,)
-
进行一次简单建模,看看模型本身在数据集上的效果
rfc = RandomForestClassifier(n_estimators=100 ,random_state=90) score_pre = cross_val_score(rfc ,data.data ,data.target ,cv=10).mean() score_pre ##这里可以看到,随机森林在乳腺癌数据上的表现本就还不错,在现实数据集上, ##基本上不可能什么都不调就看到95%以 上的准确率 """输出""" 0.9596491228070174
-
随机森林调整的第一步:无论如何先来调
n_estimators
在这里我们选择学习曲线,可以使用网格搜索吗?可以,但是只有学习曲线,才能看见趋势 倾向是,要看见n_estimators在什么取值开始变得平稳,是否一直推动模型整体准确率的上升等信息 第一次的学习曲线,可以先用来帮助我们划定范围,我们取每十个数作为一个阶段,来观察n_estimators的变化如何 引起模型整体准确率的变化
score_list = [] for i in notebook.tqdm(range(0,200,10)): rfc = RandomForestClassifier(n_estimators=i+1 ,n_jobs=-1 ,random_state=25) score = cross_val_score(rfc ,data.data ,data.target ,cv=10).mean() score_list.append(score) print('最大精度:',max(score_list)) print('最大精度所对应的n_estimators:',(score_list.index(max(score_list))*10)+1) plt.figure(figsize=(12,8)) plt.plot(range(1,201,10),score_list) plt.xlabel('n_estimators') plt.ylabel('RF accuracy') plt.show() """输出""" 最大精度: 0.9631578947368421 最大精度所对应的n_estimators: 171
-
在确定好的范围内(上面最大是171这里取160到180),进一步细化学习
score_list = [] for i in notebook.tqdm(range(160,181)): rfc = RandomForestClassifier(n_estimators= i ,n_jobs=-1 ,random_state=25) score = cross_val_score(rfc ,data.data ,data.target ,cv=10).mean() score_list.append(score) print('最大精度:',max(score_list)) print('最大精度所对应的n_estimators:',([*range(160,181)][score_list.index(max(score_list))])) plt.figure(figsize=(12,8)) plt.plot(range(160,181),score_list) plt.xlabel('n_estimators') plt.ylabel('RF accuracy') plt.show() """输出""" 最大精度: 0.9631578947368421 最大精度所对应的n_estimators: 163
调整n_estimators的效果显著,模型的准确率立刻上升了0.0035。接下来就进入网格搜索,我们将使用网格搜索对 参数一个个进行调整。为什么我们不同时调整多个参数呢?原因有两个:
- 同时调整多个参数会运行非常缓慢,耗时,这里只做演示,所以没有花太多的时间
- 同时调整多个参数,会让我们无法理解参数的组合是怎么得来的,所以即便 网格搜索调出来的结果不好,我们也不知道从哪里去改。在这里,为了使用复杂度-泛化误差方法(方差-偏差方 法),我们对参数进行一个个地调整。
-
为网格搜索做准备,书写网格搜索的参数
有一些参数是没有参照的,很难说清一个范围,这种情况下我们使用学习曲线,看趋势
从曲线跑出的结果中选取一个更小的区间,再跑曲线param_grid = {'n_estimators':np.arange(0, 200, 10)} param_grid = {'max_depth':np.arange(1, 20, 1)} param_grid = {'max_leaf_nodes':np.arange(25,50,1)}
对于大型数据集,可以尝试从1000来构建,先输入1000,每100个叶子一个区间,再逐渐缩小范围
有一些参数是可以找到一个范围的,或者说我们知道他们的取值和随着他们的取值,模型的整体准确率会如何变化,这样的参数我们就可以直接跑网格搜索
param_grid = {'criterion':['gini', 'entropy']} param_grid = {'min_samples_split':np.arange(2, 2+20, 1)} param_grid = {'min_samples_leaf':np.arange(1, 1+10, 1)} param_grid = {'max_features':np.arange(5,30,1)}
-
开始按照参数对模型整体准确率的影响程度进行调参,首先调整
max_depth
一般根据数据的大小来进行一个试探,乳腺癌数据很小,特征数就30个,所以可以采用1-10,或者1-20这样的试探
其实更应该画出学习曲线,来观察深度对模型的影响
# 调整max_depth param_grid = {'max_depth':np.arange(1,20,1)} rfc = RandomForestClassifier(n_estimators=163 ,random_state=25 ) GS = GridSearchCV(rfc,param_grid ,cv=10 ) GS.fit(data.data,data.target) print(f"max_depth 最佳参数:{GS.best_params_}") print(f"max_depth 最佳精度:{GS.best_score_}") max_depth 最佳参数:{'max_depth': 8} max_depth 最佳精度:0.9631578947368421
在这里,我们注意到,将max_depth设置为有限之后,模型的准确率没有变,或者说是下降了。限制max_depth,是为了让模型变得简 单,把模型向左推,而模型整体的准确率下降了,即整体的泛化误差上升了,这说明模型现在位于图像左边,即泛 化误差最低点的左边(偏差为主导的一边)。通常来说,随机森林应该在泛化误差最低点的右边,树模型应该倾向 于过拟合,而不是拟合不足。这和数据集本身有关,但也有可能是我们调整的n_estimators对于数据集来说太大, 因此将模型拉到泛化误差最低点去了。然而,既然我们追求最低泛化误差,那我们就保留这个n_estimators,除非 有其他的因素,可以帮助我们达到更高的准确率。当模型位于图像左边时,我们需要的是增加模型复杂度(增加方差,减少偏差)的选项,因此max_depth应该尽量 大,min_samples_leaf和min_samples_split都应该尽量小。这几乎是在说明,除了max_features,我们没有任何 参数可以调整了,因为max_depth,min_samples_leaf和min_samples_split是剪枝参数,是减小复杂度的参数。 在这里,我们可以预言,我们已经非常接近模型的上限,模型很可能没有办法再进步了。
那我们这就来调整一下max_features,看看模型如何变化。 -
调整
max_features
max_features是唯一一个即能够将模型往左(低方差高偏差)推,也能够将模型往右(高方差低偏差)推的参数。我 们需要根据调参前,模型所在的位置(在泛化误差最低点的左边还是右边)来决定我们要将max_features往哪边调。现在模型位于图像左侧,我们需要的是更高的复杂度,因此我们应该把max_features往更大的方向调整,可用的特征 越多,模型才会越复杂。max_features的默认最小值是sqrt(n_features),因此我们使用这个值作为调参范围的最小值。
param_grid = {'max_features':np.arange(5,30,1)} rfc = RandomForestClassifier(n_estimators=163 ,random_state=25) GS = GridSearchCV(rfc ,param_grid ,cv=10 ) GS.fit(data.data,data.target) print(f"max_depth 最佳参数:{GS.best_params_}") print(f"max_depth 最佳精度:{GS.best_score_}") """输出""" max_depth 最佳参数:{'max_features': 5} max_depth 最佳精度:0.9631578947368421
网格搜索返回了max_features的最小值,可见max_features升高之后,模型的准确率降低了。这说明,我们把模 型往右推,模型的泛化误差增加了。前面用max_depth往左推,现在用max_features往右推,泛化误差都增加, 这说明模型本身已经处于泛化误差最低点,已经达到了模型的预测上限,没有参数可以左右的部分了。剩下的那些 误差,是噪声决定的,已经没有方差和偏差的舞台了。如果是现实案例,我们到这一步其实就可以停下了,因为复杂度和泛化误差的关系已经告诉我们,模型不能再进步了。
调参和训练模型都需要很长的时间,明知道模型不能进步了还继续调整,不是一个有效率的做法。
如果我们希望模型更进一步,我们会选择更换算法,或者更换做数据预处理的方式。
但是在课上,出于练习和探索的目的,我们继续调整我们的参数,让大家观察一下模型的变化,看看我们预测得是否正确。依然按照参数对模型整体准确率的影响程度进行调参。 -
调整
min_samples_leaf
#调整min_samples_leaf param_grid={'min_samples_leaf':np.arange(1, 1+10, 1)} #对于min_samples_split和min_samples_leaf,一般是从他们的最小值开始向上增加10或20 #面对高维度高样本量数据,如果不放心,也可以直接+50,对于大型数据,可能需要200~300的范围 #如果调整的时候发现准确率无论如何都上不来,那可以放心大胆调一个很大的数据,大力限制模型的复杂度 rfc = RandomForestClassifier(n_estimators=163 ,random_state=25 ) GS = GridSearchCV(rfc,param_grid,cv=10) GS.fit(data.data,data.target) print(f"min_samples_leaf 最佳参数:{GS.best_params_}") print(f"min_samples_leaf 最佳精度:{GS.best_score_}") """输出""" min_samples_leaf 最佳参数:{'min_samples_leaf': 1} min_samples_leaf 最佳精度:0.9631578947368421
可以看见,网格搜索返回了min_samples_leaf的最小值,并且模型整体的准确率还不变,这和max_depth的情 况一致,参数把模型向左推,但是模型的泛化误差上升了。在这种情况下,我们显然是不要把这个参数设置起来 的,就让它默认就好了。
-
继续尝试
min_samples_split
param_grid = {'min_samples_split':np.arange(2,2+20,1)} rfc = RandomForestClassifier(n_estimators=163 ,random_state=25 ) GS = GridSearchCV(rfc,param_grid,cv=10) GS.fit(data.data,data.target) GS.best_params_ GS.best_score_ print(f"min_samples_split 最佳参数:{GS.best_params_}") print(f"min_samples_split 最佳精度:{GS.best_score_}") """输出""" min_samples_split 最佳参数:{'min_samples_split': 2} min_samples_split 最佳精度:0.9631578947368421
和
min_samples_leaf
一样的结果,返回最小值并且模型整体的准确率未变,所以这个参数最好也默认就行了 -
最后尝试一下
criterion
#调整Criterion param_grid = {'criterion':['gini', 'entropy']} rfc = RandomForestClassifier(n_estimators=163 ,random_state=25 ) GS = GridSearchCV(rfc,param_grid,cv=10) GS.fit(data.data,data.target) print(f"criterion 最佳参数:{GS.best_params_}") print(f"criterion 最佳精度:{GS.best_score_}") """输出""" criterion 最佳参数:{'criterion': 'entropy'} criterion 最佳精度:0.9666353383458647
随机森林默认的是用‘gini’系数,这里entropy使模型结果上升了,有时候就是这样,有时候调整criterion一点用都没有,有时候如神来之笔
-
调整完毕,总结出模型的最佳参数
rfc = RandomForestClassifier(criterion='entropy' ,n_estimators=163 ,random_state=25) score = cross_val_score(rfc,data.data,data.target,cv=10).mean() print('调参后最佳精度:',score) print('提升',score - score_pre) """输出""" 调参后最佳精度: 0.9666353383458647 提升 0.006986215538847262
5.2.6 总结
- 在整个调参过程之中,我们首先调整了
n_estimators
(无论如何都请先走这一步),然后调整max_depth
,通过max_depth
产生的结果,来判断模型位于复杂度-泛化误差图像的哪一边,从而选择我们应该调整的参数和调参的 方向。如果感到困惑,也可以画很多学习曲线来观察参数会如何影响我们的准确率,选取学习曲线中单调的部分来 放大研究(如同我们对n_estimators做的)。学习曲线的拐点也许就是我们一直在追求的,最佳复杂度对应的泛化 误差最低点,(也就是方差和偏差的平衡点) 网格搜索也可以一起调整多个参数,但是需要大量的时间,有时 候,它的结果比我们的好,有时候,我们手动调整的结果会比较好。当然了,乳腺癌数据集非常完美,所以 只需要调n_estimators
和criterion
这两个参数(仅仅在random_state=25的情况下)就达到了随机森林在这个数据集上表现得极限。