《菜菜的机器学习sklearn课堂》笔记目录 + 课件
集成学习(ensemble learning) 是时下非常流行的机器学习算法,它本身不是一个单独的机器学习算法,而是通过在数据上构建多个模型,集成所有模型的建模结果。
几乎所有机器学习领域都可以看到集成学习的身影,现实中集成学习也有很大作用:
集成算法的目标:集成算法会考虑多个评估器的建模结果,汇总之后得到一个综合的结果,以此来获取比单个模型更好的回归或分类表现。
多个模型集成成为的模型叫做集成评估器(ensemble estimator),组成集成评估器的每个模型都叫做基评估器(base estimator)。通常来说,有三类集成算法:
sklearn中的集成算法模块ensemble
类 | 类的功能 |
---|---|
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中的决策树
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 | 限制信息增益的大小,信息增益小于设定数值的分枝不会发生 |
单个决策树的准确率越高,随机森林的准确率也会越高,因为装袋法是依赖于平均值或者少数服从多数原则来决定集成的结果的。
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
2. 导入需要的数据集
wine = load_wine()
wine.data.shape #178行,13个标签
wine.target
3、复习:sklearn建模的基本流程
from sklearn.model_selection import train_test_split
#划分30%的数据作为测试集
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.9444444444444444 Random Forest:0.9814814814814815
可见随机森林默认就比决策树准确度要高。
4、画出随机森林和决策树在一组交叉验证下的效果对比
交叉验证
将数据集划分为n份,依次取每一份做测试集,每n-1份做训练集,多次训练模型以观测模型稳定性的方法
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 = "Decision Tree")
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"
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()
#####【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)
print(max(superpa), superpa.index(max(superpa))) # 0.9888888888888889 53
plt.figure(figsize=[20,5])
plt.plot(range(1,201),superpa)
plt.show()
思考一个问题:
我们说袋装法服从多数表决原则或对基分类器结果求平均,这即是说:我们默认森林中的每棵树应该是不同的,并且会返回不同的结果。
设想一下,如果随机森林里所有的树的判断结果都一致(全判断对或全判断错),那随机森林无论应用何种集成原则来求结果,都应该无法比单棵决策树取得更好的效果才对。但我们使用了一样的类DecisionTreeClassifier,一样的参数,一样的训练集和测试集,为什么随机森林里的众多树会有不同的判断结果?
随机森林中其实也有random_state,用法和分类树中相似:
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。我们还需要其他的随机性。
要让基分类器尽量都不一样,一种很容易理解的方法是使用不同的训练集来进行训练,而袋装法正是通过有放回的随机抽样技术来形成不同的训练数据,bootstrap就是用来控制抽样技术的参数。
在一个含有n个样本的原始训练集中,我们进行有放回的随机抽样技术:
由于是随机采样,这样每次的自助集和原始数据集不同,和其他的采样集也是不同的。这样我们就可以自由创造取之不尽用之不竭,并且互不相同的自助集,用这些自助集来训练我们的基分类器,我们的基分类器自然也就各不相同了。
默认bootstrap=True
,代表采用这种有放回的随机抽样技术;通常不会设置为False。
然而有放回抽样也会有自己的问题。由于是有放回,一些样本可能在同一个自助集中出现多次,而其他一些却可能被忽略,一般来说,自助集大约平均会包含63%的原始数据。因为每一个样本被抽到某个自助集中的概率为:
1 − ( 1 − 1 n ) n 1 - ( 1 - \frac 1 n)^n 1−(1−n1)n
当n足够大时,这个概率收敛于 1 − ( 1 e ) 1-(\frac 1 e) 1−(e1),约等于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_
rfc = RandomForestClassifier(n_estimators=20, random_state=2)
rfc = rfc.fit(Xtrain, Ytrain)
#随机森林的重要属性之一: estimators, 查看森林中树的状况
rfc.estimators_
随机森林的接口与决策树完全一致,因此也有4个常用接口:apply、fit、predict、score。
除此之外,还需要注意随机森林的predict_proba
接口,这个接口返回每个测试样本对应的被分到每一类标签的概率,标签有几个分类就返回几个概率。
传统的随机森林是利用袋装法中的规则,平均或少数服从多数来决定集成的结果,而sklearn中的随机森林是平均每个样本对应的predict_proba返回的概率,得到一个平均概率,从而决定测试样本的分类。
从红酒数据集的表现上来看,随机森林的效用比单纯的决策树要强上不少。
略…
大多数的机器学习相关的书都是遍历各种算法和案例,为大家讲解各种各样算法的原理和用途,但却对调参探究甚少,主要原因是:
通过画学习曲线,或者网格搜索,我们能够探索到调参边缘(代价可能是训练一次模型要跑三天三夜),但是在现实中,高手调参恐怕还是多依赖于经验;而这些经验来源于:
在机器学习中,衡量模型在未知数据上的准确率的指标叫做泛化误差(Genelization error)
未知数据:测试集、袋外数据
当模型在未知数据上表现糟糕时,我们说模型的泛化程度不够,泛化误差大,模型的效果不好。
泛化误差受模型的结构(复杂度)影响,下图准确地描绘了泛化误差与模型复杂度的关系:
对树模型来说,树越茂盛,深度越深,枝叶越多,模型就越复杂;所以树模型是天生位于图的右上角的模型。随机森林是以树模型为基础,所以随机森林也是天生复杂度高的模型。
随机森林的参数,都是向着一个目标去:减少模型的复杂度,把模型往图像的左边移动,防止过拟合。当然,调参无绝对,也有天生处于图像左边的随机森林,所以调参之前,我们要先判断:模型现在究竟处于图像的哪一边。
泛化误差的背后其实是 “偏差-方差困境”,原理十分复杂,我们只需记住四点:
老师基于经验,对各个参数对模型的影响程度做了一个排序:
有了以上的知识储备,我们现在也能够通过参数的变化来了解,模型什么时候到达了极限,当复杂度已经不能再降低的时候,我们就不必再调整了,因为调整大型数据的参数是一件非常费时费力的事。
除了学习曲线和网格搜索,我们现在有了基于对模型和正确的调参思路的“推测”能力,这能够让我们的调参能力更上一层楼。
调参时除了一些参数设置会变化,其他代码几乎是一样的,主要学习调参思路!
乳腺癌数据是sklearn自带的分类数据之一。
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()
data
data.data.shape
data.target
# 乳腺癌数据集有569条记录,30个特征
# 单看维度虽然不算太高, 但是样本量非常少, 过拟合的情况可能存在
3、进行一次简单的建模,看看模型本身在数据集上的效果
rfc = RandomForestClassifier(n_estimators=100, random_state=90) #建100棵树的随机森林,90是随便输的
score_pre = cross_val_score(rfc, data.data, data.target, cv=10).mean() #交叉验证
score_pre
# 这里可以看到,随机森林在乳腺癌数据上的表现本就还不错
# 在现实数据集上,基本上不可能什么都不调就看到95%以上的准确率
0.9648809523809524
4、随机森林调整的第一步:无论如何先调n_estimators
"""
在这里我们选择学习曲线,可以使用网格搜索吗?
-可以,但是只有学习曲线,才能看见趋势
我个人的倾向是,要看见n_estimators在什么取值开始变得平稳,
是否一直推动模型整体准确率的上升等信息,
第一次的学习曲线,可以先用来帮助我们划定范围,
我们取每十个数作为一个阶段,来观察n_estimators的变化如何引起模型整体准确率的变化
"""
#####【TIME WARNING: 30 seconds】#####
scorel = []
for i in range(0,200,10):
# 令 n_estimators = 1,11,21,31...,191
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) #将每次n_estimators不同的交叉验证的准确度放入scorel列表
#打印最高的交叉验证准确度及其索引
#list.index([object]) 返回这个object在列表list中的索引
print(max(scorel),(scorel.index(max(scorel))*10)+1)
plt.figure(figsize=[20,5])
plt.plot(range(1,201,10),scorel)
plt.show()
0.9631265664160402 71
5、在第4步确定好的范围内,进一步细化学习曲线
第4步中,输出的精确度最高的n_estimators,索引是71,因此我们将其范围调到 65~85
scorel = []
for i in range(64,84):
# 令 n_estimators = 65,66,67...,85
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) #将每次n_estimators不同的交叉验证的准确度放入scorel列表
#打印最高的交叉验证准确度及其索引
#list.index([object]) 返回这个object在列表list中的索引
print(max(scorel),([*range(64,84)][scorel.index(max(scorel))]))
plt.figure(figsize=[20,5])
plt.plot(range(64,84),scorel)
plt.show()
0.9666353383458647 72
调整n_estimators后,模型的准确率从0.963...
到了0.966...
。
接下来就进入网格搜索,我们将使用网格搜索对参数一个个进行调整。
为什么我们不同时调整多个参数呢?原因有两个:
- 同时调整多个参数会运行非常缓慢,在课堂上我们没有这么多的时间。
- 同时调整多个参数,会让我们无法理解参数的组合是怎么得来的,即便网格搜索调出来的结果不好,我们也不知道从哪里去改。在这里,为了使用复杂度-泛化误差方法(方差-偏差方法),我们对参数进行一个个地调整。
6、为网格搜索做准备,书写网格搜索的参数
"""
有一些参数是没有参照的,很难说清一个范围,这种情况下我们使用学习曲线,看趋势
从曲线跑出的结果中选取一个更小的区间,再跑曲线
param_grid = {'n_estimators':np.arange(0, 200, 10)}
param_grid = {'max_depth':np.arange(1, 20, 1)}
对于大型数据集,可以尝试从1000来构建,先输入1000,每100个叶子一个区间,再逐渐缩小范围
param_grid = {'max_leaf_nodes':np.arange(25,50,1)}
有一些参数是可以找到一个范围的,或者说我们知道他们的取值和
随着他们的取值模型的整体准确率会如何变化,这样的参数我们就可以直接跑网格搜索
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': 6}
GS.best_score_
0.9631265664160402
在这里,我们注意到,调整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
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': 6}
GS.best_score_
0.968421052631579
诶,这里和老师不一样呀,老师下降了,但是我却上升了…也说明了:调参无绝对
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': 4}
GS.best_score_
0.9613721804511279
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': 3}
GS.best_score_
0.9613721804511279
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': 'entropy'}
GS.best_score_
0.9649122807017543
12、调整完毕,总结出模型的最佳参数
rfc = RandomForestClassifier(n_estimators=72,
random_state=90,
max_features=6,
#min_samples_leaf=4,
#min_samples_split=3,
)
score = cross_val_score(rfc,data.data,data.target,cv=10).mean()
score
0.9648809523809525
score - score_pre
1.1102230246251565e-16
在整个调参过程之中,我们首先调整了 n_estimators(无论如何都请先走这一步);然后调整max_depth,通过max_depth产生的结果来判断模型位于复杂度-泛化误差图像的哪一边,从而选择我们应该调整的参数和调参的方向。