解码器 | 基于 Transformers 的编码器-解码器模型
基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外,建议读者对 自注意力 (self-attention) 架构 有一个基本了解,可以阅读 Jay Alammar 的 这篇博文 复习一下原始 transformer 模型。
本文分 4 个部分:
- 背景 - 简要回顾了神经编码器-解码器模型的历史,重点关注基于 RNN 的模型。
- 编码器-解码器 - 阐述基于 transformer 的编码器-解码器模型,并阐述如何使用该模型进行推理。
- 编码器 - 阐述模型的编码器部分。
- 解码器 - 阐述模型的解码器部分。
每个部分都建立在前一部分的基础上,但也可以单独阅读。这篇分享是最后一部分 解码器。
解码器
如 编码器-解码器 部分所述, 基于 transformer 的解码器定义了给定上下文编码序列条件下目标序列的条件概率分布:
根据贝叶斯法则,在给定上下文编码序列和每个目标变量的所有前驱目标向量的条件下,可将上述分布分解为每个目标向量的条件分布的乘积:
我们首先了解一下基于 transformer 的解码器如何定义概率分布。基于 transformer 的解码器由很多 解码器模块 堆叠而成,最后再加一个线性层 (即 “LM 头”)。这些解码器模块的堆叠将上下文相关的编码序列
“LM 头” 即为词嵌入矩阵的转置, 即
总结一下,为了对目标向量序列
与基于 transformer 的编码器不同,在基于 transformer 的解码器中,其输出向量
好,我们仍以英语到德语翻译为例可视化一下 基于 transformer 的解码器。
我们可以看到解码器将
因此,对每个
总条件概率如下:
其可表示为以下乘积形式:
图右侧的红框显示了前三个目标向量
与双向自注意一样,在单向自注意中, query
向量 key
向量 value
向量 query
向量 key
向量进行比较 (即 value
向量并加权求和。
我们将单向自注意力总结如下:
请注意, key
和 value
向量的索引范围都是 key
向量的索引范围。
下图显示了上例中输入向量
可以看出
那么,为什么解码器使用单向自注意力而不是双向自注意力这件事很重要呢?如前所述,基于 transformer 的解码器定义了从输入向量序列
这显然是不对的,因为这样的话,基于 transformer 的解码器永远不会学到在给定所有前驱词的情况下预测下一个词,而只是对所有
太棒了!现在我们可以转到连接编码器和解码器的层 - 交叉注意力 机制!
交叉注意层将两个向量序列作为输入: 单向自注意层的输出 query
向量 key
和 value
向量 key
、value
和 query
向量后,将 query
向量 key
向量进行比较,并用各自的得分对相应的 value
向量进行加权求和。这个过程与 双向 自注意力对所有
注意,key
和 value
向量的索引范围是
我们用上例中输入向量
我们可以看到 query
向量 query
向量 key
向量 key
向量对应于编码器对其输入 value
向量
所以,直观而言,到底发生了什么?每个输出向量 value
向量(query
投影与 来自编码器的
酷!现在我们可以看到这种架构的每个输出向量 key
向量 value
向量 value
向量 为什么基于 transformer 的解码器没有远程依赖问题而基于 RNN 的解码器有
这一问题的答案已经很显然了。因为每个解码器 logit 向量 直接 依赖于每个编码后的输出向量,因此比较第一个编码输出向量和最后一个解码器 logit 向量只需一次操作,而不像 RNN 需要很多次。
总而言之,单向自注意力层负责基于当前及之前的所有解码器输入向量建模每个输出向量,而交叉注意力层则负责进一步基于编码器的所有输入向量建模每个输出向量。
为了验证我们对该理论的理解,我们继续上面编码器部分的代码,完成解码器部分。
from transformers import MarianMTModel, MarianTokenizer import torch tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de") model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de") embeddings = model.get_input_embeddings() # create token ids for encoder input input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids # pass input token ids to encoder encoder_output_vectors = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state # create token ids for decoder input decoder_input_ids = tokenizer("<pad> Ich will ein", return_tensors="pt", add_special_tokens=False).input_ids # pass decoder input ids and encoded input vectors to decoder decoder_output_vectors = model.base_model.decoder(decoder_input_ids, encoder_hidden_states=encoder_output_vectors).last_hidden_state # derive embeddings by multiplying decoder outputs with embedding weights lm_logits = torch.nn.functional.linear(decoder_output_vectors, embeddings.weight, bias=model.final_logits_bias) # change the decoder input slightly decoder_input_ids_perturbed = tokenizer("<pad> Ich will das", return_tensors="pt", add_special_tokens=False).input_ids decoder_output_vectors_perturbed = model.base_model.decoder(decoder_input_ids_perturbed, encoder_hidden_states=encoder_output_vectors).last_hidden_state lm_logits_perturbed = torch.nn.functional.linear(decoder_output_vectors_perturbed, embeddings.weight, bias=model.final_logits_bias) # compare shape and encoding of first vector print(f"Shape of decoder input vectors {embeddings(decoder_input_ids).shape}. Shape of decoder logits {lm_logits.shape}") # compare values of word embedding of "I" for input_ids and perturbed input_ids print("Is encoding for `Ich` equal to its perturbed version?: ", torch.allclose(lm_logits[0, 0], lm_logits_perturbed[0, 0], atol=1e-3))
输出:
Shape of decoder input vectors torch.Size([1, 5, 512]). Shape of decoder logits torch.Size([1, 5, 58101]) Is encoding for `Ich` equal to its perturbed version?: True
我们首先比较解码器词嵌入层的输出维度 embeddings(decoder_input_ids)
(对应于 <pad>
对应于 BOS 且 "Ich will das" 被分为 4 个词) 和 lm_logits
(对应于 <pad>
Ich will ein” 和其轻微改编版 “<pad>
Ich will das” 与 encoder_output_vectors
一起传递给解码器,以检查对应于 “Ich” 的第二个 lm_logit 在仅改变输入序列中的最后一个单词 (“ein” -> “das”) 时是否会有所不同。
正如预期的那样,解码器输入词嵌入和 lm_logits 的输出, 即 model.config.hidden_size
,而 lm_logit
的维数对应于词汇表大小 model.config.vocab_size
。其次,可以注意到,当将最后一个单词从 “ein” 变为 “das”,
最后一点, 自回归 模型,如 GPT2,与删除了交叉注意力层的 基于 transformer 的解码器模型架构是相同的,因为纯自回归模型不依赖任何编码器的输出。因此,自回归模型本质上与 自编码 模型相同,只是用单向注意力代替了双向注意力。这些模型还可以在大量开放域文本数据上进行预训练,以在自然语言生成 (NLG) 任务中表现出令人印象深刻的性能。在 Radford 等 (2019) 的工作中,作者表明预训练的 GPT2 模型无需太多微调即可在多种 NLG 任务上取得达到 SOTA 或接近 SOTA 的结果。你可以在 此处 获取所有 🤗 transformers 支持的 自回归 模型的信息。
好了!至此,你应该已经很好地理解了 基于 transforemr 的编码器-解码器模型以及如何在 🤗 transformers 库中使用它们。
非常感谢 Victor Sanh、Sasha Rush、Sam Shleifer、Oliver Åstrand、Ted Moskovitz 和 Kristian Kyvik 提供的宝贵反馈。
附录
如上所述,以下代码片段展示了如何为 基于 transformer 的编码器-解码器模型编写一个简单的生成方法。在这里,我们使用 torch.argmax
实现了一个简单的 贪心 解码法来对目标向量进行采样。
from transformers import MarianMTModel, MarianTokenizer import torch tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de") model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de") # create ids of encoded input vectors input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids # create BOS token decoder_input_ids = tokenizer("<pad>", add_special_tokens=False, return_tensors="pt").input_ids assert decoder_input_ids[0, 0].item() == model.config.decoder_start_token_id, "`decoder_input_ids` should correspond to `model.config.decoder_start_token_id`" # STEP 1 # pass input_ids to encoder and to decoder and pass BOS token to decoder to retrieve first logit outputs = model(input_ids, decoder_input_ids=decoder_input_ids, return_dict=True) # get encoded sequence encoded_sequence = (outputs.encoder_last_hidden_state,) # get logits lm_logits = outputs.logits # sample last token with highest prob next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1) # concat decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1) # STEP 2 # reuse encoded_inputs and pass BOS + "Ich" to decoder to second logit lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits # sample last token with highest prob again next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1) # concat again decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1) # STEP 3 lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1) decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1) # let's see what we have generated so far! print(f"Generated so far: {tokenizer.decode(decoder_input_ids[0], skip_special_tokens=True)}") # This can be written in a loop as well.
输出:
Generated so far: Ich will ein
在这个示例代码中,我们准确地展示了正文中描述的内容。我们在输入 “I want to buy a car” 前面加上
在实践中,我们会使用更复杂的解码方法来采样 lm_logits
。你可以参考 这篇博文 了解更多的解码方法。
至此,《基于 Transformers 的编码器-解码器模型》的四个部分就全部分享完啦,欢迎大家阅读其他分享 🤗!
英文原文: https://hf.co/blog/encoder-decoder
原文作者: Patrick von Platen
译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的应用及大规模模型的训练推理。
审校/排版: zhongdongy (阿东)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库