LSTM神经网络 vs Transformer在量化中的应用

LSTM背景

全称Long Short-Term Memory,是一种特殊的递归神经网络。它通过巧妙的"门"结构,可以有效地捕捉时间序列数据中的长期依赖关系。这一特点,使得LSTM在处理股价这种具有时间序列特性的数据时,展现出了非凡的潜力。

这种特殊的递归神经网络 与一般的前馈神经网络不同,当使用前馈神经网络时,神经网络会认为我们 𝑡 时刻输入的内容与 𝑡+1 时刻输入的内容完全无关,对于许多情况,例如图片分类识别,这是毫无问题的,可是对于一些情景,例如自然语言处理 (NLP, Natural Language Processing) 或者我们需要分析类似于连拍照片这样的数据时,合理运用 𝑡 或之前的输入来处理 𝑡+𝑛 时刻显然可以更加合理的运用输入的信息。为了运用到时间维度上信息,人们设计了递归神经网络 (RNN, Recurssion Neural Network),一个简单的递归神经网络可表示为:

图中, 𝑥𝑡 是在 𝑡 时刻的输入信息, ℎ𝑡 是在 𝑡 时刻的输出信息,可以看到神经元 𝐴 会递归的调用自身并且将 𝑡−1 时刻的信息传递给 𝑡 时刻。递归神经网络在许多情况下运行良好,特别是在对短时间序列数据的分析时十分方便。但上图所示的简单递归神经网络存在一个“硬伤“,长期依赖问题:递归神经网络只能处理上下文较接近的情况:

Exp 1. 想象现在设计了一个基于简单RNN的句子自动补全器,当我输入"Sea is ..." 的时候会自动补全为"Sea is  blue"。

在这种情况下,我们需要获取从上文获取的信息极短,而RNN可以很好的收集到 𝑡=0 时的信息"Sea"并且补上"blue"

Exp 2. 现在,假设我们用刚刚的RNN试图补全一篇文章"我一直呆在中国,……,我会说一口流利的 (?)"。

在这里,为了补全最后的空缺,需要的信息可能在非常远的上文提到的”中国“。在实验中简单的理想状态下,经过精心调节的RNN超参数可以良好的将这些信息向后传递。可是在现实的情况中,基本没有RNN可以做到这一点。不但如此,相比于一般的神经网络,这种简单的RNN还很容易出现两种在神经网络中臭名昭著的问题:梯度消失问题(神经网络的权重/偏置梯度极小,导致神经网络参数调整速率急剧下降:0.99^{365})和梯度爆炸问题(神经网络的权重/偏置梯度极大,导致神经网络参数调整幅度过大,矫枉过正:1.01^{365})。对于任意信息递归使用足够多次同样的计算,都会导致极大或极小的结果。

根据微分链式法则,在RNN中,神经元的权重的梯度可以被表示为一系列函数的微分的连乘。因为神经元的参数(权重与偏置)都是基于学习速率(一般为常数)和参数梯度相反数(使得神经网络输出最快逼近目标输出)得到的,一个过小或过大的梯度会导致我们要么需要极长的训练时间(本来从-2.24 调节到 -1.99 只用500个样本,由于梯度过小,每次只调10^{-6} ,最后用了几万个样本),要么会导致参数调节过度(例如本来应该从-10.02调节到-9.97,由于梯度过大,直接调成了20.7)。

基于以上问题,LSTM的设计者提出了“长短期记忆”的概念——只有一部分的信息需要长期的记忆,而有的信息可以不记下来。同时,还需要一套机制可以动态的处理神经网络的“记忆”,因为有的信息可能一开始价值很高,后面价值逐渐衰减,这时候我们也需要让神经网络学会“遗忘”特定的信息,这就是下面提到的遗忘门、记忆门、输出门。

LSTM原理

一个普通的,使用tanh作为激活函数的RNN可以这么表示:

这里A在 𝑡−1 时刻的输出值h_{t-1}被复制到了 𝑡 时刻,与 𝑡 时刻的输入x_{t}整合后经过一个带权重和偏置的tanh函数后形成输出,并继续将数据复制到 𝑡+1 时刻。

