Transformer代码完全解读!

本文详细介绍了Transformer模型的结构和工作原理,包括编码器、解码器、注意力机制、多头注意力、前馈全连接层等关键组件。通过一个简单的序列预测任务展示了Transformer的训练和预测流程,帮助读者深入理解Transformer的设计思想和实现细节。

↑↑↑关注后"星标"Datawhale

每日干货 & 每月组队学习,不错过

 Datawhale干货 

作者:安晟&闫永强,Datawhale成员

本篇正文部分约10000字,分模块解读并实践了Transformer,建议收藏阅读

2017年谷歌在一篇名为《Attention Is All  You  Need》的论文中,提出了一个基于attention(自注意力机制)结构来处理序列相关的问题的模型,名为Transformer。

Transformer在很多不同nlp任务中获得了成功,例如:文本分类、机器翻译、阅读理解等。在解决这类问题时,Transformer模型摒弃了固有的定式,并没有用任何CNN或者RNN的结构,而是使用了Attention注意力机制,自动捕捉输入序列不同位置处的相对关联,善于处理较长文本,并且该模型可以高度并行地工作,训练速度很快。

本文将按照Transformer的模块进行讲解,每个模块配合代码+注释+讲解来介绍,最后会有一个玩具级别的序列预测任务进行实战。

通过本文,希望可以帮助大家,初探Transformer的原理和用法,下面直接进入正式内容:

1 模型结构概览

如下是Transformer的两个结构示意图:

上图是从一篇英文博客中截取的Transformer的结构简图,下图是原论文中给出的结构简图,更细粒度一些,可以结合着来看。

模型大致分为Encoder(编码器)和Decoder(解码器)两个部分,分别对应上图中的左右两部分。

其中编码器由N个相同的层堆叠在一起(我们后面的实验取N=6),每一层又有两个子层。

第一个子层是一个Multi-Head Attention(多头的自注意机制),第二个子层是一个简单的Feed Forward(全连接前馈网络)。两个子层都添加了一个残差连接+layer normalization的操作。

模型的解码器同样是堆叠了N个相同的层,不过和编码器中每层的结构稍有不同。对于解码器的每一层,除了编码器中的两个子层Multi-Head AttentionFeed Forward,解码器还包含一个子层Masked Multi-Head Attention,如图中所示每个子层同样也用了residual以及layer normalization。

模型的输入由Input EmbeddingPositional Encoding(位置编码)两部分组合而成,模型的输出由Decoder的输出简单的经过softmax得到。

结合上图,我们对Transformer模型的结构做了个大致的梳理,只需要先有个初步的了解,下面对提及的每个模块进行详细介绍。

2 模型输入

首先我们来看模型的输入是什么样的,先明确模型输入,后面的模块理解才会更直观。

输入部分包含两个模块,EmbeddingPositional Encoding

2.1 Embedding层

Embedding层的作用是将某种格式的输入数据,例如文本,转变为模型可以处理的向量表示,来描述原始数据所包含的信息。

Embedding层输出的可以理解为当前时间步的特征,如果是文本任务,这里就可以是Word Embedding,如果是其他任务,就可以是任何合理方法所提取的特征。

构建Embedding层的代码很简单,核心是借助torch提供的nn.Embedding,如下:

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """
        类的初始化函数
        d_model:指词嵌入的维度
        vocab:指词表的大小
        """
        super(Embeddings, self).__init__()
        #之后就是调用nn中的预定义层Embedding,获得一个词嵌入对象self.lut
        self.lut = nn.Embedding(vocab, d_model)
        #最后就是将d_model传入类中
        self.d_model =d_model
    def forward(self, x):
        """
        Embedding层的前向传播逻辑
        参数x:这里代表输入给模型的单词文本通过词表映射后的one-hot向量
        将x传给self.lut并与根号下self.d_model相乘作为结果返回
        """
        embedds = self.lut(x)
        return embedds * math.sqrt(self.d_model)

2.2 位置编码:

Positional Encodding位置编码的作用是为模型提供当前时间步的前后出现顺序的信息。因为Transformer不像RNN那样的循环结构有前后不同时间步输入间天然的先后顺序,所有的时间步是同时输入,并行推理的,因此在时间步的特征中融合进位置编码的信息是合理的。

位置编码可以有很多选择,可以是固定的,也可以设置成可学习的参数。

这里,我们使用固定的位置编码。具体地,使用不同频率的sin和cos函数来进行位置编码,如下所示:

其中pos代表时间步的下标索引,向量 也就是第pos个时间步的位置编码,编码长度同Embedding层,这里我们设置的是512。上面有两个公式,代表着位置编码向量中的元素,奇数位置和偶数位置使用两个不同的公式。

思考:为什么上面的公式可以作为位置编码?

我的理解:在上面公式的定义下,时间步p和时间步p+k的位置编码的内积,即

