浅谈对Attention机制的理解及Keras实现

上篇文章讨论了下LSTM,说到LSTM就很难绕开Attention,这俩货一般连着用,以发挥更大的效果,所以这次我们来谈谈Attention机制。

直观理解

Attention机制,也就是注意力机制。我的理解是,它是一种自动加权方案,能帮我们自动找出对当前具体任务(比如分类、机器翻译等)最有贡献的样本。举个例子,如果判断一句话“你真帅”是夸奖还是贬低,那么显然“帅”字对分类的贡献比另外两个字的贡献要大,用数值来描述重要程度则可能是[0.1, 0.1, 0.8]。Attention的作用就是帮我们找到这些权重,以便我们接下来的分类或者翻译更准确。

数学描述

Attention最开始是结合Encoder-Decoder框架提出来的,应用在机器翻译上。Encoder-Decoder框架如下图所示:

《浅谈对Attention机制的理解及Keras实现》

x1, x2,x3,x4是输入,比如要翻译的源语言”I am Iron man”,右边y1,y2,y3对应要翻译的语言“我 是 钢铁侠”(随便举的例子)。在上图中,输入句子经过一个RNN网络(比如LSTM)后,会生成一个中间语义C(我们可以把它当成输入的一个抽象表述,它蕴含了输入句子的意思)。然后C再经过一个RNN网络,将这个抽象的表述C通过另一种语言表达出来,也就是我们的目标语言。我们知道RNN会对每一个时间步的输入(x1,x2…)都生成一个隐层状态h1,h2…,而C就是一个关于h的映射,即:

《浅谈对Attention机制的理解及Keras实现》

当然可以直接把c取成Encoder的输出,即ht;或者是所有h的平均。

由于Decoder也是一个RNN网络,它也会有隐层状态,为了不与Encoder的隐层状态重复,我们把它记作s。Encoder通过RNN一个一个地把单词生成出来,输入为上一个时刻的y,上一个时刻的隐层状态,还有我们刚刚讲的那个抽象的语义C。即:

《浅谈对Attention机制的理解及Keras实现》

当前时刻单词yt是通过当前生成的st经过一番操作得到的,我们省略不谈,只要知道st能生成单词yt就行了。

从上面的过程可以看到,中间语义C是不变的,每次都用它来输出下一个翻译的词。但从直觉上来看是不合理的,因为一次生成的语义C,很难蕴含丰富的信息,从而产生不同的单词,特别是句子很长的时候。再者,这样的C只能表达总体的语义信息,针对性不够,我们更希望在翻译到不同的词的时候,C的侧重点有所不同。比如在翻译到man的时候,应该把更多的注意力放到单词iron上,忽略前面的I am。

因此提出一种带有注意力模型的语义C,每个时刻的取值不一样,新的框架如下图所示:

《浅谈对Attention机制的理解及Keras实现》

具体地,可以设计一个神经网络a,能根据当前要翻译到的词(比如“钢铁”的下一个词,s(t’-1)中包含了这种信息),计算每一个输入单词的权重大小(对xt样本的ht进行计算),(即给定当前输出情况,计算每一个输入样本的重要程度,因此是关于s和h的函数):

《浅谈对Attention机制的理解及Keras实现》

《浅谈对Attention机制的理解及Keras实现》

上面公式中,t’代表要翻译的下一时刻(比如“钢铁”的下一个词),t代表每一个输入的单词,t从1开始遍历,直到所有的输入单词(上述图例中是4)。因此会计算4个权重,代表每个输入单词对翻译当前输出单词的重要程度(注意力权重)。计算出来之后当然要归一化,所以下一步是softmax:

《浅谈对Attention机制的理解及Keras实现》

算出每一个单词的重要程度之后,可以生成当前时刻(t’,表示“钢铁”的下一个词)的中间语义C啦,刚刚讲过C就是一个关于h的映射,刚刚我们简单粗暴地取其平均,现在我知道具体每个单词的重要程度了,因此可以做个加权平均,即:

《浅谈对Attention机制的理解及Keras实现》

