原文:
zh.annas-archive.org/md5/a3861c820dde5bc35d4f0200e43cd519
译者:飞龙
第十二章:提升您的交易策略
在上一章中,我们看到随机森林通过将许多树组合成集成来改进决策树的预测。降低个别树高方差的关键在于使用装袋,简称自助聚合,它在生长个别树的过程中引入了随机性。更具体地说,装袋从数据中进行替换抽样,使每棵树都在一个不同但大小相等的随机子集上训练,一些观测值重复出现。此外,随机森林随机选择一些特征的子集,使每棵树的训练集的行和列都是原始数据的随机版本。然后,集成通过对各个树的输出进行平均来生成预测。
个别随机森林树通常生长较深,以确保低偏差,同时依靠随机化训练过程产生不同的、不相关的预测误差,当聚合时,这些误差的方差较低于个别树的预测。换句话说,随机化训练旨在去相关(考虑多样化)各个树的误差。它这样做是为了使整体对过拟合的影响较小,方差较低,从而更好地推广到新数据。
这一章探讨了提升(boosting),这是一种替代决策树的集成算法,通常能够产生更好的结果。其关键区别在于,提升根据模型到目前为止累积的错误修改每个新树的训练数据。与独立训练许多树的随机森林不同,提升使用数据的重新加权版本进行顺序处理。最先进的提升实现还采用了随机森林的随机化策略。
在过去的三十年中,提升已经成为最成功的机器学习(ML)算法之一,主导着许多结构化、表格数据的 ML 竞赛(与高维图像或具有更复杂输入输出关系的语音数据相反,在这些领域深度学习表现出色)。我们将展示提升的工作原理,介绍几种高性能实现,并将提升应用于高频数据并对日内交易策略进行回测。
更具体地说,阅读本章后,您将能够:
-
理解提升与装袋的区别,以及梯度提升如何从自适应提升演变而来。
-
使用 scikit-learn 设计和调整自适应提升和梯度提升模型。
-
使用最先进的实现 XGBoost、LightGBM 和 CatBoost 在大型数据集上构建、调整和评估梯度提升模型。
-
解释并从梯度提升模型中获得见解。
-
使用高频数据进行提升,设计日内策略。
您可以在 GitHub 存储库的相应目录中找到本章的代码示例和其他资源链接。笔记本包括图像的彩色版本。
入门–自适应增强
像装袋一样,增强是一种集成学习算法,它将基学习器(通常是决策树)组合成一个集成。增强最初是为分类问题开发的,但也可用于回归,并且被称为过去 20 年中引入的最有效的学习思想之一(Hastie,Tibshirani 和 Friedman 2009)。与装袋一样,它是一种通用方法或元方法,可应用于许多统计学习方法。
增强的动机是找到一种方法,将许多弱模型(即它们仅比随机猜测略好一点)的输出合并成高度准确的增强联合预测(Schapire 和 Freund 2012)。
一般来说,增强学习得出一个类似于线性回归的加法假设H[M]。然而,求和的每个m= 1,…, M元素都是一个称为h[t]的弱基学习器,它本身需要训练。以下公式总结了这种方法:
正如前一章所讨论的,装袋在不同的数据随机样本上训练基学习器。相比之下,增强通过在数据上顺序训练基学习器,该数据反复修改以反映累积学习。其目标是确保下一个基学习器弥补当前集成的缺陷。我们将在本章中看到,增强算法在定义缺陷方面存在差异。集成使用弱模型的预测的加权平均值进行预测。
第一个具有数学证明的增强算法,可以增强弱学习者的性能,是由罗伯特·沙皮尔和约阿夫·弗洛伊德于 1990 年左右开发的。1997 年,一种解决分类问题的实用解决方案以自适应增强(AdaBoost)算法的形式出现,该算法在 2003 年获得了哥德尔奖(Freund 和 Schapire 1997)。大约另外 5 年后,当 Leo Breiman(发明随机森林的人)将这种方法与梯度下降联系起来,并且 Jerome Friedman 于 1999 年提出梯度增强时,该算法被扩展到任意目标函数(Friedman 2001)。
近年来出现了许多优化的实现,例如 XGBoost、LightGBM 和 CatBoost,我们稍后将在本章中介绍,这些实现已经确立了梯度增强作为结构化数据的首选解决方案。在接下来的章节中,我们将简要介绍 AdaBoost,然后重点介绍梯度增强模型,以及我们刚刚提到的这个非常强大和灵活的算法的三种最新实现。
AdaBoost 算法
当 AdaBoost 在 1990 年代出现时,它是第一个集成算法,通过迭代适应累积学习进展,当拟合额外的集成成员时。特别地,AdaBoost 改变了训练数据上的权重,以反映当前集成在训练集上的累积误差,然后拟合一个新的弱学习器。AdaBoost 当时是最准确的分类算法,利奥·布雷曼在 1996 年 NIPS 会议上称其为世界上最好的现成分类器(Hastie、Tibshirani 和 Friedman 2009)。
在随后的几十年里,该算法对机器学习产生了巨大影响,因为它提供了理论性能保证。这些保证仅需要足够的数据和一个可靠地预测略优于随机猜测的弱学习器。由于这种分阶段学习的自适应方法,开发准确的 ML 模型不再需要在整个特征空间上准确地表现。相反,模型的设计可以专注于找到仅在一小部分特征上优于硬币翻转的弱学习器。
与 bagging 相反,bagging 构建了非常大的树的集成以减小偏差,AdaBoost 则以浅树为弱学习器,通常使用树桩(即由单一分裂形成的树)产生更高的准确性。该算法从均匀加权的训练集开始,然后逐步改变样本分布。在每次迭代后,AdaBoost 增加被错误分类的观察值的权重,并减少正确预测样本的权重,以便随后的弱学习器更多地关注特别困难的情况。一旦训练完成,新的决策树将根据其减少训练误差的贡献加入到集成中。
基于预测离散类别 y 的 N 个训练观测结果,基础学习器的集成算法 AdaBoost 可以总结如下:
-
对于观察值 i=1, …, N,初始化样本权重 w[i]=1/N。
-
对于每个基础分类器 h[m],m=1, …, M,执行以下操作:
-
用 w[i] 加权拟合 hm 到训练数据。
-
计算基础学习器在训练集上的加权错误率
。
-
根据其错误率计算基础学习器的集成权重
,如下公式所示:
-
根据
更新误分类样本的权重。
-
-
当集成成员的加权和为正时,预测为正类,否则为负类,如下公式所示:
AdaBoost 有许多实际的优势,包括易于实现和快速计算,并且可以与任何用于识别弱学习器的方法结合使用。除了集成大小之外,没有需要调整的超参数。AdaBoost 也适用于识别异常值,因为接收最高权重的样本是那些始终被错误分类和固有模糊的样本,这也是异常值的典型特征。
还有缺点:AdaBoost 在给定数据集上的性能取决于弱学习器充分捕获特征与结果之间关系的能力。正如理论所述,当数据不足或者集成成员的复杂度与数据的复杂度不匹配时,Boosting 效果不佳。它也容易受到数据中的噪声影响。
详细介绍和审查了提升算法,请参阅 Schapire 和 Freund (2012)。
使用 AdaBoost 预测月度价格走势
作为其集成模块的一部分,scikit-learn 提供了一个支持两个或更多类别的AdaBoostClassifier
实现。本节的代码示例在笔记本boosting_baseline
中,该笔记本将各种算法的性能与始终预测最频繁类别的虚拟分类器进行比较。
我们需要首先将base_estimator
定义为所有集成成员的模板,然后配置集成本身。我们将使用默认的DecisionTreeClassifier
,max_depth=1
— 即,具有单一拆分的桩。可选的包括符合 scikit-learn 接口的任何其他模型,从线性或逻辑回归到神经网络(请参阅文档)。然而,在实践中,决策树是最常见的。
base_estimator
的复杂度是一个关键的调节参数,因为它取决于数据的性质。正如前一章所示,对于max_depth
的更改应与适当的正则化约束相结合,使用例如对min_samples_split
的调整,如下面的代码所示:
base_estimator = DecisionTreeClassifier(criterion='gini',
splitter='best',
max_depth=1,
min_samples_split=2,
min_samples_leaf=20,
min_weight_fraction_leaf=0.0,
max_features=None,
random_state=None,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None)
在第二步中,我们将设计集成。n_estimators
参数控制弱学习器的数量,而learning_rate
确定每个弱学习器的贡献,如下面的代码所示。默认情况下,弱学习器是决策树桩:
ada_clf = AdaBoostClassifier(base_estimator=base_estimator,
n_estimators=100,
learning_rate=1.0,
algorithm='SAMME.R',
random_state=42)
负责良好结果的主要调节参数是n_estimators
和base_estimator
的复杂度。这是因为树的深度控制了特征之间的相互作用程度。
我们将使用自定义的OneStepTimeSeriesSplit
来交叉验证 AdaBoost 集成,这是MultipleTimeSeriesCV
的简化版本(请参见第六章 机器学习过程)。它实现了一个 12 折滚动时间序列拆分,以预测样本中最后 12 个月的 1 个月,使用所有可用的先前数据进行训练,如下面的代码所示:
cv = OneStepTimeSeriesSplit(n_splits=12, test_period_length=1, shuffle=True)
def run_cv(clf, X=X_dummies, y=y, metrics=metrics, cv=cv, fit_params=None):
return cross_validate(estimator=clf,
X=X,
y=y,
scoring=list(metrics.keys()),
cv=cv,
return_train_score=True,
n_jobs=-1, # use all cores
verbose=1,
fit_params=fit_params)
验证结果显示加权准确率为 0.5068,AUC 分数为 0.5348,精度和召回率分别为 0.547 和 0.576,相应地,F1 分数为 0.467。这略低于采用默认设置的随机森林,在验证 AUC 为 0.5358 时,图 12.1以箱形图显示了 12 个训练和测试折的各种指标的分布(注意,随机森林完全适应于训练集):
图 12.1:AdaBoost 交叉验证性能
有关交叉验证和处理结果的代码的详细信息,请参阅附带的笔记本。
梯度提升 - 大多数任务的集成
AdaBoost 也可以解释为一种逐步向前的方法,用于最小化二元结果的指数损失函数,y ,在每次迭代,m,中识别一个新的基学习器,h[m],具有相应的权重,
,并将其添加到集成中,如下面的公式所示:
将 AdaBoost 解释为最小化特定损失函数,即指数损失的梯度下降算法,是在其原始发布几年后才发现的。
梯度提升利用这一见解,将提升方法应用于更广泛范围的损失函数。该方法使得可以设计机器学习算法来解决任何回归、分类或排名问题,只要能够使用可微分的损失函数并且具有梯度。不同任务的常见示例损失函数包括:
-
回归(Regression):均方和绝对损失
-
分类(Classification):交叉熵
-
学习排名(Learning to rank):Lambda 排名损失
我们在第六章 机器学习过程中讨论了回归和分类损失函数;学习排名超出了本书的范围,但可以参考中本(2011 年)进行介绍和陈等人(2009 年)了解排名损失的详细信息。
将此通用方法定制为许多特定预测任务的灵活性对于提升其受欢迎程度至关重要。梯度提升也不局限于弱学习器,并且通常使用数层深度的决策树获得最佳性能。
结果梯度提升机器(GBMs)算法背后的主要思想是训练基本学习者学习集成当前损失函数的负梯度。因此,集成的每个添加直接有助于减少整体训练误差,考虑到先前集成成员的错误。由于每个新成员代表数据的新函数,因此也可以说梯度提升是以加法方式优化数据的函数 h[m]。
简而言之,该算法连续拟合弱学习者 h[m],例如决策树,到当前集成评估的损失函数的负梯度,如下公式所示:
换句话说,在给定迭代次数 m 的情况下,该算法计算每个观察值的当前损失的梯度,然后将回归树拟合到这些伪残差上。在第二步中,它确定每个叶节点的最佳预测,以最小化由于将此新学习者添加到集成中而产生的增量损失。
这与独立决策树和随机森林不同,独立决策树和随机森林的预测取决于分配给终端节点的训练样本的结果,即回归的平均值或二元分类的正类别频率。对损失函数梯度的关注还意味着梯度提升使用回归树来学习回归和分类规则,因为梯度始终是连续函数。
最终的集成模型根据个体决策树预测的加权和进行预测,每个个体决策树都已经训练以最小化集成损失,考虑到给定一组特征值的先前预测,如下图所示:
图 12.2:梯度提升算法
梯度提升树已经在许多分类、回归和排名基准上产生了最先进的性能。它们可能是最受欢迎的集成学习算法,作为多种 ML 竞赛中的独立预测器,以及现实世界生产管道中的独立预测器,例如,用于预测在线广告的点击率。
梯度提升成功的基础在于其以增量方式学习复杂的功能关系。然而,该算法的灵活性需要通过调整 超参数 来谨慎管理过拟合风险,这些超参数限制了模型学习训练数据中的噪声而不是信号的倾向。
我们将介绍控制梯度提升树模型复杂性的关键机制,然后使用 sklearn 实现说明模型调整。
如何训练和调整 GBM 模型
尽管集合增长显著,模型复杂性也增加,但提升通常表现出令人瞩目的抗过拟合性。非常低且不增加的验证错误通常与增强对预测的信心相关联:随着提升继续增加集合,以改善最具挑战性情况的预测为目标,它调整决策边界以最大化数据点的距离或间隔。
然而,过拟合确实会发生,梯度提升性能的两个关键驱动因素是集合大小和其组成决策树的复杂性。
控制决策树复杂性的目的是避免学习高度具体的规则,这些规则通常意味着叶节点中的样本数量很少。我们在上一章中介绍了用于限制决策树过拟合训练数据能力的最有效约束。它们包括最小阈值:
-
分裂节点或接受其作为终端节点所需的样本数量。
-
节点质量的改善,通常由分类的纯度或熵,或回归的均方误差衡量,以进一步增长树。
除了直接控制集合大小外,还有各种正则化技术,例如我们在第七章中遇到的缩减,用于岭回归模型和套索线性回归模型的上下文。此外,用于随机森林上下文中的随机化技术也经常应用于梯度提升机。
集合大小和早停
每个提升迭代旨在减少训练损失,增加了对大型集合过拟合的风险。交叉验证是寻找最优集合大小以最小化泛化误差的最佳方法。
由于需要在训练之前指定集合大小,因此监视验证集上的性能并在给定迭代次数时,当验证错误不再减少时中止训练过程是很有用的。这种技术称为早停,经常用于需要大量迭代且容易过拟合的模型,包括深度神经网络。
请记住,在使用相同验证集进行大量试验时,使用早停也会导致过拟合,但只会针对特定的验证集而不是训练集。在开发交易策略时最好避免运行大量实验,因为误发现的风险显著增加。无论如何,保留一个留存测试集以获得对泛化错误的无偏估计是最好的。
缩减和学习率
收缩技术对模型的复杂性增加施加惩罚,将收缩应用到模型的损失函数中。对于提升集成,收缩可以通过缩小每个新集成成员的贡献的因子在 0 和 1 之间进行。这个因子称为提升集成的学习速率。降低学习速率增加收缩,因为它降低了每个新决策树对集成的贡献。
学习速率与集成大小产生相反的影响,学习速率降低时集成大小趋于增加。已发现较低的学习速率结合较大的集成可以减少测试误差,特别是对于回归和概率估计。大量迭代在计算上更昂贵,但是只要个别树保持浅层,快速的、最新的实现通常是可行的。
根据实现的不同,您还可以使用自适应学习率,它会根据迭代次数调整,通常降低后期添加的树的影响。我们将在本章后面看到一些示例。
子采样和随机梯度提升
如前一章节详细讨论的那样,自举平均(Bagging)改善了否则嘈杂分类器的性能。
随机梯度提升在每次迭代中对训练数据进行无替换采样以生成下一棵树(而 Bagging 使用替换采样)。优点是由于较小的样本和通常更好的准确性,计算工作量较小,但是子采样应与收缩结合使用。
你可以看到,超参数的数量不断增加,这导致潜在组合的数量增加。因此,在基于有限的训练数据进行大量试验并从中选择最佳模型时,假阳性的风险增加。最佳方法是按顺序进行,并逐个选择参数值,或者使用低基数子集的组合。
如何使用 sklearn 进行梯度提升
sklearn 的集成模块包含了梯度提升树的实现,用于回归和分类,二元和多类别都有。下面的GradientBoostingClassifier
初始化代码说明了关键的调整参数。笔记本sklearn_gbm_tuning
包含了本节的代码示例。最近(版本 0.21),scikit-learn 引入了一个更快的、但仍然是实验性的HistGradientBoostingClassifier
,灵感来自以下章节中的实现。
可用的损失函数包括导致 AdaBoost 算法的指数损失和对应于概率输出的逻辑回归的偏差。friedman_mse
节点质量度量是均方误差的一种变体,其中包含一个改进分数(请参阅 GitHub 上链接的 scikit-learn 文档),如下所示的代码所示:
# deviance = logistic reg; exponential: AdaBoost
gb_clf = GradientBoostingClassifier(loss='deviance',
# shrinks the contribution of each tree
learning_rate=0.1,
# number of boosting stages
n_estimators=100,
# fraction of samples used t fit base learners
subsample=1.0,
# measures the quality of a split
criterion='friedman_mse',
min_samples_split=2,
min_samples_leaf=1,
# min. fraction of sum of weights
min_weight_fraction_leaf=0.0,
# opt value depends on interaction
max_depth=3,
min_impurity_decrease=0.0,
min_impurity_split=None,
max_features=None,
max_leaf_nodes=None,
warm_start=False,
presort='auto',
validation_fraction=0.1,
tol=0.0001)
类似于AdaBoostClassifier
,这个模型无法处理缺失值。我们将再次使用 12 折交叉验证来获取对滚动 1 个月持有期方向性回报进行分类的错误,如下所示的代码:
gb_cv_result = run_cv(gb_clf, y=y_clean, X=X_dummies_clean)
gb_result = stack_results(gb_cv_result)
我们解析并绘制结果,发现与AdaBoostClassifier
和随机森林相比略有改善,使用默认参数值,测试 AUC 提高到 0.537。图 12.3显示了我们正在跟踪的各种损失指标的箱线图:
图 12.3:scikit-learn 梯度提升分类器的交叉验证性能
如何使用 GridSearchCV 调整参数
model_selection
模块中的GridSearchCV
类便于对我们想要测试的所有超参数值的组合进行系统评估。在下面的代码中,我们将为七个调整参数说明这个功能,一旦定义,就会导致总共个不同的模型配置:
cv = OneStepTimeSeriesSplit(n_splits=12)
param_grid = dict(
n_estimators=[100, 300],
learning_rate=[.01, .1, .2],
max_depth=list(range(3, 13, 3)),
subsample=[.8, 1],
min_samples_split=[10, 50],
min_impurity_decrease=[0, .01],
max_features=['sqrt', .8, 1])
.fit()
方法使用自定义的OneStepTimeSeriesSplit
和roc_auc
分数执行 12 折交叉验证。Sklearn 让我们使用joblib
pickle 实现持久化结果,就像对任何其他模型一样,如下所示的代码:
gs = GridSearchCV(gb_clf,
param_grid,
cv=cv,
scoring='roc_auc',
verbose=3,
n_jobs=-1,
return_train_score=True)
gs.fit(X=X, y=y)
# persist result using joblib for more efficient storage of large numpy arrays
joblib.dump(gs, 'gbm_gridsearch.joblib')
GridSearchCV
对象在完成后具有几个附加属性,我们可以在加载拾取的结果后访问它们。我们可以使用它们来了解哪种超参数组合表现最佳及其平均交叉验证 AUC 分数,这导致与默认值相比略有改善。如下代码所示:
pd.Series(gridsearch_result.best_params_)
learning_rate 0.01
max_depth 9.00
max_features 1.00
min_impurity_decrease 0.01
min_samples_split 50.00
n_estimators 300.00
subsample 1.00
gridsearch_result.best_score_
0.5569
参数对测试分数的影响
GridSearchCV
结果存储了平均交叉验证分数,以便我们可以分析不同超参数设置如何影响结果。
右侧面板中的六个 seaborn swarm 图展示了所有超参数值的 AUC 测试分数分布。在这种情况下,最高的 AUC 测试分数需要低learning_rate
和大的max_features
值。一些参数设置,比如低learning_rate
,会产生一系列取决于其他参数的互补设置的结果:
图 12.4:scikit-learn 梯度提升模型的超参数影响
我们现在将探讨超参数设置如何共同影响交叉验证性能。为了深入了解参数设置如何相互作用,我们可以训练一个DecisionTreeRegressor
,以平均 CV AUC 作为结果,以及参数设置,以一位热编码或虚拟格式编码(详情请参见笔记本)。树结构突出显示,使用所有特征(max_features=1
)、低learning_rate
和max_depth
大于三导致了最佳结果,如下图所示:
图 12.5:梯度提升模型超参数设置对测试性能的影响
图 12.4左侧面板的条形图显示了超参数设置对产生不同结果的影响,通过它们对已经达到最大深度的决策树的特征重要性来衡量。自然地,出现在树顶部附近的特征也累积了最高的重要性分数。
如何在留存集上进行测试
最后,我们想要评估我们从GridSearchCV
练习中排除的留存集上最佳模型的性能。它包含样本期的最后 7 个月(截至 2018 年 2 月;详情请参阅笔记本)。
我们根据留存期的第一个月的 AUC 得分(为 0.5381)获得了一般化性能估计,使用以下代码示例:
idx = pd.IndexSlice
auc = {}
for i, test_date in enumerate(test_dates):
test_data = test_feature_data.loc[idx[:, test_date], :]
preds = best_model.predict(test_data)
auc[i] = roc_auc_score(y_true=test_target.loc[test_data.index], y_score=preds)
auc = pd.Series(auc)
sklearn 梯度提升实现的缺点是有限的训练速度,这使得快速尝试不同的超参数设置变得困难。在下一节中,我们将看到,在过去几年中出现了几个优化实现,这些实现显着减少了训练大规模模型所需的时间,并且极大地扩展了这种高效算法的应用范围。
使用 XGBoost、LightGBM 和 CatBoost
在过去几年中,出现了几个新的梯度提升实现,利用了各种创新加速训练,提高资源效率,并允许算法扩展到非常大的数据集。新实现及其来源如下:
-
XGBoost:由 T. Chen 在他的博士期间于 2014 年启动(T. Chen 和 Guestrin 2016)
-
LightGBM:由微软于 2017 年 1 月发布(Ke 等人 2017)
-
CatBoost:由 Yandex 于 2017 年 4 月发布(Prokhorenkova 等人 2019)
这些创新解决了训练梯度提升模型的特定挑战(请参阅本章的 GitHub 上的README
文件,以获取文档链接)。XGBoost 实现是第一个获得流行的新实现:在 Kaggle 于 2015 年发布的 29 个获奖解决方案中,有 17 个解决方案使用了 XGBoost。其中有 8 个仅依赖于 XGBoost,而其他解决方案将 XGBoost 与神经网络结合使用。
我们将首先介绍随时间发展并最终趋同的关键创新(以便大多数功能对于所有实现都是可用的),然后说明它们的实现。
算法创新如何提升性能
随机森林可以通过在独立的自助样本上生长个体树来并行训练。相反,梯度提升的顺序方法会减慢训练速度,从而使得需要调整的大量超参数的实验变得复杂,这些超参数需要适应任务和数据集的特性。
要向集成中添加一棵树,该算法最小化与损失函数的负梯度相关的预测误差,类似于传统的梯度下降优化器。因此,训练期间的计算成本与评估每个特征的潜在分割点的时间成正比。
二阶损失函数近似
最重要的算法创新通过使用依赖于二阶导数的近似来降低评估损失函数的成本,类似于牛顿法来寻找稳定点。因此,评分潜在分割变得更快。
如前所述,梯度提升集成H[M]是逐步训练的,以最小化预测误差和正则化惩罚的总和。将步骤m后的集成对结果y[i]的预测表示为,作为可微的凸损失函数,衡量结果与预测之间的差异,
作为随着集成H[M]的复杂性增加而增加的惩罚。增量假设h[m]旨在最小化以下目标L:
正则化惩罚有助于通过偏爱使用简单但具有预测性的回归树的模型来避免过拟合。例如,在 XGBoost 的情况下,回归树h的惩罚取决于每棵树的叶子数T、每个终端节点的回归树分数w以及超参数和
。这在下面的公式中总结如下:
因此,在每一步中,该算法贪婪地添加最能改善正则化目标的假设h[m]。基于泰勒展开的损失函数的二阶近似加速了目标的评估,如下面的公式所总结的那样:
在这里,g[i]是在给定特征值的情况下添加新学习器之前的损失函数的一阶梯度,h[i]是相应的二阶梯度(或 Hessian)值,如下面的公式所示:
XGBoost 算法是第一个利用损失函数的这种近似来计算给定树结构的最优叶子分数和损失函数对应值的开源算法。得分由终端节点中样本的梯度和 Hessian 总和的比率组成。它使用此值对信息增益进行评分,该信息增益是结果章节中看到的节点不纯度度量的一个类似版本,但适用于任意损失函数。有关详细推导,请参见 Chen 和 Guestrin(2016)。
简化的分割查找算法
sklearn 的原始梯度提升实现找到枚举连续特征的所有选项的最佳分裂。这个精确贪婪算法由于每个特征可能的分裂选项数量可能非常大,计算上是非常耗费资源的。当数据不适合内存或在多台机器上的分布式设置中进行训练时,这种方法面临额外的挑战。
一个近似分裂查找算法通过将特征值分配给用户确定的一组箱中的特征值来减少分裂点的数量,这也可以在训练期间极大地减少内存需求。这是因为每个箱只需要存储一个值。XGBoost 引入了一个分位数草图算法,将加权训练样本分成百分位数箱,以实现均匀分布。XGBoost 还引入了处理稀疏数据的能力,原因是缺失值、频繁的零梯度统计和独热编码,并且可以学习给定分裂的最佳默认方向。因此,该算法只需要评估非缺失值。
相反,LightGBM 使用基于梯度的单边采样(GOSS)来排除具有小梯度的大部分样本,并仅使用其余部分来估计信息增益并相应地选择分裂值。具有较大梯度的样本需要更多的训练,并且往往对信息增益贡献更多。
LightGBM 还使用独占特征捆绑来组合彼此互斥的特征,即它们很少同时取非零值,以减少特征数量。因此,LightGBM 是发布时最快的实现,并且通常仍然表现最佳。
深度优先与叶子节点优先增长
LightGBM 与 XGBoost 和 CatBoost 的不同之处在于它如何确定要分裂哪些节点的优先级。LightGBM 决定按叶子节点进行分裂,即,它分裂最大化信息增益的叶子节点,即使这会导致树不平衡。相反,XGBoost 和 CatBoost 按深度扩展所有节点,并首先在给定深度级别上分裂所有节点,然后再添加更多级别。这两种方法以不同的顺序扩展节点,并且除了完全树外,将产生不同的结果。以下图示了这两种方法:
图 12.6:深度优先 vs 叶子节点优先增长
LightGBM 的叶子节点优先分裂倾向于增加模型的复杂性,并可能加快收敛速度,但也增加了过拟合的风险。一个深度为n级别的树有最多 2^n 个终端节点,而具有 2^n 个叶子节点的叶子优先树可能有更多级别,并且在某些叶子中包含相应地更少的样本。因此,调整 LightGBM 的num_leaves
设置需要额外的小心,该库同时允许我们控制max_depth
以避免不必要的节点不平衡。LightGBM 的更高版本也提供了深度优先树增长。
基于 GPU 的训练
所有新的实现都支持在一个或多个 GPU 上进行训练和预测,以实现显著的加速。它们与当前支持 CUDA 的 GPU 兼容。安装要求因版本而异,而且正在迅速发展。XGBoost 和 CatBoost 实现适用于几个当前版本,但是 LightGBM 可能需要本地编译(请参阅 GitHub 获取文档链接)。
加速取决于库和数据类型,范围从低位、个位数的倍数到数十倍因子。只需更改任务参数即可激活 GPU,并且不需要进行其他超参数修改。
DART – 增加回归树的辍学
Rashmi 和 Gilad-Bachrach(2015)提出了一个新模型,用于训练梯度提升树以解决他们称之为过度专业化的问题:在后续迭代中添加的树往往只影响少数实例的预测,同时对其余实例的贡献较小。然而,该模型的样本外表现可能会受到影响,并且可能会对少数树的贡献过度敏感。
新算法采用了辍学,在学习更准确的深度神经网络时已被成功使用,其中在训练期间会静音一部分神经连接。因此,更高层次的节点无法依赖于少数连接传递预测所需的信息。这种方法对于许多任务的深度神经网络的成功做出了重要贡献,还与其他学习技术(如逻辑回归)一起使用。
DART,或者称为增加回归树的辍学,在树的层面上操作,而不是在单个特征上进行操作。其目标是使使用 DART 生成的整体树对最终预测贡献更加均匀。在某些情况下,这已被证明对排名、回归和分类任务产生更准确的预测。该方法首次在 LightGBM 中实现,并且也适用于 XGBoost。
对分类特征的处理
CatBoost 和 LightGBM 实现可直接处理分类变量,无需进行虚拟编码。
CatBoost 实现(因其对分类特征的处理而命名)包括几种处理此类特征的选项,除了自动独热编码外。它将单个特征的类别或几个特征的组合分配给数值。换句话说,CatBoost 可以从现有特征的组合创建新的分类特征。与单个特征或特征组合的类别级别相关的数值取决于它们与结果值的关系。在分类情况下,这与在样本上基于先验和平滑因子计算的观察到正类的概率相关。有关更详细的数值示例,请参阅 CatBoost 文档。
LightGBM 实现将分类特征的级别分组,以最大化组内相对于结果值的同质性(或最小化方差)。XGBoost 实现不直接处理分类特征,需要独热(或虚拟)编码。
附加功能和优化
XGBoost 在几个方面优化计算以实现多线程。最重要的是,它将数据保留在压缩的列块中,其中每列按相应特征值排序。它在训练之前计算此输入数据布局一次,并在整个过程中重复使用以分摊前期成本。因此,对列上的分割统计的搜索变成了可以并行进行的分位数的线性扫描,并支持列子抽样
随后发布的 LightGBM 和 CatBoost 库基于这些创新,而 LightGBM 通过优化线程和减少内存使用量进一步加速了训练。由于它们的开源性质,库往往随着时间的推移而趋于融合。
XGBoost 还支持单调性约束。这些约束确保给定特征的值在其整个范围内与结果呈正相关或负相关。它们有助于将关于模型的外部假设纳入其中,这些假设已知为真。
带增强的多空交易策略
在本节中,我们将设计、实现和评估一个由梯度提升模型产生的每日收益预测驱动的美国股票交易策略。我们将使用 Quandl Wiki 数据来设计一些简单的特征(详见笔记本preparing_the_model_data
),在使用 2015/16 作为验证期间选择模型,并在 2017 年进行样本外测试。
与之前的示例一样,我们将提供一个框架并构建一个具体的示例,您可以根据自己的实验进行调整。您可以变化的方面有很多,从资产类别和投资范围到更精细的方面,如特征、持有期或交易规则。例如,查看附录中的 Alpha 因子库以获取更多的附加功能。
我们将保持交易策略简单,只使用单个 ML 信号;实际应用可能会使用来自不同来源的多个信号,例如在不同数据集上训练的互补 ML 模型,或者具有不同前瞻或回溯期的模型。它还将使用复杂的风险管理,从简单的止损到价值风险分析。
使用 LightGBM 和 CatBoost 生成信号
XGBoost、LightGBM 和 CatBoost 提供了多种语言的接口,包括 Python,并且具有与其他 scikit-learn 功能兼容的 scikit-learn 接口,如GridSearchCV
,以及用于训练和预测梯度提升模型的自己的方法。我们在本章的前两节中使用的笔记本boosting_baseline.ipynb
说明了每个库的 scikit-learn 接口。该笔记本比较了各种库的预测性能和运行时间。它通过使用我们在第四章,金融特征工程-如何研究 Alpha 因子中创建的特征,来训练提升模型以预测 2001-2018 年间的美国股票月回报。
下图左侧显示了使用所有实现的默认设置预测 1 个月股票价格波动的准确性,以 12 倍交叉验证产生的平均 AUC 为指标:
图 12.7:各种梯度提升模型的预测性能和运行时间
预测性能的范围从 0.525 到 0.541 不等。这看起来可能是一个小范围,但随机基准 AUC 为 0.5,最差的模型将基准提高了 5 个百分点,而最佳模型则提高了 8 个百分点,这相当于相对增长了 60 个百分点。使用 GPU 的 CatBoost 和使用整数编码的分类变量的 LightGBM 表现最佳,突显了将分类变量转换为数值变量的之前概述的好处。
实验的运行时间变化显著大于预测性能。在此数据集上,LightGBM 比 XGBoost 或 CatBoost(使用 GPU)快 10 倍,而预测性能非常相似。由于这种巨大的速度优势,并且因为 GPU 并不是每个人都可以使用的,我们将专注于 LightGBM,但也会说明如何使用 CatBoost;XGBoost 与两者非常相似。
使用 LightGBM 和 CatBoost 模型需要:
-
创建特定于库的二进制数据格式
-
配置和调整各种超参数
-
评估结果
我们将在接下来的章节中描述这些步骤。笔记本trading_signals_with_lightgbm_and_catboost
包含了本小节的代码示例,除非另有说明。
从 Python 到 C++——创建二进制数据格式
LightGBM 和 CatBoost 都是用 C++ 编写的,并在预先计算特征统计信息之前将 Python 对象(如 pandas DataFrame)转换为二进制数据格式,以加速搜索分割点,如前一节所述。结果可以持久化以加速后续训练的启动。
我们将在前一节提到的数据集子集上进行交叉验证,直到 2016 年底,以验证多种模型配置的效果,包括不同的回溯和前瞻窗口,以及不同的向前期和超参数。我们的模型选择方法将类似于我们在上一章中使用的方法,并使用在第七章介绍的自定义 MultipleTimeSeriesCV
。
我们选择训练和验证集,识别标签和特征,并对值从零开始的分类变量进行整数编码,这是 LightGBM 预期的(只要类别代码的值小于 2³² 即可,但可以避免警告):
data = (pd.read_hdf('data.h5', 'model_data')
.sort_index()
.loc[idx[:, :'2016'], :])
labels = sorted(data.filter(like='fwd').columns)
features = data.columns.difference(labels).tolist()
categoricals = ['year', 'weekday', 'month']
for feature in categoricals:
data[feature] = pd.factorize(data[feature], sort=True)[0]
笔记本示例遍历许多配置,可选择使用随机样本来加快使用多样化子集进行模型选择的速度。目标是在不尝试每种可能的组合的情况下识别最具影响力的参数。
为此,我们创建二进制 Dataset
对象。对于 LightGBM,这看起来如下所示:
import lightgbm as lgb
outcome_data = data.loc[:, features + [label]].dropna()
lgb_data = lgb.Dataset(data=outcome_data.drop(label, axis=1),
label=outcome_data[label],
categorical_feature=categoricals,
free_raw_data=False)
CatBoost 数据结构称为 Pool
,工作原理类似:
cat_cols_idx = [outcome_data.columns.get_loc(c) for c in categoricals]
catboost_data = Pool(label=outcome_data[label],
data=outcome_data.drop(label, axis=1),
cat_features=cat_cols_idx)
对于这两个库,我们根据结果信息确定要转换为数值变量的分类变量。CatBoost 实现需要使用索引而不是标签来识别特征列。
我们可以简单地使用 MultipleTimeSeriesCV
提供的训练和验证集索引来切片二进制数据集,如下所示,在交叉验证期间进行,将两个示例合并为一个片段:
for i, (train_idx, test_idx) in enumerate(cv.split(X=outcome_data)):
lgb_train = lgb_data.subset(train_idx.tolist()).construct()
train_set = catboost_data.slice(train_idx.tolist())
如何调整超参数
LightGBM 和 CatBoost 实现带有许多允许精细控制的超参数。每个库都有参数设置来:
-
指定任务目标和学习算法
-
设计基础学习者
-
应用各种正则化技术
-
在训练期间处理提前停止
-
启用 GPU 或 CPU 并行化
每个库的文档详细介绍了各种参数。由于它们实现了相同算法的变体,参数可能指的是相同的概念,但跨库具有不同的名称。GitHub 仓库列出了澄清 XGBoost 和 LightGBM 参数具有相似效果的资源。
目标和损失函数
这些库支持几种提升算法,包括树和线性基础学习者的梯度提升,以及 LightGBM 和 XGBoost 的 DART。LightGBM 还支持我们之前描述的 GOSS 算法,以及随机森林。
梯度提升的吸引力在于对任意可微损失函数的有效支持,每个库都提供了各种选项用于回归、分类和排名任务。除了选择的损失函数外,还可以使用其他评估指标来监控训练和交叉验证期间的性能。
学习参数
梯度提升模型通常使用决策树来捕捉特征交互,并且个体树的大小是最重要的调整参数。XGBoost 和 CatBoost 将max_depth
默认设置为 6。相反,LightGBM 使用默认的num_leaves
值为 31,这对应于平衡树的五个级别,但不对级别数量施加任何限制。为了避免过拟合,num_leaves
应该小于 2^(max_depth)。例如,对于表现良好的max_depth
值为 7,您应该将num_leaves
设置为 70-80,而不是 2⁷=128,或者直接约束max_depth
。
树的数量或提升迭代次数定义了整体集合的大小。所有库都支持early_stopping
来在给定的迭代次数内一旦损失函数不再注册进一步改进就中止训练。因此,通常最有效的方法是设置大量迭代并根据验证集上的预测性能停止训练。但是,请注意,由于暗示的前瞻偏差,验证误差会被偏高。
这些库还允许使用自定义损失指标来跟踪训练和验证性能并执行early_stopping
。笔记本演示了如何为 LightGBM 和 CatBoost 编写信息系数(IC)。但是,为了避免偏差,我们不会依赖early_stopping
进行实验。
正则化
所有的库都实现了对基础学习器的正则化策略,例如对样本数量的最小值或对拆分和叶节点所需的最小信息增益的限制。
它们还支持在整体集成层面上使用收缩来进行正则化,这通过限制新树的贡献来实现学习率。也可以通过回调函数实现自适应学习率,随着训练的进行降低学习率,例如在神经网络的背景下已成功使用。此外,梯度提升损失函数可以使用 L1 或 L2 正则化进行约束,类似于岭回归和套索回归模型,例如,通过增加添加更多树的惩罚来约束梯度提升损失函数。
这些库还允许使用装袋或列抽样来随机化树的生长,用于随机森林,以及去相关化预测错误以减少总体方差。对于近似拆分查找,特征量化添加了更大的箱作为另一个选项,以防止过拟合。
随机网格搜索
为了探索超参数空间,我们指定了我们想要测试的关键参数的值的组合。sklearn 库支持RandomizedSearchCV
来交叉验证从指定分布中随机抽样的一部分参数组合。我们将实现一个自定义版本,允许我们监控性能,以便一旦满意结果就可以中止搜索过程,而不是事先指定一组迭代次数。
为此,我们为每个库的相关超参数指定了选项,使用 itertools 库提供的笛卡尔积生成器生成所有组合,并对结果进行了洗牌。
就 LightGBM 而言,我们关注学习率、树的最大大小、训练期间特征空间的随机化以及需要拆分的数据点的最小数量。这导致以下代码,其中我们随机选择了一半的配置:
learning_rate_ops = [.01, .1, .3]
max_depths = [2, 3, 5, 7]
num_leaves_opts = [2 ** i for i in max_depths]
feature_fraction_opts = [.3, .6, .95]
min_data_in_leaf_opts = [250, 500, 1000]
cv_params = list(product(learning_rate_ops,
num_leaves_opts,
feature_fraction_opts,
min_data_in_leaf_opts))
n_params = len(cv_params)
# randomly sample 50%
cvp = np.random.choice(list(range(n_params)),
size=int(n_params / 2),
replace=False)
cv_params_ = [cv_params[i] for i in cvp]
现在,我们基本上已经准备就绪:在每次迭代期间,我们根据lookahead
、train_period_length
和test_period_length
参数创建一个MultipleTimeSeriesCV
实例,并相应地在一个 2 年的时间段内交叉验证所选的超参数。
请注意,我们生成了一系列的合奏大小的验证预测,以便我们可以推断出最佳迭代次数:
num_iterations = [10, 25, 50, 75] + list(range(100, 501, 50))
num_boost_round = num_iterations[-1]
for lookahead, train_length, test_length in test_params:
n_splits = int(2 * YEAR / test_length)
cv = MultipleTimeSeriesCV(n_splits=n_splits,
lookahead=lookahead,
test_period_length=test_length,
train_period_length=train_length)
for p, param_vals in enumerate(cv_params_):
for i, (train_idx, test_idx) in enumerate(cv.split(X=outcome_data)):
lgb_train = lgb_data.subset(train_idx.tolist()).construct()
model = lgb.train(params=params,
train_set=lgb_train,
num_boost_round=num_boost_round,
verbose_eval=False)
test_set = outcome_data.iloc[test_idx, :]
X_test = test_set.loc[:, model.feature_name()]
y_test = test_set.loc[:, label]
y_pred = {str(n): model.predict(X_test, num_iteration=n) for n in num_iterations}
请查看笔记本trading_signals_with_lightgbm_and_catboost
以获取更多细节,包括如何记录结果、计算和捕获我们需要评估结果的各种指标,接下来我们将转向这一点。
如何评估结果
现在,交叉验证了大量配置,我们需要评估预测性能,以确定为我们未来的交易策略生成最可靠和最有利可图的信号的模型。笔记本evaluate_trading_signals
包含了本节的代码示例。
我们生成了更多的 LightGBM 模型,因为它的运行速度比 CatBoost 快一个数量级,因此将相应地展示一些评估策略。
交叉验证结果 – LightGBM 对比 CatBoost
首先,我们比较了两个库生成的模型在所有配置方面的预测性能,包括它们的验证 IC,既跨整个验证期间又在日预测上平均。
下图显示,LightGBM 的表现(略微)优于 CatBoost,特别是对于更长的预测期。这并不是完全公平的比较,因为我们对 LightGBM 运行了更多的配置,这也不出所料地显示了更广泛的结果分散:
图 12.8:LightGBM 和 CatBoost 模型在三个预测期内的总体和日 IC
无论如何,我们将专注于 LightGBM 的结果;请查看笔记本trading_signals_with_lightgbm_and_catboost
和evaluate_trading_signals
以获取有关 CatBoost 的更多详细信息或运行您自己的实验。
鉴于模型结果之间的显著分散,让我们更仔细地研究表现最佳的参数设置。
最佳表现参数设置
表现最佳的 LightGBM 模型使用以下参数进行三个不同的预测时间范围(详情请参阅笔记本):
Lookahead | Learning Rate | # Leaves | Feature Fraction | Min. Data in Leaf | Daily Average | Overall |
---|---|---|---|---|---|---|
IC | # Rounds | IC | # Rounds | |||
1 | 0.3 | 4 | 95% | 1,000 | 1.70 | 75 |
1 | 0.3 | 4 | 95% | 250 | 1.34 | 250 |
1 | 0.3 | 4 | 95% | 1,000 | 1.70 | 75 |
5 | 0.1 | 8 | 95% | 1,000 | 3.95 | 300 |
5 | 0.3 | 4 | 95% | 1,000 | 3.43 | 150 |
5 | 0.3 | 4 | 95% | 1,000 | 3.43 | 150 |
21 | 0.1 | 8 | 60% | 500 | 5.84 | 25 |
21 | 0.1 | 32 | 60% | 250 | 5.89 | 50 |
21 | 0.1 | 4 | 60% | 250 | 7.33 | 75 |
请注意,较浅的树在三个预测时间范围内产生了最佳的整体 IC。长达 4.5 年的较长训练也产生了更好的结果。
超参数影响 - 线性回归
接下来,我们想了解是否存在系统性的、统计上的超参数与每日预测结果之间的关系。为此,我们将使用各种 LightGBM 超参数设置作为虚拟变量,并将每日验证 IC 作为结果进行线性回归。
图 12.9中的图表显示了 1 天和 21 天预测时间范围的系数估计及其置信区间。对于较短的时间范围,更长的回溯期、更高的学习率和更深的树(更多叶节点)会产生积极影响。对于较长的时间范围,情况稍微不太清晰:较短的树效果更好,但回溯期不显著。更高的特征采样率也有所帮助。在这两种情况下,更大的集成效果更好。请注意,这些结果仅适用于此特定示例。
图 12.9:不同预测时间范围的系数估计及其置信区间
使用 IC 而不是信息系数
我们对前五个模型进行平均,并提供相应的价格给 Alphalens,以便计算在不同持有期内投资于每日因子五分位数的等权重投资组合上获得的平均周期回报:
指标 | 持有期 |
---|---|
1D | 5D |
平均周期间差异(基点) | 12.1654 |
Ann. alpha | 0.1759 |
beta | 0.0891 |
我们发现顶部和底部五分位数之间有 12 个基点的差距,这意味着年化 alpha 为 0.176,而 beta 低至 0.089(见图 12.10):
图 12.10:因子分位数的平均和累积回报
以下图表显示了在最佳表现模型的 2 年验证期内,1 天和 21 天预测的季度滚动 IC:
图 12.11:1 天和 21 天回报预测的滚动 IC
短期和长期模型的平均 IC 分别为 2.35 和 8.52,在样本中大多数天数保持正值。
我们现在将看看如何在选择模型、生成预测、定义交易策略和评估其性能之前,获得有关模型工作方式的额外见解。
在黑匣子内——解释 GBM 结果
了解为什么模型会预测特定结果对于多种原因非常重要,包括信任、可操作性、问责制和调试。当目标是更多地了解研究对象的基本驱动因素时,模型揭示的特征与结果之间的非线性关系以及特征之间的相互作用也具有价值。
获取树集成方法(如梯度提升或随机森林模型)预测见解的一种常见方法是将特征重要性值归因于每个输入变量。这些特征重要性值可以针对单个预测或全局计算整个数据集(即所有样本),以获得模型如何进行预测的更高层次的视角。
本节的代码示例位于笔记本model_interpretation
中。
特征重要性
有三种主要方法来计算全局特征重要性值:
-
增益:这是一种经典方法,由 Leo Breiman 于 1984 年引入,它使用给定特征所有拆分贡献的损失或不纯度的总减少。动机在很大程度上是启发式的,但这是一种常用的特征选择方法。
-
分割计数:这是一种替代方法,根据所选特征的选择基于产生的信息增益来计算特征用于做出分割决策的频率。
-
排列:这种方法随机排列测试集中的特征值,并测量模型误差的变化程度,假设一个重要特征应该会导致预测误差大幅增加。不同的排列选择会导致此基本方法的替代实现。
计算单个预测的个性化特征重要性值,计算特征对单个预测的相关性较少见。这是因为可用的模型不可知解释方法比树特定方法慢得多。
所有梯度提升实现在训练后都会提供特征重要性得分作为模型属性。LightGBM 库提供了两个版本,如下列表所示:
-
增益:特征对减少损失的贡献
-
split:该特征被使用的次数
这些值可通过训练模型的 .feature_importance()
方法和相应的 importance_type
参数获得。对于表现最佳的 LightGBM 模型,20 个最重要特征的结果如 图 12.12 所示:
图 12.12:LightGBM 特征重要性
时间周期指标占主导地位,其次是最新的回报、标准化 ATR、部门虚拟变量和动量指标(有关实施细节,请参见笔记本)。
部分依赖性图
除了总结单个特征对模型预测的贡献之外,部分依赖性图还可视化目标变量与一组特征之间的关系。梯度提升树的非线性性质导致这种关系取决于所有其他特征的值。因此,我们将对这些特征进行边际化。通过这样做,我们可以将部分依赖性解释为预期的目标响应。
我们只能为单个特征或特征对可视化部分依赖性。后者会产生等高线图,显示出不同预测概率的特征值组合如何产生不同的组合,如下面的代码所示:
fig, axes = plot_partial_dependence(estimator=best_model,
X=X,
features=['return_12m', 'return_6m',
'CMA', ('return_12m',
'return_6m')],
percentiles=(0.01, 0.99),
n_jobs=-1,
n_cols=2,
grid_resolution=250)
经过一些额外的格式化(请参见配套笔记本),我们得到了如 图 12.13 所示的结果:
图 12.13:scikit-learn GradientBoostingClassifier 的部分依赖性图
右下图显示了在消除[1%,99%]分位数的异常值后,对于滞后的 12 个月和 6 个月回报值范围内的下个月正回报概率的依赖性。 month_9
变量是一个虚拟变量,因此图形类似于阶梯函数。我们还可以按照以下代码将依赖性可视化为 3D:
targets = ['return_12m', 'return_6m']
pdp, axes = partial_dependence(estimator=gb_clf,
features=targets,
X=X_,
grid_resolution=100)
XX, YY = np.meshgrid(axes[0], axes[1])
Z = pdp[0].reshape(list(map(np.size, axes))).T
fig = plt.figure(figsize=(14, 8))
ax = Axes3D(fig)
surf = ax.plot_surface(XX, YY, Z,
rstride=1,
cstride=1,
cmap=plt.cm.BuPu,
edgecolor='k')
ax.set_xlabel(' '.join(targets[0].split('_')).capitalize())
ax.set_ylabel(' '.join(targets[1].split('_')).capitalize())
ax.set_zlabel('Partial Dependence')
ax.view_init(elev=22, azim=30)
这产生了关于滞后 6 个月和 12 个月回报的部分依赖性的 1 个月回报方向的以下 3D 图:
图 12.14:部分依赖性的 3D 图
SHapley Additive exPlanations
在 2017 年 NIPS 会议上,华盛顿大学的 Scott Lundberg 和 Su-In Lee 提出了一种解释树集成模型输出中单个特征贡献的新方法,称为SHapley Additive exPlanations,或SHAP值。
这种新算法与观察到的树集成的特征归因方法不一致,如我们之前所看到的那样,即,增加模型中特征对输出的影响的变化可能降低该特征的重要性值(有关详细说明,请参见 GitHub 上的参考资料)。
SHAP 值统一了协作博弈理论和局部解释的思想,并根据期望表明在理论上是最优的、一致的和局部准确的。最重要的是,Lundberg 和 Lee 开发了一种算法,成功地将这些与模型无关的、可加性的特征归因方法的复杂性从 O(TLDM) 降低到 O(TLD²),其中 T 和 M 分别是树和特征的数量,D 和 L 是树中的最大深度和叶子数。这一重要的创新使得可以在几秒钟内解释以前难以处理的具有数千棵树和特征的模型的预测。一个开源实现在 2017 年末可用,并兼容 XGBoost、LightGBM、CatBoost 和 sklearn 树模型。
夏普利值起源于博弈论,作为一种为合作博弈中的每个玩家分配价值的技术,反映了他们对团队成功的贡献。SHAP 值是对博弈论概念在基于树的模型中的一种改编,并计算每个特征和每个样本的 SHAP 值。它们衡量了一个特征对给定观察的模型输出的贡献。因此,SHAP 值提供了不同的见解,说明了特征的影响如何随着样本的变化而变化,这在这些非线性模型中的交互效应的作用中至关重要。
如何按特征总结 SHAP 值
要对多个样本的特征重要性进行高层次概述,有两种绘制 SHAP 值的方法:一种是对所有样本进行简单平均,类似于之前计算的全局特征重要性度量(如 图 12.15 左侧面板所示),或者绘制散点图以显示每个特征对每个样本的影响(如图的右侧面板所示)。使用兼容库中的训练模型和匹配输入数据,它们非常容易产生,如下面的代码所示:
# load JS visualization code to notebook
shap.initjs()
# explain the model's predictions using SHAP values
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)
shap.summary_plot(shap_values, X_test, show=False)
散点图根据其在所有样本中的总 SHAP 值对特征进行排序,然后显示每个特征对模型输出的影响,由 SHAP 值来衡量,作为特征值的函数,其颜色表示特征值,红色表示相对于特征范围的高值,蓝色表示低值:
图 12.15:SHAP 概要图
与传统特征重要性相比,图 12.12 显示的有一些有趣的差异;即 MACD 指标更为重要,以及相对收益指标。
如何使用力量图来解释一个预测
以下图像中的力量图显示了各种特征及其值对模型输出的 累积影响,在本例中为 0.6,比基础值 0.13(提供的数据集的平均模型输出)要高得多。突出显示为红色的特征并向右箭头指向的特征增加了输出。月份为十月是最重要的特征,并将输出从 0.338 增加到 0.537,而年份为 2017 则降低了输出。
因此,我们可以得到模型如何得出特定预测的详细分解,如下图所示:
图 12.16:SHAP 力量图
我们还可以同时为多个数据点或预测计算 多个数据点的力量图,并使用 聚类可视化 来洞察数据集中某些影响模式的普遍程度。下图显示了前 1,000 个观察结果的力量图,旋转了 90 度,水平堆叠,并根据给定观察结果中不同特征对结果的影响排序。
实现使用数据点的特征 SHAP 值的分层凝聚聚类来识别这些模式,并显示结果以进行探索性分析(请参见笔记本),如下图所示的代码:
shap.force_plot(explainer.expected_value, shap_values[:1000,:],
X_test.iloc[:1000])
这将产生以下输出:
图 12.17:SHAP 聚类力量图
如何分析特征交互
最后,SHAP 值使我们能够通过将这些交互作用与主要效应分开来获得有关不同特征之间交互作用效应的额外见解。shap.dependence_plot
可以定义如下:
shap.dependence_plot(ind='r01',
shap_values=shap_values,
features=X,
interaction_index='r05',
title='Interaction between 1- and 5-Day Returns')
它显示了对 1 个月回报率的不同值(x 轴)如何影响结果(y 轴上的 SHAP 值),并根据 3 个月回报率进行区分(请参见以下图):
图 12.18:SHAP 交互作用图
SHAP 值在每个单独预测的级别提供细粒度的特征归因,并通过(交互式)可视化实现对复杂模型的更丰富的检查。本节前面显示的 SHAP 摘要点图(图 12.15)比全局特征重要性条形图提供了更多差异化的见解。单个聚类预测的力量图允许更详细的分析,而 SHAP 依赖图捕获交互作用效应,因此提供比部分依赖图更准确和详细的结果。
与任何当前特征重要性度量一样,SHAP 值的局限性涉及高度相关的变量的影响归因,因为它们的相似影响可以以任意方式分解。
基于提升集成的策略回溯测试
在本节中,我们将使用 Zipline 评估一个长短策略的表现,该策略根据每日收益预测信号输入 25 个多头和 25 个空头头寸。为此,我们将选择表现最佳的模型,生成预测,并设计根据这些预测行事的交易规则。
根据我们对交叉验证结果的评估,我们将选择一个或多个模型来为新的样本外期间生成信号。在本例中,我们将结合对最佳 10 个 LightGBM 模型的预测,以减少基于 Alphalens 计算的其稳定均值分位数传播的 1 天预测周期的方差。
我们只需获取表现最佳模型的参数设置,然后相应地进行训练。笔记本making_out_of_sample_predictions
包含必要的代码。模型训练使用表现最佳模型的超参数设置和测试期数据,但在其他方面非常紧密地遵循了交叉验证时使用的逻辑,因此我们将在此省略细节。
在笔记本backtesting_with_zipline
中,我们已经组合了验证和测试期间前 10 个模型的预测,如下所示:
def load_predictions(bundle):
predictions = (pd.read_hdf('predictions.h5', 'train/01')
.append(pd.read_hdf('predictions.h5', 'test/01')
.drop('y_test', axis=1)))
predictions = (predictions.loc[~predictions.index.duplicated()]
.iloc[:, :10]
.mean(1)
.sort_index()
.dropna()
.to_frame('prediction'))
我们将使用我们在《第八章》《ML4T 工作流程-从模型到策略回测》中引入的自定义 ML 因子,将预测导入并在管道中进行可访问。
我们将从验证期的开始到测试期的结束执行Pipeline
。图 12.19显示(不足为奇)样本内表现稳健,年回报率为 27.3%,而样本外为 8.0%。图片的右侧面板显示了与标普 500 相对的累计回报率:
指标 | 全部 | 样本内 | 样本外 |
---|---|---|---|
年回报率 | 20.60% | 27.30% | 8.00% |
累计回报率 | 75.00% | 62.20% | 7.90% |
年波动率 | 19.40% | 21.40% | 14.40% |
夏普比率 | 1.06 | 1.24 | 0.61 |
最大回撤 | -17.60% | -17.60% | -9.80% |
Sortino 比率 | 1.69 | 2.01 | 0.87 |
偏度 | 0.86 | 0.95 | -0.16 |
峰度 | 8.61 | 7.94 | 3.07 |
每日风险价值 | -2.40% | -2.60% | -1.80% |
日换手率 | 115.10% | 108.60% | 127.30% |
Alpha | 0.18 | 0.25 | 0.05 |
Beta | 0.24 | 0.24 | 0.22 |
夏普比率分别为样本内为 1.24,样本外为 0.61;右侧面板显示了季度滚动数值。样本内的 Alpha 为 0.25,样本外为 0.05,对应的 Beta 值分别为 0.24 和 0.22。最严重的回撤导致 2015 年下半年损失了 17.59%:
图 12.19:策略表现—累计回报率和滚动夏普比率
多头交易略微比空头交易更有利润,空头交易平均损失:
摘要统计 | 所有交易 | 空头交易 | 多头交易 |
---|---|---|---|
总往返数 | 22,352 | 11,631 | 10,721 |
盈利百分比 | 50.0% | 48.0% | 51.0% |
获胜往返 | 11,131 | 5,616 | 5,515 |
输掉的往返 | 11,023 | 5,935 | 5,088 |
即使往返 | 198 | 80 | 118 |
学到的教训和下一步计划
总的来说,我们可以看到,尽管只使用高度流动的市场数据,梯度提升模型仍然能够提供比随机猜测显着更好的预测。显然,利润远非可以保证,尤其是因为我们对交易成本做出了非常慷慨的假设(注意高周转率)。
然而,有几种方法可以改进这个基本框架,即通过从更一般和战略性的参数变化到更具体和战术性的方面,例如:
-
尝试不同的投资范围(例如,更少的流动股票或其他资产)。
-
在添加互补数据源方面要有创意。
-
设计更复杂的特征工程。
-
使用更长或更短的持有和回望期等不同实验设置。
-
提出更有趣的交易规则,并使用多个而不是一个单一的 ML 信号。
希望这些建议能激发您在我们提出的模板上建立并提出有效的 ML 驱动交易策略!
用于日内策略的提升
我们在第一章,从想法到执行的交易机器学习中介绍了高频交易(HFT)作为加速算法策略采用的关键趋势。没有一个客观的定义能够准确定义 HFT 所涵盖的活动的特性,包括持有期、订单类型(例如,被动与主动),以及策略(动量或回归、方向性或提供流动性等)。然而,大多数更技术性的 HFT 处理似乎都同意,驱动 HFT 活动的数据往往是最精细的可用数据。通常,这将是直接来自交易所的微观结构数据,例如我们在第二章,市场和基本数据-来源和技术中介绍的 NASDAQ ITCH 数据,以演示它如何详细描述每笔下单、每笔成交和每笔取消,从而允许至少对于股票而言重建完整的限价订单簿,除了某些隐藏订单。
将 ML 应用于 HFT 包括优化交易执行,无论是在官方交易所还是在黑池中。ML 还可以用于生成交易信号,正如我们将在本节中展示的那样;另请参见 Kearns 和 Nevmyvaka(2013)以获取有关 ML 如何在 HFT 环境中增加价值的其他详细信息和示例。
本节使用来自证券信息处理器生产的一致性数据源的AlgoSeek 纳斯达克 100 数据集。该数据包括最佳买卖盘报价和分钟级别的交易价格信息。还包含一些有关价格动态的特征,例如买卖价的交易数量,或者在价格级别上下正负价格波动之后的交易数量(有关更多背景信息以及在 GitHub 存储库中的数据目录中的下载和预处理说明,请参阅第二章,市场和基本数据-来源和技术)。
我们将首先描述如何为此数据集设计特征,然后训练一个梯度提升模型来预测下一分钟的成交量加权平均价格,然后评估生成的交易信号的质量。
针对高频数据的工程特征
AlgoSeek 慷慨地为本书提供了一份数据集,其中包含了 2013-2017 年间任意给定日子、以分钟为频率的 100 只股票的 50 多个变量。数据还涵盖了盘前和盘后交易,但我们将此示例限制在正式交易时间内,即上午 9:30 到下午 4:00 的 390 分钟,以限制数据规模,并避免处理不规则交易活动期间的问题。请参阅笔记本intraday_features
,其中包含本节中的代码示例。
我们将选择 12 个变量,其中包含超过 5100 万次观察结果作为创建 ML 模型特征的原材料。这将旨在预测 1 分钟后的成交量加权平均价格:
MultiIndex: 51242505 entries, ('AAL', Timestamp('2014-12-22 09:30:00')) to ('YHOO', Timestamp('2017-06-16 16:00:00'))
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 first 51242500 non-null float64
1 high 51242500 non-null float64
2 low 51242500 non-null float64
3 last 51242500 non-null float64
4 price 49242369 non-null float64
5 volume 51242505 non-null int64
6 up 51242505 non-null int64
7 down 51242505 non-null int64
8 rup 51242505 non-null int64
9 rdown 51242505 non-null int64
10 atask 51242505 non-null int64
11 atbid 51242505 non-null int64
dtypes: float64(5), int64(7)
memory usage: 6.1+ GB
由于数据的内存占用量较大,我们只创建了 20 个简单的特征,即:
-
过去 10 分钟的滞后收益。
-
在一根 K 线中,有上涨和下跌的交易数量,除以总交易数量。
-
在一根 K 线中,交易价格相同(重复)的交易数量,除以总交易数量,其间有上涨或下跌。
-
在一根 K 线中,以卖出价和买入价进行交易的股票数量之差,除以该 K 线的总成交量。
-
包括力量平衡、商品通道指数和随机相对强度指数等多个技术指标(有关详情,请参阅附录,Alpha 因子库)。
我们会确保移动数据以避免前瞻偏差,正如示范的货币流指数的计算所示,该指数使用了 TA-Lib 实现:
data['MFI'] = (by_ticker
.apply(lambda x: talib.MFI(x.high,
x.low,
x['last'],
x.volume,
timeperiod=14)
.shift()))
下图展示了对各个特征的单独预测内容进行独立评估,评估使用它们与 1 分钟后收益的等级相关性。它显示最近的滞后收益可能是最具信息量的变量:
图 12.20:高频特征的信息系数
我们现在可以开始使用这些特征训练梯度提升模型。
使用 LightGBM 的分钟频率信号
为了为我们的 HFT 策略生成预测信号,我们将训练一个 LightGBM 提升模型来预测 1 分钟前向回报。模型在训练期间接收 12 个月的分钟数据,并为随后的 21 个交易日生成样本外预测。我们将重复这个过程 24 次训练-测试拆分,以涵盖我们 5 年样本的最后 2 年。
训练过程与前述的 LightGBM 示例密切相关;有关实现细节,请参阅笔记本 intraday_model
。
一个关键区别是将自定义 MultipleTimeSeriesCV
调整到分钟频率;我们将引用 MultiIndex
的 date_time
级别(有关实现,请参阅笔记本)。我们根据每个股票和每天的 390 个观测值计算训练和测试期间的长度如下:
DAY = 390 # minutes; 6.5 hrs (9:30 - 15:59)
MONTH = 21 # trading days
n_splits = 24
cv = MultipleTimeSeriesCV(n_splits=n_splits,
lookahead=1,
test_period_length=MONTH * DAY,
train_period_length=12 * MONTH * DAY,
date_idx='date_time')
大数据规模显著推高了训练时间,所以我们使用默认设置,但将每个集成的树数量设置为 250. 我们使用以下 ic_lgbm()
自定义指标定义跟踪测试集上的 IC,我们将其传递给模型的 .train()
方法。
自定义指标接收模型预测和二元训练数据集,我们可以使用它来计算任何感兴趣的指标;注意我们将 is_higher_better
设置为 True
,因为模型默认通过最小化损失函数来进行优化(有关更多信息,请参阅 LightGBM 文档):
def ic_lgbm(preds, train_data):
"""Custom IC eval metric for lightgbm"""
is_higher_better = True
return 'ic', spearmanr(preds, train_data.get_label())[0], is_higher_better
model = lgb.train(params=params,
train_set=lgb_train,
valid_sets=[lgb_train, lgb_test],
feval=ic_lgbm,
num_boost_round=num_boost_round,
early_stopping_rounds=50,
verbose_eval=50)
在 250 次迭代中,大多数折叠的验证 IC 仍在改善,因此我们的结果并不理想,但是这种方式的训练已经花费了数小时。现在让我们来看一下我们模型生成的信号的预测内容。
评估交易信号的质量
现在,我们想知道模型的样本外预测有多准确,以及它们是否可以成为盈利交易策略的基础。
首先,我们计算 IC,既对所有预测,也在每日基础上,如下所示:
ic = spearmanr(cv_preds.y_test, cv_preds.y_pred)[0]
by_day = cv_preds.groupby(cv_preds.index.get_level_values('date_time').date)
ic_by_day = by_day.apply(lambda x: spearmanr(x.y_test, x.y_pred)[0])
daily_ic_mean = ic_by_day.mean()
daily_ic_median = ic_by_day.median()
对于连续 2 年的样本外测试,我们得到了一个统计上显著的正值为 1.90. 每日的均值 IC 为 1.98,中位数 IC 等于 1.91。
这些结果清楚地表明,预测包含了我们可以用于交易策略的短期价格运动方向和大小的有意义信息。
接下来,我们计算每个预测的十分位的平均和累积前向回报:
dates = cv_preds.index.get_level_values('date_time').date
cv_preds['decile'] = (cv_preds.groupby(dates, group_keys=False)
min_ret_by_decile = cv_preds.groupby(['date_time', 'decile']).y_test.mean()
.apply(lambda x: pd.qcut(x.y_pred, q=10))))
cumulative_ret_by_decile = (min_ret_by_decile
.unstack('decile')
.add(1)
.cumprod()
.sub(1))
图 12.21 展示了结果。左侧面板显示了每个十分位的平均 1 分钟回报,并显示每分钟 0.5 个基点的平均差异。右侧面板显示了等权重组合投资于每个十分位的累积回报,表明在交易成本之前,一个多空策略似乎是有吸引力的:
图 12.21:每个十分位的平均 1 分钟回报和累积回报
使用分钟级数据进行回测非常耗时,因此我们省略了这一步;但是,可以随意尝试使用 Zipline 或 backtrader 在更现实的交易成本假设下评估这个策略,或者使用适当的风险控制。
总结
在本章中,我们探讨了梯度提升算法,它用于以顺序方式构建集成模型,逐步添加浅层决策树来改善已做出的预测,这些决策树仅使用极少量的特征。我们看到了梯度提升树可以非常灵活地应用于广泛的损失函数,以及提供了许多机会来调整模型以适应给定数据集和学习任务。
最近的实现大大地促进了梯度提升的使用。他们通过加速训练过程并提供更一致和详细的洞察力,以了解特征的重要性和个别预测的驱动因素。
最后,我们开发了一个简单的交易策略,由一组梯度提升模型驱动,至少在交易成本显著之前是有盈利的。我们还看到了如何使用梯度提升处理高频数据。
在下一章中,我们将转向贝叶斯方法来进行机器学习。
第十三章:使用无监督学习的数据驱动风险因子和资产配置。
第六章,机器学习过程,介绍了无监督学习通过发现数据结构而增加价值,而不需要结果变量来指导搜索过程。这与前几章的监督学习形成了对比:无监督学习不是预测未来结果,而是旨在学习数据的信息表示,帮助探索新数据、发现有用的见解或更有效地解决其他任务。
降维和聚类是无监督学习的主要任务:
-
降维将现有特征转换为新的、较小的集合,同时尽量减小信息损失。算法在如何衡量信息损失、是否应用线性或非线性转换以及对新特征集施加哪些约束方面存在差异。
-
聚类算法识别并对相似的观察结果或特征进行分组,而不是识别新特征。算法在如何定义观察结果的相似性以及对结果组的假设方面存在差异。
当一个数据集不包含结果时,这些无监督算法非常有用。例如,我们可能希望从大量财务报告或新闻文章中提取可交易的信息。在第十四章,用于交易的文本数据 - 情感分析中,我们将使用主题建模来发现隐藏的主题,以更有效地探索和总结内容,并且识别有助于我们提取信号的有意义的关系。
当我们希望独立于结果地提取信息时,这些算法也非常有用。例如,与使用第三方行业分类不同,聚类允许我们根据资产的属性(例如在一定时间范围内的回报、风险因子的暴露或类似的基本面)识别出对我们有用的合成分组。在本章中,我们将学习如何使用聚类来通过识别资产回报之间的分层关系来管理投资组合风险。
更具体地说,在阅读本章后,您将了解:
-
如何通过主成分分析(PCA)和独立成分分析(ICA)进行线性降维。
-
使用 PCA 从资产回报中识别基于数据的风险因子和特征组合。
-
使用流形学习有效地可视化非线性、高维数据。
-
使用 T-SNE 和 UMAP 来探索高维图像数据。
-
k-means、层次和基于密度的聚类算法的工作原理。
-
使用凝聚式聚类构建具有分层风险平衡的强大投资组合。
您可以在 GitHub 存储库的相应目录中找到本章的代码示例和附加资源链接。笔记本包括图像的彩色版本。
降维
用线性代数的术语来说,数据集的特征创建了一个向量空间,其维度对应于线性独立行或列的数量,取两者中较大的一个。 当两列完全相关时,它们是线性相关的,以至于一个可以使用加法和乘法的线性运算从另一个计算出来。
换句话说,它们是代表相同方向而不是不同方向的平行向量,在数据中只构成一个维度。 同样,如果一个变量是几个其他变量的线性组合,那么它是由这些列创建的向量空间的一个元素,并且不会添加自己的新维度。
数据集的维数数量很重要,因为每个新维度都可能添加有关结果的信号。 但是,还存在一个被称为维数灾难的负面效应:随着独立特征数量的增加,而观察数量保持不变,数据点之间的平均距离也增加,并且特征空间的密度呈指数级下降,这对机器学习(ML)有重大影响。 当观察值之间距离更远时,即彼此不同,预测变得更加困难。 替代数据源,如文本和图像,通常具有很高的维度,但它们通常影响依赖大量特征的模型。 下一节将解决由此产生的挑战。
降维旨在通过使用更少的特征更有效地表示数据。 为此,算法将数据投影到低维空间,同时丢弃任何不具信息量的变化,或者通过识别数据所在位置附近的低维子空间或流形。
流形是一个在局部类似于欧几里得空间的空间。 一维流形包括线或圆,但不包括数字八的可视表示,因为没有交叉点。
流形假设认为高维数据通常驻留在较低维空间中,如果识别出,就可以在此子空间中忠实地表示数据。 有关背景信息和测试此假设的算法描述,请参阅 Fefferman,Mitter 和 Narayanan(2016)。
因此,降维通过找到一个不同的、更小的变量集合来捕捉原始特征中最重要的内容,以最小化信息损失。 压缩有助于对抗维数灾难,节省内存,并允许可视化原本非常难以探索的高维数据的显着方面。
降维算法的不同之处在于它们对新变量施加的约束以及它们如何最小化信息损失(参见 Burges 2010 提供的出色概述):
-
像 PCA 和 ICA 这样的线性算法将新变量限制为原始特征的线性组合;例如,低维空间中的超平面。而 PCA 要求新特征无相关性,ICA 进一步强调统计独立性,意味着没有线性和非线性关系。
-
非线性算法不受超平面限制,可以捕捉数据中更复杂的结构。然而,鉴于无限的选项,算法仍然需要做出假设才能得出解决方案。本节稍后,我们将解释t-分布随机邻域嵌入(t-SNE)和均匀流形近似和投影(UMAP)如何用于可视化更高维度的数据。图 13.1说明了流形学习如何在三维特征空间中识别二维子空间。(笔记本
manifold_learning
说明了使用其他算法,包括局部线性嵌入。)图 13.1:非线性降维
维度诅咒
数据集维度的增加意味着在相应的欧几里得空间中,代表每个观察值的特征向量中有更多的条目。
我们使用欧几里得距离(也称为 L²范数)在向量空间中测量距离,我们将其应用于线性回归系数向量以训练正则化的岭回归。
两个具有笛卡尔坐标p = (p[1], p[2], …, p[n])和q = (q[1], q[2], …, q[n])的n维向量之间的欧几里得距离是使用毕达哥拉斯开发的熟悉公式计算的:
因此,每个新维度都会向总和中添加非负项,使得距离随着不同向量的维数增加而增加。换句话说,随着特征数量对于给定观察数的增长,特征空间变得越来越稀疏,即变得更少或更空。另一方面,较低的数据密度需要更多的观察来保持数据点之间的平均距离不变。
图 13.2说明了随着维度数量增加,为保持观察之间的平均距离所需的数据点数量呈指数增长。在线上均匀分布的 10 个点对应于二维中的 10²个点和三维中的 10³个点,以保持密度不变。
图 13.2:为保持平均距离恒定所需的特征数量随维度数量的指数增长
本节的 GitHub 存储库文件夹中的笔记本the_curse_of_dimensionality
模拟了随着维度数量增长,数据点之间的平均距离和最小距离如何增加(见图 13.3)。
图 13.3:单位超立方体中 1,000 个数据点的平均距离
模拟随机从不相关均匀或相关正态分布中的[0, 1]范围内抽取高达 2,500 个特征。对于正态分布,数据点之间的平均距离增加到超过单位特征范围的 11 倍,对于(极端)不相关均匀分布,增加到超过 20 倍。
当观察之间的距离增加时,监督式机器学习变得更加困难,因为对新样本的预测不太可能基于从类似训练特征中学习。简而言之,随着特征数量的增加,可能的唯一行数呈指数增长,使得有效地对空间进行抽样变得更加困难。同样,通过对实际关系做出较少假设的灵活算法学习的函数的复杂度随维度数量的增加呈指数增长。
灵活的算法包括我们在第十一章看到的基于树的模型,随机森林-一种日本股票的多空策略,以及第十二章,提升您的交易策略。它们还包括本书后面将介绍的深度神经网络,从第十六章开始,用于盈利电话和 SEC 提交的词嵌入。随着更多维度增加了过拟合的机会,这些算法的方差增加,导致泛化性能不佳。
降维利用了实践中特征通常相关或变化很小的事实。如果是这样,它可以在不损失信号太多的情况下压缩数据,并补充使用正则化来管理由方差和模型复杂性导致的预测误差。
随后我们将探讨的关键问题是:找到数据的低维表示的最佳方法是什么?
线性降维
线性降维算法计算线性组合,转换,旋转和重新缩放原始特征,以捕获数据中的显着变化,同时受制于对新特征特性的约束。
PCA 由 Karl Pearson 于 1901 年发明,它找到反映数据中最大方差方向的新特征,同时彼此不相关。相比之下,ICA 起源于 20 世纪 80 年代的信号处理,其目标是在施加较强的统计独立性约束的同时分离不同的信号。
本节介绍了这两种算法,然后说明了如何将 PCA 应用于资产回报,以从数据中学习风险因素,并构建所谓的特征组合以进行系统交易策略。
主成分分析
PCA 找到现有特征的线性组合,并使用这些主成分来表示原始数据。组件的数量是一个超参数,它决定了目标维度,并且最多可以等于行数或列数中较小的那个。
PCA 的目标是捕获数据中大部分的方差,以便容易地恢复原始特征,并确保每个组件都添加信息。它通过将原始数据投影到主成分空间来降低维度。
PCA 算法通过识别一系列组件来工作,每个组件都与考虑了先前计算的组件捕捉的方差后数据中的最大方差的方向对齐。顺序优化确保新组件与现有组件不相关,并为向量空间生成正交基。
这个新基是原始基的旋转,使得新轴指向逐渐减小的方差的方向。由每个主成分解释的原始数据方差量的下降反映了原始特征之间相关性的程度。换句话说,捕获例如原始变异的 95%的组件的份额提供了有关原始数据中的线性独立信息的见解。
在二维中可视化 PCA
图 13.4说明了用于二维随机数据集的 PCA 的几个方面(参考笔记本 pca_key_ideas
):
-
左侧面板显示了第一和第二主成分如何与最大方差的方向对齐并且正交。
-
中央面板显示了第一主成分如何将重构误差最小化,其衡量方式为数据点与新轴之间的距离之和。
-
右侧面板说明了有监督 OLS(参考第七章,线性模型 - 从风险因素到回报预测),它通过从单个特征x[1]计算的线来近似结果(x[2])。垂直线突出显示 OLS 最小化沿结果轴的距离,而 PCA 最小化与超平面正交的距离。
图 13.4:来自各个角度的二维 PCA
PCA 的关键假设
PCA 做出了几个重要的假设,需要记住。其中包括:
-
高方差意味着高信噪比。
-
数据经过标准化处理,使得各个特征的方差可比较。
-
线性变换捕捉了数据的相关方面。
-
第一和第二阶的统计量之外的高阶统计量并不重要,这意味着数据具有正态分布。
对第一和第二时刻的强调与标准的风险/收益度量一致,但正态性假设可能与市场数据的特征相冲突。市场数据经常表现出与正态分布不同的偏斜或峰度(厚尾),PCA 将不考虑这些特征。
PCA 算法的工作原理
该算法找到向量来创建目标维度的超平面,以最小化重构误差,重构误差以数据点到平面的平方距离之和来衡量。如前所述,这个目标对应于找到一系列向量,这些向量与最大保留方差的方向相一致,给定其他分量,同时确保所有主成分互相正交。
在实践中,该算法通过计算协方差矩阵的特征向量或使用 奇异值分解(SVD)来解决问题。
我们使用一个随机生成的具有 100 个数据点的三维椭圆来说明计算,如 图 13.5 的左面板所示,包括由前两个主成分定义的二维超平面。(参见笔记本 the_math_behind_pca
,下面三个部分的代码示例。)
图 13.5: 从 3D 到 2D 的维度缩减的可视化表示
基于协方差矩阵的 PCA
我们首先使用方阵协方差矩阵计算主成分,其中特征 x[i]、x[j] 的成对样本协方差作为第 i 行和第 j 列的条目:
对于 n 维度的方阵 M,我们将特征向量 和特征值
[i],i=1, …, n 定义如下:
因此,我们可以使用特征向量和特征值来表示矩阵 M,其中 W 是一个包含特征向量作为列向量的矩阵,L 是一个包含特征值 [i] 作为对角线条目(其他情况下为 0)的矩阵。我们将 特征值分解 定义为:
使用 NumPy,我们实现如下,其中 pandas DataFrame 数据包含椭圆的 100 个数据点:
# compute covariance matrix:
cov = np.cov(data.T) # expects variables in rows by default
cov.shape
(3, 3)
接下来,我们计算协方差矩阵的特征向量和特征值。特征向量包含主成分(符号是任意的):
eigen_values, eigen_vectors = eig(cov)
eigen_vectors
array([[ 0.71409739, -0.66929454, -0.20520656],
[-0.70000234, -0.68597301, -0.1985894 ],
[ 0.00785136, -0.28545725, 0.95835928]])
我们可以将结果与从 sklearn 获得的结果进行比较,并发现它们在绝对意义上匹配:
pca = PCA()
pca.fit(data)
C = pca.components_.T # columns = principal components
C
array([[ 0.71409739, 0.66929454, 0.20520656],
[-0.70000234, 0.68597301, 0.1985894 ],
[ 0.00785136, 0.28545725, -0.95835928]])
np.allclose(np.abs(C), np.abs(eigen_vectors))
True
我们也可以 验证特征值分解,从包含特征值的对角矩阵 L 开始:
# eigenvalue matrix
ev = np.zeros((3, 3))
np.fill_diagonal(ev, eigen_values)
ev # diagonal matrix
array([[1.92923132, 0\. , 0\. ],
[0\. , 0.55811089, 0\. ],
[0\. , 0\. , 0.00581353]])
我们发现结果确实成立:
decomposition = eigen_vectors.dot(ev).dot(inv(eigen_vectors))
np.allclose(cov, decomposition)
使用奇异值分解的 PCA
接下来,我们将查看使用 SVD 进行的备用计算。当观测数量大于特征数量时(这是典型情况),此算法较慢,但在一些特征高度相关的情况下(这通常是使用 PCA 的原因)产生更好的数值稳定性。
SVD 将我们刚刚应用于方阵和对称协方差矩阵的特征分解推广到更一般的m x n矩形矩阵情况。它的形式如下图中心所示。的对角线值是奇异值,V的转置包含作为列向量的主成分。
图 13.6:SVD 分解
在这种情况下,我们需要确保我们的数据以零均值为中心(之前的协方差计算已经处理了这个):
n_features = data.shape[1]
data_ = data - data.mean(axis=0)
使用居中的数据,我们计算 SVD:
U, s, Vt = svd(data_)
U.shape, s.shape, Vt.shape
((100, 100), (3,), (3, 3))
我们可以将仅包含奇异值的向量s
转换为一个n x m矩阵,并展示分解的工作原理:
S = np.zeros_like(data_)
S[:n_features, :n_features] = np.diag(s)
S.shape
(100, 3)
我们发现分解确实复制了标准化数据:
np.allclose(data_, U.dot(S).dot(Vt))
True
最后,我们确认V的转置的列包含主成分:
np.allclose(np.abs(C), np.abs(Vt.T))
在下一节中,我们将演示 sklearn 如何实现 PCA。
使用 sklearn 进行 PCA
sklearn.decomposition.PCA
实现遵循基于fit()
和transform()
方法的标准 API,分别计算所需数量的主成分并将数据投影到组件空间。方便的fit_transform()
方法在一个步骤中完成此操作。
PCA 提供了三种不同的算法,可以使用svd_solver
参数指定:
-
full使用由 scipy 提供的 LAPACK 求解器计算精确的 SVD。
-
arpack运行适合计算不到完整数量组件的截断版本。
-
randomized使用基于抽样的算法,当数据集具有超过 500 个观测值和特征,并且目标是计算少于 80%的组件时,它更有效率。
-
auto也随机化到最有效的地方;否则,它使用完整的 SVD。
请在 GitHub 上查看算法实现细节的参考资料。
PCA 对象的其他关键配置参数是:
-
n_components:通过传递
None
(默认值)来计算所有主成分,或将数量限制为int
。对于svd_solver=full
,还有两个额外选项:[0, 1]区间内的float
计算保留数据方差相应份额所需的组件数量,选项mle
使用最大似然估计维度数量。 -
whiten:如果为
True
,则将组件向量标准化为单位方差,在某些情况下,这可能对预测模型有用(默认值为False
)。
要计算三维椭圆的前两个主成分并将数据投影到新空间中,请使用 fit_transform()
:
pca2 = PCA(n_components=2)
projected_data = pca2.fit_transform(data)
projected_data.shape
(100, 2)
前两个成分的解释方差非常接近 100%:
pca2.explained_variance_ratio_
array([0.77381099, 0.22385721])
图 13.5 显示了数据投影到新的二维空间中。
独立成分分析
ICA 是另一个线性算法,它确定一个新的基来表示原始数据,但追求的目标与 PCA 不同。有关详细介绍,请参阅 Hyvärinen 和 Oja(2000)。
ICA 出现在信号处理中,它旨在解决的问题被称为盲源分离。通常将其描述为鸡尾酒会问题,其中给定数量的客人同时发言,以至于单个麦克风记录重叠信号。ICA 假设存在与说话者数量相同的不同麦克风,每个麦克风放置在不同的位置,以便它们记录不同的信号混合。然后,ICA 旨在从这些不同的记录中恢复单个信号。
换句话说,有 n 个原始信号和一个未知的方阵混合矩阵 A,产生一个 n 维 m 观测值集合,使得
目标是找到矩阵 W = A^(-1),解开混合信号以恢复源。
唯一确定矩阵 W 的能力取决于数据的非高斯分布。否则,由于多变量正态分布在旋转下的对称性,W 可以任意旋转。此外,ICA 假设混合信号是其组成部分的和,因此无法识别高斯分量,因为它们的总和也是正态分布的。
ICA 假设
ICA 做出了以下关键假设:
-
信号的源是统计独立的
-
线性变换足以捕获相关信息
-
独立成分不具有正态分布
-
混合矩阵 A 是可以求逆的。
ICA 还要求数据被居中和白化,即彼此不相关且具有单位方差。使用前述概述的 PCA 对数据进行预处理可以实现所需的转换。
ICA 算法
FastICA
是 sklearn 中使用的一种固定点算法,它使用高阶统计量来恢复独立源。特别是,它将每个组件的距离最大化到正态分布,作为独立性的代理。
一种称为 InfoMax
的替代算法将组件之间的互信息最小化,作为统计独立性的度量。
用 sklearn 进行 ICA
sklearn 中的 ICA 实现使用与 PCA 相同的接口,因此几乎没有额外添加。请注意,没有解释方差的度量,因为 ICA 不会连续计算组件。相反,每个组件旨在捕获数据的独立方面。
流形学习 – 非线性降维
线性降维将原始数据投影到一个与数据中信息方向对齐的较低维度超平面上。专注于线性变换简化了计算,并回应了常见的金融度量,例如 PCA 旨在捕获最大方差。
然而,线性方法自然会忽略数据中非线性关系反映的信号。这样的关系在包含图像或文本数据的替代数据集中非常重要。在探索性分析期间检测到这种关系可以提供有关数据潜在信号内容的重要线索。
相比之下,流形假设强调高维数据通常位于或接近嵌入在高维空间中的较低维度非线性流形上。在本章开头显示的二维瑞士卷(图 13.1)阐明了这样的拓扑结构。流形学旨在找到固有维度的流形,然后在该子空间中表示数据。一个简化的例子使用道路作为三维空间中的一维流形,并使用房屋编号作为局部坐标来识别数据点。
几种技术可以近似一个较低维度的流形。其中一个例子是局部线性嵌入(LLE),由劳伦斯·索尔和萨姆·罗维斯(2000 年)发明,并用于“展开”在图 13.1中显示的瑞士卷(查看manifold_learning_lle
笔记本中的示例)。
对于每个数据点,LLE 识别给定数量的最近邻居,并计算代表每个点的线性组合的权重。它通过在较低维度流形上的全局内部坐标上线性投影每个邻域来找到一个较低维度的嵌入,并可以被看作是一系列 PCA 应用。
可视化要求降维至少三个维度,可能低于固有维度,并提出了忠实地表示局部和全局结构的挑战。这个挑战与维度诅咒有关;也就是说,虽然球体的体积随着维度数量的增加呈指数级增长,但用于表示高维数据的低维空间要有限得多。例如,在 12 个维度中,可能有 13 个等距点;然而,在二维空间中,只能有 3 个形成边长相等的三角形。因此,在较低维度准确反映一个点到其高维邻居的距离会有可能扭曲所有其他点之间的关系。结果就是拥挤问题:为了保持全局距离,局部点可能需要被放置得太接近。
接下来的两个部分涵盖了使我们在处理复杂数据集的可视化中取得进展的技术。我们将使用 Fashion MNIST 数据集,这是一个更复杂的选择,用于计算机视觉的经典手写数字 MNIST 基准数据。它包含 60,000 个训练图像和 10,000 个测试图像,分为 10 个类别(在笔记本 manifold_learning_intro
中查看样本图像)。该数据的流形学习算法的目标是检测类别是否位于不同的流形上,以促进它们的识别和区分。
t-分布随机近邻嵌入
t-SNE 是由 Laurens van der Maaten 和 Geoff Hinton 于 2008 年开发的获奖算法,用于检测高维数据中的模式。它采用概率、非线性的方法来定位数据在几个不同但相关的低维流形上。该算法强调将相似的点放在低维中放在一起,而不是像 PCA 这样的算法那样保持在高维中相距较远的点之间的距离最小化。
该算法通过 将高维距离转换为(条件)概率 来进行,其中高概率意味着低距离,并反映了基于相似性对两个点进行采样的可能性。首先,在每个点上定位一个正态分布,并计算点和每个邻居的密度,其中 perplexity
参数控制有效邻居的数量。在第二步中,它将点排列在低维中,并使用类似计算的低维概率来匹配高维分布。它通过 Kullback-Leibler 散度来衡量分布之间的差异,这会对低维中的相似点放置高惩罚。
低维概率使用一个自由度为 1 的学生 t 分布,因为它有更胖的尾部,减少了放置更远的高维点的惩罚,以管理拥挤问题。
图 13.7 的上半部分显示了 t-SNE 如何区分 FashionMNIST 图像类别。更高的困惑度值增加了用于计算局部结构的邻居数,并逐渐强调全局关系。 (参考存储库以获取此图的高分辨率彩色版本。)
图 13.7:Fashion MNIST 图像数据的 t-SNE 和 UMAP 可视化,针对不同的超参数
t-SNE 是目前高维数据可视化的最新技术。其缺点包括计算复杂度随着点数 n 呈二次增长,因为它评估所有成对距离,但随后基于树的实现将成本降低到 n log n。
不幸的是,t-SNE 不利于将新数据点投影到低维空间。压缩的输出对于基于距离或密度的聚类算法不是非常有用,因为 t-SNE 对待小距离和大距离的方式不同。
统一流形近似和投影
UMAP 是一种用于可视化和通用降维的较新算法。它假设数据在局部连接流形上均匀分布,并寻找最接近的低维等价物,使用模糊拓扑。它使用一个 neighbors
参数,其影响结果与前面一节中的 perplexity
类似。
它比 t-SNE 更快,因此更适用于大型数据集,并且有时比 t-SNE 更好地保留全局结构。它还可以使用不同的距离函数,包括用于测量单词计数向量之间距离的余弦相似度。
上图说明了 UMAP 确实将不同的聚类进一步分开,而 t-SNE 则提供了更精细的局部结构洞察。
笔记本还包含交互式 Plotly 可视化,用于探索每个算法的标签,并确定哪些对象彼此靠近。
用于交易的 PCA
PCA 在算法交易中有几个方面的用处,包括:
-
将 PCA 应用于资产收益以数据驱动地推导风险因素
-
基于资产收益相关系数矩阵的主成分构建不相关投资组合
我们将在本节中说明这两个应用。
数据驱动的风险因素
在第七章中,线性模型 - 从风险因素到收益预测,我们探讨了量化金融中用于捕捉收益主要驱动因素的风险因素模型。这些模型根据资产暴露于系统性风险因素的程度以及与这些因素相关的回报来解释资产收益的差异。特别是,我们探讨了法玛-法 rench 方法,该方法根据关于平均收益的经验行为的先验知识指定因子,将这些因子视为可观察因子,然后使用线性回归估计风险模型系数。
另一种方法将风险因素视为潜在变量,并使用因子分析技术如 PCA 同时从数据中学习因子并估计它们如何影响收益。在本节中,我们将演示这种方法如何以纯粹的统计或数据驱动方式推导因子,并具有不需要事先了解资产收益行为的优点(详见笔记本 pca_and_risk_factor_models
了解更多详情)。
准备数据 - 美国前 350 家股票
我们将使用 Quandl 股票价格数据,并选择市值最大的 500 支股票的每日调整收盘价和 2010 年至 2018 年期间的数据。然后,我们将计算每日收益如下:
idx = pd.IndexSlice
with pd.HDFStore('../../data/assets.h5') as store:
stocks = store['us_equities/stocks'].marketcap.nlargest(500)
returns = (store['quandl/wiki/prices']
.loc[idx['2010': '2018', stocks.index], 'adj_close']
.unstack('ticker')
.pct_change())
我们获得了 351 只股票和超过 2000 个交易日的回报:
returns.info()
DatetimeIndex: 2072 entries, 2010-01-04 to 2018-03-27
Columns: 351 entries, A to ZTS
PCA 对异常值敏感,因此我们分别在 2.5%和 97.5%的分位数上修剪数据:
PCA 不允许缺失数据,因此我们将删除任何在至少 95%的时间段内没有数据的股票。然后,在第二步中,我们将删除在剩余股票中至少 95%的交易日没有观察到的日子:
returns = returns.dropna(thresh=int(returns.shape[0] * .95), axis=1)
returns = returns.dropna(thresh=int(returns.shape[1] * .95))
我们留下了 315 个股票回报系列,覆盖了一个类似的时期:
returns.info()
DatetimeIndex: 2071 entries, 2010-01-05 to 2018-03-27
Columns: 315 entries, A to LYB
我们使用给定交易日的平均回报来填补任何剩余的缺失值:
daily_avg = returns.mean(1)
returns = returns.apply(lambda x: x.fillna(daily_avg))
运行 PCA 以确定主要的回报驱动因素
现在我们已经准备好使用默认参数将主成分模型拟合到资产收益上,使用全 SVD 算法来计算所有组件:
pca = PCA(n_components='mle')
pca.fit(returns)
我们发现最重要的因素解释了大约 55%的日回报变动。主导因素通常被解释为“市场”,而其余因素可以根据更密切的检查结果(请参阅下一个示例)被解释为行业或风格因素,与我们在第五章,投资组合优化和绩效评估和第七章,线性模型—从风险因子到回报预测中的讨论一致。
图 13.8右侧的图显示了累积解释方差,并指出大约有 10 个因子解释了这个股票横截面收益的 60%。
图 13.8:基于 PCA 的风险因子解释回报方差(累积)
笔记本包含了对更广泛的股票横截面和更长的 2000-2018 年时间段的模拟。结果发现,平均而言,前三个组件解释了 500 只随机选择的股票的 40%、10%和 5%,如图 13.9所示:
图 13.9:前 10 个主要组件的解释方差—100 次试验
累积图显示了一种典型的“肘部”模式,可以帮助确定一个合适的目标维度,即超过该维度的组件所增加的价值较少。
我们可以选择前两个主成分来验证它们确实是不相关的:
risk_factors = pd.DataFrame(pca.transform(returns)[:, :2],
columns=['Principal Component 1',
'Principal Component 2'],
index=returns.index)
(risk_factors['Principal Component 1']
.corr(risk_factors['Principal Component 2']))
7.773256996252084e-15
此外,我们可以绘制时间序列以突出每个因子捕捉不同波动性模式的情况,如下图所示:
图 13.10:第一个和第二个主成分捕获的回报波动模式
风险因子模型将采用主成分的子集作为特征来预测未来的回报,类似于我们在第七章,线性模型—从风险因子到回报预测中的方法。
特征组合
PCA 的另一个应用涉及标准化回报的协方差矩阵。相关矩阵的主成分按降序捕捉大部分资产之间的协变化,并且彼此不相关。此外,我们可以将标准化主成分用作投资组合权重。你可以在笔记本 pca_and_eigen_portfolios
中找到此部分的代码示例。
让我们使用 2010-2018 年间有数据的 30 家最大的股票来简化阐述:
idx = pd.IndexSlice
with pd.HDFStore('../../data/assets.h5') as store:
stocks = store['us_equities/stocks'].marketcap.nlargest(30)
returns = (store['quandl/wiki/prices']
.loc[idx['2010': '2018', stocks.index], 'adj_close']
.unstack('ticker')
.pct_change())
我们再次对回报进行截尾并进行标准化处理:
normed_returns = scale(returns
.clip(lower=returns.quantile(q=.025),
upper=returns.quantile(q=.975),
axis=1)
.apply(lambda x: x.sub(x.mean()).div(x.std())))
在像上一个示例中一样剔除资产和交易日后,我们剩下了 23 个资产和超过 2000 个交易日。我们计算回报协方差并估计所有主成分,发现前两个分别解释了 55.9% 和 15.5% 的协变化:
cov = returns.cov()
pca = PCA()
pca.fit(cov)
pd.Series(pca.explained_variance_ratio_).head()
0 55.91%
1 15.52%
2 5.36%
3 4.85%
4 3.32%
接下来,我们选择并标准化四个最大的成分,使它们总和为 1,然后我们可以将它们用作投资组合的权重,以便与由所有股票组成的等权投资组合进行比较:
top4 = pd.DataFrame(pca.components_[:4], columns=cov.columns)
eigen_portfolios = top4.div(top4.sum(1), axis=0)
eigen_portfolios.index = [f'Portfolio {i}' for i in range(1, 5)]
权重显示出明显的强调,如 图 13.11 所示。例如,投资组合 3 对样本中的两个支付处理器 Mastercard 和 Visa 有较大的权重,而投资组合 2 对技术公司有更多的暴露:
图 13.11: 主成分投资组合权重
当比较样本期内每个投资组合的表现与由我们的小样本组成的“市场”时,我们发现投资组合 1 的表现非常相似,而其他投资组合捕捉到不同的回报模式(见 图 13.12)。
图 13.12: 累积主成分投资组合回报
聚类
聚类和降维都对数据进行总结。正如我们刚刚讨论的,降维通过使用新的、更少的特征来表示数据,从而压缩数据,以捕捉最相关的信息。相比之下,聚类算法将现有观察结果分配给由相似数据点组成的子组。
聚类可以通过学习连续变量得到的类别视角更好地理解数据。它还允许您根据学习到的标准自动对新对象进行分类。相关应用的示例包括层次分类、医学诊断和客户分割。或者,可以使用聚类来表示群体作为原型,例如使用聚类的中点作为学习群体的最佳代表。一个示例应用是图像压缩。
聚类算法在识别分组的策略方面存在差异:
-
组合 算法选择不同观察结果的最一致的分组。
-
概率 建模估计最可能生成聚类的分布。
-
层次聚类 找到一系列嵌套的聚类,优化任何给定阶段的一致性。
算法还通过何为需要匹配数据特征、领域和应用目标的有用对象的概念而有所不同。 分组类型包括:
-
明确分离的各种形状的组
-
原型或基于中心的紧凑聚类
-
任意形状的基于密度的聚类
-
连通性或基于图的聚类
聚类算法的其他重要方面包括:
-
需要独占式聚类成员资格
-
进行硬的,即二进制的,或软的,概率的分配
-
是完整的,并将所有数据点分配到聚类中
以下各节介绍了关键算法,包括k-means、层次和基于密度的聚类,以及高斯混合模型(GMMs)。 笔记本 clustering_algos
比较了这些算法在不同的标记数据集上的性能,以突出它们的优缺点。 它使用互信息(参见第六章,机器学习过程)来衡量聚类分配与标签的一致性。
k-means 聚类
k-means 是最知名的聚类算法,最早由 1957 年贝尔实验室的 Stuart Lloyd 提出。 它找到 k 个质心,并将每个数据点分配到恰好一个聚类中,目标是最小化簇内方差(称为 惯性)。 它通常使用欧几里得距离,但也可以使用其他度量标准。 k-means 假设聚类是球形且大小相等,并忽略特征之间的协方差。
将观察分配到聚类
该问题在计算上是困难的(NP-hard),因为有 k^N 种方法将 N 个观测分成 k 个聚类。 标准的迭代算法对于给定的 k 提供了局部最优解,并按照以下步骤进行:
-
随机定义 k 个聚类中心并将点分配给最近的质心
-
重复:
-
对于每个聚类,将特征的平均值计算为质心
-
将每个观察分配给最近的质心
-
-
收敛:分配(或簇内变异)不发生变化
笔记本 kmeans_implementation
展示了如何使用 Python 编写该算法的代码。 它可视化了算法的迭代优化,并演示了结果质心如何将特征空间划分为称为 Voronoi 的区域,这些区域勾勒出了簇。 对于给定的初始化,结果是最优的,但是不同的起始位置将产生不同的结果。 因此,我们从不同的初始值计算多个聚类,并选择最小化簇内方差的解决方案。
k-means 需要连续或独热编码的分类变量。 距离度量通常对规模敏感,因此需要对特征进行标准化以确保它们具有相同的权重。
k-means 的优点包括其广泛的适用性,快速收敛,对大数据的线性可伸缩性以及生成大小均匀的聚类。缺点包括需要调整超参数k,不能保证找到全局最优解,限制性假设聚类为球形,特征不相关。它还对离群值敏感。
评估聚类质量
聚类质量度量有助于从多个聚类结果中选择。笔记本kmeans_evaluation
说明了以下选项。
k-means 目标函数建议我们比较惯性或聚类内方差的演变。最初,额外的质心会急剧降低惯性,因为新的聚类改善了整体拟合。一旦找到适当数量的聚类(假设存在),新的质心减少了聚类内方差,因为它们倾向于分割自然的分组。
因此,当 k-means 找到数据的良好聚类表示时,惯性往往会呈现类似于 PCA 的解释方差比的拐点形状(查看笔记本以获取实现细节)。
轮廓系数提供了聚类质量的更详细的图景。它回答了一个问题:最近聚类中的点相对于分配的聚类中的点有多远?为此,它比较了平均类内距离a与最近聚类的平均距离b,并计算了以下分数s:
分数可以在-1 和 1 之间变化,但在实践中不太可能出现负值,因为它们意味着大多数点被分配到错误的聚类中。轮廓分数的一个有用的可视化将每个数据点的值与全局平均值进行比较,因为它突显了每个聚类相对于全局配置的一致性。经验法则是要避免平均分数低于所有样本的平均值的聚类。
图 13.13显示了三个和四个聚类的轮廓图节选,前者突出了通过对全局轮廓分数的不足贡献来强调聚类 1 的拟合不佳,而所有四个聚类都具有一些值,这些值显示出高于平均分数的分数。
图 13.13:三个和四个聚类的轮廓图
总之,鉴于通常是无监督的性质,有必要改变聚类算法的超参数并评估不同的结果。还重要的是校准特征的比例,特别是当一些特征应该被赋予更高的权重并因此以较大的比例进行测量时。最后,为了验证结果的稳健性,使用数据子集来确定是否会一致出现特定的聚类模式。
分层聚类
分层聚类避免了需要指定目标聚类数的需要,因为它假设数据可以被逐步合并为越来越不相似的聚类。它不追求全局目标,而是逐步决定如何产生一系列从单个聚类到由个别数据点组成的聚类的嵌套聚类。
不同的策略和不相似度度量标准
有两种分层聚类方法:
-
聚合聚类 自下而上进行,基于相似性顺序合并剩余的两个组。
-
分裂聚类 自顶向下工作,顺序地分裂剩余的聚类,以产生最不同的子组。
两个组都产生N-1 个层次聚类,并有助于在最佳将数据分区为同质组的级别上选择聚类。我们将重点放在更常见的聚合聚类方法上。
聚合聚类算法不从个别数据点出发,而是计算一个包含所有相互距离的相似度矩阵。然后,它进行N-1 步,直到没有更多的不同聚类,并且每次都更新相似度矩阵以替换被新聚类合并的元素,使矩阵逐渐缩小。
虽然分层聚类没有像 k-means 那样的超参数,但是聚类之间(而不是个别数据点之间)的不相似度度量对聚类结果有重要影响。选项有以下不同:
-
单连接法:两个聚类的最近邻之间的距离
-
完全连接法:各自聚类成员之间的最大距离
-
瓦德法:最小化簇内方差
-
组平均法:使用聚类中点作为参考距离
可视化 - 树状图
分层聚类提供了关于观察值之间相似程度的见解,因为它继续合并数据。从一次合并到下一次合并的相似度度量的显着变化表明在此点之前存在自然的聚类。
树状图 将连续的合并可视化为一棵二叉树,将个别数据点显示为叶子,并将最终合并显示为树的根。它还显示了相似度如何从底部向顶部单调递减。因此,通过切割树状图来选择聚类是很自然的。有关实现详细信息,请参阅笔记本hierarchical_clustering
。
图 13.14 展示了经典的鸢尾花数据集的树状图,其中有四类和三个特征,使用了前面部分介绍的四种不同的距离度量标准。它评估了分层聚类的拟合程度,使用了科菲尼系数,该系数比较了点之间的成对距离和聚类相似度指标,该指标显示了成对合并发生的聚类相似度度量。系数为 1 意味着更接近的点总是较早合并。
图 13.14:不同不相似度度量的树状图和共辐相关性
不同的链接方法产生不同的树状图“外观”,因此我们无法使用此可视化来跨方法比较结果。此外,最小化簇内方差的 Ward 方法可能不能正确反映方差从一个级别到下一个级别的变化。相反,树状图可以反映不同级别的总簇内方差,这可能会产生误导。与整体目标一致的替代质量度量更为适当,例如共辐相关性或与总体目标一致的度量标准,如惯性。
分层聚类的优势包括:
-
该算法不需要特定数量的聚类,而是通过直观的可视化提供了关于潜在聚类的见解。
-
它生成一系列可用作分类法的聚类层次结构。
-
它可以与 k 均值结合以减少聚合过程开始时的项目数量。
另一方面,它的缺点包括:
-
由于大量相似性矩阵更新而产生的计算和内存成本高昂。
-
所有合并都是最终的,因此它无法达到全局最优。
-
维度诅咒导致对嘈杂的高维数据困难重重。
基于密度的聚类
基于密度的聚类算法根据与其他聚类成员的接近程度分配聚类成员资格。它们追求识别任意形状和大小的密集区域的目标。它们不需要指定一定数量的聚类,而是依赖于定义邻域大小和密度阈值的参数。
我们将概述两种流行的算法:DBSCAN 及其较新的分层精化。请参考笔记本density_based_clustering
中的相关代码示例以及本章的 GitHub 上的README
链接,以了解 Jonathan Larking 使用 DBSCAN 进行配对交易策略的 Quantopian 示例。
DBSCAN
基于密度的带噪声空间聚类(DBSCAN)于 1996 年开发,并因其在理论和实践中所受到的关注,在 2014 年的 KDD 会议上获得了 KDD 时代测试奖。
它旨在识别核心和非核心样本,其中前者扩展了一个聚类,而后者是聚类的一部分,但没有足够的附近邻居来进一步扩展聚类。其他样本是异常值,并且不分配给任何聚类。
它使用参数eps
来表示邻域的半径和min_samples
来表示需要的核心样本数量。它是确定性的和独占的,并且在具有不同密度和高维数据的情况下存在困难。调整参数以满足必要密度可能是具有挑战性的,特别是因为密度通常不是恒定的。
分层 DBSCAN
层次 DBSCAN(HDBSCAN)是更近期的发展,它假设聚类是潜在密度不同的岛屿,以克服刚提到的 DBSCAN 的挑战。它还旨在识别核心和非核心样本。它使用参数min_cluster_size
和min_samples
选择邻域并扩展聚类。该算法迭代多个eps
值并选择最稳定的聚类。除了识别密度不同的聚类外,它还提供了有关数据密度和层次结构的洞察。
图 13.15显示了 DBSCAN 和 HDBSCAN 分别如何能够识别形状与 k 均值发现的聚类显著不同的簇。聚类算法的选择取决于数据的结构;请参考本节早期提到的配对交易策略以获得一个实际示例。
图 13.15:比较 DBSCAN 和 HDBSCAN 聚类算法
高斯混合模型
GMM 是假设数据由各种多元正态分布的混合生成的生成模型。该算法旨在估计这些分布的均值和协方差矩阵。
GMM 泛化了 k 均值算法:它在特征之间添加了协方差,使得聚类可以是椭球而不是球体,而聚类的中心点由每个分布的均值表示。GMM 算法执行软分配,因为每个点都有成为任何聚类成员的概率。
笔记本gaussian_mixture_models
演示了实现并可视化结果聚类的过程。当 k 均值假设球形聚类过于约束时,你可能更喜欢 GMM 而不是其他聚类算法;鉴于其更大的灵活性,GMM 通常需要更少的聚类来产生良好的拟合效果。当你需要一个生成模型时,GMM 算法也更可取;因为 GMM 估计生成样本的概率分布,所以基于结果生成新样本很容易。
用于最优组合的层次聚类
在第五章,组合优化和绩效评估中,我们讨论了几种旨在选择给定资产集的组合权重以优化所得组合的风险和回报特性的方法。这些方法包括马科维茨的现代投资组合理论的均值-方差优化、凯利准则和风险平价。在本节中,我们介绍了更近期的创新(Prado 2016)——层次风险平价(HRP),它利用层次聚类根据子组的风险特征来分配资产仓位。
我们将首先介绍 HRP 的工作原理,然后使用我们在上一章中开发的梯度提升模型,通过长仓策略比较其性能。
层次风险平价的工作原理
层次风险平价的关键思想包括以下几点:
-
使用协方差矩阵的层次聚类将具有相似相关结构的资产分组在一起
-
通过仅在构建投资组合时将相似资产视为替代品来减少自由度
有关实施详细信息,请参阅子文件夹 hierarchical_risk_parity
中的笔记本和 Python 文件。
第一步是计算一个距离矩阵,代表相关资产的接近度并满足距离度量的要求。得到的矩阵成为 SciPy 层次聚类函数的输入,该函数使用先前在本章讨论过的几种可用方法之一计算连续的群集。
def get_distance_matrix(corr):
"""Compute distance matrix from correlation;
0 <= d[i,j] <= 1"""
return np.sqrt((1 - corr) / 2)
distance_matrix = get_distance_matrix(corr)
linkage_matrix = linkage(squareform(distance_matrix), 'single')
linkage_matrix
可用作 sns.clustermap
函数的输入,以可视化结果的层次聚类。由 seaborn 显示的树状图显示了个别资产和资产集合如何根据它们的相对距离合并(参见图 13.16 的左面板)。
clustergrid = sns.clustermap(distance_matrix,
method='single',
row_linkage=linkage_matrix,
col_linkage=linkage_matrix,
cmap=cmap, center=0)
sorted_idx = clustergrid.dendrogram_row.reordered_ind
sorted_tickers = corr.index[sorted_idx].tolist()
与原始相关矩阵的 seaborn.heatmap
相比,排序数据(右面板)中现在具有显着更多的结构,与中央面板中显示的原始相关矩阵相比。
图 13.16:原始和聚类相关矩阵
使用根据聚类算法诱导的层次结构排序的标记,HRP 现在开始计算一个自上而下的逆方差分配,根据树下进一步子集的方差连续调整权重。
def get_inverse_var_pf(cov):
"""Compute the inverse-variance portfolio"""
ivp = 1 / np.diag(cov)
return ivp / ivp.sum()
def get_cluster_var(cov, cluster_items):
"""Compute variance per cluster"""
cov_ = cov.loc[cluster_items, cluster_items] # matrix slice
w_ = get_inverse_var_pf(cov_)
return (w_ @ cov_ @ w_).item()
为此,该算法使用二分搜索将群集的方差分配给其元素,这些元素基于它们的相对风险性。
def get_hrp_allocation(cov, tickers):
"""Compute top-down HRP weights"""
weights = pd.Series(1, index=tickers)
clusters = [tickers] # initialize one cluster with all assets
while len(clusters) > 0:
# run bisectional search:
clusters = [c[start:stop] for c in clusters
for start, stop in ((0, int(len(c) / 2)),
(int(len(c) / 2), len(c)))
if len(c) > 1]
for i in range(0, len(clusters), 2): # parse in pairs
cluster0 = clusters[i]
cluster1 = clusters[i + 1]
cluster0_var = get_cluster_var(cov, cluster0)
cluster1_var = get_cluster_var(cov, cluster1)
weight_scaler = 1 - cluster0_var / (cluster0_var + cluster1_var)
weights[cluster0] *= weight_scaler
weights[cluster1] *= 1 - weight_scaler
return weights
结果组合配置产生的权重总和为 1,并反映在相关矩阵中存在的结构(详见笔记本)。
使用 ML 交易策略回测 HRP
现在我们知道 HRP 的工作原理,我们想测试它在实践中的表现如何与一些替代方案相比,即简单的等权重组合和均值-方差优化组合。您可以在笔记本 pf_optimization_with_hrp_zipline_benchmark
中找到此部分的代码示例以及其他详细信息和分析。
为此,我们将建立在上一章开发的梯度提升模型的基础上。我们将对 2015-2017 年的策略进行回测,使用最流动的 1000 只美国股票的宇宙。该策略依赖于模型预测,以买入次日预测收益最高的 25 只股票。我们每天重新平衡我们的持仓,以使我们的目标位置的权重与 HRP 建议的值匹配。
合并梯度提升模型的预测
我们首先对 2015-16 交叉验证期间表现最佳的 10 个模型的预测进行平均,如下面的代码摘录所示:
def load_predictions(bundle):
path = Path('../../12_gradient_boosting_machines/data')
predictions = (pd.read_hdf(path / 'predictions.h5', 'lgb/train/01')
.append(pd.read_hdf(path / 'predictions.h5', 'lgb/test/01').drop('y_test', axis=1)))
predictions = (predictions.loc[~predictions.index.duplicated()]
.iloc[:, :10]
.mean(1)
.sort_index()
.dropna()
.to_frame('prediction'))
我们每天都会获得模型预测并选择前 25 个股票代码。如果至少有 20 个股票有正面预测,我们会进入多头仓位并关闭所有其他持仓:
def before_trading_start(context, data):
"""
Called every day before market open.
"""
output = pipeline_output('signals')['longs'].astype(int)
context.longs = output[output!=0].index
if len(context.longs) < MIN_POSITIONS:
context.divest = set(context.portfolio.positions.keys())
else:
context.divest = context.portfolio.positions.keys() - context.longs
使用 PyPortfolioOpt 计算 HRP 权重
我们在第五章《投资组合优化与绩效评估》中使用的 PyPortfolioOpt 来计算均值-方差优化权重,也实现了 HRP。我们将在每天早上进行的定期再平衡的一部分中运行它。它需要目标资产的历史回报,并返回一个我们用于下订单的股票-权重对的字典:
def rebalance_hierarchical_risk_parity(context, data):
"""Execute orders according to schedule_function()"""
for symbol, open_orders in get_open_orders().items():
for open_order in open_orders:
cancel_order(open_order)
for asset in context.divest:
order_target(asset, target=0)
if len(context.longs) > context.min_positions:
returns = (data.history(context.longs, fields='price',
bar_count=252+1, # for 1 year of returns
frequency='1d')
.pct_change()
.dropna(how='all'))
hrp_weights = HRPOpt(returns=returns).hrp_portfolio()
for asset, target in hrp_weights.items():
order_target_percent(asset=asset, target=target)
Markowitz 再平衡遵循类似的过程,如第五章《投资组合优化与绩效评估》中所述,并包含在笔记本中。
与 pyfolio 的性能比较
以下图表显示了等权重(EW)、HRP和均值-方差(MV)优化投资组合的样本内和样本外(相对于 ML 模型选择过程)的累积收益。
图 13.17:不同投资组合的累积收益
累积收益分别为 MV 为 207.3%,EW 为 133%,HRP 为 75.1%。夏普比率分别为 1.16、1.01 和 0.83。Alpha 收益分别为 MV 为 0.28,EW 为 0.16,HRP 为 0.16,对应的贝塔值分别为 1.77、1.87 和 1.67。
因此,在这种特定情境下,常受批评的MV 方法效果最好,而HRP则排在最后。然而,请注意结果对交易股票数量、时间周期和其他因素非常敏感。
试着自己尝试一下,了解在最适合您的情况下哪种技术表现最好!
总结
在本章中,我们探讨了无监督学习方法,这些方法允许我们从数据中提取有价值的信号,而无需依赖标签提供的结果信息的帮助。
我们学习了如何使用线性降维方法如 PCA 和 ICA 来从数据中提取无关或独立的组件,这些组件可以作为风险因子或投资组合权重。我们还涵盖了先进的非线性流形学习技术,这些技术可以生成复杂、替代数据集的最新可视化效果。在章节的第二部分,我们涵盖了几种根据不同假设产生数据驱动的分组的聚类方法。这些分组可以用来构建将风险平价原则应用于已经按层次聚类的资产的投资组合,例如。
在接下来的三章中,我们将学习关于一种重要的替代数据来源的各种机器学习技术,即自然语言处理文本文档。
第十四章:用于交易的文本数据——情感分析
这是专门从文本数据中提取用于算法交易策略的信号的三章之一,使用自然语言处理(NLP)和机器学习(ML)。
文本数据在内容上非常丰富,但结构非常不规则,因此需要更多的预处理以使 ML 算法能够提取相关信息。一个关键挑战是将文本转换为数字格式而不丢失其含义。我们将介绍几种能够捕捉语言细微差别的技术,以便它们可以用作 ML 算法的输入。
在本章中,我们将介绍基本的特征提取技术,重点放在个别语义单元上,即单词或称为令牌的短组合。我们将展示如何将文档表示为令牌计数的向量,方法是创建一个文档-术语矩阵,然后继续将其用作新闻分类和情感分析的输入。我们还将介绍朴素贝叶斯算法,该算法在这方面很受欢迎。
在接下来的两章中,我们将基于这些技术,并使用主题建模和词向量嵌入等 ML 算法来捕获更广泛上下文中包含的信息。
特别是在本章中,我们将涵盖以下内容:
-
基本的 NLP 工作流程是什么样的
-
如何使用 spaCy 和 TextBlob 构建多语言特征提取管道
-
执行 NLP 任务,例如词性(POS)标记或命名实体识别
-
使用文档-术语矩阵将令牌转换为数字
-
使用朴素贝叶斯模型对文本进行分类
-
如何执行情感分析
您可以在 GitHub 存储库的相应目录中找到本章的代码示例和其他资源链接。笔记本包括图像的彩色版本。
使用文本数据的 ML——从语言到特征
鉴于人类使用自然语言进行沟通和存储的信息量之大,文本数据可能非常有价值。与金融投资相关的各种数据源涵盖了从正式文件(如公司声明,合同和专利)到新闻,观点,分析师研究或评论,再到各种类型的社交媒体帖子或消息。
在网上有大量丰富多样的文本数据样本可供探索 NLP 算法的使用,其中许多列在这一章的 GitHub 的README
文件中的资源中。有关全面介绍,请参见 Jurafsky 和 Martin(2008 年)。
为了实现文本数据的潜在价值,我们将介绍专门的 NLP 技术和最有效的 Python 库,概述特定于处理语言数据的关键挑战,介绍 NLP 工作流程的关键要素,并突出与算法交易相关的 NLP 应用。
处理文本数据的关键挑战
将非结构化文本转换为机器可读格式需要仔细的预处理,以保留数据的宝贵语义方面。人类如何理解语言的内容尚不完全清楚,改进机器理解语言的能力仍然是一个非常活跃的研究领域。
自然语言处理(NLP)特别具有挑战性,因为有效利用文本数据进行机器学习需要理解语言的内在工作原理以及它所指的世界的知识。 主要挑战包括以下内容:
-
由于多义性而产生的歧义,即一个词或短语根据上下文具有不同的含义(“Local High School Dropouts Cut in Half”)
-
在社交媒体上尤其是非标准和不断发展的语言使用
-
使用诸如“throw in the towel”这样的习语
-
像“Where is A Bug’s Life playing?”这样的棘手的实体名称
-
对世界的了解:“Mary and Sue are sisters”与“Mary and Sue are mothers”
自然语言处理工作流程
从文本数据中使用机器学习进行算法交易的一个关键目标是从文档中提取信号。 文档是来自相关文本数据源的单个样本,例如公司报告、标题、新闻文章或推文。 语料库,反过来,是文档的集合。
图 14.1 概述了将文档转换为可以用于训练具有可操作预测能力的监督机器学习算法的数据集的关键步骤:
图 14.1:自然语言处理工作流程
基本技术 提取文本特征作为被称为标记的孤立语义单元,并使用规则和字典对它们进行语言和语义信息的标注。 词袋模型使用令牌频率将文档建模为令牌向量,这导致经常用于文本分类、检索或摘要的文档-术语矩阵。
高级方法 依赖于机器学习来改进基本特征,例如令牌,并生成更丰富的文档模型。 这些包括反映跨文档使用令牌的联合的主题模型和旨在捕获令牌使用上下文的单词向量模型。
在下一节中,我们将详细审查工作流程每一步的关键决策以及相关的权衡,并在示例中使用 spaCy 库说明它们的实现。 以下表格总结了自然语言处理管道的关键任务:
特征 | 描述 |
---|---|
分词 | 将文本分割成单词、标点符号等。 |
词性标注 | 为令牌分配词类型,如动词或名词。 |
依存句法分析 | 标记句法令牌依赖关系,如主语<=>宾语。 |
词干提取和词形还原 | 分配单词的基本形式:“was” => “be”,“rats” => “rat”。 |
句子边界检测 | 找到并分割单个句子。 |
命名实体识别 | 标记“真实世界”对象,如人物、公司或地点。 |
相似性 | 评估单词、文本跨度和文档的相似性。 |
解析和标记文本数据 - 选择词汇表
标记是给定文档中字符序列的实例,并被认为是一个语义单位。词汇是被认为对进一步处理相关的语料库中包含的标记的集合;不在词汇中的标记将被忽略。
当然,目标是提取最能准确反映文档含义的标记。在这一步的关键折衷是选择更大的词汇量,以更好地反映文本来源,代价是增加更多的特征和更高的模型复杂性(在第十三章,使用无监督学习评估数据驱动的风险因素和资产配置中讨论为维度诅咒)。
在这方面的基本选择涉及标点和大写的处理,拼写校正的使用,以及是否排除非常频繁的所谓“停用词”(如“and”或“the”)作为无意义的噪音。
另外,我们需要决定是否将由n个单独的标记组成的n**-gram**作为语义单位(一个单独的标记也称为unigram)包含在内。一个二元组(或bigram)的例子是“纽约”,而“纽约市”是一个三元组(或trigram)。这个决定可以依赖于词典或者对个体和联合使用的相对频率进行比较。与 unigrams 相比,标记的唯一组合更多,因此添加n-grams 将增加特征数量,并且除非按频率进行过滤,否则会添加噪音。
语言学标注 - 标记之间的关系
语言学标注包括将句法和语法规则应用于识别句子边界,尽管标点符号模糊不清,以及词标注和依赖解析中的一个标记的角色和关系。它还允许识别词根的常见形式以进行词干提取和词形还原,以将相关单词分组在一起。
以下是与标注相关的一些关键概念:
-
词干提取使用简单的规则从标记中删除常见的结尾,比如s、ly、ing或ed,并将其减少到其词干或根形式。
-
词形还原使用更复杂的规则来推导单词的规范根(lemma)。它可以检测到不规则的常见根,比如“better”和“best”,并更有效地压缩词汇,但比词干提取慢。这两种方法都是以语义细微差别为代价来简化词汇。
-
POS标注有助于根据它们的功能消除标记的歧义(例如,当动词和名词具有相同的形式时),这会增加词汇量,但可能捕捉到有意义的区别。
-
依赖解析识别标记之间的分层关系,通常用于翻译。对于需要更高级语言理解的交互应用程序,比如聊天机器人,这一点至关重要。
语义注释 - 从实体到知识图
命名实体识别(NER)旨在识别表示感兴趣对象的标记,如人物、国家或公司。它可以进一步发展成捕捉这些实体之间语义和层次关系的知识图。这对于那些旨在预测新闻事件对情绪影响的应用至关重要。
标签化 - 为预测建模分配结果
许多自然语言处理应用程序通过从文本中提取的有意义信息来学习预测结果。监督学习需要标签来教会算法真实的输入输出关系。在文本数据中,建立这种关系可能具有挑战性,并且可能需要显式的数据建模和收集。
示例包括如何量化文本文档(例如电子邮件、转录的采访或推文)中隐含的情感,与新领域相关的,或者应该分配特定结果的研究文档或新闻报告的哪些方面。
应用
使用文本数据进行交易的机器学习依赖于提取有意义的信息以形成有助于预测未来价格走势的特征。应用范围从利用新闻的短期市场影响到对资产估值驱动因素的长期基本分析。例如:
-
评估产品评论情感以评估公司的竞争地位或行业趋势
-
检测信贷合同中的异常以预测违约的概率或影响
-
预测新闻影响的方向、幅度和受影响的实体
例如,摩根大通公司基于 25 万份分析师报告开发了一个预测模型,该模型的表现优于多个基准指数,并且相对于从共识 EPS 和推荐变化中形成的情绪因素产生了不相关的信号。
从文本到标记 - 自然语言处理流程
在本节中,我们将演示如何使用开源 Python 库 spaCy 构建一个 NLP 流程。textacy 库基于 spaCy 构建,并提供了易于访问的 spaCy 属性和额外功能。
请参阅笔记本nlp_pipeline_with_spaCy
以获取以下代码示例、安装说明和更多详细信息。
使用 spaCy 和 textacy 的自然语言处理流程
spaCy 是一个广泛使用的 Python 库,具有多语言快速文本处理的综合功能集。使用标记化和注释引擎需要安装语言模型。本章中我们将使用的功能仅需要小型模型;较大的模型还包括我们将在第十六章中介绍的词向量。
安装并链接库后,我们可以实例化一个 spaCy 语言模型,然后将其应用于文档。结果是一个Doc
对象,它对文本进行标记化和处理,根据默认的可配置流水线组件进行处理,这些组件通常包括标记器、解析器和命名实体识别器:
nlp = spacy.load('en')
nlp.pipe_names
['tagger', 'parser', 'ner']
让我们用一个简单的句子来说明流水线:
sample_text = 'Apple is looking at buying U.K. startup for $1 billion'
doc = nlp(sample_text)
解析、标记和注释一个句子
解析后的文档内容是可迭代的,每个元素都有由处理流程生成的许多属性。下一个示例演示了如何访问以下属性:
-
.text
: 原始词文本 -
.lemma_
: 词的词根 -
.pos_
: 基本词性标记 -
.tag_
: 详细的词性标记 -
.dep_
: 标记词间的句法关系或依赖性 -
.shape_
: 词的形状,以大写、标点和数字为准 -
.is alpha
: 检查标记是否为字母数字 -
.is stop
: 检查标记是否在给定语言的常用词列表中
我们迭代处理每个标记,并将其属性分配给一个pd.DataFrame
:
pd.DataFrame([[t.text, t.lemma_, t.pos_, t.tag_, t.dep_, t.shape_,
t.is_alpha, t.is_stop]
for t in doc],
columns=['text', 'lemma', 'pos', 'tag', 'dep', 'shape',
'is_alpha', is_stop'])
这产生了以下结果:
text | lemma | pos | tag | dep | shape | is_alpha | is_stop |
---|---|---|---|---|---|---|---|
Apple | apple | PROPN | NNP | nsubj | Xxxxx | TRUE | FALSE |
is | be | VERB | VBZ | aux | xx | TRUE | TRUE |
looking | look | VERB | VBG | ROOT | xxxx | TRUE | FALSE |
at | at | ADP | IN | prep | xx | TRUE | TRUE |
buying | buy | VERB | VBG | pcomp | xxxx | TRUE | FALSE |
U.K. | u.k. | PROPN | NNP | compound | X.X. | FALSE | FALSE |
startup | startup | NOUN | NN | dobj | xxxx | TRUE | FALSE |
for | for | ADP | IN | prep | xxx | TRUE | TRUE |
$ | $ | SYM | $ | quantmod | $ | FALSE | FALSE |
1 | 1 | NUM | CD | compound | d | FALSE | FALSE |
billion | billion | NUM | CD | pobj | xxxx | TRUE | FALSE |
我们可以使用以下方法在浏览器或笔记本中可视化句法依赖:
displacy.render(doc, style='dep', options=options, jupyter=True)
上述代码使我们能够获得如下的依赖树:
图 14.2:spaCy 依赖树
我们可以使用spacy.explain()
获取属性含义的额外见解,例如:
spacy.explain("VBZ")
verb, 3rd person singular present
批处理文档
现在我们将读取一个更大的数据集,包含 2,225 篇 BBC 新闻文章(详见 GitHub 获取数据源细节),这些文章分属五个类别,并存储在单独的文本文件中。我们执行以下操作:
-
调用
pathlib
模块的Path
对象的.glob()
方法。 -
迭代处理结果列表中的路径。
-
读取新闻文章中除了第一行标题之外的所有行。
-
将清理后的结果附加到列表中:
files = Path('..', 'data', 'bbc').glob('**/*.txt') bbc_articles = [] for i, file in enumerate(sorted(list(files))): with file.open(encoding='latin1') as f: lines = f.readlines() body = ' '.join([l.strip() for l in lines[1:]]).strip() bbc_articles.append(body) len(bbc_articles) 2225
句子边界检测
调用 NLP 对象对文章的第一句进行句子检测:
doc = nlp(bbc_articles[0])
type(doc)
spacy.tokens.doc.Doc
spaCy 根据句法分析树计算句子边界,因此标点符号和大写字母起着重要但不决定性的作用。因此,边界将与从句边界重合,即使是标点不良的文本也是如此。
我们可以使用 .sents
属性访问解析后的句子:
sentences = [s for s in doc.sents]
sentences[:3]
[Quarterly profits at US media giant TimeWarner jumped 76% to $1.13bn (£600m) for the three months to December, from $639m year-earlier. ,
The firm, which is now one of the biggest investors in Google, benefited from sales of high-speed internet connections and higher advert sales.,
TimeWarner said fourth quarter sales rose 2% to $11.1bn from $10.9bn.]
命名实体识别
spaCy 通过 .ent_type_
属性启用了命名实体识别:
for t in sentences[0]:
if t.ent_type_:
print('{} | {} | {}'.format(t.text, t.ent_type_, spacy.explain(t.ent_type_)))
Quarterly | DATE | Absolute or relative dates or periods
US | GPE | Countries, cities, states
TimeWarner | ORG | Companies, agencies, institutions, etc.
Textacy 让访问第一篇文章中出现的命名实体变得很容易:
entities = [e.text for e in entities(doc)]
pd.Series(entities).value_counts().head()
TimeWarner 7
AOL 5
fourth quarter 3
year-earlier 2
one 2
N-grams
N-grams 结合了 n 个连续的标记。这对于词袋模型可能是有用的,因为根据文本上下文,将(例如)“数据科学家” 视为单个标记可能比将 “数据” 和 “科学家” 两个不同的标记更有意义。
Textacy 使查看至少出现 min_freq
次的给定长度 n
的 ngrams
变得很容易:
pd.Series([n.text for n in ngrams(doc, n=2, min_freq=2)]).value_counts()
fourth quarter 3
quarter profits 2
Time Warner 2
company said 2
AOL Europe 2
spaCy 的流式 API
要通过处理管道传递更多的文档,我们可以使用 spaCy 的流式 API 如下所示:
iter_texts = (bbc_articles[i] for i in range(len(bbc_articles)))
for i, doc in enumerate(nlp.pipe(iter_texts, batch_size=50, n_threads=8)):
assert doc.is_parsed
多语言自然语言处理
spaCy 包含了针对英语、德语、西班牙语、葡萄牙语、法语、意大利语和荷兰语的经过训练的语言模型,以及一个用于命名实体识别的多语言模型。由于 API 不变,跨语言使用很简单。
我们将使用 TED 演讲字幕的平行语料库来说明西班牙语言模型(请参阅数据来源参考的 GitHub 存储库)。为此,我们实例化两个语言模型:
model = {}
for language in ['en', 'es']:
model[language] = spacy.load(language)
我们在每个模型中读取相应的小文本样本:
text = {}
path = Path('../data/TED')
for language in ['en', 'es']:
file_name = path / 'TED2013_sample.{}'.format(language)
text[language] = file_name.read_text()
句子边界检测使用相同的逻辑,但找到了不同的分解:
parsed, sentences = {}, {}
for language in ['en', 'es']:
parsed[language] = modellanguage
sentences[language] = list(parsed[language].sents)
print('Sentences:', language, len(sentences[language]))
Sentences: en 22
Sentences: es 22
词性标注也是以相同的方式工作:
pos = {}
for language in ['en', 'es']:
pos[language] = pd.DataFrame([[t.text, t.pos_, spacy.explain(t.pos_)]
for t in sentences[language][0]],
columns=['Token', 'POS Tag', 'Meaning'])
pd.concat([pos['en'], pos['es']], axis=1).head()
这产生了以下表格:
Token | POS 标记 | 意思 | Token | POS 标记 | 意思 |
---|---|---|---|---|---|
There | ADV | 副词 | Existe | VERB | 动词 |
s | VERB | 动词 | una | DET | 限定词 |
a | DET | 限定词 | estrecha | ADJ | 形容词 |
tight | ADJ | 形容词 | y | CONJ | 连词 |
and | CCONJ | 并列连词 | sorprendente | ADJ | 形容词 |
下一节将说明如何使用解析和注释的标记构建可以用于文本分类的文档-术语矩阵。
使用 TextBlob 的自然语言处理
TextBlob 是一个提供简单 API 用于常见自然语言处理任务的 Python 库,它构建在 自然语言工具包 (NLTK) 和 Pattern 网络挖掘库的基础上。TextBlob 提供了词性标注、名词短语提取、情感分析、分类和翻译等功能。
要说明 TextBlob 的用法,我们采样了一篇标题为 “Robinson ready for difficult task” 的 BBC Sport 文章。与 spaCy 和其他库类似,第一步是通过由 TextBlob
对象表示的管道将文档传递,以分配所需任务的注释(请参阅笔记本 nlp_with_textblob
):
from textblob import TextBlob
article = docs.sample(1).squeeze()
parsed_body = TextBlob(article.body)
词干提取
要执行词干提取,我们从 NTLK 库实例化 SnowballStemmer
,对每个标记调用其 .stem()
方法,并显示因此而被修改的标记:
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('english')
[(word, stemmer.stem(word)) for i, word in enumerate(parsed_body.words)
if word.lower() != stemmer.stem(parsed_body.words[i])]
('Manchester', 'manchest'),
('United', 'unit'),
('reduced', 'reduc'),
('points', 'point'),
('scrappy', 'scrappi')
情感极性和主观性
TextBlob 提供使用 Pattern 库提供的字典为解析的文档提供极性和主观度估计。这些字典将产品评论中频繁出现的形容词与情感极性分数进行词典映射,分数范围从 -1 到 +1(负面 ↔ 正面),以及类似的主观度分数(客观 ↔ 主观)。
.sentiment
属性为每个分数提供相关标记的平均值,而 .sentiment_assessments
属性则列出了每个标记的基础值(请参阅笔记本):
parsed_body.sentiment
Sentiment(polarity=0.088031914893617, subjectivity=0.46456433637284694)
计算标记 - 文档-词汇矩阵
在本节中,我们首先介绍词袋模型如何将文本数据转换为数值向量空间表示。目标是通过它们在该空间中的距离来近似文档的相似性。然后,我们继续说明如何使用 sklearn 库创建文档-词汇矩阵。
词袋模型
词袋模型基于文档包含的术语或标记的频率来表示文档。每个文档变成一个向量,其中每个标记在词汇表中对应一个条目,反映了该标记对文档的相关性。
创建文档-词汇矩阵
给定词汇表,文档-词汇矩阵很容易计算。然而,它也是一种粗糙的简化,因为它抽象了单词顺序和语法关系。尽管如此,它通常能够快速在文本分类中取得良好的结果,因此为非常有用的起点。
图 14.3 的左侧面板说明了这种文档模型如何将文本数据转换为一个矩阵,其中每行对应一个文档,每列对应词汇表中的一个标记。由此产生的矩阵通常是非常高维和稀疏的,即它包含许多零条目,因为大多数文档只包含整体词汇的一小部分。
![
图 14.3:文档-词汇矩阵和余弦相似性
有几种方法可以对一个标记的向量条目进行加权,以捕捉其与文档的相关性。我们将演示如何使用 sklearn 使用二进制标志表示存在或不存在、计数以及考虑到语料库中所有文档中术语频率差异的加权计数。
测量文档的相似性
将文档表示为词向量将给每个文档分配一个位置,该位置位于由词汇表创建的向量空间中。解释该空间中的向量条目为笛卡尔坐标,我们可以使用两个向量之间的角度来测量它们的相似性,因为指向相同方向的向量包含具有相同频率权重的相同术语。
上述图的右侧面板在二维中简化地说明了一个由向量 d[1] 表示的文档与由向量 q 表示的查询向量(可以是一组搜索词或另一个文档)之间距离的计算。
余弦相似度 等于两个向量之间角度的余弦值。它将角度大小转换为范围为 [0, 1] 的数字,因为所有向量条目都是非负的标记权重。值为 1 意味着两个文档在其标记权重方面完全相同,而值为 0 意味着两个文档仅包含不同的标记。
如图所示,角度的余弦等于向量的点积,即它们坐标的和积除以它们各自向量的欧几里德范数的乘积。
使用 scikit-learn 创建文档 - 词项矩阵
scikit-learn 预处理模块提供了两个工具来创建文档 - 词项矩阵。CountVectorizer
使用二进制或绝对计数来测量每个文档 d 和标记 t 的 词频 (TF) tf(d, t)。
TfidfVectorizer
相比之下,通过逆文档频率(IDF)加权(绝对)词频。因此,在更多文档中出现的词语将比在给定文档中具有相同频率但在所有文档中频率较低的标记接收到较低的权重。更具体地说,使用默认设置,文档 - 词项矩阵的 tf-idf(d, t) 条目被计算为 tf-idf(d, t) = tf(d, t) x idf(t) ,其中:
其中 n[d] 是文档数,df(d, t) 是词项 t 的文档频率。每个文档的结果 TF-IDF 向量都针对它们的绝对或平方总数进行了归一化(有关详细信息,请参阅 sklearn 文档)。TF-IDF 度量最初用于信息检索以对搜索引擎结果进行排名,并随后被证明对于文本分类和聚类非常有用。
这两个工具使用相同的接口,并在向量化文本之前对文档列表进行分词和进一步可选的预处理,通过生成标记计数来填充文档 - 词项矩阵。
影响词汇表大小的关键参数包括以下内容:
-
stop_words
: 使用内置或用户提供的(频繁)词语列表来排除词语 -
ngram_range
: 包括 n-gram,在由元组定义的 n 范围内的 n -
lowercase
: 相应地转换字符(默认值为True
) -
min_df
/max_df
: 忽略出现在较少/较多(int
)的文档中的词语,或者出现在较小/较大比例的文档中(如果是float
[0.0,1.0]) -
max_features
: 限制词汇表中的标记数量 -
binary
: 将非零计数设置为 1(True
)
查看笔记本 document_term_matrix
以获取以下代码示例和更多详细信息。我们再次使用 2,225 篇 BBC 新闻文章作为示例。
使用 CountVectorizer
笔记本中包含一个交互式可视化,探索 min_df
和 max_df
设置对词汇量大小的影响。我们将文章读入 DataFrame,设置 CountVectorizer
以生成二进制标志并使用所有词项,并调用其 .fit_transform()
方法以生成文档-词项矩阵:
binary_vectorizer = CountVectorizer(max_df=1.0,
min_df=1,
binary=True)
binary_dtm = binary_vectorizer.fit_transform(docs.body)
<2225x29275 sparse matrix of type '<class 'numpy.int64'>'
with 445870 stored elements in Compressed Sparse Row format>
输出是一个以行格式存储的 scipy.sparse
矩阵,有效地存储了 2,225(文档)行和 29,275(词项)列中的 445,870 个非零条目中的一小部分(<0.7%)。
可视化词汇分布
在图 14.4中的可视化显示,要求词项出现在至少 1% 且少于 50% 的文档中,将词汇限制在近 30000 个词项中的约 10% 左右。
这样留下了每个文档略多于 100 个唯一词项的模式,如下图左侧面板所示。右侧面板显示了剩余词项的文档频率直方图:
图 14.4:唯一词项和每个文档的词项数量的分布
查找最相似的文档
CountVectorizer
的结果使我们能够使用由 scipy.spatial.distance
模块提供的 pairwise 距离的 pdist()
函数找到最相似的文档。它返回一个压缩的距离矩阵,其条目对应于正方形矩阵的上三角形。我们使用 np.triu_indices()
将最小距离的索引转换为相应于最接近的词项向量的行和列索引:
m = binary_dtm.todense() # pdist does not accept sparse format
pairwise_distances = pdist(m, metric='cosine')
closest = np.argmin(pairwise_distances) # index that minimizes distance
rows, cols = np.triu_indices(n_docs) # get row-col indices
rows[closest], cols[closest]
(6, 245)
文章 6 和 245 在余弦相似性上最接近,因为它们共享 303 个词汇中的 38 个词项(见笔记本)。以下表格总结了这两篇文章,并展示了基于词数的相似度测量对于识别更深层语义相似性的有限能力:
文章 6 | 文章 245 | |
---|---|---|
主题 | 商业 | 商业 |
标题 | 美国就业增长仍然缓慢 | Ebbers 对 WorldCom 的欺诈案件有所了解 |
正文 | 美国在一月份创造的工作岗位少于预期,但求职者的减少使失业率降至三年来的最低水平。根据劳工部的数据,美国企业在一月份仅增加了 146,000 个工作岗位。 | 前世界通信公司总裁 Bernie Ebbers 在该公司的 110 亿美元金融欺诈案中直接参与其中,他最亲密的同事在美国法院告诉说。在 Ebbers 先生的刑事审判中作证,前财务主管 Scott Sullivan 暗示了他的同事在该公司的会计丑闻中的牵连。 |
CountVectorizer
和 TfidfVectorizer
都可以与 spaCy 一起使用,例如,执行词形还原并在词项化过程中排除某些字符:
nlp = spacy.load('en')
def tokenizer(doc):
return [w.lemma_ for w in nlp(doc)
if not w.is_punct | w.is_space]
vectorizer = CountVectorizer(tokenizer=tokenizer, binary=True)
doc_term_matrix = vectorizer.fit_transform(docs.body)
参见笔记本以获取更多细节和更多示例。
TfidfTransformer 和 TfidfVectorizer
TfidfTransformer
从文档-词项矩阵中计算 TF-IDF 权重,类似于 CountVectorizer
生成的矩阵。
TfidfVectorizer
在一个步骤中执行这两个计算。它添加了一些参数到 CountVectorizer
API,用于控制平滑行为。
对于一个小的文本样本,TFIDF 计算如下:
sample_docs = ['call you tomorrow',
'Call me a taxi',
'please call me... PLEASE!']
我们像以前一样计算术语频率:
vectorizer = CountVectorizer()
tf_dtm = vectorizer.fit_transform(sample_docs).todense()
tokens = vectorizer.get_feature_names()
term_frequency = pd.DataFrame(data=tf_dtm,
columns=tokens)
call me please taxi tomorrow you
0 1 0 0 0 1 1
1 1 1 0 1 0 0
2 1 1 2 0 0 0
文档频率是包含该标记的文档数:
vectorizer = CountVectorizer(binary=True)
df_dtm = vectorizer.fit_transform(sample_docs).todense().sum(axis=0)
document_frequency = pd.DataFrame(data=df_dtm,
columns=tokens)
call me please taxi tomorrow you
0 3 2 1 1 1 1
TF-IDF 权重是这些值的比率:
tfidf = pd.DataFrame(data=tf_dtm/df_dtm, columns=tokens)
call me please taxi tomorrow you
0 0.33 0.00 0.00 0.00 1.00 1.00
1 0.33 0.50 0.00 1.00 0.00 0.00
2 0.33 0.50 2.00 0.00 0.00 0.00
平滑的效果
为了避免零除法,TfidfVectorizer
使用平滑处理文档和术语频率:
-
smooth_idf
: 将文档频率加一,就像额外的文档包含词汇表中的每个标记一样,以防止零除法 -
sublinear_tf
: 应用亚线性 tf 缩放,即用 1 + log(tf) 替换 tf
与规范化权重相结合,结果略有不同:
vect = TfidfVectorizer(smooth_idf=True,
norm='l2', # squared weights sum to 1 by document
sublinear_tf=False, # if True, use 1+log(tf)
binary=False)
pd.DataFrame(vect.fit_transform(sample_docs).todense(),
columns=vect.get_feature_names())
call me please taxi tomorrow you
0 0.39 0.00 0.00 0.00 0.65 0.65
1 0.43 0.55 0.00 0.72 0.00 0.00
2 0.27 0.34 0.90 0.00 0.00 0.00
使用 TfidfVectorizer 摘要新闻文章
由于它们能够分配有意义的标记权重,TF-IDF 向量也用于总结文本数据。例如,Reddit 的 autotldr
函数基于类似的算法。请参阅笔记本以查看使用 BBC 文章的示例。
关键教训而不是已学到的教训
处理自然语言以在 ML 模型中使用的技术和选项数量庞大,这对应于这个高度非结构化数据源的复杂性质。构建良好的语言特征既具有挑战性又具有回报,可以说是揭示文本数据中隐藏的语义价值的最重要步骤。
在实践中,经验有助于选择消除噪声而不是信号的变换,但很可能仍然需要交叉验证和比较不同预处理选择组合的性能。
交易的自然语言处理
一旦文本数据使用前面讨论的自然语言处理技术转换为数值特征,文本分类就像任何其他分类任务一样。
在本节中,我们将这些预处理技术应用于新闻文章、产品评论和 Twitter 数据,并教授各种分类器以预测离散的新闻类别、评论分数和情感极性。
首先,我们将介绍朴素贝叶斯模型,这是一种与词袋模型产生的文本特征很好配合的概率分类算法。
本节的代码示例位于笔记本 news_text_classification
中。
朴素贝叶斯分类器
朴素贝叶斯算法在文本分类中非常流行,因为它的低计算成本和内存需求有助于在非常大的高维数据集上进行训练。其预测性能可以与更复杂的模型竞争,提供一个良好的基线,并以成功检测垃圾邮件而闻名。
该模型依赖于贝叶斯定理和各种特征相互独立的假设。换句话说,对于给定的结果,知道一个特征的值(例如,在文档中存在一个标记)不会提供有关另一个特征值的任何信息。
贝叶斯定理复习
贝叶斯定理表达了一个事件(例如,电子邮件是垃圾邮件而不是良性“ham”)在另一个事件(例如,电子邮件包含某些词)给定的条件概率如下:
实际上,电子邮件实际上是垃圾邮件的后验概率,而它包含某些词的事实取决于三个因素的相互作用:
-
电子邮件实际上是垃圾邮件的先验概率
-
在垃圾邮件中遇到这些词的似然
-
证据,即在电子邮件中看到这些词的概率
要计算后验,我们可以忽略证据,因为对所有结果(垃圾邮件与 ham)来说它是相同的,而无条件先验可能很容易计算。
但是,这种可能性给合理大小的词汇表和实际邮件语料库带来了不可逾越的挑战。原因在于在不同文档中联合出现或未出现的单词的组合爆炸,这阻止了计算概率表和为可能性赋值所需的评估。
条件独立假设
使模型既易于处理又赢得“朴素”名称的关键假设是特征在给定结果的条件下是独立的。为了说明,让我们对包含三个词“发送资金现在”的电子邮件进行分类,这样贝叶斯定理就变成了以下形式:
形式上,假设这三个单词在条件上是独立的意味着观察到“发送”不受其他术语的影响,前提是邮件是垃圾邮件,即,P(send | money, now, spam) = P(send | spam)。因此,我们可以简化似然函数:
使用“朴素”的条件独立假设,分子中的每个术语都很容易从训练数据的相对频率中计算出来。当需要比较而不是校准后验概率时,分母在类别之间是恒定的,可以忽略。随着因素数量,即特征数量的增加,先验概率变得不太相关。
总之,朴素贝叶斯模型的优点在于训练和预测速度快,因为参数的数量与特征的数量成比例,并且它们的估计具有封闭形式的解决方案(基于训练数据频率),而不是昂贵的迭代优化。它也是直观的并且有些可解释的,不需要超参数调整,并且在存在足够信号的情况下相对不太依赖于不相关的特征。
但是,当独立性假设不成立,并且文本分类依赖于特征的组合或特征相关时,模型的性能将较差。
对新闻文章进行分类
我们从用于新闻文章分类的朴素贝叶斯模型的示例开始,使用之前阅读的 BBC 文章,以获得包含来自五个类别的 2,225 篇文章的DataFrame
:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2225 entries, 0 to 2224
Data columns (total 3 columns):
topic 2225 non-null object
heading 2225 non-null object
body 2225 non-null object
为了训练和评估多项式朴素贝叶斯分类器,我们将数据分成默认的 75:25 训练测试集比例,确保测试集类别与训练集类别紧密匹配:
y = pd.factorize(docs.topic)[0] # create integer class values
X = docs.body
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1,
stratify=y)
我们继续从训练集中学习词汇,并使用默认设置的CountVectorizer
转换两个数据集,以获得近 26,000 个特征:
vectorizer = CountVectorizer()
X_train_dtm = vectorizer.fit_transform(X_train)
X_test_dtm = vectorizer.transform(X_test)
X_train_dtm.shape, X_test_dtm.shape
((1668, 25919), (557, 25919))
训练和预测遵循标准的 sklearn fit/predict 接口:
nb = MultinomialNB()
nb.fit(X_train_dtm, y_train)
y_pred_class = nb.predict(X_test_dtm)
我们使用accuracy
评估多类预测,以找到默认分类器几乎达到了 98%的准确率:
accuracy_score(y_test, y_pred_class)
0.97666068222621
用 Twitter 和 Yelp 数据进行情感分析
情感分析是 NLP 和 ML 在交易中最流行的用途之一,因为对资产或其他价格驱动因素的积极或消极看法可能会影响收益。
通常,情感分析的建模方法依赖于词典(如 TextBlob 库)或针对特定领域结果进行训练的模型。后者通常更可取,因为它允许更有针对性的标记,例如,通过将文本特征与后续价格变化而不是间接情感分数相关联。
我们将使用具有二进制极性标签的 Twitter 数据集和具有五点结果量表的大型 Yelp 商业评论数据集来说明情感分析的 ML。
用 Twitter 数据进行二进制情感分类
我们使用一个包含来自 2009 年的 1.6 百万训练推文和 350 个测试推文的数据集,该数据集具有算法分配的二进制积极和消极情感分数,这些分数分布相对均匀(有关更详细的数据探索,请参阅笔记本)。
多项式朴素贝叶斯
我们创建一个具有 934 个标记的文档-术语矩阵如下:
vectorizer = CountVectorizer(min_df=.001, max_df=.8, stop_words='english')
train_dtm = vectorizer.fit_transform(train.text)
<1566668x934 sparse matrix of type '<class 'numpy.int64'>'
with 6332930 stored elements in Compressed Sparse Row format>
然后,我们像以前一样训练MultinomialNB
分类器并预测测试集:
nb = MultinomialNB()
nb.fit(train_dtm, train.polarity)
predicted_polarity = nb.predict(test_dtm)
结果的准确率超过了 77.5%:
accuracy_score(test.polarity, predicted_polarity)
0.7768361581920904
与 TextBlob 情感分数的比较
我们还为推文获取 TextBlob 情感分数,并注意(参见图 14.5中的左面板),积极的测试推文收到了显着较高的情感估计。然后,我们使用MultinomialNB
模型的.predict_proba()
方法计算预测概率,并使用我们在第六章“机器学习过程”中介绍的相应曲线下面积AUC来比较两个模型(参见图 14.5中的右面板)。
图 14.5:定制与通用情感分数的准确性
在这种情况下,定制的朴素贝叶斯模型优于 TextBlob,测试 AUC 为 0.848,而 TextBlob 为 0.825。
Yelp 商业评论的多类情感分析
最后,我们将情感分析应用于规模大得多的 Yelp 企业评论数据集,其中有五个结果类别(有关代码和其他详细信息,请参见笔记本 sentiment_analysis_yelp
)。
数据包含了有关企业、用户、评论以及 Yelp 提供的其他方面的信息,以鼓励数据科学创新。
我们将使用在 2010-2018 年期间产生的约六百万条评论(详见笔记本)。以下图表显示了每年的评论数量和平均星级,以及所有评论中星级的分布。
图 14.6:Yelp 评论的基本探索性分析
我们将在截至 2017 年的数据的 10%样本上训练各种模型,并使用 2018 年的评论作为测试集。除了评论文本生成的文本特征外,我们还将使用有关给定用户的评论提交的其他信息。
结合文本和数值特征
数据集包含各种数值特征(有关实现细节,请参见笔记本)。
向量化器产生 scipy.sparse
矩阵。要将向量化的文本数据与其他特征结合起来,我们首先需要将其转换为稀疏矩阵;许多 sklearn 对象和其他库(如 LightGBM)可以处理这些非常节省内存的数据结构。将稀疏矩阵转换为稠密的 NumPy 数组会有内存溢出的风险。
大多数变量都是分类变量,因此由于我们有一个相当大的数据集来容纳特征的增加,我们使用一位有效编码。
我们将编码的数值特征转换并与文档-词矩阵相结合:
train_numeric = sparse.csr_matrix(train_dummies.astype(np.uint))
train_dtm_numeric = sparse.hstack((train_dtm, train_numeric))
基准准确率
使用最频繁的星级数(=5)来预测测试集,准确率接近 52%:
test['predicted'] = train.stars.mode().iloc[0]
accuracy_score(test.stars, test.predicted)
0.5196950594793454
多项式朴素贝叶斯模型
接下来,我们使用由 CountVectorizer
生成的文档-词矩阵来训练一个朴素贝叶斯分类器,其采用默认设置。
nb = MultinomialNB()
nb.fit(train_dtm,train.stars)
predicted_stars = nb.predict(test_dtm)
预测在测试集上的准确率达到了 64.7%,比基准提高了 24.4%:
accuracy_score(test.stars, predicted_stars)
0.6465164206691094
使用文本和其他特征的组合进行训练将测试准确率提高到 0.671。
逻辑回归
在第七章,线性模型-从风险因素到回报预测中,我们介绍了二元逻辑回归。 sklearn 还实现了一个多类别模型,具有多项式和一对所有训练选项,后者训练一个针对每个类别的二元模型,同时将所有其他类别视为负类。多项式选项比一对所有实现要快得多且更准确。
我们评估正则化参数 C
的一系列值,以确定表现最佳的模型,使用 lbfgs
求解器如下(有关详细信息,请参见 sklearn 文档):
def evaluate_model(model, X_train, X_test, name, store=False):
start = time()
model.fit(X_train, train.stars)
runtime[name] = time() – start
predictions[name] = model.predict(X_test)
accuracy[result] = accuracy_score(test.stars, predictions[result])
if store:
joblib.dump(model, f'results/{result}.joblib')
Cs = np.logspace(-5, 5, 11)
for C in Cs:
model = LogisticRegression(C=C, multi_class='multinomial', solver='lbfgs')
evaluate_model(model, train_dtm, test_dtm, result, store=True)
图 14.7 显示了验证结果的图表。
使用 LightGBM 的多类别梯度提升
为了进行比较,我们还训练了一个具有默认设置和multiclass
目标的 LightGBM 梯度提升树集成:
param = {'objective':'multiclass', 'num_class': 5}
booster = lgb.train(params=param,
train_set=lgb_train,
num_boost_round=500,
early_stopping_rounds=20,
valid_sets=[lgb_train, lgb_test])
预测性能
图 14.7显示了每个模型对于组合数据的准确性。右侧面板绘制了逻辑回归模型在两个数据集和不同正则化水平下的验证性能。
多项式逻辑回归的测试准确性略高于 74%。朴素贝叶斯的表现明显较差。默认的 LightGBM 设置并未提高线性模型的准确性为 0.736. 但是,我们可以调整梯度提升模型的超参数,并且很可能会看到性能提升,使其至少与逻辑回归持平。无论哪种方式,该结果都提醒我们不要低估简单的、正则化的模型,因为它们不仅可能会产生良好的结果,而且可能会迅速做到这一点。
图 14.7:组合数据的测试性能(所有模型,左)以及逻辑回归的不同正则化下的性能
摘要
在本章中,我们探讨了许多处理非结构化数据的技术和选项,目的是提取用于机器学习模型的语义上有意义的数值特征。
我们涵盖了基本的分词和注释流水线,并使用 spaCy 和 TextBlob 说明了它在多种语言中的实现。我们建立在这些结果上,构建了一个基于词袋模型的文档模型,将文档表示为数值向量。我们学习了如何优化预处理流水线,然后使用向量化的文本数据进行分类和情感分析。
我们还有两章关于替代文本数据。在下一章中,我们将学习如何使用无监督学习总结文本以识别潜在主题。然后,在第十六章中,收益电话和 SEC 备案的词嵌入,我们将学习如何将单词表示为反映单词用法上下文的向量,这是一种非常成功的技术,为各种分类任务提供了更丰富的文本特征。