sklearn(3)—— 随机森林

1 概述

1.1 集成算法概述

集成学习(ensemble learning)是时下非常流行的机器学习算法,它本身不是一个单独的机器学习算法,而是通过在数据上构建多个模型,集成所有模型的建模结果。基本上所有的机器学习领域都可以看到集成学习的身影,在现实中集成学习也有相当大的作用,它可以用来做市场营销模拟的建模,统计客户来源,保留和流失,也可用来预测疾病的风险和病患者的易感性。在现在的各种算法竞赛中,随机森林,梯度提升树(GBDT),Xgboost等集成算法的身影也随处可见,可见其效果之好,应用之广。

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

多个模型集成成为的模型叫做集成评估器(ensemble estimator),组成集成评估器的每个模型都叫做基评估器(base estimator)。通常来说,有三类集成算法:装袋法(Bagging),提升法(Boosting)和stacking。
sklearn(3)—— 随机森林_第1张图片
装袋法的核心思想是构建多个相互独立的评估器,然后对其预测进行平均或多数表决原则来决定集成评估器的结果。装袋法的代表模型就是随机森林。

提升法中,基评估器是相关的,是按顺序一一构建的。其核心思想是结合弱评估器的力量一次次对难以评估的样本进行预测,从而构成一个强评估器。提升法的代表模型有Adaboost和梯度提升树。

1.2 sklearn中的集成算法

  • sklearn中的集成算法模块ensemble
类的功能
ensemble.AdaBoostClassifier AdaBoost分类
ensemble.AdaBoostRegressor AdaBoost回归
ensemble.BaggingClassifier 装袋分类器
ensemble.Bagging Regressor 装袋回归器
ensemble.ExtraTreesclassifier Extra-trees分类(超树,极端随机树)
ensemble.ExtraTreesRegressor Extra- trees回归
ensemble.GradientBoostingClassifier 梯度提升分类
ensemble.GradientBoostingRegressor 梯度提升回归
ensemble.IsolationForest 隔离森林
ensemble.RandomForestClassifier 随机森林分类
ensemble.RandomForestRegressor 随机森林回归
ensemble.RandomTreesEmbedding 完全随机树的集成
ensemble.VotingClassifier 用于不合适估算器的软投票/多数规则分类器

集成算法中,有一半以上都是树的集成模型,可以想见决策树在集成中必定是有很好的效果。在这堂课中,我们会以随机森林为例,慢慢为大家揭开集成算法的神秘面纱。

2 RandomForestClassifier

随机森林是非常具有代表性的Bagging集成算法,它的所有基评估器都是决策树,分类树组成的森林就叫做随机森林分类器,回归树所集成的森林就叫做随机森林回归器。这一节主要讲解RandomForestClassifier,随机森林分类器。

2.1 重要参数

2.1.1 控制基评估器的参数

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

这些参数在随机森林中的含义,和我们在上决策树时说明的内容一模一样,单个决策树的准确率越高,随机森林的准确率也会越高,因为装袋法是依赖于平均值或者少数服从多数原则来决定集成的结果的。

2.1.2 n_estimators

这是森林中树木的数量,即基基评估器的数量。这个参数对随机森林模型的精确性影响是单调的,n_estimators越大,模型的效果往往越好。但是相应的,任何模型都有决策边界,n_estimators达到一定的程度之后,随机森林的精确性往往不在上升或开始波动,并且,n_estimators越大,需要的计算量和内存也越大,训练的时间也会越来越长。对于这个参数,我们是渴望在训练难度和模型效果之间取得平衡。

n_estimators的默认值在现有版本的sklearn中是10,但是在即将更新的0.22版本中,这个默认值会被修正为100。这个修正显示出了使用者的调参倾向:要更大的n_estimators。

  • 来建立一片森林吧
    树模型的优点是简单易懂,可视化之后的树人人都能够看懂,可惜随机森林是无法被可视化的。所以为了更加直观地让大家体会随机森林的效果,我们来进行一个随机森林和单个决策树效益的对比。我们依然使用红酒数据集。
  1. 导入我们需要的包
%matplotlib inline
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_wine
  1. 导入需要的数据集
wine = load_wine()
wine.data.shape # (178, 13)
wine.target
'''
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2])
'''
  1. 复习:sklearn建模的基本流程
# 实例化

# 训练集带入实例化后的模型进行训练,使用的接口是fit
# 使用其他接口将测试集导入我们训练好的模型,去获取我们希望获取的结果(score,Y_test)

from sklearn.model_selection import train_test_split

Xtrain,Xtest,Ytrain,Ytest= train_test_split(wine.data,wine.target,test_size=0.3)

clf = DecisionTreeClassifier(random_state=0)
rfc = RandomForestClassifier(random_state=0)

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

# Single Tree:0.9259259259259259 Random Forest:1.0

  1. 画出随机森林和决策树在十组交叉验证下的效果对比
# 交叉验证 cross_val_score

from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt

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")
plt.plot(range(1,11),clf_s,label="DecisionTree")
plt.legend()
# 结果如下图所示

sklearn(3)—— 随机森林_第2张图片

