当前位置:   article > 正文

深度剖析Seq2Seq原理代码

embedding permute

仅作学术分享,不代表本公众号立场,侵权联系删除

转载于:作者 | 燃雪  来源 | 知乎专栏

编辑 | 机器学习算法与自然语言处理

地址 | https://zhuanlan.zhihu.com/p/435413218

01

前言

在动手实现transformer过程中,主要参考了李沐老师《动手学深度学习 v2》的代码和视频讲解,收获很大,非常感谢李沐老师在b站发了这么多视频来系统地进行深度学习教学和论文解读(建议教育部新增b站学位证)。但是就我自己的学习过程来说,感觉视频和教程中有些代码和tensor解释并不是很容易理解(比如“state[-1]是最后一个时刻的最后一层”),不少命令是在反复看了许多遍后才理解其中输出和输入的变化过程。因此本文以具体代码为主线,补充tensor变化例子及示意图,力求让初学者更直观地理解Seq2Seq中的向量操作。

代码来源:李沐 《动手学深度学习 v2》 9.7 序列到序列学习

https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html#

02

文本预处理

考虑一组batch_size = 3的encoder句子输入,首先进行预处理,合法字符替换、按word拆开(Splitting)、词元化(tokenize)、字典化(vocabularizen)、按照每句话的合法长度num_steps = 8进行截断和填充(padding)处理,得到批输入X和输入语言的vocab_src.

d9af8ab7d138a7c5f454f7cc1daa0f46.png

03

编码器Encoder

参数举例:batch_size = 3; seq_length = 8, vocab_size = 10000; embed_size = 512;

RNN网络参数: num_hiddens = 24; num_layers = 2;

  1. class Seq2SeqEncoder(d2l.Encoder):
  2. """用于序列到序列学习的循环神经网络编码器。"""
  3. def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
  4. super(Seq2SeqEncoder, self).__init__(**kwargs)
  5. self.embedding = nn.Embedding(vocab_size, embed_size)
  6. #Embedding NN,输入维度vocab_size,输出维度embed_size
  7. self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout)
  8. #GRU神经网络,输入维度embed_size,num_layers隐藏层,每层num_hiddens个神经元,是否dropout
  9. def forward(self, X, *args):
  10. X = self.embedding(X)
  11. # Embedding Step
  12. X = X.permute(1, 0, 2)
  13. # Switching Step, 将 batch size 和 word position 调换位置
  14. output, state = self.rnn(X)
  15. # 输出RNN网络的output和state,state表示完整的RNN神经网络的状态,是隐藏神经元状态按时间步的整合
  16. # 比如state[0]表示第1个时间步(word position/Time Step 1: I/aa/A)对应的RNN隐藏神经元状态
  17. return output, state

Batch input ➡️ Onehot 编码 ➡️ Embedding ➡️ 调换顺序 ➡️ 调整为RNN网络时序输入

38231281a6e280d0f9cfa72e0b61e1fe.png
Encoder中输入的逐层变化

Encoder_RNN神经网络的输入、隐藏状态及输出

56fed03775999ce160478c00eca95b3e.png

04

解码器Decoder

Decoder输入的是target语言,此处对于target语言设置参数:batch_size = 3; seq_length = 8, vocab_size = 25000; embed_size = 512;

假设decoder使用与encoder相同设置的RNN网络参数: num_hiddens = 24; num_layers = 2;

  1. class Seq2SeqDecoder(d2l.Decoder):
  2. """用于序列到序列学习的循环神经网络解码器。"""
  3. def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
  4. super(Seq2SeqDecoder, self).__init__(**kwargs)
  5. self.embedding = nn.Embedding(vocab_size, embed_size)
  6. self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
  7. #同样的embedding NN与RNN,区别在于此处embedding网络根据target语言的embed_size和vocab_size参数设置
  8. #且RNN网络的输入进行了一次拼接(Concat Step),输入维度为embed_size+num_hiddens
  9. self.dense = nn.Linear(num_hiddens, vocab_size)
  10. #输出层,encoder的RNN网络不需要输出,decoder的RNN网络输出翻译的目标句子每个word位置(time step)上,
  11. #长度为vocab size的概率分布,表示vocab中每个word出现的概率
  12. def init_state(self, enc_outputs, *args):
  13. return enc_outputs[1]
  14. #enc_outputs = [output, state],此处是将encoder最后一个时间步 time step 8的隐藏层参数取出作为初始化
  15. def forward(self, X, state):
  16. X = self.embedding(X).permute(1, 0, 2)
  17. #Embedding & Permute Step,将目标语言的batch input进行embedding and permute step
  18. context = state[-1].repeat(X.shape[0], 1, 1)
  19. # Expand Step,将从encoder拿到的state[7]的最后一层隐藏层的状态作为表示原句信息的`context`
  20. #并将其扩展成tensor,使其具有与目标语言输入`X`相同的`seq_length`
  21. X_and_context = torch.cat((X, context), 2)
  22. #Concat Step,将句子信息context与目标语言Embedding tensor进行拼接,作为RNN_decoder的输入
  23. output, state = self.rnn(X_and_context, state)
  24. #decoder的RNN的输出和状态
  25. output = self.dense(output).permute(1, 0, 2)
  26. #Permute Step,将decoder的RNN输出后两位对调位置,方便下文处理 vocab 概率
  27. return output, state

