Transformer学习笔记

Transformer

250

Transformer是一个完全依赖注意力(Attention)的模型,最开始主要应用在NLP的翻译任务中,主要的革命对象是RNN为代表的序列模型。不同于传统RNN模型,模型在 t t t时刻只能利用到 t − 1 t-1 t1时刻的历史信息,Transformer的注意力计算方式一次可以关注到所有的输入信息,因此具备更出色并行计算能力,这对于深度学习训练而言是十分有效的。
Transformer由Encoder和Decoder两个模块组成,Encoder负责接收一个batch的输入序列 [ b , n , c ] [b, n, c] [b,n,c],随后通过一个Embedding层进行特征嵌入 [ b , n , c ] − > [ b , n , d m o d e l ] [b,n, c] -> [b, n, d_{model}] [b,n,c]>[b,n,dmodel],其中 d m o d e l = 512 d_{model} = 512 dmodel=512,Transformer的注意力计算方式会将每个样本和其他样本都执行一次注意力计算,因此,不管输入序列的顺序几何,都不会对最终的注意力计算结果造成影响。因此在Embedding层后,需要加入一个可以指示位置信息的嵌入,即Positional Encoding,原文的方法是用Sinusoidal Position Encoding:
P E ( p o s , 2 i ) = s i n ( p o s 1000 0 2 i d m o d e l )     P E ( p o s , 2 i + 1 ) = c o s ( p o s 1000 0 2 i d m o d e l ) PE_{(pos,2i)} = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}} ) \ \ \ PE_{(pos,2i+1)} =cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}} ) PE(pos,2i)=sin(10000dmodel2ipos)   PE(pos,2i+1)=cos(10000dmodel2ipos)
本质上是用一个角频率为 w = 1 1000 0 2 i d m o d e l w = \frac{1}{10000^{\frac{2i}{d_{model}}}} w=10000dmodel2i1的正、余弦基信号来表征每个维度 i i i,这样做的好处是因为 p o s + k pos + k pos+k可以用 p o s pos pos来线性表征。
s i n ( a + b ) = s i n a   c o s b + c o s a   s i n b     c o s ( a + b ) = c o s a   c o s b − s i n a   s i n b sin(a + b) = sina \ cosb + cosa \ sinb \ \ \ cos(a+b) = cosa \ cosb - sina \ sinb sin(a+b)=sina cosb+cosa sinb   cos(a+b)=cosa cosbsina sinb
P E ( p o s + k , 2 i ) = s i n ( w i ⋅ ( p o s + k ) ) = s i n ( w i p o s ) c o s ( w i k ) + c o s ( w i p o s ) s i n ( w i k ) PE_{(pos+k,2i)}=sin(w_i\cdot(pos+k))=sin(w_ipos)cos(w_ik)+cos(w_ipos)sin(w_ik) PE(pos+k,2i)=sin(wi(pos+k))=sin(wipos)cos(wik)+cos(wipos)sin(wik)
P E ( p o s + k ; 2 i + 1 ) = c o s ( w i ⋅ ( p o s + k ) ) = c o s ( w i p o s ) c o s ( w i k ) − s i n ( w i p o s ) s i n ( w i k ) PE_{(pos+k;2i+1)}=cos(w_i\cdot(pos+k))=cos(w_ipos)cos(w_ik)-sin(w_ipos)sin(w_ik) PE(pos+k;2i+1)=cos(wi(pos+k))=cos(wipos)cos(wik)sin(wipos)sin(wik)
P E ( p o s + k , 2 i ) = c o s ( w i k ) P E ( p o s , 2 i ) + s i n ( w i k ) P E ( p o s , 2 i + 1 ) PE_{(pos+k,2i)} =cos(w_i k)PE_{(pos,2i)} +sin(w_i k)PE_{(pos,2i+1)} PE(pos+k,2i)=cos(wik)PE(pos,2i)+sin(wik)PE(pos,2i+1)
P E ( p o s + k , 2 i + 1 ) = c o s ( w i k ) P E ( p o s , 2 i + 1 ) − s i n ( w i k ) P E ( p o s , 2 i ) PE_{(pos+k,2i+1)} =cos(w_i k)PE_{(pos,2i+1)} -sin(w_i k)PE_{(pos,2i)} PE(pos+k,2i+1)=cos(wik)PE(pos,2i+1)sin(wik)PE(pos,2i)
由上,对于pos+k位置的任意一个维度而言,都可以表示为pos和k的线性组合
位置编码的向量长度需要和输入Embedding的长度保持一致,即512维,将输入嵌入和位置编码直接相加后执行自注意力,此时每个词就被赋予了一个位置信息。
由于Encoder中的注意力采用的是自注意力(Self-Attention)机制,因此输入的特征会同时作为Q、K和V计算注意力,但是Transformer中的注意力层采用的是多头注意力(Multi-Head Attention),即在计算注意力前先通过 h h h个MLP进行投影,这里多头的意义主要是想通过引入可学习的投影参数,使模型可以获得更多可学习参考的视角
在这里插入图片描述

