MENU

Transformer的PyTorch实现

July 14, 2020 • Read: 13236 • Deep Learning阅读设置

B站视频讲解

本文主要介绍一下如何使用 PyTorch 复现 Transformer,实现简单的机器翻译任务。请先花上 15 分钟阅读我的这篇文章 Transformer详解,再来看本文,方能达到醍醐灌顶,事半功倍的效果

数据预处理

这里我并没有用什么大型的数据集,而是手动输入了两对德语→英语的句子,还有每个字的索引也是我手动硬编码上去的,主要是为了降低代码阅读难度,我希望读者能更关注模型实现的部分

import math
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data

# S: Symbol that shows starting of decoding input
# E: Symbol that shows starting of decoding output
# P: Symbol that will fill in blank sequence if current batch data size is short than time steps
sentences = [
        # enc_input           dec_input         dec_output
        ['ich mochte ein bier P', 'S i want a beer .', 'i want a beer . E'],
        ['ich mochte ein cola P', 'S i want a coke .', 'i want a coke . E']
]

# Padding Should be Zero
src_vocab = {'P' : 0, 'ich' : 1, 'mochte' : 2, 'ein' : 3, 'bier' : 4, 'cola' : 5}
src_vocab_size = len(src_vocab)

tgt_vocab = {'P' : 0, 'i' : 1, 'want' : 2, 'a' : 3, 'beer' : 4, 'coke' : 5, 'S' : 6, 'E' : 7, '.' : 8}
idx2word = {i: w for i, w in enumerate(tgt_vocab)}
tgt_vocab_size = len(tgt_vocab)

src_len = 5 # enc_input max sequence length
tgt_len = 6 # dec_input(=dec_output) max sequence length

def make_data(sentences):
    enc_inputs, dec_inputs, dec_outputs = [], [], []
    for i in range(len(sentences)):
      enc_input = [[src_vocab[n] for n in sentences[i][0].split()]] # [[1, 2, 3, 4, 0], [1, 2, 3, 5, 0]]
      dec_input = [[tgt_vocab[n] for n in sentences[i][1].split()]] # [[6, 1, 2, 3, 4, 8], [6, 1, 2, 3, 5, 8]]
      dec_output = [[tgt_vocab[n] for n in sentences[i][2].split()]] # [[1, 2, 3, 4, 8, 7], [1, 2, 3, 5, 8, 7]]

      enc_inputs.extend(enc_input)
      dec_inputs.extend(dec_input)
      dec_outputs.extend(dec_output)

    return torch.LongTensor(enc_inputs), torch.LongTensor(dec_inputs), torch.LongTensor(dec_outputs)

enc_inputs, dec_inputs, dec_outputs = make_data(sentences)

class MyDataSet(Data.Dataset):
  def __init__(self, enc_inputs, dec_inputs, dec_outputs):
    super(MyDataSet, self).__init__()
    self.enc_inputs = enc_inputs
    self.dec_inputs = dec_inputs
    self.dec_outputs = dec_outputs
  
  def __len__(self):
    return self.enc_inputs.shape[0]
  
  def __getitem__(self, idx):
    return self.enc_inputs[idx], self.dec_inputs[idx], self.dec_outputs[idx]

loader = Data.DataLoader(MyDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)

模型参数

下面变量代表的含义依次是

  1. 字嵌入&位置嵌入的维度,这俩值是相同的,因此用一个变量就行了
  2. FeedForward层隐藏神经元个数
  3. Q、K、V向量的维度,其中Q与K的维度必须相等,V的维度没有限制,不过为了方便起见,我都设为64
  4. Encoder和Decoder的个数
  5. 多头注意力中head的数量
# Transformer Parameters
d_model = 512  # Embedding Size
d_ff = 2048 # FeedForward dimension
d_k = d_v = 64  # dimension of K(=Q), V
n_layers = 6  # number of Encoder of Decoder Layer
n_heads = 8  # number of heads in Multi-Head Attention

