Press "Enter" to skip to content

解读随机森林

随机森林算法的综合指南及其解释方法

Sergei A在Unsplash上的照片

现在关于大型语言模型的炒作很多,但这并不意味着老派的机器学习方法现在就应该被淘汰。如果你给ChatGPT一组数百个数值特征的数据集,并要求它预测一个目标值,我怀疑它是否能够提供帮助。

对于非结构化数据(例如文本、图像或音频),神经网络通常是最佳解决方案。但是,对于表格数据,我们仍然可以从传统的随机森林算法中获益。

随机森林算法最重要的优势包括:

  • 只需要进行少量数据预处理。
  • 随机森林算法很难出错。如果你的集合中有足够多的树,增加更多的树会降低误差,因此你不会遇到过拟合问题。
  • 结果易于解释。

因此,在处理表格数据时,随机森林算法可能是你开展新任务的第一个模型选择。

在本文中,我将介绍随机森林的基础知识,并介绍解释模型结果的方法。

我们将学习如何找到以下问题的答案:

  • 哪些特征是重要的,哪些是多余的并可以移除?
  • 每个特征值如何影响我们的目标指标?
  • 每个预测的因素是什么?
  • 如何估计每个预测的置信度?

预处理

我们将使用 葡萄酒品质数据集。它显示了葡萄牙“Vinho Verde”葡萄酒不同品种的品质与理化性质测试之间的关系。我们将尝试基于葡萄酒特征预测葡萄酒品质。

使用决策树时,我们不需要进行大量预处理:

  • 我们不需要创建虚拟变量,因为算法可以自动处理。
  • 我们不需要进行归一化或去除异常值,因为只有顺序才重要。因此,基于决策树的模型对异常值具有抵抗力。

然而,scikit-learn库中的决策树实现不能处理分类变量或缺失值。因此,我们必须自己处理。

幸运的是,我们的数据集中没有缺失值。

df.isna().sum().sum()0

我们只需要将type变量(’red’或’white’)从string转换为integer。我们可以使用pandas的Categorical转换来实现。

categories = {}  cat_columns = ['type']for p in cat_columns:    df[p] = pd.Categorical(df[p])        categories[p] = df[p].cat.categoriesdf[cat_columns] = df[cat_columns].apply(lambda x: x.cat.codes)print(categories){'type': Index(['red', 'white'], dtype='object')}

现在,df['type']的值为0表示红葡萄酒,为1表示白葡萄酒。

预处理的另一个关键部分是将数据集分割为训练集和验证集。因此,我们可以使用验证集来评估我们模型的质量。

import sklearn.model_selectiontrain_df, val_df = sklearn.model_selection.train_test_split(df,     test_size=0.2) train_X, train_y = train_df.drop(['quality'], axis = 1), train_df.qualityval_X, val_y = val_df.drop(['quality'], axis = 1), val_df.qualityprint(train_X.shape, val_X.shape)(5197, 12) (1300, 12)

我们已完成预处理步骤,准备进入最令人兴奋的部分——训练模型。

决策树的基础知识

在开始培训之前,让我们花些时间了解随机森林是如何工作的。

随机森林是一组决策树的集成模型。因此,我们应该从基本构建单元开始——决策树。

在我们预测葡萄酒质量的例子中,我们将解决回归任务,因此让我们从它开始。

决策树:回归

让我们拟合一个默认的决策树模型。

import sklearn.treeimport graphvizmodel = sklearn.tree.DecisionTreeRegressor(max_depth=3)# 我们将最大深度限制在可视化目的上model.fit(train_X, train_y)

决策树最显著的优势之一是我们可以轻松解释这些模型——它只是一组问题。让我们将其可视化。

dot_data = sklearn.tree.export_graphviz(model, out_file=None,                                       feature_names = train_X.columns,                                       filled = True) graph = graphviz.Source(dot_data) # 将树保存为png文件png_bytes = graph.pipe(format='png')with open('decision_tree.png','wb') as f:    f.write(png_bytes)
Graph by author

正如你所看到的,决策树由二分法构成。在每个节点上,我们将数据集拆分成两部分。

最后,我们将叶节点的预测计算为此节点中所有数据点的平均值。

附注:由于决策树对于叶节点返回所有数据点的平均值,决策树在外推方面表现较差。因此,在训练和推理过程中,您需要注意特征分布。

让我们一起思考如何确定数据集的最佳拆分。我们可以从一个变量开始,为其定义最佳划分。

假设我们有一个具有四个唯一值的特征:1、2、3和4。那么,在它们之间有三个可能的阈值。

Graph by author