#带大家复习一下交叉验证
#交叉验证:是数据集划分为n分,依RandomForestClassifier试集,每n-1份做训练集,多次训练模型以观测模型稳定性的方法
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()
#是否有注意到,单个决策树的波动轨迹和随机森林一致?
#再次验证了我们之前提到的,单个决策树的准确率越高,随机森林的准确率也会越高
# 结果如下图所示   

sklearn(3)—— 随机森林_第3张图片
5. n_estimators的学习曲线

#####【TIME WARNING: 2mins 30 seconds】#####
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)
    
# superpa.index(max(superpa))  >>> list.index(onject) 对象object在列表中的索引
print(max(superpa),superpa.index(max(superpa)))
plt.figure(figsize=[20,5])
plt.plot(range(1,201),superpa)
plt.show()
# 结果如下图所示   

sklearn(3)—— 随机森林_第4张图片

2.1.3 random state

随机森林的本质是一种装袋集成算法( bagging),袋集成算法是对基评估器的预测结果进行平均或用多数表决原则来决定集成评估器的结果。在刚才的红酒例子中,我们建立了25棵树,对任何一个样本而言,平均或多数表决原则下,当且仅当有13棵以上的树判断错误的时候,随机森林才会判断错误。单独一棵决策树对红酒数据集的分类准确率在0.85上下浮动,假设一棵树判断错误的可能性为0.2(ε),那20棵树以上都判断错误的可能性是:
在这里插入图片描述

其中,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()
# 输出:0.00036904803455582827

可见,判断错误的几率非常小,这让随机森林在红酒据集上的表现远远好于单棵决策树.

那现在就有一个问题了:我们说袋装法服从多数表决原则或对基分类器结果求平均,这即是说,我们默认森林中的每棵树应该是不同的,并且会返回不同的结果。设想一下,如果随机森林甲所有的树的判断结果都一致(全判断对或全判断错),那随机森林无论应用何种集成原则来求结果,都应该无法比牟棵决策树取得更好的效果才对。但我们使用了一样的类 DecisionTreeClassifier,一样的参数,一样的训练集和测试集,为什么随机森林里的众多树会有不同的判断结果?

问到这个问题,很多小伙伴可能就分类树 Decision Tree classifier自带随机性,所以随机森林中的树天生就都是不一样的。决策树从最重要的特征中随机选择出一个特征来进行分枝,因此每次生成的决策树都不一样,这个功能由参数random_state控制。

随机森林中其实也有random_state,用法和分类树中相似,只不过在分类树中,一个random_state只控制生成一棵树,而随机森林中的 Random state控制的是生成森林的模式,而非让一个森林中只有一棵树。

rfc = RandomForestClassifier(n_estimators=20,random_state=2)
rfc = rfc.fit(Xtrain,Ytrain)

# 随机森林的重要属性之一:estimators_,查看森林中树的状况
rfc.estimators_[0].random_state # 输出: 1872583848

for i  in range(len(rfc.estimators_)):
    print(rfc.estimators_[i].random_state)
'''
输出:
1872583848
794921487
111352301
1853453896
213298710
1922988331
1869695442
2081981515
1805465960
1376693511
1418777250
663257521
878959199
854108747
512264917
515183663
1287007039
2083814687
1146014426
570104212
'''

我们可以观察到,当 grandom state固定时,随机森林中生成是组固定的树,但每棵树依然是不一致的,这是用随机挑选特征进行分枝的方法得到的随机性。并且我们可以证明,。当这种随机性越大的时候,袋装法的效果一般会越来越好。用袋装法集成时,基分类器应当是相互独立的,是不相同的
但这种做法的局限性是很强的,当我们需要成干上万棵例的时候,数据不一定能够提供成干上万的特征来让我们构筑尽量多尽量不同的树。因此,除了 random state.我们还需要其他的随机性.

2.1.4 bootstrap&oob_score

要让基分类器尽量都不一样,一种很容易理解的方法是使用不同的训练集来进行训练,而袋装法正是遹过有放回的随机抽样技术来形成不同的训练数据, bootstrap就是用来控制抽样技木的参数。

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

bootstrap参数默认True,代表采用这种有放回的随机抽样技术。通常,这个参数不会被我们设置为 False
sklearn(3)—— 随机森林_第5张图片
然而有放回抽样也会有自己的问题。由于是有放回,一些样本可能在同一个自助集中出现多次,而其他一些却可能被忽略,一般来说,自助集大约平均会包含63%的原始数据。因为每一个样本被抽到某个自助集中的概率为:
在这里插入图片描述

#一个自助集中,n次抽样时都抽到这个样本的概率1/n)^n 

#一个自助集里,只要n次中有一次抽到这个样本,这个样本就算是被抽到1/n)^2*1-1/n)^(n-2#在一个自助集里,样本A永远不会被抽到的概率是
1-1-1/n)^n

当n足够大时,这个概率收敛于1-(1/e),约等于0.632.因此,会有约37%的训练数据被浪费掉,没有参与建模这些数据被称为袋外数据 out_of_bagdata,简写为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_ ,调用分数
rfc.oob_score_  
# 输出:0.9606741573033708

2.2重要属性和接口

