【组队学习】Task02:学习Attention和Transformer

举报
诡途 发表于 2022/02/23 18:11:12 2022/02/23
【摘要】 @TOC 组队学习资料:datawhale8月组队学习-基于transformers的自然语言处理(NLP)入门Task02主要学习内容:2.1-图解attention.md2.2-图解transformer.md声明:NLP纯小白,本文内容主要是作为个人学习笔记,可能很多地方我自己理解也不是很到位,仅供参考,有争议的话可以多查点儿其他资料,并请评论区留言指正!谢谢学习建议对于我这种小白来说...

@TOC

组队学习资料:

datawhale8月组队学习
-基于transformers的自然语言处理(NLP)入门

Task02主要学习内容:
2.1-图解attention.md
2.2-图解transformer.md

声明:NLP纯小白,本文内容主要是作为个人学习笔记,可能很多地方我自己理解也不是很到位,仅供参考,有争议的话可以多查点儿其他资料,并请评论区留言指正!谢谢

学习建议
对于我这种小白来说,应该以2.2transformer为主,2.1attention作为补充知识,一开始先看2.1完全不知道在讲什么,看完2.2再回过来看2.1很多就通了,所以一开始我觉得顺序错了,看到后面发现不应该有顺序,2.1应该是对2.2的补充说明。总的来说还是先看2.2的transformer更好理解些。

另外建议提前了解下word2vec,RNN

一、Transformer概述

在这里插入图片描述
前面有提到attention是对transformer的补充解释,所以笔记以介绍Transformer为主,attention作为补充知识穿插

1.1、transformer是干什么的

1.1.1相对于传统RNN网络结构一种加强
在这里插入图片描述

如上图所示:transformer作为一种网络结构取代了传统seq2seq中的RNN模型,解决了并行计算的问题,为模型训练提供了加速能力

用来并行计算的机制主要是Self.attention机制,为了区分一句话中的侧重点信息,即赋予每个单词不同权重,相比较于传统RNN模型多考虑了位置序列信息

在这里插入图片描述

1.1.2 相对于传统word2vec的区别

传统的word2vec:

  • 训练好的词向量是静态的,固定不变的,同词同向量

Transform:

  • 动态变化的词向量,结合不同的上下文,同一个词的词向量可能是不同的

1.1.3transform的整体架构
在这里插入图片描述
学习目标 | transform需要做什么?

  • 输入如何编码
  • 输出结果是什么
  • Attention的目的
  • 怎么组合

二 Self.attention机制

2.1、Attention什么意思

  • 对于输入数据,你的关注点是什么
  • 如何才能让计算机关注到这些有价值的信息

2.2、Self.attention是如何计算的

矩阵运算的方式,使得 Self Attention 的计算能够并行化,这也是 Self Attention 最终的实现方式

在这里插入图片描述

  • 第 1 步:对输入编码器的每个词向量,都创建 3 个向量,分别是:Q向量(query 要去查询的),K向量(key等着被查的),V向量(Value实际的特征信息)。这 3 个向量是词向量分别和 3 个矩阵相乘得到的,而这个矩阵是我们要学习的参数(先初始化,然后调整),最终需要整合的是V向量

  • 第 2 步:计算 Attention Score(注意力分数)

通过计算 “Thinking” 对应的 Query 向量和其他位置的每个词的 Key 向量的点积(内积),而得到的。如果我们计算句子中第一个位置单词的 Attention Score(注意力分数),那么第一个分数就是 q1 和 k1 的内积,第二个分数就是 q1 和 k2 的点积(内积)

第m个词(共n个词)得到n个内积:
qmk1,qmk2,、、、qmkm,、、、qnkn
内积分乘法:对应位置元素相乘求和,内积越大,相关性越大

  • 第 3 步:把每个分数除以 (√dkey) (dkey是 Key 向量的长度)

为了剔除向量维度影响,避免向量维度越大分数越高的不合理性

  • 第 4 步:接着把这些分数经过一个 Softmax 层,Softmax可以将分数归一化,这样使得分数都是正数并且加起来等于 1

剔除度量值,比例化,方便对比

  • 第 5 步:得到每个位置的分数后,将每个分数分别与每个 Value 向量相乘

  • 第 6 步:把上一步得到的向量相加,就得到了 Self Attention 层在这个位置的输出。

在这里插入图片描述

每个词不再代表自己,代表的是全文加权后的特征

总结:
假设所有词向量组成矩阵 X X ,权重矩阵 W Q W^Q , W K W^K , W V W^V

根据第1步,可得:
Q = X W Q Q = X \cdot W^Q
K = X W K K = X \cdot W^K
V = X W V V = X \cdot W^V

根据第2~6步,可得Self-Attention的输出
Z = Σ softmax ( Q K T d K ) V Z = \Sigma\text{softmax}\left( \frac{Q \cdot K^T}{\sqrt{d_{K}}} \right) \cdot V

三、multi-head attention多头注意力机制

类似于卷积神经网络中的filter卷积核与特征图