我们可以逐个阈值计算数据的预测值,作为叶节点的平均值。然后,我们可以使用这些预测值计算每个阈值的MSE(均方误差)。最佳拆分将是MSE最低的拆分。默认情况下,scikit-learn的DecisionTreeRegressor以类似的方式工作,并使用MSE作为准则。

让我们手动计算sulphates特征的最佳拆分,以更好地了解其工作原理。

def get_binary_split_for_param(param, X, y):    uniq_vals = list(sorted(X[param].unique()))        tmp_data = []        for i in range(1, len(uniq_vals)):        threshold = 0.5 * (uniq_vals[i-1] + uniq_vals[i])                 # split dataset by threshold        split_left = y[X[param] <= threshold]        split_right = y[X[param] > threshold]                # calculate predicted values for each split        pred_left = split_left.mean()        pred_right = split_right.mean()        num_left = split_left.shape[0]        num_right = split_right.shape[0]        mse_left = ((split_left - pred_left) * (split_left - pred_left)).mean()        mse_right = ((split_right - pred_right) * (split_right - pred_right)).mean()        mse = mse_left * num_left / (num_left + num_right) \            + mse_right * num_right / (num_left + num_right)        tmp_data.append(            {                'param': param,                'threshold': threshold,                'mse': mse            }        )                return pd.DataFrame(tmp_data).sort_values('mse')get_binary_split_for_param('sulphates', train_X, train_y).head(5)| param     |   threshold |      mse ||:----------|------------:|---------:|| sulphates |       0.685 | 0.758495 || sulphates |       0.675 | 0.758794 || sulphates |       0.705 | 0.759065 || sulphates |       0.715 | 0.759071 || sulphates |       0.635 | 0.759495 |

我们可以看到,对于硫酸盐来说,最佳阈值是0.685,因为它给出了最低的均方误差。

现在,我们可以对我们拥有的所有特征使用这个函数来定义最佳的整体分割。

def get_binary_split(X, y):    tmp_dfs = []    for param in X.columns:        tmp_dfs.append(get_binary_split_for_param(param, X, y))            return pd.concat(tmp_dfs).sort_values('mse')get_binary_split(train_X, train_y).head(5)| param   |   threshold |      mse ||:--------|------------:|---------:|| 酒精     |      10.625 | 0.640368 || 酒精     |      10.675 | 0.640681 || 酒精     |      10.85  | 0.641541 || 酒精     |      10.725 | 0.641576 || 酒精     |      10.775 | 0.641604 |

我们得到了与我们最初的决策树完全相同的结果,其第一次分割是在酒精 <= 10.625

要构建整个决策树,我们可以递归地计算每个数据集酒精 <= 10.625酒精 > 10.625的最佳分割,并获得下一层的决策树。然后,重复这个过程。

递归的停止条件可以是深度或叶子节点的最小大小。这是一个至少包含420个项目的叶子节点的决策树的示例。

model = sklearn.tree.DecisionTreeRegressor(min_samples_leaf = 420)
作者提供的图表

让我们计算在验证集上的平均绝对误差,以了解我们的模型有多好。我更喜欢使用平均绝对误差(MAE)而不是均方误差(MSE),因为它对离群值的影响较小。

import sklearn.metricsprint(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))0.5890557338155006

决策树:分类

我们已经看过回归的例子。在分类的情况下,有些不同。尽管我们不会在本文中深入讨论分类的例子,但讨论它的基础知识仍然很值得。

对于分类,我们使用最常见的类别作为每个叶子节点的预测值,而不是平均值。

我们通常使用基尼系数来估计分类的二元分割质量。想象一下从样本中随机取出一个项目,然后再取出另一个项目。基尼系数等于两个项目来自不同类别的情况的概率。

假设我们只有两个类别,并且来自第一个类别的项目的比例等于p。那么我们可以使用以下公式计算基尼系数:

解读随机森林 四海 第5张

如果我们的分类模型完全正确,基尼系数等于0。在最坏的情况下(p = 0.5),基尼系数等于0.5。

为了计算二元分割的度量值,我们计算左右部分(左部和右部)的基尼系数,并根据每个分区中的样本数量进行归一化。

解读随机森林 四海 第6张

然后,我们可以类似地计算不同阈值的优化度量,并选择最佳选项。

我们已经训练了一个简单的决策树模型并讨论了它的工作原理。现在,我们准备进行随机森林的学习。

随机森林

随机森林基于装袋(Bagging)的概念。其思想是拟合一组独立的模型,并使用它们的平均预测结果。由于模型是独立的,误差不相关。我们假设我们的模型没有系统性误差,并且许多误差的平均值应该接近于零。

如何获得大量独立模型?非常简单:我们可以在随机的行和特征子集上训练决策树。这将是一个随机森林。

让我们训练一个基本的有100棵树的随机森林,叶节点的最小大小为100。

import sklearn.ensemble
import sklearn.metrics

model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)
model.fit(train_X, train_y)
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408

使用随机森林,我们取得了比一个决策树更好的质量:0.5592对比0.5891。

过拟合

有意义的问题是随机森林是否会过拟合。

实际上不会。由于我们在添加更多树时会取得不相关的误差平均值,因此无法通过添加更多树来过拟合模型。随着树的数量增加,质量将渐近改善。

作者提供的图表

然而,如果您拥有深层树且数目不够多,您可能会遇到过拟合问题。一个决策树容易过拟合。

袋外错误

由于随机森林中每棵树仅使用部分行,我们可以用它们来估计错误。对于每一行数据,我们只选取没有使用该行的树来进行预测,并根据这些预测计算错误。这种方法被称为“袋外错误(Out-of-bag error)”。

可以看到袋外错误与验证集的错误相比更接近,这意味着它是一个很好的近似值。

# 我们需要设置oob_score = True来计算OOB误差
model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100, oob_score=True)
model.fit(train_X, train_y)

# 验证集误差
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408

# 训练集误差
print(sklearn.metrics.mean_absolute_error(model.predict(train_X), train_y))
0.5430398596179975

# 袋外误差
print(sklearn.metrics.mean_absolute_error(model.oob_prediction_, train_y))
0.5571191870008492

解读模型

正如我在开始时提到的,决策树的一大优势是易于解释。让我们试着更好地理解我们的模型。

特征重要性

计算特征重要性非常直接。我们查看集合中的每棵决策树和每个二分拆分,然后计算它对我们的度量指标(本例中为平方误差)的影响。

让我们看一下初始决策树中基于“alcohol”进行的第一个拆分。

解读随机森林 四海 第8张

解读随机森林 四海 第9张

然后,我们可以对所有决策树中的所有二分拆分进行相同的计算,将所有结果加起来,归一化并得到每个特征的相对重要性。

如果您使用scikit-learn,您无需手动计算特征重要性。您只需使用model.feature_importances_即可。

def plot_feature_importance(model, names, threshold=None):
    feature_importance_df = pd.DataFrame.from_dict({'feature_importance': model.feature_importances_,
                                                    'feature': names})\
            .set_index('feature').sort_values('feature_importance', ascending=False)
    if threshold is not None:
        feature_importance_df = feature_importance_df[feature_importance_df.feature_importance > threshold]
    fig = px.bar(
        feature_importance_df,
        text_auto='.2f',
        labels={'value': 'feature importance'},
        title='Feature importances'
    )
    fig.update_layout(showlegend=False)
    fig.show()

plot_feature_importance(model, train_X.columns)

我们可以看到,总体上最重要的特征是酒精挥发性酸度

作者绘制的图表

部分依赖性

了解每个特征如何影响我们的目标度量是令人兴奋且常常有用的。例如,随着酒精含量的增加/减少,品质是否提高或存在更复杂的关系。

我们可以从数据集中获取数据,按酒精的平均值绘制图表,但这样做是不正确的,因为可能存在一些相关性。例如,我们数据集中的更高酒精含量可能也对应着更高的糖分和更好的品质。

为了只估计酒精的影响,我们可以取数据集中的所有行,使用机器学习模型,预测每一行在不同酒精值(9、9.1、9.2等)下的品质。然后,我们可以对结果进行平均处理,得到酒精水平和葡萄酒品质之间的实际关系。因此,数据都是相等的,我们只是在变化酒精水平。

这种方法可以用于任何机器学习模型,不仅限于随机森林。

我们可以使用sklearn.inspection模块轻松地绘制这些关系。

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X,     range(12))

我们可以从这些图表中获得很多有见地的信息,例如:

  • 随着游离二氧化硫的增长,葡萄酒的品质提高,直到达到30,此后保持稳定;
  • 酒精的水平越高,品质越好。

解读随机森林 四海 第11张

我们甚至可以观察两个变量之间的关系。这可能会非常复杂。例如,如果酒精水平高于11.5,挥发性酸度就没有影响。但是,对于较低的酒精水平,挥发性酸度对品质有显著影响。

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X,     [(1, 10)])

解读随机森林 四海 第12张

预测的置信度

使用随机森林,我们还可以评估每个预测的置信度。为此,我们可以计算集成中每棵树的预测,并查看方差或标准差。

val_df['predictions_mean'] = np.stack([dt.predict(val_X.values)   for dt in model.estimators_]).mean(axis = 0)val_df['predictions_std'] = np.stack([dt.predict(val_X.values)   for dt in model.estimators_]).std(axis = 0)ax = val_df.predictions_std.hist(bins = 10)ax.set_title('预测标准差的分布')

