1 Star 6 Fork 4

OpenDocCN / apachecn-ml-zh

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
11.md 68.40 KB
一键复制 编辑 原始数据 按行查看 历史
布客飞龙 提交于 2021-11-11 00:14 . 2021-11-11 00:14:59

十一、随机森林——日本股票的长短策略

在本章中,我们将学习如何使用两类新的机器学习模型进行交易:决策树随机森林。我们将看到决策树如何从编码输入和输出变量之间非线性关系的数据中学习规则。我们将演示如何训练决策树并将其用于回归和分类问题的预测,可视化和解释模型学习到的规则,以及调整模型的超参数以优化偏差-方差权衡并防止过度拟合。

决策树不仅是重要的独立模型,而且在其他模型中经常用作组件。在本章的第二部分中,我们将介绍集成模型,该模型将多个单独的模型组合在一起,以产生具有较低预测误差方差的单个聚合预测。

我们将举例说明引导聚合,通常称为装袋,作为随机构建单个模型和减少集合组件造成的预测误差相关性的几种方法之一。我们将说明装袋如何有效地减少差异,并学习如何配置、训练和调整随机林。我们将看到随机森林,作为一个(可能很大)数量的决策树的集合,如何能够显著减少预测误差,而牺牲一些解释上的损失。

然后,我们将继续建立一个长短交易策略,使用随机森林为过去 3 年的日本大盘股产生盈利信号。我们将收集和准备股票价格数据,调整随机森林模型的超参数,并根据模型信号对交易规则进行回溯测试。由此产生的多空策略使用机器学习,而不是我们在第 9 章波动预测和统计套利的时间序列模型中看到的协整关系,识别和交易一篮子证券,这些证券的价格在给定的投资期限内可能会朝相反的方向移动。

简而言之,阅读本章后,您将能够:

  • 使用决策树进行回归和分类
  • 从决策树中获得见解,并可视化从数据中学习到的规则
  • 理解为什么集成模型倾向于提供更好的结果
  • 使用引导聚合解决决策树的过度拟合难题
  • 训练、调整和解释随机森林
  • 采用随机森林设计和评估有利可图的交易策略

您可以在 GitHub 存储库的相应目录中找到本章的代码示例以及指向其他资源的链接。笔记本电脑包括图像的彩色版本。

决策树–从数据中学习规则

决策树是一种机器学习算法,它根据从数据中学习的决策规则预测目标变量的值。通过改变控制树如何学习规则的目标函数,该算法可以应用于回归和分类问题。

我们将讨论决策树如何使用规则进行预测,如何训练它们预测(连续)收益和(分类)价格变动方向,以及如何有效地解释、可视化和调整它们。更多详细信息和更多背景信息,请参见 Rokach 和 Maimon(2008)以及 Hastie、Tibshirani 和 Friedman(2009)。

树如何学习和应用决策规则

我们在第 7 章线性模型——从风险因子到收益预测第 9 章波动预测和统计套利时间序列模型中研究的线性模型,学习一组参数,以使用输入变量的线性组合预测结果,在逻辑回归的情况下,可能通过 S 形连接函数进行转换。

决策树采用不同的方法:它们学习并顺序应用一组规则,将数据点划分为子集,然后对每个子集进行一次预测。这些预测基于应用给定规则序列所产生的训练样本子集的结果值。分类树直接根据相对类别频率或多数类别的值预测概率,而回归树根据可用数据点的结果值平均值计算预测。

这些规则中的每一个都依赖于一个特定的功能,并使用阈值将样本分成两组,其值低于或高于此功能的阈值。一棵二叉树自然地代表了模型的逻辑:根是所有样本的起点,节点代表决策规则的应用,数据沿着边缘移动,因为它被分成更小的子集,直到它到达一个叶节点,模型在那里进行预测。

对于线性模型,参数值允许解释输入变量对输出和模型预测的影响。相反,对于决策树,从根到叶的各种可能路径决定了特征及其值如何导致模型做出特定决策。因此,决策树能够捕获线性模型无法捕获的“开箱即用”特征之间的相互依赖关系

下图突出显示了模型如何学习规则。在训练过程中,该算法扫描特征,并为每个特征寻找分割数据的截止点,以最大限度地减少预测造成的损失。它使用分割产生的子集,通过每个子集中的样本数进行加权:

图 11.1:决策树如何从数据中学习规则

为了在训练期间构建整个树,学习算法重复划分特征空间的过程,即,p输入变量、X1X2Xp 的可能值集,分为相互排斥且共同穷举的区域,每个区域由叶节点表示。不幸的是,鉴于特征和阈值序列的可能组合的爆炸性数量,该算法将无法评估特征空间的每个可能分区。基于树的学习采用了一种自上而下贪婪的方法,称为递归二进制分割,以克服这种计算限制。

此过程是递归的,因为它使用先前拆分产生的数据子集。它是自上而下的,因为它从树的根节点开始,在那里所有的观测值仍然属于一个区域,然后通过向预测空间添加一个拆分来连续创建树的两个新分支。它是贪婪的,因为该算法根据对目标函数的直接影响,以特征阈值组合的形式选择最佳规则,而不是向前看,并提前几步评估损失。我们将在回归树和分类树的更具体的上下文中返回分裂逻辑,因为这代表了它们之间的主要区别。

随着递归拆分向树中添加新节点,训练样本的数量将继续减少。如果规则将样本平均分割,从而形成一个完全平衡的树,每个节点的子节点数量相等,那么在n级别将有 2 个n节点,每个节点包含观察总数的相应部分。在实践中,这是不可能的,因此一些树枝上的样本数量可能会迅速减少,而树木往往会沿着不同的路径生长到不同的深度。

递归拆分将继续进行,直到每个叶节点仅包含一个样本,并且训练错误已减少到零。我们将介绍几种方法来限制拆分,并防止决策树产生极端过度拟合的自然趋势。

为了对新观测值进行预测,该模型使用其在训练期间推断的规则来决定应将数据点分配给哪个叶节点,然后使用特征空间相应区域中训练观测值的平均值(用于回归)或模式(用于分类)。在特征空间的给定区域中,即在给定叶节点中,较小数量的训练样本降低了预测的置信度,并且可能反映过拟合。

决策树在实践中的应用

在本节中,我们将说明如何使用基于树的模型来获得洞察力和做出预测。为了演示回归树,我们预测收益,对于分类案例,我们返回到正和负资产价格变动的示例。除非另有说明,本节的代码示例在笔记本decision_trees中。

数据–月度股票收益和特征

我们将选择 2006-2017 年期间 Quandl 美国股票数据集的一个子集,并遵循与第 4 章金融特征工程——如何研究阿尔法因子中的第一个特征工程示例类似的流程。我们将根据500 只交易量最大的股票的 5 年期美元交易量移动平均值,计算月度回报和 25 个(有望)预测特征,得出 56756 个观察值。这些特点包括:

  • 过去 1、3、6 和 12 个月的历史回报
  • 将最近 1 个月或 3 个月的回报与长期回报联系起来的动量指标
  • 技术指标旨在捕捉波动性,如(归一化)平均真实范围(NATR 和 ATR)和相对强度指数RSI等动量。
  • 基于滚动 OLS 回归的五个 Fama-French 因子的因子载荷
  • 年度和月份以及部门的分类变量

图 11.2显示了这些特征与我们用于回归的月收益率(左面板)及其二值化分类对应物之间的相互信息,表示同一时期的正或负价格变动。这表明,在单变量基础上,关于这两种特征的结果,信号内容似乎存在实质性差异。

更多详细信息可在本章 GitHub 存储库的data_prep笔记本中找到。本章中的决策树模型不具备处理缺失或分类变量的能力,因此我们将放弃前者,采用虚拟编码(参见第 4 章金融特征工程——如何研究阿尔法因子第 6 章、*机器学习过程)*到分类扇区变量:

