上一节,我们介绍了Seq2Seq模型,它的生成序列来自同一个语义编码器C:

94ee8a2f1263e3dc00d132f96288ba3b.png

因此,在句子比较短时,该模型性能还可以,对于稍长一些的句子,模型的性能就不尽人意了。为此,注意力机制模型应运而生。

传送门:序列处理之RNN模型

56a2a8809a5bd8b54bcd6dbb51600b64.png

注意力机制概述

注意力机制,英文是Attention Mechanism,简称AM。它的提出是源于人类视觉的研究——当我们看一张图片时,并非会注意图片的全部内容,而是将注意力集中在图片的重要部分,通常是焦点的部分位置。也就是对这部分投入更多的注意力资源,以获取更多所需关注目标的细节信息,从而忽略其他无用信息。

深度学习的注意力机制,正是基于这样的逻辑构建的。在自然语言处理中,把注意力机制看作输出(Target)句子中某个单词和输入(Source)句子每个单词的相关性,这个是AM的本质。下面一张图展示了这种关系:

bd4194fe8e652140ed9a9a3591e72d12.png

图中,Key就是输入的所有单词,Value是其对应的取值,把<Key,Value>称为数据对,Query是输出中的某个元素:

f5f43dc29905c3c30906282eafa216af.png

给定一个Query,计算其与各个Key的相似度,得到对应Value值(权重系数),然后对Value进行加权,得到最终的Attention数值:

5b095ab5a7640e79ea5232864faeb15e.png

所以本质上注意力机制是对Source中元素的Value值进行加权求和,而Query和Key是用来计算对应Value的权重系数。

对上面架构图进行细分,注意力机制分为3个阶段:

  • 第一阶段:计算相似度

根据Query与Key计算两者的相似度,我们在机器学习基础篇的聚类章节里面给出了三种度量相似度的方式——闵可夫斯基距离,余弦相似度和核函数。除此之外,利用神经网络也可以去构造相似度,不妨称为“网络距离”。这样我们得到相似度取值si.

传送门:聚类算法(相似度与性能度量)

为什么可以用神经网络去构造相似度呢,这一点其实很容易理解。NLP入门的word2vec其实就是用神经网络来表达词语的相似性。这里我们借用生成对抗网络的逻辑来解释,也能对GAN加深理解。在生成对抗网络中,我们的初衷是期望生成的样本接近真实的样本,换句话说,也就是生成样本与真实样本的相似度越高越好

3906b74774c1d38e815eeb228bc986e8.png

而我们生成样本就是通过训练生成器G(X)得到的,而生成器G(X)就是一个神经网络,也就是说,生成样本与真实样本之间的相似度完全取决于这个生成器,因此,G(X)本质上就是两个样本空间的相似性的度量方式,用神经网络去构造了一个相似度

传送门:理解生成对抗网络(GAN)

  • 第二阶段:归一化处理

得到了相似度si,进行归一化处理就得到了权重系数,一般使用softmax函数:

7caaf58fc83f015d26a9549da103d9a8.png

  • 第三阶段:计算注意力

最后一步就是对权重系数进行加权求和:

8fce7650717e1a9da8f19a5c9561fd4a.png

94335f6ce3a3057cf4b8232be21cba56.png

带注意力机制的自编码模型

在Seq2Seq里,状态C表示为中间语义,每一个生成单词yi都由一个C进行译码C=f(X)

00067b5b3a3daad09cffc212ca7ac8e3.png

当模型带上注意力机制后,由于不同的Query对同一个Key得到的概率分布值不一样,意味着在生成单词yi的时候,原先相同的中间语义C会替换成根据当前生成单词而不断变化的Ci,其模型架构为:

96228669c3ebd068253a6c214af1c187.png

据此,生成目标句子单词的表达式如下:

002683ea5cbe43640b58f13b130ff595.png

中间语义根据权重系数加权得到:

02ca3d4f235c4db6389edba7c930ef6b.png

f是Encoder对输入词语的某种变换函数,A是权重系数矩阵,每一行代表一个Query对应的权重系数,输入长度固定为T。

那么在实现细节上,模型是如何确定权重系数的呢,它是通过对齐函数F来获得目标单词喝每个输入单词对应得对齐可能性,然后经过Softmax函数进行归一化得到注意力分配概率分布数值。

68bf97b086752b80040144aaa57da191.png

上面的含义可以理解为,输入序列中某个词对应正准备解码(翻译)的目标词语的权重(重要性),由词语本身(hj)和前一个解码的词(St-1)决定

然后,得到t时刻要解码的目标词对应中间语义:

aade389df320920ef37de0f12aa5cc52.png

最后经过解码器得到t时刻的输出词语:

e170af2d13c5a190ccd3d6edfc383398.png

一般编码器采取双向循环网络结构,带上注意力机制,其结构为:

c4473788e0cfe240762276a4859081c5.png

d7b9c649d094ca97466fe496980bc404.png

基于pytorch实现AM-Encoder模型

首先,编写编码器,利用词嵌入和GRU网络层,输出编码器结果和隐藏状态:

import torch
from torch import nn


class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size


        self.embedding = nn.Embedding(input_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)


    def forward(self, input, hidden):
        embedded = self.embedding(input).view(1, 1, -1)
        output = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden


    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

166f9a1c1bf3d956086f5664ccc73098.png

接着,构建简单的译码器,同样采取GRU网络图:

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(DecoderRNN, self).__init__()
        self.hidden_size = hidden_size


        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)


    def forward(self, input, hidden):
        output = self.embedding(input).view(1, 1, -1)
        output = F.relu(output)
        output, hidden = self.gru(output, hidden)
        output = self.softmax(self.out(output[0]))
        return output, hidden


    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

558c04fe6abd28f7bb96ce70a33a1c11.png

在简单编码器基础上,构建带有注意力机制的编码器,一共有四层:嵌入层,注意力层,RNN层和输出层。

class BahdanauAttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, n_layers=1, dropout_p=0.1):
        super(AttnDecoderRNN, self).__init__()


        #定义参数
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.n_layers = n_layers
        self.dropout_p = dropout_p
        self.max_length = max_length


        # 定义层
        self.embedding = nn.Embedding(output_size, hidden_size)
        self.dropout = nn.Dropout(dropout_p)
        self.attn = GeneralAttn(hidden_size)
        self.gru = nn.GRU(hidden_size * 2, hidden_size, n_layers, dropout=dropout_p)
        self.out = nn.Linear(hidden_size, output_size)


    def forward(self, word_input, last_hidden, encoder_outputs):
        # 前向传播每次运行一个时间步,但使用使用所有的编码器输出        
        # 获取当前词嵌入 (last output word)
        word_embedded = self.embedding(word_input).view(1, 1, -1) # S=1 x B x N
        word_embedded = self.dropout(word_embedded)


        # 计算注意力权重并使用编码器输出
        attn_weights = self.attn(last_hidden[-1], encoder_outputs)
        context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # B x 1 x N


        # 把词嵌入与注意力context结合在一起,然后传入循环网络
        rnn_input = torch.cat((word_embedded, context), 2)
        output, hidden = self.gru(rnn_input, last_hidden)


        # 定义最后输出层
        output = output.squeeze(0) # B x N
        output = F.log_softmax(self.out(torch.cat((output, context), 1)))


        #返回最后输出,隐含状态及注意力权重
        return output, hidden, attn_weights

参考资料:《深入浅出Embedding》

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