随机森林是一种基于决策树集成的强大的机器学习模型,每棵树都是使用数据集的随机子集进行生长。模型的最终预测基于森林中树的预测的多数投票(用于分类)或平均(用于回归)。

多个决策树预测的平均值减少了模型的方差,但会稍微增加偏差。这通常大大提高了最终模型的性能。
img
随机森林模型

现在让我们开始吧 😃

背景:Bagging方法

随机森林的训练基于对决策树应用的boostrap aggregating(或bagging)的一般技术。

给定一个训练集的n个样本{(xᵢ, yᵢ)},i = 1,…,n,bagging重复地从训练集中有放回地随机选择样本,并将同一个基本模型(在我们的情况下是决策树)拟合到这些样本中。

集成中的树的数量和用于训练每个树的样本数量是模型的超参数。通常使用几百到几千棵树,具体取决于训练集的大小和性质。

从Bagging到随机森林

随机森林包括另一种类型的bagging方案:在决策树的每个节点搜索最佳分割时,它们只考虑一个随机特征子集。这个过程进一步减少了森林中树之间的相关性,从而降低了模型的方差并提高了整体性能。

通常,在具有m个特征的数据集中,每次分割只考虑√m(m的平方根)个特征。在实践中,这个超参数应该与模型的其他超参数(如树的数量和用于训练它们的样本大小)一起调整。

Scikit-Learn中的随机森林

Scikit-Learn提供了两个实现随机森林模型的类:

  1. RandomForestClassifier 用于分类问题。

  2. RandomForestRegressor 用于回归问题。

除了决策树的标准超参数(如criterionmax_depth),这些类还提供以下超参数:

  1. n_estimators - 森林中树的数量(默认为100)。

  2. max_features - 在每个节点搜索最佳分割时要考虑的特征数量。选项是指定一个整数表示特征数量,一个浮点数表示要使用的特征比例,‘sqrt’(默认值)表示使用特征的平方根,'log2’表示使用特征的log2,None表示使用所有特征。

  3. max_samples - 从训练集中抽取用于训练每棵树的样本数量。选项是指定一个整数表示样本数量,一个浮点数表示要使用的训练样本的比例,None(默认值)表示使用所有训练样本。

此外,为了加快训练速度,您可以使用参数n_jobs并行训练树,该参数指定用于训练的CPU核心数(默认值为1)。如果n_jobs = -1,则将使用机器上所有可用的核心进行训练。

RandomForestClassifier

例如,让我们使用Iris数据集的前两个特征(花萼宽度和花萼长度)来拟合一个随机森林分类器。在之前的决策树文章中,我们已经看到一个完全生长的树在这个数据集上可以达到65.79%的测试准确率,而修剪的决策树(max_depth = 3)获得了76.32%的测试准确率。让我们看看随机森林是否可以做得更好。

首先加载数据集:

从sklearn库中导入load_iris函数
iris = load_iris() # 加载鸢尾花数据集
X = iris.data[:, :2] # 只取前两个特征
y = iris.target # 目标变量

然后我们将其分成训练集和测试集:

from sklearn.model_selection import train_test_split

# 导入train_test_split函数用于将数据集划分为训练集和测试集

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 使用train_test_split函数将数据集X和标签y划分为训练集和测试集
# X_train为训练集特征数据,X_test为测试集特征数据
# y_train为训练集标签数据,y_test为测试集标签数据
# random_state参数用于设置随机种子,保证每次运行代码时划分结果一致

让我们现在使用默认设置(即使用完全生长的树)创建一个RandomForestClassifier模型,并将其拟合到训练集中:

from sklearn.ensemble import RandomForestClassifier

# 导入随机森林分类器模型

clf = RandomForestClassifier(random_state=42, n_jobs=-1)
# 创建一个随机森林分类器对象,设置随机种子为42,n_jobs=-1表示使用所有的CPU核心进行并行计算

clf.fit(X_train, y_train)
# 使用训练数据集X_train和对应的标签y_train来训练随机森林分类器模型

