机器学习DAY4:集成学习和随机森林方法(完)
本文介绍了集成学习的概念及主要方法,包括 Bootstraping、Bagging、随机森林,随后计算随机森林中各个特征的重要性,找出对模型贡献较大的特征。
知识点
- 集成
- Bootstraping
- Bagging
- 随机森林
- 特征重要性
集成
之前的几个实验中,介绍了不同的分类算法,以及验证、评估模型的技术。现在,假设已经为某一特定问题选中了最佳的模型,想进一步提升其准确率,就需要应用一些更高级的机器学习技术:集成(Ensemble)。集成是使用一系列学习器进行学习,并使用某种规则把各个学习结果进行整合从而获得比单个学习器更好的学习效果的一种机器学习方法。在集成中,最终的整体输出比任何单个部分的表现更重要。
某种意义上,孔多塞陪审团定理形象的描述了上面提到的集成概念。该定理的内容为:如果评审团的每个成员做出独立判断,并且每个陪审员做出正确决策的概率高于 0.5,那么整个评审团做出正确的总体决策的概率随着陪审员数量的增加而增加,并趋向于一。另一方面,如果每个陪审员判断正确的概率小于 0.5,那么整个陪审团做出正确的总体决策的概率随着陪审员数量的增加而减少,并趋向于零。
该定理的公式为:

其中,
- N 为陪审员总数。
- m 是构成多数的最小值,即 m=floor(N/2)+1
是组合数。- p 为评审员做出正确决策的概率。
- μ 是整个评审团做出正确决策的概率。
由上式可知,若 p>0.5,则 μ>p。此外,若 N→∞,则 μ→1。
让我们看看另一个集成的例子:群体的智慧。1906 年,Francis Galton 访问了普利茅斯的一个农村集市,在那里他看到一个比赛。800 个参与者尝试估计一头屠宰的牛的重量。所有参与者的预测的平均值为 1197 磅,与牛的真实重量 1198 磅十分接近。
机器学习领域采用类似的思路以降低误差。
Bootstrapping
Leo Breiman 于 1994 年提出的 Bagging(又称 Bootstrap Aggregation,引导聚集)是最基本的集成技术之一。Bagging 基于统计学中的 Bootstraping(自助法),该方法令复杂模型的统计评估变得更加可行。
Bootstrap 方法的流程如下:假设有尺寸为 N 的样本 X,从该样本中有放回地随机抽取 N 个样本,以创建一个新样本。换句话说,从尺寸为 N 的原样本中随机选择一个元素,并重复此过程 N 次。选中所有元素的可能性是一样的,因此每个元素被抽中的概率均为 1/N。
假设用 Bootstrap 方法从一个袋子中抽球,每次抽一个。在每一步中,将选中的球放回袋子,这样下一次抽取是等概率的,即,从同样数量的 N 个球中抽取。注意,因为我们把球放回了,新样本中可能有重复的球。把这个新样本称为 X1。重复这一过程 M 次,创建 M 个 Bootstrap 样本 X1,…,XM。最后,我们的样本数量就从原先的 1 个扩充到了 M 个,就有充足的样本来计算原始分布的多种统计数据。
看一个例子,这个例子使用之前的 telecom_churn 数据集。我们曾讨论过这一数据集的特征重要性,其中最重要的特征之一是呼叫客服次数(Customer service calls)。可视化「呼叫客服次数」这一特征,看看其分布。
import warnings
import seaborn as sns
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = 10, 6
%matplotlib inline
warnings.filterwarnings('ignore')
telecom_data = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv')
fig = sns.kdeplot(telecom_data[telecom_data['Churn']
== False]['Customer service calls'], label='Loyal')
fig = sns.kdeplot(telecom_data[telecom_data['Churn']
== True]['Customer service calls'], label='Churn')
fig.set(xlabel='Number of calls', ylabel='Density')
plt.show()

上图表明,相比那些逐渐离网的客户,忠实客户呼叫客服的次数更少。估计每组客户的平均呼叫客服数可能很有用,但由于该数据集很小,如果直接计算原样本的均值,得到的估计可能较差。因此可以应用 Bootstrap 方法,基于原样本生成 1000 个新 Bootstrap 样本,然后计算均值的区间估计。
定义 get_bootstrap_samples() 函数产生 Bootstrap 样本,定义 stat_intervals() 函数产生一个区间估计。
def get_bootstrap_samples(data, n_samples):
indices = np.random.randint(0, len(data), (n_samples, len(data)))
samples = data[indices]
return samples
def stat_intervals(stat, alpha):
boundaries = np.percentile(
stat, [100 * alpha / 2., 100 * (1 - alpha / 2.)])
return boundaries
分割数据集,分组为忠实客户和离网客户。
loyal_calls = telecom_data.loc[telecom_data['Churn'] == False,
'Customer service calls'].values
churn_calls = telecom_data.loc[telecom_data['Churn'] == True,
'Customer service calls'].values
固定随机数种子,以得到可重现的结果。
np.random.seed(0)
使用 Bootstrap 生成样本,计算各自的均值。
loyal_mean_scores = [np.mean(sample)
for sample in get_bootstrap_samples(loyal_calls, 1000)]
churn_mean_scores = [np.mean(sample)
for sample in get_bootstrap_samples(churn_calls, 1000)]
打印区间估计值,使用 stat_intervals() 函数将区间定义为 95%。
print("Service calls from loyal: mean interval",
stat_intervals(loyal_mean_scores, 0.05))
print("Service calls from churn: mean interval",
stat_intervals(churn_mean_scores, 0.05))
上述结果表明,有 95% 的概率,忠实客户平均呼叫客服的次数在 1.4 到 1.49 之间,而离网客户平均呼叫客服的次数在 2.06 到 2.40 之间。另外,可以注意到忠实客户的区间更窄,这是合理的,因为相比多次呼叫客服的离网客户,忠实客户呼叫客服的次数更少(0、1、2 次)。
PS:
做 Bootstrap 时能得到什么?
- 核心:你可以直接用重复抽取样本的均值分布来估计不确定性。
- 比如说,你抽 1000 次,算出 1000 个均值,这 1000 个均值会形成一个分布(可能集中在 1.45 附近,但会有一定范围),再从这个分布的 2.5% 和 97.5% 分位数取值,就得到“95% 置信区间”。
- 这个区间告诉你:“根据现在手头的样本,假如真的是从这个数据生成机制多次采集,我的均值可能 95% 地会落在这个区间内”。
Bagging
理解了 Bootstrap 概念之后,接下来介绍 Bagging。
假设我们有一个训练集 X。我们使用 Bootstrap 生成样本 X1,…,XM。现在,我们在每个 Bootstrap 样本上分别训练分类器 ai(x),最终的分类器将对所有这些单独的分类器的输出取均值。在分类情形下,这种技术即投票(voting):

下图形象化解释了上面的公式:
在回归问题中,通过对回归结果取均值,Bagging 将均方误差降至 1/M(M 为回归器数量)。
回顾一下上一课的内容,模型的预测误差由三部分构成:
Bagging 通过在不同数据集上训练模型的方式来降低分类器的方差。换句话说,Bagging 可以预防过拟合。由于来自不同训练数据集上的模型各不相同,它们的误差在投票过程中会相互抵消,所以 Bagging 是有效的。此外,某些 Bootstrap 训练样本很可能会忽略异常值。
让我们看下 Bagging 的实际效果,并与决策树比较下,如下图所示:

决策树的误差:
Bagging 的误差:
![]()
从上图可以看到,相比决策树,Bagging 误差中的方差值显著降低了。
上图的例子不太可能在实际工作中出现,因为它做了一个强假定:单独误差是不相关的。对现实世界的应用而言,这个强假定太过乐观。当这个假定不成立时,误差的下降就不会那么显著。在后续实验中,我们将讨论一些更复杂的集成方法,能够对现实世界的问题做出更精确的预测。
袋外误差(OOB Error)
随机森林不需要使用交叉验证或留置样本,因为它内置了误差估计。随机森林中的决策树基于原始数据集中不同的 Bootstrap 样本构建。对第 K 棵树而言,其特定 Bootstrap 样本大约留置了 37% 的输入。
这很容易证明。设数据集中有 ℓ 个样本。在每一步,每个数据点最终出现在有放回的 Bootstrap 样本中的概率均为 1/ℓ 。Bootstrap 样本最终不包含特定数据集元素的概率(即,该元素在 ℓ 次抽取中都没抽中)等于![]()
下图可视化袋外误差(Out-of-Bag Error,OOBE)估计是如何工作的:
示意图上方为原始数据集。将其分为训练集(左图)和测试集(右图)。在训练集上,绘制一副网格,完美地实现了分类。现在,我们应用同一副网格到测试集上,以估计分类的正确率。右图可见,分类器在 4 个未曾在训练集中出现的数据点上给出了错误的答案。而测试集中共有 15 个数据点,这 15 个数据点未在训练集中出现。因此,该分类器的准确率为11/15*100
总结一下,每个基础算法在约 63% 的原始样本上训练。该算法可以在剩下的约 37% 的样本上验证。袋外估计仅仅是基础算法在 37% 样本上的平均估计。
随机森林
在 Bagging 中,主要使用的基础分类器为决策树,因为决策树相当复杂,并能在任何样本上达到零分类误差。随机子空间方法可以降低树的相关性,从而避免过拟合。基于 Bagging,基础算法会在不同的原始特征集的随机子集上训练。
PS:随机子空间方法(Random Subspace)指的是:在训练每棵树时,不仅对 样本 做随机抽样(Bagging),还对 特征 做随机抽样,只选取原始特征的一部分用于训练。
这个过程其实就是 随机森林(Random Forest) 的核心思想:
- 对样本层面做 Bagging;
- 对特征层面做随机子空间选择;
- 最后对所有决策树的预测结果进行投票或平均。
以下算法使用随机子空间方法构建集成模型:
- 设样本数等于 n,特征维度数等于 d。
- 选择模型的数目 M。
- 对于每个模型 m,选择特征数 dm<d。所有模型使用相同的 dm 值。
- 对每个模型 m,通过在整个 d 特征集合上随机选择 dm 个特征创建一个训练集。
- 训练每个模型。
- 通过组合 M 个模型的结果,将生成的整体模型应用于新数据中,应用时可以基于多数投票算法(majority voting)或后验概率聚合(aggregation of the posterior probabilities)。
随机森林的算法
构建 N 棵树的随机森林算法如下:
- 对每个 k=1,…,N,生成 Bootstrap 样本 Xk。
- 在样本 Xk 上创建一棵决策树 bk:
- 根据给定的标准选择最佳的特征维度。根据该特征分割样本以创建树的新层。重复这一流程,直到样本用尽。(PS:其实就是传统的决策树算法,根据信息增益或者Gini来创建树)
- 创建树,直到任何叶节点包含的实例不超过 Nmin 个,或者达到特定深度。
- 每次分割,首先从 d 个原始特征中随机选择 m 个特征,接着只在该子集上搜索最佳分割。(就这步是关键,上面的都是和原来决策树的方法一样)
最终分类器定义为:

可以将随机森林看成 Bagging 决策树的一个改动版本,因为随机森林在每个 Bagging 决策树的分割处选择了一个随机特征子空间。
PS:
Bagging=Bootstrap(随机抽样)+决策树
随机森林 = Bagging + 随机特征(随机子空间)
随机森林与 Bagging 和决策树的比较
导入所需包,配置环境:
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_circles
from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier, BaggingRegressor
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
import seaborn as sns
from matplotlib import pyplot as plt
# Disable warnings in Anaconda
import warnings
import numpy as np
warnings.filterwarnings('ignore')
%matplotlib inline
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = 10, 6
n_train = 150
n_test = 1000
noise = 0.1
生成数据。
def f(x):
x = x.ravel()
return np.exp(-x ** 2) + 1.5 * np.exp(-(x - 2) ** 2)
def generate(n_samples, noise):
X = np.random.rand(n_samples) * 10 - 5
X = np.sort(X).ravel()
y = np.exp(-X ** 2) + 1.5 * np.exp(-(X - 2) ** 2)\
+ np.random.normal(0.0, noise, n_samples)
X = X.reshape((n_samples, 1))
return X, y
X_train, y_train = generate(n_samples=n_train, noise=noise)
X_test, y_test = generate(n_samples=n_test, noise=noise)
使用单棵决策树回归。
dtree = DecisionTreeRegressor().fit(X_train, y_train)
d_predict = dtree.predict(X_test)
plt.figure(figsize=(10, 6))
plt.plot(X_test, f(X_test), "b")
plt.scatter(X_train, y_train, c="b", s=20)
plt.plot(X_test, d_predict, "g", lw=2)
plt.xlim([-5, 5])
plt.title("Decision tree, MSE = %.2f"
% np.sum((y_test - d_predict) ** 2))
使用 Bagging 决策树回归。
bdt = BaggingRegressor(DecisionTreeRegressor()).fit(X_train, y_train)
bdt_predict = bdt.predict(X_test)
plt.figure(figsize=(10, 6))
plt.plot(X_test, f(X_test), "b")
plt.scatter(X_train, y_train, c="b", s=20)
plt.plot(X_test, bdt_predict, "y", lw=2)
plt.xlim([-5, 5])
plt.title("Bagging for decision trees, MSE = %.2f" %
np.sum((y_test - bdt_predict) ** 2))
使用随机森林。
rf = RandomForestRegressor(n_estimators=10).fit(X_train, y_train)
rf_predict = rf.predict(X_test)
plt.figure(figsize=(10, 6))
plt.plot(X_test, f(X_test), "b")
plt.scatter(X_train, y_train, c="b", s=20)
plt.plot(X_test, rf_predict, "r", lw=2)
plt.xlim([-5, 5])
plt.title("Random forest, MSE = %.2f" % np.sum((y_test - rf_predict) ** 2))
从上面 3 张图像和 MSE 值可以看到,10 树随机森林和10 树Bagging 比单棵决策树的表现要好,当然随机森林在这个例子上不稳定,多次运行结果表明随机森林和 Bagging 决策树互有胜负。随机森林和 Bagging 的主要差别在于,前者分割的最佳特征是从一个随机特征子空间中选取的,而后者在分割时将考虑所有特征。
接下来,我们将查看随机森林和 Bagging 在分类问题上的表现。
np.random.seed(42)
X, y = make_circles(n_samples=500, factor=0.1, noise=0.35, random_state=42)
X_train_circles, X_test_circles, y_train_circles, y_test_circles = train_test_split(
X, y, test_size=0.2)
dtree = DecisionTreeClassifier(random_state=42)
dtree.fit(X_train_circles, y_train_circles)
x_range = np.linspace(X.min(), X.max(), 100)
xx1, xx2 = np.meshgrid(x_range, x_range)
y_hat = dtree.predict(np.c_[xx1.ravel(), xx2.ravel()])
y_hat = y_hat.reshape(xx1.shape)
plt.contourf(xx1, xx2, y_hat, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn')
plt.title("Decision tree")
plt.show()
b_dtree = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=300, random_state=42)
b_dtree.fit(X_train_circles, y_train_circles)
x_range = np.linspace(X.min(), X.max(), 100)
xx1, xx2 = np.meshgrid(x_range, x_range)
y_hat = b_dtree.predict(np.c_[xx1.ravel(), xx2.ravel()])
y_hat = y_hat.reshape(xx1.shape)
plt.contourf(xx1, xx2, y_hat, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn')
plt.title("Bagging (decision trees)")
plt.show()
rf = RandomForestClassifier(n_estimators=300, random_state=42)
rf.fit(X_train_circles, y_train_circles)
x_range = np.linspace(X.min(), X.max(), 100)
xx1, xx2 = np.meshgrid(x_range, x_range)
y_hat = rf.predict(np.c_[xx1.ravel(), xx2.ravel()])
y_hat = y_hat.reshape(xx1.shape)
plt.contourf(xx1, xx2, y_hat, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn')
plt.title("Random forest")
plt.show()

上图显示了决策树形成的边界凹凸不平,有大量锐角,这暗示出现过拟合现象。相反,随机森林和 Bagging 的边界相当平滑,没有明显的过拟合的迹象。
现在,看看哪些参数有利于提高随机森林模型的准确率。
随机森林的参数
scikit-learn 库提供了 BaggingRegressor 和 BaggingClassifier 类,其中在创建新模型时需要注意的一些参数如下:
- n_estimators 是随机森林中树的数量;
- criterion 是衡量分割质量的函数;
- max_features 是查找最佳分割时考虑的特征数;
- min_samples_leaf 是叶节点的最小样本数;
- max_depth 是树的最大深度。
随机森林在现实问题中的应用
使用之前运营商的客户离网数据集作为例子。这是一个分类问题,使用准确率评估模型。
首先,创建一个简单的分类器作为基线。出于简单的目的,只使用数值特征构建分类器。
import pandas as pd
from sklearn.model_selection import cross_val_score, StratifiedKFold, GridSearchCV
from sklearn.metrics import accuracy_score
加载数据集。
df = pd.read_csv(
"https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv")
选择数值特性。
cols = []
for i in df.columns:
if (df[i].dtype == "float64") or (df[i].dtype == 'int64'):
cols.append(i)
将数据集分离为输入变量和目标变量。
X, y = df[cols].copy(), np.asarray(df["Churn"], dtype='int8')
为验证过程进行分层分割。
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
基于默认参数初始化分类器。
rfc = RandomForestClassifier(random_state=42, n_jobs=-1, oob_score=True)
在训练集上进行训练。
results = cross_val_score(rfc, X, y, cv=skf)
得到了 91.48% 的准确率作为基线,现在,尝试改进结果,同时查看一下基本参数变化时的学习曲线。首先,从改进树的数量开始,初始化 K 折交叉验证。
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
创建列表储存训练集和测试集上的准确率。
train_acc = []
test_acc = []
temp_train_acc = []
temp_test_acc = []
进行网格搜索。下方列表中候选数值较多,会增加线上环境执行时间,可以自行减少数量以加快运行。
trees_grid = [5, 10, 15, 20, 30, 50, 75, 100]
在训练集上训练模型。
for ntrees in trees_grid:
rfc = RandomForestClassifier(
n_estimators=ntrees, random_state=42, n_jobs=-1, oob_score=True)
temp_train_acc = []
temp_test_acc = []
for train_index, test_index in skf.split(X, y):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y[train_index], y[test_index]
rfc.fit(X_train, y_train)
temp_train_acc.append(rfc.score(X_train, y_train))
temp_test_acc.append(rfc.score(X_test, y_test))
train_acc.append(temp_train_acc)
test_acc.append(temp_test_acc)
打印结果。
train_acc, test_acc = np.asarray(train_acc), np.asarray(test_acc)
print("Best accuracy on CV is {:.2f}% with {} trees".format(max(test_acc.mean(axis=1))*100,
trees_grid[np.argmax(test_acc.mean(axis=1))]))
更改树的数量为 50 时,得到了 92.44% 的准确率。接下来,绘制相应的学习曲线。
plt.style.use('ggplot')
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(trees_grid, train_acc.mean(axis=1),
alpha=0.5, color='blue', label='train')
ax.plot(trees_grid, test_acc.mean(axis=1), alpha=0.5, color='red', label='cv')
ax.fill_between(trees_grid, test_acc.mean(axis=1) - test_acc.std(axis=1),
test_acc.mean(axis=1) + test_acc.std(axis=1), color='#888888', alpha=0.4)
ax.fill_between(trees_grid, test_acc.mean(axis=1) - 2*test_acc.std(axis=1),
test_acc.mean(axis=1) + 2*test_acc.std(axis=1), color='#888888', alpha=0.2)
ax.legend(loc='best')
ax.set_ylim([0.88, 1.02])
ax.set_ylabel("Accuracy")
ax.set_xlabel("N_estimators")

上图表明,当树的数量增加到一定值时,测试集上的准确率不再提升,同时训练集上的准确率达到了 100%,这意味着出现过拟合。
为了避免过拟合,需要给模型加上正则化参数。下面将树的数目固定为 100,然后看看不同的 max_depth 效果如何,首先创建列表以保存训练和测试集上的准确率。
train_acc = []
test_acc = []
temp_train_acc = []
temp_test_acc = []
max_depth_grid = [3, 5, 7, 9, 11, 13, 15, 17, 20, 22, 24]
在训练集上训练模型。
for max_depth in max_depth_grid:
rfc = RandomForestClassifier(
n_estimators=100, random_state=42, n_jobs=-1, oob_score=True, max_depth=max_depth)
temp_train_acc = []
temp_test_acc = []
for train_index, test_index in skf.split(X, y):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y[train_index], y[test_index]
rfc.fit(X_train, y_train)
temp_train_acc.append(rfc.score(X_train, y_train))
temp_test_acc.append(rfc.score(X_test, y_test))
train_acc.append(temp_train_acc)
test_acc.append(temp_test_acc)
打印结果。
train_acc, test_acc = np.asarray(train_acc), np.asarray(test_acc)
print("Best accuracy on CV is {:.2f}% with {} max_depth".format(max(test_acc.mean(axis=1))*100,
max_depth_grid[np.argmax(test_acc.mean(axis=1))]))
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(max_depth_grid, train_acc.mean(axis=1),
alpha=0.5, color='blue', label='train')
ax.plot(max_depth_grid, test_acc.mean(axis=1),
alpha=0.5, color='red', label='cv')
ax.fill_between(max_depth_grid, test_acc.mean(axis=1) - test_acc.std(axis=1),
test_acc.mean(axis=1) + test_acc.std(axis=1), color='#888888', alpha=0.4)
ax.fill_between(max_depth_grid, test_acc.mean(axis=1) - 2*test_acc.std(axis=1),
test_acc.mean(axis=1) + 2*test_acc.std(axis=1), color='#888888', alpha=0.2)
ax.legend(loc='best')
ax.set_ylim([0.88, 1.02])
ax.set_ylabel("Accuracy")
ax.set_xlabel("Max_depth")

max_depth 在模型中起到了正则化的作用,过拟合现象有所缓解,模型准确率提高了一些。
另一个值得调整的重要参数是 min_samples_leaf,它也能起到正则化作用。首先,创建列表以保存训练和测试集上的准确率。
train_acc = []
test_acc = []
temp_train_acc = []
temp_test_acc = []
min_samples_leaf_grid = [1, 3, 5, 7, 9, 11, 13, 15, 17, 20, 22, 24]
在训练集上训练模型。
for min_samples_leaf in min_samples_leaf_grid:
rfc = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1,
oob_score=True, min_samples_leaf=min_samples_leaf)
temp_train_acc = []
temp_test_acc = []
for train_index, test_index in skf.split(X, y):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y[train_index], y[test_index]
rfc.fit(X_train, y_train)
temp_train_acc.append(rfc.score(X_train, y_train))
temp_test_acc.append(rfc.score(X_test, y_test))
train_acc.append(temp_train_acc)
test_acc.append(temp_test_acc)
打印结果。
train_acc, test_acc = np.asarray(train_acc), np.asarray(test_acc)
print("Best accuracy on CV is {:.2f}% with {} min_samples_leaf".format(max(test_acc.mean(axis=1))*100,
min_samples_leaf_grid[np.argmax(test_acc.mean(axis=1))]))
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(min_samples_leaf_grid, train_acc.mean(
axis=1), alpha=0.5, color='blue', label='train')
ax.plot(min_samples_leaf_grid, test_acc.mean(
axis=1), alpha=0.5, color='red', label='cv')
ax.fill_between(min_samples_leaf_grid, test_acc.mean(axis=1) - test_acc.std(axis=1),
test_acc.mean(axis=1) + test_acc.std(axis=1), color='#888888', alpha=0.4)
ax.fill_between(min_samples_leaf_grid, test_acc.mean(axis=1) - 2*test_acc.std(axis=1),
test_acc.mean(axis=1) + 2*test_acc.std(axis=1), color='#888888', alpha=0.2)
ax.legend(loc='best')
ax.set_ylim([0.88, 1.02])
ax.set_ylabel("Accuracy")
ax.set_xlabel("Min_samples_leaf")

在这一情形下,验证集上的准确率没有提高,但当验证集的准确率保持 92% 以上时,过拟合的情况降低了 2%。
下面考虑 max_features 这一参数。在分类问题中,默认特征为「所有特征数的平方根」。首先,检查在这个例子中选择 4 个特征是否是最佳的,创建列表以保存训练和测试集的准确率。
train_acc = []
test_acc = []
temp_train_acc = []
temp_test_acc = []
max_features_grid = [2, 4, 6, 8, 10, 12, 14, 16]
在训练集上训练模型。
for max_features in max_features_grid:
rfc = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1,
oob_score=True, max_features=max_features)
temp_train_acc = []
temp_test_acc = []
for train_index, test_index in skf.split(X, y):
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y[train_index], y[test_index]
rfc.fit(X_train, y_train)
temp_train_acc.append(rfc.score(X_train, y_train))
temp_test_acc.append(rfc.score(X_test, y_test))
train_acc.append(temp_train_acc)
test_acc.append(temp_test_acc)
打印结果。
train_acc, test_acc = np.asarray(train_acc), np.asarray(test_acc)
print("Best accuracy on CV is {:.2f}% with {} max_features".format(max(test_acc.mean(axis=1))*100,
max_features_grid[np.argmax(test_acc.mean(axis=1))]))
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(max_features_grid, train_acc.mean(axis=1),
alpha=0.5, color='blue', label='train')
ax.plot(max_features_grid, test_acc.mean(axis=1),
alpha=0.5, color='red', label='cv')
ax.fill_between(max_features_grid, test_acc.mean(axis=1) - test_acc.std(axis=1),
test_acc.mean(axis=1) + test_acc.std(axis=1), color='#888888', alpha=0.4)
ax.fill_between(max_features_grid, test_acc.mean(axis=1) - 2*test_acc.std(axis=1),
test_acc.mean(axis=1) + 2*test_acc.std(axis=1), color='#888888', alpha=0.2)
ax.legend(loc='best')
ax.set_ylim([0.88, 1.02])
ax.set_ylabel("Accuracy")
ax.set_xlabel("Max_features")

