赞
踩
前面已经写了一系列有关LSTM时间序列预测的文章:
Attention机制虽然上个世纪90年代在CV领域便已提出,但它却是在17年谷歌提出transformer后才开始真正火起来,到如今Attention已经成了灌水论文的必备trick,以至于当顶会/顶刊reviewer一看到标题中有Attention
字样时,便已经给该论文打上了不够novel
的标签。
不过灌水归灌水,Attention机制确在大多数领域确实是很work的,但是相比于NLP领域,我始终感觉Attention在纯时序预测领域没有太大的用处。这篇文章主要浅谈一下时间序列预测中常见的几种Attention机制,同时给出可即插即用的代码。在本文中,按照执行机制的位置Attention被分为输入Attention和输出Attention,按维度分为时间步Attention和变量Attention,按注意力实现方式分为:点积、缩放点积、余弦相似度、通用(矩阵乘)、加性、拼接等6种,总共 2 × 2 × 6 = 24 2 \times 2 \times 6 =24 2×2×6=24种。
LSTM/RNN的具体原理可以参考深入理解PyTorch中LSTM的输入和输出(从input输入到Linear输出),在这篇文章中,输入到LSTM中的数据x
的维度为(batch_size, seq_len, input_size)
,经过LSTM后得到的输出output
的维度为(batch_size, seq_len, num_directions * hidden_size)
,其中LSTM和BiLSTM的num_directions分别为1和2,为了书写方便,我们令output
的维度为(batch_size, seq_len, hidden_size)
。
所谓Attention机制,就是对于给定目标,通过生成一个权重系数对输入进行加权求和,来识别输入中哪些特征对于目标是重要的,哪些特征是不重要的。
LSTM中注意力机制根据使用的位置可以分为两种:对x
使用Attention和对output
使用Attention,而对于这两种,都可以选择对时间步或变量维度执行Attention。
输入Attention,即在将x
送入LSTM前执行Attention。由于x (batch_size, seq_len, input_size)
。对x
使用Attention主要分为两种:一种是对时间步维度即seq_len
执行,一种是对变量维度即对input_size
执行。为了便于讨论,我们令x(batch_size=256, seq_len=24, input_size=7)
和output(batch_size=256, seq_len=24, input_size=64)
。
对于seq_len
维度,Attention的目标是区分时间步之间的重要性。当我们利用前24个时刻点的数据预测未来的数据时,如果不使用Attention,输入进LSTM的24个长度为7的向量间是没有太多关联的,为了让所有时间步之间有所关联,我们可以对24个向量执行注意力机制,让每个向量都是所有24个向量的加权组合,这样就可以让每一个时间步的向量中包含其余时间步的信息,并且这种信息可以通过注意力权重来区分重要性,对当前时间步的越重要的时间步的权重越大。
对于input_size
维度,Attention的目标是区分所有变量之间的重要性。此时,对于每一个变量,我们都拥有一个长度为24的向量,总共7个长度为24的向量。同理,我们可以让每个变量的长度为24的向量是所有7个向量的线性加权,这样每一个变量中就包含了其他变量的信息。
输出Attention,即对output(batch_size, seq_len, hidden_size)
执行Attention。与输入Attention一样,输出Attention也分为两种:seq_len
维度Attention和hidden_size
维度Attention,这里不再赘述。
在注意力机制中,最主要的部分是如何得到向量
h
i
\mathbf{h}_i
hi和向量
h
j
\mathbf{h}_j
hj间的重要性系数
α
i
,
j
\alpha_{i,j}
αi,j,
α
i
,
j
\alpha_{i,j}
αi,j是一个实数,用于表征向量j
对向量j
的重要性。在得到向量
h
i
\mathbf{h}_i
hi与所有向量间的重要性后,向量
h
i
\mathbf{h}_i
hi可以被更新为:
h
i
←
σ
(
∑
α
i
,
j
⋅
h
j
)
\mathbf{h}_i \leftarrow \sigma(\sum \alpha_{i,j} \cdot \mathbf{h}_j)
hi←σ(∑αi,j⋅hj)
其中
σ
\sigma
σ表示非线性的激活函数。
顾名思义,点积方法使用两个向量间的点积来衡量向量间的重要性,点积越大,则两个向量间的关联性越强,相应的
α
i
,
j
\alpha_{i,j}
αi,j也就越高:
α
i
,
j
=
h
i
⊤
h
j
\alpha_{i,j} = \mathbf{h}_i^{\top}\mathbf{h}_j
αi,j=hi⊤hj
基于点积的seq_len
注意力机制可以实现如下:
def att_dot_seq_len(self, x):
# b, s, input_size / b, s, hidden_size
x = self.attention(x) # bsh--->bst
e = torch.bmm(x, x.permute(0, 2, 1)) # bst*bts=bss
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
return out
其中
self.attention = nn.Linear(hidden_size, t)
其作用是对输入进行变换,以得到更高级的表征。一般来讲,如果是对原始输入执行Attention,我一般不选择执行这一步,以防止变量个数发生变化,当然执行也是可以的。x.permute(0, 2, 1)
的维度大小为(batch_size, input_size/hidden_size, seq_len)
,两者相乘得到e(batch_size, seq_len, seq_len)
,对于后两个维度(seq_len, seq_len)
,(i, j)
就表示向量
h
i
\mathbf{h}_i
hi和向量
h
j
\mathbf{h}_j
hj的点积。值得注意的是,e
应该是一个对称矩阵,因为向量的点积具有对称性,即
h
i
⊤
h
j
=
h
j
⊤
h
i
\mathbf{h}_i^{\top}\mathbf{h}_j=\mathbf{h}_j^{\top}\mathbf{h}_i
hi⊤hj=hj⊤hi。
接着,我们使用softmax函数来让一个向量的所有的注意力系数之和为1,即:
attention = F.softmax(e, dim=-1)
例如对于第一个样本中的第i
个时间步,我们有
∑
k
=
1
s
e
[
0
,
i
,
k
]
=
1
\sum_{k=1}^{s} e[0, i, k] = 1
∑k=1se[0,i,k]=1。最后,使用矩阵乘法进行加权组合:
out = torch.bmm(attention, x)
此时out
中每一个时间步上的向量都是其他所有时间步向量的线性加权。
基于点积的input_size/hidden_size
注意力机制可以实现如下:
def att_dot_var(self, x):
# b, s, input_size / b, s, hidden_size
e = torch.bmm(x.permute(0, 2, 1), x) # bis*bsi=bii
attention = F.softmax(e, dim=-1) # b i i
out = torch.bmm(attention, x.permute(0, 2, 1)) # bii * bis ---> bis
out = F.relu(out.permute(0, 2, 1))
return out
这里过程不再叙述。
缩放点积在点积的基础上除以了向量的长度
d
d
d,即:
α
i
,
j
=
h
i
⊤
h
j
d
\alpha_{i,j} = \frac{\mathbf{h}_i^{\top}\mathbf{h}_j}{\sqrt{d}}
αi,j=d
hi⊤hj
除以
d
\sqrt{d}
d
的目的是为了降低对向量长度的敏感度,使得无论向量的长度如何,点积的方差在不考虑向量长度的情况下仍然是1,方便模型优化,提升网络训练时的稳定性。
有了前面点积的基础,缩放点积的实现也较为简单。以时间步维度为例,缩放点积注意力机制实现如下:
def att_scaled_dot_seq_len(self, x):
# b, s, input_size / b, s, hidden_size
x = self.attention(x) # bsh--->bst
e = torch.bmm(x, x.permute(0, 2, 1)) # bst*bts=bss
e = e / np.sqrt(x.shape[2])
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
return out
其中
e = e / np.sqrt(x.shape[2])
即为缩放操作。
顾名思义,余弦相似度方法使用两个向量间的夹角余弦值来衡量向量间的重要性,余弦值越大,则两个向量间的关联性越强。值得注意的是,余弦的范围为[-1, 1],为了能够进行计算,我们将其归一化到01之间。
基于余弦相似度的seq_len
注意力机制可以实现如下:
def att_cos_seq_len(self, x):
# b, s, input_size / b, s, hidden_size
x = self.attention(x) # bsh--->bst
e = torch.cosine_similarity(x.unsqueeze(2), x.unsqueeze(1), dim=-1) # bss
e = 0.5 * e + 0.5
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
return out
其中计算余弦相似度的代码为:
e = torch.cosine_similarity(x.unsqueeze(2), x.unsqueeze(1), dim=-1)
e[0, 1, 2]
表示第0个样本的第一个时间步和第二个时间步间的余弦相似度,接着将其归一化到01之间:
e = 0.5 * e + 0.5
这一步其实可有可无,因为softmax可以将负数归一化到01之间。
基于余弦相似度的input_size/hidden_size
注意力机制可以实现如下:
def att_cos_var(self, x):
# b, s, input_size / b, s, hidden_size
cos = torch.cosine_similarity(x.permute(0, 2, 1).unsqueeze(2),
x.permute(0, 2, 1).unsqueeze(1),
dim=-1) # bii
e = 0.5 * e + e
attention = F.softmax(e, dim=-1) # b i i
out = torch.bmm(attention, x.permute(0, 2, 1)) # bii * bis ---> bis
out = F.relu(out.permute(0, 2, 1))
return out
这里过程不再叙述。
通用Attention的本质是利用简单的矩阵乘法来得到相似度,即:
α
i
,
j
=
h
i
⊤
W
h
j
\alpha_{i,j} = \mathbf{h}_i^{\top}\mathbf{W}\mathbf{h}_j
αi,j=hi⊤Whj
基于矩阵相乘的seq_len
注意力机制可以实现如下:
# x = (batch_size, seq_len, input_size/hidden_size)
seq_len, size = x.shape[1], x.shape[2]
w = nn.Linear(size, size)
e = torch.matmul(w(x), x.permute(0, 2, 1)) # bss
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
原理比较简单,不再赘述。
基于矩阵相乘的变量维度注意力机制可以实现如下:
# x = (batch_size, seq_len, input_size/hidden_size)
seq_len, size = x.shape[1], x.shape[2]
x = x.permute(0, 2, 1)
w = nn.Linear(seq_len, seq_len)
e = torch.matmul(w(x), x.permute(0, 2, 1)) # bii
attention = F.softmax(e, dim=-1) # b i i
out = torch.bmm(attention, x) # bii * bis ---> bis
out = F.relu(out.permute(0, 2, 1))
简单来讲就是将x
变换维度后再执行seq_len
维度的注意力机制。
加性注意力机制的实现过程如下:
α
i
,
j
=
v
⊤
tanh
(
W
h
i
+
U
h
j
)
\alpha_{i,j}=\mathbf{v}^{\top} \tanh(\mathbf{W} \mathbf{h}_i + \mathbf{U} \mathbf{h}_j)
αi,j=v⊤tanh(Whi+Uhj)
其中
W
\mathbf{W}
W和
U
\mathbf{U}
U都是可学习的参数矩阵。
基于加性的seq_len
注意力机制可以实现如下:
# x = (batch_size, seq_len, input_size/hidden_size)
seq_len, size = x.shape[1], x.shape[2]
w = nn.Linear(size, 128)
u = nn.Linear(size, 128)
v = nn.Parameter(torch.empty(size=(128, 1)))
nn.init.xavier_uniform_(v.data, gain=1.414)
x_1 = self.w(x).repeat(1, seq_len, 1).view(x.shape[0], seq_len * seq_len, -1)
x_2 = self.u(x).repeat(1, seq_len, 1)
e = torch.matmul(torch.tanh(x_1 + x_2), self.v).view(x.shape[0], seq_len, -1) # bss
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
其中x_1
和x_2
是进行了重复操作,方便让每个向量都能和其他所有向量进行相加。
基于加性的变量维度注意力机制可以实现如下:
# x = (batch_size, seq_len, input_size/hidden_size)
seq_len, size = x.shape[1], x.shape[2]
x = x.permute(0, 2, 1) # bis
w = nn.Linear(seq_len, 128)
u = nn.Linear(seq_len, 128)
v = nn.Parameter(torch.empty(size=(128, 1)))
nn.init.xavier_uniform_(v.data, gain=1.414)
x_1 = self.w(x).repeat(1, size, 1).view(x.shape[0], size * size, -1)
x_2 = self.u(x).repeat(1, size, 1)
e = torch.matmul(torch.tanh(x_1 + x_2), self.v).view(x.shape[0], x.shape[1], -1) # bii
attention = F.softmax(e, dim=-1) # b i i
out = torch.bmm(attention, x) # bii * bis ---> bis
out = F.relu(out.permute(0, 2, 1))
原理比较简单,不再赘述。
这里灵感来源于图注意力网络GAT,GAT中使用一个可学习的参数
β
\beta
β来学习两个向量间的注意力参数。具体来讲,对于两个向量
h
i
\mathbf{h}_i
hi和
h
j
\mathbf{h}_j
hj,它们间的注意力系数
α
i
,
j
\alpha_{i,j}
αi,j可以计算如下:
α
i
,
j
=
e
x
p
(
L
e
a
k
y
R
e
L
U
(
β
⋅
[
W
h
i
∣
∣
W
h
j
]
)
)
∑
e
x
p
(
L
e
a
k
y
R
e
L
U
(
β
⋅
[
W
h
i
∣
∣
W
h
k
]
)
)
\alpha_{i,j}=\frac{\mathrm{exp}(\mathrm{LeakyReLU}(\beta \cdot [\mathbf{W}\mathbf{h}_{i} || \mathbf{W}\mathbf{h}_{j}]))}{\sum \mathrm{exp}(\mathrm{LeakyReLU}(\beta \cdot [\mathbf{W}\mathbf{h}_{i} || \mathbf{W}\mathbf{h}_{k}]))}
αi,j=∑exp(LeakyReLU(β⋅[Whi∣∣Whk]))exp(LeakyReLU(β⋅[Whi∣∣Whj]))
其中
∣
∣
||
∣∣表示concatenate操作。简单来讲,我们首先将
h
i
\mathbf{h}_i
hi和
h
j
\mathbf{h}_j
hj通过一个权重矩阵
W
\mathbf{W}
W进行变换,这一步就是前面的x=self.attention(x)
。接着将两个向量进行拼接,再乘上一个可学习的参数
β
\beta
β得到一个常数,然后再利用softmax
进行归一化。
不少文章中的拼接Attention的实现方式为:
α
i
,
j
=
s
o
f
t
m
a
x
(
v
⊤
tanh
(
W
⋅
[
h
i
∣
∣
h
j
]
)
)
\alpha_{i,j}=\mathrm{softmax}(\mathbf{v}^{\top} \tanh(\mathbf{W} \cdot [\mathbf{h}_i || \mathbf{h}_j]))
αi,j=softmax(v⊤tanh(W⋅[hi∣∣hj]))
这与前面相比只是将权重矩阵和激活函数的位置进行了调换,区别不大,这里以第一种为准。
基于拼接的seq_len
注意力机制可以实现如下:
# x (batch_size, seq_len, input_size/hidden_size)
x = self.w(x) # bsi--->bst
seq_len, size = x.shape[1], x.shape[2]
x1 = x.repeat(1, 1, seq_len).view(x.shape[0], seq_len * seq_len, -1)
x2 = x.repeat(1, seq_len, 1)
cat_x = torch.cat([x1, x2], dim=-1).view(x.shape[0], seq_len, -1, 2 * size) # b s s 2*size
e = F.leaky_relu(torch.matmul(cat_x, self.beta).squeeze(-1)) # bss
attention = F.softmax(e, dim=-1) # b s s
out = torch.bmm(attention, x) # bss * bst ---> bst
out = F.relu(out)
这里利用了repeat操作以实现所有向量两两之间的拼接。
基于拼接的变量维度注意力机制可以实现如下:
# x (batch_size, seq_len, input_size/hidden_size)
seq_len, size = x.shape[1], x.shape[2]
x = x.permute(0, 2, 1) # bis
beta = nn.Parameter(torch.empty(size=(2*seq_len, 1)))
nn.init.xavier_uniform_(beta.data, gain=1.414)
x1 = x.repeat(1, 1, size).view(x.shape[0], size * size, -1)
x2 = x.repeat(1, size, 1)
cat_x = torch.cat([x1, x2], dim=-1).view(x.shape[0], size, -1, 2 * seq_len) # b i i 2*seq_len
e = F.leaky_relu(torch.matmul(cat_x, beta).squeeze(-1)) # bii
attention = F.softmax(e, dim=-1) # b i i
out = torch.bmm(attention, x) # bii * bis ---> bis
out = F.relu(out.permute(0, 2, 1))
这里只是将x
交换了维度,然后执行了与时间步注意力一样的操作。
在深入理解PyTorch中LSTM的输入和输出(从input输入到Linear输出)中我们提到,在x
经过LSTM变成(batch_size, seq_len, hidden_size)
后,我们只需要取最后一个时间步的(batch_size, hidden_size)
进行映射,前面的时间步注意力机制可以让最后一个时间步的向量是其他所有时间步的线性组合,因此我们就同时利用了所有时间步的信息。
为了利用全部时间步的信息,最简单的一种方法便是将所有时间步展开得到一个大小为(batch_size, seq_len * hidden_size)
的矩阵,然后再进行映射以得到最终输出。
为了探究24种+Flatten总共25种方法的效果,这里以前面PyTorch搭建LSTM实现多变量时间序列预测(负荷预测)中的设置为准。
数据集:
本次实验中使用1-13总共13变量的前24个时刻的值来对未来12个时刻点的net_demand进行预测,即seq_len=24, input_size=13
。实验中所有LSTM都为单向,模型如下所示:
class LSTM(nn.Module):
def __init__(self, args):
super().__init__()
self.args = args
self.input_size = args.input_size
self.hidden_size = args.hidden_size
self.num_layers = args.num_layers
self.output_size = args.output_size
self.num_directions = 1
self.lstm = nn.LSTM(self.input_size, self.hidden_size, self.num_layers, batch_first=True)
self.linear = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input_seq):
batch_size, seq_len = input_seq.shape[0], input_seq.shape[1]
h_0 = torch.randn(self.num_directions * self.num_layers, batch_size, self.hidden_size).to(device)
c_0 = torch.randn(self.num_directions * self.num_layers, batch_size, self.hidden_size).to(device)
output, _ = self.lstm(input_seq, (h_0, c_0))
pred = self.linear(output)
pred = pred[:, -1, :]
return pred
以点击和缩放点积的Attention为例,添加注意力机制后的LSTM模型如下所示:
class Dot_LSTM(nn.Module):
def __init__(self, args,
dim_type,
where,
use_trans,
use_scaled):
super().__init__()
self.dim_type = dim_type # seq_len or size 时间步Attention或变量Attention
self.where = where # in or out 输入Attention或输出Attention
self.use_trans = use_trans # 是否对原始数据进行变换
self.use_scaled = use_scaled # 是否缩放点积
self.args = args
self.input_size = args.input_size
self.hidden_size = args.hidden_size
self.num_layers = args.num_layers
self.output_size = args.output_size
self.num_directions = 1
self.lstm = nn.LSTM(self.input_size, self.hidden_size, self.num_layers, batch_first=True)
if dim_type == 'seq_len' and use_trans and where == 'in':
self.att = dot_seq_len(input_channels=self.input_size,
output_channels=self.input_size,
use_trans=True,
use_scaled=use_scaled)
elif dim_type == 'seq_len' and use_trans and where == 'out':
self.att = dot_seq_len(input_channels=self.hidden_size,
output_channels=self.hidden_size,
use_trans=True,
use_scaled=use_scaled)
elif dim_type == 'seq_len' and not use_trans:
self.att = dot_seq_len(input_channels=0,
output_channels=0,
use_trans=False,
use_scaled=use_scaled)
elif dim_type == 'size' and use_trans and where == 'in':
self.att = dot_var(input_channels=self.args.seq_len,
output_channels=self.args.seq_len,
use_trans=True,
use_scaled=use_scaled)
elif dim_type == 'size' and use_trans and where == 'out':
self.att = dot_var(input_channels=self.args.seq_len,
output_channels=self.args.seq_len,
use_trans=True,
use_scaled=use_scaled)
elif dim_type == 'size' and not use_trans:
self.att = dot_var(input_channels=0,
output_channels=0,
use_trans=False,
use_scaled=use_scaled)
self.linear = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input_seq):
if self.where == 'in':
input_seq = self.att(input_seq)
batch_size, seq_len = input_seq.shape[0], input_seq.shape[1]
h_0 = torch.randn(self.num_directions * self.num_layers, batch_size, self.hidden_size).to(device)
c_0 = torch.randn(self.num_directions * self.num_layers, batch_size, self.hidden_size).to(device)
output, _ = self.lstm(input_seq, (h_0, c_0))
if self.where == 'out':
output = self.att(output)
output = output[:, -1, :]
pred = self.linear(output)
return pred
传入的参数包括:
dim_type: seq_len (时间步) or size (变量)
where: in (输入) or out (输出)
use_scaled: False (点积) True (缩放点积)
use_trans: False (不使用维度变换) True (使用维度变换)
总共
2
×
2
×
2
=
8
2 \times 2 \times 2 = 8
2×2×2=8种模型,个人感觉不需要对原始的输入进行变换,因此8个模型都有use_trans=False
。
相关实验结果如下表所示:
模型 | LSTM | |||||||
---|---|---|---|---|---|---|---|---|
MAPE/(%) | 6.36 | |||||||
模型 | Dot | ScaledDot | ||||||
MAPE/(%) | 7.86 | 6.77 | 7.54 | 6.89 | 11.79 | 6.96 | 7.14 | 7.01 |
模型 | Cos | Gen | ||||||
MAPE/(%) | 11.80 | 7.30 | 11.80 | 7.23 | 7.80 | 6.55 | 7.26 | 7.42 |
模型 | Add | Concat | ||||||
MAPE/(%) | 11.37 | 7.16 | 7.33 | 11.39 | 11.88 | 7.21 | 7.37 | 6.55 |
模型 | Flatten | |||||||
MAPE/(%) | 5.95 |
表格中以Dot开始的四个模型分别表示点积输入时间步、点积输出时间步、点积输入变量、点积输出变量,其他类似。
结果分析:
当然以上只是LSTM+Attention,其他模型如CNN-LSTM、GNN-LSTM等加Attention可能会有效果,后期会进行调参优化~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。