专注于文本分类、关键词抽取、文本摘要、FAQ问答系统、对话系统语义理解NLU、知识图谱等。结合工业界具体案例和学术界最新研究成果实现NLP技术场景落地。更多学习内容关注“**NLP技术交流群 **”学习。
NLP 领域的模型研究已经被 transformer 模型占领了,学习Transformer 理解Attention,Self-Attention 机制应用实现以及原理,理解BERT 模型必备内容。
作者:走在前方
本次主要分享的内容
2017 年中,有两篇类似论文,分别是 FaceBook 的《Convolutional Sequence to Sequence Learning》和 Google 的《Attention is All You Need》,它们 Seq2Seq 上的创新,本质上来说,都是抛弃了 RNN 结构来做 Seq2Seq 任务。
近年来,NLP 领域的模型研究已经被 transformer 模型占领了。Transformer是在"Attention is All You Need"中提出的,其中的TF应用是Tensor2Tensor的子模块。哈佛的NLP团队专门制作了对应的PyTorch的指南说明。Transformer 模型的火爆有很多原因,例如:
论文地址:Attention Is All You Need
论文代码解读:The Annotated Transformer
深度学习做 NLP 的方法,基本上都是先将句子分词,然后每个词转化为对应的词向量序列。这样一来,每个句子都对应的是一个矩阵 X=(x1,x2,…,xt)其中xi都代表着第i个词的词向量(行向量),维度为d维,故X维度是n×d。这样的话,问题就变成了编码这些序列了。
RNN结构本身比较简单,适合序列建模,特点如下:
无法并行,因此速度较慢
无法很好地学习到全局的结构信息
第二个思路是 CNN 层,CNN 的方案窗口式遍历
在FaceBook的论文中,纯粹使用卷积也完成了Seq2Seq的学习,是卷积的一个精致且极致的使用案例。特点
目前的工作或竞赛模型中,我都已经尽量用CNN来代替已有的RNN模型了
其中 A,B 是另外一个序列(矩阵)。如果都取 A=B=X,那么就称为 Self Attention,它的意思是直接将x(t) 与原来的每个词进行比较,最后算出y(t)。
从三个方面分析为什么Attention出世了
每一个 encoder = 一个自注意力(self-attention)层 和 Feed Forward Neural Network。
decoder 也有同样的 self-attention 和 feed-forward 结构,但是在这两层之间还有一层 encoder-decoder** attention 层,帮助 decoder 关注到某一些特别需要关注的 encoder 位置。**
*encoder-decoder attetion
NLP应用的常见例子,先将输入单词使用embedding algorithm转成向量。
Each word is embedded into a vector of size 512. We’ll represent those vectors with these simple boxes.
词的向量化在最底层的编码器的输入时,这样每个编码器的都会接收到一个list(每个元素都是512维的词向量),只不过其他编码器的输入是前个编码器的输出。list的尺寸是可以设置的超参,通常是训练集的最长句子的长度。
在对输入序列做词的向量化之后,它们流经编码器的如下两个子层。
这里能看到Transformer的一个关键特性,每个位置的词仅仅流过它自己的编码器路径。在self-attention层中,这些路径两两之间是相互依赖的。前向网络层则没有这些依赖性,但这些路径在流经前向网络时可以并行执行。
下面我们来详细解读一下编码器的工作。
我们考虑用 Transformer 模型翻译下面这一句话:
“The animal didn’t cross the street because **it **was too tired”。
当我们翻译到 it 的时候,我们知道 it 指代的是 animal 而不是 street。所以,如果有办法可以让 it 对应位置的 embedding 适当包含 animal 的信息,就会非常有用。self-attention 的出现就是为了完成这一任务。
如下图所示,self attention 会让单词 it 和 某些单词发生比较强的联系,得到比较搞的 attention 分数。
当编码"it"时(编码器的最后层输出),部分attention集中于"the animal",并将其表示合并进入到“it”的编码中。
上述可视化
第一步 QKV 向量表示,为了实现 self-attention,每个输入的位置需要产生三个向量,分别是 Query 向量,Key 向量和 Value 向量,生成方法为分别乘以三个矩阵,这些矩阵在训练过程中需要学习。
注意到这些新向量的维度比输入词向量的维度要小(512–>64),并不是必须要小的,是为了让多头attention的计算更稳定。
Multiplying x1 by the WQ weight matrix produces q1, the “query” vector associated with that word. We end up creating a “query”, a “key”, and a “value” projection of each word in the input sentence.
第二步是计算分数。当我们在用 self-attention encode 某个位置上的某个单词的时候,我们希望知道这个单词对应的句子上其他单词的分数。其他单词所得到的分数表示了当我们 encode 当前单词的时候,应该放多少的关注度在其余的每个单词上。又或者说,其他单词和我当前的单词有多大的相关性或者相似性。
在 transformer 模型中,这个分数是由 query vector 和 key vector 做点积(dot product)所得的结果。所以说,当我们在对第一个单词做 self-attention 处理的时候,第一个单词的分数是 q_1 和 k_1 的点积,第二个分数是 q_1 和 k_2 的分数。
第三步和第四步是将这些分数除以 8。8 这个数字是 64 的开方,也就是 key vector 的维度的开方。据说这么做可以稳定模型的 gradient。然后我们将这些分数传入 softmax 层产生一些符合概率分布的 probability scores。
这些分数就表示了在处理当前单词的时候我们应该分配多少的关注度给其他单词。
第五步是将每个 value vector 乘以它们各自的 attention score。第六步是把这些 weighted value vectors 相加,成为当前单词的 vector 表示。
得到了 self-attention 生成的词向量之后,我们就可以将它们传入 feed-forward network 了。
Thinking 最后通过 self.attention 后向量表示 :z1 (通过 attetion 考虑全文的信息)
首先,我们要对每一个词向量计算 Query, Key 和 Value 矩阵。我们把句子中的每个词向量拼接到一起变成一个矩阵 X,然后乘以不同的矩阵做线性变换(WQ, WK, WV)。
X:[ n_vocab,embedding_size]
输入矩阵X的每一行表示输入句子的一个词向量
然后我们就用矩阵乘法实现上面介绍过的 Self-Attention 机制了。
**Z: 每个单词的 attention weight (n_vocab,embedding_size) **
在论文当中,每个 embedding vector 并不止产生一个 key, value, query vectors,而是产生若干组这样的 vectors,称之为"multi-headed" attention。这么做有几个好处:
每个 attention head 最终都产生了一个 matrix 表示这个句子中的所有词向量。在 transformer 模型中,我们产生了八个 matrices。我们知道 self attention 之后就是一个 feed-forward network。那么我们是否需要做 8 次 feed-forward network 运算呢?事实上是不用的。我们只需要将这 8 个 matrices 拼接到一起,然后做一次前向神经网络的运算就可以了。
综合起来,我们可以用下面一张图表示 Self-Attention 模块所做的事情。
为解决词序的利用问题,Transformer新增了一个向量对每个词。
到目前为止,我们的模型完全没有考虑单词的顺序。即使我们将句子中单词的顺序完全打乱,对于 transformer 这个模型来说,并没有什么区别。为了加入句子中单词的顺序信息,我们引入一个概念叫做 positional encoding。
(具体什么数值不重要,更重要的是能够代表不同的位置)
如果假设位置向量有4维,实际的位置向量将如下所示:
在下图中,每一行表示一个位置的pos-emb,所以第一行是我们将要加到句子第一个词向量上的vector。每个行有512值,每个值范围在[-1,1]。
另外一个细节是,encoder 中的每一层都包含了一个 residual connection 和 layer-normalization。如下图所示。
下面这张图是更详细的 vector 表示。
decoder 也是同样的架构。如果我们把 encoder 和 decoder 放到一起,他们就长这样。
encoder 最后一层会输出 attention vectors K 和 V。K 和 V 会被 decoder 用作解码的原材料。
在解码的过程中,解码器每一步会输出一个 token。一直循环往复,直到它输出了一个特殊的 end of sequence token,表示解码结束了。
decoder 的 self attention 机制与 encoder 稍有不同。在 decoder 当中,self attention 层只能看到之前已经解码的文字。我们只需要把当前输出位置之后的单词全都 mask 掉(softmax 层之前全都设置成-inf)即可。
Encoder-Decoder Attention 层和普通的 multiheaded self-attention 一样,除了它的** Queries 完全来自下面的 decoder 层,然后 Key 和 Value 来自 encoder 的输出向量**。
解码器最后输出浮点向量,如何将它转成词?这是最后的线性层和 softmax 层的主要工作。
线性层是个简单的全连接层,将解码器的最后输出映射到一个非常大的 logits 向量上。假设模型已知有 1 万个单词(输出的词表)从训练集中学习得到。那么,logits 向量就有 1 万维,每个值表示是某个词的可能倾向值。
softmax 层将这些分数转换成概率值(都是正值,且加和为 1),最高值对应的维上的词就是这一步的输出单词。
logits 可以看成最后一层全连接的输出*权重参数,假设神经网络最后一层有 m 个结点,那 logits= wx+b, 其中 x 是 m 维
现在我们已经了解了一个训练完毕的 Transformer 的前向过程,在训练时,模型将经历上述的前向过程,当我们在标记训练集上训练时,可以对比预测输出与实际输出。为了可视化,假设输出一共只有 6 个单词(“a”, “am”, “i”, “thanks”, “student”, and “” (short for ‘end of sentence’)).
模型的词表是在训练之前的预处理中生成的
一旦定义了词表,我们就能够构造一个同维度的向量来表示每个单词,比如 one-hot 编码,下面举例编码“am”。
举例采用 one-hot 编码输出词表
下面让我们讨论下模型的 loss 损失,在训练过程中用来优化的指标,指导学习得到一个非常准确的模型。
我们用一个简单的例子来示范训练,比如翻译**“merci”为“thanks”**。那意味着输出的概率分布指向单词“thanks”,但是由于模型未训练是随机初始化的,不太可能就是期望的输出。
由于模型参数是随机初始化的,未训练的模型输出随机值。我们可以对比真实输出,然后利用误差后传调整模型权重,使得输出更接近与真实输出。如何对比两个概率分布呢?简单采用 cross-entropy 或者 Kullback-Leibler divergence 中的一种。鉴于这是个极其简单的例子,更真实的情况是,使用一个句子作为输入。比如,输入是“je suis étudiant”,期望输出是“i am a student”。在这个例子下,我们期望模型输出连续的概率分布满足如下条件:
对一个句子而言,训练模型的目标概率分布
在足够大的训练集上训练足够时间之后,我们期望产生的概率分布如下所示:
训练好之后,模型的输出是我们期望的翻译。当然,这并不意味着这一过程是来自训练集。注意,每个位置都能有值,即便与输出近乎无关,这也是 softmax 对训练有帮助的地方。现在,**因为模型每步只产生一组输出,假设模型选择最高概率,扔掉其他的部分,这是种产生预测结果的方法,叫做 greedy 解码。**另外一种方法是 beam search,每一步仅保留最头部高概率的两个输出,根据这俩输出再预测下一步,再保留头部高概率的两个输出,重复直到预测结束
训练预测结果两种方法:
class LayerNorm(nn.Module):
"Construct a layernorm module (See citation for details)."
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(features))
self.b_2 = nn.Parameter(torch.zeros(features))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
计算Attention图说明
QK^T
-> math.sqrt(d_k) 数值缩放(避免softmax 计算过程e的指数过大的问题,当前我们也可以换成别的)
-> score 的得分mask 填充
-> softmax 获取query在所有key上的attn_score
-> V 权重乘以score
计算Attention公式
参数说明
def attention(query, key, value, mask=None, dropout=None):
"Compute 'Scaled Dot Product Attention'"
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim = -1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn
更多代码请查看 博客