原创 | Attention is all you need 论文解析(附代码)

news2024/12/23 4:34:32

262006a318220d8a2ab5537d39a3b650.png

作者:杨金珊审校:陈之炎

本文约4300字,建议阅读8分钟“Attention is all you need”一文在注意力机制的使用方面取得了很大的进步,对Transformer模型做出了重大改进。

目前NLP任务中的最著名模型(例如GPT-2或BERT),均由几十个Transformer或它们的变体组成。

背景

减少顺序算力是扩展神经网络GPU、ByteNet和ConvS2S的基本目标,它们使用卷积神经网络作为基本构建块,并行计算所有输入和输出位置的隐含表示。在这些模型中,将来自两个任意输入或输出位置的信号关联起来,所需的操作数量随着位置距离的增加而增加,对于ConvS2S来说,二者是线性增长的;对于ByteNet来说,二者是对数增长的。这使得学习遥远位置之间的依赖关系变得更加困难。在Transformer中,将操作数量减少到一个恒定数值,这是以降低有效分辨率为代价的,因为需要对注意力权重位置做平均,多头注意力 (Multi-Head Attention)抵消了这一影响。

为什么需要transformer

在序列到序列的问题中,例如神经机器翻译,最初的建议是基于在编码器-解码器架构中使用循环神经网络(RNN)。这一架构在处理长序列时受到了很大的限制,当新元素被合并到序列中时,它们保留来自第一个元素的信息的能力就丧失了。在编码器中,每一步中的隐含状态都与输入句子中的某个单词相关联,通常是最邻近的那个单词。因此,如果解码器只访问解码器的最后一个隐含状态,它将丢失序列的第一个元素相关的信息。针对这一局限性,提出了注意力机制的概念。

与通常使用RNN时关注编码器的最后状态不同,在解码器的每一步中我们都关注编码器的所有状态,从而能够访问有关输入序列中所有元素的信息。这就是注意力所做的,它从整个序列中提取信息,即过去所有编码器状态的加权和,解码器为输出的每个元素赋予输入的某个元素更大的权重或重要性。从每一步中正确的输入元素中学习,以预测下一个输出元素。

但是这种方法仍然有一个重要的限制,每个序列必须一次处理一个元素。编码器和解码器都必须等到t-1步骤完成后才能处理第t-1步骤。因此,在处理庞大的语料库时,计算效率非常低。

什么是Transformer

Transformer是一种避免递归的模型架构,它完全依赖于注意力机制来绘制输入和输出之间的全局依赖关系。Transformer允许显著的并行化……Transformer是第一个完全依靠自注意力来计算输入和输出的表示,而不使用序列对齐的RNN或卷积的传导模型。

919922501f0cde18867c912c61facf76.png 

06a3be1fe1687e2ded43c36c9b991cb8.png

图1  Transformer 架构

从图1可以观察到,左边是一个编码器模型,右边是一个解码器模型。两者都包含一个重复N次的“一个注意力和一个前馈网络”的核心块。但为此,首先需要深入探讨一个核心概念:自注意力机制。

Self-Attention基本操作

Self-attention是一个序列到序列的操作:一个向量序列进去,一个向量序列出来。我们称它们为输入向量0a50a2a7089e564eda215f0c96b3bfd0.png, 09b9b9b1c6f31bf85dc1c29e57774ebb.png,…,c66785e5c77156c9c63c971c1682a6f6.png和相应的输出向量fdfcb3ef846dd83420b344b0fa332afa.png, cb1740d9a396fcae92ca3b3804d9bedc.png,…,0b7b4daa9a10260fc1e89379a5c7b67a.png。这些向量的维数都是k。要产生输出向量fa691bb369fd46f9922465c6c0704d2c.png,Self-attention操作只需对所有输入向量取加权平均值,最简单的选择是点积。在我们的模型的Self-attention机制中,我们需要引入三个元素:查询、值和键(Queries, Values and Keys)。

