Transformer在量化选股中的应用
由guest创建,最终由guest 被浏览 27 用户
一、基于时间嵌入的方法
当前应用于NLP领域的Transformer,结构过于庞大,并不适用于股票数据(开盘价,收盘价,最高价,最低价,等)这样的时序数据,因此,本文提出一种简化的适用于股票数据的Transformer结构,其根据时间嵌入的思想构建,能很好的应用于量化选股中。下面以一个例子来介绍用于股票数据的Transformer体系结构,以及什么是**时间嵌入,**为什么将它们与时间序列结合使用至关重要。
1、数据准备
为了说明方便,我们使用IBM的股票历史行情数据来进行实验,将数据集扩展到大量不同的股票时,可以获得相同的性能。IBM数据集始于1962-01-02,
截止于2020-05-24,包含总共14699个交易日。此外,对于每一个训练日,我们有IBM股票的开盘价,收盘价,最高价和最低价以及交易量数据。数据
走势如下图所示:
根据4种价格和交易量计算4种价格的变化率和每日交易量变化量,用价格变化率和每日交易量变化量替换原数据作为因子,并用min-max normalized来对数据进行归一化,并按时间序列分为训练集,验证集,和测试集。将股票价格和交易量转换为每日变化率可以提高数据集的平稳性。能够使得训练得出的模
型对于未来的预测具有更高的有效性。下图展示了转换后的数据。
最后,训练,验证和测试集由一条条的时间序列数据构成,每个序列的长度为128天**(seq_len = 128)。每天有五个价格因子(开盘价,最高价,最低价,收盘价和交易量的变化率和变化量),所以每天的因子数据由一个5维向量表示(feature_size = 5)。在单个训练步骤中,我们的Transformer模型将接收32个序列(batch_size = 32)**,输入数据大小如下图所示。
2、时间嵌入
作为实现Transformer的第一步,我们必须考虑如何将隐藏在股价中的时间概念编码到模型中。在处理时间序列数据时,顺序性是必需要考虑的因素。
但是,当使用Transformer处理时间序列/顺序数据时,序列数据会一次同时通过Transformer,这使得提取时间/顺序依赖关系变得困难。因此,将
Transformer应用于自然语言数据时,我们倾向于利用位置编码来向模型提供单词顺序的概念。简单来讲,位置编码是单词在句子中的位置的表示,从
而使Transformer可以获取有关句子结构和单词相互依赖性的知识。在BERT的内部查看时,可以找到位置编码的一个示例__[2] __模型,其已在许多NLP任务
中展现了很好的性能。同样,在处理我们的股票价格时,Transformer需要时间的概念。没有时间嵌入,我们的Transformer将不会收到有关我们股票价
格的时间顺序的任何信息。比如,1956年的行情信息对预测2021年的股票涨跌并没有多大帮助。
3、Time2Vec
为了实现时间嵌入,我们将使用论文Time2Vec: Learning a Vector Representation of Time __[2]__中描述的方法。文中提出一种“对于时间的模型无关向量表示,叫做Tiem2Vector”. 你可以认为一个该向量表示,就像一层普通的embedding layer一样,可以被添加到神经网络结构来提高一个模型的性能。
这篇论文有两个主要思想:1、作者发现时间的有意义的表示必须包括周期性和非周期性两个模式。周期性模式的一个例子是每一年不同季节的天气变化。相反,非周期性模式的一个例子是疾病,患者越老越容易发生。2、时间表示应该对时间缩放具有**不变性,**这意味着时间表示不受不同时间增量(例如:天,小时、秒或较长时间范围)的影响。结合周期性和非周期性模式的思想以及时间缩放的不变性,我们通过以下数学定义来表示。不用担心,它比看起来容易,我将详细解释。
时间向量t2v由两部分取得,其中ωᵢτ + φᵢ表示时间向量的非周期/线性特征,***F(ωᵢτ + φᵢ)***表示时间向量的周期性特征。对t2v(τ) = ωᵢτ + φᵢ
进行简化,新函数 y = mᵢx + bᵢ
应该看起来很熟悉,因为它是您从高中就知道的线性函数。在ωᵢτ + φᵢ
中ω
是时间序列τ
的斜率矩阵,φ
也是一个矩阵,它定义在那里我们的时间序列τ
与y轴相交。因此,ωᵢτ + φᵢ
无非是线性函数。
第二部分F(ωᵢτ + φᵢ)
代表时间向量的周期性特征。其中ωᵢτ + φᵢ
和第一部分一样代表时间向量的线性函数,然后将其输入给添加的函数***F(),作者尝试了不同的F()***以最好地描述周期性关系(sigmoid,tanh,ReLU,mod,triangle等)。最后,正弦函数获得了最佳和最稳定的性能(余弦得到了类似的结果)。
sin(ωᵢτ + φᵢ)
的2D表示如下图所示。φ
沿x轴移动正弦函数,ω
确定正弦函数的波长。
在开始实现时间嵌入之前,让我们看一下普通的LSTM网络(蓝色)和LSTM + Time2Vec网络(红色)的性能差异。如下图所示,在多个数据集上对模型加上Time2Vec之后不会使性能变差,并且总是会改善模型的性能。有了理论基础,我们接下来构建模型。
现在我们将在代码中实现Time2Vec。为了使时间向量易于集成到任何类型的神经网络体系结构,我们使用keras搭建。自定义的Time2Vector层具有两个子功能def build():和def call():。在**def build():中我们初始化4个矩阵中,2个为ω ,2个为φ **,因为我们分别需要一对(ω ,φ)矩阵用于非周期性(线性)和周期性(正弦)特征。
seq_len = 128def build(input_shape):
weights_linear = add_weight(shape=(seq_len), trainable=True)
bias_linear = add_weight(shape=(seq_len), trainable=True)
weights_periodic = add_weight(shape=(seq_len), trainable=True)
bias_periodic = add_weight(shape=(seq_len), trainable=True)
Time2Vector层将接收的输入数据具有以下形状**(batch_size, seq_len, feature_size) → (32, 128, 5)。batch_size定义每步有多少股票价格序列输入到模型。seq_len参数确定单个股票价格序列的长度。feature_size即为每天要使用的因子数量。第一步是将交易量排除在外(因为交易量因子是取得变化量而不是变化率),然后对开盘价,最高价,最低价和收盘价取平均值,得出形状(batch_size,seq_len)**。
x = tf.math.reduce_mean(x [:,:,:4],axis = -1)
接下来,我们计算非周期性(线性)时间特征,再将维度扩大1,得到矩阵大小**(batch_size, seq_len, 1)**,
time_linear = weights_linear * x + bias_linear
time_linear = tf.expand_dims(time_linear, axis=-1)
对周期性时间特征重复相同的过程,也获得相同的矩阵大小,(batch_size, seq_len, 1),
time_periodic = tf.math.sin(tf.multiply(x,weights_periodic)+ bias_periodic)
time_periodic = tf.expand_dims(time_periodic,axis = -1)
完成时间向量计算所需的最后一步是将线性和周期性时间特征拼接起来,得到到矩阵大小:(batch_size, seq_len, 2)
time_vector = tf.concat([time_linear,time_periodic],axis = -1)
最后将所有步骤组合到一个Layer函数中即可
class Time2Vector(Layer):
def __init__(self, seq_len, **kwargs):
super(Time2Vector, self).__init__()
self.seq_len = seq_len
def build(self, input_shape):
self.weights_linear = self.add_weight(name='weight_linear',
shape=(int(self.seq_len),),
initializer='uniform',
trainable=True)
self.bias_linear = self.add_weight(name='bias_linear',
shape=(int(self.seq_len),),
initializer='uniform',
trainable=True)
self.weights_periodic = self.add_weight(name='weight_periodic',
shape=(int(self.seq_len),),
initializer='uniform',
trainable=True)
self.bias_periodic = self.add_weight(name='bias_periodic',
shape=(int(self.seq_len),),
initializer='uniform',
trainable=True)
def call(self, x):
x = tf.math.reduce_mean(x[:,:,:4], axis=-1) # Convert (batch, seq_len, 5) to (batch, seq_len)
time_linear = self.weights_linear * x + self.bias_linear
time_linear = tf.expand_dims(time_linear, axis=-1) # (batch, seq_len, 1)
time_periodic = tf.math.sin(tf.multiply(x, self.weights_periodic) + self.bias_periodic)
time_periodic = tf.expand_dims(time_periodic, axis=-1) # (batch, seq_len, 1)
return tf.concat([time_linear, time_periodic], axis=-1) # (batch, seq_len, 2)
4、适用于股票数据的Transformer结构
现在我们知道提供时间概念以及如何实现时间向量非常重要,接下来我们正式来构建适用于股票数据的Transformer结构,该模型可以使模型专注于时间序列的相关部分,从而提高预测质量。自注意力机制由单头注意力或多头注意力层组成。自注意力机制能够一次性将所有时间序列步骤彼此联系起来,从而建立长期的依赖关系理解。最后,所有这些过程都是可以在Transformer架构中并行的运行,可以加速学习过程。
1、给输入数据加上时间特征
在实现时间嵌入之后,我们给输入数据加上时间向量作为我们的Transformer的输入。所述Time2Vector层接收输入数据,计算非周期和周期时间向量并拼接。然后,将计算出的时间向量与因子向量连接起来,形成形状为**(32, 128, 7)**的矩阵,具体过程如下图所示:
2、自注意力层
本层有三个输入 (Query, Key, Value),三个输入均为上一步得到的输入矩阵,然后分别输入三个分隔的Dense Layer,其有96个神经元,对因子时间向量进行embedding,得到的输出矩阵分别用q,k,v表示,大小均为(32, 128, 96),如下图所示:
初始线性转换后,我们将计算注意力得分。注意力得分决定了在预测未来股价时将注意力集中在各个时间序列上的程度。注意力得分是通过k和q的点积来计算得到的,即q矩阵乘k的转置的。然后,将点积除以Dense Layer的大小(96),以避免梯度爆炸。然后,相除的点积将通过softmax函数生成一组权重,这些权重之和为1,该权重即为各个时间点在的注意力得分。最后,将等到的注意力得分矩阵与v矩阵相乘,得出自注意力层的输出。过程如下图所示:
可以用keras代码实现:
class SingleAttention(Layer):
def __init__(self, d_k, d_v):
super(SingleAttention, self).__init__()
self.d_k = d_k
self.d_v = d_v
def build(self, input_shape):
self.query = Dense(self.d_k, input_shape=input_shape, kernel_initializer='glorot_uniform', bias_initializer='glorot_uniform')
self.key = Dense(self.d_k, input_shape=input_shape, kernel_initializer='glorot_uniform', bias_initializer='glorot_uniform')
self.value = Dense(self.d_v, input_shape=input_shape, kernel_initializer='glorot_uniform', bias_initializer='glorot_uniform')
def call(self, inputs): # inputs = (in_seq, in_seq, in_seq)
q = self.query(inputs[0])
k = self.key(inputs[1])
attn_weights = tf.matmul(q, k, transpose_b=True)
attn_weights = tf.map_fn(lambda x: x/np.sqrt(self.d_k), attn_weights)
attn_weights = tf.nn.softmax(attn_weights, axis=-1)
v = self.value(inputs[2])
attn_out = tf.matmul(attn_weights, v)
return attn_out
3、多头自注意力机制
为了进一步改善自注意机制,论文《Attention Is All You Need**》**__[4]__提出了多头注意力机制。多头注意力层的功能是拼接n
个单头注意力层,然后输出到Dense Layer进行非线性变换。下图显示了3个单头自注意力层的串联。具有多头自注意力机制允许将多个独立的单头层输出作为输入。因此,该模型能够一次关注多个时间序列步骤。注意力头数量的增加会加强模型捕获远程依赖关系的能力。
一个简洁的多头注意力实现如下所示:
class MultiAttention(Layer):
def __init__(self, d_k, d_v, n_heads):
super(MultiAttention, self).__init__()
self.d_k = d_k
self.d_v = d_v
self.n_heads = n_heads
self.attn_heads = list()
def build(self, input_shape):
for n in range(self.n_heads):
self.attn_heads.append(SingleAttention(self.d_k, self.d_v))
self.linear = Dense(7, input_shape=input_shape, kernel_initializer='glorot_uniform', bias_initializer='glorot_uniform')
def call(self, inputs):
attn = [self.attn_heads[i](inputs) for i in range(self.n_heads)]
concat_attn = tf.concat(attn, axis=-1)
multi_linear = self.linear(concat_attn)
return multi_linear
4、编码器
每个编码器层都包含一个自注意力子层和一个前馈子层。前馈子层由两个Dense Layer组成,中间有ReLU激活。顺便说一句,如果卷积层的内核大小和步幅为1,则可以用一维卷积层替换Dense Layer。Dense Layer和卷积层的数学结构相同。每个子层后面都有一个子层,每两个子层之间通过将初始Query输入添加到上一个子层的输出中来形成残差连接,然后接入归一化层,以加速训练过程。如下图所示:
以上便是Transformer层,我们可以轻松的对其进行堆叠以提高模型的性能。由于我们不需要任何Transformer解码器层,因此我们实现的Transformer架构与BERT 架构非常相似。不同之处在于时间嵌入,我们的转换器可以处理3维时间序列,而不是简单的2维序列。
5、总体架构
总之,我们首先初始化时间嵌入层以及3个Transformer编码器层。初始化之后,我们将回归头(输出大小为1的Dense Layer)堆叠到最后一个子层,然后开始训练过程。
二、基于Hierarchical Multi-Scale Gaussian的方法
由论文《Hierarchical Multi-Scale Gaussian Transformer for Stock Movement Prediction》提出。 在本文中,作者提出了一种基于transformer的新方法来解决股票价格预测的任务。其对基本的transformer在结构上做了相应的改进,首先,其提出了一个叫做Multi-Scale Gaussian Prior的操作,以提高Transformer的局域性。其次,作者开发了一种正交正则化(Orthogonal Regularization)以避免在多头自注意力机制中学习冗余头。第三,作者设计了一个transformer交易间隙分配器(Trading Gap Splitter)来学习高频金融数据的层次特征。与其他流行的递归神经网络(如LSTM)相比,该方法具有从股票时间序列数据中挖掘长期依赖的优点。
\n