本文采用的注意力属于尺度缩放点乘注意力(Scaled Dot-Product Attention),通过点乘计算每个Q和每个K的相似度矩阵,随后除一个 d k \sqrt{d_k} dk ,并通过一个softmax归一化。得到的注意力矩阵本质上就是一些权重,表示为V中的每个样本/元素对最终结果的贡献程度。
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dk QKT)V
为什么要加一个尺度放缩?如果 d k d_k dk逐渐变大,那么注意力矩阵的每一个值大概率是会不断变大,如果注意力矩阵中的每个数值之间的差距变得很大,假设此时有一个最大值 x 0 x_0 x0,那他对应的softmax后的概率基本接近1,其他位置的值接近0,这就导致softmax的梯度趋近0,因此作者在注意力矩阵计算时加了一个放缩因子 d k \sqrt{d_k} dk ,从而缓解这种情况的发生。
在这里插入图片描述

多头注意力中,需要对 h h h个头计算的注意力结果进行连接,连接后传输到一个前馈网络中。Transformer默认的注意力头数量是8,同时为了保证输入输出的维度一致(因为还要残差链接),所以会在执行注意力前通过一个MLP将QKV的维度映射到 d = d m o d e l / h d = d_{model} / h d=dmodel/h,最后拼接后仍然保持 d m o d e l d_{model} dmodel不变。
多头注意力的结果和输入的原始特征通过残差加和后,需要通过Layer Norm层进行归一化,在NLP领域中,Layer Norm通常更为常用,CV中的Normalization方法一般是Batch Norm。我的理解是,Layer Norm是对每个Token单独进行归一化,Batch Norm则是对每个Channel在全部Batch内进行归一化。这里的Token在NLP中一般是每个词,而CV的图像处理任务中,每个Token则是图像的每个通道,视频任务中每个Token则是每个时间戳下的样本(每一帧)。
完成注意力计算后,FFN本身就是一个两层的MLP,作者解释的作用是把注意力聚合的特征映射到一个“合理的语义空间中”,感觉像是进一步增加模型可学习的维度/参数,同样在FFN处也有一个残差连结。
Decoder的基本结构和Encoder相似,但是的第一个Attention层是一个带有Masked的注意力层,之所以采用Masked是因为在训练模型阶段,输入内容是完整一个sentence,但是在预测第n个词时,肯定是只能用到 [ 1 , n − 1 ] [1, n-1] [1,n1]个词的信息,后面的词信息是不能被“看到”的,所以需要一个mask来将n以后的词全部都“遮盖”,从而保证训练的严谨性和准确性。
Decoder的第二个Attention模块和之前都有不同,此时需要将输入信息加入进来,将输入的信息作为K、V,而将历史输出信息作为Q进行一个交叉注意力。其他计算方法保持一致。

Code调试

code by Tae Hwan Jung(Jeff Jung) @graykode, Derek Miller @dmmiller612, modify by shwei。
如下是代码构建的Transformer的结构图:Encoder中包括一个embedding层,编码成为一个512维度的向量,每个Encoder中有6层Encoder Layers。