class SelfAttention(nn.Module):
  def __init__(self, embed_size, heads):
    super(SelfAttention,self).__init__()
    self.embed_size=embed_size
    self.heads=heads
    self.head_dim=embed_size//heads


    assert(self.head_dim*heads==embed_size),"Embed size needs to be div by heads"


    self.values=nn.Linear(self.head_dim, self.head_dim, bias=False)
    self.keys=nn.Linear(self.head_dim, self.head_dim, bias=False)
    self.queries=nn.Linear(self.head_dim, self.head_dim, bias=False)
    self.fc_out=nn.Linear(heads*self.head_dim, embed_size)
  def forward(self,values,keys,query,mask):
    N=query.shape[0]
    value_len,key_len,query_len=values.shape[1],keys.shape[1],query.shape[1]


    #split embedding into self.heads pieces
    values=values.reshape(N,value_len,self.heads,self.head_dim)
    keys=keys.reshape(N,key_len,self.heads,self.head_dim)
    queries=query.reshape(N,query_len,self.heads,self.head_dim)


    values=self.values(values)
    keys=self.keys(keys)
    queries=self.queries(queries)
    energy=torch.einsum("nqhd,nkhd->nhqk",[queries,keys])
    #queries shape: (N,query_len, heads, heads_dim)
    #keys shape: (N,key_len, heads, heads_dim)
    #energy shape: (N,heads,query_len,key_len)
    if mask is not None:
      energy=energy.masked_fill(mask==0,float("-1e20"))#close it ,0
    attention=torch.softmax(energy/(self.embed_size**(1/2)),dim=3)#softmax
    out=torch.einsum("nhql,nlhd->nqhd",[attention,values]).reshape(N,query_len,self.heads*self.head_dim)
    #attention shape: (N,heads, query_len,key_len)
    #values shape: (N,value_len,heads,head_dim)#key_len=value_len=l
    #after einsum(N,query_len,heads,head_dim) then flatten last two dim


    out=self.fc_out(out)
    return out

Queries,Values和Keys

在自注意力机制中,通常输入向量以三种不同的方式使用:查询、键和值。在每个角色中,它将与其他向量进行比较,以获得自己的输出9becda09d77a9545f4ef2fa9adfca743.png(Query),获得第j个输出b0816e43c794bb5dc589200c8adf5234.png(Key),并在权重建立后计算每个输出向量(Value)。为了得到这些,我们需要三个维数为k * k的权重矩阵,并为每个15118c314aee3a3c6d5e39c2020e1551.png计算三个线性变换:

64c628ff9296ea0866fae01cf376ed12.png

0a182b7c706e76577040d7fe518d3e43.png

图2  查询、值和键(Queries, Values and Keys)三元素

通常称这三个矩阵为K、Q和V,这三个可学习权值层应用于相同的编码输入。因此,由于这三个矩阵都来自相同的输入,可以应用输入向量本身的注意力机制,即“Self-attention”。

TheScaledDot-ProductAttention(带缩放的点积注意力)

输入由维784c0225199bef07d0119129175124e1.png的“查询”和“键”以及维f2ee7404accf9c5be54e87c12d767106.png的“值”值组成。我们用所有“键”计算“查询”的点积,每个“键”除以b82a78e71bd0afa9e547386577eda51f.png的平方根,应用一个softmax函数来获得值的权重。

使用Q, K和V矩阵来计算注意力分数。分数衡量的是对输入序列的其他位置或单词的关注程度。也就是说,查询向量与要评分的单词的键向量的点积。对于位置1,我们计算862144d705dc9341c13cae30d9174245.png6b5251736593f6f3e00b1b8ada4e2b6a.png的点积,然后是b10bc0c38509ef726a2afc74c0b8b4ba.png957849ead83d737d1c17657783617036.png2, 6c30644643d22a908e8e897367e04279.png49ec6e95b9e3392b641e01424735f7f9.png等等,…

接下来应用“缩放”因子来获得更稳定的梯度。softmax函数在大的值下无法正常工作,会导致梯度消失和减慢学习速度[1]。在“softmax”之后,我们乘以“值”矩阵,保留想要关注的单词的值,并最小化或删除无关单词的值(它在V矩阵中的值应该非常小)。

这些操作的公式为:

21b7069fef8fe9c0861c002fe28c1767.png