图 11.2:功能和回报或价格变动方向的互信息

用时间序列数据建立回归树

回归树根据分配给给定节点的训练样本的平均结果值进行预测,并且通常在递归二进制分割期间依靠均方误差来选择最佳规则。

给定一个训练集,该算法迭代对p预测器、X1、X2Xp 和【T16N 可能的切点、ss2sn寻找最优组合。最优规则将特征空间划分为两个区域,{X**Xi<s【T38 j】和*|Xi>sj},其中Xi特征值在【T54 s】j下方或上方阈值,以便基于训练子集的预测最大限度地减少相对于当前节点的平方残差。*

*让我们从一个简化的示例开始,以便于可视化,并演示如何将时间序列数据与决策树结合使用。根据上一章的 AR(2)模型,我们将仅使用 2 个月的滞后回报预测下一个月:

使用 scikit learn,配置和训练回归树非常简单:

from sklearn.tree import DecisionTreeRegressor
# configure regression tree
regression_tree = DecisionTreeRegressor(criterion='mse',      
                                        max_depth=6,         
                                        min_samples_leaf=50)
# Create training data
y = data.target
X = data.drop(target, axis=1)
X2 = X.loc[:, ['t-1', 't-2']]
# fit model
regression_tree.fit(X=X2, y=y)
# fit OLS model
ols_model = sm.OLS(endog=y, exog=sm.add_constant(X2)).fit() 

OLS摘要和决策树前两级的可视化显示了模型之间的显著差异(参见图 11.3。OLS 模型为截距和两个特征提供了三个参数,符合该模型对功能的线性假设。

相反,回归树形图显示前两个级别的每个节点用于分割数据的特征和阈值(注意特征可以重复使用),以及当前的均方误差MSE)值、样本数、,以及基于这些训练样本的预测值。另外,请注意,训练决策树需要 58 毫秒,而线性回归需要 66 微秒。虽然这两款机型运行速度都很快,只有两个功能,但差异是 1000 倍:

图 11.3:OLS 结果和回归树

树形图还突出显示了样本在节点上的不均匀分布,因为第一次拆分后样本数在 545 到 55000 之间变化。

为了进一步说明关于输入变量和输出之间关系的函数形式的不同假设,我们可以将当前收益预测可视化为特征空间的函数,即滞后收益值范围的函数。下图显示了当前月回报率,作为线性回归(左面板)和回归树的一个和两个时期前回报率的函数:

图 11.4:线性回归和回归树的决策面

左侧的线性回归模型结果强调了滞后收益和当前收益之间的线性关系,而右侧的回归树图说明了特征空间递归划分中编码的非线性关系。

建立分类树

分类树的工作原理与回归版本类似,只是结果的分类性质要求采用不同的方法进行预测和衡量损失。回归树使用相关训练样本的平均结果预测分配给叶节点的观测响应,而分类树使用模式,即相关区域中训练样本中最常见的类别。分类树还可以根据相对类别频率生成概率预测。

如何优化节点纯度

在生长分类树时,我们也使用递归二进制分割,但我们可以使用分类错误率,它只是给定(叶)节点中训练样本的分数,而不是使用均方误差的减少来评估决策规则的质量不属于最普通的类别。

然而,可选的度量,无论是基尼杂质还是交叉熵,都是首选,因为它们比分类错误率更敏感,如图 11.5所示。节点纯度是指节点中单个类别的优势程度。仅包含结果属于单个类的样本的节点是纯节点,意味着对特征空间的这个特定区域进行了成功分类。

让我们看看如何计算分类结果的这些度量,其中K类别为 0,1,…,[T2】K-1(在二元情况下,K=2)。对于给定节点m,设pmkkth类的样本比例:

下图显示,当类别比例为偶数时,基尼杂质和交叉熵测度在[0,1]区间内均达到最大,或者在二元情况下为 0.5。当类比例接近 0 或 1 且子节点由于拆分而趋于纯净时,这两种度量都会下降。同时,它们意味着对节点杂质的惩罚高于分类错误率:

图 11.5:分类损失函数

请注意,交叉熵的计算时间几乎是基尼测度的 20 倍(详情请参见笔记本)。

如何训练分类树

现在,我们将使用 80%的样本进行训练,以预测剩余的 20%,来训练、可视化和评估一个具有多达五个连续拆分的分类树。我们将在这里采取一种简化说明的捷径,并使用内置的train_test_split,它不防止前瞻性偏差,作为我们在第 6 章中介绍的自定义MultipleTimeSeriesCV迭代器,机器学习过程,并将在本章后面使用。

树配置意味着最多 25=32 个叶节点,平均而言,在平衡情况下,将包含 1400 多个训练样本。请看下面的代码:

# randomize train-test split
X_train, X_test, y_train, y_test = train_test_split(X, y_binary, test_size=0.2, random_state=42)
# configure & train tree learner
clf = DecisionTreeClassifier(criterion='gini',
                            max_depth=5,
                            random_state=42)
clf.fit(X=X_train, y=y_train)
# Output:
DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=5,
            max_features=None, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, presort=False, random_state=42,
            splitter='best') 

模型训练后的输出显示所有DecisionTreeClassifier参数。我们将在超参数调整一节中更详细地讨论这些问题。

可视化决策树

您可以使用 Graphviz 库可视化树(有关安装说明,请参阅 GitHub),因为 scikit learn 可以使用该库使用的点语言输出树的描述。可以将输出配置为包括要素和类标签,并限制级别数以保持图表可读,如下所示:

dot_data = export_graphviz(classifier,
                           out_file=None, # save to file and convert to png
                           feature_names=X.columns,
                           class_names=['Down', 'Up'],
                           max_depth=3,
                           filled=True,
                           rounded=True,
                           special_characters=True)
graphviz.Source(dot_data) 

下图显示了模型如何使用不同的功能,并指出了连续变量和分类(虚拟)变量的拆分规则。在每个节点的标签值下,图表显示了每个类别的样本数量,在标签类别下,显示了最常见的类别(样本期内最多有个月):

图 11.6:分类树的可视化

评估决策树预测

为了评估我们第一棵分类树的预测准确性,我们将使用我们的测试集生成预测的类别概率,如下所示:

# only keep probabilities for pos. class
y_score = classifier.predict_proba(X=X_test)[:, 1] 

.predict_proba()方法为每个类生成一个概率。在二进制类中,这些概率是互补的,和为 1,所以我们只需要正类的值。为了评估泛化误差,我们将使用基于接收机工作特性的曲线下面积,这是我们在第 6 章机器学习过程中介绍的。结果表明,随机预测的基线值 0.5 以上有显著改善(但请记住,此处的交叉验证方法不考虑数据的时间序列性质):

roc_auc_score(y_score=y_score, y_true=y_test)
0.6341 

过拟合与正则化

决策树有很强的过度拟合倾向,尤其是当数据集具有大量与样本数量相关的特征时。如前几章所述,过度拟合会增加预测误差,因为模型不仅学习了训练数据中包含的信号,而且还学习了噪声。

有多种方法解决过度安装的风险,包括:

  • 降维(参见第 13 章数据驱动的风险因子和无监督学习的资产配置)通过以更少、更多信息和更少噪声的特征表示现有特征,从而提高特征样本比。
  • 集成模型(如随机森林)在对树结构进行随机化的同时组合了多棵树,我们将在本章第二部分中看到。

决策树提供了几个正则化超参数来限制树的增长和相关的复杂性。虽然每次拆分都会增加节点数,但也会减少每个节点可用于支持预测的样本数。对于每个附加级别,需要两倍的采样数才能使用相同的采样密度填充新节点。

