打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
信我!保准能看懂!深入浅出 通俗理解Transformer及其Pytorch源码!!!!
userphoto

2023.12.13 湖北

关注

PART01

Transformer是谷歌在2017年发表的论文Attention Is All You Need中提出的一种seq2seq模型,首先是在自然语言处理方面应用,现在已经取得了更大范围的应用与拓展,如今已经成为非常火热的模型。本篇博文将带着问题,边阅读文章和源码,阐述Transfomer的特点、框架、原理和相关理解。

一、为什么是Transformer?

传统的CNN、RNN(或者LSTM,GRU)计算是顺序的、迭代的、串行的,即只能从左向右依次计算或者从右向左依次计算,而这会导致两个问题:

  • 计算依赖问题。必须要等到当前字处理完才能处理下一个字,限制了模型的并行处理能力;

  • 顺序计算中的信息丢失。CNN、RNN在层次很深时,因为有max pooling,会逐渐丢失一些信息,尽管残差块、门机制等结构在一定程度上缓解了这个问题,但是对于特别长期的依赖现象,传统神经网络依旧无能为力。

而Transformer使用了位置嵌入 (Positional Encoding) 来理解语言的顺序,使用自注意力机制(Self Attention Mechanism)和全连接层进行计算,所有字都是同时训练,具有更好的并行性,不仅大大提高了计算效率,从长远来看更符合GPU的逻辑。

二、什么是Transformer?

1、整体框架

首先Transformer的整体主要分为Encoder和Decoder两大部分。输入的序列首先变成计算机便于处理的Embedding,然后Embedding传入Encoder进行编码,映射成隐藏层特征,经过Encoder后再结合上一次的output输入到Decoder中,最后用softmax计算序列下一个单词的概率。

2、Embedding

Embedding的作用是将输入(“我是一个学生”)初步整理成计算机能识别的特征信息。

Transformer将输入的序列首先转换为Word Embedding,由于 Transformer 模型没有循环神经网络的迭代操作,其还需要编码位置信息,即Positional Encoding。

2.1、字向量:Word embedding

可能你曾经学过one-hot编码,one-hot编码无法表示词与词之间的关系。Word Embedding解决了这个问题,随着训练,他能够使得意思相近的单词表达结果越来越近。

Word Embedding首先设计一个可学习的权重矩阵W,通过词向量与权重矩阵进行相乘,不断训练得到新的表示结果。如下例:

在pytorch中,我们使用torch自带的embedding功能,权重矩阵先进行随机初始化(当然也可以选择 Pre-trained 的结果),但设为 Trainable。这样在 training 过程中不断地对 Embeddings 进行改进。

class Embeddings(nn.Module):    def __init__(self, d_model, vocab): super(Embeddings, self).__init__() self.lut = nn.Embedding(vocab, d_model) #vocab表示词汇表的数量,d_model表示embedding的维度,即词向量的维度 self.d_model = d_model #表示embedding的维度
def forward(self, x): return self.lut(x) * math.sqrt(self.d_model)  

2.2、位置编码:Positional Encoding

Transformer 摈弃了 RNN 的结构,因此需要一个东西来标记各个字之间的时序 or 位置关系,而这个东西,就是位置嵌入

以往我们根据单词之间的间隔比例算距离,如果设置整个句子长度为1,如:Attention is all you need ,其中is和you之间的距离为0.5。而:To follow along you will first need to install PyTorch较长文本中子里的0.5距离则会隔很多单词,这显然不合适。

所以总结一下理想的位置编码应该满足:

  1. 为每个字输出唯一的编码

  2. 不同长度的句子之间,任何两个字之间的差值应该保持一致

  3. 值应该是有界

作者为此设计了一种Positional Encoding,首先,它不是一个数字,而是一个包含句子中特定位置信息的d维向量。其次,这种嵌入方式没有集成到模型中,相反,这个向量是用来给句子中的每个字提供位置信息的,换句话说,我们通过注入每个字位置信息的方式,增强了模型的输入(其实说白了就是将位置嵌入和字嵌入相加,然后作为输入)

符号解释:其中,pos代表是词在句子中的位置,d是词向量的维度(通常为512),2i代表d中的偶数维度,同理,i代表奇数维度。

在pytorch中,使用自带的Positional Embedding模块进行设计

