使用fast.ai库解释随机森林模型的指南(程序员机器学习 - 第2部分)

徐大白
10个月前 阅读 314 点赞 1

机器学习是一个快速发展的领域 - 但能解释机器学习模型这件事情却依然没变。如果您构建模型并且无法向业务用户解释它可就不太理想了。

您能想象将模型集成到您的产品中而不了解它的工作原理吗?或哪些特征会影响您的最终结果?

除了利益相关方的支持外,我们数据科学家还可以从解释我们的工作和改进工作中受益。这是一个双赢的局面!

这个fast.ai机器学习课程的第一篇文章看到了我们社区的一个令人难以置信的回应。我很高兴分享本系列的第2部分——主要介绍如何解释随机森林模型。我们将理解该理论并在Python中实现它以巩固我们对这一关键概念的把握。

与往常一样,我鼓励您在阅读本文时在自己的计算机上复制代码。试用代码,看看您的结果与我在本文中介绍的内容有多么不同。这将有助于您了解随机森林算法的不同方面以及可解释性的重要性。

目录

1.第1部分回顾(第1课和第2课)

2. 机器学习简介第三课

2.1构建随机森林

2.2基于树差异的置信度

2.3特征重要性

3.机器学习简介第四课

3.1一个热编码

3.2删除冗余功能

3.3部分依赖

3.4TreeInterpreter

4.机器学习简介第5课

4.1外推

4.2从头开始的随机森林

5.其他

1. 第1部分回顾(第1课和第2课)

在我们深入学习本课程的下一课之前,让我们快速回顾一下前两课的内容。

  • 数据探索和预处理:探索“Blue Book for Bulldozers”数据集,估算缺失值并将分类变量转换为机器学习模型接受的数字列。我们还使用fastai库中的date_part函数从日期列创建了多个特征。
  • 构建随机森林模型并创建验证集:我们实施了随机森林并计算了训练集上的分数。为了确保模型不会过度拟合,创建了验证集。此外,我们调整参数以改善模型的性能。
  • Bagging简介:Bagging的概念在第二个视频中介绍。我们还可以看到一棵树,它可以更好地理解随机森林的运作方式。

我们将继续在本文中使用相同的数据集。我们将了解数据集中的不同变量,以及如何构建随机森林模型以进行有价值的解释。

好吧,现在是时候启动我们的Jupyter Notebook并直接进入第3课了!

2.机器学习简介:第3课 

您可以在此处访问本课程的笔记。此笔记将用于本视频中涵盖的所有三个课程。您可以在视频中观看整个课程(或者只是向下滚动并立即开始实施)。

注意:Jeremy Howard定期提供各种tips,可以更有效地解决某个问题,正如我们在上一篇文章中所看到的那样。该视频中有提到关于如何处理非常大的数据集。我已经在本文的最后一部分中提到了这一点,因此我们可以首先关注这个话题。

让我们从第2课结束时的地方继续。我们使用日期列创建了新特征,并处理了分类列。我们将加载处理的数据集,其中包括我们的新工程化特征和log saleprice  变量(因为评价指标是RMSLE):

#importing necessary libraries
%load_ext autoreload
%autoreload 2
%matplotlib inline

from fastai.imports import *
from fastai.structured import *
from pandas_summary import DataFrameSummary
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from IPython.display import display
from sklearn import metrics

#loading preprocessed file
PATH = "data/bulldozers/"

df_raw = pd.read_feather('tmp/bulldozers-raw')
df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')

我们将定义在整个实施过程中经常使用的必要特征。

#creating a validation set

def split_vals(a,n): return a[:n], a[n:]
n_valid = 12000
n_trn = len(df_trn)-n_valid
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
raw_train, raw_valid = split_vals(df_raw, n_trn)

#define function to calculate rmse and print score
def rmse(x,y): return math.sqrt(((x-y)**2).mean())

def print_score(m):
   res = [rmse(m.predict(X_train), y_train), rmse(m.predict(X_valid), y_valid),
               m.score(X_train, y_train), m.score(X_valid, y_valid)]
   if hasattr(m, 'oob_score_'): res.append(m.oob_score_)
   print(res)

下一步将是实施随机森林模型并解释结果以更好地理解我们的数据集。到目前为止,我们已经了解到随机森林是许多树木的组合,每个树木都在不同的数据点和特征子集上进行训练。每个树都尽可能不同,从数据集中捕获唯一关系。我们通过在每个树中运行每一行并获取叶节点处的值的平均值来进行预测。该平均值被视为该行的最终预测。

在解释结果时,过程必须是交互式的,并且运行时间较短。为了实现这一点,我们将在代码中进行两处更改(与我们在上一篇文章中实现的相比):

1.获取数据的子集:

set_rf_samples(50000)

我们只使用样本,因为使用整个数据需要很长时间才能运行。这里需要注意的一件重要事情是样品不应该很小。这可能最终会产生不同的结果,这将对我们的整个项目产生不利影响。样本量为50,000的效果很好。

#building a random forest model

m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)1

2.并行预测

以前,我们使用每个树对每行进行预测,然后我们计算结果的平均值和标准偏差。

%time preds = np.stack([t.predict(X_valid) for t in m.estimators_])
np.mean(preds[:,0]), np.std(preds[:,0])

CPU times: user 1.38 s, sys: 20 ms, total: 1.4 s
Wall time: 1.4 s

您可能已经注意到这是按顺序方式工作的。相反,我们可以并行调用多个树上的预测函数!这可以使用fast.ai库中的parallel_trees函数来实现  。

def get_preds(t):return t.predict(X_valid)
%time preds = np.stack(parallel_trees(m,get_preds))
np.mean(preds [:,0]),np.std(preds [:,0])

这里花的时间少了,结果完全一样!我们现在将创建数据的副本,以便我们所做的任何更改都不会影响原始数据集。

x = raw_valid.copy()

一旦我们得到预测,我们就可以计算RMSLE来确定模型的执行情况。但最终值并不能帮助我们确定预测值与特定行的接近程度,也不能确定预测值是否正确。在这种情况下,我们将查看行的标准偏差。

如果行与训练集中存在的行不同,则每棵树将给出不同的值作为预测。因此,这意味着标准偏差将很高。另一方面,对于与训练集中存在的行非常相似的行,树将进行几乎相似的预测,即标准偏差将是低的。因此,根据标准偏差的值,我们可以决定我们对预测的置信度。

让我们保存这些预测和标准偏差:

x ['pred_std'] = np.std(preds,axis = 0)
x ['pred'] = np.mean(preds,axis = 0

基于树差异的置信度


现在,让我们从数据集中获取一个变量,并对其分布进行可视化,并了解它实际代表的内容。我们将从Enclosure变量开始  。

 1.确定变量Enclosure中存在的每个类别的值计数

x.Enclosure.value_counts()plot.barh。()

2.对于每个类别,下面是的平均值saleprice,预测和标准偏差。

flds = ['Enclosure''SalePrice''pred''pred_std']
enc_summ = x [flds] .groupby('Enclosure',as_index = False).mean()
enc_summ

 

实际销售价格和预测值在三个类别中几乎相似 - “EROPS”,“EROPS w AC”,“OROPS”(其余具有空值)。由于这些空值列不添加任何额外的信息,我们将会把它们和可视化salesprice绘图和预测:

enc_summ = enc_summ [~pd.isnull(enc_summ.SalePrice)]
enc_summ.plot('Enclosure''pred''barh',xerr ='pred_std',alpha = 0.6,xlim =(0,11));

请注意,小黑条表示标准偏差。以同样的方式,让我们看另一个变量 - ProductSize

#the value count for each category
raw_valid.ProductSize.value_counts().plot.barh();

#category wise mean for sale price, prediction and standard deviation
flds = ['ProductSize', 'SalePrice', 'pred', 'pred_std']
summ = x[flds].groupby(flds[0]).mean()
summ

我们将采用标准偏差值与预测总和的比率,以比较哪个类别具有更高的偏差。

(summ.pred_std/summ.pred).sort_values(ascending=False)



ProductSize
Large             0.034871
Compact           0.034297
Small             0.030545
Large / Medium    0.027799
Medium            0.026928
Mini              0.026247
dtype: float64

“Large”和“Compact”类别的标准偏差更高。为什么这么做?在阅读之前花一点时间思考答案。

查看ProductSize中每个类别的值的条形图 找到原因了吗?对于这两个类别,我们的行数较少。因此,该模型对这些变量的预测准确度相对较差。

使用这些信息,我们可以说我们对 mini,medium 和 medium / large产品尺寸的预测更有信心,对small,compact 和large 产品的预测更少。

特征重要性


特征重要性是机器学习模型的关键之一。了解哪个变量对模型贡献最大对于解释结果至关重要。这是数据科学家在构建需要向非技术利益相关者解释的模型时所追求的目标。

我们的数据集具有多个特征,通常很难理解哪个特征占主导地位。这是随机森林的特征重要性功能非常有用的地方。让我们看看我们当前模型的十大最重要的特征(包括根据它们的重要性对它们进行可视化):

fi = rf_feat_importance(m, df_trn)
fi[:10]

fi.plot('cols''imp',figsize =(10,6),legend = False);

这是一个非常直观的图。这是前30个特征的可视化条形图:

def plot_fifi):
return fi.plot('cols''imp''barh',figsize =(12,7),legend = False)
plot_fi(FI [:30]);