至此,我们已经讲完了所有随机森林中的重要参数,为大家复习了一下决策树的参数,并通过 n_estimators, random_state, boostrap和oob_ score这四个参数帮助大家了解了袋装法的基本流程和重要概念。同时,我们还介绍了, estimators_和, oob_score_这两个重要属性。除了这两个属性之外,作为树模型的集成算法,随机森林自然也有 .feature_ importances_这个属性。

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

rfc = RandomForestClassifier(n_estimators=25)
rfc = rfc.fit(Xtrain,Ytrain)
rfc.score(Xtest,Ytest) # 输出:0.9814814814814815

# 返回所有特征的特征值,特征值越大越重要
rfc.feature_importabces_ 
'''
array([0.20249617, 0.04436477, 0.01722322, 0.01747296, 0.01753655,
       0.06794022, 0.13289059, 0.00551955, 0.01448624, 0.13673387,
       0.07951371, 0.09690218, 0.16691998])
'''
# 返回测试集每一个样本在每一棵树中的叶子节点的索引(即返回这个数据在这棵树中叶子节点的索引)
rfc.apply(Xtest)
'''
array([[16, 12, 16, ..., 14, 18, 17],
       [ 6,  5,  6, ...,  4,  8,  2],
       [ 6, 10,  6, ...,  6, 12, 15],
       ...,
       [11,  7,  3, ..., 10, 10, 14],
       [10,  3,  8, ..., 10,  3,  6],
       [10,  5,  6, ..., 11, 15,  2]], dtype=int64)
'''
# 返回对测试集预测的标签
rfc.predict(Xtest)
'''
array([0, 1, 1, 0, 2, 2, 0, 0, 1, 1, 2, 1, 0, 0, 0, 2, 1, 2, 2, 1, 1, 2,
       0, 1, 0, 1, 2, 1, 0, 0, 1, 1, 0, 2, 1, 2, 0, 2, 1, 2, 2, 0, 1, 1,
       0, 0, 0, 0, 1, 1, 2, 2, 2, 1])
'''
 # 返回一个样本对应每一类标签的概率,选最高的值
rfc.predict_proba(Xtest)
'''
array([[0.96, 0.04, 0.  ],
       [0.  , 1.  , 0.  ],
       [0.16, 0.72, 0.12],
       [1.  , 0.  , 0.  ],
       [0.04, 0.04, 0.92],
       [0.  , 0.  , 1.  ],
       [0.92, 0.04, 0.04],
       [0.52, 0.48, 0.  ],
       [0.04, 0.92, 0.04],
       [0.2 , 0.64, 0.16],
       [0.04, 0.2 , 0.76],
       [0.36, 0.6 , 0.04],
       [0.8 , 0.08, 0.12],
       [1.  , 0.  , 0.  ],
       [0.96, 0.04, 0.  ],
       [0.04, 0.44, 0.52],
       [0.04, 0.92, 0.04],
       [0.  , 0.  , 1.  ],
       [0.  , 0.4 , 0.6 ],
       [0.32, 0.56, 0.12],
       [0.04, 0.96, 0.  ],
       [0.04, 0.28, 0.68],
       [0.44, 0.4 , 0.16],
       [0.  , 0.96, 0.04],
       [0.92, 0.08, 0.  ],
       [0.  , 1.  , 0.  ],
       [0.  , 0.  , 1.  ],
       [0.  , 1.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [0.96, 0.  , 0.04],
       [0.  , 0.96, 0.04],
       [0.  , 1.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [0.04, 0.04, 0.92],
       [0.04, 0.96, 0.  ],
       [0.  , 0.44, 0.56],
       [0.96, 0.04, 0.  ],
       [0.08, 0.2 , 0.72],
       [0.  , 1.  , 0.  ],
       [0.04, 0.4 , 0.56],
       [0.  , 0.  , 1.  ],
       [0.76, 0.16, 0.08],
       [0.  , 1.  , 0.  ],
       [0.  , 1.  , 0.  ],
       [0.88, 0.12, 0.  ],
       [0.68, 0.28, 0.04],
       [0.96, 0.04, 0.  ],
       [1.  , 0.  , 0.  ],
       [0.4 , 0.48, 0.12],
       [0.  , 1.  , 0.  ],
       [0.08, 0.12, 0.8 ],
       [0.  , 0.  , 1.  ],
       [0.  , 0.24, 0.76],
       [0.2 , 0.72, 0.08]])
'''

掌握了上面的知识,基本上要实现随机森林分类已经是没问题了。从红酒数据集的表现上来看,随机森林的效用单纯的决策树要强上不少,大家可以自己更换其他数据来试试看(比如上周完整课案例中的泰坦尼克号数据)。

3 RandomForestRegressor

所有的参数,属性与接口全部和随机森林回归器致。仅有的不同就是回归树与分类树的不同,不纯度的指标数 Criterion不一致

3.1重要参数,属性与接口

criterion

回归树衡量分枝质量的指标,支持的标准有三种:

1)输入"mse"使用均方误差 mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失

2)输入“ friedman mse"使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差

3)输入"mae"使用绝对平均误差MAE( mean absolute error),这种指标使用叶节点的中值来最小化L1损失

