MultiHeadAttention多头注意力机制的原理

这篇具有很好参考价值的文章主要介绍了MultiHeadAttention多头注意力机制的原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

MultiHeadAttention多头注意力作为Transformer的核心组件,其主要由多组自注意力组合构成。

1. self-Attention自注意力机制

在NLP任务中,自注意力能够根据上下文词来重新构建目标词的表示,其之所以被称之为注意力,在于从上下文词中去筛选目标词更需要关注的部分,比如"他叫小明","他"这个词更应该关注"小明"这个上下文。

MultiHeadAttention多头注意力机制的原理

上图提示了一个输入为两个单词[Thinking, Matchines]的序列在经过自注意力构建后的变换过程:

  • 通过Embeding层,两个单词的one-hot向量转换为embedding向量X=[x1, x2]
  • 通过三组矩阵运算得到query、key、value值,这三组矩阵的输入都是原来同一个输入向量[x1,x2],这也是被称之为自注意力的原因。

       

  • 计算query、key间的相似度得分,为了提升计算效率,此处采用缩放点积注意力,其需要query、key向量的维度是相等的,并且都满足零均值和单位方差,此时得分表示:

       

  • 对相似度得分矩阵求softmax进行归一化(按axis=1维进行),在实际中由于进行transformer中的输入序列要求是定长的,因此会有补余向量,此时这里softmax会有一个掩蔽操作,将补余部分都置为0。

       

  • 乘以value向量得到输出z:

      

完成了结构的分析后,接下来,我们考虑一个新的问题,为什么自注意力机制会有效?通过三组矩阵Q、K、V我们获得了原来的输入三种不同表征形式,其通过query-key的比较来衡量目标词和上下文词的相似性关联,通过value来提取词的本质特征,最终通过自注意力机制,我们建立了结合上下文信息的词的新特征向量,其本质是特征提取器。

2. MultiHeadAttention多头注意力机制

多头注意力是多组自注意力构件的组合,上文已经提到自注意力机制能帮助建立包括上下文信息的词特征表达,多头注意力能帮忙学习到多种不同类型的上下文影响情况,比如"今天阳光不错,适合出去跑步",在不同情景下,"今天"同"阳光"、"跑步"的相关性是不同,特别是头越多,越有利于捕获更大更多范围的相关性特征,增加模型的表达能力。

MultiHeadAttention多头注意力机制的原理

 上图描述了多头注意力的处理过程,其实际上将多个自注意机制的产出再经过参数矩阵得到一个新输出。我们将上述自注意步骤引入多头情况,介绍如何通过矩阵来计算,其由3组自注意力组合,输入为2个单词的序列。

  • query、key、value表征向量的计算

       

  • 计算query、key间的相似度得分

       

  • 对相似度得分矩阵求softmax
  • 乘以value向量得到各自注意力模块输出,并乘以输出权重矩阵得到最终输出矩阵O,其最终还是得到了多头注意力的输出,其为输出词向量维度,如果其维度等于输入词向量维度时,输出和输入的尺度是一致的,因此多头注意力机制本质仍是特征抽取器。

        

3. MultiHeadAttention多头注意力机制的代码

MultiHeadAttention多头注意力机制的原理

上图左侧为单点积注意力Dot-Product Attention组件的结构(当Q,K,V为同一输入时称之为自注意力),右侧为多个单注意力组件组成多头注意力,以下是paddle的实现代码:

class DotProductAttention(nn.Layer):
    def __init__(self, query_size, query_vec_size, hidden_vec_size, **kwargs):
        """
        input:
            query_size: int, 序列长度(词数)
            query_vec_size: int, 词向量长度
            hidden_vec_size: int, 输出词向量长度
        """
        super(DotProductAttention, self).__init__(**kwargs)
        self.query_size = query_size
        self.query_vec_size = query_vec_size
        self.hidden_vec_size = hidden_vec_size
        # 线性变换层
        self.W_q = nn.Linear(query_vec_size, hidden_vec_size)
        self.W_k = nn.Linear(query_vec_size, hidden_vec_size)
        self.W_v = nn.Linear(query_vec_size, hidden_vec_size)

    def forward(self, queries, keys, values, valid_lens):
        """
        input:
            queries,keys,values: tensor([batch_size, query_size, query_vec_size]), 输入
            valid_lens: tensor([batch_size]), 序列中有效长度
        output:
            output: tensor([batch_size, query_size, hidden_vec_size]), 输出
        """ 
        #1. Linear: queries, keys, values的线性变换, out shape: [batch_size, query_size, hidden_vec_size]
        queries = self.W_q(queries)
        keys = self.W_k(keys)
        values = self.W_v(values)
        # 2. score, shape: [batch_size, query_size, query_size]
        scores = paddle.bmm(queries, keys.transpose((0, 2, 1))) / math.sqrt(self.hidden_vec_size)
        scores = scores.reshape([-1, self.query_size])
        # 3. mask, shape: [batch_size * query_size, query_size]
        mask = paddle.arange(self.query_size, dtype=paddle.float32)[None, :] < paddle.repeat_interleave(valid_lens, self.query_size)[:, None]
        scores[~mask] = float(-1e6)
        # 4. softmax [batch_size, query_size, query_size]
        scores = scores.reshape([-1, self.query_size, self.query_size])
        scores = nn.functional.softmax(scores, axis=-1)
        # 5. output [batch_size, query_size, query_size] * [batch_size, query_size, hidden_vec_size]
        return paddle.bmm(scores, values)