显然,  YearMade是最重要的特征,其次是Coupler_System。 大多数特征似乎在最终模型中没有多大意义。让我们通过删除这些特征并检查这是否会影响模型的性能来验证此声明。

因此,我们将仅使用特征重要性大于0.005的要素构建随机森林模型:

to_keep = fi [fi.imp> 0.005] .cols
LEN(to_keep)


24


df_keep = df_trn [to_keep] .copy()
X_train,X_valid = split_vals(df_keep,n_trn)


m = RandomForestRegressor(n_estimators = 40,min_samples_leaf = 3,max_features = 0.5,
n_jobs = -1,oob_score = True)
m.fit(X_train,y_train)
print_score(M)


[0.20685390156773095,0.24454842802383558,0.91015213846294174,0.89319840835270514,0.8942078920004991]

考虑一下,删除冗余列不应该降低模型分数,对吧?在这种情况下,模型性能略有提高。我们之前删除的一些特征可能与其他特征高度共线,因此删除它们不会对模型产生负面影响。让我们再次检查特征重要性以验证我们的假设:

fi = rf_feat_importance(m,df_keep)
plot_fi(FI)

YearMade和C oupler_System  变量的特征重要性之间的差异更为显着。从删除的要素列表中,某些要素与YearMade高度共线导致它们之间的要素重要性分布。

在删除这些特征后,我们可以看到YearMadeCouplerSystem的重要性之间的差异比之前的绘图有所增加。以下是实际计算特征重要性的详细说明:

  • 考虑所有列计算r平方:假设在这种情况下它是0.89
  • 现在随机给出任何一列的值,比如YearMade,是与其他列没有关系的目标变量
  • 再次计算r平方: r平方下降到0.8。这表明  YearMade  变量是一个重要的特性
  • 拿另一个变量,比如Enclosure,然后随意洗牌
  • 计算 r 平方:  现在让我们说r-square将是0.84。这表明变量很重要,但比 YearMade变量要小

3.机器学习简介:第4课

在本课程中,杰里米·霍华德在介绍一些重要概念(如“热编码”,“树状图”和“部分依赖”)之前,先简要概述了第3课。以下是讲座的YouTube视频。

单热编码


在本系列的第一篇文章中,我们了解到许多机器学习模型无法处理分类变量。使用proc_df,我们将分类变量转换为数字列。例如,我们有一个变量UsageBand,它有三个级别 - “High”,“Low”和“Medium”。我们用数字(0,1,2)简化地替换了这些类别。

有更简单的方法吗?有!

我们可以为每个类别创建单独的列,而不是将这些类别转换为数字。UsageBand列可以替换为三列:

  • UsageBand_low
  • UsageBand_medium
  • UsageBand_high

其中每个都有1和0作为值。这称为单热编码。

当有超过3个类别时会发生什么?如果我们超过10个怎么办?我们举一个例子来理解这一点。

假设我们在数据集中有一个列' zip_code ',它对每一行都有唯一的值。在这里使用单热编码对模型没有好处,最终会增加运行时间(一个双输的场景)。

在fast.ai中使用proc_df,我们可以通过传递参数max_n_cat来执行单热编码。在这里,我们设置了max_n_cat = 7,这意味着不会对级别大于7的变量(例如邮政编码)进行编码,而所有其他变量都将进行单热编码。

df_trn2,y_trn,nas = proc_df(df_raw,'SalePrice',max_n_cat = 7)
X_train,X_valid = split_vals(df_trn2,n_trn)
m = RandomForestRegressor(n_estimators = 40,min_samples_leaf = 3,
     max_features = 0.6,n_jobs = -1,oob_score = True)
m.fit(X_train,y_train)
print_score(M)


[0.2132925755978791,0.25212838463780185,0.90966193351324276,0.88647501408921581,0.89194147155121262]