与DecisionTreeClassifier类似,我们固定随机森林分类器的随机状态,以便允许结果的可重复性。

该模型在训练集和测试集上的表现为:

# 打印训练集准确率
print(f'Training set accuracy: {clf.score(X_train, y_train):.4f}')

# 打印测试集准确率
print(f'Test set accuracy: {clf.score(X_test, y_test):.4f}')

# 训练集准确率为0.9554
Train accuracy: 0.9554

# 测试集准确率为0.7895
Test accuracy: 0.7895

测试准确率仅通过将决策树替换为随机森林从76.32%提高到78.95%。

让我们在一些随机森林的超参数上运行随机搜索,以找到更好的模型:

from sklearn.model_selection import RandomizedSearchCV

params = {
    'n_estimators': [10, 50, 100, 200, 500],  # 决策树的数量
    'max_depth': np.arange(3, 11),  # 决策树的最大深度范围
    'max_samples': np.arange(0.5, 1.0, 0.1),  # 每棵决策树的样本比例范围
    'max_features': ['sqrt', 'log2', None]  # 每棵决策树的特征选择方式
}

search = RandomizedSearchCV(RandomForestClassifier(random_state=42), params, n_iter=50, cv=3, n_jobs=-1)  # 随机搜索交叉验证
search.fit(X_train, y_train)  # 拟合训练数据

print(search.best_params_)  # 输出最佳参数

搜索找到的最佳模型是:

# 定义一个字典,包含了模型的超参数
params = {'n_estimators': 500, 'min_samples_leaf': 1, 'max_samples': 0.7999999999999999, 'max_features': 'log2'}

# 打印超参数
print(params)

输出结果:

{'n_estimators': 500, 'min_samples_leaf': 1, 'max_samples': 0.7999999999999999, 'max_features': 'log2'}

它的性能是:

# 将最佳分类器赋值给best_clf变量
best_clf = search.best_estimator_

# 输出训练集上的准确率
print(f'Train accuracy: {best_clf.score(X_train, y_train):.4f}')

# 输出测试集上的准确率
print(f'Test accuracy: {best_clf.score(X_test, y_test):.4f}')

# 输出结果
# Train accuracy: 0.8214
# Test accuracy: 0.8158

测试准确率进一步提高了,从78.95%增加到81.58%!

让我们来可视化这个随机森林的决策边界:
img
随机森林在鸢尾花数据集上的决策边界

可以看出,随机森林能够找到比独立决策树更复杂的边界。

RandomForestRegressor

现在我们将为加利福尼亚住房数据集构建一个随机森林回归器。在这个数据集中,目标是根据该区域(住宅区)的8个不同特征(如中位数收入或每户人口的平均房间数)来预测加利福尼亚某个区域(住宅区)的中位房价。

我们首先获取数据集:

from sklearn.datasets import fetch_california_housing

# 导入fetch_california_housing函数,用于获取加利福尼亚房屋数据集

data = fetch_california_housing()
# 使用fetch_california_housing函数获取加利福尼亚房屋数据集,并将结果赋值给data变量

X, y = data.data, data.target
# 将数据集中的特征数据赋值给X变量,将目标数据赋值给y变量

feature_names = data.feature_names
# 将数据集中的特征名称赋值给feature_names变量

让我们将数据集分为80%的训练集和20%的测试集:

# 导入sklearn中的train_test_split函数
from sklearn.model_selection import train_test_split

# 使用train_test_split函数将数据集X和标签y分为训练集和测试集,其中测试集占比为0.2,随机种子为0
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

接下来,我们使用默认设置将RandomForestRegressor拟合到训练集中:

从sklearn库中导入RandomForestRegressor类

reg = RandomForestRegressor(random_state=0)  # 创建一个随机森林回归器对象,设置随机种子为0

reg.fit(X_train, y_train)  # 使用训练数据X_train和目标数据y_train来训练随机森林回归器模型

这个模型的R²分数为:

# 创建一个线性回归模型
reg = LinearRegression()

# 准备数据
X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]])
y = np.dot(X, np.array([1, 2])) + 3

# 将数据分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)