Transformer(
  (encoder): Encoder(
    (src_emb): Embedding(11, 512)
    (pos_emb): PositionalEncoding(
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (layers): ModuleList(
      (0-5): 6 x EncoderLayer(
        (enc_self_attn): MultiHeadAttention(
          (W_Q): Linear(in_features=512, out_features=512, bias=False)
          (W_K): Linear(in_features=512, out_features=512, bias=False)
          (W_V): Linear(in_features=512, out_features=512, bias=False)
          (fc): Linear(in_features=512, out_features=512, bias=False)
        )
        (pos_ffn): PoswiseFeedForwardNet(
          (fc): Sequential(
            (0): Linear(in_features=512, out_features=2048, bias=False)
            (1): ReLU()
            (2): Linear(in_features=2048, out_features=512, bias=False)
          )
        )
      )
    )
  )
  (decoder): Decoder(
    (tgt_emb): Embedding(12, 512)
    (pos_emb): PositionalEncoding(
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (layers): ModuleList(
      (0-5): 6 x DecoderLayer(
        (dec_self_attn): MultiHeadAttention(
          (W_Q): Linear(in_features=512, out_features=512, bias=False)
          (W_K): Linear(in_features=512, out_features=512, bias=False)
          (W_V): Linear(in_features=512, out_features=512, bias=False)
          (fc): Linear(in_features=512, out_features=512, bias=False)
        )
        (dec_enc_attn): MultiHeadAttention(
          (W_Q): Linear(in_features=512, out_features=512, bias=False)
          (W_K): Linear(in_features=512, out_features=512, bias=False)
          (W_V): Linear(in_features=512, out_features=512, bias=False)
          (fc): Linear(in_features=512, out_features=512, bias=False)
        )
        (pos_ffn): PoswiseFeedForwardNet(
          (fc): Sequential(
            (0): Linear(in_features=512, out_features=2048, bias=False)
            (1): ReLU()
            (2): Linear(in_features=2048, out_features=512, bias=False)
          )
        )
      )
    )
  )
  (projection): Linear(in_features=512, out_features=12, bias=False)
)

本文的Encoder结构如下:

class Encoder(nn.Module):  
    def __init__(self):  
        super(Encoder, self).__init__()  
        self.src_emb = nn.Embedding(src_vocab_size, d_model)  # token Embedding  
        self.pos_emb = PositionalEncoding(  
            d_model)  # Transformer中位置编码时固定的,不需要学习  
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])  
  
    def forward(self, enc_inputs):  
        """  
        enc_inputs: [batch_size, src_len]        """        
        enc_outputs = self.src_emb(  
            enc_inputs)  # [batch_size, src_len, d_model] 
        enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(  
            0, 1)  # [batch_size, src_len, d_model]  
        # Encoder输入序列的pad mask矩阵  
        enc_self_attn_mask = get_attn_pad_mask(  
            enc_inputs, enc_inputs)  # [batch_size, src_len, src_len]  
        enc_self_attns = []  # 在计算中不需要用到,它主要用来保存你接下来返回的attention的值(这个主要是为了你画热力图等,用来看各个词之间的关系  
        for layer in self.layers:  # for循环访问nn.ModuleList对象  
            # 上一个block的输出enc_outputs作为当前block的输入  
            # enc_outputs: [batch_size, src_len, d_model], enc_self_attn: [batch_size, n_heads, src_len, src_len]  
            enc_outputs, enc_self_attn = layer(enc_outputs,  
                                               enc_self_attn_mask)  # 传入的enc_outputs其实是input,传入mask矩阵是因为你要做self attention  
            enc_self_attns.append(enc_self_attn)  # 这个只是为了可视化  
        return enc_outputs, enc_self_attns