在这里插入图片描述
其中N是样本数量,i 是每一个数据样本,fi 是模型回归出的数值,yi 是样本点实际的数值标签。所以MSE的本质其实是样本真实数据与回归结果的差异,在回归树中,MSE不只是我们的分枝质量衡量指标,也是我们最常用的衡量回归树回归质量的指标,当我们在使用交又验证,或者其他方式获取回归树的结果时,我们往往选择均方误差作为我们的评估(在分类树中这个指标是score代表的预测准确率),在回归中,我们追求的是,MSE越小越好

然而,回归树的接口score返回的是R平方,并不是MSE。R平方被定义如下

在这里插入图片描述
其中u是残差平方和(MSE*N),v是总平方和,N是样本数量,i 是每一个数据样本,fi 是模型回归出的数值,yi 是样本点实际的数值标签。y帽是真实数值标签的平均数。R平方可以为正为负(如果模型的残差平方和远远大于模型的总平方和,模型非常糟糕,R平方就会为负),而均方误差永远为正

值得一提的是,虽然均方误差永远为正,但是 sklearn当中使用均方误差作为评判标准时,却是计算"负均方误差”( neg mean_squared_error)。这是因为 sklearn在计算模型评估指标的时候,会考虑指标本身的性质,均方误差本身是一种误差,所以被 sklearn划分为模型的一种损失(loss),因此在 sklearn当中,都以负数表示。真正的均方误差MSE的数值,其实就是 neg_mean_squared_error去掉负号的数字。

  • 随机森林回归用法
    和决策树完全一致,除了多了参数 n_estimators。
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) # 实例化
'''
regressor:模型
boston.data:完整的数据集(完整的特征矩阵)
boston.target:完整的标签
cv=10:交叉验证10次
scoring="neg_mean_squared_error" :打分的标准
'''
cross_val_score(regressor,boston.data,boston.target,cv=10
                ,scoring="neg_mean_squared_error")
'''
array([-10.72900447,  -5.36049859,  -4.74614178, -20.84946337,
       -12.23497347, -17.99274635,  -6.8952756 , -93.78884428,
       -29.80411702, -15.25776814])
'''
# sklearn当中的模型评估指标(打分)列表
import sklearn
scrted(sklearn.metrics.SCORERS.keys())
'''
['accuracy',
 'adjusted_mutual_info_score',
 'adjusted_rand_score',
 'average_precision',
 'balanced_accuracy',
 'brier_score_loss',
 'completeness_score',
 'explained_variance',
 'f1',
 'f1_macro',
 'f1_micro',
 'f1_samples',
 'f1_weighted',
 'fowlkes_mallows_score',
 'homogeneity_score',
 'mutual_info_score',
 'neg_log_loss',
 'neg_mean_absolute_error',
 'neg_mean_squared_error',
 'neg_mean_squared_log_error',
 'neg_median_absolute_error',
 'normalized_mutual_info_score',
 'precision',
 'precision_macro',
 'precision_micro',
 'precision_samples',
 'precision_weighted',
 'r2',
 'recall',
 'recall_macro',
 'recall_micro',
 'recall_samples',
 'recall_weighted',
 'roc_auc',
 'v_measure_score']
'''

返回十次交叉验证的结果,注意在这里,交叉验证的打分函数是MSE,因此交叉验证的结果可能有正也可能有负。

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

  1. 以波士顿数据集为例,导入完整的数据集并探索
dataset = load_boston()
dataset.data.shape # 输出:(506, 13)
# 总共506*13=6578个数据
x_full,y_full = dataset.data,dataset.target
n_samples = x_full.shape[0]
n_features = x_full.shape[1]
  1. 为完整数据集放入缺失值
#首先确定我们希望放入的缺失数据的比例,在这里我们假设是50%,那总共就要有3289个数据缺失
rng= np.random.RandomState(0) # 确认一种随机模式
missing_rate = 0.5
n_missing_samples = int(np.floor(n_samples*n_features*missing_rate))
n_missing_samples # 输出:3289

#np, floor向下取整,返回.0格式的浮点数
# 所有数据要随机遍布在数据集的各行各列当中,而一个缺失的数据会需要一个行索引和一个列索引
# 如果能够创造一个数组,包含3289个分布在0~506中间的行索引,和3289个分布在0~13之间的列索引,
# 那我们就可以利用索引来为数据中的任意3289个位置赋空值
# 然后我们用0,均值和随机森林来填写这些缺失值,然后查看回归的结果如何
# randint(上限,下限,n) 请在下限和上限之间取出n个整数
missing_features = rng.randint(0, n_features, n_missing_samples) # 列索引
missing_samples = rng.randint(0, n_samples, n_missing_samples) # 行索引
# missing_samples = rng.choice(dataset.data.shape[0],missing_samples,replace=False)
len(missing_features) # 输出:3289
missing_samples 
'''
array([150, 125,  28, ..., 132, 456, 402])
'''
# 我们现在采样了3289个数据,远远超过我们的样本量506,所以我们使用随机抽取的函数 randint。
# 但如果我们需要的数据量小于我们的样本量506,那我们可以采用 np. random. choice 来抽样, 
# choice会随机抽取不重复的随机数因此可以帮助我们让数据更加分散,确保数据不会集中在一些行中
x_missing = x_full.copy() 
y_missing = y_full.copy() 
x_missing[missing_samples,missing_features] = np.nan 
x_missing= pd.DataFrame(x_missing)
# 转换成 DataFrame是为了后续方便各种操作, numpy对矩阵的运算速度快到拯救人生,
# 但是在索引等功能上却不如pandas来得好用
x_missing # 输出结果如下图