我们可以看到,有些预测的标准差较低(即小于0.15),而其他的std则大于0.3。

解读随机森林 四海 第13张

如果我们将模型用于商业目的,我们可以对这种情况进行不同的处理。例如,如果std大于X,可以不考虑该预测结果,或者向客户展示区间(例如百分位数25%和75%)。

预测是如何进行的?

我们还可以使用treeinterpreterwaterfallcharts包来了解每个预测是如何进行的。在某些商业案例中,这可能非常方便,例如当您需要告诉客户为什么他们的信用被拒绝时。

我们将以一种葡萄酒作为例子。它的酒精含量相对较低,挥发性酸度较高。

解读随机森林 四海 第14张

from treeinterpreter import treeinterpreterfrom waterfall_chart import plot as waterfallrow = val_X.iloc[[7]]prediction, bias, contributions = treeinterpreter.predict(model, row.values)waterfall(val_X.columns, contributions[0], threshold=0.03,           rotation_value=45, formatting='{:,.3f}');

图表显示该葡萄酒的品质优于平均水平。增加品质的主要因素是挥发性酸度较低,而主要缺点是酒精含量较低。

图表作者

因此,有很多方便的工具可以帮助你更好地理解数据和模型。

减少树的数量

Random Forest的另一个很酷的功能是,我们可以将其用于减少任何表格数据的特征数量。您可以快速拟合一个随机森林,为您的数据定义一个有意义的列列表。

更多的数据并不意味着更好的质量。而且,在训练和推理过程中,它可能会影响模型的性能。

由于我们最初的葡萄酒数据集只有12个特征,所以在这个案例中,我们将使用稍大一些的数据集——Online News Popularity。

查看特征重要性

首先,让我们构建一个随机森林并查看特征的重要性。59个特征中有34个重要性低于0.01。

让我们尝试去除它们并查看准确性。

low_impact_features = feature_importance_df[feature_importance_df.feature_importance <= 0.01].index.valuestrain_X_imp = train_X.drop(low_impact_features, axis = 1)val_X_imp = val_X.drop(low_impact_features, axis = 1)model_imp = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)model_imp.fit(train_X_sm, train_y)
  • 所有特征验证集上的MAE:2969.73
  • 25个重要特征验证集上的MAE:2975.61

品质差异并不是很大,但我们可以在训练和推理阶段使模型更快速。我们已经删除了初始特征的近60% – 做得好。

查看冗余特征

对于剩下的特征,让我们看看是否存在冗余(高度相关)特征。为此,我们将使用Fast.AI工具:

import fastbookfastbook.cluster_columns(train_X_imp)

解读随机森林 四海 第16张

我们可以看到以下特征彼此之间非常接近:

  • self_reference_avg_sharessself_reference_max_shares
  • kw_min_avgkw_min_max
  • n_non_stop_unique_tokensn_unique_tokens

让我们也移除它们。

non_uniq_features = ['self_reference_max_shares', 'kw_min_max',   'n_unique_tokens']train_X_imp_uniq = train_X_imp.drop(non_uniq_features, axis = 1)val_X_imp_uniq = val_X_imp.drop(non_uniq_features, axis = 1)model_imp_uniq = sklearn.ensemble.RandomForestRegressor(100,   min_samples_leaf=100)model_imp_uniq.fit(train_X_imp_uniq, train_y)sklearn.metrics.mean_absolute_error(model_imp_uniq.predict(val_X_imp_uniq),   val_y)2974.853274034488

品质稍微有所改善。因此,我们将功能从59个减少到22个,仅增加了0.17%的错误率。这证明了这种方法的有效性。

您可以在GitHub上找到完整的代码。

摘要

在本文中,我们讨论了决策树和随机森林算法的工作原理。同时,我们学习了如何解读随机森林:

  • 如何利用特征重要性获取最重要特征的列表,并减少模型中的参数数量。
  • 如何使用部分依赖来定义每个特征值对目标指标的影响。
  • 如何使用treeinterpreter库估计不同特征对每个预测结果的影响。

非常感谢您阅读本文。我希望这对您有所启发。如果您有任何后续问题或评论,请在评论区留言。

参考资料

数据集

  • Cortez,Paulo, Cerdeira,A., Almeida,F., Matos,T., and Reis,J.. (2009). Wine Quality. UCI Machine Learning Repository. https://doi.org/10.24432/C56S3T
  • Fernandes,Kelwin, Vinagre,Pedro, Cortez,Paulo, and Sernadela,Pedro. (2015). Online News Popularity. UCI Machine Learning Repository. https://doi.org/10.24432/C5NS3V

来源

本文受到Fast.AI深度学习课程的启发

Leave a Reply

Your email address will not be published. Required fields are marked *