# 使用训练集训练模型
reg.fit(X_train, y_train)

# 使用训练好的模型对训练集和测试集进行预测,并计算R2得分
train_score = reg.score(X_train, y_train)
print(f'R2得分(训练集):{train_score:.4f}')

test_score = reg.score(X_test, y_test)
print(f'R2得分(测试集):{test_score:.4f}')

# 输出结果
# R2得分(训练集):0.9727
# R2得分(测试集):0.7980

这个结果比我们在这个数据集上使用单个DecisionTreeRegressor获得的结果要好得多(测试集上的R²得分为0.7188),即使没有调整随机森林的超参数。

让我们对一些随机森林的超参数进行随机搜索,看看是否可以得到一个更好的模型:

from sklearn.model_selection import RandomizedSearchCV

params = {
    'n_estimators': [10, 50, 100, 200, 500],
    'min_samples_leaf': np.arange(1, 6),
    'max_samples': np.arange(0.5, 1.0, 0.1),
    'max_features': ['sqrt', 'log2', None]    
}

search = RandomizedSearchCV(RandomForestRegressor(random_state=0), params, n_iter=50, cv=3, n_jobs=-1)
search.fit(X_train, y_train)

print(search.best_params_)

搜索找到的最佳模型是:

# 定义一个随机森林分类器
# 定义一个随机森林分类器对象,传入参数为n_estimators=500,表示森林中树的数量为500
# min_samples_leaf=1,表示叶子节点最少包含1个样本
# max_samples=0.7999999999999999,表示每棵树的样本数为总样本数的80%
# max_features='log2',表示每棵树的特征数量为总特征数量的log2
rfc = RandomForestClassifier(n_estimators=500, min_samples_leaf=1, max_samples=0.7999999999999999, max_features='log2')
# 打印随机森林分类器对象的参数
print(rfc.get_params())

在这种情况下,最好的模型使用未修剪的树(min_samples_leaf = 1)。该模型的性能为:

# 定义参数范围
param_grid = {'C': [0.1, 1, 10, 100], 'gamma': [1, 0.1, 0.01, 0.001]}
# 定义SVM模型
svm = SVC()
# 使用网格搜索寻找最佳参数
search = GridSearchCV(svm, param_grid, cv=5)
search.fit(X_train, y_train)
# 获取最佳模型
best_reg = search.best_estimator_
# 输出训练集和测试集的R2得分
print(f'R2 score (train): {best_reg.score(X_train, y_train):.4f}')
print(f'R2 score (test): {best_reg.score(X_test, y_test):.4f}')
# 输出结果
# R2 score (train): 0.9636
# R2 score (test): 0.8171

不错!测试集上的R²分数从0.798提高到0.817。

特征重要性

在解释机器学习模型时,一个重要的问题是数据集中最重要的特征是什么,以及它们如何对模型的预测做出贡献?

随机森林是少数可以用来排名数据集中特征重要性的模型之一。例如,可以使用这个排名来执行特征选择。

决策树中特征的预测能力可以基于两个属性来确定:

  1. 特征在树中的位置。位于树顶部的特征对更大比例的样本的最终预测做出贡献。因此,我们可以使用到达使用该特征的节点的样本的比例作为其重要性的估计值。

  2. 使用该特征分裂节点所实现的不纯度减少量。

在Scikit-Learn中,决策树类提供了一个名为*feature_importances_*的属性,它是基于这两个因素的预测能力的归一化估计值。

在随机森林中,我们可以对所有树中的这些估计值进行平均,从而获得更可靠的特征重要性估计值。

例如,让我们绘制加利福尼亚住房数据集中特征的重要性,这是我们的随机森林回归器发现的:

# Sort the features by their importance
# 按特征的重要性对特征进行排序
feature_importance = best_reg.feature_importances_
sorted_idx = np.argsort(feature_importance)

# Plot the feature importances
# 绘制特征重要性图
pos = np.arange(len(feature_importance))
plt.barh(pos, feature_importance[sorted_idx])
plt.yticks(pos, np.array(feature_names)[sorted_idx])

img