sklearn(3)—— 随机森林_第6张图片
4. 使用0和均值填补缺失值

#使用均值进行填补
from sklearn.impute import SimpleImputer 
imp_mean= SimpleImputer(missing_values=np.nan,strategy='mean') # 实例化
x_missing_mean = imp_mean.fit_transform(x_missing) # 训练fit+导出predict >>>特殊接口fit_transform

#使用0进行填补
imp_0= SimpleImputer(missing_values=np.nan,strategy="constant",fill_value=0)
x_missing_0= imp_0.fit_transform(x_missing)

# 判断是否存在空值
# 布尔值 FaFalse = 0 True = 1
pd.DataFrame(x_missing_mean).isnull().sum()
  1. 使用随机森林填补缺失值
'''
使用随机森林回归填补缺失值

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

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

特征T不缺失的值对应的其他n-1个特征+本来的标签:X_train
特征T不缺失的值:Y_train

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

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

那如果数据中除了特征T之外,其他特征也有缺失值怎么办?
答案是遍历所有的特征,从缺失最少的开始进行填补(因为填补缺失最少的特征所需要的准确信息最少)。
填补一个特征时,先将其他特征的缺失值用0代替,毎完成一次回归预测,就将预测值放到原本的特征矩阵中,再继续填补下一个特征。每一次填补完毕,有缺失值的特征会减少一个,所以毎次循环后,需要用0来填补的特征就越来越少。当进行到最后一个特征时(这个特征应该是所有特征中缺失值最多的),已经没有任何的其他特征需要用O来进行填补了,而我们已经使用回归为其他特征填补了大量有效信息,可以用来填补缺失最多的特征。
遍历所有的特征后,数据就完整,不再有缺失值了。
'''
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] 
    df = pd.concat([df[df != fillc],pd.DataFrame(y_full)])
    # 在新特征矩阵中,对含有缺失值的列,进行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
# 结果如下图所示

sklearn(3)—— 随机森林_第7张图片
代码解析:
1)找出数据集中,缺失值从小到大排列的特征们的顺序

# 找出数据集中,缺失值从小到大排列的特征们的顺序
sortindex= np.argsort(x_missing_reg.isnull().sum(axis=0)).values 
x_missing_reg # 输出如下图
x_missing_reg.isnull() # 输出如下图
x_missing_reg.isnull().sum(axis=0)
'''
0     200
1     201
2     200
3     203
4     202
5     201
6     185
7     197
8     196
9     197
10    204
11    214
12    189
dtype: int64
'''
np.sort(x_missing_reg.isnull().sum(axis=0))
'''
array([185, 189, 196, 197, 197, 200, 200, 201, 201, 202, 203, 204, 214],
      dtype=int64)
'''
# 返回:从小到大排序的顺序所对应的索引
np.argsort(x_missing_reg.isnull().sum(axis=0))
'''
0      6
1     12
2      8
3      7
4      9
5      0
6      2
7      1
8      5
9      4
10     3
11    10
12    11
dtype: int64
'''
np.argsort(x_missing_reg.isnull().sum(axis=0)).values
'''
array([ 6, 12,  8,  7,  9,  0,  2,  1,  5,  4,  3, 10, 11], dtype=int64)
'''

2)构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和新标签(被选中去填充的特征)

# 构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和新标签(被选中去填充的特征)
df = x_missing_reg 
# 新标签
fillc = df.iloc[:,6] 
# 新特征矩阵
df = pd.concat([df.iloc[:,df.columns != 6],pd.DataFrame(y_full)],axis=1)

# 新特征矩阵
df.columns != 6
'''
array([ True,  True,  True,  True,  True,  True, False,  True,  True,
        True,  True,  True,  True])
'''
df.iloc[:,df.columns != 6]
# 输出结果如下图所示

sklearn(3)—— 随机森林_第8张图片

pd.DataFrame(y_full) # 输出结果如下图所示

sklearn(3)—— 随机森林_第9张图片

pd.concat([df.iloc[:,df.columns != 6],pd.DataFrame(y_full)],axis=1)
# 输出结果如下图所示

sklearn(3)—— 随机森林_第10张图片3)在新特征矩阵中,对含有缺失值的列,进行0的填补

# 在新特征矩阵中,对含有缺失值的列,进行0的填补
df_0 = SimpleImputer(missing_values=np.nan
                    ,strategy='constant',fill_value=0).fit_transform(df)
df_0
'''
array([[1.31848033e-02, 1.80000000e+01, 4.60000000e+00, ...,
        1.45720000e+01, 1.01908000e+01, 4.98000000e+00],
       [2.73100000e-02, 0.00000000e+00, 4.59810000e+00, ...,
        1.45130000e+01, 3.96900000e+02, 9.14000000e+00],
       [2.72900000e-02, 0.00000000e+00, 7.07000000e+00, ...,
        1.37960000e+01, 9.15320000e+00, 1.13274000e+01],
       ...,
       [1.38043600e-02, 0.00000000e+00, 1.19300000e+01, ...,
        2.10000000e+01, 9.61870000e+00, 5.64000000e+00],
       [1.09590000e-01, 0.00000000e+00, 1.19300000e+01, ...,
        2.10000000e+01, 3.93450000e+02, 6.48000000e+00],
       [4.74100000e-02, 0.00000000e+00, 1.19300000e+01, ...,
        1.44550000e+01, 3.96900000e+02, 7.88000000e+00]])


'''