这有助于确定特定列中的特定级别是否重要。由于我们已经为分类变量分隔了每个级别,因此绘制特征重要性将向我们展示它们之间的比较:

fi = rf_feat_importance(m,df_trn2)
fi[:25]

早些时候,  YearMade是数据集中最重要的特征,但EROPS w AC在上图中具有更高的特征重要性。好奇这个变量是什么?不用担心,我们将在下一节讨论EROPS w AC实际代表的内容。

删除冗余特征


到目前为止,我们已经了解到,具有大量功能会影响模型的性能,并且难以解释结果。在本节中,我们将了解如何识别冗余功能并将其从数据中删除。

我们将使用聚类分析,更具体地说是层次聚类来识别类似的变量。在这种技术中,我们会查看每个对象,并根据要素确定哪些对象最接近。然后将这些变量替换为它们的中点。为了更好地理解这一点,让我们看看我们的数据集的集群图:

from scipy.cluster import hierarchy as hc
corr = np.round(scipy.stats.spearmanr(df_keep).correlation, 4)
corr_condensed = hc.distance.squareform(1-corr)
z = hc.linkage(corr_condensed, method='average')
fig = plt.figure(figsize=(16,10))
dendrogram = hc.dendrogram(z, labels=df_keep.columns,
    orientation='left', leaf_font_size=16)
plt.show()

从上面的树形图中,我们可以看到变量SaleYearSaleElapsed彼此非常相似,并且往往代表相同的东西。同样,Grouser_TracksHydraulics_FlowCoupler_System  高度相关。同样的情况有ProductGroupProductGroupDescfiBaseModelfiModelDesc。我们将逐个删除这些特征,并了解它如何影响模型性能。

首先,我们定义一个函数来计算Out of Bag(OOB)得分(以避免重复相同的代码行):

#define function to calculate oob score
def get_oob(df):
  m = RandomForestRegressor(n_estimators=30, min_samples_leaf=5, max_features=0.6, n_jobs=-1, oob_score=True)
  x, _ = split_vals(df, n_trn)
  m.fit(x, y_train)
  return m.oob_score_

为了便于比较,下面是删除任何特征之前的原始OOB分数:

get_oob(df_keep)
0.89019425494301454

我们现在将一次删除一个变量并计算得分:

for c in'saleYear''saleElapsed''fiModelDesc''fiBaseModel''Grouser_Tracks''Coupler_System'):
  print(c,get_oob(df_keep.drop(c,axis = 1)))


saleYear 0.889037446375
saleElapsed 0.886210803445
fiModelDesc 0.888540591321
fiBaseModel 0.88893958239
Grouser_Tracks 0.890385236272
Coupler_System 0.889601052658

这并没有严重影响OOB得分。现在让我们从每对中删除一个变量并检查总分:

to_drop = ['saleYear''fiBaseModel''Grouser_Tracks']
get_oob(df_keep.drop(to_drop,axis = 1))
0.88858458047200739

得分从0.8901变为0.8885。我们将在完整数据集上使用这些选定的特征,并查看我们的模型如何执行:

df_keep.drop(to_drop,axis = 1,inplace = True)
X_train,X_valid = split_vals(df_keep,n_trn)
reset_rf_samples()

m = RandomForestRegressor(n_estimators = 40,min_samples_leaf = 3,max_features = 0.5,n_jobs = -1,oob_score = True)
m.fit(X_train,y_train)
print_score(M)


[0.12615142089579687,0.22781819082173235,0.96677727309424211,0.90731173105384466,0.9084359846323049]

从原始数据框中删除这些变量后,模型的分数在验证集上变为0.907。

部分依赖


我将在这里介绍另一种技术,它有可能帮助我们更好地理解数据。这种技术称为部分依赖性,它用于了解特征与目标变量的关系。

from pdpbox import pdp
from plotnine import *

set_rf_samples(50000)

df_trn2, y_trn, nas = proc_df(df_raw, 'SalePrice', max_n_cat=7)
X_train, X_valid = split_vals(df_trn2, n_trn)
m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.6, n_jobs=-1)
m.fit(X_train, y_train);

plot_fi(rf_feat_importance(m, df_trn2)[:10]);

 让我们比较YearMadeSalePrice。如果您为YearMadeSaleElapsed创建散点图,您会注意到在year 1000时建造了很多的车辆,这实际上是不可能的。

df_raw.plot('YearMade', 'saleElapsed', 'scatter', alpha=0.01, figsize=(10,8));

