自从Google发布那篇具有划时代意义的文章 以来,Transformer及其他变体就迅速在各种NLP和CV任务达到SOTA。Transformer舍弃了卷积以及递归网络,仅使用了Attention机制。正如这篇文章的标题所说的:Attention Is All You Need . 相较于RNN和CNN,transformer具有这些优势:
更好的并行计算能力,而RNN需要等待前一个Token的结果;
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) x = self.dropout((self.tok_embedding(x) * self.scale) + self.pos_embedding(pos)) return x
2. Multi-Head Attention 注意力机制无疑是这篇文章的重点。对于一组给定的句子(序列) , 其中 的维度为 , 经过上述的Embeding后得到一个三维向量 .
接着对 做线性映射 ,分配三个权重 ,将三个权重再与原来的 相乘做线性映射,最后一个维度抵消,得到三个新的矩阵 .即Q uery, K ey与V alue。
接下来沿着 的维度将三个矩阵分为 份, 为超参数,指注意力头的个数。注意 必需被 整除。分割后的矩阵维度为 (万恶的四维矩阵). 为了方便计算与理解,将 与 交换维度,并取出其中一份注意力头,每一份的维度为
接着计算 与 的转置,上图中的 代表第 个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 ] Q = self.fc_q(query) K = self.fc_k(key) V = self.fc_v(value) 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 ) energy = torch.matmul(Q, K.permute(0 , 1 , 3 , 2 )) / self.scale if mask is not None : energy = energy.masked_fill(mask == 0 , -1e10 ) attention = torch.softmax(energy, dim=-1 ) x = torch.matmul(self.dropout(attention), V) x = x.permute(0 , 2 , 1 , 3 ).contiguous() x = x.view(batch_size, -1 , self.hid_dim) x = self.fc_o(x) return x, attention
3. Normalization Layer与残差连接 在上一步我们得到了加权后的 , 即 , 将其转置后将其维度和 一致,为 , 然后两者进行元素相加,得到残差连接:
在之后的运算里, 每经过一个模块的运算,都要把运算之前的值和运算之后的值相加,训练的时候可以使梯度直接走捷径反传到最初始层:
而标准化的作用是把神经网络中隐藏层归一为标准正态分布, 也就是 独立同分布, 以起到加快训练速度, 加速收敛的作用.
下面来梳理一下Transformer的整体结构, 和 的差异主要在 多了一个多头注意力层。
4.1 字向量与位置向量的嵌入
4.2 将得到的 进行注意力和残差的计算:
计算注意力
计算残差与标准化
Decoder的第二个Attention层
对于 来说还要将上述的 再进行一次注意力和残差的计算,此时由 的输出贡献 和
4.3 将 输入一个前向神经网络,增加模型的非线性
5.参考资料
Attention Is All You Need
a_journey_into_math_of_ml -aespresso的transformer图解
pytorch-seq2seq -bentrevett的开源代码