上图表明,在这个例子中,max_features=12 时模型表现最好。
我们已经看到学习曲线如何随着基本参数的变化而变化。现在,直接使用 GridSearch() 来查找最佳参数。首先,初始化参数集以进行穷举搜索和拟合。
# 原实验搜索参数
# parameters = {'max_features': [4, 7, 10, 13], 'min_samples_leaf': [
# 1, 3, 5, 7], 'max_depth': [5, 10, 15, 20]}
# 为加速线上环境执行,优化搜索参数
parameters = {'max_features': [10, 13], 'min_samples_leaf': [1, 3], 'max_depth': [5, 10]}
rfc = RandomForestClassifier(n_estimators=100, random_state=42,
n_jobs=-1, oob_score=True)
gcv = GridSearchCV(rfc, parameters, n_jobs=-1, cv=skf, verbose=1)
gcv.fit(X, y)
打印结果。
gcv.best_estimator_, gcv.best_score_
调参之后,该模型的准确率达到了 92.7%。随机森林最重要的一点是它的准确率不会随着树的增加而下降,这意味着可以使用 10 棵树调整超参数,接着把树的数量从 10 增至 500 来提升准确率。
随机森林的方差和去相关
随机森林的方差可以用下式表达:


PS:
很容易将 ρ(x) 误认为随机森林中已完成训练的树的平均相关性,其实并非如此。ρ(x) 是一对随机树在输入 x 上估计的理论相关性,它的值源自重复取样训练集以及之后随机选择的决策树对。用统计学术语来说,这个相关性是由 Z 和 Θ 的取样分布导致的。
换句话说:
- 很多人以为:ρ(x) ≈ “我训练完 100 棵树后,直接算它们在 x 上预测值的相关系数并平均”。
- 实际上:ρ(x) 在随机森林的理论推导里,指的是“在所有可能的Bagging/特征采样下,随机得到的两棵树,如果用它们对同一个 x 做预测,这两个预测在统计意义上会有多大相关性”。
- 也就是说,它是一个理论期望:想象你有无数次(或很多次)采样、训练树的机会,然后随机挑两棵树来做预测,记录它们对 x 的输出,再计算相关系数并取期望。
PS:
-
Z 取样分布
- Z 表示“训练数据”本身的随机来源。
- 在 Bagging(Bootstrap)中,通常会从原始训练集(假设有 N 条样本)有放回地抽取N 条样本,形成一个新的训练子集。每次抽取都可能使部分样本重复、部分样本未出现。
- 因此,对不同的树来说,各自用到的“训练子集”就不一样,训练出来的树也会有差异。
- 从更理论的角度说:如果我们的训练样本也被看作来自某个未知的真实分布 D,那么你可以想象在做大量重复实验时,每一次都从 D 中采样得到一个训练集 Z。不同的 Z 会导致学到的树结构不同。
-
Θ 的取样分布
- Θ 表示模型内部的“随机选择”过程,主要包含:
- 随机特征选择(Random Subspace):在节点分裂时,只从部分特征中挑最佳分裂特征。
- 可能还有其它形式的随机性,比如随机地限制最大深度、随机抛弃部分划分节点等(在一些变体里)。
- Θ 表示模型内部的“随机选择”过程,主要包含:
任何一对树的条件相关性等于 0,因为 Bootstrap 和特征选取是独立同分布。如果考虑单棵树的方差,它几乎不受分割参数的影响(mm),但分割参数在集成中是关键参数。另外,单棵决策树的方差要比集成高很多,如下图所示:(图来自《The Elements of Statistical Learning》)