上面都比较简单,下面开始涉及到模型就比较复杂了,因此我会将模型拆分成以下几个部分进行讲解

  • Positional Encoding
  • Pad Mask(针对句子不够长,加了pad,因此需要对pad进行mask)
  • Subsequence Mask(Decoder input不能看到未来时刻单词信息,因此需要mask)
  • ScaledDotProductAttention(计算context vector)
  • Multi-Head Attention
  • FeedForward Layer
  • Encoder Layer
  • Encoder
  • Decoder Layer
  • Decoder
  • Transformer

关于代码中的注释,如果值为src_len或者tgt_len的,我一定会写清楚,但是有些函数或者类,Encoder和Decoder都有可能调用,因此就不能确定究竟是src_len还是tgt_len,对于不确定的,我会记作seq_len

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)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / 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), :]
        return self.dropout(x)

Pad Mask

def get_attn_pad_mask(seq_q, seq_k):
    '''
    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()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # [batch_size, 1, len_k], False is masked
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # [batch_size, len_q, len_k]

由于在Encoder和Decoder中都需要进行mask操作,因此就无法确定这个函数的参数中seq_len的值,如果是在Encoder中调用的,seq_len就等于src_len;如果是在Decoder中调用的,seq_len就有可能等于src_len,也有可能等于tgt_len(因为Decoder有两次mask)

这个函数最核心的一句代码是seq_k.data.eq(0),这句的作用是返回一个大小和seq_k一样的tensor,只不过里面的值只有True和False。如果seq_k某个位置的值等于0,那么对应位置就是True,否则即为False。举个例子,输入为seq_data = [1, 2, 3, 4, 0]seq_data.data.eq(0)就会返回[False, False, False, False, True]

剩下的代码主要是扩展维度,强烈建议读者打印出来,看看最终返回的数据是什么样子

Subsequence Mask

def get_attn_subsequence_mask(seq):
    '''
    seq: [batch_size, tgt_len]
    '''
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
    subsequence_mask = np.triu(np.ones(attn_shape), k=1) # Upper triangular matrix
    subsequence_mask = torch.from_numpy(subsequence_mask).byte()
    return subsequence_mask # [batch_size, tgt_len, tgt_len]

Subsequence Mask只有Decoder会用到,主要作用是屏蔽未来时刻单词的信息。首先通过np.ones()生成一个全1的方阵,然后通过np.triu()生成一个上三角矩阵,下图是np.triu()用法

ScaledDotProductAttention

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]
        '''
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size, n_heads, len_q, len_k]
        scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is True.
        
        attn = nn.Softmax(dim=-1)(scores)
        context = torch.matmul(attn, V) # [batch_size, n_heads, len_q, d_v]
        return context, attn

这里要做的是,通过QK计算出scores,然后将scoresV相乘,得到每个单词的context vector

第一步是将QK的转置相乘没什么好说的,相乘之后得到的scores还不能立刻进行softmax,需要和attn_mask相加,把一些需要屏蔽的信息屏蔽掉,attn_mask是一个仅由True和False组成的tensor,并且一定会保证attn_maskscores的维度四个值相同(不然无法做对应位置相加)

mask完了之后,就可以对scores进行softmax了。然后再与V相乘,得到context

MultiHeadAttention

class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)
        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)
        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, S, D) -proj-> (B, S, D_new) -split-> (B, S, H, W) -trans-> (B, H, S, W)
        Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # Q: [batch_size, n_heads, len_q, d_k]
        K = self.W_K(input_K).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # K: [batch_size, n_heads, len_k, d_k]
        V = self.W_V(input_V).view(batch_size, -1, n_heads, d_v).transpose(1,2)  # V: [batch_size, n_heads, len_v(=len_k), d_v]

        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size, n_heads, seq_len, seq_len]

        # 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 = context.transpose(1, 2).reshape(batch_size, -1, n_heads * d_v) # context: [batch_size, len_q, n_heads * d_v]
        output = self.fc(context) # [batch_size, len_q, d_model]
        return nn.LayerNorm(d_model).cuda()(output + residual), attn

完整代码中一定会有三处地方调用MultiHeadAttention(),Encoder Layer调用一次,传入的input_Qinput_Kinput_V全部都是enc_inputs;Decoder Layer中两次调用,第一次传入的全是dec_inputs,第二次传入的分别是dec_outputsenc_outputsenc_outputs