Multi-head Attention(多头注意力)

在前面的描述中,注意力分数一次集中在整个句子上,即使两个句子包含相同的单词,但顺序不同,也将产生相同的结果。相反,如果想关注单词的不同部分,”self-attention”的辨别能力则比较大,通过组合几个自注意力头,将单词向量分成固定数量(h,头的数量)的块,然后使用Q, K和V子矩阵将自注意力应用到相应的块。

b563b6143c09c01765252104fbefff23.png 

fe3b81bc1ec2c618bb1060d35fa9b6fd.png

图3 多头注意力机制

由于下一层(前馈层)只需要一个矩阵,每个单词的一个向量,所以“在计算每个头部的点积之后,需要连接输出矩阵,并将它们乘以一个附加的权重矩阵Wo”[2]。最后输出的矩阵从所有的注意力头部获取信息。

PositionalEncoding(位置编码)

前文已经简单地提到,由于网络和self-attention机制是排列不变的,句子中单词的顺序是该模型中需要解决的问题。如果我们打乱输入句子中的单词,会得到相同的解。需要创建单词在句子中位置的表示,并将其添加到单词嵌入(embedding)中。

为此,我们在编码器和解码器栈底部的输入嵌入中添加了“位置编码”。位置编码与嵌入具有相同的维数,因此两者可以求和,位置编码有多种选择。

应用一个函数将句子中的位置映射为实值向量之后,网络将学习如何使用这些信息。另一种方法是使用位置嵌入,类似于单词嵌入,用向量对每个已知位置进行编码。“它需要训练循环中所有被接受的位置的句子,但位置编码允许模型外推到比训练中遇到的序列长度更长的序列”,[1]。

6fcaaaaa7eda4684852e4520c7fc24dc.png

TransformerBlock(Transformer代码块)

class TransformerBlock(nn.Module):
  def __init__(self, embed_size,heads,dropout,forward_expansion):
    super(TransformerBlock,self).__init__()
    self.attention=SelfAttention(embed_size,heads)
    self.norm1=nn.LayerNorm(embed_size)
    self.norm2=nn.LayerNorm(embed_size)


    self.feed_forward=nn.Sequential(
        nn.Linear(embed_size, forward_expansion*embed_size),
        nn.ReLU(),
        nn.Linear(forward_expansion*embed_size,embed_size)
    )
    self.dropout=nn.Dropout(dropout)
  def forward(self,values,keys,query,mask):
    attention=self.attention(values,keys,query,mask)
    x=self.dropout(self.norm1(attention+query))
    forward=self.feed_forward(x)
    out=self.dropout(self.norm2(forward+x))
    return out

Theencoder(编码器)

  • 位置编码:将位置编码添加到输入嵌入(将输入单词被转换为嵌入向量)。

  • N=6个相同的层,包含两个子层:一个多头自注意力机制,和一个全连接的前馈网络(两个线性转换与一个ReLU激活)。它按位置应用于输入,这意味着相同的神经网络会应用于属于句子序列的每一个“标记”向量。

64e855ae169a794bb62251c1f31af8ec.png 11c850c41ddc62c3bdcbc4109c69be83.png

  • 每个子层(注意和FC网络)周围都有一个残余连接,将该层的输出与其输入相加,然后进行归一化。

  • 在每个残余连接之前,应用正则化:“对每个子层的输出应用dropout,然后将其添加到子层输入并正则化。

564800fc6793e62f52b923788cc5cc28.png 

487227ddbed770cb2127d2fe4652a2e4.png

图4 编码器结构

class Encoder(nn.Module):
  def __init__(
      self,
      src_vocab_size,
      embed_size,
      num_layers,
      heads,
      device,
      forward_expansion,
      dropout,
      max_length):
    super(Encoder,self).__init__()
    self.embed_size=embed_size
    self.device=device
    self.word_embedding=nn.Embedding(src_vocab_size,embed_size)
    self.position_embedding=nn.Embedding(max_length,embed_size)
    self.layers=nn.ModuleList(
        [
         TransformerBlock(
             embed_size,
             heads,
             dropout=dropout,
             forward_expansion=forward_expansion
         ) for _ in range(num_layers)
        ]
    )
    self.dropout=nn.Dropout(dropout)
  def forward(self,x,mask):
    N,seq_length=x.shape
    positions=torch.arange(0,seq_length).expand(N,seq_length).to(self.device)


    out=self.dropout(self.word_embedding(x)+self.position_embedding(positions))
    for layer in self.layers:
      out=layer(out,out,out,mask)   #key,query,value all the same
    return out