树木修剪是一种额外工具,用于降低树木的复杂性。它是通过消除节点或树的整个部分来实现的,这些节点或部分增加的值很少,但会增加模型的方差。例如,成本复杂度修剪从一棵大树开始,通过用叶子替换节点,递归地减小其大小,本质上是反向运行树构造。各个步骤生成一系列树,然后可以使用交叉验证进行比较,以选择理想的大小。

如何规范化决策树

下表列出了 scikit 学习决策树实现中用于此目的的关键参数。在介绍了最重要的参数之后,我们将说明如何使用交叉验证优化超参数设置,以实现偏差-方差权衡和更低的预测误差:

| 参数 | 描述 | 违约 | 选择权 | | `max_depth` | 最大级别数:拆分节点,直到达到`max_depth`为止。所有叶子都是纯的或含有比`min_samples_split`更少的样品。 | 没有一个 | int | | `max_features` | 为分割而考虑的特征数量。 | 没有一个 | 无:所有功能`int`:#功能`float`:分数`auto`、`sqrt`:sqrt(`n_features`)`log2`:log2(`n_features`) | | `max_leaf_nodes` | 分割节点,直到创建这么多叶子。 | 没有一个 | 无:无限制`int` | | `min_impurity_decrease` | 如果杂质至少减少此值,则分割节点。 | 0 | `float` | | `min_samples_leaf` | 仅当左、右分支中至少有`min_samples_leaf`个训练样本时,才会考虑拆分。 | 1. | `int`;`float`(以*N*的百分比表示) | | `min_samples_split` | 拆分内部节点所需的最小样本数。 | 2. | `int`;`float`(占*N*的百分比) | | `min_weight_fraction_leaf` | 叶节点所需的所有样本权重总和的最小加权分数。除非拟合方法中提供了`sample_weight`,否则样品重量相等。 | 0 | |

max_depth参数对连续拆分的数量施加了严格限制,并表示限制树生长的最直接方法。

min_samples_splitmin_samples_leaf参数是数据驱动的备选方法,用于限制树的生长。这些参数控制进一步拆分数据所需的最小样本数,而不是对连续拆分的数量施加硬限制。后者保证每片叶子有一定数量的样本,而前者可以在分裂导致分布非常不均匀的情况下产生非常小的叶子。较小的参数值有助于过度拟合,而较大的参数值可能会阻止树学习数据中的信号。默认值通常很低,您应该使用交叉验证来探索一系列潜在值。您还可以使用浮点表示百分比,而不是绝对数。

scikit 学习文档包含关于如何在不同用例中使用各种参数的附加细节;有关更多信息,请参阅 GitHub 上链接的参考资料。

决策树剪枝

递归二进制分割可能会在训练集上产生良好的预测,但往往会过度拟合数据并产生较差的泛化性能。这是因为它导致了过于复杂的树,这些树反映在大量的叶节点中,或者说是特征空间的划分中。较少的拆分和叶节点意味着整体较小的树,通常会导致更好的预测性能和可解释性。

限制叶节点数量的一种方法是避免进一步拆分,除非它们在目标度量中产生显著改进。然而,这种策略的缺点是,有时,导致小改进的拆分会在以后随着样本的组成不断变化而启用更有价值的拆分。

相比之下,树修剪则是先生长一棵非常大的树,然后再删除或修剪节点,从而将大型树缩减为不太复杂且过拟合的子树。成本复杂度修剪通过向树模型添加叶节点的惩罚和调节惩罚影响的正则化参数(类似于 lasso 和 ridge 线性回归模型)生成子树序列。应用于大型树,增加的惩罚将自动生成一系列子树。正则化参数的交叉验证可用于识别最佳修剪子树。

此方法在 scikit 学习版本 0.22 中引入;参见 Esposito 等人(1997 年)对各种方法如何工作和执行的调查。

超参数调谐

决策树提供了一系列超参数来控制和调整训练结果。交叉验证是获得泛化误差无偏估计的最重要工具,这反过来又允许在各种配置选项中进行知情选择。scikit learn 提供了几个工具来促进交叉验证众多参数设置的过程,即GridSearchCV便利类,我们将在下一节中说明。学习曲线还允许进行诊断,评估收集额外数据以减少泛化错误的潜在好处。

将 GridsearchCV 与自定义度量一起使用

正如第 6 章**机器学习过程中强调的,scikit 学习提供了一种方法来定义多个超参数的值范围。它自动化交叉验证这些参数值的各种组合的过程,以确定最佳配置。让我们浏览一下自动调整模型的过程。

第一步实例化一个模型对象,定义一个字典,其中关键字命名超参数,值列出要测试的参数设置:

reg_tree = DecisionTreeRegressor(random_state=42)
param_grid = {'max_depth': [2, 3, 4, 5, 6, 7, 8, 10, 12, 15],
              'min_samples_leaf': [5, 25, 50, 100],
              'max_features': ['sqrt', 'auto']} 

然后,实例化GridSearchCV对象,为初始化方法提供估计器对象和参数网格,以及评分方法和交叉验证选择。

我们将我们的定制MultipleTimeSeriesSplit课程设置为对模型进行 60 个月(或 5 年)的数据培训,并使用随后的 6 个月验证性能,将该过程重复 10 次,以覆盖 5 年的样本期:

cv = MultipleTimeSeriesCV(n_splits=10,
                          train_period_length=60,
                          test_period_length=6,
                          lookahead=1) 

我们使用roc_auc度量对分类器进行评分,并使用 scikit learn 的make_scorer函数为回归模型定义自定义信息系数(IC)度量:

def rank_correl(y, y_pred):
    return spearmanr(y, y_pred)[0]
ic = make_scorer(rank_correl) 

我们可以使用n_jobs参数并行搜索,并通过设置refit=True自动获得使用最优超参数的训练模型。

所有设置就绪后,我们可以像其他车型一样安装GridSearchCV

gridsearch_reg = GridSearchCV(estimator=reg_tree,
                          param_grid=param_grid,
                          scoring=ic,
                          n_jobs=-1,
                          cv=cv,  # custom MultipleTimeSeriesSplit
                          refit=True,
                          return_train_score=True)
gridsearch_reg.fit(X=X, y=y) 

培训过程为我们的GridSearchCV对象生成一些新属性,最重要的是关于最佳设置和最佳交叉验证分数的信息(现在使用正确的设置,以避免前瞻性偏差)。

下表分别列出了最佳回归和分类模型的参数和分数。对于较浅的树和更规则化的叶节点,回归树的 IC 为 0.083,而分类器的 AUC 分数为 0.525:

| 参数 | 回归 | 分类 | | **最大深度** | 6. | 12 | | **最大功能** | sqrt | sqrt | | **min_samples_leaf** | 50 | 5. | | **得分** | 0.0829 | 0.5250 |

自动化非常方便,但我们也希望检查不同参数值下的性能演变。完成此过程后,GridSearchCV对象将提供详细的交叉验证结果,以便我们获得更多的见解。

如何检查树结构

笔记本还演示了如何更手动地运行交叉验证以获取自定义树属性,例如与某些超参数设置关联的节点或叶节点的总数。下面的函数访问内部.tree_ attribute以获取有关总节点数的信息,以及这些节点中有多少是叶节点:

def get_leaves_count(tree):
    t = tree.tree_
    n = t.node_count
    leaves = len([i for i in range(t.node_count) if t.children_left[i]== -1])
    return leaves 

我们可以将此信息与训练和测试分数结合起来,以获得关于交叉验证过程中模型行为的详细信息,如下所示:

train_scores, val_scores, leaves = {}, {}, {}
for max_depth in range(1, 26):
    print(max_depth, end=' ', flush=True)
    clf = DecisionTreeClassifier(criterion='gini', 
                                 max_depth=max_depth,
                                 min_samples_leaf=10,
                                 max_features='auto',
                                 random_state=42)
    train_scores[max_depth], val_scores[max_depth] = [], [] 
    leaves[max_depth] = []
    for train_idx, test_idx in cv.split(X):
        X_train, = X.iloc[train_idx], 
        y_train  = y_binary.iloc[train_ idx]
        X_test, y_test = X.iloc[test_idx], y_binary.iloc[test_idx]
        clf.fit(X=X_train, y=y_train)
        train_pred = clf.predict_proba(X=X_train)[:, 1]
        train_score = roc_auc_score(y_score=train_pred, y_true=y_train)
        train_scores[max_depth].append(train_score)
        test_pred = clf.predict_proba(X=X_test)[:, 1]
        val_score = roc_auc_score(y_score=test_pred, y_true=y_test)
        val_scores[max_depth].append(val_score)    
        leaves[max_depth].append(get_leaves_count(clf)) 

下图显示了叶节点的数量如何随树的深度而增加。由于每个交叉验证折叠包含 60 个月的样本量,每个样本约有 500 个数据点,当min_samples_leaf数量限制为 10 个样本时,叶节点的数量限制为 3000 个左右:

图 11.7:分类树的可视化

比较回归和分类性能

为了更仔细地观察模型的性能,我们将展示不同深度的交叉验证性能,同时保持产生最佳网格搜索结果的其他参数设置。图 11.8显示了序列和验证分数,并突出显示了较深树木的过度拟合程度。这是因为培训分数稳步增加,而验证性能保持不变或下降。

注意,对于分类树,网格搜索建议 12 个级别以获得最佳预测精度。然而,该图显示了不太复杂的树的相似 AUC 分数,有三个或七个级别。我们更喜欢较浅的树,该树承诺具有可比的泛化性能,同时降低过度拟合的风险:

图 11.8:两种模型的训练和验证分数

用学习曲线诊断训练集大小

学习曲线是一个有用的工具,可以显示验证和培训分数如何随着培训样本数量的增加而变化。

学习曲线的目的是找出模型是否以及在多大程度上受益于在训练期间使用更多数据。它还有助于诊断模型的泛化误差是否更可能由偏差或方差驱动。

如果培训分数满足性能预期,并且随着培训样本的增长,验证分数表现出显著的改善,那么进行更长的回顾期培训或获取更多数据可能会增加价值。另一方面,如果验证和训练分数都收敛到一个类似的差值,尽管训练集的大小在增加,错误更可能是由于偏差,额外的训练数据也不太可能有帮助。

下图描述了最佳回归和分类模型的学习曲线:

图 11.9:每个模型最佳版本的学习曲线

特别是对于回归模型,随着训练集的增加,验证性能提高。这表明更长的训练时间可能会产生更好的结果。你自己试试看是否有效!

从功能重要性中获得洞察力

决策树不仅可以可视化来检查给定特征的决策路径,还可以总结每个特征对模型学习的规则的贡献,以适应训练数据。

特征重要性捕获每个特征产生的分割有多少有助于优化用于评估分割质量的模型度量,在我们的例子中是基尼杂质。特征的重要性计算为该度量的(标准化)总减少量,并考虑受分割影响的样本数。因此,在树中较早使用的特征(节点往往包含更多样本)通常被认为具有更高的重要性。

图 11.10显示了每个模型前 15 个特征的特征重要性图。注意特征顺序与基于本节开头给出的互信息分数的单变量评估之间的差异。显然,决策树捕获相互依赖性的能力(例如时间段和其他特征之间的相互依赖性)可以改变每个特征的值:

图 11.10:最佳回归和分类模型的特征重要性

决策树的优缺点

回归和分类树方法的预测与我们在前几章中探讨的线性模型非常不同。您如何决定哪种型号更适合当前的问题?考虑以下事项:

  • 如果结果和特征之间的关系是近似线性的(或者可以进行相应的转换),那么线性回归可能会优于更复杂的方法,例如不利用这种线性结构的决策树。
  • 如果这种关系表现为高度非线性和更复杂,决策树可能会优于经典模型。请记住,关系的复杂性需要是系统的或“真实的”,而不是由噪音驱动的,噪音会导致更复杂的模型过度拟合。

几个优点使得决策树非常流行:

  • 它们的理解和解释相当简单,尤其是因为它们可以很容易地可视化,因此更容易为非技术观众所理解。决策树也被称为白盒模型,考虑到它们如何得出预测的高度透明性。黑盒模型,如集合和神经网络,可以提供更好的预测精度,但决策逻辑往往更难理解和解释。
  • 决策树所需的数据准备比模型所需的数据准备要少,因为模型对数据做出了更强有力的假设,或者对异常值更敏感,并且需要数据标准化(如正则化回归)。
  • 一些决策树实现处理分类输入,不需要创建虚拟变量(提高内存效率),并且可以处理缺失的值,正如我们将在第 12 章中看到的,提升您的交易策略,但 scikit 学习并非如此。
  • 预测速度很快,因为它的叶节点数是对数的(除非树变得极不平衡)。
  • 可以使用统计测试验证模型,并说明其可靠性(有关更多详细信息,请参阅参考资料)。

决策树也有几个关键缺点

  • 决策树有一种内在的倾向,即过度拟合训练集并产生较高的泛化误差。解决这一弱点的关键步骤是使用限制树生长的早期停止标准进行修剪和正则化,如本节所述。
  • 决策树对不平衡的类权重也很敏感,可能会产生有偏树。一种选择是对代表性不足的班级进行过抽样,或者对更频繁的班级进行过抽样。不过,通常最好使用类权重并直接调整目标函数。
  • 决策树的高方差与它们紧密适应训练集的能力有关。因此,数据中的微小变化会导致树的结构发生较大的变化,从而导致模型的预测。一个关键的预防机制是使用一组随机决策树,这些决策树具有低偏差并产生不相关的预测误差。
  • 决策树学习的贪婪方法优化了局部准则,从而减少了当前节点的预测误差,并且不保证全局最优结果。同样,由随机树组成的集合有助于缓解这个问题。

现在,我们将使用集成方法来降低使用决策树时固有的过度拟合风险。

随机森林-使树木更可靠

决策树不仅对其透明度和可解释性有用。它们也是更强大的集成模型的基本构建块,集成了许多单独的树,同时随机改变它们的设计以解决我们刚才讨论的过拟合问题。

为什么集成模型表现更好

集成学习包括将多个机器学习模型组合成一个新模型,其目的是比任何单个模型做出更好的预测。更具体地说,集成集成了使用一个或多个学习算法训练的多个基础估计器的预测,以减少这些模型自身产生的泛化误差。

为了实现这一目标的集成学习,单个模型必须是

  • 准确:优于简单基线(如样本平均值或类别比例)
  • 独立:预测生成方式不同,产生的误差也不同

集成方法是最成功的机器学习算法之一,特别是对于标准数值数据。大型集成在机器学习竞赛中非常成功,可能由许多不同的单独模型组成,这些模型通过手工或使用其他机器学习算法组合而成。

组合不同模型的预测有几个缺点。其中包括降低可解释性和更高的复杂性以及培训、预测和模型维护的成本。因此,在实践中(在比赛之外),从大规模加密中获得的微小精度收益可能不值得增加成本。

通常有两组集合方法可以区分,这取决于它们如何优化组成模型,然后集成单个集合预测的结果:

  • 平均方法独立训练多个基础估计器,然后对其预测进行平均。如果基础模型没有偏差,并且产生不同的预测误差,且相关性不高,则组合预测可能具有更低的方差,并且更可靠。这类似于从具有不相关回报的资产构建投资组合,以减少波动性而不牺牲回报。
  • Boosting 方法相比之下,训练基估计器顺序使用特定的目标,即减少组合估计器的偏差。其动机是将几个弱模型组合成一个强大的集合。