在实际中,多个单注意力组件的计算可以通过同一矩阵进行并行计算,如第2节所描述,以下完成最终多头注意力的代码,可以看出其同单注意力的代码几乎差不多:

class MultiHeadAttention(nn.Layer):
    def __init__(self, query_size, query_vec_size, hidden_vec_size, output_vec_size, head_num, **kwargs):
        """
        input:
            query_size: int, 序列长度(词数)
            query_vec_size: int, 词向量长度
            hidden_vec_size: int, 变换层词向量长度
            output_vec_size: int, 输出层词向量长度
            head_num: int, 头数
        """
        super(MultiHeadAttention, self).__init__(**kwargs)
        self.query_size = query_size
        self.query_vec_size = query_vec_size
        self.hidden_vec_size = hidden_vec_size
        self.output_vec_size = output_vec_size
        self.head_num = head_num
        # 线性变换层
        self.W_q = nn.Linear(query_vec_size, hidden_vec_size * head_num)
        self.W_k = nn.Linear(query_vec_size, hidden_vec_size * head_num)
        self.W_v = nn.Linear(query_vec_size, hidden_vec_size * head_num)
        self.W_o = nn.Linear(hidden_vec_size * head_num, output_vec_size)

    def forward(self, queries, keys, values, valid_lens):
        """
        input:
            queries,keys,values: tensor([batch_size, query_size, query_vec_size]), 输入
            valid_lens: tensor([batch_size]), 序列中有效长度
        output:
            output: tensor([batch_size, query_size, hidden_vec_size]), 输出
        """ 
        #1. Linear: queries, keys, values的线性变换, out shape: [batch_size, query_size, hidden_vec_size * head_num]
        queries = self.W_q(queries)
        keys = self.W_k(keys)
        values = self.W_v(values)
        # 2. score, shape: [batch_size * head_num, query_size, query_size]
        queries = queries.reshape([-1, self.query_size, self.hidden_vec_size, self.head_num])\
                         .transpose((0, 3, 1, 2))\
                         .reshape([-1, self.query_size, self.hidden_vec_size])
        keys = keys.reshape([-1, self.query_size, self.hidden_vec_size, self.head_num])\
                   .transpose((0, 3, 1, 2))\
                   .reshape([-1, self.query_size, self.hidden_vec_size])
        values = values.reshape([-1, self.query_size, self.hidden_vec_size, self.head_num])\
                   .transpose((0, 3, 1, 2))\
                   .reshape([-1, self.query_size, self.hidden_vec_size])
        scores = paddle.bmm(queries, keys.transpose((0, 2, 1))) / math.sqrt(self.hidden_vec_size)
        scores = scores.reshape([-1, self.query_size])
        # 3. mask, shape: [batch_size * head_num * query_size, query_size]
        mask = paddle.arange(self.query_size, dtype=paddle.float32)[None, :] < paddle.repeat_interleave(valid_lens, self.query_size * self.head_num)[:, None]
        scores[~mask] = float(-1e6)
        # 4. softmax [batch_size, query_size * head_num, query_size]
        scores = scores.reshape([-1, self.head_num, self.query_size, self.query_size])
        scores = nn.functional.softmax(scores, axis=-1)
        # 5. output [batch_size, query_size, head_num * hidden_vec_size]
        z = paddle.bmm(scores.reshape([-1, self.query_size, self.query_size]), values)
        z = z.reshape([-1, self.head_num, self.query_size, self.hidden_vec_size]).transpose((0, 2, 1, 3))
        # 6. output linear
        return self.W_o(z.reshape([-1, self.query_size, self.head_num * self.hidden_vec_size]))

4. 为什么要用注意力机制

MultiHeadAttention多头注意力机制的原理

按论文Attention Is All You Need的观点,上图为 self-attention同cnn、rnn在复杂度上的比较,其中n是指序列的长度,d是序列词向量的维度,k表示卷积核的大小,我们为什么将self-attention作为序列数据特征编码或解码器,主要基于三点理由:文章来源地址https://www.toymoban.com/news/detail-419926.html

  • Complexity per Layer 计算量:计算量是指每层的计算量,可以看出当词向量的维度大于序列长度时,self-attention的计算量是要更小的。
  • Sequential Operations 并行实现:self-attention完全可以通过矩阵运算来实现并行计算。
  • Maximum Path Length 上下文依赖特征获取难易:比如序列中第1个词同最后1个词,上下文依赖特征获取难易是指要获取这两个词相互特征需要经过多少步,对于RNN网络需要经过序列长度的步,而对于CNN同卷积核和层数有关,对于self-attention只需要1步就能建立第1个词同最后1个词的关系。

到了这里,关于MultiHeadAttention多头注意力机制的原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包