本层的输入为enc_inputs,是一个 [ 2 , 8 ] [2,8] [2,8]大小的tensor,其中2为batch size,8是句子长度。输入后的enc_inputs会首先经过一个nn.Embedding层。nn.Embedding是PyTorch中的一个常用模块,其主要作用是将输入的整数序列转换为密集向量表示。NLP任务中,可以将每个单词表示成一个向量,从而方便进行下一步的计算和处理。本文的Embedding层是将每个词转换为一个512维的向量,得到 [ 2 , 8 , 512 ] [2, 8, 512] [2,8,512]
注意:这里返回的结果记为“enc_outputs”,这是因为后续在计算Attention时需要反复将上一层的输出作为当前层的输入,这样命名便于理解。
经过编码后的特征将会和Positional Encoding的结果进行加和,这里的位置编码采用的是论文中的正余弦位置编码。此外,还有一个需要注意的地方是,经过编码后的特征enc_outputs会先进行一次转置,转置为 [ n , b , c ] [n, b, c] [n,b,c]形状,随后送入Positional Encoding中。

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)  # 假设有max_len个词,每个词512维
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)  # 每个词的位置,也需要一个512维度的编码
        div_term = torch.exp(torch.arange(  
            0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # 2i/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: [seq_len, batch_size, d_model]"""        
        x = x + self.pe[:x.size(0), :]  # 此时的x.size(0)不是batch size了,而是句子长度。对每个句子进行位置编码,加在每个batch上。
        return self.dropout(x)

此外,我们注意到,在论文中并没有提到在Encoder中也需要进行Mask。

enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)  # [batch_size, src_len, src_len]  

这里的这句话是给self Attention构建一个mask,实现方式为:

def get_attn_pad_mask(seq_q, seq_k):  
    # pad mask的作用:在对value向量加权平均的时候,可以让pad对应的alpha_ij=0,这样注意力就不会考虑到pad向量  
    """这里的q,k表示的是两个序列(跟注意力机制的q,k没有关系),例如encoder_inputs (x1,x2,..xm)和encoder_inputs (x1,x2..xm)  
    encoder和decoder都可能调用这个函数,所以seq_len视情况而定  
    seq_q: [batch_size, seq_len]    seq_k: [batch_size, seq_len]    seq_len could be src_len or it could be tgt_len    seq_len in seq_q and seq_len in seq_k maybe not equal    """    
    batch_size, len_q = seq_q.size()  # 这个seq_q只是用来expand维度的  
    batch_size, len_k = seq_k.size()  
    # eq(zero) is PAD token  
    # 例如:seq_k = [[1,2,3,4,0], [1,2,3,5,0]]  
    # [batch_size, 1, len_k], True is masked    
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  
    # [batch_size, len_q, len_k] 构成一个立方体(batch_size个这样的矩阵)  
    return pad_attn_mask.expand(batch_size, len_q, len_k)

输入的seq_k和seq_q是一样的,都是之前的enc_inputs,pad_attn_mask的结果是将seq_k中非零的位置表征为False,0的位置则为True。这里查了一下资料,解释是:“在计算自注意力的时候,只对有效序列长度进行attention计算”,而NLP任务中,每一个Batch中的sentence的长度不一定一致,如果不一致,会对长度较短的句子补0对齐,因此0的位置属于“无效序列”,需要被mask掉。最后返回的是一个 [ b , n , n ] [b, n, n] [b,n,n]大小的矩阵。为什么是 [ b , n , n ] [b, n, n] [b,n,n]?因为最后得到的注意力矩阵是一个 [ n , n ] [n,n] [n,n]的矩阵,矩阵上的每个元素 ( i , j ) (i,j) (i,j)都代表第 i i i个字和第 j j j个字之间的注意力权重,对于序列为0的位置则不需要计算权重。
接下来将enc_inputs和enc_self_attn_mask通过每一层的Attention和FFN。

for layer in self.layers:  # for循环访问nn.ModuleList对象  
    # 上一个block的输出enc_outputs作为当前block的输入  
    # enc_outputs: [batch_size, src_len, d_model], enc_self_attn: [batch_size, n_heads, src_len, src_len]  
    enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)  # 传入的enc_outputs其实是input,传入mask矩阵是因为你要做self attention  

代码遍历Transformer的每一层,同时将上一层的输出作为下一层的输入,下面是Layer部分的结构:

ModuleList(
  (0-5): 6 x EncoderLayer(
    (enc_self_attn): MultiHeadAttention(
      (W_Q): Linear(in_features=512, out_features=512, bias=False)
      (W_K): Linear(in_features=512, out_features=512, bias=False)
      (W_V): Linear(in_features=512, out_features=512, bias=False)
      (fc): Linear(in_features=512, out_features=512, bias=False)
    )
    (pos_ffn): PoswiseFeedForwardNet(
      (fc): Sequential(
        (0): Linear(in_features=512, out_features=2048, bias=False)
        (1): ReLU()
        (2): Linear(in_features=2048, out_features=512, bias=False)
      )
    )
  )
)

可以看到是6层Attention+FFN,我们以其中一层的操作举例,其他层的操作其实是一致的。

class EncoderLayer(nn.Module):  
    def __init__(self):  
        super(EncoderLayer, self).__init__()  
        self.enc_self_attn = MultiHeadAttention()  
        self.pos_ffn = PoswiseFeedForwardNet()  
  
    def forward(self, enc_inputs, enc_self_attn_mask):  
        """E
        enc_inputs: [batch_size, src_len, d_model]        enc_self_attn_mask: [batch_size, src_len, src_len]  mask矩阵(pad mask or sequence mask)  
		"""        
		# enc_outputs: [batch_size, src_len, d_model], attn: [batch_size, n_heads, src_len, src_len]
		# 第一个enc_inputs * W_Q = Q  
        # 第二个enc_inputs * W_K = K  
        # 第三个enc_inputs * W_V = V  
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,  
                                               enc_self_attn_mask)  # enc_inputs to same Q,K,V(未线性变换前)  
        enc_outputs = self.pos_ffn(enc_outputs)  
        # enc_outputs: [batch_size, src_len, d_model]  
        return enc_outputs, attn

其中self.enc_self_attn是计算多头自注意力,这里放一张图,对照着看。
在这里插入图片描述
代码如下:

class MultiHeadAttention(nn.Module):  
    """这个Attention类可以实现:  
    Encoder的Self-Attention  
    Decoder的Masked Self-Attention  
    Encoder-Decoder的Attention  
    输入:seq_len x d_model  
    输出:seq_len x d_model  
    """  
    def __init__(self):  
        super(MultiHeadAttention, self).__init__()  
        self.W_Q = nn.Linear(d_model, d_k * n_heads,  
                             bias=False)  # q,k必须维度相同,不然无法做点积
        self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)  
        self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)  
        # 这个全连接层可以保证多头attention的输出仍然是seq_len x d_model
        self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)  
  
    def forward(self, input_Q, input_K, input_V, attn_mask):  
        """  
        input_Q: [batch_size, len_q, d_model]        input_K: [batch_size, len_k, d_model]        input_V: [batch_size, len_v(=len_k), d_model]        attn_mask: [batch_size, seq_len, seq_len]        """        
        residual, batch_size = input_Q, input_Q.size(0)  
        # 下面的多头的参数矩阵是放在一起做线性变换的,然后再拆成多个头,这是工程实现的技巧  
        # B: batch_size, S:seq_len, D: dim  
        # (B, S, D) -proj-> (B, S, D_new) -split-> (B, S, Head, W) -trans-> (B, Head, S, W)   #线性变换拆成多头  
  
        # Q: [batch_size, n_heads, len_q, d_k]  
        Q = self.W_Q(input_Q).view(batch_size, -1,  
                                   n_heads, d_k).transpose(1, 2)  
        # K: [batch_size, n_heads, len_k, d_k] # K和V的长度一定相同,维度可以不同  
        K = self.W_K(input_K).view(batch_size, -1,  
                                   n_heads, d_k).transpose(1, 2)  
        # V: [batch_size, n_heads, len_v(=len_k), d_v]  
        V = self.W_V(input_V).view(batch_size, -1,  
                                   n_heads, d_v).transpose(1, 2)  
  
        # 因为是多头,所以mask矩阵要扩充成4维的  
        # attn_mask: [batch_size, seq_len, seq_len] -> [batch_size, n_heads, seq_len, seq_len]  
        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)  
  
        # context: [batch_size, n_heads, len_q, d_v], attn: [batch_size, n_heads, len_q, len_k]  
        context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)  
        # 下面将不同头的输出向量拼接在一起  
        # context: [batch_size, n_heads, len_q, d_v] -> [batch_size, len_q, n_heads * d_v]  
        context = context.transpose(1, 2).reshape(  
            batch_size, -1, n_heads * d_v)  
  
        # 这个全连接层可以保证多头attention的输出仍然是seq_len x d_model  
        output = self.fc(context)  # [batch_size, len_q, d_model]  
        return nn.LayerNorm(d_model).to(device)(output + residual), attn

