原文:
annas-archive.org/md5/681e70f53a5cd12054ae0d01b7b855ea
译者:飞龙
第六章:提升
我们将讨论的第二种生成方法是提升。提升旨在将多个弱学习器结合成一个强大的集成。它能够减少偏差,同时也能降低方差。在这里,弱学习器是指那些表现略好于随机预测的独立模型。例如,在一个包含两个类别且每个类别的实例数量相等的分类数据集上,弱学习器的准确率会稍微高于 50%。
在本章中,我们将介绍两种经典的提升算法:梯度提升(Gradient Boosting)和 AdaBoost。此外,我们还将探讨使用 scikit-learn 实现进行分类和回归。最后,我们将实验一种近期的提升算法及其实现——XGBoost。
本章涵盖的主要主题如下:
-
使用提升集成的动机
-
各种算法
-
利用 scikit-learn 在 Python 中创建提升集成
-
使用 XGBoost 库进行 Python 编程
技术要求
你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 语法和约定。最后,熟悉 NumPy 库将有助于读者理解一些自定义算法实现。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter06
查看以下视频以观看代码演示:bit.ly/2ShWstT
。
AdaBoost
AdaBoost 是最流行的提升算法之一。与袋装法类似,该算法的主要思想是创建若干个无关的弱学习器,然后将它们的预测结果结合起来。与袋装法的主要区别在于,算法不是创建多个独立的自助法训练集,而是顺序地训练每一个弱学习器,给所有实例分配权重,基于实例的权重采样下一组训练集,然后重复整个过程。作为基学习器算法,通常使用由单一节点构成的决策树。这些深度为一层的决策树被称为决策桩。
加权采样
加权采样是指每个候选者都有一个相应的权重,这个权重决定了该候选者被采样的概率。权重经过归一化处理,使得它们的总和为 1。然后,归一化后的权重对应每个候选者被选中的概率。以下表格展示了一个简单示例,其中有三个候选者,权重分别为 1、5 和 10,并展示了归一化权重以及相应的候选者被选中的概率。
候选者 | 权重 | 归一化权重 | 概率 |
---|---|---|---|
1 | 1 | 0.0625 | 6.25% |
2 | 5 | 0.3125 | 31.25% |
3 | 10 | 0.625 | 62.50% |
实例权重转为概率
创建集成模型
假设是一个分类问题,AdaBoost 算法可以从其基本步骤高层次地描述。对于回归问题,步骤类似:
-
初始化所有训练集实例的权重,使它们的总和等于 1。
-
根据权重进行有放回的采样,生成一个新的数据集。
-
在采样集上训练弱学习器。
-
计算它在原始训练集上的错误率。
-
将弱学习器添加到集成模型中并保存其错误率。
-
调整权重,增加错误分类实例的权重,减少正确分类实例的权重。
-
从 步骤 2 重复。
-
弱学习器通过投票组合,每个学习器的投票按其错误率加权。
整个过程如下面的图所示:
为第 n 个学习器创建集成模型的过程
本质上,这使得每个新的分类器都专注于前一个学习器无法正确处理的实例。假设是一个二分类问题,我们可以从如下图所示的数据集开始:
我们的初始数据集
这里,所有权重都相等。第一个决策树桩决定按如下方式划分问题空间。虚线代表决策边界。两个黑色的 + 和 - 符号表示决策树桩将每个实例分类为正类或负类的子空间。这留下了两个错误分类的实例。它们的实例权重将被增加,而其他所有权重将被减少:
第一个决策树桩的空间划分和错误
通过创建另一个数据集,其中两个错误分类的实例占主导地位(由于我们进行有放回的采样并且它们的权重大于其他实例,它们可能会被多次包含),第二个决策树桩按如下方式划分空间:
第二个决策树桩的空间划分和错误
最后,在重复第三个决策树桩的过程后,最终的集成模型按如下图所示划分了空间:
最终集成模型的空间划分
在 Python 中实现 AdaBoost
为了更好地理解 AdaBoost 是如何工作的,我们将展示一个基本的 Python 实现。我们将使用乳腺癌分类数据集作为示例。像往常一样,我们首先加载库和数据:
# --- SECTION 1 ---
# Libraries and data loading
from copy import deepcopy
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
from sklearn import metrics
import numpy as np
bc = load_breast_cancer()
train_size = 400
train_x, train_y = bc.data[:train_size], bc.target[:train_size]
test_x, test_y = bc.data[train_size:], bc.target[train_size:]
np.random.seed(123456)
然后我们创建集成模型。首先,声明集成模型的大小和基础学习器类型。如前所述,我们使用决策树桩(决策树仅有一层)。
此外,我们为数据实例的权重、学习器的权重和学习器的错误创建了一个 NumPy 数组:
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 3
base_classifier = DecisionTreeClassifier(max_depth=1)
# Create the initial weights
data_weights = np.zeros(train_size) + 1/train_size
# Create a list of indices for the train set
indices = [x for x in range(train_size)]
base_learners = []
learners_errors = np.zeros(ensemble_size)
learners_weights = np.zeros(ensemble_size)
对于每个基本学习器,我们将创建一个原始分类器的deepcopy
,在一个样本数据集上训练它,并进行评估。首先,我们创建副本并根据实例的权重,从原始测试集中进行有放回的抽样:
# Create each base learner
for i in range(ensemble_size):
weak_learner = deepcopy(base_classifier)
# Choose the samples by sampling with replacement.
# Each instance's probability is dictated by its weight.
data_indices = np.random.choice(indices, train_size, p=data_weights)
sample_x, sample_y = train_x[data_indices], train_y[data_indices]
然后,我们在采样数据集上拟合学习器,并在原始训练集上进行预测。我们使用predictions
来查看哪些实例被正确分类,哪些实例被误分类:
# Fit the weak learner and evaluate it
weak_learner.fit(sample_x, sample_y)
predictions = weak_learner.predict(train_x)
errors = predictions != train_y
corrects = predictions == train_y
在下面,权重误差被分类。errors
和corrects
都是布尔值列表(True
或False
),但 Python 将它们处理为 1 和 0。这使得我们可以与data_weights
进行逐元素相乘。然后,学习器的误差通过加权误差的平均值计算得出:
# Calculate the weighted errors
weighted_errors = data_weights*errors
# The base learner's error is the average of the weighted errors
learner_error = np.mean(weighted_errors)
learners_errors[i] = learner_error
最后,学习器的权重可以通过加权准确率与加权误差的自然对数的一半来计算。接下来,我们可以使用学习器的权重来计算新的数据权重。对于误分类的实例,新权重等于旧权重乘以学习器权重的自然指数。对于正确分类的实例,则使用负倍数。最后,新的权重进行归一化,基本学习器被添加到base_learners
列表中:
# The learner's weight
learner_weight = np.log((1-learner_error)/learner_error)/2
learners_weights[i] = learner_weight
# Update the data weights
data_weights[errors] = np.exp(data_weights[errors] * learner_weight)
data_weights[corrects] = np.exp(-data_weights[corrects] * learner_weight)
data_weights = data_weights/sum(data_weights)
# Save the learner
base_learners.append(weak_learner)
为了使用集成进行预测,我们通过加权多数投票将每个单独的预测结果结合起来。由于这是一个二分类问题,如果加权平均值大于0.5
,则实例被分类为0
;否则,它被分类为1
:
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble_predictions = []
for learner, weight in zip(base_learners, learners_weights):
# Calculate the weighted predictions
prediction = learner.predict(test_x)
ensemble_predictions.append(prediction*weight)
# The final prediction is the weighted mean of the individual predictions
ensemble_predictions = np.mean(ensemble_predictions, axis=0) >= 0.5
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 4 ---
# Print the accuracy
print('Boosting: %.2f' % ensemble_acc)
该集成方法最终实现的准确率为 95%。
优势与劣势
提升算法能够同时减少偏差和方差。长期以来,它们被认为能够免疫过拟合,但事实上,它们也有可能过拟合,尽管它们非常健壮。一种可能的解释是,基本学习器为了分类异常值,创建了非常强大且复杂的规则,这些规则很少能适应其他实例。在下面的图示中,给出了一个示例。集成方法生成了一组规则来正确分类异常值,但这些规则如此强大,以至于只有一个完全相同的例子(即,具有完全相同特征值)才能适应由规则定义的子空间:
为异常值生成的规则
许多提升算法的一个缺点是它们难以并行化,因为模型是顺序生成的。此外,它们还存在集成学习技术的常见问题,例如可解释性的降低和额外的计算成本。
梯度提升
梯度提升是另一种提升算法。与 AdaBoost 相比,它是一个更广泛的提升框架,这也使得它更复杂且需要更多数学推导。梯度提升不是通过分配权重并重新采样数据集来强调有问题的实例,而是通过构建每个基本学习器来纠正前一个学习器的误差。此外,梯度提升使用不同深度的决策树。在这一部分,我们将介绍梯度提升,而不深入探讨其中的数学原理。相反,我们将介绍基本概念以及一个自定义的 Python 实现。
创建集成模型
梯度提升算法(用于回归目的)从计算训练集目标变量的均值开始,并将其作为初始预测值。然后,计算每个实例目标与预测值(均值)的差异,以便计算误差。这些误差也称为伪残差。
接下来,它创建一个决策树,尝试预测伪残差。通过重复这个过程若干次,整个集成模型被构建出来。类似于 AdaBoost,梯度提升为每棵树分配一个权重。与 AdaBoost 不同的是,这个权重并不依赖于树的表现,而是一个常数项,这个常数项称为学习率。它的目的是通过限制过拟合的能力来提高集成模型的泛化能力。算法的步骤如下:
-
定义学习率(小于 1)和集成模型的大小。
-
计算训练集的目标均值。
-
使用均值作为非常简单的初始预测,计算每个实例目标与均值的差异。这些误差称为伪残差。
-
使用原始训练集的特征和伪残差作为目标,构建决策树。
-
使用决策树对训练集进行预测(我们尝试预测伪残差)。将预测值乘以学习率。
-
将乘积值加到之前存储的预测值上,使用新计算的值作为新的预测。
-
使用计算得到的预测值来计算新的伪残差。
-
从步骤 4开始重复,直到达到所需的集成模型大小。
请注意,为了产生最终集成模型的预测,每个基本学习器的预测值会乘以学习率,并加到前一个学习器的预测上。计算出的均值可以视为第一个基本学习器的预测值。
在每一步 s 中,对于学习率 lr,预测值计算如下:
残差计算为实际目标值 t 与预测值的差异:
整个过程如下面的图所示:
创建梯度提升集成模型的步骤
进一步阅读
由于这是一本实战书籍,我们不会深入探讨算法的数学方面。然而,对于数学上有兴趣的人,我们推荐以下论文。第一篇是更具体的回归框架,而第二篇则更加一般化:
-
Friedman, J.H., 2001. 贪婪函数逼近:梯度提升机。《统计学年鉴》,pp.1189-1232。
-
Mason, L., Baxter, J., Bartlett, P.L. 和 Frean, M.R., 2000. 提升算法作为梯度下降方法。在《神经信息处理系统进展》中(第 512-518 页)。
在 Python 中实现梯度提升
尽管梯度提升可能很复杂且需要数学知识,但如果我们专注于传统的回归问题,它可以变得非常简单。为了证明这一点,我们在 Python 中使用标准的 scikit-learn 决策树实现了一个自定义的例子。对于我们的实现,我们将使用糖尿病回归数据集。首先,加载库和数据,并设置 NumPy 的随机数生成器的种子:
# --- SECTION 1 ---
# Libraries and data loading
from copy import deepcopy
from sklearn.datasets import load_diabetes
from sklearn.tree import DecisionTreeRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
接下来,我们定义集成模型的大小、学习率和决策树的最大深度。此外,我们创建一个列表来存储各个基础学习器,以及一个 NumPy 数组来存储之前的预测。
如前所述,我们的初始预测是训练集的目标均值。除了定义最大深度外,我们还可以通过将 max_leaf_nodes=3
参数传递给构造函数来定义最大叶节点数:
# --- SECTION 2 ---
# Create the ensemble
# Define the ensemble's size, learning rate and decision tree depth
ensemble_size = 50
learning_rate = 0.1
base_classifier = DecisionTreeRegressor(max_depth=3)
# Create placeholders for the base learners and each step's prediction
base_learners = []
# Note that the initial prediction is the target variable's mean
previous_predictions = np.zeros(len(train_y)) + np.mean(train_y)
下一步是创建和训练集成模型。我们首先计算伪残差,使用之前的预测。然后我们创建基础学习器类的深层副本,并在训练集上使用伪残差作为目标进行训练:
# Create the base learners
for _ in range(ensemble_size):
# Start by calculating the pseudo-residuals
errors = train_y - previous_predictions
# Make a deep copy of the base classifier and train it on the
# pseudo-residuals
learner = deepcopy(base_classifier)
learner.fit(train_x, errors)
predictions = learner.predict(train_x)
最后,我们使用训练好的基础学习器在训练集上预测伪残差。我们将预测乘以学习率,加到之前的预测上。最后,我们将基础学习器追加到 base_learners
列表中:
# Multiply the predictions with the learning rate and add the results
# to the previous prediction
previous_predictions = previous_predictions + learning_rate*predictions
# Save the base learner
base_learners.append(learner)
为了使用我们的集成模型进行预测和评估,我们使用测试集的特征来预测伪残差,将其乘以学习率,然后加到训练集的目标均值上。重要的是要使用原始训练集的均值作为起始点,因为每棵树都预测相对于那个原始均值的偏差:
# --- SECTION 3 ---
# Evaluate the ensemble
# Start with the train set's mean
previous_predictions = np.zeros(len(test_y)) + np.mean(train_y)
# For each base learner predict the pseudo-residuals for the test set and
# add them to the previous prediction,
# after multiplying with the learning rate
for learner in base_learners:
predictions = learner.predict(test_x)
previous_predictions = previous_predictions + learning_rate*predictions
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, previous_predictions)
mse = metrics.mean_squared_error(test_y, previous_predictions)
print('Gradient Boosting:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
该算法能够通过这种特定设置实现 0.59 的 R 平方值和 2253.34 的均方误差。
使用 scikit-learn
尽管出于教育目的编写自己的算法很有用,但 scikit-learn 在分类和回归问题上有一些非常好的实现。在本节中,我们将介绍这些实现,并看看如何提取生成的集成模型的信息。
使用 AdaBoost
Scikit-learn 中的 AdaBoost 实现位于 sklearn.ensemble
包中的 AdaBoostClassifier
和 AdaBoostRegressor
类中。
与所有 scikit-learn 分类器一样,我们使用fit
和predict
函数来训练分类器并在测试集上进行预测。第一个参数是算法将使用的基本分类器。algorithm="SAMME"
参数强制分类器使用离散提升算法。对于这个例子,我们使用手写数字识别问题:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn import metrics
digits = load_digits()
train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 200
ensemble = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1),
algorithm="SAMME",
n_estimators=ensemble_size)
# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)
# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 5 ---
# Print the accuracy
print('Boosting: %.2f' % ensemble_acc)
这导致了在测试集上准确率为 81% 的集成。使用提供的实现的一个优势是,我们可以访问并绘制每个单独的基本学习器的误差和权重。我们可以通过ensemble.estimator_errors_
和ensemble.estimator_weights_
分别访问它们。通过绘制权重,我们可以评估集成在哪些地方停止从额外的基本学习器中获益。通过创建一个由 1,000 个基本学习器组成的集成,我们可以看到大约从 200 个基本学习器开始,权重已经稳定。因此,再增加超过 200 个基本学习器几乎没有意义。通过事实也得到了进一步证实:1,000 个基本学习器的集成达到了 82% 的准确率,比使用 200 个基本学习器时提高了 1%。
1,000 个基本学习器的集成基本学习器权重
回归实现遵循相同的原理。这里,我们在糖尿病数据集上测试该算法:
# --- SECTION 1 ---
# Libraries and data loading
from copy import deepcopy
from sklearn.datasets import load_diabetes
from sklearn.ensemble import AdaBoostRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 1000
ensemble = AdaBoostRegressor(n_estimators=ensemble_size)
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble.fit(train_x, train_y)
predictions = ensemble.predict(test_x)
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, predictions)
mse = metrics.mean_squared_error(test_y, predictions)
print('Gradient Boosting:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
集成生成的 R 平方为 0.59,均方误差(MSE)为 2256.5。通过绘制基本学习器的权重,我们可以看到算法由于预测能力的改进微不足道,在第 151 个基本学习器之后提前停止。这可以通过图中的零权重值看出。此外,通过打印ensemble.estimators_
的长度,我们观察到它的长度仅为 151。这与我们实现中的base_learners
列表等效:
回归 Adaboost 的基本学习器权重
使用梯度提升
Scikit-learn 还实现了梯度提升回归和分类。这两者也被包含在ensemble
包中,分别为GradientBoostingRegressor
和GradientBoostingClassifier
。这两个类在每一步存储误差,保存在对象的train_score_
属性中。这里,我们展示了一个糖尿病回归数据集的例子。训练和验证过程遵循 scikit-learn 的标准,使用fit
和predict
函数。唯一需要指定的参数是学习率,它通过learning_rate
参数传递给GradientBoostingRegressor
构造函数:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_diabetes
from sklearn.ensemble import GradientBoostingRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 200
learning_rate = 0.1
ensemble = GradientBoostingRegressor(n_estimators=ensemble_size,
learning_rate=learning_rate)
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble.fit(train_x, train_y)
predictions = ensemble.predict(test_x)
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, predictions)
mse = metrics.mean_squared_error(test_y, predictions)
print('Gradient Boosting:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
集成模型达到了 0.44 的 R 平方值和 3092 的均方误差(MSE)。此外,如果我们使用 matplotlib 绘制ensemble.train_score_
,可以看到大约在 20 个基学习器之后,收益递减现象出现。如果进一步分析误差,通过计算改进(基学习器之间的差异),我们发现,在 25 个基学习器之后,添加新的基学习器可能会导致性能下降。
尽管平均性能持续提高,但在使用 50 个基学习器后,性能没有显著改进。因此,我们重复实验,设定ensemble_size = 50
,得到了 0.61 的 R 平方值和 2152 的均方误差(MSE):
梯度提升回归的误差与差异
对于分类示例,我们使用手写数字分类数据集。同样,我们定义了n_estimators
和learning_rate
参数:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
from sklearn.datasets import load_digits
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn import metrics
digits = load_digits()
train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 200
learning_rate = 0.1
ensemble = GradientBoostingClassifier(n_estimators=ensemble_size,
learning_rate=learning_rate)
# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)
# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 5 ---
# Print the accuracy
print('Boosting: %.2f' % ensemble_acc)
使用特定集成大小达到的准确率为 89%。通过绘制误差及其差异,我们再次看到收益递减现象,但没有出现性能显著下降的情况。因此,我们不期待通过减少集成大小来提高预测性能。
XGBoost
XGBoost 是一个支持并行、GPU 和分布式执行的提升库。它帮助许多机器学习工程师和数据科学家赢得了 Kaggle.com 的竞赛。此外,它提供了一个类似于 scikit-learn 接口的 API。因此,已经熟悉该接口的人可以快速利用这个库。此外,它允许对集成的创建进行非常精细的控制。它支持单调约束(即,预测值应当根据特定特征只增加或减少),以及特征交互约束(例如,如果一个决策树创建了一个按年龄分裂的节点,那么它不应当对该节点的所有子节点使用性别作为分裂特征)。最后,它增加了一个额外的正则化参数 gamma,进一步减少了生成集成模型的过拟合能力。相关论文为 Chen, T. 和 Guestrin, C., 2016 年 8 月,Xgboost: A scalable tree boosting system. 见《第 22 届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集》,(第 785-794 页)。ACM。
使用 XGBoost 进行回归
我们将使用糖尿病数据集展示一个简单的回归示例。正如所示,其使用方法非常简单,类似于 scikit-learn 的分类器。XGBoost 通过XGBRegressor
实现回归。该构造函数包含大量参数,并且在官方文档中有详细的说明。在我们的示例中,我们将使用n_estimators
、n_jobs
、max_depth
和learning_rate
参数。按照 scikit-learn 的约定,它们分别定义了集成的大小、并行处理的数量、树的最大深度以及学习率:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_diabetes
from xgboost import XGBRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 200
ensemble = XGBRegressor(n_estimators=ensemble_size, n_jobs=4,
max_depth=1, learning_rate=0.1,
objective ='reg:squarederror')
其余的代码评估生成的ensemble
,与之前的任何示例类似:
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble.fit(train_x, train_y)
predictions = ensemble.predict(test_x)
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, predictions)
mse = metrics.mean_squared_error(test_y, predictions)
print('Gradient Boosting:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
XGBoost 的 R-squared 为 0.65,MSE 为 1932.9,是我们在本章中测试和实现的所有提升方法中表现最好的。此外,我们并未对其任何参数进行微调,这进一步显示了它的建模能力。
使用 XGBoost 进行分类
对于分类任务,相应的类是 XGBClassifier
。构造函数的参数与回归实现相同。以我们的示例为例,我们使用的是手写数字分类问题。我们将 n_estimators
参数设置为 100
,n_jobs
设置为 4
。其余的代码遵循常规模板:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_digits
from xgboost import XGBClassifier
from sklearn import metrics
import numpy as np
digits = load_digits()
train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 100
ensemble = XGBClassifier(n_estimators=ensemble_size, n_jobs=4)
# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)
# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 5 ---
# Print the accuracy
print('Boosting: %.2f' % ensemble_acc)
该集成方法以 89% 的准确率正确分类了测试集,也是所有提升算法中表现最好的。
其他提升方法库
另外两个越来越流行的提升方法库是微软的 LightGBM 和 Yandex 的 CatBoost。在某些情况下,这两个库的性能可以与 XGBoost 相媲美(甚至超过)。尽管如此,XGBoost 在所有三者中仍然是最优秀的,无需微调和特殊的数据处理。
总结
本章介绍了最强大的集成学习技术之一——提升方法。我们介绍了两种流行的提升算法,AdaBoost 和梯度提升。我们提供了这两种算法的自定义实现,以及 scikit-learn 实现的使用示例。此外,我们还简要介绍了 XGBoost,这是一个专注于正则化和分布式提升的库。XGBoost 在回归和分类问题中都能超越所有其他方法和实现。
AdaBoost 通过使用弱学习器(略优于随机猜测)来创建多个基础学习器。每个新的基础学习器都在来自原始训练集的加权样本上进行训练。数据集的加权抽样为每个实例分配一个权重,然后根据这些权重从数据集中抽样,以计算每个实例被抽样的概率。
数据权重是基于前一个基础学习器的错误计算的。基础学习器的错误还用于计算学习器的权重。通过投票的方式结合基础学习器的预测结果,投票时使用每个学习器的权重。梯度提升通过训练每个新的基础学习器,使用前一次预测的错误作为目标,来构建其集成方法。初始预测是训练数据集的目标均值。与袋装方法相比,提升方法无法在相同程度上并行化。尽管提升方法对过拟合具有较强的鲁棒性,但它们仍然可能会过拟合。
在 scikit-learn 中,AdaBoost 的实现存储了各个学习器的权重,这些权重可以用来识别额外的基学习器不再对整体集成的预测能力有贡献的点。梯度提升实现在每一步(基学习器)都存储了集成的误差,这也有助于确定最佳的基学习器数量。XGBoost 是一个专注于提升(boosting)的库,具有正则化能力,进一步减少集成模型的过拟合能力。XGBoost 经常成为许多 Kaggle 竞赛中获胜的机器学习模型的一部分。
第七章:随机森林
Bagging 通常用于降低模型的方差。它通过创建一个基础学习器的集成,每个学习器都在原始训练集的独特自助样本上进行训练,从而实现这一目标。这迫使基础学习器之间保持多样性。随机森林在 Bagging 的基础上进行扩展,不仅在每个基础学习器的训练样本上引入随机性,还在特征选择上也引入了随机性。此外,随机森林的性能类似于提升方法,尽管它们不像提升方法那样需要进行大量的精调。
在本章中,我们将提供关于随机森林的基本背景,并讨论该方法的优缺点。最后,我们将展示使用 scikit-learn 实现的使用示例。本章涵盖的主要内容如下:
-
随机森林如何构建基础学习器
-
如何利用随机性来构建更好的随机森林集成模型
-
随机森林的优缺点
-
使用 scikit-learn 实现进行回归和分类
技术要求
你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 的约定和语法。最后,熟悉 NumPy 库将极大地帮助读者理解一些自定义算法的实现。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter07
查看以下视频,查看代码的实际应用:bit.ly/2LY5OJR
。
理解随机森林树
在本节中,我们将介绍构建基本随机森林树的方法论。虽然有其他方法可以使用,但它们的目标都是一致的:构建多样化的树,作为集成模型的基础学习器。
构建树
如第一章《机器学习回顾》所述,在每个节点选择一个特征和分割点来创建一棵树,以便最佳地划分训练集。当创建一个集成模型时,我们希望基础学习器尽可能地不相关(多样化)。
Bagging 通过引导采样使每棵树的训练集多样化,从而能够生成合理不相关的树。但 bagging 仅通过一个轴进行树的多样化:每个集合的实例。我们仍然可以在第二个轴上引入多样性,即特征。在训练过程中通过选择可用特征的子集,生成的基学习器可以更加多样化。在随机森林中,对于每棵树和每个节点,在选择最佳特征/分裂点组合时,仅考虑可用特征的一个子集。选择的特征数量可以通过手动优化,但回归问题通常选用所有特征的三分之一,而所有特征的平方根被认为是一个很好的起点。
算法的步骤如下:
-
选择在每个节点上将要考虑的特征数量 m
-
对于每个基学习器,执行以下操作:
-
创建引导训练样本
-
选择要拆分的节点
-
随机选择 m 个特征
-
从 m 中选择最佳特征和分裂点
-
将节点拆分为两个节点
-
从步骤 2-2 开始重复,直到满足停止准则,如最大树深度
-
示例说明
为了更好地展示过程,我们考虑以下数据集,表示第一次肩部脱位后是否发生了第二次肩部脱位(复发):
年龄 | 手术 | 性别 | 复发 |
---|---|---|---|
15 | y | m | y |
45 | n | f | n |
30 | y | m | y |
18 | n | m | n |
52 | n | f | y |
肩部脱位复发数据集
为了构建一个随机森林树,我们必须首先决定在每次分裂时将考虑的特征数量。由于我们有三个特征,我们将使用 3 的平方根,约为 1.7。通常,我们使用该数字的下取整(将其四舍五入到最接近的整数),但为了更好地展示过程,我们将使用两个特征。对于第一棵树,我们生成一个引导样本。第二行是从原始数据集中被选择了两次的实例:
年龄 | 手术 | 性别 | 复发 |
---|---|---|---|
15 | y | m | y |
15 | y | m | y |
30 | y | m | y |
18 | n | m | n |
52 | n | f | y |
引导样本
接下来,我们创建根节点。首先,我们随机选择两个特征进行考虑。我们选择手术和性别。在手术特征上进行最佳分裂,结果得到一个准确率为 100%的叶子节点和一个准确率为 50%的节点。生成的树如下所示:
第一次分裂后的树
接下来,我们再次随机选择两个特征,并选择提供最佳分裂的特征。我们现在选择手术和年龄。由于两个误分类的实例均未进行手术,因此最佳分裂通过年龄特征来实现。
因此,最终的树是一个具有三个叶子节点的树,其中如果某人做了手术,他们会复发;如果他们没有做手术并且年龄超过 18 岁,则不会复发:
请注意,医学研究表明,年轻男性肩膀脱位复发的几率最高。这里的数据集是一个玩具示例,并不反映现实。
最终的决策树
Extra Trees
创建随机森林集成中的另一种方法是 Extra Trees(极度随机化树)。与前一种方法的主要区别在于,特征和分割点的组合不需要是最优的。相反,多个分割点会被随机生成,每个可用特征生成一个。然后选择这些生成的分割点中的最佳点。该算法构造树的步骤如下:
-
选择每个节点将要考虑的特征数m以及分割节点所需的最小样本数n
-
对于每个基础学习器,执行以下操作:
-
创建一个自助法训练样本
-
选择要分割的节点(该节点必须至少包含n个样本)
-
随机选择m个特征
-
随机生成m个分割点,值介于每个特征的最小值和最大值之间
-
选择这些分割点中的最佳点
-
将节点分割成两个节点,并从步骤 2-2 开始重复,直到没有可用节点为止
-
创建森林
通过使用任何有效的随机化方法创建多棵树,我们基本上就创建了一个森林,这也是该算法名称的由来。在生成集成的树之后,必须将它们的预测结果结合起来,才能形成一个有效的集成。这通常通过分类问题的多数投票法和回归问题的平均法来实现。与随机森林相关的超参数有许多,例如每个节点分割时考虑的特征数、森林中的树木数量以及单棵树的大小。如前所述,考虑的特征数量的一个良好起始点如下:
-
对于分类问题,选择总特征数的平方根
-
对于回归问题,选择总特征数的三分之一
总树的数量可以手动微调,因为随着该数量的增加,集成的误差会收敛到一个极限。可以利用袋外误差来找到最佳值。最后,每棵树的大小可能是过拟合的决定性因素。因此,如果观察到过拟合,应减小树的大小。
分析森林
随机森林提供了许多其他方法无法轻易提供的关于底层数据集的信息。一个突出的例子是数据集中每个特征的重要性。估计特征重要性的一种方法是使用基尼指数计算每棵树的每个节点,并比较每个特征的累计值。另一种方法则使用袋外样本。首先,记录所有基学习器的袋外准确度。然后,选择一个特征,并在袋外样本中打乱该特征的值。这会导致袋外样本集具有与原始集相同的统计特性,但任何可能与目标相关的预测能力都会被移除(因为此时所选特征的值与目标之间的相关性为零)。通过比较原始数据集与部分随机化数据集之间的准确度差异,可以作为评估所选特征重要性的标准。
关于偏差与方差,尽管随机森林似乎能够很好地应对这两者,但它们显然并非完全免疫。当可用特征数量很大,但只有少数与目标相关时,可能会出现偏差。在使用推荐的每次划分时考虑的特征数量(例如,总特征数的平方根)时,相关特征被选中的概率可能较小。以下图表展示了作为相关特征和无关特征函数的情况下,至少选中一个相关特征的概率(当每次划分时考虑总特征数的平方根):
选择至少一个相关特征的概率与相关特征和无关特征数量的关系
基尼指数衡量错误分类的频率,假设随机抽样的实例会根据特定节点所规定的标签分布进行分类。
方差在随机森林中也可能出现,尽管该方法对其有足够的抵抗力。通常,当允许单个树完全生长时,会出现方差。我们之前提到过,随着树木数量的增加,误差会接近某个极限。虽然这一说法依然成立,但该极限本身可能会过拟合数据。在这种情况下,限制树的大小(例如,通过增加每个叶节点的最小样本数或减少最大深度)可能会有所帮助。
优势与劣势
随机森林是一种非常强大的集成学习方法,能够减少偏差和方差,类似于提升方法。此外,该算法的性质使得它在训练和预测过程中都可以完全并行化。这相较于提升方法,尤其是在处理大数据集时,是一个显著的优势。此外,与提升技术(尤其是 XGBoost)相比,随机森林需要更少的超参数微调。
随机森林的主要弱点是它们对类别不平衡的敏感性,以及我们之前提到的问题,即训练集中相关特征和无关特征的比例较低。此外,当数据包含低级非线性模式(例如原始高分辨率图像识别)时,随机森林通常会被深度神经网络超越。最后,当使用非常大的数据集并且树深度没有限制时,随机森林的计算成本可能非常高。
使用 scikit-learn
scikit-learn 实现了传统的随机森林树和 Extra Trees。在本节中,我们将提供使用 scikit-learn 实现的两种算法的基本回归和分类示例。
随机森林分类
随机森林分类类在 RandomForestClassifier
中实现,位于 sklearn.ensemble
包下。它有许多参数,例如集成的大小、最大树深度、构建或拆分节点所需的样本数等。
在这个示例中,我们将尝试使用随机森林分类集成来对手写数字数据集进行分类。像往常一样,我们加载所需的类和数据,并为随机数生成器设置种子:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics
import numpy as np
digits = load_digits()
train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
np.random.seed(123456)
接下来,我们通过设置 n_estimators
和 n_jobs
参数来创建集成模型。这些参数决定了将生成的树的数量和将要运行的并行作业数。我们使用 fit
函数训练集成,并通过测量其准确率在测试集上进行评估:
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 500
ensemble = RandomForestClassifier(n_estimators=ensemble_size, n_jobs=4)
# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)
# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 5 ---
# Print the accuracy
print('Random Forest: %.2f' % ensemble_acc)
该分类器能够实现 93% 的准确率,甚至高于之前表现最好的方法 XGBoost(见第六章,Boosting)。我们可以通过绘制验证曲线(来自第二章,Getting Started with Ensemble Learning),来可视化我们之前提到的误差极限的近似值。我们测试了 10、50、100、150、200、250、300、350 和 400 棵树的集成大小。曲线如下图所示。我们可以看到,集成模型的 10 倍交叉验证误差接近 96%:
不同集成大小的验证曲线
随机森林回归
Scikit-learn 还在 RandomForestRegressor
类中实现了用于回归的随机森林。它也具有高度的可参数化性,具有与集成整体以及单个树相关的超参数。在这里,我们将生成一个集成模型来对糖尿病回归数据集进行建模。代码遵循加载库和数据、创建集成模型并调用 fit
和 predict
方法的标准过程,同时计算 MSE 和 R² 值:
# --- SECTION 1 ---
# Libraries and data loading
from copy import deepcopy
from sklearn.datasets import load_diabetes
from sklearn.ensemble import RandomForestRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 100
ensemble = RandomForestRegressor(n_estimators=ensemble_size, n_jobs=4)
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble.fit(train_x, train_y)
predictions = ensemble.predict(test_x)
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, predictions)
mse = metrics.mean_squared_error(test_y, predictions)
print('Random Forest:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
该集成方法能够在测试集上实现 0.51 的 R 方和 2722.67 的 MSE。由于训练集上的 R 方和 MSE 分别为 0.92 和 468.13,因此可以合理推断该集成方法存在过拟合。这是一个误差限制过拟合的例子,因此我们需要调节单个树木以获得更好的结果。通过减少每个叶节点所需的最小样本数(将其从默认值 2 增加到 20)通过 min_samples_leaf=20
,我们能够将 R 方提高到 0.6,并将 MSE 降低到 2206.6。此外,通过将集成大小增加到 1000,R 方进一步提高到 0.61,MSE 进一步降低到 2158.73。
Extra Trees 用于分类
除了传统的随机森林,scikit-learn 还实现了 Extra Trees。分类实现位于 ExtraTreesClassifier
,在 sklearn.ensemble
包中。这里,我们重复手写数字识别的例子,使用 Extra Trees 分类器:
# --- SECTION 1 ---
# Libraries and data loading
from sklearn.datasets import load_digits
from sklearn.ensemble import ExtraTreesClassifier
from sklearn import metrics
import numpy as np
digits = load_digits()
train_size = 1500
train_x, train_y = digits.data[:train_size], digits.target[:train_size]
test_x, test_y = digits.data[train_size:], digits.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 500
ensemble = ExtraTreesClassifier(n_estimators=ensemble_size, n_jobs=4)
# --- SECTION 3 ---
# Train the ensemble
ensemble.fit(train_x, train_y)
# --- SECTION 4 ---
# Evaluate the ensemble
ensemble_predictions = ensemble.predict(test_x)
ensemble_acc = metrics.accuracy_score(test_y, ensemble_predictions)
# --- SECTION 5 ---
# Print the accuracy
print('Extra Tree Forest: %.2f' % ensemble_acc)
如您所见,唯一的不同之处在于将 RandomForestClassifier
切换为 ExtraTreesClassifier
。尽管如此,该集成方法仍然实现了更高的测试准确率,达到了 94%。我们再次为多个集成大小创建了验证曲线,结果如下所示。该集成方法的 10 折交叉验证误差限制大约为 97%,进一步确认了它优于传统的随机森林方法:
Extra Trees 在多个集成大小下的验证曲线
Extra Trees 回归
最后,我们展示了 Extra Trees 的回归实现,位于 ExtraTreesRegressor
中。在以下代码中,我们重复之前展示的使用 Extra Trees 回归版本对糖尿病数据集建模的示例:
# --- SECTION 1 ---
# Libraries and data loading
from copy import deepcopy
from sklearn.datasets import load_diabetes
from sklearn.ensemble import ExtraTreesRegressor
from sklearn import metrics
import numpy as np
diabetes = load_diabetes()
train_size = 400
train_x, train_y = diabetes.data[:train_size], diabetes.target[:train_size]
test_x, test_y = diabetes.data[train_size:], diabetes.target[train_size:]
np.random.seed(123456)
# --- SECTION 2 ---
# Create the ensemble
ensemble_size = 100
ensemble = ExtraTreesRegressor(n_estimators=ensemble_size, n_jobs=4)
# --- SECTION 3 ---
# Evaluate the ensemble
ensemble.fit(train_x, train_y)
predictions = ensemble.predict(test_x)
# --- SECTION 4 ---
# Print the metrics
r2 = metrics.r2_score(test_y, predictions)
mse = metrics.mean_squared_error(test_y, predictions)
print('Extra Trees:')
print('R-squared: %.2f' % r2)
print('MSE: %.2f' % mse)
与分类示例类似,Extra Trees 通过实现 0.55 的测试 R 方(比随机森林高 0.04)和 2479.18 的 MSE(差异为 243.49)来超越传统的随机森林。不过,集成方法似乎仍然出现过拟合,因为它能够完美预测样本内数据。通过设置 min_samples_leaf=10
和将集成大小设置为 1000,我们能够使 R 方达到 0.62,MSE 降低到 2114。
总结
在本章中,我们讨论了随机森林,这是一种利用决策树作为基本学习器的集成方法。我们介绍了两种构建树的基本方法:传统的随机森林方法,其中每次分裂时考虑特征的子集,以及 Extra Trees 方法,在该方法中,分裂点几乎是随机选择的。我们讨论了集成方法的基本特征。此外,我们还展示了使用 scikit-learn 实现的随机森林和 Extra Trees 的回归和分类示例。本章的关键点总结如下。
随机森林使用装袋技术来为其基学习器创建训练集。在每个节点,每棵树只考虑一部分可用特征,并计算最佳特征/分割点组合。每个点考虑的特征数量是一个必须调整的超参数。良好的起点如下:
-
分类问题的总参数平方根
-
回归问题的总参数的三分之一
极端随机树和随机森林对每个基学习器使用整个数据集。在极端随机树和随机森林中,每个特征子集的每个节点不再计算最佳特征/分割点组合,而是为子集中的每个特征生成一个随机分割点,并选择最佳的。随机森林可以提供关于每个特征重要性的信息。虽然相对抗过拟合,但随机森林并非免疫。当相关特征与不相关特征的比例较低时,随机森林可能表现出高偏差。随机森林可能表现出高方差,尽管集成规模并不会加剧问题。在下一章中,我们将介绍可以应用于无监督学习方法(聚类)的集成学习技术。
第四部分:聚类
本节将介绍集成方法在聚类应用中的使用。
本节包含以下章节:
- 第八章,聚类
第八章:聚类
其中一种最广泛使用的无监督学习方法是聚类。聚类旨在揭示未标记数据中的结构。其目标是将数据实例分组,使得同一聚类中的实例之间相似度高,而不同聚类之间的实例相似度低。与有监督学习方法类似,聚类也能通过结合多个基本学习器来受益。在本章中,我们将介绍 K-means 聚类算法;这是一种简单且广泛使用的聚类算法。此外,我们还将讨论如何通过集成方法来提升该算法的性能。最后,我们将使用 OpenEnsembles,这是一个兼容 scikit-learn 的 Python 库,能够实现集成聚类。本章的主要内容如下:
-
K-means 算法的工作原理
-
优势与劣势
-
集成方法如何提升其性能
-
使用 OpenEnsembles 创建聚类集成方法
技术要求
你需要具备机器学习技术和算法的基础知识。此外,还需要了解 Python 的约定和语法。最后,熟悉 NumPy 库将极大帮助读者理解一些自定义算法实现。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter08
查看以下视频,了解代码的实际操作:bit.ly/2YYzniq
。
共识聚类
共识聚类是集成学习在聚类方法中应用时的别名。在聚类中,每个基本学习器都会为每个实例分配一个标签,尽管这个标签并不依赖于特定的目标。相反,基本学习器会生成多个聚类,并将每个实例分配到一个聚类中。标签就是聚类本身。如后面所示,由同一算法生成的两个基本学习器可能会生成不同的聚类。因此,将它们的聚类预测结果结合起来并不像回归或分类预测结果的结合那么直接。
层次聚类
层次聚类最初会根据数据集中实例的数量创建相同数量的聚类。每个聚类仅包含一个实例。之后,算法会反复找到两个距离最小的聚类(例如,欧几里得距离),并将它们合并为一个新的聚类。直到只剩下一个聚类时,过程结束。该方法的输出是一个树状图,展示了实例是如何按层次组织的。以下图示为例:
树状图示例
K-means 聚类
K-means 是一种相对简单有效的聚类数据的方法。其主要思想是,从K个点开始作为初始聚类中心,然后将每个实例分配给最近的聚类中心。接着,重新计算这些中心,作为各自成员的均值。这个过程会重复,直到聚类中心不再变化。主要步骤如下:
-
选择聚类的数量,K
-
选择K个随机实例作为初始聚类中心
-
将每个实例分配给最近的聚类中心
-
重新计算聚类中心,作为每个聚类成员的均值
-
如果新的中心与上一个不同,则返回到步骤 3
如下所示是一个图形示例。经过四次迭代,算法收敛:
对一个玩具数据集进行的前四次迭代。星号表示聚类中心
优势与劣势
K-means 是一个简单的算法,既容易理解,也容易实现。此外,它通常会比较快速地收敛,所需的计算资源较少。然而,它也有一些缺点。第一个缺点是对初始条件的敏感性。根据选择作为初始聚类中心的样本,可能需要更多的迭代才能收敛。例如,在以下图示中,我们呈现了三个初始点,它使得算法处于不利位置。事实上,在第三次迭代中,两个聚类中心恰好重合:
一个不幸的初始聚类中心的示例
因此,算法不会确定性地生成聚类。另一个主要问题是聚类的数量。这是一个需要数据分析师选择的参数。通常这个问题有三种不同的解决方案。第一种是针对一些有先验知识的问题。例如,数据集需要揭示一些已知事物的结构,比如,如何根据运动员的统计数据,找出哪些因素导致他们在一个赛季中表现的提高?在这个例子中,运动教练可能会建议,运动员的表现实际上要么大幅提升,要么保持不变,要么恶化。因此,分析师可以选择 3 作为聚类的数量。另一种可能的解决方案是通过实验不同的K值,并衡量每个值的适用性。这种方法不需要关于问题领域的任何先验知识,但引入了衡量每个解决方案适用性的问题。我们将在本章的其余部分看到如何解决这些问题。
使用 scikit-learn
scikit-learn 提供了多种可用的聚类技术。在这里,我们简要介绍如何使用 K-means 算法。该算法在 KMeans
类中实现,属于 sklearn.cluster
包。该包包含了 scikit-learn 中所有可用的聚类算法。在本章中,我们将主要使用 K-means,因为它是最直观的算法之一。此外,本章中使用的技术可以应用于几乎所有聚类算法。在本次实验中,我们将尝试对乳腺癌数据进行聚类,以探索区分恶性病例和良性病例的可能性。为了更好地可视化结果,我们将首先进行 t-分布随机邻域嵌入(t-SNE)分解,并使用二维嵌入作为特征。接下来,我们先加载所需的数据和库,并设置 NumPy 随机数生成器的种子:
你可以在 lvdmaaten.github.io/tsne/
阅读更多关于 t-SNE 的内容。
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import load_breast_cancer
from sklearn.manifold import TSNE
np.random.seed(123456)
bc = load_breast_cancer()
接下来,我们实例化 t-SNE,并转换我们的数据。我们绘制数据,以便直观检查和审视数据结构:
data = tsne.fit_transform(bc.dataa)
reds = bc.target == 0
blues = bc.target == 1
plt.scatter(data[reds, 0], data[reds, 1], label='malignant')
plt.scatter(data[blues, 0], data[blues, 1], label='benign')
plt.xlabel('1st Component')
plt.ylabel('2nd Component')
plt.title('Breast Cancer dataa')
plt.legend()
上述代码生成了以下图表。我们观察到两个不同的区域。蓝色点所代表的区域表示嵌入值,暗示着肿瘤是恶性的风险较高:
乳腺癌数据的两个嵌入(成分)图
由于我们已经识别出数据中存在某些结构,因此我们将尝试使用 K-means 聚类来进行建模。直觉上,我们假设两个簇就足够了,因为我们试图分离两个不同的区域,并且我们知道数据集有两个类别。尽管如此,我们还将尝试使用四个和六个簇,因为它们可能能提供更多的数据洞察。我们将通过衡量每个类别在每个簇中的分布比例来评估簇的质量。为此,我们通过填充 classified
字典来实现。每个键对应一个簇。每个键还指向一个二级字典,记录了特定簇中恶性和良性病例的数量。此外,我们还会绘制簇分配图,因为我们想看到数据在簇之间的分布情况:
plt.figure()
plt.title('2, 4, and 6 clusters.')
for clusters in [2, 4, 6]:
km = KMeans(n_clusters=clusters)
preds = km.fit_predict(data)
plt.subplot(1, 3, clusters/2)
plt.scatter(*zip(*data), c=preds)
classified = {x: {'m': 0, 'b': 0} for x in range(clusters)}
for i in range(len(data)):
cluster = preds[i]
label = bc.target[i]
label = 'm' if label == 0 else 'b'
classified[cluster][label] = classified[cluster][label]+1
print('-'*40)
for c in classified:
print('Cluster %d. Malignant percentage: ' % c, end=' ')
print(classified[c], end=' ')
print('%.3f' % (classified[c]['m'] /
(classified[c]['m'] + classified[c]['b'])))
结果显示在下表和图中:
簇 | 恶性 | 良性 | 恶性百分比 |
---|---|---|---|
2 个簇 | |||
0 | 206 | 97 | 0.68 |
1 | 6 | 260 | 0.023 |
4 个簇 | |||
0 | 2 | 124 | 0.016 |
1 | 134 | 1 | 0.993 |
2 | 72 | 96 | 0.429 |
3 | 4 | 136 | 0.029 |
6 个簇 | |||
0 | 2 | 94 | 0.021 |
1 | 81 | 10 | 0.89 |
2 | 4 | 88 | 0.043 |
3 | 36 | 87 | 0.0293 |
4 | 0 | 78 | 0 |
5 | 89 | 0 | 1 |
恶性和良性病例在簇中的分布
我们观察到,尽管算法没有关于标签的信息,它仍然能够相当有效地分离属于每个类别的实例:
每个实例的簇分配;2、4 和 6 个簇
此外,我们看到,随着簇数的增加,分配到恶性或良性簇的实例数量没有增加,但这些区域的分离性更强。这使得粒度更细,可以更准确地预测所选实例属于哪个类别的概率。如果我们在不转换数据的情况下重复实验,得到以下结果:
簇 | 恶性 | 良性 | 恶性百分比 |
---|---|---|---|
2 个簇 | |||
0 | 82 | 356 | 0.187 |
1 | 130 | 1 | 0.992 |
4 个簇 | |||
0 | 6 | 262 | 0.022 |
1 | 100 | 1 | 0.99 |
2 | 19 | 0 | 1 |
3 | 87 | 94 | 0.481 |
6 个簇 | |||
0 | 37 | 145 | 0.203 |
1 | 37 | 0 | 1 |
2 | 11 | 0 | 1 |
3 | 62 | 9 | 0.873 |
4 | 5 | 203 | 0.024 |
5 | 60 | 0 | 1 |
没有 t-SNE 转换的数据聚类结果
还有两种度量可以用来确定聚类质量。对于已知真实标签的数据(本质上是有标签的数据),同质性度量每个簇中由单一类别主导的比例。对于没有已知真实标签的数据,轮廓系数度量簇内的凝聚力和簇间的可分离性。这些度量在 scikit-learn 的 metrics
包中由 silhouette_score
和 homogeneity_score
函数实现。每种方法的两个度量如以下表格所示。同质性对于转换后的数据较高,但轮廓系数较低。
这是预期的,因为转换后的数据只有两个维度,因此实例之间的可能距离变小:
度量 | 簇 | 原始数据 | 转换后的数据 |
---|---|---|---|
同质性 | 2 | 0.422 | 0.418 |
4 | 0.575 | 0.603 | |
6 | 0.620 | 0.648 | |
轮廓系数 | 2 | 0.697 | 0.500 |
4 | 0.533 | 0.577 | |
6 | 0.481 | 0.555 |
原始数据和转换数据的同质性与轮廓系数
使用投票
投票可以用来结合同一数据集的不同聚类结果。这类似于监督学习中的投票,每个模型(基础学习器)通过投票对最终结果做出贡献。在此出现了一个问题:如何链接来自两个不同聚类的簇。由于每个模型会生成不同的簇和不同的中心,我们需要将来自不同模型的相似簇链接起来。通过将共享最多实例的簇链接在一起,可以实现这一点。例如,假设对于某个特定数据集,发生了以下的聚类表格和图形:
三种不同的聚类结果
下表展示了每个实例在三种不同聚类中的簇分配情况。
实例 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
聚类 1 | 0 | 0 | 2 | 2 | 2 | 0 | 0 | 1 | 0 | 2 |
聚类 2 | 1 | 1 | 2 | 2 | 2 | 1 | 0 | 1 | 1 | 2 |
聚类 3 | 0 | 0 | 2 | 2 | 2 | 1 | 0 | 1 | 1 | 2 |
每个实例的簇成员资格
使用之前的映射,我们可以计算每个实例的共现矩阵。该矩阵表示一对实例被分配到同一个簇的次数:
实例 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 3 | 3 | 0 | 0 | 0 | 2 | 2 | 1 | 2 | 0 |
2 | 3 | 3 | 0 | 0 | 0 | 2 | 2 | 1 | 2 | 0 |
3 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | 0 | 0 | 3 |
4 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | 0 | 0 | 3 |
5 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | 0 | 0 | 3 |
6 | 2 | 2 | 0 | 0 | 0 | 3 | 1 | 0 | 3 | 0 |
7 | 2 | 2 | 0 | 0 | 0 | 1 | 3 | 0 | 1 | 0 |
8 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 3 | 2 | 0 |
9 | 2 | 2 | 0 | 0 | 0 | 3 | 1 | 2 | 3 | 0 |
10 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | 0 | 0 | 3 |
上述示例的共现矩阵
通过将每个元素除以基础学习器的数量,并将值大于 0.5 的样本聚集在一起,我们得到了以下的簇分配:
实例 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
投票聚类 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
投票聚类成员资格
如所示,聚类结果更为稳定。进一步观察可以发现,两个簇对于这个数据集来说是足够的。通过绘制数据及其簇成员资格,我们可以看到有两个明显的组别,这正是投票集成所能建模的,尽管每个基础学习器生成了三个不同的聚类中心:
投票集成的最终簇成员资格
使用 OpenEnsembles
OpenEnsembles 是一个专注于聚类集成方法的 Python 库。在本节中,我们将展示如何使用它并利用它对我们的示例数据集进行聚类。为了安装该库,必须在终端执行 pip install openensembles
命令。尽管它依赖于 scikit-learn,但其接口不同。一个主要的区别是数据必须作为 data
类传递,该类由 OpenEnsembles 实现。构造函数有两个输入参数:一个包含数据的 pandas DataFrame
和一个包含特征名称的列表:
# --- SECTION 1 ---
# Libraries and data loading
import openensembles as oe
import pandas as pd
import sklearn.metrics
from sklearn.datasets import load_breast_cancer
bc = load_breast_cancer()
# --- SECTION 2 ---
# Create the data object
cluster_data = oe.data(pd.DataFrame(bc.data), bc.feature_names)
为了创建一个cluster
集成,创建一个cluster
类对象,并将数据作为参数传入:
ensemble = oe.cluster(cluster_data)
在这个例子中,我们将计算多个K值和集成大小的同质性得分。为了将一个基学习器添加到集成中,必须调用cluster
类的cluster
方法。该方法接受以下参数:source_name
,表示源数据矩阵的名称,algorithm
,决定基学习器将使用的算法,output_name
,将作为字典键来访问特定基学习器的结果,和K
,表示特定基学习器的簇数。最后,为了通过多数投票计算最终的簇成员,必须调用finish_majority_vote
方法。唯一必须指定的参数是threshold
值:
# --- SECTION 3 ---
# Create the ensembles and calculate the homogeneity score
for K in [2, 3, 4, 5, 6, 7]:
for ensemble_size in [3, 4, 5]:
ensemble = oe.cluster(cluster_data)
for i in range(ensemble_size):
name = f'kmeans_{ensemble_size}_{i}'
ensemble.cluster('parent', 'kmeans', name, K)
preds = ensemble.finish_majority_vote(threshold=0.5)
print(f'K: {K}, size {ensemble_size}:', end=' ')
print('%.2f' % sklearn.metrics.homogeneity_score(
bc.target, preds.labels['majority_vote']))
显然,五个簇对于所有三种集成大小来说都产生了最佳结果。结果总结如下表所示:
K | 大小 | 同质性 |
---|---|---|
2 | 3 | 0.42 |
2 | 4 | 0.42 |
2 | 5 | 0.42 |
3 | 3 | 0.45 |
3 | 4 | 0.47 |
3 | 5 | 0.47 |
4 | 3 | 0.58 |
4 | 4 | 0.58 |
4 | 5 | 0.58 |
5 | 3 | 0.6 |
5 | 4 | 0.61 |
5 | 5 | 0.6 |
6 | 3 | 0.35 |
6 | 4 | 0.47 |
6 | 5 | 0.35 |
7 | 3 | 0.27 |
7 | 4 | 0.63 |
7 | 5 | 0.37 |
OpenEnsembles 胸癌数据集的多数投票簇同质性
如果我们将数据转换为两个 t-SNE 嵌入,并重复实验,则得到以下同质性得分:
K | 大小 | 同质性 |
---|---|---|
2 | 3 | 0.42 |
2 | 4 | 0.42 |
2 | 5 | 0.42 |
3 | 3 | 0.59 |
3 | 4 | 0.59 |
3 | 5 | 0.59 |
4 | 3 | 0.61 |
4 | 4 | 0.61 |
4 | 5 | 0.61 |
5 | 3 | 0.61 |
5 | 4 | 0.61 |
5 | 5 | 0.61 |
6 | 3 | 0.65 |
6 | 4 | 0.65 |
6 | 5 | 0.65 |
7 | 3 | 0.66 |
7 | 4 | 0.66 |
7 | 5 | 0.66 |
转换后的胸癌数据集的多数投票簇同质性
使用图闭合和共现链路
还有两种可以用来组合簇结果的方法,分别是图闭合和共现链路。这里,我们展示了如何使用 OpenEnsembles 创建这两种类型的集成。
图闭合
图闭包通过共现矩阵创建图形。每个元素(实例对)都被视为一个节点。具有大于阈值的对将通过边连接。接下来,根据指定的大小(由团内节点的数量指定),会发生团的形成。团是图的节点的子集,每两个节点之间都有边连接。最后,团会组合成独特的聚类。在 OpenEnsembles 中,它通过finish_graph_closure
函数在cluster
类中实现。clique_size
参数确定每个团中的节点数量。threshold
参数确定一对实例必须具有的最小共现值,以便通过图中的边连接。与之前的示例类似,我们将使用图闭包来对乳腺癌数据集进行聚类。请注意,代码中唯一的变化是使用finish_graph_closure
,而不是finish_majority_vote
。首先,我们加载库和数据集,并创建 OpenEnsembles 数据对象:
# --- SECTION 1 ---
# Libraries and data loading
import openensembles as oe
import pandas as pd
import sklearn.metrics
from sklearn.datasets import load_breast_cancer
bc = load_breast_cancer()
# --- SECTION 2 ---
# Create the data object
cluster_data = oe.data(pd.DataFrame(bc.data), bc.feature_names)
然后,我们创建集成并使用graph_closure
来组合聚类结果。请注意,字典的键也更改为'graph_closure'
:
# --- SECTION 3 ---
# Create the ensembles and calculate the homogeneity score
for K in [2, 3, 4, 5, 6, 7]:
for ensemble_size in [3, 4, 5]:
ensemble = oe.cluster(cluster_data)
for i in range(ensemble_size):
name = f'kmeans_{ensemble_size}_{i}'
ensemble.cluster('parent', 'kmeans', name, K)
preds = ensemble.finish_majority_vote(threshold=0.5)
print(f'K: {K}, size {ensemble_size}:', end=' ')
print('%.2f' % sklearn.metrics.homogeneity_score(
bc.target, preds.labels['majority_vote']))
K和集成大小对聚类质量的影响类似于多数投票,尽管它没有达到相同的性能水平。结果如以下表所示:
K | 大小 | 同质性 |
---|---|---|
2 | 3 | 0.42 |
2 | 4 | 0.42 |
2 | 5 | 0.42 |
3 | 3 | 0.47 |
3 | 4 | 0 |
3 | 5 | 0.47 |
4 | 3 | 0.58 |
4 | 4 | 0.58 |
4 | 5 | 0.58 |
5 | 3 | 0.6 |
5 | 4 | 0.5 |
5 | 5 | 0.5 |
6 | 3 | 0.6 |
6 | 4 | 0.03 |
6 | 5 | 0.62 |
7 | 3 | 0.63 |
7 | 4 | 0.27 |
7 | 5 | 0.27 |
图闭包聚类在原始乳腺癌数据上的同质性
共现矩阵连接
共现矩阵连接将共现矩阵视为实例之间的距离矩阵,并利用这些距离执行层次聚类。当矩阵中没有元素的值大于阈值时,聚类过程停止。再次,我们重复示例。我们使用finish_co_occ_linkage
函数,利用threshold=0.5
执行共现矩阵连接,并使用'co_occ_linkage'
键来访问结果:
# --- SECTION 1 ---
# Libraries and data loading
import openensembles as oe
import pandas as pd
import sklearn.metrics
from sklearn.datasets import load_breast_cancer
bc = load_breast_cancer()
# --- SECTION 2 ---
# Create the data object
cluster_data = oe.data(pd.DataFrame(bc.data), bc.feature_names)
# --- SECTION 3 ---
# Create the ensembles and calculate the homogeneity score
for K in [2, 3, 4, 5, 6, 7]:
for ensemble_size in [3, 4, 5]:
ensemble = oe.cluster(cluster_data)
for i in range(ensemble_size):
name = f'kmeans_{ensemble_size}_{i}'
ensemble.cluster('parent', 'kmeans', name, K)
preds = ensemble.finish_co_occ_linkage(threshold=0.5)
print(f'K: {K}, size {ensemble_size}:', end=' ')
print('%.2f' % sklearn.metrics.homogeneity_score(
bc.target, preds.labels['co_occ_linkage']))
以下表总结了结果。请注意,它优于其他两种方法。此外,结果更加稳定,并且所需执行时间比其他两种方法少:
K | 大小 | 同质性 |
---|---|---|
2 | 3 | 0.42 |
2 | 4 | 0.42 |
2 | 5 | 0.42 |
3 | 3 | 0.47 |
3 | 4 | 0.47 |
3 | 5 | 0.45 |
4 | 3 | 0.58 |
4 | 4 | 0.58 |
4 | 5 | 0.58 |
5 | 3 | 0.6 |
5 | 4 | 0.6 |
5 | 5 | 0.6 |
6 | 3 | 0.59 |
6 | 4 | 0.62 |
6 | 5 | 0.62 |
7 | 3 | 0.62 |
7 | 4 | 0.63 |
7 | 5 | 0.63 |
原始乳腺癌数据集上共现聚类连接的同质性结果
小结
本章介绍了 K-means 聚类算法和聚类集成方法。我们解释了如何使用多数投票方法来结合集成中的聚类分配,并如何使其超越单个基础学习器。此外,我们还介绍了专门用于聚类集成的 OpenEnsembles Python 库。本章可以总结如下。
K-means 创建 K 个聚类,并通过迭代地将每个实例分配到各个聚类中,使得每个聚类的中心成为其成员的均值。它对初始条件和选定的聚类数目敏感。多数投票可以帮助克服该算法的缺点。多数投票 将具有高共现的实例聚集在一起。共现矩阵 显示了一对实例被同一基础学习器分配到同一聚类的频率。图闭包 使用共现矩阵来创建图,并基于团簇对数据进行聚类。共现连接 使用一种特定的聚类算法——层次聚类(聚合型),将共现矩阵视为成对距离矩阵。在下一章中,我们将尝试利用本书中介绍的所有集成学习技术,以对欺诈信用卡交易进行分类。
第五部分:现实世界的应用
在本节中,我们将介绍集成学习在各种实际机器学习任务中的应用。
本节包括以下章节:
-
第九章,分类欺诈交易
-
第十章,预测比特币价格
-
第十一章,评估 Twitter 上的情感
-
第十二章,使用 Keras 推荐电影
-
第十三章,聚类世界幸福感
第九章:分类欺诈交易
在本章中,我们将尝试对 2013 年 9 月期间发生的欧洲信用卡持有者的信用卡交易数据集进行欺诈交易分类。该数据集的主要问题是,欺诈交易的数量非常少,相比数据集的规模几乎可以忽略不计。这类数据集被称为不平衡数据集,因为每个标签的百分比不相等。我们将尝试创建能够分类我们特定数据集的集成方法,该数据集包含极少数的欺诈交易。
本章将涵盖以下主题:
-
熟悉数据集
-
探索性分析
-
投票法
-
堆叠
-
自助法
-
提升法
-
使用随机森林
-
集成方法的比较分析
技术要求
你需要具备机器学习技术和算法的基础知识。此外,还需要了解 Python 的约定和语法。最后,熟悉 NumPy 库将有助于读者理解一些自定义算法的实现。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter09
请查看以下视频,了解代码的实际应用:bit.ly/2ShwarF
.
熟悉数据集
该数据集最初在 Andrea Dal Pozzolo 的博士论文《用于信用卡欺诈检测的自适应机器学习》中使用,现已由其作者公开发布供公众使用(www.ulb.ac.be/di/map/adalpozz/data/creditcard.Rdata)。该数据集包含超过 284,000 个实例,但其中只有 492 个欺诈实例(几乎为 0.17%)。
目标类别值为 0 时表示交易不是欺诈,1 时表示交易是欺诈。该数据集的特征是一些主成分,因为数据集已通过主成分分析(PCA)进行转化,以保留数据的机密性。数据集的特征包含 28 个 PCA 组件,以及交易金额和从数据集中的第一次交易到当前交易的时间。以下是数据集的描述性统计:
特征 | 时间 | V1 | V2 | V3 | V4 |
---|---|---|---|---|---|
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 94,813.86 | 1.17E-15 | 3.42E-16 | -1.37E-15 | 2.09E-15 |
标准差 | 47,488.15 | 1.96 | 1.65 | 1.52 | 1.42 |
最小值 | 0.00 | -56.41 | -72.72 | -48.33 | -5.68 |
最大值 | 172,792.00 | 2.45 | 22.06 | 9.38 | 16.88 |
特征 | V5 | V6 | V7 | V8 | V9 |
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 9.60E-16 | 1.49E-15 | -5.56E-16 | 1.18E-16 | -2.41E-15 |
标准差 | 1.38 | 1.33 | 1.24 | 1.19 | 1.10 |
最小值 | -113.74 | -26.16 | -43.56 | -73.22 | -13.43 |
最大值 | 34.80 | 73.30 | 120.59 | 20.01 | 15.59 |
特征 | V10 | V11 | V12 | V13 | V14 |
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 2.24E-15 | 1.67E-15 | -1.25E-15 | 8.18E-16 | 1.21E-15 |
标准差 | 1.09 | 1.02 | 1.00 | 1.00 | 0.96 |
最小值 | -24.59 | -4.80 | -18.68 | -5.79 | -19.21 |
最大值 | 23.75 | 12.02 | 7.85 | 7.13 | 10.53 |
特征 | V15 | V16 | V17 | V18 | V19 |
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 4.91E-15 | 1.44E-15 | -3.80E-16 | 9.57E-16 | 1.04E-15 |
标准差 | 0.92 | 0.88 | 0.85 | 0.84 | 0.81 |
最小值 | -4.50 | -14.13 | -25.16 | -9.50 | -7.21 |
最大值 | 8.88 | 17.32 | 9.25 | 5.04 | 5.59 |
特征 | V20 | V21 | V22 | V23 | V24 |
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 6.41E-16 | 1.66E-16 | -3.44E-16 | 2.58E-16 | 4.47E-15 |
标准差 | 0.77 | 0.73 | 0.73 | 0.62 | 0.61 |
最小值 | -54.50 | -34.83 | -10.93 | -44.81 | -2.84 |
最大值 | 39.42 | 27.20 | 10.50 | 22.53 | 4.58 |
特征 | V25 | V26 | V27 | V28 | 金额 |
计数 | 284,807 | 284,807 | 284,807 | 284,807 | 284,807 |
均值 | 5.34E-16 | 1.69E-15 | -3.67E-16 | -1.22E-16 | 88.34962 |
标准差 | 0.52 | 0.48 | 0.40 | 0.33 | 250.12 |
最小值 | -10.30 | -2.60 | -22.57 | -15.43 | 0.00 |
最大值 | 7.52 | 3.52 | 31.61 | 33.85 | 25,691.16 |
信用卡交易数据集的描述性统计
探索性分析
数据集的一个重要特点是没有缺失值,这一点可以从计数统计中看出。所有特征都有相同数量的值。另一个重要方面是大多数特征已经进行了归一化处理。这是因为数据应用了 PCA(主成分分析)。PCA 在将数据分解为主成分之前会先进行归一化处理。唯一两个没有进行归一化的特征是时间和金额。以下是每个特征的直方图:
数据集特征的直方图
通过更加细致地观察每笔交易的时间和金额,我们发现,在第一次交易后的 75,000 秒到 125,000 秒之间,交易频率出现了突然下降(大约 13 小时)。这很可能是由于日常时间周期(例如,夜间大多数商店关闭)。每笔交易金额的直方图如下所示,采用对数尺度。可以明显看出,大部分交易金额较小,平均值接近 88.00 欧元:
金额直方图,对数尺度的y-轴
为了避免特征之间的权重分布不均问题,我们将对金额和时间这两个特征进行标准化。比如使用距离度量的算法(如 K 最近邻算法)在特征未正确缩放时,性能可能会下降。以下是标准化特征的直方图。请注意,标准化将变量转换为均值接近 0,标准差为 1:
标准化金额直方图
以下图表显示了标准化时间的直方图。我们可以看到,它并未影响夜间交易量的下降:
标准化时间直方图
评估方法
由于我们的数据集高度倾斜(即具有较高的类别不平衡),我们不能仅仅通过准确率来评估模型的表现。因为如果将所有实例都分类为非欺诈行为,我们的准确率可以达到 99.82%。显然,这个数字并不代表一个可接受的表现,因为我们根本无法检测到任何欺诈交易。因此,为了评估我们的模型,我们将使用召回率(即我们检测到的欺诈行为的百分比)和 F1 得分,后者是召回率和精确度的加权平均值(精确度衡量的是预测为欺诈的交易中,实际为欺诈的比例)。
投票
在这一部分,我们将尝试通过使用投票集成方法来对数据集进行分类。对于我们的初步集成方法,我们将利用朴素贝叶斯分类器、逻辑回归和决策树。这个过程将分为两部分,首先测试每个基础学习器本身,然后将这些基础学习器组合成一个集成模型。
测试基础学习器
为了测试基础学习器,我们将单独对基础学习器进行基准测试,这将帮助我们评估它们单独表现的好坏。为此,首先加载库和数据集,然后将数据划分为 70%的训练集和 30%的测试集。我们使用pandas
来轻松导入 CSV 文件。我们的目标是在训练和评估整个集成模型之前,先训练和评估每个单独的基础学习器:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
在加载库和数据后,我们训练每个分类器,并打印出来自sklearn.metrics
包的必要指标。F1 得分通过f1_score
函数实现,召回率通过recall_score
函数实现。为了避免过拟合,决策树的最大深度被限制为三(max_depth=3
):
# --- SECTION 2 ---
# Base learners evaluation
base_classifiers = [('DT', DecisionTreeClassifier(max_depth=3)),
('NB', GaussianNB()),
('LR', LogisticRegression())]
for bc in base_classifiers:
lr = bc[1]
lr.fit(x_train, y_train)
predictions = lr.predict(x_test)
print(bc[0]+' f1', metrics.f1_score(y_test, predictions))
print(bc[0]+' recall', metrics.recall_score(y_test, predictions))
结果在以下表格中有所展示。显然,决策树的表现优于其他三个学习器。朴素贝叶斯的召回率较高,但其 F1 得分相较于决策树要差得多:
学习器 | 指标 | 值 |
---|---|---|
决策树 | F1 | 0.770 |
召回率 | 0.713 | |
朴素贝叶斯 | F1 | 0.107 |
召回率 | 0.824 | |
逻辑回归 | F1 | 0.751 |
召回率 | 0.632 |
我们还可以实验数据集中包含的特征数量。通过绘制它们与目标的相关性,我们可以过滤掉那些与目标相关性较低的特征。此表格展示了每个特征与目标的相关性:
每个变量与目标之间的相关性
通过过滤掉任何绝对值小于 0.1 的特征,我们希望基本学习器能够更好地检测欺诈交易,因为数据集的噪声将会减少。
为了验证我们的理论,我们重复实验,但删除 DataFrame 中任何绝对相关性低于 0.1 的列,正如fs = list(correlations[(abs(correlations)>threshold)].index.values)
所示。
在这里,fs
包含了所有与指定阈值相关性大于的列名:
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations)>threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
for bc in base_classifiers:
lr = bc[1]
lr.fit(x_train, y_train)
predictions = lr.predict(x_test)
print(bc[0]+' f1', metrics.f1_score(y_test, predictions))
print(bc[0]+' recall', metrics.recall_score(y_test, predictions))
再次,我们展示了以下表格中的结果。正如我们所看到的,决策树提高了其 F1 得分,同时降低了召回率。朴素贝叶斯在两个指标上都有所提升,而逻辑回归模型的表现大幅下降:
学习器 | 指标 | 值 |
---|---|---|
决策树 | F1 | 0.785 |
召回率 | 0.699 | |
朴素贝叶斯 | F1 | 0.208 |
召回率 | 0.846 | |
逻辑回归 | F1 | 0.735 |
召回率 | 0.610 |
过滤数据集上三个基本学习器的性能指标
优化决策树
我们可以尝试优化树的深度,以最大化 F1 或召回率。为此,我们将在训练集上尝试深度范围为* [3, 11] *的不同深度。
以下图表展示了不同最大深度下的 F1 得分和召回率,包括原始数据集和过滤后的数据集:
不同树深度的测试指标
在这里,我们观察到,对于最大深度为 5 的情况,F1 和召回率在过滤后的数据集上得到了优化。此外,召回率在原始数据集上也得到了优化。我们将继续使用最大深度为 5,因为进一步优化这些指标可能会导致过拟合,尤其是在与这些指标相关的实例数量极其少的情况下。此外,使用最大深度为 5 时,在使用过滤后的数据集时,F1 和召回率都有所提高。
创建集成模型
我们现在可以继续创建集成模型。再次,我们将首先在原始数据集上评估集成模型,然后在过滤后的数据集上进行测试。代码与之前的示例相似。首先,我们加载库和数据,并按以下方式创建训练集和测试集的划分:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from sklearn.ensemble import VotingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
在加载所需的库和数据后,我们创建集成模型,然后对其进行训练和评估。最后,我们按照以下方式通过过滤掉与目标变量相关性较低的特征来减少特征,从而重复实验:
# --- SECTION 2 ---
# Ensemble evaluation
base_classifiers = [('DT', DecisionTreeClassifier(max_depth=5)),
('NB', GaussianNB()),
('ensemble', LogisticRegression())]
ensemble = VotingClassifier(base_classifiers)
ensemble.fit(x_train, y_train)
print('Voting f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Voting recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations)>threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
ensemble = VotingClassifier(base_classifiers)
ensemble.fit(x_train, y_train)
print('Voting f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Voting recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
以下表格总结了结果。对于原始数据集,投票模型提供了比任何单一分类器更好的 F1 和召回率的组合。
然而,最大深度为五的决策树在 F1 分数上稍微超越了它,而朴素贝叶斯能够回忆起更多的欺诈交易:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.822 |
召回率 | 0.779 | |
过滤后 | F1 | 0.828 |
召回率 | 0.794 |
对两个数据集的投票结果
我们可以通过添加两个额外的决策树,分别具有最大深度为三和八,进一步多样化我们的集成模型。这将集成模型的性能提升至以下数值。
尽管在过滤数据集上的性能保持不变,但该集成模型在原始数据集上的表现有所提升。特别是在 F1 指标上,它能够超越所有其他数据集/模型组合:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.829 |
召回率 | 0.787 | |
过滤后 | F1 | 0.828 |
召回率 | 0.794 |
对两个数据集使用额外两个决策树的投票结果
堆叠
我们也可以尝试将基本学习器堆叠,而不是使用投票。首先,我们将尝试堆叠一个深度为五的决策树,一个朴素贝叶斯分类器和一个逻辑回归模型。作为元学习器,我们将使用逻辑回归。
以下代码负责加载所需的库和数据、训练和评估原始数据集和过滤数据集上的集成模型。我们首先加载所需的库和数据,并创建训练集和测试集分割:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from stacking_classifier import Stacking
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import LinearSVC
from sklearn.model_selection import train_test_split
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
在创建训练集和测试集分割后,我们在原始数据集以及减少特征的数据集上训练并评估集成模型,如下所示:
# --- SECTION 2 ---
# Ensemble evaluation
base_classifiers = [DecisionTreeClassifier(max_depth=5),
GaussianNB(),
LogisticRegression()]
ensemble = Stacking(learner_levels=[base_classifiers,
[LogisticRegression()]])
ensemble.fit(x_train, y_train)
print('Stacking f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Stacking recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations) > threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(data.drop('Class', axis=1).values,
data.Class.values, test_size=0.3)
ensemble = Stacking(learner_levels=[base_classifiers,
[LogisticRegression()]])
ensemble.fit(x_train, y_train)
print('Stacking f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Stacking recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
如下表所示,该集成模型在原始数据集上取得了略高的 F1 分数,但召回率较差,相比之下,投票集成模型使用相同的基本学习器表现较好:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.823 |
召回率 | 0.750 | |
过滤后 | F1 | 0.828 |
召回率 | 0.794 |
使用三个基本学习器的堆叠集成模型表现
我们可以进一步尝试不同的基本学习器。通过添加两个分别具有最大深度为三和八的决策树(与第二次投票配置相同),观察堆叠模型表现出相同的行为。在原始数据集上,堆叠模型在 F1 分数上超越了其他模型,但在召回率上表现较差。
在过滤数据集上,性能与投票模型持平。最后,我们尝试第二层次的基本学习器,由一个深度为二的决策树和一个线性支持向量机组成,其表现不如五个基本学习器的配置:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.844 |
召回率 | 0.757 | |
过滤后 | F1 | 0.828 |
召回率 | 0.794 |
使用五个基本学习器的性能
下表展示了堆叠集成的结果,增加了一个基础学习器层次。显然,它的表现不如原始集成。
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.827 |
召回率 | 0.757 | |
过滤 | F1 | 0.827 |
召回率 | 0.772 |
五个基础学习器在第 0 层,两个基础学习器在第 1 层的表现
袋装法
在本节中,我们将使用袋装法对数据集进行分类。正如我们之前所示,最大深度为五的决策树是最优的,因此我们将使用这些树来进行袋装法示例。
我们希望优化集成的大小。我们将通过在*【5,30】*范围内测试不同大小来生成原始训练集的验证曲线。实际的曲线如下图所示:
原始训练集的验证曲线,针对不同集成大小
我们观察到,集成大小为 10 时方差最小,因此我们将使用大小为 10 的集成。
以下代码加载数据和库(第一部分),将数据拆分为训练集和测试集,并在原始数据集(第二部分)和减少特征的数据集(第三部分)上拟合并评估集成:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
在创建了训练集和测试集划分后,我们在原始数据集和减少特征的数据集上训练并评估我们的集成,如下所示:
# --- SECTION 2 ---
# Ensemble evaluation
ensemble = BaggingClassifier(n_estimators=10,
base_estimator=DecisionTreeClassifier(max_depth=5))
ensemble.fit(x_train, y_train)
print('Bagging f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Bagging recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations)>threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
ensemble = BaggingClassifier(n_estimators=10,
base_estimator=DecisionTreeClassifier(max_depth=5))
ensemble.fit(x_train, y_train)
print('Bagging f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('Bagging recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
使用最大深度为 5 且每个集成有 10 棵树的袋装法集成,我们能够在以下 F1 和召回率得分中取得较好成绩。在所有度量上,它在两个数据集上均优于堆叠法和投票法,唯一的例外是,原始数据集的 F1 分数略逊于堆叠法(0.843 对比 0.844):
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.843 |
召回率 | 0.787 | |
过滤 | F1 | 0.831 |
召回率 | 0.794 |
原始数据集和过滤数据集的袋装性能
尽管我们已得出最大深度为 5 对于单一决策树是最优的结论,但这确实限制了每棵树的多样性。通过将最大深度增加到 8,我们能够在过滤数据集上获得 0.864 的 F1 分数和 0.816 的召回率,这也是迄今为止的最佳表现。
然而,原始数据集上的性能有所下降,这确认了我们移除的特征确实是噪声,因为现在决策树能够拟合样本内的噪声,因此它们的样本外表现下降:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.840 |
召回率 | 0.772 | |
过滤 | F1 | 0.864 |
召回率 | 0.816 |
提升法
接下来,我们将开始使用生成方法。我们将实验的第一个生成方法是提升法。我们将首先尝试使用 AdaBoost 对数据集进行分类。由于 AdaBoost 根据误分类重新采样数据集,因此我们预期它能够相对较好地处理我们不平衡的数据集。
首先,我们必须决定集成的大小。我们生成了多个集成大小的验证曲线,具体如下所示:
AdaBoost 的不同集成大小验证曲线
如我们所见,70 个基学习器提供了偏差与方差之间的最佳权衡。因此,我们将继续使用 70 个基学习器的集成。
以下代码实现了 AdaBoost 的训练和评估:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from sklearn.ensemble import AdaBoostClassifier
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
然后我们使用 70 个基学习器和学习率 1.0 来训练和评估我们的集成方法:
# --- SECTION 2 ---
# Ensemble evaluation
ensemble = AdaBoostClassifier(n_estimators=70, learning_rate=1.0)
ensemble.fit(x_train, y_train)
print('AdaBoost f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('AdaBoost recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
我们通过选择与目标高度相关的特征来减少特征数量。最后,我们重复训练和评估集成的方法:
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations)>threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
ensemble = AdaBoostClassifier(n_estimators=70, learning_rate=1.0)
ensemble.fit(x_train, y_train)
print('AdaBoost f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('AdaBoost recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
结果如下表所示。显而易见,它的表现不如我们之前的模型:
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.778 |
召回率 | 0.721 | |
过滤后数据 | F1 | 0.794 |
召回率 | 0.721 |
AdaBoost 的表现
我们可以尝试将学习率增加到 1.3,这似乎能提高整体表现。如果我们再将其增加到 1.4,则会发现性能下降。如果我们将基学习器的数量增加到 80,过滤后的数据集性能有所提升,而原始数据集则似乎在召回率和 F1 表现之间做出了权衡:
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.788 |
召回率 | 0.765 | |
过滤后数据 | F1 | 0.815 |
召回率 | 0.743 |
AdaBoost 的表现,学习率=1.3
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.800 |
召回率 | 0.765 | |
过滤后数据 | F1 | 0.800 |
召回率 | 0.735 |
AdaBoost 的表现,学习率=1.4
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.805 |
召回率 | 0.757 | |
过滤后数据 | F1 | 0.805 |
召回率 | 0.743 |
AdaBoost 的表现,学习率=1.4,集成大小=80
事实上,我们可以观察到一个 F1 和召回率的帕累托前沿,它直接与学习率和基学习器数量相关。这个前沿如下图所示:
AdaBoost 的 F1 和召回率的帕累托前沿
XGBoost
我们还将尝试使用 XGBoost 对数据集进行分类。由于 XGBoost 的树最大深度为三,我们预期它会在没有任何微调的情况下超越 AdaBoost。的确,XGBoost 在两个数据集上,以及所有指标方面(如下表所示),都能表现得比大多数先前的集成方法更好:
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.846 |
召回率 | 0.787 | |
过滤后数据 | F1 | 0.849 |
召回率 | 0.809 |
XGBoost 的开箱即用表现
通过将每棵树的最大深度增加到五,集成方法的表现得到了进一步提升,结果如下:
数据集 | 指标 | 值 |
---|---|---|
原始数据 | F1 | 0.862 |
召回率 | 0.801 | |
过滤后 | F1 | 0.862 |
召回率 | 0.824 |
最大深度为 5 时的性能
使用随机森林
最后,我们将使用随机森林集成方法。再次通过验证曲线,我们确定最佳的集成大小。从下图可以看出,50 棵树提供了模型的最小方差,因此我们选择集成大小为 50:
随机森林的验证曲线
我们提供以下训练和验证代码,并给出两个数据集的性能表现。以下代码负责加载所需的库和数据,并在原始数据集和过滤后的数据集上训练和评估集成模型。我们首先加载所需的库和数据,同时创建训练集和测试集:
# --- SECTION 1 ---
# Libraries and data loading
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from sklearn import metrics
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
np.random.seed(123456)
data = pd.read_csv('creditcard.csv')
data.Time = (data.Time-data.Time.min())/data.Time.std()
data.Amount = (data.Amount-data.Amount.mean())/data.Amount.std()
# Train-Test slpit of 70%-30%
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
然后,我们在原始数据集和过滤后的数据集上训练和评估集成模型:
# --- SECTION 2 ---
# Ensemble evaluation
ensemble = RandomForestClassifier(n_jobs=4)
ensemble.fit(x_train, y_train)
print('RF f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('RF recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
# --- SECTION 3 ---
# Filter features according to their correlation to the target
np.random.seed(123456)
threshold = 0.1
correlations = data.corr()['Class'].drop('Class')
fs = list(correlations[(abs(correlations)>threshold)].index.values)
fs.append('Class')
data = data[fs]
x_train, x_test, y_train, y_test = train_test_split(
data.drop('Class', axis=1).values, data.Class.values, test_size=0.3)
ensemble = RandomForestClassifier(n_jobs=4)
ensemble.fit(x_train, y_train)
print('RF f1', metrics.f1_score(y_test, ensemble.predict(x_test)))
print('RF recall', metrics.recall_score(y_test, ensemble.predict(x_test)))
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.845 |
召回率 | 0.743 | |
过滤后 | F1 | 0.867 |
召回率 | 0.794 |
随机森林性能
由于我们的数据集高度偏斜,我们可以推测,改变树的分割标准为熵会对我们的模型有帮助。事实上,通过在构造函数中指定criterion='entropy'
(ensemble = RandomForestClassifier(n_jobs=4)
),我们能够将原始数据集的性能提高到F1得分为0.859和召回率得分为0.786,这是原始数据集的两个最高得分:
数据集 | 指标 | 值 |
---|---|---|
原始 | F1 | 0.859 |
召回率 | 0.787 | |
过滤后 | F1 | 0.856 |
召回率 | 0.787 |
使用熵作为分割标准的性能
集成方法的对比分析
在实验中,我们使用了一个减少特征的数据集,其中去除了与目标变量相关性较弱的特征,我们希望提供每种方法最佳参数下的最终得分。在以下图表中,结果按升序排列。Bagging 在应用于过滤后的数据集时似乎是最稳健的方法。XGBoost 是第二好的选择,在应用于过滤后的数据集时也能提供不错的 F1 和召回率得分:
F1 得分
召回率得分,如下图所示,清楚地显示了 XGBoost 在该指标上相较于其他方法的明显优势,因为它能够在原始数据集和过滤后的数据集上都超过其他方法:
召回率得分
总结
在本章中,我们探讨了使用各种集成学习方法检测欺诈交易的可能性。虽然一些方法表现优于其他方法,但由于数据集的特点,在某种程度上对数据集进行重采样(过采样或欠采样)是很难得到好结果的。
我们展示了如何使用每种集成学习方法,以及如何探索调整其各自参数的可能性,以实现更好的性能。在下一章,我们将尝试利用集成学习技术来预测比特币价格。
第十章:预测比特币价格
多年来,比特币和其他加密货币吸引了许多方的关注,主要是由于其价格水平的爆炸性增长以及区块链技术所提供的商业机会。在本章中,我们将尝试使用历史数据预测第二天的比特币(BTC)价格。有许多来源提供加密货币的历史价格数据。我们将使用来自雅虎财经的数据,地址为finance.yahoo.com/quote/BTC-USD/history/
。本章将重点预测未来价格,并利用这些知识进行比特币投资。
本章将涵盖以下主题:
-
时间序列数据
-
投票
-
堆叠
-
装袋法
-
提升法
-
随机森林
技术要求
你需要具备基本的机器学习技术和算法知识。此外,还需要了解 Python 的语法和惯例。最后,熟悉 NumPy 库将极大帮助读者理解一些自定义算法实现。
本章的代码文件可以在 GitHub 上找到:
github.com/PacktPublishing/Hands-On-Ensemble-Learning-with-Python/tree/master/Chapter10
查看以下视频,观看代码示例:bit.ly/2JOsR7d
。
时间序列数据
时间序列数据关注的是每个实例与特定时间点或时间间隔相关的数据实例。我们选择测量变量的频率定义了时间序列的采样频率。例如,大气温度在一天中以及一整年中都有不同。我们可以选择每小时测量一次温度,从而得到每小时的频率,或者选择每天测量一次温度,从而得到每天的频率。在金融领域,采样频率介于主要时间间隔之间并不罕见;例如,可以是每 10 分钟一次(10 分钟频率)或每 4 小时一次(4 小时频率)。时间序列的另一个有趣特点是,通常相邻时间点之间的数据实例存在相关性。
这叫做自相关。例如,大气温度在连续的几分钟内不能发生很大的变化。此外,这使我们能够利用早期的数据点来预测未来的数据点。下面是 2016 年至 2019 年期间雅典和希腊的温度(3 小时平均)示例。请注意,尽管温度有所变化,但大多数温度都相对接近前一天的温度。此外,我们看到热月和冷月(季节)的重复模式,这就是所谓的季节性:
2016–2019 年雅典,希腊的温度
为了检查不同时间点之间的相关性水平,我们利用自相关函数(ACF)。ACF 衡量数据点与前一个数据点之间的线性相关性(称为滞后)。下图展示了温度数据(按月平均重新采样)的自相关函数(ACF)。它显示出与第一个滞后的强正相关性。这意味着一个月的温度不太可能与前一个月相差太大,这是合乎逻辑的。例如,12 月和 1 月是寒冷的月份,它们的平均温度通常比 12 月和 3 月更接近。此外,第 5 和第 6 滞后之间存在强烈的负相关性,表明寒冷的冬季导致炎热的夏季,反之亦然:
温度数据的自相关函数(ACF)
比特币数据分析
比特币数据与温度数据有很大不同。温度在每年的相同月份基本保持相似值。这表明温度的分布随着时间变化并未发生改变。表现出这种行为的时间序列被称为平稳时间序列。这使得使用时间序列分析工具,如自回归(AR)、滑动平均(MA)和自回归积分滑动平均(ARIMA)模型进行建模相对简单。财务数据通常是非平稳的,正如下图中所示的每日比特币收盘数据所示。这意味着数据在其历史的整个过程中并未表现出相同的行为,而是行为在变化。
财务数据通常提供开盘价(当天的第一笔价格)、最高价(当天的最高价格)、最低价(当天的最低价格)和收盘价(当天的最后一笔价格)。
数据中存在明显的趋势(价格在某些时间间隔内平均上升或下降),以及异方差性(随时间变化的可变方差)。识别平稳性的一种方法是研究自相关函数(ACF)。如果存在非常强的高阶滞后之间的相关性且不衰减,则该时间序列很可能是非平稳的。下图展示了比特币数据的自相关函数(ACF),显示出相关性衰减较弱:
2014 年中期至今的比特币/USD 价格
下图展示了比特币的自相关函数(ACF)。我们可以清楚地看到,在非常高的滞后值下,相关性并没有显著下降:
比特币数据的自相关函数(ACF)
请看以下公式:
其中 p 是百分比变化,t[n] 是时间 n 时的价格,tn-1 是时间 n-1 时的价格。通过对数据进行转化,我们得到一个平稳但相关性较弱的时间序列。
下图展示了数据的图形,并提供了自相关函数(ACF)和平均 30 天标准差:
转换后的数据
滚动 30 天标准差和转换数据的自相关函数(ACF)
建立基准
为了建立基准,我们将尝试使用线性回归建模数据。虽然这是一个时间序列,我们不会直接考虑时间。相反,我们将使用大小为S的滑动窗口在每个时间点生成特征,并利用这些特征预测下一个数据点。接下来,我们将时间窗口向前移动一步,以包含我们预测的真实数据点的值,并丢弃窗口中的最旧数据点。我们将继续这个过程,直到所有数据点都被预测。这叫做向前验证。一个缺点是我们无法预测前S个数据点,因为我们没有足够的数据来生成它们的特征。另一个问题是我们需要重新训练模型L-S次,其中L是时间序列中的数据点总数。以下图展示了前两个步骤的图示:
向前验证程序的前两个步骤。该程序会继续应用于整个时间序列。
首先,我们从BTC-USD.csv
文件加载所需的库和数据。我们还设置了 NumPy 随机数生成器的种子:
import numpy as np
import pandas as pd
from simulator import simulate
from sklearn import metrics
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
np.random.seed(123456)
lr = LinearRegression()
data = pd.read_csv('BTC-USD.csv')
然后,我们通过使用data.dropna()
删除包含 NaN 值的条目,使用pd.to_datetime
解析日期,并将日期设置为索引,来清理数据。最后,我们计算Close
值的百分比差异(并丢弃第一个值,因为它是 NaN),并保存 Pandas 系列的长度:
data = data.dropna()
data.Date = pd.to_datetime(data.Date)
data.set_index('Date', drop=True, inplace=True)
diffs = (data.Close.diff()/data.Close).values[1:]
diff_len = len(diffs)
我们创建了一个函数,用于在每个数据点生成特征。特征本质上是前几个滞后的不同百分比。因此,为了用值填充数据集的特征,我们只需将数据向前移动滞后点数即可。任何没有可用数据计算滞后的特征,其值将为零。以下图示了一个包含数字 1、2、3 和 4 的时间序列的示例:
滞后特征的填充方式
实际的函数,填充滞后t,选择时间序列中的所有数据,除了最后的t,并将其放入相应的特征中,从索引t开始。我们选择使用过去 20 天的数据,因为在那之后似乎没有显著的线性相关性。此外,我们将特征和目标缩放 100 倍,并四舍五入到 8 位小数。这一点很重要,因为它确保了结果的可重复性。如果数据没有四舍五入,溢出错误会给结果带来随机性,如下所示:
def create_x_data(lags=1):
diff_data = np.zeros((diff_len, lags))
for lag in range(1, lags+1):
this_data = diffs[:-lag]
diff_data[lag:, lag-1] = this_data
return diff_data
# REPRODUCIBILITY
x_data = create_x_data(lags=20)*100
y_data = diffs*100
最后,我们执行前向验证。我们选择了 150 个点的训练窗口,大约相当于 5 个月。考虑到数据的特性和波动性,这提供了一个良好的折衷,既能保证训练集足够大,又能捕捉到近期的市场行为。更大的窗口将包括不再反映现实的市场条件。更短的窗口则提供的数据过少,容易导致过拟合。我们通过利用预测值与原始百分比差异之间的均方误差来衡量我们模型的预测质量:
window = 150
preds = np.zeros(diff_len-window)
for i in range(diff_len-window-1):
x_train = x_data[i:i+window, :]
y_train = y_data[i:i+window]
lr.fit(x_train, y_train)
preds[i] = lr.predict(x_data[i+window+1, :].reshape(1, -1))
print('Percentages MSE: %.2f'%metrics.mean_absolute_error(y_data[window:], preds))
简单线性回归可能产生一个均方误差(MSE)为 18.41。我们还可以尝试通过将每个数据点乘以(1 + 预测值)来重建时间序列,以获得下一个预测值。此外,我们可以尝试利用数据集的特点来模拟交易活动。每次预测值大于+0.5%的变化时,我们投资 100 美元购买比特币。如果我们持有比特币并且预测值低于-0.5%,则在当前市场收盘时卖出比特币。为了评估我们模型作为交易策略的质量,我们使用简化的夏普比率,计算方式是将平均回报率(百分比利润)与回报的标准差之比。较高的夏普值表示更好的交易策略。这里使用的公式如下。通常,预期回报会减去一个替代的安全回报百分比,但由于我们仅希望比较我们将生成的模型,因此将其省略:
作为交易策略使用时,线性回归能够产生 0.19 的夏普值。下图显示了我们的模型生成的交易和利润。蓝色三角形表示策略购买 100 美元比特币的时间点,红色三角形表示策略卖出之前购买的比特币的时间点:
我们模型的利润和进出点
在本章的其余部分,我们将通过利用前几章介绍的集成方法来改进均方误差(MSE)和夏普值。
模拟器
在这里,我们将简要解释模拟器的工作原理。它作为一个函数实现,接受我们的标准 Pandas 数据框和模型预测作为输入。首先,我们将定义买入阈值和投资额度(我们在每次买入时投资的金额),以及占位符变量。这些变量将用于存储真实和预测的时间序列,以及我们模型的利润(balances
)。此外,我们定义了buy_price
变量,它存储我们购买比特币时的价格。如果价格为0
,我们假设我们没有持有任何比特币。buy_points
和sell_points
列表表示我们买入或卖出比特币的时间点,仅用于绘图。此外,我们还存储了起始索引,这相当于滑动窗口的大小,如以下示例所示:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn import metrics
def simulate(data, preds):
# Constants and placeholders
buy_threshold = 0.5
stake = 100
true, pred, balances = [], [], []
buy_price = 0
buy_points, sell_points = [], []
balance = 0
start_index = len(data)-len(preds)-1
接下来,对于每个点,我们存储实际值和预测值。如果预测值大于 0.5 且我们没有持有任何比特币,我们将买入价值 100 美元的比特币。如果预测值小于-0.5 且我们已经购买了比特币,我们将以当前的收盘价将其卖出。我们将当前的利润(或亏损)添加到我们的余额中,将真实值和预测值转换为 NumPy 数组,并生成图表:
# Calculate predicted values
for i in range(len(preds)):
last_close = data.Close[i+start_index-1]
current_close = data.Close[i+start_index]
# Save predicted values and true values
true.append(current_close)
pred.append(last_close*(1+preds[i]/100))
# Buy/Sell according to signal
if preds[i] > buy_threshold and buy_price == 0:
buy_price = true[-1]
buy_points.append(i)
elif preds[i] < -buy_threshold and not buy_price == 0:
profit = (current_close - buy_price) * stake/buy_price
balance += profit
buy_price = 0
sell_points.append(i)
balances.append(balance)
true = np.array(true)
pred = np.array(pred)
# Create plots
plt.figure()
plt.subplot(2, 1, 1)
plt.plot(true, label='True')
plt.plot(pred, label='pred')
plt.scatter(buy_points, true[buy_points]+500, marker='v',
c='blue', s=5, zorder=10)
plt.scatter(sell_points, true[sell_points]-500, marker='^'
, c='red', s=5, zorder=10)
plt.title('Trades')
plt.subplot(2, 1, 2)
plt.plot(balances)
plt.title('Profit')
print('MSE: %.2f'%metrics.mean_squared_error(true, pred))
balance_df = pd.DataFrame(balances)
pct_returns = balance_df.diff()/stake
pct_returns = pct_returns[pct_returns != 0].dropna()
print('Sharpe: %.2f'%(np.mean(pct_returns)/np.std(pct_returns)))
投票
我们将尝试通过投票将三种基本回归算法结合起来,以提高简单回归的 MSE。为了组合这些算法,我们将利用它们预测值的平均值。因此,我们编写了一个简单的类,用于创建基本学习器的字典,并处理它们的训练和预测平均。主要逻辑与我们在第三章实现的自定义投票分类器Voting相同:
import numpy as np
from copy import deepcopy
class VotingRegressor():
# Accepts a list of (name, classifier) tuples
def __init__(self, base_learners):
self.base_learners = {}
for name, learner in base_learners:
self.base_learners[name] = deepcopy(learner)
# Fits each individual base learner
def fit(self, x_data, y_data):
for name in self.base_learners:
learner = self.base_learners[name]
learner.fit(x_data, y_data)
预测结果存储在一个 NumPy 矩阵中,其中每一行对应一个实例,每一列对应一个基本学习器。行平均值即为集成的输出,如下所示:
# Generates the predictions
def predict(self, x_data):
# Create the predictions matrix
predictions = np.zeros((len(x_data), len(self.base_learners)))
names = list(self.base_learners.keys())
# For each base learner
for i in range(len(self.base_learners)):
name = names[i]
learner = self.base_learners[name]
# Store the predictions in a column
preds = learner.predict(x_data)
predictions[:,i] = preds
# Take the row-average
predictions = np.mean(predictions, axis=1)
return predictions
我们选择了支持向量机、K 近邻回归器和线性回归作为基本学习器,因为它们提供了多样的学习范式。为了使用这个集成,我们首先导入所需的模块:
import numpy as np
import pandas as pd
from simulator import simulate
from sklearn import metrics
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from voting_regressor import VotingRegressor
接下来,在我们之前展示的代码中,我们将lr = LinearRegression()
这一行替换为以下代码:
base_learners = [('SVR', SVR()),
('LR', LinearRegression()),
('KNN', KNeighborsRegressor())]
lr = VotingRegressor(base_learners)
通过增加两个额外的回归器,我们能够将 MSE 减少到 16.22,并产生 0.22 的夏普值。
改进投票
尽管我们的结果优于线性回归,但我们仍然可以通过去除线性回归进一步改善结果,从而仅保留基本学习器,如下所示:
base_learners = [('SVR', SVR()), ('KNN', KNeighborsRegressor())]
这进一步改善了 MSE,将其减少到 15.71。如果我们将这个模型作为交易策略使用,可以实现 0.21 的夏普值;比简单的线性回归要好得多。下表总结了我们的结果:
Metric | SVR-KNN | SVR-LR-KNN |
---|---|---|
MSE | 15.71 | 16.22 |
Sharpe | 0.21 | 0.22 |
投票集成结果
堆叠
接下来,我们将使用堆叠法来更有效地结合基本回归器。使用第四章中的StackingRegressor
,堆叠,我们将尝试与投票法一样组合相同的算法。首先,我们修改我们的集成的predict
函数(以允许单实例预测),如下所示:
# Generates the predictions
def predict(self, x_data):
# Create the predictions matrix
predictions = np.zeros((len(x_data), len(self.base_learners)))
names = list(self.base_learners.keys())
# For each base learner
for i in range(len(self.base_learners)):
name = names[i]
learner = self.base_learners[name]
# Store the predictions in a column
preds = learner.predict(x_data)
predictions[:,i] = preds
# Take the row-average
predictions = np.mean(predictions, axis=1)
return predictions
再次,我们修改代码以使用堆叠回归器,如下所示:
base_learners = [[SVR(), LinearRegression(), KNeighborsRegressor()],
[LinearRegression()]]
lr = StackingRegressor(base_learners)
在这种设置下,集成方法生成的模型具有 16.17 的均方误差(MSE)和 0.21 的夏普比率。
改进堆叠法
我们的结果略逊于最终的投票集成,因此我们将尝试通过去除线性回归来改进它,就像我们在投票集成中做的那样。通过这样做,我们可以稍微改善模型,达到 16.16 的均方误差(MSE)和 0.22 的夏普比率。与投票法相比,堆叠法作为一种投资策略略好一些(相同的夏普比率和略微更好的 MSE),尽管它无法达到相同水平的预测准确度。其结果总结在下表中:
指标 | SVR-KNN | SVR-LR-KNN |
---|---|---|
MSE | 16.17 | 16.16 |
夏普比率 | 0.21 | 0.22 |
堆叠法结果
自助法(Bagging)
通常,在将预测模型拟合到金融数据时,方差是我们面临的主要问题。自助法是对抗方差的非常有用的工具;因此,我们希望它能够比简单的投票法和堆叠法生成表现更好的模型。为了利用自助法,我们将使用 scikit 的BaggingRegressor
,该方法在第五章中介绍,自助法。为了在我们的实验中实现它,我们只需使用lr = BaggingRegressor()
来替代之前的回归器。这样做的结果是均方误差(MSE)为 19.45,夏普比率为 0.09。下图展示了我们的模型所生成的利润和交易:
自助法的利润和交易
改进自助法
我们可以进一步改进自助法,因为它的表现比任何之前的模型都差。首先,我们可以尝试浅层决策树,这将进一步减少集成中的方差。通过使用最大深度为3
的决策树,使用lr = BaggingRegressor(base_estimator=DecisionTreeRegressor(max_depth=3))
,我们可以改进模型的性能,得到 16.17 的均方误差(MSE)和 0.15 的夏普比率。进一步将树的生长限制为max_depth=1
,可以使模型达到 16.7 的 MSE 和 0.27 的夏普比率。如果我们检查模型的交易图,我们会观察到交易数量的减少,以及在比特币价格大幅下跌的时期性能的显著改善。这表明该模型能够更有效地从实际信号中过滤噪声。
方差的减少确实帮助了我们的模型提高了性能:
最终自助法的利润和交易
下表总结了我们测试的各种袋装模型的结果:
指标 | DT_max_depth=1 | DT_max_depth=3 | DT |
---|---|---|---|
MSE | 16.70 | 17.59 | 19.45 |
Sharpe | 0.27 | 0.15 | 0.09 |
表 3:袋装结果
提升
最强大的集成学习技术之一是提升。它能够生成复杂的模型。在本节中,我们将利用 XGBoost 来建模我们的时间序列数据。由于在使用 XGBoost 建模时有许多自由度(超参数),我们预计需要一些微调才能取得令人满意的结果。通过将示例中的回归器替换为lr = XGBRegressor()
,我们可以使用 XGBoost 并将其拟合到我们的数据上。这将产生一个 MSE 为 19.20,Sharpe 值为 0.13 的结果。
图表展示了模型生成的利润和交易。虽然 Sharpe 值低于其他模型,但我们可以看到,即使在比特币价格下跌的时期,它仍然能持续生成利润:
由提升模型生成的交易
改进提升方法
由于样本外的表现以及提升模型的买入和卖出频率,我们可以假设它在训练数据上发生了过拟合。因此,我们将尝试对其学习进行正则化。第一步是限制单个树的最大深度。我们首先施加一个上限为 2 的限制,使用max_depth=2
。这略微改善了我们的模型,得到了一个 MSE 值为 19.14,Sharpe 值为 0.17。通过进一步限制模型的过拟合能力,仅使用 10 个基学习器(n_estimators=10
),模型进一步得到了提升。
模型的 MSE 降低至 16.39,Sharpe 值提高至 0.21。添加一个 L1 正则化项 0.5(reg_alpha=0.5
)只将 MSE 减少至 16.37。我们已经到了一个点,进一步的微调不会对模型性能贡献太大。在这个阶段,我们的回归模型如下所示:
lr = XGBRegressor(max_depth=2, n_estimators=10, reg_alpha=0.5)
鉴于 XGBoost 的能力,我们将尝试增加模型可用的信息量。我们将把可用特征滞后增加到 30,并将之前 15 个滞后的滚动均值添加到特征中。为此,我们将修改代码中的特征创建部分,如下所示:
def create_x_data(lags=1):
diff_data = np.zeros((diff_len, lags))
ma_data = np.zeros((diff_len, lags))
diff_ma = (data.Close.diff()/data.Close).rolling(15).mean().fillna(0).values[1:]
for lag in range(1, lags+1):
this_data = diffs[:-lag]
diff_data[lag:, lag-1] = this_data
this_data = diff_ma[:-lag]
ma_data[lag:, lag-1] = this_data
return np.concatenate((diff_data, ma_data), axis=1)
x_data = create_x_data(lags=30)*100
y_data = diffs*100
这增加了我们模型的交易表现,达到了 0.32 的 Sharpe 值——所有模型中最高,同时 MSE 也增加到了 16.78。此模型生成的交易如图和后续的表格所示。值得注意的是,买入的次数大大减少,这种行为也是袋装方法在我们改进其作为投资策略时所展现的:
最终提升模型性能
指标 | md=2/ne=10/reg=0.5+data | md=2/ne=10/reg=0.5 | md=2/ne=10 | md=2 | xgb |
---|---|---|---|---|---|
MSE | 16.78 | 16.37 | 16.39 | 19.14 | 19.20 |
Sharpe | 0.32 | 0.21 | 0.21 | 0.17 | 0.13 |
所有增强模型的度量
随机森林
最后,我们将使用随机森林来建模数据。尽管我们预计集成方法能够利用额外滞后期和滚动平均的所有信息,但我们将首先仅使用 20 个滞后期和回报百分比作为输入。因此,我们的初始回归器仅为RandomForestRegressor()
。这导致了一个表现不太理想的模型,MSE 为 19.02,Sharpe 值为 0.11。
下图展示了模型生成的交易:
随机森林模型的交易
改进随机森林
为了改进我们的模型,我们尝试限制其过拟合能力,给每棵树设置最大深度为3
。这大大提高了模型的性能,模型达到了 MSE 值为 17.42 和 Sharpe 值为 0.17。进一步将最大深度限制为2
,虽然使 MSE 得分稍微提高到 17.13,但 Sharpe 值下降至 0.16。最后,将集成模型的大小增加到 50,使用n_estimators=50
,生成了一个性能大幅提升的模型,MSE 为 16.88,Sharpe 值为 0.23。由于我们仅使用了原始特征集(20 个回报百分比的滞后期),我们希望尝试在增强部分使用的扩展数据集。通过添加 15 日滚动平均值,并将可用滞后期数量增加到 30,模型的 Sharpe 值提高到 0.24,尽管 MSE 也上升到 18.31。模型生成的交易如图所示:
使用扩展数据集的随机森林结果
模型的结果总结如下表:
Metric | md=2/ne=50+data | md=2/ne=50 | md=2 | md=3 | RF |
---|---|---|---|---|---|
MSE | 18.31 | 16.88 | 17.13 | 17.42 | 19.02 |
Sharpe | 0.24 | 0.23 | 0.16 | 0.17 | 0.11 |
所有随机森林模型的度量
总结
在本章中,我们尝试使用本书前几章介绍的所有集成方法来建模历史比特币价格。与大多数数据集一样,模型质量受多种决策的影响。数据预处理和特征工程是其中最重要的因素,尤其是当数据集的性质不允许直接建模时。时间序列数据集就属于这种情况,需要构建适当的特征和目标。通过将我们的非平稳时间序列转化为平稳序列,我们提高了算法对数据建模的能力。
为了评估我们模型的质量,我们使用了收益百分比的均方误差(MSE),以及夏普比率(我们假设模型被用作交易策略)。在涉及到 MSE 时,表现最佳的集成模型是简单投票集成。该集成包括一个 SVM 和 KNN 回归器,没有进行超参数调优,实现了一个 MSE 值为 15.71。作为交易策略,XGBoost 被证明是最佳集成模型,实现了一个夏普值为 0.32。尽管不全面,本章探索了使用集成学习方法进行时间序列建模的可能性和技术。
在接下来的章节中,我们将利用集成学习方法的能力,以预测各种推特的情感。