# Positional Encodingclass PositionalEncoding(nn.Module):    '实现PE功能'    def __init__(self, d_model, dropout, max_len=5000):  ##vocab表示词汇表的数量,d_model表示embedding的维度,即词向量的维度        super(PositionalEncoding, self).__init__()        self.dropout = nn.Dropout(p=dropout)                pe = torch.zeros(max_len, d_model)   # max_len=5000表示句子最多有5000个词        position = torch.arange(0., max_len).unsqueeze(1)         div_term = torch.exp(torch.arange(0., d_model, 2) *                             -(math.log(10000.0) / d_model))  # 那个分数
pe[:, 0::2] = torch.sin(position * div_term) # 偶数列 pe[:, 1::2] = torch.cos(position * div_term) # 奇数列 pe = pe.unsqueeze(0) # [1, max_len, d_model] self.register_buffer('pe', pe) # 将pe注册到模型的buffers()属性中,这代表该变量对应的是一个持久态,不会有梯度传播给它,但是能被模型的state_dict记录下来。
def forward(self, x): x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) #输入模型的整个Embedding是Word Embedding与Positional Encoding直接相加之后的结果。 return self.dropout(x)

3、Encoder

Encoder的作用是将刚刚初步整理的Embedding,在Encoder中通过注意力机制等进行进一步的编码。

Encoder部分是由个层相同小Encoder Layer串联而成。小Encoder Layer可以简化为两个部分:(1)多头自注意力层:Multi-Head Self Attention (2) 前向反馈网络层:Feed-Forward network。前者用于捕捉特征之间的关系,后者是进一步编码学习

3.1、自注意力机制:Self-Attention

至此我们得到了单个自注意力的输出,当然可以把上面的向量计算变成矩阵的形式(即将多个字向量摞成矩阵形式,矩阵第t行为第t个词的向量),从而一次计算出所有时刻的输出。

3.2、多头自注意力层:Multi-Head Self Attention

pytorch实现:

class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):'Take in model size and number of heads.' super(MultiHeadedAttention, self).__init__()assert d_model % h == 0# 每个head中向量的维度,通常是512/8=64 self.d_k = d_model // h# head的数量,通常为8 self.h = h# 设置4个变换,其中3个用于生成Q、K、V clones是一个方法,代表将结构相同的层实例化n次 self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):''' 实现MultiHeadedAttention。 输入的q,k,v是形状 [batch, L, d_model]。 输出的x 的形状同上。 '''if mask is not None:# Same mask applied to all h heads. mask = mask.unsqueeze(1) nbatches = query.size(0)
# 1) 矩阵点乘生成Q、K、V# 这一步qkv变化:[batch, L, d_model] ->[batch, h, L, d_model/h] query, key, value = \ [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)for l, x in zip(self.linears, (query, key, value))]
# 2) 计算注意力attn 并加权到V,得到Z,维度与Q、K、V类似。# qkv :[batch, h, L, d_model/h] -->x:[b, h, L, d_model/h], attn[b, h, L, L] x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)# 3) 维度复原# 上一步的结果合并在一起还原成原始输入序列的形状 x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)# 最后将Z输入最后一个全连接层return self.linears[-1](x)

3.3、连接与归一化:Add & Norm

连接Add

Add&Norm的pytorch实现如下:

class LayerNorm(nn.Module):'''构造一个layernorm模块'''def __init__(self, features, eps=1e-6):        super(LayerNorm, self).__init__()        self.a_2 = nn.Parameter(torch.ones(features))        self.b_2 = nn.Parameter(torch.zeros(features))        self.eps = eps
def forward(self, x):'Norm' mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True)return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

class SublayerConnection(nn.Module):'''Add+Norm'''def __init__(self, size, dropout): super(SublayerConnection, self).__init__() self.norm = LayerNorm(size) self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):'add norm'return x + self.dropout(sublayer(self.norm(x)))

3.4、前向反馈网络层:Feed-Forward network

# Position-wise Feed-Forward Networksclass PositionwiseFeedForward(nn.Module):'实现FFN函数'def __init__(self, d_model, d_ff, dropout=0.1):super(PositionwiseFeedForward, self).__init__()self.w_1 = nn.Linear(d_model, d_ff)self.w_2 = nn.Linear(d_ff, d_model)self.dropout = nn.Dropout(dropout)
def forward(self, x):return self.w_2(self.dropout(F.relu(self.w_1(x))))