与上图朴素的RNN相比,单个LSTM单元拥有更加复杂的内部结构和输入输出:

上图中每一个红色圆形代表对向量做出的操作(pointwise operation, 对位操作),而黄色的矩形代表一个神经网络层,上面的字符代表神经网络所使用的激活函数

point-wise operation 对位操作
如果我要对向量<1, 2, 3> 和 <1, 3, 5>进行逐分量的相乘操作,会获得结果 <1, 6, 15>
layer 函数层
一个函数层拥有两个属性:权重向量(Weight) 和 偏置向量(bias),对于输入向量 𝐴 的每一个分量 𝑖 , 函数层会对其进行以下操作(假设激活函数为 𝐹(𝑥) ): output_{i}=F(W_{i}*A_{i}+b_{i})常见的激活函数(也就是套在最外面的 𝐹(𝑥) )有ReLU(线性修正单元),sigmoid(写作 𝜎 ),和 tanh

LSTM的关键

单元状态

LSTM能够从RNN中脱颖而出的关键就在于图中从单元中贯穿而过的线 ——神经元的隐藏态(单元状态),可将神经元的隐藏态简单的理解成递归神经网络对于输入数据的“记忆”,用C_{t}表示神经元在 𝑡 时刻过后的“记忆”,这个向量涵盖了在 𝑡+1 时刻前神经网络对于所有输入信息的“概括总结”

LSTM_1 遗忘门

对于上一时刻LSTM中的单元状态来说,一些“信息”可能会随着时间的流逝而“过时”。为了不让过多记忆影响神经网络对现在输入的处理,模型应该选择性遗忘一些在之前单元状态中的分量——这个工作就交给了“遗忘门”,每一次输入一个新的输入,LSTM会先根据新的输入和上一时刻的输出决定遗忘掉之前的哪些记忆——输入和上一步的输出会整合为一个单独的向量,然后通过sigmoid神经层,最后点对点的乘在单元状态上。因为sigmoid 函数会将任意输入压缩到 (0,1) 的区间上,因此可以非常直觉的得出这个门的工作原理 —— 如果整合后的向量某个分量在通过sigmoid层后变为0,那么显然单元状态在对位相乘后对应的分量也会变成0,换句话说,“遗忘”了这个分量上的信息;如果某个分量通过sigmoid层后为1,单元状态会“保持完整记忆”。不同的sigmoid输出会带来不同信息的记忆与遗忘。通过这种方式,LSTM可以长期记忆重要信息,并且记忆可以随着输入进行动态调整, 计算遗忘门的公式为,其中 𝑓𝑡 就是sigmoid神经层的输出向量:

画图表示为:

LSTM_2 & 3 记忆门

记忆门是用来控制是否将在 𝑡时刻(现在)的数据并入单元状态中的控制单位。首先,用tanh函数层将现在的向量中的有效信息提取出来,然后使用(图上tanh函数层左侧)sigmoid函数来控制这些记忆要放“多少”进入单元状态。这两者结合起来就可以做到:

  1. 从当前输入中提取有效信息
  2. 对提取的有效信息做出筛选,为每个分量做出评级(0 ~ 1),评级越高的最后会有越多的记忆进入单元状态

下面的公式分别表示这两个步骤在LSTM中的计算:

LSTM_4 输出门

输出门就是LSTM单元用于计算当前时刻的输出值的神经层。输出层会先将当前输入值与上一时刻输出值整合后的向量(也就是公式中的 [ℎ𝑡−1,𝑥𝑡] )用sigmoid函数提取其中的信息,接着会将当前的单元状态通过tanh函数压缩映射到区间(-1, 1)中。

为什么我们要在LSTM的输出门上使用tanh函数?
1. 为了防止 梯度消失问题,我们需要一个二次导数在大范围内不为0的函数,而tanh函数可以满足这一点
2. 为了便于凸优化,我们需要一个 单调函数
3. tanh函数一般收敛的更快
4. tanh函数的求导占用系统的资源更少

