菜菜的sklearn课堂——随机森林

1.概述

1.1 集成算法概述
集成算法会考虑多个评估器的建模结果,汇总之后得到一个综合的结果,以此来获取比单个模型更好的回归或分类表现。
1.2 sklearn中的集成算法
sklearn中的集成算法模块ensemble

类的功能
ensemble.AdaBoostClassifierAdaBoost分类
ensemble.AdaBoostRegressorAdaboost回归
ensemble.BaggingClassifier装袋分类器
ensemble.BaggingRegressor装袋回归器
ensemble.ExtraTreesClassifierExtra-trees分类(超树,极端随机树)
ensemble.ExtraTreesRegressorExtra-trees回归
ensemble.GradientBoostingClassifier梯度提升分类
ensemble.GradientBoostingRegressor梯度提升回归
ensemble.IsolationForest隔离森林
ensemble.RandomForestClassifier随机森林分类
ensemble.RandomForestRegressor随机森林回归
ensemble.RandomTreesEmbedding完全随机树的集成
ensemble.VotingClassifier用于不合适估算器的软投票/多数规则分类器

2.RandomForestClassifier

class 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)
随机森林是非常具有代表性的Bagging集成算法,它的所有基评估器都是决策树,分类树组成的森林就叫做随机森林分类器,回归树所集成的森林就叫做随机森林回归器。这一节主要讲解RandomForestClassifier,随机森林分类器。
2.1 重要参数
2.1.1 控制基评估器的参数

参数含义
criterion不纯度的衡量指标,有基尼系数和信息熵两种选择
max_depth树的最大深度,超过最大深度的树枝都会被剪掉
min_samples_leaf一个节点在分枝后的每个子节点都必须包含至少min_samples_leaf个训练样本,否则分枝就不会发生
min_samples_split一个节点必须要包含至少min_samples_split个训练样本,这个节点才允许被分枝,否则分枝就不会发生
max_featuresmax_features限制分枝时考虑的特征个数,超过限制个数的特征都会被舍弃,默认值为总特征个数开平方取整
min_impurity_decrease限制信息增益的大小,信息增益小于设定数值的分枝不会发生

2.1.2 n_estimators
这个参数是指随机森林中树的个数,对随机森林模型的精确性影响是单调的,n_estimators越大,模型的效果往往越好。但是相应的,任何模型都有决策边界,n_estimators达到一定的程度之后,随机森林的精确性往往不再上升或开始波动,并且,n_estimators越大,需要的计算量和内存也越大,训练的时间也会越来越长。所以这个参数要在训练难度和模型效果之间取得平衡。
n_estimators的默认值在现有版本的sklearn中是10,但是在即将更新的0.22版本中,这个默认值会被修正为100。这个修正显示出了使用者的调参倾向:要更大的n_estimators。一般来说,[0,200]间取值比较好。

# 针对红酒数据集,一个随机森林和单个决策树的效益对比

# 1.导入需要的包
# %matplotlib inline # 告诉python画图需要这个环境,
# 是IPython的魔法函数,可以在IPython编译器里直接使用,作用是内嵌画图,省略掉plt.show()这一步,直接显示图像。
# 如果不加这一句的话,我们在画图结束之后需要加上plt.show()才可以显示图像
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# 交叉验证的包
import matplotlib.pyplot as plt# 画图用的
# 2.导入需要的数据集
wine = load_wine()
print(wine.data) # wine是一个字典 (178,13)
print(wine.target)

# 3.sklearn建模的基本流程
# 实例化
# 训练集代入实例化后的模型进行训练,使用的接口是fit
# 使用其它接口将测试集导入训练好的模型,获得结果(score,y_test)

Xtrain, Xtest, Ytrain, Ytest = train_test_split(wine.data, wine.target, test_size=0.3)
clf = DecisionTreeClassifier(random_state=0)# random_state控制树的生成模式,random_state=0让它只能生成一棵树
rfc = RandomForestClassifier(random_state=0)# random_state也是控制随机性的,但和决策树的有些许不同
clf = clf.fit(Xtrain, Ytrain)
rfc = rfc.fit(Xtrain, Ytrain)
score_c = clf.score(Xtest, Ytest)
score_r = rfc.score(Xtest, Ytest)
print('Single Tree:{}'.format(score_c)
      , 'Random Forest:{}'.format(score_r)# format后面()里的内容是放到前面{}里的
      )