随机森林的偏差
随机森林、Bagging 的偏差和单棵决策树一样:
从绝对值上说,随机森林和 Bagging 的偏差通常比单棵决策树要大,因为随机过程和样本空间缩减使得模型受到了限制。因此,Bagging 和随机森林在预测准确率上比单棵决策树要高的原因仅仅源自方差的降vv


PS:因此,这个偏差公式告诉我们:随机森林通过集成并不会显著降低‘偏差’(它不是像深度增加、特征工程那样增强树的表达能力),而是主要靠‘降低方差’来提升模型性能。
从绝对值上说,随机森林和 Bagging 的偏差通常比单棵决策树要大,因为随机过程和样本空间缩减使得模型受到了限制。因此,Bagging 和随机森林在预测准确率上比单棵决策树要高的原因仅仅源自方差的降低。
极端随机树
极端随机树(Extremely Randomized Trees)在节点分岔时应用了更多随机性。和随机森林一样,极端随机树使用一个随机特征子空间。然而,极端随机数并不搜寻最佳阈值,相反,它为每个可能的特征随机生成一个阈值,然后挑选最佳阈值对应的特征来分割节点,通过增加少量偏差来降低方差。
PS:在决策树的每次节点分裂中,需要选择某个特征以及该特征上的某个取值作为“分割阈值”(split threshold),将数据分割成“左子节点”和“右子节点”。举个简单的例子:
- 如果在某次分裂中选到的特征是“年龄”,然后决策树模型确定了一个阈值(例如“年龄 < 30”),就可以把所有“年龄小于 30” 的样本放到左分支,把“年龄不小于 30” 的样本放到右分支。
- 这里的“30” 就是分裂阈值。
在随机森林中,通常会穷举或近似枚举这个特征(比如“年龄”)在训练数据中的所有可能候选分割点,然后用信息增益、基尼指数等指标找到最优的阈值(比如“30”)来使得分割后纯度提升最多。
而在“极端随机树”里,则会针对某个候选特征(如“年龄”)随机挑选一个或多个阈值(比如随机生成 25、31、45 等),再在这些随机选出的阈值里挑一个“最优”来做分裂。这就比“穷举所有可能阈值”更随机,导致不同树之间分裂点不一样,从而增加模型的多样性。
scikit-learn 库的 ExtraTreesClassifier() 和 ExtraTreesRegressor() 类实现了该方法。如果使用随机森林或梯度提升遇到了严重的过拟合,可以试试极端随机树。
随机森林和 k 近邻的相似性
随机森林和 k-近邻方法有相似之处。随机森林的预测基于训练集中相似样本的标签,这些样本越常出现在同一叶节点,它们的相似度就越高。
下面来证明这一点,考虑一个二次损失函数的回归问题:设 Tn(x) 为输入 x 在随机森林中第 n 棵树的叶节点数。算法对输入向量 x 的响应等于所有落入叶节点 Tn(x) 的训练样本的平均响应。
PS:在回归或监督学习的背景下,“响应”(response)指的是目标变量的取值。也就是说,对每个训练样本 (xi,yi) 而言,xi 是输入特征向量(自变量),yi 就是它对应的“响应”或“输出”,在回归问题里通常是连续数值。
单棵树的预测
对于第 n 棵树,如果输入 x 落入叶节点 Tn(x)(这是一个函数,表示“输入 x 在第 n 棵树中落入的叶节点编号”),该叶节点内所有训练样本(记为索引 i) 的平均响应可写为:

