当前位置:   article > 正文

Transformer源代码搭建逐类逐行逐句阅读分析

transformer源代码

模块1:ScaledDotProductAttention

缩放点积注意力机制的类,其中包含了前向传播函数,用于计算给定查询(q)、键(k)、值(v)以及可选掩码(mask)的注意力加权输出和注意力权重。

1.计算注意力:attention=q/t*K

2.掩码计算:mask掉attention=0的部分,替换成1e-9

让激活函数激活时,使得概率趋近于0,实现了在注意力中对应掩码中值为0的位置的抑制效果
  • 1

3.激活函数激活:F.softmax(attn, dim=-1)

将注意力分数(未经缩放的)转换为概率分布,使得这些分数的总和等于1
  • 1

正则化:dropout

以一定概率随机将部分神经元输出设为零,可以降低模型对于特定训练数据的依赖,有助于防止过拟合。这里设置参数为0.1
  • 1

4.output =attention* v

激活好之后,再乘以V,计算注意力输出
  • 1

模块2:MultiHeadAttention

这是一个多头注意力模块的类,用于将输入的查询、键和值进行多头注意力计算,并通过全连接层、残差连接、Layer Normalization 等操作进行特征变换和整合。

这里对主要参数进行解释:

  • n_head 表示多头注意力中头的数量。
  • d_model 是模型的输入和输出的特征维度。
  • d_kd_v 是每个头中查询(q)和值(v)的维度,其中 d_k 通常等于 d_model / n_head
  • dropout 是在多头注意力模块中使用的 dropout 概率。

该类的初始化方法 __init__ 设置了模型的参数,并定义了一些线性变换(nn.Linear)以及一个缩放点积注意力模块(ScaledDotProductAttention)。

  • self.w_qs, self.w_ks, self.w_vs: 分别表示用于将输入映射到查询、键、值的线性变换。
  • self.fc: 用于将多头注意力的输出映射回原始特征维度的线性变换。
  • self.attention: 是一个 ScaledDotProductAttention 类的实例,用于实现缩放点积注意力。
  • self.dropout: 是一个 dropout 操作,用于在模型训练过程中随机丢弃一些神经元。
  • self.layer_norm: 是一个 Layer Normalization 操作,用于对模型的输出进行归一化

1.计算sz_b, len_q, len_k, len_v=q.size(0), q.size(1), k.size(1), v.size(1)

  • sz_b: 表示输入张量的 batch size(批大小),即一次训练中输入的样本数。

  • len_q: 表示查询张量 q 的长度(或序列长度)。

  • len_k: 表示键张量 k 的长度。

  • len_v: 表示值张量 v 的长度。

    查询、键和值的长度需要匹配,以确保正确的权重计算

2.残差矩阵保留:residual = q

3.计算qkv矩阵:q = self.w_qs(q).view(sz_b, len_q, n_head, d_k) k = self.w_ks(k).view(sz_b, len_k, n_head, d_k) v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)

qkv通过线性映射,映射到batch_size, seq_len, n_head, d_k维度空间里

线性映射将注意力张量投影到多头注意力机制中的空间,以便后续的注意力计算
  • 1
  • 2
  • 3

4.q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)

形状从 `(batch_size, n_head, seq_len, d_k)` 变成 `(batch_size, seq_len, n_head, d_k)`有助于注意力计算的效率和正确性
  • 1

5.mask=mask.unsqueeze(1)

将较小形状的张量沿着适当的维度进行复制,使得它们的形状能够匹配,并执行元素级的操作。这样,即使张量的形状不同,也能够进行逻辑操作、数学运算等。

将 `mask` 的形状从 `(batch_size, seq_len)` 扩展为 `(batch_size, 1, seq_len)`。对掩码进行广播以匹配注意力权重张量的维度,确保在注意力计算中正确地应用掩码
  • 1

6.q, attn = self.attention(q, k, v, mask=mask)

多头注意力机制中的核心步骤
  • 1
  • q: 是多头注意力机制的输出,经过注意力权重加权和汇总的值。
  • attn: 是注意力权重张量,表示模型在输入序列中的每个位置对输出的注意力程度。

7.q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)