这个,就是我们最终要的C了,每输出一个单词的时候,C会跟着变化,它蕴含了最能体现当前输出单词的语义信息。

当然,有时候我们并不需要多输出(比如分类任务),因此Decoder可以去掉,只要把上面公式中的t’去掉即可(只需要计算一次各个输入样本对分类任务的重要程度即可)。然后把算得的c放到后面的Dense层,进行分类即可。

一些额外的思考:

为什么可以通过神经网络去计算每个输入对当前输出值的贡献程度?

由于样本都是经过Embedding,变成了向量的形式,它们在向量空间中其实是有邻近关系的,Embedding每一个维度的取值都刻画了一个词的某一个方面。所以它们是可以进行运算的,因此可以通过神经网络去算得每一个输入样本对当前输出值的贡献程度,即注意力权重。

Keras代码实现:

Keras中没有定义Attention层,因此要自己去定义。Keras中实现一个Layer,要按照下面骨架编写:只需要实现三个方法即可:

  • build(input_shape): 这是你定义权重的地方。这个方法必须设 self.built = True,可以通过调用 super([Layer], self).build() 完成。

  • call(x): 这里是编写层的功能逻辑的地方。你只需要关注传入 call 的第一个参数:输入张量,除非你希望你的层支持masking。

  • compute_output_shape(input_shape): 如果你的层更改了输入张量的形状,你应该在这里定义形状变化的逻辑,这让Keras能够自动推断各层的形状

 

最后附上代码: 

class AttentionWeightedAverage(Layer):
    """
    Computes a weighted average of the different channels across timesteps.
    Uses 1 parameter pr. channel to compute the attention value for a single timestep.
    """

    def __init__(self, return_attention=False, **kwargs):
        self.init = initializers.get('uniform')
        self.supports_masking = True
        self.return_attention = return_attention
        super(AttentionWeightedAverage, self).__init__(**kwargs)

    def build(self, input_shape):
        self.input_spec = [InputSpec(ndim=3)]
        assert len(input_shape) == 3

        self.W = self.add_weight(shape=(input_shape[2], 1),
                                 name='{}_W'.format(self.name),
                                 initializer=self.init)
        self.trainable_weights = [self.W]
        super(AttentionWeightedAverage, self).build(input_shape)

    def call(self, x, mask=None):
        # computes a probability distribution over the timesteps
        # uses 'max trick' for numerical stability
        # reshape is done to avoid issue with Tensorflow
        # and 1-dimensional weights
        logits = K.dot(x, self.W)
        x_shape = K.shape(x)
        logits = K.reshape(logits, (x_shape[0], x_shape[1]))
        ai = K.exp(logits - K.max(logits, axis=-1, keepdims=True))

        # masked timesteps have zero weight
        if mask is not None:
            mask = K.cast(mask, K.floatx())
            ai = ai * mask
        att_weights = ai / (K.sum(ai, axis=1, keepdims=True) + K.epsilon())
        weighted_input = x * K.expand_dims(att_weights)
        result = K.sum(weighted_input, axis=1)
        if self.return_attention:
            return [result, att_weights]
        return result

 代码的使用:

input = Input(shape=(100,))
x = Embedding(500,100)(input)
x = BatchNormalization()(x) #归一化
x = Bidirectional(CuDNNLSTM(60, return_sequences=True))(x) #双向LSTM
x = SpatialDropout1D(dropout_p)(x)
attn = AttentionWeightedAverage()(x) #注意力层

...
 

从另一个角度去看Attention

刚刚是从Encoder-Decoder框架去讨论Attention的,但其实可以将其抽象成下面的问题:将Source中的构成元素想象成是由一系列的数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。所以本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。

《浅谈对Attention机制的理解及Keras实现》

从这个角度去看,又可以得到更多别的东西,比如当K=V=P的时候,就是著名的Self-Attention啦,感兴趣的读者可以自行查询更多的内容。

    原文作者:林大大zzz
    原文地址: https://blog.csdn.net/weixin_44540892/article/details/103058883
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