金融机器学习与数据科学蓝图-全-

金融机器学习与数据科学蓝图(全)

原文:annas-archive.org/md5/d5f4b91728e84d1cbeabc13a1198818d

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

机器学习(ML)在金融领域的价值每天都在变得更加明显。机器学习预计将对金融市场的运作变得至关重要。分析师、投资组合经理、交易员和首席投资官都应该熟悉 ML 技术。对于试图改善金融分析、简化流程和增强安全性的银行和其他金融机构来说,ML 正在成为首选技术。ML 在机构中的使用是一个增长趋势,其在交易策略、定价和风险管理方面提升系统潜力的潜力也在不断增加。

尽管机器学习在金融服务行业的所有垂直领域都在取得重大进展,但在机器学习算法的想法和实施之间存在差距。在这些领域网上提供了大量的资料,但很少有组织的资料。此外,大多数文献仅限于交易算法。金融机器学习和数据科学蓝图填补了这一空白,为金融市场量身定制了一个机器学习工具箱,让读者成为机器学习革命的一部分。本书不仅限于投资或交易策略;它侧重于利用构建在金融业务中至关重要的 ML 驱动算法的艺术和技术。

在金融领域实施机器学习模型比普遍认为的要容易。还有一个误解是建立机器学习模型需要大数据。本书中的案例研究涵盖了几乎所有机器学习领域,并旨在解决这些误解。本书不仅涵盖了在交易策略中使用 ML 相关的理论和案例研究,还深入探讨了其他重要的“必须了解”概念,如投资组合管理、衍生品定价、欺诈检测、企业信用评级、机器人顾问开发和聊天机器人开发。它将解决从业者面临的现实问题,并提供科学支持的代码和示例。

本书在 GitHub 上的Python 代码库将对行业从业者在他们的项目上起到有用的作用,并作为一个起点。本书中展示的示例和案例研究展示了可以轻松应用于各种数据集的技术。未来主义的案例研究,如交易的强化学习、建立机器人顾问和使用机器学习进行工具定价,激励读者超越传统思维,并激励他们充分利用现有的模型和数据。

本书适合谁

本书的格式和涵盖主题的列表适合从事对冲基金、投资和零售银行以及金融科技公司工作的专业人士。他们可能担任数据科学家、数据工程师、量化研究员、机器学习架构师或软件工程师等职务。此外,本书对从事支持职能的专业人士如合规和风险方面的工作也非常有用。

无论是量化对冲基金交易员寻找有关使用强化学习进行加密货币交易的想法,还是投资银行量化分析师寻找改进定价模型校准速度的机器学习技术,本书都将为其增加价值。本书中提到的理论、概念和代码库将在模型开发生命周期的每个阶段都非常有用,从构思到模型实施。读者可以使用共享的代码库并自行测试提议的解决方案,实现亲身实践的阅读体验。读者应具备统计学、机器学习和 Python 的基本知识。

本书的组织方式

本书全面介绍了如何利用机器学习和数据科学设计金融领域不同领域的模型。本书分为四个部分组织。

第 I 部分:框架

第一部分概述了金融中的机器学习及其实施的基本构建块。这些章节为本书后续介绍的不同类型机器学习的案例研究奠定了基础。

第一部分的章节如下:

第一章,金融中的机器学习:概览

本章概述了机器学习在金融中的应用,并简要介绍了几种机器学习的类型。

第二章,Python 中开发机器学习模型

本章探讨了基于 Python 的机器学习生态系统。还涵盖了 Python 框架中进行机器学习模型开发的步骤。

第三章,人工神经网络

鉴于人工神经网络(ANN)是所有类型机器学习中使用的主要算法,本章详细讨论了 ANN 的细节,随后使用 Python 库详细实现了一个 ANN 模型。

第 II 部分:监督学习

第二部分涵盖了基本的监督学习算法,并展示了具体的应用和案例研究。

第二部分的章节如下:

第四章,监督学习:模型与概念

本章介绍了监督学习技术(包括分类和回归)。鉴于许多模型在分类和回归之间是共通的,本章将详细介绍这些模型,同时还涵盖了模型选择、分类和回归的评估指标等概念。

第五章,监督学习:回归(包括时间序列模型)

基于监督学习的回归模型是金融领域中最常用的机器学习模型。本章涵盖了从基本线性回归到高级深度学习的模型。本节的案例研究包括股票价格预测、衍生品定价和投资组合管理模型。

第六章,监督学习:分类

分类是监督学习的一个子类别,其目标是基于过去的观察预测新实例的分类类标签。本节讨论了基于分类技术的几个案例研究,如逻辑回归、支持向量机和随机森林等。

第三部分:无监督学习

第三部分涵盖了基本的无监督学习算法,并提供了应用和案例研究。

第三部分的章节如下:

第七章,无监督学习:降维

本章描述了减少数据集中特征数量的基本技术,同时保留大部分有用和区分信息的标准方法,如主成分分析,并涵盖了在投资组合管理、交易策略和收益率曲线构建中的案例研究。

第八章,无监督学习:聚类

本章涵盖了与聚类相关的算法和技术,用于识别共享相似度的对象群。本章还涵盖了在交易策略和投资组合管理中应用聚类的案例研究。

第四部分:强化学习和自然语言处理

第四部分涵盖了强化学习和自然语言处理(NLP)技术。

第四部分的章节如下:

第九章,强化学习

本章涵盖了强化学习的概念和案例研究,这在金融行业具有广泛的应用潜力。强化学习的主要思想“最大化回报”与金融多个领域的核心动机完美契合。本章还涵盖了与交易策略、投资组合优化和衍生品对冲相关的案例研究。

第十章,自然语言处理

本章描述了自然语言处理中的技术,并讨论了将文本数据转化为金融领域中有意义的表示的基本步骤。涵盖了与情感分析、聊天机器人和文档解释相关的案例研究。

本书使用的约定

本书中使用了以下印刷约定:

斜体

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序列表,以及段落内引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

提示

此元素表示提示或建议。

注意

此元素表示一般注意事项。

警告

此元素表示警告或注意事项。

注意

此元素表示蓝图。

使用本书中的代码

本书中的所有代码(案例研究和主模板)都可以在 GitHub 目录中找到:https://github.com/tatsath/fin-ml。代码托管在云平台上,因此可以通过点击https://mybinder.org/v2/gh/tatsath/fin-ml/master在本地机器上不安装任何包就运行每个案例研究。

本书旨在帮助您完成工作。一般情况下,如果提供示例代码,您可以在自己的程序和文档中使用它。除非您要复制代码的大部分,否则无需联系我们以获取许可。例如,编写使用本书中几个代码块的程序不需要许可。销售或分发 O’Reilly 书籍中的示例需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书中大量示例代码整合到产品文档中需要许可。

我们感谢您的支持,但通常不要求署名。署名通常包括标题、作者、出版社和 ISBN 号。例如:Machine Learning and Data Science Blueprints for Finance,作者 Hariom Tatsat、Sahil Puri 和 Brad Lookabaugh(O’Reilly,2021),978-1-492-07305-5。

如果您觉得您对代码示例的使用超出了公平使用范围或上述授权,请随时通过 permissions@oreilly.com 联系我们。

Python 库

本书使用 Python 3.7. 推荐安装 Conda 包管理器,以创建 Conda 环境来安装所需的库。安装说明请参见 GitHub 仓库的 README 文件

O’Reilly 在线学习

注意

40 多年来,O’Reilly Media 一直提供技术和商业培训、知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台让您随需应变地访问直播培训课程、深入学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多个出版商的大量文本和视频内容。欲了解更多信息,请访问 http://oreilly.com

如何联系我们

请将关于本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • 加利福尼亚州塞巴斯托波尔 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为这本书创建了一个网页,列出勘误、示例和任何额外信息。您可以访问该页面 https://oreil.ly/ML-and-data-science-blueprints

通过电子邮件 bookquestions@oreilly.com 发表评论或提出关于本书的技术问题。

获取关于我们书籍和课程的新闻和信息,请访问 http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://youtube.com/oreillymedia

致谢

我们要感谢所有帮助使这本书变成现实的人。特别感谢 Jeff Bleiel 提供诚实、有见地的反馈,并指导我们完成整个过程。我们非常感激 Juan Manuel Contreras、Chakri Cherukuri 和 Gregory Bronner,他们抽出宝贵时间详细审查了我们的书籍。书籍受益于他们宝贵的反馈和建议。同样感谢 O’Reilly 出色的工作人员,特别是 Michelle Smith,她对这个项目的支持和帮助让我们明确了其范围。

Hariom 的特别感谢

我要感谢我的妻子 Prachi 和我的父母,感谢他们的爱和支持。特别感谢我的父亲,在我所有追求中鼓励我,并持续成为我的灵感源泉。

Sahil 的特别感谢

感谢我的家人,在我所有努力中始终鼓励和支持我。

Brad 的特别感谢

感谢我的妻子 Megan,她无尽的爱和支持。

第一部分:框架

第一章:机器学习在金融中的景观

机器学习有望彻底改变金融的大片领域

经济学人(2017 年)

金融中有一波新的机器学习和数据科学浪潮,相关应用将在未来几十年内彻底改变这个行业。

目前,包括对冲基金、投资和零售银行以及金融科技公司在内的大多数金融公司,都在采用并大量投资于机器学习。未来,金融机构将需要越来越多的机器学习和数据科学专家。

由于大量数据的可用性和更加负担得起的计算能力,近年来机器学习在金融中的使用正呈指数级增长。

机器学习在金融领域的成功取决于构建高效的基础设施,使用正确的工具包和应用合适的算法。本书全程演示和利用了机器学习在金融中这些基础模块的相关概念。

在本章中,我们介绍了机器学习在金融中当前和未来的应用,包括对不同类型机器学习的简要概述。本章和随后的两章为本书其余部分中介绍的案例研究奠定了基础。

金融中当前和未来的机器学习应用

让我们看看金融中一些有前途的机器学习应用。本书中介绍的案例研究涵盖了这里提到的所有应用。

算法交易

算法交易(或简称algo trading)是使用算法自主进行交易的方法。起源可以追溯到上世纪 70 年代,算法交易(有时被称为自动交易系统,这可能更准确地描述)涉及使用自动预编程的交易指令,以做出极快速、客观的交易决策。

机器学习有望将算法交易推向新的高度。不仅可以采用和实时调整更先进的策略,而且基于机器学习的技术可以提供更多获得市场走向特殊洞察的途径。大多数对冲基金和金融机构并不公开披露他们基于机器学习的交易方法(出于正当理由),但机器学习在实时校准交易决策中的作用越来越重要。

投资组合管理和智能投顾

资产和财富管理公司正在探索潜在的人工智能(AI)解决方案,以改善他们的投资决策并利用他们的大量历史数据。

一个例子是使用智能投顾,这是为了根据用户的目标和风险承受能力调整金融投资组合的算法。此外,它们为最终投资者和客户提供自动化的财务指导和服务。

用户输入他们的财务目标(例如在 65 岁时以 25 万美元的储蓄退休)、年龄、收入和当前财务资产。顾问(分配器)然后将投资分散到资产类别和金融工具中,以达到用户的目标。

系统然后根据用户的目标和市场实时变化进行校准,始终致力于找到用户原始目标的最佳匹配。机器顾问已经在那些无需人类顾问即可感到舒适的消费者中获得了显著的吸引力。

欺诈检测

对金融机构而言,欺诈是一个巨大的问题,也是在金融中利用机器学习的首要原因之一。

当前存在显著的数据安全风险,由于高计算能力、频繁的互联网使用以及存储在线上的公司数据量不断增加。虽然以前的金融欺诈检测系统严重依赖复杂而健壮的规则集,但现代欺诈检测超越了遵循风险因素清单的检查——它积极学习并校准到新的潜在(或实际)安全威胁。

机器学习非常适合于打击欺诈金融交易。这是因为机器学习系统可以扫描庞大的数据集,检测异常活动,并立即标记它们。鉴于安全性可能被侵犯的方式不可胜数,真正的机器学习系统将在未来成为绝对必要。

贷款/信用卡/保险核保

核保可以被描述为金融中机器学习的完美工作,确实有很多担忧,即机器将取代今天存在的大量核保职位。

尤其是在大公司(大银行和上市保险公司),机器学习算法可以基于数百万个消费者数据和金融贷款或保险结果进行训练,例如一个人是否违约其贷款或抵押贷款。

利用算法可以评估潜在的金融趋势,并持续分析,以便检测可能影响未来贷款和核保风险的趋势。算法可以执行自动化任务,如匹配数据记录、识别异常情况,并计算申请人是否符合信贷或保险产品的资格。

自动化与聊天机器人

自动化显然非常适合于金融领域。它减少了重复、低价值任务对人类员工的压力。它处理日常例行流程,使团队能够完成其高价值工作。通过这样做,它带来了巨大的时间和成本节约。

将机器学习和人工智能融入自动化中,为员工提供了另一层支持。通过获取相关数据,机器学习和人工智能可以提供深入的数据分析,支持金融团队进行困难决策。在某些情况下,甚至可以推荐最佳行动方案供员工批准和实施。

金融领域的人工智能和自动化还可以学习识别错误,减少在发现和解决之间浪费的时间。这意味着人类团队成员在提供报告时更不容易延迟,能够减少工作中的错误。

AI 聊天机器人可以被用来支持金融和银行客户。随着银行和金融企业中实时聊天软件的流行,聊天机器人是自然演变的产物。

风险管理

机器学习技术正在改变我们对风险管理的方式。通过机器学习驱动的解决方案的增长,所有了解和控制风险的方面都正在发生革命性变化。例子包括决定银行应该向客户贷款多少,改善合规性并减少模型风险。

资产价格预测

资产价格预测被认为是金融领域中讨论最频繁、最复杂的领域。预测资产价格使人们能够了解推动市场的因素并推测资产表现。传统上,通过分析过去的财务报告和市场表现来预测资产价格,以确定特定证券或资产类别的持仓位置。然而,随着金融数据量的大幅增加,传统的分析方法和股票选择策略正在补充机器学习技术。

衍生品定价

最近机器学习取得的成功以及创新的快速步伐表明,未来几年衍生品定价的机器学习应用将广泛应用。黑-舒尔斯模型、波动率笑曲线和 Excel 电子表格模型的世界应逐渐消退,因为更先进的方法变得更易获取。

经典的衍生品定价模型建立在几个不切实际的假设上,旨在复制市场上观察到的衍生品价格与基础输入数据(行权价格、到期时间、期权类型)之间的经验关系。机器学习方法不依赖于多个假设;它们只是尝试估计输入数据与价格之间的函数,最小化模型结果与目标之间的差异。

最先进的机器学习工具实现的更快部署时间只是加速衍生品定价机器学习应用使用的优势之一。

情绪分析

情感分析涉及对大量非结构化数据进行审视,例如视频、转录、照片、音频文件、社交媒体帖子、文章和商业文件,以确定市场情绪。情感分析在今天的工作场所对所有企业至关重要,并且是金融中机器学习的一个极好的示例。

在金融领域中,情感分析最常见的用途是分析金融新闻,特别是预测市场的行为和可能的趋势。股市的波动是由无数与人类相关的因素引起的,机器学习能够通过发现新的趋势和发出信号来复制和增强人类对金融活动的直觉。

然而,未来机器学习的大部分应用将集中在理解社交媒体、新闻趋势和其他与预测客户对市场发展的情绪相关的数据来源上。它不仅限于预测股票价格和交易。

交易结算

交易结算是在金融资产交易后将证券转入买方账户并将现金转入卖方账户的过程。

尽管大多数交易都是自动结算的,并且很少或没有人类干预,约 30% 的交易仍然需要手动结算。

使用机器学习不仅可以识别失败交易的原因,还可以分析为什么交易被拒绝,提供解决方案,并预测未来可能失败的交易。通常人类需要花费五到十分钟来解决的问题,机器学习可以在几秒钟内完成。

洗钱

联合国的一份报告估计,全球每年洗钱金额占全球 GDP 的 2%–5%。机器学习技术可以分析客户广泛网络中的内部、公开存在的和交易数据,试图发现洗钱迹象。

机器学习、深度学习、人工智能和数据科学

对于大多数人来说,机器学习深度学习人工智能数据科学 这些术语很令人困惑。事实上,很多人会将其中一个术语与其他术语混为一谈。

图 1-1 显示了人工智能、机器学习、深度学习和数据科学之间的关系。机器学习是人工智能的一个子集,包括使计算机能够识别数据中的模式并交付人工智能应用的技术。而深度学习则是机器学习的一个子集,使计算机能够解决更复杂的问题。

数据科学并不完全是机器学习的一个子集,但它利用机器学习、深度学习和人工智能来分析数据并得出可操作的结论。它结合了机器学习、深度学习和人工智能与其他学科,如大数据分析和云计算。

mlbf 0101

图 1-1. AI、机器学习、深度学习和数据科学

以下是关于人工智能、机器学习、深度学习和数据科学的详细信息摘要:

人工智能

人工智能是研究计算机(及其系统)如何成功完成通常需要人类智能的复杂任务的领域。这些任务包括但不限于视觉感知、语音识别、决策制定和语言之间的翻译。人工智能通常被定义为使计算机在人类执行时需要智能的事物的科学。

机器学习

机器学习是人工智能的一种应用,它使 AI 系统能够自动从环境中学习,并将这些教训应用于做出更好的决策。机器学习使用各种算法来迭代学习、描述和改进数据,识别模式,然后对这些模式进行操作。

深度学习

深度学习是机器学习的一个子集,涉及与人工神经网络相关的算法研究,这些网络包含许多块(或层)堆叠在一起。深度学习模型的设计受到人类大脑生物神经网络的启发。它努力分析具有类似逻辑结构的数据,就像人类如何得出结论一样。

数据科学

数据科学是一个类似于数据挖掘的跨学科领域,利用科学方法、过程和系统从各种形式的数据中提取知识或洞见。数据科学与机器学习和人工智能不同,因为它的目标是通过使用不同的科学工具和技术来洞察和理解数据。然而,机器学习和数据科学都有一些共同的工具和技术,其中一些在本书中有所展示。

机器学习类型

本节将概述本书中用于各种金融应用的不同案例研究中使用的所有类型的机器学习。如图 1-2 所示,机器学习的三种类型是监督学习、无监督学习和强化学习。

mlbf 0102

图 1-2. 机器学习类型

监督

监督学习 的主要目标是从带标签数据中训练模型,使我们能够对未见或未来的数据进行预测。这里,“监督”一词指的是已知期望输出信号(标签)的一组样本。监督学习算法有两种类型:分类和回归。

分类

分类 是监督学习的一个子类,其目标是基于过去的观察预测新实例的分类类别标签。

回归

回归 是监督学习的另一子类,用于预测连续结果。在回归中,我们有多个预测(解释)变量和一个连续的响应变量(结果或目标),并尝试找到这些变量之间的关系,以便预测结果。

回归与分类的一个示例显示在 图 1-3 中。左侧的图表显示了一个回归的示例。连续的响应变量是回报,观察到的值与预测的结果相对比。右侧的图表显示了一个分类的示例,结果是一个分类类别标签,即市场是牛市还是熊市。

mlbf 0103

图 1-3. 回归与分类

无监督

无监督学习是一种机器学习类型,用于从不带标记响应的输入数据集中推断。无监督学习分为两种类型:降维和聚类。

降维

降维 是在保留信息和整体模型性能的同时减少数据集中特征或变量数量的过程。这是处理具有大量维度的数据集的常见且强大的方式。

图 1-4 展示了这一概念,其中数据的维度从两个维度(X[1]X[2])转换为一个维度(Z[1])。 Z[1] 传达了嵌入在 X[1]X[2] 中的相似信息,并且减少了数据的维度。

mlbf 0104

图 1-4. 降维

聚类

聚类 是无监督学习技术的一个子类,允许我们发现数据中隐藏的结构。聚类的目标是在数据中找到自然的分组,使同一组中的项目彼此更相似,而与来自不同组的项目更不相似。

聚类的一个示例显示在 图 1-5 中,聚类算法将整个数据聚集为两个明显的组。

mlbf 0105

图 1-5. 聚类

强化学习

从经验中学习,并伴随奖励或惩罚,是强化学习(RL)的核心概念。它涉及在特定情况下采取适当的行动以最大化奖励。学习系统称为智能体,可以观察环境,选择和执行动作,并获得回报(或以负奖励形式的惩罚),如 图 1-6 所示。

强化学习在以下方面与监督学习不同:在监督学习中,训练数据有答案标签,因此模型是根据可用的正确答案进行训练的。在强化学习中,没有显式的答案。学习系统(代理)决定如何执行给定任务,并根据奖励学习该动作是否正确。算法通过其经验确定答案标签。

mlbf 0106

图 1-6. 强化学习

强化学习的步骤如下:

  1. 首先,代理通过执行一个动作与环境进行交互。

  2. 然后,代理根据执行的动作获得奖励。

  3. 根据奖励,代理接收一个观察结果并理解该动作是好是坏。如果动作是好的,也就是代理收到了正面奖励,那么代理将更倾向于执行这个动作。如果奖励不太好,代理将尝试执行其他动作以获得正面奖励。这基本上是一个试错学习过程。

自然语言处理

自然语言处理(NLP)是人工智能的一个分支,处理机器理解人类使用的自然语言结构和含义的问题。NLP 内部使用了多种机器学习和深度学习技术。

NLP 在金融领域有许多应用,如情感分析、聊天机器人和文档处理等。很多信息,如卖方报告、收益电话和报纸头条,是通过文本信息传达的,使得 NLP 在金融领域非常有用。

鉴于基于机器学习的 NLP 算法在金融中的广泛应用,本书有一章(第十章)专门介绍 NLP 及相关案例研究。

章节总结

机器学习正在金融服务行业的各个垂直领域取得显著进展。本章介绍了机器学习在金融领域的不同应用,从算法交易到智能投顾。这些应用将在本书后面的案例研究中详细介绍。

后续步骤

在用于机器学习的平台方面,Python 生态系统正在发展壮大,是最主要的机器学习编程语言之一。在下一章中,我们将学习基于 Python 框架的模型开发步骤,从数据准备到模型部署。

第二章:在 Python 中开发机器学习模型

在用于机器学习的平台方面,有许多算法和编程语言可选择。然而,Python 生态系统是最主流和增长最快的机器学习编程语言之一。

鉴于 Python 的流行度和高采纳率,我们将其作为本书的主要编程语言。本章将概述基于 Python 的机器学习框架。首先,我们将回顾用于机器学习的 Python 包的详细信息,然后介绍 Python 框架中的模型开发步骤。

本章介绍的 Python 模型开发步骤将作为本书其余案例研究的基础。在金融领域开发任何基于机器学习的模型时,也可以利用 Python 框架。

为何选择 Python?

Python 受欢迎的原因有:

  • 高级语法(与 C、Java 和 C++等低级语言相比)。编写更少的代码即可开发应用程序,这使得 Python 对初学者和高级程序员都很有吸引力。

  • 高效的开发生命周期。

  • 社区管理的大量开源库。

  • 强大的可移植性。

Python 的简单性吸引了许多开发者为机器学习创建新的库,从而使 Python 的使用率大幅提升。

Python 机器学习包

主要用于机器学习的 Python 包在图 2-1 中得到了突出显示。

mlbf 0201

图 2-1. Python 软件包

下面简要总结了每个软件包:

NumPy

提供对大型多维数组的支持以及广泛的数学函数集合。

Pandas

用于数据处理和分析的库。除其他功能外,它还提供了处理表格的数据结构和相关工具。

Matplotlib

允许创建 2D 图表和图形的绘图库。

SciPy

NumPy、Pandas 和 Matplotlib 的组合通常被称为 SciPy。SciPy 是用于数学、科学和工程的 Python 库生态系统。

Scikit-learn(或 sklearn)

提供广泛算法和工具的机器学习库。

StatsModels

一个 Python 模块,提供类和函数,用于众多不同统计模型的估计,以及进行统计检验和统计数据探索。

TensorFlowTheano

数据流编程库有助于处理神经网络。

Keras

一个人工神经网络库,可以作为简化的 TensorFlow/Theano 软件包接口。

Seaborn

基于 Matplotlib 的数据可视化库。它提供了一个高级接口,用于绘制引人入胜且信息丰富的统计图形。

pipConda

这些是 Python 包管理器。pip 是一个包管理器,用于简化 Python 包的安装、升级和卸载。Conda 是一个包管理器,处理 Python 包以及 Python 包之外的库依赖。

Python 和包安装

安装 Python 有不同的方法。然而,强烈建议通过Anaconda安装 Python。Anaconda 包含 Python、SciPy 和 Scikit-learn。

安装完 Anaconda 后,可以通过打开机器的终端并输入以下代码来在本地启动 Jupyter 服务器:

$jupyter notebook
注意

本书中的所有代码示例均使用 Python 3,并以 Jupyter 笔记本形式呈现。在案例研究中广泛使用了多个 Python 包,特别是 Scikit-learn 和 Keras。

Python 生态系统中模型开发步骤

从头到尾解决机器学习问题至关重要。除非从开始到结束定义了步骤,否则应用的机器学习将无法真正发挥作用。

图 2-2 提供了一个简单的七步机器学习项目模板概述,可用于快速启动 Python 中的任何机器学习模型。前几个步骤包括探索性数据分析和数据准备,这是典型的数据科学步骤,旨在从数据中提取含义和洞见。这些步骤之后是模型评估、微调和最终化模型。

mlbf 0202

图 2-2. 模型开发步骤
注意

本书中的所有案例研究均遵循标准的七步模型开发过程。然而,有些案例研究会根据步骤的适当性和直观性跳过、重命名或重新排序一些步骤。

模型开发蓝图

下一节详细介绍了每个模型开发步骤及其支持的 Python 代码细节。

1. 问题定义

任何项目的第一步都是定义问题。可以使用强大的算法来解决问题,但如果解决了错误的问题,结果将毫无意义。

应使用以下框架来定义问题:

  1. 非正式和正式地描述问题。列出假设和类似问题。

  2. 列出解决问题的动机,解决方案提供的好处以及解决方案的使用方式。

  3. 描述如何使用领域知识解决问题。

2. 加载数据和包

第二步提供了开始解决问题所需的一切。这包括加载用于模型开发所需的库、包和单个函数。

2.1. 加载库

加载库的示例代码如下:

# Load libraries
import pandas as pd
from matplotlib import pyplot

特定功能的库和模块的详细信息在个案研究中进一步定义。

2.2. 加载数据

在加载数据之前,应检查并删除以下项目:

  • 列标题

  • 注释或特殊字符

  • 分隔符

有许多加载数据的方法。以下是一些最常见的方法:

使用 Pandas 加载 CSV 文件

from pandas import read_csv
filename = 'xyz.csv'
data = read_csv(filename, names=names)

从 URL 加载文件

from pandas import read_csv
url = 'https://goo.gl/vhm1eU'
names = ['age', 'class']
data = read_csv(url, names=names)

使用 pandas_datareader 加载文件

import pandas_datareader.data as web

ccy_tickers = ['DEXJPUS', 'DEXUSUK']
idx_tickers = ['SP500', 'DJIA', 'VIXCLS']

stk_data = web.DataReader(stk_tickers, 'yahoo')
ccy_data = web.DataReader(ccy_tickers, 'fred')
idx_data = web.DataReader(idx_tickers, 'fred')

3. 探索性数据分析

在此步骤中,我们查看数据集。

3.1. 描述统计

了解数据集是模型开发中最重要的步骤之一。了解数据的步骤包括:

  1. 查看原始数据。

  2. 查看数据集的维度。

  3. 查看属性的数据类型。

  4. 汇总数据集中变量的分布、描述统计和关系。

使用示例 Python 代码演示以下步骤:

查看数据

set_option('display.width', 100)
dataset.head(1)

输出

年龄 性别 工作 住房 储蓄账户 支票账户 信用金额 期限 目的 风险
0 67 男性 2 自有 NaN 少量 1169 6 无线电/电视 良好

查看数据集的维度

dataset.shape

输出

(284807, 31)

结果显示数据集的维度,并表明数据集有 284,807 行和 31 列。

查看数据属性的数据类型

# types
set_option('display.max_rows', 500)
dataset.dtypes

使用描述性统计汇总数据

# describe data
set_option('precision', 3)
dataset.describe()

输出

年龄 职业 信用金额 期限
count 1000.000 1000.000 1000.000 1000.000
mean 35.546 1.904 3271.258 20.903
std 11.375 0.654 2822.737 12.059
min 19.000 0.000 250.000 4.000
25% 27.000 2.000 1365.500 12.000
50% 33.000 2.000 2319.500 18.000
75% 42.000 2.000 3972.250 24.000
max 75.000 3.000 18424.000 72.000

3.2. 数据可视化

最快了解数据的方法是将其可视化。可视化包括独立理解数据集的每个属性。

以下是一些绘图类型:

单变量图

直方图和密度图

多变量图

相关矩阵图和散点图

以下是单变量绘图类型的 Python 代码示例:

单变量绘图:直方图

from matplotlib import pyplot
dataset.hist(sharex=False, sharey=False, xlabelsize=1, ylabelsize=1,\
figsize=(10,4))
pyplot.show()

单变量绘图:密度图

from matplotlib import pyplot
dataset.plot(kind='density', subplots=True, layout=(3,3), sharex=False,\
legend=True, fontsize=1, figsize=(10,4))
pyplot.show()

图 2-3 说明了输出。

mlbf 0203

图 2-3. 直方图(上)和密度图(下)

以下是多变量绘图类型的 Python 代码示例:

多变量绘图:相关矩阵图

from matplotlib import pyplot
import seaborn as sns
correlation = dataset.corr()
pyplot.figure(figsize=(5,5))
pyplot.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True,annot=True,cmap='cubehelix')

多变量绘图:散点图矩阵

from pandas.plotting import scatter_matrix
scatter_matrix(dataset)

图 2-4 说明了输出。

mlbf 0204

图 2-4. 相关性(左)和散点图(右)

4. 数据准备

数据准备是一种预处理步骤,其中来自一个或多个来源的数据被清理和转换,以提高其质量,以便在使用之前使用。

4.1. 数据清洗

在机器学习建模中,不正确的数据可能会很昂贵。数据清洗包括检查以下内容:

有效性

数据类型、范围等。

准确性

数据接近真实值的程度。

完整性

所需数据完全已知的程度。

统一性

使用相同测量单位指定数据的程度。

执行数据清洗的不同选项包括:

删除数据中的“NA”值

dataset.dropna(axis=0)

用 0 填充“NA”

dataset.fillna(0)

用列均值填充 NA 值

dataset['col'] = dataset['col'].fillna(dataset['col'].mean())

4.2. 特征选择

用于训练机器学习模型的数据特征对性能有很大影响。不相关或部分相关的特征可能会对模型性能产生负面影响。特征选择¹ 是一个过程,在这个过程中,自动选择对预测变量或输出贡献最大的数据特征。

在对数据建模之前执行特征选择的好处是:

减少过拟合²

较少冗余数据意味着模型基于噪声进行决策的机会更少。

改善性能

较少误导性数据意味着改进的建模性能。

减少训练时间和内存占用

较少的数据意味着更快的训练速度和更低的内存占用。

下面的样本特征是一个示例,演示了如何使用SelectKBest函数选择最佳的两个特征。SelectKBest函数使用底层函数对特征进行评分,然后移除除了k个最高评分特征以外的所有特征:

from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
bestfeatures = SelectKBest( k=5)
fit = bestfeatures.fit(X,Y)
dfscores = pd.DataFrame(fit.scores_)
dfcolumns = pd.DataFrame(X.columns)
featureScores = pd.concat([dfcolumns,dfscores],axis=1)
print(featureScores.nlargest(2,'Score'))  #print 2 best features

输出

                  Specs      Score
2              Variable1  58262.490
3              Variable2    321.031

当特征无关时,它们应该被删除。删除无关特征的方法如下示例代码所示:

#dropping the old features
dataset.drop(['Feature1','Feature2','Feature3'],axis=1,inplace=True)

4.3. 数据转换

许多机器学习算法对数据有假设。以最佳方式对数据进行准备,使数据能够最好地暴露给机器学习算法,这是一个良好的实践。这可以通过数据转换来实现。

下面是不同的数据转换方法:

重新调整比例

当数据包含具有不同比例的属性时,许多机器学习算法可以通过将所有属性重新调整到相同的比例来受益。属性通常重新调整到零到一的范围内。这对于机器学习算法中使用的优化算法非常有用,也有助于加速算法中的计算:

from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(0, 1))
rescaledX = pd.DataFrame(scaler.fit_transform(X))

标准化

标准化 是一种有用的技术,用于将属性转换为均值为零、标准差为一的标准正态分布。对于假设输入变量表示正态分布的技术非常适用:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(X)
StandardisedX = pd.DataFrame(scaler.fit_transform(X))

标准化

标准化 指的是将每个观察(行)重新缩放为长度为一(称为单位范数或向量)。在使用加权输入值的算法处理具有不同尺度属性的稀疏数据集时,这种预处理方法非常有用:

from sklearn.preprocessing import Normalizer
scaler = Normalizer().fit(X)
NormalizedX = pd.DataFrame(scaler.fit_transform(X))

5. 评估模型

一旦我们估算了算法的性能,我们可以在整个训练数据集上重新训练最终的算法,并准备好用于操作。这样做的最佳方法是在新数据集上评估算法的性能。不同的机器学习技术需要不同的评估指标。在选择模型时,除了模型性能之外,还考虑了几个其他因素,如简易性、可解释性和训练时间。有关这些因素的详细信息在第四章中有所涵盖。

5.1. 训练和测试分割

评估机器学习算法性能的最简单方法是使用不同的训练和测试数据集。我们可以将原始数据集分成两部分:在第一部分上训练算法,对第二部分进行预测,并将预测结果与期望结果进行评估。分割的大小可以取决于数据集的大小和具体情况,尽管通常使用 80%的数据进行训练,剩余的 20%用于测试。训练和测试数据集之间的差异可能导致准确度估计的显著差异。可以使用 sklearn 中提供的train_test_split函数轻松地将数据分割成训练集和测试集:

# split out validation dataset for the end
validation_size = 0.2
seed = 7
X_train, X_validation, Y_train, Y_validation =\
train_test_split(X, Y, test_size=validation_size, random_state=seed)

5.2. 确定评估指标

选择用于评估机器学习算法的度量标准非常重要。评估指标的一个重要方面是其在区分模型结果方面的能力。本书的多个章节详细介绍了不同类型的 ML 模型所使用的不同类型的评估指标。

5.3. 比较模型和算法

选择机器学习模型或算法既是一门艺术,也是一门科学。没有一种适合所有情况的解决方案或方法。除了模型性能之外,还有多个因素可能影响选择机器学习算法的决策。

让我们通过一个简单的例子来理解模型比较的过程。我们定义两个变量,XY,并尝试构建一个预测Y使用X的模型。作为第一步,数据按前面部分提到的训练和测试分割进行分割:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
validation_size = 0.2
seed = 7
X = 2 - 3 * np.random.normal(0, 1, 20)
Y = X - 2 * (X ** 2) + 0.5 * (X ** 3) + np.exp(-X)+np.random.normal(-3, 3, 20)
# transforming the data to include another axis
X = X[:, np.newaxis]
Y = Y[:, np.newaxis]
X_train, X_test, Y_train, Y_test = train_test_split(X, Y,\
test_size=validation_size, random_state=seed)

我们不知道哪种算法在这个问题上表现良好。现在让我们设计我们的测试。我们将使用两个模型——一个线性回归和第二个多项式回归来拟合YX。我们将使用均方根误差(RMSE)指标来评估算法的性能,这是模型性能的一种度量。RMSE 将给出所有预测的错误程度的大致概念(零是完美的):

from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import PolynomialFeatures

model = LinearRegression()
model.fit(X_train, Y_train)
Y_pred = model.predict(X_train)

rmse_lin = np.sqrt(mean_squared_error(Y_train,Y_pred))
r2_lin = r2_score(Y_train,Y_pred)
print("RMSE for Linear Regression:", rmse_lin)

polynomial_features= PolynomialFeatures(degree=2)
x_poly = polynomial_features.fit_transform(X_train)

model = LinearRegression()
model.fit(x_poly, Y_train)
Y_poly_pred = model.predict(x_poly)

rmse = np.sqrt(mean_squared_error(Y_train,Y_poly_pred))
r2 = r2_score(Y_train,Y_poly_pred)
print("RMSE for Polynomial Regression:", rmse)

输出

RMSE for Linear Regression: 6.772942423315028
RMSE for Polynomial Regression: 6.420495127266883

我们可以看到多项式回归的 RMSE 略优于线性回归的 RMSE。由于前者拟合效果更好,因此在这一步骤中它是首选模型。

6. 模型调优

找到模型最佳超参数组合可以被视为一个搜索问题。⁴ 这种搜索练习通常被称为模型调优,是模型开发中最重要的步骤之一。通过使用诸如网格搜索等技术,通过创建所有可能的超参数组合网格并对每一个进行训练来寻找最佳模型的参数。除了网格搜索外,还有几种其他模型调优技术,包括随机搜索、贝叶斯优化和超品牌。

在本书介绍的案例研究中,我们主要集中于模型调优的网格搜索。

继续前述示例,以多项式作为最佳模型:接下来,对模型进行网格搜索,使用不同的次数重新拟合多项式回归。我们比较所有模型的 RMSE 结果:

Deg= [1,2,3,6,10]
results=[]
names=[]
for deg in Deg:
    polynomial_features= PolynomialFeatures(degree=deg)
    x_poly = polynomial_features.fit_transform(X_train)

    model = LinearRegression()
    model.fit(x_poly, Y_train)
    Y_poly_pred = model.predict(x_poly)

    rmse = np.sqrt(mean_squared_error(Y_train,Y_poly_pred))
    r2 = r2_score(Y_train,Y_poly_pred)
    results.append(rmse)
    names.append(deg)
plt.plot(names, results,'o')
plt.suptitle('Algorithm Comparison')

输出

mlbf 02in01

当多项式模型的次数增加时,RMSE 减小,而次数为 10 的模型具有最低的 RMSE。然而,次数低于 10 的模型表现非常好,测试集将用于确定最佳模型。

每个算法的通用输入参数集为分析提供了一个起点,但可能并非特定数据集和业务问题的最优配置。

7. 完善模型

在这里,我们执行选择模型的最后步骤。首先,我们使用训练好的模型对测试数据集进行预测。然后,我们尝试理解模型的直觉并保存以备进一步使用。

7.1. 测试集上的性能

在训练步骤中选择的模型会在测试集上进一步评估。测试集能够以无偏的方式比较不同模型,通过在训练的任何部分都未使用的数据进行比较。以下是前述步骤开发的模型的测试结果示例:

Deg= [1,2,3,6,8,10]
for deg in Deg:
    polynomial_features= PolynomialFeatures(degree=deg)
    x_poly = polynomial_features.fit_transform(X_train)
    model = LinearRegression()
    model.fit(x_poly, Y_train)
    x_poly_test = polynomial_features.fit_transform(X_test)
    Y_poly_pred_test = model.predict(x_poly_test)
    rmse = np.sqrt(mean_squared_error(Y_test,Y_poly_pred_test))
    r2 = r2_score(Y_test,Y_poly_pred_test)
    results_test.append(rmse)
    names_test.append(deg)
plt.plot(names_test, results_test,'o')
plt.suptitle('Algorithm Comparison')

输出

mlbf 02in02

在训练集中,我们看到随着多项式模型次数的增加,RMSE 减小,并且次数为 10 的多项式具有最低的 RMSE。然而,正如前面输出的次数为 10 的多项式所示,尽管训练集结果最佳,但测试集结果较差。对于次数为 8 的多项式,测试集中的 RMSE 相对较高。次数为 6 的多项式在测试集中表现最佳(尽管与测试集中其他次数较低的多项式相比差距不大),并且在训练集中也有良好的结果。因此,这是首选的模型。

除了模型性能之外,在选择模型时还有几个其他因素需要考虑,例如简单性、可解释性和训练时间。这些因素将在接下来的章节中进行讨论。

7.2. 模型/变量直觉

本步骤涉及综合考虑解决问题所采用的方法,包括模型在实现所期望的结果时的局限性、使用的变量以及选择的模型参数。关于不同类型的机器学习模型的模型和变量直觉的详细信息将在随后的章节和案例研究中呈现。

7.3. 保存/部署

找到准确的机器学习模型后,必须保存和加载它以确保以后的使用。

Pickle 是 Python 中用于保存和加载训练模型的包之一。使用 pickle 操作,训练好的机器学习模型可以以序列化的格式保存到文件中。稍后,可以加载这个序列化文件以反序列化模型进行使用。以下示例代码演示了如何将模型保存到文件并加载以在新数据上进行预测:

# Save Model Using Pickle
from pickle import dump
from pickle import load
# save the model to disk
filename = 'finalized_model.sav'
dump(model, open(filename, 'wb'))
# load the model from disk
loaded_model = load(filename)
提示

近年来,像AutoML这样的框架已经被构建出来,以自动化机器学习模型开发过程中的最大数量步骤。这些框架允许模型开发人员以高规模、高效率和高生产力构建 ML 模型。建议读者探索这些框架。

章节总结

由于其流行度、采纳率和灵活性,Python 往往是机器学习开发的首选语言。有许多可用的 Python 包来执行多种任务,包括数据清洗、可视化和模型开发。其中一些关键包括 Scikit-learn 和 Keras。

本章提到的模型开发的七个步骤可以在金融中开发任何基于机器学习的模型时使用。

下一步

在接下来的章节中,我们将涵盖机器学习的关键算法——人工神经网络。人工神经网络是金融领域机器学习的另一个构建模块,并且在各类机器学习和深度学习算法中广泛使用。

¹ 特征选择对监督学习模型更为重要,在第五章和第六章的个案研究中有详细描述。

² 过拟合在第四章中有详细讨论。

³ 应注意,在这种情况下,RMSE 的差异很小,并且可能在不同的训练/测试数据拆分下不会复制。

⁴ 超参数是模型的外部特性,可以被视为模型的设置,并且不是基于数据估计的模型参数。

第三章:人工神经网络

在机器学习中使用许多不同类型的模型。然而,一种突出的机器学习模型类别是人工神经网络(ANNs)。鉴于人工神经网络应用于所有类型的机器学习,本章将介绍 ANN 的基础知识。

ANN 是基于一组连接单位或节点(称为人工神经元)的计算系统,它们松散地模拟生物大脑中的神经元。每个连接就像生物大脑中的突触一样,可以将信号从一个人工神经元传输到另一个。接收信号的人工神经元可以处理它,然后将信号传递给连接到它的其他人工神经元。

深度学习 包括复杂的与 ANN 相关的算法研究。其复杂性归因于信息如何在模型中的精细模式中流动。深度学习能够将世界表示为概念的嵌套层次结构,每个概念都与更简单的概念相关定义。我们将在第 9 和 10 章中详细探讨深度学习技术在强化学习和自然语言处理应用中的使用。

ANN:架构、训练和超参数

ANN 包含多个层中排列的神经元。ANN 通过将建模输出与期望输出进行比较,通过训练阶段来学习在数据中识别模式。让我们详细介绍 ANN 的组成部分。

架构

ANN 架构包括神经元、层和权重。

神经元

ANN 的构建模块是神经元(也称为人工神经元、节点或感知器)。神经元具有一个或多个输入和一个输出。可以构建神经元网络来计算复杂的逻辑命题。这些神经元中的激活函数创建输入和输出之间复杂的非线性功能映射。²

如图 3-1 所示,一个神经元接受输入 (x[1], x[2]x[n]),应用学习参数生成加权和 (z),然后将该和传递给计算输出 f(z) 的激活函数 f

mlbf 0301

图 3-1. 人工神经元

单个神经元的输出 f(z)(如图 3-1 所示)无法对复杂任务进行建模。因此,为了处理更复杂的结构,我们使用多层这样的神经元。随着我们在水平和垂直方向上堆叠神经元,我们可以得到的函数类变得越来越复杂。图 3-2 展示了具有输入层、输出层和隐藏层的 ANN 架构。

mlbf 0302

图 3-2. 神经网络架构

输入层

输入层从数据集获取输入并且是网络的暴露部分。神经网络通常以数据集中每个输入值(或列)的一个神经元作为输入层来绘制。输入层中的神经元只是将输入值传递给下一层。

隐藏层

输入层之后的层称为隐藏层,因为它们不直接暴露给输入。最简单的网络结构是在隐藏层中有一个单独的神经元直接输出该值。

多层 ANN 能够解决更复杂的与机器学习相关的任务,因为它具有隐藏层。随着计算能力的增加和高效的库的出现,可以构建具有许多层的神经网络。具有许多隐藏层(超过三层)的 ANN 被称为深度神经网络。多个隐藏层允许深度神经网络学习数据的特征,因为简单的特征在一层到下一层的重新组合形成更复杂的特征。具有许多层的 ANN 将输入数据(特征)通过更多的数学运算而不是具有较少层的 ANN,并因此在训练过程中需要更多的计算量。

输出层

最终层称为输出层;它负责输出与解决问题所需格式相对应的值或值向量。

神经元权重

神经元权重表示单元之间连接的强度,并测量输入对输出的影响。如果从神经元一到神经元二的权重具有较大的幅度,则意味着神经元一对神经元二的影响较大。接近零的权重意味着改变此输入不会改变输出。负权重意味着增加此输入会减少输出。

训练

训练神经网络基本上意味着校准 ANN 中的所有权重。这种优化是使用涉及前向传播和反向传播步骤的迭代方法执行的。

前向传播

前向传播是将输入值馈送到神经网络并获得输出的过程,我们称之为预测值。当我们将输入值馈送到神经网络的第一层时,它会直接通过,不进行任何操作。第二层从第一层获取值,并在将该值传递到下一层之前应用乘法、加法和激活操作。同样的过程对任何后续层都重复,直到从最后一层接收到输出值。

反向传播

经过前向传播,我们从人工神经网络得到预测值。假设网络的期望输出是Y,前向传播的预测值是Y′。预测输出与期望输出的差异(YY′)被转换为损失(或成本)函数J(w),其中w表示人工神经网络中的权重。³ 目标是优化损失函数(即使损失尽可能小)在训练集上的表现。

使用的优化方法是梯度下降。梯度下降方法的目标是找到J(w)关于w的梯度,并沿着负梯度方向迈出一小步,直到达到最小值,如图 3-3 所示。

mlbf 0303

图 3-3. 梯度下降

在人工神经网络中,函数J(w)本质上是多个层的组合,如前文所述。因此,如果将第一层表示为函数p(),第二层表示为q(),第三层表示为r(),那么整体函数为J(w)=r(q(p()))w包含所有三层中的所有权重。我们希望找到J(w)关于w的每个分量的梯度。

略过数学细节,以上实质上意味着第一层中某个分量w的梯度将取决于第二层和第三层中的梯度。类似地,第二层中的梯度将取决于第三层中的梯度。因此,我们从最后一层开始反向计算导数,并使用反向传播计算前一层的梯度。

总体上,在反向传播过程中,模型误差(预测输出与期望输出之间的差异)逐层传播回网络,并根据它们对误差的贡献量更新权重。

几乎所有人工神经网络都使用梯度下降和反向传播。反向传播是找到梯度的一种清晰高效的方法之一。

超参数

超参数是在训练过程之前设置的变量,无法在训练中学习。人工神经网络具有大量超参数,这使得它们非常灵活。然而,这种灵活性使得模型调整过程变得困难。理解超参数及其背后的直觉有助于确定每个超参数的合理值,从而限制搜索空间。让我们从隐藏层和节点的数量开始。

隐藏层和节点数量

更多的隐藏层或每层节点意味着 ANN 中有更多的参数,使模型能够拟合更复杂的函数。为了有一个泛化能力良好的训练好的网络,我们需要选择一个最佳的隐藏层数量,以及每个隐藏层中的节点数量。节点和层数量过少会导致系统错误率高,因为预测因素可能对于少数节点来说过于复杂,难以捕捉。节点和层数量过多则会导致对训练数据过拟合,泛化能力差。

没有硬性规定来决定层数和节点的数量。

隐藏层数量主要取决于任务的复杂性。非常复杂的任务,如大规模图像分类或语音识别,通常需要具有数十层和大量训练数据的网络。对于大多数问题,我们可以从只有一个或两个隐藏层开始,然后逐渐增加隐藏层数量,直到开始过拟合训练集。

隐藏节点的数量应与输入和输出节点的数量、可用的训练数据量以及正在建模的函数复杂性有关。作为经验法则,每层隐藏节点的数量应该在输入层大小和输出层大小之间,理想情况下应接近平均值。每层隐藏节点的数量不应超过输入节点数量的两倍,以避免过拟合。

学习率

当我们训练 ANN 时,我们使用前向传播和反向传播的多次迭代来优化权重。在每次迭代中,我们计算损失函数对每个权重的导数,并从该权重中减去。学习率决定了我们希望更新权重值的速度。学习率应该足够高,以便在合理的时间内收敛。但它应该足够低,以便找到损失函数的最小值。

激活函数

激活函数(如在图 3-1 中所示)指的是 ANN 中用于获取期望输出的加权输入的函数。激活函数允许网络以更复杂的方式组合输入,并在建模关系和生成输出方面提供更丰富的能力。它们决定哪些神经元将被激活,即传递给更深层的信息。

没有激活函数,ANN 将失去其表示学习能力的大部分功能。有几种激活函数。最广泛使用的如下:

线性(恒等)函数

由直线方程表示(即,f ( x ) = m x + c ),其中激活与输入成正比。如果我们有多个层,并且所有层都是线性的,那么最后一层的激活函数与第一层的线性函数相同。线性函数的范围是 –inf+inf

Sigmoid 函数

指的是作为 S 形图像投射的函数(如图 3-4 所示)。其数学方程为 f ( x ) = 1 / ( 1 + e x ) ,范围从 0 到 1。大正输入导致大正输出;大负输入导致大负输出。它也称为 logistic 激活函数。

双曲正切函数

与上述 sigmoid 激活函数类似,具有数学方程 T a n h ( x ) = 2 S i g m o i d ( 2 x ) 1 ,其中 Sigmoid 表示上述讨论的 sigmoid 函数。此函数的输出范围为 –1 到 1,在零轴两侧具有相等的质量,如图 3-4 所示。

ReLU 函数

ReLU 代表修正线性单元,表示为 f ( x ) = m a x ( x , 0 ) 。因此,如果输入是正数,则函数返回该数本身,如果输入是负数,则函数返回零。由于其简单性,它是最常用的函数。

图 3-4 显示了本节讨论的激活函数的总结。

mlbf 0304

图 3-4. 激活函数

激活函数的选择没有硬性规定。决策完全依赖于问题的性质和建模的关系。我们可以尝试不同的激活函数,并选择帮助提供更快收敛和更高效训练过程的激活函数。输出层的激活函数的选择在很大程度上受到建模问题类型的限制。⁴

成本函数

成本函数(也称为损失函数)是 ANN 性能的一种度量,用于衡量 ANN 对经验数据的拟合程度。最常见的两种成本函数是:

均方误差(MSE)

这是主要用于回归问题的损失函数,其中输出是连续值。MSE 被定义为预测值与实际观察值之间差异的平方的平均值。在第四章中进一步描述了 MSE。

交叉熵(或log loss

这个成本函数主要用于分类问题,其中输出是介于零和一之间的概率值。交叉熵损失随着预测概率与实际标签的偏差增大而增加。一个完美的模型的交叉熵为零。

优化器

优化器更新权重参数以最小化损失函数。⁵ 成本函数作为地形的指南,告诉优化器是否朝着达到全局最小值的正确方向移动。以下是一些常见的优化器:

动量

动量优化器除了当前步骤外,还查看先前的梯度。如果先前的更新和当前更新将权重移动到相同方向(增加动量),则会采取较大步长。如果梯度方向相反,则采取较小步长。可以将这种情况巧妙地想象成一个球在山谷中滚动——它在接近山谷底部时会获得动量。

AdaGrad(自适应梯度算法)

AdaGrad根据参数调整学习率,对于频繁出现的特征,执行较小的更新,并对于不频繁出现的特征执行较大的更新。

RMSProp

RMSProp代表 Root Mean Square Propagation。在 RMSProp 中,学习率会自动调整,并为每个参数选择不同的学习率。

Adam(自适应矩估计)

Adam结合了 AdaGrad 和 RMSProp 算法的最佳特性,提供了一种优化方式,是最流行的梯度下降优化算法之一。

Epoch

将整个训练数据集更新一轮称为epoch。根据数据大小和计算约束,网络可能会训练数十次、数百次,甚至数千次epoch

Batch size

Batch size 是一次前向/反向传递中的训练样本数量。批量大小为 32 意味着在更新模型权重之前,将使用来自训练数据集的 32 个样本来估计误差梯度。批量大小越大,所需的内存空间就越多。

在 Python 中创建人工神经网络模型

在第二章中,我们讨论了在 Python 中进行端到端模型开发的步骤。在本节中,我们深入探讨了在 Python 中构建基于 ANN 的模型所涉及的步骤。

我们的第一步将是查看 Keras,这是专为人工神经网络(ANN)和深度学习而构建的 Python 软件包。

安装 Keras 和机器学习包

有几个 Python 库可以轻松快速地构建 ANN 和深度学习模型,而无需深入了解底层算法的细节。Keras 是最用户友好的软件包之一,可以有效进行与 ANN 相关的数值计算。使用 Keras,可以在几行代码中定义和实现复杂的深度学习模型。我们将主要使用 Keras 软件包来实现本书的几个案例研究中的深度学习模型。

Keras只是TensorFlowTheano等更复杂的数值计算引擎的一个包装器。要安装 Keras,必须先安装 TensorFlow 或 Theano。

本节描述了在 Keras 中定义和编译基于 ANN 的模型的步骤,重点放在以下步骤上。⁶

导入包

在您开始构建 ANN 模型之前,您需要从 Keras 软件包中导入两个模块:SequentialDense

from Keras.models import Sequential
from Keras.layers import Dense
import numpy as np

载入数据

此示例使用 NumPy 的random模块快速生成一些数据和标签,供我们在下一步中构建的 ANN 使用。具体来说,首先构建一个大小为(1000,10)的数组。接下来,我们创建一个包含零和一的标签数组,大小为(1000,1)

data = np.random.random((1000,10))
Y = np.random.randint(2,size= (1000,1))
model = Sequential()

模型构建-定义神经网络架构

快速入门的一种方式是使用 Keras 的 Sequential 模型,它是层的线性堆栈。我们创建一个 Sequential 模型,并一次添加一个层,直到网络拓扑结构最终确定。正确的第一步是确保输入层具有正确数量的输入。我们可以在创建第一层时指定这一点。然后,我们选择一个密集或全连接层,通过使用参数input_dim来指示我们正在处理一个输入层。

我们使用add()函数向模型添加一层,并指定每层中的节点数。最后,另一个密集层被添加为输出层。

图 3-5 中显示的模型架构如下:

  • 该模型期望具有 10 个变量的数据行(input_dim_=10参数)。

  • 第一个隐藏层有 32 个节点,并使用relu激活函数。

  • 第二个隐藏层有 32 个节点,并使用relu激活函数。

  • 输出层有一个节点,并使用sigmoid激活函数。

mlbf 0305

图 3-5. ANN 架构

图中 3-5 中的网络的 Python 代码如下所示:

model = Sequential()
model.add(Dense(32, input_dim=10, activation= 'relu' ))
model.add(Dense(32, activation= 'relu' ))
model.add(Dense(1, activation= 'sigmoid'))

编译模型

模型构建完毕后,可以通过compile()函数进行编译。编译模型利用 Theano 或 TensorFlow 软件包中的高效数值库。在编译时,重要的是指定训练网络时所需的附加属性。训练网络意味着找到一组最佳权重以对所面临的问题进行预测。因此,我们必须指定用于评估一组权重的损失函数,用于搜索网络不同权重的优化器,并在训练过程中收集和报告任何可选的度量标准。

在下面的示例中,我们使用了cross-entropy损失函数,在 Keras 中被定义为binary_crossentropy。我们还将使用 adam 优化器,这是默认选项。最后,因为这是一个分类问题,我们将收集并报告分类准确性作为度量标准。⁷以下是 Python 代码:

model.compile(loss= 'binary_crossentropy' , optimizer= 'adam' , \
  metrics=[ 'accuracy' ])

适配模型

定义并编译了我们的模型后,现在是在数据上执行它的时候了。我们可以通过在模型上调用fit()函数来训练或适配我们加载的数据。

训练过程将在数据集上通过固定次数的迭代(epochs)运行,使用nb_epoch参数进行指定。我们还可以设置在网络执行权重更新之前评估的实例数。这是通过batch_size参数设置的。对于这个问题,我们将运行少量 epochs(10),并使用相对较小的批量大小为 32。同样,这些可以通过试验和错误选择。以下是 Python 代码:

model.fit(data, Y, nb_epoch=10, batch_size=32)

评估模型

我们已经在整个数据集上训练了我们的神经网络,并且可以评估网络在同一数据集上的性能。这将使我们了解我们对数据集建模的效果(例如,训练准确性),但不会提供有关算法在新数据上表现如何的洞察。为此,我们将数据分为训练和测试数据集。使用evaluation()函数在训练数据集上评估模型。这将为每个输入和输出对生成预测,并收集分数,包括平均损失和配置的任何指标,如准确性。以下是 Python 代码:

scores = model.evaluate(X_test, Y_test)
print("%s: %.2f%%" % (model.metrics_names[1], scores[1]*100))

加速运行 ANN 模型:GPU 和云服务

对于训练 ANNs(特别是具有许多层的深度神经网络),需要大量的计算能力。可用的 CPU,或称为中央处理单元,在本地机器上负责处理和执行指令。由于 CPU 在核心数量上受限,并且顺序地执行作业,它们无法快速执行训练深度学习模型所需的大量矩阵计算。因此,在 CPU 上训练深度学习模型可能非常慢。

对于通常需要大量时间在 CPU 上运行的 ANNs,以下备选方案非常有用:

  • 在本地 GPU 上运行笔记本。

  • 在 Kaggle Kernels 或 Google Colaboratory 上运行笔记本。

  • 使用亚马逊网络服务。

GPU

GPU 由数百个核心组成,可以同时处理数千个线程。使用 GPU 可以加速运行 ANN 和深度学习模型。

GPU 特别擅长处理复杂的矩阵运算。GPU 核心高度专业化,并通过将处理从 CPU 转移到 GPU 子系统中的核心来大幅加速深度学习训练等过程。

所有与机器学习相关的 Python 包,包括 Tensorflow、Theano 和 Keras,都可以配置为使用 GPU。

云服务,如 Kaggle 和 Google Colab

如果您有启用 GPU 的计算机,可以在本地运行 ANNs。如果没有,我们建议您使用 Kaggle Kernels、Google Colab 或 AWS 等服务:

Kaggle

由 Google 拥有的一个流行的数据科学网站,托管 Jupyter 服务,也称为Kaggle Kernels。Kaggle Kernels 可免费使用,并预先安装了最常用的包。您可以将内核连接到托管在 Kaggle 上的任何数据集,或者您也可以随时上传新的数据集。

Google Colaboratory

由 Google 提供的免费 Jupyter Notebook 环境,您可以使用免费的 GPU。Google Colaboratory的功能与 Kaggle 相同。

亚马逊网络服务(AWS)

AWS 深度学习提供了一个基础设施,可以在云中加速深度学习,无论规模大小。您可以快速启动预先安装了流行深度学习框架和接口的 AWS 服务器实例,用于训练复杂的自定义 AI 模型,尝试新算法,或学习新技能和技术。这些 Web 服务器可以比 Kaggle Kernels 运行更长时间。因此,对于大型项目,使用 AWS 而不是内核可能更值得。

章节总结

ANNs 是一类用于各种类型的机器学习的算法。这些模型受生物神经网络启发,包含构成动物大脑的神经元和神经元层。具有许多层的 ANN 称为深度神经网络。训练这些 ANN 需要几个步骤,包括前向传播和反向传播。诸如 Keras 之类的 Python 包可以让这些 ANN 的训练在几行代码内完成。训练这些深度神经网络需要更多的计算能力,仅靠 CPU 可能不够。备选方案包括使用 GPU 或云服务,如 Kaggle Kernels、Google Colaboratory 或 Amazon Web Services 来训练深度神经网络。

下一步

作为下一步,我们将深入探讨监督学习的机器学习概念的详细内容,然后进行使用本章涵盖的概念的案例研究。

¹ 读者被鼓励参考由 Aaron Courville、Ian Goodfellow 和 Yoshua Bengio(MIT Press)合著的书籍《深度学习》,以获取有关 ANN 和深度学习的更多细节。

² 激活函数将在本章后面详细描述。

³ 下一节讨论了许多可用的损失函数。我们问题的性质决定了我们对损失函数的选择。

⁴ 通过改变输出层的激活函数来导出回归或分类输出在第四章中进一步描述。

⁵ 有关优化的更多细节,请参阅https://oreil.ly/FSt-8

⁶ 使用 Keras 实现深度学习模型的步骤和 Python 代码,如本节所示,在后续章节中的几个案例研究中使用。

⁷ 分类模型的评估指标的详细讨论在第四章中呈现。

第二部分:监督学习

第四章:监督学习:模型与概念

监督学习是机器学习的一个领域,选择的算法试图使用给定的输入来拟合目标。算法提供了包含标签的训练数据集。基于大量数据,算法将学习一条规则,用于预测新观察值的标签。换句话说,监督学习算法基于历史数据,并试图找到具有最佳预测能力的关系。

有两种类型的监督学习算法:回归算法和分类算法。基于回归的监督学习方法试图根据输入变量预测输出。基于分类的监督学习方法确定数据项属于哪个类别。分类算法是基于概率的,意味着算法找到数据集属于哪个类别的概率最高,就会输出该类别。相反,回归算法估计具有无限解(可能结果的连续集)的问题的结果。

在金融领域,监督学习模型代表了最常用的机器学习模型之一。许多在算法交易中广泛应用的算法依赖于监督学习模型,因为它们可以高效训练,相对稳健地处理嘈杂的金融数据,并且与金融理论有着密切联系。

学术界和行业研究人员已经利用基于回归的算法开发了许多资产定价模型。这些模型用于预测各种时间段的回报,并识别影响资产回报的重要因素。在投资组合管理和衍生品定价中还有许多其他基于回归的监督学习的用例。

另一方面,基于分类的算法已经在金融领域的许多领域中得到应用,这些领域需要预测分类响应。这些包括欺诈检测、违约预测、信用评分、资产价格运动方向的预测以及买入/卖出建议。在投资组合管理和算法交易中还有许多其他基于分类的监督学习的用例。

在第五章和第六章中介绍了基于回归和分类的监督学习的许多用例。

Python 及其库提供了在几行代码中实现这些监督学习模型的方法和方式。这些库中的一些在第二章中有详细介绍。借助易于使用的机器学习库,如 Scikit-learn 和 Keras,可以简单地对给定的预测建模数据集拟合不同的机器学习模型。

在本章中,我们提供了监督学习模型的高级概述。有关这些主题的全面覆盖,请参阅 Aurélien Géron 的《使用 Scikit-Learn、Keras 和 TensorFlow 进行实战机器学习》,第 2 版(O'Reilly)。

监督学习模型:概述

分类预测建模问题与回归预测建模问题有所不同,因为分类是预测离散类别标签的任务,而回归是预测连续量的任务。然而,两者共享利用已知变量进行预测的概念,并且在两种模型之间存在显著的重叠。因此,分类和回归模型在本章中一起介绍。图 4-1 总结了用于分类和回归的常用模型列表。

一些模型可以通过小的修改同时用于分类和回归。这些模型包括 K 近邻、决策树、支持向量机、集成装袋/提升方法以及人工神经网络(包括深度神经网络),如图 4-1 所示。然而,一些模型,如线性回归和逻辑回归,不能(或者不容易)同时用于两种问题类型。

mlbf 0401

图 4-1. 回归和分类模型

本节包含以下有关模型的详细信息:

  • 模型理论。

  • 在 Scikit-learn 或 Keras 中的实现。

  • 不同模型的网格搜索。

  • 模型的优缺点。

注意

在金融领域,重点放在从先前观察到的数据中提取信号以预测同一时间序列的未来值的模型上。这类时间序列模型预测连续输出,并且更符合监督回归模型的特性。时间序列模型在监督回归章节中单独进行讨论(第五章)。

线性回归(普通最小二乘法)

线性回归(普通最小二乘回归或 OLS 回归)或许是统计学和机器学习中最为知名且理解最透彻的算法之一。线性回归是一个线性模型,例如,它假设输入变量 (x) 和单一输出变量 (y) 之间存在线性关系。线性回归的目标是训练一个线性模型,以尽可能小的误差来预测新的 y 给定先前未见的 x

我们的模型将是一个函数,其预测 y 给定 x 1 , x 2 . . . x i

y = β 0 + β 1 x 1 + . . . + β i x i

其中,β 0 被称为截距,β 1 . . . β i 是回归系数。

Python 中的实现

from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X, Y)

在接下来的部分中,我们涵盖了线性回归模型的训练和模型的网格搜索。然而,总体概念和相关方法适用于所有其他监督学习模型。

训练模型

正如我们在第三章中提到的,训练模型基本上意味着通过最小化成本(损失)函数来检索模型参数。训练线性回归模型的两个步骤是:

定义成本函数(或损失函数)

衡量模型预测的不准确性。如方程 4-1 中定义的残差平方和(RSS),衡量实际值与预测值之间差异的平方和,是线性回归的成本函数。

方程 4-1. 残差平方和

R S S = i=1 n y i β 0 j=1 n β j x ij 2

在这个方程中,β 0 是截距;β j 代表系数;β 1 , . . , β j 是回归的系数;x ij 表示第i th观测和第j th变量。

找到最小化损失的参数

例如,使我们的模型尽可能准确。在二维图中,这会导致最佳拟合线,如图 4-2 所示。在更高维度中,我们将会有更高维的超平面。从数学角度来看,我们关注每个真实数据点(y)与我们模型预测(ŷ)之间的差异。平方这些差异以避免负数并惩罚较大的差异,然后将它们相加并取平均值。这衡量了我们的数据与拟合线的拟合程度。

mlbf 0402

图 4-2. 线性回归

网格搜索

网格搜索的总体思想是创建所有可能的超参数组合的网格,并使用每个组合来训练模型。超参数是模型的外部特征,可以被视为模型的设置,不像模型参数那样基于数据估计。这些超参数在网格搜索过程中被调整以实现更好的模型性能。

由于其穷举搜索,网格搜索保证在网格内找到最优参数。缺点是随着参数或考虑值的增加,网格的大小呈指数增长。

sklearn 软件包的model_selection模块中的GridSearchCV类有助于系统地评估我们希望测试的所有超参数值的组合。

第一步是创建一个模型对象。然后我们定义一个字典,其中关键字命名超参数,值列表显示要测试的参数设置。对于线性回归,超参数是fit_intercept,它是一个布尔变量,确定是否计算此模型的截距。如果设置为False,计算中将不使用截距:

model = LinearRegression()
param_grid = {'fit_intercept': [True, False]}
}

第二步是实例化GridSearchCV对象,并提供评估器对象和参数网格,以及评分方法和交叉验证选择给初始化方法。交叉验证是一种用于评估机器学习模型的重采样过程,评分参数是模型的评估指标:¹

在所有设置就位后,我们可以拟合GridSearchCV

grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring= 'r2', \
  cv=kfold)
grid_result = grid.fit(X, Y)

优点和缺点

在优点方面,线性回归易于理解和解释。然而,当预测变量与预测变量之间存在非线性关系时,它可能效果不佳。线性回归容易出现过拟合(我们将在下一节讨论)问题,而且当存在大量特征时,可能无法很好地处理不相关的特征。线性回归还要求数据遵循某些假设,如不存在多重共线性。如果假设不成立,则无法信任所得到的结果。

正则化回归

当线性回归模型包含许多自变量时,它们的系数将难以确定,模型将倾向于非常适合训练数据(用于构建模型的数据),但对测试数据(用于测试模型好坏的数据)适配不佳。这被称为过拟合或高方差。

控制过拟合的一种流行技术是正则化,它涉及向误差或损失函数添加一个惩罚项,以防止系数达到较大值。简单来说,正则化是一种惩罚机制,通过收缩模型参数(使其接近于零)来建立预测精度更高且易于解释的模型。正则化回归比线性回归有两个优点:

预测精度

模型在测试数据上的表现更好表明,模型试图从训练数据中概括。具有过多参数的模型可能尝试拟合特定于训练数据的噪声。通过收缩或将一些系数设为零,我们权衡了适合复杂模型(更高偏差)的能力,以换取更具泛化能力的模型(更低方差)。

解释

大量预测子可能会复杂化结果的解释或传达大局。为了限制模型仅包括对结果影响最大的一小部分参数,可能需要牺牲一些细节。

正则化线性回归模型的常见方法如下:

L1 正则化或 Lasso 回归

Lasso 回归 通过在线性回归的成本函数(RSS)中添加系数绝对值之和的因素(如 Equation 4-1 所述)执行 L1 正则化。Lasso 正则化的方程可以表示如下:

C o s t F u n c t i o n = R S S + λ * j=1 p β j

L1 正则化可以导致零系数(即某些特征在输出评估中被完全忽略)。λ 值越大,被收缩至零的特征越多。这可以完全消除一些特征,并给出预测子集,从而降低模型复杂度。因此,Lasso 回归不仅有助于减少过拟合,还可以帮助进行特征选择。未收缩至零的预测子集表明它们很重要,因此 L1 正则化允许进行特征选择(稀疏选择)。正则化参数( λ )可控制,lambda 值为零时产生基本线性回归方程。

可以使用 Python 的 sklearn 包中的 Lasso 类构建 Lasso 回归模型,如下所示的代码片段:

from sklearn.linear_model import Lasso
model = Lasso()
model.fit(X, Y)

L2 正则化或 Ridge 回归

Ridge 回归 通过在线性回归的成本函数(RSS)中添加系数平方和的因素执行 L2 正则化(如 Equation 4-1 所述)。Ridge 正则化的方程可以表示如下:

C o s t F u n c t i o n = R S S + λ * j=1 p β j 2

岭回归对系数施加了约束。惩罚项( λ )正则化系数,如果系数取大值,则优化函数受到惩罚。因此,岭回归会收缩系数并有助于降低模型复杂度。收缩系数会导致较低的方差和较低的误差值。因此,岭回归减少了模型的复杂度,但并不减少变量的数量;它只是缩小它们的影响。当 λ 接近零时,成本函数变得类似于线性回归成本函数。因此,对于特征的约束越低(低 λ ),模型越类似于线性回归模型。

可以使用 Python 的 sklearn 包中的 Ridge 类构建岭回归模型,如下面的代码片段所示:

from sklearn.linear_model import Ridge
model = Ridge()
model.fit(X, Y)

弹性网络

弹性网络 向模型添加了正则化项,这是 L1 和 L2 正则化的组合,如下方程所示:

C o s t F u n c t i o n = R S S + λ (1α) / 2 j=1 p β j 2 + α * j=1 p β j

除了设置和选择 λ 值外,弹性网还允许我们调整 alpha 参数,其中 α = 0 对应于岭回归,α = 1 对应于拉索。因此,我们可以选择一个介于 01 之间的 α 值来优化弹性网。这将有效地收缩一些系数并将一些系数设置为 0 以进行稀疏选择。

可以使用 Python 的 sklearn 包中的 ElasticNet 类构建弹性网络回归模型,如下面的代码片段所示:

from sklearn.linear_model import ElasticNet
model = ElasticNet()
model.fit(X, Y)

对于所有正则化回归,在 Python 的网格搜索期间调整的关键参数是 λ 。在弹性网中,α 可以是一个额外的可调参数。

逻辑回归

逻辑回归 是最广泛使用的分类算法之一。逻辑回归模型出现的原因是希望在 x 的线性函数中建模输出类的概率,同时确保输出概率总和为一,并且保持在零到一之间,这是我们从概率中期望的结果。

如果我们在几个示例上训练线性回归模型,其中 Y = 01,我们可能会预测出一些小于零或大于一的概率,这是不合理的。相反,我们使用逻辑回归模型(或 logit 模型),这是线性回归的修改版,通过应用 sigmoid 函数确保输出的概率在零到一之间。²

方程 4-2 展示了逻辑回归模型的方程式。类似于线性回归,输入值(x)通过权重或系数值线性组合以预测输出值(y)。从方程 4-2 得到的输出是一个概率,被转换成二进制值(01)以获取模型预测。

方程 4-2. 逻辑回归方程

y = exp(β 0 +β 1 x 1 +....+β i x 1 ) 1+exp(β 0 +β 1 x 1 +....+β i x 1 )

其中y是预测输出,β 0是偏置或截距项,而 B[1]是单个输入值(x)的系数。输入数据中的每一列都有一个关联的β系数(一个常数实数值),必须从训练数据中学习。

在逻辑回归中,成本函数基本上是衡量我们在真实答案为零时多少次预测为一,反之亦然。训练逻辑回归系数使用诸如最大似然估计(MLE)的技术,以预测接近1的默认类别值和接近0的其他类别值。³

可以使用 Python 的 sklearn 包的LogisticRegression类构建逻辑回归模型,如下代码片段所示:

from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
model.fit(X, Y)

超参数

正则化(penalty在 sklearn 中)

类似于线性回归,逻辑回归可以进行正则化,可以是L1L2elasticnet。在sklearn库中的取值为[l1, l2, elasticnet]。

正则化强度(C在 sklearn 中)

此参数控制正则化强度。想要的惩罚参数的好值可以是[100, 10, 1.0, 0.1, 0.01]

优缺点

就优点而言,逻辑回归模型易于实现,具有良好的解释性,并且在线性可分的类别上表现非常好。模型的输出是概率,提供更多的见解并可用于排名。该模型具有少量的超参数。虽然可能存在过拟合的风险,但可以通过类似于线性回归模型的L1/L2正则化来解决这个问题。

在缺点方面,当提供大量特征时,模型可能会过拟合。逻辑回归只能学习线性函数,并且不太适合处理特征与目标变量之间的复杂关系。此外,如果特征强相关,可能无法很好地处理无关特征。

支持向量机

支持向量机(SVM)算法的目标是最大化边界(如图 4-3 中的阴影区域所示),即分隔超平面(或决策边界)与最接近该超平面的训练样本之间的距离,即所谓的支持向量。边界计算为从线到仅最接近点的垂直距离,如图 4-3 所示。因此,SVM 计算出导致所有数据点均匀分区的最大间隔边界。

mlbf 0403

图 4-3. 支持向量机

在实践中,数据杂乱无章,并且不能使用超平面完美分离。必须放宽最大化分隔类别的线条的约束。此变更允许训练数据中的一些点违反分隔线。引入了一组额外的系数,这些系数在每个维度中提供间隙余地。引入了一个调整参数,简称为C,它定义了允许跨所有维度的摆动幅度。C 的值越大,允许的超平面违规就越多。

在某些情况下,无法找到超平面或线性决策边界,因此使用内核。内核只是输入数据的转换,允许 SVM 算法更轻松地处理数据。使用内核,原始数据被投影到更高的维度以更好地分类数据。

SVM 用于分类和回归。我们通过将原始优化问题转换为对偶问题来实现这一点。对于回归,技巧在于颠倒目标。在试图在两个类之间拟合尽可能大的街道同时限制边界违规时,SVM 回归试图在街道上(图 4-3 中阴影区域)尽可能多地拟合实例,同时限制边界违规。街道的宽度由超参数控制。

SVM 回归和分类模型可以使用 Python 的 sklearn 包构建,如下面的代码片段所示:

回归

from sklearn.svm import SVR
model = SVR()
model.fit(X, Y)

分类

from sklearn.svm import SVC
model = SVC()
model.fit(X, Y)

超参数

sklearn 实现的 SVM 中存在以下关键参数,并可在执行网格搜索时进行调整:

内核(sklearn 中的 kernel

内核的选择控制输入变量将被投影的方式。有许多内核可供选择,但线性RBF是最常见的。

惩罚(sklearn 中的 C

惩罚参数告诉 SVM 优化希望避免每个训练样本的错误分类程度。对于惩罚参数的大值,优化会选择一个较小间隔的超平面。良好的值可能在对数尺度从 10 到 1,000 之间。

优缺点

就优点而言,SVM 对过拟合相当鲁棒,特别是在高维空间中。它能够很好地处理非线性关系,提供多种核函数选择。此外,对数据没有分布要求。

就缺点而言,SVM 在训练时可能效率低且内存占用高,并且调整困难。它在处理大型数据集时表现不佳。它要求对数据进行特征缩放。还有许多超参数,它们的含义通常不直观。

K-最近邻算法

K-最近邻(KNN)被认为是一种“惰性学习器”,因为模型不需要学习。对于新数据点,预测是通过在整个训练集中搜索K个最相似的实例(邻居),并总结这些K个实例的输出变量来实现的。

为了确定训练数据集中与新输入最相似的K个实例,使用了一个距离度量。最流行的距离度量是欧几里得距离,其计算方法是在所有输入属性i上,点a和点b之间的平方差的平方根,表示为 d ( a , b ) = i=1 n (a i b i ) 2 。欧几里得距离在输入变量类型相似时是一种很好的距离度量。

另一种距离度量是曼哈顿距离,其中点a和点b之间的距离表示为 d ( a , b ) = i=1 n | a i b i | 。曼哈顿距离在输入变量类型不相似时是一种很好的度量。

KNN 的步骤可以总结如下:

  1. 选择K的数量和距离度量。

  2. 找到我们要分类的样本的K个最近邻居。

  3. 通过多数投票分配类别标签。

可以使用 Python 的 sklearn 包构建 KNN 回归和分类模型,如下所示:

分类

from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier()
model.fit(X, Y)

回归

from sklearn.neighbors import KNeighborsRegressor
model = KNeighborsRegressor()
model.fit(X, Y)

超参数

在 sklearn 实现的 KNN 中存在以下关键参数,可以在执行网格搜索时调整:

邻居数(sklearn 中的n_neighbors

KNN 最重要的超参数是邻居数(n_neighbors)。良好的值在 1 到 20 之间。

距离度量(在 sklearn 中称为metric

测试不同距离度量来选择邻域组成可能也会很有趣。良好的值为euclideanmanhattan

优缺点

在优点方面,无需训练,因此没有学习阶段。由于算法在进行预测之前不需要训练,因此可以轻松添加新数据而不影响算法的准确性。它直观且易于理解。该模型自然处理多类别分类,并能学习复杂的决策边界。如果训练数据量大,则 KNN 非常有效。它还对噪声数据具有鲁棒性,无需过滤异常值。

在缺点方面,选择距离度量并不明显,很难在许多情况下进行证明。KNN 在高维数据集上表现不佳。预测新实例的成本高且速度慢,因为必须重新计算到所有邻居的距离。KNN 对数据集中的噪声敏感。我们需要手动输入缺失值并移除异常值。此外,在应用 KNN 算法之前需要进行特征缩放(标准化和归一化),否则 KNN 可能会生成错误的预测。

线性判别分析

线性判别分析(LDA)算法的目标是以一种方式将数据投影到低维空间,使得类别的可分离性最大化,类内方差最小化。⁴

在训练 LDA 模型期间,会计算每个类别的统计特性(即均值和协方差矩阵)。这些统计特性是基于以下关于数据的假设进行估计的:

  • 数据是normally distributed,因此每个变量在绘制时都呈钟形曲线。

  • 每个属性具有相同的方差,并且每个变量的值平均围绕均值变化相同量。

要进行预测,LDA 估计新输入数据属于每个类别的概率。输出类别是具有最高概率的类别。

Python 中的实现及超参数

可以使用 Python 的 sklearn 包构建 LDA 分类模型,如下面的代码片段所示:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
model = LinearDiscriminantAnalysis()
model.fit(X, Y)

LDA 模型的关键超参数是number of components,用于降维,而在 sklearn 中表示为n_components

优缺点

在优点方面,LDA 是一个相对简单的模型,实现快速且易于实现。在缺点方面,它需要特征缩放并涉及复杂的矩阵操作。

分类和回归树

从最一般的角度来看,通过树构建算法进行分析的目的是确定一组if–then逻辑(分裂)条件,以便准确预测或分类案例。分类与回归树(或CART决策树分类器)是具有吸引力的模型,如果我们关心解释性的话。我们可以将这个模型视为分解数据并基于一系列问题做出决策的过程。这种算法是随机森林和梯度提升方法等集成方法的基础。

表示

该模型可以通过二叉树(或决策树)来表示,其中每个节点是一个输入变量 x,带有一个分割点,每个叶子包含用于预测的输出变量 y

图 4-4 展示了一个简单分类树的示例,根据身高(以厘米为单位)和体重(以公斤为单位)两个输入预测一个人是男性还是女性。

mlbf 0404

图 4-4. 分类与回归树示例

学习 CART 模型

创建一个二叉树实际上是一个将输入空间分割的过程。采用一种称为递归二元分割的贪婪方法来分割空间。这是一个数值过程,在此过程中,所有值都被排列,并尝试使用成本(损失)函数测试不同的分割点。选择具有最佳成本(因为我们最小化成本)的分割点。所有输入变量和所有可能的分割点都以贪婪的方式进行评估和选择(例如,每次都选择最佳分割点)。

对于回归预测建模问题,用于选择分裂点的成本函数是在所有落入矩形内的训练样本上最小化的平方误差和

i=1 n (y i prediction i ) 2

其中y i是训练样本的输出,prediction 是矩形的预测输出。对于分类问题,使用基尼成本函数;它提供了叶子节点纯度的指示(即分配给每个节点的训练数据的混合程度),并定义为:

G = i=1 n p k * ( 1 p k )

其中G是兴趣区域矩形中所有类的基尼成本,p k是具有类k的训练实例数量。具有完全类纯度(完美类纯度)的节点将具有G = 0,而在二元分类问题中具有50-50类分布(最差纯度)的节点将具有G = 0.5

停止准则

在前面的部分描述的递归二元分裂过程中,需要知道何时停止分裂,因为它在训练数据中沿着树的路径工作。最常见的停止程序是在每个叶节点分配的训练实例数达到最小值时停止。如果计数少于某个最小值,则不接受分裂,并将节点视为最终叶节点。

修剪树

停止准则非常重要,因为它强烈影响树的性能。学习树后可以使用修剪来进一步提升性能。决策树的复杂性定义为树中的分裂数。更简单的树更受欢迎,因为它们运行更快,易于理解,在处理和存储过程中消耗更少的内存,并且不太可能过度拟合数据。最快和最简单的修剪方法是通过测试集逐个处理树中的每个叶节点,并评估移除它的效果。仅当这样做会导致整个测试集上成本函数的下降时才移除叶节点。当不能进一步改善时,可以停止删除节点。

Python 实现

使用 Python 的 sklearn 包可以构建 CART 回归和分类模型,如下面的代码片段所示:

分类

from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier()
model.fit(X, Y)

回归

from sklearn.tree import DecisionTreeRegressor
model = DecisionTreeRegressor ()
model.fit(X, Y)

超参数

CART 有许多超参数。然而,关键的超参数是树模型的最大深度,这是降维的组件数量,用max_depth在 sklearn 包中表示。好的取值范围可以从230,具体取决于数据中的特征数量。

优缺点

在优点方面,CART 易于解释并能够适应学习复杂关系。它需要很少的数据准备工作,通常不需要缩放数据。由于决策节点的构建方式,特征重要性是内置的。它在大型数据集上表现良好。它适用于回归和分类问题。

在缺点方面,除非使用修剪,否则 CART 易于过拟合。它可能非常不稳健,意味着训练数据集的小变化可能导致所学习的假设函数存在较大差异。通常情况下,CART 的性能不如下面将要讨论的集成模型。

集成模型

集成模型的目标是将不同的分类器组合成一个元分类器,其泛化性能优于单个分类器。例如,假设我们从 10 位专家收集了预测结果,集成方法允许我们策略性地结合他们的预测,得到比专家个体预测更准确和更稳健的预测。

最流行的两种集成方法是装袋(bagging)和提升(boosting)。装袋(或自举聚合)是一种并行训练多个独立模型的集成技术。每个模型由数据的随机子集训练。提升则是一种串行训练多个独立模型的集成技术。通过从训练数据构建一个模型,然后创建第二个模型来纠正第一个模型的错误。模型逐步添加,直到训练集被完美预测或者达到最大模型数量。每个独立模型从前一个模型的错误中学习。就像决策树本身一样,装袋和提升可用于分类和回归问题。

通过结合独立模型,集成模型更加灵活(偏差较小)且对数据不敏感(方差较小)。⁵ 集成方法结合多个简单算法以获得更好的性能。

在本节中,我们将涵盖随机森林、AdaBoost、梯度提升方法和额外树,以及它们在 sklearn 包中的实现。

随机森林

随机森林是装袋决策树的调整版本。为了理解随机森林算法,首先了解装袋算法。假设我们有一个包含一千个实例的数据集,装袋的步骤如下:

  1. 创建许多(例如一百个)数据集的随机子样本。

  2. 对每个样本训练一个 CART 模型。

  3. 给定一个新数据集,计算每个模型的平均预测,并通过每棵树的预测结果进行多数投票来确定最终标签。

像 CART 这样的决策树存在一个问题,即它们是贪婪的。它们通过最小化误差的贪婪算法选择分裂变量。即使在装袋之后,决策树可能具有很多结构相似性,并导致其预测高度相关。如果子模型的预测相互不相关,或者最好是弱相关,那么从多个模型的预测中组合预测将效果更好。随机森林通过改变学习算法的方式,使得所有子树的预测结果相关性更低。

在 CART 中,选择分裂点时,学习算法允许查看所有变量和所有变量值,以选择最优的分裂点。随机森林算法改变了这个过程,使得每个子树在选择分裂点时只能访问一部分随机抽取的特征。在算法中必须指定一个参数来表示每个分裂点可以搜索的特征数量(m)。

当构建装袋决策树时,我们可以计算每个分裂点的变量的误差函数降低量。在回归问题中,这可能是总平方误差的减少量,在分类问题中,可能是基尼成本。装袋方法可以通过计算并平均单个变量的误差函数降低量来提供特征重要性。

Python 实现

可使用 Python 的 sklearn 包构建随机森林回归和分类模型,如下所示的代码:

分类

from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
model.fit(X, Y)

回归

from sklearn.ensemble import RandomForestRegressor
model = RandomForestRegressor()
model.fit(X, Y)

超参数

sklearn 实现的随机森林中存在一些主要的超参数,可以在执行网格搜索时进行调整。

最大特征数(max_features在 sklearn 中)

这是最重要的参数。它是在每个分裂点随机抽取的特征数量。您可以尝试一系列整数值,例如从 1 到 20,或从 1 到输入特征数量的一半。

树的数量(n_estimators在 sklearn 中)

此参数代表树的数量。理想情况下,应该增加此数量,直到模型不再显示进一步改善为止。良好的值可能在对数尺度从 10 到 1,000 之间。

优缺点

随机森林算法(或模型)由于其良好的性能、可扩展性和易用性,在过去十年中在机器学习应用中获得了巨大的流行。它灵活,并自然地分配特征重要性分数,因此可以处理冗余的特征列。它适用于大型数据集,并且通常对过拟合具有较强的鲁棒性。该算法不需要对数据进行缩放,并且可以建模非线性关系。

在缺点方面,随机森林可能感觉像一个黑盒方法,因为我们对模型的操作非常有限,结果可能难以解释。虽然随机森林在分类方面表现良好,但对于回归问题可能不太适用,因为它无法给出精确的连续性预测。在回归情况下,它不会预测超出训练数据范围,并可能在特别嘈杂的数据集上过拟合。

极端随机树(Extra trees)

Extra trees,又称极端随机树,是随机森林的一个变种;它构建多棵树,并使用特征的随机子集来分裂节点,类似于随机森林。然而,与随机森林不同的是,在 extra trees 中,观测样本是不重复抽取的。因此,观测样本不会重复出现。

此外,随机森林选择最佳分裂点将父节点转换为两个最同质的子节点。⁶ 然而,extra trees 选择一个随机分裂来将父节点分割为两个随机子节点。在 extra trees 中,随机性不是来自于数据的自助抽样,而是来自于所有观测样本的随机分割。

在实际案例中,性能与普通随机森林可比,有时稍好。额外树的优缺点与随机森林类似。

Python 实现

可以使用 Python 的 sklearn 包构建 Extra trees 的回归和分类模型,如下面的代码片段所示。Extra trees 的超参数与随机森林相似,如前一节所示:

分类

from sklearn.ensemble import ExtraTreesClassifier
model = ExtraTreesClassifier()
model.fit(X, Y)

回归

from sklearn.ensemble import ExtraTreesRegressor
model = ExtraTreesRegressor()
model.fit(X, Y)

自适应增强(AdaBoost)

自适应增强AdaBoost是一种提升技术,其基本思想是依次尝试预测器,每个后续模型试图修正其前任的错误。每次迭代中,AdaBoost 算法通过修改附加到每个实例的权重来改变样本分布。它增加错误预测实例的权重,减少正确预测实例的权重。

AdaBoost 算法的步骤如下:

  1. 最初,所有观测样本被赋予相等的权重。

  2. 模型建立在数据子集上,使用该模型对整个数据集进行预测。通过比较预测值和实际值来计算错误。

  3. 在创建下一个模型时,对预测错误的数据点赋予更高的权重。可以使用错误值确定权重。例如,错误越大,分配给观察值的权重越大。

  4. 该过程重复进行,直到错误函数不再改变,或者达到最大估计器数量的限制。

Python 实现

可以使用 Python 的 sklearn 包构建 AdaBoost 的回归和分类模型,如下面的代码片段所示:

分类

from sklearn.ensemble import AdaBoostClassifier
model = AdaBoostClassifier()
model.fit(X, Y)

回归

from sklearn.ensemble import AdaBoostRegressor
model = AdaBoostRegressor()
model.fit(X, Y)

超参数

sklearn 实现的 AdaBoost 中的一些主要超参数,在执行网格搜索时可以进行调整,包括以下内容:

学习率 (learning_rate 在 sklearn 中)

学习率缩小每个分类器/回归器的贡献。可以考虑在对数尺度上。网格搜索的样本值可以是 0.001、0.01 和 0.1。

估计器数量 (n_estimators 在 sklearn 中)

此参数表示树的数量。理想情况下,应该增加到在模型中不再看到进一步改进的情况下。良好的值可能是从 10 到 1,000 的对数尺度。

优势和劣势

在优势方面,AdaBoost 具有较高的精度。AdaBoost 可以在几乎不调整参数或设置的情况下达到与其他模型类似的结果。该算法不需要数据进行缩放,并且可以建模非线性关系。

在劣势方面,AdaBoost 的训练时间较长。AdaBoost 对噪声数据和异常值敏感,并且数据不平衡导致分类精度降低。

梯度提升方法

梯度提升方法(GBM)是另一种类似于 AdaBoost 的提升技术,其一般思想是顺序地尝试预测器。梯度提升通过将前一步骤中未拟合的预测逐步添加到集成中来工作,确保先前的错误得到纠正。

梯度提升算法的步骤如下:

  1. 在一个数据子集上构建模型(可以称为第一个弱学习器)。使用该模型,在整个数据集上进行预测。

  2. 通过比较预测值和实际值计算错误,并使用损失函数计算损失。

  3. 使用前一步骤的错误作为目标变量创建一个新模型。其目标是找到数据中的最佳分割以最小化误差。该新模型的预测值与前一模型的预测值相结合。使用此预测值和实际值计算新的错误。

  4. 直到错误函数不再变化或达到最大估计器数的限制为止,重复此过程。

与 AdaBoost 相反,后者在每次交互中调整实例权重,该方法试图将新的预测器拟合到前一个预测器产生的残差错误中。

Python 中的实现和超参数

使用 Python 的 sklearn 包可以构建梯度提升方法的回归和分类模型,如下面的代码片段所示。梯度提升方法的超参数与 AdaBoost 相似,如前一节所示:

分类

from sklearn.ensemble import GradientBoostingClassifier
model = GradientBoostingClassifier()
model.fit(X, Y)

回归

from sklearn.ensemble import GradientBoostingRegressor
model = GradientBoostingRegressor()
model.fit(X, Y)

优势和劣势

在优势方面,梯度提升方法对于缺失数据、高度相关的特征和无关特征具有鲁棒性,与随机森林相似。它自然地分配特征重要性分数,稍微优于随机森林的性能。该算法不需要数据进行缩放,并且可以建模非线性关系。

在劣势方面,可能比随机森林更容易过拟合,因为提升方法的主要目的是减少偏差而不是方差。它有许多超参数需要调整,因此模型开发可能不那么迅速。此外,特征重要性可能对训练数据集的变化不太稳健。

基于 ANN 的模型

在第三章中,我们讨论了 ANN 的基础知识,以及 ANN 的架构及其在 Python 中的训练和实现。该章提供的细节适用于机器学习的所有领域,包括监督学习。然而,从监督学习的角度来看,还有一些额外的细节,我们将在本节中介绍。

神经网络可以通过输出层节点的激活函数减少到分类或回归模型。在回归问题中,输出节点具有线性激活函数(或无激活函数)。线性函数产生从-inf+inf的连续输出。因此,输出层将是前一层节点的线性函数,并且它将是基于回归的模型。

在分类问题中,输出节点具有 sigmoid 或 softmax 激活函数。sigmoid 或 softmax 函数产生一个从零到一的输出,表示目标值的概率。softmax 函数还可以用于多组分类。

使用 sklearn 的 ANN

可以使用 Python 的 sklearn 包构建 ANN 回归和分类模型,如下面的代码片段所示:

分类

from sklearn.neural_network import MLPClassifier
model = MLPClassifier()
model.fit(X, Y)

回归

from sklearn.neural_network import MLPRegressor
model = MLPRegressor()
model.fit(X, Y)

超参数

正如我们在第三章中看到的,ANN 具有许多超参数。在 sklearn 的 ANN 实现中存在的一些超参数,在执行网格搜索时可以调整:

隐藏层(sklearn 中的hidden_layer_sizes

它代表 ANN 架构中的层数和节点数。在 sklearn 的 ANN 实现中,第 i 个元素表示第 i 个隐藏层中的神经元数。在 sklearn 实现的网格搜索中,用于示例值的样本值可以是[(20,), (50,), (20, 20), (20, 30, 20)]。

激活函数(sklearn 中的activation

它代表隐藏层的激活函数。一些在第三章中定义的激活函数,如sigmoidrelutanh,可以使用。

深度神经网络

具有多个隐藏层的人工神经网络通常被称为深度网络。我们喜欢使用库 Keras 来实现这样的网络,因为这个库非常灵活。详细介绍了在 Keras 中实现深度神经网络的具体实现。类似于MLPClassifierMLPRegressor在 sklearn 中用于分类和回归,Keras 还有名为KerasClassifierKerasRegressor的模块,可用于创建具有深度网络的分类和回归模型。

金融领域中一个流行的问题是时间序列预测,即基于历史概述预测时间序列的下一个值。一些深度神经网络,如循环神经网络(RNN),可以直接用于时间序列预测。该方法的详细信息在第五章中提供。

优缺点

人工神经网络(ANN)的主要优势在于它相当好地捕捉了变量之间的非线性关系。ANN 可以更轻松地学习丰富的表示,并且在大数据集和大量输入特征的情况下表现良好。ANN 在使用方式上非常灵活。这一点可以从其在机器学习和人工智能中广泛应用的各种领域中看出,包括强化学习和自然语言处理,正如第三章所讨论的那样。

ANN 的主要缺点是模型的可解释性,这是一个常常不能忽视的缺点,有时在选择模型时是决定性因素。ANN 在处理小数据集方面表现不佳,需要大量的调整和猜测。选择正确的拓扑结构/算法来解决问题是困难的。此外,ANN 在计算上很昂贵,训练所需时间较长。

在金融监督学习中使用人工神经网络

如果一个简单的模型(如线性或逻辑回归)完全适合您的问题,那么不要考虑使用 ANN。然而,如果您正在建模复杂的数据集并感觉需要更好的预测能力,那么试试 ANN 吧。ANN 是最灵活的模型之一,能够自适应数据的形状,在监督学习问题中使用它可能是一个有趣且有价值的练习。

模型性能

在前一节中,我们讨论了网格搜索作为寻找正确超参数以获得更好性能的方法。在本节中,我们将扩展该过程,讨论评估模型性能的关键组成部分,即过拟合、交叉验证和评估指标。

过拟合和欠拟合

机器学习中常见的问题是过拟合,它被定义为学习一个完美解释模型从中学到的训练数据的函数,但对未见过的测试数据泛化效果不佳。过拟合发生在模型从训练数据中过度学习,以至于开始捕捉到不代表真实世界模式的特质。随着我们的模型变得越来越复杂,这种问题变得尤为严重。欠拟合是一个相关问题,即模型不够复杂,无法捕捉数据中的潜在趋势。Figure 4-5 说明了过拟合和欠拟合。Figure 4-5 的左侧面板显示了一个线性回归模型;一条直线明显地对真实函数拟合不足。中间面板显示了高次多项式相对合理地近似了真实关系。另一方面,高次多项式几乎完美地适应了小样本,并且在训练数据上表现最好,但这种情况并不具有泛化性,并且在解释新数据点时效果非常糟糕。

过拟合和欠拟合的概念与偏差-方差权衡密切相关。偏差是由于学习算法中过于简化或错误的假设而导致的错误。偏差导致数据的拟合不足,如 Figure 4-5 的左侧面板所示。高偏差意味着我们的学习算法忽略了特征之间的重要趋势。方差是由于一个过于复杂的模型试图尽可能紧密地拟合训练数据而导致的错误。在高方差的情况下,模型的预测值与训练集中的实际值非常接近。高方差导致过拟合,如 Figure 4-5 的右侧面板所示。最终,为了得到一个好的模型,我们需要低偏差和低方差。

mlbf 0405

图 4-5. 过拟合和欠拟合

可以有两种方法来对抗过拟合:

使用更多的训练数据

我们拥有的训练数据越多,就越难通过从任一训练示例中学习过多来过拟合数据。

使用正则化

在损失函数中为建立模型增加一项惩罚,以使模型不会赋予任何一个特征过多的解释力量,或者允许考虑太多的特征。

过拟合的概念及其对策适用于所有监督学习模型。例如,正则化回归可以解决线性回归中的过拟合问题,正如本章前面讨论的那样。

交叉验证

机器学习面临的挑战之一是训练能够很好地泛化到未见数据的模型(过拟合与欠拟合或偏差-方差权衡)。交叉验证的主要思想是将数据一次或多次分割,以便每次分割都将一个部分作为验证集,其余部分作为训练集:数据的一部分(训练样本)用于训练算法,剩余部分(验证样本)用于估计算法的风险。交叉验证允许我们获得模型泛化误差的可靠估计。通过一个例子最容易理解它。在进行k折交叉验证时,我们将训练数据随机分成k折。然后我们使用k-1折训练模型,并在第k折上评估性能。我们重复这个过程k次,并平均得分。

Figure 4-6 显示了一个交叉验证的示例,其中数据被分成五组,在每一轮中,其中一个组被用作验证集。

mlbf 0406

图 4-6. 交叉验证

交叉验证的一个潜在缺点是计算成本,特别是与超参数调整的网格搜索结合时。使用 sklearn 包可以在几行代码中执行交叉验证;我们将在监督学习案例研究中执行交叉验证。

在下一节中,我们将介绍用于测量和比较模型性能的监督学习模型的评估指标。

评估指标

评估机器学习算法使用的指标非常重要。选择使用的指标影响着如何衡量和比较机器学习算法的性能。这些指标影响您如何权衡结果中不同特征的重要性,以及最终选择的算法。

回归和分类的主要评估指标在 Figure 4-7 中有所说明。

mlbf 0407

图 4-7. 回归和分类的评估指标

让我们首先看一下监督回归的评估指标。

平均绝对误差

平均绝对误差(MAE)是预测值与实际值之间绝对差值的总和。MAE 是线性评分,这意味着平均值中所有个体差异的权重相等。它提供了预测有多大错误的想法。该指标给出了误差的大小,但不指示方向(例如,过高或过低预测)。

均方误差

均方误差(MSE)表示预测值与观测值(称为残差)之间差异的样本标准偏差。这与平均绝对误差类似,提供了误差大小的大致概念。将均方误差的平方根取出,可以将单位转换回输出变量的原始单位,并且对描述和展示有意义。这被称为均方根误差(RMSE)。

R²指标

R²指标提供了预测与实际值“拟合度”的指示。在统计文献中,这个度量被称为决定系数。它的值介于零和一之间,分别表示无拟合和完美拟合。

调整后的 R²指标

一样,调整后的 R²也显示了项在曲线或直线上拟合的程度,但会根据模型中的项数进行调整。其表达式如下:

R adj 2 = 1 (1R 2 )(n1)) nk1

其中n为总观测数,k为预测变量数。调整后的永远小于或等于

选择监督回归的评估指标

在这些评估指标中,如果主要目标是预测准确性,则 RMSE 是最好的选择。它计算简单,易于区分。损失对称,但较大误差在计算中权重更大。MAE 对称,但不会给较大误差加权。和调整后的通常用于解释目的,指示选择的独立变量如何解释因变量的变异性。

让我们首先看看监督分类的评估指标。

分类

为简便起见,我们将大多数讨论限于二元分类问题(例如,仅有两个结果,如真或假);一些常见术语包括:

真阳性(TP)

预测为正实际为正。

假阳性(FP)

预测为正实际为负。

真阴性(TN)

预测为负实际为负。

假阴性(FN)

预测为负实际为正。

展示了分类常用的三个评估指标——准确率、精确率和召回率——之间的区别,如图 4-8 所示。

mlbf 0408

图 4-8. 准确率、精确率和召回率

准确率

如图 4-8 所示,准确率是所有预测中正确预测的比例。这是分类问题最常见的评估指标,但也是最常被误用的。它在每个类中观察数相等时最合适(这种情况很少出现),以及所有预测和相关预测误差同等重要时最合适(这种情况通常不成立)。

精确率

精确率 是总预测正实例中的正实例百分比。在这里,分母是整个给定数据集中模型预测为正的部分。当假阳性的成本较高时(例如电子邮件垃圾邮件检测),精确率是一个很好的度量标准。

召回率

召回率(或敏感度真正率)是总实际正实例中的正实例百分比。因此,分母(真正阳性+假阴性)是数据集中实际存在的正实例数量。当虚假阴性成本高昂时(例如欺诈检测),召回率是一个很好的度量标准。

除了准确率、精确率和召回率之外,还讨论了分类的其他常用评估指标。

ROC 曲线下的面积

ROC 曲线下的面积(AUC)是用于二元分类问题的评估指标。ROC 是一个概率曲线,AUC 表示可分离性的程度或度量。它告诉我们模型区分类别的能力有多强。AUC 越高,模型在将零预测为零和一预测为一方面的能力越好。AUC 为0.5意味着模型根本没有类别分离能力。AUC 分数的概率解释是,如果您随机选择一个正案例和一个负案例,那么正案例根据分类器的排序超过负案例的概率由 AUC 给出。

混淆矩阵

混淆矩阵展示了学习算法的性能。混淆矩阵简单地是报告分类器预测的真阳性(TP)、真阴性(TN)、假阳性(FP)和假阴性(FN)的计数的方阵,如图 4-9 所示。

mlbf 0409

图 4-9. 混淆矩阵

混淆矩阵是对具有两个或多个类别的模型准确性的便利展示。表格展示了x 轴上的预测和y 轴上的准确结果。表格的单元格是模型做出的预测数量。例如,模型可以预测零或一,每个预测实际上可能是零或一。实际为零的预测出现在预测=0 且实际=0 的单元格中,而实际为一的预测出现在预测=0 且实际=1 的单元格中。

为监督分类选择评估指标

分类的评估指标严重依赖于手头的任务。例如,当虚假阴性(如欺诈检测)带来高昂成本时,召回率是一个很好的度量标准。我们将在案例研究中进一步研究这些评估指标。

模型选择

选择完美的机器学习模型既是艺术也是科学。观察机器学习模型时,并没有适合所有情况的单一解决方案或方法。有几个因素可能会影响您选择机器学习模型的选择。大多数情况下的主要标准是我们在前一节中讨论的模型性能。然而,在进行模型选择时,还有许多其他因素需要考虑。在接下来的部分中,我们将详细介绍所有这些因素,并讨论模型的权衡。

模型选择的因素

模型选择过程中考虑的因素如下:

简单性

模型的简单程度。简单性通常导致模型和结果更快、更可扩展和更易理解。

训练时间

模型训练中的速度、性能、内存使用情况和总体时间。

处理数据中的非线性关系

模型处理变量之间的非线性关系的能力。

对过拟合的鲁棒性

模型处理过拟合的能力。

数据集的大小

模型处理数据集中大量训练样本的能力。

特征数量

模型处理特征空间高维度的能力。

模型解释性

模型的解释能力如何?模型的可解释性很重要,因为它允许我们采取具体措施解决潜在问题。

特征缩放

模型是否要求变量进行缩放或者服从正态分布?

图 4-10 对之前提到的因素比较了监督学习模型,并概述了在给定问题下缩小最佳机器学习算法搜索范围的经验法则⁷。该表基于本章节中讨论的不同模型的优缺点。

mlbf 0410

图 4-10. 模型选择

从表中可以看出,相对简单的模型包括线性回归和逻辑回归,而随着向集成和人工神经网络(ANN)方向发展,复杂性增加。在训练时间方面,与集成方法和 ANN 相比,线性模型和 CART 的训练速度相对较快。

线性和逻辑回归不能处理非线性关系,而其他所有模型都可以。支持向量机(SVM)可以通过非线性核处理因变量和自变量之间的非线性关系。

支持向量机(SVM)和随机森林倾向于比线性回归、逻辑回归、梯度提升和 ANN 过拟合较少。过拟合程度还取决于其他参数,如数据集大小和模型调整,并且可以通过查看每个模型的测试集结果来检查。此外,与随机森林等装袋方法相比,梯度提升等增强方法的过拟合风险更高。需要注意的是,梯度提升的重点是最小化偏差而不是方差。

线性回归和逻辑回归在处理大数据集和大量特征时效果不佳。然而,CART、集成方法和人工神经网络(ANN)能够很好地处理大数据集和许多特征。在数据集较小的情况下,线性回归和逻辑回归通常表现优于其他模型。应用变量减少技术(如第七章所示)可以使线性模型处理大数据集。随着数据集大小的增加,ANN 的性能也会提高。

相比于集成模型和人工神经网络(ANN),线性回归、逻辑回归和 CART 等相对简单的模型具有更好的模型解释性能。

模型权衡

在选择模型时,通常需要在不同因素之间进行权衡。人工神经网络(ANN)、支持向量机(SVM)和某些集成方法可以用来创建非常精确的预测模型,但它们可能缺乏简单性和可解释性,并且可能需要大量资源进行训练。

选择最终模型时,当预测性能是最重要的目标时,通常会更倾向于可解释性较低的模型,而不需要解释模型的工作原理和预测过程。然而,在某些情况下,模型的解释性是必须的。

在金融行业经常看到以可解释性为驱动的示例。在许多情况下,选择机器学习算法与算法的优化或技术方面关系较少,更多地与业务决策有关。假设一个机器学习算法用于接受或拒绝个人的信用卡申请。如果申请人被拒绝并决定提出投诉或采取法律行动,金融机构将需要解释如何做出这个决定。对于人工神经网络(ANN)而言,这几乎是不可能的,但对于基于决策树的模型而言相对简单。

不同类别的模型擅长建模不同类型的数据潜在模式。因此,一个很好的第一步是快速测试几种不同类别的模型,以了解哪些模型最有效地捕捉数据集的潜在结构。在所有基于监督学习的案例研究中,我们将遵循这种方法进行模型选择。

章节总结

在本章中,我们讨论了监督学习模型在金融中的重要性,随后简要介绍了几种监督学习模型,包括线性回归、逻辑回归、SVM、决策树、集成学习、KNN、LDA 和 ANN。我们展示了如何使用 sklearn 和 Keras 库的几行代码进行这些模型的训练和调优。

我们讨论了回归和分类模型中最常见的错误度量标准,解释了偏差-方差的权衡,并且用交叉验证说明了管理模型选择过程的各种工具。

我们介绍了每个模型的优缺点,并讨论了在选择最佳模型时需要考虑的因素。我们还讨论了模型性能和可解释性之间的权衡。

在接下来的章节中,我们将深入研究回归和分类的案例研究。在接下来的两章中的所有案例研究都利用了本章和前两章提出的概念。

¹ 交叉验证将在本章后面详细介绍。

² 有关sigmoid函数的详细信息,请参阅第三章的激活函数部分。

³ MLE 是一种估计概率分布参数的方法,使得在假定的统计模型下观察到的数据最有可能。

⁴ 数据投影的方法类似于第七章中讨论的 PCA 算法。

⁵ 偏差和方差将在本章后面详细描述。

⁶ 分裂是将非同质父节点转换为两个同质子节点的过程(最佳可能的)。

⁷ 在这张表中,我们不包括AdaBoostextra trees,因为它们在所有参数上的整体行为与Gradient BoostingRandom Forest类似。

第五章:监督学习:回归(包括时间序列模型)

监督回归型机器学习是一种预测性建模形式,其目标是建立目标与预测变量之间的关系,以估计一组连续可能的结果。这些是金融中最常用的机器学习模型。

金融机构(以及整个金融领域)分析师的关注重点之一是预测投资机会,通常是资产价格和资产回报的预测。在这种情况下,监督回归型机器学习模型天生适用。这些模型帮助投资和金融经理了解预测变量的特性及其与其他变量的关系,并帮助他们确定推动资产回报的重要因素。这有助于投资者估计回报概况、交易成本、基础设施所需的技术和金融投资,从而最终评估策略或投资组合的风险概况和盈利能力。

随着大量数据和处理技术的可用性,监督回归型机器学习不仅仅局限于资产价格预测。这些模型被应用于金融领域的广泛范围,包括投资组合管理、保险定价、工具定价、套期保值和风险管理等领域。

在本章中,我们涵盖了三个基于监督回归的案例研究,涉及资产价格预测、工具定价和投资组合管理等多个领域。所有案例研究都遵循第二章中介绍的标准化七步模型开发过程;这些步骤包括定义问题、加载数据、探索性数据分析、数据准备、模型评估和模型调优。这些案例研究的设计不仅涵盖了金融视角下的多个主题,还涵盖了多个机器学习和建模概念,包括从基本的线性回归到第四章中介绍的高级深度学习模型。

在金融行业的资产建模和预测问题中,涉及时间成分和连续输出的估计是相当重要的。因此,也有必要讨论时间序列模型。在其最广泛的形式下,时间序列分析是关于推断过去一系列数据点的情况,并试图预测未来会发生什么。关于监督回归和时间序列模型之间的差异,学术界和行业中已经进行了大量的比较和辩论。大多数时间序列模型是参数化的(即假设已知函数表示数据),而大多数监督回归模型是非参数化的。时间序列模型主要利用预测变量的历史数据进行预测,而监督学习算法则使用外生变量作为预测变量。² 然而,监督回归可以通过时间延迟方法嵌入预测变量的历史数据(本章后面会介绍),时间序列模型(如 ARIMAX,也会在本章后面介绍)可以利用外生变量进行预测。因此,时间序列模型和监督回归模型在可以使用外生变量以及预测变量的历史数据进行预测方面是相似的。在最终输出方面,监督回归和时间序列模型都估计了变量的一组连续可能结果。

在第四章中,我们涵盖了在监督回归和监督分类之间共通的模型概念。考虑到时间序列模型更接近于监督回归而不是监督分类,我们将在本章节单独讨论时间序列模型的概念。我们还将演示如何在金融数据上使用时间序列模型来预测未来的数值。我们将在案例研究中呈现时间序列模型与监督回归模型的比较。此外,一些机器学习和深度学习模型(如 LSTM)可以直接用于时间序列预测,我们也将对其进行讨论。

在“案例研究 1:股票价格预测”中,我们展示了金融领域中最流行的预测问题之一,即预测股票回报率。除了准确预测未来股票价格外,这个案例研究的目的是讨论基于机器学习的通用资产类价格预测框架。在这个案例研究中,我们将讨论几个机器学习和时间序列的概念,同时重点关注可视化和模型调整。

在“案例研究 2:衍生品定价”中,我们将深入探讨使用监督回归进行衍生品定价,并展示如何在传统量化问题背景下部署机器学习技术。与传统衍生品定价模型相比,机器学习技术可以在不依赖于多个不切实际假设的情况下更快地进行衍生品定价。在金融风险管理等领域,利用机器学习进行高效数值计算往往具有越来越多的好处,这些领域通常需要在效率与准确性之间进行权衡。

在“案例研究 3:投资者风险容忍度与智能顾问”中,我们展示了基于监督回归的框架来估计投资者的风险容忍度。在这个案例研究中,我们在 Python 中构建了一个智能顾问仪表板,并在该仪表板中实现了这个风险容忍度预测模型。我们展示了这类模型如何能够自动化投资组合管理流程,包括利用智能顾问进行投资管理。这也旨在说明机器学习如何有效地用于克服传统风险容忍度评估或风险容忍度问卷存在的多种行为偏差问题。

在“案例研究 4:收益率曲线预测”中,我们使用基于监督回归的框架同时预测不同收益率曲线期限。我们展示了如何使用机器学习模型同时生成多个期限,以建模收益率曲线。

用于监督回归的模型见第三章和第四章。在案例研究之前,我们将讨论时间序列模型。我们强烈建议读者参考 Robert H. Shumway 和 David S. Stoffer 的《时间序列分析及其应用》,第 4 版(Springer),以深入理解时间序列概念,并参考 Aurélien Géron 的《Scikit-Learn、Keras 和 TensorFlow 实战》,第 2 版(O’Reilly),了解监督回归模型中的更多概念。

本章代码库

本书的代码库中包含了基于 Python 的监督回归主模板、时间序列模型模板以及本章中所有案例研究的 Jupyter 笔记本,位于第五章 - 监督学习 - 回归和时间序列模型的文件夹中。

对于任何新的基于监督回归的案例研究,使用代码库中的通用模板,修改特定于案例研究的元素,并借鉴本章中提出的案例研究的概念和见解。该模板还包括 ARIMA 和 LSTM 模型的实现和调整。³这些模板设计用于在云端运行(即 Kaggle、Google Colab 和 AWS)。所有案例研究都是根据统一的回归模板设计的。⁴

时间序列模型

时间序列是按时间索引排序的数字序列。

在本节中,我们将介绍时间序列模型的以下方面,我们在案例研究中进一步利用这些方面:

  • 时间序列的组成部分

  • 时间序列的自相关性和平稳性

  • 传统的时间序列模型(例如 ARIMA)

  • 使用深度学习模型进行时间序列建模

  • 将时间序列数据转换为用于监督学习框架的形式

时间序列分解

时间序列可以分解为以下组件:

趋势分量

趋势是时间序列中的一致方向性运动。这些趋势将是确定性随机性的。前者允许我们为趋势提供根本的解释,而后者是我们不太可能解释的系列的随机特征。趋势经常出现在金融系列中,并且许多交易模型使用复杂的趋势识别算法。

季节性分量

许多时间序列包含季节性变化。这在代表业务销售或气候水平的系列中特别真实。在量化金融中,我们经常看到季节性变化,特别是与假期季节或年度温度变化相关的系列(例如天然气)。

我们可以将时间序列y t的组成部分写成

y t = S t + T t + R t

其中S t是季节性分量,T t是趋势分量,R t代表了时间序列中未被季节性或趋势分量捕获的剩余部分。

将时间序列(Y)分解为其组件的 Python 代码如下:

import statsmodels.api as sm
sm.tsa.seasonal_decompose(Y,freq=52).plot()

图 5-1 显示了时间序列分解为趋势、季节性和剩余部分组件。将时间序列分解为这些组件可能有助于我们更好地理解时间序列,并确定其行为以进行更好的预测。

三个时间序列组件分别显示在底部的三个面板中。这些组件可以相加以重建顶部面板中显示的实际时间序列(标为“观察到”)。注意,时间序列在 2017 年后显示出趋势性组件。因此,该时间序列的预测模型应该包含有关 2017 年后趋势行为的信息。在季节性方面,每年的开始有一些幅度增加。底部面板中显示的残差分量是从数据中减去季节性和趋势组件后剩余的部分。残差分量在 2018 年和 2019 年左右有些尖峰和噪声,整体上是平坦的。此外,每个图的比例不同,趋势分量的范围最大,如图中的比例尺所示。

mlbf 0501

图 5-1. 时间序列组件

自相关和平稳性

当给定一个或多个时间序列时,将时间序列分解为趋势、季节性和残差分量相对比较简单。然而,在处理时间序列数据时,特别是在金融领域,还有其他因素需要考虑。

自相关

许多情况下,时间序列的连续元素呈现相关性。即,序列中连续点的行为以一种依赖的方式相互影响。自相关 是观测值随时间滞后之间相似性的度量。可以使用自回归模型来建模这种关系。术语 自回归 表示它是变量对自身的回归。

在自回归模型中,我们使用变量过去值的线性组合来预测感兴趣的变量。

因此,p 阶自回归模型可以写成

y t = c + ϕ 1 y t1 + ϕ 2 y t2 + . . . . ϕ p y tp + ϵ

其中 ϵ t 是白噪声。⁵ 自回归模型类似于多元回归,但使用滞后的 y t 值作为预测因子。我们称之为 AR(p)模型,即 p 阶自回归模型。自回归模型在处理各种不同的时间序列模式时非常灵活。

平稳性

如果一个时间序列的统计特性随时间不变,则称为平稳时间序列。因此,带有趋势或季节性的时间序列不是平稳的,因为趋势和季节性会在不同时间点影响时间序列的值。另一方面,白噪声序列是平稳的,因为无论何时观察,它看起来都应该是相似的。

图 5-2 展示了一些非平稳序列的示例。

mlbf 0502

图 5-2. 非平稳绘图

在第一个图中,我们可以清楚地看到均值随时间变化(增加),导致了上升趋势。因此,这是一个非平稳序列。要将序列分类为平稳序列,它不应该展示出趋势。转到第二个图,我们确实看不到序列中的趋势,但是序列的方差是时间的函数。平稳序列必须具有恒定的方差;因此,这个序列也是非平稳序列。在第三个图中,随着时间的增加,波动变得更接近,这意味着协方差是时间的函数。图 5-2 中显示的三个示例代表非平稳时间序列。现在看一下第四个图,如图 5-3 所示。

mlbf 0503

图 5-3 。平稳图

在这种情况下,均值、方差和协方差随时间保持不变。这就是平稳时间序列的样子。使用这个第四个图来预测未来值将更加容易。大多数统计模型要求序列是平稳的,才能进行有效和精确的预测。

时间序列的非平稳性主要由趋势和季节性导致,如图 5-2 所示。为了使用时间序列预测模型,通常将任何非平稳序列转换为平稳序列,这样更容易进行建模,因为统计属性随时间不变。

差分

差分是使时间序列平稳的方法之一。在这种方法中,我们计算序列中相邻项的差值。差分通常用于消除变化的均值。数学上,差分可以写成:

y t = y t y t1

其中 y t 是时间 t 的值。

当差分后的序列为白噪声时,原始序列被称为一阶非平稳序列。

传统时间序列模型(包括 ARIMA 模型)

有很多方法可以对时间序列建模以进行预测。大多数时间序列模型旨在同时考虑趋势、季节性和残差分量,并解决嵌入在时间序列中的自相关性和平稳性问题。例如,在前一节讨论的自回归(AR)模型中,解决了时间序列中的自相关性问题。

时间序列预测中最广泛使用的模型之一是 ARIMA 模型。

ARIMA

如果我们将平稳性与自回归和移动平均模型结合起来(本节进一步讨论),我们就得到了 ARIMA 模型。ARIMA 是自回归积分移动平均的缩写,具有以下组成部分:

AR(p)

它表示自回归,即将时间序列回归到自身,正如前一节中讨论的那样,假设当前系列值取决于其前几个值(或多个滞后)。模型中的最大滞后被称为p

I(d)

它表示积分的阶数。它简单地是使系列平稳所需的差分次数。

MA(q)

它表示移动平均。不详细讨论,它对时间序列的误差进行建模;再次假设当前误差取决于以前的误差和一些滞后,这称为q

移动平均方程的写法如下:

y t = c + ϵ t + θ 1 ϵ t1 + θ 2 ϵ t2

其中,ϵ t是白噪声。我们将其称为MA(q)阶数为q的模型。

结合所有组件,完整的 ARIMA 模型可以写成:

y t = c + ϕ 1 y t1 + + ϕ p y tp + θ 1 ε t1 + + θ q ε tq + ε t

其中y t '是差分后的系列(可能进行了多次差分)。右侧的预测变量包括y t '的滞后值和滞后误差。我们称之为 ARIMA(p,d,q)模型,其中p是自回归部分的阶数,d是涉及的第一阶差分的程度,q是移动平均部分的阶数。用于自回归和移动平均模型的同样平稳性和可逆性条件也适用于 ARIMA 模型。

拟合 ARIMA 模型(1,0,0)的 Python 代码如下所示:

from statsmodels.tsa.arima_model import ARIMA
model=ARIMA(endog=Y_train,order=[1,0,0])

ARIMA 模型有几个变体,其中一些如下:

ARIMAX

包括外生变量的 ARIMA 模型。我们将在案例研究 1 中使用这个模型。

SARIMA

“S”在这个模型中代表季节性,这个模型旨在对时间序列中嵌入的季节性组件进行建模,以及其他组件。

VARMA

这是将模型扩展到多元情况的方法,当有许多变量同时预测时,我们可以同时预测多个变量在“案例研究 4: 收益率曲线预测”中进行预测。

时间序列建模的深度学习方法

传统的时间序列模型如 ARIMA 在许多问题上都被充分理解和有效。然而,这些传统方法也存在一些局限性。传统的时间序列模型是线性函数,或线性函数的简单变换,并且需要手动诊断参数,如时间依赖性,并且在数据损坏或缺失时表现不佳。

如果我们看一下时间序列预测领域的进展,我们会发现循环神经网络(RNN)近年来引起了越来越多的关注。这些方法可以识别结构和模式,如非线性,可以无缝地建模具有多个输入变量的问题,并且对缺失数据相对健壮。RNN 模型可以通过使用它们自己的输出作为下一步的输入来保留一次迭代到下一次迭代的状态。这些深度学习模型可以被称为时间序列模型,因为它们可以使用过去的数据点进行未来预测,类似于传统的时间序列模型,如 ARIMA。因此,在金融领域有广泛的应用,可以利用这些深度学习模型。让我们来看一下时间序列预测的深度学习模型。

RNNs

循环神经网络(RNN)之所以被称为“循环”,是因为它们对序列中的每个元素执行相同的任务,输出取决于先前的计算。RNN 模型具有一种记忆,它捕获了迄今为止计算的信息。正如在图 5-4 中所示,循环神经网络可以被视为同一网络的多个副本,每个副本将消息传递给后续副本。

mlbf 0504

图 5-4. 循环神经网络

在图 5-4 中:

  • X[t]是时间步t的输入。

  • O[t]是时间步t的输出。

  • S[t]是时间步t的隐藏状态。它是网络的记忆。它是基于先前的隐藏状态和当前步骤的输入计算的。

RNN 的主要特征是隐藏状态,它捕捉了序列的某些信息,并在需要时相应地使用它。

长短期记忆网络

长短期记忆(LSTM)是一种专门设计的 RNN,旨在避免长期依赖问题。长时间记忆信息对于 LSTM 模型来说几乎是默认行为。[⁶] 这些模型由一组具有记忆数据序列特征的单元组成。这些单元捕获并存储数据流。此外,单元将过去的一个模块与当前一个模块相互连接,将信息从多个过去时间瞬间传递到当前时间瞬间。由于每个单元中使用了门控,每个单元中的数据可以被处理、过滤或添加到下一个单元中。

基于人工神经网络层的使单元能够选择性地通过或丢弃数据。每个层产生的数字范围从零到一,描述了每个数据段在每个单元中应该通过的数量。更确切地说,估计零值表示“不要让任何东西通过”。估计为一表示“让所有东西通过”。每个 LSTM 中涉及三种类型的门,旨在控制每个单元的状态:

遗忘门

输出介于零和一之间的数字,其中一表示“完全保留此内容”,零表示“完全忽略此内容”。该门有条件地决定过去是应该被遗忘还是保留。

输入门

选择需要存储在单元格中的新数据。

输出门

决定每个单元格将产生什么值。产生的值将基于单元格状态以及过滤和新添加的数据。

Keras 封装了高效的数值计算库和函数,并允许我们用几行简短的代码定义和训练 LSTM 神经网络模型。在下面的代码中,使用 keras.layers 中的 LSTM 模块来实现 LSTM 网络。网络使用变量 X_train_LSTM 进行训练。网络有一个包含 50 个 LSTM 块或神经元的隐藏层和一个输出层,用于进行单个值的预测。还可以参考第三章以获取对所有术语(即,sequential、learning rate、momentum、epoch 和 batch size)的更详细描述。

在 Keras 中实现 LSTM 模型的示例 Python 代码如下所示:

from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
from keras.layers import LSTM

def create_LSTMmodel(learn_rate = 0.01, momentum=0):
       # create model
   model = Sequential()
   model.add(LSTM(50, input_shape=(X_train_LSTM.shape[1],\
      X_train_LSTM.shape[2])))
   #More number of cells can be added if needed
   model.add(Dense(1))
   optimizer = SGD(lr=learn_rate, momentum=momentum)
   model.compile(loss='mse', optimizer='adam')
   return model
LSTMModel = create_LSTMmodel(learn_rate = 0.01, momentum=0)
LSTMModel_fit = LSTMModel.fit(X_train_LSTM, Y_train_LSTM, validation_data=\
  (X_test_LSTM, Y_test_LSTM),epochs=330, batch_size=72, verbose=0, shuffle=False)

就学习和实现而言,与 ARIMA 模型相比,LSTM 在微调方面提供了更多选择。尽管深度学习模型比传统时间序列模型有几个优点,但深度学习模型更复杂,训练起来更困难。⁷

修改时间序列数据以用于监督学习模型

时间序列是按时间索引排序的一系列数字。监督学习是指我们有输入变量(X)和一个输出变量(Y)。给定一个时间序列数据集的数字序列,我们可以将数据重构为一组预测变量和被预测变量,就像在监督学习问题中一样。我们可以通过使用先前的时间步作为输入变量,并使用下一个时间步作为输出变量来实现这一点。让我们通过一个例子来具体说明。

我们可以通过使用上一个时间步的值来预测下一个时间步的值,将左表中显示的时间序列重组为一个监督学习问题。一旦我们以这种方式重新组织了时间序列数据集,数据将看起来像右边的表格。

mlbf 0505

图 5-5。修改时间序列以用于监督学习模型

我们可以看到,在我们的监督学习问题中,上一个时间步是输入(X),下一个时间步是输出(Y)。观察之间的顺序被保留,当使用这个数据集来训练一个监督模型时,必须继续保留这种顺序。在训练我们的监督模型时,我们将删除第一行和最后一行,因为我们既没有 X 的值,也没有 Y 的值。

在 Python 中,帮助将时间序列数据转换为监督学习问题的主要函数是来自 Pandas 库的shift()函数。我们将在案例研究中演示这种方法。使用先前时间步来预测下一个时间步称为滑动窗口时间延迟滞后方法。

在讨论了监督学习和时间序列模型的所有概念后,让我们转向案例研究。

案例研究 1:股票价格预测

金融领域面临的最大挑战之一是预测股票价格。然而,随着最近机器学习应用的进展,这个领域已经在利用非确定性解决方案来学习当前情况,以做出更准确的预测。基于历史数据,机器学习技术自然而然地适用于股票价格预测。可以对单个时间点或未来一系列时间点进行预测。

作为高级概述,除了股票本身的历史价格外,通常用于股票价格预测的特征如下:

相关资产

一个组织依赖并与许多外部因素互动,包括其竞争对手、客户、全球经济、地缘政治局势、财政和货币政策、资本获取等等。因此,其股票价格可能与其他公司的股票价格以及商品、外汇、广泛的指数甚至固定收益证券等其他资产相关。

技术指标

许多投资者关注技术指标。移动平均线、指数移动平均线和动量是最流行的指标。

基本分析

用于基本分析的两个主要数据源包括:

绩效报告

公司的年度和季度报告可用于提取或确定关键指标,如 ROE(净资产收益率)和 P/E(市盈率)。

新闻

新闻可以指示即将发生的事件,这些事件有可能在某个方向上推动股票价格。

在这个案例研究中,我们将使用各种基于监督学习的模型来预测微软的股票价格,利用相关资产及其自身的历史数据。到案例研究结束时,读者将熟悉股票预测建模的一般机器学习方法,从数据收集和清洗到构建和调整不同的模型。

使用监督学习模型预测股票价格的蓝图

1. 问题定义

在本案例研究中使用的监督回归框架中,微软股票的每周回报是预测变量。我们需要理解什么影响微软股票价格,并将尽可能多的信息纳入模型中。在相关资产、技术指标和基本分析(在前面的部分中讨论)中,我们将专注于相关资产作为本案例研究中的特征。

除了微软的历史数据外,本案例研究使用的独立变量包括以下潜在相关资产:

股票

IBM (IBM) 和 Alphabet (GOOGL)

货币⁹

USD/JPY 和 GBP/USD

索引

标准普尔 500、道琼斯和 VIX

用于本案例研究的数据集来自 Yahoo Finance 和FRED 网站。除了准确预测股票价格外,本案例研究还将展示每一步时间序列和基于监督回归的股票价格预测建模的基础设施和框架。我们将使用自 2010 年以来最近 10 年的每日收盘价。

2. 开始——加载数据和 Python 包

2.1. 加载 Python 包

数据加载、数据分析、数据准备、模型评估和模型调优所使用的库列表如下所示。用于不同目的的包在接下来的 Python 代码中有所区分。这些包和函数的详细信息在第二章和第四章中已经提供。这些包的使用将在模型开发过程的不同步骤中进行演示。

监督回归模型的函数和模块

from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.neural_network import MLPRegressor

数据分析和模型评估的函数和模块

from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import mean_squared_error
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2, f_regression

深度学习模型的函数和模块

from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
from keras.layers import LSTM
from keras.wrappers.scikit_learn import KerasRegressor

时间序列模型的函数和模块

from statsmodels.tsa.arima_model import ARIMA
import statsmodels.api as sm

数据准备和可视化的函数和模块

# pandas, pandas_datareader, numpy and matplotlib
import numpy as np
import pandas as pd
import pandas_datareader.data as web
from matplotlib import pyplot
from pandas.plotting import scatter_matrix
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from pandas.plotting import scatter_matrix
from statsmodels.graphics.tsaplots import plot_acf

2.2. 加载数据

机器学习和预测建模中最重要的步骤之一是收集好的数据。以下步骤演示了使用 Pandas 的DataReader函数从 Yahoo Finance 和 FRED 网站加载数据:

stk_tickers = ['MSFT', 'IBM', 'GOOGL']
ccy_tickers = ['DEXJPUS', 'DEXUSUK']
idx_tickers = ['SP500', 'DJIA', 'VIXCLS']

stk_data = web.DataReader(stk_tickers, 'yahoo')
ccy_data = web.DataReader(ccy_tickers, 'fred')
idx_data = web.DataReader(idx_tickers, 'fred')

接下来,我们定义我们的因变量 (Y) 和独立变量 (X)。预测变量是微软(MSFT)的每周回报。一周的交易日数假定为五天,并使用五个交易日计算回报。作为独立变量,我们使用相关资产和不同频率下微软的历史回报。

用作独立变量的变量是股票(IBM 和 GOOG)、货币(USD/JPY 和 GBP/USD)和指数(标准普尔 500、道琼斯和 VIX)的滞后五天回报,以及 MSFT 的滞后 5 天、15 天、30 天和 60 天回报。

使用滞后五天变量通过时间延迟方法嵌入时间序列组件,其中滞后变量被包括为独立变量之一。这一步骤是将时间序列数据重构为基于监督回归的模型框架。

return_period = 5
Y = np.log(stk_data.loc[:, ('Adj Close', 'MSFT')]).diff(return_period).\
shift(-return_period)
Y.name = Y.name[-1]+'_pred'

X1 = np.log(stk_data.loc[:, ('Adj Close', ('GOOGL', 'IBM'))]).diff(return_period)
X1.columns = X1.columns.droplevel()
X2 = np.log(ccy_data).diff(return_period)
X3 = np.log(idx_data).diff(return_period)

X4 = pd.concat([np.log(stk_data.loc[:, ('Adj Close', 'MSFT')]).diff(i) \
for i in [return_period, return_period*3,\
return_period*6, return_period*12]], axis=1).dropna()
X4.columns = ['MSFT_DT', 'MSFT_3DT', 'MSFT_6DT', 'MSFT_12DT']

X = pd.concat([X1, X2, X3, X4], axis=1)

dataset = pd.concat([Y, X], axis=1).dropna().iloc[::return_period, :]
Y = dataset.loc[:, Y.name]
X = dataset.loc[:, X.columns]

3. 探索性数据分析

在本节中,我们将查看描述性统计、数据可视化和时间序列分析。

3.1. 描述性统计

让我们来看看我们拥有的数据集:

dataset.head()

Output

mlbf 05in01

变量 MSFT_pred 是微软股票的回报并且是预测变量。数据集包含其他相关股票、货币和指数的滞后系列,以及 MSFT 的滞后历史回报。

3.2. 数据可视化

了解数据更多信息的最快方法是将其可视化。可视化涉及独立理解数据集的每个属性。我们将查看散点图和相关矩阵。这些图表让我们了解数据的相互依赖关系。通过创建相关矩阵,可以为每对变量计算和显示相关性。因此,除了独立和依赖变量之间的关系外,它还显示了数据中独立变量之间的相关性。了解这一点很有用,因为如果数据中存在高度相关的输入变量,一些机器学习算法如线性和逻辑回归的性能可能会较差:

correlation = dataset.corr()
pyplot.figure(figsize=(15,15))
pyplot.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True,annot=True,cmap='cubehelix')

Output

mlbf 05in02

查看相关性图(完整版可在 GitHub 上查看),我们可以看到预测变量与 MSFT 滞后 5 天、15 天、30 天和 60 天回报之间的某种相关性。此外,我们还看到许多资产回报与 VIX 的负相关性较高,这是直观的。

接下来,我们可以使用下面显示的散点图矩阵来可视化回归中所有变量之间的关系:

pyplot.figure(figsize=(15,15))
scatter_matrix(dataset,figsize=(12,12))
pyplot.show()

Output

mlbf 05in03

查看散点图(完整版可在 GitHub 上查看),我们可以看到预测变量与 MSFT 滞后 15 天、30 天和 60 天回报之间的某种线性关系。否则,我们没有看到预测变量与特征之间的任何特殊关系。

3.3. 时间序列分析

接下来,我们深入研究时间序列分析,并查看预测变量的时间序列分解为趋势和季节性组成部分:

res = sm.tsa.seasonal_decompose(Y,freq=52)
fig = res.plot()
fig.set_figheight(8)
fig.set_figwidth(15)
pyplot.show()

Output

mlbf 05in04

我们可以看到,对于 MSFT,收益系列存在明显的上升趋势。这可能是由于 MSFT 近年来大幅上涨,导致正周回报数据点比负周回报数据点更多。¹¹ 这种趋势可能在我们模型的常数/偏差项中显示出来。残差(或白噪声)项在整个时间序列中相对较小。

4. 数据准备

这一步通常涉及数据处理、数据清洗、查看特征重要性以及执行特征减少。这个案例研究中获取的数据相对清洁,不需要进一步处理。在这里,特征减少可能是有用的,但考虑到考虑的变量数量相对较少,我们将保留所有变量。我们将在一些后续的案例研究中详细演示数据准备。

5. 评估模型

5.1. 训练-测试分离和评估指标

如第二章所述,将原始数据集划分为训练集测试集是一个好主意。测试集是我们在分析和建模过程中保留的数据样本。我们在项目的最后使用它来确认我们最终模型的性能。它是最终测试,为我们提供了对未见数据准确性估计的信心。我们将 80%的数据集用于建模,将 20%用于测试。对于时间序列数据,值的顺序是重要的。因此,我们不会随机分配数据集到训练集和测试集,而是选择在有序观察值列表中的任意拆分点,并创建两个新数据集:

validation_size = 0.2
train_size = int(len(X) * (1-validation_size))
X_train, X_test = X[0:train_size], X[train_size:len(X)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(X)]

5.2. 测试选项和评估指标

为了优化模型的各种超参数,我们使用十折交叉验证(CV)并重新计算结果十次,以考虑部分模型和 CV 过程中的固有随机性。我们将使用均方误差度量来评估算法。这个度量给出了监督回归模型的性能的一个概念。所有这些概念,包括交叉验证和评估指标,都在第四章中有描述:

num_folds = 10
scoring = 'neg_mean_squared_error'

5.3. 比较模型和算法

现在我们已经完成了数据加载并设计了测试框架,我们需要选择一个模型。

5.3.1. 来自 Scikit-learn 的机器学习模型

在这一步中,使用 sklearn 包来实现监督回归模型:

回归和树回归算法

models = []
models.append(('LR', LinearRegression()))
models.append(('LASSO', Lasso()))
models.append(('EN', ElasticNet()))
models.append(('KNN', KNeighborsRegressor()))
models.append(('CART', DecisionTreeRegressor()))
models.append(('SVR', SVR()))

神经网络算法

models.append(('MLP', MLPRegressor()))

集成模型

# Boosting methods
models.append(('ABR', AdaBoostRegressor()))
models.append(('GBR', GradientBoostingRegressor()))
# Bagging methods
models.append(('RFR', RandomForestRegressor()))
models.append(('ETR', ExtraTreesRegressor()))

一旦我们选择了所有模型,我们就会对每个模型进行循环。首先,我们进行k次交叉验证分析。接下来,我们在整个训练和测试数据集上运行模型。

所有算法都使用默认调整参数。我们将计算每个算法的评估指标的平均值和标准差,并将结果收集起来以供后续模型比较使用:

names = []
kfold_results = []
test_results = []
train_results = []
for name, model in models:
    names.append(name)
    ## k-fold analysis:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    #converted mean squared error to positive. The lower the better
    cv_results = -1* cross_val_score(model, X_train, Y_train, cv=kfold, \
      scoring=scoring)
    kfold_results.append(cv_results)
    # Full Training period
    res = model.fit(X_train, Y_train)
    train_result = mean_squared_error(res.predict(X_train), Y_train)
    train_results.append(train_result)
    # Test results
    test_result = mean_squared_error(res.predict(X_test), Y_test)
    test_results.append(test_result)

让我们通过观察交叉验证结果来比较算法:

交叉验证结果

fig = pyplot.figure()
fig.suptitle('Algorithm Comparison: Kfold results')
ax = fig.add_subplot(111)
pyplot.boxplot(kfold_results)
ax.set_xticklabels(names)
fig.set_size_inches(15,8)
pyplot.show()

输出

mlbf 05in05

尽管其中一些模型的结果看起来不错,我们看到线性回归和包括套索回归(LASSO)和弹性网络(EN)的正则化回归似乎表现最佳。这表明因变量和自变量之间存在强烈的线性关系。回到探索性分析,我们看到目标变量与不同滞后 MSFT 变量之间存在良好的相关性和线性关系。

让我们也来看看测试集的误差:

训练和测试误差

# compare algorithms
fig = pyplot.figure()

ind = np.arange(len(names))  # the x locations for the groups
width = 0.35  # the width of the bars

fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.bar(ind - width/2, train_results,  width=width, label='Train Error')
pyplot.bar(ind + width/2, test_results, width=width, label='Test Error')
fig.set_size_inches(15,8)
pyplot.legend()
ax.set_xticks(ind)
ax.set_xticklabels(names)
pyplot.show()

输出

mlbf 05in06

检查训练和测试误差,我们仍然看到线性模型表现更强。一些算法,如决策树回归器(CART),在训练数据上过拟合,并在测试集上产生非常高的误差。集成模型如梯度提升回归(GBR)和随机森林回归(RFR)具有低偏差但高方差。我们还看到人工神经网络算法(在图表中显示为 MLP)在训练和测试集中显示出较高的误差。这可能是由于 ANN 未准确捕捉到变量之间的线性关系,超参数设置不正确或模型训练不足造成的。从交叉验证结果和散点图中,我们原始的直觉似乎也显示出线性模型的更好性能。

现在我们来看一些可以使用的时间序列和深度学习模型。一旦我们创建了这些模型,我们将比较它们与基于监督回归的模型的性能。由于时间序列模型的性质,我们无法进行k-fold 分析。尽管如此,我们仍然可以根据完整的训练和测试结果来比较我们的结果与其他模型的表现。

5.3.2. 时间序列模型:ARIMA 和 LSTM

到目前为止使用的模型已经通过使用时间延迟方法嵌入了时间序列组件,其中滞后变量被包括为一个独立变量之一。然而,对于基于时间序列的模型,我们不需要 MSFT 的滞后变量作为独立变量。因此,作为第一步,我们在这些模型中删除了 MSFT 的先前回报。我们将所有其他变量作为这些模型中的外生变量使用。

让我们首先通过只使用相关变量作为外生变量来为 ARIMA 模型准备数据集:

X_train_ARIMA=X_train.loc[:, ['GOOGL', 'IBM', 'DEXJPUS', 'SP500', 'DJIA', \
'VIXCLS']]
X_test_ARIMA=X_test.loc[:, ['GOOGL', 'IBM', 'DEXJPUS', 'SP500', 'DJIA', \
'VIXCLS']]
tr_len = len(X_train_ARIMA)
te_len = len(X_test_ARIMA)
to_len = len (X)

现在我们使用(1,0,0)的阶数配置 ARIMA 模型,并将独立变量作为模型中的外生变量。在这种使用外生变量的 ARIMA 模型版本中,被称为ARIMAX模型,其中"X"代表外生变量:

modelARIMA=ARIMA(endog=Y_train,exog=X_train_ARIMA,order=[1,0,0])
model_fit = modelARIMA.fit()

现在我们拟合 ARIMA 模型:

error_Training_ARIMA = mean_squared_error(Y_train, model_fit.fittedvalues)
predicted = model_fit.predict(start = tr_len -1 ,end = to_len -1, \
  exog = X_test_ARIMA)[1:]
error_Test_ARIMA = mean_squared_error(Y_test,predicted)
error_Test_ARIMA

输出

0.0005931919240399084

这个 ARIMA 模型的误差是合理的。

现在让我们为 LSTM 模型准备数据集。我们需要将数据以所有输入变量和输出变量的数组形式准备好。

LSTM 背后的逻辑是从前一天获取数据(当天的所有其他特征数据——相关资产和 MSFT 的滞后变量),然后试图预测下一天。然后我们将一天的窗口移动一天,并再次预测下一天。我们像这样在整个数据集上迭代(当然是以批次形式)。以下代码将创建一个数据集,其中X是给定时间(t)的自变量集合,Y是下一个时间(t + 1)的目标变量:

seq_len = 2 #Length of the seq for the LSTM

Y_train_LSTM, Y_test_LSTM = np.array(Y_train)[seq_len-1:], np.array(Y_test)
X_train_LSTM = np.zeros((X_train.shape[0]+1-seq_len, seq_len, X_train.shape[1]))
X_test_LSTM = np.zeros((X_test.shape[0], seq_len, X.shape[1]))
for i in range(seq_len):
    X_train_LSTM[:, i, :] = np.array(X_train)[i:X_train.shape[0]+i+1-seq_len, :]
    X_test_LSTM[:, i, :] = np.array(X)\
    [X_train.shape[0]+i-1:X.shape[0]+i+1-seq_len, :]

接下来,我们创建 LSTM 架构。正如我们所见,LSTM 的输入为X_train_LSTM,进入 LSTM 层的 50 个隐藏单元,然后转换为单一输出——股票回报值。超参数(即学习率、优化器、激活函数等)详见书籍的第三章:

# LSTM Network
def create_LSTMmodel(learn_rate = 0.01, momentum=0):
        # create model
    model = Sequential()
    model.add(LSTM(50, input_shape=(X_train_LSTM.shape[1],\
      X_train_LSTM.shape[2])))
    #More cells can be added if needed
    model.add(Dense(1))
    optimizer = SGD(lr=learn_rate, momentum=momentum)
    model.compile(loss='mse', optimizer='adam')
    return model
LSTMModel = create_LSTMmodel(learn_rate = 0.01, momentum=0)
LSTMModel_fit = LSTMModel.fit(X_train_LSTM, Y_train_LSTM, \
  validation_data=(X_test_LSTM, Y_test_LSTM),\
  epochs=330, batch_size=72, verbose=0, shuffle=False)

现在我们用数据拟合 LSTM 模型,并同时查看训练集和测试集中模型性能指标的变化。

pyplot.plot(LSTMModel_fit.history['loss'], label='train', )
pyplot.plot(LSTMModel_fit.history['val_loss'], '--',label='test',)
pyplot.legend()
pyplot.show()

输出

mlbf 05in07

error_Training_LSTM = mean_squared_error(Y_train_LSTM,\
  LSTMModel.predict(X_train_LSTM))
predicted = LSTMModel.predict(X_test_LSTM)
error_Test_LSTM = mean_squared_error(Y_test,predicted)

现在,为了比较时间序列模型和深度学习模型,我们将这些模型的结果附加到基于监督回归的模型的结果中:

test_results.append(error_Test_ARIMA)
test_results.append(error_Test_LSTM)

train_results.append(error_Training_ARIMA)
train_results.append(error_Training_LSTM)

names.append("ARIMA")
names.append("LSTM")

输出

mlbf 05in08

观察图表,我们发现基于时间序列的 ARIMA 模型与线性监督回归模型(如线性回归(LR)、套索回归(LASSO)和弹性网(EN))可相媲美。这主要是由于之前讨论过的强线性关系。LSTM 模型表现良好;然而,在测试集中,ARIMA 模型优于 LSTM 模型。因此,我们选择 ARIMA 模型进行模型调优。

6. 模型调优和网格搜索

让我们进行 ARIMA 模型的模型调优。

监督学习或时间序列模型的模型调优和网格搜索逻辑

关于所有基于监督学习模型的网格搜索详细实现,包括 ARIMA 和 LSTM 模型,在《Regression-Master》模板中的GitHub 存储库提供了信息。有关 ARIMA 和 LSTM 模型的网格搜索,请参考《Regression-Master》模板中的“ARIMA 和 LSTM 网格搜索”部分。

ARIMA 模型通常表示为 ARIMA(p,d,q)模型,其中p为自回归部分的阶数,d为首次差分的程度,q为移动平均部分的阶数。ARIMA 模型的阶数设置为(1,0,0)。因此,我们通过不同的pdq组合进行网格搜索,选择最小化拟合误差的组合:

def evaluate_arima_model(arima_order):
    #predicted = list()
    modelARIMA=ARIMA(endog=Y_train,exog=X_train_ARIMA,order=arima_order)
    model_fit = modelARIMA.fit()
    error = mean_squared_error(Y_train, model_fit.fittedvalues)
    return error

# evaluate combinations of p, d and q values for an ARIMA model
def evaluate_models(p_values, d_values, q_values):
    best_score, best_cfg = float("inf"), None
    for p in p_values:
        for d in d_values:
            for q in q_values:
                order = (p,d,q)
                try:
                    mse = evaluate_arima_model(order)
                    if mse < best_score:
                        best_score, best_cfg = mse, order
                    print('ARIMA%s MSE=%.7f' % (order,mse))
                except:
                    continue
    print('Best ARIMA%s MSE=%.7f' % (best_cfg, best_score))

# evaluate parameters
p_values = [0, 1, 2]
d_values = range(0, 2)
q_values = range(0, 2)
warnings.filterwarnings("ignore")
evaluate_models(p_values, d_values, q_values)

输出

ARIMA(0, 0, 0) MSE=0.0009879
ARIMA(0, 0, 1) MSE=0.0009721
ARIMA(1, 0, 0) MSE=0.0009696
ARIMA(1, 0, 1) MSE=0.0009685
ARIMA(2, 0, 0) MSE=0.0009684
ARIMA(2, 0, 1) MSE=0.0009683
Best ARIMA(2, 0, 1) MSE=0.0009683

我们看到,在网格搜索中测试的所有组合中,ARIMA 模型的阶数为(2,0,1)时表现最佳,尽管与其他组合相比,均方误差(MSE)没有显著差异。这意味着具有自回归滞后两期和移动平均一期的模型能够产生最佳结果。我们不应忘记的是,模型中还有其他影响最佳 ARIMA 模型阶数的外生变量。

7. 完成模型

在最后一步,我们将在测试集上检查最终的模型。

7.1. 测试数据集的结果

# prepare model
modelARIMA_tuned=ARIMA(endog=Y_train,exog=X_train_ARIMA,order=[2,0,1])
model_fit_tuned = modelARIMA_tuned.fit()
# estimate accuracy on validation set
predicted_tuned = model_fit.predict(start = tr_len -1 ,\
  end = to_len -1, exog = X_test_ARIMA)[1:]
print(mean_squared_error(Y_test,predicted_tuned))

输出

0.0005970582461404503

模型在测试集上的均方误差(MSE)看起来很好,实际上比训练集的要小。

在最后一步,我们将可视化所选模型的输出,并将建模数据与实际数据进行比较。为了可视化图表,我们将回报时间序列转换为价格时间序列。为了简化起见,我们假设测试集开始时的价格为一。让我们看一下实际数据与预测数据的图表:

# plotting the actual data versus predicted data
predicted_tuned.index = Y_test.index
pyplot.plot(np.exp(Y_test).cumprod(), 'r', label='actual',)

# plotting t, a separately
pyplot.plot(np.exp(predicted_tuned).cumprod(), 'b--', label='predicted')
pyplot.legend()
pyplot.rcParams["figure.figsize"] = (8,5)
pyplot.show()

mlbf 05in09

从图表可以看出,模型完美地捕捉到了趋势。预测的系列波动性较实际时间序列较小,并且在测试集的前几个月与实际数据一致。需要注意的是,该模型的目的是根据截至当天观察到的数据计算第二天的回报,而不是根据当前数据预测未来数天的股票价格。因此,随着我们远离测试集开始时的时间点,预期会与实际数据有所偏差。该模型在开始后的几个月内表现良好,但在测试集开始后六到七个月,与实际数据的偏差逐渐增加。

结论

我们可以得出结论,简单模型——如线性回归、正则化回归(即 Lasso 和弹性网络)——以及时间序列模型,例如 ARIMA,是解决股票价格预测问题的有前途的建模方法。这种方法帮助我们应对在金融预测问题中的过拟合和欠拟合等一些关键挑战。

我们还应该注意,我们可以使用更广泛的指标,如市盈率(P/E)、交易量、技术指标或新闻数据,这可能会带来更好的结果。我们将在本书的某些未来案例研究中演示这一点。

总的来说,我们创建了一个监督回归和时间序列建模框架,可以利用历史数据进行股票价格预测。这个框架在冒任何资本风险之前生成结果,以分析风险和盈利能力。

案例研究 2:衍生品定价

在计算金融和风险管理中,常用几种数值方法(例如有限差分、傅里叶方法和蒙特卡洛模拟)来估计金融衍生品的价值。

布莱克-斯科尔斯公式 可能是衍生品定价中被引用和使用最广泛的模型之一。该公式的许多变体和扩展被用来定价许多类型的金融衍生品。然而,该模型基于几个假设。它假设衍生品价格的特定运动形式,即几何布朗运动(GBM)。它还假设在期权到期时有条件支付和经济约束,例如无套利条件。几种其他衍生品定价模型也有类似不切实际的模型假设。金融从业者清楚这些假设在实践中是不符合的,因此从这些模型得出的价格会通过从业者的判断进一步调整。

传统衍生品定价模型的另一个方面是模型校准,通常不是通过历史资产价格,而是通过衍生品价格(即通过将高交易量期权的市场价格与数学模型的衍生品价格进行匹配来进行)。在模型校准过程中,需要确定成千上万个衍生品价格以拟合模型的参数,整个过程非常耗时。在金融风险管理中,尤其是在处理实时风险管理(例如高频交易)时,高效的数值计算变得越来越重要。然而,由于需要高效的计算,某些高质量的资产模型和方法在传统衍生品定价模型的模型校准过程中被舍弃。

机器学习有望解决与不切实际的模型假设和低效的模型校准相关的这些缺点。机器学习算法具有在几乎没有理论假设的情况下处理更多微妙细节的能力,并且可以在包括摩擦力的世界中有效地用于衍生品定价。随着硬件的进步,我们可以在高性能 CPU、GPU 和其他专用硬件上训练机器学习模型,实现与传统衍生品定价模型相比几个数量级的速度提升。

此外,市场数据丰富,因此可以训练机器学习算法来学习市场中生成衍生品价格的集体功能。机器学习模型能够捕捉数据中不可通过其他统计方法获取的微妙非线性关系。

在这个案例研究中,我们从机器学习的角度来看待衍生品定价,并使用基于监督回归的模型来从模拟数据中定价期权。这里的主要思想是建立一个用于衍生品定价的机器学习框架。实现高精度的机器学习模型意味着我们可以利用机器学习的高效数值计算来进行衍生品定价,减少底层模型假设。

衍生品定价机器学习模型开发蓝图

1. 问题定义

在我们用于这个案例研究的监督回归框架中,预测变量是期权的价格,预测变量是作为输入用于布莱克-肖尔斯期权定价模型的市场数据。

选择用于估计期权市场价格的变量包括股票价格、行权价格、到期时间、波动率、利率和股息收益率。本案例研究的预测变量是使用随机输入生成,并将其输入到众所周知的布莱克-肖尔斯模型中得出的。

根据布莱克-肖尔斯期权定价模型,看涨期权的价格被定义为方程 5-1。

方程 5-1. 看涨期权的布莱克-肖尔斯方程

S e qτ Φ ( d 1 ) e rτ K Φ ( d 2 )

具体到

d 1 = ln(S/K)+(rq+σ 2 /2)τ στ

d 2 = ln(S/K)+(rqσ 2 /2)τ στ = d 1 σ τ

其中我们有股票价格S;行权价格K;无风险利率r;年度股息收益率q;到期时间τ = T t(表示为一年的无量纲分数);以及波动率σ

为了简化逻辑,我们将金钱度定义为M = K / S,并以每单位当前股票价格为单位查看价格。我们还将q设为 0。

这简化了以下公式:

e qτ Φ ln(M)+(r+σ 2 /2)τ στ e rτ M Φ ln(M)+(rσ 2 /2)τ στ

观察上述方程,输入布莱克-肖尔斯期权定价模型的参数包括金钱度、无风险利率、波动率和到期时间。

在衍生品市场中起核心作用的参数是波动率,因为它直接与股票价格的波动相关。随着波动率的增加,股价波动的范围比低波动率股票要大得多。

在期权市场上,没有一个单一的波动率用于定价所有期权。该波动率取决于期权的货币性和到期时间。一般来说,波动率随着到期时间的增加和货币性的增加而增加。这种行为被称为波动率微笑/偏斜。我们通常从市场上现有的期权价格中推导波动率,这种波动率称为“隐含”波动率。在本练习中,我们假设波动率曲面的结构,并使用方程 5-2,其中波动率取决于期权的货币性和到期时间来生成期权波动率曲面。

方程 5-2. 波动率方程

σ ( M , τ ) = σ 0 + α τ + β (M1) 2

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

加载 Python 包与本章第 1 个案例类似。有关详细信息,请参阅本案例研究的 Jupyter 笔记本。

2.2. 定义函数和参数

要生成数据集,我们需要模拟输入参数,然后创建预测变量。

首先我们定义常量参数。用于波动率曲面的常量参数如下所示。这些参数不太可能对期权价格产生重大影响;因此,这些参数被设置为一些有意义的值:

true_alpha = 0.1
true_beta = 0.1
true_sigma0 = 0.2

无风险利率,作为布莱克-斯科尔斯期权定价模型的输入,定义如下:

risk_free_rate = 0.05

波动率和期权定价函数

在这一步中,我们定义函数来计算看涨期权的波动率和价格,如方程 5-1 和 5-2 所示:

def option_vol_from_surface(moneyness, time_to_maturity):
    return true_sigma0 + true_alpha * time_to_maturity +\
     true_beta * np.square(moneyness - 1)

def call_option_price(moneyness, time_to_maturity, option_vol):
    d1=(np.log(1/moneyness)+(risk_free_rate+np.square(option_vol))*\
    time_to_maturity)/ (option_vol*np.sqrt(time_to_maturity))
    d2=(np.log(1/moneyness)+(risk_free_rate-np.square(option_vol))*\
    time_to_maturity)/(option_vol*np.sqrt(time_to_maturity))
    N_d1 = norm.cdf(d1)
    N_d2 = norm.cdf(d2)

    return N_d1 - moneyness * np.exp(-risk_free_rate*time_to_maturity) * N_d2

2.3. 数据生成

我们按以下步骤生成输入和输出变量:

  • 到期时间(Ts)使用np.random.random函数生成,该函数生成介于零和一之间的均匀随机变量。

  • 货币性(Ks)使用np.random.randn函数生成,该函数生成一个正态分布的随机变量。随机数乘以 0.25 生成罢工价格偏离现货价格的偏差,¹³整体方程确保货币性大于零。

  • 波动率(sigma)根据到期时间和货币性使用方程 5-2 生成。

  • 期权价格使用方程 5-1 来生成布莱克-斯科尔斯期权价格。

总共我们生成了 10,000 个数据点(N):

N = 10000

Ks = 1+0.25*np.random.randn(N)
Ts = np.random.random(N)
Sigmas = np.array([option_vol_from_surface(k,t) for k,t in zip(Ks,Ts)])
Ps = np.array([call_option_price(k,t,sig) for k,t,sig in zip(Ks,Ts,Sigmas)])

现在我们创建预测和预测变量的变量:

Y = Ps
X = np.concatenate([Ks.reshape(-1,1), Ts.reshape(-1,1), Sigmas.reshape(-1,1)], \
axis=1)

dataset = pd.DataFrame(np.concatenate([Y.reshape(-1,1), X], axis=1),
                       columns=['Price', 'Moneyness', 'Time', 'Vol'])

3. 探索性数据分析

让我们看一下我们拥有的数据集。

3.1. 描述统计

dataset.head()

输出

价格 货币性 时间 波动率
0 1.390e-01 0.898 0.221 0.223
1 3.814e-06 1.223 0.052 0.210
2 1.409e-01 0.969 0.391 0.239
3 1.984e-01 0.950 0.628 0.263
4 2.495e-01 0.914 0.810 0.282

数据集包含价格—即期权的价格,也是预测变量—以及货币性(行权价与现价的比率)、到期时间波动率,这些是模型中的特征。

3.2. 数据可视化

在这一步中,我们查看散点图以理解不同变量之间的相互作用:¹⁴

pyplot.figure(figsize=(15,15))
scatter_matrix(dataset,figsize=(12,12))
pyplot.show()

Output

mlbf 05in10

散点图揭示了变量之间非常有趣的依赖关系。让我们看看图表的第一行,以查看价格与不同变量之间的关系。我们观察到随着货币性的减少(即与股票价格相比,行权价格的减少),价格增加,这与前一节描述的理由一致。观察价格与到期时间的关系,我们看到期权价格增加。价格与波动率图表也显示出价格随波动率增加而增加。然而,期权价格似乎与大多数变量呈非线性关系。这意味着我们预计我们的非线性模型将比线性模型做得更好。

另一个有趣的关系是波动率和行权价格之间的关系。当我们远离一个货币性时,我们观察到更高的波动率。这种行为是由我们之前定义的波动率函数所显示的,并说明了波动率笑曲线/偏斜。

4. 数据准备和分析

我们在前面的步骤中执行了大部分数据准备工作(即获取依赖变量和独立变量)。在这一步中,我们查看特征重要性。

4.1. 单变量特征选择

我们首先单独查看每个特征,并使用单变量回归拟合作为标准,查看最重要的变量:

bestfeatures = SelectKBest(k='all', score_func=f_regression)
fit = bestfeatures.fit(X,Y)
dfscores = pd.DataFrame(fit.scores_)
dfcolumns = pd.DataFrame(['Moneyness', 'Time', 'Vol'])
#concat two dataframes for better visualization
featureScores = pd.concat([dfcolumns,dfscores],axis=1)
featureScores.columns = ['Specs','Score']  #naming the dataframe columns
featureScores.nlargest(10,'Score').set_index('Specs')

Output

Moneyness : 30282.309
Vol : 2407.757
Time : 1597.452

我们观察到货币性是期权价格最重要的变量,其次是波动率和到期时间。鉴于只有三个预测变量,我们保留所有变量进行建模。

5. 评估模型

5.1. 训练集-测试集分离和评估指标

首先,我们分离训练集和测试集:

validation_size = 0.2

train_size = int(len(X) * (1-validation_size))
X_train, X_test = X[0:train_size], X[train_size:len(X)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(X)]

我们使用预构建的 sklearn 模型对我们的训练数据进行k-fold 分析。然后,我们在完整的训练数据上训练模型,并将其用于测试数据的预测。我们将使用均方误差指标评估算法。k-fold 分析和评估指标的参数定义如下:

num_folds = 10
seed = 7
scoring = 'neg_mean_squared_error'

5.2. 比较模型和算法

现在我们已经完成了数据加载并设计了测试工具,我们需要从一系列监督回归模型中选择一个模型。

Linear models and regression trees

models = []
models.append(('LR', LinearRegression()))
models.append(('KNN', KNeighborsRegressor()))
models.append(('CART', DecisionTreeRegressor()))
models.append(('SVR', SVR()))

Artificial neural network

models.append(('MLP', MLPRegressor()))

Boosting and bagging methods

# Boosting methods
models.append(('ABR', AdaBoostRegressor()))
models.append(('GBR', GradientBoostingRegressor()))
# Bagging methods
models.append(('RFR', RandomForestRegressor()))
models.append(('ETR', ExtraTreesRegressor()))

一旦我们选择了所有的模型,我们对每个模型进行循环。首先,我们进行k-fold 分析。接下来,我们在整个训练和测试数据集上运行模型。

算法使用默认调整参数。我们将计算错误度量的平均值和标准差,并保存结果供以后使用。

输出

mlbf 05in11

k 折分析步骤的 Python 代码类似于案例研究 1 中使用的代码。读者也可以参考代码库中此案例研究的 Jupyter 笔记本以获取更多详细信息。让我们看看训练集中模型的性能。

我们清楚地看到,非线性模型(包括分类和回归树(CART)、集成模型以及在上图中由 MLP 表示的人工神经网络)比线性算法表现得更好。这是合理的,考虑到我们在散点图中观察到的非线性关系。

人工神经网络(ANN)具有通过快速实验和部署时间(定义、训练、测试、推断)来建模任何函数的自然能力。在复杂的衍生品定价情况下,ANN 可以有效地使用。因此,在所有表现良好的模型中,我们选择 ANN 进行进一步分析。

6. 模型调优和最终模型的确定

确定 ANN 中间层节点的适当数量更像是一门艺术而不是科学,正如在第三章中讨论的那样。中间层中过多的节点,以及因此而产生的过多连接,会导致神经网络仅仅记忆输入数据而失去泛化能力。因此,增加中间层节点的数量将提高训练集上的性能,而减少中间层节点的数量将改善对新数据集的性能。

正如在第三章中讨论的那样,ANN 模型还有几个超参数,如学习率、动量、激活函数、迭代次数和批次大小。在网格搜索过程中,可以调整所有这些超参数。然而,在这一步骤中,我们简化地只对隐藏层的数量进行网格搜索。对其他超参数进行网格搜索的方法与下面代码片段中描述的相同:

'''
hidden_layer_sizes : tuple, length = n_layers - 2, default (100,)
 The ith element represents the number of neurons in the ith
 hidden layer.
'''
param_grid={'hidden_layer_sizes': [(20,), (50,), (20,20), (20, 30, 20)]}
model = MLPRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, \
  cv=kfold)
grid_result = grid.fit(X_train, Y_train)
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

输出

Best: -0.000024 using {'hidden_layer_sizes': (20, 30, 20)}
-0.000580 (0.000601) with: {'hidden_layer_sizes': (20,)}
-0.000078 (0.000041) with: {'hidden_layer_sizes': (50,)}
-0.000090 (0.000140) with: {'hidden_layer_sizes': (20, 20)}
-0.000024 (0.000011) with: {'hidden_layer_sizes': (20, 30, 20)}

最佳模型有三层,分别为 20、30 和 20 个节点。因此,我们按照这个配置准备一个模型,并检查其在测试集上的表现。这是一个关键步骤,因为更多的层可能导致过拟合,并在测试集上表现不佳。

# prepare model
model_tuned = MLPRegressor(hidden_layer_sizes=(20, 30, 20))
model_tuned.fit(X_train, Y_train)
# estimate accuracy on validation set
# transform the validation dataset
predictions = model_tuned.predict(X_test)
print(mean_squared_error(Y_test, predictions))

输出

3.08127276609567e-05

我们看到均方根误差(RMSE)为 3.08e-5,小于一分钱。因此,ANN 模型非常适合拟合 Black-Scholes 期权定价模型。增加更多层和调整其他超参数可能会使 ANN 模型更好地捕捉数据中的复杂关系和非线性。总体而言,结果表明,ANN 可以用于训练与市场价格匹配的期权定价模型。

7. 额外分析:移除波动率数据

作为额外分析,我们试图在没有波动率数据的情况下预测价格,这使得过程更加复杂。如果模型表现良好,我们将取消之前描述的波动率函数的需求。在这一步骤中,我们进一步比较线性模型和非线性模型的表现。在下面的代码片段中,我们从预测变量的数据集中移除波动率变量,并重新定义训练集和测试集:

X = X[:, :2]
validation_size = 0.2
train_size = int(len(X) * (1-validation_size))
X_train, X_test = X[0:train_size], X[train_size:len(X)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(X)]

接下来,我们使用新数据集(除了正则化回归模型),使用与之前相同的参数和类似的 Python 代码来运行一系列模型。移除波动率数据后所有模型的表现如下:

mlbf 05in12

从结果来看,我们得出与之前类似的结论,看到线性回归的表现不佳,而集成模型和 ANN 模型表现良好。现在线性回归的表现甚至比以前更差。然而,ANN 和其他集成模型的表现与之前的表现差异不大。这表明,波动率的信息很可能被其他变量捕捉到,例如货币性和到期时间。总体而言,这是个好消息,因为这意味着可能只需要更少的变量就能达到相同的性能。

结论

我们知道,衍生品定价是一个非线性问题。如预期的那样,我们的线性回归模型表现不如非线性模型好,而非线性模型的整体表现非常出色。我们还观察到,移除波动率会增加线性回归预测问题的难度。然而,诸如集成模型和人工神经网络(ANN)等非线性模型仍能在预测过程中表现良好。这表明,可能可以避开期权波动率曲面的开发,并且用更少的变量达到良好的预测。

我们发现,人工神经网络(ANN)可以高度准确地复制 Black-Scholes 期权定价公式,这意味着我们可以利用机器学习在衍生品定价中进行高效的数值计算,而无需依赖传统衍生品定价模型中不切实际的假设。ANN 及其相关的机器学习架构可以轻松扩展到实际世界中的衍生品定价,而无需了解衍生品定价理论。与传统衍生品定价模型相比,机器学习技术的使用可以大大加快衍生品定价的速度。我们可能需要为这种额外速度付出的代价是一些精度损失。然而,从实际角度来看,这种降低的准确性通常仍然在合理的限度内,并且可以被接受。新技术已经使 ANN 的使用变得普遍化,因此银行、对冲基金和金融机构探索这些模型用于衍生品定价可能是值得的。

案例研究 3:投资者风险容忍度与智能投顾

投资者的风险容忍度是投资组合管理过程中资产配置和再平衡步骤中最重要的输入之一。有多种风险评估工具采用不同的方法来理解投资者的风险容忍度。大多数这些方法包括定性判断,并涉及大量的手动工作。在大多数情况下,投资者的风险容忍度是基于风险容忍度问卷来决定的。

几项研究表明,这些风险容忍度问卷易于出错,因为投资者受到行为偏见的影响,在压力下市场时尤其如此。此外,考虑到这些问卷必须由投资者手动完成,它们排除了全自动化投资管理过程的可能性。

机器学习能否比风险容忍问卷更好地理解投资者的风险偏好?机器学习是否可以通过剔除客户而自动化整个投资组合管理过程?是否可以编写算法为客户开发一个人格概要,以更好地反映他们在不同市场情景下的处理方式?

本案例研究的目标是回答这些问题。我们首先构建了一个基于监督回归的模型来预测投资者的风险容忍度。然后我们在 Python 中构建了一个智能投顾仪表板,并在仪表板中实现了风险容忍度预测模型。总体目的是展示如何通过机器学习自动化投资组合管理过程中的手动步骤。这对智能投顾来说可能是非常有用的。

仪表板是 Robo-Advisor 的关键功能之一,因为它提供对重要信息的访问,并允许用户在没有任何人的依赖的情况下与他们的帐户交互,使投资组合管理过程高效。

图 5-6 提供了为这个案例研究构建的 Robo-Advisor 仪表板的快速概览。该仪表板为投资者执行端到端资产配置,嵌入了本案例研究中构建的基于机器学习的风险容忍模型。

mlbf 0506

图 5-6. Robo-Advisor 仪表板

这个仪表板是用 Python 构建的,并在本案例研究的附加步骤中详细描述。虽然它是在 Robo-Advisor 的背景下构建的,但它可以扩展到金融领域的其他领域,并嵌入其他案例研究中讨论的机器学习模型,为金融决策者提供一个用于分析和解释模型结果的图形界面。

建模投资者风险容忍度并启用基于机器学习的 Robo-Advisor 的蓝图

1. 问题定义

在本案例研究中使用的监督回归框架中,预测变量是个体的“真实”风险容忍度,¹⁵ 而预测变量是个体的人口统计、财务和行为属性。

本案例研究使用的数据来自消费者金融调查(SCF),由美联储委员会进行。 调查包括关于 2007 年(危机前)和 2009 年(危机后)相同个体的家庭人口统计、净值、金融和非金融资产的响应。 这使我们能够看到每个家庭在 2008 年全球金融危机后的配置如何改变。 有关此调查的更多信息,请参阅数据字典

2. 入门—加载数据和 Python 软件包

2.1. 载入 Python 软件包

关于加载标准 Python 软件包的详细信息已在先前的案例研究中提供。 有关更多详细信息,请参阅本案例研究的 Jupyter 笔记本。

2.2. 载入数据

在这一步中,我们从消费者金融调查中加载数据并查看数据形状:

# load dataset
dataset = pd.read_excel('SCFP2009panel.xlsx')

让我们看一下数据的规模:

dataset.shape

输出

(19285, 515)

如我们所见,数据集共有 19,285 条观察值,其中包含 515 列。 列的数量表示特征的数量。

3. 数据准备与特征选择

在这一步中,我们准备用于建模的预测变量和预测器变量。

3.1. 准备预测变量

在第一步中,我们准备了预测变量,即真实风险容忍度。

计算真实风险容忍的步骤如下:

  1. 计算调查数据中所有个人的风险资产和无风险资产。 风险资产和无风险资产的定义如下:

    风险资产

    投资于共同基金、股票和债券。

    无风险资产

    检查和储蓄余额、存款证书和其他现金余额及等价物。

  2. 将一个人的风险资产与总资产的比率(其中总资产是风险资产和无风险资产的总和)作为个人风险容忍度的衡量标准。¹⁶ 从 SCF 中,我们获取了 2007 年和 2009 年个人的风险和无风险资产数据。我们使用这些数据,并将 2007 年与 2009 年的股票指数(S&P500)的价格标准化风险资产,以获取风险容忍度。

  3. 识别“聪明”的投资者。有些文献将聪明的投资者描述为在市场变动期间不改变其风险容忍度的投资者。因此,我们认为在 2007 年和 2009 年之间其风险容忍度变化不超过 10%的投资者为聪明的投资者。当然,这是一个定性判断,还可以有几种其他定义聪明投资者的方法。然而,正如前面提到的,除了得出真正风险容忍度的精确定义之外,本案例研究的目的是展示机器学习的使用,并提供一个基于机器学习的投资组合管理框架,可以进一步用于更详细的分析。

让我们计算预测变量。首先,我们获取风险资产和无风险资产,并计算以下代码片段中的 2007 年和 2009 年的风险容忍度:

# Compute the risky assets and risk-free assets for 2007
dataset['RiskFree07']= dataset['LIQ07'] + dataset['CDS07'] + dataset['SAVBND07']\
 + dataset['CASHLI07']
dataset['Risky07'] = dataset['NMMF07'] + dataset['STOCKS07'] + dataset['BOND07']

# Compute the risky assets and risk-free assets for 2009
dataset['RiskFree09']= dataset['LIQ09'] + dataset['CDS09'] + dataset['SAVBND09']\
+ dataset['CASHLI09']
dataset['Risky09'] = dataset['NMMF09'] + dataset['STOCKS09'] + dataset['BOND09']

# Compute the risk tolerance for 2007
dataset['RT07'] = dataset['Risky07']/(dataset['Risky07']+dataset['RiskFree07'])

#Average stock index for normalizing the risky assets in 2009
Average_SP500_2007=1478
Average_SP500_2009=948

# Compute the risk tolerance for 2009
dataset['RT09'] = dataset['Risky09']/(dataset['Risky09']+dataset['RiskFree09'])*\
                (Average_SP500_2009/Average_SP500_2007)

让我们查看数据的详细信息:

dataset.head()

Output

mlbf 05in13

上述数据显示了数据集中的 521 列中的一些列。

让我们计算 2007 年和 2009 年之间风险容忍度的百分比变化:

dataset['PercentageChange'] = np.abs(dataset['RT09']/dataset['RT07']-1)

接下来,我们删除包含“NA”或“NaN”的行:

# Drop the rows containing NA
dataset=dataset.dropna(axis=0)

dataset=dataset[~dataset.isin([np.nan, np.inf, -np.inf]).any(1)]

让我们调查个人在 2007 年和 2009 年的风险容忍度行为。首先我们看看 2007 年的风险容忍度:

sns.distplot(dataset['RT07'], hist=True, kde=False,
             bins=int(180/5), color = 'blue',
             hist_kws={'edgecolor':'black'})

Output

mlbf 05in14

查看 2007 年的风险容忍度,我们发现许多个人的风险容忍度接近 1,这意味着投资更偏向于风险资产。现在让我们看看 2009 年的风险容忍度:

sns.distplot(dataset['RT09'], hist=True, kde=False,
             bins=int(180/5), color = 'blue',
             hist_kws={'edgecolor':'black'})

Output

mlbf 05in15

显然,个人在危机后的行为发生了逆转。总体风险容忍度减少,这表现在 2009 年风险容忍度接近零的家庭比例过大。这些个人的大多数投资都是在无风险资产上。

在下一步中,我们挑选了在 2007 年和 2009 年之间风险容忍度变化少于 10%的聪明投资者,如“3.1. 准备预测变量”中所述:

dataset3 = dataset[dataset['PercentageChange']<=.1]

我们将这些聪明投资者在 2007 年和 2009 年之间的平均风险容忍度作为真正的风险容忍度:

dataset3['TrueRiskTolerance'] = (dataset3['RT07'] + dataset3['RT09'])/2

这是本案例研究的预测变量。

让我们舍弃其他可能不需要的标签:

dataset3.drop(labels=['RT07', 'RT09'], axis=1, inplace=True)
dataset3.drop(labels=['PercentageChange'], axis=1, inplace=True)

3.2. 特征选择—限制特征空间

在本节中,我们将探讨压缩特征空间的方法。

3.2.1. 特征消除

为了进一步筛选特征,我们查看数据字典中的描述,仅保留相关特征。

看整个数据,我们在数据集中有超过500个特征。然而,学术文献和行业实践表明,风险容忍度受投资者的人口统计、财务和行为属性的影响很大,如年龄、当前收入、净资产和愿意承担的风险。所有这些属性都包含在数据集中,并在以下部分进行了总结。这些属性被用作预测投资者风险容忍度的特征。

mlbf 05in16

在数据集中,每列包含与属性值对应的数值。详情如下:

AGE

有六个年龄类别,其中 1 代表 35 岁以下,6 代表 75 岁以上。

EDUC

有四个教育类别,其中 1 代表没有高中,4 代表大学学位。

MARRIED

有两个类别来表示婚姻状况,其中 1 代表已婚,2 代表未婚。

OCCU

这代表职业类别。值为 1 代表管理地位,4 代表失业状态。

KIDS

孩子的数量。

WSAVED

这代表个人的支出与收入比,分为三个类别。例如,1 代表支出超过收入。

NWCAT

这代表净资产类别。有五个类别,其中 1 代表净资产低于第 25 百分位数,5 代表净资产高于第 90 百分位数。

INCCL

这代表收入类别。有五个类别,其中 1 代表收入低于$10,000,5 代表收入超过$100,000。

RISK

这代表愿意承担的风险程度,范围从 1 到 4,其中 1 代表最高水平的愿意承担风险。

我们仅保留 2007 年的直观特征,并删除所有中间特征和 2009 年相关特征,因为 2007 年的变量是预测风险容忍度所需的唯一变量:

keep_list2 = ['AGE07','EDCL07','MARRIED07','KIDS07','OCCAT107','INCOME07',\
'RISK07','NETWORTH07','TrueRiskTolerance']

drop_list2 = [col for col in dataset3.columns if col not in keep_list2]

dataset3.drop(labels=drop_list2, axis=1, inplace=True)

现在让我们看一下特征之间的相关性:

# correlation
correlation = dataset3.corr()
plt.figure(figsize=(15,15))
plt.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True,annot=True,cmap='cubehelix')

Output

mlbf 05in17

查看相关图表(GitHub 上有完整版:GitHub),净资产和收入与风险容忍度呈正相关。随着子女数量和婚姻状况的增加,风险容忍度降低。随着愿意承担的风险减少,风险容忍度也减少。随着年龄增长,风险容忍度呈正相关。根据 Hui Wang 和 Sherman Hanna 的论文“随年龄增长风险容忍度减少吗?”,在其他变量保持不变的情况下,随着人们年龄增长,风险容忍度增加(即随着人们年龄增长,投资于风险资产的净财富比例增加)。

因此,总结来说,这些变量与风险承受能力的关系似乎是直观的。

4. 评估模型

4.1. 训练集-测试集分割

让我们将数据分割成训练集和测试集:

Y= dataset3["TrueRiskTolerance"]
X = dataset3.loc[:, dataset3.columns != 'TrueRiskTolerance']
validation_size = 0.2
seed = 3
X_train, X_validation, Y_train, Y_validation = \
train_test_split(X, Y, test_size=validation_size, random_state=seed)

4.2. 测试选项和评估指标

我们使用 R²作为评估指标,并选择 10 作为交叉验证的折数。¹⁷

num_folds = 10
scoring = 'r2'

4.3. 比较模型和算法

接下来,我们选择回归模型套件并进行k-折交叉验证。

回归模型

# spot-check the algorithms
models = []
models.append(('LR', LinearRegression()))
models.append(('LASSO', Lasso()))
models.append(('EN', ElasticNet()))
models.append(('KNN', KNeighborsRegressor()))
models.append(('CART', DecisionTreeRegressor()))
models.append(('SVR', SVR()))
#Ensemble Models
# Boosting methods
models.append(('ABR', AdaBoostRegressor()))
models.append(('GBR', GradientBoostingRegressor()))
# Bagging methods
models.append(('RFR', RandomForestRegressor()))
models.append(('ETR', ExtraTreesRegressor()))

k-折分析步骤的 Python 代码与之前案例研究类似。读者还可以参考代码库中本案例研究的 Jupyter 笔记本获取更多细节。让我们来看看训练集中模型的表现。

mlbf 05in18

非线性模型的表现优于线性模型,这意味着风险承受能力与用于预测它的变量之间存在非线性关系。鉴于随机森林回归是最佳方法之一,我们将其用于进一步的网格搜索。

5. 模型调优和网格搜索

如在第四章中讨论的,随机森林有许多可以在网格搜索中调整的超参数。然而,在进行网格搜索时,我们将限制在评估器数量(n_estimators),因为它是最重要的超参数之一。它表示随机森林模型中树的数量。理想情况下,应增加到模型不再显示进展为止:

# 8\. Grid search : RandomForestRegressor
'''
n_estimators : integer, optional (default=10)
 The number of trees in the forest.
'''
param_grid = {'n_estimators': [50,100,150,200,250,300,350,400]}
model = RandomForestRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, \
  cv=kfold)
grid_result = grid.fit(X_train, Y_train)
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']

Output

Best: 0.738632 using {'n_estimators': 250}

经过网格搜索后,以 250 为评估器数量的随机森林是最佳模型。

6. 完成模型

让我们来看看在测试数据集上的结果,并检查特征的重要性。

6.1. 测试数据集的结果

我们准备以 250 为评估器数量的随机森林模型:

model = RandomForestRegressor(n_estimators = 250)
model.fit(X_train, Y_train)

让我们来看看训练集的表现:

from sklearn.metrics import r2_score
predictions_train = model.predict(X_train)
print(r2_score(Y_train, predictions_train))

Output

0.9640632406817223

训练集的 R²为 96%,这是一个很好的结果。现在让我们来看看测试集的表现:

predictions = model.predict(X_validation)
print(mean_squared_error(Y_validation, predictions))
print(r2_score(Y_validation, predictions))

Output

0.007781840953471237
0.7614494526639909

根据上述测试集的均方误差和 R²为 76%,随机森林模型在拟合风险承受能力方面表现出色。

6.2. 特征重要性和特征直觉

让我们来看看随机森林模型内变量的特征重要性:

import pandas as pd
import numpy as np
model = RandomForestRegressor(n_estimators= 200,n_jobs=-1)
model.fit(X_train,Y_train)
#use inbuilt class feature_importances of tree based classifiers
#plot graph of feature importances for better visualization
feat_importances = pd.Series(model.feature_importances_, index=X.columns)
feat_importances.nlargest(10).plot(kind='barh')
plt.show()

Output

mlbf 05in19

在图表中,x 轴表示特征重要性的大小。因此,收入和净值,其次是年龄和愿意承担的风险,是决定风险承受能力的关键变量。

6.3. 保存模型以备后用

在这一步中,我们将模型保存以备后用。保存的模型可以直接用于根据输入变量集进行预测。使用 pickle 包的dump模块将模型保存为finalized_model.sav。可以使用load模块加载此保存的模型。

让我们将模型保存为第一步:

# Save Model Using Pickle
from pickle import dump
from pickle import load

# save the model to disk
filename = 'finalized_model.sav'
dump(model, open(filename, 'wb'))

现在让我们加载已保存的模型,并用于预测:

# load the model from disk
loaded_model = load(open(filename, 'rb'))
# estimate accuracy on validation set
predictions = loaded_model.predict(X_validation)
result = mean_squared_error(Y_validation, predictions)
print(r2_score(Y_validation, predictions))
print(result)

Output

0.7683894847939692
0.007555447734714956

7. 附加步骤:机器人顾问仪表板

我们在本案例研究的开头提到了机器人顾问仪表板。机器人顾问仪表板自动化了投资组合管理流程,并旨在解决传统风险容忍度分析的问题。

Python 代码用于机器人顾问仪表板

该机器人顾问仪表板是使用 Python 中的 plotly dash 包构建的。Dash是一个用于构建具有良好用户界面的 Web 应用程序的高效 Python 框架。机器人顾问仪表板的代码已添加到本书的代码库中。代码位于名为“Sample Robo-advisor”的 Jupyter 笔记本中。本案例研究不涉及代码的详细描述。但是,该代码库可用于创建任何新的机器学习增强仪表板。

仪表板有两个面板:

  • 投资者特征的输入

  • 资产配置和投资组合表现

投资者特征的输入

图 5-7 显示了投资者特征的输入面板。该面板收集关于投资者人口统计学、财务和行为属性的所有输入。这些输入用于我们在前面步骤中创建的风险容忍度模型中的预测变量。界面设计为以正确格式输入分类和连续变量。

一旦提交了输入,我们利用保存在“6.3. 保存模型以备后用”中的模型。此模型接受所有输入并生成投资者的风险容忍度(有关更多详细信息,请参阅本书代码库中名为“Sample Robo-advisor”的 Jupyter 笔记本中的predict_riskTolerance函数)。风险容忍度预测模型嵌入在此仪表板中,并在提交输入后按下“计算风险容忍度”按钮时触发。

mlbf 0507

图 5-7. 机器人顾问输入面板

7.2 资产配置和投资组合表现

图 5-8 显示了“资产配置和投资组合表现”面板,该面板执行以下功能:

  • 一旦使用模型计算了风险容忍度,它将显示在此面板的顶部。

  • 在下一步中,我们从下拉菜单中选择我们的投资组合资产。

  • 提交资产列表后,使用传统的均值-方差投资组合分配模型来分配所选资产的投资组合。风险容忍度是此过程的关键输入之一。(有关更多详细信息,请参阅本书代码库中名为“Sample Robo-advisor”的 Jupyter 笔记本中的get_asset_allocation函数。)

  • 仪表板还显示了以100 美元初始投资的分配投资组合的历史表现。

mlbf 0508

图 5-8. 机器人顾问资产配置和投资组合表现面板

虽然仪表板是机器人顾问仪表板的基本版本,但它可以为投资者进行端到端资产配置,并提供选定期间的投资组合视图和历史绩效。在界面和底层模型使用方面,这个原型可以进行多个潜在的增强。仪表板可以增加额外的工具,并加入实时投资组合监控、投资组合再平衡和投资咨询等附加功能。在用于资产配置的底层模型方面,我们使用了传统的均值-方差优化方法,但可以进一步改进为使用基于机器学习技术的分配算法,如特征组合投资组合、层次风险平价或基于强化学习的模型,分别在第 7、8 和 9 章节中描述。风险承受能力模型可以通过使用额外的特征或使用投资者的实际数据而非使用消费者金融调查数据进一步改进。

结论

在这个案例研究中,我们介绍了基于回归的算法,用于计算投资者的风险承受能力,随后演示了该模型在机器人顾问设置中的应用。我们展示了机器学习模型可以在不断变化的市场中客观分析不同投资者的行为,并将这些变化归因于决定风险偏好的相关变量。随着投资者数据量的增加和丰富的机器学习基础设施的可用性,这些模型可能比现有的手动流程更加实用。

我们发现变量与风险承受能力之间存在非线性关系。我们分析了特征的重要性,并发现案例研究的结果非常直观。收入和净值,其次是年龄和愿意承担风险,是决定风险承受能力的关键变量。这些变量被认为是跨学术和行业文献中模拟风险承受能力的关键变量。

通过由机器学习驱动的机器人顾问仪表板,我们展示了数据科学和机器学习在财富管理中的有效结合。机器人顾问和投资经理可以利用这些模型和平台增强投资组合管理流程,借助机器学习的帮助。

案例研究 4:收益率曲线预测

收益率曲线是一条绘制具有相同信用质量但到期日期不同的债券收益率(利率)的线。这条收益率曲线被用作市场上其他债务(如抵押利率或银行贷款利率)的基准。最常报告的收益率曲线比较了 3 个月、2 年、5 年、10 年和 30 年的美国国债。

收益率曲线是固定收益市场的核心。固定收益市场是政府、国家和超国家机构、银行以及私人和公共公司的重要融资来源。此外,收益率曲线对养老基金和保险公司的投资者非常重要。

收益率曲线是债券市场状况的关键表征。投资者密切关注债券市场,因为它是未来经济活动和通货膨胀水平的强有力预测者,这些因素会影响商品、金融资产和房地产的价格。收益率曲线的斜率是短期利率的重要指标,并受到投资者的密切关注。

因此,准确的收益率曲线预测在金融应用中至关重要。已经应用了几种通常用于建模收益率曲线的计量经济学和金融学中常用的统计技术和工具。

在本案例研究中,我们将使用基于监督学习的模型来预测收益率曲线。本案例研究受到曼努埃尔·努内斯等人(2018 年)的论文《固定收益市场中的人工神经网络用于收益率曲线预测》的启发。

总的来说,本案例研究与本章前面介绍的股价预测案例研究相似,但存在以下区别:

  • 我们同时预测多个输出,而不是单个输出。

  • 本案例研究中的预测变量不是回报变量。

  • 鉴于我们已经在案例研究 1 中涵盖了时间序列模型,我们将在本案例研究中专注于人工神经网络进行预测。

使用监督学习模型预测收益率曲线的蓝图

1. 问题定义

在本案例研究中使用的监督回归框架中,收益率曲线的三个期限(1M、5Y 和 30Y)是预测变量。这些期限代表了收益率曲线的短期、中期和长期期限。

我们需要了解什么影响了收益率曲线的变动,因此尽可能将更多信息纳入我们的模型中。作为高层次的概述,除了收益率曲线的历史价格外,我们还考虑了其他可能影响收益率曲线的相关变量。我们考虑的独立或预测变量是:

  • 不同期限的国债收益率的前值。使用的期限为 1 个月、3 个月、1 年、2 年、5 年、7 年、10 年和 30 年。

  • 联邦债务由公众、外国政府和联邦储备所持有的百分比。

  • *Baa 评级债务的公司利差相对于 10 年期国债利率。

联邦债务和公司利差是相关变量,可能对收益率曲线的建模有用。本案例研究使用的数据集从 Yahoo Finance 和FRED中提取。我们将使用自 2010 年以来过去 10 年的每日数据。

在本案例研究结束时,读者将熟悉一个通用的机器学习方法来进行收益率曲线建模,从数据收集和清理到构建和调整不同的模型。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

Python 包的加载与本章其他案例研究类似。更多细节请参考本案例研究的 Jupyter 笔记本。

2.2. 加载数据

下面的步骤演示了使用 Pandas 的DataReader函数加载数据:

# Get the data by webscraping using pandas datareader
tsy_tickers = ['DGS1MO', 'DGS3MO', 'DGS1', 'DGS2', 'DGS5', 'DGS7', 'DGS10',
               'DGS30',
               'TREAST', # Treasury securities held by the Federal Reserve ($MM)
               'FYGFDPUN', # Federal Debt Held by the Public ($MM)
               'FDHBFIN', # Federal Debt Held by International Investors ($BN)
               'GFDEBTN', # Federal Debt: Total Public Debt ($BN)
               'BAA10Y', # Baa Corporate Bond Yield Relative to Yield on 10-Year
              ]
tsy_data = web.DataReader(tsy_tickers, 'fred').dropna(how='all').ffill()
tsy_data['FDHBFIN'] = tsy_data['FDHBFIN'] * 1000
tsy_data['GOV_PCT'] = tsy_data['TREAST'] / tsy_data['GFDEBTN']
tsy_data['HOM_PCT'] = tsy_data['FYGFDPUN'] / tsy_data['GFDEBTN']
tsy_data['FOR_PCT'] = tsy_data['FDHBFIN'] / tsy_data['GFDEBTN']

接下来,我们定义我们的依赖变量(Y)和独立变量(X)。如前所述,预测变量是三个期限的收益率(即 1 个月、5 年和 30 年)。一周的交易日数假设为五天,并且我们使用五个交易日的滞后版本作为独立变量来计算问题定义部分提到的变量。

使用滞后五天的变量通过时间延迟方法嵌入了时间序列组件,其中滞后变量被包含为一个独立变量。这一步将时间序列数据重新构建为基于监督回归的模型框架。

3. 探索性数据分析

在本节中,我们将查看描述性统计和数据可视化。

3.1. 描述性统计

让我们看一下数据集的形状和列:

dataset.shape

输出

(505, 15)

数据包含大约 500 个观测值和 15 列。

3.2. 数据可视化

让我们首先绘制预测变量并观察它们的行为:

Y.plot(style=['-','--',':'])

输出

mlbf 05in20

在图中,我们看到短期、中期和长期利率之间的偏差在 2010 年较大,此后有所减少。2011 年长期和中期利率下降,此后也一直在下降。利率的顺序符合期限的顺序。然而,近年来有几个月,5Y利率低于1M利率。在所有期限的时间序列中,我们可以看到均值随时间变化,呈上升趋势。因此,这些系列是非平稳时间序列。

在某些情况下,对于这样的非平稳依赖变量,线性回归可能不适用。然而,我们使用的滞后变量作为独立变量同样是非平稳的。因此,我们实际上是在对非平稳时间序列进行建模,这可能仍然有效。

接下来,我们看一下散点图(本案例研究跳过了相关性图,因为它与散点图有类似的解释)。我们可以通过下面显示的散点矩阵可视化回归中所有变量之间的关系:

# Scatterplot Matrix
pyplot.figure(figsize=(15,15))
scatter_matrix(dataset,figsize=(15,16))
pyplot.show()

输出

mlbf 05in21

查看散点图(完整版本可在GitHub上找到),我们看到了预测变量与它们的滞后值和收益率曲线的其他期限之间的显著线性关系。 还存在线性关系,1M、5Y 利率与企业利差和外国政府购买变化之间的斜率为负。 30Y 利率与这些变量之间存在线性关系,尽管斜率为负。 总体而言,我们看到许多线性关系,并且我们期望线性模型表现良好。

4. 数据准备和分析

我们在前面的步骤中执行了大部分数据准备步骤(即获取因变量和自变量),因此我们将跳过此步骤。

5. 评估模型

在这一步中,我们评估模型。 这一步的 Python 代码与案例研究 1 中的代码类似,并且跳过了一些重复的代码。 读者还可以参考本书代码存储库中此案例研究的 Jupyter 笔记本以获取更多详细信息。

5.1. 训练-测试分割和评估指标

我们将使用数据集的 80%进行建模,并使用 20%进行测试。 我们将使用均方误差度量评估算法。 所有算法使用默认调整参数。

5.2. 比较模型和算法

在本案例研究中,主要目的是将线性模型与人工神经网络在收益率曲线建模中进行比较。 因此,我们坚持使用线性回归(LR)、正则化回归(LASSO 和 EN)和人工神经网络(表示为 MLP)。 我们还包括一些其他模型,如 KNN 和 CART,因为这些模型更简单,具有良好的解释性,如果变量之间存在非线性关系,CART 和 KNN 模型将能够捕捉到并提供 ANN 的良好比较基准。

查看训练和测试误差,我们看到线性回归模型的性能良好。 我们看到套索和弹性网表现不佳。 这些都是正则化回归模型,如果不重要,它们会减少变量的数量。 变量数量的减少可能导致信息丢失,从而导致模型性能不佳。 KNN 和 CART 都不错,但仔细观察,我们会发现测试错误高于训练错误。 我们还看到人工神经网络(MLP)算法的性能与线性回归模型相当。 尽管简单,但在变量之间存在显著线性关系时,线性回归是难以超越的一步预测的坚实基准。

输出

mlbf 05in22

6. 模型调整和网格搜索。

与本章案例研究 2 类似,我们对 ANN 模型进行了网格搜索,尝试不同的隐藏层组合。在网格搜索过程中还可以调整几个其他超参数,如学习率、动量、激活函数、迭代次数和批量大小,这与下面提到的步骤类似。

'''
hidden_layer_sizes : tuple, length = n_layers - 2, default (100,)
 The ith element represents the number of neurons in the ith
 hidden layer.
'''
param_grid={'hidden_layer_sizes': [(20,), (50,), (20,20), (20, 30, 20)]}
model = MLPRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, \
  cv=kfold)
grid_result = grid.fit(X_train, Y_train)
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

Output

Best: -0.018006 using {'hidden_layer_sizes': (20, 30, 20)}
-0.036433 (0.019326) with: {'hidden_layer_sizes': (20,)}
-0.020793 (0.007075) with: {'hidden_layer_sizes': (50,)}
-0.026638 (0.010154) with: {'hidden_layer_sizes': (20, 20)}
-0.018006 (0.005637) with: {'hidden_layer_sizes': (20, 30, 20)}

最佳模型是具有三层的模型,每个隐藏层分别具有 20、30 和 20 个节点。因此,我们准备了一个具有这种配置的模型,并在测试集上检验其性能。这是一个关键步骤,因为更多的层可能导致过度拟合,并在测试集上表现不佳。

预测比较

在最后一步,我们查看了实际数据与线性回归和 ANN 模型预测之间的预测图。有关本节 Python 代码,请参阅本案例研究的 Jupyter 笔记本。

mlbf 05in23mlbf 05in24mlbf 05in25

从上面的图表中可以看出,线性回归和 ANN 的预测是可比较的。对于 1M 期限,ANN 的拟合稍逊于回归模型。然而,对于 5Y 和 30Y 期限,ANN 的表现与回归模型一样好。

结论

在这个案例研究中,我们应用监督回归来预测多种期限的收益率曲线。尽管线性回归模型简单,但在预测单步前景时是一个难以超越的严格基准,因为其主要特征是要预测的变量的最后一个可用值。在这个案例研究中,ANN 的结果与线性回归模型相当。ANN 的另一个优点是,它对市场条件的变化更加灵活。此外,通过对几个其他超参数进行网格搜索和包括递归神经网络(如 LSTM)的选项,可以增强 ANN 模型。

总体而言,我们使用了 ANN 构建了一个基于机器学习的模型,在固定收益工具的背景下取得了令人鼓舞的结果。这使我们能够使用历史数据进行预测并分析风险和盈利能力,而不必在固定收益市场中冒任何实际资本的风险。

章节总结

在 “股票价格预测案例研究 1” 中,我们涵盖了基于机器学习和时间序列的股票价格预测框架。我们展示了可视化的重要性,并比较了时间序列与机器学习模型。在 “衍生品定价案例研究 2” 中,我们探讨了用于传统衍生品定价问题的机器学习应用,并展示了高模型性能。在 “投资者风险容忍度和智能顾问案例研究 3” 中,我们展示了如何使用监督学习模型来模拟投资者的风险容忍度,这可以导致投资组合管理过程的自动化。“收益率曲线预测案例研究 4” 与股票价格预测案例研究类似,在固定收益市场的背景下比较了线性和非线性模型的另一个例子。

我们看到时间序列和线性监督学习模型在资产价格预测问题(即案例研究 1 和 4)中表现良好,其中预测变量与其滞后组件有显著的线性关系。然而,在衍生品定价和风险容忍度预测中,存在非线性关系,集成和 ANN 模型表现更好。鼓励有兴趣使用监督回归或时间序列模型进行案例研究实施的读者,在进行模型选择之前理解变量关系和模型直觉的细微差别。

总的来说,本章通过案例研究展示的 Python、机器学习、时间序列和金融概念可以作为金融中任何其他基于监督回归的问题的蓝图。

练习

  • 使用案例研究 1 中指定的机器学习和时间序列模型的概念和框架,为另一个资产类别(如货币对 EUR/USD 或比特币)开发预测模型。

  • 在案例研究 1 中,添加一些技术指标,如趋势或动量,并检查模型性能的提升。某些技术指标的想法可以从 “比特币交易策略案例研究 3” 中借鉴,在第六章。

  • 使用 “衍生品定价案例研究 2” 中的概念,开发一个基于机器学习的模型来定价美式期权

  • 在收益率曲线预测案例研究中,利用多变量时间序列建模,例如VARMAX,并与基于机器学习的模型进行性能比较。

  • 加强 “投资者风险容忍度和智能顾问案例研究 3” 中呈现的智能顾问仪表板,以纳入除股票以外的工具。

¹ 根据步骤和子步骤的适当性和直觉性可能会重新排序或重命名步骤或子步骤。

² 外生变量是指其值由模型外部决定并强加于模型的变量。

³ 这些模型将在本章后面讨论。

⁴ 根据步骤或子步骤的适当性和直觉性可能会重新排序或重命名步骤或子步骤。

⁵ 白噪声过程是一种随机过程,其随机变量是不相关的,均值为零,有有限方差。

⁶ LSTM 模型的详细解释可以在 Christopher Olah 的博客文章中找到。

⁷ 将在其中一个案例研究中展示 ARIMA 模型和基于 Keras 的 LSTM 模型。

⁸ 参考“第三案例研究:比特币交易策略”(见第六章)和“第一案例研究:基于 NLP 和情感分析的交易策略”(见第十章)了解技术指标和基于新闻的基本分析作为价格预测中的特征的使用。

⁹ 股票市场有交易假期,而货币市场没有。然而,在进行任何建模或分析之前,确保所有时间序列的日期对齐。

¹⁰ 在本书的不同案例研究中,我们将展示通过不同来源(例如 CSV 和像 quandl 这样的外部网站)加载数据的方法。

¹¹ 时间序列不是股票价格而是股票回报率,因此与股票价格序列相比,趋势较为温和。

¹² 预测变量即期权价格,理想情况下应直接从市场获取。鉴于此案例研究更多用于演示目的,我们为了方便起见使用模型生成的期权价格。

¹³ 当现货价格等于行权价格时,称为平值期权。

¹⁴ 参考本案例研究的 Jupyter 笔记本,浏览其他图表如直方图和相关图。

¹⁵ 鉴于模型的主要目的是在投资组合管理背景下使用,本案例研究中个人也被称为投资者。

¹⁶ 计算风险容忍度可能有多种方法。在这个案例研究中,我们使用直观的方法来衡量个人的风险容忍度。

¹⁷ 我们本可以选择 RMSE 作为评估指标;然而,鉴于我们在之前的案例研究中已经使用了 RMSE 作为评估指标,因此选择了 R² 作为评估指标。

第六章:监督学习:分类

这里是金融分析师试图解决的一些关键问题:

  • 借款人会按时还贷款还是违约?

  • 工具价格会涨还是跌?

  • 这笔信用卡交易是欺诈还是正常?

所有这些问题陈述,目标是预测分类类标签,本质上都适合于基于分类的机器学习。

分类算法已广泛应用于金融领域的许多方面,需要预测定性响应。这些包括欺诈检测、违约预测、信用评分、资产价格运动的方向预测以及买入/卖出建议。在投资组合管理和算法交易中也有许多其他基于分类的监督学习用例。

本章我们涵盖了三个基于分类的案例研究,涵盖了多个领域,包括欺诈检测、贷款违约概率和制定交易策略。

在“案例研究 1: 欺诈检测”,我们使用基于分类的算法来预测交易是否存在欺诈行为。本案例研究的重点还包括处理不平衡数据集,因为欺诈数据集中欺诈观测数量较少。

在“案例研究 2: 贷款违约概率”,我们使用基于分类的算法来预测贷款是否会违约。案例研究侧重于数据处理、特征选择和探索性分析的各种技术和概念。

在“案例研究 3: 比特币交易策略”,我们使用基于分类的算法来预测比特币当前交易信号是买入还是卖出,具体取决于短期和长期价格之间的关系。我们使用技术指标预测比特币价格的趋势。预测模型可以轻松转化为交易机器人,无需人工干预进行买入、卖出或持有操作。

本章的代码库

本书代码库的第六章 - 监督学习 - 分类模型文件夹中包含有监督分类模型的基于 Python 的主模板,以及本章案例研究的 Jupyter 笔记本。本章所有案例研究均使用 第 2 章 中提出的标准化的七步模型开发过程。¹

对于任何新的基于分类的问题,可以通过代码库中的主模板修改具体问题的元素。这些模板设计为在云基础设施上运行(例如 Kaggle、Google Colab 或 AWS)。为了在本地机器上运行模板,必须成功安装模板中使用的所有软件包。

案例研究 1: 欺诈检测

欺诈是金融部门面临的重大问题之一,它成本极高。根据一项研究,估计典型组织每年损失其年收入的 5%至欺诈。将其应用于 2017 年估计的全球总生产总值($79.6 万亿),这意味着潜在的全球损失高达 4 万亿美元。

欺诈检测是机器学习天生适合的任务,因为基于机器学习的模型可以扫描庞大的交易数据集,检测异常活动,并识别可能存在欺诈风险的所有案例。此外,与传统基于规则的方法相比,这些模型的计算速度更快。通过从各种来源收集数据,然后映射到触发点,机器学习解决方案能够发现每个潜在客户和交易的违约率或欺诈倾向,为金融机构提供关键的警报和洞察力。

在本案例研究中,我们将使用各种基于分类的模型来检测交易是正常支付还是欺诈。

使用分类模型确定交易是否欺诈的蓝图

1. 问题定义

在为本案例研究定义的分类框架中,响应(或目标)变量具有列名“Class”。该列在欺诈情况下的值为 1,在其他情况下的值为 0。

使用的数据集来自Kaggle。该数据集包含 2013 年 9 月两天内发生的欧洲持卡人的交易,其中包含 284,807 笔交易中的 492 起欺诈案例。

由于隐私原因,数据集已匿名化处理。鉴于某些特征名称未提供(即它们被称为 V1、V2、V3 等),可视化和特征重要性不会对模型行为提供太多见解。

通过本案例研究结束时,读者将熟悉欺诈建模的一般方法,从数据收集和清理到建立和调整分类器。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

下面显示了用于数据加载、数据分析、数据准备、模型评估和模型调整的库列表。有关大多数这些包和函数的详细信息已在第二章和第四章中提供:

用于数据加载、数据分析和数据准备的包

import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot

from pandas import read_csv, set_option
from pandas.plotting import scatter_matrix
from sklearn.preprocessing import StandardScaler

用于模型评估和分类模型的包

from sklearn.model_selection import train_test_split, KFold,\
 cross_val_score, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import Pipeline
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier,
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.metrics import classification_report, confusion_matrix,\
  accuracy_score

用于深度学习模型的包

from keras.models import Sequential
from keras.layers import Dense
from keras.wrappers.scikit_learn import KerasClassifier

用于保存模型的包

from pickle import dump
from pickle import load

3. 探索性数据分析

以下章节将介绍一些高级数据检查。

3.1. 描述性统计

我们必须做的第一件事是对数据有一个基本的了解。请记住,除了交易和金额之外,我们不知道其他列的名称。我们唯一知道的是这些列的值已经被缩放了。让我们来看看数据的形状和列:

# shape
dataset.shape

输出

(284807, 31)
#peek at data
set_option('display.width', 100)
dataset.head(5)

输出

mlbf 06in01

5 行 × 31 列

如图所示,变量名不具描述性(V1V2等)。此外,整个数据集的数据类型为float,除了Class是整数类型。

有多少是欺诈,多少是非欺诈?让我们来检查一下:

class_names = {0:'Not Fraud', 1:'Fraud'}
print(dataset.Class.value_counts().rename(index = class_names))

输出

Not Fraud    284315
Fraud           492
Name: Class, dtype: int64

请注意数据标签的明显不平衡。大多数交易都不是欺诈的。如果我们将此数据集用作建模的基础,大多数模型将不会足够重视欺诈信号;非欺诈数据点将淹没任何欺诈信号提供的权重。按照现状,我们可能会在预测欺诈方面遇到困难,因为这种不平衡会导致模型简单地假设所有交易都是非欺诈的。这将是一个不可接受的结果。我们将在随后的部分探讨一些解决此问题的方法。

3.2. 数据可视化

由于未提供特征描述,可视化数据不会带来太多见解。在这个案例研究中,将跳过此步骤。

4. 数据准备

这些数据来自 Kaggle,并且已经以无任何空行或空列的清理格式提供。数据清理或分类是不必要的。

5. 评估模型

现在我们准备好拆分数据并评估模型了。

5.1. 训练-测试分割和评估指标

如第二章所述,将原始数据集划分为训练集和测试集是一个好主意。测试集是我们从分析和建模中留出的数据样本。我们在项目结束时使用它来确认我们最终模型的准确性。它是最终测试,为我们提供了对未见数据准确性的估计的信心。我们将使用 80%的数据集进行模型训练,20%进行测试:

Y= dataset["Class"]
X = dataset.loc[:, dataset.columns != 'Class']
validation_size = 0.2
seed = 7
X_train, X_validation, Y_train, Y_validation =\
train_test_split(X, Y, test_size=validation_size, random_state=seed)

5.2. 检查模型

在这一步中,我们将评估不同的机器学习模型。为了优化模型的各种超参数,我们使用十折交叉验证,并重新计算结果十次,以考虑某些模型和 CV 过程中固有的随机性。所有这些模型,包括交叉验证,都在第四章中描述。

让我们设计我们的测试工具。我们将使用准确性指标评估算法。这是一个粗略的指标,将为我们提供给定模型的正确性的快速概念。它在二元分类问题上很有用。

# test options for classification
num_folds = 10
scoring = 'accuracy'

让我们为这个问题创建一个性能基准,并检查多种不同的算法。所选算法包括:

线性算法

逻辑回归(LR)和线性判别分析(LDA)。

非线性算法

分类与回归树(CART)和K-最近邻(KNN)。

选择这些模型有充分理由。这些模型是简单且快速的模型,对于具有大型数据集的问题具有良好的解释性。CART 和 KNN 能够区分变量之间的非线性关系。关键问题在于使用了不平衡样本。除非我们解决这个问题,否则更复杂的模型(如集成模型和人工神经网络)将预测效果较差。我们将在案例研究的后续部分集中解决这个问题,并评估这些类型模型的性能。

# spot-check basic Classification algorithms
models = []
models.append(('LR', LogisticRegression()))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))

所有算法使用默认调优参数。我们将计算并收集每种算法的准确率的均值和标准偏差,以备后用。

results = []
names = []
for name, model in models:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_results = cross_val_score(model, X_train, Y_train, cv=kfold, \
      scoring=scoring)
    results.append(cv_results)
    names.append(name)
    msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
    print(msg)

Output

LR: 0.998942 (0.000229)
LDA: 0.999364 (0.000136)
KNN: 0.998310 (0.000290)
CART: 0.999175 (0.000193)
# compare algorithms
fig = pyplot.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(names)
fig.set_size_inches(8,4)
pyplot.show()

mlbf 06in02

整体结果的准确率非常高。但让我们检查一下它对欺诈案例的预测效果。从上述结果中选择一个 CART 模型,并查看测试集的结果:

# prepare model
model = DecisionTreeClassifier()
model.fit(X_train, Y_train)

# estimate accuracy on validation set
predictions = model.predict(X_validation)
print(accuracy_score(Y_validation, predictions))
print(classification_report(Y_validation, predictions))

Output

0.9992275552122467
              precision    recall  f1-score   support

           0       1.00      1.00      1.00     56862
           1       0.77      0.79      0.78       100

    accuracy                           1.00     56962
   macro avg       0.89      0.89      0.89     56962
weighted avg       1.00      1.00      1.00     56962

生成混淆矩阵如下:

df_cm = pd.DataFrame(confusion_matrix(Y_validation, predictions), \
columns=np.unique(Y_validation), index = np.unique(Y_validation))
df_cm.index.name = 'Actual'
df_cm.columns.name = 'Predicted'
sns.heatmap(df_cm, cmap="Blues", annot=True,annot_kws={"size": 16})

mlbf 06in03

总体准确率很高,但混淆矩阵讲述了一个不同的故事。尽管准确率水平高,但有 21 个欺诈实例中的 100 个被错过,并错误预测为非欺诈。假阴性率相当可观。

一个欺诈检测模型的意图是尽量减少这些假阴性。因此,第一步是选择正确的评估指标。

在第四章中,我们讨论了用于分类问题的评估指标,如准确率、精确率和召回率。准确率是所有预测正确的比例。精确率是模型正确识别为正例的项目数占所有被模型识别为正例的项目总数的比例。召回率是所有正确识别为正例的项目数占所有真实正例的总数的比例。

对于这种类型的问题,我们应该关注召回率,即真正例占所有真实正例与假阴性之和的比率。因此,如果假阴性较高,则召回率值将较低。

在接下来的步骤中,我们进行模型调优,选择使用召回率的模型,并进行欠采样。

6. 模型调优

模型调优步骤的目的是对前一步骤选择的模型进行网格搜索。然而,由于在上一节中由于不平衡数据集而遇到了模型性能不佳的问题,我们将集中注意力解决这个问题。我们将分析选择正确评估指标的影响,并查看使用调整后的平衡样本的影响。

6.1. 通过选择正确的评估指标进行模型调优

如前所述,如果假阴性较高,则召回率将较低。模型将根据这一指标进行排序:

scoring = 'recall'

让我们对一些基本分类算法进行召回率的点检查:

models = []
models.append(('LR', LogisticRegression()))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))

运行交叉验证:

results = []
names = []
for name, model in models:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_results = cross_val_score(model, X_train, Y_train, cv=kfold, \
      scoring=scoring)
    results.append(cv_results)
    names.append(name)
    msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
    print(msg)

Output

LR: 0.595470 (0.089743)
LDA: 0.758283 (0.045450)
KNN: 0.023882 (0.019671)
CART: 0.735192 (0.073650)

我们看到 LDA 模型在四个模型中具有最佳的召回率。我们继续通过训练后的 LDA 评估测试集:

# prepare model
model = LinearDiscriminantAnalysis()
model.fit(X_train, Y_train)
# estimate accuracy on validation set

predictions = model.predict(X_validation)
print(accuracy_score(Y_validation, predictions))

输出

0.9995435553526912

mlbf 06in04

LDA 表现更好,仅错过了 100 例诈骗案中的 18 例。此外,我们也发现假阳性更少了。不过,还有改进的空间。

6.2. 模型调优——通过随机欠采样平衡样本

当前的数据显示了显著的类别不平衡,很少有数据点标记为“欺诈”。这种类别不平衡问题可能导致对多数类的严重偏向,降低分类性能并增加假阴性的数量。

解决这种情况的一种方法是对数据进行欠采样。一个简单的技术是随机均匀地对多数类进行欠采样。这可能导致信息的丢失,但通过很好地建模少数类,可能会产生强大的结果。

接下来,我们将实施随机欠采样,即删除数据以获得更平衡的数据集。这将有助于确保我们的模型避免过拟合。

实施随机欠采样的步骤如下:

  1. 首先,我们使用value_counts()函数来确定类别列中被视为欺诈交易(fraud = 1)的实例数量。

  2. 我们将非欺诈交易观察计数与欺诈交易数量相同。假设我们想要 50/50 的比例,这将相当于 492 例欺诈案和 492 例非欺诈交易。

  3. 现在我们有一个数据框的子样本,其类别的比例是 50/50。我们在这个子样本上训练模型。然后我们再次执行这个迭代,以在训练样本中打乱非欺诈观察。我们跟踪模型性能,看看我们的模型在每次重复此过程时是否能保持一定的准确性:

df = pd.concat([X_train, Y_train], axis=1)
# amount of fraud classes 492 rows.
fraud_df = df.loc[df['Class'] == 1]
non_fraud_df = df.loc[df['Class'] == 0][:492]

normal_distributed_df = pd.concat([fraud_df, non_fraud_df])

# Shuffle dataframe rows
df_new = normal_distributed_df.sample(frac=1, random_state=42)
# split out validation dataset for the end
Y_train_new= df_new["Class"]
X_train_new = df_new.loc[:, dataset.columns != 'Class']

让我们看看数据集中各类别的分布情况:

print('Distribution of the Classes in the subsample dataset')
print(df_new['Class'].value_counts()/len(df_new))
sns.countplot('Class', data=df_new)
pyplot.title('Equally Distributed Classes', fontsize=14)
pyplot.show()

输出

Distribution of the Classes in the subsample dataset
1    0.5
0    0.5
Name: Class, dtype: float64

mlbf 06in05

数据现在平衡了,接近 1000 个观察结果。我们将重新训练所有模型,包括 ANN。现在数据已经平衡,我们将专注于准确率作为主要评估指标,因为它考虑了假阳性和假阴性。如果需要,总是可以产生召回率:

#setting the evaluation metric
scoring='accuracy'
# spot-check the algorithms
models = []
models.append(('LR', LogisticRegression()))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))
models.append(('NB', GaussianNB()))
models.append(('SVM', SVC()))
#Neural Network
models.append(('NN', MLPClassifier()))
# Ensemble Models
# Boosting methods
models.append(('AB', AdaBoostClassifier()))
models.append(('GBM', GradientBoostingClassifier()))
# Bagging methods
models.append(('RF', RandomForestClassifier()))
models.append(('ET', ExtraTreesClassifier()))

在 Keras 中定义和编译基于 ANN 的深度学习模型的步骤,以及以下代码中提到的所有术语(神经元、激活、动量等),已在第三章中描述。该代码可用于任何基于深度学习的分类模型。

基于 Keras 的深度学习模型:

# Function to create model, required for KerasClassifier
def create_model(neurons=12, activation='relu', learn_rate = 0.01, momentum=0):
    # create model
    model = Sequential()
    model.add(Dense(X_train.shape[1], input_dim=X_train.shape[1], \
      activation=activation))
    model.add(Dense(32,activation=activation))
    model.add(Dense(1, activation='sigmoid'))
    # Compile model
    optimizer = SGD(lr=learn_rate, momentum=momentum)
    model.compile(loss='binary_crossentropy', optimizer='adam', \
    metrics=['accuracy'])
    return model
models.append(('DNN', KerasClassifier(build_fn=create_model,\
epochs=50, batch_size=10, verbose=0)))

对新模型集进行交叉验证的结果如下:

mlbf 06in06

尽管包括随机森林(RF)和逻辑回归(LR)在内的几个模型表现良好,GBM 稍微领先于其他模型。我们选择进一步分析这个模型。请注意,使用 Keras 的深度学习模型(即“DNN”)的结果较差。

对 GBM 模型进行网格搜索,通过调整估计器的数量和最大深度。GBM 模型的详细信息和调整参数在第四章中有描述。

# Grid Search: GradientBoosting Tuning
n_estimators = [20,180,1000]
max_depth= [2, 3,5]
param_grid = dict(n_estimators=n_estimators, max_depth=max_depth)
model = GradientBoostingClassifier()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, \
  cv=kfold)
grid_result = grid.fit(X_train_new, Y_train_new)
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))

Output

Best: 0.936992 using {'max_depth': 5, 'n_estimators': 1000}

接下来,准备最终模型,并检查在测试集上的结果:

# prepare model
model = GradientBoostingClassifier(max_depth= 5, n_estimators = 1000)
model.fit(X_train_new, Y_train_new)
# estimate accuracy on Original validation set
predictions = model.predict(X_validation)
print(accuracy_score(Y_validation, predictions))

Output

0.9668199852533268

模型的准确率很高。让我们来看看混淆矩阵:

Output

mlbf 06in07

测试集上的结果令人印象深刻,准确率很高,而且最重要的是没有假阴性。然而,我们看到在使用欠采样数据的情况下,一个结果是存在假阳性的倾向——即将非欺诈交易误分类为欺诈交易。这是金融机构必须考虑的一种权衡。在操作开销和可能影响客户体验的潜在财务损失之间存在固有的成本平衡,这是处理假阴性所导致的财务损失。

结论

在这个案例研究中,我们对信用卡交易进行了欺诈检测。我们展示了不同分类机器学习模型之间的对比,并且证明选择正确的评估指标在模型评估中可以产生重要的差异。欠采样显示出显著改进,因为在应用欠采样后,测试集中的所有欺诈案例都被正确识别。然而,这也伴随着一个权衡:减少了假阴性的同时增加了假阳性。

总体而言,通过使用不同的机器学习模型、选择正确的评估指标和处理不平衡数据,我们展示了如何实施基于简单分类模型的诈骗检测,可以产生稳健的结果。

案例研究 2:贷款违约概率

放贷是金融业最重要的活动之一。放贷人向借款人提供贷款,以换取借款人承诺的还款和利息。这意味着只有借款人还清贷款,放贷人才能获利。因此,放贷行业中最关键的两个问题是:

  1. 借款人的风险有多大?

  2. 鉴于借款人的风险,我们是否应该向他们放贷?

对于机器学习来说,违约预测可以被描述为一项完美的工作,因为算法可以基于数百万个消费者数据示例进行训练。算法可以执行自动化任务,如匹配数据记录、识别异常情况,以及计算申请人是否有资格获得贷款。算法可以评估潜在趋势,并持续分析以检测可能影响未来放贷和核保风险的趋势。

本案例研究的目标是建立一个机器学习模型,预测贷款违约的概率。

在大多数现实生活案例中,包括贷款违约建模在内,我们无法处理干净完整的数据。我们可能会遇到的一些潜在问题包括缺失值、不完整的分类数据和无关的特征。尽管数据清洗可能不经常被提及,但它对机器学习应用的成功至关重要。我们使用的算法可能非常强大,但如果没有相关或适当的数据,系统可能无法产生理想的结果。因此,本案例研究的一个重点领域将是数据准备和清洗。各种数据处理技术和概念,包括数据清洗、特征选择和探索性分析,都被用来整理特征空间。

创造一个用于预测贷款违约概率的机器学习模型的蓝图

1. 问题定义

在本案例研究的分类框架中,预测变量是违约,即借款人在数月内错过付款后,债权人放弃尝试收回的债务。在违约情况下,预测变量取值为 1,否则为 0。

我们将分析来自 Lending Club 的 2007 年至 2017 年第 3 季度的贷款数据,可在 Kaggle 上找到。Lending Club 是一家美国的 P2P(个人对个人)借贷公司。它运营一个在线借贷平台,允许借款人获得贷款,并允许投资者购买由这些贷款上的支付支持的票据。该数据集包含超过 887,000 条观察结果,包含 150 个变量,包含指定时间段内所有贷款的完整贷款数据。这些特征包括收入、年龄、信用评分、住房所有权、借款人位置、收藏等等。我们将调查这 150 个预测变量以进行特征选择。

通过本案例研究的结束,读者将熟悉从收集和清理数据到构建和调整分类器的贷款违约建模的一般方法。

2. 开始—加载数据和 Python 包

2.1. 加载 Python 包

标准的 Python 包在此步骤中被加载。详细信息已在之前的案例研究中呈现。请参考本案例研究的 Jupyter 笔记本以获取更多细节。

2.2. 加载数据

从 2007 年到 2017 年第 3 季度的贷款数据已加载:

# load dataset
dataset = pd.read_csv('LoansData.csv.gz', compression='gzip', \
low_memory=True)

3. 数据准备和特征选择

在第一步中,让我们来看看数据的大小:

dataset.shape

Output

(1646801, 150)

鉴于每笔贷款有 150 个特征,我们首先将重点放在限制特征空间上,然后进行探索性分析。

3.1. 准备预测变量

在这里,我们查看预测变量的详细信息并准备它。预测变量将从 loan_status 列中派生。让我们检查值分布:²

dataset['loan_status'].value_counts(dropna=False)

Output

Current                                                788950
Fully Paid                                             646902
Charged Off                                            168084
Late (31-120 days)                                      23763
In Grace Period                                         10474
Late (16-30 days)                                        5786
Does not meet the credit policy. Status:Fully Paid       1988
Does not meet the credit policy. Status:Charged Off       761
Default                                                    70
NaN                                                        23
Name: loan_status, dtype: int64

根据数据定义文档:

已全额偿还

已经全额偿还的贷款。

违约

已经超过 121 天未达到 Current 状态的贷款。

已违约

不再有合理预期可以进一步付款的贷款。

大部分观测结果显示为 Current 状态,我们不知道这些贷款在未来是会 已违约已全额偿还 还是 违约Default 的观测数量与 已全额偿还已违约 相比非常少,因此不予考虑。对于此分析来说,贷款状态 的其余类别并不是主要关注点。因此,为了将其转换为二元分类问题,并详细分析重要变量对贷款状态的影响,我们只考虑两个主要类别—已违约和已全额偿还:

dataset = dataset.loc[dataset['loan_status'].isin(['Fully Paid', 'Charged Off'])]
dataset['loan_status'].value_counts(normalize=True, dropna=False)

Output

Fully Paid     0.793758
Charged Off    0.206242
Name: loan_status, dtype: float64

剩余贷款中大约 79%已全额偿还,21%已违约,所以我们面临一个有些不平衡的分类问题,但不像我们在之前案例研究中看到的欺诈检测数据集那么不平衡。

接下来,我们在数据集中创建一个新的二元列,将“已全额偿还”分类为 0,而“已违约”分类为 1。这一列代表了这个分类问题的预测变量。这一列中的值为 1 表示借款人已经违约:

dataset['charged_off'] = (dataset['loan_status'] == 'Charged Off').apply(np.uint8)
dataset.drop('loan_status', axis=1, inplace=True)

3.2. 特征选择—限制特征空间

完整数据集每笔贷款有 150 个特征,但并非所有特征都对预测变量有贡献。删除重要性较低的特征可以提高准确性,减少模型复杂性和过拟合。对于非常大的数据集,还可以减少训练时间。我们将使用三种不同的方法在以下步骤中消除特征:

  • 消除超过 30%空值的特征。

  • 根据主观判断消除不直观的特征。

  • 根据预测变量消除与之低相关性的特征。

3.2.1. 基于显著缺失值消除特征

首先,我们计算每个特征的缺失数据百分比:

missing_fractions = dataset.isnull().mean().sort_values(ascending=False)

#Drop the missing fraction
drop_list = sorted(list(missing_fractions[missing_fractions > 0.3].index))
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

Output

(814986, 92)

一些列的空值较多的列被删除后,此数据集还剩下 92 列。

3.2.2. 基于直觉消除特征

为了进一步筛选特征,我们检查数据字典中的描述,并保留直观上有助于预测违约的特征。我们保留包含借款人相关信用细节的特征,包括年收入、FICO 分数和债务收入比。我们还保留那些在考虑投资贷款时投资者可用的特征,例如贷款等级和利率。

以下代码片段显示了保留的特征列表:

keep_list = ['charged_off','funded_amnt','addr_state', 'annual_inc', \
'application_type','dti', 'earliest_cr_line', 'emp_length',\
'emp_title', 'fico_range_high',\
'fico_range_low', 'grade', 'home_ownership', 'id', 'initial_list_status', \
'installment', 'int_rate', 'loan_amnt', 'loan_status',\
'mort_acc', 'open_acc', 'pub_rec', 'pub_rec_bankruptcies', \
'purpose', 'revol_bal', 'revol_util', \
'sub_grade', 'term', 'title', 'total_acc',\
'verification_status', 'zip_code','last_pymnt_amnt',\
'num_actv_rev_tl', 'mo_sin_rcnt_rev_tl_op',\
'mo_sin_old_rev_tl_op',"bc_util","bc_open_to_buy",\
"avg_cur_bal","acc_open_past_24mths" ]

drop_list = [col for col in dataset.columns if col not in keep_list]
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

Output

(814986, 39)

在此步骤中删除特征后,剩下 39 列。

3.2.3. 基于相关性的特征消除

下一步是检查与预测变量的相关性。相关性为我们提供了预测变量与特征之间的相互依赖关系。我们选择与目标变量具有中等到强相关性的特征,并且删除那些与预测变量相关性低于 3%的特征:

correlation = dataset.corr()
correlation_chargeOff = abs(correlation['charged_off'])
drop_list_corr = sorted(list(correlation_chargeOff\
  [correlation_chargeOff < 0.03].index))
print(drop_list_corr)

Output

['pub_rec', 'pub_rec_bankruptcies', 'revol_bal', 'total_acc']

数据集中相关性较低的列已被删除,我们只剩下 35 列:

dataset.drop(labels=drop_list_corr, axis=1, inplace=True)

4. 特征选择和探索性分析

在这一步中,我们执行特征选择的探索性数据分析。考虑到许多特征必须被排除,最好在特征选择后执行探索性数据分析,以更好地可视化相关的特征。我们还将继续通过视觉筛选和删除那些被认为不相关的特征进行特征消除。

4.1. 特征分析和探索

在接下来的几节中,我们将深入研究数据集的特征。

4.1.1. 分析分类特征

让我们看看数据集中一些分类特征。

首先,让我们看看idemp_titletitlezip_code特征:

dataset[['id','emp_title','title','zip_code']].describe()

Output

id emp_title title zip_code
count 814986 766415 807068 814986
unique 814986 280473 60298 925
top 14680062 Teacher Debt consolidation 945xx
freq 1 11351 371874 9517

ID 是唯一的且与建模无关。雇佣头衔和职位标题有太多的唯一值。职业和职位头衔可能为默认建模提供一些信息;然而,我们假设这些信息的大部分都包含在客户的已验证收入中。此外,对这些特征进行额外的清理步骤,例如标准化或分组头衔,将需要提取任何边际信息。这项工作超出了本案例研究的范围,但可以在模型的后续迭代中探索。

地理位置可能在信用确定中起到作用,邮政编码提供了这一维度的细致视图。再次强调,需要额外的工作来准备这个特征用于建模,并且被认为超出了这个案例研究的范围。

dataset.drop(['id','emp_title','title','zip_code'], axis=1, inplace=True)

让我们来看看 term 特征。

期限 指的是贷款的支付期数。值以月计,并且可以是 36 或 60。60 个月的贷款更有可能违约。

让我们将期限转换为整数,并按期限分组进行进一步分析:

dataset['term'] = dataset['term'].apply(lambda s: np.int8(s.split()[0]))
dataset.groupby('term')['charged_off'].value_counts(normalize=True).loc[:,1]

输出

term
36    0.165710
60    0.333793
Name: charged_off, dtype: float64

五年期贷款比三年期贷款更有可能违约。该特征似乎对预测很重要。

让我们来看看 emp_length 特征:

dataset['emp_length'].replace(to_replace='10+ years', value='10 years',\
  inplace=True)

dataset['emp_length'].replace('< 1 year', '0 years', inplace=True)

def emp_length_to_int(s):
    if pd.isnull(s):
        return s
    else:
        return np.int8(s.split()[0])

dataset['emp_length'] = dataset['emp_length'].apply(emp_length_to_int)
charge_off_rates = dataset.groupby('emp_length')['charged_off'].value_counts\
  (normalize=True).loc[:,1]
sns.barplot(x=charge_off_rates.index, y=charge_off_rates.values)

输出

mlbf 06in08

贷款状态在就业年限上似乎变化不大(平均而言);因此,此特征被舍弃:

dataset.drop(['emp_length'], axis=1, inplace=True)

让我们来看看 sub_grade 特征:

charge_off_rates = dataset.groupby('sub_grade')['charged_off'].value_counts\
(normalize=True).loc[:,1]
sns.barplot(x=charge_off_rates.index, y=charge_off_rates.values)

输出

mlbf 06in09

如图所示,随着子等级恶化,违约的可能性呈明显趋势,因此被认为是一个关键特征。

4.1.2. 分析连续特征

让我们来看看 annual_inc 特征:

dataset[['annual_inc']].describe()

输出

年收入
count 8.149860e+05
mean 7.523039e+04
std 6.524373e+04
min 0.000000e+00
25% 4.500000e+04
50% 6.500000e+04
75% 9.000000e+04
max 9.550000e+06

年收入范围从$0 到$9,550,000,中位数为$65,000。由于收入范围很大,我们使用年收入变量的对数变换:

dataset['log_annual_inc'] = dataset['annual_inc'].apply(lambda x: np.log10(x+1))
dataset.drop('annual_inc', axis=1, inplace=True)

让我们来看看 FICO 分数(fico_range_lowfico_range_high)特征:

dataset[['fico_range_low','fico_range_high']].corr()

输出

fico_range_low fico_range_high
fico_range_low 1.0 1.0
fico_range_high 1.0 1.0

鉴于 FICO 低和高之间的相关性为 1,建议仅保留一个特征,我们采用 FICO 分数的平均值:

dataset['fico_score'] = 0.5*dataset['fico_range_low'] +\
 0.5*dataset['fico_range_high']

dataset.drop(['fico_range_high', 'fico_range_low'], axis=1, inplace=True)

4.2. 编码分类数据

为了在分类模型中使用特征,我们需要将分类数据(即文本特征)转换为其数值表示。这个过程称为编码。可以有不同的编码方式。然而,在这个案例研究中,我们将使用标签编码器,它将标签编码为介于 0 和n之间的值,其中n是不同标签的数量。在以下步骤中使用来自 sklearn 的LabelEncoder函数,一次性对所有分类列进行编码:

from sklearn.preprocessing import LabelEncoder
# Categorical boolean mask
categorical_feature_mask = dataset.dtypes==object
# filter categorical columns using mask and turn it into a list
categorical_cols = dataset.columns[categorical_feature_mask].tolist()

让我们来看看分类列:

categorical_cols

输出

['grade',
 'sub_grade',
 'home_ownership',
 'verification_status',
 'purpose',
 'addr_state',
 'initial_list_status',
 'application_type']

4.3. 数据抽样

鉴于贷款数据偏斜,对其进行抽样以使违约和无违约观测数量相等。抽样会导致更平衡的数据集,并避免过拟合:³

loanstatus_0 = dataset[dataset["charged_off"]==0]
loanstatus_1 = dataset[dataset["charged_off"]==1]
subset_of_loanstatus_0 = loanstatus_0.sample(n=5500)
subset_of_loanstatus_1 = loanstatus_1.sample(n=5500)
dataset = pd.concat([subset_of_loanstatus_1, subset_of_loanstatus_0])
dataset = dataset.sample(frac=1).reset_index(drop=True)
print("Current shape of dataset :",dataset.shape)

虽然抽样可能有其优势,但也可能存在一些不利因素。抽样可能会排除一些可能与采取的数据不均匀的数据。这会影响结果的准确性水平。此外,选择适当大小的样本是一项困难的工作。因此,在相对平衡的数据集情况下,应谨慎进行抽样,并且通常应该避免。

5. 评估算法和模型

5.1. 训练-测试分离

下一步是为模型评估分离出验证数据集:

Y= dataset["charged_off"]
X = dataset.loc[:, dataset.columns != 'charged_off']
validation_size = 0.2
seed = 7
X_train, X_validation, Y_train, Y_validation = \
train_test_split(X, Y, test_size=validation_size, random_state=seed)

5.2. 测试选项和评估指标

在这一步中,选择测试选项和评估指标。选择了roc_auc评估指标用于此分类。该指标的详细信息在 第四章 中提供。此指标代表模型区分正类和负类的能力。roc_auc为 1.0 表示模型完美预测所有情况,而 0.5 表示模型等同于随机预测。

num_folds = 10
scoring = 'roc_auc'

模型不能承受高数量的假阴性,因为这会对投资者和公司的信誉产生负面影响。因此,我们可以像在欺诈检测用例中一样使用召回率。

5.3. 比较模型和算法

让我们现场检查分类算法。我们在待检查的模型列表中包括 ANN 和集成模型:

models = []
models.append(('LR', LogisticRegression()))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))
models.append(('NB', GaussianNB()))
# Neural Network
models.append(('NN', MLPClassifier()))
# Ensemble Models
# Boosting methods
models.append(('AB', AdaBoostClassifier()))
models.append(('GBM', GradientBoostingClassifier()))
# Bagging methods
models.append(('RF', RandomForestClassifier()))
models.append(('ET', ExtraTreesClassifier()))

在上述模型上执行k折交叉验证后,整体性能如下:

mlbf 06in10

梯度提升方法(GBM)模型表现最佳,我们选择它进行下一步的网格搜索。GBM 的详细信息以及模型参数在 第四章 中描述。

6. 模型调整和网格搜索

我们调整了讨论过的第四章中的估计器数量和最大深度超参数:

# Grid Search: GradientBoosting Tuning
n_estimators = [20,180]
max_depth= [3,5]
param_grid = dict(n_estimators=n_estimators, max_depth=max_depth)
model = GradientBoostingClassifier()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, \
  cv=kfold)
grid_result = grid.fit(X_train, Y_train)
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))

输出

Best: 0.952950 using {'max_depth': 5, 'n_estimators': 180}

具有max_depth为 5 和估计器数量为 150 的 GBM 模型导致最佳模型。

7. 完成模型

现在,我们执行最终步骤来选择一个模型。

7.1. 测试数据集上的结果

让我们使用在网格搜索步骤中找到的参数准备 GBM 模型,并在测试数据集上检查结果:

model = GradientBoostingClassifier(max_depth= 5, n_estimators= 180)
model.fit(X_train, Y_train)

# estimate accuracy on validation set
predictions = model.predict(X_validation)
print(accuracy_score(Y_validation, predictions))

输出

0.889090909090909

模型在测试集上的准确率为合理的 89%。让我们来查看混淆矩阵:

mlbf 06in11

查看混淆矩阵和测试集的整体结果,假阳性率和假阴性率都较低;整体模型性能看起来良好,并且与训练集结果一致。

7.2. 变量直觉/特征重要性

在这一步中,我们计算并显示了我们训练模型的变量重要性:

print(model.feature_importances_) #use inbuilt class feature_importances
feat_importances = pd.Series(model.feature_importances_, index=X.columns)
#plot graph of feature importances for better visualization
feat_importances.nlargest(10).plot(kind='barh')
pyplot.show()

输出

mlbf 06in12

模型重要性的结果是直观的。最后一次付款金额似乎是最重要的特征,其次是贷款期限和子等级。

结论

在这个案例研究中,我们介绍了应用于贷款违约预测的基于分类的树算法。我们展示了数据准备是最重要的步骤之一。我们通过使用不同的技术,如特征直觉、相关性分析、可视化和数据质量检查来进行特征消除。我们说明了处理和分析分类数据以及将分类数据转换为模型可用格式的不同方法。

我们强调,在模型开发过程中,进行数据处理并建立变量重要性理解至关重要。专注于这些步骤导致了一个简单的基于分类的模型的实施,为违约预测产生了稳健的结果。

案例研究 3:比特币交易策略

比特币由化名为中本聪的人在 2009 年首次开源发布,是历史最悠久且最知名的加密货币。

加密货币交易的一个主要缺点是市场的波动性。由于加密货币市场 24/7 交易,跟踪加密货币在快速变化的市场动态中的位置可能迅速变得难以管理。这就是自动化交易算法和交易机器人可以提供帮助的地方。

各种机器学习算法可用于生成交易信号,以试图预测市场的运动。可以使用机器学习算法将明天的市场运动分类为三类:市场将上涨(采取多头),市场将下跌(采取空头),或市场将横向移动(不采取任何头寸)。由于我们了解市场走势,我们可以决定最佳的进出点。

机器学习有一个关键方面称为特征工程。这意味着我们可以创建新的直观特征,并将它们提供给机器学习算法,以获得更好的结果。我们可以引入不同的技术指标作为特征,以帮助预测资产未来的价格。这些技术指标来源于市场变量,如价格或交易量,并在其中嵌入了额外的信息或信号。技术指标有许多不同的类别,包括趋势、交易量、波动率和动量指标。

在这个案例研究中,我们将使用各种基于分类的模型来预测当前位置信号是买入还是卖出。我们将从市场价格中创建额外的趋势和动量指标,作为预测中的额外特征。

使用基于分类模型预测在比特币市场中买入还是卖出的蓝图

1. 问题定义

预测交易策略的买卖信号问题在分类框架中定义,其中预测变量的值为 1 表示买入,0 表示卖出。此信号通过比较短期和长期价格趋势来决定。

使用的数据来自于日均交易量最大的比特币交易所之一,Bitstamp。数据涵盖了从 2012 年 1 月到 2017 年 5 月的价格。从数据中创建不同的趋势和动量指标,并将其作为特征添加以提升预测模型的性能。

通过本案例研究结束时,读者将熟悉建立交易策略的一般方法,从数据清理和特征工程到模型调整和开发回测框架。

2. 入门—载入数据和 Python 包

让我们载入必要的包和数据。

2.1. 载入 Python 包

标准的 Python 包在这一步中被载入。详细信息已在之前的案例研究中呈现。有关更多细节,请参阅本案例研究的 Jupyter 笔记本。

2.2. 载入数据

从 Bitstamp 网站获取的比特币数据在此步骤中载入:

# load dataset
dataset = pd.read_csv('BitstampData.csv')

3. 探索性数据分析

在这一步中,我们将详细查看这些数据。

3.1. 描述统计

首先,让我们看看数据的形状:

dataset.shape

输出

(2841377, 8)
# peek at data
set_option('display.width', 100)
dataset.tail(2)

输出

时间戳 开盘价 最高价 最低价 收盘价 比特币交易量 交易货币交易量 加权平均价
2841372 1496188560 2190.49 2190.49 2181.37 2181.37 1.700166 3723.784755 2190.247337
2841373 1496188620 2190.50 2197.52 2186.17 2195.63 6.561029 14402.811961 2195.206304

数据集包含比特币的开盘价、最高价、最低价、收盘价和交易量的分钟级数据。数据集很大,总共约有 280 万条观测值。

4. 数据准备

在这一部分,我们将清理数据以准备建模。

4.1. 数据清理

我们通过填充 NaN 值使用最后可用的值来清理数据:

dataset[dataset.columns.values] = dataset[dataset.columns.values].ffill()

时间戳列对于建模不实用,因此从数据集中删除:

dataset=dataset.drop(columns=['Timestamp'])

4.2. 准备分类数据

作为第一步,我们将为我们的模型创建目标变量。这是一个列,指示交易信号是买入还是卖出。我们将短期价格定义为 10 天滚动平均,长期价格定义为 60 天滚动平均。如果短期价格高于(低于)长期价格,则附加标签为10):

# Create short simple moving average over the short window
dataset['short_mavg'] = dataset['Close'].rolling(window=10, min_periods=1,\
center=False).mean()

# Create long simple moving average over the long window
dataset['long_mavg'] = dataset['Close'].rolling(window=60, min_periods=1,\
center=False).mean()

# Create signals
dataset['signal'] = np.where(dataset['short_mavg'] >
dataset['long_mavg'], 1.0, 0.0)

4.3. 特征工程

我们通过分析预期可能影响预测模型性能的特征来开始特征工程。基于对推动投资策略的关键因素的概念理解,当前任务是识别并构建可能捕捉这些回报驱动因素所体现的风险或特征的新特征。对于本案例研究,我们将探索特定动量技术指标的有效性。

比特币的当前数据包括日期、开盘价、最高价、最低价、收盘价和成交量。利用这些数据,我们计算以下动量指标:

移动平均

移动平均通过减少系列中的噪音来指示价格趋势。

随机振荡器 %K

随机振荡器是一种动量指标,它将安全性的收盘价与一定时间内其前期价格范围进行比较。%K%D 是慢速和快速指标。相比于慢速指标,快速指标对基础安全性价格变动更为敏感,可能导致许多交易信号。

相对强弱指数(RSI)

这是一种动量指标,用于衡量最近价格变化的幅度,以评估股票或其他资产价格的超买或超卖状况。RSI 的范围从 0 到 100。一旦 RSI 接近 70,资产被认为是超买的,意味着资产可能被高估,适合回调。同样,如果 RSI 接近 30,这表明资产可能被超卖,因此可能会被低估。

变动率(ROC)

这是一种动量振荡器,衡量当前价格与n期前价格之间的百分比变化。具有较高 ROC 值的资产被认为更可能被超买;具有较低 ROC 值的资产更可能被超卖。

动量(MOM)

这是安全价格或成交量加速度的速率,即价格变化的速度。

以下步骤展示了如何为预测生成一些有用的特征。趋势和动量的特征可以用于其他交易策略模型中:

#calculation of exponential moving average
def EMA(df, n):
    EMA = pd.Series(df['Close'].ewm(span=n, min_periods=n).mean(), name='EMA_'\
     + str(n))
    return EMA
dataset['EMA10'] = EMA(dataset, 10)
dataset['EMA30'] = EMA(dataset, 30)
dataset['EMA200'] = EMA(dataset, 200)
dataset.head()

#calculation of rate of change
def ROC(df, n):
    M = df.diff(n - 1)
    N = df.shift(n - 1)
    ROC = pd.Series(((M / N) * 100), name = 'ROC_' + str(n))
    return ROC
dataset['ROC10'] = ROC(dataset['Close'], 10)
dataset['ROC30'] = ROC(dataset['Close'], 30)

#calculation of price momentum
def MOM(df, n):
    MOM = pd.Series(df.diff(n), name='Momentum_' + str(n))
    return MOM
dataset['MOM10'] = MOM(dataset['Close'], 10)
dataset['MOM30'] = MOM(dataset['Close'], 30)

#calculation of relative strength index
def RSI(series, period):
 delta = series.diff().dropna()
 u = delta * 0
 d = u.copy()
 u[delta > 0] = delta[delta > 0]
 d[delta < 0] = -delta[delta < 0]
 u[u.index[period-1]] = np.mean( u[:period] ) #first value is sum of avg gains
 u = u.drop(u.index[:(period-1)])
 d[d.index[period-1]] = np.mean( d[:period] ) #first value is sum of avg losses
 d = d.drop(d.index[:(period-1)])
 rs = u.ewm(com=period-1, adjust=False).mean() / \
 d.ewm(com=period-1, adjust=False).mean()
 return 100 - 100 / (1 + rs)
dataset['RSI10'] = RSI(dataset['Close'], 10)
dataset['RSI30'] = RSI(dataset['Close'], 30)
dataset['RSI200'] = RSI(dataset['Close'], 200)

#calculation of stochastic osillator.

def STOK(close, low, high, n):
 STOK = ((close - low.rolling(n).min()) / (high.rolling(n).max() - \
 low.rolling(n).min())) * 100
 return STOK

def STOD(close, low, high, n):
 STOK = ((close - low.rolling(n).min()) / (high.rolling(n).max() - \
 low.rolling(n).min())) * 100
 STOD = STOK.rolling(3).mean()
 return STOD

dataset['%K10'] = STOK(dataset['Close'], dataset['Low'], dataset['High'], 10)
dataset['%D10'] = STOD(dataset['Close'], dataset['Low'], dataset['High'], 10)
dataset['%K30'] = STOK(dataset['Close'], dataset['Low'], dataset['High'], 30)
dataset['%D30'] = STOD(dataset['Close'], dataset['Low'], dataset['High'], 30)
dataset['%K200'] = STOK(dataset['Close'], dataset['Low'], dataset['High'], 200)
dataset['%D200'] = STOD(dataset['Close'], dataset['Low'], dataset['High'], 200)

#calculation of moving average
def MA(df, n):
    MA = pd.Series(df['Close'].rolling(n, min_periods=n).mean(), name='MA_'\
     + str(n))
    return MA
dataset['MA21'] = MA(dataset, 10)
dataset['MA63'] = MA(dataset, 30)
dataset['MA252'] = MA(dataset, 200)

完成特征后,我们将为它们做好准备。

4.4. 数据可视化

在这一步中,我们可视化不同特征和预测变量的不同属性:

dataset[['Weighted_Price']].plot(grid=True)
plt.show()

Output

mlbf 06in13

图表显示比特币价格急剧上涨,从接近 0 美元增加到 2017 年左右的 2500 美元。此外,高价格波动性一目了然。

让我们看一下预测变量的分布:

fig = plt.figure()
plot = dataset.groupby(['signal']).size().plot(kind='barh', color='red')
plt.show()

Output

mlbf 06in14

预测变量在 52%以上的时间内为 1,意味着买入信号比卖出信号更多。预测变量相对平衡,特别是与我们在第一个案例研究中看到的欺诈数据集相比。

5. 评估算法和模型

在这一步中,我们将评估不同的算法。

5.1. 训练集和测试集分割

我们首先将数据集分成训练集(80%)和测试集(20%)。对于这个案例研究,我们使用了 10 万条观测数据以便更快地计算。如果我们想使用整个数据集,下一步将是相同的:

# split out validation dataset for the end
subset_dataset= dataset.iloc[-100000:]
Y= subset_dataset["signal"]
X = subset_dataset.loc[:, dataset.columns != 'signal']
validation_size = 0.2
seed = 1
X_train, X_validation, Y_train, Y_validation =\
train_test_split(X, Y, test_size=validation_size, random_state=1)

5.2. 测试选项和评估指标

由于数据中不存在显著的类别不平衡问题,可以使用准确率作为评估指标:

# test options for classification
num_folds = 10
scoring = 'accuracy'

5.3. 比较模型和算法

为了知道哪种算法最适合我们的策略,我们评估了线性、非线性和集成模型。

5.3.1. 模型

检查分类算法:

models = []
models.append(('LR', LogisticRegression(n_jobs=-1)))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', KNeighborsClassifier()))
models.append(('CART', DecisionTreeClassifier()))
models.append(('NB', GaussianNB()))
#Neural Network
models.append(('NN', MLPClassifier()))
# Ensemble Models
# Boosting methods
models.append(('AB', AdaBoostClassifier()))
models.append(('GBM', GradientBoostingClassifier()))
# Bagging methods
models.append(('RF', RandomForestClassifier(n_jobs=-1)))

在进行k折交叉验证后,模型比较如下所示:

mlbf 06in15

尽管一些模型显示出有希望的结果,但考虑到数据集的庞大规模、大量特征以及预测变量与特征之间预期的非线性关系,我们更倾向于使用集成模型。随机森林在集成模型中表现最佳。

6. 模型调优和网格搜索

对随机森林模型进行网格搜索,通过调整估计器的数量和最大深度来进行。随机森林模型的详细信息和要调整的参数在第四章中讨论:

n_estimators = [20,80]
max_depth= [5,10]
criterion = ["gini","entropy"]
param_grid = dict(n_estimators=n_estimators, max_depth=max_depth, \
  criterion = criterion )
model = RandomForestClassifier(n_jobs=-1)
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, \
  scoring=scoring, cv=kfold)
grid_result = grid.fit(X_train, Y_train)
print("Best: %f using %s" % (grid_result.best_score_,\
  grid_result.best_params_))

Output

Best: 0.903438 using {'criterion': 'gini', 'max_depth': 10, 'n_estimators': 80}

7. 完成模型

让我们在调优步骤中找到的最佳参数的基础上完成模型,并进行变量直觉分析。

7.1. 测试数据集的结果

在这一步中,我们评估了在测试集上选择的模型:

# prepare model
model = RandomForestClassifier(criterion='gini', n_estimators=80,max_depth=10)

#model = LogisticRegression()
model.fit(X_train, Y_train)

# estimate accuracy on validation set
predictions = model.predict(X_validation)
print(accuracy_score(Y_validation, predictions))

Output

0.9075

所选模型表现出色,准确率达到 90.75%。让我们看看混淆矩阵:

mlbf 06in16

整体模型性能合理,与训练集结果一致。

7.2. 变量直觉/特征重要性

让我们查看模型的特征重要性:

Importance = pd.DataFrame({'Importance':model.feature_importances_*100},\
 index=X.columns)
Importance.sort_values('Importance', axis=0, ascending=True).plot(kind='barh', \
color='r' )
plt.xlabel('Variable Importance')

Output

mlbf 06in17

变量重要性的结果看起来很直观,而 RSI 和 MOM 过去 30 天的动量指标似乎是两个最重要的特征。特征重要性图表证实了引入新特征会改善模型性能的事实。

7.3. 回测结果

在这一额外步骤中,我们对开发的模型进行回测。我们通过将昨天收盘时持有的头寸的日收益乘以每日收益来创建策略收益列,并将其与实际收益进行比较。

回测交易策略

类似于本案例研究中提出的回测方法可以用于快速回测任何交易策略。

backtestdata = pd.DataFrame(index=X_validation.index)
backtestdata['signal_pred'] = predictions
backtestdata['signal_actual'] = Y_validation
backtestdata['Market Returns'] = X_validation['Close'].pct_change()
backtestdata['Actual Returns'] = backtestdata['Market Returns'] *\
backtestdata['signal_actual'].shift(1)
backtestdata['Strategy Returns'] = backtestdata['Market Returns'] * \
backtestdata['signal_pred'].shift(1)
backtestdata=backtestdata.reset_index()
backtestdata.head()
backtestdata[['Strategy Returns','Actual Returns']].cumsum().hist()
backtestdata[['Strategy Returns','Actual Returns']].cumsum().plot()

Output

mlbf 06in18mlbf 06in19

查看回测结果,我们与实际市场回报并无显著偏差。事实上,所实现的动量交易策略使我们更善于预测价格方向以便盈利。然而,由于我们的准确率并非 100%(但超过 96%),与实际回报相比,我们遭受的损失相对较少。

结论

本案例研究表明,在使用机器学习解决金融问题时,问题的界定是一个关键步骤。通过这样做,我们确定了根据投资目标转换标签并执行特征工程对于这种交易策略是必要的。我们展示了使用与价格趋势和动量相关的直觉特征如何增强模型的预测能力。最后,我们引入了一个回测框架,允许我们使用历史数据模拟交易策略。这使我们能够在冒险使用任何实际资本之前生成结果并分析风险和盈利能力。

章节总结

在“案例研究 1:欺诈检测”中,我们探讨了非平衡数据集的问题以及正确评估指标的重要性。在“案例研究 2:贷款违约概率”中,涵盖了数据处理、特征选择和探索性分析的各种技术和概念。在“案例研究 3:比特币交易策略”中,我们探讨了如何创建技术指标作为模型增强的特征。我们还为交易策略准备了一个回测框架。

总体而言,本章介绍的 Python、机器学习和金融概念可以作为金融中任何其他基于分类的问题的蓝图。

练习

  • 使用与股票或宏观经济变量相关的特征来预测股价是上涨还是下跌(使用本章介绍的基于比特币案例研究的想法)。

  • 创建一个使用交易特征来检测洗钱的模型。可以从Kaggle获取此练习的示例数据集。

  • 使用与信用评级相关的特征对企业进行信用评级分析。

¹ 可能会根据步骤或子步骤的适当性和直觉性重新排序或重命名步骤或子步骤。

² 预测变量进一步用于基于相关性的特征减少。

³ 详细讨论了“案例研究 1:欺诈检测”中的抽样。

第三部分:无监督学习

第七章:无监督学习:降维

在之前的章节中,我们使用监督学习技术构建了机器学习模型,使用已知答案的数据(即输入数据中已有的类标签)。现在我们将探讨无监督学习,在这种学习中,我们从数据集中推断出数据的特征,而输入数据的答案是未知的。无监督学习算法尝试从数据中推断出模式,而不知道数据本应产生的输出。这类模型不需要标记数据,而创建或获取标记数据可能耗时且不切实际,因此可以方便地使用更大的数据集进行分析和模型开发。

降维 是无监督学习中的关键技术。它通过找到一组较小、不同的变量来压缩数据,这些变量捕捉原始特征中最重要的内容,同时最小化信息损失。降维帮助缓解高维度带来的问题,并允许探索高维数据的显著方面,这在其他情况下很难实现。

在金融领域,数据集通常庞大且包含许多维度,因此降维技术证明非常实用和有用。降维技术使我们能够减少数据集中的噪声和冗余,并使用更少的特征找到数据集的近似版本。减少要考虑的变量数量后,探索和可视化数据集变得更加简单。降维技术还通过减少特征数量或找到新特征来增强基于监督学习的模型。从业者使用这些降维技术来跨资产类别和个别投资分配资金,识别交易策略和信号,实施投资组合对冲和风险管理,以及开发工具定价模型。

在本章中,我们将讨论基本的降维技术,并通过投资组合管理、利率建模和交易策略开发三个案例研究进行详细说明。这些案例研究旨在不仅从金融角度涵盖多样化的主题,还突出多个机器学习和数据科学概念。包含 Python 实现的建模步骤和机器学习与金融概念的标准模板可以作为在金融领域其他基于降维的问题的蓝图使用。

在 “案例研究 1:投资组合管理:找到特征投资组合” 中,我们使用降维算法将资本分配到不同的资产类别中,以最大化风险调整后的回报。我们还介绍了一个回测框架,评估我们构建的投资组合的表现。

在“案例研究 2:收益率曲线构建与利率建模”中,我们使用降维技术来生成收益率曲线的典型运动。这将说明如何利用降维技术来降低跨多种资产类别的市场变量的维度,以促进更快速和有效的投资组合管理、交易、套期保值和风险管理。

在“案例研究 3:比特币交易:提升速度和准确性”中,我们使用降维技术进行算法交易。这个案例研究展示了低维度数据探索。

本章代码库

本书代码库中包含基于 Python 的降维模板,以及本章所有案例研究的 Jupyter 笔记本,位于第七章 - 无监督学习 - 降维文件夹中。要在 Python 中解决任何涉及本章介绍的降维模型(如 PCA、SVD、Kernel PCA 或 t-SNE)的机器学习问题,读者需要稍微修改模板以与其问题陈述对齐。本章中提供的所有案例研究都使用标准的 Python 主模板,并按照第三章中呈现的标准化模型开发步骤进行。对于降维案例研究,步骤 6(即模型调优)和步骤 7(即最终化模型)相对较轻,因此这些步骤已与步骤 5 合并。对于步骤无关的情况,它们已被跳过或与其他步骤合并,以使案例研究的流程更加直观。

降维技术

降维通过使用更少的特征更有效地表示给定数据集中的信息。这些技术通过丢弃数据中不含信息的变异或识别数据所在位置或附近的较低维子空间,将数据投影到较低维空间。

有许多种类的降维技术。在本章中,我们将介绍这些最常用的降维技术:

  • 主成分分析(PCA)

  • 核主成分分析(KPCA)

  • t-分布随机邻居嵌入(t-SNE)

应用这些降维技术后,低维特征子空间可以是对应的高维特征子空间的线性或非线性函数。因此,从广义上讲,这些降维算法可以分为线性和非线性。线性算法如 PCA,强制新变量是原始特征的线性组合。

KPCA 和 t-SNE 等非线性算法能够捕捉数据中更复杂的结构。然而,由于选项的无限性,这些算法仍然需要做出假设以得出解决方案。

主成分分析

主成分分析(PCA)的理念是在保留数据中尽可能多的方差的同时,减少具有大量变量的数据集的维度。PCA 使我们能够了解是否存在一个不同的数据表示,可以解释大部分原始数据点。

PCA 找到了一组新的变量,通过线性组合可以得到原始变量。这些新变量称为主成分(PC)。这些主成分是正交的(或者独立的),可以表示原始数据。主成分的数量是 PCA 算法的一个超参数,用于设置目标维度。

PCA 算法通过将原始数据投影到主成分空间上来工作。然后它识别一系列主成分,每个主成分都与数据中的最大方差方向对齐(考虑到先前计算的成分捕捉的变化)。顺序优化还确保新的成分与现有成分不相关。因此,生成的集合构成了向量空间的正交基础。

每个主成分解释的原始数据方差量的减少反映了原始特征之间相关性的程度。例如,捕获 95% 原始变化相对于总特征数量的组件数量提供了对原始数据线性独立信息的见解。为了理解 PCA 的工作原理,让我们考虑 图 7-1 中显示的数据分布。

mlbf 0701

图 7-1. PCA-1

PCA 找到了一个新的象限系统(y’x’ 轴),这是从原始系统中通过平移和旋转得到的。它将坐标系的中心从原点 (0, 0) 移动到数据点的分布中心。然后将 x 轴移动到主要变化的主轴上,这是相对于数据点具有最大变化的方向(即最大散布方向)。然后将另一个轴正交地移动到主轴以外的一个次要变化方向。

图 7-2 展示了一个 PCA 的例子,其中两个维度几乎解释了底层数据的所有方差。

mlbf 0702

图 7-2. PCA-2

这些包含最大方差的新方向被称为主成分,并且设计上彼此正交。

寻找主成分有两种方法:特征分解和奇异值分解(SVD)。

特征分解

特征分解的步骤如下:

  1. 首先为特征创建一个协方差矩阵。

  2. 计算完协方差矩阵后,计算协方差矩阵的特征向量。[¹]

  3. 然后创建特征值。它们定义了主成分的大小。

因此,对于n维度,将有一个n × n的方差-协方差矩阵,结果将是n个特征值和n个特征向量。

Python 的 sklearn 库提供了 PCA 的强大实现。sklearn.decomposition.PCA函数计算所需数量的主成分,并将数据投影到组件空间中。以下代码片段说明了如何从数据集创建两个主成分。

实现

# Import PCA Algorithm
from sklearn.decomposition import PCA
# Initialize the algorithm and set the number of PC's
pca = PCA(n_components=2)
# Fit the model to data
pca.fit(data)
# Get list of PC's
pca.components_
# Transform the model to data
pca.transform(data)
# Get the eigenvalues
pca.explained_variance_ratio

还有额外的项目,如因子负载,可以使用 sklearn 库中的函数获得。它们的使用将在案例研究中进行演示。

奇异值分解

奇异值分解(SVD)是将一个矩阵分解为三个矩阵,并适用于更一般的m × n矩形矩阵。

如果A是一个m × n矩阵,则 SVD 可以将矩阵表示为:

A = U Σ V T

其中A是一个m × n矩阵,U是一个(m × m)正交矩阵,Σ是一个(m × n)非负矩形对角矩阵,V是一个(n × n)正交矩阵。给定矩阵的 SVD 告诉我们如何精确地分解矩阵。Σ是一个对角线上有m个对角值的对角矩阵,称为奇异值。它们的大小表明它们对保留原始数据信息的重要性。V包含作为列向量的主成分。

如上所示,特征值分解和奇异值分解告诉我们使用 PCA 有效地从不同角度查看初始数据。两者始终给出相同的答案;然而,SVD 比特征值分解更高效,因为它能处理稀疏矩阵(即包含极少非零元素的矩阵)。此外,SVD 在数值稳定性方面表现更佳,特别是当某些特征强相关时。

截断 SVD是 SVD 的一个变体,它仅计算最大的奇异值,其中计算的数量是用户指定的参数。这种方法与常规 SVD 不同,因为它产生的分解中列数等于指定的截断数。例如,给定一个n × n矩阵,SVD 将生成具有n列的矩阵,而截断 SVD 将生成具有少于n个列的指定数目的矩阵。

实现

from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(ncomps=20).fit(X)

在 PCA 技术的弱点方面,虽然它在降低维数方面非常有效,但生成的主成分可能比原始特征的解释性要差。此外,结果可能对选择的主成分数量敏感。例如,与原始特征列表相比,如果主成分太少,可能会丢失一些信息。此外,如果数据非常非线性,PCA 可能效果不佳。

核主成分分析

PCA 的一个主要局限性是它只适用于线性变换。核主成分分析(KPCA)扩展了 PCA 以处理非线性。它首先将原始数据映射到某些非线性特征空间(通常是更高维度之一),然后在该空间中应用 PCA 来提取主成分。

KPCA 适用的一个简单示例显示在图 7-3 中。线性变换适用于左图中的蓝色和红色数据点。然而,如果所有点按右图中的图表排列,结果就不是线性可分的。我们随后需要应用 KPCA 来分离这些组件。

mlbf 0703

图 7-3. 核主成分分析

Implementation

from sklearn.decomposition import KernelPCA
kpca = KernelPCA(n_components=4, kernel='rbf').fit_transform(X)

在 Python 代码中,我们指定kernel='rbf',这是径向基函数核。这通常用作机器学习技术中的核,例如在 SVMs 中(见第四章)。

使用 KPCA,在更高维度空间中进行组件分离变得更加容易,因为映射到更高维度空间通常提供更大的分类能力。

t-分布随机邻居嵌入

t-分布随机邻域嵌入(t-SNE)是一种降维算法,通过建模每个点周围邻居的概率分布来减少维度。这里,术语邻居指的是离给定点最近的一组点。与在高维度中保持点之间距离不同,该算法强调在低维度中将相似的点放在一起。

该算法首先计算对应高维和低维空间中数据点相似性的概率。点的相似性被计算为条件概率,即如果邻居是按照以点A为中心的正态分布的概率密度比例来选择的话,点A会选择点B作为其邻居的概率。然后,该算法试图最小化这些条件概率(或相似性)在高维和低维空间中的差异,以完美地表示低维空间中的数据点。

Implementation

from sklearn.manifold import TSNE
X_tsne = TSNE().fit_transform(X)

在本章的第三个案例研究中展示了 t-SNE 的实现。

案例研究 1:投资组合管理:找到一个特征投资组合

投资组合管理的主要目标之一是将资本分配到不同的资产类别中,以最大化风险调整回报。均值方差投资组合优化是资产配置中最常用的技术。该方法需要估计协方差矩阵和考虑的资产的预期回报。然而,财务回报的不稳定性导致这些输入的估计误差,特别是当回报样本量不足以与被分配的资产数量相比时。这些错误严重危及了结果投资组合的最优性,导致结果不佳和不稳定的结果。

降维是一种可以用来解决这个问题的技术。使用 PCA,我们可以取我们资产的n × n协方差矩阵,并创建一组线性不相关的主要投资组合(有时在文献中称为eigen portfolio),由我们的资产及其对应的方差组成。协方差矩阵的主成分捕捉了资产之间的大部分协变性,并且彼此之间是互不相关的。此外,我们可以使用标准化的主成分作为投资组合权重,其统计保证是这些主要投资组合的回报是线性不相关的。

在本案例研究结束时,读者将熟悉通过 PCA 找到用于资产配置的特征组合(eigen portfolio)的一般方法,从理解 PCA 概念到回测不同的主成分。

使用降维进行资产配置的蓝图

1. 问题定义

我们在本案例研究中的目标是通过在股票回报数据集上使用 PCA 来最大化一个权益投资组合的风险调整回报。

本案例研究使用的数据集是道琼斯工业平均指数(DJIA)及其 30 只股票。使用的回报数据将从 2000 年开始,并可从 Yahoo Finance 下载。

我们还将比较我们假设投资组合的表现与基准的表现,并回测模型以评估方法的有效性。

2. 开始——加载数据和 Python 包

2.1. 加载 Python 包

下面是用于数据加载、数据分析、数据准备、模型评估和模型调优的库列表。这些包和函数的详细信息可以在第二章和第四章中找到。

用于降维的包

from sklearn.decomposition import PCA
from sklearn.decomposition import TruncatedSVD
from numpy.linalg import inv, eig, svd
from sklearn.manifold import TSNE
from sklearn.decomposition import KernelPCA

用于数据处理和可视化的包

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import read_csv, set_option
from pandas.plotting import scatter_matrix
import seaborn as sns
from sklearn.preprocessing import StandardScaler

2.2. 加载数据

我们导入包含 DJIA 指数所有公司调整后收盘价格的数据框架:

# load dataset
dataset = read_csv('Dow_adjcloses.csv', index_col=0)

3. 探索性数据分析

接下来,我们检查数据集。

3.1. 描述性统计

让我们来看看数据的形状:

dataset.shape

输出

(4804, 30)

数据由 30 列和 4,804 行组成,包含自 2000 年以来指数中 30 只股票的日收盘价格。

3.2. 数据可视化

我们必须首先对数据有一个基本的了解。让我们看一下收益相关性:

correlation = dataset.corr()
plt.figure(figsize=(15, 15))
plt.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True,annot=True, cmap='cubehelix')

日常回报之间存在显著的正相关性。图表(完整版本可在GitHub上找到)还表明,数据中嵌入的信息可以由更少的变量表示(即小于我们现在有的 30 个维度)。在实施降维之后,我们将进一步详细查看数据。

Output

mlbf 07in01

4. 数据准备

我们在接下来的几节中为建模准备数据。

4.1. 数据清理

首先,我们检查行中的缺失值,然后要么删除它们,要么用列的均值填充:

#Checking for any null values and removing the null values'''
print('Null Values =',dataset.isnull().values.any())

Output

Null Values = True

在我们开始日期后,有些股票被添加到指数中。为了确保适当的分析,我们将放弃那些超过 30%缺失值的股票。两只股票符合此条件—道琼斯化学和 Visa:

missing_fractions = dataset.isnull().mean().sort_values(ascending=False)
missing_fractions.head(10)
drop_list = sorted(list(missing_fractions[missing_fractions > 0.3].index))
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

Output

(4804, 28)

我们最终得到了 28 家公司的回报数据,另外还有一家 DJIA 指数的数据。现在我们用列的均值填充缺失值:

# Fill the missing values with the last value available in the dataset.
dataset=dataset.fillna(method='ffill')

4.2. 数据转换

除了处理缺失值外,我们还希望将数据集特征标准化到单位比例尺上(均值 = 0,方差 = 1)。在应用 PCA 之前,所有变量应处于相同的尺度;否则,具有较大值的特征将主导结果。我们使用 sklearn 中的StandardScaler来标准化数据集,如下所示:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(datareturns)
rescaledDataset = pd.DataFrame(scaler.fit_transform(datareturns),columns =\
 datareturns.columns, index = datareturns.index)
# summarize transformed data
datareturns.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)

总体而言,清理和标准化数据对于创建可用于降维的有意义且可靠的数据集至关重要。

让我们看看清理和标准化数据集中其中一只股票的回报:

# Visualizing Log Returns for the DJIA
plt.figure(figsize=(16, 5))
plt.title("AAPL Return")
rescaledDataset.AAPL.plot()
plt.grid(True);
plt.legend()
plt.show()

Output

mlbf 07in02

5. 评估算法和模型

5.1. 训练测试拆分

投资组合被分为训练集和测试集,以进行有关最佳投资组合的分析和回测:

# Dividing the dataset into training and testing sets
percentage = int(len(rescaledDataset) * 0.8)
X_train = rescaledDataset[:percentage]
X_test = rescaledDataset[percentage:]

stock_tickers = rescaledDataset.columns.values
n_tickers = len(stock_tickers)

5.2. 模型评估:应用主成分分析

作为下一步,我们创建一个函数,使用 sklearn 库执行 PCA。此函数从数据生成主成分,用于进一步分析:

pca = PCA()
PrincipalComponent=pca.fit(X_train)

5.2.1. 使用 PCA 解释方差

在这一步中,我们观察使用 PCA 解释的方差。每个主成分解释的原始数据方差的减少反映了原始特征之间的相关程度。第一个主成分捕获了原始数据中的最大方差,第二个成分是第二大方差的表示,依此类推。具有最低特征值的特征向量描述了数据集中最少的变化量。因此,可以放弃这些值。

下面的图表显示了每个主成分的数量及其解释的方差。

NumEigenvalues=20
fig, axes = plt.subplots(ncols=2, figsize=(14,4))
Series1 = pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).sort_values()
Series2 = pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).cumsum()
Series1.plot.barh(title='Explained Variance Ratio by Top Factors', ax=axes[0]);
Series1.plot(ylim=(0,1), ax=axes[1], title='Cumulative Explained Variance');

Output

mlbf 07in03

我们发现,最重要的因素解释了每日回报变化的约 40%。这个主导的主成分通常被解释为“市场”因素。在查看投资组合权重时,我们将讨论这个因素及其他因素的解释。

右侧图表显示了累计解释的方差,并指出约十个因素解释了 28 只股票回报中的 73%方差。

5.2.2. 查看投资组合权重

在这一步中,我们更详细地查看各个主成分。这些可能比原始特征更难以解释。然而,我们可以查看每个主成分上因子的权重,以评估相对于这 28 只股票的任何直觉主题。我们构建了五个投资组合,将每只股票的权重定义为前五个主成分中的每一个。然后,我们创建一个散点图,以可视化当前所选主成分的每家公司的组织排列下降绘图重量:

def PCWeights():
    #Principal Components (PC) weights for each 28 PCs

    weights = pd.DataFrame()
    for i in range(len(pca.components_)):
        weights["weights_{}".format(i)] = \
        pca.components_[i] / sum(pca.components_[i])
    weights = weights.values.T
    return weights
weights=PCWeights()
sum(pca.components_[0])

Output

-5.247808242068631
NumComponents=5
topPortfolios = pd.DataFrame(pca.components_[:NumComponents],\
   columns=dataset.columns)
eigen_portfolios = topPortfolios.div(topPortfolios.sum(1), axis=0)
eigen_portfolios.index = [f'Portfolio {i}' for i in range( NumComponents)]
np.sqrt(pca.explained_variance_)
eigen_portfolios.T.plot.bar(subplots=True, layout=(int(NumComponents),1),  \
figsize=(14,10), legend=False, sharey=True, ylim= (-1,1))

鉴于图表的尺度相同,我们还可以如下查看热图:

Output

mlbf 07in04

# plotting heatmap
sns.heatmap(topPortfolios)

Output

mlbf 07in05

热图和条形图显示了每个特征向量中不同股票的贡献。

传统上,每个主要投资组合背后的直觉是它代表某种独立的风险因素。这些风险因素的表现取决于投资组合中的资产。在我们的案例研究中,这些资产都是美国国内的股票。方差最大的主要投资组合通常是系统性风险因素(即“市场”因素)。观察第一个主成分(Portfolio 0),我们看到权重在各个股票之间均匀分布。这个几乎等权重的投资组合解释了指数方差的 40%,是系统性风险因素的一个公平代表。

其余的特征组合通常对应于部门或行业因素。例如,Portfolio 1 高度权重于来自健康保健部门的 JNJ 和 MRK 等股票。同样,Portfolio 3 高度权重于技术和电子公司,如 AAPL、MSFT 和 IBM。

当我们的投资组合资产范围扩展到包括广泛的全球投资时,我们可能会识别出国际股票风险、利率风险、商品暴露、地理风险等因素。

在下一步中,我们找到最佳的特征组合。

5.2.3. 寻找最佳的特征组合

为了确定最佳的特征组合,我们使用夏普比率。这是一种根据投资组合的年化回报与年化波动率对风险调整后表现进行评估的方法。高夏普比率解释了在特定投资组合中的高回报和/或低波动率。年化夏普比率通过将年化回报除以年化波动率来计算。对于年化回报,我们应用所有周期内的几何平均数(一年内交易所的运作日)。年化波动率通过计算回报的标准偏差并乘以每年操作的周期的平方根来计算。

下面的代码计算一个投资组合的夏普比率:

# Sharpe Ratio Calculation
# Calculation based on conventional number of trading days per year (i.e., 252).
def sharpe_ratio(ts_returns, periods_per_year=252):
    n_years = ts_returns.shape[0]/ periods_per_year
    annualized_return = np.power(np.prod(1+ts_returns), (1/n_years))-1
    annualized_vol = ts_returns.std() * np.sqrt(periods_per_year)
    annualized_sharpe = annualized_return / annualized_vol

    return annualized_return, annualized_vol, annualized_sharpe

我们构建一个循环来计算每个特征组合的主成分权重。然后使用夏普比率函数查找具有最高夏普比率的投资组合。一旦我们知道哪个投资组合具有最高的夏普比率,我们可以将其性能与指数进行比较可视化:

def optimizedPortfolio():
    n_portfolios = len(pca.components_)
    annualized_ret = np.array([0.] * n_portfolios)
    sharpe_metric = np.array([0.] * n_portfolios)
    annualized_vol = np.array([0.] * n_portfolios)
    highest_sharpe = 0
    stock_tickers = rescaledDataset.columns.values
    n_tickers = len(stock_tickers)
    pcs = pca.components_

    for i in range(n_portfolios):

        pc_w = pcs[i] / sum(pcs[i])
        eigen_prtfi = pd.DataFrame(data ={'weights': pc_w.squeeze()*100}, \
        index = stock_tickers)
        eigen_prtfi.sort_values(by=['weights'], ascending=False, inplace=True)
        eigen_prti_returns = np.dot(X_train_raw.loc[:, eigen_prtfi.index], pc_w)
        eigen_prti_returns = pd.Series(eigen_prti_returns.squeeze(),\
         index=X_train_raw.index)
        er, vol, sharpe = sharpe_ratio(eigen_prti_returns)
        annualized_ret[i] = er
        annualized_vol[i] = vol
        sharpe_metric[i] = sharpe

        sharpe_metric= np.nan_to_num(sharpe_metric)

    # find portfolio with the highest Sharpe ratio
    highest_sharpe = np.argmax(sharpe_metric)

    print('Eigen portfolio #%d with the highest Sharpe. Return %.2f%%,\
 vol = %.2f%%, Sharpe = %.2f' %
          (highest_sharpe,
           annualized_ret[highest_sharpe]*100,
           annualized_vol[highest_sharpe]*100,
           sharpe_metric[highest_sharpe]))

    fig, ax = plt.subplots()
    fig.set_size_inches(12, 4)
    ax.plot(sharpe_metric, linewidth=3)
    ax.set_title('Sharpe ratio of eigen-portfolios')
    ax.set_ylabel('Sharpe ratio')
    ax.set_xlabel('Portfolios')

    results = pd.DataFrame(data={'Return': annualized_ret,\
    'Vol': annualized_vol,
    'Sharpe': sharpe_metric})
    results.dropna(inplace=True)
    results.sort_values(by=['Sharpe'], ascending=False, inplace=True)
    print(results.head(5))

    plt.show()

optimizedPortfolio()

Output

Eigen portfolio #0 with the highest Sharpe. Return 11.47%, vol = 13.31%, \
Sharpe = 0.86
    Return    Vol  Sharpe
0    0.115  0.133   0.862
7    0.096  0.693   0.138
5    0.100  0.845   0.118
1    0.057  0.670   0.084

mlbf 07in06

如上结果所示,组合 0 是表现最佳的,具有最高的回报和最低的波动率。让我们看看这个投资组合的构成:

weights = PCWeights()
portfolio = portfolio = pd.DataFrame()

def plotEigen(weights, plot=False, portfolio=portfolio):
    portfolio = pd.DataFrame(data ={'weights': weights.squeeze() * 100}, \
    index = stock_tickers)
    portfolio.sort_values(by=['weights'], ascending=False, inplace=True)
    if plot:
        portfolio.plot(title='Current Eigen-Portfolio Weights',
            figsize=(12, 6),
            xticks=range(0, len(stock_tickers), 1),
            rot=45,
            linewidth=3
            )
        plt.show()

    return portfolio

# Weights are stored in arrays, where 0 is the first PC's weights.
plotEigen(weights=weights[0], plot=True)

Output

mlbf 07in07

请记住,这是解释了 40%方差并代表系统风险因子的投资组合。查看投资组合权重(y 轴上的百分比),它们变化不大,所有股票的权重都在 2.7%到 4.5%的范围内。然而,权重在金融部门较高,像 AXP、JPM 和 GS 等股票的权重高于平均水平。

5.2.4. 对特征组合进行回测

现在我们将尝试在测试集上对这个算法进行回测。我们将查看一些表现最佳的和最差的投资组合。对于表现最佳的投资组合,我们查看第三和第四名的特征组合(组合 51),而被评为最差表现的是第 19 名的投资组合(组合 14):

def Backtest(eigen):

    '''
 Plots principal components returns against real returns.
 '''

    eigen_prtfi = pd.DataFrame(data ={'weights': eigen.squeeze()}, \
    index=stock_tickers)
    eigen_prtfi.sort_values(by=['weights'], ascending=False, inplace=True)

    eigen_prti_returns = np.dot(X_test_raw.loc[:, eigen_prtfi.index], eigen)
    eigen_portfolio_returns = pd.Series(eigen_prti_returns.squeeze(),\
     index=X_test_raw.index)
    returns, vol, sharpe = sharpe_ratio(eigen_portfolio_returns)
    print('Current Eigen-Portfolio:\nReturn = %.2f%%\nVolatility = %.2f%%\n\
 Sharpe = %.2f' % (returns * 100, vol * 100, sharpe))
    equal_weight_return=(X_test_raw * (1/len(pca.components_))).sum(axis=1)
    df_plot = pd.DataFrame({'EigenPorfolio Return': eigen_portfolio_returns, \
    'Equal Weight Index': equal_weight_return}, index=X_test.index)
    np.cumprod(df_plot + 1).plot(title='Returns of the equal weighted\
 index vs. First eigen-portfolio',
                          figsize=(12, 6), linewidth=3)
    plt.show()

Backtest(eigen=weights[5])
Backtest(eigen=weights[1])
Backtest(eigen=weights[14])

Output

Current Eigen-Portfolio:
Return = 32.76%
Volatility = 68.64%
Sharpe = 0.48

mlbf 07in08

Current Eigen-Portfolio:
Return = 99.80%
Volatility = 58.34%
Sharpe = 1.71

mlbf 07in09

Current Eigen-Portfolio:
Return = -79.42%
Volatility = 185.30%
Sharpe = -0.43

mlbf 07in10

如前面的图表所示,顶级投资组合的特征组合回报优于等权重指数。第 19 名的特征组合在测试集中表现显著低于市场。这种超额表现和表现不佳归因于特征组合中股票或部门的权重。我们可以进一步深入了解每个投资组合的单个驱动因素。例如,组合 1 在多个医疗保健股票中分配了高权重,如前所述。这个部门从 2017 年开始出现了显著增长,这在特征组合 1 的图表中有所体现。

鉴于这些特征组合是独立的,它们还提供了分散投资的机会。因此,我们可以跨这些不相关的特征组合进行投资,从而带来其他潜在的投资组合管理好处。

结论

在这个案例研究中,我们在投资组合管理的背景下应用了降维技术,利用 PCA 中的特征值和特征向量进行资产配置。

我们展示了尽管可能失去一些可解释性,但得到的投资组合背后的理念可以与风险因素相匹配。在这个例子中,第一个特征组合代表了一个系统性风险因素,而其他的则展示了特定行业或行业集中度。

通过回测,我们发现在训练集上表现最佳的投资组合也在测试集上取得了最强的表现。根据夏普比率,这些投资组合中的几个表现优于指数,夏普比率是本次练习中使用的风险调整后的绩效指标。

总体而言,我们发现使用主成分分析(PCA)和分析特征组合能够提供一种稳健的资产配置和投资组合管理方法。

案例研究 2:收益率曲线构建与利率建模

在投资组合管理、交易和风险管理中存在许多问题需要深入理解和建模收益率曲线。

收益率曲线表示在一系列到期期限上的利率或收益率,通常以折线图形式呈现,如第五章的“案例研究 4:收益率曲线预测”中所讨论的。收益率曲线反映了某一时点的“资金价格”,由于货币的时间价值,通常显示出利率随到期期限延长而上升的情况。

金融研究人员对收益率曲线进行了研究,发现曲线形状的变化主要由几个不可观测的因素引起。具体来说,经验研究表明,超过 99%的美国国债收益率变动可以归因于三个因素,通常称为水平、斜率和曲率。这些名称描述了每个因素在冲击下如何影响收益率曲线的形状。水平冲击几乎同等程度地改变所有到期收益率,导致整条曲线整体上移或下移,形成平行位移。斜率因子的冲击改变了短期和长期利率之间的差异。例如,当长期利率的增幅超过短期利率时,曲线变得更为陡峭(即曲线在视觉上更向上倾斜)。短期和长期利率的变化也可能导致较平坦的收益率曲线。曲率因子的冲击主要影响中期利率,导致出现驼峰、扭曲或 U 型特征。

降维将收益率曲线的运动分解为这三个因子。将收益率曲线减少到较少的组成部分意味着我们可以专注于收益率曲线中的几个直观维度。交易员和风险经理使用这种技术来在对冲利率风险时压缩曲线中的风险因素。同样,投资组合经理在分配资金时分析的维度更少。利率结构师使用这种技术来建模收益率曲线并分析其形状。总体而言,这促进了更快速和更有效的投资组合管理、交易、对冲和风险管理。

在这个案例研究中,我们使用 PCA 来生成收益率曲线的典型运动,并展示前三个主成分分别对应曲线的水平、斜率和曲率。

使用降维生成收益率曲线的蓝图

1. 问题定义

在本案例研究中,我们的目标是使用降维技术生成收益率曲线的典型运动。

本案例研究使用的数据来自Quandl,这是一个主要的金融、经济和替代数据集来源。我们使用 1960 年以来的每日频率数据,涵盖了从 1 个月到 30 年的 11 个期限(或到期时间)的国债曲线数据。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

加载 Python 包的步骤与前一次降维案例研究类似。有关详细信息,请参阅本案例研究的 Jupyter 笔记本。

2.2. 加载数据

在第一步中,我们从 Quandl 加载不同期限的国债曲线数据:

# In order to use quandl, ApiConfig.api_key will need to be
# set to identify you to the quandl API. Please see API
# Documentation of quandl for more details
quandl.ApiConfig.api_key = 'API Key'
treasury = ['FRED/DGS1MO','FRED/DGS3MO','FRED/DGS6MO','FRED/DGS1',\
'FRED/DGS2','FRED/DGS3','FRED/DGS5','FRED/DGS7','FRED/DGS10',\
'FRED/DGS20','FRED/DGS30']

treasury_df = quandl.get(treasury)
treasury_df.columns = ['TRESY1mo','TRESY3mo','TRESY6mo','TRESY1y',\
'TRESY2y','TRESY3y','TRESY5y','TRESY7y','TRESY10y',\'TRESY20y','TRESY30y']
dataset = treasury_df

3. 探索性数据分析

在这里,我们将首次查看数据。

3.1. 描述统计

在下一步中,我们来看一下数据集的形状:

# shape
dataset.shape

Output

(14420, 11)

数据集有 14,420 行,包含 50 多年来 11 个期限的国债曲线数据。

3.2. 数据可视化

让我们来看一下从下载数据中得到的利率变动:

dataset.plot(figsize=(10,5))
plt.ylabel("Rate")
plt.legend(bbox_to_anchor=(1.01, 0.9), loc=2)
plt.show()

Output

mlbf 07in11

在下一步中,我们来看一下不同期限之间的相关性:

# correlation
correlation = dataset.corr()
plt.figure(figsize=(15, 15))
plt.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True, annot=True, cmap='cubehelix')

Output

mlbf 07in12

正如您在输出中所看到的(GitHub 上提供全尺寸版本),不同期限之间存在显著的正相关性。这表明,在模型化数据时减少维度可能是有用的。在实施降维模型后,将对数据进行更多的可视化分析。

4. 数据准备

在这个案例研究中,数据清理和转换是必要的建模前提。

4.1. 数据清理

在这里,我们检查数据中的缺失值,并将其删除或用列的均值填充。

4.2. 数据转换

在应用 PCA 之前,我们将变量标准化到相同的尺度上,以防止具有较大值的特征主导结果。我们使用 sklearn 中的 StandardScaler 函数将数据集的特征标准化到单位尺度(均值 = 0,方差 = 1):

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(dataset)
rescaledDataset = pd.DataFrame(scaler.fit_transform(dataset),\
columns = dataset.columns,
index = dataset.index)
# summarize transformed data
dataset.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)

可视化标准化数据集

rescaledDataset.plot(figsize=(14, 10))
plt.ylabel("Rate")
plt.legend(bbox_to_anchor=(1.01, 0.9), loc=2)
plt.show()

Output

mlbf 07in13

5. 评估算法和模型

5.2. 应用主成分分析进行模型评估

接下来,我们创建一个使用 sklearn 库执行 PCA 的函数。此函数从数据中生成主成分,用于进一步分析:

pca = PCA()
PrincipalComponent=pca.fit(rescaledDataset)

5.2.1. 使用 PCA 解释方差

NumEigenvalues=5
fig, axes = plt.subplots(ncols=2, figsize=(14, 4))
pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).sort_values().\
plot.barh(title='Explained Variance Ratio by Top Factors',ax=axes[0]);
pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).cumsum()\
.plot(ylim=(0,1),ax=axes[1], title='Cumulative Explained Variance');
# explained_variance
pd.Series(np.cumsum(pca.explained_variance_ratio_)).to_frame\
('Explained Variance_Top 5').head(NumEigenvalues).style.format('{:,.2%}'.format)

Output

解释的方差前 5
0 84.36%
1 98.44%
2 99.53%
3 99.83%
4 99.94%

mlbf 07in14

前三个主成分分别占方差的 84.4%,14.08% 和 1.09%。累计起来,它们描述了数据中超过 99.5% 的所有运动。这是维度非常高效的降低。回想一下,在第一个案例研究中,我们看到前 10 个成分仅占方差的 73%。

5.2.2. 主成分背后的直觉

理想情况下,我们可以对这些主成分有一些直觉和解释。为了探索这一点,我们首先有一个确定每个主成分权重的函数,然后执行主成分的可视化:

def PCWeights():
    '''
 Principal Components (PC) weights for each 28 PCs
 '''
    weights = pd.DataFrame()

    for i in range(len(pca.components_)):
        weights["weights_{}".format(i)] = \
        pca.components_[i] / sum(pca.components_[i])

    weights = weights.values.T
    return weights

weights=PCWeights()
weights = PCWeights()
NumComponents=3

topPortfolios = pd.DataFrame(weights[:NumComponents], columns=dataset.columns)
topPortfolios.index = [f'Principal Component {i}' \
for i in range(1, NumComponents+1)]

axes = topPortfolios.T.plot.bar(subplots=True, legend=False, figsize=(14, 10))
plt.subplots_adjust(hspace=0.35)
axes[0].set_ylim(0, .2);

Output

mlbf 07in15

pd.DataFrame(pca.components_[0:3].T).plot(style= ['s-','o-','^-'], \
                            legend=False, title="Principal Component")

Output

mlbf 07in16

通过绘制特征向量的成分,我们可以得出以下解释:

主成分 1

这个特征向量的所有值都是正的,所有期限方向的权重都是相同的。这意味着第一个主成分反映了导致所有到期收益率朝同一方向移动的运动,对应于收益率曲线的方向性运动。这些是使整个收益率曲线上移或下移的运动。

主成分 2

第二个特征向量的前半部分为负,后半部分为正。曲线的短端(长端)的国库利率权重为正(负)。这意味着第二主成分反映了使得短端朝一个方向移动,而长端朝另一个方向移动的运动,因此代表了收益率曲线的斜率运动

主成分 3

第三个特征向量的前三分之一成分为负,中间三分之一为正,最后三分之一为负。这意味着第三主成分反映了使得短端和长端朝一个方向移动,而中间部分朝另一个方向移动的运动,导致了收益率曲线的曲率运动

5.2.3. 使用主成分重建曲线

主成分分析(PCA)的关键特性之一是利用 PCA 的输出重建初始数据集的能力。通过简单的矩阵重建,我们可以生成几乎精确的初始数据副本:

pca.transform(rescaledDataset)[:, :2]

输出

array([[ 4.97514826, -0.48514999],
       [ 5.03634891, -0.52005102],
       [ 5.14497849, -0.58385444],
       ...,
       [-1.82544584,  2.82360062],
       [-1.69938513,  2.6936174 ],
       [-1.73186029,  2.73073137]])

从机械上讲,PCA 只是一个矩阵乘法:

Y=XW

其中Y是主成分,X是输入数据,W是系数矩阵,我们可以使用下面的等式来恢复原始矩阵:

X=YW

其中W'是系数矩阵W的逆。

nComp=3
reconst= pd.DataFrame(np.dot(pca.transform(rescaledDataset)[:, :nComp],\
pca.components_[:nComp,:]),columns=dataset.columns)
plt.figure(figsize=(10,8))
plt.plot(reconst)
plt.ylabel("Treasury Rate")
plt.title("Reconstructed Dataset")
plt.show()

该图显示了复制的国库利率图表,并展示了仅使用前三个主成分,我们能够复制原始图表。尽管将数据从 11 个维度减少到三个,我们仍保留了超过 99%的信息,并且可以轻松复制原始数据。此外,我们还能直观地理解这三个收益率曲线的驱动因素。将收益率曲线降低到更少的组件意味着从业者可以专注于影响利率的更少因素。例如,为了对冲投资组合,仅保护前三个主成分的投资组合可能已经足够。

输出

mlbf 07in17

结论

在本案例研究中,我们介绍了降维以将国库利率曲线分解为较少的组件。我们看到这些主成分对于本案例研究非常直观。前三个主成分解释了超过 99.5%的变化,并分别代表方向性移动、斜率移动和曲率移动。

通过主成分分析、分析特征向量并理解背后的直觉,我们展示了如何通过降维在收益率曲线中引入更少的直觉维度。这种对收益率曲线的降维可能会导致更快速和更有效的投资组合管理、交易、对冲和风险管理。

案例研究 3:比特币交易:提升速度和准确性

随着交易变得更加自动化,交易者将继续寻求使用尽可能多的特征和技术指标,以使其策略更加准确和高效。其中一个挑战是添加更多变量会导致复杂性增加,越来越难以得出可靠的结论。使用降维技术,我们可以将许多特征和技术指标压缩为几个逻辑集合,同时仍保留原始数据的显著变异量。这有助于加速模型训练和调优。此外,它通过消除相关变量来防止过拟合,后者可能导致更多损害而非好处。降维还增强了数据集探索和可视化,以了解分组或关系,这在构建和持续监控交易策略时是一个重要任务。

在本案例研究中,我们将使用降维技术来增强“案例研究 3:比特币交易策略”,该案例研究在第六章中介绍。在本案例研究中,我们设计了一种比特币交易策略,考虑了短期和长期价格之间的关系,以预测买入或卖出信号。我们创建了几个新的直观的技术指标特征,包括趋势、成交量、波动性和动量。我们对这些特征应用了降维技术,以获得更好的结果。

使用降维来增强交易策略的蓝图

1. 问题定义

在本案例研究中,我们的目标是使用降维技术来增强算法交易策略。本案例研究中使用的数据和变量与“案例研究 3:比特币交易策略”相同。作为参考,我们使用的是自 2012 年 1 月至 2017 年 10 月的比特币日内价格数据、成交量和加权比特币价格。本案例研究中介绍的步骤 3 和 4 使用了与第六章中案例研究相同的步骤。因此,在本案例研究中将这些步骤压缩,以避免重复。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究中使用的 Python 包与本章前两个案例研究中介绍的相同。

3. 探索性数据分析

参考“3. 探索性数据分析”以获取此步骤的更多细节。

4. 数据准备

我们将在以下几节中为建模准备数据。

4.1. 数据清洗

我们通过使用最后可用值填充 NA 值来清理数据:

dataset[dataset.columns] = dataset[dataset.columns].ffill()

4.2. 为分类准备数据

我们给每次移动附加以下标签:如果短期价格比长期价格上涨,则为 1;如果短期价格比长期价格下跌,则为 0。这个标签被分配给我们将称为信号的变量,这是本案例研究的预测变量。让我们看一下预测数据:

dataset.tail(5)

Output

mlbf 07in18

数据集包含信号列以及所有其他列。

4.3. 特征工程

在这一步中,我们构建了一个数据集,其中包含用于进行信号预测的预测变量。使用比特币每日开盘价、最高价、最低价、收盘价和交易量数据,我们计算以下技术指标:

  • 移动平均线

  • 随机震荡器 %K 和 %D

  • 相对强弱指数(RSI)

  • 变动率(ROC)

  • 动量(MOM)

所有指标的构建代码以及它们的描述都在第六章中呈现。最终数据集和使用的列如下:

mlbf 07in19

4.4. 数据可视化

让我们看一下预测变量的分布:

fig = plt.figure()
plot = dataset.groupby(['signal']).size().plot(kind='barh', color='red')
plt.show()

Output

mlbf 07in20

预测信号“购买”的时间为 52.9%。

5. 评估算法和模型

接下来,我们进行维度约简并评估模型。

5.1. 训练测试分离

在这一步中,我们将数据集分割为训练集和测试集:

Y= subset_dataset["signal"]
X = subset_dataset.loc[:, dataset.columns != 'signal'] validation_size = 0.2
X_train, X_validation, Y_train, Y_validation = train_test_split\
(X, Y, test_size=validation_size, random_state=1)

在应用维度约简之前,我们将变量标准化到相同的尺度上。数据标准化是使用以下 Python 代码执行的:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(X_train)
rescaledDataset = pd.DataFrame(scaler.fit_transform(X_train),\
columns = X_train.columns, index = X_train.index)
# summarize transformed data
X_train.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)
rescaledDataset.head(2)

Output

mlbf 07in21

5.2. 奇异值分解(特征降维)

在这里,我们将使用 SVD 执行 PCA。具体来说,我们使用 sklearn 包中的TruncatedSVD方法,将完整数据集转换为仅使用前五个组件的表示:

ncomps = 5
svd = TruncatedSVD(n_components=ncomps)
svd_fit = svd.fit(rescaledDataset)
Y_pred = svd.fit_transform(rescaledDataset)
ax = pd.Series(svd_fit.explained_variance_ratio_.cumsum()).plot(kind='line', \
figsize=(10, 3))
ax.set_xlabel("Eigenvalues")
ax.set_ylabel("Percentage Explained")
print('Variance preserved by first 5 components == {:.2%}'.\
format(svd_fit.explained_variance_ratio_.cumsum()[-1]))

Output

mlbf 07in22

通过仅使用五个组件而不是原始的 25+个特征,我们保留了 92.75%的方差。这对于模型分析和迭代是非常有用的压缩。

为了方便起见,我们将专门为这五个顶级组件创建一个 Python 数据框架:

dfsvd = pd.DataFrame(Y_pred, columns=['c{}'.format(c) for \
c in range(ncomps)], index=rescaledDataset.index)
print(dfsvd.shape)
dfsvd.head()

Output

(8000, 5)
c0 c1 c2 c3 c4
2834071 –2.252 1.920 0.538 –0.019 –0.967
2836517 5.303 –1.689 –0.678 0.473 0.643
2833945 –2.315 –0.042 1.697 –1.704 1.672
2835048 –0.977 0.782 3.706 –0.697 0.057
2838804 2.115 –1.915 0.475 –0.174 –0.299

5.2.1. 减少特征的基本可视化

让我们可视化压缩后的数据集:

svdcols = [c for c in dfsvd.columns if c[0] == 'c']

对角线图

对角线图是一组 2D 散点图的简单表示,其中每个组件都与其他每个组件进行绘制。数据点根据其信号分类着色:

plotdims = 5
ploteorows = 1
dfsvdplot = dfsvd[svdcols].iloc[:, :plotdims]
dfsvdplot['signal']=Y_train
ax = sns.pairplot(dfsvdplot.iloc[::ploteorows, :], hue='signal', size=1.8)

Output

mlbf 07in23

我们可以看到,彩色点有明显的分离(完整的彩色版本可以在GitHub上找到),这意味着来自同一信号的数据点倾向于聚集在一起。随着从第一到第五个成分的进展,信号分布的特征越来越相似。尽管如此,这幅图表支持我们在模型中使用所有五个成分。

5.3. t-SNE 可视化

在这一步骤中,我们实现了 t-SNE,并查看了相关的可视化。我们将使用 Scikit-learn 中可用的基本实现:

tsne = TSNE(n_components=2, random_state=0)

Z = tsne.fit_transform(dfsvd[svdcols])
dftsne = pd.DataFrame(Z, columns=['x','y'], index=dfsvd.index)

dftsne['signal'] = Y_train

g = sns.lmplot('x', 'y', dftsne, hue='signal', fit_reg=False, size=8
                , scatter_kws={'alpha':0.7,'s':60})

输出

mlbf 07in24

图表显示了交易信号的良好聚类程度。长期和短期信号存在一些重叠,但是在减少的特征数目下,它们可以很好地区分开来。

5.4. 比较有无降维的模型

在这一步骤中,我们分析了降维对分类的影响,以及对整体精度和计算时间的影响:

# test options for classification
scoring = 'accuracy'

5.4.1. 模型

首先,我们查看了没有降维的模型所花费的时间,其中包括所有技术指标:

import time
start_time = time.time()

# spot-check the algorithms
models =  RandomForestClassifier(n_jobs=-1)
cv_results_XTrain= cross_val_score(models, X_train, Y_train, cv=kfold, \
  scoring=scoring)
print("Time Without Dimensionality Reduction--- %s seconds ---" % \
(time.time() - start_time))

输出

Time Without Dimensionality Reduction
7.781347990036011 seconds

没有降维时的总耗时约为八秒钟。让我们看看在使用截断 SVD 的五个主成分进行降维时所需的时间:

start_time = time.time()
X_SVD= dfsvd[svdcols].iloc[:, :5]
cv_results_SVD = cross_val_score(models, X_SVD, Y_train, cv=kfold, \
  scoring=scoring)
print("Time with Dimensionality Reduction--- %s seconds ---" % \
(time.time() - start_time))

输出

Time with Dimensionality Reduction
2.281977653503418 seconds

降维后的总耗时约为两秒钟,时间减少了四分之一,这是一个显著的改进。让我们来探讨在使用压缩数据集时,是否存在精度下降的情况:

print("Result without dimensionality Reduction: %f (%f)" %\
 (cv_results_XTrain.mean(), cv_results_XTrain.std()))
print("Result with dimensionality Reduction: %f (%f)" %\
 (cv_results_SVD.mean(), cv_results_SVD.std()))

输出

Result without dimensionality Reduction: 0.936375 (0.010774)
Result with dimensionality Reduction: 0.887500 (0.012698)

精度大约下降了 5%,从 93.6%降到 88.7%。速度的提升必须与精度的损失进行权衡。是否可以接受精度损失可能取决于具体问题。如果这是一个需要经常重新校准的模型,那么较低的计算时间将至关重要,特别是在处理大型、高速数据集时。计算时间的提升在交易策略开发的早期阶段尤其有益,它使我们能够在更短的时间内测试更多的特征(或技术指标)。

结论

在这个案例研究中,我们展示了在交易策略背景下,降维和主成分分析在减少维度方面的效率。通过降维,我们在模型速度提升了四倍的同时,达到了与原模型相当的精确率。在涉及庞大数据集的交易策略开发中,这种速度增强可以改善整个过程。

我们演示了 SVD 和 t-SNE 都生成了可以轻松可视化以评估交易信号数据的精简数据集。这使我们能够以不可能通过原始特征数实现的方式区分这种交易策略的多空信号。

章节总结

本章介绍的案例研究集中于理解不同降维方法的概念,发展关于主成分的直觉,并可视化精简的数据集。

总体而言,本章通过案例研究呈现的 Python、机器学习和金融领域的概念可以作为金融领域任何基于降维的问题的蓝图。

在接下来的章节中,我们探讨了另一种无监督学习——聚类的概念和案例研究。

练习

  1. 使用降维技术,从不同指数内的股票中提取不同的因子,并用它们构建交易策略。

  2. 选择第五章中的任何基于回归的案例研究,并使用降维技术来查看计算时间是否有所改进。使用因子载荷解释组件,并对其进行高级直觉的开发。

  3. 对本章介绍的案例研究 3 进行因子载荷,并理解不同组件的直觉。

  4. 获取不同货币对或不同商品价格的主要成分。确定主要主成分的驱动因素,并将其与一些直观的宏观经济变量联系起来。

¹ 特征向量和特征值 是线性代数的概念。

第八章:无监督学习:聚类

在上一章中,我们探讨了降维,这是一种无监督学习的类型。在本章中,我们将探讨聚类,一类无监督学习技术,它允许我们发现数据中隐藏的结构。

聚类和降维都是对数据进行总结的方法。降维通过使用新的、较少的特征来表示数据,同时仍捕捉到最相关的信息,从而压缩数据。类似地,聚类是一种通过对原始数据进行分类而不是创建新变量来减少数据量和发现模式的方法。聚类算法将观察结果分配给包含相似数据点的子组。聚类的目标是找到数据中的自然分组,使得同一组中的项目彼此更相似,而与不同组的项目则更不相似。聚类有助于通过几个类别或群组的视角更好地理解数据。它还允许根据学到的标准自动对新对象进行分类。

在金融领域,交易员和投资经理使用聚类来找到具有类似特征的资产、类别、行业和国家的同质化群体。聚类分析通过提供交易信号类别的洞察,增强了交易策略。这一技术已被用于将客户或投资者分成几组,以更好地理解其行为并进行额外的分析。

在本章中,我们将讨论基础聚类技术,并介绍三个关于投资组合管理和交易策略开发的案例研究。

在“案例研究 1:配对交易的聚类”中,我们使用聚类方法为交易策略选择股票对。配对交易策略涉及在两个密切相关的金融工具中匹配多头头寸和空头头寸。当金融工具的数量较多时,找到合适的配对可能是一项挑战。在这个案例研究中,我们展示了聚类在交易策略开发和类似情况下的有用性。

在“案例研究 2:投资组合管理:投资者聚类”中,我们识别出具有类似能力和愿意承担风险程度的投资者群体。我们展示了如何利用聚类技术进行有效的资产配置和投资组合再平衡。这说明了投资组合管理过程的一部分可以自动化,这对投资经理和智能投顾都非常有用。

在“案例研究 3:层次风险平价”中,我们使用基于聚类的算法将资金分配到不同的资产类别,并将结果与其他投资组合分配技术进行比较。

本章的代码库

本书代码库中的第八章 - 无监督学习 - 聚类中包含用于聚类的基于 Python 的主模板,以及本章案例研究的 Jupyter 笔记本。要解决任何涉及聚类模型(如k-均值、分层聚类等)的 Python 机器学习问题,读者只需修改模板以符合其问题陈述。与前几章类似,本章的案例研究使用标准 Python 主模板,其中包含在第二章中介绍的标准化模型开发步骤。对于聚类案例研究,步骤 6(模型调整和网格搜索)和步骤 7(最终化模型)已与步骤 5(评估算法和模型)合并。

聚类技术

有许多种类的聚类技术,它们在识别分组策略上有所不同。选择应用哪种技术取决于数据的性质和结构。在本章中,我们将涵盖以下三种聚类技术:

  • k-均值聚类

  • 分层聚类

  • 亲和传播聚类

下一节总结了这些聚类技术,包括它们的优缺点。每种聚类方法的额外细节在案例研究中提供。

k 均值聚类

k-均值是最著名的聚类技术。k-均值算法旨在找到并将数据点分组到彼此之间具有高相似性的类别中。这种相似性被理解为数据点之间距离的反义。数据点越接近,它们属于同一簇的可能性就越大。

该算法找到k个质心,并将每个数据点分配到一个簇,以最小化簇内方差(称为惯性)。通常使用欧几里得距离(两点之间的普通距离),但也可以使用其他距离度量。k-均值算法对于给定的k提供局部最优解,并按以下步骤进行:

  1. 此算法指定要生成的簇数。

  2. 数据点被随机选择为簇中心。

  3. 将每个数据点分配给最近的簇中心。

  4. 簇中心更新为分配点的均值。

  5. 步骤 3–4 重复,直到所有簇中心保持不变。

简而言之,我们在每次迭代中随机移动指定数量的质心,将每个数据点分配给最近的质心。完成后,我们计算每个质心中所有点的平均距离。一旦无法进一步减少数据点到其各自质心的最小距离,我们就找到了我们的聚类。

k 均值超参数

k-均值的超参数包括:

聚类数

要生成的聚类数和质心。

最大迭代次数

算法单次运行的最大迭代次数。

初始数

算法将以不同的质心种子运行的次数。最终结果将是连续运行定义数量的最佳输出,从惯性的角度来看。

对于k-means,为群集中心选择不同的随机起始点通常会导致非常不同的聚类解决方案。因此,在 sklearn 中运行k-means 算法至少使用 10 种不同的随机初始化,并选择出现最多次数的解决方案。

  • k * -means 的优势包括其简单性、广泛的适用性、快速收敛以及对大数据的线性可扩展性,同时生成大小均匀的聚类。当我们事先知道确切的聚类数k时,它非常有用。事实上,* k * -means 的主要缺点是需要调整这个超参数。其他缺点包括缺乏找到全局最优解的保证以及对异常值的敏感性。

Python 实现

Python 的 sklearn 库提供了* k * -means 的强大实现。以下代码片段演示了如何在数据集上应用* k * -means 聚类:

from sklearn.cluster import KMeans
#Fit with k-means
k_means = KMeans(n_clusters=nclust)
k_means.fit(X)

聚类数是需要调整的关键超参数。我们将在本章的案例研究 1 和 2 中查看* k * -means 聚类技术,其中提供了选择正确聚类数以及详细可视化的进一步细节。

层次聚类

层次聚类涉及创建从顶部到底部具有主导排序的聚类。层次聚类的主要优势在于,我们不需要指定聚类的数量;模型自行确定。此聚类技术分为两种类型:聚合层次聚类和分裂层次聚类。

聚合层次聚类是最常见的层次聚类类型,用于根据它们的相似性对对象进行分组。它是一种“自底向上”的方法,其中每个观测值从其自身的独立聚类开始,并且随着层次结构的上升,成对的聚类被合并。聚合层次聚类算法提供了一个局部最优解,并按以下方式进行:

  1. 将每个数据点作为单点聚类,并形成* N *聚类。

  2. 取两个最接近的数据点并组合它们,留下* N-1 *聚类。

  3. 取两个最接近的聚类并组合它们,形成* N-2 *聚类。

  4. 重复步骤 3,直到仅剩一个聚类。

分裂式分层聚类采用“自顶向下”的方式,依次将剩余的聚类分割,以产生最不同的子群。

两者都产生* N-1 *层次水平,并促进将数据分区为同质群的聚类创建。我们将专注于更常见的聚合聚类方法。

分层聚类使得可以绘制树状图,它是二叉分层聚类的可视化。树状图是一种显示不同数据集之间层次关系的树状图。它们提供了分层聚类结果的有趣和信息丰富的可视化。树状图包含了分层聚类算法的记忆,因此通过检查图表就可以了解聚类是如何形成的。

图 8-1 展示了基于分层聚类的树状图示例。数据点之间的距离表示不相似性,方块的高度表示聚类之间的距离。

在底部融合的观察结果是相似的,而在顶部则相当不同。通过树状图,可以基于垂直轴的位置而不是水平轴来得出结论。

分层聚类的优点在于易于实现,不需要指定聚类数量,并且生成的树状图在理解数据方面非常有用。然而,与其他算法(如k-means)相比,分层聚类的时间复杂度可能导致较长的计算时间。如果数据集很大,通过查看树状图确定正确的聚类数量可能会很困难。分层聚类对离群值非常敏感,在它们存在时,模型性能显著降低。

mlbf 0801

图 8-1. 分层聚类

Python 实现

下面的代码片段演示了如何在数据集上应用包含四个聚类的聚合分层聚类:

from sklearn.cluster import AgglomerativeClustering
model = AgglomerativeClustering(n_clusters=4, affinity='euclidean',\
  linkage='ward')
clust_labels1 = model.fit_predict(X)

关于凝聚分层聚类的超参数的更多详细信息可以在sklearn 网站找到。我们将在本章的案例研究 1 和 3 中探讨分层聚类技术。

亲和传播聚类

亲和传播通过在数据点之间发送消息直到收敛来创建聚类。与k-means 等聚类算法不同,亲和传播在运行算法之前不需要确定或估计聚类的数量。亲和传播中使用两个重要参数来确定聚类数量:偏好控制使用多少典范(或原型);阻尼因子则减弱消息的责任和可用性,以避免更新这些消息时的数值振荡。

一个数据集使用少量样本来描述。这些样本是输入集合的代表性成员。亲和传播算法接受一组数据点之间的成对相似性,并通过最大化数据点与其代表的总相似性来找到聚类。传递的消息表示一个样本成为另一个样本的代表的适合程度,这会根据来自其他对的值进行更新。这种更新是迭代的,直到收敛为止,此时选择最终的代表,并获得最终的聚类。

就优势而言,亲和传播不需要在运行算法之前确定簇的数量。该算法速度快,可以应用于大型相似性矩阵。然而,该算法经常收敛于次优解,并且有时可能无法收敛。

Python 中的实现

以下代码片段说明了如何为数据集实现亲和传播算法:

from sklearn.cluster import AffinityPropagation
# Initialize the algorithm and set the number of PC's
ap = AffinityPropagation()
ap.fit(X)

关于亲和传播聚类的超参数的更多详细信息可以在sklearn 网站上找到。我们将在本章的案例研究 1 和 2 中看到亲和传播技术。

案例研究 1:配对交易的聚类

配对交易策略构建了一个具有类似市场风险因子暴露的相关资产组合。这些资产的临时价格差异可以通过在一种工具中建立多头仓位,同时在另一种工具中建立空头仓位来创造盈利机会。配对交易策略旨在消除市场风险,并利用这些股票相对回报的临时差异。

配对交易的基本前提是均值回归是资产的预期动态。这种均值回归应该导致长期均衡关系,我们试图通过统计方法来近似这种关系。当(假定为暂时的)与这种长期趋势背离的时刻出现时,可能会产生利润。成功的配对交易的关键在于选择要使用的正确的资产对。

传统上,配对选择使用试错法。仅仅处于相同部门或行业的股票或工具被分组在一起。这个想法是,如果这些股票属于相似行业的公司,它们的股票也应该以类似的方式移动。然而,这并不一定是事实。此外,对于庞大的股票池,找到一个合适的对是一项困难的任务,因为可能有* n(n-1)/2 种可能的配对,其中n*是工具的数量。聚类在这里可能是一个有用的技术。

在这个案例研究中,我们将使用聚类算法为配对交易策略选择股票对。

使用聚类选择配对的蓝图

1. 问题定义

在本案例研究中,我们的目标是对 S&P 500 股票进行聚类分析,以制定成对交易策略。从 Yahoo Finance 使用pandas_datareader获取了 S&P 500 股票数据。数据包括 2018 年以来的价格数据。

2. 入门—加载数据和 Python 包

数据加载、数据分析、数据准备和模型评估所使用的库列表如下。

2.1. 加载 Python 包

大多数这些包和函数的详细信息已在第二章和第四章中提供。这些包的使用将在模型开发过程的不同步骤中进行演示。

用于聚类的包

from sklearn.cluster import KMeans, AgglomerativeClustering, AffinityPropagation
from scipy.cluster.hierarchy import fcluster
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
from scipy.spatial.distance import pdist
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold

用于数据处理和可视化的包

# Load libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import read_csv, set_option
from pandas.plotting import scatter_matrix
import seaborn as sns
from sklearn.preprocessing import StandardScaler
import datetime
import pandas_datareader as dr
import matplotlib.ticker as ticker
from itertools import cycle

2.2. 加载数据

下面加载股票数据。¹

dataset = read_csv('SP500Data.csv', index_col=0)

3. 探索性数据分析

我们在本节快速查看数据。

3.1. 描述统计

让我们来看看数据的形状:

# shape
dataset.shape

输出

(448, 502)

数据包含 502 列和 448 个观察值。

3.2. 数据可视化

我们将详细查看聚类后的可视化。

4. 数据准备

我们在以下几节中为建模准备数据。

4.1. 数据清理

在这一步中,我们检查行中的 NAs,并且要么删除它们,要么用列的均值填充它们:

#Checking for any null values and removing the null values'''
print('Null Values =',dataset.isnull().values.any())

输出

Null Values = True

让我们去除超过 30%缺失值的列:

missing_fractions = dataset.isnull().mean().sort_values(ascending=False)
missing_fractions.head(10)
drop_list = sorted(list(missing_fractions[missing_fractions > 0.3].index))
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

输出

(448, 498)

鉴于存在空值,我们删除了一些行:

# Fill the missing values with the last value available in the dataset.
dataset=dataset.fillna(method='ffill')

数据清洗步骤识别出具有缺失值的数据,并对其进行了填充。此步骤对于创建一个有意义、可靠且清洁的数据集至关重要,该数据集可以在聚类中无误地使用。

4.2. 数据转换

为了进行聚类分析,我们将使用年度回报方差作为变量,因为它们是股票表现和波动性的主要指标。以下代码准备这些变量:

#Calculate average annual percentage return and volatilities
returns = pd.DataFrame(dataset.pct_change().mean() * 252)
returns.columns = ['Returns']
returns['Volatility'] = dataset.pct_change().std() * np.sqrt(252)
data = returns

在应用聚类之前,所有变量应处于相同的尺度上;否则,具有较大值的特征将主导结果。我们使用 sklearn 中的StandardScaler将数据集特征标准化为单位尺度(均值=0,方差=1):

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(data)
rescaledDataset = pd.DataFrame(scaler.fit_transform(data),\
  columns = data.columns, index = data.index)
# summarize transformed data
rescaledDataset.head(2)

输出

返回 波动性
ABT 0.794067 –0.702741 ABBV

准备好数据后,我们现在可以探索聚类算法。

5. 评估算法和模型

我们将查看以下模型:

  • k-均值

  • 分层聚类(凝聚聚类)

  • 亲和传播

5.1. k-均值聚类

在这里,我们使用k-均值建模,并评估两种方法来找到最优聚类数。

5.1.1. 寻找最优聚类数

我们知道k-均值最初将数据点随机分配到集群中,然后计算质心或均值。此外,它计算每个集群内的距离,对这些距离进行平方,并将它们求和以得到平方误差和。

基本思想是定义k个聚类,以使总的聚类内变异(或误差)最小化。以下两种方法有助于找到k-means 中的聚类数:

肘方法

基于聚类内的平方误差(SSE)

轮廓方法

基于轮廓分数

首先,让我们来看看肘方法。每个点的 SSE 是该点与其表示(即其预测聚类中心)之间距离的平方。对于一系列聚类数的值,绘制平方误差和。第一个聚类将添加大量信息(解释大量方差),但最终边际增益会下降,在图表中形成一个角度。在这一点选择聚类数;因此被称为“肘准则”。

让我们使用 sklearn 库在 Python 中实现这一点,并绘制一系列值对于k的 SSE:

distortions = []
max_loop=20
for k in range(2, max_loop):
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(X)
    distortions.append(kmeans.inertia_)
fig = plt.figure(figsize=(15, 5))
plt.plot(range(2, max_loop), distortions)
plt.xticks([i for i in range(2, max_loop)], rotation=75)
plt.grid(True)

Output

mlbf 08in01

检查聚类内平方误差图表,数据显示肘部拐点大约在五或六个聚类处。当聚类数超过六个时,我们可以看到聚类内的 SSE 开始趋于平稳。

现在让我们看看轮廓方法。轮廓分数衡量一个点与其所属聚类的相似程度(内聚性)与其他聚类的相似程度(分离性)。轮廓值的范围在 1 到-1 之间。高值是理想的,表示该点正确地放置在其聚类中。如果许多点具有负轮廓值,则可能表明我们创建了过多或过少的聚类。

让我们使用 sklearn 库在 Python 中实现这一点,并绘制一系列值对于k的轮廓分数:

from sklearn import metrics

silhouette_score = []
for k in range(2, max_loop):
        kmeans = KMeans(n_clusters=k,  random_state=10, n_init=10, n_jobs=-1)
        kmeans.fit(X)
        silhouette_score.append(metrics.silhouette_score(X, kmeans.labels_, \
          random_state=10))
fig = plt.figure(figsize=(15, 5))
plt.plot(range(2, max_loop), silhouette_score)
plt.xticks([i for i in range(2, max_loop)], rotation=75)
plt.grid(True)

Output

mlbf 08in02

查看轮廓分数图表,我们可以看到图表中的各个部分都能看到一个拐点。由于在六个聚类之后 SSE 没有太大的差异,这意味着在这个k-means 模型中六个聚类是首选选择。

结合两种方法的信息,我们推断出最优的聚类数为六。

5.1.2. 聚类和可视化

让我们建立六个聚类的k-means 模型并可视化结果:

nclust=6
#Fit with k-means
k_means = cluster.KMeans(n_clusters=nclust)
k_means.fit(X)
#Extracting labels
target_labels = k_means.predict(X)

当数据集中的变量数量非常大时,要想可视化聚类形成是一项不易的任务。基本散点图是在二维空间中可视化聚类的一种方法。我们在下面创建一个来识别数据中固有的关系:

centroids = k_means.cluster_centers_
fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=k_means.labels_, \
  cmap="rainbow", label = X.index)
ax.set_title('k-means results')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

plt.plot(centroids[:,0],centroids[:,1],'sg',markersize=11)

Output

mlbf 08in03

在前面的图中,我们可以看到不同颜色分开的明显聚类(全彩版可在GitHub上找到)。图中的数据分组似乎分离得很好。聚类中心也有一定程度的分离,用方形点表示。

让我们看看每个聚类中的股票数量:

# show number of stocks in each cluster
clustered_series = pd.Series(index=X.index, data=k_means.labels_.flatten())
# clustered stock with its cluster label
clustered_series_all = pd.Series(index=X.index, data=k_means.labels_.flatten())
clustered_series = clustered_series[clustered_series != -1]

plt.figure(figsize=(12,7))
plt.barh(
    range(len(clustered_series.value_counts())), # cluster labels, y axis
    clustered_series.value_counts()
)
plt.title('Cluster Member Counts')
plt.xlabel('Stocks in Cluster')
plt.ylabel('Cluster Number')
plt.show()

Output

mlbf 08in04

每个聚类中的股票数量大约在 40 到 120 之间。虽然分布不均匀,但每个聚类中都有相当数量的股票。

让我们来看看层次聚类。

5.2. 层次聚类(凝聚聚类)

在第一步中,我们查看层次图,并检查聚类的数量。

5.2.1. 构建层次图/树状图

层次类具有一个树状图方法,该方法接受同一类的linkage 方法返回的值。linkage 方法接受数据集和最小化距离的方法作为参数。我们使用ward作为方法,因为它最小化了集群之间距离的方差:

from scipy.cluster.hierarchy import dendrogram, linkage, ward

#Calculate linkage
Z= linkage(X, method='ward')
Z[0]

Output

array([3.30000000e+01, 3.14000000e+02, 3.62580431e-03, 2.00000000e+00])

最佳可视化凝聚聚类算法的方式是通过树状图,它显示了一个聚类树,叶子是单独的股票,根是最终的单一聚类。每个聚类之间的距离显示在 y 轴上。分支越长,两个聚类之间的相关性越低:

#Plot Dendrogram
plt.figure(figsize=(10, 7))
plt.title("Stocks Dendrograms")
dendrogram(Z,labels = X.index)
plt.show()

Output

mlbf 08in05

该图表可以用来直观地检查选择的距离阈值会创建多少个聚类(尽管横轴上股票的名称不太清晰,我们可以看到它们被分成了几个聚类)。一条假设的水平直线穿过的垂直线的数量是在该距离阈值下创建的聚类数。例如,在值为 20 时,水平线将穿过树状图的两个垂直分支,暗示该距离阈值下有两个聚类。该分支的所有数据点(叶子)将被标记为该水平线穿过的聚类。

在 13 的阈值处切割选择会产生四个聚类,如下 Python 代码所确认:

distance_threshold = 13
clusters = fcluster(Z, distance_threshold, criterion='distance')
chosen_clusters = pd.DataFrame(data=clusters, columns=['cluster'])
chosen_clusters['cluster'].unique()

Output

array([1, 4, 3, 2], dtype=int64)

5.2.2. 聚类和可视化

让我们建立具有四个聚类的层次聚类模型并可视化结果:

nclust = 4
hc = AgglomerativeClustering(n_clusters=nclust, affinity='euclidean', \
linkage='ward')
clust_labels1 = hc.fit_predict(X)
fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=clust_labels1, cmap="rainbow")
ax.set_title('Hierarchical Clustering')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

类似于k-均值聚类的图表,我们看到有一些不同颜色分离的明显聚类(完整版本可在GitHub上找到)。

Output

mlbf 08in06

现在让我们来看看亲和传播聚类。

5.3. 亲和传播

让我们建立亲和传播模型并可视化结果:

ap = AffinityPropagation()
ap.fit(X)
clust_labels2 = ap.predict(X)

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=clust_labels2, cmap="rainbow")
ax.set_title('Affinity')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

Output

mlbf 08in07

选择了的亲和传播模型与k-均值和层次聚类相比产生了更多的聚类。虽然有一些明显的分组,但由于聚类数量较多,也存在更多的重叠(完整版本可在GitHub上找到)。在下一步中,我们将评估聚类技术。

5.4. 聚类评估

如果不知道真实标签,则必须使用模型本身进行评估。轮廓系数(sklearn.metrics.silhouette_score)就是一个可以使用的例子。较高的轮廓系数分数意味着具有更好定义的群集的模型。轮廓系数计算针对上述每种定义的聚类方法:

from sklearn import metrics
print("km", metrics.silhouette_score(X, k_means.labels_, metric='euclidean'))
print("hc", metrics.silhouette_score(X, hc.fit_predict(X), metric='euclidean'))
print("ap", metrics.silhouette_score(X, ap.labels_, metric='euclidean'))

输出

km 0.3350720873411941
hc 0.3432149515640865
ap 0.3450647315156527

鉴于亲和传播效果最佳,我们继续使用亲和传播,并按照此聚类方法指定的 27 个群集。

在群集内部可视化回报

我们已经确定了聚类技术和群集数量,但需要检查聚类是否导致合理的输出。为了做到这一点,我们可视化几个群集中股票的历史行为:

# all stock with its cluster label (including -1)
clustered_series = pd.Series(index=X.index, data=ap.fit_predict(X).flatten())
# clustered stock with its cluster label
clustered_series_all = pd.Series(index=X.index, data=ap.fit_predict(X).flatten())
clustered_series = clustered_series[clustered_series != -1]
# get the number of stocks in each cluster
counts = clustered_series_ap.value_counts()
# let's visualize some clusters
cluster_vis_list = list(counts[(counts<25) & (counts>1)].index)[::-1]
cluster_vis_list
# plot a handful of the smallest clusters
plt.figure(figsize=(12, 7))
cluster_vis_list[0:min(len(cluster_vis_list), 4)]

for clust in cluster_vis_list[0:min(len(cluster_vis_list), 4)]:
    tickers = list(clustered_series[clustered_series==clust].index)
    # calculate the return (lognormal) of the stocks
    means = np.log(dataset.loc[:"2018-02-01", tickers].mean())
    data = np.log(dataset.loc[:"2018-02-01", tickers]).sub(means)
    data.plot(title='Stock Time Series for Cluster %d' % clust)
plt.show()

输出

mlbf 08in08mlbf 08in09

查看上述图表,跨所有具有少量股票的群集,我们看到不同群集下的股票出现相似的运动,这证实了聚类技术的有效性。

6. 配对选择

创建群集后,可以在群集内的股票上应用几种基于协整性的统计技术来创建配对。如果两个或更多时间序列是协整的,那么它们是非平稳的并且倾向于共同移动。² 通过几种统计技术,包括增广迪基-富勒检验Johansen 检验,可以验证时间序列之间的协整性。

在这一步中,我们扫描一个群集内的证券列表,并测试配对之间的协整性。首先,我们编写一个返回协整测试分数矩阵、p 值矩阵以及 p 值小于 0.05 的任何配对的函数。

协整性和配对选择功能

def find_cointegrated_pairs(data, significance=0.05):
    # This function is from https://www.quantopian.com
    n = data.shape[1]
    score_matrix = np.zeros((n, n))
    pvalue_matrix = np.ones((n, n))
    keys = data.keys()
    pairs = []
    for i in range(1):
        for j in range(i+1, n):
            S1 = data[keys[i]]
            S2 = data[keys[j]]
            result = coint(S1, S2)
            score = result[0]
            pvalue = result[1]
            score_matrix[i, j] = score
            pvalue_matrix[i, j] = pvalue
            if pvalue < significance:
                pairs.append((keys[i], keys[j]))
    return score_matrix, pvalue_matrix, pairs

接下来,我们使用上述创建的函数检查几个群集内不同配对的协整性,并返回找到的配对。

from statsmodels.tsa.stattools import coint
cluster_dict = {}
for i, which_clust in enumerate(ticker_count_reduced.index):
    tickers = clustered_series[clustered_series == which_clust].index
    score_matrix, pvalue_matrix, pairs = find_cointegrated_pairs(
        dataset[tickers]
    )
    cluster_dict[which_clust] = {}
    cluster_dict[which_clust]['score_matrix'] = score_matrix
    cluster_dict[which_clust]['pvalue_matrix'] = pvalue_matrix
    cluster_dict[which_clust]['pairs'] = pairs

pairs = []
for clust in cluster_dict.keys():
    pairs.extend(cluster_dict[clust]['pairs'])

print ("Number of pairs found : %d" % len(pairs))
print ("In those pairs, there are %d unique tickers." % len(np.unique(pairs)))

输出

Number of pairs found : 32
In those pairs, there are 47 unique tickers.

现在让我们可视化配对选择过程的结果。有关使用 t-SNE 技术进行配对可视化的步骤的详细信息,请参考本案例研究的 Jupyter 笔记本。

以下图表显示了k-means 在寻找非传统配对方面的强度(在可视化中用箭头指出)。DXC 是 DXC Technology 的股票代码,XEC 是 Cimarex Energy 的股票代码。这两只股票来自不同的行业,在表面上看似乎没有共同点,但使用k-means 聚类和协整测试识别为配对。这意味着它们的股票价格走势之间存在长期稳定的关系。

mlbf 08in10

一旦形成股票对,它们可以用于成对交易策略。当这对股票的股价偏离确定的长期关系时,投资者将寻求在表现不佳的证券上建立多头头寸,并空头卖出表现良好的证券。如果证券的价格重新回到其历史关系,投资者将从价格的收敛中获利。

结论

在这个案例研究中,我们展示了聚类技术的效率,通过找到可以用于成对交易策略的股票小池。超越这个案例研究的下一步将是探索和回测来自股票组合中的股票对的各种多空交易策略。

聚类可以用于将股票和其他类型的资产分成具有相似特征的组,以支持多种类型的交易策略。它在投资组合构建中也非常有效,有助于确保我们选择的资产池具有足够的分散化。

案例研究 2:投资组合管理:聚类投资者

资产管理和投资配置是一个繁琐且耗时的过程,在这个过程中,投资经理通常必须为每个客户或投资者设计定制化的方法。

如果我们能将这些客户组织成特定的投资者档案或集群,其中每个群体都代表具有类似特征的投资者,那该有多好?

根据类似特征对投资者进行聚类可以简化和标准化投资管理流程。这些算法可以根据年龄、收入和风险承受能力等不同因素将投资者分组。它可以帮助投资经理识别其投资者群体中的不同群体。此外,通过使用这些技术,经理们可以避免引入可能会对决策产生不利影响的任何偏见。通过聚类分析的因素可以对资产配置和再平衡产生重大影响,使其成为更快速和有效的投资管理工具。

在这个案例研究中,我们将使用聚类方法来识别不同类型的投资者。

本案例研究使用的数据来自美联储委员会进行的消费者金融调查,该数据集还在“案例研究 3:投资者风险承受能力和智能顾问”中使用,该案例研究位于第五章中。

使用聚类将投资者分组的蓝图

1. 问题定义

本案例研究的目标是构建一个聚类模型,根据与承担风险能力和意愿相关的参数来对个人或投资者进行分组。我们将专注于使用常见的人口统计和财务特征来实现这一目标。

我们使用的调查数据包括 2007 年(危机前)和 2009 年(危机后)超过 10,000 名个体的回答。数据包含 500 多个特征。由于数据变量众多,我们首先减少变量数量,选择直接与投资者承担风险能力相关的最直观特征。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究加载的包类似于第五章案例研究中加载的包。然而,与聚类技术相关的一些附加包显示在下面的代码片段中:

#Import packages for clustering techniques
from sklearn.cluster import KMeans, AgglomerativeClustering,AffinityPropagation
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold

2.2. 加载数据

数据(同样在第五章中使用过)经进一步处理,得到以下表示个体承担风险能力和意愿的属性。这些预处理数据是 2007 年调查的结果,并且已经加载如下:

# load dataset
dataset = pd.read_excel('ProcessedData.xlsx')

3. 探索性数据分析

接下来,我们仔细查看数据中不同的列和特征。

3.1. 描述性统计

首先,看数据的形状:

dataset.shape

Output

(3866, 13)

数据包含 3,886 个个体的信息,分布在 13 列中:

# peek at data
set_option('display.width', 100)
dataset.head(5)

mlbf 08in11

正如我们在上表中看到的,每个个体有 12 个属性。这些属性可以归类为人口统计、财务和行为属性。它们在图 8-2 中总结。

mlbf 0802

图 8-2. 用于对个体进行聚类的属性

这些大多数曾在第五章案例研究中使用并定义。在本案例研究中使用并定义了一些额外属性(LIFECYCL、HHOUSES 和 SPENDMOR):

LIFECYCL

这是一个生命周期变量,用于近似一个人承担风险的能力。有六个类别,逐渐增加承担风险的能力。数值 1 代表“年龄小于 55 岁,未婚,无子女”,数值 6 代表“年龄超过 55 岁且不再工作”。

HHOUSES

这是一个指示个体是否拥有房屋的标志。数值 1(0)表示个体拥有(不拥有)房屋。

SPENDMOR

如果资产增值的话,这表示更高的消费偏好,取值范围为 1 到 5。

3.2. 数据可视化

我们将详细查看聚类后的可视化。

4. 数据准备

在这里,我们对数据进行必要的变更,为建模做准备。

4.1. 数据清理

在这一步中,我们检查行中是否存在 NA 值,然后删除或用列的平均值填充。

print('Null Values =', dataset.isnull().values.any())

Output

Null Values = False

鉴于没有任何缺失数据,并且数据已经是分类格式,无需进一步的数据清理。ID列是不必要的,已经被删除:

X=X.drop(['ID'], axis=1)

4.2. 数据转换

正如我们在第 3.1 节中看到的,所有列都代表具有相似数值范围的分类数据,没有异常值。因此,在进行聚类时不需要数据转换。

5. 评估算法和模型

我们将分析 k-均值和亲和传播的性能。

5.1. k-均值聚类

我们在这一步看一下 k-均值聚类的细节。首先,我们找到最佳的聚类数,然后创建一个模型。

5.1.1. 寻找最佳聚类数

我们看以下两个指标来评估 k-均值模型中的聚类数。获取这两个指标的 Python 代码与案例研究 1 中的代码相同:

  1. 簇内平方和误差(SSE)

  2. 轮廓分数

簇内平方和误差(SSE)

mlbf 08in12

轮廓分数

mlbf 08in13

通过观察前面两张图表,最佳聚类数似乎在 7 左右。我们可以看到,当聚类数超过 6 时,簇内 SSE 开始趋于平稳。从第二张图中可以看出,图表的各个部分都有一个转折点。由于超过 7 个聚类后 SSE 的差异不大,我们决定在下面的 k-均值模型中使用 7 个聚类。

5.1.2. 聚类和可视化

让我们创建一个包含 7 个聚类的 k-均值模型:

nclust=7

#Fit with k-means
k_means = cluster.KMeans(n_clusters=nclust)
k_means.fit(X)

让我们为数据集中的每个个体分配一个目标聚类。此分配进一步用于探索性数据分析,以了解每个聚类的行为:

#Extracting labels
target_labels = k_means.predict(X)

5.2. 亲和传播

在这里,我们建立了一个亲和传播模型,并观察了聚类的数量:

ap = AffinityPropagation()
ap.fit(X)
clust_labels2 = ap.predict(X)

cluster_centers_indices = ap.cluster_centers_indices_
labels = ap.labels_
n_clusters_ = len(cluster_centers_indices)
print('Estimated number of clusters: %d' % n_clusters_)

输出

Estimated number of clusters: 161

亲和传播结果超过 150 个聚类。这么多聚类可能会导致很难区分它们之间的差异。

5.3. 聚类评估

在这一步中,我们使用轮廓系数(sklearn.metrics.silhouette_score)检查聚类的性能。请记住,较高的轮廓系数分数与定义更好的聚类模型相关:

from sklearn import metrics
print("km", metrics.silhouette_score(X, k_means.labels_))
print("ap", metrics.silhouette_score(X, ap.labels_))

输出

km 0.170585217843582
ap 0.09736878398868973

k-均值模型的轮廓系数比亲和传播高得多。此外,亲和传播产生的大量聚类是不可持续的。在手头问题的背景下,拥有更少的聚类或投资者类型分类有助于在投资管理流程中建立简单性和标准化。这为信息的使用者(例如财务顾问)提供了一些管理投资者类型的直觉。理解和能够描述六到八种投资者类型要比理解和维护超过 100 种不同配置的意义更为实际。考虑到这一点,我们决定将 k-均值作为首选的聚类技术。

6. 聚类直觉

接下来,我们将分析这些簇,并试图从中得出结论。我们通过绘制每个簇的变量平均值并总结结果来进行分析:

cluster_output= pd.concat([pd.DataFrame(X),  pd.DataFrame(k_means.labels_, \
  columns = ['cluster'])],axis=1)
output=cluster_output.groupby('cluster').mean()

人口统计特征:每个簇的绘图

output[['AGE','EDUC','MARRIED','KIDS','LIFECL','OCCAT']].\
plot.bar(rot=0, figsize=(18,5));

输出

mlbf 08in14

这里的图显示了每个簇的属性平均值(完整版本请参见GitHub)。例如,在比较簇 0 和 1 时,簇 0 的平均年龄较低,但平均受教育程度较高。然而,这两个簇在婚姻状况和子女数量上更为相似。因此,基于人口统计属性,簇 0 中的个体平均而言比簇 1 中的个体更具有较高的风险承受能力。

财务和行为属性:每个簇的绘图

output[['HHOUSES','NWCAT','INCCL','WSAVED','SPENDMOR','RISK']].\
plot.bar(rot=0, figsize=(18,5));

输出

mlbf 08in15

这里的图显示了每个簇的财务和行为属性的平均值(完整版本请参见GitHub)。再次比较簇 0 和 1,前者具有更高的平均房屋所有权,更高的平均净资产和收入,以及较低的风险承受意愿。在储蓄与收入比较和愿意储蓄方面,这两个簇是可比较的。因此,我们可以推断,与簇 1 中的个体相比,簇 0 中的个体平均而言具有更高的能力,但更低的风险承受意愿。

结合这两个簇的人口统计、财务和行为属性信息,簇 0 中个体的整体风险承受能力高于簇 1 中的个体。在所有其他簇中执行类似的分析后,我们在下表中总结结果。风险承受能力列代表了每个簇风险承受能力的主观评估。

特征 风险能力
簇 0 年龄低,净资产和收入高,生活风险类别较低,愿意更多消费
簇 1 年龄高,净资产和收入低,生活风险类别高,风险承受意愿高,教育水平低
簇 2 年龄高,净资产和收入高,生活风险类别高,风险承受意愿高,拥有住房 中等
簇 3 年龄低,收入和净资产非常低,风险承受意愿高,有多个孩子
簇 4 年龄中等,收入和净资产非常高,风险承受意愿高,有多个孩子,拥有住房
簇 5 年龄低,收入和净资产非常低,风险承受意愿高,无子女 中等
簇 6 年龄低,收入和净资产中等,风险承受意愿高,有多个孩子,拥有住房

结论

这个案例研究的一个关键要点是理解集群直觉的方法。我们使用可视化技术通过定性解释每个集群中变量的平均值来理解集群成员的预期行为。我们展示了通过风险承受能力将不同投资者的自然群体发现在一起的聚类的效率。

给定聚类算法能够成功根据不同因素(如年龄、收入和风险承受能力)对投资者进行分组,它们可以进一步被投资组合经理用于跨集群标准化投资组合分配和再平衡策略,从而使投资管理过程更快速、更有效。

案例研究 3:层次风险平价

马科维茨的均值-方差组合优化是投资组合构建和资产配置中最常用的技术。在这种技术中,我们需要估计用作输入的资产的协方差矩阵和预期收益。正如在“案例研究 1:投资组合管理:找到一种特征投资组合” 中所讨论的,金融回报的不稳定性导致了预期收益和协方差矩阵的估计误差,特别是当资产数量远大于样本量时。这些错误极大地危及了最终投资组合的最优性,导致错误和不稳定的结果。此外,假定的资产收益、波动率或协方差的微小变化可能对优化过程的输出产生很大影响。从这个意义上讲,马科维茨的均值-方差优化是一个病态(或病态)的逆问题。

“构建在样本外表现优异的多样化投资组合” 中,马科斯·洛佩斯·德·普拉多(2016)提出了一种基于聚类的投资组合分配方法,称为层次风险平价。层次风险平价的主要思想是在股票回报的协方差矩阵上运行层次聚类,然后通过将资金平均分配给每个集群层次来找到分散的权重(这样许多相关策略将获得与单个不相关策略相同的总分配)。这减轻了马科维茨的均值-方差优化中发现的一些问题(上面突出显示)并提高了数值稳定性。

在这个案例研究中,我们将基于聚类方法实施层次风险平价,并将其与马科维茨的均值-方差优化方法进行比较。

用于本案例研究的数据集是从 2018 年开始的标准普尔 500 指数股票的价格数据。该数据集可以从 Yahoo Finance 下载。这是与案例研究 1 中使用的相同数据集。

使用聚类实施层次风险平价的蓝图

1. 问题定义

本案例研究的目标是使用基于聚类的算法对股票数据集进行资本分配到不同资产类别。为了对投资组合分配进行回测和与传统的 Markowitz 均值-方差优化进行比较,我们将进行可视化,并使用性能指标,如夏普比率。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究加载的包与上一案例研究中加载的包类似。然而,下面的代码片段显示了一些与聚类技术相关的额外包:

#Import Model Packages
import scipy.cluster.hierarchy as sch
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import fcluster
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold
import ffn

#Package for optimization of mean variance optimization
import cvxopt as opt
from cvxopt import blas, solvers

由于本案例研究使用与案例研究 1 相同的数据,因此已跳过某些接下来的步骤(即加载数据),以避免重复。作为提醒,数据包含约 500 只股票和 448 个观察值。

3. 探索性数据分析

我们稍后将详细查看聚类后的可视化。

4. 数据准备

4.1. 数据清洗

参考案例研究 1 进行数据清洗步骤。

4.2. 数据转换

我们将使用年收益率进行聚类。此外,我们将训练数据和测试数据。在这里,我们通过将数据集的 20%分开以进行测试,并生成收益率序列来为训练和测试准备数据集:

X= dataset.copy('deep')
row= len(X)
train_len = int(row*.8)

X_train = X.head(train_len)
X_test = X.tail(row-train_len)

#Calculate percentage return
returns = X_train.to_returns().dropna()
returns_test=X_test.to_returns().dropna()

5. 评估算法和模型

在这一步中,我们将研究层次聚类并进行进一步的分析和可视化。

5.1. 构建层次图/树状图

第一步是使用凝聚层次聚类技术寻找相关性的群集。层次类具有树状图方法,该方法采用同一类的链接方法返回的值作为参数。链接方法采用数据集和最小化距离的方法作为参数。有不同的选项用于测量距离。我们将选择的选项是 ward,因为它最小化了群集之间的距离的方差。其他可能的距离度量包括单一和质心。

链接在一行代码中执行实际的聚类并以以下格式返回群集的列表:

Z= [stock_1, stock_2, distance, sample_count]

作为前提,我们定义一个函数将相关性转换为距离:

def correlDist(corr):
    # A distance matrix based on correlation, where 0<=d[i,j]<=1
    # This is a proper distance metric
    dist = ((1 - corr) / 2.) ** .5  # distance matrix
    return dist

现在我们将股票收益的相关性转换为距离,然后计算以下步骤中的链接。链接计算后通过树状图的可视化来展示群集。再次,叶子是单个股票,根是最终的单一群集。在 y 轴上显示每个群集之间的距离;分支越长,两个群集之间的相关性越低。

#Calculate linkage
dist = correlDist(returns.corr())
link = linkage(dist, 'ward')

#Plot Dendrogram
plt.figure(figsize=(20, 7))
plt.title("Dendrograms")
dendrogram(link,labels = X.columns)
plt.show()

在下面的图表中,横轴表示簇。尽管横轴上股票的名称不太清晰(考虑到有 500 只股票,这并不奇怪),但我们可以看到它们被分成了几个簇。合适的簇数似乎是 2、3 或 6,具体取决于所需的距离阈值级别。接下来,我们将利用从这一步骤计算出的链接来计算基于层次风险平价的资产配置。

输出

mlbf 08in16

5.2. 层次风险平价的步骤

层次风险平价(HRP)算法按照 Prado 的论文概述的三个阶段运行:

树形聚类

根据它们的相关矩阵将相似的投资分组成簇。具有层次结构有助于我们在反转协方差矩阵时改善二次优化器的稳定性问题。

拟对角化

重新组织协方差矩阵,以便将相似的投资放在一起。该矩阵对角化使我们能够根据反方差分配优化地分配权重。

递归二分法

通过基于簇协方差的递归二分法分配配置。

在前一节中进行了第一阶段,我们基于距离度量确定了簇,现在我们进行拟对角化。

5.2.1. 拟对角化

拟对角化是一个被称为矩阵序列化的过程,它重新组织协方差矩阵的行和列,使得最大值位于对角线上。如下所示,该过程重新组织协方差矩阵,使得相似的投资被放在一起。该矩阵对角化允许我们根据反方差分配优化地分配权重:

def getQuasiDiag(link):
    # Sort clustered items by distance
    link = link.astype(int)
    sortIx = pd.Series([link[-1, 0], link[-1, 1]])
    numItems = link[-1, 3]  # number of original items
    while sortIx.max() >= numItems:
        sortIx.index = range(0, sortIx.shape[0] * 2, 2)  # make space
        df0 = sortIx[sortIx >= numItems]  # find clusters
        i = df0.index
        j = df0.values - numItems
        sortIx[i] = link[j, 0]  # item 1
        df0 = pd.Series(link[j, 1], index=i + 1)
        sortIx = sortIx.append(df0)  # item 2
        sortIx = sortIx.sort_index()  # re-sort
        sortIx.index = range(sortIx.shape[0])  # re-index
    return sortIx.tolist()

5.2.2. 递归二分法

在下一步中,我们执行递归二分法,这是一种基于聚合方差的反比例拆分投资组合权重的自上而下方法。函数 getClusterVar 计算簇方差,在这个过程中,它需要来自函数 getIVP 的反方差组合。函数 getClusterVar 的输出由函数 getRecBipart 使用,根据簇协方差计算最终的分配:

def getIVP(cov, **kargs):
# Compute the inverse-variance portfolio
ivp = 1. / np.diag(cov)
ivp /= ivp.sum()
return ivp

def getClusterVar(cov,cItems):
    # Compute variance per cluster
    cov_=cov.loc[cItems,cItems] # matrix slice
    w_=getIVP(cov_).reshape(-1, 1)
    cVar=np.dot(np.dot(w_.T,cov_),w_)[0, 0]
    return cVar

def getRecBipart(cov, sortIx):
    # Compute HRP alloc
    w = pd.Series(1, index=sortIx)
    cItems = [sortIx]  # initialize all items in one cluster
    while len(cItems) > 0:
        cItems = [i[j:k] for i in cItems for j, k in ((0,\
           len(i) // 2), (len(i) // 2, len(i))) if len(i) > 1]  # bi-section
        for i in range(0, len(cItems), 2):  # parse in pairs
            cItems0 = cItems[i]  # cluster 1
            cItems1 = cItems[i + 1]  # cluster 2
            cVar0 = getClusterVar(cov, cItems0)
            cVar1 = getClusterVar(cov, cItems1)
            alpha = 1 - cVar0 / (cVar0 + cVar1)
            w[cItems0] *= alpha  # weight 1
            w[cItems1] *= 1 - alpha  # weight 2
    return w

下面的函数 getHRP 结合了三个阶段——聚类、拟对角化和递归二分法——以生成最终的权重:

def getHRP(cov, corr):
    # Construct a hierarchical portfolio
    dist = correlDist(corr)
    link = sch.linkage(dist, 'single')
    #plt.figure(figsize=(20, 10))
    #dn = sch.dendrogram(link, labels=cov.index.values)
    #plt.show()
    sortIx = getQuasiDiag(link)
    sortIx = corr.index[sortIx].tolist()
    hrp = getRecBipart(cov, sortIx)
    return hrp.sort_index()

5.3. 与其他资产配置方法的比较

本案例研究的一个主要焦点是开发一种利用聚类代替马科维茨均值方差组合优化的方法。在这一步骤中,我们定义一个函数来计算基于马科维茨均值方差技术的投资组合配置。该函数 (getMVP) 接受资产的协方差矩阵作为输入,执行均值方差优化,并产生投资组合配置:

def getMVP(cov):
    cov = cov.T.values
    n = len(cov)
    N = 100
    mus = [10 ** (5.0 * t / N - 1.0) for t in range(N)]

    # Convert to cvxopt matrices
    S = opt.matrix(cov)
    #pbar = opt.matrix(np.mean(returns, axis=1))
    pbar = opt.matrix(np.ones(cov.shape[0]))

    # Create constraint matrices
    G = -opt.matrix(np.eye(n))  # negative n x n identity matrix
    h = opt.matrix(0.0, (n, 1))
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    # Calculate efficient frontier weights using quadratic programming
    solvers.options['show_progress'] = False
    portfolios = [solvers.qp(mu * S, -pbar, G, h, A, b)['x']
                  for mu in mus]
    ## Calculate risk and return of the frontier
    returns = [blas.dot(pbar, x) for x in portfolios]
    risks = [np.sqrt(blas.dot(x, S * x)) for x in portfolios]
    ## Calculate the 2nd degree polynomial of the frontier curve.
    m1 = np.polyfit(returns, risks, 2)
    x1 = np.sqrt(m1[2] / m1[0])
    # CALCULATE THE OPTIMAL PORTFOLIO
    wt = solvers.qp(opt.matrix(x1 * S), -pbar, G, h, A, b)['x']

    return list(wt)

5.4. 获取所有类型资产配置的投资组合权重

在这一步骤中,我们使用上述函数计算资产配置,使用两种资产配置方法。然后我们可视化资产配置结果:

def get_all_portfolios(returns):

    cov, corr = returns.cov(), returns.corr()
    hrp = getHRP(cov, corr)
    mvp = getMVP(cov)
    mvp = pd.Series(mvp, index=cov.index)
    portfolios = pd.DataFrame([mvp, hrp], index=['MVP', 'HRP']).T
    return portfolios

#Now getting the portfolios and plotting the pie chart
portfolios = get_all_portfolios(returns)

portfolios.plot.pie(subplots=True, figsize=(20, 10),legend = False);
fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(30,20))
ax1.pie(portfolios.iloc[:, 0], );
ax1.set_title('MVP',fontsize=30)
ax2.pie(portfolios.iloc[:, 1]);
ax2.set_title('HRP',fontsize=30)

下面的饼图显示了 MVP 与 HRP 的资产配置情况。我们清楚地看到 HRP 中有更多的多样化。现在让我们看看回测结果。

输出

mlbf 08in17

6. 回测

现在,我们将对算法生成的投资组合性能进行回测,分析样本内和样本外结果:

Insample_Result=pd.DataFrame(np.dot(returns,np.array(portfolios)), \
'MVP','HRP'], index = returns.index)
OutOfSample_Result=pd.DataFrame(np.dot(returns_test,np.array(portfolios)), \
columns=['MVP', 'HRP'], index = returns_test.index)

Insample_Result.cumsum().plot(figsize=(10, 5), title ="In-Sample Results",\
                              style=['--','-'])
OutOfSample_Result.cumsum().plot(figsize=(10, 5), title ="Out Of Sample Results",\
                                 style=['--','-'])

输出

mlbf 08in18mlbf 08in19

通过查看图表,我们可以看出 MVP 在样本内测试中有相当长一段时间表现不佳。在样本外测试中,MVP 在 2019 年 8 月至 2019 年 9 月中旬的短暂时期内表现优于 HRP。接下来,我们将分析两种配置方法的夏普比率:

样本内结果

#In_sample Results
stddev = Insample_Result.std() * np.sqrt(252)
sharp_ratio = (Insample_Result.mean()*np.sqrt(252))/(Insample_Result).std()
Results = pd.DataFrame(dict(stdev=stddev, sharp_ratio = sharp_ratio))
Results

输出

stdev sharp_ratio
MVP 0.086 0.785
HRP 0.127 0.524

样本外结果

#OutOf_sample Results
stddev_oos = OutOfSample_Result.std() * np.sqrt(252)
sharp_ratio_oos = (OutOfSample_Result.mean()*np.sqrt(252))/(OutOfSample_Result).\
std()
Results_oos = pd.DataFrame(dict(stdev_oos=stddev_oos, sharp_ratio_oos = \
  sharp_ratio_oos))
Results_oos

输出

stdev_oos sharp_ratio_oos
MVP 0.103 0.787
HRP 0.126 0.836

尽管 MVP 的样本内结果看起来很有希望,但是使用分层聚类方法构建的投资组合的样本外夏普比率和总体回报更佳。HRP 在非相关资产之间实现的分散化使得该方法在面对冲击时更加健壮。

结论

在这个案例研究中,我们看到基于分层聚类的投资组合配置提供了更好的资产分群分离,而无需依赖于马科维茨均值方差投资组合优化中使用的经典相关性分析。

使用马科维茨的技术会产生一个较少多样化、集中在少数股票上的投资组合。而基于分层聚类的 HRP 方法则产生了更多样化和分布更广的投资组合。这种方法展示了最佳的样本外表现,并且由于分散化,提供了更好的尾部风险管理。

实际上,相应的分层风险平衡策略弥补了基于最小方差的投资组合配置的缺陷。它视觉化和灵活,似乎为投资组合配置和管理提供了一个强大的方法。

章节总结

在本章中,我们学习了不同的聚类技术,并使用它们来捕捉数据的自然结构,以增强金融领域决策的效果。通过案例研究,我们展示了聚类技术在增强交易策略和投资组合管理方面的实用性。

除了提供解决不同金融问题的方法外,案例研究还聚焦于理解聚类模型的概念、培养直觉和可视化聚类。总体而言,本章通过案例研究呈现的 Python、机器学习和金融概念可以作为解决金融中任何基于聚类的问题的蓝图。

在讲解了监督学习和无监督学习之后,我们将在下一章探讨另一种类型的机器学习,强化学习。

练习题

  • 使用层次聚类来形成不同资产类别(如外汇或大宗商品)的投资组合。

  • 在债券市场上应用聚类分析进行对冲交易。

¹ 参考 Jupyter 笔记本,了解如何使用 pandas_datareader 获取价格数据。

² 参考第五章获取更多细节。

第四部分:强化学习和自然语言处理

第九章:强化学习

激励驱动着几乎所有事物,金融也不例外。人类不是从数百万个标记示例中学习,而是经常从我们与行动相关联的积极或消极经验中学习。从经验和相关奖励或惩罚中学习是强化学习(RL)的核心思想。¹

强化学习是一种通过最大化奖励和最小化惩罚的最优策略来训练机器找到最佳行动的方法。

赋予AlphaGo(第一个击败职业人类围棋选手的计算机程序)力量的 RL 算法也正在金融领域发展。强化学习的主要思想是最大化奖励,与金融中的多个领域(包括算法交易和投资组合管理)非常契合。强化学习特别适合算法交易,因为在不确定且动态的环境中,最大化回报的代理概念与与金融市场互动的投资者或交易策略有许多共同之处。基于强化学习的模型比前几章讨论的基于价格预测的交易策略更进一步,并确定了基于规则的行动策略(即下订单、不做任何事情、取消订单等)。

类似地,在投资组合管理和资产配置中,基于强化学习的算法不产生预测,也不隐式地学习市场结构。它们做得更多。它们直接学习在不断变化的市场中动态改变投资组合配置权重的策略。强化学习模型也对涉及完成市场工具买卖订单的订单执行问题非常有用。在这里,算法通过试错学习,自行找出执行的最优路径。

强化学习算法具有在操作环境中处理更多细微差别和参数的能力,也可以生成衍生对冲策略。与传统基于金融的对冲策略不同,这些对冲策略在现实世界的市场摩擦下(如交易成本、市场影响、流动性限制和风险限制)是最优和有效的。

在本章中,我们涵盖了三个基于强化学习的案例研究,涵盖了主要的金融应用:算法交易、衍生品对冲和投资组合配置。在模型开发步骤方面,这些案例研究遵循了在第二章中提出的标准化七步模型开发过程。模型开发和评估是强化学习的关键步骤,这些步骤将得到强调。通过实施多个机器学习和金融概念,这些案例研究可以作为解决金融领域中任何其他基于强化学习的问题的蓝图。

在“案例研究 1:基于强化学习的交易策略”中,我们演示了使用强化学习开发算法交易策略。

在“案例研究 2:衍生品对冲”中,我们实施和分析了基于强化学习的技术,用于计算在市场摩擦下的衍生品组合的最优对冲策略。

在“案例研究 3:投资组合配置”中,我们展示了使用基于强化学习的技术处理加密货币数据集,以将资本分配到不同的加密货币以最大化风险调整后收益。我们还介绍了一个基于强化学习的仿真环境,用于训练和测试模型。

本章代码库

本书代码库中的第九章 - 强化学习文件夹中包含了本章中所有案例研究的基于 Python 的 Jupyter 笔记本。要解决涉及 RL 模型(如 DQN 或策略梯度)的任何 Python 机器学习问题,请读者稍微修改模板,以与其问题陈述保持一致。

强化学习——理论与概念

强化学习是一个广泛涵盖各种概念和术语的主题。本章理论部分涵盖了图 9-1 中列出的项目和主题。²

mlbf 0901

图 9-1. RL 概念总结

要使用 RL 解决任何问题,首先理解和定义 RL 组件至关重要。

RL 组件

RL 系统的主要组件包括代理、动作、环境、状态和奖励。

代理

执行动作的实体。

动作

一个代理在其环境内可以执行的操作。

环境

代理所居住的世界。

状态

当前的情况。

奖励

环境即时返回,用于评估代理的最后一个动作。

强化学习的目标是通过实验试验和相对简单的反馈循环学习最优策略。有了最优策略,代理能够积极适应环境以最大化奖励。与监督学习不同,这些奖励信号不会立即提供给模型,而是作为代理进行一系列行动的结果而返回。

代理的行动通常取决于代理从环境中感知到的内容。代理感知到的内容被称为观察或环境的状态。图 9-2 总结了强化学习系统的组成部分。

mlbf 0902

图 9-2. 强化学习组件

代理和环境之间的互动涉及时间上的一系列动作和观察到的奖励,t = 1 , 2 . . . T 。在这个过程中,代理累积关于环境的知识,学习最优策略,并决定下一步应采取哪种行动,以有效地学习最佳策略。让我们用时间步 t 标记状态、动作和奖励,分别为 S t , A t . . . R t 。因此,互动序列完全由一个情节(也称为“试验”或“轨迹”)描述,并且该序列以终端状态结束 S T : S 1 , A 1 , R 2 , S 2 , A 2 . . . A T

除了迄今为止提到的强化学习的五个组成部分之外,还有三个额外的强化学习组成部分:策略、值函数(以及 Q 值)和环境模型。让我们详细讨论这些组成部分。

策略

策略是描述代理如何做出决策的算法或一组规则。更正式地说,策略是一个函数,通常表示为 π,它映射一个状态 (s) 和一个动作 (a):

a t = π ( s t )

这意味着一个 agent 根据其当前状态决定其行动。策略可以是确定性的,也可以是随机的。确定性策略将一个状态映射到行动。另一方面,随机策略输出在动作上的概率分布。这意味着与其确定地采取行动a不同,给定一个状态,对该行动分配了一个概率。

我们在强化学习中的目标是学习一个最优策略(也称为π *)。最优策略告诉我们如何在每个状态下采取行动以最大化回报。

值函数(和 Q 值)

强化学习 agent 的目标是学习在环境中执行任务。从数学上讲,这意味着最大化未来奖励或累积折现奖励G,可以将其表达为不同时间奖励函数R的函数:

G t = R t+1 + γ R t+2 + . . . = 0 y k R t+k+1

折扣因子γ是一个介于 0 和 1 之间的值,用于惩罚未来的奖励,因为未来的奖励不会提供即时的好处,可能具有更高的不确定性。未来的奖励是值函数的重要输入。

值函数(或状态值)通过对未来奖励的预测G t 来衡量状态的吸引力。如果我们在时间t处于这个状态,状态s的值函数是预期回报,采取策略π

V ( s ) = E [ G t | S t = s ]

同样地,我们定义状态-动作对(s , a )的动作值函数(Q 值)为:

Q ( s , a ) = E [ G t | S t = s , A t = a ]

因此,值函数是遵循策略π的状态的预期回报。Q 值是遵循策略π的状态-动作对的预期奖励。

值函数和 Q 值也是相互关联的。由于我们遵循目标策略π,我们可以利用可能行动的概率分布和 Q 值来恢复值函数:

V ( s ) = aA Q ( s , a ) π ( a | s )

上述方程表示值函数和 Q 值之间的关系。

奖励函数(R)、未来奖励(G)、值函数和 Q 值之间的关系被用来推导贝尔曼方程(本章后面讨论),这是许多强化学习模型的关键组成部分之一。

模型

模型是环境的描述符。有了模型,我们可以学习或推断环境将如何与代理人交互并提供反馈。模型被用于规划,这意味着通过考虑可能的未来情况来决定行动方式的任何方式。例如,股票市场的模型负责预测未来价格走势。模型有两个主要部分:转移概率函数P)和奖励函数。我们已经讨论了奖励函数。转移函数(P)记录了在采取行动后从一个状态转移到另一个状态的概率。

总体而言,强化学习代理人可能直接或间接地尝试学习在图 9-3 中显示的策略或值函数。学习策略的方法因强化学习模型类型而异。当我们完全了解环境时,我们可以通过使用基于模型的方法找到最优解。³ 当我们不了解环境时,我们遵循无模型方法并尝试在算法的一部分明确学习模型。

mlbf 0903

图 9-3. 模型、价值和策略

在交易环境中的强化学习组件

让我们尝试理解强化学习组件在交易设置中的对应关系:

代理人

代理人是我们的交易代理人。我们可以将代理人视为根据交易所的当前状态和其账户做出交易决策的人类交易员。

行动

会有三种操作:买入持有卖出

奖励函数

一个明显的奖励函数可能是实现的盈亏(Profit and Loss,PnL)。其他奖励函数可以是夏普比率最大回撤。⁴ 可能存在许多复杂的奖励函数,这些函数在利润和风险之间提供权衡。

环境

在交易环境中,环境被称为交易所。在交易所交易时,我们无法观察到环境的完整状态。具体来说,我们不知道其他代理人,代理人观察到的并非环境的真实状态,而是其某种推导。

这被称为部分可观察马尔可夫决策过程(POMDP)。这是我们在金融领域中遇到的最常见类型的环境。

强化学习建模框架

在本节中,我们描述了多个强化学习模型中使用的核心框架。

贝尔曼方程

贝尔曼方程是一组方程,将值函数和 Q 值分解为即时奖励加上折现未来价值。

在强化学习中,代理人的主要目标是从其到达的每个状态中获得最大的期望奖励总和。为了实现这一点,我们必须尝试获得最优的值函数和 Q 值;贝尔曼方程帮助我们做到这一点。

我们利用奖励函数(R)、未来奖励(G)、价值函数和 Q 值之间的关系推导出了价值函数的贝尔曼方程,如方程 9-1 所示。

方程 9-1。价值函数的贝尔曼方程

V ( s ) = E [ R t+1 + γ V ( S t+1 ) | S t = s ]

在这里,价值函数分解为两部分;即即时奖励,R t+1,以及继任状态的折现价值,γ V ( S t+1 ),如前述方程所示。因此,我们将问题分解为即时奖励和折现后继状态。在时间t的状态s的状态值V(s)可以使用当前奖励R t+1和时间t+1 的价值函数来计算。这就是价值函数的贝尔曼方程。可以最大化这个方程,得到一个称为价值函数贝尔曼最优方程的方程,用V(s)*表示。

我们采用了一个非常类似的算法来估计最优状态-动作值(Q 值)。价值函数和 Q 值的简化迭代算法分别显示在方程 9-2 和 9-3 中。

方程 9-2。价值函数的迭代算法

V k+1 ( s ) = m a a x s P ss a R ss a + γ V k (s )

方程 9-3。Q 值的迭代算法

Q k+1 ( s , a ) = s P ss a [ R ss a + γ m a a x Q k ( s , a ) ]

其中

  • P ss a 是从状态s到状态s′的转移概率,假设选择了动作a

  • R ss a 是当代理从状态s到状态s′时获得的奖励,假设选择了动作a

贝尔曼方程之所以重要,是因为它们让我们将状态的价值表达为其他状态的价值。这意味着,如果我们知道s[t+1]的价值函数或 Q 值,我们可以非常容易地计算s[t]的价值。这为迭代方法计算每个状态的价值打开了很多门,因为如果我们知道下一个状态的价值,我们就可以知道当前状态的价值。

如果我们对环境有完整的信息,Equations 9-2 和 9-3 中显示的迭代算法就会变成一个规划问题,可以通过我们将在下一节中演示的动态规划来解决。不幸的是,在大多数情况下,我们不知道R ss P ss ,因此无法直接应用贝尔曼方程,但它们为许多强化学习算法奠定了理论基础。

马尔可夫决策过程

几乎所有的强化学习问题都可以被建模为马尔可夫决策过程(MDPs)。MDPs 正式描述了强化学习的环境。马尔可夫决策过程由五个元素组成:M = S , A , P , R , γ,符号的含义与前一节中定义的相同:

  • S: 一组状态

  • A: 一组行动

  • P: 转移概率

  • R: 奖励函数

  • γ: 未来奖励的折现因子

MDP 将代理-环境交互框架化为随时间步 t = 1,…,T 的序列决策问题。代理和环境持续交互,代理选择行动,环境对这些行动作出响应,并向代理呈现新的情况,目的是提出一个最优策略或战略。贝尔曼方程构成了整个算法的基础。

MDP 中的所有状态都具有马尔可夫性质,指的是未来仅取决于当前状态,而不取决于历史。

让我们在金融背景下看一个马尔可夫决策过程(MDP)的例子,并分析贝尔曼方程。市场交易可以形式化为一个 MDP,这是一个具有从状态到状态的指定转移概率的过程。Figure 9-4 展示了金融市场中 MDP 的一个示例,具有一组状态、转移概率、行动和奖励。

mlbf 0904

Figure 9-4. 马尔可夫决策过程

此处介绍的 MDP 有三个状态:牛市、熊市和停滞市场,分别由三个状态(s[0]、s[1]、s[2])表示。交易员的三个动作是持有、买入和卖出,分别由 a[0]、a[1]、a[2]表示。这是一个假设性的设置,我们假设转移概率是已知的,交易员的行动会导致市场状态的变化。在接下来的章节中,我们将探讨解决 RL 问题的方法,而不需做出这样的假设。图表还显示了不同行动的转移概率和奖励。如果我们从状态 s[0](牛市)开始,代理可以选择 a[0]、a[1]、a[2](卖出、买入或持有)之间的行动。如果它选择行动买入(a[1]),它可以肯定地留在状态 s[0],但没有任何奖励。因此,如果它想要的话,它可以决定永远留在那里。但如果它选择行动持有(a[0]),它有 70%的概率获得+50 的奖励,并保持在状态 s[0]。然后它可以再次尝试尽可能多地获得奖励。但在某个时候,它会以状态 s[1](停滞市场)结束。在状态 s[1]中,它只有两个可能的动作:持有(a[0])或买入(a[1])。它可以选择重复选择行动 a[1]来保持不动,或者选择进入状态 s[2](熊市),并获得-250 的负奖励。在状态 s[2]中,它别无选择,只能采取买入行动(a[1]),这很可能会使其回到状态 s[0](牛市),并在途中获得+200 的奖励。

现在,通过查看这个 MDP,可以提出一个最优策略或策略,以实现长期内最大的奖励。在状态 s[0]中,明显行动 a[0]是最佳选择,在状态 s[2]中,代理没有选择,只能采取行动 a[1],但在状态 s[1]中,不明确代理应该保持不动(a[0])还是卖出(a[2])。

让我们根据以下贝尔曼方程(参见 方程 9-3)来获取最优的 Q 值:

Q k+1 ( s , a ) = s P ss a [R ss a + γ m a a x Q k (s ,a )]

import numpy as np
nan=np.nan # represents impossible actions
#Array for transition probability
P = np.array([ # shape=[s, a, s']
[[0.7, 0.3, 0.0], [1.0, 0.0, 0.0], [0.8, 0.2, 0.0]],
[[0.0, 1.0, 0.0], [nan, nan, nan], [0.0, 0.0, 1.0]],
[[nan, nan, nan], [0.8, 0.1, 0.1], [nan, nan, nan]],
])

# Array for the return
R = np.array([ # shape=[s, a, s']
[[50., 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[50., 0.0, 0.0], [nan, nan, nan], [0.0, 0.0, -250.]],
[[nan, nan, nan], [200., 0.0, 0.0], [nan, nan, nan]],
])
#Actions
A = [[0, 1, 2], [0, 2], [1]]
#The data already obtained from yahoo finance is imported.

#Now let's run the Q-Value Iteration algorithm:
Q = np.full((3, 3), -np.inf) # -inf for impossible actions
for state, actions in enumerate(A):
    Q[state, actions] = 0.0 # Initial value = 0.0, for all possible actions
discount_rate = 0.95
n_iterations = 100
for iteration in range(n_iterations):
    Q_prev = Q.copy()
    for s in range(3):
        for a in A[s]:
            Q[s, a] = np.sum([
                T[s, a, sp] * (R[s, a, sp] + discount_rate * np.max(Q_prev[sp]))
        for sp in range(3)])
print(Q)

输出

[[109.43230584 103.95749333  84.274035  ]
 [  5.5402017          -inf   5.83515676]
 [        -inf 269.30353051         -inf]]

当使用折现率为 0.95 时,这为我们提供了该 MDP 的最优策略(Q 值)。在牛市(s[0])中选择持有行动(a[0]);在停滞市场(s[1])中选择卖出行动(a[2]);在熊市(s[2])中选择买入行动(a[1])。

上述示例演示了通过动态规划(DP)算法获取最优策略的过程。这些方法假设了对环境的完全了解,虽然在实践中往往是不现实的,但它们构成了大多数其他方法的概念基础。

时间差分学习

具有离散动作的强化学习问题通常可以建模为马尔可夫决策过程,正如我们在前面的例子中看到的,但在大多数情况下,代理程序最初对转移概率一无所知。它也不知道奖励会是什么。这就是时间差分(TD)学习可以发挥作用的地方。

TD 学习算法与基于 Bellman 方程的值迭代算法(Equation 9-2)非常相似,但调整为考虑到代理只有对 MDP 的部分知识。通常情况下,我们假设代理最初只知道可能的状态和动作,什么也不知道。例如,代理使用探索策略,即纯随机策略,来探索 MDP,随着进展,TD 学习算法基于实际观察到的转换和奖励更新状态值的估计。

TD 学习中的关键思想是向估计的回报更新值函数 V(S[t]),接近一个估算的回报 R t+1 + γ V ( S t+1 )(称为TD 目标)。我们希望更新值函数的程度由学习率超参数 α 控制,它定义了我们在更新值时的侵略性。当 α 接近零时,更新不太侵略。当 α 接近一时,我们简单地用更新后的值替换旧值:

V ( s t ) V ( s t ) + α ( R t+1 + γ V ( s t+1 ) V ( s t ) )

类似地,对于 Q 值估计:

Q ( s t , a t ) Q ( s t , a t ) + α ( R t+1 + γ Q ( s t+1 , a t+1 ) Q ( s t , a t ) )

许多 RL 模型使用我们将在下一节中看到的 TD 学习算法。

人工神经网络和深度学习

强化学习模型通常利用人工神经网络和深度学习方法来近似值或策略函数。也就是说,人工神经网络可以学习将状态映射到值,或者将状态-动作对映射到 Q 值。在 RL 的上下文中,ANN 可以使用系数权重来近似将输入映射到输出的函数。ANN 的学习意味着通过迭代调整权重,使得奖励最大化。详见 3 和 5 ,了解与 ANN 相关的方法(包括深度学习)的更多细节。

强化学习模型

根据每步的奖励和概率是否易于访问,强化学习可以分为基于模型无模型算法。

基于模型的算法

基于模型的算法试图理解环境并创建一个代表它的模型。当 RL 问题包括明确定义的转移概率以及有限数量的状态和动作时,可以将其框架化为一个有限 MDP,动态规划(DP)可以计算出一个精确解,类似于前面的例子。⁵

无模型算法

无模型算法仅尝试从实际经验中最大化预期奖励,而不使用模型或先验知识。 当我们对模型有不完整的信息时,我们使用无模型算法。 代理的策略 π(s) 提供了在某一状态下采取的最优行动的指导方针,目标是最大化总奖励。 每个状态都与一个值函数 V(s) 相关联,该函数预测我们能够在该状态上采取相应策略时获得的未来奖励的预期数量。 换句话说,值函数量化了状态的好坏程度。 无模型算法进一步分为基于值的基于策略的。 基于值的算法通过选择状态中的最佳行动来学习状态或 Q 值。 这些算法通常基于我们在 RL 框架部分讨论的时序差分学习。 基于策略的算法(也称为直接策略搜索)直接学习将状态映射到动作的最优策略(或者,如果无法达到真正的最优策略,则尝试近似最优策略)。

在大多数金融情况下,我们并不完全了解环境、奖励或转移概率,因此必须依赖于无模型算法和相关方法。因此,下一节和案例研究的重点将放在无模型方法和相关算法上。

图 9-5 展示了无模型强化学习的分类。我们强烈建议读者参考 强化学习: 一种介绍 以更深入地了解算法和概念。

mlbf 0905

图 9-5. RL 模型分类

在无模型方法的背景下,时序差分学习是其中最常用的方法之一。在 TD 中,算法根据自身的先前估计来优化其估计。 基于值的算法Q-learningSARSA 使用了这种方法。

无模型方法通常利用人工神经网络来近似值或策略函数。 策略梯度深度 Q 网络(DQN) 是两种常用的无模型算法,它们使用人工神经网络。 策略梯度是一种直接参数化策略的基于策略的方法。 深度 Q 网络是一种基于值的方法,它将深度学习与 Q-learning 结合在一起,将学习目标设置为优化 Q 值的估计。

Q-学习

Q-learning 是 TD 学习的一种适应。该算法根据 Q 值(或动作值)函数评估要采取的动作,该函数确定处于某个状态并采取某个动作时的价值。对于每个状态-动作对(s, a),该算法跟踪奖励的运行平均值R,代理在离开状态s并采取动作a后获得的奖励,以及它预计在之后获得的奖励。由于目标策略将最优地行动,我们取下一状态的 Q 值估计的最大值。

学习进行离策略,即算法需要根据仅由值函数暗示的策略选择动作。然而,收敛需要在整个训练过程中更新所有状态-动作对,并确保这一点的简单方法是使用ε-贪心策略,该策略在以下章节进一步定义。

Q-learning 的步骤如下:

  1. 在时间步t,我们从状态s[t]开始,并根据 Q 值选择动作,a t = m a x a Q ( s t , a )

  2. 我们应用一个ε-贪心方法,根据ε的概率随机选择动作,或者根据 Q 值函数选择最佳动作。这确保了在给定状态下探索新动作,同时利用学习经验。⁸

  3. 通过动作a[t],我们观察奖励R[t+1]并进入下一个状态S[t+1]

  4. 我们更新动作值函数:

    Q ( s t , a t ) Q ( s t , a t ) + α ( R t+1 + γ max a Q ( s t+1 , a t ) Q ( s t , a t ) )

  5. 我们增加时间步长,t = t+1,然后重复步骤。

经过足够的迭代步骤,该算法将收敛到最优的 Q 值。

SARSA

SARSA 也是基于 TD 学习的算法。它指的是通过遵循一系列. . . S t , A t , R t+1 , S t+1 , A t+1 , . . . 的步骤更新 Q 值。SARSA 的前两个步骤与 Q 学习的步骤类似。然而,与 Q 学习不同,SARSA 是一个在策略算法,其中代理掌握最优策略并使用相同策略来行动。在这个算法中,用于更新行动的策略是相同的。Q 学习被认为是一个离策略算法。

深度 Q 网络

在前面的部分中,我们看到了如何使用 Q 学习基于贝尔曼方程进行迭代更新,在具有离散状态动作的环境中学习最优 Q 值函数。然而,Q 学习可能具有以下缺点:

  • 在状态和动作空间较大的情况下,最优 Q 值表很快变得计算上不可行。

  • Q 学习可能会遭受不稳定性和发散问题。

为了解决这些问题,我们使用人工神经网络来近似 Q 值。例如,如果我们使用一个参数为θ的函数来计算 Q 值,我们可以将 Q 值函数标记为Q(s,a;θ)。深度 Q 学习算法通过学习一个多层次深度 Q 网络的权重θ来近似 Q 值,旨在通过两种创新机制显著改进和稳定 Q 学习的训练过程:

经验回放

不是在仿真或实际经验中运行 Q 学习的状态-动作对,算法将代理在一个大的回放记忆中存储状态、动作、奖励和下一个状态的转换历史。这可以称为小批量观察。在 Q 学习更新过程中,随机从回放记忆中抽取样本,因此一个样本可以被多次使用。经验回放提高了数据效率,消除了观察序列中的相关性,并平滑了数据分布中的变化。

周期性更新目标

Q被优化为仅周期性更新的目标值。Q 网络被克隆并保持冻结,作为优化目标的每个C步骤(C是一个超参数)。这种修改使训练更加稳定,因为它克服了短期振荡。为了学习网络参数,算法将梯度下降应用于损失函数,该损失函数定义为 DQN 对目标的估计与当前状态-动作对的 Q 值的估计之间的平方差,Q(s,a:θ)。损失函数如下:

L ( θ i ) = 𝔼 [ r + γ max a Q (s ,a;θ i1 ) Q (s,a;θ i ) 2 ]

损失函数本质上是一个均方误差(MSE)函数,其中r + γ max a Q (s ,a;θ i1 )表示目标值,而Q [ s , a ; θ i ]表示预测值。θ是网络的权重,当最小化损失函数时计算出来。目标和当前估计都依赖于权重集合,强调了与监督学习的区别,在监督学习中,目标在训练之前是固定的。

包含买入、卖出和持有动作的交易示例的 DQN 示例在图 9-6 中表示。在这里,我们只将状态(s)作为输入提供给网络,并一次性接收所有可能动作(即买入、卖出和持有)的 Q 值。我们将在本章的第一和第三个案例研究中使用 DQN。

mlbf 0906

图 9-6. DQN

策略梯度

策略梯度是一种基于策略的方法,我们在其中学习一个策略函数,π,它是从每个状态直接映射到该状态的最佳对应动作的直接映射。这是一种比基于价值的方法更为直接的方法,无需 Q 值函数。

策略梯度方法直接学习参数化函数关于θ, π(a|s;θ)。这个函数可能是一个复杂的函数,可能需要一个复杂的模型。在策略梯度方法中,我们使用人工神经网络将状态映射到动作,因为它们在学习复杂函数时是有效的。人工神经网络的损失函数是期望回报的相反数(累积未来奖励)。

策略梯度方法的目标函数可以定义为:

J ( θ ) = V π θ ( S 1 ) = 𝔼 π θ [ V 1 ]

其中,θ表示将状态映射到动作的人工神经网络(ANN)的权重集合。这里的思想是最大化目标函数并计算人工神经网络的权重(θ)。

由于这是一个最大化问题,我们通过梯度上升(与用于最小化损失函数的梯度下降相反)来优化策略,使用策略参数θ的偏导数:

θ θ + θ J ( θ )

使用梯度上升,我们可以找到产生最高回报的最佳θ。通过在第 k 维度中微调θ的小量ε或使用分析方法来计算数值梯度。

在本章后面的案例研究 2 中,我们将使用策略梯度方法。

强化学习中的关键挑战

到目前为止,我们只讨论了强化学习算法能够做到的事情。然而,以下列出了几个缺点:

资源效率

当前的深度强化学习算法需要大量的时间、训练数据和计算资源,以达到理想的熟练水平。因此,使强化学习算法在有限资源下可训练将继续是一个重要问题。

信用分配

在强化学习中,奖励信号可能出现比导致结果的行动晚得多,复杂化了行动与后果的关联。

可解释性

在强化学习中,模型很难提供任何有意义的、直观的输入与相应输出之间的关系,这些关系能够轻松理解。大多数先进的强化学习算法采用深度神经网络,由于神经网络内部有大量的层和节点,这使得解释性变得更加困难。

现在让我们来看看案例研究。

案例研究 1:基于强化学习的交易策略

算法交易主要包括三个组成部分:策略开发参数优化回测。策略根据市场当前状态决定采取什么行动。参数优化通过搜索策略参数的可能值(如阈值或系数)来执行。最后,回测通过探索如何使用历史数据进行交易来评估交易策略的可行性。

强化学习的基础是设计一种策略来最大化给定环境中的奖励。与手工编写基于规则的交易策略不同,强化学习直接学习策略。不需要明确规定规则和阈值。它们自行决定策略的能力使得强化学习模型非常适合创建自动化算法交易模型,或者交易机器人

就参数优化和回测步骤而言,强化学习允许端到端优化并最大化(潜在的延迟)奖励。强化学习代理在一个可以复杂到任意程度的模拟中训练。考虑到延迟、流动性和费用,我们可以无缝地将回测和参数优化步骤结合在一起,而无需经历单独的阶段。

此外,强化学习算法通过人工神经网络参数化学习强大的策略。强化学习算法还可以通过历史数据中的经验来适应各种市场条件,前提是它们经过长时间的训练并具有足够的记忆。这使它们比基于监督学习的交易策略更能适应市场变化,因为监督学习策略由于策略的简单性可能无法具备足够强大的参数化来适应市场变化。

强化学习,凭借其处理策略、参数优化和回测的能力,是下一波算法交易的理想选择。传闻称,一些大型投资银行和对冲基金的高级算法执行团队开始使用强化学习来优化决策。

在这个案例研究中,我们将基于强化学习创建一个端到端的交易策略。我们将使用深度 Q 网络(DQN)的 Q 学习方法来制定策略和实施交易策略。正如之前讨论的,名称“Q-learning”是指 Q 函数,它根据状态 s 和提供的行动 a 返回预期的奖励。除了开发具体的交易策略,本案例研究还将讨论基于强化学习的交易策略的一般框架和组成部分。

创建基于强化学习的交易策略的蓝图

1. 问题定义

在这个案例研究的强化学习框架中,算法根据股票价格的当前状态采取行动(买入、卖出或持有)。该算法使用深度 Q 学习模型进行训练以执行最佳行动。这个案例研究强化学习框架的关键组成部分包括:

代理人

交易代理人。

行动

买入、卖出或持有。

奖励函数

实现的盈亏(PnL)被用作这个案例研究的奖励函数。奖励取决于行动:卖出(实现的盈亏)、买入(无奖励)或持有(无奖励)。

状态

在给定时间窗口内过去股票价格的差异的 sigmoid 函数¹⁰被用作状态。状态 S[t] 被描述为 ( d t-τ+1 , d t-1 , d t ) ,其中 d T = s i g m o i d ( p t p t1 )p t 是时间 t 的价格,τ 是时间窗口大小。sigmoid 函数将过去股票价格的差异转换为介于零和一之间的数字,有助于将值标准化为概率,并使状态更易于解释。

环境

股票交易或股市。

选择用于交易策略的强化学习组件

制定基于强化学习的交易策略的智能行为始于正确识别 RL 模型的组件。因此,在进入模型开发之前,我们应仔细识别以下 RL 组件:

奖励函数

这是一个重要的参数,因为它决定了 RL 算法是否将学习优化适当的指标。除了回报或利润和损失外,奖励函数还可以包括嵌入在基础工具中的风险或包括其他参数,如波动性或最大回撤。它还可以包括买入/卖出操作的交易成本。

状态

状态确定了代理从环境中接收用于决策的观察结果。状态应该代表与过去相比的当前市场行为,并且还可以包括被认为具有预测性的任何信号的值或与市场微观结构相关的项目,例如交易量。

我们将使用的数据是标准普尔 500 指数的收盘价格。该数据来自 Yahoo Finance,包含从 2010 年到 2019 年的十年日常数据。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

这里列出了用于模型实现的所有步骤,从数据加载模型评估,包括基于深度学习的模型开发。大多数这些软件包和函数的细节已在第二章、第三章和第四章中提供。用于不同目的的软件包已在此处的 Python 代码中分开,并且它们的用法将在模型开发过程的不同步骤中进行演示。

用于强化学习的软件包

import keras
from keras import layers, models, optimizers from keras import backend as K
from collections import namedtuple, deque
from keras.models import Sequential
from keras.models import load_model
from keras.layers import Dense
from keras.optimizers import Adam

用于数据处理和可视化的软件/模块

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import read_csv, set_option
import datetime
import math
from numpy.random import choice
import random
from collections import deque

2.2. 加载数据

加载了 2010 年至 2019 年的时间段的获取数据:

dataset = read_csv('data/SP500.csv', index_col=0)

3. 探索性数据分析

在本节中,我们将查看描述性统计和数据可视化。让我们看一下我们的数据集:

# shape
dataset.shape

Output

(2515, 6)
# peek at data
set_option('display.width', 100)
dataset.head(5)

Output

mlbf 09in02

数据总共有 2,515 行和六列,其中包含开盘价最高价最低价收盘价调整后的收盘价总成交量等类别。调整后的收盘价是根据拆分和股利调整后的收盘价。对于本案例研究,我们将重点放在收盘价上。

mlbf 09in03

图表显示,标准普尔 500 指数在 2010 年至 2019 年间呈上升趋势。让我们进行数据准备。

4. 数据准备

这一步是为了创建一个有意义、可靠且干净的数据集,以便在强化学习算法中使用,而无需任何错误。

4.1. 数据清洗

在此步骤中,我们检查行中的 NAs,并且要么删除它们,要么用列的平均值填充它们:

#Checking for any null values and removing the null values'''
print('Null Values =', dataset.isnull().values.any())

Output

Null Values = False

由于数据中没有空值,因此无需进行进一步的数据清理。

5. 评估算法和模型

这是强化学习模型开发的关键步骤,我们将在此定义所有相关函数和类并训练算法。在第一步中,我们为训练集和测试集准备数据。

5.1. 训练测试拆分

在此步骤中,我们将原始数据集分成训练集和测试集。我们使用测试集来确认我们最终模型的性能,并了解是否存在过度拟合。我们将使用 80%的数据集进行建模,20%用于测试:

X=list(dataset["Close"])
X=[float(x) for x in X]
validation_size = 0.2
train_size = int(len(X) * (1-validation_size))
X_train, X_test = X[0:train_size], X[train_size:len(X)]

5.2. 实施步骤和模块

本案例研究(以及通常的强化学习)的整体算法有点复杂,因为它需要构建基于类的代码结构并同时使用许多模块和函数。为了提供对程序中正在发生的事情的功能性解释,本案例研究添加了这一附加部分。

算法简单来说,是在提供当前市场价格时决定是买入、卖出还是持有。

图 9-7 提供了本案例研究背景下基于 Q-learning 的算法训练概述。该算法评估基于 Q 值采取哪种操作,Q 值确定处于某一状态并采取某一动作时的价值。

根据图 9-7,状态 (s) 基于当前和历史价格行为 (P[t], P[t–1],…)。根据当前状态,采取“购买”操作。这一动作导致 $10 的奖励(即与动作相关的 PnL),并进入下一个状态。使用当前奖励和下一个状态的 Q 值,算法更新 Q 值函数。算法继续通过下一个时间步骤。通过足够的迭代步骤,该算法将收敛到最优 Q 值。

mlbf 0907

图 9-7. 交易强化学习

在本案例研究中,我们使用深度 Q 网络来近似 Q 值;因此,动作价值函数定义为 Q(s,a;θ)。深度 Q 学习算法通过学习多层 DQN 的一组权重 θ 来近似 Q 值函数。

模块和函数

实现这个 DQN 算法需要实现几个相互交互的函数和模块。以下是这些模块和函数的摘要:

代理类

代理被定义为Agent类。该类包含变量和成员函数,执行 Q-learning。使用训练阶段创建Agent类的对象,并用于模型训练。

辅助函数

在这个模块中,我们创建了一些对训练有帮助的额外函数。

训练模块

在这一步中,我们使用代理和辅助方法中定义的变量和函数对数据进行训练。在训练过程中,预测每一天的规定动作,计算奖励,并迭代更新基于深度学习的 Q-learning 模型权重。此外,将每个动作的盈亏相加,以确定是否发生了总体利润。旨在最大化总利润。

我们深入探讨了在“5.5. 训练模型”中不同模块和函数之间的交互。

让我们详细看看每一个。

5.3. 代理类

agent类包括以下组件:

  • 构造函数

  • 函数model

  • 函数act

  • 函数expReplay

Constructor 被定义为 init 函数,包含重要的参数,如奖励函数的 discount factorε-greedy 方法的 epsilonstate sizeaction size。动作的数量设定为三个(即购买、出售和持有)。memory 变量定义了 replay memory 的大小。此函数的输入参数还包括 is_eval 参数,用于定义是否正在进行训练。在评估/测试阶段,此变量被设置为 True。此外,如果需要在评估/训练阶段使用预训练模型,则使用 model_name 变量传递:

class Agent:
    def __init__(self, state_size, is_eval=False, model_name=""):
        self.state_size = state_size # normalized previous days
        self.action_size = 3 # hold, buy, sell
        self.memory = deque(maxlen=1000)
        self.inventory = []
        self.model_name = model_name
        self.is_eval = is_eval

        self.gamma = 0.95
        self.epsilon = 1.0
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995

        self.model = load_model("models/" + model_name) if is_eval \
         else self._model()

函数 model 是一个深度学习模型,将环境的状态映射到动作。此函数接受环境状态并返回一个 Q-value 表或指代动作的策略,该策略指代动作的概率分布。此函数是使用 Python 的 Keras 库构建的。¹¹ 所使用的深度学习模型的架构是:

  • 模型预期数据行数与输入的 state size 相等,作为输入。

  • 第一、第二和第三隐藏层分别具有 64328 个节点,所有这些层都使用 ReLU 激活函数。

  • 输出层的节点数等于动作大小(即三个),节点使用线性激活函数。¹²

    def _model(self):
        model = Sequential()
        model.add(Dense(units=64, input_dim=self.state_size, activation="relu"))
        model.add(Dense(units=32, activation="relu"))
        model.add(Dense(units=8, activation="relu"))
        model.add(Dense(self.action_size, activation="linear"))
        model.compile(loss="mse", optimizer=Adam(lr=0.001))

        return model

函数 act 根据状态返回一个动作。此函数使用 model 函数并返回购买、出售或持有动作:

    def act(self, state):
        if not self.is_eval and random.random() <= self.epsilon:
            return random.randrange(self.action_size)

        options = self.model.predict(state)
        return np.argmax(options[0])

函数 expReplay 是关键函数,其中基于观察到的经验训练神经网络。此函数实现了之前讨论过的 Experience replay 机制。Experience replay 存储了代理所经历的状态、动作、奖励和下一个状态转换的历史记录。它将迷你批次的观察数据(replay memory)作为输入,并通过最小化损失函数更新基于深度学习的 Q-learning 模型权重。在此函数中实现了 epsilon greedy 方法,以防止过拟合。为了解释函数,以下 Python 代码的评论中编号了不同的步骤,并概述了这些步骤:

  1. 准备回放缓冲内存,这是用于训练的一组观察。使用循环将新的经验添加到回放缓冲内存中。

  2. Loop 遍历迷你批次中的所有状态、动作、奖励和下一个状态转换的观察。

  3. 基于贝尔曼方程更新 Q 表的目标变量。如果当前状态是终端状态或者是本集的末尾,则进行更新。变量 done 表示是否结束,并在训练函数中进一步定义。如果不是 done,则目标仅设置为奖励。

  4. 使用深度学习模型预测下一个状态的 Q 值。

  5. 这种状态在当前回放缓冲区中的动作的 Q 值被设置为目标。

  6. 使用model.fit函数更新深度学习模型权重。

  7. 实施ε贪婪方法。回想一下,这种方法以ε的概率随机选择一个动作,或者根据 Q 值函数以 1–ε的概率选择最佳动作。

    def expReplay(self, batch_size):
        mini_batch = []
        l = len(self.memory)
        #1: prepare replay memory
        for i in range(l - batch_size + 1, l):
            mini_batch.append(self.memory[i])

        #2: Loop across the replay memory batch.
        for state, action, reward, next_state, done in mini_batch:
            target = reward # reward or Q at time t
            #3: update the target for Q table. table equation
            if not done:
                target = reward + self.gamma * \
                 np.amax(self.model.predict(next_state)[0])
            #set_trace()

            # 4: Q-value of the state currently from the table
            target_f = self.model.predict(state)
            # 5: Update the output Q table for the given action in the table
            target_f[0][action] = target
            # 6\. train and fit the model.
            self.model.fit(state, target_f, epochs=1, verbose=0)

        #7\. Implement epsilon greedy algorithm
        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay

5.4. 辅助函数

在这个模块中,我们创建了一些对训练有帮助的额外函数。这里讨论了一些重要的辅助函数。有关其他辅助函数的详细信息,请参考该书籍的 GitHub 存储库中的 Jupyter 笔记本。

函数getState根据股票数据、时间t(预测日)和窗口n(向前回溯的天数)生成状态。首先计算价格差向量,然后使用sigmoid函数将该向量从零缩放到一。这将作为状态返回。

def getState(data, t, n):
    d = t - n + 1
    block = data[d:t + 1] if d >= 0 else -d * [data[0]] + data[0:t + 1]
    res = []
    for i in range(n - 1):
        res.append(sigmoid(block[i + 1] - block[i]))
    return np.array([res])

函数plot_behavior返回市场价格的图表,并显示买入和卖出动作的指示器。它用于训练和测试阶段算法的整体评估。

def plot_behavior(data_input, states_buy, states_sell, profit):
    fig = plt.figure(figsize = (15, 5))
    plt.plot(data_input, color='r', lw=2.)
    plt.plot(data_input, '^', markersize=10, color='m', label='Buying signal',\
     markevery=states_buy)
    plt.plot(data_input, 'v', markersize=10, color='k', label='Selling signal',\
     markevery = states_sell)
    plt.title('Total gains: %f'%(profit))
    plt.legend()
    plt.show()

5.5. 训练模型

我们将继续训练数据。根据我们的代理,我们定义以下变量并实例化股票代理:

剧集

代码在整个数据上训练的次数。在本案例研究中,我们使用 10 集。

窗口大小

考虑评估状态的市场日数。

批大小

回放缓冲区大小或训练期间的内存使用。

一旦定义了这些变量,我们通过集数训练模型。图 9-8 提供了深入的训练步骤,并整合了到目前为止讨论的所有元素。显示步骤 1 到 7 的上部分描述了训练模块中的步骤,下部分描述了回放缓冲区函数中的步骤(即exeReplay函数)。

mlbf 0908

图 9-8. Q 交易的训练步骤

在图 9-8 中展示的 1 到 6 步骤在以下 Python 代码中编号,并描述如下:

  1. 使用辅助函数getState获取当前状态。它返回一个状态向量,其长度由窗口大小定义,状态值在零到一之间。

  2. 使用代理类的act函数获取给定状态的动作。

  3. 获取给定动作的奖励。行动和奖励的映射在本案例研究的问题定义部分中描述。

  4. 使用getState函数获取下一个状态。下一个状态的详细信息进一步用于更新 Q 函数的贝尔曼方程。

  5. 状态的细节、下一个状态、动作等保存在代理对象的内存中,该对象进一步由exeReply函数使用。一个示例小批次如下:

    mlbf 09in04

  6. 检查批次是否完整。批次的大小由批次大小变量定义。如果批次完整,则我们转到Replay buffer功能,并通过最小化 Q 预测和 Q 目标之间的 MSE 来更新 Q 函数。如果不完整,则我们转到下一个时间步骤。

该代码生成每个周期的最终结果,以及显示训练阶段每个周期的买卖操作和总利润的图表。

window_size = 1
agent = Agent(window_size)
l = len(data) - 1
batch_size = 10
states_sell = []
states_buy = []
episode_count = 3

for e in range(episode_count + 1):
    print("Episode " + str(e) + "/" + str(episode_count))
    # 1-get state
    state = getState(data, 0, window_size + 1)

    total_profit = 0
    agent.inventory = []

    for t in range(l):
        # 2-apply best action
        action = agent.act(state)

        # sit
        next_state = getState(data, t + 1, window_size + 1)
        reward = 0

        if action == 1: # buy
            agent.inventory.append(data[t])
            states_buy.append(t)
            print("Buy: " + formatPrice(data[t]))

        elif action == 2 and len(agent.inventory) > 0: # sell
            bought_price = agent.inventory.pop(0)
             #3: Get Reward

            reward = max(data[t] - bought_price, 0)
            total_profit += data[t] - bought_price
            states_sell.append(t)
            print("Sell: " + formatPrice(data[t]) + " | Profit: " \
            + formatPrice(data[t] - bought_price))

        done = True if t == l - 1 else False
        # 4: get next state to be used in bellman's equation
        next_state = getState(data, t + 1, window_size + 1)

        # 5: add to the memory
        agent.memory.append((state, action, reward, next_state, done))
        state = next_state

        if done:

            print("--------------------------------")
            print("Total Profit: " + formatPrice(total_profit))
            print("--------------------------------")

        # 6: Run replay buffer function
        if len(agent.memory) > batch_size:
            agent.expReplay(batch_size)

    if e % 10 == 0:
        agent.model.save("models/model_ep" + str(e))

Output

Running episode 0/10
Total Profit: $6738.87

mlbf 09in05

Running episode 1/10
Total Profit: –$45.07

mlbf 09in06

Running episode 9/10
Total Profit: $1971.54

mlbf 09in07

Running episode 10/10
Total Profit: $1926.84

mlbf 09in08

图表显示了买卖模式的详细信息以及前两个(0 和 1)和后两个(9 和 10)周期的总收益。其他周期的详细信息可以在本书的 GitHub 存储库中的 Jupyter 笔记本中查看。

正如我们所看到的,在第 0 和 1 周期的开始阶段,由于代理人对其行动后果没有先验观念,它会采取随机化的行动来观察相关的奖励。在第零周期中,总体利润为$6,738,确实是一个强劲的结果,但在第一个周期中,我们经历了总体损失$45。每个周期的累积奖励波动较大,说明了算法正在经历的探索过程。观察第 9 和 10 周期,似乎代理人开始从训练中学习。它发现了策略并开始始终稳定地利用它。这些后两个周期的买卖行动导致的利润可能少于第零周期,但更加稳健。后续周期的买卖行动在整个时间段内都是一致的,并且总体利润是稳定的。

理想情况下,训练周期的数量应该高于本案例研究中使用的数量。更多的训练周期将导致更好的训练表现。在我们进入测试之前,让我们详细了解模型调优的细节。

5.6. 模型调优

类似于其他机器学习技术,我们可以通过使用网格搜索等技术来找到 RL 中的最佳模型超参数组合。针对基于 RL 的问题进行的网格搜索计算密集。因此,在本节中,我们不进行网格搜索,而是呈现需要考虑的关键超参数、它们的直觉以及对模型输出的潜在影响。

Gamma(折现因子)

随着学习的进行,衰减的伽马会使代理人优先考虑短期奖励,并且对长期奖励的重视程度降低。在这个案例研究中降低折现因子可能导致算法集中于长期奖励。

Epsilon

epsilon 变量驱动模型的“探索与利用”属性。我们越了解我们的环境,我们就越不想进行随机探索。当我们减少 epsilon 时,随机行动的可能性变小,我们会更多地利用我们已经发现的高价值行动机会。然而,在交易设置中,我们不希望算法对训练数据进行过拟合,因此 epsilon 应相应地进行修改。

情节和批次大小

训练集中更多的情节和更大的批次大小将导致更好的训练和更优化的 Q 值。然而,存在一种权衡,增加情节和批次大小会增加总训练时间。

窗口大小

窗口大小确定了考虑以评估状态的市场日数。如果我们希望状态由过去更多天数确定,则可以增加这个数量。

深度学习模型的层数和节点数

这可以修改以实现更好的训练和更优化的 Q 值。有关改变 ANN 模型的层数和节点的影响的详细信息在第三章中讨论,并且在第五章中讨论了深度学习模型的网格搜索。

6. 测试数据

训练数据后,对测试数据集进行评估是一个重要的步骤,特别是对于强化学习,因为代理可能会错误地将奖励与数据的某些伪特征相关联,或者可能会过度拟合特定的图表模式。在测试步骤中,我们查看已经训练好的模型(model_ep10)在测试数据上的表现。Python 代码看起来与我们之前看到的训练集类似。但是,is_eval 标志设置为 truereply buffer 函数不被调用,也没有训练。让我们看看结果:

#agent is already defined in the training set above.
test_data = X_test
l_test = len(test_data) - 1
state = getState(test_data, 0, window_size + 1)
total_profit = 0
is_eval = True
done = False
states_sell_test = []
states_buy_test = []
model_name = "model_ep10"
agent = Agent(window_size, is_eval, model_name)
state = getState(data, 0, window_size + 1)
total_profit = 0
agent.inventory = []

for t in range(l_test):
    action = agent.act(state)

    next_state = getState(test_data, t + 1, window_size + 1)
    reward = 0

    if action == 1:

        agent.inventory.append(test_data[t])
        print("Buy: " + formatPrice(test_data[t]))

    elif action == 2 and len(agent.inventory) > 0:
        bought_price = agent.inventory.pop(0)
        reward = max(test_data[t] - bought_price, 0)
        total_profit += test_data[t] - bought_price
        print("Sell: " + formatPrice(test_data[t]) + " | profit: " +\
         formatPrice(test_data[t] - bought_price))

    if t == l_test - 1:
        done = True
    agent.memory.append((state, action, reward, next_state, done))
    state = next_state

    if done:
        print("------------------------------------------")
        print("Total Profit: " + formatPrice(total_profit))
        print("------------------------------------------")

输出

Total Profit: $1280.40

mlbf 09in09

从上面的结果来看,我们的模型在测试集上总体上实现了 $1,280 的利润,我们可以说我们的 DQN 代理在测试集上表现相当不错。

结论

在这个案例研究中,我们创建了一个自动化交易策略,或者交易机器人,只需提供运行中的股票市场数据就能产生交易信号。我们看到算法自行决定策略,总体方法比基于监督学习的方法简单得多,更有原则性。经过训练的模型在测试集上盈利,证实了基于强化学习的交易策略的有效性。

在使用基于深度神经网络的强化学习模型如 DQN 时,我们可以学习到比人类交易员更复杂和更强大的策略。

考虑到基于强化学习的模型的高复杂性和低可解释性,可视化和测试步骤变得非常重要。为了可解释性,我们使用训练算法的训练周期图表,并发现模型随着时间开始学习,发现策略并开始利用它。在将模型用于实时交易之前,应在不同时间段进行足够数量的测试。

在使用基于强化学习的模型时,我们应该仔细选择强化学习组件,例如奖励函数和状态,并确保理解它们对整体模型结果的影响。在实施或训练模型之前,重要的是考虑以下问题:“我们如何设计奖励函数或状态,使得强化学习算法有潜力学习优化正确的度量标准?”

总体而言,这些基于强化学习的模型可以使金融从业者以非常灵活的方式创建交易策略。本案例研究提供的框架可以成为开发算法交易更强大模型的绝佳起点。

案例研究 2:衍生品对冲

处理衍生品定价和风险管理的传统金融理论大部分基于理想化的完全市场假设,即完美对冲性,没有交易限制、交易成本、市场冲击或流动性约束。然而,在实践中,这些摩擦是非常真实的。因此,使用衍生品进行实际风险管理需要人类的监督和维护;单靠模型本身是不够的。实施仍然部分受到交易员对现有工具局限性直觉理解的驱动。

强化学习算法因其在操作环境中处理更多细微差别和参数的能力,天生与对冲目标一致。这些模型能够生成动态策略,即使在存在摩擦的世界中也是最优的。无模型强化学习方法几乎不需要理论假设。这使得对冲自动化无需频繁人为干预,显著加快了整个对冲过程。这些模型可以从大量历史数据中学习,并考虑多个变量以做出更精确和准确的对冲决策。此外,大量数据的可用性使得基于强化学习的模型比以往任何时候都更加有用和有效。

在这个案例研究中,我们实施了一种基于强化学习的对冲策略,采用了汉斯·比勒等人在论文"深度对冲"中提出的观点。我们将通过最小化风险调整后的损益(PnL)来构建一种特定类型衍生品(认购期权)的最优对冲策略。我们使用CVaR(条件价值风险)来量化头寸或投资组合的尾部风险作为风险评估措施。

基于强化学习的对冲策略实施蓝图

1. 问题定义

在这个案例研究的强化学习框架中,算法利用基础资产的市场价格决定看涨期权的最佳对冲策略。采用直接的策略搜索强化学习策略。总体思路源自于“Deep Hedging”论文,其目标是在风险评估度量下最小化对冲误差。从t=1 到t=T 的一段时间内,看涨期权对冲策略的总体 PnL 可以写成:

P n L T ( Z , δ ) = Z T + t=1 T δ t1 ( S t S t1 ) t=1 T C t

其中

  • Z T 是到期日看涨期权的收益。

  • δ t1 ( S t S t1 ) 是第t天对冲工具的现金流,其中δ是对冲,S t是第t天的现货价格。

  • C t 是第t时间点的交易成本,可能是常数或与对冲规模成比例。

方程中的各个组成部分是现金流的组成部分。然而,在设计奖励函数时,最好考虑到任何头寸带来的风险。我们使用 CVaR 作为风险评估度量。CVaR 量化了尾部风险的数量,并且是对置信水平αexpected shortfall(风险厌恶参数)¹³。现在奖励函数修改如下:

V T = f ( Z T + t=1 T δ t1 ( S t S t1 ) t=1 T C t )

其中f代表 CVaR。

我们将训练一个RNN-based网络来学习最优的对冲策略(即,δ 1 , δ 2 . . . , δ T ),给定股票价格、行权价格和风险厌恶参数(α),通过最小化 CVaR 来实现。我们假设交易成本为零以简化模型。该模型可以轻松扩展以包括交易成本和其他市场摩擦。

用于合成基础股票价格的数据是通过蒙特卡洛模拟生成的,假设价格服从对数正态分布。我们假设利率为 0%,年波动率为 20%。

模型的关键组成部分是:

代理人

交易员或交易代理人。

行动

对冲策略(即,δ 1 , δ 2 . . . , δ T)。

奖励函数

CVaR——这是一个凸函数,在模型训练过程中被最小化。

状态

状态是当前市场和相关产品变量的表示。该状态代表模型输入,包括模拟的股票价格路径(即,S 1 , S 2 . . . , S T ),行权价格和风险厌恶参数(α)。

环境

股票交易或股票市场。

2. 入门指南

2.1. 加载 Python 包

加载 Python 包类似于以前的案例研究。有关更多详细信息,请参阅此案例研究的 Jupyter 笔记本。

2.2. 生成数据

在这一步中,我们使用 Black-Scholes 模拟生成了本案例研究的数据。

此函数生成股票价格的蒙特卡罗路径,并获取每个蒙特卡罗路径上的期权价格。如所示的计算基于股票价格的对数正态假设:

S t+1 = S t e μ1 2σ 2 Δt+σΔtZ

其中 S 是股票价格,σ 是波动率,μ 是漂移,t 是时间,Z 是标准正态变量。

def monte_carlo_paths(S_0, time_to_expiry, sigma, drift, seed, n_sims, \
  n_timesteps):
    """
 Create random paths of a stock price following a brownian geometric motion
 return:

 a (n_timesteps x n_sims x 1) matrix
 """
    if seed > 0:
            np.random.seed(seed)
    stdnorm_random_variates = np.random.randn(n_sims, n_timesteps)
    S = S_0
    dt = time_to_expiry / stdnorm_random_variates.shape[1]
    r = drift
    S_T = S * np.cumprod(np.exp((r-sigma**2/2)*dt+sigma*np.sqrt(dt)*\
    stdnorm_random_variates), axis=1)
    return np.reshape(np.transpose(np.c_[np.ones(n_sims)*S_0, S_T]), \
    (n_timesteps+1, n_sims, 1))

我们对一个月内的现货价格生成了 50,000 次模拟。总时间步数为 30。因此,每个蒙特卡罗场景每天观察一次。模拟所需的参数如下定义:

S_0 = 100; K = 100; r = 0; vol = 0.2; T = 1/12
timesteps = 30; seed = 42; n_sims = 5000

# Generate the monte carlo paths
paths_train = monte_carlo_paths(S_0, T, vol, r, seed, n_sims, timesteps)

3. 探索性数据分析

我们将在本节中查看描述性统计和数据可视化。考虑到数据是通过模拟生成的,我们简单地检查一个路径,作为模拟算法的健全性检查:

#Plot Paths for one simulation
plt.figure(figsize=(20, 10))
plt.plot(paths_train[1])
plt.xlabel('Time Steps')
plt.title('Stock Price Sample Paths')
plt.show()

输出

mlbf 09in10

4. 评估算法和模型

在这种直接策略搜索方法中,我们使用人工神经网络(ANN)将状态映射到动作。在传统的 ANN 中,我们假设所有输入(和输出)都彼此独立。然而,时间 t 的对冲决策(由 δ[t] 表示)是路径相关的,并且受到前几个时间步的股票价格和对冲决策的影响。因此,使用传统的 ANN 是不可行的。循环神经网络(RNN)是一种能够捕捉底层系统时间变化动态的 ANN 类型,在这种情况下更为合适。RNN 具有记忆能力,可以记录到目前为止计算过的信息。我们利用了 RNN 模型在时间序列建模中的这一特性,如第五章所述。长短期记忆网络(LSTM)(也在第五章中讨论)是一种特殊的 RNN,能够学习长期依赖关系。在映射到动作时,过去的状态信息对网络可用;从而在训练过程中学习相关的过去数据。我们将使用 LSTM 模型将状态映射到动作,并获取对冲策略(即,δ[1]、δ[2]、…δ[T])。

4.1. 策略梯度脚本

我们将在本节中涵盖实施步骤和模型训练。我们向训练模型提供输入变量——股票价格路径( S 1 , S 2 , . . . S T )、行权价格和风险厌恶参数, α —并接收对冲策略(即, δ 1 , δ 2 , . . . δ T ))作为输出。 图 9-9 概述了本案例研究中策略梯度训练的过程。

mlbf 0909

图 9-9. 用于衍生对冲的策略梯度训练

我们已经在本案例研究的第二部分中执行了步骤 1。步骤 2 到 5 是不言自明的,并在稍后定义的 agent 类中实现。agent 类保存执行训练的变量和成员函数。通过 agent 类的对象进行训练模型的创建。在执行了足够数量的步骤 2 到 5 迭代后,生成了一个最优的策略梯度模型。

课程包括以下模块:

  • Constructor

  • 函数 execute_graph_batchwise

  • 函数 trainingpredictrestore

让我们深入研究每个函数的 Python 代码。

Constructor被定义为init函数,我们在其中定义模型参数。我们可以在构造函数中传递 LSTM 模型的timestepsbatch_size和每层节点数。我们将模型的输入变量(即股价路径、行权价和风险厌恶参数)定义为TensorFlow placeholders。Placeholders 用于从计算图外部提供数据,并在训练阶段提供这些输入变量的数据。我们通过使用tf.MultiRNNCell函数在 TensorFlow 中实现 LSTM 网络。LSTM 模型使用四层,节点数分别为 62、46、46 和 1。损失函数是 CVaR,在调用tf.train进行训练步骤时最小化。我们对交易策略的负实现 PnL 进行排序,并计算(1−α)个顶部损失的均值:

class Agent(object):
    def __init__(self, time_steps, batch_size, features,\
       nodes = [62, 46, 46, 1], name='model'):

        #1\. Initialize the variables
        tf.reset_default_graph()
        self.batch_size = batch_size # Number of options in a batch
        self.S_t_input = tf.placeholder(tf.float32, [time_steps, batch_size, \
          features]) #Spot
        self.K = tf.placeholder(tf.float32, batch_size) #Strike
        self.alpha = tf.placeholder(tf.float32) #alpha for cVaR

        S_T = self.S_t_input[-1,:,0] #Spot at time T
        # Change in the Spot
        dS = self.S_t_input[1:, :, 0] - self.S_t_input[0:-1, :, 0]
        #dS = tf.reshape(dS, (time_steps, batch_size))

        #2\. Prepare S_t for use in the RNN remove the \
        #last time step (at T the portfolio is zero)
        S_t = tf.unstack(self.S_t_input[:-1, :,:], axis=0)

        # Build the lstm
        lstm = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.LSTMCell(n) \
        for n in nodes])

        #3\. So the state is a convenient tensor that holds the last
        #actual RNN state,ignoring the zeros.
        #The strategy tensor holds the outputs of all cells.
        self.strategy, state = tf.nn.static_rnn(lstm, S_t, initial_state=\
          lstm.zero_state(batch_size, tf.float32), dtype=tf.float32)

        self.strategy = tf.reshape(self.strategy, (time_steps-1, batch_size))

        #4\. Option Price
        self.option = tf.maximum(S_T-self.K, 0)

        self.Hedging_PnL = - self.option + tf.reduce_sum(dS*self.strategy, \
          axis=0)

        #5\. Total Hedged PnL of each path
        self.Hedging_PnL_Paths = - self.option + dS*self.strategy

        # 6\. Calculate the CVaR for a given confidence level alpha
        # Take the 1-alpha largest losses (top 1-alpha negative PnLs)
        #and calculate the mean
        CVaR, idx = tf.nn.top_k(-self.Hedging_PnL, tf.cast((1-self.alpha)*\
        batch_size, tf.int32))
        CVaR = tf.reduce_mean(CVaR)
        #7\. Minimize the CVaR
        self.train = tf.train.AdamOptimizer().minimize(CVaR)
        self.saver = tf.train.Saver()
        self.modelname = name

函数execute_graph_batchwise是程序的关键函数,在此函数中,我们根据观察到的经验训练神经网络。它将一批状态作为输入,并通过最小化 CVaR 来更新基于策略梯度的 LSTM 模型权重。此函数通过循环遍历各个时期和批次来训练 LSTM 模型以预测对冲策略。首先,它准备了一批市场变量(股价、行权价和风险厌恶),并使用sess.run函数进行训练。这里,sess.run是一个 TensorFlow 函数,用于运行其中定义的任何操作。它获取输入并运行在构造函数中定义的tf.train函数。经过足够数量的迭代后,生成了一个最优的策略梯度模型:

    def _execute_graph_batchwise(self, paths, strikes, riskaversion, sess, \
      epochs=1, train_flag=False):
        #1: Initialize the variables.
        sample_size = paths.shape[1]
        batch_size=self.batch_size
        idx = np.arange(sample_size)
        start = dt.datetime.now()
        #2:Loop across all the epochs
        for epoch in range(epochs):
            # Save the hedging Pnl for each batch
            pnls = []
            strategies = []
            if train_flag:
                np.random.shuffle(idx)
            #3\. Loop across the observations
            for i in range(int(sample_size/batch_size)):
                indices = idx[i*batch_size : (i+1)*batch_size]
                batch = paths[:,indices,:]

                #4\. Train the LSTM
                if train_flag:#runs the train, hedging PnL and strategy.
                    _, pnl, strategy = sess.run([self.train, self.Hedging_PnL, \
                      self.strategy], {self.S_t_input: batch,\
                        self.K : strikes[indices],\
                        self.alpha: riskaversion})
                        #5\. Evaluation and no training
                else:
                    pnl, strategy = sess.run([self.Hedging_PnL, self.strategy], \
                      {self.S_t_input: batch,\
                      self.K : strikes[indices],
                      self.alpha: riskaversion})\

                pnls.append(pnl)
                strategies.append(strategy)
            #6\. Calculate the option price # given the risk aversion level alpha

            CVaR = np.mean(-np.sort(np.concatenate(pnls))\
            [:int((1-riskaversion)*sample_size)])
            #7\. Return training metrics, \
            #if it is in the training phase
            if train_flag:
                if epoch % 10 == 0:
                    print('Time elapsed:', dt.datetime.now()-start)
                    print('Epoch', epoch, 'CVaR', CVaR)
                    #Saving the model
                    self.saver.save(sess, "model.ckpt")
        self.saver.save(sess, "model.ckpt")

        #8\. return CVaR and other parameters
        return CVaR, np.concatenate(pnls), np.concatenate(strategies,axis=1)

training函数简单地触发execute_graph_batchwise函数,并向该函数提供所有训练所需的输入。predict函数根据状态(市场变量)返回动作(对冲策略)。restore函数恢复保存的训练模型,以便用于进一步的训练或预测:

    def training(self, paths, strikes, riskaversion, epochs, session, init=True):
        if init:
            sess.run(tf.global_variables_initializer())
        self._execute_graph_batchwise(paths, strikes, riskaversion, session, \
          epochs, train_flag=True)

    def predict(self, paths, strikes, riskaversion, session):
        return self._execute_graph_batchwise(paths, strikes, riskaversion,\
          session,1, train_flag=False)

    def restore(self, session, checkpoint):
        self.saver.restore(session, checkpoint)

4.2. 训练数据

训练基于策略的模型的步骤是:

  1. 为 CVaR 定义风险厌恶参数、特征数(这是股票的总数,在本例中我们只有一个)、行权价和批量大小。CVaR 表示我们希望最小化的损失量。例如,CVaR 为 99%表示我们希望避免极端损失,而 CVaR 为 50%则最小化平均损失。我们使用 50%的 CVaR 进行训练,以获得较小的均值损失。

  2. 实例化策略梯度代理,其具有基于 RNN 的策略和基于 CVaR 的损失函数。

  3. 通过批次进行迭代;策略由基于 LSTM 的网络的策略输出定义。

  4. 最后,保存训练好的模型。

batch_size = 1000
features = 1
K = 100
alpha = 0.50 #risk aversion parameter for CVaR
epoch = 101 #It is set to 11, but should ideally be a high number
model_1 = Agent(paths_train.shape[0], batch_size, features, name='rnn_final')
# Training the model takes a few minutes
start = dt.datetime.now()
with tf.Session() as sess:
    # Train Model
    model_1.training(paths_train, np.ones(paths_train.shape[1])*K, alpha,\
     epoch, sess)
print('Training finished, Time elapsed:', dt.datetime.now()-start)

Output

Time elapsed: 0:00:03.326560
Epoch 0 CVaR 4.0718956
Epoch 100 CVaR 2.853285
Training finished, Time elapsed: 0:01:56.299444

5. 测试数据

测试是一个重要的步骤,特别是对于强化学习,因为模型很难提供任何能够直观理解输入与相应输出之间关系的有意义关系。在测试步骤中,我们将比较对冲策略的有效性,并将其与基于 Black-Scholes 模型的 delta 对冲策略进行比较。我们首先定义在此步骤中使用的辅助函数。

5.1. 用于与 Black-Scholes 比较的辅助函数

在本模块中,我们创建了用于与传统 Black-Scholes 模型进行比较的额外函数。

5.1.1. Black-Scholes 价格和 delta

函数BlackScholes_price实现了看涨期权价格的解析公式,BS_delta实现了看涨期权的 delta 解析公式:

def BS_d1(S, dt, r, sigma, K):
    return (np.log(S/K) + (r+sigma**2/2)*dt) / (sigma*np.sqrt(dt))

def BlackScholes_price(S, T, r, sigma, K, t=0):
    dt = T-t
    Phi = stats.norm(loc=0, scale=1).cdf
    d1 = BS_d1(S, dt, r, sigma, K)
    d2 = d1 - sigma*np.sqrt(dt)
    return S*Phi(d1) - K*np.exp(-r*dt)*Phi(d2)

def BS_delta(S, T, r, sigma, K, t=0):
    dt = T-t
    d1 = BS_d1(S, dt, r, sigma, K)
    Phi = stats.norm(loc=0, scale=1).cdf
    return Phi(d1)

5.1.2. 测试结果和绘图

以下函数用于计算评估对冲效果的关键指标及相关图表。函数test_hedging_strategy计算不同类型的 PnL,包括 CVaR、PnL 和对冲 PnL。函数plot_deltas绘制了不同时间点上 RL delta 与 Black-Scholes 对冲的比较。函数plot_strategy_pnl用于绘制基于 RL 策略的总 PnL 与 Black-Scholes 对冲的比较图:

def test_hedging_strategy(deltas, paths, K, price, alpha, output=True):
    S_returns = paths[1:,:,0]-paths[:-1,:,0]
    hedge_pnl = np.sum(deltas * S_returns, axis=0)
    option_payoff = np.maximum(paths[-1,:,0] - K, 0)
    replication_portfolio_pnls = -option_payoff + hedge_pnl + price
    mean_pnl = np.mean(replication_portfolio_pnls)
    cvar_pnl = -np.mean(np.sort(replication_portfolio_pnls)\
    [:int((1-alpha)*replication_portfolio_pnls.shape[0])])
    if output:
        plt.hist(replication_portfolio_pnls)
        print('BS price at t0:', price)
        print('Mean Hedging PnL:', mean_pnl)
        print('CVaR Hedging PnL:', cvar_pnl)
    return (mean_pnl, cvar_pnl, hedge_pnl, replication_portfolio_pnls, deltas)

def plot_deltas(paths, deltas_bs, deltas_rnn, times=[0, 1, 5, 10, 15, 29]):
    fig = plt.figure(figsize=(10,6))
    for i, t in enumerate(times):
        plt.subplot(2,3,i+1)
        xs =  paths[t,:,0]
        ys_bs = deltas_bs[t,:]
        ys_rnn = deltas_rnn[t,:]
        df = pd.DataFrame([xs, ys_bs, ys_rnn]).T

        plt.plot(df[0], df[1], df[0], df[2], linestyle='', marker='x' )
        plt.legend(['BS delta', 'RNN Delta'])
        plt.title('Delta at Time %i' % t)
        plt.xlabel('Spot')
        plt.ylabel('$\Delta$')
    plt.tight_layout()

def plot_strategy_pnl(portfolio_pnl_bs, portfolio_pnl_rnn):
    fig = plt.figure(figsize=(10,6))
    sns.boxplot(x=['Black-Scholes', 'RNN-LSTM-v1 '], y=[portfolio_pnl_bs, \
    portfolio_pnl_rnn])
    plt.title('Compare PnL Replication Strategy')
    plt.ylabel('PnL')

5.1.3. Black-Scholes 复制的对冲误差

以下函数用于基于传统 Black-Scholes 模型获取对冲策略,进一步用于与基于 RL 的对冲策略进行比较:

def black_scholes_hedge_strategy(S_0, K, r, vol, T, paths, alpha, output):
    bs_price = BlackScholes_price(S_0, T, r, vol, K, 0)
    times = np.zeros(paths.shape[0])
    times[1:] = T / (paths.shape[0]-1)
    times = np.cumsum(times)
    bs_deltas = np.zeros((paths.shape[0]-1, paths.shape[1]))
    for i in range(paths.shape[0]-1):
        t = times[i]
        bs_deltas[i,:] = BS_delta(paths[i,:,0], T, r, vol, K, t)
    return test_hedging_strategy(bs_deltas, paths, K, bs_price, alpha, output)

5.2. Black-Scholes 与强化学习的比较

我们将通过观察 CVaR 风险厌恶参数的影响来比较对冲策略的有效性,并检查基于 RL 的模型在改变期权资金性质、漂移和基础过程波动性时的泛化能力。

5.2.1. 在 99%风险厌恶的测试中

如前所述,CVaR 代表我们希望最小化的损失量。我们使用 50%的风险厌恶训练模型以最小化平均损失。然而,出于测试目的,我们将风险厌恶提高到 99%,意味着我们希望避免极端损失。这些结果与 Black-Scholes 模型进行了比较:

n_sims_test = 1000
# Monte Carlo Path for the test set
alpha = 0.99
paths_test =  monte_carlo_paths(S_0, T, vol, r, seed_test, n_sims_test, \
  timesteps)

我们使用训练好的函数,并在以下代码中比较 Black-Scholes 和 RL 模型:

with tf.Session() as sess:
    model_1.restore(sess, 'model.ckpt')
    #Using the model_1 trained in the section above
    test1_results = model_1.predict(paths_test, np.ones(paths_test.shape[1])*K, \
    alpha, sess)

_,_,_,portfolio_pnl_bs, deltas_bs = black_scholes_hedge_strategy\
(S_0,K, r, vol, T, paths_test, alpha, True)
plt.figure()
_,_,_,portfolio_pnl_rnn, deltas_rnn = test_hedging_strategy\
(test1_results[2], paths_test, K, 2.302974467802428, alpha, True)
plot_deltas(paths_test, deltas_bs, deltas_rnn)
plot_strategy_pnl(portfolio_pnl_bs, portfolio_pnl_rnn)

输出

BS price at t0: 2.3029744678024286
Mean Hedging PnL: -0.0010458505607415178
CVaR Hedging PnL: 1.2447953011695538
RL based BS price at t0: 2.302974467802428
RL based Mean Hedging PnL: -0.0019250998451393934
RL based CVaR Hedging PnL: 1.3832611348053374

mlbf 09in11mlbf 09in12

对于第一个测试集(行权价 100,相同漂移,相同波动率),在 99%的风险厌恶下,结果看起来非常不错。我们看到从第 1 天到第 30 天,来自 Black-Scholes 和基于 RL 的方法的 delta 逐渐收敛。两种策略的 CVaR 相似且数量较低,Black-Scholes 和 RL 的值分别为 1.24 和 1.38。此外,两种策略的波动性相似,如第二张图所示。

5.2.2. 改变资金性质

现在让我们来比较各种策略,当贴现率被定义为行使价格与现货价格的比率时,它发生了变化。为了改变贴现率,我们将行使价格降低了 10%。代码片段类似于前一种情况,并且输出如下:

BS price at t0: 10.07339936955367
Mean Hedging PnL: 0.0007508571761945107
CVaR Hedging PnL: 0.6977526775080665
RL based BS price at t0: 10.073
RL based Mean Hedging PnL: -0.038571546628968216
RL based CVaR Hedging PnL: 3.4732447615593975

随着贴现率的变化,我们看到 RL 策略的损益(PnL)明显差于 Black-Scholes 策略。我们看到两者之间的 delta 在所有天数内有显著偏差。基于 RL 的策略的 CVaR 和波动性要高得多。结果表明,在将模型推广到不同贴现率水平时,我们应该谨慎,并且在将其应用于生产环境之前,应该用多种行使价格训练模型。

mlbf 09in13mlbf 09in14

5.2.3. 改变漂移

现在让我们来比较各种策略,当漂移改变时。为了改变漂移,我们假设股票价格的漂移率为每月 4%,年化为 48%。输出如下所示:

Output

BS price at t0: 2.3029744678024286
Mean Hedging PnL: -0.01723902964827388
CVaR Hedging PnL: 1.2141220199385756
RL based BS price at t0: 2.3029
RL based Mean Hedging PnL: -0.037668804359885316
RL based CVaR Hedging PnL: 1.357201635552361

mlbf 09in15mlbf 09in16

总体而言,改变漂移的结果看起来很不错。结论类似于在风险厌恶改变时的结果,两种方法的 delta 随时间趋于收敛。再次说明,CVaR 在数量上相似,Black-Scholes 产生 1.21 的值,而 RL 产生 1.357 的值。

5.2.4. 转移后的波动性

最后,我们来看看波动性转移的影响。为了改变波动性,我们将其增加了 5%:

Output

BS price at t0: 2.3029744678024286
Mean Hedging PnL: -0.5787493248269506
CVaR Hedging PnL: 2.5583922824407566
RL based BS price at t0: 2.309
RL based Mean Hedging PnL: -0.5735181045192523
RL based CVaR Hedging PnL: 2.835487824499669

mlbf 09in17

查看结果,两种模型的 delta、CVaR 和总体波动性相似。因此,从总体比较来看,基于 RL 的对冲表现与基于 Black-Scholes 的对冲持平。

mlbf 09in18

结论

在这个案例研究中,我们比较了使用 RL 进行看涨期权对冲策略的有效性。即使在修改某些输入参数时,基于 RL 的对冲策略也表现不错。然而,该策略无法推广到不同贴现率水平的策略。这强调了 RL 是一种数据密集型方法的事实,如果打算在各种衍生品中使用该模型,训练模型以适应不同的场景变得更加重要。

尽管我们发现 RL 和传统的 Black-Scholes 策略相比可比,但 RL 方法提供了更高的改进潜力。RL 模型可以通过使用不同的超参数训练更多种类的工具,从而提高性能。探索这两种对冲模型在更多异国衍生品上的比较将是有趣的,考虑到这些方法之间的权衡。

总体上,基于 RL 的方法是模型独立且可扩展的,它为许多经典问题提供了效率提升。

案例研究 3:投资组合分配

正如先前的案例研究所讨论的,最常用的投资组合分配技术——均值方差投资组合优化,存在一些弱点,包括:

  • 预期收益和协方差矩阵的估计误差,由金融回报的不稳定性引起。

  • 不稳定的二次优化严重危及了最终投资组合的优越性。

我们在“案例研究 1:投资组合管理:寻找特征投资组合”和第七章中,以及“案例研究 3:层次风险平价”和第八章中解决了一些这些弱点。在这里,我们从 RL 的角度来解决这个问题。

强化学习算法具有自主决策策略的能力,是在无需连续监督的情况下自动执行投资组合分配的强大模型。自动化投资组合分配中涉及的手动步骤可以证明是极为有用的,特别是对于机器顾问。

在 RL 框架中,我们将投资组合分配视为不仅仅是一步优化问题,而是对具有延迟奖励的投资组合进行连续控制。我们从离散的最优分配转向了连续控制领域,在不断变化的市场环境中,RL 算法可以用来解决复杂和动态的投资组合分配问题。

在这个案例研究中,我们将采用基于 Q 学习的方法和 DQN 来制定一种在一组加密货币中进行最优投资组合分配的策略。总体上,基于 Python 的实现方法与案例研究 1 类似。因此,在本案例研究中跳过了一些重复的部分或者代码解释。

创建基于强化学习的投资组合分配算法的蓝图

1. 问题定义

在为本案例研究定义的强化学习框架中,算法根据投资组合的当前状态执行最优投资组合分配操作。该算法使用深度 Q 学习框架进行训练,模型的组成部分如下:

代理

投资组合经理,机器顾问或个人投资者。

行动

分配和重新平衡投资组合权重。DQN 模型提供了 Q 值,这些 Q 值被转换为投资组合权重。

奖励函数

夏普比率。虽然可以存在多种复杂的奖励函数,提供了利润与风险之间的权衡,例如百分比回报或最大回撤。

状态

状态是基于特定时间窗口的工具的相关矩阵。相关矩阵作为投资组合分配的适当状态变量,因为它包含了不同工具之间关系的信息,并且在执行投资组合分配时非常有用。

环境

加密货币交易所。

这个案例研究中使用的数据集来自Kaggle平台。它包含 2018 年加密货币的每日价格。数据包含一些最流动的加密货币,包括比特币、以太坊、瑞波、莱特币和达世币。

2. 开始——加载数据和 Python 包

2.1. 加载 Python 包

在这一步中加载标准 Python 包。详细信息已在前面的案例研究中介绍过。有关更多详情,请参考本案例研究的 Jupyter 笔记本。

2.2. 加载数据

在这一步中加载获取的数据:

dataset = read_csv('data/crypto_portfolio.csv',index_col=0)

3. 探索性数据分析

3.1. 描述性统计

在本节中,我们将查看数据的描述性统计和数据可视化:

# shape
dataset.shape

Output

(375, 15)
# peek at data
set_option('display.width', 100)
dataset.head(5)

Output

mlbf 09in19

数据总共有 375 行和 15 列。这些列包含了 2018 年 15 种不同加密货币的每日价格。

4. 评估算法和模型

这是强化学习模型开发的关键步骤,我们将定义所有函数和类,并训练算法。

4.1. 代理和加密货币环境脚本

我们有一个Agent类,它包含变量和成员函数,执行 Q-learning。这与案例研究 1 中定义的Agent类类似,还增加了一个函数,用于将来自深度神经网络的 Q 值输出转换为投资组合权重,反之亦然。训练模块通过多个 episode 和批次进行迭代,并保存状态、动作、奖励和下一个状态的信息用于训练。我们跳过详细描述Agent类和训练模块的 Python 代码。读者可以参考本书代码库中的 Jupyter 笔记本了解更多详情。

我们使用名为CryptoEnvironment的类来实现加密货币的仿真环境。仿真环境或gym的概念在强化学习问题中非常普遍。强化学习的一个挑战是缺乏可以进行实验的仿真环境。OpenAI gym是一个工具包,提供各种模拟环境(如 Atari 游戏,2D/3D 物理仿真),因此我们可以训练代理、进行比较或开发新的强化学习算法。此外,它旨在成为强化学习研究的标准化环境和基准。我们在CryptoEnvironment类中引入类似的概念,创建了一个针对加密货币的仿真环境。这个类具有以下关键功能:

getState

此函数根据 is_cov_matrixis_raw_time_series 标志返回状态以及历史回报或原始历史数据。

getReward

此函数根据投资组合权重和回溯期返回投资组合的奖励(即夏普比率)。

class CryptoEnvironment:

    def __init__(self, prices = './data/crypto_portfolio.csv', capital = 1e6):
        self.prices = prices
        self.capital = capital
        self.data = self.load_data()

    def load_data(self):
        data =  pd.read_csv(self.prices)
        try:
            data.index = data['Date']
            data = data.drop(columns = ['Date'])
        except:
            data.index = data['date']
            data = data.drop(columns = ['date'])
        return data

    def preprocess_state(self, state):
        return state

    def get_state(self, t, lookback, is_cov_matrix=True\
       is_raw_time_series=False):

        assert lookback <= t

        decision_making_state = self.data.iloc[t-lookback:t]
        decision_making_state = decision_making_state.pct_change().dropna()

        if is_cov_matrix:
            x = decision_making_state.cov()
            return x
        else:
            if is_raw_time_series:
                decision_making_state = self.data.iloc[t-lookback:t]
            return self.preprocess_state(decision_making_state)

    def get_reward(self, action, action_t, reward_t, alpha = 0.01):

        def local_portfolio(returns, weights):
            weights = np.array(weights)
            rets = returns.mean() # * 252
            covs = returns.cov() # * 252
            P_ret = np.sum(rets * weights)
            P_vol = np.sqrt(np.dot(weights.T, np.dot(covs, weights)))
            P_sharpe = P_ret / P_vol
            return np.array([P_ret, P_vol, P_sharpe])

        data_period = self.data[action_t:reward_t]
        weights = action
        returns = data_period.pct_change().dropna()

        sharpe = local_portfolio(returns, weights)[-1]
        sharpe = np.array([sharpe] * len(self.data.columns))
        ret = (data_period.values[-1] - data_period.values[0]) / \
        data_period.values[0]

        return np.dot(returns, weights), ret

让我们在下一步中探讨 RL 模型的训练。

4.3. 训练数据

首先,我们初始化 Agent 类和 CryptoEnvironment 类。然后,我们为训练目的设置 episodes 数和 batch size。鉴于加密货币的波动性,我们将状态 window size 设置为 180,rebalancing frequency 设置为 90 天:

N_ASSETS = 15
agent = Agent(N_ASSETS)
env = CryptoEnvironment()
window_size = 180
episode_count = 50
batch_size = 32
rebalance_period = 90

图 9-10 深入探讨了用于开发基于 RL 的投资组合配置策略的 DQN 算法的训练。如果我们仔细观察,该图与案例研究 1 中定义的步骤 图 9-8 类似,只是 Q-矩阵奖励函数动作 有细微差别。步骤 1 到 7 描述了训练和 CryptoEnvironment 模块;步骤 8 到 10 显示了 Agent 模块中 replay buffer 函数(即 exeReplay 函数)中发生的事情。

mlbf 0910

图 9-10. 用于组合优化的 DQN 训练

步骤 1 到 6 的详细信息为:

  1. 使用 CryptoEnvironment 模块中定义的辅助函数 getState 获取当前状态。它根据窗口大小返回加密货币的相关矩阵。

  2. 使用 Agent 类的 act 函数获取给定状态的动作。动作是加密货币组合的权重。

  3. 使用 CryptoEnvironment 模块中定义的 getReward 函数为给定动作获取奖励

  4. 使用 getState 函数获取下一个状态。下一个状态的详细信息进一步用于更新 Q 函数的贝尔曼方程。

  5. Agent 对象的内存中保存了状态、下一个状态和动作的细节。这个内存进一步由 exeReply 函数使用。

  6. 检查批处理是否完成。批处理的大小由批处理大小变量定义。如果批处理未完成,则转到下一个时间迭代。如果批处理已完成,则转到 Replay buffer 函数,并通过在步骤 8、9 和 10 中最小化 Q 预测与 Q 目标之间的 MSE 来更新 Q 函数。

如下图所示,代码生成了最终结果以及每轮的两张图表。第一张图显示了随时间累计的总回报,而第二张图显示了组合中每种加密货币的百分比。

输出

第 0/50 轮 epsilon 1.0

mlbf 09in20mlbf 09in21

第 1/50 轮 epsilon 1.0

mlbf 09in22mlbf 09in23

第 48/50 轮 epsilon 1.0

mlbf 09in24mlbf 09in25

第 49/50 轮 epsilon 1.0

mlbf 09in26mlbf 09in27

图表概述了前两个和后两个周期的投资组合配置细节。其他周期的详细信息可以在本书的 GitHub 存储库下的 Jupyter 笔记本中查看。黑线显示了投资组合的表现,虚线灰线显示了基准的表现,基准是加密货币的等权重投资组合。

在第零和第一集的开始,代理没有对其行动后果的预设,因此采取随机化的行动以观察回报,这些回报非常波动。第零集展示了行为表现不稳定的明显例子。第一集显示了更为稳定的运动,但最终表现不及基准。这表明每集累积奖励的波动在训练初期显著。

最后两张图表,即第 48 集和第 49 集,显示代理开始从训练中学习并发现最佳策略。总体回报相对稳定且优于基准。然而,由于短期时间序列和基础加密货币资产的高波动性,整体投资组合权重仍然相当波动。理想情况下,我们可以增加训练周期数量和历史数据长度,以增强训练性能。

让我们看看测试结果。

5. 测试数据

请注意,黑线显示了投资组合的表现,虚线灰线显示了加密货币等权重投资组合的表现:

agent.is_eval = True

actions_equal, actions_rl = [], []
result_equal, result_rl = [], []

for t in range(window_size, len(env.data), rebalance_period):

    date1 = t-rebalance_period
    s_ = env.get_state(t, window_size)
    action = agent.act(s_)

    weighted_returns, reward = env.get_reward(action[0], date1, t)
    weighted_returns_equal, reward_equal = env.get_reward(
        np.ones(agent.portfolio_size) / agent.portfolio_size, date1, t)

    result_equal.append(weighted_returns_equal.tolist())
    actions_equal.append(np.ones(agent.portfolio_size) / agent.portfolio_size)

    result_rl.append(weighted_returns.tolist())
    actions_rl.append(action[0])

result_equal_vis = [item for sublist in result_equal for item in sublist]
result_rl_vis = [item for sublist in result_rl for item in sublist]

plt.figure()
plt.plot(np.array(result_equal_vis).cumsum(), label = 'Benchmark', \
color = 'grey',ls = '--')
plt.plot(np.array(result_rl_vis).cumsum(), label = 'Deep RL portfolio', \
color = 'black',ls = '-')
plt.xlabel('Time Period')
plt.ylabel('Cumulative Returnimage::images\Chapter9-b82b2.png[]')
plt.show()

尽管在初始期间表现不佳,但模型整体表现更好,主要是因为避免了基准投资组合在测试窗口后期经历的急剧下降。回报率看起来非常稳定,可能是由于避开了最具波动性的加密货币。

Output

mlbf 09in28

让我们检查投资组合和基准的回报率、波动率、夏普比率、阿尔法和贝塔:

import statsmodels.api as sm
from statsmodels import regression
def sharpe(R):
    r = np.diff(R)
    sr = r.mean()/r.std() * np.sqrt(252)
    return sr

def print_stats(result, benchmark):

    sharpe_ratio = sharpe(np.array(result).cumsum())
    returns = np.mean(np.array(result))
    volatility = np.std(np.array(result))

    X = benchmark
    y = result
    x = sm.add_constant(X)
    model = regression.linear_model.OLS(y, x).fit()
    alpha = model.params[0]
    beta = model.params[1]

    return np.round(np.array([returns, volatility, sharpe_ratio, \
      alpha, beta]), 4).tolist()
print('EQUAL', print_stats(result_equal_vis, result_equal_vis))
print('RL AGENT', print_stats(result_rl_vis, result_equal_vis))

Output

EQUAL [-0.0013, 0.0468, -0.5016, 0.0, 1.0]
RL AGENT [0.0004, 0.0231, 0.4445, 0.0002, -0.1202]

总体而言,强化学习(RL)投资组合在各方面表现更佳,具有更高的回报率、更高的夏普比率、更低的波动率、略高的阿尔法,并且与基准之间呈现负相关。

结论

在本案例研究中,我们超越了传统的投资组合优化有效边界,直接学习了动态调整投资组合权重的策略。我们通过建立标准化的仿真环境训练了基于 RL 的模型。这种方法简化了训练过程,并可以进一步探索用于通用 RL 模型训练。

训练有素的基于强化学习的模型在测试集中表现优于等权重基准。通过优化超参数或使用更长的时间序列进行训练,可以进一步提升基于强化学习模型的性能。然而,考虑到强化学习模型的高复杂性和低可解释性,在将模型用于实时交易之前,应在不同的时间段和市场周期中进行测试。此外,正如案例研究 1 中讨论的,我们应当仔细选择强化学习的组成部分,例如奖励函数和状态,并确保理解它们对整体模型结果的影响。

本案例研究提供的框架可以使金融从业者以非常灵活和自动化的方式进行投资组合配置和再平衡。

章节总结

奖励最大化是驱动算法交易、投资组合管理、衍生品定价、对冲和交易执行的关键原则之一。在本章中,我们看到当我们使用基于强化学习的方法时,不需要明确定义交易、衍生品对冲或投资组合管理的策略或政策。算法自己确定策略,这可以比其他机器学习技术更简单和更原则性地进行。

在 “案例研究 1: 基于强化学习的交易策略” 中,我们看到强化学习使得算法交易变成了一个简单的游戏,可能涉及或不涉及理解基本信息。在 “案例研究 2: 衍生品对冲” 中,我们探讨了使用强化学习解决传统衍生品对冲问题。这项练习表明,我们可以利用强化学习在衍生品对冲中的高效数值计算来解决传统模型的一些缺点。在 “案例研究 3: 投资组合配置” 中,我们通过学习在不断变化的市场环境中动态改变投资组合权重的策略,进行了投资组合配置,从而进一步实现了投资组合管理流程的自动化。

尽管强化学习存在一些挑战,例如计算成本高、数据密集和缺乏可解释性,但它与某些适合基于奖励最大化的策略框架的金融领域完美契合。强化学习已在有限动作空间(例如围棋、国际象棋和 Atari 游戏中)中取得超越人类的表现。展望未来,随着更多数据的可用性、优化的强化学习算法和更先进的基础设施,强化学习将继续在金融领域中证明其极大的实用价值。

练习

  • 利用案例研究 1 和 2 中提出的思想和概念,基于策略梯度算法实施外汇交易策略。变化关键组件(如奖励函数、状态等)进行此实施。

  • 使用案例研究 2 中提出的概念,实施固定收益衍生品的对冲。

  • 将交易成本纳入案例研究 2,并观察其对整体结果的影响。

  • 基于第三个案例研究中提出的想法,在股票、外汇或固定收益工具组合上实施基于 Q-learning 的投资组合配置策略。

¹ 本章中也将统称强化学习为 RL。

² 欲知更多详情,请查阅理查德·萨顿(Richard Sutton)和安德鲁·巴托(Andrew Barto)的《强化学习导论》(MIT 出版社),或是戴维·银(David Silver)在伦敦大学学院的免费在线RL 课程

³ 查看“强化学习模型”以获取关于基于模型和无模型方法的更多详细信息。

⁴ 最大回撤是在达到新高之前投资组合从峰值到谷底的最大观察损失;它是指定时间段内下行风险的指标。

⁵ 如果 MDP 的状态和动作空间是有限的,则称为有限马尔可夫决策过程。

⁶ 前一节讨论的基于动态规划的 MDP 示例是模型基础算法的一个示例。正如在那里看到的那样,这些算法需要示例奖励和转移概率。

⁷ 有一些模型,如演员-评论家模型,同时利用基于策略和基于价值的方法。

离策略ε-贪婪探索开发 是 RL 中常用的术语,将在其他章节和案例研究中使用。

⁹ 参考第三章以获取更多关于梯度下降的详细信息。

¹⁰ 参考第三章以获取关于 Sigmoid 函数的更多详细信息。

¹¹ Keras-based 深度学习模型的详细实现细节显示在第三章中。

¹² 参考第三章以获取关于线性和 ReLU 激活函数的更多详细信息。

¹³ 预期损失是在尾部情景中投资的预期价值。

第十章:自然语言处理

自然语言处理(NLP)是人工智能的一个子领域,用于帮助计算机理解自然人类语言。大多数 NLP 技术依赖于机器学习来从人类语言中提取含义。当提供文本后,计算机利用算法从每个句子中提取相关的含义,并收集其中的关键数据。NLP 在许多领域以不同形式表现,有许多别名,包括(但不限于)文本分析、文本挖掘、计算语言学和内容分析。

在金融领域,NLP 最早的应用之一是由美国证券交易委员会(SEC)实施的。该组织使用文本挖掘和自然语言处理来检测会计欺诈。NLP 算法扫描和分析法律和其他文件的能力提供了银行和其他金融机构巨大的效率增益,帮助它们符合合规法规并打击欺诈行为。

在投资过程中,揭示投资见解不仅需要金融领域的专业知识,还需要对数据科学和机器学习原理有深厚的掌握。自然语言处理工具可以帮助检测、衡量、预测和预见重要的市场特征和指标,如市场波动性、流动性风险、金融压力、房价和失业率。

新闻一直是投资决策的关键因素。众所周知,公司特定的、宏观经济的和政治新闻强烈影响金融市场。随着技术的进步和市场参与者的日益联结,每日产生的文本数据量和频率将继续迅速增长。即使在今天,每天产生的文本数据量也使得即使是一个庞大的基础研究团队也难以应对。通过 NLP 技术辅助的基础分析现在对解锁专家和大众对市场感受的完整图片至关重要。

在银行和其他组织中,团队的分析师专注于浏览、分析和试图量化从新闻和 SEC 规定的报告中提取的定性数据。在这种背景下,利用 NLP 进行自动化是非常合适的。NLP 可以在分析和解释各种报告和文件时提供深入支持。这减轻了重复的、低价值任务给人类员工带来的压力。它还为本来主观解释提供了客观性和一致性;减少了人为错误带来的错误。NLP 还可以使公司获取见解,用于评估债权人风险或从网络内容中评估与品牌相关的情绪。

随着银行业和金融业中实时聊天软件的普及,基于自然语言处理的聊天机器人是其自然演变。预计将机器人顾问与聊天机器人结合起来,自动化整个财富和投资组合管理过程。

在本章中,我们介绍了三个基于自然语言处理的案例研究,涵盖了算法交易、聊天机器人创建以及文档解释与自动化等应用。这些案例研究遵循了在第二章中呈现的标准化七步模型开发过程。解决基于自然语言处理的问题的关键模型步骤包括数据预处理、特征表示和推理。因此,在本章中概述了这些领域及其相关概念和基于 Python 的示例。

“案例研究 1:基于情感分析的交易策略”展示了情感分析和词嵌入在交易策略中的应用。该案例研究突出了实施基于自然语言处理的交易策略的关键重点。

在“案例研究 2:聊天机器人数字助理”中,我们创建了一个聊天机器人,并展示了自然语言处理如何使聊天机器人理解消息并恰当地回应。我们利用基于 Python 的包和模块,在几行代码中开发了一个聊天机器人。

“案例研究 3:文档摘要”展示了使用基于自然语言处理的主题建模技术来发现文档间的隐藏主题或主题。这个案例研究的目的是演示利用自然语言处理自动总结大量文档以便于组织管理、搜索和推荐。

本章的代码库

本章的 Python 代码包含在在线 GitHub 代码库的第十章 - 自然语言处理文件夹中。对于任何新的基于自然语言处理的案例研究,使用代码库中的通用模板,并修改特定于案例研究的元素。这些模板设计为在云端运行(例如 Kaggle、Google Colab 和 AWS)。

自然语言处理:Python 包

Python 是构建基于自然语言处理专家系统的最佳选择之一,为 Python 程序员提供了大量开源自然语言处理库。这些库和包包含了可直接使用的模块和函数,用于集成复杂的自然语言处理步骤和算法,使得实施快速、简单且高效。

在本节中,我们将描述三个我们认为最有用的基于 Python 的自然语言处理库,并将在本章中使用它们。

NLTK

NLTK 是最著名的 Python 自然语言处理库,已在多个领域取得了令人惊叹的突破。其模块化结构使其非常适合学习和探索自然语言处理的概念。然而,它的功能强大,学习曲线陡峭。

NLTK 可以使用常规安装程序安装。安装 NLTK 后,还需要下载 NLTK Data。NLTK Data 包含了一个用于英语的预训练分词器 punkt,也可以下载:

import nltk
import nltk.data
nltk.download('punkt')

TextBlob

TextBlob 建立在 NLTK 之上。这是一个用于快速原型设计或构建应用程序的最佳库之一,其性能要求最低。TextBlob 通过为 NLTK 提供直观的接口,简化了文本处理。可以使用以下命令导入 TextBlob:

from textblob import TextBlob

spaCy

spaCy 是一个专为快速、简洁和即用型设计的 NLP 库。其理念是为每个目的只提供一种算法(最佳算法)。我们不必做出选择,可以专注于提高工作效率。spaCy 使用自己的管道同时执行多个预处理步骤。我们将在随后的部分进行演示。

spaCy 的模型可以安装为 Python 包,就像任何其他模块一样。要加载模型,请使用 spacy.load 与模型的快捷链接或包名称或数据目录的路径:

import spacy
nlp = spacy.load("en_core_web_lg")

除了这些之外,还有一些其他库,比如 gensim,我们将在本章的一些示例中探索它们。

自然语言处理:理论与概念

正如我们已经确定的,NLP 是人工智能的一个子领域,涉及编程使计算机处理文本数据以获取有用的洞见。所有 NLP 应用程序都经历常见的顺序步骤,包括某种形式的文本数据预处理,并在将其输入统计推断算法之前将文本表示为预测特征。图 10-1 概述了基于 NLP 的应用程序中的主要步骤。

mlbf 1001

图 10-1. 自然语言处理流水线

下一节将回顾这些步骤。如需全面了解该主题,可参考 Steven Bird、Ewan Klein 和 Edward Loper(O’Reilly)的Python 自然语言处理

1. 预处理

在为 NLP 预处理文本数据时通常涉及多个步骤。图 10-1 显示了用于 NLP 预处理步骤的关键组件。这些步骤包括分词、去停用词、词干提取、词形还原、词性标注和命名实体识别。

1.1. 分词

分词是将文本分割成有意义的片段(称为标记)的任务。这些片段可以是单词、标点符号、数字或其他构成句子的特殊字符。一组预定的规则使我们能够有效地将句子转换为标记列表。以下代码片段展示了使用 NLTK 和 TextBlob 包进行示例词分词的样本:

#Text to tokenize
text = "This is a tokenize test"

NLTK 数据包中包含了一个预训练的英文 Punkt 分词器,之前已加载:

from nltk.tokenize import word_tokenize
word_tokenize(text)

输出

['This', 'is', 'a', 'tokenize', 'test']

让我们看看使用 TextBlob 进行标记化:

TextBlob(text).words

输出

WordList(['This', 'is', 'a', 'tokenize', 'test'])

1.2. 停用词移除

有时,在建模中排除那些提供很少价值的极为常见的单词。这些单词被称为停用词。使用 NLTK 库去除停用词的代码如下所示:

text = "S&P and NASDAQ are the two most popular indices in US"

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
stop_words = set(stopwords.words('english'))
text_tokens = word_tokenize(text)
tokens_without_sw= [word for word in text_tokens if not word in stop_words]

print(tokens_without_sw)

输出

['S', '&', 'P', 'NASDAQ', 'two', 'popular', 'indices', 'US']

首先加载语言模型并将其存储在停用词变量中。stopwords.words('english') 是 NLTK 语言模型中默认的英语停用词集合。接下来,我们只需迭代输入文本中的每个单词,如果该单词存在于 NLTK 语言模型的停用词集合中,则将其移除。正如我们所见,像 aremost 这样的停用词已从句子中移除。

1.3. 词干提取

词干提取 是将屈折(或有时是派生)的单词减少为它们的词干、基本形式或根形式(通常是书面单词形式)的过程。例如,如果我们对 Stems, Stemming, Stemmed, 和 Stemitization 进行词干提取,结果将是一个单词:Stem。使用 NLTK 库进行词干提取的代码如下:

text = "It's a Stemming testing"

parsed_text = word_tokenize(text)

# Initialize stemmer.
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('english')

# Stem each word.
[(word, stemmer.stem(word)) for i, word in enumerate(parsed_text)
 if word.lower() != stemmer.stem(parsed_text[i])]

输出

[('Stemming', 'stem'), ('testing', 'test')]

1.4. 词形还原

词形归并 是词干提取的一个轻微变体。两个过程的主要区别在于,词干提取通常会创建不存在的单词,而词形归并产生的是实际的单词形式。词形归并的一个例子是将 run 作为 runningran 等词的基本形式,或者将 bettergood 视为相同的词形。使用 TextBlob 库进行词形归并的代码如下所示:

text = "This world has a lot of faces "

from textblob import Word
parsed_data= TextBlob(text).words
[(word, word.lemmatize()) for i, word in enumerate(parsed_data)
 if word != parsed_data[i].lemmatize()]

输出

[('has', 'ha'), ('faces', 'face')]

1.5. 词性标注

词性标注 是将一个标记分配给它的语法类别(例如动词、名词等)的过程,以便理解它在句子中的角色。词性标记已被用于各种自然语言处理任务,并且非常有用,因为它们提供了一个关于单词在短语、句子或文档中使用方式的语言信号。

在将句子分割为标记后,使用一个标记器或词性标注器将每个标记分配到一个词性类别中。在历史上,使用隐马尔可夫模型(HMM)来创建这样的标注器。近年来,也开始使用人工神经网络。使用 TextBlob 库进行词性标注的代码如下所示:

text = 'Google is looking at buying U.K. startup for $1 billion'
TextBlob(text).tags

输出

[('Google', 'NNP'),
 ('is', 'VBZ'),
 ('looking', 'VBG'),
 ('at', 'IN'),
 ('buying', 'VBG'),
 ('U.K.', 'NNP'),
 ('startup', 'NN'),
 ('for', 'IN'),
 ('1', 'CD'),
 ('billion', 'CD')]

1.6. 命名实体识别

命名实体识别(NER)是数据预处理中的可选下一步,旨在将文本中的命名实体定位并分类到预定义的类别中。这些类别可以包括人名、组织名、地点名、时间表达、数量、货币值或百分比。使用 spaCy 进行的命名实体识别如下所示:

text = 'Google is looking at buying U.K. startup for $1 billion'

for entity in nlp(text).ents:
    print("Entity: ", entity.text)

输出

Entity:  Google
Entity:  U.K.
Entity:  $1 billion

在文本中使用 displacy 模块来可视化命名实体,如 图 10-2 所示,可以极大地帮助加快开发和调试代码以及训练过程:

from spacy import displacy
displacy.render(nlp(text), style="ent", jupyter = True)

mlbf 1002

图 10-2. 命名实体识别输出

1.7. spaCy:一步到位地执行上述所有步骤

所有上述预处理步骤可以在 spaCy 中一步完成。当我们在文本上调用nlp时,spaCy 首先对文本进行标记化以生成Doc对象。然后,Doc在多个不同的步骤中进行处理。这也称为处理管道。默认模型使用的管道由标记器解析器实体识别器组成。每个管道组件返回处理后的Doc,然后传递给下一个组件,如图 10-3 所示。

mlbf 1003

图 10-3. spaCy 处理管道(基于spaCy 网站上的一幅图像)。
Python code text = 'Google is looking at buying U.K. startup for $1 billion'
doc = nlp(text)
pd.DataFrame([[t.text, t.is_stop, t.lemma_, t.pos_]
              for t in doc],
             columns=['Token', 'is_stop_word', 'lemma', 'POS'])

输出

标记 是否停止词 词形 词性
0 Google False Google PROPN
1 True be VERB
2 False look VERB
3 True at ADP
4 购买 False buy VERB
5 英国 False U.K. PROPN
6 startup False startup NOUN
7 对于 True for ADP
8 $ | False | $ SYM
9 1 False 1 NUM
10 十亿 False billion NUM

每个预处理步骤的输出如上表所示。考虑到 spaCy 在单一步骤中执行广泛的自然语言处理任务,它是一个强烈推荐的包。因此,在我们的案例研究中,我们将广泛使用 spaCy。

除了上述预处理步骤外,还有其他经常使用的预处理步骤,例如小写处理非字母数字数据去除,这些步骤取决于数据类型可以执行。例如,从网站上爬取的数据必须进一步清洗,包括去除 HTML 标签。从 PDF 报告中提取的数据必须转换为文本格式。

其他可选的预处理步骤包括依赖分析、核心指代消解、三元组提取和关系提取:

依赖分析

为句子分配句法结构,以理解句子中单词之间的关系。

核心指代消解

连接代表同一实体的标记的过程。在语言中,通常在一句话中引入主语并在随后的句子中用他/她/它代指他们。

三元组提取

在句子结构中记录主语、动词和宾语三元组的过程(可用时)。

关系提取

这是一个更广泛的三元组提取形式,其中实体可以有多种交互。

只有在有助于手头任务时才执行这些额外的步骤。我们将在本章的案例研究中展示这些预处理步骤的示例。

2. 特征表示

大多数与自然语言处理相关的数据,如新闻稿、PDF 报告、社交媒体帖子和音频文件,都是为人类消费而创建的。因此,它们通常以非结构化格式存储,计算机无法直接处理。为了将预处理信息传递给统计推断算法,需要将标记转换为预测特征。模型用于将原始文本嵌入到向量空间中。

特征表示涉及两个方面:

  • 已知单词的词汇表。

  • 已知单词存在的度量。

一些特征表示方法包括:

  • 词袋模型

  • TF-IDF

  • 词嵌入

    • 预训练模型(例如 word2vec,GloVe,spaCy 的词嵌入模型)

    • 自定义深度学习的特征表示¹

让我们更多地了解每种方法。

2.1. 词袋模型—词频统计

在自然语言处理中,从文本中提取特征的常见技术是将文本中出现的所有单词放入一个桶中。这种方法称为词袋模型。它被称为词袋模型,因为它丢失了关于句子结构的任何信息。在这种技术中,我们从一组文本中构建一个单一的矩阵,如图 10-4 所示,其中每一行表示一个标记,每一列表示我们语料库中的一个文档或句子。矩阵的值表示标记出现的次数。

mlbf 1004

图 10-4. 词袋模型

CountVectorizer来自 sklearn,提供了一种简单的方法来对文本文档集合进行标记化,并使用该词汇表对新文档进行编码。fit_transform函数从一个或多个文档中学习词汇,并将每个文档编码为一个词向量:

sentences = [
'The stock price of google jumps on the earning data today',
'Google plunge on China Data!'
]
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
print( vectorizer.fit_transform(sentences).todense() )
print( vectorizer.vocabulary_ )

输出

[[0 1 1 1 1 1 1 0 1 1 2 1]
 [1 1 0 1 0 0 1 1 0 0 0 0]]
{'the': 10, 'stock': 9, 'price': 8, 'of': 5, 'google': 3, 'jumps':\
 4, 'on': 6, 'earning': 2, 'data': 1, 'today': 11, 'plunge': 7,\
 'china': 0}

我们可以看到编码向量的数组版本显示每个单词出现一次,除了the(索引 10),它出现了两次。词频是一个很好的起点,但它们非常基础。简单计数的一个问题是,像the这样的一些词会出现很多次,它们的大量计数在编码向量中意义不大。这些词袋表示是稀疏的,因为词汇量庞大,给定的单词或文档将由大部分零值组成。

2.2. TF-IDF

另一种选择是计算词频,迄今为止最流行的方法是TF-IDF,即词频-逆文档频率

词频

这总结了特定单词在文档中出现的频率。

逆文档频率

这降低了跨文档频繁出现的词的权重。

简单地说,TF-IDF 是一个单词频率分数,试图突出显示更有趣的单词(即在文档内频繁但在文档间不频繁)。TfidfVectorizer 将标记文档、学习词汇表和反文档频率加权,并允许您对新文档进行编码:

from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')
TFIDF = vectorizer.fit_transform(sentences)
print(vectorizer.get_feature_names()[-10:])
print(TFIDF.shape)
print(TFIDF.toarray())

输出

['china', 'data', 'earning', 'google', 'jumps', 'plunge', 'price', 'stock', \
'today']
(2, 9)
[[0\.         0.29017021 0.4078241  0.29017021 0.4078241  0.
  0.4078241  0.4078241  0.4078241 ]
 [0.57615236 0.40993715 0\.         0.40993715 0\.         0.57615236
  0\.         0\.         0\.        ]]

在提供的代码片段中,从文档中学习了一个包含九个单词的词汇表。每个单词在输出向量中被分配了一个唯一的整数索引。句子被编码为一个九元稀疏数组,我们可以通过不同于词汇表中其他单词的值来审查每个单词的最终得分。

2.3. 单词嵌入

单词嵌入 使用稠密向量表示单词和文档。在嵌入中,单词通过稠密向量表示,其中向量表示单词投射到连续向量空间中。单词在向量空间中的位置是从文本中学习的,基于在使用单词时周围的单词。单词在学习的向量空间中的位置称为其嵌入

从文本学习单词嵌入的一些模型包括 word2Vec、spaCy 的预训练单词嵌入模型和 GloVe。除了这些精心设计的方法外,单词嵌入还可以作为深度学习模型的一部分进行学习。这可能是一种较慢的方法,但它会根据特定的训练数据集调整模型。

2.3.1. 预训练模型:通过 spaCy

spaCy 自带文本的向量表示,包括单词、句子和文档的不同级别。底层的向量表示来自于单词嵌入模型,通常生成单词的稠密、多维语义表示(如下例所示)。单词嵌入模型包括 20,000 个唯一的 300 维向量。利用这种向量表示,我们可以计算标记、命名实体、名词短语、句子和文档之间的相似性和不相似性。

在 spaCy 中,单词嵌入是通过首先加载模型然后处理文本来执行的。可以直接使用每个处理过的标记(即单词)的.vector属性访问向量。还可以通过使用向量来简单计算整个句子的平均向量,为基于句子的机器学习模型提供非常便捷的输入:

doc = nlp("Apple orange cats dogs")
print("Vector representation of the sentence for first 10 features: \n", \
doc.vector[0:10])

输出:

Vector representation of the sentence for first 10 features:
 [ -0.30732775 0.22351399 -0.110111   -0.367025   -0.13430001
   0.13790375 -0.24379876 -0.10736975  0.2715925   1.3117325 ]

在输出中显示了预训练模型的前十个特征的句子的向量表示。

2.3.2. 预训练模型:使用 gensim 包的 Word2Vec

这里演示了使用gensim 包的基于 Python 的 word2vec 模型的实现:

from gensim.models import Word2Vec

sentences = [
['The','stock','price', 'of', 'Google', 'increases'],
['Google','plunge',' on','China',' Data!']]

# train model
model = Word2Vec(sentences, min_count=1)

# summarize the loaded model
words = list(model.wv.vocab)
print(words)
print(model['Google'][1:5])

输出

['The', 'stock', 'price', 'of', 'Google', 'increases', 'plunge', ' on', 'China',\
' Data!']
[-1.7868265e-03 -7.6242397e-04  6.0105987e-05  3.5568199e-03
]

上面显示了预训练的 word2vec 模型的前五个特征的句子的向量表示。

3. 推理

与其他人工智能任务一样,由自然语言处理应用程序生成的推理通常需要被翻译成决策以便可执行。推理属于前面章节涵盖的三种机器学习类别之一(即,监督、无监督和强化学习)。虽然所需的推理类型取决于业务问题和训练数据的类型,但最常用的算法是监督和无监督。

在自然语言处理中,最常用的监督方法之一是 Naive Bayes 模型,因为它可以使用简单的假设产生合理的准确性。更复杂的监督方法是使用人工神经网络架构。在过去的几年中,这些架构,如循环神经网络 (RNNs),已经主导了基于自然语言处理的推理。

自然语言处理中的大部分现有文献都集中在监督学习上。因此,无监督学习应用构成了一个相对不太发达的子领域,其中衡量 文档相似性 是最常见的任务之一。在自然语言处理中应用的一种流行的无监督技术是 潜在语义分析 (LSA)。LSA 通过生成与文档和词相关的一组潜在概念来查看一组文档和它们包含的单词之间的关系。LSA 为一种更复杂的方法铺平了道路,这种方法称为 潜在狄利克雷分配 (LDA),在其中,文档被建模为主题的有限混合。这些主题又被建模为词汇表中的单词的有限混合。LDA 已被广泛用于 主题建模 ——这是一个研究日益增长的领域,在该领域中,自然语言处理从业者构建概率生成模型以揭示单词可能的主题归属。

由于我们在前面的章节中已经审查了许多监督和无监督学习模型,所以我们将仅在接下来的章节中详细介绍 Naive Bayes 和 LDA 模型。这些模型在自然语言处理中被广泛使用,并且在前面的章节中没有涉及到。

3.1. 监督学习示例—Naive Bayes

Naive Bayes 是一类基于应用 贝叶斯定理 的算法族,其强(天真)假设是用于预测给定样本类别的每个特征都与其他特征无关。它们是概率分类器,因此将使用贝叶斯定理计算每个类别的概率。输出的将是具有最高概率的类别。

在自然语言处理中,Naive Bayes 方法假定所有单词特征在给定类标签的情况下彼此独立。由于这一简化假设,Naive Bayes 与词袋表示法非常兼容,并且已经证明在许多自然语言处理应用中快速、可靠和准确。此外,尽管有简化的假设,但它在某些情况下与更复杂的分类器相比具有竞争力甚至表现更好。

让我们来看看朴素贝叶斯在情感分析问题中的推理使用。我们拿一个包含两个带情感的句子的数据框架。在下一步中,我们使用CountVectorizer将这些句子转换为特征表示。这些特征和情感被用来训练和测试朴素贝叶斯模型:

sentences = [
'The stock price of google jumps on the earning data today',
'Google plunge on China Data!']
sentiment = (1, 0)
data = pd.DataFrame({'Sentence':sentences,
        'sentiment':sentiment})

# feature extraction
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer().fit(data['Sentence'])
X_train_vectorized = vect.transform(data['Sentence'])

# Running naive bayes model
from sklearn.naive_bayes import MultinomialNB
clfrNB = MultinomialNB(alpha=0.1)
clfrNB.fit(X_train_vectorized, data['sentiment'])

#Testing the model
preds = clfrNB.predict(vect.transform(['Apple price plunge',\
 'Amazon price jumps']))
preds

Output

array([0, 1])

正如我们所见,朴素贝叶斯从这两个句子中很好地训练了模型。该模型为测试句子“Apple price plunge”和“Amazon price jumps”分别给出了情感值零和一,因为训练时使用的句子也具有关键词“plunge”和“jumps”,并对应情感分配。

3.2. 无监督学习示例:LDA

LDA 广泛用于主题建模,因为它倾向于生成人类可以解释的有意义的主题,并为新文档分配主题,并且是可扩展的。它的工作方式首先是做出一个关键假设:文档是通过首先选择主题,然后对于每个主题选择一组单词来生成的。然后算法逆向工程这个过程来找出文档中的主题。

在下面的代码片段中,我们展示了一个用于主题建模的 LDA 实现。我们拿两个句子,并使用CountVectorizer将这些句子转换为特征表示。这些特征和情感被用来训练模型,并生成代表主题的两个较小的矩阵:

sentences = [
'The stock price of google jumps on the earning data today',
'Google plunge on China Data!'
]

#Getting the bag of words
from sklearn.decomposition import LatentDirichletAllocation
vect=CountVectorizer(ngram_range=(1, 1),stop_words='english')
sentences_vec=vect.fit_transform(sentences)

#Running LDA on the bag of words.
from sklearn.feature_extraction.text import CountVectorizer
lda=LatentDirichletAllocation(n_components=3)
lda.fit_transform(sentences_vec)

Output

array([[0.04283242, 0.91209846, 0.04506912],
       [0.06793339, 0.07059533, 0.86147128]])

在本章的第三个案例研究中,我们将使用 LDA 进行主题建模,并详细讨论概念和解释。

回顾一下,为了解决任何基于 NLP 的问题,我们需要遵循预处理、特征提取和推理步骤。现在,让我们深入研究案例研究。

案例研究 1:基于 NLP 和情感分析的交易策略

自然语言处理提供了量化文本的能力。人们可以开始问这样的问题:这篇新闻有多正面或负面?我们如何量化这些词语?

自然语言处理最显著的应用可能是在算法交易中的应用。NLP 提供了一种有效的监控市场情绪的手段。通过将基于 NLP 的情感分析技术应用于新闻文章、报告、社交媒体或其他网络内容,可以有效地确定这些来源的情感积分是正面的还是负面的。情感分数可以用作买入具有正面分数的股票和卖出具有负面分数的股票的定向信号。

基于文本数据的交易策略因非结构化数据量的增加而越来越受欢迎。在这个案例研究中,我们将看看如何使用基于 NLP 的情感来构建交易策略。

本案例研究结合了前几章介绍的概念。本案例研究的整体模型开发步骤与前几个案例研究中的七步模型开发类似,略有修改。

建立基于情感分析的交易策略的蓝图

1. 问题定义

我们的目标是(1)使用 NLP 从新闻标题中提取信息,(2)为该信息分配情感,以及(3)使用情感分析构建交易策略。

本案例研究使用的数据将来自以下来源:

从几家新闻网站的 RSS 源编译的新闻标题数据

为了本研究的目的,我们只关注新闻标题,而不是整篇文章。我们的数据集包含从 2011 年 5 月至 2018 年 12 月约 82,000 个新闻标题。²

Yahoo Finance 网站上的股票数据

本案例研究中使用的股票回报数据来自 Yahoo Finance 的价格数据。

Kaggle

我们将使用带标签的新闻情感数据进行基于分类的情感分析模型。请注意,这些数据可能并不完全适用于本案例,仅用于演示目的。

股市词汇表

词汇表指的是 NLP 系统中包含有关单词或词组的信息(语义、语法)的组件。这是根据微博服务中的股市交流创建的。³

本案例研究的关键步骤详见图 10-5。

mlbf 1005

图 10-5. 基于情感分析的交易策略步骤

一旦我们完成预处理,我们将研究不同的情感分析模型。情感分析步骤的结果用于开发交易策略。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

首先加载的一组库是上述的 NLP 专用库。有关其他库的详细信息,请参阅本案例研究的 Jupyter 笔记本。

from textblob import TextBlob
import spacy
import nltk
import warnings
from nltk.sentiment.vader import SentimentIntensityAnalyzer
nltk.download('vader_lexicon')
nlp = spacy.load("en_core_web_lg")

2.2. 加载数据

在此步骤中,我们从 Yahoo Finance 加载股票价格数据。我们选择了 10 支股票作为本案例研究的对象。这些股票是标准普尔 500 指数中市值最大的股票之一:

tickers = ['AAPL','MSFT','AMZN','GOOG','FB','WMT','JPM','TSLA','NFLX','ADBE']
start = '2010-01-01'
end = '2018-12-31'
df_ticker_return = pd.DataFrame()
for ticker in tickers:
    ticker_yf = yf.Ticker(ticker)
    if df_ticker_return.empty:
        df_ticker_return = ticker_yf.history(start = start, end = end)
        df_ticker_return['ticker']= ticker
    else:
        data_temp = ticker_yf.history(start = start, end = end)
        data_temp['ticker']= ticker
        df_ticker_return = df_ticker_return.append(data_temp)
df_ticker_return.to_csv(r'Data\Step3.2_ReturnData.csv')
df_ticker_return.head(2)

mlbf 10in01

数据包含股票的价格和成交量数据以及它们的代码名称。在下一步中,我们将研究新闻数据。

3. 数据准备

在此步骤中,我们加载并预处理新闻数据,然后将新闻数据与股票回报数据合并。这个合并后的数据集将用于模型开发。

3.1. 预处理新闻数据

新闻数据从新闻 RSS 源下载,并以 JSON 格式保存。不同日期的 JSON 文件存储在一个压缩文件夹中。数据使用标准的网络抓取 Python 包 Beautiful Soup 下载。让我们来看看下载的 JSON 文件内容:

z = zipfile.ZipFile("Data/Raw Headline Data.zip", "r")
testFile=z.namelist()[10]
fileData= z.open(testFile).read()
fileDataSample = json.loads(fileData)['content'][1:500]
fileDataSample

输出

'li class="n-box-item date-title" data-end="1305172799" data-start="1305086400"
data-txt="Tuesday, December 17, 2019">Wednesday, May 11,2011</li><li
class="n-box-item sa-box-item" data-id="76179" data-ts="1305149244"><div
class="media media-overflow-fix"><div class-"media-left"><a class="box-ticker"
href="/symbol/CSCO" target="blank">CSCO</a></div><div class="media-body"<h4
class="media-heading"><a href="/news/76179" sasource="on_the_move_news_
fidelity" target="_blank">Cisco (NASDAQ:CSCO): Pr'

我们可以看到,JSON 格式不适合该算法。我们需要从 JSON 中获取新闻。在此步骤中,正则表达式成为关键部分。正则表达式可以在原始、混乱的文本中找到模式并执行相应的操作。以下函数通过使用 JSON 文件中编码的信息解析 HTML:

def jsonParser(json_data):
    xml_data = json_data['content']

    tree = etree.parse(StringIO(xml_data), parser=etree.HTMLParser())

    headlines = tree.xpath("//h4[contains(@class, 'media-heading')]/a/text()")
    assert len(headlines) == json_data['count']

    main_tickers = list(map(lambda x: x.replace('/symbol/', ''),\
           tree.xpath("//div[contains(@class, 'media-left')]//a/@href")))
    assert len(main_tickers) == json_data['count']
    final_headlines = [''.join(f.xpath('.//text()')) for f in\
           tree.xpath("//div[contains(@class, 'media-body')]/ul/li[1]")]
    if len(final_headlines) == 0:
        final_headlines = [''.join(f.xpath('.//text()')) for f in\
           tree.xpath("//div[contains(@class, 'media-body')]")]
        final_headlines = [f.replace(h, '').split('\xa0')[0].strip()\
                           for f,h in zip (final_headlines, headlines)]
    return main_tickers, final_headlines

让我们看看运行 JSON 解析器后的输出:

jsonParser(json.loads(fileData))[1][1]

Output

'Cisco Systems (NASDAQ:CSCO) falls further into the red on FQ4
 guidance of $0.37-0.39 vs. $0.42 Street consensus. Sales seen flat
 to +2% vs. 8% Street view. CSCO recently -2.1%.'

如我们所见,JSON 解析后的输出转换为更易读的格式。

在评估情感分析模型时,我们还分析情感与后续股票表现之间的关系。为了理解这种关系,我们使用事件回报,即与事件相对应的回报。我们这样做是因为有时新闻报道较晚(即市场参与者已经了解公告),或者在市场关闭后。略微扩大窗口可以确保捕捉事件的本质。事件回报的定义如下:

R t1 + R t + R t+1

其中 R t1 , R t+1 是新闻数据前后的回报,而 R t 是新闻当天的回报(即时间 t)。

让我们从数据中提取事件回报:

#Computing the return
df_ticker_return['ret_curr'] = df_ticker_return['Close'].pct_change()
#Computing the event return
df_ticker_return['eventRet'] = df_ticker_return['ret_curr']\
 + df_ticker_return['ret_curr'].shift(-1) + df_ticker_return['ret_curr'].shift(1)

现在我们已经准备好了所有数据。我们将准备一个合并的数据框架,其中将新闻标题映射到日期,回报(事件回报、当前回报和次日回报)和股票代码。此数据框将用于构建情感分析模型和交易策略:

combinedDataFrame = pd.merge(data_df_news, df_ticker_return, how='left', \
left_on=['date','ticker'], right_on=['date','ticker'])
combinedDataFrame = combinedDataFrame[combinedDataFrame['ticker'].isin(tickers)]
data_df = combinedDataFrame[['ticker','headline','date','eventRet','Close']]
data_df = data_df.dropna()
data_df.head(2)

Output

ticker headline date eventRet Close
5 AMZN Whole Foods (WFMI) –5.2% following a downgrade… 2011-05-02 0.017650 201.19
11 NFLX Netflix (NFLX +1.1%) shares post early gains a… 2011-05-02 –0.013003 33.88

让我们看看数据的整体形状:

print(data_df.shape, data_df.ticker.unique().shape)

Output

(2759, 5) (10,)

在此步骤中,我们准备了一个干净的数据框架,其中包含 10 个股票代码的标题、事件回报、给定日期的回报和未来 10 天的回报,总共有 2759 行数据。让我们在下一步中评估情感分析模型。

4. 评估情感分析模型

在本节中,我们将讨论以下三种计算新闻情绪的方法:

  • 预定义模型—TextBlob 包

  • 调整模型—分类算法和 LSTM

  • 基于金融词汇的模型

让我们逐步进行。

4.1. 预定义模型—TextBlob 包

TextBlob 情绪函数是基于朴素贝叶斯分类算法的预训练模型。该函数将经常出现在电影评论中的形容词映射到从–1 到+1(负面到正面)的情绪极性分数,将句子转换为数值。我们将其应用在所有的头条新闻上。以下是获取新闻文本情感的示例:

text = "Bayer (OTCPK:BAYRY) started the week up 3.5% to €74/share in Frankfurt, \
touching their
highest level in 14 months, after the U.S. government said \
 a $25M glyphosate decision against the
company should be reversed."

TextBlob(text).sentiment.polarity

输出

0.5

该声明的情绪为 0.5。我们将其应用在我们拥有的所有头条新闻上:

data_df['sentiment_textblob'] = [TextBlob(s).sentiment.polarity for s in \
data_df['headline']]

让我们检查散点图的情绪和回报,以检查所有 10 只股票之间的相关性。

mlbf 1006

单个股票(APPL)的图表也显示在以下图表中(有关代码的详细信息,请参见 GitHub 存储库中的 Jupyter 笔记本):

mlbf 10in03

从散点图中我们可以看出,新闻和情绪之间没有很强的关系。回报与情绪之间的相关性是正的(4.27%),这意味着情绪积极的新闻导致积极的回报,这是预期的。然而,相关性并不是很高。即使在整体的散点图上看,我们看到大多数情绪集中在零附近。这引发了一个问题,即电影评论训练的情感评分是否适用于股票价格。sentiment_assessments属性列出了每个标记的基础值,可以帮助我们理解句子整体情绪的原因:

text = "Bayer (OTCPK:BAYRY) started the week up 3.5% to €74/share\
in Frankfurt, touching their highest level in 14 months, after the\
U.S. government said a $25M glyphosate decision against the company\
should be reversed."
TextBlob(text).sentiment_assessments

输出

Sentiment(polarity=0.5, subjectivity=0.5, assessments=[(['touching'], 0.5, 0.5, \
None)])

我们看到这个声明的情绪是 0.5,但似乎是“touching”这个词引起了积极情绪。更直观的词语,如“high”,却没有。这个例子显示了训练数据的上下文对于情感分数的意义是重要的。在进行情感分析之前,有许多预定义的包和函数可供使用,但在使用函数或算法进行情感分析之前,认真并全面了解问题的背景是很重要的。

对于这个案例研究,我们可能需要针对金融新闻进行情感训练。让我们在下一步中看看。

4.2. 监督学习——分类算法和 LSTM

在这一步中,我们根据可用的标记数据开发了一个定制的情绪分析模型。这些标签数据是从Kaggle 网站获取的。

sentiments_data = pd.read_csv(r'Data\LabelledNewsData.csv', \
encoding="ISO-8859-1")
sentiments_data.head(1)

输出

日期时间 标题 股票 情绪
0 1/16/2020 5:25 $MMM 遭遇困境,但可能即将… MMM 0
1 1/11/2020 6:43 Wolfe Research 将 3M $MMM 升级为“同行表现…… MMM 1

数据包括 30 个不同股票的新闻标题,总计 9,470 行,并且有情感标签为零或一。我们使用第六章中呈现的分类模型开发模板执行分类步骤。

为了运行监督学习模型,我们首先需要将新闻标题转换为特征表示。在本练习中,底层的向量表示来自于一个spaCy 词嵌入模型,通常会生成单词的密集的、多维的语义表示(如下例所示)。词嵌入模型包括 20,000 个唯一向量,每个向量有 300 维。我们在前一步骤处理的所有新闻标题上应用此模型:

all_vectors = pd.np.array([pd.np.array([token.vector for token in nlp(s) ]).\
mean(axis=0)*pd.np.ones((300))\
 for s in sentiments_data['headline']])

现在我们已经准备好独立变量,我们将以与第六章讨论类似的方式训练分类模型。我们将情感标签为零或一作为因变量。首先我们将数据分成训练集和测试集,并运行关键的分类模型(即逻辑回归、CART、SVM、随机森林和人工神经网络)。

我们还将包括 LSTM 在内,这是一种基于 RNN 的模型,⁵列入考虑的模型列表中。基于 RNN 的模型在自然语言处理中表现良好,因为它存储当前特征以及相邻特征以进行预测。它根据过去的信息维持记忆,使得模型能够根据长距离特征预测当前输出,并查看整个句子上下文中的单词,而不仅仅是看个别单词。

为了能够将数据输入我们的 LSTM 模型,所有输入文档必须具有相同的长度。我们使用 Keras tokenizer函数对字符串进行标记化,然后使用texts_to_sequences将单词序列化。更多细节可以在Keras 网站上找到。我们将通过截断较长的评论并使用空值(0)填充较短的评论,将最大评论长度限制为max_words。我们可以使用 Keras 中的pad_sequences函数来实现这一点。第三个参数是input_length(设置为 50),即每个评论序列的长度:

### Create sequence
vocabulary_size = 20000
tokenizer = Tokenizer(num_words= vocabulary_size)
tokenizer.fit_on_texts(sentiments_data['headline'])
sequences = tokenizer.texts_to_sequences(sentiments_data['headline'])
X_LSTM = pad_sequences(sequences, maxlen=50)

在以下代码片段中,我们使用 Keras 库基于底层的 LSTM 模型构建了一个人工神经网络分类器。网络从一个嵌入层开始。该层允许系统将每个令牌扩展到一个较大的向量,使得网络能够以有意义的方式表示单词。该层将 20,000 作为第一个参数(即我们词汇的大小),300 作为第二个输入参数(即嵌入的维度)。最后,考虑到这是一个分类问题,输出需要被标记为零或一,KerasClassifier函数被用作 LSTM 模型的包装器以生成二进制(零或一)输出:

from keras.wrappers.scikit_learn import KerasClassifier
def create_model(input_length=50):
    model = Sequential()
    model.add(Embedding(20000, 300, input_length=50))
    model.add(LSTM(100, dropout=0.2, recurrent_dropout=0.2))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(loss='binary_crossentropy', optimizer='adam', \
    metrics=['accuracy'])
    return model
model_LSTM = KerasClassifier(build_fn=create_model, epochs=3, verbose=1, \
  validation_split=0.4)
model_LSTM.fit(X_train_LSTM, Y_train_LSTM)

所有机器学习模型的比较如下:

mlbf 10in04

如预期,LSTM 模型在测试集中表现最佳(准确率为 96.7%),相比其他模型。ANN 的性能,训练集准确率为 99%,测试集准确率为 93.8%,与基于 LSTM 的模型相媲美。随机森林(RF)、支持向量机(SVM)和逻辑回归(LR)的性能也很合理。CART 和 KNN 的表现不如其他模型。CART 显示出严重的过拟合。让我们使用 LSTM 模型来计算数据中的情感值。

4.3. 无监督——基于金融词汇表的模型

在这个案例研究中,我们将 VADER 词汇表与适用于股市微博服务的词汇和情感进行更新:

词典

专门用于分析情感的特殊词典或词汇表。大多数词典都列有带有与之相关的分数的正面和负面 极性 词语。使用各种技术,如词语的位置、周围的词语、上下文、词类和短语,为我们想要计算情感的文档分配分数。在聚合这些分数之后,我们得到最终的情感:

VADER(情感推理的价值感知词典)

NLTK 包中包含的预建情感分析模型。它可以给出文本样本的正负极性分数以及情感强度。这是基于规则的,并且在很大程度上依赖于人工标注的文本。这些是根据它们的语义取向(正面或负面)标记的单词或任何文本形式的通信。

这个词汇资源是利用各种统计措施和大量来自 StockTwits 的标记消息自动创建的,StockTwits 是一个专为投资者、交易员和企业家分享想法而设计的社交媒体平台。⁶ 这些情感分数介于-1 和 1 之间,与 TextBlob 的情感分析类似。在以下代码片段中,我们基于金融情感来训练模型:

# stock market lexicon
sia = SentimentIntensityAnalyzer()
stock_lex = pd.read_csv('Data/lexicon_data/stock_lex.csv')
stock_lex['sentiment'] = (stock_lex['Aff_Score'] + stock_lex['Neg_Score'])/2
stock_lex = dict(zip(stock_lex.Item, stock_lex.sentiment))
stock_lex = {k:v for k,v in stock_lex.items() if len(k.split(' '))==1}
stock_lex_scaled = {}
for k, v in stock_lex.items():
    if v > 0:
        stock_lex_scaled[k] = v / max(stock_lex.values()) * 4
    else:
        stock_lex_scaled[k] = v / min(stock_lex.values()) * -4

final_lex = {}
final_lex.update(stock_lex_scaled)
final_lex.update(sia.lexicon)
sia.lexicon = final_lex

让我们来检查一条新闻的情感:

text = "AAPL is trading higher after reporting its October sales\
rose 12.6% M/M. It has seen a 20%+ jump in orders"
sia.polarity_scores(text)['compound']

Output

0.4535

我们根据数据集中的所有新闻标题获取情感值:

vader_sentiments = pd.np.array([sia.polarity_scores(s)['compound']\
 for s in data_df['headline']])

让我们来看看基于词典的方法计算整个数据集的回报和情感之间的关系。

mlbf 10in05

针对低情感分数的高回报实例不多,但数据可能不太清晰。我们将在下一节更深入地比较不同类型的情感分析。

4.4. 探索性数据分析和比较

在本节中,我们比较了使用上述不同技术计算的情感。让我们看一下样本标题和三种不同方法的情感分析,然后进行视觉分析:

股票代码 头条 情感 _textblob 情感 _LSTM 情感 _ 词汇
4620 台积电 台积电(TSM +1.8%)在报告其 10 月销售额环比上升 12.6%后交易更高。《DigiTimes》补充道,TSMC 从高通、英伟达、联发科和联咏等公司那里看到订单增长超过 20%。这些数字表明,尽管 12 月通常疲软,但 TSMC 可能会超过其第四季度的指导,而芯片需求可能正在通过库存调整后稳定下来。(此前)(联电销售) 0.036667 1 0.5478

查看其中一个标题,这个句子的情感是积极的。然而,TextBlob 的情感结果较小,表明情感更为中性。这再次指向之前的假设,即基于电影情感训练的模型可能不适合股票情感。基于分类的模型正确指出情感是积极的,但是它是二进制的。Sentiment_lex给出了一个更直观的输出,情感显著为积极。

让我们审视来自不同方法的所有情感与回报的相关性:

mlbf 10in06

所有情感与回报都有正相关关系,这是直觉和预期的。从词汇学方法来看,所有股票的事件回报可以通过这种方法预测得最好。请记住,这种方法利用了金融术语来建模。基于 LSTM 的方法性能也优于 TextBlob 方法,但与基于词汇的方法相比稍逊一筹。

让我们来看看股票级别的方法论表现。我们选择了市值最高的几个股票进行分析:

mlbf 10in07

查看图表,从词汇学方法论来看,所有股票代码中的相关性最高,这与之前分析的结论一致。这意味着可以最好地使用词汇学方法预测回报。基于 TextBlob 的情感分析在某些情况下显示出不直观的结果,比如在 JPM 的情况下。

让我们来看看 AMZN 和 GOOG 的词汇学与 TextBlob 方法的散点图。由于二进制情感在散点图中没有意义,我们将 LSTM 方法搁置一边:

mlbf 10in08mlbf 10in09

左侧基于词汇的情感显示出情感与收益之间的正相关关系。一些具有最高收益的点与最积极的新闻相关联。此外,与 TextBlob 相比,基于词汇的散点图更加均匀分布。TextBlob 的情感集中在零附近,可能是因为该模型无法很好地分类金融情感。对于交易策略,我们将使用基于词汇的情感,因为根据本节的分析,这些是最合适的选择。基于 LSTM 的情感也不错,但它们被标记为零或一。更为细粒度的基于词汇的情感更受青睐。

5. 模型评估—构建交易策略

情感数据可以通过多种方式用于构建交易策略。情感可以作为独立信号用于决定买入、卖出或持有操作。情感评分或词向量还可以用于预测股票的收益或价格。该预测可以用于构建交易策略。

在本节中,我们展示了一种交易策略,根据以下方法买入或卖出股票:

  • 当情感评分变化(当前情感评分/前一情感评分)大于 0.5 时购买股票。当情感评分变化小于-0.5 时卖出股票。此处使用的情感评分基于前一步骤中计算的基于词汇的情感。

  • 除了情感之外,在做出买卖决策时我们还使用了移动平均(基于过去 15 天的数据)。

  • 交易(即买入或卖出)以 100 股为单位。用于交易的初始金额设定为$100,000。

根据策略的表现,可以调整策略阈值、手数和初始资本。

5.1. 设置策略

为了设置交易策略,我们使用backtrader,这是一个便捷的基于 Python 的框架,用于实现和回测交易策略。Backtrader 允许我们编写可重用的交易策略、指标和分析器,而无需花费时间建设基础设施。我们使用backtrader 文档中的快速入门代码作为基础,并将其调整为基于情感的交易策略。

以下代码片段总结了策略的买入和卖出逻辑。详细的实现请参考本案例研究的 Jupyter 笔记本:

# buy if current close more than simple moving average (sma)
# AND sentiment increased by >= 0.5
if self.dataclose[0] > self.sma[0] and self.sentiment - prev_sentiment >= 0.5:
  self.order = self.buy()

# sell if current close less than simple moving average(sma)
# AND sentiment decreased by >= 0.5
if self.dataclose[0] < self.sma[0] and self.sentiment - prev_sentiment <= -0.5:
  self.order = self.sell()

5.2. 单个股票的结果

首先,我们在 GOOG 上运行我们的策略并查看结果:

ticker = 'GOOG'
run_strategy(ticker, start = '2012-01-01', end = '2018-12-12')

输出显示了某些日子的交易日志和最终收益:

Output

Starting Portfolio Value: 100000.00
2013-01-10, Previous Sentiment 0.08, New Sentiment 0.80 BUY CREATE, 369.36
2014-07-17, Previous Sentiment 0.73, New Sentiment -0.22 SELL CREATE, 572.16
2014-07-18, OPERATION PROFIT, GROSS 22177.00, NET 22177.00
2014-07-18, Previous Sentiment -0.22, New Sentiment 0.77 BUY CREATE, 593.45
2014-09-12, Previous Sentiment 0.66, New Sentiment -0.05 SELL CREATE, 574.04
2014-09-15, OPERATION PROFIT, GROSS -1876.00, NET -1876.00
2015-07-17, Previous Sentiment 0.01, New Sentiment 0.90 BUY CREATE, 672.93
.
.
.
2018-12-11, Ending Value 149719.00

我们分析了由 backtrader 包生成的下图中的回测结果。详细的图表版本请参考本案例研究的 Jupyter 笔记本。

mlbf 10in10

结果显示总体利润为$49,719。图表是由 backtrader 包生成的典型图表⁷,分为四个面板:

顶部面板

顶部面板是现金价值观察者。它在回测运行期间跟踪现金和总投资组合价值。在这次运行中,我们以$100,000 起步,以$149,719 结束。

第二面板

此面板是交易观察者。它显示每笔交易的实现利润/损失。交易定义为开仓和将头寸归零(直接或从多头到空头或空头到多头)。从这个面板来看,对于策略来说,有八次交易中的五次是盈利的。

第三面板

此面板是买卖观察者。它指示了买入和卖出操作的发生位置。总的来说,我们看到买入行为发生在股价上涨时,而卖出行为发生在股价开始下跌时。

底部面板

此面板显示情绪得分,介于-1 和 1 之间。

现在我们选择了其中一天(2015-07-17),当买入行动被触发,并分析了该天和前一天的谷歌新闻:

GOOG_ticker= data_df[data_df['ticker'].isin([ticker])]
New= list(GOOG_ticker[GOOG_ticker['date'] ==  '2015-07-17']['headline'])
Old= list(GOOG_ticker[GOOG_ticker['date'] ==  '2015-07-16']['headline'])
print("Current News:",New,"\n\n","Previous News:", Old)

Output

Current News: ["Axiom Securities has upgraded Google (GOOG +13.4%, GOOGL +14.8%)
to Buy following the company's Q2 beat and investor-pleasing comments about
spending discipline, potential capital returns, and YouTube/mobile growth. MKM
has launched coverage at Buy, and plenty of other firms have hiked their targets.
Google's market cap is now above $450B."]

Previous News: ["While Google's (GOOG, GOOGL) Q2 revenue slightly missed
estimates when factoring traffic acquisitions costs (TAC), its ex-TAC revenue of
$14.35B was slightly above a $14.3B consensus. The reason: TAC fell to 21% of ad
revenue from Q1's 22% and Q2 2014's 23%. That also, of course, helped EPS beat
estimates.", 'Google (NASDAQ:GOOG): QC2 EPS of $6.99 beats by $0.28.']

显然,选定日的新闻提到了谷歌的升级,这是一则积极的新闻。前一天提到了收入低于预期,这是一则负面新闻。因此,在选定的日子,新闻情绪发生了显著变化,导致交易算法触发了买入行动。

接下来,我们对 FB 运行策略:

ticker = 'FB'
run_strategy(ticker, start = '2012-01-01', end = '2018-12-12')

Output

Start Portfolio value: 100000.00
Final Portfolio Value: 108041.00
Profit: 8041.00

mlbf 10in12

策略的回测结果的详细信息如下:

顶部面板

现金价值面板显示总体利润为$8,041。

第二面板

交易观察者面板显示,七次交易中有六次是盈利的。

第三面板

买卖观察者显示,总的来说,买入(卖出)行为发生在股价上涨(下跌)时。

底部面板

它显示了在 2013 年至 2014 年期间对于 FB 的积极情绪较高的数量。

5.3. 多只股票的结果

在上一步中,我们对各个股票执行了交易策略。在这里,我们对我们计算了情绪的所有 10 支股票进行了运行:

results_tickers = {}
for ticker in tickers:
    results_tickers[ticker] = run_strategy(ticker, start = '2012-01-01', \
    end = '2018-12-12')
pd.DataFrame.from_dict(results_tickers).set_index(\
  [pd.Index(["PerUnitStartPrice", StrategyProfit'])])

Output

mlbf 10in13

该策略表现相当不错,并为所有股票带来了总体利润。如前所述,买入和卖出行为的执行是以 100 手为单位进行的。因此,使用的美元金额与股票价格成比例。我们看到 AMZN 和 GOOG 的名义利润最高,这主要归因于对这些股票的高金额投资,考虑到它们的高股价。除了总体利润之外,还可以使用几个其他指标,如夏普比率和最大回撤,来分析绩效。

5.4. 变化策略时间段

在前面的分析中,我们使用了从 2011 年到 2018 年的时间段进行了回测。在这一步骤中,为了进一步分析我们策略的有效性,我们变化了回测的时间段并分析了结果。首先,我们在 2012 年到 2014 年之间为所有股票运行了该策略:

results_tickers = {}
for ticker in tickers:
    results_tickers[ticker] = run_strategy(ticker, start = '2012-01-01', \
    end = '2014-12-31')

Output

mlbf 10in14

该策略使得除了 AMZN 和 WMT 之外的所有股票总体上获利。现在我们在 2016 年到 2018 年之间运行该策略:

results_tickers = {}
for ticker in tickers:
    results_tickers[ticker] = run_strategy(ticker, start = '2016-01-01', \
    end = '2018-12-31')

Output

mlbf 10in15

我们看到情感驱动策略在所有股票中的表现良好,除了 AAPL 外,我们可以得出它在不同时间段表现相当不错的结论。该策略可以通过修改交易规则或手数大小进行调整。还可以使用其他指标来理解策略的表现。情感还可以与其他特征一起使用,如相关变量和技术指标用于预测。

结论

在这个案例研究中,我们探讨了将非结构化数据转换为结构化数据,并使用自然语言处理工具进行分析和预测的各种方法。我们展示了三种不同的方法,包括使用深度学习模型开发计算情绪的模型。我们对这些模型进行了比较,并得出结论:在训练情绪分析模型时,使用领域特定的词汇表是其中一个最重要的步骤。

我们还使用了 spaCy 的预训练英语模型将句子转换为情感,并将情感用作开发交易策略的信号。初步结果表明,基于金融词汇的情感模型训练可能是一个可行的交易策略模型。可以通过使用更复杂的预训练情感分析模型(如 Google 的 BERT)或开源平台上其他预训练的自然语言处理模型来进一步改进这一模型。现有的 NLP 库填补了一些预处理和编码步骤,使我们能够专注于推理步骤。

通过包括更多相关变量、技术指标或使用更复杂的预处理步骤和基于更相关的金融文本数据的模型,我们可以进一步完善基于情感的交易策略。

案例研究 2:聊天机器人数字助理

Chatbots 是能够用自然语言与用户进行对话的计算机程序。它们能够理解用户的意图,并根据组织的业务规则和数据发送响应。这些聊天机器人使用深度学习和自然语言处理(NLP)来处理语言,从而能够理解人类的语音。

越来越多的聊天机器人正在金融服务领域得到应用。银行业的机器人使消费者能够查询余额、转账、支付账单等。经纪业的机器人使消费者能够找到投资选项、进行投资并跟踪余额。客户支持机器人提供即时响应,显著提高客户满意度。新闻机器人提供个性化的当前事件信息,企业机器人使员工能够查询休假余额、提交费用、检查库存余额并批准交易。除了自动化协助客户和员工的过程外,聊天机器人还可以帮助金融机构获取有关客户的信息。这种机器人现象有潜力在金融部门的许多领域引发广泛的颠覆。

根据机器人的编程方式,我们可以将聊天机器人分为两种变体:

基于规则

这种类型的聊天机器人根据规则进行训练。这些聊天机器人不通过交互学习,并且有时无法回答超出定义规则的复杂查询。

自学习

这种类型的聊天机器人依赖于机器学习和人工智能技术与用户交谈。自学习聊天机器人进一步分为检索型生成型

检索型

这些聊天机器人被训练来从有限的预定义响应集中排名最佳响应。

生成型

这些聊天机器人不是通过预定义的响应构建的。相反,它们是使用大量先前对话来训练的。它们需要大量的对话数据来进行训练。

在这个案例研究中,我们将原型化一个可以回答财务问题的自学习聊天机器人。

使用自然语言处理创建自定义聊天机器人的蓝图

1. 问题定义

这个案例研究的目标是建立一个基本的基于自然语言处理的对话式聊天机器人原型。这种聊天机器人的主要目的是帮助用户检索特定公司的财务比率。这些聊天机器人旨在快速获取有关股票或工具的详细信息,以帮助用户进行交易决策。

除了检索财务比率,聊天机器人还可以与用户进行随意的对话,执行基本的数学计算,并为训练使用的问题提供答案。我们打算使用 Python 包和函数来创建聊天机器人,并定制聊天机器人架构的多个组件,以适应我们的需求。

在这个案例研究中创建的聊天机器人原型旨在理解用户输入和意图,并检索他们正在寻找的信息。这是一个小型原型,可以改进为在银行业务、经纪业务或客户支持中用作信息检索机器人。

2. 入门—加载库

对于这个案例研究,我们将使用两个基于文本的库:spaCy 和 ChatterBot。spaCy 已经被介绍过;ChatterBot 是一个用于创建简单聊天机器人的 Python 库,只需很少的编程即可。

一个未经训练的 ChatterBot 实例开始时不具有沟通的知识。每次用户输入语句时,库都会保存输入和响应文本。随着 ChatterBot 收到更多的输入,它能够提供的响应数量和这些响应的准确性会增加。程序通过搜索与输入最接近的已知语句来选择响应。然后,根据每个响应被与机器人交流的人们发出的频率,返回对该语句的最有可能的响应。

2.1. 加载库

我们使用以下 Python 代码导入 spaCy:

import spacy #Custom NER model.
from spacy.util import minibatch, compounding

ChatterBot 库具有模块 LogicAdapterChatterBotCorpusTrainerListTrainer。我们的机器人使用这些模块构建响应用户查询的响应。我们从导入以下开始:

from chatterbot import ChatBot
from chatterbot.logic import LogicAdapter
from chatterbot.trainers import ChatterBotCorpusTrainer
from chatterbot.trainers import ListTrainer

此练习中使用的其他库如下:

import random
from itertools import product

在我们转向定制的聊天机器人之前,让我们使用 ChatterBot 包的默认特性开发一个聊天机器人。

3. 训练默认聊天机器人

ChatterBot 和许多其他聊天机器人包都带有一个数据实用程序模块,可用于训练聊天机器人。以下是我们将要使用的 ChatterBot 组件:

逻辑适配器

逻辑适配器确定了 ChatterBot 如何选择响应给定输入语句的逻辑。您可以输入任意数量的逻辑适配器供您的机器人使用。在下面的示例中,我们使用了两个内置适配器:BestMatch,它返回最佳已知响应,以及 MathematicalEvaluation,它执行数学运算。

预处理器

ChatterBot 的预处理器是简单的函数,它们在逻辑适配器处理语句之前修改聊天机器人接收到的输入语句。预处理器可以定制以执行不同的预处理步骤,比如分词和词形还原,以便得到干净且处理过的数据。在下面的示例中,使用了清理空格的默认预处理器 clean_whitespace

语料库训练

ChatterBot 自带一个语料库数据和实用程序模块,使得快速训练机器人进行通信变得容易。我们使用已有的语料库 english, english.greetingsenglish.conversations 来训练聊天机器人。

列表训练

就像语料库训练一样,我们使用 ListTrainer 训练聊天机器人可以用于训练的对话。在下面的示例中,我们使用了一些示例命令来训练聊天机器人。可以使用大量的对话数据来训练聊天机器人。

chatB = ChatBot("Trader",
                preprocessors=['chatterbot.preprocessors.clean_whitespace'],
                logic_adapters=['chatterbot.logic.BestMatch',
                                'chatterbot.logic.MathematicalEvaluation'])

# Corpus Training
trainerCorpus = ChatterBotCorpusTrainer(chatB)

# Train based on English Corpus
trainerCorpus.train(
    "chatterbot.corpus.english"
)
# Train based on english greetings corpus
trainerCorpus.train("chatterbot.corpus.english.greetings")

# Train based on the english conversations corpus
trainerCorpus.train("chatterbot.corpus.english.conversations")

trainerConversation = ListTrainer(chatB)
# Train based on conversations

# List training
trainerConversation.train([
    'Help!',
    'Please go to google.com',
    'What is Bitcoin?',
    'It is a decentralized digital currency'
])

# You can train with a second list of data to add response variations
trainerConversation.train([
    'What is Bitcoin?',
    'Bitcoin is a cryptocurrency.'
])

一旦聊天机器人被训练好,我们可以通过以下对话来测试训练好的聊天机器人:

>Hi
How are you doing?

>I am doing well.
That is good to hear

>What is 78964 plus 5970
78964 plus 5970 = 84934

>what is a dollar
dollar: unit of currency in the united states.

>What is Bitcoin?
It is a decentralized digital currency

>Help!
Please go to google.com

>Tell me a joke
Did you hear the one about the mountain goats in the andes? It was "ba a a a d".

>What is Bitcoin?
Bitcoin is a cryptocurrency.

在这个例子中,我们看到一个聊天机器人对输入做出直观回复。前两个回复是由于对英语问候语和英语对话语料库的训练。此外,对Tell me a jokewhat is a dollar的回复是由于对英语语料库的训练。第四行中的计算是聊天机器人在MathematicalEvaluation逻辑适配器上训练的结果。对Help!What is Bitcoin?的回复是定制列表训练器的结果。此外,我们看到对What is Bitcoin?有两种不同的回复,这是因为我们使用列表训练器进行了训练。

接下来,我们将创建一个设计用于使用定制逻辑适配器给出财务比率的聊天机器人。

4. 数据准备:定制聊天机器人

我们希望我们的聊天机器人能够识别和分组微妙不同的查询。例如,有人可能想询问关于公司苹果公司,只是简单地称之为苹果,而我们希望将其映射到一个股票代码——在本例中为AAPL。通过以下方式使用字典构建通常用于引用公司的短语:

companies = {
    'AAPL':  ['Apple', 'Apple Inc'],
    'BAC': ['BAML', 'BofA', 'Bank of America'],
    'C': ['Citi', 'Citibank'],
    'DAL': ['Delta', 'Delta Airlines']
}

同样,我们希望为财务比率建立映射:

ratios = {
    'return-on-equity-ttm': ['ROE', 'Return on Equity'],
    'cash-from-operations-quarterly': ['CFO', 'Cash Flow from Operations'],
    'pe-ratio-ttm': ['PE', 'Price to equity', 'pe ratio'],
    'revenue-ttm': ['Sales', 'Revenue'],
}

这个字典的键可以用来映射到内部系统或 API。最后,我们希望用户能够以多种格式请求短语。说Get me the [RATIO] for [COMPANY]应该与What is the [RATIO] for [COMPANY]?类似对待。我们通过以下方式构建这些句子模板供我们的模型训练:

string_templates = ['Get me the {ratio} for {company}',
                   'What is the {ratio} for {company}?',
                   'Tell me the {ratio} for {company}',
                  ]

4.1. 数据构造

我们通过创建反向 字典来开始构建我们的模型:

companies_rev = {}
for k, v in companies.items():
  for ve in v:
      companies_rev[ve] = k
  ratios_rev = {}
  for k, v in ratios.items():
      		for ve in v:
          			ratios_rev[ve] = k
  companies_list = list(companies_rev.keys())
  ratios_list = list(ratios_rev.keys())

接下来,我们为我们的模型创建样本语句。我们构建一个函数,该函数给出一个随机的句子结构,询问一个随机公司的随机财务比率。我们将在 spaCy 框架中创建一个自定义命名实体识别模型。这需要训练模型以在样本句子中捕捉单词或短语。为了训练 spaCy 模型,我们需要提供一个示例,例如(Get me the ROE for Citi,{"entities":[(11, 14,RATIO),(19, 23,COMPANY)]})

4.2. 训练数据

训练示例的第一部分是句子。第二部分是一个包含实体及其标签起始和结束索引的字典:

N_training_samples = 100
def get_training_sample(string_templates, ratios_list, companies_list):
  string_template=string_templates[random.randint(0, len(string_templates)-1)]
      ratio = ratios_list[random.randint(0, len(ratios_list)-1)]
      company = companies_list[random.randint(0, len(companies_list)-1)]
      sent = string_template.format(ratio=ratio,company=company)
      ents = {"entities": [(sent.index(ratio), sent.index(ratio)+\
  len(ratio), 'RATIO'),
                   	(sent.index(company), sent.index(company)+len(company), \
                    'COMPANY')]}
       return (sent, ents)

让我们定义训练数据:

TRAIN_DATA = [
get_training_sample(string_templates, ratios_list, companies_list) \
for i in range(N_training_samples)
]

5. 模型创建和训练

一旦我们有了训练数据,我们在 spaCy 中构建一个 空白 模型。spaCy 的模型是统计的,它们做出的每个决定 — 例如分配哪个词性标签,或者一个词是否是命名实体 — 都是一个预测。这个预测基于模型在训练过程中看到的示例。要训练一个模型,首先需要训练数据 — 文本示例和您希望模型预测的标签。这可以是词性标签、命名实体或任何其他信息。然后,模型将展示未标记的文本并做出预测。因为我们知道正确答案,所以我们可以以 损失函数的误差梯度 的形式给模型反馈其预测的差异。这计算出训练示例与期望输出之间的差异,如 图 10-6 所示。差异越大,梯度越显著,我们就需要对模型进行更多更新。

mlbf 1007

图 10-6. 基于机器学习的 spaCy 训练
nlp = spacy.blank("en")

接下来,我们为我们的模型创建一个 NER 流水线:

ner = nlp.create_pipe("ner")
nlp.add_pipe(ner)

然后,我们添加我们使用的训练标签:

ner.add_label('RATIO')
ner.add_label('COMPANY')

5.1. 模型优化函数

现在我们开始优化我们的模型:

optimizer = nlp.begin_training()
move_names = list(ner.move_names)
pipe_exceptions = ["ner", "trf_wordpiecer", "trf_tok2vec"]
other_pipes = [pipe for pipe in nlp.pipe_names if pipe not in pipe_exceptions]
with nlp.disable_pipes(*other_pipes):  # only train NER
     sizes = compounding(1.0, 4.0, 1.001)
     # batch up the examples using spaCy's minibatch
     for itn in range(30):
        random.shuffle(TRAIN_DATA)
        batches = minibatch(TRAIN_DATA, size=sizes)
        losses = {}
        for batch in batches:
           texts, annotations = zip(*batch)
           nlp.update(texts, annotations, sgd=optimizer,
           drop=0.35, losses=losses)
        print("Losses", losses)

训练 NER 模型类似于更新每个标记的权重。使用良好的优化器是最重要的步骤。我们提供给 spaCy 的训练数据越多,它在识别广义结果方面的表现就会越好。

5.2. 自定义逻辑适配器

接下来,我们构建我们的自定义逻辑适配器:

from chatterbot.conversation import Statement
class FinancialRatioAdapter(LogicAdapter):
    	def __init__(self, chatbot, **kwargs):
        		super(FinancialRatioAdapter, self).__init__(chatbot, **kwargs)
    	def process(self, statement, additional_response_selection_parameters):
      		user_input = statement.text
      		doc = nlp(user_input)
      		company = None
      		ratio = None
      		confidence = 0
      		# We need exactly 1 company and one ratio
      		if len(doc.ents) == 2:
      			for ent in doc.ents:
          			if ent.label_ == "RATIO":
              				ratio = ent.text
              			if ratio in ratios_rev:
                  				confidence += 0.5
          			if ent.label_ == "COMPANY":
              				company = ent.text
              				if company in companies_rev:
                  					confidence += 0.5
      		if confidence > 0.99: (its found a ratio and company)
      			outtext = '''https://www.zacks.com/stock/chart\
 /{comanpy}/fundamental/{ratio} '''.format(ratio=ratios_rev[ratio]\
                  , company=companies_rev[company])
      			confidence = 1
      		else:
      			outtext = 'Sorry! Could not figure out what the user wants'
      			confidence = 0
      		output_statement = Statement(text=outtext)
      		output_statement.confidence = confidence
      		return output_statement

使用这个自定义逻辑适配器,我们的聊天机器人将接受每个输入语句,并尝试使用我们的 NER 模型识别 RATIO 和/或 COMPANY。如果模型确切地找到一个 COMPANY 和一个 RATIO,它将构建一个 URL 来指导用户。

5.3. 模型使用 — 训练和测试

现在我们开始使用以下导入使用我们的聊天机器人:

from chatterbot import ChatBot

我们通过将上述创建的 FinancialRatioAdapter 逻辑适配器添加到聊天机器人中构建我们的聊天机器人。虽然下面的代码片段仅显示我们添加了 FinancialRatioAdapter,但请注意之前训练过程中使用的其他逻辑适配器、列表和语料库也都包含在内。有关更多详情,请参阅案例研究的 Jupyter 笔记本。

chatbot = ChatBot(
    			"My ChatterBot",
    			logic_adapters=[
        'financial_ratio_adapter.FinancialRatioAdapter'
    ]
)

现在我们使用以下语句测试我们的聊天机器人:

converse()

>What is ROE for Citibank?
https://www.zacks.com/stock/chart/C/fundamental/return-on-equity-ttm

>Tell me PE for Delta?
https://www.zacks.com/stock/chart/DAL/fundamental/pe-ratio-ttm

>What is Bitcoin?
It is a decentralized digital currency

>Help!
Please go to google.com

>What is 786940 plus 75869
786940 plus 75869 = 862809

>Do you like dogs?
Sorry! Could not figure out what the user wants

如上所示,我们聊天机器人的自定义逻辑适配器可以在句子中找到 RATIO 和/或 COMPANY,使用我们的 NLP 模型。如果检测到一个确切的配对,模型将构建一个 URL 来引导用户获取答案。此外,其他逻辑适配器(如数学评估)也能如预期地工作。

结论

总的来说,这个案例研究介绍了聊天机器人开发的多个方面。

在 Python 中使用 ChatterBot 库可以构建一个简单的接口来解决用户输入。要训练一个空模型,必须有大量的训练数据集。在这个案例研究中,我们查看了可用的模式,并使用它们生成训练样本。获得正确数量的训练数据通常是构建自定义聊天机器人的最困难的部分。

本案例研究是一个演示项目,每个组件都可以进行重大改进,以扩展到各种任务。可以添加额外的预处理步骤以获得更清洁的数据。为了从我们的机器人中生成输入问题的响应,逻辑可以进一步优化,以包含更好的相似度测量和嵌入。聊天机器人可以使用更先进的 ML 技术在更大的数据集上进行训练。一系列自定义逻辑适配器可以用于构建更复杂的 ChatterBot。这可以推广到更有趣的任务,如从数据库检索信息或向用户请求更多输入。

案例研究 3:文档摘要

文档摘要指的是在文档中选择最重要的观点和主题,并以全面的方式进行整理。如前所述,银行及其他金融服务机构的分析师们仔细研究、分析并试图量化来自新闻、报告和文件的定性数据。利用自然语言处理进行文档摘要可以在分析和解释过程中提供深入的支持。当应用于财务文件(如收益报告和财经新闻)时,文档摘要能够帮助分析师快速提取内容中的关键主题和市场信号。文档摘要还可用于改善报告工作,并能够及时更新关键事项。

在自然语言处理中,主题模型(如本章节早些时候介绍的 LDA)是最常用的工具,用于提取复杂而可解释的文本特征。这些模型能够从大量文档中浮出关键的主题、主题或信号,并且可以有效用于文档摘要。

使用自然语言处理进行文档摘要的蓝图

1. 问题定义

本案例研究的目标是利用 LDA 有效地从上市公司的收益电话会议记录中发现共同的主题。与其他方法相比,这种技术的核心优势在于不需要先验的主题知识。

2. 入门 - 加载数据和 Python 包

2.1. 加载 Python 包

对于本案例研究,我们将从 PDF 中提取文本。因此,Python 库pdf-miner用于将 PDF 文件处理为文本格式。还加载了用于特征提取和主题建模的库。可视化库将在案例研究的后续加载:

PDF 转换库

from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
import re
from io import StringIO

特征提取和主题建模库

from sklearn.feature_extraction.text import CountVectorizer,TfidfVectorizer
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS

其他库

import numpy as np
import pandas as pd

3. 数据准备

下面定义的convert_pdf_to_txt函数从 PDF 文档中提取除图片外的所有字符。该函数简单地接收 PDF 文档,提取文档中的所有字符,并将提取的文本输出为 Python 字符串列表:

def convert_pdf_to_txt(path):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, laparams=laparams)
    fp = open(path, 'rb')
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    password = ""
    maxpages = 0
    caching = True
    pagenos=set()

    for page in PDFPage.get_pages(fp, pagenos,\
            maxpages=maxpages, password=password,caching=caching,\
            check_extractable=True):
        interpreter.process_page(page)

    text = retstr.getvalue()

    fp.close()
    device.close()
    retstr.close()
    return text

在接下来的步骤中,使用上述函数将 PDF 转换为文本,并保存在文本文件中:

Document=convert_pdf_to_txt('10K.pdf')
f=open('Finance10k.txt','w')
f.write(Document)
f.close()
with open('Finance10k.txt') as f:
    clean_cont = f.read().splitlines()

让我们来看一下原始文档:

clean_cont[1:15]

输出

[' ',
 '',
 'SECURITIES AND EXCHANGE COMMISSION',
 ' ',
 '',
 'Washington, D.C. 20549',
 ' ',
 '',
 '\xa0',
 'FORM ',
 '\xa0',
 '',
 'QUARTERLY REPORT PURSUANT TO SECTION 13 OR 15(d) OF',
 ' ']

从 PDF 文档提取的文本包含需要移除的无信息字符。这些字符会降低模型的效果,因为它们提供了不必要的计数比率。以下函数使用一系列正则表达式(regex)搜索以及列表推导来将无信息字符替换为空格:

doc=[i.replace('\xe2\x80\x9c', '') for i in clean_cont ]
doc=[i.replace('\xe2\x80\x9d', '') for i in doc ]
doc=[i.replace('\xe2\x80\x99s', '') for i in doc ]

docs = [x for x in doc if x != ' ']
docss = [x for x in docs if x != '']
financedoc=[re.sub("[^a-zA-Z]+", " ", s) for s in docss]

4. 模型构建和训练

使用 sklearn 模块中的CountVectorizer函数进行最小参数调整,将干净的文档表示为文档术语矩阵。这是因为我们的建模需要将字符串表示为整数。CountVectorizer显示了在去除停用词后单词在列表中出现的次数。文档术语矩阵被格式化为 Pandas 数据框以便检查数据集。该数据框显示了文档中每个术语的词频统计:

vect=CountVectorizer(ngram_range=(1, 1),stop_words='english')
fin=vect.fit_transform(financedoc)

在下一步中,文档术语矩阵将作为输入数据用于 LDA 算法进行主题建模。该算法被拟合以隔离五个不同的主题上下文,如下代码所示。此值可以根据建模的粒度调整:

lda=LatentDirichletAllocation(n_components=5)
lda.fit_transform(fin)
lda_dtf=lda.fit_transform(fin)
sorting=np.argsort(lda.components_)[:, ::-1]
features=np.array(vect.get_feature_names())

以下代码使用mglearn库显示每个特定主题模型中的前 10 个词:

import mglearn
mglearn.tools.print_topics(topics=range(5), feature_names=features,
sorting=sorting, topics_per_chunk=5, n_words=10)

输出

topic 1       topic 2       topic 3       topic 4       topic 5
--------      --------      --------      --------      --------
assets        quarter       loans         securities    value
balance       million       mortgage      rate          total
losses        risk          loan          investment    income
credit        capital       commercial    contracts     net
period        months        total         credit        fair
derivatives   financial     real          market        billion
liabilities   management    estate        federal       equity
derivative    billion       securities    stock         september
allowance     ended         consumer      debt          december
average       september     backed        sales         table

预计表格中的每个主题都代表一个更广泛的主题。然而,由于我们仅对单一文档进行了模型训练,因此各主题间的主题可能并不十分明显。

在更广泛的主题方面,主题 2 讨论了与资产估值相关的季度、月份和货币单位。主题 3 揭示了关于房地产收入、抵押贷款及相关工具的信息。主题 5 也涉及与资产估值相关的术语。第一个主题涉及资产负债表项目和衍生品。主题 4 与主题 1 略有相似,涉及投资过程中的词汇。

就整体主题而言,主题 2 和主题 5 与其他主题有很大的区别。基于前几个词,主题 1 和主题 4 可能也存在某种相似性。在下一节中,我们将尝试使用 Python 库pyLDAvis来理解这些主题之间的区分。

5. 主题可视化

在本节中,我们使用不同的技术来可视化主题。

5.1. 主题可视化

主题可视化有助于通过人类判断评估主题质量。pyLDAvis是一个库,显示了主题之间的全局关系,同时通过检查与每个主题最相关的术语以及与术语相关的主题来促进其语义评估。它还解决了文档中频繁使用的术语倾向于主导定义主题的单词分布的挑战。

下面使用pyLDAvis_库来展示主题模型:

from __future__ import  print_function
import pyLDAvis
import pyLDAvis.sklearn

zit=pyLDAvis.sklearn.prepare(lda,fin,vect)
pyLDAvis.show(zit)

输出

mlbf 10in16

我们注意到主题 2 和主题 5 相距甚远。这与上面章节中我们从整体主题和词汇列表中观察到的情况相符。主题 1 和主题 4 非常接近,这验证了我们上面的观察。这些相近的主题如果需要的话可以更详细地分析和合并。右侧图表中显示的每个主题下的术语的相关性也可以用来理解它们的差异。主题 3 和主题 4 也比较接近,尽管主题 3 与其他主题相距较远。

5.2. 词云

在这一步骤中,生成了一个词云,用于记录文档中最频繁出现的术语:

#Loading the additional packages for word cloud
from os import path
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from wordcloud import WordCloud,STOPWORDS

#Loading the document and generating the word cloud
d = path.dirname(__name__)
text = open(path.join(d, 'Finance10k.txt')).read()

stopwords = set(STOPWORDS)
wc = WordCloud(background_color="black", max_words=2000, stopwords=stopwords)
wc.generate(text)

plt.figure(figsize=(16,13))
plt.imshow(wc, interpolation='bilinear')
plt.axis("off")
plt.show()

输出

mlbf 10in17

词云与主题建模的结果基本一致,如贷款房地产第三季度公平价值等重复出现的词更大更粗。

通过整合上述步骤中的信息,我们可以列出文档所代表的主题列表。在我们的案例研究文档中,我们发现像第三季度前九个月九个月这样的词频繁出现。在词汇列表中,有几个与资产负债表项目相关的主题。因此,该文档可能是一个第三季度的财务资产负债表,包含该季度的所有信用和资产价值。

结论

在这个案例研究中,我们探讨了主题建模在理解文档内容中的应用。我们展示了 LDA 模型的使用,该模型提取出合理的主题,并允许我们以自动化的方式对大量文本进行高层次理解。

我们从 PDF 格式的文档中提取了文本并进行了进一步的数据预处理。结果与可视化一起表明,这些主题直观且意义深远。

总体而言,案例研究展示了机器学习和自然语言处理如何在诸如投资分析、资产建模、风险管理和监管合规性等多个领域中应用,以总结文档、新闻和报告,从而显著减少手动处理。有了这种快速访问和验证相关信息的能力,分析师可以提供更全面和信息丰富的报告,供管理层基于其决策。

章节总结

自然语言处理领域取得了显著进展,导致了将继续改变金融机构运营方式的技术的出现。在近期,我们可能会看到基于自然语言处理的技术在金融的不同领域中的增加,包括资产管理、风险管理和流程自动化。金融机构采用和理解自然语言处理方法及相关基础设施非常重要。

总的来说,本章通过案例研究中呈现的 Python、机器学习和金融概念可以作为金融领域中任何其他基于自然语言处理的问题的蓝图。

练习

  • 利用案例研究 1 中的概念,使用基于自然语言处理的技术开发一个利用 Twitter 数据的交易策略。

  • 在案例研究 1 中,使用 word2vec 词嵌入方法生成词向量,并将其纳入交易策略中。

  • 利用案例研究 2 中的概念,测试一些更多的逻辑适配器到聊天机器人。

  • 利用案例研究 3 中的概念,对一组金融新闻文章进行主题建模,并提取当天的关键主题。

¹ 本章案例研究 1 中构建了一个定制的基于深度学习的特征表示模型。

² 这些新闻可以通过 Python 中的简单网页抓取程序下载,使用诸如 Beautiful Soup 之类的包。读者应该与网站沟通或遵循其服务条款,以便将新闻用于商业目的。

³ 这个词典的来源是 Nuno Oliveira、Paulo Cortez 和 Nelson Areal 的文章,“利用微博数据和统计量获取股票市场情绪词典”,决策支持系统 85(2016 年 3 月):62–73。

⁴ 我们还在随后的章节中对金融数据进行情感分析模型的训练,并将结果与 TextBlob 模型进行比较。

⁵ 更多关于 RNN 模型的详细信息,请参考第五章。

⁶ 这个词典的来源是 Nuno Oliveira、Paulo Cortez 和 Nelson Areal 的文章,“利用微博数据和统计量获取股票市场情绪词典”,决策支持系统 85(2016 年 3 月):62–73。

⁷ 更多关于 backtrader 的图表和面板的绘制部分的详细信息,请参考backtrader 网站

posted @ 2024-06-17 18:15  绝不原创的飞龙  阅读(383)  评论(0)    收藏  举报