DecoderBlock(解码器代码块)

  • 位置编码:编码器的编码相类似。

  • N=6个相同的层,包含3个子层。第一,屏蔽多头注意力或屏蔽因果注意力,以防止位置注意到后续位置。禁用点积注意力模块的软最大层,对应的值设置为−∞。第二个组件或“编码器-解码器注意力”对解码器的输出执行多头注意力,“键”和“值”向量来自编码器的输出,但“查询”来自前面的解码器层,使得解码器中的每个位置都能覆盖输入序列中的所有位置。,最后是完连接的网络。

  • 每个子层周围的残差连接和层归一化,类似于编码器。

  • 然后重复在编码器中执行的相同残差dropout。

class DecoderBlock(nn.Module):
  def __init__(self, embed_size,heads,dropout,forward_expansion,device):
    super(DecoderBlock,self).__init__()
    self.attention=SelfAttention(embed_size,heads)
    self.norm=nn.LayerNorm(embed_size)
    self.transformer_block=TransformerBlock(
        embed_size, heads, dropout, forward_expansion
    )
    self.dropout=nn.Dropout(dropout)
  def forward(self,x,value,key,src_mask,trg_mask):
    #source mask and target mask
    attention=self.attention(x,x,x,trg_mask)#trg_mask is the mask mult-headed attention the first one in decoder block
    query=self.dropout(self.norm(attention+x))
    out=self.transformer_block(value,key,query,src_mask)
    return out

在N个堆叠的解码器的最后,线性层,一个全连接的网络,将堆叠的输出转换为一个更大的向量,logits。

52ef61bed42ebf728ba3deb42a0d2a88.png

图5 解码器结构

Joining all the pieces: the Transformer(全部拼接起来构成Transformer)

定义并创建了编码器、解码器和linear-softmax最后一层等部件之后,便可以将这些部件连接起来,形成Transformer模型。

值得一提的是,创建了3个掩码,包括:

编码器掩码:它是一个填充掩码,从注意力计算中丢弃填充标记。

解码器掩码1:该掩码是填充掩码和前向掩码的结合,它将帮助因果注意力丢弃“未来”的标记,我们取填充掩码和前向掩码之间的最大值。

解码器掩码2:为填充掩码,应用于编码器-解码器注意力层。

class Transformer(nn.Module):
  def __init__(
      self,
      src_vocab_size,
      trg_vocab_size,
      src_pad_idx,
      trg_pad_idx,
      embed_size=256,
      num_layers=6,
      forward_expansion=4,
      heads=8,
      dropout=0,
      device="cuda",
      max_length=100):
    super(Transformer,self).__init__()
    self.encoder=Encoder(
        src_vocab_size,
        embed_size,
        num_layers,
        heads,
        device,
        forward_expansion,
        dropout,
        max_length
    )
    self.decoder=Decoder(
        trg_vocab_size,
        embed_size,
        num_layers,
        heads,
        forward_expansion,
        dropout,
        device,
        max_length
    )
    self.src_pad_idx=src_pad_idx
    self.trg_pad_idx=trg_pad_idx
    self.device=device
  def make_src_mask(self,src):
    src_mask=(src!= self.src_pad_idx).unsqueeze(1).unsqueeze(2)
    #(N,1,1,src_len)
    return src_mask.to(self.device)
  def make_trg_mask(self,trg):
    N,trg_len=trg.shape
    trg_mask=torch.tril(torch.ones((trg_len,trg_len))).expand(
        N,1,trg_len,trg_len
    )
    return trg_mask.to(self.device)
  def forward(self,src,trg):
    src_mask=self.make_src_mask(src)
    trg_mask=self.make_trg_mask(trg)
    enc_src=self.encoder(src,src_mask)
    out=self.decoder(trg, enc_src,src_mask, trg_mask)
    return out

