MENU

Transformer的PyTorch实现

July 14, 2020 • Read: 115139 • 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], True 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: [batch_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, 0).type_as(enc_input.data)
    terminal = False
    next_symbol = start_symbol
    while not terminal:         
        dec_input = torch.cat([dec_input.detach(),torch.tensor([[next_symbol]],dtype=enc_input.dtype).cuda()],-1)
        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[-1]
        next_symbol = next_word
        if next_symbol == tgt_vocab["."]:
            terminal = True
        print(next_word)            
    return dec_input

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

测试部分代码由网友qq2014提供,在此表示感谢

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

Last Modified: March 20, 2022
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment

180 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在理,哈哈,感谢!

    5. 一只楚楚猫 一只楚楚猫

      @zane博主,我觉得您关于encode-decode-attention中的mask解释的并不太对,就像您说的,虽然在进行 encode-decode-attention 之前还做了一次 add & norm,这样确实会导致将未来的信息加入到dec_outputs中,但是加入这个残差连接是为了避免神经网络层数过深而导致网络出现退化的问题。encode-decode-attention中的mask是为了避免Decoder中的Query被Encoder中的Key中的pad吸引过多的注意力(“吃”到Encoder中无用的信息)

  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)

    1. 123456 123456

      @cp一样的 博主的是 x=e的lnx 这样转换了一下

  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是什么,是数值错误,还是说下标越界,或者说其它?

    2. L L

      @深恪为什么我感觉你这个是公式看错了呢,公式里面用的是2i,也就是奇数偶数的情况下均为同一个,但是你这个里面却是奇数偶数分开计算了

  20. idealcp idealcp

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

    1. Moment Moment

      @idealcp训练的时候可以并行,不需要greedy_decoder

  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。少打了两个字,提前感谢。

    2. Moment Moment

      @berlin因为,我们只要对k,v对应的序列进行pad mask就行了(目的是在对value向量加权平均的时候,可以让pad对应的alpha_ij=0,这样注意力就不会考虑到pad向量),seq_q只是用来扩充维度的

  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的输入到底是什么呢,求解谢谢您

    1. 低调流年的微凉 低调流年的微凉

      @lb它这个是有监督的。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吗

    1. scirocc scirocc

      @dyc同问。。您搞明白了么。。我觉得最后一行确实应该全是mask,但是mask以后全是-inf进入softmax,如果是3个单词的话,过softmax后得出的结果应该是【1/3,1/3,1/3】,这样也会有问题。期待博主解答。。

    2. mathor mathor

      @scirocc我在这篇文章底下已经回复过相关问题了,提问前请仔细看下评论区好么

  31. fpf fpf

    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # [batch_size, 1, len_k], False is masked
    这个不是为0的ture,那不应该是ture被masked?还是我理解错了,求指教。

    1. mathor mathor

      @fpfsorry,我这里确实写错了,应该是True is masked

  32. 恶霸派大星~~ 恶霸派大星~~

    请问一下encoder和decoder的输入能是相同的嘛?比如encoder:I love you, decoder: <s> I love you 这种嘛?

    1. mathor mathor

      @恶霸派大星~~可以,你这种想法实际上类似于BART

  33. scirocc scirocc

    博主您好,又有问题需要打扰您了。是有关encoder中get_attn_pad_mask的问题。现在假设我们的最大句长为3,实际待翻译的句子为“bye bye”(句长为2,我们padding成“bye bye somethingNeedToMaskedLater” ),所以这句话我们的mask应该为【0,0,1】,故对应的Q为[[0],[0],[1]],对应的K转置为[0,0,1],那么q*k转置后的mask不是应该是个3乘3的矩阵,最后一行和最后一列的所有数据都应该mask?对应您这里的图https://s1.ax1x.com/2020/04/25/Jy5rt0.png#shadow,而在您get_attn_pad_mask的实现中,您用expand广播,这样的画最后一行并非全部mask了阿?

    1. mathor mathor

      @scirocc最后一行实际上,可以mask,也可以不mask(下述内容是以行进行softmax的)

      现在假设最后一行不mask,那么做完softmax之后最后一行表示'pad'这个token对其他token的注意力分数,接着我们乘以V矩阵,这样的话最终得到的结果最后一行不是非0的,这个很重要吗?这并不重要,只是表示此时的结果矩阵内不那么稀疏(0多)而已。我们要保证只是在attention socre中,最后一列一定是0就行了

    2. mathor mathor

      @scirocc当然了,你要实在不放心,自己修改下代码,使得列方向上pad的位置也被mask也行。在真正的大型数据上结果差不了多少

    3. scirocc scirocc

      @mathor多谢您!

    4. scirocc scirocc

      @mathor我琢磨了,如果魔改,把最后一行全mask的话还是会有问题,因为如果全部fill with -1e9,那么过softmax后,attn是【1/3,1/3,1/3】而不是我想要的【0,0,0】,我应该把soft (q*k)的最后一行全部改为0才对。

    5. 低调流年的微凉 低调流年的微凉

      @scirocc博主其实还是没说清楚为啥最后一行不重要。你说的对,只有把softmax之后的最后一行全改为0才会,但是我不知道,这个最后一行,是在后面的哪一块用到的?

    6. Amo Amo

      @scirocc我知道为啥只设列不设行,像你所说要经过softmax之后进行设置才能达到我们想要的权重清0的结果,如果单纯将pad行设为true再变为-inf,那么权重清不了0,自然乘以V矩阵后就无法达到attention value的效果,如果在softmax之后再清0会增加代码难度,首先mask和softmax放在两个函数,要将pad行的index作为参数传入,在大量数据影响不大结果,同时又要使得编码简单,那只能退而求其次把最后一列转为true,行不动。

  34. Van_goph Van_goph

    作者您好,我有两个小问题,首先,在Encoder中的mask编码时,为enc_self_attn_mask变量增加了一维,并对这个维进行了扩展。是这个维度的长度变成了src_len,这块我没太明白是为什么要这么做。其次,我对比了您的代码和源码,感觉pad这块其实在源码中并没有事先设定好,因外在文本中,pad其实是不透明的,而您的文本其实是事先写好了pad,并可以直接给予了mask,若随便给定一个文本这个代码是不是需要改一下这块才可以实现。@(哈哈)

    1. Van_goph Van_goph

      @Van_goph还有就是,对于第一个问题来说,应该是有多少个单词就加扩展多少维度,我是这么理解的,但是在最后一行时pad,那这一行应该都是True吧,为什么只有最后一个值是ture呢

  35. 青文 青文

    tql@(乖)

  36. chu chu

    我想问一下MultiHeadAttention中定义的fc是什么

    1. mathor mathor

      @chufc: fully connected layer、全连接神经网络

  37. kc c kc c

    pad mask错了,没有将pad对其他词的注意力置为0。

    1. kc c kc c

      @kc c可能并不影响

  38. Jarvis Jarvis

    请问翻译的时候是不是应该把句子结尾那个“E”也翻译出来呀?if next_symbol == tgt_vocab["."] 这里应该要把句号'.'的这个序列信息加进去

  39. lei xiao lei xiao

    position embedding这部分代码是不是和论文上的公式不一样,请问为什么要做这样的调整呢?
    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)
    1. mathor mathor

      @lei xiao那可能是我写错了

  40. 喜喜 喜喜

    我有一个疑问,在解码器刚开始输入的时候既然有了Subsequence Mask让他看不到以后的单词,那自然看不到后面的的pad,代码中在这一步Subsequence Mask加Pad Mask有必要吗

    1. Moment Moment

      @喜喜pad可能很长,seq mask覆盖不到

  41. 阿强 阿强

    博主您好,在仔细阅读了所有评论后,我对您在九个月前对用户zane的问题“请问Decoder 中的 encoder-decoder-attention 的过程中进行了 pad_mask 该怎么理解呢”的回复“在进行 encode-decode-attention 之前,是不是还做了一次 add & norm,要命的就是这个 add。由于进行了残差连接,使得原本为 0 的地方,又产生了值

    此时接下来又要进行一次 attention(即 encoder 的输出和 decoder 进行的 attention),如果这个时候,不重新 mask 一次,是不是未来时刻的信息仍然被用上了,因此要进行 mask”
    我比较认可,但是在您代码中关于这部分的操作是用的get_attn_pad_mask函数,那我认为这样的操作并没能达到您回复中所说的效果,我觉得正确的做法应该是重写一个类似于get_attn_pad_mask函数的函数

    1. 阿强 阿强

      @阿强具体如这样:“
      def get_enc_dec_attn_mask(seq_dec,seq_enc):

      ''' seq_dec: [batch_size, tgt_len] seq_enc: [batch_size, src_len] ''' attn_shape = [seq_dec.size(0), seq_dec.size(1), seq_enc.size(1)] subsequence_mask = np.triu(np.ones(attn_shape), k=1) # Upper triangular matrix subsequence_mask = torch.from_numpy(subsequence_mask).bool() return subsequence_mask # [batch_size, tgt_len, src_len]”

      并将对应的操作改为调用此函数,如下
      “dec_enc_attn_mask = get_enc_dec_attn_mask(dec_inputs, enc_inputs).cuda() # [batc_size, tgt_len, src_len]”
      方能达到您回复中所说的效果,您觉着我理解的对吗?
      (注:经过这样的修改之后,1000次epoch,在测试阶段所达到的效果比未修改前好,具体好在:未修改前,硬编码“my_enc_inputs = torch.LongTensor([[2,3,4,0]])”进行测试,正确结果应为"want a beer",但模型预测结果始终为"i want a beer",这个"i"始终不会消失,而在修改后,模型可以正确预测"want a beer"。因此,我有理由相信我的修改是正确的。)

    2. mathor mathor

      @阿强感谢提醒,由于您回复的内容实在太长,我需要花一点时间消化

    3. 阿强 阿强

      @mathor好的,感谢回复!(另外,修改一下我评论里的一句话:“我觉得正确的做法应该是重写一个类似于 get_attn_pad_mask 函数的函数”。这里我说错了,应该是类似于get_attn_subsequence_mask函数的函数)

  42. Ryan Ryan

    @(真棒)tql

  43. lyq lyq

    在运行测试代码的时候会显示 Input, output and indices must be on the current device, device不一致

  44. 恶霸派大星~~ 恶霸派大星~~

    您好, 请问一下训练中有遇到attention值很奇怪的情况嘛? Attention的值就像被平分了一样, 几乎都没没有区别, 就像这样: [0.10000031 0.10000069 0.09999802 0.09999687 0.10000107 0.09999916 0.10000107 0.10000069 0.10000221 0.09999993]. 感觉self-attention都没有起到作用

  45. yt yt

    博主您好,,用了这个模型做对话机器人,效果及其不好,用的小黄鸡语料库。。。。在测试对话时,总会输出标点,并且loss一直在5~6波动,不收敛。。@(泪)这到底为什么啊,比用seq2seq+attention效果还差

    1. mathor mathor

      @yt有很多原因,最主要是:不要从头训练(from scrath),而是推荐使用预训练模型

    2. yt yt

      @mathor感谢回复!@(泪)新的一天新的折磨

  46. graces graces

    博主您好,您的代码数据好像没有在同一个gpu上跑, 我fork下来出现了Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu! (when checking arugment for argument index in method wrapper_index_select)这样的问题

    1. mathor mathor

      @graces那您自己改一下就行了

    2. Miracy Miracy

      @graces代码小白也遇到了这个问题,请问您知道怎么修改了吗?

    3. graces graces

      @Miracy
      def greedy_decoder(model, enc_input, start_symbol):

      dec_input=torch.cat([dec_input.detach(),torch.tensor([[next_symbol]],dtype=enc_input.dtype).cuda()],-1)

      enc_inputs, , = next(iter(loader))
      enc_inputs = enc_inputs.cuda()

    4. Miracy Miracy

      @graces感谢!

    5. Kulasuo Kulasuo

      @graces你好,按照你的修改,还是报这个错误,是粘贴少了修改语句吗

    6. Jack Jack

      @graces大佬能分享下具体怎么改吗?把代码转到 GPU 上报错了,RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!,把 input、label、model 都移到 GPU 上还是报错,哭了,训练时就出错了,dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0) # [batch_size, tgt_len, tgt_len],这一行报错的!!

    7. mathor mathor

      @graces感谢回复

  47. Lex Lex

    博主您好,我有两个问题希望能请您帮我解惑下,万分感激!!!

    第一个是,我想请问下可以用Transformer模型来重构文本吗,还是说用别的模型呢?比如说把信息(比如一段文字)先通过NLP模型进行encode,然后再通过decoder来恢复出原来的样子(重构,而不是翻译到另一种语言)?

    另外,我想请教下如果文本通过encoder之后(进入decoder之前),中间状态会呈现怎么样一种形式呢?是0 1 二进制码还是随机分布的数字组合呢?如果这种中间状态受到部分损坏(比如有噪声干扰),decoder在这种情况下是否还能完整恢复出原意呢?

    期待您的回复,谢谢!

  48. timetravel timetravel

    你好,我复现出现了一个错误:Expected tensor for argument #1 indices to have scalar type Long; but got torch.FloatTensor instead (while checking arguments for embedding 只能处理longTensor类型的数据么?如果是flaotTensor类型的数据该怎么办?求大佬解答

    1. mathor mathor

      @timetravel没空

  49. LG LG

    博主,pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) ,这里的eq是一个定义好的矩阵还是本来的内置函数啊

    1. mathor mathor

      @LG内置函数

  50. Zhenjiang Zhenjiang

    学习了

  51. xiaoxiaofang xiaoxiaofang

    博主太厉害了!
    有个小错误:enc_input = [[src_vocab[n] for n in sentencesi.split()]] # [[1, 2, 3, 4, 0], [1, 2, 3, 5, 0]]这行代码中#后边的结果应该是enc_inputs吧?

    1. mathor mathor

      @xiaoxiaofangyes

  52. 浅度学习 浅度学习

    大佬有没有用过其他的训练策略比如Scheduled sampling,我用您代码训练很快收敛,但是测试会误差累积输出不了正确结果

  53. li_简单 li_简单

    大佬你好,为什么我在运行测试程序的时候报错,Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu! (when checking arugment for argument tensors in method wrapper__cat),好像是部分数据没有放到cuda上,得怎么解决呢?

    1. mathor mathor

      @li_简单请参考上面昵称为 graces 用户的评论内容

    2. Cor Cor

      @mathor感觉这个问题主要是因为LayerNorm的问题,因为两处调用LayerNorm的时候都在forward里面,所以是写死的.cuda()。如果把这层放在init里面,就可以通过model.to(device)调用了。(可以改一下,这样子后面测试的时候,就可以用cpu数据跑了,不会遇见上面的问题
      anyway,再次感谢博主的分享,这几篇transformer的文章受益颇多。QAQ @(乖)

    3. Jack Jack

      @Cor大佬能分享下具体怎么改吗?把代码转到GPU上报错了,RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!,把input、label、model都移到GPU上还是报错,哭了

    4. Cor Cor

      @Jackgreedy_decoder函数里面把dec_input加上cuda应该就好了,我记得原文里面这里忘记加了,除此之外应该没有。
      dec_input = torch.cat([dec_input.detach(), torch.tensor(

      [[next_symbol]], dtype=enc_input.dtype).cuda()], -1)

      剩下的应该都好找

    5. Jack Jack

      @Cor我修改了训练时的enc_inputs, dec_inputs, dec_outputs = enc_inputs.to(device), dec_inputs.to(device), dec_outputs.to(device),model = Transformer().to(device) 哭了,训练时在这一句报错了 :dec_self_attn_mask = torch.gt ((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0) # [batch_size, tgt_len, tgt_len],错误信息是RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!能问下还要修改什么吗

  54. Zhenjiang Zhenjiang

    您好,输入encoder的字向量矩阵不应该是一个三维向量吗,batchseq_lendim

  55. JuniorSummer JuniorSummer

    太牛逼了,叹为观止,写得详细易懂,让我受益匪浅,感谢博主!

  56. 低调流年的微凉 低调流年的微凉

    在多头注意力那里,您好像没有用残差

  57. 小白 小白

    学长,你好,请问数据为什么要有开始和结束标志,还有就是这个模型是不是直接可以用于预测,预测的时候是否还需要开始结束标志

  58. su su

    您好,想问下我跑您的代码,为什么一直报错device type cuda but got device type cpu for argument #3 呢?

    1. su su

      @su没事了 已经看到grace的评论了

    2. Jack Jack

      @su大佬能分享下具体怎么改吗?把代码转到 GPU 上报错了,RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!,把 input、label、model 都移到 GPU 上还是报错,哭了

    3. 123456 123456

      @Jack在代码里新创建的变量 也需要转到gpu

  59. 刘伟利 刘伟利

    大佬,你好,self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)这里为什么要用全连接,不是应该用一个矩阵乘吗

    1. mathor mathor

      @刘伟利矩阵乘法,就是全连接。全连接就是做的矩阵乘法

    2. 刘伟利 刘伟利

      @mathor非常感谢!!

  60. Pytorch implementation of Bert (super detailed) R11; Open Source Biology & Genetics Interest Group

    [...]The model structure mainly adopts Transformer Of Encoder, So I won’t repeat it here , You can read my article directly Transformer Of PyTorch Realization , as well as B Station video explanation[...]

  61. 123456 123456

    您好 做pad mask的时候 最后几行和最后几列不应该都是True吗 为什么只把最后几列设置成True

  62. Amo Amo

    博主,代码没有体现normalization

    1. mathor mathor

      @Amolayernorm不算norm?

  63. 芳华 芳华

    大佬,Encoder中的这句代码和你视频中讲的不一样啊?
    enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1) # [batch_size, src_len, d_model]

    enc_outputs.transpose(0, 1):而且为什么要对enc_outputs进行第一维度和第二维度的变换?

    1. 芳华 芳华

      @芳华大佬,我看懂了,刚刚自己脑壳瓦特了

  64. IT-fig IT-fig

    这里模型的最后一层不添加softmax, 这样的话,输出的是概率分布吗,对训练和测试有影响吗,测试的时候取最大值,应该怎么?

  65. wulele wulele

    大佬 我想问下;我试着用nn.Transformer自带模块替换上述模型,但模型一直不收敛。怀疑是不是参数设置有问题。比较令我困惑的是memory_mask参数,求大佬指教。

    1. mathor mathor

      @wulele我没用过nn.Transformer,我一直用的是huggingface的transformer

    2. wulele wulele

      @mathor好的 谢谢博主,我在研究研究

    3. wulele wulele

      @mathor感谢博主,我已经用nn.Transformer对齐了。https://github.com/wulele2/nn.Transformer, 并对您的GitHub做了引用。再次表示感谢。

  66. albrecht albrecht

    博主,我想问一下模型参数是都需要优化的吗,哪些需要加上*.detach呢

  67. 耀斑切线 耀斑切线

    关于第九行注释里的e 应该是ending of output吧

  68. 【手撕Transformer】Transformer输入输出细节以及代码实现(pytorch)_Johngo学长

    [...]https://wmathor.com/index.php/archives/1455/[...]

  69. gao gao

    感谢大佬的视频

    1. mathor mathor

      @gao感谢你的打赏

  70. Echo Echo

    谢谢您的创作

    1. mathor mathor

      @Echo感谢你的打赏

  71. 哈哈哈哈 哈哈哈哈

    大佬牛批!!

  72. pylearning pylearning

    没有人在这里执行报错吗?dec_input = torch.zeros(1, 0).type_as(enc_input.data)
    我看视频里面代码是:
    dec_input = torch.zeros(1, tgt_len).type_as(enc_input.data)
    请问区别是啥

  73. luna2 luna2

    看了代码 很有帮助 非常感谢!
    下面是我看到的可能的问题:
    代码测试部分,用了greedy_decoder计算dec_inputs,greedy_decoder中while结束的条件是预测的最后一个词是'.'
    但greedy_decoder中,while的循环结束后,并没有把预测的最后一个词(也就是结束符'.'),拼接到dec_inputs中,导致dec_inputs并不完整,也就是少了'.',进而导致最后测试部分的预测结果也不完整,没有预测到最后的'E'

  74. 大佬大佬救救我 大佬大佬救救我

    您好大佬,不知道时隔这么久您是否还能看到我的消息,我对于这个模型有一些不理解的地方。在您的这篇博客中,提到“Transformer 主要就是调用 Encoder 和 Decoder。最后返回 dec_logits 的维度是 [batch_size tgt_len, tgt_vocab_size],可以理解为,一个句子,这个句子有 batch_sizetgt_len 个单词,每个单词有 tgt_vocab_size 种情况,取概率最大者”。也就是说transformer在训练阶段的输出是预测的一整个句子。
    然而在应用模型预测的时候您又使用模型一次预测一个单词,为什么是这样的操作呢?训练阶段使用模型的方式与预测阶段使用模型的方式不一样,在我看来这两个输出代表的甚至是两个分类任务,用第一个分类任务训练出来的网络完成第二个分类任务的预测,这里我实在是没搞明白。

    1. mathor mathor

      @大佬大佬救救我训练和预测都是一次输出一个token,可能是我里面有什么部分写错了,不好意思。总之训练和预测都是自回归的方式,一次只输出一个token

  75. ace ace

    想问下,为什么在计算dec_enc_attn_mask,只考虑了enc_inputs的pad部分,而没有考虑dec_inputs的pad部分

    1. ace ace

      @ace明白了