这些可能是最初缺失的已被1000替换的值。为了保持实用性,我们将重点关注YearMade变量大于1930的值,并使用流行的ggplot包创建绘图。

x_all = get_sample(df_raw [df_raw.YearMade> 1930],500)
ggplot(x_all,aes('YearMade''SalePrice'))+ stat_smooth(se = True,method ='loess'

该图显示,最近制造的车辆的销售价格较高,除了1991年至1997年之间的一次下降。这种下降可能有多种原因 - 经济衰退,客户首选的价格较低的车辆,或其他一些外部因素。为了理解这一点,我们将创建一个图表,显示YearMadeSalePrice之间的关系,因为所有其他特征值都是相同的。

x = get_sample(X_train [X_train.YearMade> 1930],500def plot_pdpfeatclusters = Nonefeat_name = None):
   feat_name = feat_name或feat
   p = pdp.pdp_isolate(m,x,feat)
   return pdp.pdp_plot(p,feat_name,plot_lines = True,cluster = clusters不是None,n_cluster_centers = clusters)

plot_pdp( 'YearMade'

该图是通过将每行的YearMade固定为1960年,然后是1961年等来获得的。简单地说,我们一组行和计算SalePrice每一行时YearMade是1960年。然后,我们再取整集,并计算SalePrice通过设置YearMade到1962年。我们重复这个多次,这会导致我们在上图中看到的多蓝线。深黑色线代表平均值。这证实了我们的假设,即最近生产的车辆的销售价格上涨。

同样,您可以检查其他特征,如SaleElapsed,或YearMadeSaleElpased。对Enclosure下的类别执行相同的步骤(因为Enclosure_EROPS w AC被证明是最重要的特征之一),结果图如下所示:

plot_pdp(['Enclosure_EROPS w AC''Enclosure_EROPS''Enclosure_OROPS'],5,'Enclosure'

Enclosure_EROPS w AC与其他两个变量(具有几乎相等的值)相比似乎具有更高的销售价格。那么EROPS在世界上是什么?它是一个封闭的侧翻保护结构,可以带或不带AC。显然,带有AC的EROPS将具有更高的销售价格。

Tree Interpreter


TreeInterpreter 在另一个有趣的可以分析数据集中的每一行的技术中。到目前为止,我们已经看到如何解释模型,以及每个特征(以及每个分类特征中的级别)如何影响模型预测。因此,我们现在将使用此TreeInterpreter概念并可视化特定行的预测。

让我们导入TreeInterpreter库并评估验证集中第一行的结果。

from treeinterpreter import treeinterpreter as ti
df_train, df_valid = split_vals(df_raw[df_keep.columns], n_trn)
row = X_valid.values[None,0]
row


array([[4364751, 2300944, 665, 172, 1.0, 1999, 3726.0, 3, 3232, 1111, 0, 63, 0, 5, 17, 35, 4, 4, 0, 1, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 19, 29, 3, 2, 1, 0, 0, 0, 0, 0, 2010, 9, 37,
       16, 3, 259, False, False, False, False, False, False, 7912, False, False]], dtype=object)

这些是验证集中第一行(以及它的每一列)的原始值。使用TreeInterpreter,我们将使用随机森林模型对其进行预测。TreeInterpreter给出了三个结果 - 预测,偏差和贡献。

  • 预测是随机森林模型预测的值
  • 偏差是完整数据集的目标变量的平均值
  • 贡献是每列更改预测值的量

Coupler_System 的值<0.5从10.189增加值到10.345,enclosure的值<0.2从10.345减少到9.955,等等。因此,贡献将代表预测值的这种变化。要以更好的方式理解这一点,请查看下表:

在此表中,我们已针对每个要素和拆分点存储值(从上图中验证)。改变的是拆分前后的值之间的差异。这些是使用Excel中的瀑布图绘制的。这里看到的变化是针对一棵树。随机森林中所有树木的平均变化由TreeInterpreter中的 贡献 给出。

打印验证集中第一行的预测和偏差:

prediction, bias, contributions = ti.predict(m, row)
prediction[0], bias[0]

第一行数据集中每个要素的贡献值:

idxs = np.argsort(contributions[0])
[o for o in zip(df_keep.columns[idxs], df_valid.iloc[0][idxs], contributions[0][idxs])]


[('ProductSize', 'Mini', -0.54680742853695008),
('age', 11, -0.12507089451852943),
('fiProductClassDesc',
 'Hydraulic Excavator, Track - 3.0 to 4.0 Metric Tons',
 -0.11143111128570773),
('fiModelDesc', 'KX1212', -0.065155113754146801),
('fiSecondaryDesc', nan, -0.055237427792181749),
('Enclosure', 'EROPS', -0.050467175593900217),
('fiModelDescriptor', nan, -0.042354676935508852),
('saleElapsed', 7912, -0.019642242073500914),
('saleDay', 16, -0.012812993479652724),
('Tire_Size', nan, -0.0029687660942271598),
('SalesID', 4364751, -0.0010443985823001434),
('saleDayofyear', 259, -0.00086540581130196688),
('Drive_System', nan, 0.0015385818526195915),
('Hydraulics', 'Standard', 0.0022411701338458821),
('state', 'Ohio', 0.0037587658190299409),
('ProductGroupDesc', 'Track Excavators', 0.0067688906745931197),
('ProductGroup', 'TEX', 0.014654732626326661),
('MachineID', 2300944, 0.015578052196894499),
('Hydraulics_Flow', nan, 0.028973749866174004),
('ModelID', 665, 0.038307429579276284),
('Coupler_System', nan, 0.052509808150765114),
('YearMade', 1999, 0.071829996446492878)]

注意:如果您正在与本文同时观看视频,则值可能会有所不同。这是因为最初的值是根据提供错误信息的索引进行排序的。这在后来的视频中以及我们在整个课程中一直关注的笔记本中得到了纠正。

4.机器学习简介:第5课

在这个阶段你应该对随机森林算法有很好的理解。在第5课中,我们将重点关注如何识别模型是否正常化。Jeremy Howard还使用瀑布图(我们已经在上一课中介绍过,所以不会详细阐述这一点)来讨论TreeInterpreter,贡献和理解它们。  视频的主要焦点是外推和理解我们如何从头开始构建随机森林算法。

外推


如果模型建立在跨越四年的数据上,然后用于预测下一年的值,那么模型可能表现不佳。换句话说,该模型没有进行外推。我们之前已经看到,训练分数和验证分数之间存在显着差异,这可能是因为我们的验证集由一组最近的数据点组成(并且模型使用时间相关变量进行预测)。

此外,验证分数比OOB分数差 ,不应该是这种情况,对吧?该系列的第1部分详细解释了OOB得分。解决这个问题的一种方法是直接处理时间相关的变量。

为了确定哪些变量是时间相关的,我们将创建一个随机森林模型,试图预测特定行是否在验证集中。然后我们将检查哪个变量在进行成功预测方面贡献最大。

定义目标变量:

df_ext = df_keep.copy()
df_ext ['is_valid'] = 1
df_ext.is_valid [:n_trn] = 0
x,y,nas = proc_df(df_ext,'is_valid')

m = RandomForestClassifier(n_estimators = 40,min_samples_leaf = 3,max_features = 0.5,n_jobs = -1,oob_score = True)
m.fit(x,y);
m.oob_score_


0.99998753505765037

该模型能够将训练和验证集分开为r平方值0.99998,最重要的特征是SaleIDSaleElapsedMachineID。

fi = rf_feat_importance(m,x)
fi[:10]

  • SaleID当然不是随机标识符,理想情况下应该是递增顺序
  • 看起来MachineID具有相同的趋势,并且能够分离训练集和验证集
  • SaleElapsed  是数据集中第一个日期的天数。由于我们的验证集具有完整数据中的最新值,因此SaleElapsed在此集合中会更高。为了证实这个假设,这里是列车和测试中三个变量的分布:
feats=['SalesID', 'saleElapsed', 'MachineID']
(X_train[feats]/1000).describe()

(X_valid[feats]/1000).describe()

 

从上表中可以明显看出,这三个变量的平均值是显着不同的。我们将丢弃这些变量,再次适应随机森林并检查特征重要性:

x.drop(feats, axis=1, inplace=True)
m = RandomForestClassifier(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
m.fit(x, y);
m.oob_score_


0.9789018385789966


fi = rf_feat_importance(m, x)
fi[:10]

虽然这些变量具有明显的时间依赖,但它们对于进行预测也很重要。在我们删除这些变量之前,我们需要检查它们如何影响OOB分数。计算样本中的初始OOB分数以进行比较:

set_rf_samples(50000)
feats = ['SalesID''saleElapsed''MachineID''age''YearMade''saleDayofyear']
X_train , X_valid  =  split_vals (df_keep , n_trn )
m  =  RandomForestRegressor (n_estimators = 40 , min_samples_leaf = 3 , max_features = 0.5 , n_jobs =  - 1 , oob_score = True )
m 。fit (X_train , y_train )
print_score (m )


[0.21136509778791376,0.2493668921196425,0.90909393040946562,0.88894821098056087,0.89255408392415925]

逐个删除每个特征:

for f in feats:

  df_subs = df_keep.drop(f, axis=1)
  X_train, X_valid = split_vals(df_subs, n_trn)
  m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
  m.fit(X_train, y_train)
  print(f)
  print_score(m)


SalesID
0.20918653475938534, 0.2459966629213187, 0.9053273181678706, 0.89192968797265737, 0.89245205174299469]

saleElapsed
[0.2194124612957369, 0.2546442621643524, 0.90358104739129086, 0.8841980790762114, 0.88681881032219145]

MachineID
[0.206612984511148, 0.24446409479358033, 0.90312476862123559, 0.89327205732490311, 0.89501553584754967]

age
[0.21317740718919814, 0.2471719147150774, 0.90260198977488226, 0.89089460707372525, 0.89185129799503315]

YearMade
[0.21305398932040326, 0.2534570148977216, 0.90555219348567462, 0.88527538596974953, 0.89158854973045432]

saleDayofyear
[0.21320711524847227, 0.24629839782893828, 0.90881970943169987, 0.89166441133215968, 0.89272793857941679]

看看结果,年龄,MachineIDSaleDayofYear实际上提高了分数,而其他人则没有。因此,我们将删除剩余的变量,并将随机林拟合到完整的数据集上。

reset_rf_samples()
df_subs = df_keep.drop(['SalesID', 'MachineID', 'saleDayofyear'],axis=1)
X_train, X_valid = split_vals(df_subs, n_trn)
m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)


[0.1418970082803121, 0.21779153679471935, 0.96040441863389681, 0.91529091848161925, 0.90918594039522138]

删除时间因变量后,验证分数(0.915)现在优于OOB分数(0.909)。我们现在可以在max_features上使用其他参数,例如n_estimator。为了创建最终模型,Jeremy将树的数量增加到160,结果如下:

m = RandomForestRegressor(n_estimators=160, max_features=0.5, n_jobs=-1, oob_score=True)
%time m.fit(X_train, y_train)
print_score(m)


CPU times: user 6min 3s, sys: 2.75 s, total: 6min 6s
Wall time: 16.7 s
[0.08104912951128229, 0.2109679613161783, 0.9865755186304942, 0.92051576728916762, 0.9143700001430598]

验证得分为0.92,而RMSE降至0.21。确实有很大改进!

从零开始的随机森林


我们已经了解了随机森林模型如何实际运作,如何选择特征以及最终如何进行预测。在本节中,我们将从absolute scratch创建自己的随机森林模型。以下是本节的笔记:从头开始的随机森林

我们将从导入基本库开始:

%load_ext autoreload
%autoreload 2
%matplotlib inline

from fastai.imports import *
from fastai.structured import *
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier

from IPython.display import display
from sklearn import metrics

我们将只使用两个变量。一旦我们确信模型适用于这些选定的变量,我们就可以使用完整的功能集。

PATH = "data/bulldozers/"

df_raw = pd.read_feather('tmp/bulldozers-raw')
df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')
def split_vals(a,n): return a[:n], a[n:]

n_valid = 12000
n_trn = len(df_trn)-n_valid

X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
raw_train, raw_valid = split_vals(df_raw, n_trn)
x_sub = X_train[['YearMade', 'MachineHoursCurrentMeter']]

我们已经加载了数据集,将其拆分为训练集和验证集,并选择了两个特征-  YearMadeMachineHoursCurrentMeter。 从头开始构建任何模型时要考虑的第一件事是 - 我们需要哪些信息?因此,对于随机森林,我们需要:

  • 一组特征 - x
  • 目标变量 - y
  • 随机森林中的树木数量 - n_trees
  • 用于定义样本大小的变量 - sample_sz
  • 最小叶子大小的变量 - min_leaf
  • 用于测试的随机种子

让我们用上面提到的输入定义一个类,并将随机种子设置为42。

class TreeEnsemble():
   def __init__(self, x, y, n_trees, sample_sz, min_leaf=5):
       np.random.seed(42)
       self.x,self.y,self.sample_sz,self.min_leaf = x,y,sample_sz,min_leaf
       self.trees = [self.create_tree() for i in range(n_trees)]

   def create_tree(self):
       rnd_idxs = np.random.permutation(len(self.y))[:self.sample_sz]
       return DecisionTree(self.x.iloc[rnd_idxs], self.y[rnd_idxs], min_leaf=self.min_leaf)
       
   def predict(self, x):
       return np.mean([t.predict(x) for t in self.trees], axis=0)

我们创建了一个函数create_trees,它将被调用为分配给n_trees的数字的次数。函数create_trees生成一组随机混乱的行​​(size = sample_sz)并返回DecisionTree。我们将在一段时间内看到DecisionTree,但首先让我们弄清楚如何创建和保存预测。

我们之前了解到,在随机森林模型中,每个树对每行进行预测,并通过获取所有预测的平均值来计算最终预测。因此,我们将创建一个预测函数,其中.predict 用于每个树以创建预测列表,此列表的平均值计算为我们的最终值。

最后一步是创建DecisionTree。我们首先选择一个给出最小错误的特征和分裂点。目前,此代码仅适用于单一决策。如果代码成功运行,我们可以使这个递归。

class DecisionTree():
   def __init__(self, x, y, idxs=None, min_leaf=5):
       if idxs is None: idxs=np.arange(len(y))
       self.x,self.y,self.idxs,self.min_leaf = x,y,idxs,min_leaf
       self.n,self.c = len(idxs), x.shape[1]
       self.val = np.mean(y[idxs])
       self.score = float('inf')
       self.find_varsplit()
       
   # This just does one decision; we'll make it recursive later
   def find_varsplit(self):
       for i in range(self.c): self.find_better_split(i)
           
   # We'll write this later!
   def find_better_split(self, var_idx): pass
   
   @property
   def split_name(self): return self.x.columns[self.var_idx]
   
   @property
   def split_col(self): return self.x.values[self.idxs,self.var_idx]

   @property
   def is_leaf(self): return self.score == float('inf')
   
   def __repr__(self):
       s = f'n: {self.n}; val:{self.val}'
       if not self.is_leaf:
           s += f'; score:{self.score}; split:{self.split}; var:{self.split_name}'
       return s

self.n定义每个树中使用的行数,self.c是列数。Self.val计算每个索引的预测平均值。此代码仍然不完整,将在下一课中继续。是的,第3部分即将推出!

5.其他

在几秒钟内读取大型数据集:如果我们在读取文件本身时提供变量的数据类型,则加载数据集的时间会减少。使用具有超过1亿行的此数据集来查看此操作。

types = {'id': 'int64',
       'item_nbr': 'int32',
       'store_nbr': 'int8',
       'unit_sales': 'float32',
       'onpromotion': 'object'}
%%time
df_test = pd.read_csv(f'{PATH}test.csv', parse_dates = ['date'], dtype=types, infer_datetime_format=True)


CPU times: user 1min 41s, sys: 5.08s, total: 1min 46s
Wall time: 1min 48s

基数:这是分类变量中的级别数。对于UsageBand变量,我们有三个级别 - high,low和medium。因此基数是3。

训练验证测试:在我们在测试集上使用模型之前,有一个验证集来检查模型的性能是很重要的。我们经常会在验证集上过度拟合我们的模型。如果验证集不是测试集的真实代表,那么模型也将失败。因此,完整的数据应分为训练,验证和测试集,其中测试集应仅在最后使用(而不是在参数调整期间)。

交叉验证:交叉验证集创建多个验证集并在每个验证集上测试模型。完整的数据被混洗并分成组,例如5。其中四组用于训练模型,一组用作验证集。在下一次迭代中,另外四个用于训练,一个用于验证。此步骤将重复五次,其中每组用作验证集一次。

小结

我认为这是这个正在进行的系列中最重要的文章之一。我不能强调模型可解释性的重要性。在现实生活中的行业场景中,您经常会遇到必须向利益相关者(通常是非技术人员)解释模型结果的情况。

获得模型批准的机会在于您能够解释模型的行为方式和原因。另外,总是一个好主意总是以外行人能理解的方式向自己解释任何模特的表现 - 这总是一个好习惯!


参考链接:https://www.analyticsvidhya.com/blog/2018/10/interpret-random-forest-model-machine-learning-programmers/

| 1
登录后可评论,马上登录吧~
评论 ( 0 )

还没有人评论...

相关推荐