我们将在本章剩余部分重点介绍自动平均法和第 12 章**推进您的交易策略中的推进法。

引导聚合

我们发现决策树可能由于高方差而做出糟糕的预测,这意味着树结构对可用的训练样本非常敏感。我们还看到,低方差模型,如线性回归,尽管训练样本不同,但只要在给定特征数量的情况下有足够的样本,就可以产生类似的估计。

对于给定的一组独立观测值,每个观测值的方差为,样本平均值的标准误差由给出。换句话说,在一组较大的观测值上求平均值可以减少方差。因此,减少模型方差及其泛化误差的自然方法是从总体中收集许多训练集,在每个数据集上训练不同的模型,并对结果预测进行平均。

在实践中,我们通常没有许多不同训练集的奢侈。这就是打包,即引导聚合的简称。Bagging 是一种通用的方法,用于减少机器学习模型的方差,它在应用于决策树时特别有用和流行。

我们将首先解释此技术如何缓解过度拟合,然后展示如何将其应用于决策树。

装袋如何降低模型差异

装袋是指引导样本的聚集,是带有替换的随机样本。这样的随机样本与原始数据集具有相同数量的观测值,但由于替换,可能包含重复的观测值。

装袋增加了预测精度,但降低了模型的可解释性,因为不再可能通过可视化树来理解每个特征的重要性。作为一种集成算法,bagging 方法在这些自举样本上训练给定数量的基估计量,然后将它们的预测聚合为最终的集成预测。

Bagging 通过以下方式减少基本估计量的方差,以减少其泛化误差:

  1. 随机化每棵树的生长方式
  2. 平均他们的预测

改进给定模型通常是一种简单的方法,无需更改底层算法。这种技术最适用于具有低偏差和高方差的复杂模型,例如深度决策树,因为其目标是限制过度拟合。相比之下,Boosting 方法最适用于弱模型,如浅决策树。

有几种装袋方法因其适用于培训集的随机抽样过程而不同:

  • 粘贴从训练数据中随机抽取样本而不进行替换,而装袋样本则进行替换。
  • 随机子空间从特征(即列)中随机采样,无需替换。
  • 随机斑块通过随机抽样观察值和特征来训练基估计量。

袋装决策树

为了将 bagging 应用于决策树,我们通过重复采样和替换,从训练数据中创建引导样本。然后,我们在每个样本上训练一棵决策树,并通过对不同树的预测进行平均来创建集合预测。您可以在笔记本bagged_decision_trees中找到此示例的代码,除非另有说明。

袋装决策树通常生长得很大,也就是说,它们有许多层次和叶节点,并且没有修剪,因此每棵树都具有低偏差但高方差。然后,平均预测的效果旨在减少其方差。袋装已被证明可以通过构建集合来显著提高预测性能,该集合结合了在引导样本上训练的数百甚至数千棵树。

为了说明套袋对回归树方差的影响,我们可以使用 scikit learn 提供的BaggingRegressor元估计量。它根据指定采样策略的参数训练用户定义的基本估计器:

  • max_samplesmax_features分别控制从行和列中提取的子集的大小。
  • bootstrapbootstrap_features确定是否在更换或不更换的情况下抽取这些样品。

下面的示例使用指数函数为单个DecisionTreeRegressorBaggingRegressor集合生成训练样本,该集合由 10 棵树组成,每棵树的深度为 10 层。这两个模型都是在随机样本上训练的,并预测带有附加噪声的实际函数的结果。

由于我们知道真实函数,我们可以将均方误差分解为偏差、方差和噪声,并根据以下细分比较两种模型中这些分量的相对大小:

我们将抽取 100 个随机样本,每个样本包括 250 个训练和 500 个测试观察,以训练每个模型并收集预测:

noise = .5  # noise relative to std(y)
noise = y.std() * noise
X_test = choice(x, size=test_size, replace=False)
max_depth = 10
n_estimators=10
tree = DecisionTreeRegressor(max_depth=max_depth)
bagged_tree = BaggingRegressor(base_estimator=tree, n_estimators=n_estimators)
learners = {'Decision Tree': tree, 'Bagging Regressor': bagged_tree}
predictions = {k: pd.DataFrame() for k, v in learners.items()}
for i in range(reps):
    X_train = choice(x, train_size)
    y_train = f(X_train) + normal(scale=noise, size=train_size)
    for label, learner in learners.items():
        learner.fit(X=X_train.reshape(-1, 1), y=y_train)
        preds = pd.DataFrame({i: learner.predict(X_test.reshape(-1, 1))},
                              index=X_test)
        predictions[label] = pd.concat([predictions[label], preds], axis=1) 

对于每个模型,图 11.11中的曲线图显示:

  • 平均预测值和平均值周围的两个标准偏差带(上面板)
  • 基于真函数值的偏差方差噪声分解(底部面板)

我们发现基于自举样本,单个决策树(左侧)预测的方差几乎是 10 棵袋装树小集合预测方差的两倍:

图 11.11:个体和袋装决策树的偏差方差分解

具体实施见bagged_decision_trees笔记本。

如何建立一个随机森林

随机森林算法建立在 bagging 引入的随机化基础上,以进一步减少方差并提高预测性能。

除了根据自举训练数据对每个集合成员进行训练外,随机森林还从模型中使用的特征中随机采样(无需替换)。根据实现情况,可以为每个树或每个分割提取随机样本。因此,该算法在学习新规则时面临不同的选择,无论是在树的级别还是在每次拆分时。

回归树和分类树的特征的样本量不同:

  • 对于分类,样本量通常是特征数量的平方根。
  • 对于回归,它可以是所有特征的三分之一,并且应该基于交叉验证进行选择。

下图说明了随机森林如何将单个树的训练随机化,然后将其预测聚合为集合预测:

图 11.12:随机森林如何生长单株树木

除了训练观测之外,随机化特征的目的是进一步去相关单个树的预测误差。并非所有特征都是平等创建的,在树构建过程中,少数高度相关的特征将更频繁、更早地被选择,从而使整个集合中的决策树更加相似。然而,单个树的泛化误差相关性越小,总体方差将减少得越多。

如何训练和调整随机森林

关键配置参数包括如何调整超参数部分中介绍的单个决策树的各种超参数。下表列出了两个RandomForest类的附加选项:

| 关键词 | 违约 | 描述 | | `bootstrap` | `TRUE` | 训练期间的引导样本 | | `n_estimators` | `10` | 森林中的树木数量 | | `oob_score` | `FALSE` | 使用袋外样本来估计未知数据的 R2 |

