MENU

Transformer 的 PyTorch 实现

July 14, 2020 • Read: 171702 • 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

189 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 明白了

  76. DNE DNE

    在 UP 代码、教程等等的基础上实现的基于 Transformer 的英译中项目,带有预测函数等等详细中文注释,代码放在了 github 上:https://github.com/songqibin/Transformer_Machine_Translation_EN2CHN

  77. 十月 十月

    请问大佬,这里对 pytorch 的版本以及 python 的版本有要求吗?

  78. gl gl

    感觉不太对,数据集全部参与训练了,应该拿测试集去验证。

    1. mathor mathor

      @gl 这篇文章重要的在于帮助你理解 transformer 模型的代码,至于训练或是测试并不重要,自己写即可

    2. gl gl

      @mathor 了然。

    3. gl gl

      @mathor 博主,我这边试了,测试集不参与训练,推理出来失败率很高。100 条翻译数据 95 条参与训练,5 条用来验证。

    4. mathor mathor

      @gl 那是因为 transformer 模型很大,你妄想用一百条数据训练一个很好的结果是不可能的。换句话讲,你拿 100 条数据,训练一个简单的 lstm 效果都肯定不好,何况 transformer 了

    5. gl gl

      @mathor 对于英文翻译中文这种基于 transformer 的模型,一般最小的训练数据量级是多少条,epoch 最少是多少次?才能到达一个 90 以上的准确率。博主有这方面的经验没

    6. mathor mathor

      @gl 几十万起步