维度交换后连续化,再进行合并。将 `q` 张量的第二个维度(通常是序列长度)和第三个维度(通常是多头注意力的头数)进行交换,将 `q` 的形状从 `(batch_size, seq_len, n_head, d_k)` 变成 `(batch_size, n_head, seq_len, d_k)``.contiguous()` 会重新分配存储,使得张量变得连续,将多头注意力中的头数和维度进行了合并,得到一个形状更适合后续全连接层的张量,将(batch_size, n_head, seq_len, d_k)变换为(batch_size, seq_len, n_head * d_k)`
  • 1

8.q = self.dropout(self.fc(q))

在多头注意力机制的输出上应用一个全连接变换,以获得更适合下一层处理的特征表示,然后再进行正则化。通过使用全连接层引入非线性变换,再通过 dropout 操作提高模型的鲁棒性
  • 1

9.q += residual

残差连接:通过将输入直接添加到某一层的输出中,从而实现在层之间传递原始输入的机制。这种残差连接有助于梯度消失问题,使得梯度能够更轻松地传播回较早的层
  • 1

10.q = self.layer_norm(q)

对查询张量 q 进行 Layer Normalization。对于每个样本,对其每个特征维度进行标准化,使得每个特征的均值为零,方差为一,使得输出的张量在每个样本的每个特征维度上都满足标准正态分布。这有助于提高训练的稳定性,并可以在一定程度上替代 dropout 的正则化效果。

模块3:PositionwiseFeedForward

位置前馈神经网络,

residual = x  
x = self.w_2(F.relu(self.w_1(x)))  
x = self.dropout(x)  
x += residual  
x = self.layer_norm(x)  
  • 1
  • 2
  • 3
  • 4
  • 5

通过线性映射,激活再线性映射,进行位置前馈神经网络的核心计算。正则化增加鲁棒性,残差连接保留原始输入信息,标准化输出张量。

每个位置上对输入进行非线性映射和变换,引入了非线性性质,使得模型可以更好地学习序列中的复杂模式

模块4:EncoderLayer

encoder的前向传播

 def forward(self, enc_input, slf_attn_mask=None):
        enc_output, enc_slf_attn = self.slf_attn(
            enc_input, enc_input, enc_input, mask=slf_attn_mask)
        enc_output = self.pos_ffn(enc_output)
        return enc_output, enc_slf_attn
  • 1
  • 2
  • 3
  • 4
  • 5

经过多头自注意力计算得到注意力输出和注意力分数,注意力输出再进行位置前馈进行进一步的非线性映射和变换

模块5:DecoderLayer

由三个主要的子模块组成:自注意力层(self.slf_attn)、编码器-解码器注意力层(self.enc_attn)、和位置前馈神经网络层(self.pos_ffn)。

self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
  • 1
  • 2
  • 3

创建解码器的注意力层,关注编码器的注意力输出和解码器的注意力输入之间的关系,在编码器-解码器注意力计算中,enc_output 作为键(key)和值(value)的输入,而 dec_output 作为查询(query)的输入。
transformer

如图所示,左侧为encoder,右侧为decoder。这里创建的类是创建的单个块,具体的拼接要到后面才能连起来。

模块6:PositionalEncoding

d_hid 表示位置编码的维度(embedding dimension),n_position 表示序列的最大长度

1.缓冲区

self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
  • 1

通过调用 _get_sinusoid_encoding_table 方法生成一个正弦编码表,并将其作为不需要训练的缓冲区(buffer)注册到模型中。缓冲区是模型的一部分,但不会被优化器更新,因此在训练过程中保持不变。

ps:缓冲区通常用于存储模型的非学习状态的持久性数据,这里存储了位置编码表。缓冲区的好处包括:

  1. 持久性: 缓冲区的内容在模型的多次前向传播中保持不变。这对于一些不需要训练的常数或者在整个训练过程中保持不变的数据非常有用。

  2. 共享模型状态: 缓冲区是模型的一部分,但不会参与梯度计算,因此不会影响反向传播。这使得模型的状态可以在多个GPU上共享,而不需要额外的同步操作。

2.正弦编码表

    def _get_sinusoid_encoding_table(self, n_position, d_hid):
        ''' Sinusoid position encoding table '''
        def get_position_angle_vec(position):
            return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]

        sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
        sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i
        sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1

        return torch.FloatTensor(sinusoid_table).unsqueeze(0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • get_position_angle_vec()计算给定位置的角度向量。这个向量的每个维度对应于编码的每个维度。

正弦位置编码的公式如下:

对于给定的位置 `pos` 和编码维度 `dim`,位置编码的值被计算为:
  • 1

计算方法

其中,`i` 表示编码的维度。

在这个公式中,`pos` 是位置的索引,`dim` 是编码的维度。这个计算方式使得相邻位置的编码在编码空间中存在一定的正弦和余弦关系,有助于模型捕捉序列中元素的相对位置信息。
  • 1
  • 2
  • 3
  • sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])

创建一个数组,其中每行对应于序列中的一个位置,每列对应于编码的一个维度。这里使用了 NumPy

  • 对编码表的偶数列(维度 2i)应用正弦函数。对编码表的奇数列(维度 2i+1)应用余弦函数。

  • 将生成的 NumPy 数组转换为 PyTorch 的浮点张量,并在第一维上添加一个维度,以适应 PyTorch 的张量形状。

类似于[m,n…]变成了[[m,n…]]

3.添加位置编码

x + self.pos_table[:, :x.size(1)].clone().detach()

这一步首先截取了位置编码表 `self.pos_table` 的前 `x.size(1)` 列,以匹配输入序列的长度。然后使用 `clone()` 方法创建了一个副本,并通过 `detach()` 方法将其从计算图中分离,确保在后续的计算中不会影响梯度传播。使模型在处理序列数据时能够感知元素的相对位置。这对于提高模型对序列中元素的理解能力
  • 1
  • 2
  • 3

模块7:Encoder

整个transformer的encoder

参数:

  • n_src_vocab: 源语言词汇的大小,即源语言的词汇表中词的数量。

  • d_word_vec: 词嵌入的维度,表示词向量的长度。

  • n_layers: 编码器层数,即编码器由多少个相同结构的层堆叠而成。

  • n_head: 注意力头的数量,即每个多头自注意力机制中有多少个注意力头。

  • d_k: 每个注意力头的键(key)的维度。

  • d_v: 每个注意力头的值(value)的维度。

  • d_model: 模型的总体维度,即每个位置的输出向量的维度。

  • d_inner: 编码器层内部前馈神经网络的隐藏层维度。

  • pad_idx: 用于填充的标记索引。

  • dropout: 用于进行随机失活的概率。

  • n_position: 位置编码的最大长度。

  • scale_emb: 一个布尔值,表示是否对词嵌入进行缩放。

1.初始化

 self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
        self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
        self.dropout = nn.Dropout(p=dropout)
        self.layer_stack = nn.ModuleList([
            EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
            for _ in range(n_layers)])
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
        self.scale_emb = scale_emb
        self.d_model = d_model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • self.src_word_emb: 一个词嵌入层,用于将输入序列中的词索引映射为词嵌入向量。

  • self.position_enc: 一个位置编码层,用于将位置信息嵌入到词嵌入中。

  • self.dropout: 一个用于进行随机失活的 Dropout 层。

  • self.layer_stack: 由多个编码器层堆叠而成的模型层。

  • self.layer_norm: 一个 Layer Normalization 层,用于归一化每个位置的输出。

  • self.scale_emb: 一个布尔值,表示是否对词嵌入进行缩放。

  • self.d_model: 模型的总体维度。

嵌入层、位置编码层、多个堆叠的编码器层和一些额外的正则化和归一化层。

2.forward前馈

  • enc_output = self.src_word_emb(src_seq)

    进行词嵌入,具体就是根据词汇表转换为固定维度向量

  • if self.scale_emb:

    enc_output *= self.d_model ** 0.5
    
    • 1

    这个缩放的目的是为了防止词嵌入的值过大,从而平衡不同维度的输入对模型的影响。

  • enc_output = self.dropout(self.position_enc(enc_output))位置编码并进行随机失活

  • enc_output = self.layer_norm(enc_output)

    正则化

  • for enc_layer in self.layer_stack:

    enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)  
    enc_slf_attn_list += [enc_slf_attn] if return_attns else []
    
    • 1
    • 2

    逐层遍历编码器层。如果需要,存储每个编码器层的自注意力权重。获得最后的输出

模块8:Decoder

decoder模块,在输入时,q为encoder的output ,其余与encoder结构类似

        dec_output = self.trg_word_emb(trg_seq)
        if self.scale_emb:
            dec_output *= self.d_model ** 0.5
        dec_output = self.dropout(self.position_enc(dec_output))
        dec_output = self.layer_norm(dec_output)

        for dec_layer in self.layer_stack:
            dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
                dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
            dec_slf_attn_list += [dec_slf_attn] if return_attns else []
            dec_enc_attn_list += [dec_enc_attn] if return_attns else []

        if return_attns:
            return dec_output, dec_slf_attn_list, dec_enc_attn_list
        return dec_output,
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

多层堆叠形成网络连接

模块9:Transformer

1.初始化

  • self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx

找到目标和源语言的填充索引

  • assert scale_emb_or_prj in [‘emb’, ‘prj’, ‘none’]scale_emb = (scale_emb_or_prj == ‘emb’) if trg_emb_prj_weight_sharing else Falseself.scale_prj = (scale_emb_or_prj == ‘prj’) if trg_emb_prj_weight_sharing else False显示判断是否缩放嵌入层、投影层,或者都不缩放如果启用了目标词嵌入与投影层权重的共享,并且 scale_emb_or_prj 的值为 ‘emb’,则 scale_emb 被设置为 True,表示在嵌入层输出后进行权重缩放。否则,scale_emb 被设置为 False。prj同理。在模型中同时使用两种缩放方式可能导致不一致或混乱,因此一般来说,选择其中一种缩放方式更为常见和合理。理解原因的关键点有两个:

    1. 统一性和一致性: 为了简化模型的设计和理解,通常希望在整个模型中保持一致性。选择一种缩放方式可以减少设计的复杂性,并确保模型在训练和推断时行为一致。

    2. 可解释性: 在 Transformer 模型中,emb 缩放方式意味着在嵌入层输出后进行缩放,而 prj 缩放方式意味着在线性投影层输出后进行缩放。同时使用两种缩放方式可能使模型的行为变得难以解释,不利于理解模型如何处理输入和生成输出。

    因此,通常会选择在设计模型时明确定义是使用 emb 缩放、prj 缩放还是不进行额外缩放。

  • self.encoder = Encoder(

    n_src_vocab=n_src_vocab, n_position=n_position,  
    d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,  
    n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,  
    pad_idx=src_pad_idx, dropout=dropout, scale_emb=scale_emb)  
    
    • 1
    • 2
    • 3
    • 4

    self.decoder = Decoder(

    n_trg_vocab=n_trg_vocab, n_position=n_position,  
    d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,  
    n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,  
    pad_idx=trg_pad_idx, dropout=dropout, scale_emb=scale_emb)  
    
    • 1
    • 2
    • 3
    • 4

    self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)encoder、decoder和目标词投影层定义。这个线性层的作用是将解码器最后一层的输出映射为目标语言词汇表的概率分布,从而用于生成目标语言的下一个词。

  • for p in self.parameters():

    if p.dim() > 1:  
        nn.init.xavier_uniform_(p)
    
    • 1
    • 2

    使用 Xavier 均匀初始化方法对参数 p 进行初始化。Xavier 初始化是一种常用的权重初始化方法,旨在在前向传播和反向传播中保持梯度的稳定性。

  • assert d_model == d_word_vec保证输入输出维度相同

  • if trg_emb_prj_weight_sharing:

    # Share the weight between target word embedding & last dense layer  
    self.trg_word_prj.weight = self.decoder.trg_word_emb.weight
    
    • 1
    • 2

    共享这两个层的权重,模型在训练中会学习一组参数,这些参数同时用于嵌入目标词和将解码器的输出映射到目标词汇表。这有助于减少模型的参数量,提高模型的泛化性能,并可能加速模型的训练过程。

2.forward前馈

  • src_mask = get_pad_mask(src_seq, self.src_pad_idx)trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)掩码会将源序列中的填充标记位置标记为 True,其余位置标记为 False

  • enc_output, *_ = self.encoder(src_seq, src_mask)dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)经过encoder和decoder层

  • seq_logit = self.trg_word_prj(dec_output)这行代码通过将解码器的输出传递给线性层 self.trg_word_prj 来计算目标序列的 logits。这些 logits 可以通过 softmax 函数转换为概率分布,从而用于生成目标序列的下一个词。

  • if self.scale_prj:

    seq_logit *= self.d_model ** -0.5
    
    • 1

    调整 logits 的值,以确保在 softmax 操作中的数值稳定性transformer模块搭建结束

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号