将经过tanh函数处理后的单元状态与sigmoid函数处理后的,整合后的向量点对点的乘起来就可以得到LSTM在 𝑡 时刻的输出了!

数据准备

对于任何机器学习任务,数据都是一切的基石。而对于股价预测这样的时间序列问题,我们需要的是结构化的时间序列数据。为了方便直接用接口了:

import akshare as ak
# data = ak.stock_zh_a_minute(symbol='sz000001',period='1',adjust="") #  1, 5, 15, 30, 60
df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20100301", end_date='20240508', adjust="")

数据预处理

对数据进行一些预处理。这包括检查和填补缺失值,以及将数据归一化到0到1的范围内。归一化可以帮助神经网络更快地收敛。

pd.isna(df).sum()

import pandas as pd
from sklearn.preprocessing import MinMaxScaler
df = df[['日期', '开盘', '收盘', '最高', '最低', '成交量', '成交额']]
df.columns=['trade_date', 'open', 'close', 'high', 'low', 'vol','amount']

df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)


# 填补缺失值(如果有的话)
# df.fillna(method="ffill", inplace=True)

# 选择要预测的特征
features = ["open", "high", "low", "close", "vol"]

# 归一化数据
scaler = MinMaxScaler()
scaled_data = scaler.fit_transform(df[features])

# 将归一化后的数据转换为DataFrame
scaled_df = pd.DataFrame(scaled_data, columns=features, index=df.index)

搭建神经网络 

Keras库来搭建神经网络。首先需要将数据转换为LSTM所需的格式。LSTM期望数据具有以下形状:[样本数,时间步数,特征数]。时间步数表示我们要根据过去多少天的数据来预测未来的股价。

import numpy as np

# 设定时间步数
time_steps = 60

# 准备LSTM输入数据
X = []
y = []

for i in range(time_steps, len(scaled_df)):
    X.append(scaled_df[i-time_steps:i])
    y.append(scaled_df["close"][i])
    
X = np.array(X)
y = np.array(y)

这里设定时间步数为60,即根据过去60天的数据预测下一天的收盘价。然后,用一个循环来准备LSTM的输入数据。对于每个时间点,取前60天的数据作为输入,下一天的收盘价作为预测目标。接下来搭建LSTM模型:

from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout

# 构建LSTM模型
model = Sequential()
model.add(LSTM(units=50, return_sequences=True, input_shape=(X.shape[1], X.shape[2])))
model.add(Dropout(0.2))
model.add(LSTM(units=50, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(units=50))
model.add(Dropout(0.2))
model.add(Dense(units=1))

model.compile(optimizer="adam", loss="mean_squared_error")

KerasSequential模型来搭建LSTM网络。网络包含三层LSTM,每层有50个神经元。在每个LSTM层之后,我们添加了一个Dropout层来防止过拟合。最后添加一个全连接层来输出预测结果。用Adam优化器和均方误差损失函数MSE来编译模型。

模型训练

开始训练模型,神经网络通过不断调整权重,从数据中学习模式和规律:

# 训练模型
model.fit(X, y, epochs=100, batch_size=32)

这里调用模型的fit方法来训练网络。设置训练的轮数(epochs)为100,每个批次(batch)的大小为32。这意味着,神经网络将遍历整个训练数据集100次,每次更新权重时使用32个样本。在训练过程中会看到每个epoch的损失值(loss)。损失值越低,通常意味着模型的性能越好。

模型评估

将数据分为训练集和测试集,用训练集数据训练模型,然后在测试集上评估模型的预测能力。

from sklearn.metrics import mean_squared_error

# 进行预测
y_pred = model.predict(X)

# 计算均方误差
mse = mean_squared_error(y, y_pred)
print(f"Mean Squared Error: {mse}")

简单画了一下走势图:

预测值和实际值偏离度不大。 

上面是用训练好的模型对输入数据进行预测,得到预测值y_pred。然后,我们使用mean_squared_error函数计算预测值和真实值之间的均方误差(MSE)。MSE是一个常用的回归问题的评估指标,它衡量了预测值与真实值之间的平均squared距离。MSE越小,通常意味着模型的预测性能越好。得到的结果为:

结果预测

如果你对模型的性能满意,就可以用它来进行实际的股价预测了。为了进行预测,需要准备最近60天的股价数据作为输入(这里仅训练了一个特征,需要重新初始化一下):

# 准备最近60天的数据
last_60_days = scaled_df[-60:]
last_60_days_arr = np.array(last_60_days)
last_60_days_arr = np.reshape(last_60_days_arr, (1, last_60_days_arr.shape[0], last_60_days_arr.shape[1]))

# 重新初始化scaler对象,只包含一个特征的归一化信息
scaler = MinMaxScaler()
scaled_data = scaler.fit_transform(df['close'].values.reshape(-1, 1))


# 进行预测
pred_price = model.predict(last_60_days_arr)

# 将预测值反归一化
pred_price = scaler.inverse_transform(pred_price)

print(f"Predicted Price: {pred_price[0][0]}")

下面输出了预测的股价。这个预测价格表示模型对下一个交易日收盘价的预期。当然,这只是一个预测,你如果相信就死定了(明天可能会回到10.8):

模型优化

虽然已经能够用LSTM模型来预测股价,但模型的性能还有很大的提升空间。以下是一些可能的优化策略:

  1. 增加特征:除了开盘价、最高价、最低价、收盘价和成交量,我们还可以加入其他技术指标,如移动平均线、RSI、MACD等。这些指标可能包含了一些有助于预测的信息。

  2. 调整模型结构:我们可以尝试不同的LSTM层数、神经元数、激活函数、正则化方法等,以找到最优的模型结构。这可能需要一些反复试验和调优。

  3. 引入注意力机制:注意力机制可以帮助模型更好地关注到重要的时间步。我们可以在LSTM层之上添加一个注意力层,让模型自动学习每个时间步的权重。

  4. 集成学习:我们可以训练多个不同的LSTM模型,然后将它们的预测结果进行平均或投票。这种集成学习的方法通常可以提高预测的稳健性和准确性。

  5. 引入更多数据:除了历史价格数据,我们还可以考虑引入一些基本面数据,如公司财报、宏观经济指标等。这可能需要我们对数据进行更复杂的预处理和特征工程。

如3引入self-attention机制的模型训练:

from keras.models import Model
from keras.layers import Input, LSTM, Dense, Dropout, Activation, Permute, multiply

# 构建带注意力机制的LSTM模型
input_data = Input(shape=(time_steps, X.shape[2]))
lstm_out = LSTM(60, return_sequences=True)(input_data)
attention_probs = Dense(time_steps, activation='softmax', name='attention_probs')(lstm_out)
attention_mul = multiply([lstm_out, attention_probs])
attention_mul = Permute((2,1))(attention_mul)
final_output = Dense(1)(attention_mul)

model = Model(input_data, final_output)
model.compile(optimizer='adam', loss='mean_squared_error')

# 训练模型
model.fit(X, y, epochs=200, batch_size=32)

得到的结果为:

Transformer模型背景

在NLP领域掀起了一场革命,其独特的自注意力机制和并行计算架构,让其在机器翻译、文本生成等任务上大幅刷新了SOTA记录。更重要的是,Transformer并不限于文本数据,其处理序列数据的能力同样可以拓展到时间序列领域。将Transformer应用于金融市场建模时,其记忆容量和建模能力可显著超越传统的RNN模型。直观来说,股票市场蕴藏着极为复杂的多尺度动力学过程,价格波动受基本面、资金面、市场情绪等诸多因素的交互影响,这些影响往往跨越不同的时间尺度。Transformer凭借其强大的注意力机制,能够自适应地关注不同时间尺度上的关键特征,挖掘出价格波动背后的深层逻辑,可谓深度长期记忆建模的利器。

Transformer的一些重要组成部分和特点:

自注意力机制(Self-Attention):这是Transformer的核心概念之一,它使模型能够同时考虑输入序列中的所有位置,而不是像循环神经网络(RNN)或卷积神经网络(CNN)一样逐步处理。自注意力机制允许模型根据输入序列中的不同部分来赋予不同的注意权重,从而更好地捕捉语义关系。
多头注意力(Multi-Head Attention):Transformer中的自注意力机制被扩展为多个注意力头,每个头可以学习不同的注意权重,以更好地捕捉不同类型的关系。多头注意力允许模型并行处理不同的信息子空间。
堆叠层(Stacked Layers):Transformer通常由多个相同的编码器和解码器层堆叠而成。这些堆叠的层有助于模型学习复杂的特征表示和语义。
位置编码(Positional Encoding):由于Transformer没有内置的序列位置信息,它需要额外的位置编码来表达输入序列中单词的位置顺序。
残差连接和层归一化(Residual Connections and Layer Normalization):这些技术有助于减轻训练过程中的梯度消失和爆炸问题,使模型更容易训练。
编码器和解码器:Transformer通常包括一个编码器用于处理输入序列和一个解码器用于生成输出序列,这使其适用于序列到序列的任务,如机器翻译。

Transformer结构:

Nx = 6,Encoder block由6个encoder堆叠而成,图中的一个框代表的是一个encoder的内部结构,一个Encoder是由Multi-Head Attention和全连接神经网络Feed Forward Network构成。如下图所示:

每一个编码器都对应上图的一个encoder结构:

Transformer的编码组件是由6个编码器叠加在一起组成的,解码器同样如此。所有的编码器在结构上是相同的,但是它们之间并没有共享参数。

从编码器输入的句子首先会经过一个自注意力层,这一层帮助编码器在对每个单词编码的时候时刻关注句子的其它单词。解码器中的解码注意力层的作用是关注输入句子的相关部分,类似于seq2seq的注意力。

1、自注意力机制

自注意力的作用:随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。在处理过程中,自注意力机制会将对所有相关单词的理解融入到我们正在处理的单词中。更具体的功能如下:

序列建模:自注意力可以用于序列数据(例如文本、时间序列、音频等)的建模。它可以捕捉序列中不同位置的依赖关系,从而更好地理解上下文。这对于机器翻译、文本生成、情感分析等任务非常有用。
并行计算:自注意力可以并行计算,这意味着可以有效地在现代硬件上进行加速。相比于RNN和CNN等序列模型,它更容易在GPU和TPU等硬件上进行高效的训练和推理。(因为在自注意力中可以并行的计算得分)
长距离依赖捕捉:传统的循环神经网络(RNN)在处理长序列时可能面临梯度消失或梯度爆炸的问题。自注意力可以更好地处理长距离依赖关系,因为它不需要按顺序处理输入序列。
自注意力的结构如下所示:

自注意力的计算:从每个编码器的输入向量(每个单词的词向量,即Embedding,可以是任意形式的词向量,比如说word2vec,GloVe,one-hot编码)

中生成三个向量,即查询向量、键向量和一个值向量。(这三个向量是通过词嵌入与三个权重矩阵即W^{Q} W^{K} W^{V}相乘后创建出来的)新向量在维度上往往比词嵌入向量更低。(512\rightarrow64)

将以上所得到的查询向量、键向量、值向量组合起来就可以得到三个向量矩阵Query、Keys、Values。

计算自注意力的第二步是计算得分,假设我们在为这个例子中的第一个词“Thinking”计算自注意力向量,我们需要拿输入句子中的每个单词对“Thinking”打分。这些分数是通过所有输入句子的单词的键向量与“Thinking”的查询向量相乘点积来计算的。

第三步和第四步是将得到的分数除以8(让梯度更稳定。这里也可以使用其它值,8只是默认值,防止内积过大),然后通过softmax传递结果。随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。softmax的作用是使所有单词的分数归一化,得到的分数都是正值且和为1。这个softmax分数决定了每个单词对编码当下位置(“Thinking”)的贡献。显然,已经在这个位置上的单词将获得最高的softmax分数。第五步是将每个值向量乘以softmax分数(这是为了准备之后将它们求和)。这里的直觉是希望关注语义上相关的单词,并弱化不相关的单词(例如,让它们乘以0.001这样的小数)。第六步是对加权值向量求和,然后即得到自注意力层在该位置的输出。
Softmax函数:或称归一化指数函数,它将每一个元素的范围都压缩到(0,1)之间,并且所有元素的和为1。整体计算为:

最终得到了自注意力,并将得到的向量传递给前馈神经网络。以上二到六步合为一个公式计算自注意力层的输出。

2、Multi——Head Attention:

1、扩展了模型专注于不同位置的能力。
2、有多个查询/键/值权重矩阵集合,(Transformer使用八个注意力头)并且每一个都是随机初始化的。和上边一样,用矩阵X乘以W^{Q} W^{K} W^{V}来产生查询、键、值矩阵。
3、self-attention只是使用了一组W^{Q} W^{K} W^{V}来进行变换得到查询、键、值矩阵,而Multi-Head Attention使用多组W^{Q} W^{K} W^{V}得到多组查询、键、值矩阵,然后每组分别计算得到一个Z矩阵。

前馈层只需要一个矩阵,则把得到的8个注意力头矩阵拼接在一起,然后用一个附加的权重矩阵W^{0}与它们相乘。
 

整个流程为:

exp:编码it一词时,不同注意力的头集中在:

  • 一个注意力头集中在The animal
  • 另一个注意力头集中在tire上。即形象解释it代指的是animal和tire

3、为什么要用位置编码来表示序列的顺序?

如果不添加位置编码,那么无论单词在什么位置,它的注意力分数都是确定的。这不是我们想要的。为了理解单词顺序,Transformer为每个输入的词嵌入添加了一个向量,这样能够更好的表达词与词之间的关系。词嵌入与位置编码相加,而不是拼接,他们的效率差不多,但是拼接的话维度会变大,所以不考虑。

为了让模型理解单词的顺序,添加了位置编码向量,这些向量的值遵循特定的模式。

4、Add&Normalize

在经过多头注意力机制得到矩阵Z之后,并没有直接传入全连接神经网络,而是经过了一步Add&Normalize。

Add & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下:

LayerNorm(X+MultiHeadAttention(X))

LayerNorm(X+FeedForward(X))

其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 X 维度是一样的,所以可以相加)。

Add
Add,就是在z的基础上加了一个残差块X,加入残差块的目的是为了防止在深度神经网络的训练过程中发生退化的问题,退化的意思就是深度神经网络通过增加网络的层数,Loss逐渐减小,然后趋于稳定达到饱和,然后再继续增加网络层数,Loss反而增大。


ResNet残差神经网络
为了了解残差块,引入ResNet残差神经网络,神经网络退化指的是在达到最优网络层数之后,神经网络还在继续训练导致Loss增大,对于多余的层,我们需要保证多出来的网络进行恒等映射。只有进行了恒等映射之后才能保证这多出来的神经网络不会影响到模型的效果。残差连接主要是为了防止网络退化。

上图就是构造的一个残差块,X是输入值,F(X)是经过第一层线性变换后并且激活的输出,在第二层线性变化之后,激活之前,F(X)加入了这一层输入值X,然后再进行激活后输出。

要恒等映射,我们只需要让F(X)=0就可以了。x经过线性变换(随机初始化权重一般偏向于0),输出值明显会偏向于0,而且经过激活函数Relu会将负数变为0,过滤了负数的影响。
这样当网络自己决定哪些网络层为冗余层时,使用ResNet的网络很大程度上解决了学习恒等映射的问题,用学习残差F(x)=0更新该冗余层的参数来代替学习h(x)=x更新冗余层的参数。

