go . => va au !, bleu 0.000
i lost . => j’ai perdu perdu ., bleu 0.783
he’s calm . => il est essaye il partie paresseux ., bleu 0.418
i’m home . => je suis chez tom chez triste pas pas pas , bleu 0.376
V1
go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est riche ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000
V2
go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000
class Seq2SeqDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
# 解码器要有自己的embedding层,因为翻译一个英语一个法语
self.embedding = nn.Embedding(vocab_size, embed_size)
# 这里假设encoder隐藏层大小和decoder隐藏层大小是一样的
self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
# 做一个vocab_size的分类
self.dense = nn.Linear(num_hiddens, vocab_size)
# enc的输出有两部分:outputs和state,只要state
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
# 如果没有上下文操作,那就是一个普通的rnn,没有什么区别。
def forward(self, X, state):
# 把时间步放到前面
X = self.embedding(X).permute(1, 0, 2)
'''
上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
'''
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), dim=2)
output, state = self.rnn(X_and_context, state)
# 再把维度调整回(batch_size, num_step, vocab_Size)
output = self.dense(output).permute(1, 0, 2)
return output, state
修正原因见下面“训练部分”
class Seq2SeqDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
# 解码器要有自己的embedding层,因为翻译一个英语一个法语
self.embedding = nn.Embedding(vocab_size, embed_size)
# 这里假设encoder隐藏层大小和decoder隐藏层大小是一样的
self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
# 做一个vocab_size的分类
self.dense = nn.Linear(num_hiddens, vocab_size)
# enc的输出有两部分:outputs和state,只要state
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
# 如果没有上下文操作,那就是一个普通的rnn,没有什么区别。
def forward(self, X, enc_state, state=None):
# 把时间步放到前面
X = self.embedding(X).permute(1, 0, 2)
'''
上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
'''
context = enc_state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), dim=2)
output, state = self.rnn(X_and_context, state)
# 再把维度调整回(batch_size, num_step, vocab_Size)
output = self.dense(output).permute(1, 0, 2)
return output, state
# 关键部分:
def forward(self, X, enc_state, state=None):
# X: (batch_size, num_step, emb_size)
X = nn.Embedding(X).permute(1, 0, 2)
context = enc_state[-1].repeat(X.shape[0], 1, 1) # (num_step, batch_size, num_hidden)
X_and_Context = torch.cat((X, context), dim=2) # (num_step, batch_size, emb_size + num_hidden)
# 如果state == None,那nn.GRU.forward中的第二个参数就是None,会自动生成(num_layer, batch_size, num_hiddens)的全0张量
output, state = self.rnn(X_and_Context, state) # (num_step, batch_size, num_hidden)
output = self.dense(output).permute(1, 0, 2)
return output, state
在V1的基础上,增加对这个类的修改:
#@save
class EncoderDecoder(nn.Module):
"""编码器-解码器架构的基类"""
def __init__(self, encoder, decoder, **kwargs):
super(EncoderDecoder, self).__init__(**kwargs)
self.encoder = encoder
self.decoder = decoder
def forward(self, enc_X, dec_X, *args):
enc_outputs = self.encoder(enc_X, *args)
dec_state = self.decoder.init_state(enc_outputs, *args)
return self.decoder(dec_X, dec_state, dec_state)
【修订前的预测部分代码】
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
device, save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为评估模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
src_vocab['' ]]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['' ])
# 添加批量轴
enc_X = torch.unsqueeze(
torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
enc_outputs = net.encoder(enc_X)
dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
# 添加批量轴
dec_X = torch.unsqueeze(torch.tensor(
[tgt_vocab['' ]], dtype=torch.long, device=device), dim=0)
output_seq, attention_weight_seq = [], []
for _ in range(num_steps):
Y, dec_state = net.decoder(dec_X, dec_state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['' ]:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
【修订后代码】
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
device, save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为评估模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
src_vocab['' ]]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['' ])
# 添加批量轴
enc_X = torch.unsqueeze(
torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
enc_outputs = net.encoder(enc_X)
dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
# 添加批量轴
dec_X = torch.unsqueeze(torch.tensor(
[tgt_vocab['' ]], dtype=torch.long, device=device), dim=0)
output_seq, attention_weight_seq = [], []
state = None # V2版本的修订代码这里换成 state = dec_state
for _ in range(num_steps):
Y, state = net.decoder(dec_X, dec_state, state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
# dim=2是vocab维度
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['' ]:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
这里写一下自己学这部分的心得,以及整理下混乱的地方,以及回答为什么沐神的decoder也有问题。
孙笑川-7742的动态-哔哩哔哩 (bilibili.com)
9.7. 序列到序列学习(seq2seq) — 动手学深度学习 2.0.0-beta0 documentation (d2l.ai)——讨论区
首先是关于nn.RNN【nn.LSTM,nn.GRU同理】。
nn.RNN()初始化的时候需要的参数:(vocab_size,num_hiddens,num_layers)
而在调用net() 即forward方法的时候,需要传入的参数是X输入与state隐状态。
初始化隐状态state的时候需要的参数:(num_layers, batch_size, num_hiddens)
其次,train和predict有一个本质的区别在于:train的时候是已知num_step的,输入X是(batch_size, num_step)的,所以在decoder里调用self.rnn()其实只调用了一次!
# 关键部分:
def forward(self, X, enc_state, state=None):
# X: (batch_size, num_step, emb_size)
X = nn.Embedding(X).permute(1, 0, 2)
context = enc_state[-1].repeat(X.shape[0], 1, 1) # (num_step, batch_size, num_hidden)
X_and_Context = torch.cat((X, context), dim=2) # (num_step, batch_size, emb_size + num_hidden)
# 如果state == None,那nn.GRU.forward中的第二个参数就是None,会自动生成(num_layer, batch_size, num_hiddens)的全0张量
output, state = self.rnn(X_and_Context, state) # (num_step, batch_size, num_hidden)
output = self.dense(output).permute(1, 0, 2)
return output, state
为什么这么说? 这里可以以手写的”从零开始实现rnn"中的计算函数来说:
我们已经permute了,把时间维度放到了第一维。在nn.GRU.forward调用的时候,内部其实是有一个for循环的【就像下面这样】,由于训练的时候已知时间步,所以隐状态在forward的时候是自动隐蔽的更新了:
# 计算。给一个小批量,将里面所有的时间步都算一遍,得到输出。
# input里包括所有的时间步(X_0到X_t),state是上一次运算的隐藏状态, params是可以学习的参数
def rnn(inputs, state, params):
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state # 这里是一个tuple,但是只有一个元素
outputs = []
for X in inputs: # inputs是一个三维的矩阵:(时间步,batch_size, one_hot长),这样循环会按时间步分,所以前面要转置
H = torch.tanh(torch.matmul(X, W_xh) + torch.matmal(H, W_hh) + b_h)
Y = torch.matmul(H, W_hq) + b_q # Y是当前时间步预测下一个单词是谁,但是这里是一个for循环,所以要append
outputs.append(Y)
# cat之后是一个二维矩阵,可以认为是n个矩阵按照竖着摞起来的。列数还是vocab_size,行数是batch_size * 时间步数
return torch.cat(outputs, dim=0), (H, )
对于修改版V1,一开始的时候,从0手写rnn也要对state做初始化全0操作(init_state函数), 如果第二个参数传None,不会影响初始化(而且在编码器的时候根本也没写state, 默认会初始化成0)。
对于修改版V2,在训练的时候传入的参数是两个相同的,效果和沐神的一样,解码器的隐状态初始化为编码器的输出。
所以对于训练来说,沐神代码【修订前】的也是可以的,因为:
def forward(self, X, state):
# 把时间步放到前面
X = self.embedding(X).permute(1, 0, 2)
'''
上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
'''
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), dim=2)
output, state = self.rnn(X_and_context, state)
# 再把维度调整回(batch_size, num_step, vocab_Size)
output = self.dense(output).permute(1, 0, 2)
return output, state
就算只有一个state,但是时间步那一重循环是在self.rnn.forward里做的,所以依然可以保证每次输入都是X和最后编码器的隐状态concat起来。【这里啰嗦一句,因为时间步在训练的时候是已知的,而且我们已经repeat过state[-1],so…】
但是对于训练来说就G了。
for _ in range(num_steps):
Y, dec_state = net.decoder(dec_X, dec_state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['' ]:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
这里因为是预测,不知道时间步是多长,只能自己写一个显式的for循环(而不是之前在self.rnn.forward里的隐式for循环), 所以net.decoder(也就是forward函数)可不止调用了一次!!
那在原来的decoder调用的时候,就G了,因为state显然是每次都在变化的…而这时候的可以看出dec_X(也就是输入)是batch_size = 1, num_step = 1的,所以每次X_and_context都不一样,根本就不是编码器的最终结果,而是上一次decoder的输出【 forward 函数里 context 与 rnn 的初始 hidden layer 耦合了(使用的都是输入参数 state )】
所以要分开才行——enc_state应该干两件事:
1、初始化解码器的state;
2、每个输入都要加上(这里的加是concat,inception式而非resnet式)enc_state
所以我们要解耦合,搞两个变量来记录。【V1,V2效果都可以,但是我认为V2更好,更合理】:
state = dec_state
for _ in range(num_steps):
Y, state = net.decoder(dec_X, dec_state, state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
# dim=2是vocab维度
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重(稍后讨论)
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['' ]:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq