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

282 阅读53分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

第九章:强化学习

激励驱动着几乎所有事物,金融也不例外。人类不是从数百万个标记示例中学习,而是经常从我们与行动相关联的积极或消极经验中学习。从经验和相关奖励或惩罚中学习是强化学习(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 ) = ∑ a∈A 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′;θ i–1 ) – Q (s,a;θ i ) 2 ]

损失函数本质上是一个均方误差(MSE)函数,其中r + γ max a ′ Q (s ′ ,a′;θ i–1 )表示目标值,而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 t–1 ) ,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,确实是一个强劲的结果,但在第一个周期中,我们经历了总体损失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 δ t–1 ( S t – S t–1 ) – ∑ t=1 T C t

其中

  • Z T 是到期日看涨期权的收益。

  • δ t–1 ( S t – S t–1 ) 是第t天对冲工具的现金流,其中δ是对冲,S t是第t天的现货价格。

  • C t 是第t时间点的交易成本,可能是常数或与对冲规模成比例。

方程中的各个组成部分是现金流的组成部分。然而,在设计奖励函数时,最好考虑到任何头寸带来的风险。我们使用 CVaR 作为风险评估度量。CVaR 量化了尾部风险的数量,并且是对置信水平α 的expected shortfall(风险厌恶参数)¹³。现在奖励函数修改如下:

V T = f ( – Z T + ∑ t=1 T δ t–1 ( S t – S t–1 ) – ∑ 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'])

输出

标记是否停止词词形词性
0GoogleFalseGooglePROPN
1TruebeVERB
2FalselookVERB
3TrueatADP
4购买FalsebuyVERB
5英国FalseU.K.PROPN
6startupFalsestartupNOUN
7对于TrueforADP
8$False$SYM
91False1NUM
10十亿FalsebillionNUM

每个预处理步骤的输出如上表所示。考虑到 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 t–1 + R t + R t+1

其中 R t–1 , 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

tickerheadlinedateeventRetClose
5AMZNWhole Foods (WFMI) –5.2% following a downgrade…2011-05-020.017650201.19
11NFLXNetflix (NFLX +1.1%) shares post early gains a…2011-05-02–0.01300333.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)

输出

日期时间标题股票情绪
01/16/2020 5:25$MMM 遭遇困境,但可能即将…MMM0
11/11/2020 6:43Wolfe Research 将 3M $MMM 升级为“同行表现……MMM1

数据包括 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.03666710.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起步,以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 网站