【机器学习】Python中随机森林的实现与解释
通过从单个决策树构建来使用和理解随机森林的指南。
幸运的是,对于像Scikit-Learn这样的库,现在很容易在Python中实现数百种机器学习算法。这很容易,我们通常不需要任何关于模型如何工作的潜在知识来使用它。虽然不需要知道所有细节,但了解机器学习模型如何在幕后工作仍然是有帮助的。这使我们可以在模型表现不佳时进行诊断,或者解释模型如何做出决策,如果我们想要说服别人相信我们的模型,这是至关重要的。
在本文中,我们将介绍如何在Python中构建和使用Random Forest。除了查看代码之外,我们还将尝试了解此模型的工作原理。因为由许多决策树组成的随机森林,我们首先要了解单个决策树如何对一个简单的问题进行分类。然后,我们将努力使用随机森林来解决现实世界的数据科学问题。本文的完整代码在GitHub上以Jupyter Notebook的形式提供。
目录
理解决策树
一个决策树是一个随机森林的基石,是一个直观的模型。我们可以将决策树视为一系列是/否问题,询问我们的数据最终导致预测类(或回归情况下的连续值)。这是一个可解释的模型,因为它可以像我们一样进行分类:在我们做出决定之前(在理想世界中),我们会询问有关可用数据的一系列查询。
决策树的技术细节是如何形成有关数据的问题。在CART算法中,通过确定问题(称为节点的分裂)来构建决策树,这些问题在被回答时导致基尼系数的最大减少。这意味着决策树试图通过在将数据干净地划分为类的特征中查找值来形成包含来自单个类的高比例样本(数据点)的节点。
我们稍后将讨论关于Gini杂质的低级细节,但首先,让我们构建一个决策树,以便我们能够在高层次上理解它。
关于简单问题的决策树
我们将从一个非常简单的二进制分类问题开始,如下所示:
我们的数据仅具有两个特征(预测变量),x1
以及x2
具有6个数据点-样品-分成2个不同的标签。虽然这个问题很简单,但它不是线性可分的,这意味着我们不能通过数据绘制一条直线来对点进行分类。
然而,我们可以绘制一系列直线,将数据点划分为多个框,我们称之为节点。实际上,这就是决策树在培训期间所做的事情。有效地,决策树是通过构造许多线性边界而构建的非线性模型。
要创建决策树并对数据进行训练(fit
),我们使用Scikit-Learn。
from sklearn.tree import DecisionTreeClassifier
# Make a decision tree and train
tree = DecisionTreeClassifier(random_state=RSEED)
tree.fit(X, y)
在训练过程中,我们为模型提供特征和标签,以便学习根据特征对点进行分类。(我们没有针对这个简单问题的测试集,但在测试时,我们只为模型提供功能并让它对标签进行预测。)
我们可以在训练数据上测试我们模型的准确性:
print(f'Model Accuracy: {tree.score(X, y)}')
Model Accuracy: 1.0
我们看到它获得了100%的准确度,这正是我们所期望的,因为我们给了它y
训练的答案()并没有限制树的深度。事实证明,完全学习训练数据的能力可能是决策树的缺点,因为它可能会导致过度拟合,我们稍后会讨论。
可视化决策树
那么当我们训练决策树时,实际上会发生什么?我找到了一种有用的方法来理解决策树是通过可视化来实现的,我们可以使用Scikit-Learn功能(有关详细信息,请查看笔记本或本文)。
除叶子节点(彩色终端节点)外,所有节点都有5个部分:
- 问题基于特征的值询问了数据。每个问题都有一个分裂节点的真或假答案。根据问题的答案,数据点向下移动。
-
gini
:节点的Gini杂质。当我们向下移动树时,平均加权基尼系数值会减少。 -
samples
:节点中的观察数。 -
value
:每个班级的样本数量。例如,顶部节点在类0中有2个样本,在类1中有4个样本。 -
class
:节点中点的多数分类。在叶节点的情况下,这是对节点中所有样本的预测。
叶节点没有问题,因为这些是最终预测的地方。要对新点进行分类,只需向下移动树,使用点的特征来回答问题,直到到达class
预测的叶节点。
为了以不同的方式查看树,我们可以在原始数据上绘制由决策树构建的分割。
每个拆分都是一条线,它根据特征值将数据点划分为节点。对于这个简单的问题并且对最大深度没有限制,划分将节点中的每个点仅放置在同一类的点上。(再次,稍后我们将看到训练数据的这种完美划分可能不是我们想要的,因为它可能导致过度拟合。)
基尼系数
在这一点上,深入了解基尼系数的概念(数学并不令人生畏!)节点的基尼系数是指节点中随机选择的样本如果被标记的错误标记的概率。节点中样本的分布。例如,在顶部(根)节点中,有44.4%的可能性错误地根据节点中的样本标签对随机选择的数据点进行分类。我们使用以下等式得出这个值:
节点的Gini系数n
是1减去J
每个类的p_i
平方的所有类的总和(对于二进制分类任务,这是2)。这可能有点令人困惑,所以让我们计算出根节点的基尼系数。
在每个节点处,决策树在要素中搜索要拆分的值,从而最大限度地减少基尼系数。(拆分节点的替代方法是使用信息增益,相关概念)。
然后,它以贪婪的递归过程重复此拆分过程,直到达到最大深度,或者每个节点仅包含来自一个类的样本。每层树木的加权总基尼系数必须减少。在树的第二层,总加权基尼系数值为0.333:
(每个节点的Gini Impurity由来自该节点中父节点的点的分数加权。)您可以继续为每个节点计算Gini杂质(检查可视化的答案)。在一些基本的数学中,出现了一个强大的模型!
最终,最后一层的加权总Gini杂质变为0意味着每个节点都是纯粹的,并且从该节点随机选择的点不会被错误分类。虽然这似乎是积极的,但这意味着模型可能过度拟合,因为节点仅使用训练数据构建。
过度拟合:或者为什么森林比一棵树更好
您可能会想问为什么不只使用一个决策树?它似乎是完美的分类器,因为它没有犯任何错误!要记住的关键点是树在训练数据上没有犯错。我们预计会出现这种情况,因为我们给树提供了答案,并没有限制最大深度(级别数)。机器学习模型的目标是很好地概括它以前从未见过的新数据。
当我们具有非常灵活的模型(模型具有高容量)时,过度拟合发生,其基本上通过紧密拟合来记忆训练数据。问题是模型不仅学习训练数据中的实际关系,还学习任何存在的噪声。据说灵活模型具有高方差,因为学习参数(例如决策树的结构)将随着训练数据而显着变化。
另一方面,据说一个不灵活的模型具有较高的偏差,因为它对训练数据做出了假设(它偏向于预先设想的数据思想。)例如,线性分类器假设数据是线性的,不具备适应非线性关系的灵活性。一个不灵活的模型可能无法适应训练数据,在这两种情况下 - 高方差和高偏差 - 模型无法很好地推广到新数据。
创建一个非常灵活的模型来记忆训练数据与不能学习训练数据的不灵活模型之间的平衡称为偏差 - 方差权衡,是机器学习的基本概念。
当我们不限制最大深度时决策树容易过度拟合的原因是因为它具有无限的灵活性,这意味着它可以保持增长,直到它为每个单独的观察只有一个叶节点,完美地对它们进行分类。如果您返回决策树的图像并将最大深度限制为2(仅进行一次拆分),则分类不再100%正确。我们减少了决策树的方差,但代价是增加了偏差。
作为限制树深度的替代方案,它可以减少方差(好)并增加偏差(差),我们可以将许多决策树组合成一个称为随机森林的单一集合模型。
随机森林
在随机森林是许多决策树组成的模型。这个模型不是简单地平均树的预测(我们可以称之为“森林”),而是使用两个关键概念,使其名称为随机:
- 在构建树时对训练数据点进行随机抽样
- 分割节点时考虑的随机特征子集
随机抽样训练观察
在训练时,随机森林中的每棵树都会从数据点的随机样本中学习。样本用替换绘制,称为自举,这意味着一些样本将在一棵树中多次使用。这个想法是通过对不同样本上的每棵树进行训练,尽管每棵树相对于特定训练数据集可能具有高度差异,但总体而言,整个森林将具有较低的方差,但不会以增加偏差为代价。
在测试时,通过平均每个决策树的预测来进行预测。这种在不同的数据引导子集上训练每个学习者然后对预测求平均值的过程称为套袋,是引导聚合的缩写。
用于拆分节点的随机特征子集
随机林中的另一个主要概念是,只考虑所有特征的子集来分割每个决策树中的每个节点。通常,这被设置为sqrt(n_features)
用于分类,这意味着如果存在16个特征,则在每个树中的每个节点处,将仅考虑4个随机特征来分割节点。(随机林也可以考虑每个节点的所有特征,如回归中常见的那样。这些选项可以在Scikit-Learn Random Forest实现中控制)。
如果你能理解一个决策树,装袋的想法,以及随机的特征子集,那么你对随机森林的工作方式有了很好的理解:
随机森林将数百或数千个决策树组合在一起,在略微不同的观察集上训练每个决策树,考虑到有限数量的特征,在每棵树中分割节点。随机森林的最终预测是通过平均每个树的预测来做出的。
要理解为什么随机森林优于单一决策树,请想象以下情况:您必须决定特斯拉股票是否会上涨,并且您可以访问十几位对该公司没有先验知识的分析师。每个分析师都有较低的偏见,因为他们没有任何假设,并且可以从新闻报道的数据集中学习。
这似乎是一个理想的情况,但问题是报告除了真实的信号外可能还包含噪音。因为分析师完全根据数据做出预测 - 他们具有很高的灵活性 - 他们可能会被无关的信息所左右。分析师可能会从同一数据集中得出不同的预测。此外,如果给出不同的报告培训集,每个分析师的差异很大,并且会得出截然不同的预测。
解决方案是不依赖于任何一个人,而是汇集每个分析师的投票。此外,与随机森林一样,允许每个分析人员仅访问报告的一部分,并希望通过采样取消噪声信息的影响。在现实生活中,我们依赖于多种来源(从不信任亚马逊的单独评论),因此,决策树不仅直观,而且在随机森林中将它们组合在一起的想法也是如此。
实践中的随机森林
接下来,我们将使用Scikit-Learn在Python中构建一个随机林。我们将使用分为训练和测试集的真实数据集,而不是学习一个简单的问题。我们使用测试集作为模型对新数据的执行方式的估计,这也可以让我们确定模型过度拟合的程度。
数据集
我们要解决的问题是二元分类任务,目的是预测个人的健康状况。这些特征是个人的社会经济和生活方式特征,标签是0
健康状况不佳和1
身体健康。此数据集是由收集中心疾病控制和预防,是可以在这里找到。
通常,80%的数据科学项目用于清理,探索和制作数据中的功能。但是,对于本文,我们将坚持使用建模。(有关其他步骤的详细信息,请参阅本文)。
这是一个不平衡的分类问题,因此准确性不是一个合适的指标。相反,我们将测量曲线下的接收器工作特性区域(ROC AUC),从0(最差)到1(最佳)的度量,随机猜测得分为0.5。我们还可以绘制ROC曲线以评估模型。
该笔记本包含了决策树和随机森林都实现,但在这里我们只专注于随机森林。在读取数据后,我们可以实例化和训练随机森林如下:
from sklearn.ensemble import RandomForestClassifier
# Create the model with 100 trees
model = RandomForestClassifier(n_estimators=100,
bootstrap = True,
max_features = 'sqrt')
# Fit on training data
model.fit(train, train_labels)
在训练几分钟后,模型准备好对测试数据进行如下预测:
# Actual class predictions
rf_predictions = model.predict(test)
# Probabilities for each class
rf_probs = model.predict_proba(test)[:, 1]
我们进行类预测(predict
)以及预测概率(predict_proba
)来计算ROC AUC。一旦我们有了测试预测,我们就可以计算出ROC AUC。
from sklearn.metrics import roc_auc_score
# Calculate roc auc
roc_value = roc_auc_score(test_labels, rf_probs)
结果
随机森林的最终测试ROC AUC为0.87,而具有无限最大深度的单一决策树的最终测试ROC AUC为0.67。如果我们查看训练分数,两个模型都达到了1.0 ROC AUC,这也是预期的,因为我们给这些模型提供了训练答案,并没有限制每棵树的最大深度。
虽然随机森林过度拟合(在训练数据上比在测试数据上做得更好),但它能够比单一决策树更好地推广测试数据。随机森林具有较低的方差(良好),同时保持决策树的相同低偏差(也良好)。
我们还可以绘制单个决策树(顶部)和随机森林(底部)的ROC曲线。顶部和左侧的曲线是更好的模型:
随机森林明显优于单一决策树。
我们可以采用的模型的另一个诊断措施是绘制测试预测的混淆矩阵(有关详细信息,请参阅笔记本):
这显示了模型在左上角和右下角的正确预测以及模型在左下角和右上角错过的预测。我们可以使用这些图来诊断我们的模型,并确定它是否表现良好,可以投入生产。
功能重要性
随机林中的要素重要性表示在该要素上拆分的所有节点上Gini系数减少的总和。我们可以使用这些来尝试找出随机森林认为最重要的预测变量。可以从训练有素的随机森林中提取要素重要性,并将其放入Pandas数据框中,如下所示:
import pandas as pd
# Extract feature importances
fi = pd.DataFrame({'feature': list(train.columns),
'importance': model.feature_importances_}).\
sort_values('importance', ascending = False)
# Display
fi.head()
feature importance
DIFFWALK 0.036200
QLACTLM2 0.030694
EMPLOY1 0.024156
DIFFALON 0.022699
USEEQUIP 0.016922
通过告诉我们哪些变量在类之间最具辨别力,特征重要性可以让我们洞察问题。例如,这里DIFFWALK,
指示患者是否行走困难,是在问题环境中有意义的最重要的特征。
通过构建最重要的附加功能,可以将要素重要性用于要素工程。我们还可以通过删除低重要性功能,使用要素重要性来选择要素。
在森林中可视化树
最后,我们可以在森林中可视化单个决策树。这次,我们必须限制树的深度,否则它将太大而无法转换为图像。为了制作下图,我将最大深度限制为6.这仍然导致我们无法完全解析的大树!然而,鉴于我们深入研究决策树,我们掌握了模型的工作原理。
随机森林中的单一决策树。
下一步
进一步的步骤是使用RandomizedSearchCV
Scikit-Learn中的随机搜索来优化随机森林。优化是指在给定数据集上找到模型的最佳超参数。最佳超参数将在数据集之间变化,因此我们必须在每个数据集上单独执行优化(也称为模型调整)。
我喜欢将模型调整视为寻找机器学习算法的最佳设置。我们可以在随机林中优化的示例包括决策树的数量,每个决策树的最大深度,考虑拆分每个节点的最大特征数,以及叶节点中所需的最大数据点数。
有关随机森林模型优化的随机搜索的实现,请参阅Jupyter Notebook。
完整的运行示例
下面的代码是使用repl.it创建的,它提供了Python中随机林的完整交互式运行示例。随意运行和更改代码(加载包可能需要一些时间)。
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
RSEED = 50
# Load in data
df = pd.read_csv('https://s3.amazonaws.com/projects-rf/clean_data.csv')
# Full dataset: https://www.kaggle.com/cdc/behavioral-risk-factor-surveillance-system
# Extract the labels
labels = np.array(df.pop('label'))
# 30% examples in test data
train, test, train_labels, test_labels = train_test_split(df,
labels,
stratify = labels,
test_size = 0.3,
random_state = RSEED)
# Imputation of missing values
train = train.fillna(train.mean())
test = test.fillna(test.mean())
# Features for feature importances
features = list(train.columns)
# Create the model with 100 trees
model = RandomForestClassifier(n_estimators=100,
random_state=RSEED,
max_features = 'sqrt',
n_jobs=-1, verbose = 1)
# Fit on training data
model.fit(train, train_labels)
n_nodes = []
max_depths = []
# Stats about the trees in random forest
for ind_tree in model.estimators_:
n_nodes.append(ind_tree.tree_.node_count)
max_depths.append(ind_tree.tree_.max_depth)
print(f'Average number of nodes {int(np.mean(n_nodes))}')
print(f'Average maximum depth {int(np.mean(max_depths))}')
# Training predictions (to demonstrate overfitting)
train_rf_predictions = model.predict(train)
train_rf_probs = model.predict_proba(train)[:, 1]
# Testing predictions (to determine performance)
rf_predictions = model.predict(test)
rf_probs = model.predict_proba(test)[:, 1]
from sklearn.metrics import precision_score, recall_score, roc_auc_score, roc_curve
import matplotlib.pyplot as plt
# Plot formatting
plt.style.use('fivethirtyeight')
plt.rcParams['font.size'] = 18
def evaluate_model(predictions, probs, train_predictions, train_probs):
"""Compare machine learning model to baseline performance.
Computes statistics and shows ROC curve."""
baseline = {}
baseline['recall'] = recall_score(test_labels,
[1 for _ in range(len(test_labels))])
baseline['precision'] = precision_score(test_labels,
[1 for _ in range(len(test_labels))])
baseline['roc'] = 0.5
results = {}
results['recall'] = recall_score(test_labels, predictions)
results['precision'] = precision_score(test_labels, predictions)
results['roc'] = roc_auc_score(test_labels, probs)
train_results = {}
train_results['recall'] = recall_score(train_labels, train_predictions)
train_results['precision'] = precision_score(train_labels, train_predictions)
train_results['roc'] = roc_auc_score(train_labels, train_probs)
for metric in ['recall', 'precision', 'roc']:
print(f'{metric.capitalize()} Baseline: {round(baseline[metric], 2)} Test: {round(results[metric], 2)} Train: {round(train_results[metric], 2)}')
# Calculate false positive rates and true positive rates
base_fpr, base_tpr, _ = roc_curve(test_labels, [1 for _ in range(len(test_labels))])
model_fpr, model_tpr, _ = roc_curve(test_labels, probs)
plt.figure(figsize = (8, 6))
plt.rcParams['font.size'] = 16
# Plot both curves
plt.plot(base_fpr, base_tpr, 'b', label = 'baseline')
plt.plot(model_fpr, model_tpr, 'r', label = 'model')
plt.legend();
plt.xlabel('False Positive Rate');
plt.ylabel('True Positive Rate'); plt.title('ROC Curves');
plt.show();
evaluate_model(rf_predictions, rf_probs, train_rf_predictions, train_rf_probs)
plt.savefig('roc_auc_curve.png')
from sklearn.metrics import confusion_matrix
import itertools
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Oranges):
"""
This function prints and plots the confusion matrix.
Normalization can be applied by setting `normalize=True`.
Source: http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html
"""
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
else:
print('Confusion matrix, without normalization')
print(cm)
# Plot the confusion matrix
plt.figure(figsize = (10, 10))
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title, size = 24)
plt.colorbar(aspect=4)
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45, size = 14)
plt.yticks(tick_marks, classes, size = 14)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
# Labeling the plot
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt), fontsize = 20,
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.grid(None)
plt.tight_layout()
plt.ylabel('True label', size = 18)
plt.xlabel('Predicted label', size = 18)
# Confusion matrix
cm = confusion_matrix(test_labels, rf_predictions)
plot_confusion_matrix(cm, classes = ['Poor Health', 'Good Health'],
title = 'Health Confusion Matrix')
plt.savefig('cm.png')
结论
虽然我们可以在Python中构建功能强大的机器学习模型而不了解它们,但我发现了解幕后发生的事情会更有效。在本文中,我们不仅在Python中构建和使用了随机林,而且我们还从基础开始了解了该模型。
我们首先查看了一个单独的决策树,一个随机森林的构建块,然后看到我们如何通过在一个称为随机森林的集合模型中组合数百个决策树来克服单个决策树的高方差。随机森林使用观察的随机抽样,特征的随机抽样和平均预测的概念。
从这篇文章中理解的关键概念是:
- 决策树:一种直观的模型,可根据询问有关特征值的一系列问题做出决策。具有低偏差和高差异导致训练数据过度拟合。
- Gini Impurity:决策树在拆分每个节点时尝试最小化的度量。表示根据节点中样本的分布对来自节点的随机选择的样本进行错误分类的概率。
- 引导:用替换对随机观察组进行采样。
- 随机子集的特征:在考虑决策树中每个节点的分割时选择一组随机特征。
- 随机森林:由许多决策树组成的集合模型,使用自举,随机特征子集和平均投票来进行预测。这是套袋整体的一个例子。
- 偏差方差权衡:机器学习中的核心问题,描述高灵活性(高方差)模型之间的平衡,以不能推广到新数据的成本非常好地学习训练数据,以及不灵活的模型(高偏见),无法学习训练数据。随机森林减少了单个决策树的方差,从而可以更好地预测新数据。
希望本文为您提供了开始在项目中使用随机林所需的信心和理解。随机森林是一种强大的机器学习模型,但这不应该阻止我们知道它是如何工作的。我们对模型的了解越多,我们就越有能力有效地使用它并解释它如何进行预测。