菜鸟学NLP(六)

Transformer

模型结构:
pic1
这是Transformer的网络结构,同样继承了先前的Seq2Seq模型的encoder(左侧)和decoder(右侧)结构.
与之前的模型不同的是,Transformer中的encoder和decoder都不用RNN,而是换成了多个attention.

Encoder

pic1中的左侧就是一个Encoder.
input首先输入到input embedding中,这是NLP中的比较常规的操作.
input embedding层的输出会遇到Positional Encodeing.

Positional Encoding

这里在图上写成的是Positional Encoding,有些地方也写作Positional Embedding.
Position Embedding 被理解为补充了词向量的位置信息.
之前说了,这里摒弃了先前用的RNN形式,既然不用RNN了,仍需要一种方法来表征序列顺序.因此要引入position Encoding.

论文中PE的数学表达形式为

其中这里pos是绝对位置,i表示维度.
显然上述编码方式能表达绝对位置编码.
但有些时候词语的相对位置也非常重要.
一些文章会引入相对位置信息,比如google大佬之后提出的self-Attention with Relative Position Representations.
具体实现

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
import torch
import torch.nn as nn


class PositionalEncoding(nn.Module):

def __init__(self, d_model, max_seq_len):
"""初始化。

Args:
d_model: 一个标量。模型的维度,论文默认是512
max_seq_len: 一个标量。文本序列的最大长度
"""
super(PositionalEncoding, self).__init__()

# 根据论文给的公式,构造出PE矩阵
position_encoding = np.array([
[pos / np.pow(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
for pos in range(max_seq_len)])
# 偶数列使用sin,奇数列使用cos
position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

# 在PE矩阵的第一行,加上一行全是0的向量,代表这`PAD`的positional encoding
# 在word embedding中也经常会加上`UNK`,代表位置单词的word embedding,两者十分类似
# 那么为什么需要这个额外的PAD的编码呢?很简单,因为文本序列的长度不一,我们需要对齐,
# 短的序列我们使用0在结尾补全,我们也需要这些补全位置的编码,也就是`PAD`对应的位置编码
pad_row = torch.zeros([1, d_model])
position_encoding = torch.cat((pad_row, position_encoding))

# 嵌入操作,+1是因为增加了`PAD`这个补全位置的编码,
# Word embedding中如果词典增加`UNK`,我们也需要+1。看吧,两者十分相似
self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
self.position_encoding.weight = nn.Parameter(position_encoding,
requires_grad=False)
def forward(self, input_len):
"""神经网络的前向传播。

Args:
input_len: 一个张量,形状为[BATCH_SIZE, 1]。每一个张量的值代表这一批文本序列中对应的长度。

Returns:
返回这一批序列的位置编码,进行了对齐。
"""

# 找出这一批序列的最大长度
max_len = torch.max(input_len)
tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
# 对每一个序列的位置进行对齐,在原序列位置的后面补上0
# 这里range从1开始也是因为要避开PAD(0)的位置
input_pos = tensor(
[list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
return self.position_encoding(input_pos)

主要模块

之后将数据送入Encoder的主要模块中.可以看到图上有N×的符号,表示存在N个这样的模块.这里N=6.

解释一下这个主要模块.
可以看到这里有4个部分:1个multi-head attention和2个Add&Norm和1个FeedForward

Multi-Head Attention

实际上就是在这篇文章中提出的self attention.
所谓的multi-head就是有点像ensemble的操作,先在进入该部件的时候进行split,拆分成h个数据分别输入到h个attention部件中.之后在该模块输出口处对h个进行合并concat.
单个attention里进行的操作:

事实上这里Q、K、V是相同的.
pic2
我们知道,atttion的一般性定义就是一个Q和一个V的匹配程度.这里增加了一个V作为value.
所谓的self attention就是自己和自己进行比较.
在Encoder端的self attention,就是计算输入的各个数据对当前位置上的数据的重要性(或者说attention score).
看下图
pic3
我们可以得到各个词之间的相关性.
注意:不同的head可能拥有不同的attention.

Decoder

-------------本文结束感谢您的阅读-------------