4)找出我们的训练集和测试集

# 找出我们的训练集和测试集
# 是被选中要填充的特征中(现在是我们的标签),存在的那些值,非空值
Ytrain = fillc[fillc.notnull()]
# 是被中的要填充的特征中(现在是我们的标签),不存在的那些值,是空值
# 我们需要的不是 Test的值,需要的是 Test所带的索引
Ytest = fillc[fillc.isnull()]
# 在新特征矩阵上,递出来的要填充的特征的非空值所对应的记录
Xtrain = df_0[Ytrain.index,:] 
# 新特征矩阵上,被选出来的要填充的那个特征的空值所对应的记录
Xtest = df_0[Ytest.index,:]

fillc.notnull()
'''
0      True
1      True
2      True
3      True
4      True
       ... 
501    True
502    True
503    True
504    True
505    True
Name: 6, Length: 506, dtype: bool
'''
fillc[fillc.notnull()]
'''
0      65.200
1      78.900
2      61.100
3      45.800
4      28.693
        ...  
501    69.100
502    76.700
503    91.000
504    89.300
505    42.147
Name: 6, Length: 506, dtype: float64
'''
fillc.isnull()
'''
0      False
1      False
2      False
3      False
4      False
       ...  
501    False
502    False
503    False
504    False
505    False
Name: 6, Length: 506, dtype: bool
'''

5)用随机森林回归来填补缺失值

# 用随机森林回归来填补缺失值 
rfc = RandomForestRegressor(n_estimators=100) # 实例化
rfc = rfc.fit(Xtrain,Ytrain) # 导入训练集
# 用 predict接口将 Xtest导入,得到我们的预测结果(回归结果),就是我们要用来填补空值的这些值
Ypredict = rfc.predict(Xtest) 

6)将填补好的特征返回到我们的原始的特征矩阵

# 将填补好的特征返回到我们的原始的特征矩阵
x_missing_reg.loc[x_missing_reg.iloc[:,6].isnull(),6] = Ypredict
len(Ypredict) # 输出 214
  1. 对填补好的数据进行建模
X = [x_full,x_missing_mean,x_missing_0,x_missing_reg]

mse = []

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)

mse 
'''
[21.62860460743544, 40.84405476955929, 49.50657028893417, 20.357290501300717]
'''
[*zip(["x_full","x_missing_mean","x_missing_0","x_missing_reg"],mse)]
'''
[('x_full', 21.62860460743544),
 ('x_missing_mean', 40.84405476955929),
 ('x_missing_0', 49.50657028893417),
 ('x_missing_reg', 20.357290501300717)]
'''
  1. 用所得结果画出条形图
x_labels = ['Full data'
            ,'Zero Imputation'
            ,'Mean Imputation'
            ,'Regressor Imputation']

colors =['r','g','b','orange']

plt.figure(figsize=(12,6)) # 画出画布
ax = plt.subplot(111) # plt.subplot()添加子图

for i in np.arange(len(mse)):
    ax.barh(i,mse[i],color=colors[i],alpha=0.6,align='center') # barh 表示横向

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') # x轴名称
ax.invert_yaxis()
ax.set_yticklabels(x_labels) # y轴命名
plt.show()
# 输出结果如下图所示

sklearn(3)—— 随机森林_第11张图片

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

在准备这一套课程的时候,我发现大多数的机器学习相关的书都是遍历各种算法和案例,为大家讲解各种各样算法的原理和用途,但却对调参探究甚少。这中间有许多原因,其一是因为,调参的方式总是根据数据的状况而定,所以没有办法一概而论;其二是因为,其实大家也都没有特别好的办法。

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

我们也许无法学到高手们多年累积的经验,但我们可以学习他们对模型评估指标的理解和调参的思路。

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

  • 泛化误差

当模型在未知数据(测试集或者袋外数据)上表现糟糕时,我们说模型的泛化程度不够,泛化误差大,模型的效果不好。泛化误差受到模型的结构(复杂度)影响。看下面这张图,它准确地描绘了泛化误差与模型复杂度的关系,当模型太复杂,模型就会过拟合,泛化能力就不够,所以泛化误差大。当模型太简单,模型就会欠拟合,拟合能力就不够,所以误差也会大。只有当模型的复杂度刚刚好的才能够达到泛化误差最小的目标。

sklearn(3)—— 随机森林_第12张图片
那模型的复杂度与我们的参数有什么关系呢?对树模型来说,树越茂盛,深度越深,枝叶越多,模型就越复杂。所以树模型是天生位于图的右上角的模型,随机森林是以树模型为基础,所以随机森林也是天生复杂度高的模型。随机森林的参数,都是向着一个目标去:减少模型的复杂度,把模型往图像的左边移动,防止过拟合。当然了,调参没有绝对,也有天生处于图像左边的随机森林,所以调参之前,我们要先判断,模型现在究竟处于图像的哪一边。