Target Batch input ➡️ Onehot 编码 ➡️ Embedding ➡️ 调换顺序 ➡️ Concat Step

从Encoder拿到最后一个时间步的State ➡️ Extract Step ➡️ Expand Step ➡️ Concat Step

af852ed66043e4791fc24fd38042a717.png

Decoder_RNN神经网络的输入、隐藏状态及输出

c3f7a9e188b6dd087ffa9e36ae581929.png

05

交叉熵损失

Mask Function,0值化屏蔽

  1. def sequence_mask(X, valid_len, value=0):
  2. """在序列中屏蔽不相关的项。"""
  3. maxlen = X.size(1)
  4. mask = torch.arange((maxlen), dtype=torch.float32,
  5. device=X.device)[None, :] < valid_len[:, None]
  6. X[~mask] = value
  7. return X

举例:Input X = tensor( [1, 2, 3, 4, 5, 6, 7, 8], [a, b, c, d, e, f, g, h] ), 有效字符数 [4, 6]

0ad9e8af5c214e2aaea97efee249bad7.png
sequence_mask,零值化屏蔽

带遮蔽的softmax交叉熵损失函数,MaskedCrossEntropy

  1. class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
  2. """带遮蔽的softmax交叉熵损失函数"""
  3. # `pred` 的形状:(`batch_size`, `seq_length_tgt`, `vocab_size_tgt`)
  4. # `label` 的形状:(`batch_size`, `seq_length_tgt`)
  5. # `valid_len` 的形状:(`batch_size`,)
  6. def forward(self, pred, label, valid_len):
  7. weights = torch.ones_like(label) #按照label的shape进行单位矩阵初始化
  8. weights = sequence_mask(weights, valid_len) #按照合法字符数量valid_len进行mask,遮蔽<pad>
  9. self.reduction='none'
  10. unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)
  11. weighted_loss = (unweighted_loss * weights).mean(dim=1)
  12. return weighted_loss

06

训练过程

Train Function

  1. def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
  2. """训练序列到序列模型。"""
  3. def xavier_init_weights(m):
  4. if type(m) == nn.Linear:
  5. nn.init.xavier_uniform_(m.weight)
  6. if type(m) == nn.GRU:
  7. for param in m._flat_weights_names:
  8. if "weight" in param:
  9. nn.init.xavier_uniform_(m._parameters[param])
  10. net.apply(xavier_init_weights)
  11. net.to(device)
  12. optimizer = torch.optim.Adam(net.parameters(), lr=lr)
  13. loss = MaskedSoftmaxCELoss()
  14. net.train()
  15. animator = d2l.Animator(xlabel='epoch', ylabel='loss',
  16. xlim=[10, num_epochs])
  17. for epoch in range(num_epochs):
  18. timer = d2l.Timer()
  19. metric = d2l.Accumulator(2) # 训练损失总和,词元数量
  20. for batch in data_iter:
  21. X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
  22. bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
  23. device=device).reshape(-1, 1)
  24. dec_input = torch.cat([bos, Y[:, :-1]], 1) # 教师强制
  25. Y_hat, _ = net(X, dec_input, X_valid_len)
  26. l = loss(Y_hat, Y, Y_valid_len)
  27. l.sum().backward() # 损失函数的标量进行“反传”
  28. d2l.grad_clipping(net, 1)
  29. num_tokens = Y_valid_len.sum()
  30. optimizer.step()
  31. with torch.no_grad():
  32. metric.add(l.sum(), num_tokens)
  33. if (epoch + 1) % 10 == 0:
  34. animator.add(epoch + 1, (metric[0] / metric[1],))
  35. print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
  36. f'tokens/sec on {str(device)}')
