浅尝Transformer
Kotori Y 27 Posts

自从Google发布那篇具有划时代意义的文章以来,Transformer及其他变体就迅速在各种NLP和CV任务达到SOTA。Transformer舍弃了卷积以及递归网络,仅使用了Attention机制。正如这篇文章的标题所说的:Attention Is All You Need. 相较于RNN和CNN,transformer具有这些优势:

  • 更好的并行计算能力,而RNN需要等待前一个Token的结果;
  • Transformer的视野是全局的;
  • 自注意力可以产生更具可解释性的模型。

0. Transformer架构总览

图.1 Transformer框架

和其他传统的架构一样,Transformer也使用了Encoder-Decoder的架构。其中Encoder将符号表征的输入序列映射到一个连续的向量。对于一个给定的,Decoder将其输出为输出序列. 在每一步,模型都是自回归的,在生成下一个时使用先前生成的符号作为额外的输入。

1. Positional Encoding

由于Transformer没有RNN中的迭代的操作,特此引入了位置编码(Positional Encoding, PE)的概念,其维度和输入的Embedding的维度一致,为。文章中使用来给模型来提供位置信息:

分别用上面的函数做处理, 从而产生不同的周期性变化, 而位置编码在维度上随着维度序号增大, 周期变化会越来越慢, 而产生一种包含位置信息的纹理, 每一个位置在维度上都会得到不同周期的函数的取值组合, 从而产生独一的纹理位置信息, 模型从而获取位置之间的依赖关系和自然语言的时序特性。

正如上面所说的,原始的Transformer模型没有去实现位置嵌入(Positional Embedding)的学习,它使用固定的值对每个固定的位置。 现代 Transformer 架构,如 BERT,则使用Positional Embedding作为代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TransformerEmbedding(nn.Module):
def __init__(self, x_dim, max_length, embedding_dim, dropout, device):
super().__init__()

self.device = device

self.tok_embedding = nn.Embedding(x_dim, embedding_dim)
self.pos_embedding = nn.Embedding(max_length, embedding_dim)

self.scale = torch.sqrt(torch.LongTensor([embedding_dim])).to(device)
self.dropout = nn.Dropout(dropout)

def forward(self, x):
batch_size, length = x.shape
pos = torch.arange(0, length).unsqueeze(0).repeat(batch_size, 1).to(self.device)
# pos = [batch_size, length]

x = self.dropout((self.tok_embedding(x) * self.scale) + self.pos_embedding(pos)) # 缩放减小方差
# # x = [batch_size, length, embedding_dim]

return x

2. Multi-Head Attention

注意力机制无疑是这篇文章的重点。对于一组给定的句子(序列), 其中的维度为, 经过上述的Embeding后得到一个三维向量.

接着对做线性映射,分配三个权重,将三个权重再与原来的相乘做线性映射,最后一个维度抵消,得到三个新的矩阵.即Query, Key与Value。

接下来沿着的维度将三个矩阵分为份,为超参数,指注意力头的个数。注意必需被整除。分割后的矩阵维度为(万恶的四维矩阵). 为了方便计算与理解,将交换维度,并取出其中一份注意力头,每一份的维度为

接着计算的转置,上图中的代表第个Token和第个Token的相关度(相似性)。

通过上述的运算得到注意力矩阵后,需要除以 使矩阵变成正态分布,是的之后更加稳定。注意,对于\字符,需要在注意力矩阵对应的位置填充一个非常小的负数,这样才能通过softmax后的值近乎为0。将经过标准化的注意力矩阵与进行点积运算

最后附上aespresso的图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class MultiHeadAttentionLayer(nn.Module):
def __init__(self, hid_dim, n_heads, dropout, device):
super().__init__()

assert hid_dim % n_heads == 0

self.hid_dim = hid_dim
self.n_heads = n_heads
self.head_dim = hid_dim // n_heads

self.fc_q = nn.Linear(hid_dim, hid_dim)
self.fc_k = nn.Linear(hid_dim, hid_dim)
self.fc_v = nn.Linear(hid_dim, hid_dim)

self.fc_o = nn.Linear(hid_dim, hid_dim)

self.dropout = nn.Dropout(dropout)

self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).to(device)

def forward(self, query, key, value, mask=None):
batch_size = query.shape[0]

# query = [batch size, query len, hid dim]
# key = [batch size, key len, hid dim]
# value = [batch size, value len, hid dim]

Q = self.fc_q(query)
K = self.fc_k(key)
V = self.fc_v(value)

# Q = [batch size, query len, hid dim]
# K = [batch size, key len, hid dim]
# V = [batch size, value len, hid dim]

Q = Q.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
K = K.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
V = V.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)

# Q = [batch size, n heads, query len, head dim]
# K = [batch size, n heads, key len, head dim]
# V = [batch size, n heads, value len, head dim]

energy = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale

# energy = [batch size, n heads, query len, key len]

if mask is not None:
energy = energy.masked_fill(mask == 0, -1e10)

attention = torch.softmax(energy, dim=-1)

# attention = [batch size, n heads, query len, key len]

x = torch.matmul(self.dropout(attention), V)

# x = [batch size, n heads, query len, head dim]

x = x.permute(0, 2, 1, 3).contiguous()

# x = [batch size, query len, n heads, head dim]

x = x.view(batch_size, -1, self.hid_dim)

# x = [batch size, query len, hid dim]

x = self.fc_o(x)

# x = [batch size, query len, hid dim]
# attention = [batch size, n heads, query len, key len]

return x, attention

3. Normalization Layer与残差连接

在上一步我们得到了加权后的, 即, 将其转置后将其维度和一致,为, 然后两者进行元素相加,得到残差连接:

在之后的运算里, 每经过一个模块的运算,都要把运算之前的值和运算之后的值相加,训练的时候可以使梯度直接走捷径反传到最初始层:

而标准化的作用是把神经网络中隐藏层归一为标准正态分布, 也就是独立同分布, 以起到加快训练速度, 加速收敛的作用.

4. Transformer整体架构

下面来梳理一下Transformer的整体结构,的差异主要在多了一个多头注意力层。

4.1 字向量与位置向量的嵌入

4.2 将得到的进行注意力和残差的计算:

  1. 计算注意力

  2. 计算残差与标准化

  3. Decoder的第二个Attention层

    对于来说还要将上述的再进行一次注意力和残差的计算,此时由的输出贡献

4.3 将输入一个前向神经网络,增加模型的非线性

5.参考资料

  1. Attention Is All You Need
  2. a_journey_into_math_of_ml-aespresso的transformer图解
  3. pytorch-seq2seq-bentrevett的开源代码
  • Post title:浅尝Transformer
  • Post author:Kotori Y
  • Create time:2021-11-02 19:54
  • Post link:https://blog.iamkotori.com/2021/11/02/浅尝Transformer/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments