Transformer代码流程学习笔记

这篇主要是以下这篇文献的阅读笔记: https://towardsdatascience.com/how-to-code-the-transformer-in-pytorch-24db27c8f9ec

同时在YouTube上面有非常详细的代码讲解:https://www.youtube.com/watch?v=ISNdQcPhsts

Github上这个项目非常详细:GitHub - hyunwoongko/transformer: Transformer: PyTorch Implementation of "Attention Is All You Need"

参考文献:

https://medium.com/ching-i/transformer-attention-is-all-you-need-c7967f38af14

Transformer图解 - 李理的博客

13.【李宏毅机器学习2021】Transformer (下)_哔哩哔哩_bilibili

总体结构:

图来自文章《attention is all you need》

Transfomer主要分为Encoder和Decoder两个部分。 每个模块均为N个完全相同的模块堆叠组成(图中N=6)。 每一个encoder的输入是上一层ecoder的输出。

Encoder中输入的信息为Input,也就是用于预测的那部分信息; Decoder中输入的信息包括预测的结果,包括实际的预测信息和从encoder的输出。 最后是根据预测出来的结果与实际的想要预测的结果进行比较对比,计算loss值。

Encoder

Input Embedding

Encoder的第一步是将input转换为一个向量,第一步得到的数据维度将为: (batch_size, sequence_length, d_model)

#vocab_size是token的数量,d_model是transfomer的输出维度
class Embedder(nn.Module):
    def __init__(self, vocab_size, d_model):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
    def forward(self, x):
        return self.embed(x)

Note: 在代码中存在forward, forward方法是 PyTorch 模块中的一个特殊方法。每当你向 PyTorch 模型或自定义模块传递输入时(例如,model(input)),forward 方法就会被调用。一般用于说明input是如何被转化用于output。

Positional encoding 

在将input转换为一个向量以后,接着就是加上位置信息(positional encoding),位置信息使用的方法如下:

class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_seq_len = 80):
        super().__init__()
        self.d_model = d_model
        
        # create constant 'pe' matrix with values dependant on 
        # pos and i
        pe = torch.zeros(max_seq_len, d_model)
        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                pe[pos, i] = \
                math.sin(pos / (10000 ** ((2 * i)/d_model)))
                pe[pos, i + 1] = \
                math.cos(pos / (10000 ** ((2 * (i + 1))/d_model)))
                
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
 
    
    def forward(self, x):
        # make embeddings relatively larger
        x = x * math.sqrt(self.d_model)
        #add constant to embedding
        seq_len = x.size(1)
        x = x + Variable(self.pe[:,:seq_len], \
        requires_grad=False).cuda()
        return x

下一步就是将位置信息加入到input embedding里面,流程如图所示:

我们之所以在相加之前增加嵌入值,是为了使位置编码相对较小。这意味着当我们将它们相加时,嵌入向量中的原始含义不会丢失。

这一步得到的数据维度为: (batch_size, seq_len, d_model)

Multi-head Attention 

以下为multi-head attention的结构: 

多头注意力机制是由h个相同的注意力机制堆叠形成的,首先对其中一个注意力机制的原理进行介绍:

关于Q,K,V, B站这个视频非常直观:注意力机制的本质|Self-Attention|Transformer|QKV矩阵_哔哩哔哩_bilibili

注意力机制中有三个重要的向量 Q,K,V 分别代表:

Q (Query) :查询通常是你想要查找的信息,代表当前我们关注的内容

K (Key):键与查询配对,与查询配对用来计算注意力分数。这个分数决定了每个值的重要性

V (Value): 注意力分数,这些分数就用于加权“值”。这样,生成的输出是值的加权组合,权重由键和查询之间的相似度决定

在某一个时刻下,通过 q对每一个k做内积得到q,k之间匹配的相似程度 α1, α2……,然后使用softmax把得分变成概率,除于\sqrt{d_k} (向量的长度)的原因是为了使梯度稳定。 

这个步骤叫做:scaled dot-product attention. 

在李宏毅老师的课程中,使用了一个例子来说明。 

Q,K,V 均是由X投影形成,投影到较低维度。对于Q,K,V而言,他们都是由同一个X产生的,乘上不同的W生成的。投影h次生成不一样的结果,学习到不一样的特征,增加了多样性。在所有的计算中,只有W^Q, W^K, W^V是未知的,在计算的过程中将由算法决定。

所以得到详细的公式如下:

class MultiHeadAttention(nn.Module):
    def __init__(self, heads, d_model, dropout = 0.1):
        super().__init__()
        
        self.d_model = d_model
        self.d_k = d_model // heads
        self.h = heads
        
        self.q_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        self.out = nn.Linear(d_model, d_model)
    
    def forward(self, q, k, v, mask=None):
        
        bs = q.size(0)
        
        # perform linear operation and split into h heads
        在进行分头的时候一般均使用以下的代码,view
        
        k = self.k_linear(k).view(bs, -1, self.h, self.d_k)
        q = self.q_linear(q).view(bs, -1, self.h, self.d_k)
        v = self.v_linear(v).view(bs, -1, self.h, self.d_k)
        
        # transpose to get dimensions bs * h * sl * d_model
       
        k = k.transpose(1,2)
        q = q.transpose(1,2)
        v = v.transpose(1,2)
# calculate attention using function we will define next
        scores = attention(q, k, v, self.d_k, mask, self.dropout)
        
        # concatenate heads and put through final linear layer
        concat = scores.transpose(1,2).contiguous()\
        .view(bs, -1, self.d_model)
        
        output = self.out(concat)
    
        return output

 对于多头注意力机制而言,其实就是以上的程序重复h次,使用h次不一样的投影,来学习不同方向。为什么要做多次的原因在于:是为了使不同的head关注不同的信息。 最后再对多头计算出来的结果进行contact。

Add&Norm

在多头注意力机制以后,后一层是add&norm层,这一层是进行残差连接(Residual)以及layer normalization. 

在这张图里更加清晰:也就是通过自注意力层得到的结果还会加上原本的x值来通过全连接层。

归一化在深度神经网络中非常重要。它可以防止层中的数值范围变化过大,这意味着模型的训练速度更快,泛化能力更强。

在tranformer里面中使用的不是batch normalization不是,而是layer normalization。主要的原因是因为对于不同的样本,长度是不相同的,layer normalization的话可以降低不同样本之间长度不同带来的影响。

class Norm(nn.Module):
    def __init__(self, d_model, eps = 1e-6):
        super().__init__()
    
        self.size = d_model
        # create two learnable parameters to calibrate normalisation
        self.alpha = nn.Parameter(torch.ones(self.size))
        self.bias = nn.Parameter(torch.zeros(self.size))
        self.eps = eps
    def forward(self, x):
        norm = self.alpha * (x - x.mean(dim=-1, keepdim=True)) \
        / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias
        return norm

Feed-Forward Network

这一层其实就是一个MLP。

两层线性计算,首先对上一层的结果进行线性运算,Relu计算再进行线性计算。

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=2048, dropout = 0.1):
        super().__init__() 
        # We set d_ff as a default to 2048
        self.linear_1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model)
    def forward(self, x):
        x = self.dropout(F.relu(self.linear_1(x)))
        x = self.linear_2(x)
        return x

Decoder

Decoder是解码层,Decoder的其他部分与encoder基本一致,与encoder只有multi-head attention和feed forward不同的是,在decoder中还有一个block: masked multi-head attention. 

Decoder的输入是真实值 (teaching forcing). 

Create masks

在这里主要讲述一下Masked Multi-head attention的部分,因为在decoder中预测的信息是一个一个产生的,所以在生产结果的时候需要对之后的预测结果进行遮挡。

masks的目的主要有: 

  • Padding mask在编码器和解码器中: 如果输入句子中仅有填充(pad),则输出为零。填充是一种特殊的标记,在将多个序列批量处理在一起是,它被添加到时间序列中,使其长度相等。 
  • Sequence mask 在解码器中: 防止解码器在预测下一个单词时 "峰值 "超前翻译句子的其余部分。
batch = next(iter(train_iter))
input_seq = batch.English.transpose(0,1)
input_pad = EN_TEXT.vocab.stoi['<pad>']
# creates mask with 0s wherever there is padding in the input
input_msk = (input_seq != input_pad).unsqueeze(1)

# create mask as before
target_seq = batch.French.transpose(0,1)
target_pad = FR_TEXT.vocab.stoi['<pad>']
target_msk = (target_seq != target_pad).unsqueeze(1)
size = target_seq.size(1) # get seq_len for matrix
nopeak_mask = np.triu(np.ones(1, size, size),
k=1).astype('uint8')
nopeak_mask = Variable(torch.from_numpy(nopeak_mask) == 0)
target_msk = target_msk & nopeak_mask

mask的工作原理:

从个体的角度来看: 

对于a^2来说,在masked self-attention的时候,使用q^2只对k^1k^2进行查询,后面的k^3k^4则是被挡住的状态,这与self-attention的机制是不一样的。

从矩阵的角度来看:

使用一个非常大的数来做后面的K对应的V。也就是使用一个上三角为1的矩阵来使当前单词之后的信息看不到

Cross attention

中间层的multi-head attention的输入q来自于本身前一层的输出,而k,v 均属于N个encoder中最后一层输出的结果。当decoder生成一个新词时,它会使用自己的输出作为查询(Q),并使用encoder的输出作为键(K)和值(V),以此来决定输入序列的哪些部分是与当前生成的词最相关的。

以下图为例,在decoder中生成q, 然后分别与encoder中的k和v进行查询,得到生成的结果,得到后续的结果。用一个通俗的话来说就是:解码器的这个q在编码器的k里面挑选与自己关联度高的值。

这一步主要是考虑在建模的时候考虑上下文的信息。 

猜你喜欢

转载自blog.csdn.net/weixin_44897685/article/details/133495813