Informer时间序列模型内容与代码解读
1. 前言
论文:Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting
1.1 摘要:
许多现实世界的应用都需要对长序列时间序列进行预测,例如电力消耗规划。长序列时间序列预测(LSTF)对模型的预测能力提出了很高的要求,即能够有效地捕捉输出和输入之间精确的长期依赖耦合。最近的研究表明,Transformer有潜力提高预测能力。然而,Transformer存在一些严重的问题,阻碍了它直接应用于LSTF,包括二次时间复杂性、高内存使用率和编码器-解码器架构的固有限制。
为了解决这些问题,我们为LSTF设计了一个高效的基于转换器的模型,名为Informer,具有三个显著的特征:(i)ProbeSparse自注意机制,它在时间复杂性和内存使用方面实现了O(L*logL),并且在序列的依赖性对齐方面具有可比的性能。(ii)自注意力提取通过将级联层输入减半来突出主导注意力,并有效地处理超长输入序列。(iii)生成式解码器虽然概念上很简单,但它以一次正向操作而不是一步一步的方式预测长时间序列,这大大提高了长序列预测的推理速度。在四个大型数据集上的大量实验表明,Informer显著优于现有方法,并为LSTF问题提供了一种新的解决方案。
本文是少有的用Transformer模型去解决长序列的时间预测问题。是AAAI 2021 Best Paper。内容比较新颖,尤其是提出了一种新的注意力层——ProbSparse Self-Attention和Distilling操作,在减少模型参数的同时大大提升模型的效果,值得在许多场合中借鉴。
1.2 背景
LSTF(Long Sequence Time-Series Forecasting) 任务的方法因为预测能力的限制,大多专注于短期预测,而现在越来越长的序列似乎成为了 LSTF 研究的阻碍。LSTF 任务面临的主要挑战是需要提高模型的预测能力,从而满足日益增长的长序列需求,这要求模型具备:(a)卓越的 long-range alignment(远程对比)能力;(b)对长序列输入和输出的高效操作能力。
序列预测是一个涉及使用历史序列信息来预测序列中的下一个或多个值的问题。 这是一个基本但重要的研究问题。对于传统的循环神经网络在长序列问题上一般都不是很好的效果:比如传统的RNN-based模型不能够用于长时序预测问题。
Transformer模型是当下最火的模型之一,被广泛使用在nlp,cv等各个领域之中。2017 年出现的 Transformer因其 Self-Attention Mechanism 可以将网络信号传播路径的最大长度减少到理论上的 o ( 1 ) o(1) o(1)且避免了类似于 RNN 中的递归结构,使其能更好地捕获长期依赖关系,从而在处理 LSTF 问题上展现出巨大的潜力。但是,用Transformer模型去解决时间序列问题却非常少见。
Transformer的不足之处:
-
平方时间复杂:Self-Attention 的二次计算:Self-Attention Mechanism 的点积操作使各层的时间复杂度和内存使用量均为 o ( L 2 ) o(L^2) o(L2);
-
高内存占用:作用于长输入的堆叠层存在内存瓶颈:J 个 encoder/decoder layer 的堆叠使得总内存使用量为 o ( J ∗ L 2 ) o(J * L^2) o(J∗L2),这限制了模型在接收长输入时的可伸缩性;
-
现有编码结构的局限性:预测长输出时速度骤降:因为原始的Transformer的decoder在inference的过程中是动态输出的,即上一次的Output是下一次的Input,进而和隐藏层一起再预测下一次的Output,使得Transformer的Inference过程和RNN一样慢。
Long sequence predicion 本文所研究的问题,关注的是LSTF的output的准确性,以及长序列预测中input的统一表征,建立一个Long sequence output 与 Long sequence input 之间的映射关系 。Informer的目标是解决长序列持续预测问题,这个问题与之前LSIL(长序列输入表征)问题很相似。
1.3 贡献
(1)提出的 Informer 在 LSTF 问题中成功提高了预测能力,验证了类 Transformer 模型在捕获长序列时序输出和输入之间的个体长期依赖性方面具有潜在价值;【证明了 Transformer 模型在捕获长期依赖关系方面的作用】
(2)提出了 ProbSparse Self-Attention Mechanism
并有效取代了常规的 Self-Attention Mechanism ,使得时间和空间复杂度降为
o
(
L
∗
l
o
g
L
)
o(L*logL)
o(L∗logL)。【降低了常规 Self-Attention 计算复杂度和空间复杂度】
(3)提出了在 J-Stacking Layers
中控制 attention scores 的 Self-Attention Distilling 操作权限,并将总空间复杂度降低到
o
(
(
2
−
ε
)
∗
l
o
g
L
)
o((2-\varepsilon)*logL)
o((2−ε)∗logL) ;【使用自注意蒸馏技术缩短每一层的输入序列长度,降低了 J 个堆叠层的内存使用量】
(4)提出了 Generative Style Decoder
,只需要一个前向步骤(而非 step by step)就可以获得长序列输出,同时避免了预测阶段的累计误差传播。时间复杂夫降为
O
(
1
)
O(1)
O(1)。【改变解码方式直接一步输出结果】
2. Informer框架
问题描述:在
t
t
t 时刻,已知序列,
X
t
=
x
1
t
,
.
.
.
,
x
L
x
t
X_t = {x_1^t, ... , x_{Lx}^t}
Xt=x1t,...,xLxt,求
Y
t
=
y
1
t
,
.
.
.
,
y
L
u
t
Y_t = {y_1^t, ... , y_{Lu}^t}
Yt=y1t,...,yLut,编码器-解码器结构,编码器将输入状态表示
X
t
X_t
Xt转为隐藏状态表示
H
t
H_t
Ht,解码器将隐藏状态表示
H
t
H_t
Ht转为输入
Y
t
Y_t
Yt
- 红色圈:用 ProbSparse Self-attention 代替了 self-attention ,采用一种新的attention机制减少了计算量
- 蓝色圈:Self-attention Distilling,减少维度和网络参数量
- 黄色圈:Layer stacking replicas 提高了鲁棒性
- 绿色圈:Decoder 获得长序列输入,目标部分用0进行padding
- 紫色圈:Generative style预测目标部分,一步生成预测,而不是动态解码
2.1. ProbSparse self-attention
现在的很多改进self-attention的工作,缺少理论分析,而且对于multi-head self-attention中的每一个head都采用相同的优化策略。
2.1.1 Efficient Self-Attention Mechanism
Transformer中的self-attention模块中的Attention(Q,K,V)如下:
其中如果用 q i , k i , v i q_i, k_i, v_i qi,ki,vi代表QKV矩阵中的第 i i i行,那么可以把最终输出𝑍的第 i i i行表示成:
k ( q i , k j ) k(q_i,k_j) k(qi,kj)其实是非对称的指数核函数 e x p ( q i k j T d ) exp(\frac{q_i k_j^T}{\sqrt{d}}) exp(dqikjT);
那么也就是用
p
(
k
j
∣
q
i
)
p(k_j | q_i)
p(kj∣qi)对值向量也就是
V
V
V矩阵进行加权求和。
这部分结果其实是一个2*2的矩阵,像下面这样。经过softmax之后变成了概率矩阵。也就是其中的0.88,0.12代表的是,“在”这个字分配给“在”它本身和“电”这个字的注意力概率分别为0.88和0.12.用这两个概率分别和他们对应的值向量加权,再求和,从而得到最终的输出向量z。
上面回顾了传统的注意力机制,引入了注意力概率矩阵更便于理解,传统Self-attention需要 O ( L Q L K ) O(L_QL_K) O(LQLK)的内存以及二次点积计算代价,是其预测能力的主要缺点。通过研究发现,稀疏性self-attention得分形成长尾分布,即少数点积对主要注意有贡献,其他点积对可以忽略。
也就是说上面的概率矩阵不一定都是(0.88,0.12)这样有侧重的分布(也就是Active Query),有可能是0.5,0.5这样接近均匀分布(Lazy Query)。类似于原文作者画的下面的图。
最基本的一个思路就是降低Attention的计算量,仅计算一些非常重要的或者说有代表性的Attention即可,一些相近的思路在近期不断的提出,比如Sparse-Attention,这个方法涉及了稀疏化Attention的操作,来减少Attention计算量.然后涉及的呈log分部的稀疏化方法, LogSparse-Attention 更大程度上减小Attention计算量,再比如说Restart+LogSparse。
但是它们都具有随机局限性,是一类启发式方法,在多头视角下,这些注意力为每个头生成相同的稀疏的“查询-键对”(query-key pairs),可能会造成严重的信息丢失。
实际上,点积后的结果服从long tail distribution(长尾分布),也就是说,少量的query和key的点积结果是dominating(显著的)。(实际上也是比较合理的,因为某个元素可能之和几个元素高度相关,其他的并无显著关联)。
2.1.2 Query Sparsity Measurement
那么如何对这里的active和lazy进行量化区分呢?作者认为突出的点积对导致对应的query的注意力概率分布远离均匀分布。
那么提出一个比较自然的想法:度量两种分布的距离:运用KL散度公式。
KL散度(Kullback-Leibler divergence)又称相对熵(relative entropy)是描述两个概率分布P和Q差异的一种方法,是非对称的。
KL散度定义如下:
基于这样的结果,引入了ith query’s ( qi 's) attention:
由于想要得到domonating(显著的)的结果,所以将注意力概率分布 p ( k i , q i ) p(k_i, q_i) p(ki,qi) 与uniform distribution(均匀分布) q ( k i , q i ) q(k_i, q_i) q(ki,qi)进行比较。(esimate 两个distribution之间的KL divergence,如果分布 p ( k i , q i ) p(k_i, q_i) p(ki,qi) 和分布 q ( k i , q i ) q(k_i, q_i) q(ki,qi)相差大,也就是sparsity measurement大,那么 第 i i i个query sparsity ( q i q_i qi) 就更有机会dominate点积结果):
计算过程如下:
舍弃常数项,最终定义第i个query sparsity(查询向量稀疏性)的评估如sparsity measurement第一项是 $q_i $和所有keys内积的log-sum-exp,第二项是他们的算术平均。
如果第 i i i个 query 获得了较大的 M ( q i , K ) M(q_i, K) M(qi,K),则其 attention probability p p p 更具区别性,且更有可能包含长尾 self-attention 分布的头部区域中的主要点积对。如下图红框区域:
2.1.3 ProbSparse Self-Attention
然而这样的计算依然有
O
(
L
Q
L
K
)
O(L_QL_K)
O(LQLK)的内存使用,并且LSE存在潜在的数值稳定性问题。据此提出query sparsity评估的近似:
近似过程如图所示:
定理:
上述计算过程只涉及到取 max、求和即点积,比起 LSE 操作要简单得多。
但是,如果对每个 query 都计算稀疏性得分会带来额外的计算量,因此可以利用点积结果服从长尾分布的假设,从而在计算每个 query 稀疏性得分时,仅需与采样出的部分 key 进行计算就可以了。
基于所提出的度量,文中只通过允许每个 key 只关注 u u u 个主要 query 来实现 Probsparse Self-Attention :
其中 Q ˉ \bar{Q} Qˉ 是大小与 q q q 相同的稀疏矩阵且其只包含稀疏度量 M ( q , K ) M(q,K) M(q,K) 下的 Top- u u u 个 query 。加入一个采样因子(超参数) c c c ,设定 u = c ∗ l n L Q u = c * lnL_Q u=c∗lnLQ ,这使得 ProbSparse Self-Attention 算法仅需为每个 query-key lookup 计算 O ( l n L Q ) O(lnL_Q) O(lnLQ)点积,则 layer 的内存使用量为 O ( L K l n L Q ) O(L_KlnL_Q) O(LKlnLQ)。
上述说法也可理解为:
随机选择 $U=L_QlnL_K $个点积对计算 $\bar M(q_i,K) $这样使复杂度降低到 O ( L l n L ) O(LlnL) O(LlnL),选出其中的top u个 queries 也就是查询向量。在代码的默认参数中U使25。
那么这里的25个queries就是作者认为相对远离均匀分布,也就是不那么lazy的分布。
传统transformer计算attention的过程:
而probsparse使用 Q ˉ \bar{Q} Qˉ计算
输入: 32 × 8 × 96 × 64 ( 8×64=512 ,这也是多头的原理,即8个头)
输出: 32 × 8 × 25 × 64
因此,在长尾分布下,只需要随机抽样 U = L Q l n L K U = L_QlnL_K U=LQlnLK个点积对集合计算 M ˉ ( q i , K ) \bar M(q_i,K) Mˉ(qi,K),用零填充其他对。选择稀疏的 Top- u u u 作为 Q ˉ \bar{Q} Qˉ 。 M ˉ ( q i , K ) \bar M(q_i,K) Mˉ(qi,K)中的 max-operator 对零值较不敏感,且数值稳定。实际上,query 和 key 的输入长度通常相同,即 L Q = L K = L L_Q = L_K = L LQ=LK=L ,使得 ProbSparse Self-Attention 的总时间复杂度和空间复杂度为 O ( L l n L ) O(LlnL) O(LlnL) 。
- 为每个query都随机采样定量的key
- 计算每个query的sparsity measurement M ( q , K ) M(q,K) M(q,K)
- 选择sparsity measurement最高的u个query, u = c ∗ l n L Q u = c * lnL_Q u=c∗lnLQ
- 仅计算u个query和所有key的点积,从而得到attention score
- 其他的query对应的score是将self-attention层的输入取均值(mean(V)),来保证输入和输出序列长度都是 L
ProbSparse Attention 在为每个query随机采样key时,每个head的采样结果是相同的,也就是采样的key是相同的。但是由于每一层self-attention都会先对Q、K、V做线性转换,这使得序列中同一个位置上不同head对应的query、key向量不同,所以每个head的同一个query的sparsity measurement都不相同,这就使得每个head中得到的前u个measurement 最高的query也是不同的。这也等价于每个head都采取了不同的优化策略。
2.1.4 Lazy queries 的处理和多头拼接
从上可以看出来,其实用$ \bar{Q} $计算 attention,得到的 z 是 25*96 维的,也就是说只表示了25个 active queires 对应的时间点自注意力编码后的向量。那么剩下的时间点怎么办呢?
作者采取的办法是,用V向量的平均来代替剩余的 Lazy queries 对应的时间点的向量,如下图所示。32×8×25×64 -> 32 × 8 × 96 × 64 的过程
这样做的原因是,通过判定得到的 Lazy queries 的概率分布本来就接近均匀向量,也就是说这个时间点对96个时间点的注意力是均衡的,每个都差不多,所以他们atteniton计算后的向量也应当接近于全部V向量的平均。
2.2 Encoder: self-attention distilling
Encoder 处理长序列输入:论文中提出的EncoderStack 其实是由多个Encoder 和蒸馏层组合而成的。
编码器的 feature map中存在 V 值的冗余组合,利用 distilling 操作对具有主要特征的优势特征赋予更高的权重,并在下一层生成聚焦的 self-attention feature map。
从下图中 attention block 的 n-heads 权重矩阵(重叠的红色方块)可见,它大幅削减了输入的时间维度。受 dilated convolution 的启发,distilling 从第 j 层推进到第 j+1 层的过程为:
其中 [ . ] A B [.]_{AB} [.]AB 包含 Multi-Head ProbSparse self-attention 以及 attention block 的关键操作;Conv1d 表示在时间序列上的一维卷积操作(核宽度为3),并通过 ELU 作为激活函数。在堆叠一个层之后,添加一个步长为 2 的 max-pooling 层,并对 x T x^T xT 进行下采样将整个内存使用量减少到 o ( ( 2 − ε ) ∗ l o g L ) o((2-\varepsilon)*logL) o((2−ε)∗logL),其中 ε \varepsilon ε是一个小数值。
-
每个水平stack代表一个单独的Encoder Module;
-
上面的stack是主stack,它接收整个输入序列,而第二个stack取输入的一半,并且第二个stack扔掉一层自我注意蒸馏层的数量,使这两个stack的输出维度使对齐的;
-
红色层是自我注意机制的点积矩阵,通过在每层上施加自我注意蒸馏来使级联降低; 此操作也就是蒸馏的操作,即convLayer层,通过一维卷积来进行蒸馏的。
-
将2个堆栈的特征图视为编码器的输出。最后很简单,把手画图里的Encoder1 和 Encoder2 25和26的输出连接起来,就能得到最终的51维输出了。(这里的51或许还是因为卷积取整,导致这个数看起来不太整)
-
attention block 1到attention block 2之间需要进行一个convld操作,每层 O ( L l o g L ) O(L log L) O(LlogL)减半。
也就是说,下面用红色笔圈出来的这一坨是第一个stack,而剩下那部分是第二个stack,下面的stack还写了similar operation
红色圈圈的部分中输入减半,作为第二个stack的输入。(这里减半的处理方法就是直接用96个时间点的后48个得到一半)
作者说前面我们是在计算复杂度和空间复杂度上做了缩小,进一步对attention 做简化操作,由于每一层的输入输出,不同的head都存在一定的规律性,还有更重要的一点是ProbSparse self-attention有很多都是用mean填充的,所以天然就存在冗余的attention sorce ,因此应用卷积与池化来进行减半操作,是一个合情合理的过程(逐层减半,参数融合,feature map 融合)。
如果你觉得这样做还不够,我们再采用多个encoder,即Encoder Stack 时,再对输入也进行减半,Attention Block 结构一样,也就是同(这样操作实验证明对一些短周期的数据集可能会更有效)
输入也进行减半,也可增强 distilling 操作的鲁棒性,我们构建了主栈的一半副本,并通过一次丢弃一层来逐步减少 self-attention distilling layers 的数量,从而使它们的输出维度对齐。因此,将所有堆栈的输出串联起来,可得到 encoder 的最终隐藏表示。
**2.2 **Decoder: generative inference
decoder 的输入有两部分,包含解码器输出的concattenated feature map 和 词向量embedding输入。第一个32×51×512 是 encoder 的输出。第二个 32 × 72 × 512 是 embedding 后的 decoder 的输入,即用0遮住了后半部分的输入。
下图就是decoder 的第二个输入,从图中可以看出,上面的是encoder的输入,下面是decoder,decoder 的后半部分被0遮盖住。整个模型的目的即为预测被遮盖的decoder的输出的部分。
在此,使用标准的 decoder 结构,由2个相同的multi-head attentionlayer 堆叠组成。但是,在长期预测问题中,利用 generative inference 可缓解速度骤降。将下述向量馈入解码器:
其中, X t o k e n t X_{token}^t Xtokent 为 start token, X 0 t X_{0}^t X0t是目标序列的占位符(将标量设为0)。通过把 mask 的点积设为负无穷,可将 Masked multi-head attention 应用于 ProbSparse self-attention。它可以防止每个位置都关注未来的位置,从而避免了自回归。一个全连接层获得最终的输出,其超大的 d y d_y dy 取决于我们是在执行单变量预测还是在执行多变量预测。
start token 是 NLP的"dynamic decoding"中的一种有效技术,在此我们将其扩展为一种生成性的方式。我们不选择特定的标志作为 token,而是在输入序列中采样一个 L t o k e n L_{token} Ltoken 长序列,该序列是输出序列之前的较早片段。以预测168个点为例(7天温度预测),将目标序列已知的前5天的值作为"start token",并将 X f e e d d e = X 5 d , X 0 X_{feedde} = { X_{5d}, X_{0}} Xfeedde=X5d,X0 馈入到生成式推理解码器。 X 0 {X_0} X0包含目标序列的时间戳,即目标周的上下文。注意,本文提出的 decoder 通过一个前向过程预测所有输出,并且不存在常规解码器中耗时的“dynamic decoding”。
transformer step-by-step 动态解码是逐步进行的,在获取上一步输出后,作为下一步的输入,逐步输出结果。
而 Informer 提出了一种一步到位的预测,整个流程如下图所示。
-
1.最上面的 Encoder1 对应的是Encoder Stack 里的主 Stack, 而 Encoder2 则是将输入减半后的第二个 Stack。将他们的输出连接,得到 32∗51∗512 的输出。也就是decoder的一部分输入。用这部分输入得到 keys 和values。
2.decoder 的第二部分输入,即将预测部分用0遮盖后的输入向量计算 ProbAttention,也就是和Encoder的ProbSparse Self-Atteniton类似的操作,这里与encoder中不同的地方是 为了防止提前看到未来的部分进行了mask,原理如下图所示。
同时 Lazy queries 不再用 mean(V) 填充,而是用 Cumsum(累计),也就是每一个queries之前所有时间点V向量的累加,来防止模型关注未来的信息。也就是将下图的mean 改成累加
利用 ProbAttention 的结果计算得到 queries。最终对这样的 queries, keys, values 计算 Full Attention, 得到想预测的部分(之前被0遮盖的部分)。这样使得模型通过一个前向过程直接预测所有输出,而不需要step - by-step 的动态解码。
因此,所用的generative inference 可以一次decode所有元素,比dynamic decoding (step-by-step) 更加高效。
3. 论文源码
3.1 pytorch EarlyStopping
早停机制原理: EarlyStopping的原理是提前结束训练轮次来达到“早停“的目的,故训练轮次需要设置的大一点以求更好的早停(比如可以设置100epoch)。
首先,我们需要一个一个标识,可以采用**‘val_acc’(测试集准确率)或者‘val_loss’(测试集损失值)等等,这些量在每一个轮次中都会不断更新自己的值,也和模型的参数息息相关**,所以我们想通过他们间接操作模型参数。以val_loss举例,当模型训练时可能会出现当val_loss到一定值的时候会出现回弹的情况,所以我们希望在loss值回弹之前结束模型的训练。
最简单的一种的早停方法:
参数有5个:
第一个patience:这个是当有连续的patience个轮次数值没有继续下降,反而上升的时候结束训练的条件(以val_loss为例)
第二个verbose:这个其实就是是否print一些值,可也不传参,因为他有默认值
第三个delta:这个就是控制对比是的”标准线“
第四个path:这个是权重保存路径,早停法会在每一轮次次产生最优解(就是val_loss继续减少)的时候保存当前的模型参数。
注:只要保存路径不变,每一次保存在文件里面的参数都会覆盖上一次保存在文件里面的参数。
第五个trace_func:这个就是显示每一个轮次变化的数值的方式,默认print,也可以改成进度条显示(进度条库 tqdm的对象)
class EarlyStopping:
def __init__(self, patience=7, verbose=False, delta=0):
self.patience = patience
self.verbose = verbose
self.counter = 0
self.best_score = None
self.early_stop = False
self.val_loss_min = np.Inf
self.delta = delta
def __call__(self, val_loss, model, path):
score = -val_loss
if self.best_score is None:
self.best_score = score
self.save_checkpoint(val_loss, model, path)
elif score < self.best_score + self.delta: //与最好的loss对比
self.counter += 1
print(f'EarlyStopping counter: {self.counter} out of {self.patience}')
if self.counter >= self.patience: //当超过耐心值时就会停止模型训练,以防止过拟合了
self.early_stop = True
else:
self.best_score = score
self.save_checkpoint(val_loss, model, path)
self.counter = 0
def save_checkpoint(self, val_loss, model, path):
if self.verbose:
print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model ...')
torch.save(model.state_dict(), path+'/'+'checkpoint.pth')
self.val_loss_min = val_loss
重点就在中间那个__call__方法里面,比较的是这一轮的val_loss和之前最好的val_loss(可以加上一个数实现‘标准线’的‘上移’或者‘下移’)
3.2 实际模型应用
# 训练步数
train_steps = len(train_loader)
# 提前停止
early_stopping = EarlyStopping(patience=self.args.patience, verbose=True)
# 模型优化器
model_optim = self._select_optimizer()
# 损失函数
criterion = self._select_criterion()
for epoch in range(self.args.train_epochs):
epoch_count += 1
iter_count = 0
# 存储当前epoch下的每个迭代步的训练损失
train_loss = []
-------------------
# 完成每个epoch的训练就打印一次
print("Epoch: {0}, Steps: {1} | Train Loss: {2:.7f} Vali Loss: {3:.7f} Test Loss: {4:.7f}".format(
epoch + 1, train_steps, train_loss, vali_loss, test_loss))
# 判断是否提前停止
early_stopping(vali_loss, self.model, path)
if early_stopping.early_stop:
print("Early stopping")
break