可以看到整个MultiHeadAttention包括四个Linear层,分别是Q、K和V的投影层和最后的FFN。但是在工程实现的角度,并不是从 d m o d e l d_{model} dmodel投影到 d m o d e l h e a d \frac{d_{model}}{head} headdmodel维度,而是先做了线性变换 d m o d e l → d m o d e l d_{model} \to d_{model} dmodeldmodel,然后再拆开成 h e a d = 8 head=8 head=8个头。
( B , S , D ) − p r o j − > ( B , S , D n e w ) − s p l i t − > ( B , S , H e a d , W ) − t r a n s − > ( B , H e a d , S , W ) (B, S, D) -proj-> (B, S, D_{new}) -split-> (B, S, Head, W) -trans-> (B, Head, S, W) (B,S,D)proj>(B,S,Dnew)split>(B,S,Head,W)trans>(B,Head,S,W)
实际的代码就是,先将input_Q、input_K和input_V通过对应的Linear层,然后view(batch_size, -1, n_heads, d_k)变成一个 [ B , S , H e a d , d k ] [B, S, Head, d_{k}] [B,S,Head,dk],再转置transpose(1, 2) [ B , H e a d , S , d k ] [B, Head, S, d_{k}] [B,Head,S,dk]
由于采用多头注意力,因此需要将mask进行维度扩展,其实就是对mask矩阵在每个头上都进行一次拓展。

attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) 

随后得到的这个attn_mask和QKV一起送入注意力计算层中,以下是注意力计算代码:

class ScaledDotProductAttention(nn.Module):  
    def __init__(self):  
        super(ScaledDotProductAttention, self).__init__()  
  
    def forward(self, Q, K, V, attn_mask):  
        """  
        Q: [batch_size, n_heads, len_q, d_k]        K: [batch_size, n_heads, len_k, d_k]        V: [batch_size, n_heads, len_v(=len_k), d_v]        attn_mask: [batch_size, n_heads, seq_len, seq_len]        说明:在encoder-decoder的Attention层中len_q(q1,..qt)和len_k(k1,...km)可能不同  
        """        
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)  # scores : [batch_size, n_heads, len_q, len_k]  
        # mask矩阵填充scores(用-1e9填充scores中与attn_mask中值为1位置相对应的元素)  
        # Fills elements of self tensor with value where mask is True.  
        scores.masked_fill_(attn_mask, -1e9)  
        attn = nn.Softmax(dim=-1)(scores)  # 对最后一个维度(v)做softmax  
        # scores : [batch_size, n_heads, len_q, len_k] * V: [batch_size, n_heads, len_v(=len_k), d_v]        # context: [batch_size, n_heads, len_q, d_v]        
        context = torch.matmul(attn, V)  
        # context:[[z1,z2,...],[...]]向量, attn注意力稀疏矩阵(用于可视化的)  
        return context, attn

我们知道Transformer中的注意力计算方法是 Attention ( Q , K , V ) = softmax ( Q K T d k ) V \begin{aligned}\text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V\end{aligned} Attention(Q,K,V)=softmax(dk QKT)V,对应的代码部分:

 scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)

首先将K的最后两个维度进行转置,然后和Q做矩阵点乘,本文的Q、K的初始维度都是 [ 2 , 8 , 8 , 64 ] [2, 8, 8, 64] [2,8,8,64],经过转置点乘后得到的数据大小为 [ 2 , 8 , 8 , 8 ] [2,8, 8, 8] [2,8,8,8],并除一个 d k \sqrt{d_k} dk 进行尺度缩放,随后将需要mask的位置替换为极小值1e-9,并通过一个softmax层,转换为每个词的重要性权重,这个权重最终需要和Value进行加权:

context = torch.matmul(attn, V)  

这就是最终经过一层Attention得到的特征,后续需要将8个头的特征全部拼接起来:

context = context.transpose(1, 2).reshape(batch_size, -1, n_heads * d_v)

得到的就是又是一个512维的向量,随后还需要通过一个FC网络(MultiHeadAttention的第48行代码),这个网络一方面保证多头注意力的输出仍然是 d m o d e l = 512 d_{model} = 512 dmodel=512,也是将特征再次投影到一个更适配模型任务的特征空间中。(很玄学…)
注意MultiHeadAttention的第21行代码

residual, batch_size = input_Q, input_Q.size(0)  

这里的residual是为了后续的残差连接做准备的,就是在第49行代码:

return nn.LayerNorm(d_model).to(device)(output + residual), attn

在这里插入图片描述
这里对应的就是Add & Norm这个操作,这里的norm采用Layer Normalization方法,至此就完成了Encoder中一层Block操作。
对于Decoder,先放一张图,便于和代码对接。可以看到Decoder的输入首先需要经过一层Embedding进行嵌入,随后加入Positional Encoding的位置编码,随后通过一个Masked多头自注意力,并通过Add & Norm后和Encoder的输入进行交叉注意力计算,最后再通过FNN等即可。
在这里插入图片描述