加利福尼亚房屋数据集中特征的重要性

该数据集中最重要的特征是MedInc(收入中位数)、AveOccup(平均家庭成员数)以及房屋位置(经度和纬度)。

随机森林的理论保证

如果我们能够为机器学习模型提供一些理论保证,并且证明其泛化误差受到某个值的限制,那将是非常理想的。不幸的是,只有少数机器学习模型能够提供这样的保证,而随机森林就是其中之一。

Breiman [1] 表明,随机森林的泛化误差可以受到森林中树的强度(预测能力)和它们之间的相关性的限制。更具体地说,
img
随机森林的泛化误差的理论界限

其中ρ是树之间的平均相关性,s衡量每棵树的预测能力。我将对这些变量的正式定义感兴趣的读者转到Breiman的论文中。

我们可以从这个不等式中得出结论,当树之间的相关性较低或它们的预测能力变强时,泛化误差的界限会减小。通常在这两个因素之间存在权衡:当我们向树中添加更多的随机性(例如,通过减少用于训练的样本大小)时,它们的相关性减弱,但预测能力也减弱。

ExtraTrees

ExtraTrees(极端随机树)在节点分割的选择方式上增加了另一层随机性。与计算每个特征的最佳切点(基于不纯度度量)不同,它从训练集中特征的范围内的均匀分布中选择一个随机切点。然后,在所有随机生成的分割中,选择产生最大不纯度减少的分割来分割节点。

这通常会进一步减小模型的方差,但会稍微增加偏差。

与随机森林类似,Scikit-Learn提供了两个实现此模型的类:

  1. ExtraTreesClassifier 用于分类问题。

  2. ExtraTreesRegressor 用于回归问题。

例如,让我们在加利福尼亚房屋数据集上使用ExtraTreesRegressor:

from sklearn.ensemble import ExtraTreesRegressor
reg = ExtraTreesRegressor(random_state=0)  # 创建一个ExtraTreesRegressor对象,设置random_state为0

reg.fit(X_train, y_train)  # 使用训练数据X_train和y_train拟合模型

train_score = reg.score(X_train, y_train)  # 使用训练数据计算R2得分
print(f'R2 score (train): {train_score:.4f}')  # 打印训练数据的R2得分

test_score = reg.score(X_test, y_test)  # 使用测试数据计算R2得分
print(f'R2 score (test): {test_score:.4f}')  # 打印测试数据的R2得分

输出结果:
R2 score (train): 1.0000
R2 score (test): 0.8091

额外树在调参之前的测试集上比随机森林回归器的R²分数稍好(0.809而不是0.798)。

概述

让我们总结一下与其他监督式机器学习模型相比,随机森林的优缺点。

优点

  • 在许多真实数据集上已知能提供高度准确的模型。

  • 通过结合多个决策树的预测,可以捕捉数据集中的复杂交互和模式。

  • 通过自动选择相关特征,可以有效处理高维数据集。

  • 与单个决策树相比,不容易过拟合。自助采样和每个节点的随机特征选择有助于减少过拟合并改善泛化能力。

  • 可以处理包括数值和分类特征在内的异构数据类型。

  • 可以处理缺失值而不需要填充。

  • 提供特征重要性的度量。

  • 集成中的树可以并行训练,因为每个决策树可以独立构建。

  • 模型的泛化误差具有理论上的界限。

  • 可应用于分类和回归任务。

缺点

  • 训练可能需要大量计算资源,特别是在处理大数据集或集成中有许多树时。

  • 比简单模型(如决策树或线性回归)的可解释性较差,因为解释模型的决策需要遵循许多树的路径。

  • 需要调整多个超参数,包括树的数量、每棵树的大小以及每次分割要考虑的特征数量。

  • 预测速度较慢,与其他模型相比,需要遍历多个树并聚合它们的预测结果。

最后说明

您可以在GitHub上找到本文的示例:https://github.com/roiyeho/medium/tree/main/random_forests

感谢阅读!

参考文献

[1] Breiman, Random Forests. Machine Learning, 45(1), 5–32, 2001.

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