TensorFlow 金融数据之旅
我将展示几个复杂程度不同的模型,从简单的回归到 LSTM 和策略网络。这个系列可以作为 TensorFlow 或深度学习的教育资源、参考辅助工具,或者是如何将深度学习技术应用于常规深度学习领域(视觉、自然语言)之外的问题的创意来源。
并非所有示例都能正常运行。有些过于简单,甚至不能被视为可行的交易策略,仅出于教育目的展示。其他一些以笔记本形式呈现的示例,训练时间不够充分。也许租用一些 GPU 时间后会有更好的表现,我将此作为读者的练习。希望这个项目能激发一些人尝试将深度学习技术用于更有趣的问题。如果您有兴趣了解更多或对添加和改进有建议,请联系我。
算法复杂度逐渐提高,并在过程中引入新概念:
简单回归 [(笔记本)][1]
我们将过去 100 天的价格回归到下一天的价格,训练方程 y = Wx + b 中的 W 和 b,其中 y 是下一天的价格,x 是一个 100 维向量,W 是 100x1 矩阵,b 是 1x1 矩阵。我们运行梯度下降算法以最小化预测价格和实际次日价格之间的均方误差。恭喜你,你通过了高中统计学。但希望这个简单而朴素的例子能帮助演示张量图的概念,同时也展示了一个极度过拟合的绝佳例子。
多符号简单回归 [(笔记本)][2]
一旦我们引入多个符号,事情就变得更有趣了。如何最好地建模我们最终的投资策略?我们开始意识到,我们的模型仅通过预测实际价格变动来模糊地暗示一种策略(投资行为)。这种隐含的策略很简单:如果预测价格变动为正则买入,为负则卖出。但这听起来一点都不现实。我们买入多少?即使我们非常小心地避免过拟合,优化这个模型是否真能产生符合我们目标的结果?我们实际上还没有明确定义我们的目标,但对于不熟悉投资指标的人来说,常见目标包括:
如果市场很容易理解,我们能准确预测第二天的回报,那就无关紧要了。我们隐含的策略将符合一些目标(但不包括多空股票),这个策略就可行。现实是我们的模型无法准确预测,我们的策略也永远不会完美。我们最好的情况总是只比亏损多赢一点。在这些微小的差距下运作时,明确考虑策略变得更加重要,因此我们转向"基于策略"的深度学习。
策略梯度训练 [(笔记本)][3]
我们的策略仍然简单。我们将为投资组合中的每个符号选择一个头寸:做多/中性/做空。但现在,我们不再让对未来回报的估计来指导我们的决策,而是训练我们的网络来选择最佳头寸。因此,我们不再有"隐含"的策略,而是"明确"的策略,并直接进行"训练"。 尽管在这种情况下策略很简单,但训练它涉及的内容更多。在编写这段代码时,我尽力参考了 Andrej Karpathy 关于强化学习的精彩文章。值得阅读他的解释,但我会尽力总结我做了什么。
我们更新了回归引擎,使其输出 y 成为一个维度为 [batch_size, number_positions x number_symbols] 的向量(每个符号有做多、做空、中性三个桶)。
对于投资组合中的每个符号,我们采样三个头寸桶的概率分布来获得策略决策(做多/做空/中性头寸),我们将决策({-1,0,1}中的元素)乘以目标值得到该符号的日回报。然后我们将所有符号的这个值相加得到完整的日回报。由于我们实际上是批量输入的(稍后会详细介绍),我们还可以获得总回报和夏普比率等其他指标。正如 Karpathy 指出的,我们只对采样的头寸的梯度感兴趣,所以我们从输出中选择适当的列并将它们组合成一个新的张量。
这部分代码有点棘手,原因有几个。首先,我们需要循环并隔离每个符号,因为我需要每个符号一个头寸。我还使用了多项概率分布,所以我需要对这些值进行 softmax 处理。Softmax 会推动值使其总和为 1,因此可以表示概率分布。用伪代码表示:softmax[i, j] = exp(logits[i, j]) / sum(exp(logits[i]))
。
for i in range(len(symbol_list)):
symbol_probs = y[:,i*num_positions:(i+1)*num_positions]
symbol_probs_softmax = tf.nn.softmax(symbol_probs)
接下来,我们对该概率分布进行采样。尽管由于 TensorFlow 的 multinomial 函数,代码是一个不错的单行语句,但该函数是不可微的,这意味着我们在反向传播过程中无法"穿过"这一步。我们通过从采样得到的列索引中减去 1 来简单计算头寸向量,从而得到 {-1,0,1} 中的元素。
pos = {}
for i in range(len(symbol_list)):
# 之前的隔离操作
# ...
sample = tf.multinomial(tf.log(symbol_probs_softmax), 1)
pos[i] = tf.reshape(sample, [-1]) - 1 # 选择(-1,0,1)
然后我们将该仓位乘以每天的目标(未来收益)。这就得到了我们的收益。它看起来已经像一个成本函数,但请记住它是不可微的。
symbol_returns = {}
for i in range(len(symbol_list)):
# 之前的 ISOLATE 和 SAMPLE
# ...
symbol_returns[i] = tf.mul(tf.cast(pos[i], float32), y_[:,i])
最后,我们从概率分布中分离出相关的列(我们在样本中选择的那一列)。这个想法并不难,但代码有点棘手,记住我们一次要处理一整批输出。这一步必须是可微的,因为我们将使用这个张量来计算梯度。不幸的是,TensorFlow 仍在开发一个可以自动完成这一步的函数,这里是相关讨论。我实际上认为我的解决方案是最好的,并在讨论中提出了建议,但我真的不是高效计算方面的专家。如果有人想到更有效的解决方案或者 TensorFlow 完成了他们的方案,请告诉我。
relevant_target_column = {}
for i in range(len(symbol_list)):
# ...
# ...
sample_mask = tf.reshape(tf.one_hot(sample, 3), [-1,3])
relevant_target_column[i] = tf.reduce_sum(symbol_probs_softmax * sample_mask,1)
这里是所有这些内容的综合:
# 遍历符号,每次取一个符号的桶
pos = {}
symbol_returns = {}
relevant_target_column = {}
for i in range(len(symbol_list)):
# 分离与该符号相关的桶,并获得softmax
symbol_probs = y[:,i*num_positions:(i+1)*num_positions]
symbol_probs_softmax = tf.nn.softmax(symbol_probs)
# 采样概率来选择我们策略的行动
sample = tf.multinomial(tf.log(symbol_probs_softmax), 1)
pos[i] = tf.reshape(sample, [-1]) - 1 # 选择(-1,0,1)
# 通过将策略(采取的仓位)乘以当天的目标收益(y_)来获得收益
symbol_returns[i] = tf.mul(tf.cast(pos[i], float32), y_[:,i])
# 分离选定策略的输出概率(用于计算梯度)
sample_mask = tf.reshape(tf.one_hot(sample, 3), [-1,3])
relevant_target_column[i] = tf.reduce_sum(symbol_probs_softmax * sample_mask,1)
现在我们得到了一个张量,包含了每个符号和每天所选择(采样)行动的回归概率。我们还有一些性能指标可以选择,如每日和总收益,但它们是不可微的,因为我们对概率进行了采样,所以我们不能简单地"梯度下降最大化"利润...遗憾的是。相反,我们找到第一个表格(我们选择/采样的概率)和相同形状的全1张量之间的sigmoid交叉熵(一种距离函数)。我们得到一个相同大小的交叉熵表(符号数乘以批量大小)。这基本上等同于说,对于我做出的每个决定,我如何做更多我已经在做的事。
training_target_cols = tf.concat(1, [tf.reshape(t, [-1,1]) for t in relevant_target_column.values()])
ones = tf.ones_like(training_target_cols)
gradient = tf.nn.sigmoid_cross_entropy_with_logits(training_target_cols, ones)
现在我们不一定想要做更多我们正在做的事,但其反面肯定是做更少,这是有用的。我们将该张量乘以我们的适应度函数(每日或总收益),并使用梯度下降优化器来最小化成本。所以你看?如果适应度函数是负的,它将训练回归的权重不去做它刚刚做的事。这是一个很酷的想法,可以应用于许多更有趣的问题。我在笔记本中给出了一些例子,说明可以应用哪些不同的适应度函数,我认为通过查看这些例子可以更好地解释。这里是代码:
cost = tf.mul(gradient , returns) #returns 是 symbol_returns 的某种重塑版本(来自上面)
optimizer = tf.train.GradientDescentOptimizer(0.1).minimize(cost)
随机梯度下降 [(笔记本)][4]
正如你在笔记本中看到的,当我们根据整个数据集的收益来评分时,策略梯度训练效果并不好,但当它使用每天的收益或每天每个符号的仓位时,训练效果很好。这是有道理的,如果我们只取几年的总收益,而且略微为正,然后我们告诉机器要做更多这样的事。这几乎不会起作用,因为很多决策实际上是亏损的。目前的问题设置需要分解成更小的单元。幸运的是,有一些数学证明表明这是合法的,甚至更快。太好了!
随机梯度下降基本上就是将数据分成更小的批次,并对每个批次进行梯度下降。相对于整个数据集的成本函数,它的梯度会稍微不那么准确,但由于你可以用更小的批次更快地迭代,所以可以运行更多次。SGD 可能还有一些我没有提到的其他优势,所以你可以阅读维基百科或听 Andrew Ng 讲解。或者就直接使用它,因为它有效而且更快。如果你要进行维基百科学习狂欢,不妨也了解一下动量和 Adagrad,这些只是一些变体。后两者只对做更大项目的人有用。如果你正在进行一个大项目,而你的每小时两美元的 AWS GPU 实例太慢,那么你肯定应该使用它们(而不是阅读这个入门教程)。在更高层面上,优化器调优和整体效率的问题已经得到了充分的研究。
多重采样 [(笔记本)][5]
由于我们正在对策略进行采样,我们可以重复采样以获得更好的计算结果。Karpathy的文章很好地总结了背后的数学原理,而这篇论文也值得一读。这个概念虽然直观简单,但要使数学计算正确并理顺张量图却非常复杂。一旦问题变得更深入,人们就会意识到对numpy的掌握和对线性代数的扎实理解对于tensorflow来说非常重要。
多重采样增加了一个有用的计算优势,使网络能够更高效地训练。结果已经令人印象深刻。使用少于75天的批次,并且仅对该时间段内的总回报进行训练,我们就能够"过拟合"我们的网络。请记住,我们所做的只是告诉网络在表现良好时多做一些,表现不佳时少做一些!当然,我们离拥有任何有价值的样本外结果还很远,但那是因为我们仍在使用线性回归。
到现在为止,你可能要么在想"这家伙到底懂不懂深度学习?我一个神经网络都没看到!",要么你完全忘记了我们仍在使用16岁学生在数学课上学习的同一线性回归。好吧,我们接下来会讲到神经网络,但我想在谈及神经网络之前先讨论其他内容,以展示在提到神经网络之前tensorflow可以用于多少方面,并展示深度学习中存在多少与神经网络无关的重要数学知识。Tensorflow使神经网络变得如此简单,以至于你几乎没有注意到它们是整个画面的一部分,如果你对交叉熵、策略梯度等背后的数学原理没有扎实的理解,那就绝对不值得被它们的数学所困扰。它们可能甚至会分散人们对真正难点的注意力。也许我应该尝试让回归模型来玩乒乓球游戏,这样每个人就会停止谈论神经网络,开始讨论策略学习了...
神经网络(笔记本)
所以我们终于讲到这个主题了。这里是用神经网络实现的相同内容。这是最简单的网络类型,但仍然非常强大。比我们那个微不足道的回归模型强大得多,因为它在其他层(基本上就是单独的回归)之间有非线性(RELU层)。显然,一系列线性回归仍然是线性的。但如果我们在层之间加入非线性,那么我们的网络就可以做任何事情。这就是让人们对神经网络感到兴奋的原因,因为经过良好训练后,它们可以容纳巨量的信息。幸运的是,我们刚刚在步骤1-5中学习了一些很酷的训练方法。现在加入网络变得非常容易。我真的没有改变什么,除了变量,方程基本上仍然是y = Wx + b,只是W是非线性的。
我之所以这么晚才介绍网络,是因为它们有时候很难调整。选择正确的网络大小和合适的训练步骤可能很困难,有时从简单开始直到你把所有的细节都安排好会更有帮助。
在步骤3-5中,我们花了很多时间研究训练步骤的技巧,这是目前广泛研究的领域,可能与算法交易最相关。现在我们开始展示预测引擎中使用的一些技术(之前是回归,现在是神经网络)。我相信这是一个研究得更多的领域,TensorFlow在这方面装备得更好。许多人将我们将要学习的神经网络类型描述为细胞或"乐高积木"。只要你知道它的功能,你就不需要过多考虑它是如何工作的。如果你注意到了,这就是我对神经网络所做的。还有更多值得学习的内容,但当你实际构建它时,你不会像考虑输入/输出和中间的黑匣子那样多地考虑RELU层。或者至少我是这样...图像处理领域有一群人会深入研究网络内部,做一些非常酷的事情。
正则化和模块化(笔记本)
根据维基百科,"正则化是引入额外信息以解决不适定问题或防止过拟合的过程。"对我们来说,它是任何用于减少过拟合的技术。最简单却最重要的例子之一是提前停止,即在评估性能没有显著提高时结束梯度下降。这个想法是,一旦模型停止改进,它就会开始过拟合训练数据。大多数过拟合问题只用这种技术就可以避免。
我们还有更多的过拟合问题。金融数据非常嘈杂,信号微弱但非常复杂。市场是零和游戏,已经有大量非常聪明的人获得巨额报酬来开发交易策略。可以学习到许多不同的模式,但我们可以假设所有简单的模式都已被发现(因此已从市场中交易出去)。此外,市场结构和特性随时间变化,所以即使我们为过去五年找到了一个很棒的模式,一个月后它可能就不管用了。
因此,我们需要更多策略来防止过拟合。一种策略叫做L2正则化。基本上,我们通过添加权重的L2范数1/2(||W||^2_2)乘以一个常数B(用来确定我们想要正则化的程度)来惩罚网络拥有非常大的权重。它允许我们控制模型复杂度的水平。 另一种方法称为dropout正则化,这是由Geoffrey Hinton开发的技术。通过在每次训练迭代中随机省略一半的神经元来避免过拟合。这听起来有些古怪,但其理念是避免神经网络中特征的共同适应,使得识别某一组特征不会暗示另一组特征,这种情况在许多过度训练的网络中很常见。这样,每个神经元都会学会检测普遍有用的特征。在验证和正常使用时,不会丢弃神经元,以便使用所有信息。这种技术使模型更加稳健,避免过拟合,并本质上将你的网络转变为达成共识的集成模型。TensorFlow的dropout层包括了在验证时安全集成所需的缩放。
由于我们希望同一模型有两种不同的配置——一种启用dropout,另一种不启用——我们现在将履行我们长期以来重构代码的职责。虽然代码是在单个笔记本中呈现的,但请记住,我本质上是在进行模块化。我不会声称我的代码组织得最好,但我试图与我观察到其他人在开源项目中使用的最佳实践保持一致。从现在开始,我们将保持这种结构,因为它显然优于我之前使用的组织方式。
LSTM [(笔记本)][8]
我最喜欢的神经网络,也是真正迈入深度学习的垫脚石是长短期记忆网络,即LSTM。Colah写了一篇非常清晰的LSTM解释,没有什么能替代阅读他的文章。简要描述这个设置,你将数据一个时间步一个时间步地输入LSTM单元。每个时间步,单元不仅接收新的输入,还接收上一个时间步的输出和所谓的单元状态,这是一个携带过去发生事情信息的向量。在单元内部,你有训练好的门(基本上是小型神经网络),它们根据三个输入决定从过去的单元状态中忘记什么,记住(或添加)什么到新状态中,以及在这个时间步输出什么。这是一个非常强大的工具,其有效性令人着迷。
现在我已经深入到这个系列的相当程度,我对接下来的发展有了很好的想法。以下是我必须解决的问题,没有特定顺序:
- 新策略,如
- 两个符号之间的多头/空头平等性等
- 价差交易(如果与上述不同的话)
- 最小化相关性/适用于大量符号的新风险衡量
- 迁移到AWS并使用GPU计算能力
- 集成大量使用相同代码生成的策略
- 策略梯度找到局部最大值,所以没理由不利用这一点
- 测试套件,能够客观测试策略是否可行
- 卷积网络,特别是在更大的符号组中,我们可以预期一些模式是分形的
- 将其转变为更正式的项目或Web应用
当然,我们也可以开始转向其他数据源:
- 文本
- 网络爬虫
- 游戏
请继续关注我将要写的一些文章,这些文章将讨论这里使用的算法以及使用这些技术进行算法交易开发的困难。
1:我希望你没有在线性代数课上睡觉!我所有的线性代数技能都归功于Comrade Otto Bretscher,他是Colby College的教授,我确实在他的课上睡着了,但他的教科书价值连城。