bootstrap参数激活刚才描述的装袋算法。反过来,Bagging 支持计算包外分数(oob_score,该分数根据用于训练给定树的引导样本中未包含的样本估计泛化精度(参见包外测试部分)。

参数n_estimators定义作为森林一部分种植的树木数量。更大的森林表现更好,但也需要更多的时间来建设。随着基础学习者数量的增加,监控交叉验证错误非常重要。我们的目标是确定培训额外树的成本何时超过了减少验证错误的好处,或者验证错误何时开始再次增加。

max_features参数控制学习新决策规则和分割节点时可用的随机选择特征子集的大小。较低的值会降低树的相关性,从而降低集合的方差,但也可能会增加偏差。正如本节开头所指出的,良好的起始值是回归问题的训练特征数和分类问题的训练特征数的平方根,但取决于特征之间的关系,应使用交叉验证进行优化。

随机森林被设计为包含深度完全生长的树木,可以使用max_depth=Nonemin_samples_split=2创建。然而,这些值不一定是最优的,特别是对于具有许多样本的高维数据,因此,可能非常深的树可能会变得非常计算和内存密集。

scikit learn 提供的RandomForest类通过将n_jobs参数设置为k个要在不同内核上运行的作业数,支持并行训练和预测。-1值使用所有可用的磁芯。进程间通信的开销可能会限制线性加速,因此k个作业可能会占用单个作业 1/k个以上的时间。尽管如此,对于大型森林或较深的单株树木,加速效果通常非常显著,当数据较大时,可能需要相当长的时间进行训练,并且分割评估成本较高。

一如既往,最好的参数配置应通过交叉验证确定。以下步骤说明了该过程。本例的代码在笔记本random_forest_tuning中。

我们将使用GridSearchCV为分类树集合确定一组最佳参数:

rf_clf = RandomForestClassifier(n_estimators=100,
                                criterion='gini',
                                max_depth=None,
                                min_samples_split=2,
                                min_samples_leaf=1,
                                min_weight_fraction_leaf=0.0,
                                max_features='auto',
                                max_leaf_nodes=None,
                                min_impurity_decrease=0.0,
                                min_impurity_split=None,
                                bootstrap=True, oob_score=False,
                                n_jobs=-1, random_state=42) 

我们使用与前面决策树示例中相同的 10 倍自定义交叉验证,并使用关键配置设置的值填充参数网格:

cv = MultipleTimeSeriesCV(n_splits=10, train_period_length=60,
                          test_period_length=6, lookahead=1)
clf = RandomForestClassifier(random_state=42, n_jobs=-1)
param_grid = {'n_estimators': [50, 100, 250],
              'max_depth': [5, 15, None],
              'min_samples_leaf': [5, 25, 100]} 

使用前面的输入配置GridSearchCV

gridsearch_clf = GridSearchCV(estimator=clf,
                              param_grid=param_grid,
                              scoring='roc_auc',
                              n_jobs=-1,
                              cv=cv,
                              refit=True,
                              return_train_score=True,
                              verbose=1) 

我们像以前一样运行我们的网格搜索,并为表现最好的回归和分类模型找到以下结果。与分类器相比,随机森林回归模型对较浅树木的效果更好,但在其他方面使用相同的设置:

| 参数 | 回归 | 分类 | | 最大深度 | 5. | 15 | | 小叶 | 5. | 5. | | n_ 估计量 | 100 | 100 | | 分数 | 0.0435 | 0.5205 |

然而,这两种模型的表现都不如各自的决策树模型,这突出表明,更复杂的模型并不一定优于更简单的方法,尤其是在数据嘈杂且过度拟合风险较高的情况下。

随机森林的特征重要性

一个随机森林集合可能包含数百棵单独的树木,但仍然可以从袋装模型中获得特征重要性的总体汇总度量。

对于给定的特征,重要性分数是由于该特征上的拆分而导致的目标函数值的总减少量,并在所有树上取平均值。由于目标函数考虑了受拆分影响的特征数量,因此在树顶部附近使用的特征将获得更高的分数,因为可用节点数量越少,包含的观测数量越多。通过对以随机方式生长的多棵树进行平均,特征重要性估计会丢失一些方差并变得更加准确。

分数是根据回归树的均方误差和分类树的基尼杂质或熵来衡量的。scikit learn 进一步规范化了功能重要性,使其总和为 1。这样计算出的特征重要性也适用于特征选择,作为我们在第 6 章机器学习过程(参见sklearn.feature_selection模块中的SelectFromModel中看到的互信息度量的替代方法。

图 11.13显示了两款车型的前 15 个特征值。回归模型比性能更好的决策树更依赖于时间段:

图 11.13:随机森林特征重要性

袋外测试

随机林提供了内置交叉验证的好处,因为单个树是根据自举版本的训练数据进行训练的。因此,平均而言,每棵树只使用了可用观测值的三分之二。为了理解为什么,考虑引导样本具有相同的大小,作为原始样本,并且每一个 T3 观测值都具有相同的概率,1 /ωT4。因此,根本不进入自举样本的概率为(1-1/n**n,其(快速)收敛到 1/e,或大约三分之一。

未包含在训练集中的其余三分之一的观察值用于从袋中(OOB观察值)生长一棵称为的袋装树,并可作为验证集。正如交叉验证一样,我们为每个未经观察构建的树预测 OOB 样本的响应,然后平均预测响应(如果目标是回归),或者对每个 OOB 样本的单个集合预测进行多数投票或预测概率(如果目标是分类)。这些预测产生了泛化误差的无偏估计,在训练期间可以方便地进行计算。

由此产生的 OOB 误差是对该观测的泛化误差的有效估计。这是因为预测是使用在没有观察的情况下学习的决策规则生成的。一旦随机林足够大,OOB 错误将非常接近遗漏交叉验证错误。OOB 方法用于估计测试错误,对于交叉验证计算成本较高的大型数据集非常有效。

然而,同样的警告适用于交叉验证:如果 OOB 观察可能被选为无序,您需要小心避免出现前瞻性偏差。在实践中,这使得对时间序列数据使用 OOB 测试非常困难,其中需要根据数据的顺序性质选择验证集。

随机森林的利弊

袋装集合模型既有优点也有缺点。

随机林的优势包括:

  • 根据使用情况,随机森林可以执行与最佳监督学习算法相媲美。
  • 随机森林提供了可靠的特征重要性估计。
  • 它们提供了对测试误差的有效估计,而不会产生与交叉验证相关的重复模型训练的成本。

另一方面,随机森林的缺点包括:

  • 集成模型本质上比单个决策树更难解释。
  • 训练大量的深树可能会有很高的计算成本(但可以并行化),并且会占用大量内存。
  • 预测速度较慢,这可能会给需要低延迟的应用程序带来挑战。

现在让我们来看看如何使用随机森林作为交易策略。

日本股市的多空信号

第 9 章波动性预测和统计套利时间序列模型中,我们使用协整检验来识别具有长期均衡关系的对股票,其价格以共同趋势的形式恢复。

在本章中,我们将使用机器学习模型的预测来识别可能上升或下降的资产,从而相应地进入市场中性多头和空头头寸。该方法类似于我们在第 7 章线性模型——从风险因子到回报预测第 8 章ML4T 工作流——从模型到策略回溯测试中使用的线性回归的初始交易策略。

我们将使用 LightGBM 包,而不是 scikit learn random forest 实现的,它主要是为梯度提升而设计的。其中一个优点是 LightGBM 能够有效地将分类变量编码为数字特征,而不是使用一种热虚拟编码(Fisher 1958)。我们将在下一章中提供更详细的介绍,但是代码示例应该很容易理解,因为逻辑类似于 scikit 学习版本。

数据——日本股市

我们将利用波兰数据提供商 Stooq 提供的数据,为日本股市设计一套策略。Stooq 目前提供各种资产类别、市场和频率的有趣数据集,我们在第 9 章中也依赖这些数据波动预测和统计套利的时间序列模型

虽然数据的来源和质量几乎没有透明度,但它具有目前免费的强大优势。换句话说,我们可以以每天、每小时和 5 分钟的频率对股票、债券、大宗商品和外汇的数据进行实验,但应该对结果持保留态度。

本书 GitHub 存储库数据目录中的create_datasets笔记本包含下载数据并以 HDF5 格式存储的说明。在本例中,我们使用的是 2010-2019 年期间约 3000 只日本股票的价格数据。过去两年将作为样本外测试期,而前几年将作为模型选择的交叉验证样本。

本节代码示例请参考笔记本japanese_equity_features。我们删除了连续五个以上缺失值的股票,只保留 1000 只交易量最大的股票。

特点–滞后回报和技术指标

我们将保持相对简单,并将 1、5、10、21 和 63 个交易日的历史收益与 TA Lib 提供的几个技术指标相结合(参见第 4 章金融特征工程——如何研究阿尔法因子

更具体地说,我们计算每种股票:

  • 百分比价格振荡器PPO):衡量之间差异的移动平均收敛/分歧MACD指标的标准化版本 14 天和 26 天指数移动平均线,以捕捉各资产动量的差异。
  • 标准化平均真实范围NATR):以可以跨资产进行比较的方式衡量价格波动。
  • 相对强弱指数RSI):另一个热门动量指标(详见第 4 章金融特征工程——如何研究阿尔法因子)。
  • 布林带:移动平均值与移动标准差的比率,用于确定均值回归的机会。

我们还将包括年、月和工作日的时间段标记,并根据每个交易日六个区间的最新回报率,按照 1 到 20 的等级对股票进行排名。

结果——不同视野的远期回报

为了测试给定这些特征的随机森林的预测能力,我们在 21 个交易日(1 个月)的相同间隔内生成远期回报。

历史和远期回报所隐含的领先和滞后会导致一些数据损失,随着投资期限的延长而增加。最后,我们对 941 只股票的 18 个特征和 4 个结果进行了 230 万次观察。

使用 LightGBM 的 ML4T 工作流

我们现在将着手选择一个随机森林模型,该模型产生可交易的信号。几项研究已经成功地做到了这一点;例如,参见 Krauss、Do 和 Huck(2017 年)和 Rasekhschafe 和 Jones(2019 年)以及其中引用的资源。

我们将使用快速且内存高效的 LightGBM 实现,该实现由 Microsoft 开源,在渐变增强中最受欢迎,这是下一章的主题,我们将更仔细地了解各种 LightGBM 功能。

我们将从讨论关键的实验设计决策开始,然后建立并评估一个预测性的模型,其信号将驱动我们将在最后一步设计和评估的交易策略。除非另有说明,本节中的代码示例请参考笔记本random_forest_return_signals

从宇宙选择到超参数调整

为了开发使用机器学习模型的交易策略,我们需要对模型的范围和设计做出若干决定,包括:

  • 回望期:培训需要多少个历史交易日
  • 前瞻期:预测未来收益的天数
  • 测试周期:对于使用同一模型连续多少天进行预测
  • 超参数:需要评估哪些参数和配置
  • 融合:是依赖单个模型还是多个模型的组合

为了评估感兴趣的选项,我们还需要选择宇宙时间段进行交叉验证,以及样本外测试周期和宇宙。更具体地说,我们在日本股票样本的子集上交叉验证了截至 2017 年的若干期权。

一旦我们确定了一个模型,我们将定义交易规则并对策略进行回溯测试,该策略使用我们的模型在过去 2 年的样本中的信号来验证其性能。

对于时间序列交叉验证,我们将依赖于我们在第 7 章中开发的MultipleTimeSeriesCV线性模型——从风险因子到回报预测,以参数化训练和测试周期的长度,同时避免前瞻性偏差。此定制 CV 等级允许我们:

  • 对模型进行连续样本培训,每个报价器包含train_length天。
  • 除培训期外,在包含test_length天和lookahead天数的后续期间验证其性能,以避免数据泄漏。
  • 在滚动列车的同时重复给定数量的n_splits,验证周期向前推进test_length天。

我们将在本节中进行模型选择步骤,并在下一节中进行策略回溯测试。

采样计时器加快交叉验证

训练一个随机森林需要比线性回归更长的时间,并且取决于配置,其中树木的数量和深度是主要驱动因子。

为了使我们的实验易于管理,我们将选择 2010-17 年期间交易量最大的 250 只股票,以评估不同结果和模型配置的表现,如下所示:

DATA_DIR = Path('..', 'data')
prices = (pd.read_hdf(DATA_DIR / 'assets.h5', 'stooq/jp/tse/stocks/prices')
          .loc[idx[:, '2010': '2017'], :])
dollar_vol = prices.close.mul(prices.volume)
dollar_vol_rank = dollar_vol.groupby(level='date').rank(ascending=False)
universe = dollar_vol_rank.groupby(level='symbol').mean().nsmallest(250).index 

定义回顾、前瞻和前滚周期

运行我们的策略需要在滚动的基础上训练模型,使用我们宇宙中的特定交易日(回望期)来学习模型参数并预测未来一定天数的结果。在我们的例子中,我们将考虑 63, 126, 252 个、756 个和 1260 个交易日,同时滚动和预测 5, 21,或 63 天在每次迭代中。

我们将在列表中配对参数,以便于迭代和可选采样和/或洗牌,如下所示:

train_lengths = [1260, 756, 252, 126, 63]
test_lengths = [5, 21, 63]
test_params = list(product(train_lengths, test_lengths))
n = len(test_params)
test_param_sample = np.random.choice(list(range(n)), 
                                     size=int(n), 
                                     replace=False)
test_params = [test_params[i] for i in test_param_sample] 

LightGBM 超参数调谐

LightGBM模型接受大量参数,如文档中详细说明的(参见https://lightgbm.readthedocs.io/ 和下一章)。出于我们的目的,我们只需要通过定义boosting_type,将bagging_freq设置为正数,将objective设置为regression来启用随机森林算法:

base_params = dict(boosting_type='rf',
                   objective='regression',
                   bagging_freq=1) 

接下来,我们选择最可能影响预测精度的超参数,即:

  • 模型要生长的树数(num_boost_round
  • 用于装袋的行(bagging_fraction和列(feature_fraction的份额
  • 叶片中控制过度装配所需的最小样本数(min_data_in_leaf

LightGBM 的另一个好处是,我们可以评估一个经过训练的模型的树的子集(或在一定数量的评估之后继续训练),这允许我们在单个训练会话中测试多个num_iteration值。

或者,当验证集的损失度量不再改善时,您可以启用early_stopping中断培训。然而,交叉验证性能估计将向上倾斜,因为模型使用了在现实情况下无法获得的结果信息。

我们将使用以下超参数值,这些超参数控制装袋方法和树木生长:

bagging_fraction_opts = [.5, .75, .95]
feature_fraction_opts = [.75, .95]
min_data_in_leaf_opts = [250, 500, 1000]
cv_params = list(product(bagging_fraction_opts,
                         feature_fraction_opts,
                         min_data_in_leaf_opts))
n_cv_params = len(cv_params) 

跨不同视界交叉验证信号

为了评估一组给定超参数的模型,我们将使用回顾、前瞻和前滚周期生成预测。

首先,我们将识别分类变量,因为 LightGBM 不需要一个热编码;相反,它根据结果对类别进行排序,根据 Fisher(1958)的说法,这为回归树提供了更好的结果。我们将创建变量来标识不同的时段:

categoricals = ['year', 'weekday', 'month']
for feature in categoricals:
    data[feature] = pd.factorize(data[feature], sort=True)[0] 

为此,我们将创建二进制 LightGBM 数据集,并使用给定的train_lengthtest_length配置MultipleTimeSeriesCV,这决定了我们 2 年验证期的拆分数量:

for train_length, test_length in test_params:
    n_splits = int(2 * YEAR / test_length)
    cv = MultipleTimeSeriesCV(n_splits=n_splits,
                              test_period_length=test_length,
                              lookahead=lookahead,
                              train_period_length=train_length)
    label = label_dict[lookahead]
    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) 

接下来我们采取以下步骤:

  1. 选择此迭代的超参数。

  2. 将刚刚创建的二进制 LightGM 数据集切片为训练集和测试集。

  3. 训练模型。

  4. 为一系列num_iteration设置生成验证集预测:

    for p, (bagging_fraction, feature_fraction, min_data_in_leaf) \
            in enumerate(cv_params_):
        params = base_params.copy()
        params.update(dict(bagging_fraction=bagging_fraction,
                           feature_fraction=feature_fraction,
                           min_data_in_leaf=min_data_in_leaf))
        start = time()
        cv_preds, nrounds = [], []
        for i, (train_idx, test_idx) in \
                enumerate(cv.split(X=outcome_data)):
            lgb_train = lgb_data.subset(train_idx.tolist()).construct()
            lgb_test = lgb_data.subset(test_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}
            cv_preds.append(y_test.to_frame('y_test')
                            .assign(**y_pred).assign(i=i))
            nrounds.append(model.best_iteration) 
  5. 为了评估验证性能,我们计算完整预测集的 IC,以及每天迭代次数范围内的 IC:

    df = [by_day.apply(lambda x: spearmanr(x.y_test,
                                           x[str(n)])[0]).to_frame(n)
          for n in num_iterations]
    ic_by_day = pd.concat(df, axis=1)
    daily_ic.append(ic_by_day.assign(bagging_fraction=bagging_fraction,
                                     feature_fraction=feature_fraction,
                                     min_data_in_leaf=min_data_in_leaf))
    cv_ic = [spearmanr(cv_preds.y_test, cv_preds[str(n)])[0]
             for n in num_iterations]
    ic.append([bagging_fraction, feature_fraction,
               min_data_in_leaf, lookahead] + cv_ic) 

现在,我们需要评估预测的信号内容,为我们的交易策略选择一个模型。

交叉验证性能分析

首先,我们将查看各种列车和测试窗口的 IC 分布,以及所有超参数设置中的和预测范围。然后,我们将进一步了解超参数设置对模型预测精度的影响。

用于不同回溯、前滚和前瞻期间的 IC

下图显示了四个预测视界和五个训练窗口的日平均 IC 的分布和分位数,以及表现最佳的 21 天测试窗口。不幸的是,它不能得出短窗口还是长窗口效果更好的结论性见解,而是说明了由于我们测试的模型配置的范围以及由此导致的结果缺乏一致性,数据中的噪音程度:

图 11.14:各种模型配置的日平均信息系数分布

随机森林结构参数的 OLS 回归

为了更详细地了解我们的实验参数如何影响结果,我们可以在每日平均 IC 上对这些参数进行 OLS 回归。图 11.15显示了 1 天和 5 天前瞻期的系数和置信区间。

所有变量都是一个热编码变量,可以相对于常数捕获的每个变量的最小类别进行解释。结果在不同的层次上有所不同;最长的训练周期对 1 天的预测效果最好,但在 5 天内表现最差,没有明确的模式。长时间的训练似乎可以在一定程度上改善 1 天模型,但对于 5 天模型,这一点不太清楚。唯一稍微一致的结果似乎表明装袋分数较低,最低样品设置较高:

图 11.15:各种随机森林配置参数的 OLS 系数和置信区间

融合预测–使用 Alphalens 进行信号分析

最终,我们关心关于我们投资范围和持有期的模型预测的信号内容。为此,我们将使用 Alphalens 评估投资于不同预测收益分位数的等权投资组合产生的收益利差。

正如第 4 章金融特征工程——如何研究阿尔法因子中所述,阿尔法透镜计算并可视化各种指标,总结阿尔法因子的预测性能。笔记本alphalens_signals_quality说明了如何使用效用函数get_clean_factor_and_forward_returns将模型预测与适当格式的价格数据相结合。

为了解决 CV 预测中固有的一些噪声,我们根据平均每日 IC 和平均结果选择前三个 1 天模型。

当我们向 Alphalens 提供结果信号时,我们发现在 1 天的保持期内:

  • 年化α为 0.081,β为 0.083
  • 顶部和底部五分位数回报之间的平均周期利差为 5.16 个基点

下图显示了按因子五分位数计算的平均周期收益率以及与每五分位数中的股票相关的累积每日远期收益率:

图 11.16:Alphalens 因子信号评估

上图显示,基于顶部和底部五分位数的收益利差,提前 1 天的预测似乎包含短期内有用的交易信号。现在,我们将继续开发和回溯测试一个策略,该策略使用前十个 1 天前瞻模型生成的预测,该模型生成了验证期的结果。

策略–使用拉链进行回溯测试

为了使用 Zipline 设计和回溯测试交易策略,我们需要生成测试期内我们的宇宙预测,吸收日本股票数据并将信号加载到 Zipline 中,建立管道,并定义再平衡规则以触发相应的交易。

将日本股票纳入 Zipline

我们遵循ML4T 工作流程第 8 章**中所述的流程——从模型到策略回溯测试,将我们的 Stooq 股权 OHLCV 数据转换为一个拉链包。目录custom_bundle包含预处理模块,该模块创建资产 ID 和元数据,定义执行重载的摄取函数,并使用扩展注册捆绑包。

该文件夹包含一个带有附加说明的README

运行样本内和样本外策略回溯测试

笔记本random_forest_return_signals展示了如何选择产生最佳验证 IC 性能的超参数,并据此生成预测。

我们将使用我们的 1 天模型预测,并应用一些简单的逻辑:我们将为 25 种预测回报率最高和最低的资产输入多头和空头头寸。我们将每天进行交易,只要双方至少有 15 名候选人,并平仓所有不在当前最高预测范围内的头寸。

这一次,我们还将包括每股 0.05 美元的小额交易佣金,但不会使用打滑,因为我们交易的是流动性最高的日本股票,资本基础相对较低。

结果–使用 pyfolio 进行评估

图 11.17所示的左面板显示了该策略相对于日经 225 指数的样本内(2016-17)和样本外(2018-19)表现,在整个期间基本持平。

该策略在样本内和样本外的年化收益分别为 10.4%和 5.5%。

右面板显示了 3 个月的滚动夏普比率,样本中达到 0.96,样本外达到 0.61:

图 11.17:Pyfolio 战略评估

整体业绩统计数据显示,在每股 0.05 美元的(低)交易成本后,累计回报率为 36.6%,这意味着样本外的阿尔法为 0.06,贝塔为 0.08(相对于日经 225 指数)。样品中的最大压降为 11.0%,样品外的最大压降为 8.7%:

| | 全部的 | 抽样 | 样品外 | | #月份 | 48 | 25 | 23 | | 年报 | 8.00% | 10.40% | 5.50% | | 累积回报 | 36.60% | 22.80% | 11.20% | | 年度波动率 | 10.20% | 10.90% | 9.60% | | 夏普比率 | 0.8 | 0.96 | 0.61 | | 回收系数 | 0.72 | 0.94 | 0.63 | | 稳定性 | 0.82 | 0.82 | 0.64 | | 最大降深 | -11.00% | -11.00% | -8.70% | | 索提诺比率 | 1.26 | 1.53 | 0.95 | | 每日风险价值 | -1.30% | -1.30% | -1.20% | | 阿尔法 | 0.08 | 0.11 | 0.06 | | 贝塔 | 0.06 | 0.04 | 0.08 |

pyfolio 泪纸包含许多关于暴露、风险状况和其他方面的额外细节。

总结

在本章中,我们学习了一类新的模型,它能够捕捉非线性关系,这与我们迄今为止探索的经典线性模型不同。我们看到了决策树如何学习规则,将特征空间划分为产生预测的区域,从而将输入数据分割为特定区域。

决策树非常有用,因为它们为特征和目标变量之间的关系提供了独特的见解,我们还了解了如何可视化树结构中编码的决策规则序列。

不幸的是,决策树容易过度拟合。我们了解到集成模型和引导聚合方法能够克服决策树的一些缺点,并使它们成为更强大的复合模型的组件。

在下一章中,我们将探讨另一个集成模型 boosting,它被认为是最重要的机器学习算法之一。*

1
https://gitee.com/OpenDocCN/apachecn-ml-zh.git
git@gitee.com:OpenDocCN/apachecn-ml-zh.git
OpenDocCN
apachecn-ml-zh
apachecn-ml-zh
master

搜索帮助