随机森林
1 概述
集成学习(ensemble learning)是时下非常流行的机器学习算法,它本身不是一个单独的机器学习算法,而是通过在数据上构建多个模型,集成所有模型的建模结果。基本上所有的机器学习领域都可以看到集成学习的身影,在现实中集成学习也有相当大的作用,它可以用来做市场营销模拟的建模,统计客户来源,保留和流失,也可用来预测疾病的风险和病患者的易感性。在现在的各种算法竞赛中,随机森林,梯度提升树(GBDT),Xgboost等集成算法的身影也随处可见,可见其效果之好,应用之广。
集成算法的目标 |
集成算法会考虑多个评估器的建模结果,汇总之后得到一个综合的结果,以此来获取比单个模型更好的回归或 分类表现。 |
多个模型集成成为的模型叫做集成评估器(ensemble estimator),组成集成评估器的每个模型都叫做基评估器(base estimator)(对随机森林来说,基评估器就是决策树)。通常来说,有三类集成算法:装袋法(Bagging),提升法(Boosting)和stacking。
装袋法:核心思想是构建多个相互独立的评估器,然后对其预测进行平均或多数表决原则来决定集成评估器的结果。装袋法的代表模型就是随机森林。(采样n次,建n个模型,互不影响,例:随机森林中每棵树都是互相独立的)
提升法:基评估器是相关的,是按顺序一一构建的。其核心思想是结合弱评估器的力量一次次对难以评估的样本进行预测,从而构成一个强评估器。提升法的代表模型有Adaboost和梯度提升树。
类 |
类的功能 |
ensemble.AdaBoostClassifier |
AdaBoost分类 |
ensemble.AdaBoostRegressor |
Adaboost回归 |
ensemble.BaggingClassifier |
装袋分类器 |
ensemble.BaggingRegressor |
装袋回归器 |
ensemble.ExtraTreesClassifier |
Extra-trees分类(超树,极端随机树) |
ensemble.ExtraTreesRegressor |
Extra-trees回归 |
ensemble.GradientBoostingClassifier |
梯度提升分类 |
ensemble.GradientBoostingRegressor |
梯度提升回归 |
ensemble.IsolationForest |
隔离森林 |
ensemble.RandomForestClassifier |
随机森林分类 |
ensemble.RandomForestRegressor |
随机森林回归 |
ensemble.RandomTreesEmbedding |
完全随机树的集成 |
ensemble.VotingClassifier |
用于不合适估算器的软投票/多数规则分类器 |
集成算法中,有一半以上都是树的集成模型,可以想见决策树在集成中必定是有很好的效果。在这堂课中,我们会以随机森林为例,慢慢为大家揭开集成算法的神秘面纱。
· 复习:sklearn中的决策树
在开始随机森林之前,我们先复习一下决策树。决策树是一种原理简单,应用广泛的模型,它可以同时被用于分类和回归问题。决策树的主要功能是从一张有特征和标签的表格中,通过对特定特征进行提问,为我们总结出一系列决策规则(对样本分类),并用树状图来呈现这些决策规则。
(例如图中体温是否恒温为特征,哺乳动物为标签)
决策树的核心问题有两个:
一:是如何找出正确的特征来进行提问,即如何分枝
二:树生长到什么时候应该停下。
对于第一个问题,我们定义了用来衡量分枝质量的指标不纯度,分类树的不纯度用基尼系数或信息熵来衡量,回归树的不纯度用MSE均方误差来衡量。每次分枝时,决策树对所有的特征进行不纯度计算,选取不纯度最低的特征进行分枝,分枝后,又再对被分枝的不同取值下,计算每个特征的不纯度,继续选取不纯度最低的特征进行分枝。
每分枝一层,树整体的不纯度会越来越小,决策树追求的是最小不纯度。因此,决策树会一致分枝,直到没有更多的特征可用,或整体的不纯度指标已经最优,决策树就会停止生长。
决策树非常容易过拟合,这是说,它很容易在训练集上表现优秀,却在测试集上表现很糟糕。为了防止决策树的过拟合,我们要对决策树进行剪枝,sklearn中提供了大量的剪枝参数,
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,随机森林分类器。
参数 |
含义 |
criterion |
不纯度的衡量指标,有基尼系数和信息熵(使用父节点和子节点之差即信息增益)两种选择 |
max_depth |
树的最大深度,超过最大深度的树枝都会被剪掉(限制树的生长参数) |
min_samples_leaf |
一个节点在分枝后的每个子节点都必须包含至少min_samples_leaf个训练样 本,否则分枝就不会发生(限制叶子节点的生长) |
min_samples_split |
一个节点必须要包含至少min_samples_split个训练样本,这个节点才允许被分 枝,否则分枝就不会发生 |
max_features |
max_features限制分枝时考虑的特征个数,超过限制个数的特征都会被舍弃, 默认值为总特征个数开平方取整(限制树的尺寸) |
min_impurity_decrease |
限制信息增益的大小,信息增益小于设定数值的分枝不会发生(限制树的尺寸的参数) |
这些参数在随机森林中的含义,和我们在上决策树时说明的内容一模一样,单个决策树的准确率越高,随机森林的准确率也会越高,因为装袋法是依赖于平均值或者少数服从多数原则来决定集成的结果的。
·来建立一片森林吧
树模型的优点是简单易懂,可视化之后的树人人都能够看懂,可惜随机森林是无法被可视化的。所以为了更加直观地让大家体会随机森林的效果,我们来进行一个随机森林和单个决策树效益的对比。我们依然使用红酒数据集。
1.导入我们需要的包
#导入我们需要的包 #画图的时候,需要这个环境,将这个jupyter导入到这个环境 %matplotlib inline from sklearn.tree import DecisionTreeClassifier #分类树 from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_wine #红酒数据集 |
2.导入需要的数据集
wine = load_wine() wine.data wine.target |
3.复习:sklearn建模的基本流程
#1.实例化 #2.训练集带入实例化后的模型去进行训练,使用的接口是fit #3.使用其他接口将测试集导入我们训练好的模型去获取我们希望获取的结果(score,Y_test) #导入划分训练集和测试集的类 from sklearn.model_selection import train_test_split #test_size=0.3 即70%做训练集,30%测试集(训练集和测试集随机划分导致结果不同) Xtrain,Xtest,Ytrain,Ytest = train_test_split(wine.data,wine.target,test_size=0.3) #1.实例化 clf = DecisionTreeClassifier(random_state=0) #实例化随机森林 rfc = RandomForestClassifier(random_state=0) #2.训练数据 clf = clf.fit(Xtrain,Ytrain) rfc = rfc.fit(Xtrain,Ytrain) #3.测试 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.8703703703703703 Random Forest:0.9629629629629629 |
4.画出随机森林和决策树在一组交叉验证下的效果对比
#交叉验证:cross_val_score #(分训练集和测试集时,不同的分法会对模型结果产生影响),探讨不同的训练集和测试集中,模型的稳定性 #将数据集划分为n分,依次取每一份做测试集,每n-1份做训练集,多次训练模型以观测模型稳定性的方法 from sklearn.model_selection import cross_val_score import matplotlib.pyplot as plt #实例化随机森林(森林中数目的数量=25) 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) #交叉验证的次数10次 plt.plot(range(1,11),rfc_s,label="RandomForest") plt.plot(range(1,11),clf_s,label="DecisionTree") #显示图例 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_1 = [] clf_1 = [] #进行10次建模,总共100次验证 for i in range(10): rfc = RandomForestClassifier(n_estimators=25) #参数(实例化的模型,特征矩阵,标签,循环次数) rfc_s = cross_val_score(rfc,wine.data,wine.target,cv=10).mean() rfc_1.append(rfc_s)
clf = DecisionTreeClassifier() clf_s = cross_val_score(clf,wine.data,wine.target,cv=10).mean() clf_1.append(clf_s) plt.plot(range(1,11),rfc_1,label = "Random Forest") plt.plot(range(1,11),clf_1,label = "Decision Tree") plt.legend() plt.show() |
进一步体现出随机森林的效果比决策树好,且两条曲线走势相同,即决策树效果越好,随机森林效果也越好
6.n_estimators的学习曲线
#####【TIME WARNING: 2mins 30 seconds】##### superpa = [] for i in range(200): #n_estimators=i+1从建1棵到200棵,跑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) #找出准确率最高的n_estimators,list.index(object)返回对象object在列表list中的索引 print(max(superpa),superpa.index(max(superpa))) plt.figure(figsize=[20,5]) plt.plot(range(1,201),superpa) plt.show() |
结果:0.9944444444444445 12
可以看出随着n_estimators的增长,效果越来越好,但当达到一定数量时,随机森林的精确性不再上升或波动
思考
随机森林用了什么方法,来保证集成的效果一定好于单个分类器?
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 #假设一棵树判断错误的概率0.2 np.array([comb(25,i)*(0.2**i)*((1-0.2)**(25-i)) for i in range(13,26)]).sum() -->0.00036904803455582827 |
可见,判断错误的几率非常小,这让随机森林在红酒数据集上的表现远远好于单棵决策树。
那现在就有一个问题了:我们说袋装法服从多数表决原则或对基分类器结果求平均,这即是说,我们默认森林中的每棵树应该是不同的,并且会返回不同的结果。设想一下,如果随机森林里所有的树的判断结果都一致(全判断对 或全判断错),那随机森林无论应用何种集成原则来求结果,都应该无法比单棵决策树取得更好的效果才对。但我们使用了一样的类DecisionTreeClassifier,一样的参数,一样的训练集和测试集,为什么随机森林里的众多树会有不同的判断结果?
问到这个问题,很多小伙伴可能就会想到了:sklearn中的分类树DecisionTreeClassifier自带随机性,所以随机森林中的树天生就都是不一样的。我们在讲解分类树时曾提到,决策树从最重要的特征中随机选择出一个特征来进行分枝,因此每次生成的决策树都不一样,这个功能由参数random_state控制。
随机森林中其实也有random_state,用法和分类树中相似,只不过在分类树中,一个random_state只控制生成一棵树,而随机森林中的random_state控制的是生成森林的模式,而非让一个森林中只有一棵树。
随机森林的重要属性之一:estimators,查看森林中树的状况
rfc=RandomForestClassifier(n_estimators=20 ,random_state=2) rfc = rfc.fit(Xtrain, Ytrain) #随机森林的重要属性之一:estimators,查看森林中树的状况rfc.estimators_[0].random_state for i in range(len(rfc.estimators_)): print(rfc.estimators_[i].random_state)
|
我们可以观察到,当random_state固定时,随机森林中生成是一组固定的树,但每棵树依然是不一致的,这是用”随机挑选特征进行分枝“的方法得到的随机性。并且我们可以证明,当这种随机性越大的时候,袋装法的效果一般会越来越好。用袋装法集成时,基分类器应当是相互独立的,是不相同的。
但这种做法的局限性是很强的,当我们需要成千上万棵树的时候,数据不一定能够提供成千上万的特征来让我们构筑尽量多尽量不同的树。因此,除了random_state。我们还需要其他的随机性。
2.1.4 bootstrap & oob_score
要让基分类器尽量都不一样,一种很容易理解的方法是使用不同的训练集来进行训练,而袋装法正是通过有放回的随机抽样技术来形成不同的训练数据,bootstrap就是用来控制抽样技术的参数。
在一个含有n个样本的原始训练集中,我们进行随机采样,每次采样一个样本,并在抽取下一个样本之前将该样本放回原始训练集,也就是说下次采样时这个样本依然可能被采集到,这样采集n次,最终得到一个和原始训练集一样大的,n个样本组成的自助集。由于是随机采样,这样每次的自助集和原始数据集不同,和其他的采样集也是不同的。这样我们就可以自由创造取之不尽用之不竭,并且互不相同的自助集,用这些自助集来训练我们的基分类器,我们的基分类器自然也就各不相同了。
bootstrap参数默认True,代表采用这种有放回的随机抽样技术。通常,这个参数不会被我们设置为False。
然而有放回抽样也会有自己的问题。由于是有放回,一些样本可能在同一个自助集中出现多次,而其他一些却可能被忽略,一般来说,自助集大约平均会包含63%的原始数据。因为每一个样本被抽到某个自助集中的概率为:
(逆向思维:在一个自助集里,样本A永远不会被抽到的概率为(1-1/n)^n,所有被抽到的概率就是1-(1-1/n)^n)
当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_ rfc.oob_score_ -->0.9550561797752809 |
随机森林的接口与决策树完全一致,因此依然有四个常用接口: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) -->0.9629629629629629 #特征重要性,可以用zip加特征名,显示更直观 rfc.feature_importances_ -->array([0.15289792, 0.05454116, 0.01171529, 0.022256 , 0.02074498, 0.06950304, 0.16920349, 0.01004513, 0.03146696, 0.2042654 , 0.12648034, 0.04322132, 0.08365897]) #apply输入测试集返回每个测试样本所在的叶子节点的索引(一般需要提取叶子节点使用) rfc.apply(Xtest) -->array([[ 8, 4, 1, ..., 4, 13, 3], [ 6, 19, 11, ..., 16, 12, 12], ..., [ 3, 19, 1, ..., 9, 8, 5], [ 3, 10, 3, ..., 9, 8, 5]], dtype=int64) #predict输入测试集返回每个测试样本的标签(预测结果) rfc.predict(Xtest) -->array([2, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 1, 1, 2, 0, 2, 1, 1, 1, 0, 2, 1, 1, 0, 0, 0, 2, 0, 2, 1, 2, 1, 1, 2, 0, 2, 1, 0, 1, 2, 0, 0, 2, 0, 1, 0, 2, 1, 1, 1, 1]) #返回每个测试样本对应的被分到每一类标签的概率,标签有几个分类就返回几个概率 rfc.predict_proba(Xtest) -->array([[0. , 0. , 1. ], [0.96, 0. , 0.04], [0. , 1. , 0. ], ... [0.32, 0.52, 0.16], [0.28, 0.56, 0.16], [0. , 1. , 0. ]]) |
掌握了上面的知识,基本上要实现随机森林分类已经是没问题了。从红酒数据集的表现上来看,随机森林的效用比单纯的决策树要强上不少,大家可以自己更换其他数据来试试看(比如上周完整课案例中的泰坦尼克号数据)。
之前我们说过,在使用袋装法时要求基评估器要尽量独立。其实,袋装法还有另一个必要条件:基分类器的判断准确率至少要超过随机分类器,即时说,基分类器的判断准确率至少要超过50%。之前我们已经展示过随机森林的准确率公式,基于这个公式,我们画出了基分类器的误差率ε和随机森林的误差率之间的图像。大家可以自己运行一下这段代码,看看图像呈什么样的分布。
import numpy as np from scipy.special import comb import matplotlib.pyplot as plt #linspace函数,在0,1间返回20个均匀分布的数 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-",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() |
estimators:基分类器 x轴:单基分类器误差 y轴:随机森林误差
可以从图像上看出,当基分类器的误差率小于0.5,即准确率大于0.5时,集成的效果(随机森林)是比基分类器(单个决策树)要好的。相反,当基分类器的误差率大于0.5,袋装的集成算法就失效了。所以在使用随机森林之前,一定要检查,用来组成随机森林的分类树们是否都有至少50%的预测正确率。(随机森林不一定效果比决策树好)
3 RandomForestRegressor(回归)
class sklearn.ensemble.RandomForesRegressor(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不一致。(①不纯度的指标:分类树和随机森林分类器用基尼系数,信息熵,回归树和随机森林回归:mse均方误差 ②模型的衡量指标:分类树和随机森林分类器:accuracy准确率,回归树和随机森林回归:mse均方误差(通常)和R² )
1)输入"mse"使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失
2)输入“friedman_mse”使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差
3)输入"mae"使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值来最小化L1损失
其中N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi是样本点i实际的数值标签。所以MSE的本质,其实是样本真实数据与回归结果的差异。在回归树中,MSE不只是我们的分枝质量衡量指标,也是我们最常用的衡量回归树回归质量的指标,当我们在使用交叉验证,或者其他方式获取回归树的结果时,我们往往选择均方误差MSE作为我们的评估
(在分类树中这个指标是score代表的预测准确率。在回归中,我们追求的是,MSE越小越好。然而,回归树的接口score返回的是R平方,并不是MSE。R平方被定义如下:
其中u是残差平方和(MSE * N),v是总平方和,N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi 是样本点i实际的数值标签。y帽是真实数值标签的平均数。R平方可以为正为负(如果模型的残差平方和远远大于模型的总平方和,模型非常糟糕,R平方就会为负,R平方的取值范围[-oo,-1]),而均方误差MSE永远为正。
值得一提的是,虽然均方误差永远为正,但是sklearn当中使用均方误差作为评判标准时,却是计算”负均方误差“(neg_mean_squared_error)。这是因为sklearn在计算模型评估指标的时候,会考虑指标本身的性质,均方误差本身是一种误差,所以被sklearn划分为模型的一种损失(loss),因此在sklearn当中,都以负数表示。真正的均方误差MSE的数值,其实就是neg_mean_squared_error去掉负号的数字。
重要属性和接口
最重要的属性和接口,都与随机森林的分类器相一致,还是apply, fit, predict和score最为核心。值得一提的是,随机森林回归并没有predict_proba这个接口,因为对于回归来说,并不存在一个样本要被分到某个类别的概率问题,因此没有predict_proba这个接口。
·随机森林回归用法
和决策树完全一致,除了多了参数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) #交叉验证(实例化的模型,完整特征矩阵,完整标签,交叉验证次数,交叉验证打分标准) 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 sorted(sklearn.metrics.SCORERS.keys()) -->['accuracy', 'adjusted_mutual_info_score', 'adjusted_rand_score', ...'roc_auc_ovr', 'roc_auc_ovr_weighted','v_measure_score'] |
返回十次交叉验证的结果,注意在这里,如果不填写scoring = "neg_mean_squared_error",交叉验证默认的模型衡量指标是R平方(取值[-oo,-1]),因此交叉验证的结果可能有正也可能有负。而如果写上scoring,则衡量标准是负MSE,交叉验证的结果只可能为负。
我们从现实中收集的数据,几乎不可能是完美无缺的,往往都会有一些缺失值。面对缺失值,很多人选择的方式是直接将含有缺失值的样本删除,这是一种有效的方法,但是有时候填补缺失值会比直接丢弃样本效果更好,即便我们其实并不知道缺失值的真实样貌。在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 #随机森林回归器 |
2.以波士顿数据集为例,导入完整的数据集并探索
#以波士顿数据集为例,导入完整的数据集并探索 dataset = load_boston() dataset.target #标签是连续型变量,用回归 dataset.data.shape #506个样本,13个特征 #总共506*13=6578个数据 X_full,y_full = dataset.data,dataset.target n_samples = X_full.shape[0] #样本数量506 n_features = X_full.shape[1] #标签数量13 |
3.为完整数据集放入缺失值
#为完整的数据集放入缺失值 #首先确定我们希望放入的缺失数据的比例(即多少数据变成缺失值),在这里我们假设是50%,那总共就要有3289个数据缺失 #确认一种固定的随机模式 rng = np.random.RandomState(0) missing_rate = 0.5 #总共506*13*0.5=3289个数据缺失,用int与np.floor取整 n_missing_samples = int(np.floor(n_samples * n_features * missing_rate)) #np.floor向下取整,返回.0格式的浮点数 #所有数据要随机遍布在数据集的各行各列当中,而一个缺失的数据会需要一个行索引和一个列索引 #如果能够创造一个数组,包含3289个分布在0~506中间的行索引,和3289个分布在0~13之间的列索引,那我们就可以利用索引来为数据中的任意3289个位置赋空值 #然后我们用0,均值和随机森林来填写这些缺失值,然后查看回归的结果如何 #randint(下限,上限,n)请在下限和上限之间取出n个整数 missing_features = rng.randint(0,n_features,n_missing_samples) #0-13取出3289个随机整数 missing_samples = rng.randint(0,n_samples,n_missing_samples) #0-506任意取出3289个随机整数 len(missing_features) -->3289 #missing_samples = rng.choice(n_samples,n_missing_samples,replace=False) #我们现在采样了3289个数据,远远超过我们的样本量506,所以我们使用随机抽取的函数randint #但如果我们需要的数据量小于我们的样本量506 #那我们可以采用np.random.choice来抽样,choice会随机抽取不重复的随机数,因此可以帮助我们让数据更加分散,确保数据不会集中在一些行中 #不改变原数据集 X_missing = X_full.copy() y_missing = y_full.copy() #填充nan,不用处理y_missing,因为特征可以空,标签不能空 X_missing[missing_samples,missing_features] = np.nan X_missing = pd.DataFrame(X_missing) #转换成DataFrame是为了后续方便各种操作,numpy对矩阵的运算速度快到拯救人生,但是在索引等功能上却不如pandas来得好用 X_missing -->
506 rows × 13 columns |
4.使用0和均值填补缺失值
#使用0和均值填补缺失值 from sklearn.impute import SimpleImputer #使用均值填补 #参数:缺失值(missing_values=np.nan)是什么样子,strategy="",mean代表均值 imp_mean = SimpleImputer(missing_values=np.nan,strategy="mean") #实例化 #训练fit+导出predict >>> 特殊的接口 fit_transform X_missing_mean = imp_mean.fit_transform(X_missing) pd.DataFrame(X_missing_mean).isnull().sum() #布尔值 False = 0,True = 1 ,结果都为0说明缺失值都填补了 --> 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 10 0 11 0 12 0 dtype: int64 #使用0进行填补 #constant:常数,fill_value=0即用0 imp_0 = SimpleImputer(missing_values=np.nan, strategy="constant",fill_value=0) X_missing_0 = imp_0.fit_transform(X_missing) pd.DataFrame(X_missing_0) -->之前nan的位置都被0填补了
506 rows × 13 columns |
5.使用随机森林填补缺失值
"""(理解很重要) 使用随机森林回归填补缺失值(例如用0,均值等填补缺失值都不合适的时候) 任何回归都是从特征矩阵中学习,然后求解连续型标签y的过程,之所以能够实现这个过程,是因为回归算法认为,特征矩阵和标签之前存在着某种联系。实际上,标签和特征是可以相互转换的,比如说,在一个“用地区,环境,附近学校数量”预测“房价”的问题中,我们既可以用“地区”,“环境”,“附近学校数量”的数据来预测“房价”,也可以反过来,用“环境”,“附近学校数量”和“房价”来预测“地区”。而回归填补缺失值,正是利用了这种思想。 对于一个有n个特征的数据来说,其中特征T有缺失值,我们就把特征T当作标签,其他的n-1个特征和原本的标签组成新的特征矩阵。那对于T来说,它没有缺失的部分,就是我们的Y_train,这部分数据既有标签也有特征,而它缺失的部分,只有特征没有标签,就是我们需要预测的部分。 特征T不缺失的值对应的其他n-1个特征 + 本来的标签:X_train(作为特征) 特征T不缺失的值:Y_train(作为标签) 特征T缺失的值对应的其他n-1个特征 + 本来的标签:X_test 特征T缺失的值:未知,我们需要预测的Y_test 这种做法,对于某一个特征大量缺失,其他特征却很完整的情况,非常适用。 那如果数据中除了特征T之外,其他特征也有缺失值怎么办? 答案是遍历所有的特征,从缺失最少的开始进行填补(因为填补缺失最少的特征所需要的准确信息最少)。 填补一个特征时,先将其他特征的缺失值用0代替,每完成一次回归预测,就将预测值放到原本的特征矩阵中,再继续填补下一个特征。每一次填补完毕,有缺失值的特征会减少一个,所以每次循环后,需要用0来填补的特征就越来越少。当进行到最后一个特征时(这个特征应该是所有特征中缺失值最多的),已经没有任何的其他特征需要用0来进行填补了,而我们已经使用回归为其他特征填补了大量有效信息,可以用来填补缺失最多的特征。 遍历所有的特征后,数据就完整,不再有缺失值了。 """ #回归填补缺失值的矩阵依然使用copy X_missing_reg = X_missing.copy() #找出数据集中缺失值从小到大排列的特征的顺序(本质是找索引),有了这些特征按缺失值从小到大排序的索引 sortindex = np.argsort(X_missing_reg.isnull().sum(axis=0)).values #np.argsort会返回从小到大排序的顺序所对应的索引,np.sort排序结果没有索引 (#.values将结果取出 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) ) for i in sortindex:
#构建我们的新特征矩阵和新标签df = X_missing_reg df = X_missing_reg fillc = df.iloc[:,i] df = pd.concat([df.iloc[:,df.columns != i],pd.DataFrame(y_full)],axis=1) #在新特征矩阵中,对含有缺失值的列,进行0的填补 df_0=SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df) #找出我们的训练集和测试集(特征T为有缺失需要填补的特征) Ytrain = fillc[fillc.notnull()] #特征T不缺失的值,非空的值 Ytest = fillc[fillc.isnull()] #特征T缺失的值:未知,我们需要预测 Xtrain = df_0[Ytrain.index,:] #特征T不缺失的值对应的其他n-1个特征 + 本来的标签 Xtest = df_0[Ytest.index,:] #特征T缺失的值对应的其他n-1个特征 + 本来的标签
#用随机森林回归来填补缺失值 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 X_missing_reg.head() -->nan都填补了
|
【代码讲解】
#***********代码讲解************* #构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和新标签(被选中去填充的特征) #不改变X_missing_reg,所以赋值给df,先在df上操作,避免X_missing_reg被填入0,最后再把结果放回原来的X_missing_reg df = X_missing_reg #作为新标签,以索引为6(即第七列nan最少)为例 fillc = df.iloc[:,6] #新特征矩阵(没有被选中去填充的特征+原始的标签)(可以取索引!= 6 的列) df = pd.concat([df.iloc[:,df.columns != 6],pd.DataFrame(y_full)],axis=1) #df.iloc[:,df.colums ! = 6] 取所有的行,索引不为6的列 #y_full为原始的标签,把其变为DataFrame(506行,1列) #通过pd.concat连接,(axis=1即把y_full作为新的一列,不然会加到下面) #注意:只能执行一次,不然y_full会一直加进去 #在新特征矩阵中,对含有缺失值的列,进行0的填补,实例化 df_0 =SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df) #找出我们的训练集和测试集 Ytrain = fillc[fillc.notnull()] #Ytrain是被选中要填充的特征中(现在是作为标签),存在的那些值,非空值 #Ytest需要我们预测,所以全为nan的Ytest,我们需要的不是Ytest的值,需要的是Ytest的索引 Ytest = fillc[fillc.isnull()] #Ytest是被选中要填充的特征中(现在使我们的标签),不存在的那些值,是空值 #在新特征矩阵上,被选出来的要填充的特征的非空值所对应的记录 Xtrain = df_0[Ytrain.index,:] #Xtrain是要填充的特征不缺失的值(非空值)对应的其他n-1个特征 + 本来的标签 #在新特征矩阵上,被选出来的要填充的那个特征的空值所对应的记录 Xtest = df_0[Ytest.index,:] #Xtest是要填充的特征缺失的值(空值)对应的其他n-1个特征 + 本来的标签' #用随机森林回归来填补缺失值 rfc = RandomForestRegressor(n_estimators=100) #实例化 rfc = rfc.fit(Xtrain, Ytrain) #导入训练集进行训练 Ypredict = rfc.predict(Xtest) #用predict接口,将Xtest导入得到预测(回归)结果,就是用来填补缺失值的值 #将填补好的特征返回到我们的原始的特征矩阵中 X_missing_reg.loc[X_missing_reg.iloc[:,6].isnull(),6] = Ypredict |
6.对填补好的数据进行建模
#6.对填补好的数据进行建模 #对所有数据进行建模,取得MSE结果 #分别为原本的数据集,用0填补的数据集,用均值填补的数据集,用随机森林回归填补缺失的数据集 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) #元组形式查看结果 [*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.567284947369437)] #(mse均方误差越小越好,随机森林回归的效果甚至比原数据集更好) |
7.用所得结果画出条形图
#7.用所得结果画出条形图 x_labels = ["X_full","X_missing_mean","X_missing_0","X_missing_reg"] colors = ["r","g","b","orange"] plt.figure(figsize=(12,6),dpi=80) #画出画布 ax = plt.subplot(111) #plt.subplot添加子图 for i in np.arange(len(mse)): #np.arange = range(len(mse)) 0-3 #横条形图,alpha是条的粗度,align条形放在中间 ax.barh(i,mse[i],color=colors[i],alpha=0.6,align="center") ax.set_title('Imputation Techniques with Boston Data') #使x轴不从0开始 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_labels里面内容作y轴命名 ax.set_yticklabels(x_labels) plt.show() |
可见随机森林回归的效果最好
4 机器学习中调参的基本思想
大多数的机器学习相关的书都是遍历各种算法和案例,为大家讲解各种各样算法的原理和用途,但却对调参探究甚少。
这中间有许多原因,其一是因为,调参的方式总是根据数据的状况而定,所以没有办法一概而论;其二是因为,其实大家也都没有特别好的办法。
通过画学习曲线,或者网格搜索,我们能够探索到调参边缘(代价可能是训练一次模型要跑三天三夜),但是在现实中,高手调参恐怕还是多依赖于经验,而这些经验,来源于:1)非常正确的调参思路和方法,2)对模型评估指标的理解,3)对数据的感觉和经验,4)用洪荒之力去不断地尝试。
我们也许无法学到高手们多年累积的经验,但我们可以学习他们对模型评估指标的理解和调参的思路。
那我们首先来讲讲正确的调参思路。模型调参,第一步是要找准目标:我们要做什么?一般来说,这个目标是提升某个模型评估指标,比如对于随机森林来说,我们想要提升的是模型在未知数据上的准确率(由score或oob_score_来衡量)。找准了这个目标,我们就需要思考:模型在未知数据上的准确率受什么因素影响?在机器学习中,我们用来衡量模型在未知数据上的准确率的指标,叫做泛化误差(Genelization error)。
·泛化误差
当模型在未知数据(测试集或者袋外数据)上表现糟糕时,我们说模型的泛化程度不够,泛化误差大,模型的效果不好。泛化误差受到模型的结构(复杂度)影响。看下面这张图,它准确地描绘了泛化误差与模型复杂度的关系,当模型太复杂(训练数据学习太具体),模型就会过拟合,泛化能力就不够,所以泛化误差大。当模型太简单,模型就会欠拟合,拟合能力就不够,所以误差也会大。只有当模型的复杂度刚刚好的才能够达到泛化误差最小的目标。
那模型的复杂度与我们的参数有什么关系呢?对树模型来说,树越茂盛,深度越深,枝叶越多,模型就越复杂。所以树模型是天生位于图的右上角的模型,随机森林是以树模型为基础,所以随机森林也是天生复杂度高的模型。随机森林的参数,都是向着一个目标去:减少模型的复杂度,把模型往图像的左边移动,防止过拟合。当然了,调参没有绝对,也有天生处于图像左边的随机森林,所以调参之前,我们要先判断,模型现在究竟处于图像的哪一边。
泛化误差的背后其实是“偏差-方差困境”,原理十分复杂,无论你翻开哪一本书,你都会看见长篇的数学论证和每个 字都能看懂但是连在一起就看不懂的文字解释。在下一节偏差vs方差中,我用最简单易懂的语言为大家解释了泛化 误差背后的原理,大家选读。那我们只需要记住这四点:
1.模型太复杂或者太简单,都会让泛化误差高,我们追求的是位于中间的平衡点
2.模型太复杂就会过拟合,模型太简单就会欠拟合
3.对树模型和树的集成模型来说,树的深度越深,枝叶越多,模型越复杂
4.树模型和树的集成模型的目标,都是减少模型复杂度,把模型往图像的左边移动
那具体每个参数,都如何影响我们的复杂度和模型呢?我们一直以来调参,都是在学习曲线上轮流找最优值,盼望能够将准确率修正到一个比较高的水平。然而我们现在了解了随机森林的调参方向:降低复杂度,我们就可以将那些对复杂度影响巨大的参数挑选出来,研究他们的单调性,然后专注调整那些能最大限度让复杂度降低的参数。对于那些不单调的参数,或者反而会让复杂度升高的参数,我们就视情况使用,大多时候甚至可以退避。基于经验,我对各个参数对模型的影响程度做了一个排序。在我们调参的时候,大家可以参考这个顺序。
(树模型天生过拟合,一般使用)
参数 |
对模型在未知数据上的评估性能的影响 |
影响程度 |
n_estimators |
提升至平稳,n_estimators↑,不影响单个模型的复杂度 |
4 |
max_depth |
有增有减,默认最大深度,即最高复杂度,向复杂度降低的方向调参 max_depth↓,模型更简单,且向图像的左边移动 |
3 |
min_samples _leaf |
有增有减,默认最小限制1,即最高复杂度,向复杂度降低的方向调参 min_samples_leaf↑,模型更简单,且向图像的左边移动 |
2 |
min_samples _split |
有增有减,默认最小限制2,即最高复杂度,向复杂度降低的方向调参 min_samples_split↑,模型更简单,且向图像的左边移动 |
2 |
max_features |
有增有减,默认auto,是特征总数的开平方,位于中间复杂度,既可以向复杂度升高的方向,也可以向复杂度降低的方向调参max_features下降, 模 型 更 简 单 , 图 像 左 移 max_features上升,模型更复杂,图像右移 max_features是唯一的,既能够让模型更简单,也能够让模型更复杂的参数,所以在调整这个参数的时候,需要考虑我们调参的方向 |
1 |
criterion |
有增有减,一般使用gini |
看具体情况 |
有了以上的知识储备,我们现在也能够通过参数的变化来了解,模型什么时候到达了极限,当复杂度已经不能再降 低的时候,我们就不必再调整了,因为调整大型数据的参数是一件非常费时费力的事。除了学习曲线和网格搜索, 我们现在有了基于对模型和正确的调参思路的“推测”能力,这能够让我们的调参能力更上一层楼。
在这节课中,我们了解了随机森林,并且学习了机器学习中调参的基本思想,了解了方差和偏差如何受到随机森林的参数们的影响。这一节,我们就来使用我们刚才学的,基于方差和偏差的调参方法,在乳腺癌数据上进行一次随机森林的调参。乳腺癌数据是sklearn自带的分类数据之一。
案例中,往往使用真实数据,为什么我们要使用sklearn自带的数据呢?因为真实数据在随机森林下的调参过程,往往非常缓慢。真实数据量大,维度高,在使用随机森林之前需要一系列的处理,因此不太适合用来做直播中的案例演示。在本章,我为大家准备了kaggle上下载的辨别手写数字的数据,有4W多条记录700多个左右的特征,随机森林在这个辨别手写数字的数据上有非常好的表现,其调参案例也是非常经典,但是由于数据的维度太高,太过复杂,运行一次完整的网格搜索需要四五个小时,因此不太可能拿来给大家进行演示。我们上周的案例中用的泰坦尼克号数据,用来调参的话也是需要很长时间,因此我才选择sklearn当中自带的,结构相对清晰简单的数据来为大家做这个案例。大家感兴趣的话,可以直接到kaggle上进行下载,数据集名称是Digit Recognizer(Digit Recognizer | Kaggle)。
那我们接下来,就用乳腺癌数据,来看看我们的调参代码。
1.导入需要的库
#导入库 from sklearn.datasets import load_breast_cancer #从datasets中导入乳腺癌数据 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() data data.data.shape #569个样本,30个特征,数据少容易过拟合 -->(569, 30) data.target #二分类0和1 #可以看到,乳腺癌数据集有569条记录,30个特征,单看维度虽然不算太高,但是样本量非常少。过拟合的情况可能存在。 |
3.进行一次简单的建模,看看模型本身在数据集上的效果
#进行一次简单的建模,看看模型本身在数据集上的效果 #实例化 rfc = RandomForestClassifier(n_estimators=100,random_state=90) #交叉验证参数(实例化模型,分训练集测试集前的完整的特征矩阵,特征标签,交叉验证次数,(回归时还有scoring选择以什么回归)) score_pre = cross_val_score(rfc,data.data,data.target,cv=10).mean() score_pre -->0.9648809523809524 #这里可以看到,随机森林在乳腺癌数据上的表现本就还不错,在现实数据集上,基本上不可能什么都不调就看到95%以上的准确率 |
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, #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) #返回score取最大值时的索引,索引*10+1则为最高的score对应的n_estimators 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中的索引 |
0.9631265664160402 71 即n_estimators为71时最高
5.在确定好的范围内,进一步细化学习曲线
#在确定好的范围内,进一步细化学习曲线 scorel = [] for i in range(65,75): rfc = RandomForestClassifier(n_estimators=i, #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) #返回score取最大值时的索引,*是序列解包,*range(0,6) 等效于0 1 2 3 4 5 print(max(scorel),([*range(65,75)][scorel.index(max(scorel))])) plt.figure(figsize=[20,5]) plt.plot(range(65,75),scorel) plt.show() |
0.9666353383458647 73
调整n_estimators的效果显著,模型的准确率立刻上升了0.003。接下来就进入网格搜索,我们将使用网格搜索对参数一个个进行调整。为什么我们不同时调整多个参数呢?原因有两个:1)同时调整多个参数会运行非常缓慢,在课堂上我们没有这么多的时间。2)同时调整多个参数,会让我们无法理解参数的组合是怎么得来的,所以即便网格搜索调出来的结果不好,我们也不知道从哪里去改。在这里,为了使用复杂度-泛化误差方法(方差-偏差方 法),我们对参数进行一个个地调整。
6.为网格搜索做准备,书写网格搜索的参数
""" 有一些参数是没有参照的,很难说清一个范围,这种情况下我们使用学习曲线, 看趋势从曲线跑出的结果中选取一个更小的区间,再跑曲线。(参数范围说不清,通过学习曲线缩小区间) #n_estimators 树最大数量 param_grid = {'n_estimators':np.arange(0, 200, 10)} #max_depth param_grid = {'max_depth':np.arange(1, 20, 1)} #max_leaf_nodes param_grid = {'max_leaf_nodes':np.arange(25,50,1)} 对于大型数据集,可以尝试从1000来构建,先输入1000,每100个叶子一个区间,再逐渐缩小范围 有一些参数是可以找到一个范围的,或者说我们知道他们的取值和随着他们的取值,模型的整体准确率会如何变化, 这样的参数我们就可以直接跑网格搜索(能通过参数取值知道准确率如何变化用网格搜索) #criterion param_grid = {'criterion':['gini', 'entropy']} #min_samples_split 节点向下分枝所需的最小样本数 param_grid = {'min_samples_split':np.arange(2, 2+20, 1)} #min_samples_leaf 分枝的叶子节点处所需的最小样本数 param_grid = {'min_samples_leaf':np.arange(1, 1+10, 1)} #max_features 最大特征数 param_grid = {'max_features':np.arange(5,30,1)} """ |
7.开始按照参数对模型整体准确率的影响程度进行调参,首先调整max_depth
#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=73 ,random_state=90 ) #网格搜索GridSearchCV(实例化的模型,搜索的参数,交叉验证的次数) GS = GridSearchCV(rfc,param_grid,cv=10) #训练模型 GS.fit(data.data,data.target) #显示调整出来的最佳参数 GS.best_params_ -->{'max_depth': 8} #返回调整好的最佳参数对应的准确率 GS.best_score_ -->0.9666353383458647 |
在这里,我们注意到,将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,看看模型如何变化。
8.调整max_features
#调整max_features #特征从5-30 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) #显示调整出来的最佳参数 GS.best_params_ -->{'max_features': 24} #返回调整好的最佳参数对应的准确率 GS.best_score_ -->0.9666666666666668 |
网格搜索返回了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=73 ,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_ -->9666353383458647 |
可以看见,网格搜索返回了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=73 ,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.9666353383458647 |
和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) #显示调整出来的最佳参数 GS.best_params_ -->{'criterion': 'gini'} #返回调整好的最佳参数对应的准确率 GS.best_score_ -->0.9666353383458647 |
仍然为默认的“gini”
12.调整完毕,总结出模型的最佳参数
#最佳参数就是n_estimators,我们在实验中取得最佳的为73 rfc = RandomForestClassifier(n_estimators=73,random_state=90) score = cross_val_score(rfc,data.data,data.target,cv=10).mean() #调完后的分数 score -->0.9666353383458647 #调完-调参前 score-score_pre -->0.0017543859649122862 |
在整个调参过程之中,我们首先调整了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%,这对于调参来说其实是一个非常巨大的进步。不过,泰坦尼克号数据的运行缓慢,大家量力量时间而行,可以试试看用复杂度-泛化误差方法(方差-偏 差方法)来解读一下这个调参结果和过程。
6.1 Bagging vs Boosting
装袋法 Bagging |
提升法 Boosting |
|
评估器 |
相互独立,同时运行 |
相互关联,按顺序依次构建,后建的模型会在先建模型预测失败的样本上有更多的权重 |
抽样数集 |
有放回抽样 |
有放回抽样,但会确认数据的权重,每次抽样都会给容易预测失败的样本更多的权重 |
决定集成的结果 |
平均或少数服从多数原则 |
加权平均,在训练集上表现更好的模型会有更大的权重 |
目标 |
降低方差,提高模型整体的稳定性 |
降低偏差,提高模型整体的精确度 |
单个评估器存在过拟合问题的时候 |
能够一定程度上解决过拟合问题 |
可能会加剧过拟合问题 |
单个评估器的效力比较弱的时候 |
不是非常有帮助 |
很可能会提升模型表现 |
代表算法 |
随机森林 |
梯度提升树,Adaboost |
6.2 RFC的参数列表
6.3 RFC的属性列表
6.4 RFC的接口列表