FeedForward Layer

class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(d_model, d_ff, bias=False),
            nn.ReLU(),
            nn.Linear(d_ff, d_model, bias=False)
        )
    def forward(self, inputs):
        '''
        inputs: [batch_size, seq_len, d_model]
        '''
        residual = inputs
        output = self.fc(inputs)
        return nn.LayerNorm(d_model).cuda()(output + residual) # [batch_size, seq_len, d_model]

这段代码非常简单,就是做两次线性变换,残差连接后再跟一个Layer Norm

Encoder Layer

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):
        '''
        enc_inputs: [batch_size, src_len, d_model]
        enc_self_attn_mask: [batch_size, src_len, src_len]
        '''
        # enc_outputs: [batch_size, src_len, d_model], attn: [batch_size, n_heads, src_len, src_len]
        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

将上述组件拼起来,就是一个完整的Encoder Layer

Encoder

class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.src_emb = nn.Embedding(src_vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model)
        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]
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) # [batch_size, src_len, src_len]
        enc_self_attns = []
        for layer in self.layers:
            # 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_self_attns.append(enc_self_attn)
        return enc_outputs, enc_self_attns

使用nn.ModuleList()里面的参数是列表,列表里面存了n_layers个Encoder Layer

由于我们控制好了Encoder Layer的输入和输出维度相同,所以可以直接用个for循环以嵌套的方式,将上一次Encoder Layer的输出作为下一次Encoder Layer的输入

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)
        # 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)
        dec_outputs = self.pos_ffn(dec_outputs) # [batch_size, tgt_len, d_model]
        return dec_outputs, dec_self_attn, dec_enc_attn

在Decoder Layer中会调用两次MultiHeadAttention,第一次是计算Decoder Input的self-attention,得到输出dec_outputs。然后将dec_outputs作为生成Q的元素,enc_outputs作为生成K和V的元素,再调用一次MultiHeadAttention,得到的是Encoder和Decoder Layer之间的context vector。最后将dec_outptus做一次维度变换,然后返回

Decoder

class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

    def forward(self, dec_inputs, enc_inputs, enc_outputs):
        '''
        dec_inputs: [batch_size, tgt_len]
        enc_intpus: [batch_size, src_len]
        enc_outputs: [batsh_size, src_len, d_model]
        '''
        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).cuda() # [batch_size, tgt_len, d_model]
        dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).cuda() # [batch_size, tgt_len, tgt_len]
        dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).cuda() # [batch_size, tgt_len, tgt_len]
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0).cuda() # [batch_size, tgt_len, tgt_len]

        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]
            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)
        return dec_outputs, dec_self_attns, dec_enc_attns

Decoder中不仅要把"pad"mask掉,还要mask未来时刻的信息,因此就有了下面这三行代码,其中torch.gt(a, value)的意思是,将a中各个位置上的元素和value比较,若大于value,则该位置取1,否则取0

dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs) # [batch_size, tgt_len, tgt_len]
        dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs) # [batch_size, tgt_len, tgt_len]
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0) # [batch_size, tgt_len, tgt_len]

Transformer

class Transformer(nn.Module):
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder = Encoder().cuda()
        self.decoder = Decoder().cuda()
        self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False).cuda()
    def forward(self, enc_inputs, dec_inputs):
        '''
        enc_inputs: [batch_size, src_len]
        dec_inputs: [batch_size, tgt_len]
        '''
        # tensor to store decoder outputs
        # outputs = torch.zeros(batch_size, tgt_len, tgt_vocab_size).to(self.device)
        
        # enc_outputs: [batch_size, src_len, d_model], enc_self_attns: [n_layers, batch_size, n_heads, src_len, src_len]
        enc_outputs, enc_self_attns = self.encoder(enc_inputs)
        # dec_outpus: [batch_size, tgt_len, d_model], dec_self_attns: [n_layers, batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [n_layers, batch_size, tgt_len, src_len]
        dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)
        dec_logits = self.projection(dec_outputs) # dec_logits: [batch_size, tgt_len, tgt_vocab_size]
        return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns

Transformer主要就是调用Encoder和Decoder。最后返回dec_logits的维度是[batch_size * tgt_len, tgt_vocab_size],可以理解为,一个句子,这个句子有batch_size*tgt_len个单词,每个单词有tgt_vocab_size种情况,取概率最大者

模型&损失函数&优化器

model = Transformer().cuda()
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)

这里的损失函数里面我设置了一个参数ignore_index=0,因为"pad"这个单词的索引为0,这样设置以后,就不会计算"pad"的损失(因为本来"pad"也没有意义,不需要计算),关于这个参数更详细的说明,可以看我这篇文章的最下面,稍微提了一下

训练

for epoch in range(30):
    for enc_inputs, dec_inputs, dec_outputs in loader:
      '''
      enc_inputs: [batch_size, src_len]
      dec_inputs: [batch_size, tgt_len]
      dec_outputs: [batch_size, tgt_len]
      '''
      enc_inputs, dec_inputs, dec_outputs = enc_inputs.cuda(), dec_inputs.cuda(), dec_outputs.cuda()
      # outputs: [batch_size * tgt_len, tgt_vocab_size]
      outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
      loss = criterion(outputs, dec_outputs.view(-1))
      print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))

      optimizer.zero_grad()
      loss.backward()
      optimizer.step()

测试

def greedy_decoder(model, enc_input, start_symbol):
    """
    For simplicity, a Greedy Decoder is Beam search when K=1. This is necessary for inference as we don't know the
    target sequence input. Therefore we try to generate the target input word by word, then feed it into the transformer.
    Starting Reference: http://nlp.seas.harvard.edu/2018/04/03/attention.html#greedy-decoding
    :param model: Transformer Model
    :param enc_input: The encoder input
    :param start_symbol: The start symbol. In this example it is 'S' which corresponds to index 4
    :return: The target input
    """
    enc_outputs, enc_self_attns = model.encoder(enc_input)
    dec_input = torch.zeros(1, tgt_len).type_as(enc_input.data)
    next_symbol = start_symbol
    for i in range(0, tgt_len):
        dec_input[0][i] = next_symbol
        dec_outputs, _, _ = model.decoder(dec_input, enc_input, enc_outputs)
        projected = model.projection(dec_outputs)
        prob = projected.squeeze(0).max(dim=-1, keepdim=False)[1]
        next_word = prob.data[i]
        next_symbol = next_word.item()
    return dec_input

# Test
enc_inputs, _, _ = next(iter(loader))
greedy_dec_input = greedy_decoder(model, enc_inputs[0].view(1, -1).cuda(), start_symbol=tgt_vocab["S"])
predict, _, _, _ = model(enc_inputs[0].view(1, -1).cuda(), greedy_dec_input)
predict = predict.data.max(1, keepdim=True)[1]
print(enc_inputs[0], '->', [idx2word[n.item()] for n in predict.squeeze()])

最后给出完整代码链接(需要科学的力量)
Github 项目地址:nlp-tutorial

Last Modified: April 29, 2021
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment

80 Comments
  1. ash-bin ash-bin

    好家伙,要我看看大佬又在学啥~

    1. mathor mathor

      @ash-bin老板的任务罢了#(尴尬)

  2. ze'e ze'e

    请问在最后测试那里,贪心搜索要改变哪个参数才能变成beam search ?
    我在github上下了您的代码,那里我就改不成

    1. mathor mathor

      @ze'e我亲自测试过,将最后测试代码那里,改为以下代码即可
      predict, , _, = model(enc_inputs[0].view(1, -1), greedy_dec_input)

    2. zee zee

      @mathor嗯嗯,我看见了那段代码,我理解的是那段代码将贪心搜索带到了decoder,我想看到的是beam search中排序那个概率,并选出最好的那个,如你代码,若我想K=3,那么我该改哪个参数,或者怎么改?谢谢。

    3. 荸荠 荸荠

      @zee这里显然没有现成的参数可以改

  3. 孙行者 孙行者

    同研0相见恨晚啊 博主好强

    1. mathor mathor

      @孙行者一般一般,菲律宾第三,hh

  4. 孙行者 孙行者

    刚刚发现博主的宝藏 打赏了一下下嘻嘻

    1. mathor mathor

      @孙行者我看到了,正纳闷是谁呢,感谢支持

  5. 孙行者 孙行者

    大佬可以加个联系方式有机会交流吗 不可以的话就自动忽略哈哈@(笑眼)

  6. q q

    Padding Mask部分的代码是不是有问题,因为例如带有一个padding的句子,经过selfAttention后得到的scores矩阵的最后一行和最后一列应该为0,对应的mask矩阵的最后一行和一列因该为True可是我测试的Padding Mask部分的代码的输出和上述不符,当然有可能我理解的也有问题,请大佬指出,谢谢

    1. mathor mathor

      @q你测试出的应该是只有最后一列为True,最后一行不为True吧,这是正常的,因为最后一行是否Mask并不重要。重要的是保证最后一列Mask就行,因为我们不希望每个正常词被pad这个词吸引了一部分注意力

      最后一行表示的是pad这个词关于其他词的注意力,由于pad这个词本身没有任何意义,所以可以不管它

      当然您要是想最后一行也mask,也是可以的

    2. q q

      @mathor非常感谢,解惑了

    3. vip vip

      @qpadding mask其实我也一直有疑问,我看过tf的对于transformer的实现,也只是对于列进行了mask,而padding id那一行没有进行mask,那么softmax以后应该最后一行是有值的,再乘以V以后,padding id的那一行也有了当前这句话的全部信息

    4. mathor mathor

      @vip是这样的,可以这么理解

      最后一行,不管mask与否,对结果的影响都不大

      假如mask了最后一行,实际上就是少"吃"一点Value矩阵的信息
      假如不mask最后一行,实际上就是多"吃"一点Value矩阵的信息

      最后一列实在是太重要了,因此必须mask。相比之下,最后一行没有那么重要,所以随意

    5. vip vip

      @mathor嗯嗯,非常感谢大佬

  7. nodg nodg

    测试时把dec_inputs传给模型,不是作弊吗?

    1. mathor mathor

      @nodg不好意思,我已修改测试代码,之前的确实有问题

    2. nodg nodg

      @mathor我也改了下,然后发现和你一模一样@(太开心)

  8. haoxuan haoxuan

    博主您好,我想问一下 position embedding 那里为什么PE的第一维是 vocab_size,我觉得应该是 seq_len 呀。位置编码是提供词在句子中的位置信息的,为什么这里的第一维是词库大小呢?
    谢谢!@(太开心)

    1. mathor mathor

      @haoxuan我确实写的有问题,已修改,感谢指正

    2. 鸭鸭 鸭鸭

      @mathor博主,我之前的代码是按照你原来的demo写的,是不是在get_sinusoid_encoding_table这个函数修正vocab_size成seq_len就可以了?

    3. mathor mathor

      @鸭鸭我觉得应该不是,https://www.bilibili.com/video/BV1mk4y1q7eK?p=3
      从02:36秒开始,你看一下,这是我理解的

    4. 鸭鸭 鸭鸭

      @mathor博主我觉得你新的理解是正确的,但是我想问一下,在encoder部分代码里,forward函数传入pos_emb的enc_outputs为什么要transpose两次(0,1),这样有什么意义吗

    5. mathor mathor

      @鸭鸭还没传入pos_emb时,enc_outputs的维度是[batch_size, seq_len, d_model],为了在PositionalEncoding里运算不报错,需要将输入参数的维度变成[seq_len, batch_size, d_model]

      而传出来的维度我需要的是[batch_size, seq_len, d_model],因此做了两次transpose

    6. 鸭鸭 鸭鸭

      @mathor谢谢博主

    7. Mikasa Mikasa

      @mathor在class PositionalEncoding中,__init__里的pe = pe.unsqueeze(1)改成pe = pe.unsqueeze(0),forward里的x = x + self.pe[:x.size(0), :]改成x = x + self.pe[:, :x.size(1), :],然后encoder里就不做transpose(0,1),这样是不是也可以?
      我试了一下,好像没啥问题。

    8. mathor mathor

      @Mikasa如果您试了没问题就行,具体改法太多了,我不可能一个一个去试

    9. Mikasa Mikasa

      @mathor好的,谢谢博主!

  9. zane zane

    博主您好,请问在Decoder中的encoder-decoder-attention的过程中进行了pad_mask该怎么理解呢,这里Q是由Decoder中的masked-self-attention输入的,K和V是Encoder的输出,已经不存在pad的问题了吧,有点想不明白,还望指点。

    1. mathor mathor

      @zane我这么解释你可能会好理解一些。首先我们达成一个共识,decoder 中 pad mask 的作用是为了避免获取后面时刻 词的信息。您再仔细想一想,为了避免获取后面时刻词的信息,我们做的工作就是使后面时刻的向量加上一个 mask,无论是使得它的值变为负无穷也好,0 也好,总之就是要屏蔽掉后面时刻的信息

      假设现在 decoder 是时刻 t,那 decoder layer 中,应该保证从下到上整个layer的运算过程中,所有 t 时刻以后时刻的向量信息都被 mask 掉

      那我现在反问您,decoder self-attention 要做 mask,为什么 encoder-decoder-attention 过程中不需要 mask 了呢?

    2. zane zane

      @mathor感谢您的回复,您的代码中pad_mask是为了避免为了保证句长一致而填充的单词的影响,subsequence_mask是为了避免看到未来的信息。在decoder的masked self-attention中同时用这两种mask我能理解。但是对在Decoder的第二块,即encoder-decoder-attention中使用pad_mask无法理解,这一部分的pad_mask是为了mask掉什么呢?。
      我的理解是,在decoder第一步的masked self-attention过程中已经进行了pad_mask+sequence_mask,即在注意力矩阵中,所有涉及pad和后续时刻信息的权重已经为0,那么使用这些权重对V求加权和之后,也已经不包含来自未来的信息。那么masked self-attention这一部分的计算结果,即传输到encoder-decoder-attention中作为Q的矩阵,再与Encoder的输出求注意力,也应该不再涉及mask吧。

    3. mathor mathor

      @zane是这样的,您看一下https://wmathor.com/index.php/archives/1438/这篇文章倒数第四张图

      在进行encode-decode-attention之前,是不是还做了一次add & norm,要命的就是这个add。由于进行了残差连接,使得原本为0的地方,又产生了值

      此时接下来又要进行一次attention(即encoder的输出和decoder进行的attention),如果这个时候,不重新mask一次,是不是未来时刻的信息仍然被用上了,因此要进行mask

      如果您还不理解,可以继续回复

    4. zane zane

      @mathor在理,哈哈,感谢!

  10. 啊啊啊 啊啊啊

    context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)

    楼主,这是multiheadattention中的代码,我不太懂为什么要这样用,而不是context, attn = ScaledDotProductAttention(Q, K, V, attn_mask)

    1. mathor mathor

      @啊啊啊因为ScaledDotProductAttention是个类
      ScaledDotProductAttention()就是初始化了一个对象,用java的思想来说,这其实是个匿名对象
      然后为了掉用这个对象的forward方法,因此要用ScaledDotProductAttention()(Q,K,V)

      或者换句话说,ScaledDotProductAttention(a, b, c)这样传入的参数,其实是赋值给了__init__()方法

    2. 啊啊啊 啊啊啊

      @mathor谢谢哈,楼主,还有一个问题就是FeedForward中的
      return nn.LayerNorm(d_model).cuda()(output + residual)
      也是相同的原理吗?我接触不太好,给您添麻烦了。

    3. mathor mathor

      @啊啊啊是的,d_model赋值给了__init__()方法,而output+residual赋值给了forward()方法

    4. 啊啊啊 啊啊啊

      @mathor谢谢您啦,楼主好人!

  11. syrincsu syrincsu

    大佬,小白有俩问题想问问

    pe = pe.unsqueeze(0).transpose(0, 1)和直接pe.unsqueeze(1)是不是没有区别呀,为啥要这样多写一步呀,还是只是习惯?positional encoding部分的forward: pe的shape应该[max_len, 1, dmodel], x的shape是[seq_len, batchsize, dmodel]对应位置的相加不应该是x+self.pe[:x.shape[0], 0, :]吗?还是我太菜想错了,请大佬指教一下,感激不尽
    1. 蚊香片 蚊香片

      @syrincsu同问!请大佬指教,谢谢

    2. cp980328 cp980328

      @syrincsu你好,看到你的问题,我想提出我的一些见解。
      问题一:我觉得pe = pe.unsqueeze (0).transpose (0, 1) 和 pe.unsqueeze (1) 确实是没有区别的,
      问题二:如果使用 x+self.pe [:x.shape [0], 0, :] ,那取出来的size() 将会将第二个维度取消掉,也就是将符合条件的维度进行拼接.当然你可以写成 x+self.pe [:x.shape [0], :, :]

  12. zhang zhang

    有一个地方不是很懂
    既然greedy_decoder()函数中,已经用<START>去decoder得到了后面的一个又一个单词,那么为什么不直接把这些单词拿去当最后的预测结果,而又要用这些作为输入,再过一道模型?
    如果是因为认为用<START>之后greedy_decoder的结果比较粗糙不够精确的话,那这样把粗糙的结果拿去再作为已经训练好的模型的输入不是会累积更多的误差吗?

    1. mathor mathor

      @zhang其实通过greedy_decoder就够了

  13. uc uc

    真不戳~@(haha)

  14. gq gq

    你好,在《attention is all you need》论文3.4小节中最后一句话是:
    In the embedding layers, we multiply those weights by √dmodel.
    是不是说在embedding中要乘以sqrt(d_model)?你好像并没有在embedding中乘以sqrt(d_model),请问为什么呢?

  15. theSun theSun

    请问一下Position Encoding那里,添加dropout有什么作用?@(疑问)

    1. yepper yepper

      @theSun好像是防止过拟合,会随机丢弃一部分参数

  16. cp cp

    你好,博主。在阅读你的position encoding部分代码时发现变量名为div_term的定义和原论文pe的定义方法不同,而且k/d的线性关系似乎颠倒了。下面是我的定义方式div_term = torch.tensor(10000.0).pow(-2*torch.arange(0, d_model, 2).float()/d_model)

  17. Tiger Tiger

    您好,请问一下Transformer()最终的outputs不是要用softmax取概率最大的word吗?我看您的结构最终进过projection成tgt_vocab_size维度后就结束了,是不是漏了softmax

    1. Tiger Tiger

      @Tiger噢噢,好像是nn.CrossEntropyLoss()里面算了?

    2. mathor mathor

      @Tiger是的

  18. shylock shylock

    请教下博主:使用greedy_decoder 函数已经解码出结果了,为什么还要再使用模型预测一次?

    1. shylock shylock

      @shylock即 greedy_dec_input 已经是通过自回归方式的到的结果,为什么还要将encoder 与 decoder输入送入到一次模型?
      Thx

    2. biu biu

      @shylock同问!如果在greedy_decoder函数中直接保存预测出来的next_symbol就是模型解码出的结果吧

    3. biu biu

      @biu请教一下博主~

    4. mathor mathor

      @biu是的,其实只用通过一次自回归就可以了

  19. 深恪 深恪

    博主的字位置嵌入类需要改一下,当d_moel是偶数的时候正常运行,当是基数的时候会出现bug。这里我做了以下简单的更改:
    div_term_sin = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))

    div_term_cos = torch.exp(torch.arange(1, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term_sin) pe[:, 1::2] = torch.cos(position * div_term_cos)
    1. mathor mathor

      @深恪您具体说一下bug是什么,是数值错误,还是说下标越界,或者说其它?

  20. idealcp idealcp

    博主你好,在使用greedy_decoder得到dec_input的这个过程和RNN是不是类似的,这里是不是并不是并行计算的?那么在每次训练的过程中都要先greedy_decoder一次?这样的话在decoder部分transformer的并行计算的优势是不是就小一些了?非常感谢!

  21. 啊啊啊 啊啊啊

    我起, 这代码一刀把我秒了#(狂汗)

  22. 黄学生 黄学生

    老师,您好,我在运行您的代码时,最后的loss如下错。
    Traceback (most recent call last):
    File "/home/admin/workspace/codeup.aliyun.com/5f2014eddf9df74e36afb9f9/repo_2020-09-03_2020090300092428/stockanalytics/transformer/trainer copy 1.py", line 313, in <module>

    loss = criterion(outputs, dec_outputs.view(-1))

    ······

    "Expected input batch_size ({}) to match target batch_size ({}).".format(input.size(0), target.size(0))

    ValueError: Expected input batch_size (14) to match target batch_size (16).

    1. 黄学生 黄学生

      @黄学生解决了

  23. ZylOo ZylOo

    博主太细心了,每次计算数据的形状都标注出来了,对我这种PyTorch初学者来说非常友好,十分感谢!代码逻辑非常清晰,命名也很容易理解,厉害!但我还是有一些建议和疑惑:1. Attention部分的实现似乎和原论文不一样。原文在ScaledDotProductAttention中只计算了单头的context,并在MultiHeadAttention中进行多头concatenate。您在ScaledDotProductAttention中已经扩展了一个维度,用来存放多头数据,在MultiHeadAttention中直接变形就可以了。这两种做法是等效的,但是我认为,按您的方法,这两个类合并成一个类会比较方便?2. 我初步接触PyTorch,有些写法不太理解。比如PE层的输入x,为什么不把batchsize放在第0维,导致Encoder里面要反复转置?还有MultiHeadAttention中这一句Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1,2),为什么不在view里一步到位,而是要经过转置?

    1. mathor mathor

      @ZylOo其实您这两个问题可以统一解答,写代码不是要追求精简,而是要让自己让别人好理解。您要是觉得我写的复杂了,可以自行修改的,没问题

    2. ZylOo ZylOo

      @mathor我没有觉得您写的复杂,我自己也是要追求容易理解。我只是认为,与原论文不同的是,您在ScaledDotProductAttention中已经体现出“多头”了,也许可以和MultiHeadAttention合并一下。总之感谢回复。

  24. zzjyg zzjyg

    博主终于让我明白了!!不过我还想问一下为什么Attention的输出最后不加relu()激活层,而是直接连接到后面的MLP模块?

    1. mathor mathor

      @zzjyg所有的疑问都可以归结为一句话:这样做效果更好,或者说,你那样做效果不好

  25. berlin berlin

    请问在 get_attn_pad_mask(seq_q, seq_k) 中,为什么只 seq_k 中哪些为 0,而不判断 seq_q 呢?当 seq_q 和 seq_k 分别来自 decoder 和 encoder 并长度不一时,不会出错吗?

    1. berlin berlin

      @berlin为什么只【判断】 seq_k 中哪些为 0。少打了两个字,提前感谢。

  26. 小白 小白

    大佬你好,你看看在Transformer中注释里dec_enc_attns 是不是应该是[n_layers,batch_size,n_heads,tgt_len,src_len]

  27. lb lb

    大佬您好,我想请问一下,创建数据时有encoder输入,decoder输入,decoder输出, 但是decoder输入和输出是一样的?这不是在做翻译吗,为什么decoder的输出我们可以直接从decoder的输入得到,decoder的输入到底是什么呢,求解谢谢您

  28. wahaha wahaha

    请问,求损失的时候,开始和结束标志位要放在里面一起算嘛

    1. mathor mathor

      @wahaha算进去对结果影响并不大

  29. scirocc scirocc

    您ScaledDotProductAttention类中返回的attn我没看懂是啥意思,后来encoder也没用到啊

    1. scirocc scirocc

      @sciroccMultiHeadAttention也返回了这个attn,EncoderLayer也返回这个attn,Encoder返回一系列attn的列表,Transformer返回attn的列表,然后您就在没用到..是否是您自己debug用的,这个attn我是不是可以删了?@(太开心)

    2. mathor mathor

      @sciroccattn是用来画图的,如果不画图可以不用

    3. scirocc scirocc

      @mathor@(大拇指)多谢

  30. dyc dyc

    get_attn_pad_mask这里为什么只有最后一列是True,这里后两维是len_q, len_k,所有最后一列和最后一行不都是代表pad,不应该最后一列和最后一行都是True吗