multi-head:

  • 通过不同的head得到多个特征表达(一般做8个头)
  • 将所有特征拼接在一起
  • 可以通过再一层全连接来降维

通俗地讲,不知道哪种方法好,多做几种方法,最后把几种方法的特征拼接在一起再降维

四、其他相关知识点

4.1堆叠多层:

  • 经过self.attention&多头机制得到的输出还是向量
  • 把得到的向量再重复做self.attention&多头机制继续输出向量
  • 循环多层堆叠,计算方法一致,一般只做一层是不够的

4.2位置信息表达:

在self-attention中每个词都会考虑整个序列的加权,所以其出现位置并不会对结果产生什么影响,相当于放哪都无所谓,但是这跟实际就有些不符合了,我们希望模型能对位置有额外的认识。

解决方法如下图:在embeddings后加一个位置编码
在这里插入图片描述

传统机器学习一般使用one-hot编码,transformer中使用余弦|正弦的周期性表达

4.3 残差连接

卷积神经网络中《深度残差网络》

在这里插入图片描述
两条路:

  • 经过self.attention 多头等特征变换
  • 直接把原先特征拿过来

丢到网络中,让网络自己选择最优的特征

残差链接的结果:至少不比原来差

4.4 归一化

传统:batch的归一化
transform:layer层的归一化

五、代码实现

5.1使用PyTorch的MultiheadAttention来实现Attention的计算

torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None)

参数说明:

  • embed_dim:最终输出的 K、Q、V 矩阵的维度,这个维度需要和词向量的维度一样
  • num_heads:设置多头注意力的数量。如果设置为 1,那么只使用一组注意力。如果设置为其他数值,那么 num_heads 的值需要能够被 embed_dim 整除
  • dropout:这个 dropout 加在 attention score 后面

forward(query, key, value, key_padding_mask=None, need_weights=True, attn_mask=None)

参数说明:

  • query:对应于 Key 矩阵,形状是 (L,N,E) 。其中 L 是输出序列长度,N 是 batch size,E 是词向量的维度
  • key:对应于 Key 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度
  • value:对应于 Value 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度
  • key_padding_mask:如果提供了这个参数,那么计算 attention score 时,忽略 Key 矩阵中某些 padding 元素,不参与计算 attention。形状是 (N,S)。其中 N 是 batch size,S 是输入序列长度。
    • 如果 key_padding_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
    • 如果 key_padding_mask 是 BoolTensor,那么 True 对应的位置会被忽略
  • attn_mask:计算输出时,忽略某些位置。形状可以是 2D (L,S),或者 3D (N∗numheads,L,S)。其中 L 是输出序列长度,S 是输入序列长度,N 是 batch size。
    • 如果 attn_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
    • 如果 attn_mask 是 BoolTensor,那么 True 对应的位置会被忽略
import torch
import torch.nn as nn

## nn.MultiheadAttention 输入第0维为length
# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(12,64,300)
# batch_size 为 64,有 10 个词,每个词的 Key 向量是 300 维
key = torch.rand(10,64,300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value= torch.rand(10,64,300)

embed_dim = 300
num_heads = 1
# 输出是 (attn_output, attn_output_weights)
multihead_attn = nn.MultiheadAttention(embed_dim, num_heads)
attn_output = multihead_attn(query, key, value)[0]
# output: torch.Size([12, 64, 300])
# batch_size 为 64,有 12 个词,每个词的向量是 300 维
print(attn_output.shape)

torch.Size([12, 64, 300])

5.2使用自编程实现Attention的计算

import torch
from torch import nn


class MultiheadAttention(nn.Module):

    def __init__(self, hid_dim, n_heads, dropout):
        super(MultiheadAttention, self).__init__()
        # 每个词输出的向量维度
        self.hid_dim = hid_dim
        # 多头注意力的数量
        self.n_heads = n_heads

        # 强制 hid_dim 必须整除 n_heads
        assert hid_dim % n_heads == 0
        # 定义 W_q 矩阵
        self.w_q = nn.Linear(hid_dim, hid_dim)
        # 定义 W_k 矩阵
        self.w_k = nn.Linear(hid_dim, hid_dim)
        # 定义 W_v 矩阵
        self.w_v = nn.Linear(hid_dim, hid_dim)
        self.fc = nn.Linear(hid_dim, hid_dim)
        self.do = nn.Dropout(dropout)
        # 缩放
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim // n_heads]))

    def forward(self, query, key, value, mask=None):
        # K: [64,10,300], batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
        # V: [64,10,300], batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
        # Q: [64,12,300], batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
        bsz = query.shape[0]
        Q = self.w_q(query)
        K = self.w_k(key)
        V = self.w_v(value)
        # 这里把 K Q V 矩阵拆分为多组注意力,变成了一个 4 维的矩阵
        # 最后一维就是是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度, 每个 head 的向量长度是:300/6=50
        # 64 表示 batch size,6 表示有 6组注意力,10 表示有 10 词,50 表示每组注意力的词的向量长度
        # K: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # V: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        # Q: [64,12,300] 拆分多组注意力 -> [64,12,6,50] 转置得到 -> [64,6,12,50]
        # 转置是为了把注意力的数量 6 放到前面,把 10 和 50 放到后面,方便下面计算
        Q = Q.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        K = K.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        V = V.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)

        # 第 1 步:Q 乘以 K的转置,除以scale
        # [64,6,12,50] * [64,6,50,10] = [64,6,12,10]
        # attention:[64,6,12,10]
        attention = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale

        # 把 mask 不为空,那么就把 mask 为 0 的位置的 attention 分数设置为 -1e10
        if mask is not None:
            attention = attention.masked_fill(mask == 0, -1e10)

        # 第 2 步:计算上一步结果的 softmax,再经过 dropout,得到 attention。
        # 注意,这里是对最后一维做 softmax,也就是在输入序列的维度做 softmax
        # attention: [64,6,12,10]
        attention = self.do(torch.softmax(attention, dim=-1))

        # 第三步,attention结果与V相乘,得到多头注意力的结果
        # [64,6,12,10] * [64,6,10,50] = [64,6,12,50]
        # Z: [64,6,12,50]
        Z = torch.matmul(attention, V)

        # 因为 query 有 12 个词,所以把 12 放到前面,把 5 和 60 放到后面,方便下面拼接多组的结果
        # Z: [64,6,12,50] 转置-> [64,12,6,50]
        Z = Z.permute(0, 2, 1, 3).contiguous()
        # 这里的矩阵转换就是:把多组注意力的结果拼接起来
        # 最终结果就是 [64,12,300]
        # Z: [64,12,6,50] -> [64,12,300]
        Z = Z.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))
        Z = self.fc(Z)
        return Z



# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(64, 12, 300)
# batch_size 为 64,有 12 个词,每个词的 Key 向量是 300 维
key = torch.rand(64, 10, 300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value = torch.rand(64, 10, 300)
attention = MultiheadAttention(hid_dim=300, n_heads=6, dropout=0.1)
output = attention(query, key, value)
# output: torch.Size([64, 12, 300])
print(output.shape)

torch.Size([64, 12, 300])

六、篇章小测

问题1: Transformer中的softmax计算为什么需要除以dk?

为了剔除向量维度影响,避免向量维度越大分数叠加越高的不合理性
具体可参考:transformer中的attention为什么scaled?

问题2: Transformer中attention score计算时候如何mask掉padding位置?

在训练的过程中,自然语言数据往往都是以Batch的形式输入进的模型,而一个batch中的每一句话不能保证长度都是一样的,所以需要使用PADDING的方式将所有的句子都补全到最长的长度,比如拿0进行填充,但是我们知道这种用0填充的位置的信息是完全没有意义的,因此我们希望这个位置不参与后期的反向传播过程。以此避免最后影响模型自身的效果,因此提出了在训练时将补全的位置给Mask掉的做法。而在Self-attention的计算当中,我们自然也不希望有效词的注意力集中在这些没有意义的位置上,因此使用了PADDING MASK的方式.
PADDING MASK在attention的计算过程中处于softmax之前,通过PADDING MASK的操作,使得补全位置上的值成为一个非常大的负数(可以是负无穷),这样的话,经过Softmax层的时候,这些位置上的概率就是0。以此操作就相当于把补全位置的无用信息给遮蔽掉了(Mask掉了)
具体可参考:Transformer 中self-attention以及mask操作的原理以及代码解析

问题3: 为什么Transformer中加入了positional embedding?

位置和顺序对于一些任务十分重要,例如理解一个句子、一段视频。位置和顺序定义了句子的语法、视频的构成,它们是句子和视频语义的一部分。循环神经网络RNN本质上考虑到了句子中单词的顺序。因为RNN以顺序的方式逐字逐句地解析一个句子,这将把单词的顺序整合到RNN中。
Transformer使用MHSA(Multi-Head Self-Attention),从而避免使用了RNN的递归方法,加快了训练时间,同时,它可以捕获句子中的长依赖关系,能够应对更长的输入。
当句子中的每个单词同时经过Transformer的Encoder/Decoder堆栈时,模型本身对于每个单词没有任何位置/顺序感 (permutation invariance)。 因此,仍然需要一种方法来将单词的顺序信息融入到模型中。
为了使模型能够感知到输入的顺序,可以给每个单词添加有关其在句子中位置的信息,这样的信息就是位置编码(positional embedding, PE)
具体可参考:Transformer 中的 positional embedding

其他参考资料

attention主要的发展路径及目前的主流方法
唐宇迪老师-深度学习-PyTorch框架实战系列
面经:什么是Transformer位置编码?
深度学习attention机制中的Q,K,V分别是从哪来的?

插个题外话:
csdn的markdown写it类的文章还是比较方便的,但是我要微信公众号上再排版一遍还是比较麻烦的,今天发现一个特别的东西,可以一键转微信公众号排版,还是比较方便的,
微信公众号:诡途 欢迎关注
如何将Markdown文章轻松地搬运到微信公众号并完美地呈现代码内容
微信公众号 Markdown 编辑器 - OpenWrite
数学公式转图片生成网站

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。