# 4.画出随机森林和决策树在一组交叉验证下的效果对比
# 复习交叉验证:将数据集划分为n份,依次取每一份做测试集,其余n-1份做训练集,多次训练模型以观测模型稳定性的方法
rfc = RandomForestClassifier(n_estimators=25)
rfc_s = cross_val_score(rfc, wine.data, wine.target, cv=10)
# 四个参数分别是:实例化好的模型,完整的特征矩阵,完整的标签,交叉验证的次数
clf = DecisionTreeClassifier()
clf_s = cross_val_score(clf, wine.data, wine.target, cv=10)
plt.plot(range(1, 11), rfc_s, label="RandomForest")
# 三个参数分别是:x的取值[1,11),y的取值,折线的标签
plt.plot(range(1, 11), clf_s, label="Decision Tree")
# 一个plot里面只能有一个标签,所以想显示折线的标签就只能写两行
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"
"""

# 5.画出随即森林和决策树在十组交叉验证下的效果对比(一般这部分不用做)
rfc_l = []
clf_l = []
for i in range(10):
    rfc = RandomForestClassifier(n_estimators=25)
    rfc_s = cross_val_score(rfc, wine.data, wine.target, cv=10).mean()
    rfc_l.append(rfc_s)
    clf = DecisionTreeClassifier()
    clf_s = cross_val_score(clf, wine.data, wine.target, cv=10).mean()
    clf_l.append(clf_s)

plt.plot(range(1, 11), rfc_l, label="Random Forest")
plt.plot(range(1, 11), clf_l, label="Decision Tree")
plt.legend()
plt.show()
# 是否有注意到,单个决策树的波动轨迹和随机森林一致?
# 再次验证了我们之前提到的,单个决策树的准确率越高,随机森林的准确率也会越高

# 6.n_estimators的学习曲线
superpa = []
for i in range(200):
    rfc = RandomForestClassifier(n_estimators=i+1, n_jobs=-1) # 实例化
    rfc_s = cross_val_score(rfc, wine.data, wine.target, cv=10).mean() # 交叉验证
    superpa.append(rfc_s) # 将交叉验证的结果放到列表里
print(max(superpa), superpa.index(max(superpa))) # list.index(object)会返回对象object在列表list当中的索引
plt.figure(figsize=[20, 5])
plt.plot(range(1, 201), superpa)
plt.show()

2.1.3 random_state
为什么随机森林的集成效果一定好于单个分类器?

在刚才的红酒例子中,我们建立了25棵树,对任何一个样本而言,平均或多数表决原则下,当且仅当有13棵以上的树判断错误的时候,随机森林才会判断错误。单独一棵决策树对红酒数据集的分类准确率在0.85上下浮动,假设一棵树判断错误的可能性为ε=0.2,那25棵树中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()

可见,判断错误的几率非常小,这让随机森林在红酒数据集上的表现远远好于单棵决策树。
袋装法是服从多数表决原则或对基分类器结果求平均,这即是说,默认森林中的每棵树应该是不同的,并且会返回不同的结果。如果随机森林里所有的树的判断结果都一致(全判断对或全判断错),那随机森林无论应用何种集成原则来求结果,都应该无法比单棵决策树取得更好的效果才对。但上面使用了一样的类DecisionTreeClassifier,一样的参数,一样的训练集和测试集,为什么随机森林里的众多树会有不同的判断结果?
因为sklearn中的分类树DecisionTreeClassifier自带随机性,所以随机森
林中的树天生就都是不一样的。在分类时,决策树从最重要的特征中随机选择出一个特征来进行分枝,因此每次生成的决策树都不一样,这个功能由参数random_state控制。随机森林中也有random_state,用法和分类树中相似,不过在分类树中,一个random_state只控制生成一棵树,而随机森林中的random_state控制的是生成森林的模式,生成的是一片固定的森林,而非让一个森林中只有一棵树。

# 7.讨论参数random_state
rfc = RandomForestClassifier(n_estimators=20,random_state=2)
rfc = rfc.fit(Xtrain, Ytrain)
#随机森林的重要属性之一:estimators_,查看森林中树的状况
#rfc.estimators_     # 模型.estimators_  查看森林中所有树的状况
rfc.estimators_[0].random_state # 查看森林里一棵树的random_state参数
for i in range(len(rfc.estimators_)):# 查看森林里所有树的random_state参数
    print(rfc.estimators_[i].random_state)

当random_state固定时,随机森林中生成是一组固定的树,但每棵树依然是不一致的,这是用”随机挑选特征进行分枝“的方法得到的随机性。并且可以证明,当这种随机性越大的时候,袋装法的效果一般会越来越好。用袋装法集成时,基分类器应当是相互独立的,是不相同的。
但这种做法的局限性是很强的,当我们需要成千上万棵树的时候,数据不一定能够提供成千上万的特征来让我们构筑尽量多尽量不同的树。因此,除了random_state, 还需要其他的随机性。
2.1.4 bootstrap & oob_score
要让基分类器尽量都不一样,一种很容易理解的方法是使用不同的训练集来进行训练,而袋装法正是通过有放回的随机抽样技术来形成不同的训练数据,bootstrap就是用来控制抽样技术的参数。

在一个含有n个样本的原始训练集中,我们进行随机采样,每次采样一个样本,并在抽取下一个样本之前将该样本放回原始训练集,也就是说下次采样时这个样本依然可能被采集到,这样采集n次,最终得到一个和原始训练集一样大的,n个样本组成的自助集。由于是随机采样,这样每次的自助集和原始数据集不同,和其他的采样集也是不同的。这样我们就可以自由创造取之不尽用之不竭,并且互不相同的自助集,用这些自助集来训练我们的基分类器,我们的基分类器自然也就各不相同了(要建立多少棵树,就抽样多少个自助集)。

bootstrap参数默认True,代表采用这种有放回的随机抽样技术。通常,这个参数不会被我们设置为False。
在这里插入图片描述
有放回抽样也会有自己的问题。一些样本可能在同一个自助集中出现多次,而其他一些却可能被忽略,一般来说,自助集大约平均会包含63%的原始数据。因为每一个样本被抽到某个自助集中的概率为:
在这里插入图片描述
因为在一个自助集里,只要n次中有一次抽到这个样本,这个样本就算是被抽到
而(1/n)^n则表示在一个自助集中,n次抽样时都抽到这个样本的概率
(1-1/n)^n表示在一个自助集中,某一个样本A永远不会被抽到的概率

当n足够大时,这个概率收敛于1-(1/e),约等于0.632。因此,会有约37%的训练数据被浪费掉,没有参与建模,这些数据被称为袋外数据(out of bag data,简写为oob)。除了我们最开始就划分好的测试集之外,这些数据也可以被用来作为集成算法的测试集。也就是说,在使用随机森林时,我们可以不划分测试集和训练集,只需要用袋外
数据来测试我们的模型即可。当然,这也不是绝对的,当n和n_estimators都不够大的时候,很可能就没有数据掉落在袋外,自然也就无法使用oob数据来测试模型了。如果希望用袋外数据来测试,则需要在实例化时就将oob_score这个参数调整为True,训练完毕之后,我们可以用随机森林的另一个重要属性:oob_score_来查看我们的在袋外数据上测试的结果:

# 无需划分训练集和测试集
rfc = RandomForestClassifier(n_estimators=25, oob_score=True)
rfc = rfc.fit(wine.data, wine.target)
# 重要属性oob_score_
print(rfc.oob_score_)

2.2 重要属性和接口
随机森林的接口与决策树完全一致,因此依然有四个常用接口:apply, fit, predict和score。除此之外,还需要注意随机森林的predict_proba接口,这个接口返回每个测试样本对应的被分到每一类标签的概率,标签有几个分类就返回几个概率。如果是二分类问题,则predict_proba返回的数值大于0.5的,被分为1,小于0.5的,被分为0。
传统的随机森林是利用袋装法中的规则,平均或少数服从多数来决定集成的结果,而sklearn中的随机森林是平均每个样本对应的predict_proba返回的概率,得到一个平均概率,从而决定测试样本的分类。

#大家可以分别取尝试一下这些属性和接口
rfc = RandomForestClassifier(n_estimators=25)
rfc = rfc.fit(Xtrain, Ytrain)
rfc.score(Xtest,Ytest)# 模型预测值和标签相比的准确率

rfc.feature_importances_# 所有特征的重要性
rfc.apply(Xtest)# 返回测试集中每个样本在每棵树中的叶子节点的索引
rfc.predict(Xtest)# 返回对测试集的预测的标签
rfc.predict_proba(Xtest)# 每一个样本分到每一类标签的概率

Bonus:Bagging的另一个必要条件
上面说过,袋装法使用时要求基评估器要尽量独立。袋装法还有另一个必要条件:基分类器的判断准确率至少要超过随机分类器,即时说,基分类器的判断准确率至少要超过50%。之前我们已经展示过随机森林的准确率公式,基于这个公式,我们画出了基分类器的误差率ε和随机森林的误差率之间的图像。

import numpy as np
from scipy.special import comb
import matplotlib.pyplot as plt
x = np.linspace(0,1,20)
y = []
# epsilon是基分类器的误差率,使其在(0,1)之间变动
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()# 25棵树中13棵树都判断错误的可能性
    y.append(E)
#绘制图形  
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,即准确率大于0.5时,集成的效果是比基分类器要好的。相反,当基分类器的误差率大于0.5,袋装的集成算法就失效了。所以在使用随机森林之前,一定要检查,用来组成随机森林的分类树们是否都有至少50%的预测正确率。

3.RandomForestRegressor

class 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)
所有的参数,属性与接口,全部和随机森林分类器一致。仅有的不同就是回归树与分类树的不同,不纯度的指标——参数Criterion不一致。
随机森林分类和随机森林回归的不同点(其实就是分类树和回归树的不同点):
1.不纯度的指标criterion的取值不同。(分类树采用基尼系数和信息熵,回归树采用均方误差MSE)
2.模型的衡量指标不同。(分类树采用准确率accuracy,回归树采用R方或者均方误差MSE,通常使用MSE)

3.1 重要参数,属性与接口
criterion
回归树衡量分枝质量的指标,支持的标准有三种:
1)输入"mse"使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失;
2)输入“friedman_mse”使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差
3)输入"mae"使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值来最小化L1损失
在这里插入图片描述
其中N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi是样本点i实际的数值标签。所以MSE的本质,其实是样本真实数据与回归结果的差异。在回归树中,MSE不只是我们的分枝质量衡量指标,也是我们最常用的衡量回归树回归质量的指标,当我们在使用交叉验证,或者其他方式获取回归树的结果时,我们往往选择均方误差作为我们的评估(在分类树中这个指标是score代表的预测准确率)。在回归中,我们追求的是,MSE越小越好。
然而,回归树的接口score返回的是R平方,并不是MSE。R平方被定义如下:
在这里插入图片描述
其中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去掉负号的数字。

重要属性和接口
随机森林回归并没有predict_proba这个接口,因为对于回归来说,并不存在一个样本要被分到某个类别的概率问题,因此没有predict_proba这个接口。

from sklearn.datasets import load_boston
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
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")# 不填写scoring = "neg_mean_squared_error"时,交叉验证默认的模型衡量指标是R平方,因此结果可正可负,加上这行,衡量标准是负MSE,结果为负。
sorted(sklearn.metrics.SCORERS.keys())# 返回10次交叉验证的结果
# sklearn当中的模型评估指标(尤其是打分性质的)的列表

3.2 实例:用随机森林回归填补缺失值
对于缺失值,一种方法是直接将含有缺失值的样本删除,另一种是填补缺失值。在sklearn中,可以用sklearn.impute.SimpleImputer来轻松地将均值,中值,或者其他最常用的数值填补到数据中。
在这个案例中,我们将使用均值,0,和随机森林回归来填补缺失值,并验证四种状况下的拟合状况,找出对使用的数据集来说最佳的缺失值填补方法。

# 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

# 2.以波士顿数据集为例,导入完整的数据集并探索
dataset = load_boston()
print(dataset.data.shape)
# 总共506*13=6578个数据
X_full, y_full = dataset.data, dataset.target
n_samples = X_full.shape[0] # 样本的数量
n_features = X_full.shape[1] # 标签的数量

# 3.为完整数据集放入缺失值
# 首先确定希望放入的缺失数据的比例,在这里我们假设是50%,那总共就要有3289个数据缺失
rng = np.random.RandomState(0)
missing_rate = 0.5
n_missing_samples = int(np.floor(n_samples * n_features * missing_rate))
# n_samples * n_features * missing_rate=506*13*0.5
# np.floor向下取整,返回.0格式的浮点数   int用于取整

# 所有数据要随机遍布在数据集的各行各列当中,而一个缺失的数据会需要一个行索引和一个列索引
# 创造一个数组,包含3289个分布在0~506中间的行索引,和3289个分布在0~13之间的列索引,
# 就可以利用索引来为数据中的任意3289个位置赋空值。
# 然后用0,均值和随机森林来填补缺失值,查看回归的结果如何
missing_features = rng.randint(0,n_features,n_missing_samples) # 列索引
# randint(下限,上限,n),找随机数的函数,在下限和上限之间取出n个整数
missing_samples = rng.randint(0,n_samples,n_missing_samples) # 行索引
# 现在采样了3289个数据,远远超过样本量506,所以使用随机抽取的函数randint。

# missing_samples = rng.choice(n_samples,n_missing_samples,replace=False)
# 但如果需要的数据量小于样本量506,可以采用np.random.choice来抽样,
# choice会随机抽取不重复的随机数,可以帮助让数据更加分散,确保数据不会集中在一些行中
X_missing = X_full.copy()
print(X_missing)
y_missing = y_full.copy()#
X_missing[missing_samples, missing_features] = np.nan # np.nan是空值
print(X_missing)
# 特征可以为空,但是标签不能为空,标签为空就是无监督学习了
X_missing = pd.DataFrame(X_missing) # 换成dataframe格式
# 转换成DataFrame是为了后续方便各种操作,
# numpy对矩阵的运算速度很快,但是在索引等功能上却不如pandas来得好用

# 4.使用0和均值来填补缺失值
# 使用均值进行填补
from sklearn.impute import SimpleImputer
imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean') # 实例化
# missing_values=np.nan告诉缺失值长什么样子
X_missing_mean = imp_mean.fit_transform(X_missing)
# 训练fit+转出transform
print(pd.DataFrame(X_missing_mean).isnull().sum())
# 先转成dataframe的形式,再用isnull()查看是否有nan值,sum()输出布尔值False=0,True=1,统计每列的nan值个数
# 使用0进行填补
imp_0 = SimpleImputer(missing_values=np.nan, strategy="constant",fill_value=0)
# strategy="constant"表示填入一个常数, fill_value=0表示填入的常数为0
X_missing_0 = imp_0.fit_transform(X_missing)

使用随机森林回归填补缺失值
任何回归都是从特征矩阵中学习,然后求解连续型标签y的过程,之所以能够实现这个过程,是因为回归算法认为,特征矩阵和标签之前存在着某种联系,而且这种联系是可以被数学原理、算法等方法捕捉出来的。实际上,标签和特征是可以相互转换的,比如说,在一个“用地区,环境,附近学校数量”预测“房价”的问题中,我们既可以用“地区”,“环境”,“附近学校数量”的数据来预测“房价”,也可以反过来,用“环境”,“附近学校数量”和“房价”来预测“地区”。而回归填补缺失值,正是利用了这种思想。

对于一个有n个特征的数据来说,其中特征T有缺失值,可以把特征T当作标签,其他的n-1个特征和原本的标签组成新的特征矩阵。对于T来说,它没有缺失的那一部分就是Y_train,既有标签也有特征,而T缺失的部分,只有特征没有标签,就是我们需要预测的部分。

特征T不缺失的值对应的其他n-1个特征 + 本来的标签:X_train
特征T不缺失的值:Y_train
特征T缺失的值对应的其他n-1个特征 + 本来的标签:X_test
特征T缺失的值:未知,需要预测的Y_test

这种做法,对于某一个特征大量缺失,其他特征却很完整的情况,非常适用.

那如果数据中除了特征T之外,其他特征也有缺失值怎么办?

答案是遍历所有的特征,从缺失最少的开始进行填补(因为填补缺失最少的特征所需要的准确信息最少)。(在特征有缺失值的情况下,训练数据越多,模型鲁棒性越强,回归预测越准。)

填补一个特征时,先将其他特征的缺失值用0代替,每完成一次回归预测,就将预测值放到原本的特征矩阵中,再继续填补下一个特征。每一次填补完毕,有缺失值的特征会减少一个,所以每次循环后,需要用0来填补的特征就越来越少。当进行到最后一个特征时(这个特征应该是所有特征中缺失值最多的),已经没有任何的其他特征需要用0来进行填补了,而我们已经使用回归为其他特征填补了大量有效信息,可以用来填补缺失最多的特征。

遍历所有的特征后,数据就完整,不再有缺失值了。

# 5.用随机森林回归填补缺失值
X_missing_reg = X_missing.copy()
sortindex = np.argsort(X_missing_reg.isnull().sum(axis=0)).values # axis=0就是sum的默认值,表示按列加和
# 找出数据集中,缺失值从小到大排列的特征们的顺序(本质就是找索引)
# np.sort也能使缺失值从小到大排序,但不输出索引
# np.argsort返回从小到大排序的顺序所对应的索引
print(sortindex)
for i in sortindex:
    # 构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和新标签(被选中去填充的特征)
    df = X_missing_reg # 避免X_missing_reg的没有被选中去填充的特征被填为0
    fillc = df.iloc[:,i] # 缺失值最小的那一列取出来
    df = pd.concat([df.iloc[:,df.columns != i],pd.DataFrame(y_full)],axis=1)
    # df.iloc[:,df.columns != i]除了取出来的那一列的其它列取出来
    # pd.concat把两个dataframe连起来,axis=1表示按行匹配,第二个会放到新的一列,而不是第一个dataframe的下面

    # 在新特征矩阵中,对含有缺失值的列,进行0的填补
    df_0 =SimpleImputer(missing_values=np.nan,
                        strategy='constant',fill_value=0).fit_transform(df) # 实例化和接口同时进行

    # 找出我们的训练集和测试集
    Ytrain = fillc[fillc.notnull()]# 被选中要填充的特征(现在是标签)里面那些不缺失的值
    Ytest = fillc[fillc.isnull()]# 被选中要填充的特征(现在是标签)里面那些缺失的值
    Xtrain = df_0[Ytrain.index, :]# 用索引找出标签所对应的特征
    Xtest = df_0[Ytest.index, :]

    # 用随机森林回归来填补缺失值
    rfc = RandomForestRegressor(n_estimators=100)# 实例化
    rfc = rfc.fit(Xtrain, Ytrain)
    Ypredict = rfc.predict(Xtest)

    # 将填补好的特征返回到我们的原始的特征矩阵中
    X_missing_reg.loc[X_missing_reg.iloc[:, i].isnull(), i] = Ypredict
print(X_missing_reg)
# 6.对填补好的数据进行建模,取出MSE结果
X = [X_full,X_missing_mean,X_missing_0,X_missing_reg]
mse = []
std = []
for x in X:
    estimator = RandomForestRegressor(random_state=0, n_estimators=100)
    scores = cross_val_score(estimator,x,y_full,scoring='neg_mean_squared_error',
                             cv=5).mean()
    mse.append(scores * -1)

# 7.用所得结果画出条形图
x_labels = ['Full data',
            'Zero Imputation',
            'Mean Imputation',
            'Regressor Imputation']
colors = ['r', 'g', 'b', 'orange']
plt.figure(figsize=(12, 6)) # 画出画布
ax = plt.subplot(111) # 添加子图
for i in np.arange(len(mse)):
    ax.barh(i, mse[i], color=colors[i], alpha=0.6, align='center')# bar是画条形图,后面加h表示横过来

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()

4.机器学习中调参的基本思想

大多数的机器学习相关的书对调参探究甚少。这中间有许多原因,其一是因为,调参的方式总是根据数据的状况而定,所以没有办法一概而论;其二是因为,其实大家也都没有特别好的办法。

通过画学习曲线,或者网格搜索,能够探索到调参边缘(代价可能是训练一次模型要跑三天三夜),但是在现实中,高手调参恐怕还是多依赖于经验,而这些经验,来源于:1)非常正确的调参思路和方法,2)对模型评估指标的理解,3)对数据的感觉和经验,4)用洪荒之力去不断地尝试。

可以学习他们对模型评估指标的理解和调参的思路。

模型调参,第一步是要找准目标:一般来说,这个目标是提升某个模型评估指标,比如对于随机森林来说,想要提升的是模型在未知数据上的准确率(由score或oob_score_来衡量)。找准了这个目标,就需要思考:模型在未知数据上的准确率受什么因素影响?在机器学习中,用来衡量模型在未知数据上的准确率的指标,叫做泛化误差(Genelization error)

泛化误差
当模型在未知数据(测试集或者袋外数据)上表现糟糕时,表示模型的泛化程度不够,泛化误差大,模型的效果不好。泛化误差受到模型的结构(复杂度)影响。看下面这张图,它准确地描绘了泛化误差与模型复杂度的关系,当模型太复杂,模型就会过拟合,泛化能力就不够,所以泛化误差大。当模型太简单,模型就会欠拟合,拟合能力就不够,所以误差也会大。只有当模型的复杂度刚刚好时才能达到泛化误差最小的目标。
在这里插入图片描述
模型的复杂度和参数有什么关系?对于树模型来说,树越茂盛,深度越深,枝叶越多,模型就越复杂。所以树模型是天生位于图的右上角的模型,随机森林是以树模型为基础,所以随机森林也是天生复杂度高的模型。随机森林的参数,都是向着一个目标去:减少模型的复杂度,把模型往图像的左边移动,防止过拟合。当然了,调参没有绝对,也有天生处于图像左边的随机森林,所以调参之前要先判断,模型现在究竟处于图像的哪一边。

泛化误差的背后其实是“偏差-方差困境”,原理十分复杂,只需要记住这四点:
1)模型太复杂或者太简单,都会让泛化误差高,我们追求的是位于中间的平衡点
2)模型太复杂就会过拟合,模型太简单就会欠拟合
3)对树模型和树的集成模型来说,树的深度越深,枝叶越多,模型越复杂
4)树模型和树的集成模型的目标,都是减少模型复杂度,把模型往图像的左边移动

那具体每个参数,都如何影响复杂度和模型呢?一直以来,调参都是在学习曲线上轮流找最优值,盼望能够将准确率修正到一个比较高的水平。然而现在了解了随机森林的调参方向:降低复杂度。就可以将那些对复杂度影响巨大的参数挑选出来,研究他们的单调性,然后专注调整那些能最大限度让复杂度降低的参数。对于那些不单调的参数,或者反而会让复杂度升高的参数,就视情况使用,大多时候甚至可以退避。基于经验,对各个参数对模型的影响程度做了一个排序。

参数对模型在未知数据上的评估性能的影响影响程度
n_estimators提升至平稳,n_estimators↑,不影响单个模型的复杂度⭐⭐⭐⭐
max_depth有增有减,默认最大深度,即最高复杂度,向复杂度降低的方向调参。 max_depth↓,模型更简单,且向图像的左边移动⭐⭐⭐
min_samples_leaf有增有减,默认最小限制1,即最高复杂度,向复杂度降低的方向调参。min_samples_leaf↑,模型更简单,且向图像的左边移动⭐⭐
min_samples_split有增有减,默认最小限制2,即最高复杂度,向复杂度降低的方向调参。min_samples_split↑,模型更简单,且向图像的左边移动⭐⭐
max_features有增有减,默认auto,是特征总数的开平方,位于中间复杂度,既可以向复杂度升高的方向,也可以向复杂度降低的方向调参。max_features↓,模型更简单,图像左移;max_features↑,模型更复杂,图像右移。max_features是唯一的,既能够让模型更简单,也能够让模型更复杂的参数,所以在调整这个参数的时候,需要考虑调参的方向
criterion有增有减,一般使用gini看具体情况

偏差VS方差
一个集成模型(f)在未知数据集(D)上的泛化误差E(f;D),由方差(var),偏差(bais)和噪声(ε)共同决定。
在这里插入图片描述
在下面的图像中,每个点就是集成算法中的一个基评估器产生的预测值。红色虚线代表着这些预测值的均值,而蓝色的线代表着数据本来的面貌。

偏差:模型的预测值与真实值之间的差异,即每一个红点到蓝线的距离。在集成算法中,每个基评估器都会有自己的偏差,集成评估器的偏差是所有基评估器偏差的均值。模型越精确,偏差越低
方差:反映的是模型每一次输出结果与模型预测值的平均水平之间的误差,即每一个红点到红色虚线的距离,衡量模型的稳定性。模型越稳定,方差越低

一个好的模型要对大多数未知数据都预测的又“准”又“稳”,也就是说,当偏差和方差都很低的时候,模型的泛化误差就小,在未知数据上的准确率就高。
在这里插入图片描述

偏差大偏差小
方差大模型不适合这个数据,换模型过拟合,模型很复杂,对某些数据集预测很准确,对某些数据集预测很糟糕
方差小欠拟合,模型相对简单,预测很稳定,但对所有的数据预测都不太准确泛化误差小,我们的目标

通常来说,方差和偏差有一个很大,泛化误差都会很大。然而,方差和偏差是此消彼长的,不可能同时达到最小值。
在这里插入图片描述

如上图所示,模型复杂度大的时候,方差高,偏差低。偏差低,就是要求模型要预测得“准”。模型就会更努力去学习更多信息,会具体于训练数据,这会导致,模型在一部分数据上表现很好,在另一部分数据上表现却很糟糕。模型泛化性差,在不同数据上表现不稳定,所以方差就大。而要尽量学习训练集,模型的建立必然更多细节,复杂程度必然上升。所以,复杂度高,方差高,总泛化误差高

复杂度低的时候,方差低,偏差高。方差低,要求模型预测得“稳”,泛化性更强,那对于模型来说,它就不需要对数据进行一个太深的学习,只需要建立一个比较简单,判定比较宽泛的模型就可以了。结果就是,模型无法在某一类或者某一组数据上达成很高的准确度,所以偏差就会大。所以,复杂度低,偏差高,总泛化误差高

调参的目标是达到方差和偏差的完美平衡。虽然方差和偏差不能同时达到最小值,但他们组成的泛化误差却可以有一个最低点,调参就是要寻找这个最低点。对复杂度大的模型,要降低方差,对相对简单的模型,要降低偏差。随机森林的基评估器都拥有较低的偏差和较高的方差,因为决策树本身是预测比较”准“,比较容易过拟合的模型,装袋法本身也要求基分类器的准确率必须要有50%以上。所以以随机森林为代表的装袋法的训练过程旨在降低方差,即降低模型复杂度,所以随机森林参数的默认设定都是假设模型本身在泛化误差最低点的右边。

5.实例:随机森林在乳腺癌数据上的调参

这一节,就基于方差和偏差的调参方法,在乳腺癌数据上进行一次随机森林的调参。乳腺癌数据是sklearn自带的分类数据之一。数据集名称是Digit Recognizer(https://www.kaggle.com/c/digit-recognizer)。

# 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
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

# 2.导入数据集,探索数据
data = load_breast_cancer()
print(data)
print(data.data.shape)
print(data.target) # 二分类数据
# 乳腺癌数据集有569条记录,30个特征
# 单看维度虽然不算太高,但是样本量非常少。过拟合的情况可能存在

# 3.进行一次简单的建模,看看模型本身在数据集上的效果
rfc = RandomForestClassifier(n_estimators=100, random_state=90)
score_pre = cross_val_score(rfc, data.data, data.target, cv=10).mean()
print(score_pre)
# 可以看到,随机森林在乳腺癌数据上的表现本就还不错,在现实数据集上,基本上不可能什么都不调就看到95%以上的准确率

# 随机森林调参第一步:调n_estimators
# 在这里选择学习曲线,可以看见趋势
# 看见n_estimators在什么取值开始变得平稳,是否一直推动模型整体准确率的上升等信息
# 第一次的学习曲线,可以先用来帮助我们划定范围,我们取每十个数作为一个阶段,来观察n_estimators的变化如何引起模型整体准确率的变化
scorel = []
for i in range(0,200,10):
    rfc = RandomForestClassifier(n_estimators=i+1,# n_estimators不能为0
                                 n_jobs=-1,
                                 random_state=90)
    score = cross_val_score(rfc,data.data,data.target,cv=10).mean()
    scorel.append(score)
print(max(scorel),(scorel.index(max(scorel))*10)+1)
plt.figure(figsize=[20,5])
plt.plot(range(1,201,10),scorel)
plt.show()
# list.index([object])
# 返回这个object在列表list中的索引

# 5.在确定好的范围内,进一步细化学习曲线
scorel = []
for i in range(65,75):# 从上面得到最大值的索引为71,所以范围取65~75
    rfc = RandomForestClassifier(n_estimators=i,
                                 n_jobs=-1,
                                 random_state=90)
    score = cross_val_score(rfc,data.data,data.target,cv=10).mean()
    scorel.append(score)
print(max(scorel),([*range(65,75)][scorel.index(max(scorel))]))
plt.figure(figsize=[20,5])
plt.plot(range(65,75),scorel)
plt.show()

调整n_estimators的效果显著,模型准确率上升了0.005。接下来就进入网格搜索,为什么不同时调整多个参数呢?原因有两个:1)同时调整多个参数会运行非常缓慢,2)同时调整多个参数,会让我们无法理解参数的组合是怎么得来的,所以即便网格搜索调出来的结果不好,我们也不知道从哪里去改。在这里,为了使用复杂度-泛化误差方法(方差-偏差方法),采用对参数进行一个个地调整。

# 6.为网格搜索做准备,书写网格搜索的参数
# 有一些参数是没有参照的,很难说清一个范围,这种情况下使用学习曲线,看趋势,从曲线跑出的结果中选取一个更小的区间,再跑曲线
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)}
# 7.开始按照参数对模型整体准确率的影响程度进行调参,首先调整max_depth
param_grid = {'max_depth':np.arange(1, 20, 1)}
# 一般根据数据的大小来进行一个试探,乳腺癌数据很小,所以可以采用1~10,或者1~20这样的试探
# 但对于像digit recognition那样的大型数据来说,我们应该尝试30~50层深度(或许还不足够
# 更应该画出学习曲线,来观察深度对模型的影响
rfc = RandomForestClassifier(n_estimators=73
                             ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_) # 显示调整出来的最佳参数
print(GS.best_score_) # 返回调整好的最佳参数对应的准确率

在这里将max_depth设置为有限之后,模型的准确率反而下降了(即GS.best_score_小于第五节得到的分数)。这是因为,限制max_depth,是让模型变得简单,把模型向左推,而模型整体的准确率下降了,即整体的泛化误差上升了,这说明模型现在位于图像左边,即泛化误差最低点的左边(偏差为主导的一边)。通常来说,随机森林应该在泛化误差最低点的右边,树模型应该倾向于过拟合,而不是拟合不足。这和数据集本身有关,但也有可能是调整的n_estimators对于数据集来说太大,因此将模型拉到泛化误差最低点去了。然而,既然追求的是最低泛化误差,那就保留这个n_estimators,除非有其他的因素,可以达到更高的准确率。
在这里插入图片描述
当模型位于图像左边时,需要增加模型复杂度(增加方差,减少偏差)的选项,因此max_depth应该尽量大,min_samples_leaf和min_samples_split都应该尽量小。这几乎是在说明,除了max_features,没有任何参数可以调整了,因为max_depth,min_samples_leaf和min_samples_split是剪枝参数,是减小复杂度的参数。在这里,可以说已经非常接近模型的上限,模型很可能没有办法再进步了。

# 8.调整max_features
param_grid = {'max_features':np.arange(5,30,1)}
# max_features是唯一一个既能够将模型往左(低方差高偏差)推,也能够将模型往右(高方差低偏差)推的参数。
# 需要根据调参前,模型所在的位置(在泛化误差最低点的左边还是右边)来决定要将max_features往哪边调。

# 现在模型位于图像左侧,需要的是更高的复杂度,因此应该把max_features往更大的方向调整,可用的特征越多,模型才会越复杂。
# max_features的默认最小值是sqrt(n_features),因此使用这个值作为调参范围的最小值。
rfc = RandomForestClassifier(n_estimators=73
                             ,random_state=90
                             )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_)
print(GS.best_score_)

在这里GS.best_score_仍然小于第五节得到的分数,这说明,把模型往右推,模型的泛化误差增加了。前面用max_depth往左推,现在用max_features往右推,泛化误差都增加,这说明模型本身已经处于泛化误差最低点,已经达到了模型的预测上限,没有参数可以左右的部分了。剩下的那些误差,是噪声决定的。

如果是现实案例,到这一步其实就可以停下了,因为复杂度和泛化误差的关系已经说明,模型不能再进步了。调参和训练模型都需要很长的时间,明知道模型不能进步了还继续调整,不是一个有效率的做法。如果希望模型更进一步,会选择更换算法,或者更换做数据预处理的方式。

后面依然按照参数对模型整体准确率的影响程度进行调参,观察一下模型的变化,看看预测得是否正确。

# 9.调整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=73
                             ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_)
print(GS.best_score_)

可以看出,网格搜索返回了min_samples_leaf的最小值,并且模型整体的准确率还降低了,这和max_depth的情况一致,参数把模型向左推,但是模型的泛化误差上升了。在这种情况下,显然是不要把这个参数设置起来的,就让它默认就好了。

# 10.调整min_samples_split
param_grid={'min_samples_split':np.arange(2, 2+20, 1)}
rfc = RandomForestClassifier(n_estimators=73
                            ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_)
print(GS.best_score_)

和min_samples_leaf一样的结果,返回最小值并且模型整体的准确率降低了。

# 11.调整Criterion
param_grid = {'criterion':['gini', 'entropy']}
rfc = RandomForestClassifier(n_estimators=39
                             ,random_state=90
                             )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_)
print(GS.best_score_)

# 12.调整完毕,总结模型的最佳参数
rfc = RandomForestClassifier(n_estimators=39,random_state=90)
score = cross_val_score(rfc,data.data,data.target,cv=10).mean()
print(score)
print(score - score_pre)

在整个调参过程之中,首先调整了n_estimators(无论如何都请先走这一步),然后调整max_depth,通过max_depth产生的结果,来判断模型位于复杂度-泛化误差图像的哪一边,从而选择应该调整的参数和调参的方向。如果感到困惑,也可以画很多学习曲线来观察参数会如何影响准确率,选取学习曲线中单调的部分来放大研究(如同对n_estimators做的)。学习曲线的拐点也许就是最佳复杂度对应的泛化误差最低点(也是方差和偏差的平衡点)。

网格搜索也可以一起调整多个参数,有时候,它的结果比手动调整好,有时候,手动调整的结果会比较好。由于乳腺癌数据集非常完美,所以只需要调n_estimators一个参数就达到了随机森林在这个数据集上表现的极限。在泰坦尼克号的数据中,采用同样的方法调出的参数组合如下:

frc= RandomForestClassifier(n_estimators=68
                            ,random_state=90
                            ,criterion="gini"
                            ,min_samples_split=8
                            ,min_samples_leaf=1
                            ,max_depth=12
                            ,max_features=2
                            ,max_leaf_nodes=36
                            )

这个组合的准确率达到了83.915%,比单棵决策树提升了大约7%,比调参前的随机森林提升了2.02%,这对于调参来说其实是一个非常巨大的进步。不过,泰坦尼克号数据的运行缓慢,量力量时间而行,可以试试看用复杂度-泛化误差方法(方差-偏差方法)来解读一下这个调参结果和过程。

6.附录

6.1 Bagging vs Boosting

装袋法 Bagging提升法 Boosting
评估器相互独立,同时运行相互关联,按顺序依次构建,后建的模型会在先建模型预测失败的样本上有更多的权重
抽样数集有放回抽样有放回抽样,但会确认数据的权重,每次抽样都会给容易预测失败的样本更多的权重
决定集成的结果平均或少数服从多数原则加权平均,在训练集上表现更好的模型会有更大的权重
目标降低方差,提高模型整体的稳定性降低偏差,提高模型整体的精确度
单个评估器存在过拟合问题的时候能够一定程度上解决过拟合问题可能会加剧过拟合问题
单个评估器的效力比较弱的时候不是非常有帮助很可能会提升模型表现
代表算法随机森林梯度提升树,Adaboost

6.2 RFC的参数列表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
6.3 RFC的属性列表
在这里插入图片描述
6.4 RFC的接口列表
在这里插入图片描述

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值