encoder decoder模型_3分钟|聊一聊 Decoder 模块

微信公众号:NLP从入门到放弃

本文大概需要阅读 4.1 分钟

聊一下对 Decoder 的个人总结和理解,我保证里面大部分内容你没在别人那看过,绝对原创。

我先说一个很小的细节点,当时花了点时间才琢磨出来,其实不难,就是当时没转过弯来。

我们都知道,Decoder 的交互层,Q 矩阵来自本身,K/V 矩阵来自整个 Encoder 层输出。

但是不知道大家当时有没有这样一个疑惑点,就是 Encoder 层是怎么生成这个 K/V 矩阵的?

对于每个单词的输入,Encoder 都会对应一个输出。想一下 Bert 是不是每个单词都会对应一个自己本身的 Token 输出。

那么 K/V 矩阵在计算的时候是用的其中哪个单词的输出?第一个单词?最后一个单词?还是其他情况?

OK,我这个问题问法肯定是错误的。

后来仔细的取看了看了一下代码,恍然大悟,这才明白自己这个思维究竟错在哪里?我先说结论:

K/V 矩阵的计算不是来自于某一个单词的输出,而是对所有单词的输出汇总然后计算 K/V 矩阵,记住这里的 K/V 矩阵也是多头的。

这个过程和在 Encoder 中计算 K/V 矩阵是一样的,只不过放在了交互层,一时没想明白。大家如果有点疑惑的话,可以在这里停一下思考一下。

如果还没明白的同学,可以这样去理解。这个过程就是非常的类似Seq2Seq 的 attention 阶段。

Seq2Seq 初始的时候,只是使用最后一个时刻的隐层输出作为 context vector。加入attention之后,增加了一个对输入 LSTM每一个时刻计算权重的过程。

所以类比回来,在交互层,当然需要对每一个时刻(当然Transformer不存在时刻这个概念)的输出做一个交互的attention。

接下来,谈一下 Decoder 的整体架构。与Encoder很类似,Decoder同样由完全相同的N个大模块堆叠而成,原论文中N为6。

每个大的模块分为三部分:多头注意力层,交互层,前馈神经层;每个层内部尾端都含有 Add&Norm。

和Encoder重复的内容我就跳过了,之前讲过,没看过的同学可以点击这里去看一下那个文章。

对于多头自注意力层,我个人认为这里需要注意的细节点是:需要对当前单词和之后的单词做mask。

为什么需要mask?最常规的解释就是在预测阶段,你的模型看不见当前时刻的输出以及未来时刻单词。

从代码角度讲,你只需要把当前时刻之后所有单词 mask 掉就好了,也就是在计算注意力的时候,忽略这些内容。

我自己的理解是我们需要确保模型在训练和测试的时候没有 GAP

我举个简单的例子来解释我这句话,如果做机器翻译,你需要翻译出来的句子是 "我/爱/吃/苹果"。

当前时刻是'爱'这个单词作为输入的一部分,另一部分是上一个时刻'我'作为输入的时候的输出值。

当然在机器翻译中,我们一般使用 Teacher forcing加速收敛,所以这里就使用”我“作为当前时刻输入的另一个部分。

所以这个时候,输入就是”我“的编码信息和”爱“的编码信息(当然还有位置编码)。我要预测的是”吃“这个单词。

如果我们没有mask,模型也是可以运行的,也就说此时”吃“和”苹果“两个词对”爱“这个时刻的输出是有贡献的。

那么问题来了,测试数据中你根本没有 ground truth,你怎么办?

也就说,训练的时候,你的模型是基于知道这个时刻后面的单词进行的训练,但是测试的时候,做机器翻译,你不知道自己应该翻译出来什么东西。

这就是问题的核心。你训练模型的时候,一部分精力花在了'吃'和'苹果'两个词上,这不就是无用功吗?

而且,训练的时候知道后面的单词,相当于你这个模型在'作弊',在测试集这个真正的考场,效果好才是有鬼啊。

所以,确保模型在训练和测试的时候没有GAP,我们需要mask掉”吃“和”苹果“两个词。

对于交互模块,这一块需要注意的细节点就是之前文章提到的,Q矩阵来自本身,K/V 矩阵来自 Encoder 的输出。

还有一个细节点是,K/V 矩阵对应的是来自整个encoder的输出这一点一定要注意哦,是整个,而不是每一层

如果看 Transformer 那个经典图的话,初期很容易理解为encoder和decode对应的每一层互相交互,这是不对的。

是整个输出与decoder做交互。

Decoder 其他部分就比较简单了,应用 Decoder 的一个重要模型就是 GPT,这个的讲解我会放在之后词向量的部分去讲。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这段代码是定义了一个 Seq2Seq 模型类,它继承自 nn.Module 类。其中: - `__init__(self,encoder_embedding_num,encoder_hidden_num,en_corpus_len,decoder_embedding_num,decoder_hidden_num,ch_corpus_len)` 是类的构造函数,用于初始化模型。其中 `encoder_embedding_num` 表示编码器的嵌入层维度,`encoder_hidden_num` 表示编码器的隐藏层维度,`en_corpus_len` 表示英文语料库的大小,`decoder_embedding_num` 表示解码器的嵌入层维度,`decoder_hidden_num` 表示解码器的隐藏层维度,`ch_corpus_len` 表示中文语料库的大小。 - `self.encoder = Encoder(encoder_embedding_num,encoder_hidden_num,en_corpus_len)` 创建了一个 Encoder 对象,实现了编码器的功能。 - `self.decoder = Decoder(decoder_embedding_num,decoder_hidden_num,ch_corpus_len)` 创建了一个 Decoder 对象,实现了解码器的功能。 - `self.classifier = nn.Linear(decoder_hidden_num,ch_corpus_len)` 创建了一个线性层,用于将解码器的输出转换为中文语料库的维度。 - `self.cross_loss = nn.CrossEntropyLoss()` 创建了一个交叉熵损失函数。 - `def forward(self,en_index,ch_index)` 是类的前向传播函数,它接受英文语料和中文语料的索引作为输入,输出损失值。其中: - `decoder_input = ch_index[:,:-1]` 是将中文语料的索引序列切片,去掉最后一个字符,作为解码器的输入。 - `label = ch_index[:,1:]` 是将中文语料的索引序列切片,去掉第一个字符,作为损失函数的标签。 - `encoder_hidden = self.encoder(en_index)` 是将英文语料输入编码器,得到编码器的隐藏状态。 - `decoder_output,_ = self.decoder(decoder_input,encoder_hidden)` 是将解码器的输入和编码器的隐藏状态输入解码器,得到解码器的输出。 - `pre = self.classifier(decoder_output)` 是将解码器的输出输入线性层,得到最终的预测结果。 - `loss = self.cross_loss(pre.reshape(-1,pre.shape[-1]),label.reshape(-1))` 是将预测结果和标签输入交叉熵损失函数,得到损失值。其中 `pre.reshape(-1,pre.shape[-1])` 将预测结果展平成二维数组,`label.reshape(-1)` 将标签展平成一维数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值