【NLP】Transformer模型架构

该文章已生成可运行项目,

目录

一、Transformer整体结构

二、Transformer的输入

2.1 单词Embedding

2.2 位置Embedding

2.3 Decoder 的输入

三、Multi-Head Attention

3.1 Self-Attention

3.2 Multi-Head Attention

四、Encoder编码器结构

4.1 Add & Norm

4.2 Feed Forward

4.3 Encoder Block 

五、Decoder解码器结构

5.1 第一个Multi-Head Attention

5.2 第二个Multi-Head Attention 

六、Softmax预测输出

七、Transformer的特点

7.1 并行计算能力

7.1.1 Encoder的并行化

7.1.2 Decoder的并行化

7.2 特征抽取能力

7.3 取代seq2seq的原因

八、Transformer模型代码


Transformer是一种用于处理序列数据的深度学习模型架构,谷歌在2017年的论文《Attention Is All You Need》中提出的,用于NLP的各项任务,现在是谷歌云TPU推荐的参考模型

相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:

  1. Transformer能够利用分布式GPU进行并行训练,提升模型训练效率
  2. 在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好

一、Transformer整体结构

在机器翻译中,可以使用Transformer将一种语言翻译成另外一种语言。若将Transformer看成一个黑盒,那其结构如下:

若将Transformer拆开可以看到其由若干 编码器(Encoder) 和 解码器(Decoder) 组成

若要将法语"Je suis etudiant"翻译成英文需要经过以下步骤:

第一步:获取输入句子的每一个单词的表示向量,由单词的Embedding和单词位置的Embedding 相加得到

第二步:将单词向量矩阵X传入Encoder,经过N个Encoder后得到句子所有单词的编码信息矩阵C。输入单词向量矩阵用X \in R^{n*d},n为单词个数(矩阵行数),d为向量维度(矩阵列数,SequenceLength,论文中为512)。每一个Encoder输出的矩阵维度与输入完全一致

第三步:将Encoder输出的编码信息矩阵C传入Decoder中,Decoder会根据当前翻译过的单词 1 ~ i 翻译下一个单词 i + 1

上图,Decoder接收了Encoder的编码矩阵,然后首先输入一个开始符 "<Begin>",预测第一个单词,输出为"I";然后输入翻译开始符 "<Begin>" 和单词 "I",预测第二个单词,输出为"am",以此类推

二、Transformer的输入

Transformer中单词的输入表示由 单词Embedding 和 位置Embedding(Positional Encoding)相加得到

2.1 单词Embedding

单词的Embedding可以通过Word2vec等模型预训练得到,可以在Transformer中加入Embedding层

class Embeddings(nn.Module):
    def __init__(self, dimension, vocab_size):
        super(Embeddings, self).__init__()
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=dimension)
        self.dimension = dimension
    def forward(self, data):
        # 扩大embedding vector,让原来的 embedding vector 中的信息在和 position encoding 的信息相加时不至于丢失掉
        return self.embedding(data) * math.sqrt(self.dimension)

2.2 位置Embedding

Transformer 中除了单词的Embedding,还需要使用位置Embedding 表示单词出现在句子中的位置。因为 Transformer不采用RNN结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于NLP来说非常重要。所以Transformer中使用位置Embedding保存单词在序列中的相对或绝对位置

位置Embedding用PE表示,PE的维度与单词Embedding的维度相同。PE可以通过训练获得,也可以通过公式计算,在Transformer中采用了后者:

PE_{pos,2i} = sin(pos/10000^{2i/d_{model}})   PE_{pos,2i+1} = cos(pos/10000^{2i/d_{model}})

  • PE_{pos,2i} 表示位置 pos 的偶数维度的编码值
  • PE_{pos,2i+1}​ 表示位置 pos 的奇数维度的编码值
  • pos 是单词在句子中的位置(通常是从 0 开始的索引)
  • i 是维度索引
  • d_{model}​ 是模型的维度大小

这些函数具有周期性,可以为模型提供不同位置之间的相对关系。由于正弦和余弦函数在不同维度上以不同的频率变化,能确保不同位置的编码在每一维上有所区分。具体来说,较低维度的频率较低,较高维度的频率较高,这使得每个位置的编码都在各个维度上有不同的变化,从而帮助模型捕捉位置信息