览Encoder

通过上面的步骤,我们大致已经基本了解了Encoder的全部原理和主要构成,那么在此回顾整理一下:

根据图上的,将步骤总结如下

4、Decoder

因为输入(“我是一个学生”)在Encoder中进行了编码,这里我们具体讨论Decoder的操作,也就是如何得到输出(“I am a student”)的过程。

Encoder与Decoder的关系可以用下图描述(以机器翻译为例):

Decoder的大框架如下:

可以看到Decoder主要由Masked Multi-Head Self-Attention、Multi-Head Encoder-Decoder Attention、FeedForward Network组成,和 Encoder 一样,上面三个部分的每一个部分,都有一个残差连接,后接一个 Layer Normalization。

4.1、掩码多头注意力层:Mask-Multi-Head-Attention

传统 Seq2Seq 中 Decoder 使用的是 RNN 模型,因此在训练过程中输入t时刻的词,模型无论如何也看不到未来时刻的词,因为循环神经网络是时间驱动的,但是Transformer 抛弃了RNN改为Self-Attention,会发生一个问题:整个ground truth都暴露在Decoder中,这显然不对。

所以这一层目的是忽略某些位置,不计算与其相关的注意力权重。为的是为了防止模型看到要预测的数据,防止泄露

我们要对 Scaled Scores 进行 Mask,举个例子现在的ground truth 为 ' I am fine',当我们输入 'I' 时,模型目前仅知道包括 'I' 在内之前所有字的信息,即 '' 和 'I' 的信息,不应该让其知道 'I' 之后词的信息。道理很简单,我们做预测的时候是按照顺序一个字一个字的预测,怎么能这个字都没预测完,就已经知道后面字的信息了呢?Mask 非常简单,首先生成一个下三角全 0,上三角全为负无穷的矩阵然后将其与 Scaled Scores 相加即可:

之后为了数据更方便处理,做一次softmax,就能将负无穷变换为0,得到各个字之间的权重。而Multi无非就是并行的对上述步骤多做几次,不再赘述。这一部分的pytorch实现为:

def subsequent_mask(size):'''    mask后续的位置,返回[size, size]尺寸下三角Tensor    对角线及其左下角全是1,右上角全是0    '''    attn_shape = (1, size, size)    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')return torch.from_numpy(subsequent_mask) == 0

4.2、编码-解码 多头注意力层:Encoder-Decoder Multi-head Attention

这一部分就是之前讲过的多头自注意力层,只不过输入不同,这部分的K,V为之前Encoder的输出,Q为Decoder中Masked Multi-head Attention 的输出。

这一部分的pytorch实现为:

class DecoderLayer(nn.Module):'Decoder is made of self-attn, src-attn, and feed forward (defined below)'def __init__(self, size, self_attn, src_attn, feed_forward, dropout):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):'将decoder的三个Sublayer串联起来' m = memory x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))return self.sublayer[2](x, self.feed_forward)

4.3、输出层:Output

当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,只需要在结尾再添加一个全连接层和softmax层,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果

总览Decoder

对应上图,回顾整个Decoder的全部流程如下 Time Step 1 初始输入:起始符 + Positional Encoding(位置编码) 中间输入:(我是一个学生)Encoder Embedding(Encoder编码后的文本特征) 最终输出:产生预测“I”

Time Step 2 初始输入:起始符 + “I”+ Positonal Encoding 中间输入:(我是一个学生)Encoder Embedding 最终输出:产生预测“I am” .... ... Time Step n 初始输入:起始符 + “I”+ “am”+ Positonal Encoding 中间输入:(我是一个学生)Encoder Embedding 最终输出:产生预测“I am a student”

原文链接:
https://juejin.cn/post/7082683591653589029?searchId=202312042124344E6B61CEF5277E763BE0

01

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
搞懂 Vision Transformer 原理和代码,看这篇技术综述就够了
Step-by-step to Transformer:深入解析工作原理(以Pytorch机器翻译为...
Transformer算法完全解读
Transformer 一起动手编码学原理
代码阅读
如何从零开始用PyTorch实现Chatbot?(附完整代码)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服