现在看一下对于Decoder部分的代码实现:

class Decoder(nn.Module):  
    def __init__(self):  
        super(Decoder, self).__init__()  
        self.tgt_emb = nn.Embedding(  
            tgt_vocab_size, d_model)  # Decoder输入的embed词表  
        self.pos_emb = PositionalEncoding(d_model)  
        self.layers = nn.ModuleList([DecoderLayer()  
                                     for _ in range(n_layers)])  # Decoder的blocks  
  
    def forward(self, dec_inputs, enc_inputs, enc_outputs):  
        """  
        dec_inputs: [batch_size, tgt_len]        enc_inputs: [batch_size, src_len]        enc_outputs: [batch_size, src_len, d_model]   # 用在Encoder-Decoder Attention层  
        """       
        dec_outputs = self.tgt_emb(  
            dec_inputs)  # [batch_size, tgt_len, d_model]  
        dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1).to(  
            device)  # [batch_size, tgt_len, d_model]  
        # Decoder输入序列的pad mask矩阵(这个例子中decoder是没有加pad的,实际应用中都是有pad填充的)  
        dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).to(  
            device)  # [batch_size, tgt_len, tgt_len]  
        # Masked Self_Attention:当前时刻是看不到未来的信息的  
        dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).to(  
            device)  # [batch_size, tgt_len, tgt_len]  
  
        # Decoder中把两种mask矩阵相加(既屏蔽了pad的信息,也屏蔽了未来时刻的信息)  
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask),  
                                      0).to(device)  # [batch_size, tgt_len, tgt_len]; torch.gt比较两个矩阵的元素,大于则返回1,否则返回0  
  
        # 这个mask主要用于encoder-decoder attention层  
        # get_attn_pad_mask主要是enc_inputs的pad mask矩阵(因为enc是处理K,V的,求Attention时是用v1,v2,..vm去加权的,要把pad对应的v_i的相关系数设为0,这样注意力就不会关注pad向量)  
        #                       dec_inputs只是提供expand的size的  
        dec_enc_attn_mask = get_attn_pad_mask(  
            dec_inputs, enc_inputs)  # [batc_size, tgt_len, src_len]  
  
        dec_self_attns, dec_enc_attns = [], []  
        for layer in self.layers:  
            # dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]  
            # Decoder的Block是上一个Block的输出dec_outputs(变化)和Encoder网络的输出enc_outputs(固定)  
            dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,  
                                                             dec_enc_attn_mask)  
            dec_self_attns.append(dec_self_attn)  
            dec_enc_attns.append(dec_enc_attn)  
        # dec_outputs: [batch_size, tgt_len, d_model]  
        return dec_outputs, dec_self_attns, dec_enc_attns

可以看到,dec_inputs是最开始的句子,随后通过一个Embedding层将dec_inputs编码为512维度的特征嵌入,得到dec_outputs,这里的命名也是为了方便后面的残差连接。

dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).to(device)

这里的pad_mask其实和Encoder中的pad_mask意义是一样的,也是掩蔽掉无效长度。随后就是Decoder中的Masked_Attention部分,让当前时刻看不到未来的信息。以下是函数的实现部分:

def get_attn_subsequence_mask(seq):  
    """建议打印出来看看是什么的输出(一目了然)  
    seq: [batch_size, tgt_len]    
    """    
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]  
    # attn_shape: [batch_size, tgt_len, tgt_len]  
    subsequence_mask = np.triu(np.ones(attn_shape), k=1)  # 生成一个上三角矩阵
    subsequence_mask = torch.from_numpy(subsequence_mask).byte()  
    return subsequence_mask  # [batch_size, tgt_len, tgt_len]

注意这里的第7行代码,既然要让当前时刻看不到未来的信息,这里的代码采用一个上三角矩阵来表示这种关系:
在这里插入图片描述
可以看到,第 i i i行代表当前看到的是第 i i i个词,此时除了前 i i i个位置之外,其余列均为1,表示需要mask。随后代码将两个mask矩阵相加,既屏蔽了pad_mask的信息,也屏蔽了未来时刻的信息。

dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0).to(device)

torch.gt的作用是比较两个矩阵的元素,大于则返回1,否则返回0。
随后代码中又计算了一次pad_attn,这个pad_attn是用于encoder-decoder的Attention层(交叉注意力层)。所以输入此时就为dec_inputs和enc_inputs。

dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)

至此,Decoder全部的数据准备工作就都结束了,下面正式是Decoder Layer的部分:

for layer in self.layers:
    # dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]  
    # Decoder的Block是上一个Block的输出dec_outputs(变化)和Encoder网络的输出enc_outputs(固定)  
    dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,  
                                                             dec_enc_attn_mask)  
    dec_self_attns.append(dec_self_attn)  
    dec_enc_attns.append(dec_enc_attn)  
    # dec_outputs: [batch_size, tgt_len, d_model]  

Decoder的输入包括enc_inputs和dec_outputs,enc_inputs本身是的固定的,dec_outputs则是不断变化的。我们还是按照一层Decoder Layer来举例:

class DecoderLayer(nn.Module):  
    def __init__(self):  
        super(DecoderLayer, self).__init__()  
        self.dec_self_attn = MultiHeadAttention()  
        self.dec_enc_attn = MultiHeadAttention()  
        self.pos_ffn = PoswiseFeedForwardNet()  
  
    def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):  
        """  
        dec_inputs: [batch_size, tgt_len, d_model]        
        enc_outputs: [batch_size, src_len, d_model]        
        dec_self_attn_mask: [batch_size, tgt_len, tgt_len]        
        dec_enc_attn_mask: [batch_size, tgt_len, src_len]        """        
	    # dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len]  
	    dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs,  
                                                        dec_self_attn_mask)  # 这里的Q,K,V全是Decoder自己的输入  
        # dec_outputs: [batch_size, tgt_len, d_model], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]  
        dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs,  
                                                      dec_enc_attn_mask)  # Attention层的Q(来自decoder) 和 K,V(来自encoder)  
        # [batch_size, tgt_len, d_model]        
        dec_outputs = self.pos_ffn(dec_outputs)  
        # dec_self_attn, dec_enc_attn这两个是为了可视化的  
        return dec_outputs, dec_self_attn, dec_enc_attn

首先第一层Attention是对于Decoder输入的自注意力层计算,dec_outputs和enc_outputs一起,经过一次交叉注意力计算,其中dec_outputs作为Q,enc_outputs作为K和V,最后通过一个FFN即可完成整个Decoder的计算过程。
Decoder的Attention Layers输出的dec_outputs最后需要通过一个线性层,这个线性层将 d m o d e l d_{model} dmodel投影到tgt_vocab维度上,这个tgt_vocab是指输出的词字典中有多少个词,后续通过一个Softmax即可得到每个词的概率。

dec_logits = self.projection(dec_outputs)  
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns

在这里插入图片描述
输出的概率会和decoder的输入进行损失计算(Transformer用的是交叉熵),至此就完成了一次训练过程。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
李宏毅是一位著名的机器学习和深度学习专家,他在教学视频中也提到了Transformer模型。下面是一些关于李宏毅关于Transformer笔记总结: 1. Transformer 是一种基于注意力机制(attention mechanism)的序列到序列(sequence-to-sequence)模型。它在自然语言处理任务中取得了很大的成功。 2. Transformer 模型的核心思想是完全摒弃了传统的循环神经网络(RNN)结构,而是采用了自注意力机制(self-attention mechanism)来建模输入序列之间的依赖关系。 3. 自注意力机制能够将输入序列中的每个位置与其他位置建立联系,从而捕捉到全局上下文的信息。它能够解决传统的RNN模型在处理长序列时的梯度消失和梯度爆炸问题。 4. Transformer 模型由编码器(Encoder)和解码器(Decoder)两部分组成。编码器负责将输入序列表示为高维向量,解码器则根据编码器的输出生成目标序列。 5. 编码器和解码器由多个层堆叠而成,每一层都包含了多头自注意力机制和前馈神经网络。多头自注意力机制可以并行地学习输入序列中不同位置之间的关系。 6. Transformer 模型还引入了残差连接(residual connection)和层归一化(layer normalization)来帮助模型更好地进行训练和优化。 这些是李宏毅关于Transformer的一些主要笔记总结,希望对你有所帮助。注意,这些总结仅代表了我对李宏毅在其教学视频中所讲述内容的理解,如有误差请以李宏毅本人的观点为准。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CUCKyrie

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

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

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

打赏作者

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

抵扣说明:

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

余额充值