Normalize
归一化目的:
1、加快训练速度
2、提高训练的稳定性
使用到的归一化方法是Layer Normalization。

LN是在同一个样本中不同神经元之间进行归一化,而BN是在同一个batch中不同样本之间的同一位置的神经元之间进行归一化。BN是对于相同的维度进行归一化,但是在NLP中输入的都是词向量,一个300维的词向量,单独去分析它的每一维是没有意义地,在每一维上进行归一化也是适合地,因此这里选用的是LN。

5、全连接层Feed Forward

全连接层是一个两层的神经网络,先线性变换,然后ReLU非线性,再线性变换。

FFN(X)=max(0,W_{1}X+b_{1})W_{2}+b_{2}
这两层网络就是为了将输入的Z映射到更加高维的空间中然后通过非线性函数ReLU进行筛选,筛选完后再变回原来的维度。经过6个encoder后输入到decoder中。

Decoder整体结构

和Encoder Block一样,Encoder也是由6个decoder堆叠而成的,Nx=6。包含两个 Multi-Head Attention 层。第一个 Multi-Head Attention 层采用了 Masked 操作。第二个 Multi-Head Attention 层的K, V矩阵使用 Encoder 的编码信息矩阵C进行计算,而Q使用上一个 Decoder block 的输出计算。