Transformer是一种基于自注意力机制的神经网络模型,广泛应用于自然语言处理任务中,如机器翻译、文本生成等。下面是Transformer代码解读: 首先,我们需要导入必要的库和模块: ```python import torch import torch.nn as nn import torch.nn.functional as F ``` 接着,我们定义了一个叫做PositionalEncoding的类,用于对输入的序列进行位置编码。位置编码的目的是为了让模型能够感知输入序列中每个元素的位置信息,从而更好地处理序列中的长距离依赖关系。 ```python class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout=0.1, max_len=5000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(p=dropout) pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-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).transpose(0, 1) self.register_buffer('pe', pe) def forward(self, x): x = x + self.pe[:x.size(0), :] return self.dropout(x) ``` 在这个类中,我们首先定义了一个构造函数,其中d_model表示输入序列的维度,dropout表示dropout的概率,max_len表示输入序列的最大长度。在构造函数中,我们首先调用了父类的构造函数,然后定义了一个dropout层。 接着,我们创建了一个max_len x d_model的矩阵pe,并对其进行位置编码。具体来说,我们首先创建了一个长度为max_len的位置向量position,然后对每个位置向量应用一组不同的正弦和余弦函数,得到一个d_model维的位置编码向量。最后,我们将所有位置编码向量拼接成一个矩阵,并将其转置,以便与输入序列进行相加。 在forward函数中,我们将输入序列x与位置编码矩阵相加,并对结果进行dropout操作。 接下来,我们定义了一个叫做MultiHeadAttention的类,用于实现多头注意力机制。多头注意力机制是指将输入序列分别映射到多个不同的子空间中,并在每个子空间中计算注意力分数,最后将所有子空间的注意力分数加权求和得到最终的输出。 ```python class MultiHeadAttention(nn.Module): def __init__(self, d_model, nhead, dropout=0.1): super(MultiHeadAttention, self).__init__() self.nhead = nhead self.d_model = d_model self.head_dim = d_model // nhead self.qkv_proj = nn.Linear(d_model, 3 * d_model) self.out_proj = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(p=dropout) def forward(self, query, key, value, attn_mask=None): batch_size = query.size(0) qkv = self.qkv_proj(query).chunk(3, dim=-1) q, k, v = qkv[0], qkv[1], qkv[2] q = q.view(batch_size * self.nhead, -1, self.head_dim).transpose(0, 1) k = k.view(batch_size * self.nhead, -1, self.head_dim).transpose(0, 1) v = v.view(batch_size * self.nhead, -1, self.head_dim).transpose(0, 1) attn_scores = torch.bmm(q, k.transpose(1, 2)) attn_scores = attn_scores / math.sqrt(self.head_dim) if attn_mask is not None: attn_scores = attn_scores.masked_fill(attn_mask == 0, -1e9) attn_probs = F.softmax(attn_scores, dim=-1) attn_probs = self.dropout(attn_probs) attn_output = torch.bmm(attn_probs, v) attn_output = attn_output.transpose(0, 1).contiguous().view(batch_size, -1, self.d_model) attn_output = self.out_proj(attn_output) attn_output = self.dropout(attn_output) return attn_output ``` 在这个类中,我们首先定义了一个构造函数,其中d_model表示输入序列的维度,nhead表示头的数量,dropout表示dropout的概率。在构造函数中,我们首先调用了父类的构造函数,然后定义了一个线性层qkv_proj,用于将输入序列映射到三个不同的子空间中。接着,我们定义了一个线性层out_proj,用于将多头注意力机制的输出映射回原始的输入维度。最后,我们定义了一个dropout层。 在forward函数中,我们首先获取输入序列的batch_size,并将输入序列通过线性层qkv_proj映射到三个不同的子空间中。然后,我们将每个子空间的向量分别重塑为(batch_size * nhead, seq_len, head_dim)的形状,并将其转置,以便进行矩阵乘法。接着,我们计算每个位置之间的注意力分数,并对其进行缩放。如果存在attn_mask,则将其应用于注意力分数。然后,我们对注意力分数进行softmax操作,并对结果进行dropout。接着,我们将注意力分数与value矩阵相乘,并将结果重塑为(batch_size, seq_len, d_model)的形状。最后,我们将输出通过线性层out_proj映射回原始的输入维度,并对结果进行dropout。 最后,我们定义了一个叫做TransformerEncoderLayer的类,用于实现Transformer的编码器层。编码器层由两个子层组成:多头自注意力机制和前馈神经网络。 ```python class TransformerEncoderLayer(nn.Module): def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1): super(TransformerEncoderLayer, self).__init__() self.self_attn = MultiHeadAttention(d_model, nhead, dropout=dropout) self.linear1 = nn.Linear(d_model, dim_feedforward) self.dropout = nn.Dropout(p=dropout) self.linear2 = nn.Linear(dim_feedforward, d_model) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) def forward(self, src, src_mask=None): src2 = self.self_attn(src, src, src, attn_mask=src_mask) src = src + self.dropout(src2) src = self.norm1(src) src2 = self.linear2(self.dropout(F.relu(self.linear1(src)))) src = src + self.dropout(src2) src = self.norm2(src) return src ``` 在这个类中,我们首先定义了一个构造函数,其中d_model表示输入序列的维度,nhead表示头的数量,dim_feedforward表示前馈神经网络的隐藏层维度,dropout表示dropout的概率。在构造函数中,我们定义了一个多头自注意力机制self_attn,一个线性层linear1,一个dropout层,一个线性层linear2,以及两个LayerNorm层。 在forward函数中,我们首先使用多头自注意力机制self_attn对输入序列进行编码,并将结果与原始输入序列相加。然后,我们对结果进行归一化,并通过一个前馈神经网络进行非线性变换。最后,我们再次将结果与原始输入序列相加,并对结果进行归一化。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值