参考文献:

[1] Peter Bloem, “Transformers from scratch” blog post, 2019.

[2] Jay Alammar, “The Ilustrated Transformer” blog post, 2018.

编辑:王菁

校对:林亦霖

数据派研究部介绍

数据派研究部成立于2017年初,以兴趣为核心划分多个组别,各组既遵循研究部整体的知识分享实践项目规划,又各具特色:

算法模型组:积极组队参加kaggle等比赛,原创手把手教系列文章;

调研分析组:通过专访等方式调研大数据的应用,探索数据产品之美;

系统平台组:追踪大数据&人工智能系统平台技术前沿,对话专家;

自然语言处理组:重于实践,积极参加比赛及策划各类文本分析项目;

制造业大数据组:秉工业强国之梦,产学研政结合,挖掘数据价值;

数据可视化组:将信息与艺术融合,探索数据之美,学用可视化讲故事;

网络爬虫组:爬取网络信息,配合其他各组开发创意项目。

点击文末“阅读原文”,报名数据派研究部志愿者,总有一组适合你~

转载须知

如需转载,请在开篇显著位置注明作者和出处(转自:数据派THUID:DatapiTHU),并在文章结尾放置数据派醒目二维码。有原创标识文章,请发送【文章名称-待授权公众号名称及ID】至联系邮箱,申请白名单授权并按要求编辑。

未经许可的转载以及改编者,我们将依法追究其法律责任。

e89a71e50b4d59bdc3a02a56083d1582.png点击“阅读原文”加入组织~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/87366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数集项目之 MCDF】(四) 整形器 formatter

根据上一章的arbiter结构图,结合设计文档中MCDF的整体结构图,可以发现formatter整形器模块是arbiter的上级,负责最终的数据输出,与外界数据接收端相连。 第一节 fromatter文档理解 设计文档formatter的部分时序介绍如下 如图所示…

钡铼技术S274数据遥测终端机

钡铼技术S274数据遥测终端机功能特点: 内置 2 路 DC 直流电源输出,无需单独额外增加变送器的电源适配器,节省布线成本;  采用完备的防掉线机制,保证数据终端永远在线,掉线重发数据以及掉线短信通知用户…

第38篇 网络(八)TCP(二)

导语 在上一节里我们使用TCP服务器发送一个字符串,然后在TCP客户端进行接收。在这一节将重新写一个客户端程序和一个服务器程序,这次实现客户端进行文件的发送,服务器进行文件的接收。有了上一节的基础,这一节的内容就很好理解了…

postgresql_internals-14 学习笔记(三)冻结、rebuild

一、 Freezing 冻结 1. 引入原因 简单说来就是目前pg事务id只有32位,大业务量下很可能用完,触发事务id回卷(循环使用)。而pg是根据事务id大小判断可见性的,如果新事务却使用了小id,旧事务将可以看到新事务…

win下 conda 虚拟环境没有名字怎么进入

本文主要介绍windows下,在conda 虚拟环境名字消失后的解决办法。主要介绍两种解决方案。 文章目录前言解决方案一:往.condarc文件中添加envs_dirs1. 设置envs_dirs2. 重新查看虚拟环境解决方案二:直接通过path 激活虚拟环境总结前言 我们都知…

Grafana 监控大屏可视化图表

Grafana 系列文章,版本:OOS v9.3.1 Grafana 的介绍和安装Grafana监控大屏配置参数介绍(一)Grafana监控大屏配置参数介绍(二)Grafana监控大屏可视化图表 前面我们以Time series 图表为例,学习了面…

每天投递一两个公司,我连续投了三个月

作者:阿秀校招八股文学习网站:https://interviewguide.cn你好,我是阿秀。阿秀以前在秋招的时候投递过八九十份简历,当时还没有简历一键上传功能,很多时候都需要自己去那些公司注册账号,然后找到校园招聘模块…

手机备忘录误删恢复的操作方法