Masked Multi-Head Attention
与Encoder的Multi-Head Attention计算原理一样,只是多加了一个mask码。mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。为什么需要添加这两种mask码呢?

padding mask:因为每个批次输入序列长度是不一样的,也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!

sequence mask:为了使得 decoder 不能看见未来的信息。对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。这在训练的时候有效,因为训练的时候每次我们是将target数据完整输入进decoder中地,预测时不需要,预测的时候我们只能得到前一时刻预测出的输出。具体也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。

注意:
1、在Encoder中的Multi-Head Attention也是需要进行mask的,只不过Encoder中只需要padding mask即可,而Decoder中需要padding mask和sequence mask。
2、Encoder中的Multi-Head Attention是基于Self-Attention地,Decoder中的第二个Multi-Head Attention就只是基于Attention,它的输入Quer来自于Masked Multi-Head Attention的输出,Keys和Values来自于Encoder中最后一层的输出。

6、输出

Output如图中所示,首先经过一次线性变换(线性变换层是一个简单的全连接神经网络,它可以把解码组件产生的向量投射到一个比它大得多的,被称为对数几率的向量里),然后Softmax得到输出的概率分布(softmax层会把向量变成概率),然后通过词典,输出概率最大的对应的单词作为我们的预测输出。

ransformer的优缺点:
优点:
1、效果好
2、可以并行训练,速度快
3、很好的解决了长距离依赖的问题
缺点:
完全基于self-attention,对于词语位置之间的信息有一定的丢失,虽然加入了positional encoding来解决这个问题,但也还存在着可以优化的地方。

基于以上原理来进行预测,用大A的日频数据进行测试


数据集准备:

import numpy as np
import pandas as pd
import akshare as ak
from datetime import datetime

import tensorflow as tf
from tensorflow.keras.models import *
from tensorflow.keras.layers import *
from tensorflow.keras.callbacks import *

# 获取上证指数日频数据
stock_df = ak.stock_zh_index_daily(symbol="sh000001")  
stock_df['date'] = pd.to_datetime(stock_df['date'])
stock_df.set_index('date', inplace=True)
stock_df = stock_df[['close']]

# 计算对数收益率
stock_df['return'] = np.log(stock_df.close) - np.log(stock_df.close.shift(1))

结果为:

 搭建模型:

搭建一个简单的Transformer模型,用于对收益率序列进行建模预测。模型由输入层、多头注意力编码层、位置编码层和输出层组成

# 参数设置
input_window = 30  # 输入序列长度
output_window = 5  # 输出序列长度
num_layers = 3  # Transformer编码层数
d_model = 128  # 输入输出维度
num_heads = 8  # 注意力头数
dff = 512  # 前馈网络隐藏层维度
dropout_rate = 0.2  # dropout比例

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)

    def call(self, x):
        x = tf.math.reduce_mean(x[:,:,:4], axis=-1) 
        x = x * self.weights_linear + self.bias_linear
        x = tf.expand_dims(x, axis=-1) 
        return x
    
class TransformerEncoder(Layer):
    def __init__(self, d_model, num_heads, dff, rate=0.1):
        super(TransformerEncoder, self).__init__()
        self.att = MultiHeadAttention(num_heads=num_heads, key_dim=d_model)
        self.ffn = Sequential([Dense(dff, activation="relu"),
                               Dense(2)]  # Output dimension adjusted to (None, 30, 2)
                               )

        self.layernorm1 = LayerNormalization(epsilon=1e-6)
        self.layernorm2 = LayerNormalization(epsilon=1e-6)
        self.dropout1 = Dropout(rate)
        self.dropout2 = Dropout(rate)
        self.rate = rate  # Store dropout rate

    def call(self, inputs, training=None):
        attn_output = self.att(query=inputs, value=inputs, attention_mask=None, training=training)  
        attn_output = self.dropout1(attn_output, training=training)
        out1 = self.layernorm1(inputs + attn_output)
        
        ffn_output = self.ffn(out1)
        ffn_output = self.dropout2(ffn_output, training=training)
        
        # Adjusted addition operation
        print("Shapes - ffn_output:", ffn_output.shape)
        out2 = out1 + ffn_output
        
        return self.layernorm2(out2)
    
def transformer_model():
    '''Transformer时序预测模型'''
    inputs = Input(shape=(input_window, 1))
    x = Time2Vector(input_window)(inputs)
    x = Concatenate(axis=-1)([inputs, x])
    
    for _ in range(num_layers):
        x = TransformerEncoder(d_model, num_heads, dff)(x, training=True)  # Pass training=True here
    
    # Add TimeDistributed layer to ensure consistent output shape
    x = TimeDistributed(Dense(1))(x)
    
    x = GlobalAveragePooling1D(data_format='channels_first')(x)
    x = Dropout(0.2)(x)
    x = Dense(64, activation='relu')(x)
    x = Dropout(0.2)(x)
    outputs = Dense(output_window)(x)
    
    model = Model(inputs=inputs, outputs=outputs)
    model.compile(loss='mse', optimizer='adam', metrics=['mae'])
    return model

数据预处理与模型训练:

为训练Transformer模型,将时间序列数据划分为输入输出对。这里我们设定输入长度为30,即用过去一个月的收益率序列来预测未来5天的走势。


input_window = 30
output_window = 5

x = []
y = []

for i in range(len(stock_df) - input_window - output_window):
    x.append(np.array(stock_df['return'].iloc[i:i+input_window]).reshape(input_window, 1))
    y.append(np.array(stock_df['return'].iloc[i+input_window:i+input_window+output_window]))
    
# 划分训练集和测试集
train_size = int(len(x) * 0.8)
x_train, y_train = np.array(x[:train_size]), np.array(y[:train_size])
x_test, y_test = np.array(x[train_size:]), np.array(y[train_size:])

model = transformer_model() 

# 设置早停策略
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)

# 模型训练
history = model.fit(x_train, y_train,
                    batch_size=32, epochs=100, 
                    callbacks=[early_stopping],
                    validation_split=0.2)

训练结果为:

模型评估与预测:

用训练好的模型对测试集进行预测,并计算MAE等评价指标。

# 模型预测  
y_pred = model.predict(x_test)

# 反归一化
y_test = np.squeeze(y_test)
y_pred = np.squeeze(y_pred)

# 计算评价指标
mae = np.mean(np.abs(y_pred - y_test))
rmse = np.sqrt(np.mean(np.square(y_pred - y_test)))

print(f'MAE: {mae:.4f}, RMSE: {rmse:.4f}')

两个指标结果为:

Transformer模型在测试集上取得了约0.0076的MAE,和0.0107的RMSE,预测效果明显优于基准模型。

最后,我们用模型对未来一周的走势进行预测:


# 获取最新数据
last_data = np.array(stock_df.return.iloc[-input_window:]).reshape(1, -1, 1)
next_week_pred = model.predict(last_data)

# 输出预测结果
next_week_dates = stock_df.index[-1] + pd.to_timedelta(np.arange(1,6), unit='D')
next_week_pred = pd.Series(next_week_pred.squeeze(), index=next_week_dates)

print('未来一周收益率预测值:')
print(next_week_pred)

输出结果:

用模型对未来一周的走势进行了预测,为量化交易决策提供参考。Transformer作为前沿模型,其机制相对复杂,调参空间巨大,如何进一步优化模型性能,还需要进一步地探索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值