TransformerTransformer由论文《Attention is All You Need》提出接下来我会用通俗的方法解释整体结构流程并且各个模块的pytorch代码实现。整体结构Transformer 由Encoder和Decoder两个部分组成Encoder 和 Decoder 都包含 6 个 block。1. Embedding无论源文本嵌入还是目标文本嵌入都是为了将文本中词汇的数字表示转变为向量的表示希望在这样的高维空间捕捉词汇间的关系。其中Embedding方式有很多我们可以使用Word2Vec等方式得到也可以使用一个可学习的Embedding进行表示。代码实现class Embedding(nn.Module): def __init__(self,d_model,vocab) - None: d_model表示词嵌入的维度 vocab表示词表的大小 super().__init__() self.d_model d_model self.vocab vocab self.embed nn.Embedding(vocab,d_model) def forward(self,x): # x代表输入进模型的文本通过词汇映射后的数字张量 return self.embed(x)*math.sqrt(self.d_model)其中我们会发现embeding之后乘以了为什么这个其实主要是为了平衡后面位置编码向量因为我们初始化embedding的时候服从正态分布方差会随着变大导致方差变小取值返回就会比较小而位置编码使用的是正余弦函数范围为[-1,1]相加之后会导致词向量被淹没语义丢失。2. PositionalEncoding在Transformer的编码器结构中并没有针对词汇位置信息的处理因此需要在Embedding层后加入位置编码器将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中以弥补位置信息的缺失公式其中pose表示单词在句子中的绝对位置表示向量嵌入维度与词向量维度一致表示偶数维度 表示奇数维度优点外推性当我们训练集里面的最大长度为max_len当来了一个更长的句子我们可以根据公式直接计算出超出max_len的位置编码模型可以计算出两个单词位置的相对距离即posk可以用pos的位置计算得到根据正余弦函数的准则、代码实现from torch import nn import torch class PositionalEncoding(nn.Module): def __init__(self,d_model,dropout,max_length) - None: super().__init__() d_model词嵌入维度 dropout置零比例 max_length每个句子的最大长度 self.dropout nn.Dropout(dropout) # 初始化位置编码矩阵pe形状为[max_length, d_model] pe torch.zeros(max_length,d_model) # 初始化一个绝对位置矩阵词汇的绝对位置就是通过它的索引去表示形状为[max_length, 1] position torch.arange(0,max_length).unsqueeze(1) # 初始化完成绝对位置矩阵接下来需要考虑如何将绝对位置加入到位置编码矩阵 # 其中最简单的方式就是我们需要将绝对位置矩阵从[max_length, 1] 变换成[max_length, d_model]形状然后覆盖位置编码矩阵 # 做这种变换需要一个1×d_model的矩阵且这个矩阵能够将位置编码缩放为足够小的数字方便收敛 # 初始化一个连续的1×d_model/2的矩阵然后分别将位置编码矩阵的奇数位置取cos偶数位置取sin div_term torch.exp(torch.arange(0,d_model,2)*(-(torch.log(torch.Tensor([10000]))/d_model))) pe[:,0::2] torch.sin(position*div_term) pe[:,1::2] torch.cos(position*div_term) # 此时得到的pe位置编码矩阵是一个二维矩阵[max_length,d_model]embedding的输出为一个三维矩阵[batch_size,vec_len,d_model] # 因此我们需要在第0维度添加一个维度 pepe.unsqueeze(0) # 将位置编码矩阵注册到模型中不需要跟随模型的优化而变化 self.register_buffer(pe,pe) def forward(self,x): # 初始化的max_length是为了能够初始化足够大的位置在使用过程中需要和文本的输入进行适配所以需要根据输入进行切分 emb_pe self.pe[:,:x.shape[1],:] self.pe.requires_gradFalse return self.dropout(x emb_pe)3. Selft-Attention自注意力机制结构Self-Attention 的结构中我们需要Q(查询),K(键值),V(值)三个矩阵其中Q、K、V是通过对数据进行线性变换得到。公式从公式中我们可以看出Attention的计算过程就是Q和K的转置进行内积计算为了防止内积过大因此除以进行归一化再经过softmax计算注意力权重再与V相乘得到。流程input-1的查询向量为[1, 0, 2]分别乘上input-1、input-2、input-3的键向量获得三个score为244。然后对这三个score取softmax获得了input-1、input-2、input-3各自的重要程度。然后将这个重要程度乘上input-1、input-2、input-3的值向量求和。获得input-1的输出。代码实现import torch def attention(query,key,value,maskNone,dropoutNone): # 一般query的最后一个维度是词嵌入的维度 d_k query.shape[-1] # query和key的转置进行相乘在除以d_k进行归一化操作 scores torch.matmul(query,key.transpose(-2,-1))/torch.sqrt(torch.Tensor([d_k])) # 如果mask不为空mask等于0的位置对应的scores矩阵位置的值改为-1e9变成很小的值 if mask is not None: scores.masked_fill(mask0,-1e9) # 对scores矩阵的最后一个维度进行softmax操作,得到最终的注意力张量 p_att torch.softmax(scores,dim-1) # 如果dropout不为空则使用dropout操作 if dropout is not None: p_att dropout(p_att) # 将p_att与value相乘得到最终的query注意力表示同时返回注意力张量 return torch.matmul(p_att,value),p_att4. Encoder结构Encoder block 结构可以看到是由 Multi-Head Attention, Add Norm, Feed Forward模块组成的4.1 Add Norm结构中分为两部分残差和层归一化 X表示Multi-Head Attention或者Feed Forward的输入其中残差再resnet中提出可以解决深层网络梯度消失的问题Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的这样可以加快收敛。代码实现from torch import nn import torch class AddNorm(nn.Module): def __init__(self,d_model) - None: super().__init__() d_model词嵌入维度 dropout置零比例 max_length每个句子的最大长度 self.layer_norm nn.LayerNorm(d_model) def forward(self,x,model_out): return self.layer_norm(xmodel_out)4.2 MultiHeadAttention公式多头注意力机制核心就是将分割成N份然后各自去进行self-attention的计算为了更加直观就是假设我们输入的QKV的向量形状为[batch_sizeseq_len d_model]然后我们将d_model分成N份即每一份的形状为[batch_sizeseq_len Nd_model/N]然后将N和seq_len进行维度变换得到[batch_size Nseq_lend_model/N]然后计算self-attention计算完成后再将N和seq_len维度还原再将N和d_model/N拼接还原为d_model。优点能够让每个注意力机制去优化每个词汇的不同特征部分从而均衡同一种注意力机制可能出现的偏差让词义拥有来自更多元的表达。代码实现import torch from torch import nn import copy # 多头注意力机制 # 实现克隆函数因为在多头注意力机制下需要用到多个结构相同的线性层 # 需要使用clone函数将他们一同初始化到一个网络层列表中 def clones(module,N): # module: 代表要克隆的目标网络层 # N: 克隆几个 return nn.ModuleList([copy.deepcopy(module) for _ in range(N)]) # 实现多头注意力机制的类 class MultiHeadAttention(nn.Module): def __init__(self,head,embedding_dim,dropout0.1) - None: super().__init__() head: 代表几个头的参数 embedding: 代表词嵌入的维度 dropout: 置零的比率 # 断言多头的数量能够整除词嵌入的维度embedding_dim assert embedding_dim%head 0 # 获得每个头词向量的维度 self.d_k embedding_dim//head self.head head self.embedding_dim embedding_dim # 获得线性层要获取4个分别为Q、K、V以及最终的输出线性层 self.linears clones(nn.Linear(embedding_dim,embedding_dim),4) # 初始化注意力张量 self.att None # 初始化dropout self.dropout nn.Dropout(dropout) def forward(self,query,key,value,maskNone): # query,key,value是注意力机制的三个输入张量mask为掩码张量 if mask is not None: # 使用unsqueeze将掩码张量进行维度扩充代表多头中的第n个头 mask mask.unsqueeze(1) # 得到batch_size batch_size query.shape[0] # 首先使用zip将网络层和输入数据连接在一起模型的输出利用view和transpose进行维度和形状的变换输出 # 将句子长度的维度和头数维度进行交换目的更好的学习和表达句子长度和词嵌入维度之间的关系 query,key,value [ model(x).view(batch_size,-1,self.head,self.d_k).transpose(1,2) for model,x in zip(self.linears,(query,key,value)) ] # 将每个头的输出传入到注意力层 x,self.attn attention(query,key,value,maskmask,dropoutself.dropout) # 得到每个头的计算结果是4维张量需要进行形状的转换 # 前面已经将1,2两个维度进行转置需要重新转置回来 # 经历的transpose方法后必须使用contiguous不然无法使用view()方法 x x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k) # 最后将x输入线性层列表中的最后一个线性层中得到最终的多头注意力结构输出 return self.linears[-1](x)4.3 FeedForwardFeed Forward 层比较简单是一个两层的全连接层再加一层 Relu激活函数。公式代码实现from torch import nn import torch class FeedForward(nn.Module): def __init__(self,d_model,d_hidden,dropout0.1) - None: d_model: 词嵌入维度 d_hidden: 隐藏层维度 dropout: 置零 super().__init__() self.w1 nn.Linear(d_model,d_hidden) self.w2 nn.Linear(d_hidden,d_model) self.dropout nn.Dropout(dropout) def forward(self,x): # 首先x先送入第一个全连接经过relu激活、dropout # 最后送入第二个全连接 return self.w2(self.dropout(torch.relu(self.w1(x))))5. Decoder结构5.1 Masked MultiHeadAttentionDecoder中的第一个多头注意力层是有一个Masked操作主要是在预测下一个词的时候我们只能看到前面的信息无法看到后面的信息需要把后面的信息进行掩盖。预测过程如下首先根据输入 begin 预测出第一个单词为 我然后根据输入 begin 我 预测下一个单词 爱依次知道遇到end结束符预测结束。训练阶段为什么要使用masked答因为在我们训练过程中在计算loss时是用当前decoder输入所有单词对应位置的输出与真实的翻译结果ground truth去分别算cross entropy loss然后把t个loss加起来的如果我们不加mask那么就会出现在输出的时候使用了的信息其中包含了我们要预测的信息这个在我们推理过程中是不可能给到的所以我们需要把当前预测位置后面的未来信息进行masked。mask矩阵如何构建答mask矩阵就是一个下三角矩阵这样我们会发现对于每一个token都只能观察到当前token以及之前的信息未来的信息都会被遮盖mask在计算注意力分数的softmax之前进行使用将mask0的位置赋一个很小的值这样在softmax计算时未来的token注意力分数无限小趋近0。代码实现def subsquent_mask(size): 生成向后遮掩的掩码张量参数size是掩码张量最后两个维度的大小 最后两个维度形成一个方阵 # 定义掩码的张量 att_shape (1,size,size) # 生成上三角矩阵其中参数1表示上三角的对角线向右上移动一个单位0表示不动-1表示向左下移动一个单位 subsquent_mask torch.triu(torch.ones(att_shape,dtypetorch.uint8),1) # 使用1-上三角矩阵得到一个下三角矩阵 return 1-subsquent_mask5.2 MultiHeadAttentionDecoder block 第二个 Multi-Head Attention 变化不大 主要的区别在于其中 Self-Attention 的K, V矩阵不是使用上一个 Decoder block 的输出计算的而是使用Encoder 的输出计算得到K、V根据上一个 Decoder block 的输出计算Q(如果是第一个 Decoder block 则使用输入矩阵X进行计算)。这样做的好处是在 Decoder 的时候每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需Mask)。代码实现同4.2一致5.3 Softmax输出Softmax 根据输出矩阵的每一行预测下一个单词代码实现class Generator(nn.Module): def __init__(self,d_model,vocab_size) - None: d_model:词嵌入维度 vocab_size:词表大小 super().__init__() self.project nn.Linear(d_model,vocab_size) def forward(self,x): return torch.softmax(self.project(x),dim-1)
Transformer详解附代码实现
TransformerTransformer由论文《Attention is All You Need》提出接下来我会用通俗的方法解释整体结构流程并且各个模块的pytorch代码实现。整体结构Transformer 由Encoder和Decoder两个部分组成Encoder 和 Decoder 都包含 6 个 block。1. Embedding无论源文本嵌入还是目标文本嵌入都是为了将文本中词汇的数字表示转变为向量的表示希望在这样的高维空间捕捉词汇间的关系。其中Embedding方式有很多我们可以使用Word2Vec等方式得到也可以使用一个可学习的Embedding进行表示。代码实现class Embedding(nn.Module): def __init__(self,d_model,vocab) - None: d_model表示词嵌入的维度 vocab表示词表的大小 super().__init__() self.d_model d_model self.vocab vocab self.embed nn.Embedding(vocab,d_model) def forward(self,x): # x代表输入进模型的文本通过词汇映射后的数字张量 return self.embed(x)*math.sqrt(self.d_model)其中我们会发现embeding之后乘以了为什么这个其实主要是为了平衡后面位置编码向量因为我们初始化embedding的时候服从正态分布方差会随着变大导致方差变小取值返回就会比较小而位置编码使用的是正余弦函数范围为[-1,1]相加之后会导致词向量被淹没语义丢失。2. PositionalEncoding在Transformer的编码器结构中并没有针对词汇位置信息的处理因此需要在Embedding层后加入位置编码器将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中以弥补位置信息的缺失公式其中pose表示单词在句子中的绝对位置表示向量嵌入维度与词向量维度一致表示偶数维度 表示奇数维度优点外推性当我们训练集里面的最大长度为max_len当来了一个更长的句子我们可以根据公式直接计算出超出max_len的位置编码模型可以计算出两个单词位置的相对距离即posk可以用pos的位置计算得到根据正余弦函数的准则、代码实现from torch import nn import torch class PositionalEncoding(nn.Module): def __init__(self,d_model,dropout,max_length) - None: super().__init__() d_model词嵌入维度 dropout置零比例 max_length每个句子的最大长度 self.dropout nn.Dropout(dropout) # 初始化位置编码矩阵pe形状为[max_length, d_model] pe torch.zeros(max_length,d_model) # 初始化一个绝对位置矩阵词汇的绝对位置就是通过它的索引去表示形状为[max_length, 1] position torch.arange(0,max_length).unsqueeze(1) # 初始化完成绝对位置矩阵接下来需要考虑如何将绝对位置加入到位置编码矩阵 # 其中最简单的方式就是我们需要将绝对位置矩阵从[max_length, 1] 变换成[max_length, d_model]形状然后覆盖位置编码矩阵 # 做这种变换需要一个1×d_model的矩阵且这个矩阵能够将位置编码缩放为足够小的数字方便收敛 # 初始化一个连续的1×d_model/2的矩阵然后分别将位置编码矩阵的奇数位置取cos偶数位置取sin div_term torch.exp(torch.arange(0,d_model,2)*(-(torch.log(torch.Tensor([10000]))/d_model))) pe[:,0::2] torch.sin(position*div_term) pe[:,1::2] torch.cos(position*div_term) # 此时得到的pe位置编码矩阵是一个二维矩阵[max_length,d_model]embedding的输出为一个三维矩阵[batch_size,vec_len,d_model] # 因此我们需要在第0维度添加一个维度 pepe.unsqueeze(0) # 将位置编码矩阵注册到模型中不需要跟随模型的优化而变化 self.register_buffer(pe,pe) def forward(self,x): # 初始化的max_length是为了能够初始化足够大的位置在使用过程中需要和文本的输入进行适配所以需要根据输入进行切分 emb_pe self.pe[:,:x.shape[1],:] self.pe.requires_gradFalse return self.dropout(x emb_pe)3. Selft-Attention自注意力机制结构Self-Attention 的结构中我们需要Q(查询),K(键值),V(值)三个矩阵其中Q、K、V是通过对数据进行线性变换得到。公式从公式中我们可以看出Attention的计算过程就是Q和K的转置进行内积计算为了防止内积过大因此除以进行归一化再经过softmax计算注意力权重再与V相乘得到。流程input-1的查询向量为[1, 0, 2]分别乘上input-1、input-2、input-3的键向量获得三个score为244。然后对这三个score取softmax获得了input-1、input-2、input-3各自的重要程度。然后将这个重要程度乘上input-1、input-2、input-3的值向量求和。获得input-1的输出。代码实现import torch def attention(query,key,value,maskNone,dropoutNone): # 一般query的最后一个维度是词嵌入的维度 d_k query.shape[-1] # query和key的转置进行相乘在除以d_k进行归一化操作 scores torch.matmul(query,key.transpose(-2,-1))/torch.sqrt(torch.Tensor([d_k])) # 如果mask不为空mask等于0的位置对应的scores矩阵位置的值改为-1e9变成很小的值 if mask is not None: scores.masked_fill(mask0,-1e9) # 对scores矩阵的最后一个维度进行softmax操作,得到最终的注意力张量 p_att torch.softmax(scores,dim-1) # 如果dropout不为空则使用dropout操作 if dropout is not None: p_att dropout(p_att) # 将p_att与value相乘得到最终的query注意力表示同时返回注意力张量 return torch.matmul(p_att,value),p_att4. Encoder结构Encoder block 结构可以看到是由 Multi-Head Attention, Add Norm, Feed Forward模块组成的4.1 Add Norm结构中分为两部分残差和层归一化 X表示Multi-Head Attention或者Feed Forward的输入其中残差再resnet中提出可以解决深层网络梯度消失的问题Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的这样可以加快收敛。代码实现from torch import nn import torch class AddNorm(nn.Module): def __init__(self,d_model) - None: super().__init__() d_model词嵌入维度 dropout置零比例 max_length每个句子的最大长度 self.layer_norm nn.LayerNorm(d_model) def forward(self,x,model_out): return self.layer_norm(xmodel_out)4.2 MultiHeadAttention公式多头注意力机制核心就是将分割成N份然后各自去进行self-attention的计算为了更加直观就是假设我们输入的QKV的向量形状为[batch_sizeseq_len d_model]然后我们将d_model分成N份即每一份的形状为[batch_sizeseq_len Nd_model/N]然后将N和seq_len进行维度变换得到[batch_size Nseq_lend_model/N]然后计算self-attention计算完成后再将N和seq_len维度还原再将N和d_model/N拼接还原为d_model。优点能够让每个注意力机制去优化每个词汇的不同特征部分从而均衡同一种注意力机制可能出现的偏差让词义拥有来自更多元的表达。代码实现import torch from torch import nn import copy # 多头注意力机制 # 实现克隆函数因为在多头注意力机制下需要用到多个结构相同的线性层 # 需要使用clone函数将他们一同初始化到一个网络层列表中 def clones(module,N): # module: 代表要克隆的目标网络层 # N: 克隆几个 return nn.ModuleList([copy.deepcopy(module) for _ in range(N)]) # 实现多头注意力机制的类 class MultiHeadAttention(nn.Module): def __init__(self,head,embedding_dim,dropout0.1) - None: super().__init__() head: 代表几个头的参数 embedding: 代表词嵌入的维度 dropout: 置零的比率 # 断言多头的数量能够整除词嵌入的维度embedding_dim assert embedding_dim%head 0 # 获得每个头词向量的维度 self.d_k embedding_dim//head self.head head self.embedding_dim embedding_dim # 获得线性层要获取4个分别为Q、K、V以及最终的输出线性层 self.linears clones(nn.Linear(embedding_dim,embedding_dim),4) # 初始化注意力张量 self.att None # 初始化dropout self.dropout nn.Dropout(dropout) def forward(self,query,key,value,maskNone): # query,key,value是注意力机制的三个输入张量mask为掩码张量 if mask is not None: # 使用unsqueeze将掩码张量进行维度扩充代表多头中的第n个头 mask mask.unsqueeze(1) # 得到batch_size batch_size query.shape[0] # 首先使用zip将网络层和输入数据连接在一起模型的输出利用view和transpose进行维度和形状的变换输出 # 将句子长度的维度和头数维度进行交换目的更好的学习和表达句子长度和词嵌入维度之间的关系 query,key,value [ model(x).view(batch_size,-1,self.head,self.d_k).transpose(1,2) for model,x in zip(self.linears,(query,key,value)) ] # 将每个头的输出传入到注意力层 x,self.attn attention(query,key,value,maskmask,dropoutself.dropout) # 得到每个头的计算结果是4维张量需要进行形状的转换 # 前面已经将1,2两个维度进行转置需要重新转置回来 # 经历的transpose方法后必须使用contiguous不然无法使用view()方法 x x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k) # 最后将x输入线性层列表中的最后一个线性层中得到最终的多头注意力结构输出 return self.linears[-1](x)4.3 FeedForwardFeed Forward 层比较简单是一个两层的全连接层再加一层 Relu激活函数。公式代码实现from torch import nn import torch class FeedForward(nn.Module): def __init__(self,d_model,d_hidden,dropout0.1) - None: d_model: 词嵌入维度 d_hidden: 隐藏层维度 dropout: 置零 super().__init__() self.w1 nn.Linear(d_model,d_hidden) self.w2 nn.Linear(d_hidden,d_model) self.dropout nn.Dropout(dropout) def forward(self,x): # 首先x先送入第一个全连接经过relu激活、dropout # 最后送入第二个全连接 return self.w2(self.dropout(torch.relu(self.w1(x))))5. Decoder结构5.1 Masked MultiHeadAttentionDecoder中的第一个多头注意力层是有一个Masked操作主要是在预测下一个词的时候我们只能看到前面的信息无法看到后面的信息需要把后面的信息进行掩盖。预测过程如下首先根据输入 begin 预测出第一个单词为 我然后根据输入 begin 我 预测下一个单词 爱依次知道遇到end结束符预测结束。训练阶段为什么要使用masked答因为在我们训练过程中在计算loss时是用当前decoder输入所有单词对应位置的输出与真实的翻译结果ground truth去分别算cross entropy loss然后把t个loss加起来的如果我们不加mask那么就会出现在输出的时候使用了的信息其中包含了我们要预测的信息这个在我们推理过程中是不可能给到的所以我们需要把当前预测位置后面的未来信息进行masked。mask矩阵如何构建答mask矩阵就是一个下三角矩阵这样我们会发现对于每一个token都只能观察到当前token以及之前的信息未来的信息都会被遮盖mask在计算注意力分数的softmax之前进行使用将mask0的位置赋一个很小的值这样在softmax计算时未来的token注意力分数无限小趋近0。代码实现def subsquent_mask(size): 生成向后遮掩的掩码张量参数size是掩码张量最后两个维度的大小 最后两个维度形成一个方阵 # 定义掩码的张量 att_shape (1,size,size) # 生成上三角矩阵其中参数1表示上三角的对角线向右上移动一个单位0表示不动-1表示向左下移动一个单位 subsquent_mask torch.triu(torch.ones(att_shape,dtypetorch.uint8),1) # 使用1-上三角矩阵得到一个下三角矩阵 return 1-subsquent_mask5.2 MultiHeadAttentionDecoder block 第二个 Multi-Head Attention 变化不大 主要的区别在于其中 Self-Attention 的K, V矩阵不是使用上一个 Decoder block 的输出计算的而是使用Encoder 的输出计算得到K、V根据上一个 Decoder block 的输出计算Q(如果是第一个 Decoder block 则使用输入矩阵X进行计算)。这样做的好处是在 Decoder 的时候每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需Mask)。代码实现同4.2一致5.3 Softmax输出Softmax 根据输出矩阵的每一行预测下一个单词代码实现class Generator(nn.Module): def __init__(self,d_model,vocab_size) - None: d_model:词嵌入维度 vocab_size:词表大小 super().__init__() self.project nn.Linear(d_model,vocab_size) def forward(self,x): return torch.softmax(self.project(x),dim-1)