编码器 | 基于 Transformers 的编码器-解码器模型
基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外,建议读者对 自注意力 (self-attention) 架构 有一个基本了解,可以阅读 Jay Alammar 的 这篇博文 复习一下原始 transformer 模型。
本文分 4 个部分:
- 背景 - 简要回顾了神经编码器-解码器模型的历史,重点关注基于 RNN 的模型。
- 编码器-解码器 - 阐述基于 transformer 的编码器-解码器模型,并阐述如何使用该模型进行推理。
- 编码器 - 阐述模型的编码器部分。
- 解码器 - 阐述模型的解码器部分。
每个部分都建立在前一部分的基础上,但也可以单独阅读。这篇分享是第三部分 编码器。
编码器
如前一节所述, 基于 transformer 的编码器将输入序列映射到上下文相关的编码序列:
仔细观察架构,基于 transformer 的编码器由许多 残差注意力模块 堆叠而成。每个编码器模块都包含一个 双向 自注意力层,其后跟着两个前馈层。这里,为简单起见,我们忽略归一化层 (normalization layer)。此外,我们不会深入讨论两个前馈层的作用,仅将其视为每个编码器模块
我们对 编码器如何将输入序列 "I want to buy a car EOS" 变换为上下文编码序列
这一过程进行一下可视化。与基于 RNN 的编码器类似,基于 transformer 的编码器也在输入序列最后添加了一个 EOS,以提示模型输入向量序列已结束
上图中的 基于 transformer 的编码器由三个编码器模块组成。我们在右侧的红框中详细列出了第二个编码器模块的前三个输入向量:
可以看出,自注意力层的每个输出向量
我们更深入了解一下双向自注意力的工作原理。编码器模块的输入序列 key
向量 value
向量 query
向量
请注意,对每个输入向量 query
、 key
和 value
向量后,将每个 query
向量 key
向量 key
向量与 query
向量 value
向量 value
向量的加权和 value
向量的权重与 key
向量
好吧,又复杂起来了。我们以上例中的一个 query
向量为例图解一下双向自注意层。为简单起见,本例中假设我们的 基于 transformer 的解码器只有一个注意力头 config.num_heads = 1
并且没有归一化层。
图左显示了上个例子中的第二个编码器模块,右边详细可视化了第二个输入向量 query
向量 query
向量), value
向量 key
向量 query
向量 key
向量的转置 ( 即 value
向量相乘,并加上输入向量
为了进一步理解双向自注意力层的含义,我们假设以下句子: “ 房子很漂亮且位于市中心,因此那儿公共交通很方便 ”。 “那儿”这个词指的是“房子”,这两个词相隔 12 个字。在基于 transformer 的编码器中,双向自注意力层运算一次,即可将“房子”的输入向量与“那儿”的输入向量相关联。相比之下,在基于 RNN 的编码器中,相距 12 个字的词将需要至少 12 个时间步的运算,这意味着在基于 RNN 的编码器中所需数学运算与距离呈线性关系。这使得基于 RNN 的编码器更难对长程上下文表征进行建模。此外,很明显,基于 transformer 的编码器比基于 RNN 的编码器-解码器模型更不容易丢失重要信息,因为编码的序列长度相对输入序列长度保持不变, 即
除了更容易学到长程依赖外,我们还可以看到 transformer 架构能够并行处理文本。从数学上讲,这是通过将自注意力机制表示为 query
、 key
和 value
的矩阵乘来完成的:
输出
太好了,现在我们应该对:
a) 基于 transformer 的编码器模型如何有效地建模长程上下文表征,以及
b) 它们如何有效地处理长序列向量输入这两个方面有了比较好的理解了。
现在,我们写一个 MarianMT
编码器-解码器模型的编码器部分的小例子,以验证这些理论在实践中行不行得通。
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 ids of encoded input vectors input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids # pass input_ids to encoder encoder_hidden_states = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state # change the input slightly and pass to encoder input_ids_perturbed = tokenizer("I want to buy a house", return_tensors="pt").input_ids encoder_hidden_states_perturbed = model.base_model.encoder(input_ids_perturbed, return_dict=True).last_hidden_state # compare shape and encoding of first vector print(f"Length of input embeddings {embeddings(input_ids).shape[1]}. Length of encoder_hidden_states {encoder_hidden_states.shape[1]}") # compare values of word embedding of "I" for input_ids and perturbed input_ids print("Is encoding for `I` equal to its perturbed version?: ", torch.allclose(encoder_hidden_states[0, 0], encoder_hidden_states_perturbed[0, 0], atol=1e-3))
输出:
Length of input embeddings 7. Length of encoder_hidden_states 7 Is encoding for `I` equal to its perturbed version?: False
我们比较一下输入词嵌入的序列长度 ( 即 embeddings(input_ids)
,对应于 encoder_hidden_states
的长度 (对应于
不出意外,输入词嵌入和编码器输出编码的长度, 即
顺带一提, 自编码 模型 (如 BERT) 的架构与 基于 transformer 的编码器模型是完全一样的。 自编码 模型利用这种架构对开放域文本数据进行大规模自监督预训练,以便它们可以将任何单词序列映射到深度双向表征。在 Devlin 等 (2018) 的工作中,作者展示了一个预训练 BERT 模型,其顶部有一个任务相关的分类层,可以在 11 个 NLP 任务上获得 SOTA 结果。你可以从 此处 找到 🤗 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 打造的强大开源交互式图表库