class PositionalEncodeing(nn.Module):
    def __init__(self, dimension, dropout, max_length):
        super(PositionalEncodeing, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        pe = torch.zeros(max_length, dimension)
        # Position indices (0, 1, 2, ..., max_length-1)
        position = torch.arange(0, max_length).unsqueeze(1)
        # e^{-(log_{e}10000) * (2i/dim)}
        # 提出负号 1 / (e^{ln(10000) * (2i / dim)})
        # 因为 a^{m * n} == (a^{m})^{n}, 所以 e^{ln(10000) * (2i / dim)} 可化为 (e^{ln(10000)}) ^ {2i / dim}
        # 因为 e^{ln(x)} == x, 所以 (e^{ln(10000)}) ^ {2i / dim} 可化简为 10000^{2i/dim}
        # 所以 e^{-(log_{e}10000) * (2i/dim)} == 1 / (10000^{2i/dim})
        div_term = torch.exp(-(math.log(10000.0) * torch.arange(0, dimension, 2) / dimension))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        # 将pe位置编码矩阵注册成模型的buffer,注册后就可以在模型保存后重加载时和模型结构与参数一同被加载
        self.register_buffer('pe', pe) # (1, max_length, dimension)
    def forward(self, data):
        data = data + self.pe[:, :data.size(1)]
        return self.dropout(data)

2.3 Decoder 的输入

训练时输入

  • 从第二层 Block 到第六层 Block 的输入模式一致,都是固定操作的循环处理
  • 聚焦在第一层的 Block 上:训练阶段每一个 time step 的输入是上一个 time step 的输入加上真实标签序列向后移一位。假设现在的真实标签序列是 "How are you?",当 time step=1 时, 输入张量为一个特殊的 token,如 "SOS";当 time step=2 时,输入张量为 "SOS How"; 当 time step=3 时,输入张量为 "SOS How are",以此类推...
  • 注意:在真实的代码实现中,训练阶段不会这样动态输入,而是一次性的把目标序列全部输入给第一层的Block,然后通过多头 self-attention 中的 MASK 机制对序列进行同样的遮掩即可

推理时输入

  • 推理时从第二层 Block 到第六层 Block 的输入模式一致,无需特殊处理,都是固定操作的循环处理
  • 聚焦在第一层的 Block 上:因为每一步的输入都会有Encoder的输出张量,因此不做特殊讨论,只专注于纯粹从Decoder端接收的输入。推理阶段每一个 time step 的输入是从 time step=0,input_tensor="SOS" 开始,一直到上一个 time step 的预测输出的累计拼接张量
    • 当 time step=1 时,输入的 input_tensor="SOS",推理出来的输出值是 output_tensor="What"
    • 当 time step=2 时,输入的 input_tensor="SOS What",预测出来的输出值是 output_tensor="is"
    • 当 time step=3 时,输入的 input_tensor="SOS What is",预测出来的输出值是 output_tensor="the"
    • 当 time step=4 时,输入的 input_tensor="SOS What is the",预测出来的输出值是 output_tensor="matter"
    • 当 time step=5 时,输入的 input_tensor="SOS What is the matter",预测出来的输出值是 output_tensor="?"
    • 当 time step=6 时,输入的 input_tensor="SOS What is the matter ?",预测出来的输出值是 output_tensor="EOS",代表句子的结束符,说明解码结束,预测结束

三、Multi-Head Attention

上图是Transformer的内部结构,其中红色方框内为Multi-Head Attention,由多个Self-Attention组成,具体结构如下图:

3.1 Self-Attention

上图是Self-Attention结构,最下面是 Q(查询)、K (键值)、V(值)矩阵,通过输入矩阵X和权重矩阵W^{Q}W^{K}W^{V}相乘得到

得到Q、K、V后就可以计算出Self-Attention的输出

W^{Q}W^{K}W^{V}是可训练的权重矩阵,形状分别为(d,d_{Q})(d,d_{K})(d,d_{V}),其中d_{Q}d_{K}d_{V}分别是查询、键和值的维度

def self_attention(query, key, value, mask=None, dropout=None):
    dim = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(dim)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    scores_softmax = torch.softmax(scores, dim=-1)
    if dropout is not None:
        attention_weight = dropout(scores_softmax)
    return torch.matmul(attention_weight, value), attention_weight

理解机制

self-attention 是一种通过自身和自身进行关联的 attention 机制,从而得到更好的 representation 来表达自身。self-attention 是 attention 机制的一种特殊情况,在 self-attention 中,Q=K=V,序列中的每个单词(token)都和该序列中的其他所有单词(token)进行 attention 规则的计算

attention 机制计算的特点在于,可以直接跨越一句话中不同距离的 token,可以远距离的学习到序列的知识依赖和语序结构

应用传统的RNN、LSTM,在获取长距离语义特征和结构特征时,需按照序列顺序依次计算,距离越远的联系信息的损耗越大,有效提取和捕获的可能性越小。但应用 self-attention 时,计算过程中会直接将句子中任意两个 token 的联系通过一个计算步骤直接联系起来

self-attention为什么要使用(Q, K, V)三元组而不是其他形式

  • 从分析的角度看,查询Query是一条独立的序列信息,通过关键词Key的提示作用,得到最终语义的真实值Value表达,数学意义更充分、完备
  • 不使用(K, V)或者(V)没有什么必须的理由,也没有相关的论文来严格阐述比较试验的结果差异,所以可以作为开放性问题未来去探索,明确在经典self-attention实现中用的是三元组即可

Self-attention中的归一化

  • 训练上的意义:随着词嵌入维度 d_k 的增大,q * k 点积后的结果也会增大,在训练时会将 softmax 函数推入梯度非常小的区域,可能出现梯度消失的现象,造成模型收敛困难.

  • 数学上的意义:假设 q 和 k 的统计变量是满足标准正态分布的独立随机变量,意味着 q 和 k 满足均值为 0、方差为1。那么 q 和 k 的点积结果就是均值为0、方差为 d_k,为了抵消这种方差被放大 d_k 倍的影响,在计算中主动将点积缩放 1/sqrt(d_k),这样点积后的结果依然满足均值为0、方差为1

掩码机制

Transformer 中一共存在两种掩码机制,填充掩码 和 未来掩码

Encoder中仅存在填充掩码,Decoder中同时存在 填充掩码 和 未来掩码

  • 未来掩码(Look-Ahead Mask)

未来掩码用于防止模型在生成当前 token 时看到未来的 token。这种掩码确保了生成过程的顺序性,避免信息泄露

  • 填充掩码(Padding Mask)

在 Transformer 中,输入数据通常需要填充以使 batch 中所有序列长度一致。填充的值通常是 0,这种填充方式被称为 padding

注意力机制中,填充部分会被掩码,使其在计算注意力权重时被忽略(进行 softmax 之前,掩码矩阵应用于注意力分数)。填充部分的注意力权重会被设置为负无穷(-1e9 或其他极小值)。经过 softmax 后,其权重会趋近于 0

3.2 Multi-Head Attention

Multi-Head Attention包含多个Self-Attention层,首先将输入X分别传递到h个不同的Self-Attention中,计算得到h个输出矩阵Z。下图是 h=8 的情况,此时会得到 8 个输出矩阵Z

得到8个输出矩阵Z_{0} \sim Z_{8}后,Multi-Head Attention将其拼接在一起(Concat),然后传入一个Linear层,得到Multi-Head Attention最终的输出矩阵Z

class MultiHeadedAttention(nn.Module):
    def __init__(self, head_number, embedding_dim, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        assert embedding_dim % head_number == 0
        # 每个头分配的分割词向量维度
        self.d_k = embedding_dim // head_number
        self.head_number = head_number
        # 计算得到的注意力张量
        self.attention_weights = None

        self.query_linear = nn.Linear(embedding_dim, embedding_dim)
        self.key_linear = nn.Linear(embedding_dim, embedding_dim)
        self.value_linear = nn.Linear(embedding_dim, embedding_dim)
        self.last_linear = nn.Linear(embedding_dim, embedding_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        if mask is not None:
            mask = mask.unsqueeze(0)
        query = self.query_linear(query).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        key = self.key_linear(key).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        value = self.value_linear(value).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        new_value, self.attention_weights = self_attention(query, key, value, mask, self.dropout)
        new_value = new_value.transpose(1, 2).contiguous().view(batch_size, -1, self.head_number * self.d_k)
        new_value = self.last_linear(new_value)
        return new_value

使用 Multi-Head Attention 的原因

  • 将模型分为多个头,可以形成多个子空间,让模型去关注不同方面的信息,最后再将各个方面的信息综合起来得到更好的效果
  • 多个头进行 attention 计算最后再综合起来,类似于CNN中采用多个卷积核的作用,不同的卷积核提取不同的特征,关注不同的部分,最后再进行融合

多头注意力有助于神经网络捕捉到更丰富的特征信息

计算方式

Multi-head Attention 和单一 head 的 Attention 唯一的区别就在于,其对特征张量的最后一个维度进行了分割,一般是对词嵌入的 embedding_dim=512 进行切割成 head=8,这样每一个 head 的嵌入维度就是 512/8=64,后续的 Attention 计算公式完全一致,只不过是在 64 这个维度上进行一系列的矩阵运算而已

在 head=8 个头上分别进行注意力规则的运算后,简单采用拼接 concat 的方式对结果张量进行融合就得到了 Multi-head Attention 的计算结果

粗暴的理解,其实就是将一条数据长度为 embedding_dim 的数据分为了 head_number 条,每条长度为dk,进行了 head_number 次 self_attention,最后进行拼接

Q、K、V 为什么使用不同的权重生成?

主要是为了增加模型的表示能力,允许模型从不同的角度捕捉输入序列中的语义信息,模型可以在不同的表示空间中学习元素之间的关系。这种设计允许模型学习更复杂的模式

四、Encoder编码器结构

红色部分是Transformer的Encoder结构, N表示 Encoder Block 的个数,由Multi-Head Attention、Add & Norm、Feed Forward、Add & Norm组成

经典的 Transformer 结构中的 Encoder 模块包含 6 个 Encoder Block

4.1 Add & Norm

Add & Norm 是指残差连接后使用 LayerNorm,即 LayerNorm(X + Sublayer(X))

class LayerNormalization(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super(LayerNormalization, self).__init__()
        # 使用nn.parameter封装,代表其是模型的参数
        self.lambda_weight = nn.Parameter(torch.ones(dim))
        self.beta_bias = nn.Parameter(torch.zeros(dim))
        self.eps = eps

    def forward(self, data):
        mean = data.mean(-1, keepdim=True)
        std = data.std(-1, keepdim=True)
        return self.lambda_weight * (data - mean) / (std + self.eps) + self.beta_bias

Sublayer表示经过的变换,比如第一个Add & Norm中Sublayer表示Multi-Head Attention

Add残差连接的作用

为了将信息传递的更深,增强模型的拟合能力。同时,使用残差连接使得梯度可以直接传递,从而缓解了梯度消失和梯度爆炸的问题

LayerNorm的作用

随着网络层数的额增加,通过多层的计算后参数可能会出现过大、过小, 方差变大等现象,这会导致学习过程出现异常,模型的收敛慢。因此对每一层计算后的数值进行规范化可以提升模型的表现

为什么用LayerNorm而不用BatchNorm?

主要是因为 LayerNorm 更适合处理序列数据,在 NLP 中输入序列经常会出现序列长度不一的情况。BatchNorm 依赖一个批次中的所有数据来计算均值和方差,对于长度不一的序列数据处理起来比较困难。LayerNorm 则对每个样本在特征维度上进行归一化,因此可以自然的处理不同长度的序列。推理时 BatchNorm 在计算均值和方差时依赖整个批次的数据,使得性能受到批次大小影响。若出现推理的样本长度比批量中文本长很多的情况,推理的性能会下降很多

4.2 Feed Forward

Feed Forward是指前馈全连接层:

FFN(X) = max(0, XW_{1} + b_{1})W_{2} + b_{2}

class PositionwiseFeedForward(nn.Module):
    def __init__(self, in_dim, tmp_dim, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(in_dim, tmp_dim)
        self.linear2 = nn.Linear(tmp_dim, in_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, data):
        output = torch.relu(self.linear1(data))
        output = self.dropout(output)
        output = self.linear2(output)
        return output

4.3 Encoder Block 

因此输入矩阵X经过一个Encoder后,输出如下:

O = LayerNorm(X + MultiHeadAttention(X))

O = LayerNorm(O + FNN(O))

输入矩阵X\in R^{n*d}通过单个Encoder得到输出矩阵O \in R^{n*d},通过多个Encoder叠加,最后便是编码器Encoder的输出

class EncoderLayer(nn.Module):
    def __init__(self, dim, dropout):
        super(EncoderLayer, self).__init__()
        self.multi_head_self_attention = MultiHeadedAttention(16, dim, dropout)
        self.feed_forward = PositionwiseFeedForward(dim, 64, dropout)
        self.layer_norm1 = LayerNormalization(dim)
        self.layer_norm2 = LayerNormalization(dim)
        self.dropout = nn.Dropout(p=dropout)
        self.dim = dim

    def forward(self, data, mask):
        attention_output = self.multi_head_self_attention(data, data, data, mask)
        attention_output = self.dropout(attention_output)
        output1 = self.layer_norm1(data + attention_output)

        ffn_output = self.feed_forward(output1)
        ffn_output = self.dropout(ffn_output)
        output2 = self.layer_norm2(output1 + ffn_output)
        return output2


class Encoder(nn.Module):
    def __init__(self, number):
        super(Encoder, self).__init__()
        self.encoder_layer = EncoderLayer(512, 0.1)
        self.layers = nn.ModuleList([copy.deepcopy(self.encoder_layer) for _ in range(number)])

    def forward(self, data, mask):
        for layer in self.layers:
            data = layer(data, mask)
        return data

五、Decoder解码器结构

经典的 Transformer 结构中的 Decoder 模块包含 6 个 Decoder Block

上图红色部分为Transformer的Decoder结构,与Encoder相似,但是存在一些区别:

  • 包含两个 Multi-Head Attention
  • 第一个 Multi-Head Attention 采用了 Masked 操作
  • 第二个 Multi-Head Attention 的 K,V 矩阵使用 Encoder 的编码信息矩阵C进行计算,而 Q 使用上一个 Decoder 的输出计算
  • 最后连接一个 Softmax 层计算下一个翻译单词的概率

5.1 第一个Multi-Head Attention

Decoder的第一个 Multi-Head Attention 采用了 Masked 操作,因为在翻译的过程中是顺序翻译的,即翻译完第 i 个单词,才可以翻译第 i + 1 个单词。通过 Masked 操作可以防止第 i 个单词知道 i + 1 个单词之后的信息。下面以法语 "Je suis etudiant" 翻译成英文 "I am a student" 为例,了解一下 Masked 操作

在Decoder的时候,需要根据之前翻译的单词,预测当前最有可能翻译的单词。如下图所示,先根据输入"<Begin>"预测出第一个单词为"I",然后根据输入"<Begin> I" 预测下一个单词 "am"

Decoder在预测第 i 个输出时,需要将第 i 之后的单词掩盖住,Mask操作是在Self-Attention的Softmax之前使用的

第一步:是 Decoder 的输入矩阵和 Mask 矩阵,输入矩阵包含 "<Begin> I am a student <EOS>" 6 个单词的表示向量,而Mask是一个 6 * 6 的矩阵。在 Mask 中可以发现单词 "I" 只能使用单词 "<Begin>" 的信息,而单词 "am" 可以使用单词 "<Begin> I" 的信息,即只能使用之前的信息

第二步:下面的操作和之前Encoder中的Self-Attention一样,只是在Softmax前需要进行Mask操作

第三步:通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵 Z_{i},然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出 Z_{i},然后计算得到第一个 Multi-Head Attention 的输出 ZZ与输入X维度一样

5.2 第二个Multi-Head Attention 

Decoder 的第二个 Multi-Head Attention 变化不大, 主要的区别在于其中 Attention 的 K,V 矩阵不是使用上一个 Multi-Head Attention 的输出,而是使用Encoder的编码信息矩阵计算的。根据 Encoder 的输出 C 计算得到 K,V,根据上一个 Multi-Head Attention 的输出 Z 计算 Q。这样做的好处是在 Decoder 时,每一位单词(即"I am a student")都可以利用到Encoder所有单词的信息(即"Je suis etudiant")

class DecoderLayer(nn.Module):
    def __init__(self, dim, dropout):
        super(DecoderLayer, self).__init__()
        self.dim = dim
        self.multi_head_self_attention = MultiHeadedAttention(16, 512)
        self.multi_head_attention = MultiHeadedAttention(16, 512)
        self.feed_forward = PositionwiseFeedForward(dim, 64, dropout)
        self.layer_norm1 = LayerNormalization(dim)
        self.layer_norm2 = LayerNormalization(dim)
        self.layer_norm3 = LayerNormalization(dim)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, data, memory, mask):
        attention_output1 = self.multi_head_self_attention(data, data, data, mask)
        attention_output1 = self.dropout(attention_output1)
        output1 = self.layer_norm1(data + attention_output1)

        attention_output2 = self.multi_head_attention(data, memory, memory, None)
        attention_output2 = self.dropout(attention_output2)
        output2 = self.layer_norm1(output1 + attention_output2)

        ffn_output = self.feed_forward(output2)
        ffn_output = self.dropout(ffn_output)
        output3 = self.layer_norm1(output2 + ffn_output)
        return output3


class Decoder(nn.Module):
    def __init__(self, number):
        super(Decoder, self).__init__()
        self.decoder_layer = DecoderLayer(512, 0.1)
        self.layers = nn.ModuleList([copy.deepcopy(self.decoder_layer) for _ in range(number)])

    def forward(self, data, memory, mask):
        for layer in self.layers:
            data = layer(data, memory, mask)
        return data

六、Softmax预测输出

编码器Decoder最后的部分是利用 Softmax 预测下一个单词,在Softmax之前,会经过Linear变换,将维度转换为词表的个数

假设词表只有6个单词,表示如下:

输出表示如下: 

class Generator(nn.Module):
    def __init__(self, dim, vocab_size):
        super(Generator, self).__init__()
        self.linear = nn.Linear(dim, vocab_size)

    def forward(self, data):
        output = self.linear(data)
        return torch.softmax(output, dim=-1)

七、Transformer的特点

7.1 并行计算能力

Transformer 比传统序列模型 RNN/LSTM 具备优势的第一大原因就是强大的并行计算能力

  • 对于RNN,任意时刻 t 的输入是时刻 t 的输入 x(t) 和上一时刻的隐藏层输出 h(t-1),经过运算后得到当前时刻隐藏层的输出h(t),这个 h(t) 也将作为下一时刻 t+1 的输入的一部分。RNN的历史信息是需要通过时间步一步一步向后传递的,这就意味着RNN序列后面的信息只能等到前面的计算结束后,将历史信息通过 hidden state 传递给后面才能开始计算,形成链式的序列依赖关系,无法实现并行
  • 对于 Transformer 结构,在 self-attention 层无论序列的长度是多少,都可以一次性计算所有单词之间的注意力关系,这个attention的计算是同步的,可以实现并行

7.1.1 Encoder的并行化

  • 整个序列所有的 token 可以并行的进行 Embedding 操作,这一层的处理是没有依赖关系的

  • Transformer 中的 self-attention 部分,这里对于任意一个单词,如x1,要计算x1对于其他所有 token 的注意力分布,得到z1。这个过程是具有依赖性的,必须等到序列中所有的单词完成 Embedding 才能进行,因此这一步是不能并行处理的。但从另一个角度看,真实计算注意力分布的时候,采用的都是矩阵运算,也就是可以一次性的计算出所有 token 的注意力张量,从这个角度看也算是实现了并行,只是矩阵运算的 "并行" 和词嵌入的 "并行" 概念上不同

  • 前馈全连接层, 对于不同的向量 z 之间也是没有依赖关系的,所以这一层是可以实现并行化处理的。即所有的向量 z 输入 Feed Forward 网络的计算可以同步进行,互不干扰.

7.1.2 Decoder的并行化

  • Decoder 模块在训练阶段采用了并行化处理。其中 Self-Attention 和 Encoder-Decoder Attention 两个子层的并行化也是在进行矩阵乘法,和Encoder的理解是一致的。在进行 Embedding 和 Feed Forward 的处理时,因为各个token之间没有依赖关系,所以也是可以完全并行化处理的

  • Decoder 模块在推理阶段基本上不认为采用了并行化处理。因为第一个 time step 的输入只是一个 "SOS",后续每一个 time step 的输入也只是依次添加之前所有的预测token

最重要的区别是训练阶段目标文本如果有20个token,在训练过程中是一次性的输入给 Decoder,可以做到一些子层的并行化处理。但是在预测阶段,若预测的结果语句总共有20个token,则需要重复处理20次循环的过程,每次的输入添加进去一个token,每次的输入序列比上一次多一个token,所以不认为是并行处理

7.2 特征抽取能力

Transformer 比传统序列模型 RNN/LSTM 具备优势的第二大原因就是强大的特征抽取能力

  • Transformer 因为采用了 Multi-head Attention 结构和计算机制,拥有比 RNN/LSTM 更强大的特征抽取能力。并不仅仅由理论分析得来,而是大量的试验数据和对比结果,清楚的展示了 Transformer 的特征抽取能力远远胜于 RNN/LSTM

注意:不是越先进的模型就越无敌, 在很多具体的应用中RNN/LSTM依然大有用武之地, 要具体问题具体分析

7.3 取代seq2seq的原因

seq2seq的两大缺陷

  • seq2seq 架构的第一大缺陷是将 Encoder 端的所有信息压缩成一个固定长度的语义向量中,用这个固定的向量来代表编码器端的全部信息。既会造成信息的损耗,也无法让Decoder端在解码的时候去用注意力聚焦哪些是更重要的信息
  • seq2seq架构的第二大缺陷是无法并行, 本质上和 RNN/LSTM 无法并行的原因一样

Transformer的改进

  • Transformer架构同时解决了seq2seq的两大缺陷,既可以并行计算,又应用 Multi-head Attention 机制来解决 Encoder 固定编码的问题,让 Decoder 在解码的每一步可以通过注意力去关注编码器输出中最重要的那些部分
  • Transformer 编码器的输出是一个矩阵,其中每一行对应输入序列中的一个 token 的上下文化表示(即一个向量)。这种设计保存了更丰富的语义信息

八、Transformer模型代码

import torch
import torch.nn as nn
import math
import copy
import numpy as np
 
 
class WordEmbeddings(nn.Module):
    def __init__(self, dim, vocab_size):
        super(WordEmbeddings, self).__init__()
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=dim)
        self.dim = dim
 
    def forward(self, data):
        # 扩大embedding vector,让原来的 embedding vector 中的信息在和 position encoding 的信息相加时不至于丢失掉
        return self.embedding(data) * math.sqrt(self.dim)
 
 
class PositionalEncoding(nn.Module):
    def __init__(self, dimension, dropout, max_length):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        pe = torch.zeros(max_length, dimension)
        # Position indices (0, 1, 2, ..., max_length-1)
        position = torch.arange(0, max_length).unsqueeze(1)
        # e^{-(log_{e}10000) * (2i/dim)}
        # 提出负号 1 / (e^{ln(10000) * (2i / dim)})
        # 因为 a^{m * n} == (a^{m})^{n}, 所以 e^{ln(10000) * (2i / dim)} 可化为 (e^{ln(10000)}) ^ {2i / dim}
        # 因为 e^{ln(x)} == x, 所以 (e^{ln(10000)}) ^ {2i / dim} 可化简为 10000^{2i/dim}
        # 所以 e^{-(log_{e}10000) * (2i/dim)} == 1 / (10000^{2i/dim})
        div_term = torch.exp(-(math.log(10000.0) * torch.arange(0, dimension, 2) / dimension))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        # 将pe位置编码矩阵注册成模型的buffer,注册后就可以在模型保存后重加载时和模型结构与参数一同被加载
        self.register_buffer('pe', pe)  # (1, max_length, dimension)
 
    def forward(self, data):
        data = data + self.pe[:, :data.size(1)]
        return self.dropout(data)
 
 
def subsequent_mask(batch_size, head_number, seq_length):
    # 生成向后遮掩的掩码张量, 参数size是掩码张量最后两个维度的大小, 最后两维形成一个方阵
    attention_shape = (batch_size, head_number, seq_length, seq_length)
    # 然后使用np.ones方法向这个形状中添加1元素,形成上三角阵, 最后为了节约空间, 再使其中的数据类型变为无符号8位整形unit8
    subsequent_mask = np.triu(np.ones(attention_shape), k=1).astype('uint8')
    # 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作,
    # 本质是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减, 0变成1、1变成0
    return torch.from_numpy(1 - subsequent_mask)
 
 
def self_attention(query, key, value, mask=None, dropout=None):
    dim = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(dim)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    scores_softmax = torch.softmax(scores, dim=-1)
    if dropout is not None:
        attention_weight = dropout(scores_softmax)
    return torch.matmul(attention_weight, value), attention_weight
 
 
class MultiHeadedAttention(nn.Module):
    def __init__(self, head_number, embedding_dim, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        assert embedding_dim % head_number == 0
        # 每个头分配的分割词向量维度
        self.d_k = embedding_dim // head_number
        self.head_number = head_number
        # 计算得到的注意力张量
        self.attention_weights = None
 
        self.query_linear = nn.Linear(embedding_dim, embedding_dim)
        self.key_linear = nn.Linear(embedding_dim, embedding_dim)
        self.value_linear = nn.Linear(embedding_dim, embedding_dim)
        self.last_linear = nn.Linear(embedding_dim, embedding_dim)
        self.dropout = nn.Dropout(dropout)
 
    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)
        if mask is not None:
            mask = mask.unsqueeze(0)
        query = self.query_linear(query).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        key = self.key_linear(key).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        value = self.value_linear(value).view(batch_size, -1, self.head_number, self.d_k).transpose(1, 2)
        new_value, self.attention_weights = self_attention(query, key, value, mask, self.dropout)
        new_value = new_value.transpose(1, 2).contiguous().view(batch_size, -1, self.head_number * self.d_k)
        new_value = self.last_linear(new_value)
        return new_value
 
 
class PositionwiseFeedForward(nn.Module):
    def __init__(self, in_dim, tmp_dim, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(in_dim, tmp_dim)
        self.linear2 = nn.Linear(tmp_dim, in_dim)
        self.dropout = nn.Dropout(dropout)
 
    def forward(self, data):
        output = torch.relu(self.linear1(data))
        output = self.dropout(output)
        output = self.linear2(output)
        return output
 
 
class LayerNormalization(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super(LayerNormalization, self).__init__()
        # 使用nn.parameter封装,代表其是模型的参数
        self.lambda_weight = nn.Parameter(torch.ones(dim))
        self.beta_bias = nn.Parameter(torch.zeros(dim))
        self.eps = eps
 
    def forward(self, data):
        mean = data.mean(-1, keepdim=True)
        std = data.std(-1, keepdim=True)
        return self.lambda_weight * (data - mean) / (std + self.eps) + self.beta_bias
 
 
class EncoderLayer(nn.Module):
    def __init__(self, dim, dropout):
        super(EncoderLayer, self).__init__()
        self.multi_head_self_attention = MultiHeadedAttention(16, dim, dropout)
        self.feed_forward = PositionwiseFeedForward(dim, 64, dropout)
        self.layer_norm1 = LayerNormalization(dim)
        self.layer_norm2 = LayerNormalization(dim)
        self.dropout = nn.Dropout(p=dropout)
        self.dim = dim
 
    def forward(self, data, mask=None):
        attention_output = self.multi_head_self_attention(data, data, data, mask)
        attention_output = self.dropout(attention_output)
        output1 = self.layer_norm1(data + attention_output)
 
        ffn_output = self.feed_forward(output1)
        ffn_output = self.dropout(ffn_output)
        output2 = self.layer_norm2(output1 + ffn_output)
        return output2
 
 
class Encoder(nn.Module):
    def __init__(self, dim, number):
        super(Encoder, self).__init__()
        self.layers = nn.ModuleList([copy.deepcopy(EncoderLayer(dim, 0.1)) for _ in range(number)])
 
    def forward(self, data, mask):
        for layer in self.layers:
            data = layer(data, mask)
        return data
 
 
class DecoderLayer(nn.Module):
    def __init__(self, dim, dropout):
        super(DecoderLayer, self).__init__()
        self.dim = dim
        self.multi_head_self_attention = MultiHeadedAttention(16, 512)
        self.multi_head_attention = MultiHeadedAttention(16, 512)
        self.feed_forward = PositionwiseFeedForward(dim, 64, dropout)
        self.layer_norm1 = LayerNormalization(dim)
        self.layer_norm2 = LayerNormalization(dim)
        self.layer_norm3 = LayerNormalization(dim)
        self.dropout = nn.Dropout(p=dropout)
 
    def forward(self, data, memory, mask):
        attention_output1 = self.multi_head_self_attention(data, data, data, mask)
        attention_output1 = self.dropout(attention_output1)
        output1 = self.layer_norm1(data + attention_output1)
 
        attention_output2 = self.multi_head_attention(data, memory, memory, None)
        attention_output2 = self.dropout(attention_output2)
        output2 = self.layer_norm1(output1 + attention_output2)
 
        ffn_output = self.feed_forward(output2)
        ffn_output = self.dropout(ffn_output)
        output3 = self.layer_norm1(output2 + ffn_output)
        return output3
 
 
class Decoder(nn.Module):
    def __init__(self, dim, number):
        super(Decoder, self).__init__()
        self.layers = nn.ModuleList([copy.deepcopy(DecoderLayer(dim, 0.1)) for _ in range(number)])
 
    def forward(self, data, memory, mask):
        for layer in self.layers:
            data = layer(data, memory, mask)
        return data
 
 
class Generator(nn.Module):
    def __init__(self, dim, vocab_size):
        super(Generator, self).__init__()
        self.linear = nn.Linear(dim, vocab_size)
 
    def forward(self, data):
        output = self.linear(data)
        return torch.softmax(output, dim=-1)
 
 
class Transformer(nn.Module):
    def __init__(self, layer_number, embedding_dim, source_vocab_size, target_vocab_size, max_seq_length):
        super(Transformer, self).__init__()
        self.source_word_embedding = WordEmbeddings(embedding_dim, source_vocab_size)
        self.target_word_embedding = WordEmbeddings(embedding_dim, target_vocab_size)
        self.pos_encoding = PositionalEncoding(embedding_dim, 0.1, max_seq_length)
        self.encoder = Encoder(embedding_dim, layer_number)
        self.decoder = Decoder(embedding_dim, layer_number)
        self.generator = Generator(embedding_dim, target_vocab_size)
 
    def forward(self, data):
        embedding1 = self.source_word_embedding(data)
        embedding1 = self.pos_encoding(embedding1)
        c = self.encoder(embedding1, None)

        embedding2 = self.target_word_embedding(data)
        mask = subsequent_mask(4, 16, 3)
        output = self.decoder(embedding2, c, mask)
        output = self.generator(output)
        return output
 

本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GG_Bond21

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值