泛化误差的背后其实是“偏差-方差困境”,原理十分复杂,无论你翻开哪一本书,你都会看见长篇的数学论证和每个字都能看懂但是连在一起就看不懂的文字解释。在下一节偏差vs方差中,我用最简单易懂的语言为大家解释了泛化误差背后的原理,大家选读。那我们只需要记住这四点:

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)和噪声(ε)共同决定。

在这里插入图片描述

关键概念:偏差与方差
观察下面的图像,每个点就是集成算法中的一个基评估器产生的预测值。红色虚线代表着这些预测值的均值,代表着数据本来的面貌。
偏差:模型的预测值与真实值之间的差异,即毎一个红点到蓝线的距离。在集成算法中,毎个基评估器都会有自己的偏差,集成评估器的偏差是所有基评估器偏差的均值。模型越精确,偏差越低。
方差:反映的是模型毎一次输出结果与模型预测值的平均水平之间的误差,即毎一个红点到红色虚线的距离,衡量模型的稳定性。模型越稳定,方差越低。

sklearn(3)—— 随机森林_第13张图片
其中偏差衡量模型是否预测得准确,偏差越小,模型越“准”;而方差衡量模型每次预测的结果是否接近,即是说方差越小,模型越“稳”;噪声是机器学习无法干涉的部分,为了让世界美好一点,我们就不去研究了。一个好的模型,要对大多数未知数据都预测得”准“又”稳“。即是说,当偏差和方差都很低的时候,模型的泛化误差就小,在未知数据上的准确率就高。

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

通常来说,方差和偏差有一个很大,泛化误差都会很大。然而,方差和偏差是此消彼长的,不可能同时达到最小值。这个要怎么理解呢?来看看下面这张图:
sklearn(3)—— 随机森林_第14张图片

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

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

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

所以,我们在降低复杂度的时候,本质其实是在降低随机森林的方差,随机森林所有的参数,也都是朝着降低方差的目标去。有了这一层理解,我们对复杂度和泛化误差的理解就更上一层楼了,对于我们调参,也有了更大的帮助。

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

我们就来使用我们刚才学的,基于方差和偏差的调参方法,在乳腺癌数据上进行一次随机森林的调参。乳腺癌数据是sklearn自带的分类数据之一。

案例中,往往使用真实数据,为什么我们要使用sklearn自带的数据呢?因为真实数据在随机森林下的调参过程,往往非常缓慢。真实数据量大,维度高,在使用随机森林之前需要一系列的处理,因此不太适合用来做直播中的案例演示。在本章,我为大家准备了kaggle上下载的辨别手写数字的数据,有4W多条记录700多个左右的特征,随机森林在这个辨别手写数字的数据上有非常好的表现,其调参案例也是非常经典,但是由于数据的维度太高,太过复杂,运行一次完整的网格搜索需要四五个小时,因此不太可能拿来给大家进行演示。我们上周的案例中用的泰坦尼克号数据,用来调参的话也是需要很长时间,因此我才选择sklearn当中自带的,结构相对清晰简单的数据来为大家做这个案例。大家感兴趣的话,可以进群去下载数据,也可以直接到kaggle上进行下载,数据集名称是DigitRecognizer(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. 导入数据集,探索数据

#可以看到,乳腺癌数据集有569条记录,30个特征,单看维度虽然不算太高,但是样本量非常少。过拟合的情况可能存在
data= load_breast_cancer()
# data
data.data.shape  # 输出 (569, 30)
data.target
'''
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0,
       1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0,
       1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1,
       1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0,
       0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1,
       1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0,
       0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0,
       1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1,
       1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0,
       0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
       0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0,
       1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1,
       1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1,
       1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
       1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
       1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1,
       1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1])
'''

3. 进行一次简单的建模,看看模型本身在数据集上的效果

#这里可以看到,随机森林在乳腺癌数据上的表现本就还不错,在现实数据集上,基本上不可能什么都不调就看到95%以上的准确率
rfc = RandomForestClassifier(n_estimators=100,random_state=90)
score_pre = cross_val_score(rfc,data.data,data.target,cv=10).mean()
score_pre # 输出 0.9666925935528475

4. 随机森林调整的第一步:无论如何先来调 n_estimators

"""
在这里我们选择学习曲线,可以使用网格搜索吗?可以,但是只有学习曲线,才能看见趋势
我个人的倾向是,要看见n_estimators在什么取值开始变得平稳,是否一直推动模型整体准确率的上升等信息
第一次的学习曲线,可以先用来帮助我们划定范围,我们取每十个数作为一个阶段,来观察n_estimators的变化如何
引起模型整体准确率的变化
"""
#####【TIME WARNING: 30 seconds】#####
scorel = []
for i in range(0,200,10):
    rfc = RandomForestClassifier(n_estimators=i+1,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中的索引

sklearn(3)—— 随机森林_第15张图片

5. 在确定好的范围内,进一步细化学习曲线

scorel = []
for i in range(35,45):
    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(35,45)][scorel.index(max(scorel))]))
plt.figure(figsize=[20,5])
plt.plot(range(35,45),scorel)
plt.show()