bn(x):这是第 n 棵树对新输入 x 的预测值(对于回归树,就是一个实数)。


上式表明,随机森林的响应为所有训练样本响应的加权和。
同时,值得注意的是,实例 x 最终的叶节点数 Tn(x) 本身,就是一个有价值的特征。例如,我们可以这么做:
- 基于随机森林或梯度提升技术在样本上训练决策树的复合模型
- 再将类别特征 T1(x),…,Tn(x) 加入样本。
T1(x),…,Tn(x) 特征是非线性空间分割的结果,它们提供了关于样本之间的相似性的信息。《The Elements of Statistical Learning》一书中有一个例子,演示了随机森林和 k-近邻技术的相似性,如下图所示:

PS:为什么与 k-近邻相似
- 在 k-NN 中,找邻居的依据是“几何距离是否接近”;
- 在随机森林里,找邻居的依据是“是否经过树的多次分裂后仍落到同一叶节点”。
- 这就像把特征空间先做了一种“非线性网格”(每个叶子是一个格子),能够把“相似”的样本聚到相同叶节点里。
转换数据集为高维表示
随机森林主要用于监督学习,不过也可以应用于无监督学习。使用 scikit-learn的 RandomTreesEmbedding() 方法,可以将数据集转换为高维的稀疏表示,继而用于无监督学习中。
PS:在使用 RandomTreesEmbedding() 时,模型会对每个样本在若干棵树中“落到哪几个叶子”进行记录,并把这条信息编码成一个高维向量。因为对于大部分叶节点来说,某个样本不会落进去(对应 0),只会落进极少数几个叶节点(对应 1),于是向量中的大部分元素都是 0,称之为“稀疏”。而「所有可能叶节点」的数量通常很多,所以维度会比较高,这就是“高维稀疏表示”。
1. 随机树在无监督场景下怎么做?
- 在监督学习中,随机森林要借助已有的标签,选择最优分裂来划分数据。
- 而
RandomTreesEmbedding(无监督版本)是通过随机选特征、随机分裂来建树,或基于数据的一些统计信息但不使用标签。每棵树相当于一个「随机划分器」,把数据空间切成很多叶节点。
2. 生成“高维稀疏表示”具体步骤
-
训练阶段
- 训练若干棵“随机树”。(此时没有标签,仅根据特征随机或基于分裂准则划分数据。)
- 每棵树有许多叶节点,假设全部树的叶节点总数是 MMM。那么我们可以为这些叶节点依次编号。
-
转换(Embedding)阶段
- 对于某个样本 x,让它“走”过每一棵树并最终落到一个特定叶节点。
- 在一个长度为 M 的向量里,把 “x 对应叶子” 的位置标记为 1,其它绝大部分位置为 0。
- 因为对某棵树来说,x 只会落在 1 个叶子,故整棵森林里所有叶子加起来,x 只会落到少量叶子 → 向量大部分是 0,少量位置是 1。
- 于是,你就得到一个「非常稀疏、但维度较高」的二进制向量,表示 “x 与哪些叶节点相匹配”。
举例:
- 若总叶节点数 M=1000,某个样本在 5 棵树里分别落到第 8、22、501、... 号叶子,那么对应的向量在这几个索引位置是 1,其余 995 个位置是 0。
首先创建一些极端随机树,接着使用包含样本的叶节点索引作为新特征。例如,若第一个叶节点包含输入,就分配 1 为特征值,否则,分配 0。这被称为二进制编码(binary coding),我们可以通过增减树的数目和深度来控制特征数量和稀疏性。由于相近(邻居)的数据点倾向于落入同一叶节点,所以这一转换提供了一个隐式的对数据点密度的非参数估计。
随机森林的优势和劣势
优势:
- 高预测准确率。在大多数问题上表现优于线性算法,准确率与 Boosting 相当;
- 随机取样导致其对离散值的鲁棒性较好;
- 随机选取子空间导致其对特征缩放及其他单调转换不敏感;
- 不需要精细的参数调整。
- 在具有大量特征和类别的数据集上很高效;
- 既可处理连续值,也可处理离散值;
- 不容易出现过拟合。在实践中,增加树的数量几乎总是能提升总体表现。不过,当树的数量增大到一定值后,学习曲线将趋于平稳;
- 有理论方法可以估计特征的重要性;
- 能够很好地处理数据缺失。即使有大部分的数据缺失现象,仍能保持较好的准确率;
- 支持整个数据集及单棵树样本上的加权分类;
- 决策树底层使用的相似性计算可以用于后续的聚类、离散值检测或兴趣数据表示;
- 以上功能和性质可以扩展到未标注数据,以支持无监督聚类,数据可视化和离散值检测;
- 易于并行化,伸缩性强。
劣势:
- 相比单棵决策树,随机森林的输出更难解释;
- 特征重要性估计没有形式化的 p 值;
- 在数据稀疏时(比如,文本输入、词袋),表现不如线性模型好;
- 和线性回归不同,随机森林无法外推。不过,这样也就不会因为离散值的存在导致极端值的出现;
- 在某些问题上容易过拟合,特别是处理高噪声数据时;
- 处理数量级不同的类别数据时,随机森林偏重数量级较高的数据,因为这能更明显的提高准确率;
- 所得模型较大,需要大容量的 RAM 来支撑。
特征的重要性
我们常常需要对算法的输出结果做出解释,在不能完全理解算法的情况下,至少希望能找出哪个输入特征对结果的贡献最大。基于随机森林,我们可以相当容易地获取这类信息。
方法精要
下图很直观地表明,在信用评分问题中,年龄比收入更重要。基于信息增益这一概念,我们可以形式化地解释这一点。
在随机森林中,某一特征在所有树中离树根的平均距离越近,这一特征在给定的分类或回归问题中就越重要。按照分割标准,在每棵树的每处最优分割中取得的增益(例如基尼不纯度),是与分割特征直接相关的重要度测度。
让我们深入了解一下。通过计算袋外误差可以判定是哪个变量导致平均准确率下降。哪一变量导致的准确率下降幅度越大,该变量的重要性评分(importance score)就越高。基尼不纯度或回归问题中 MSE 的下降程度代表了每个变量对所得随机森林模型节点的同质性贡献程度,下降较高标志着基于该变量进行分割可以得到纯度更高的节点。
以上分析可以用下述公式表达:

