Transformer笔记
本笔记主要供自己复习,只记录一些关键的点。参考链接:http://nlp.seas.harvard.edu/2018/04/03/attention.html#prelims
模型架构
一般的神经序列模型都包含encoder-decoder架构。其中,encoder将输入序列$(x_1,x_2,..,x_n)$的符号表示(symbol representations)映射到连续表示序列$z=(z_1,z_2,…z_n)$。给定$z$,decoder随后一次一个元素地生成输出序列$(y_1,y_2,…,y_m)$。在每个步骤中,模型是自动回归(auto-regressive),在生成下一个时,把先前生成的符号作为附加输入。
1 | class EncoderDecoder(nn.Module): |
1 | class Generator(nn.Module): |
Transformer的模型架构如下:
Encoder and Decoder Stack
Encoder
Transformer的encoder由$N=6$个独立块组成,可看模型框架图得知,位于图的左半部分。
1 | def clones(module, N): |
1 | class Encoder(nn.Module): |
另外,每个layer(块)包括了两个sub-layers. 其中,第一个layer是多头注意力机制,第二个layer是简单的全连接层。
1 | class EncoderLayer(nn.Module): |
另外,从模型图中可以看到,每个子layer中都有残差连接部分,随后跟随着layer norm层。
1 | class SublayerConnection(nn.Module): |
1 | class LayerNorm(nn.Module): |
Decoder
decoder也是由6个块构成。
1 | class Decoder(nn.Module): |
除了每个encoder层中的两个子层之外,decoder还插入第三子层,其对堆叠encoder的输出执行多头注意(multi-head attention)。与编码器类似,我们在每个子层周围使用残差连接(residual connections),然后进行层规范化(layer normalization)。
1 | class DecoderLayer(nn.Module): |
另外,需要修改解码器中的自注意子层(self-attention sub-layer)以防止位置出现在后续位置(subsequent positions)。这种掩蔽与输出嵌入偏移一个位置的事实相结合,确保了位置i的预测仅依赖于小于i的位置处的已知输出。
1 | def subsequent_mask(size): |
我们可以通过图示来展示这个掩码的作用(其中,显示了每个tgt单词(行)允许查看的位置(列)):
Attention
An attention function can be described as mapping a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility function of the query with the corresponding key.
Scaled Dot-Product Attention
这个Attention机制称为”Scaled Dot-Product Attention”。对应公式如下:
实现如下:
1 | def attention(query, key, value, mask=None, dropout=None): |
截止参考博客发出来的时候,比较常用的两种attention机制分别是 additive attention(使用具有单隐层的神经网络来计算compatibility function)和dot-product (multiplicative) attention(这种attention和scaled dot-product attention基本一致,除了多了分母部分)。两者相比,后者的运行速度和空间存储利用更好。
之所以除$\sqrt{d_k}$,解释如下,主要是为了消除点乘的值可能会太大,导致softmax函数进行低梯度空间的情况:
Multi-head Attention
其中,投影矩阵为$W_i^Q \in \mathbb{R}^{d_{model} \times d_k}$, $W_i^K \in \mathbb{R}^{d_{model} \times d_k}$, $W_i^V \in \mathbb{R}^{d_{model} \times d_v}$。在Transformer中,一共用了8个头,其中$d_k=d_v=d_{model}/h$。由于每个头部的维数减少,总的计算成本与全维度的单头部注意的计算成本相似。
1 | class MultiHeadedAttention(nn.Module): |
模型中Attention的应用
在encoder-decoder层中,queries来自先前时刻的decoder输出,而keys和values来自encoder层的输出。这样可以允许每个位置的decoder输出能够注意到输入序列的全部位置,这模仿了典型的encoder-decoder注意机制模型。
encoder层使用了自注意力机制,其中,query,key,value的值都相同,即encoder的输出。这样的话,每个位置的encoder都可以与encoder之前的所有时刻有关联。而self-attention的实际意义是:在序列内部做Attention,寻找序列内部的联系。
在decoder层也使用了自注意力机制,即允许解码器中的每个时刻可以关注在该时刻之前的所有时刻。为了保持decoder层的自回归特性,需要防止解码器中的信息向左流动(因为是并行训练,防止看到后面的信息),因此要通过mask掉输入中所有非法连接的值(设置为负无穷大)。具体可以参考下图,感觉讲的应该有点道理,之后再补充苏剑林老师的理解。
Attention机制的好处
Attention层的好处是能够一步到位捕捉到全局的联系,因为它直接把序列两两比较(代价是计算量变为$O(n^2)$,当然由于是纯矩阵运算,这个计算量也不是很严重);相比之下,RNN需要一步步递推才能捕捉到,而CNN则需要通过层叠来扩大感受野,这是Attention层的明显优势。
Position-wise Feed-Forward Network
每个子层都包含了全连接FFN,分别独立的应用于每个position中。其实它的作用有点类似卷积核大小为1的情况。输入和输出的维度都是$d_{model}=512$,中间隐层维度为$d_{ff}=2048$。
1 | class PositionwiseFeedForward(nn.Module): |
Embeddings and Softmax
1 | class Embeddings(nn.Module): |
Postional Encoding
由于模型中即没有recurrence和convolution,为了可以利用语句中的词序,我们必须将位置信息想办法加进去。因此,模型中对在encoder和decoder的输入处加入了positional encoding。
对应公式如下:
其中,$pos$是position,$i$是维度。
1 | class PositionalEncoding(nn.Module): |
完整模型
1 | def make_model(src_vocab, tgt_vocab, N=6, |
接下里就是模型的训练部分了,不进行讲解。
Optimizer
要学会这种写法。
1 | class NoamOpt: |
正则化
Label Smoothing
在训练中,使用了label smoothing。这种做法会使perplexity增大,as the model learns to be more unsure, but improves accuracy and BLEU score. (中文好差,不知道怎么翻译得好一些)。标签平滑的优势是能够防止模型追求确切概率而不影响模型学习正确分类。
1 | class LabelSmoothing(nn.Module): |
scatter_函数理解举例
1
2
3
4
5
6
7
8
9
10
11x = torch.rand(2, 5)
#0.4319 0.6500 0.4080 0.8760 0.2355
#0.2609 0.4711 0.8486 0.8573 0.1029
# LongTensor的shape刚好与x的shape对应,也就是LongTensor每个index指定x中一个数据的填充位置。
# dim=0,表示按行填充,主要理解按行填充。
torch.zeros(3, 5).scatter_(0, torch.LongTensor([[0, 1, 2, 0, 0], [2, 0, 0, 1, 2]]), x)
# 举例LongTensor中的第0行第2列index=2,表示在第2行(从0开始)进行填充填充,对应到zeros(3, 5)中就是位置(2,2)。 所以此处要求zeros(3, 5)的列数要与x列数相同,而LongTensor中的index最大值应与zeros(3, 5)行数相一致。
0.4319 0.4711 0.8486 0.8760 0.2355
0.0000 0.6500 0.0000 0.8573 0.0000
0.2609 0.0000 0.4080 0.0000 0.1029
参考链接:https://www.cnblogs.com/dogecheng/p/11938009.html,有公式说明scatter()一般可以用来对标签进行one-hot编码,举例如下:
1
2
3
4
5
6
7
8
9
10
11
12class_num = 10
batch_size = 4
label = torch.LongTensor(batch_size, 1).random_() % class_num
#tensor([[6],
# [0],
# [3],
# [2]])
torch.zeros(batch_size, class_num).scatter_(1, label, 1)
#tensor([[0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
# [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
# [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
# [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.]])
普通的label smoothing写法:
1 | new_labels = (1.0 - label_smoothing) * one_hot_labels + label_smoothing / num_classes |