1681c67f833bf98d59654604b6cfff93.png

开始训练

  1. embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
  2. batch_size, num_steps = 64, 10
  3. lr, num_epochs, device = 0.005, 300, d2l.try_gpu()
  4. train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
  5. encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
  6. dropout)
  7. decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
  8. dropout)
  9. net = d2l.EncoderDecoder(encoder, decoder)
  10. train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

07

预测及评估

预测网络搭建

  1. def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
  2. device, save_attention_weights=False):
  3. """序列到序列模型的预测"""
  4. # 在预测时将`net`设置为评估模式
  5. net.eval()
  6. src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
  7. src_vocab['<eos>']]
  8. enc_valid_len = torch.tensor([len(src_tokens)], device=device)
  9. src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
  10. # 添加批量轴
  11. enc_X = torch.unsqueeze(
  12. torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
  13. enc_outputs = net.encoder(enc_X, enc_valid_len)
  14. dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
  15. # 添加批量轴
  16. dec_X = torch.unsqueeze(torch.tensor(
  17. [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
  18. output_seq, attention_weight_seq = [], []
  19. for _ in range(num_steps):
  20. Y, dec_state = net.decoder(dec_X, dec_state)
  21. # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
  22. dec_X = Y.argmax(dim=2)
  23. pred = dec_X.squeeze(dim=0).type(torch.int32).item()
  24. # 保存注意力权重(稍后讨论)
  25. if save_attention_weights:
  26. attention_weight_seq.append(net.decoder.attention_weights)
  27. # 一旦序列结束词元被预测,输出序列的生成就完成了
  28. if pred == tgt_vocab['<eos>']:
  29. break
  30. output_seq.append(pred)
  31. return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
f3659a7924bfe5613d8b37d91dc1c11d.png

BLEU预测序列评价指标

  1. def bleu(pred_seq, label_seq, k):
  2. """计算 BLEU"""
  3. pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
  4. len_pred, len_label = len(pred_tokens), len(label_tokens)
  5. score = math.exp(min(0, 1 - len_label / len_pred))
  6. for n in range(1, k + 1):
  7. num_matches, label_subs = 0, collections.defaultdict(int)
  8. for i in range(len_label - n + 1):
  9. label_subs[''.join(label_tokens[i: i + n])] += 1
  10. for i in range(len_pred - n + 1):
  11. if label_subs[''.join(pred_tokens[i: i + n])] > 0:
  12. num_matches += 1
  13. label_subs[''.join(pred_tokens[i: i + n])] -= 1
  14. score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
  15. return score

预测实例

  1. engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
  2. fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
  3. for eng, fra in zip(engs, fras):
  4. translation, attention_weight_seq = predict_seq2seq(
  5. net, eng, src_vocab, tgt_vocab, num_steps, device)
  6. print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

08

Related Work

[1] LSTM神经网络状态,知乎用户 Scofield:

https://www.zhihu.com/question/41949741/answer/318771336

[2] @盛源车,知乎原文找不到了,此处是微信公众号的原文,可视化理解做的很直观:

https://mp.weixin.qq.com/s/0k71fKKv2SRLv9M6BjDo4w

[3] 可视化图解Attention based Seq2Seq模型:

https://zhuanlan.zhihu.com/p/60127009

[4] 基于TensorFlow的Seq2Seq代码实现:

https://zhuanlan.zhihu.com/p/47929039

[5] 本文使用的可视化工具——ML Visuals:

https://github.com/dair-ai/ml-visuals

推荐阅读:

我的2022届互联网校招分享

我的2021总结

浅谈算法岗和开发岗的区别

互联网校招研发薪资汇总

2022届互联网求职现状,金9银10快变成铜9铁10!!

公众号:AI蜗牛车

保持谦逊、保持自律、保持进步

36defb775ce79a0cbcc3baee244b24a8.png

发送【蜗牛】获取一份《手把手AI项目》(AI蜗牛车著)

发送【1222】获取一份不错的leetcode刷题笔记

发送【AI四大名著】获取四本经典AI电子书

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/802389
推荐阅读
相关标签
  

闽ICP备14008679号