该差值可理解为:“打乱前袋外准确率” − “打乱后袋外准确率”
现在,可以给出集成中计算特征重要性的公式:
- 未经正则化时的公式:
其中:
- N 是森林中树的总数。
是第 T 棵树的特征重要性。
- 使用标准差正则化后的公式:
为了进一步量化特征重要性,可以通过标准误差归一化来给出标准化的重要性得分:

计算现实问题中的特征重要性
让我们考虑一个数据集,这数据集的内容是 Booking.com 和 TripAdvisor.com 上所列旅馆的一些信息,数据集的特征是不同类别(包括服务质量、房间状况、性价比等)的平均评分,目标变量为旅馆在网站上的总评分。首先导入相关库。
from sklearn.ensemble.forest import RandomForestRegressor
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
导入数据集。
hostel_data = pd.read_csv(
"https://labfile.oss.aliyuncs.com/courses/1283/hostel_factors.csv")
features = {"f1": u"Staff",
"f2": u"Hostel booking",
"f3": u"Check-in and check-out",
"f4": u"Room condition",
"f5": u"Shared kitchen condition",
"f6": u"Shared space condition",
"f7": u"Extra services",
"f8": u"General conditions & conveniences",
"f9": u"Value for money",
"f10": u"Customer Co-creation"}
使用随机森林训练模型。
forest = RandomForestRegressor(n_estimators=1000, max_features=10,
random_state=0)
forest.fit(hostel_data.drop(['hostel', 'rating'], axis=1),
hostel_data['rating'])
importances = forest.feature_importances_
indices = np.argsort(importances)[::-1]
标出随机森林模型中的特征重要性。
num_to_plot = 10
feature_indices = [ind+1 for ind in indices[:num_to_plot]]
打印特征重要性排名(重要性从高到低排列)。
print("Feature ranking:")
for f in range(num_to_plot):
print("%d. %s %f " % (f + 1,
features["f"+str(feature_indices[f])],
importances[indices[f]]))
plt.figure(figsize=(15, 5))
plt.title(u"Feature Importance")
bars = plt.bar(range(num_to_plot),
importances[indices[:num_to_plot]],
color=([str(i/float(num_to_plot+1))
for i in range(num_to_plot)]),
align="center")
ticks = plt.xticks(range(num_to_plot),
feature_indices)
plt.xlim([-1, num_to_plot])
plt.legend(bars, [u''.join(features["f"+str(i)])
for i in feature_indices])


上图表明,消费者常常更关心服务人员素质(Staff)和性价比(Value for money),这两个因子对最终评分的影响最大。然而,这两项特征和其他特征的差别不是非常大,因此排除任何特征都会导致模型准确率的下降。
梯度提升介绍
在机器学习中,有一些算法通过常被称为弱模型,这里所说的“弱模型”指的是像决策树这样简单的基本模型,也指的是那些精度相对较差的模型,也就是比随机模型好那么一点点的模型。
先来思考一个 问题:是否可以从大量相对较弱和简单的模型中通过某种手段而得到一个强大的模型?
正如你想的一样,这个问题的 答案 显然是肯定的。集成方法就是其中的一类,集成学习的算法有很多,先从最简单的开始:AdaBoost。
AdaBoost 采用的是一种贪婪的学习方法。在算法学习时,会给数据样本的每个都会附一个权重值,每一个基分类器开始进行分类前,都会根据前一个分类器的分类误差来调节样本的权重值。然后通线性组合的方式把所有的基分类器集成起来得到一个强分类器。整个过程可以描述为下图所示。
从上图中可知,在第一棵分类树(t=1)建立之后,会有三个蓝色的数据点被错分。因此在第二棵树建立的时候会增大这三个数据点的权值,以此类推。最终我们将三个基分类器结合起来,就得到了一个完美的分类器。如下图所示:

整个过程要是用数学公式描述的话,整个过程如下。




当进行迭代时,可以看到误分类点权重的增加,特别是在类之间的边界上。 这里 有一个关于 AdaBoost 的更详细的例子。
AdaBoost 分类效果确实不错,但为何这一算法如此成功却 缺乏解释。这会让一些人持怀疑态度,认为 AdaBoost 只是过度拟合而已。
过度拟合问题也确实存在,尤其是在当数据有非常怪异的异常值时,过拟合现象较为严重。因此,在这些类型的问题中,AdaBoost 是不稳定的。为解决这些问题,1999 年,杰罗姆·弗里德曼 ( Jerome Friedman ) 提出了 AdaBoost 的泛化版本:梯度提升 ( Machine ),也称为 GBM。
CART、bootstrap 和许多其他算法都起源于斯坦福大学的统计部门。这些算法都非常的实用,但也有一些近年来研究工作尚未得到广泛的应用。例如, glinternet。
现在关于弗里德曼的录像资料不多。不过,有一个非常有趣的 访谈,关于如何提出 CART,以及 CART 如何解决统计学问题。Hastie 也有一个很棒的 讲座。
对于 AdaBoost 算法,虽然我们可以通过添加弱算法和逐步改进误分类数据的权值来增强集成算法。但是,这还不够,例如 GBM 不仅建立在重新加权的数据点上,而且改进了它对整体目标函数梯度的逼近。这个概念极大地打开了集成算法的研究思路。

GBM 的历史
自从 GBM 诞生以来,经过了 10 多年的时间,它才成为数据科学工具箱中必不可少的一部分。为了应用于不同的统计问题,GBM 拥有许多的扩展版本,例如:GLMboost 和 GAMboost 用于增强已有的 GAM 模型,CoxBoost 用于存活曲线,RankBoost 和 LambdaMART 用于排序问题。
GBM 的许多实现也以不同的名称出现在不同的平台上。例如:随机 GBM、GBDT (梯度提升决策树)、GBRT (梯度提升回归树)、MART (多元加性回归树) 等等。此外,由于 ML 社区又是各自为营,所以这使我们很难去知道 GBM 到底得到多广泛的应用,到底有多少个版本等问题。
同时,GBM 也在搜索排序中也得到了广泛的应用。GBM 的加入使得搜索排序被看作为一个优化损失函数问题。最初,AltaVista 是第一批引入提升排名的公司之一。但很快,这些思路就传播到了雅虎、Yandex、必应等网站。从此之后,GBM 成为了主流的算法之一,不仅用于研究,而且也广泛用于工业生产中。

许多的机器学习竞赛,尤其是 Kaggle,在促进普及算法的应用方面发挥了重要作用。因为 Kaggle 提供了一个共同的平台,这使得数据科学家们可以在不同的数据科学问题上与来自世界各地的大量参与者竞争。人们可以在 Kaggle 上用真实数据上测试新的算法,让许多算法有机会“发光”。
自 2011 年以来,在 Kaggle 的冠军访谈中,许多的获奖选手都提到自己使用了梯度提升算法。这也使得梯度提升变得流行起来。尤其是在 XGBoost 库出现之后。XGBoost 虽然不是一个新的、独特的算法,但其是一个经典 GBM 的高效实现版。
GBM 的经历与许多机器学习算法一样:从被提出到成功的实践应用和大规模使用都花了许多年的时间。
本文简要介绍了机器学习中一个重要的技术:集成学习,文中介绍的 Bootstraping、Bagging、随机森林都是较为成熟好用的方法。通过随机森林衡量特征的重要性,能更好的帮助大家分析自己所构建的模型。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)