手机备忘录在使用的过程中,会有多种不同的操作,通过不同的操作来实现不同的效果。对于有的内容来说,是可以过期删除的,但是在删除这个操作的过程当中,如果不小心把有用的东西误删了,那么恢复误删内容的操作…

行内块元素因换行带来的间隔问题及解决方法

行内块元素因换行带来的间隔问题 先看一个案例所展示的行内块元素因换行带来的间隔问题,俺直接上截图 再来一张截图可以更加清楚地看见行内块元素之间因换行而带来的间隔 从上方所有图片可以看出,行内块元素之间一行并排放置时编译器中的换行操作会在浏览器渲染时带来行内块元…

鸡汤来了

这几天,网上铺天盖地都是各种感染新冠的消息,连我一直关注的和菜头也感染上了,关键是连怎么感染的都不知道。他写道:我也很委屈。自从北京开始比拼首阳之后,我的确是缩在家里,想着越晚感染越好。为了达到这…

基于STM32单片机和RFID的智能考勤系统设计

提示:记录2022年4月做的毕设 文章目录前言一、任务书1.1设计(研究)目标:1.2设计(研究)内容:二、思路三、硬件3.1RFID模块3.2 时钟模块四、联系我五、代码以及框图等资料喜欢请点赞哦!前言 基于STM32的考勤系统,主控使用STM32F103ZET6&#x…

代码随想录算法训练营第四十八天|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III

198.打家劫舍 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。 所以这里就更感觉到,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。 动规五部曲分析如下: 确定dp数组以及下标的含义 dp[i]&#xf…

华为的 HCIE 和 HCIP 的发展趋势如何

华为认证: 华为认证是华为基于“平台生态”战略,围绕“云-管-端”协同的新ICT技术架构,打造的业界覆盖ICT领域最广的认证体系,华为认证覆盖ICT领域,致力于提供领先的人才培养体系和认证标准,培养数字化时代…

基于51单片机的DS12C887电子钟万年历带农历温度

基于51单片机的DS12C887电子钟万年历( proteus仿真程序讲解视频) 仿真图proteus 7.8及以上 程序编译器:keil 4/keil 5 编程语言:C语言 设计编号:S0028 资料下载链接(可点击): 讲解视频 基…

通信协议综述-第2讲 | 网络分层的真实含义是什么?

长时间从事计算机网络相关的工作,我发现,计算机网络有一个显著的特点,就是这是一个不仅需要背诵,而且特别需要将原理烂熟于胸的学科。很多问题看起来懂了,但是就怕往细里问,一问就发现你懂得没有那么透彻。 我们上一节列了之后要讲的网络协议。这些协议本来没什么稀奇,每…

微服务框架 SpringCloud微服务架构 分布式缓存 41 Redis 持久化 41.2 RDB 持久化【底层原理】

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 分布式缓存 文章目录微服务框架分布式缓存41 Redis 持久化41.2 RDB 持久化【底层原理】41.2.1 RDB41.2.2 总结41 Redis 持久化 41.2 RDB 持…

【IDEA】解决IDEA插件使用Lombok找不到符号问题

1.概述 今天合并了有冲突的代码,合并完毕后,发现运行的时候找不到符号。类似如下报错 java: 找不到符号* 符号: 方法 setData(java.lang.String)* 位置: 类型为com.fastjson.demo.lombok.

Vue渲染器(三):简单diff算法

渲染器(三):简单diff算法 我们将介绍渲染器的核心Diff算法。简单来说就是当新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点。 1.减少DOM操作的性能开销: 核心Di…

第46篇 进阶(六) 国际化

导语 在第2篇中讲述如何显示中文时,曾提到使用QTextCodec和tr()的方式直接显示中文,其实这只是一种临时的方法,方便我们快速完成程序,显示效果。当真正要发布一个程序时,最好的方式是在程序中使用英文字符串&#xff…

四、函数基础、函数种类、形实参和映射关系

四、函数基础、函数种类、形实参和映射关系 编程基本原则:高内聚、低耦合。 我总结: 模块自身,要求高内聚:模块中代码相关性变强,代码紧密联系程度变高,希望它能独立完成一个功能。模块之间,…