sklearn(3)—— 随机森林_第16张图片
调整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

#调整max_depth
param_grid = {'max_depth':np.arange(1, 20, 1)}
# 一般根据数据的大小来进行一个试探,乳腺癌数据很小,所以可以采用1~10,或者1~20这样的试探
# 但对于像digit recognition那样的大型数据来说,我们应该尝试30~50层深度(或许还不足够
#   更应该画出学习曲线,来观察深度对模型的影响
rfc = RandomForestClassifier(n_estimators=39
                             ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
GS.best_params_  # 输出  {'max_depth': 11}
GS.best_score_ # 输出 0.9718804920913884

在这里,我们注意到,将 max_depth设置为有限之后,模型的准确率下降了。限制 max_depth,是让模型变得简单,把模型向左推,而模型整体的准确率下降了,即整体的泛化误差上升了,这说明模型现在位于圉像左边,即泛化误差最低点的左边(偏差为主导的一边)。通常来说,随机森林应该在泛化误差最低点的右边,树模型应该倾向于过拟合,而不是拟合不足。这和数据集本身有关,但也有可能是我们调整的 n_estimators对于数据集来说太大,因此将模型拉到泛化误差最低点去了。然而,既然我们追求最低泛化误差,那我们就保留这个 n_estimators,除非有其他的因素,可以帮助我们达到更高的准确率。

当模型位于图像左边时,我们需要的是增加模型复杂度(增加方差,减少偏差)的选项,因此 max_depth应该尽量大, min_samples_lea和 min_ samples_ spli都应该尽量小。这几乎是在说明,除了 max_features,我们没有任何参数可以调整了,因为 max_depth, min_samples_lea和 min_samples_ split是剪枝参数,是减小复杂度的参数在这里,我们可以预言,我们已经非常接近模型的上限,模型很可能没有办法再进步了。

那我们这就来调整一下 max_features,看看模型如何变化

8.调整 max_features

#调整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=39
                             ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
# 显示调整好最佳的参数  
GS.best_params_ # 输出 {'max_features': 5}
# 显示调整好的分数
GS.best_score_ # 输出 0.9718804920913884

网格搜索返回了 max_features的最小值,可见 max_features升高之后,模型的准确率降低了。这说明,我们把模型往右推,模型的泛化误差增加了。前面用 max_depth往左推,现在用 max_features往右推,泛化误差都增加,这说明模型本身已经处于泛化误差最低点,已经达到了模型的预测上限,没有参数可以左右的部分了。剩下的那些误差,是噪声决定的,已经没有方差和偏差的舞台了如果是现实案例,我们到这一步其实就可以停下了,因为复杂度和泛化误差的关系已经告诉我们,模型不能再进步了。调参和训练模型都需要很长的时间,明知道模型不能进步了还继续调整,不是—个有效率的做法。如果我们希望模型更进一步,我们会选择更换算法,或者更换做数据预处理的方式。但是在课上,出于练习和探索的目的,我们继续调整我们的参数,让大家观察一下模型的变化,看看我们预测得是否正确。

依然按照参数对模型整体准确率的影响程度进行调参。

9. 调整 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=39
                             ,random_state=90
                            )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
GS.best_params_ # 输出 {'min_samples_leaf': 1}
GS.best_score_ # 输出  0.9718804920913884

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

10. 不懈努力,继续尝试 min_samples_split

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

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

11. 最后尝试一下 criterion

#调整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)
GS.best_params_  # 输出 {'criterion': 'gini'}
GS.best_score_ # 输出 0.9718804920913884

12. 调整完毕,总结出模型的最佳参数

rfc = RandomForestClassifier(n_estimators=39,random_state=90)
score = cross_val_score(rfc,data.data,data.target,cv=10).mean()
score
score - score_pre
# 输出  0.005264238181661218
# 表示调参后分数提高了0.005264238181661218

在整个调参过程之中,我们首先调整了 n_estimators(无论如何都请先走这一步),然后调整 max_depth,通过max_depth产生的结果,来判断模型位于复杂度泛化误差图像的哪一边,从而选择我们应该调整的参数和调参的方向。如果感到困惑,也可以画很多学习曲线来观察参数会如何影响我们的准确率,选取学习曲线中单调的部分来放大研究(如同我们对 n_estimators做的)。学习曲线的拐点也许就是我们一直在追求的,最佳复杂度对应的泛化误差最低点(也是方差和偏差的平衡点)。
网格搜索也可以一起调整多个参数,大家只要有时间,可以自己跑一下,看看网格搜索会给我们怎样的结果,有时候,它的结果比我们的好,有时候,我们手动调整的结果会比较好。当然了,我们的乳腺癌数据集非常完美,所以只需要调n_ estimators 一个参数就达到了随机森林在这个数据集上表现得极限。在我们上周使用的泰坦尼克号案例的数据中,我们使用同样的方法调出了如下的参数组合。

rfc = 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%,这对于调参来说其实是一个非常巨大的进步。不过,泰坦尼克号数据的运行缓慢,大家量力量时间而行,可以试试看用复杂度泛化误差方法(方差·偏差方法)来解读一下这个调参结果和过程。

你可能感兴趣的:(机器学习)