做网站图片用什么格式,filetype ppt 网站建设,宁夏建设局网站,温州旅游 网站建设AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [ 编码器 ]的简单整理 目录
AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [ 编码器 ]的简单整理
一、简单介绍
二、Transformer
三、Transformer架构
四、编码器
1、自注意…AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [ 编码器 ]的简单整理 目录
AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [ 编码器 ]的简单整理
一、简单介绍
二、Transformer
三、Transformer架构
四、编码器
1、自注意力机制
2、 前馈层
3、位置嵌入
4、添加分类头 一、简单介绍
AGI即通用人工智能Artificial General Intelligence是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务而且能够理解、学习和应用知识于广泛的问题解决中具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整并能在没有人为干预的情况下解决各种复杂问题。 AGI能做的事情非常广泛 跨领域任务执行AGI能够处理多领域的任务不受限于特定应用场景。 自主学习与适应AGI能够从经验中学习并适应新环境和新情境。 创造性思考AGI能够进行创新思维提出新的解决方案。 社会交互AGI能够与人类进行复杂的社会交互理解情感和社会信号。 关于AGI的未来发展前景它被认为是人工智能研究的最终目标之一具有巨大的变革潜力 技术创新随着机器学习、神经网络等技术的进步AGI的实现可能会越来越接近。 跨学科整合实现AGI需要整合计算机科学、神经科学、心理学等多个学科的知识。 伦理和社会考量AGI的发展需要考虑隐私、安全和就业等伦理和社会问题。 增强学习和自适应能力未来的AGI系统可能利用先进的算法从环境中学习并优化行为。 多模态交互AGI将具备多种感知和交互方式与人类和其他系统交互。 Hugging Face作为当前全球最受欢迎的开源机器学习社区和平台之一在AGI时代扮演着重要角色。它提供了丰富的预训练模型和数据集资源推动了机器学习领域的发展。Hugging Face的特点在于易用性和开放性通过其Transformers库为用户提供了方便的模型处理文本的方式。随着AI技术的发展Hugging Face社区将继续发挥重要作用推动AI技术的发展和应用尤其是在多模态AI技术发展方面Hugging Face社区将扩展其模型和数据集的多样性包括图像、音频和视频等多模态数据。 在AGI时代Hugging Face可能会通过以下方式发挥作用 模型共享作为模型共享的平台Hugging Face将继续促进先进的AGI模型的共享和协作。 开源生态Hugging Face的开源生态将有助于加速AGI技术的发展和创新。 工具和服务提供丰富的工具和服务支持开发者和研究者在AGI领域的研究和应用。 伦理和社会责任Hugging Face注重AI伦理将推动负责任的AGI模型开发和应用确保技术进步同时符合伦理标准。 AGI作为未来人工智能的高级形态具有广泛的应用前景而Hugging Face作为开源社区将在推动AGI的发展和应用中扮演关键角色。
注意以下代码运行可能需要科学上网 二、Transformer
Transformer 是一种深度学习模型架构最初由 Vaswani 等人在 2017 年的论文《Attention is All You Need》中提出。它引入了基于注意力机制的结构克服了传统 RNN递归神经网络在处理长序列输入时的效率和效果问题。Transformer 模型特别适用于自然语言处理NLP任务如机器翻译、文本生成和文本分类等。 Transformer 架构主要由编码器Encoder和解码器Decoder两个部分组成 编码器Encoder 编码器由多个相同的编码层堆叠而成。每个编码层包含两个子层多头自注意力机制Multi-Head Self-Attention Mechanism和前馈神经网络Feedforward Neural Network。编码器的输入是源序列通过各层的处理编码器生成对源序列的表示。 解码器Decoder 解码器也由多个相同的解码层堆叠而成。每个解码层包含三个子层多头自注意力机制、编码器-解码器注意力机制Encoder-Decoder Attention Mechanism和前馈神经网络。解码器的输入是目标序列通过各层的处理解码器生成对目标序列的预测。 Hugging Face 是一个致力于推动 NLP 研究和应用的公司。其主要产品是开源的 Transformers 库该库实现了各种基于 Transformer 架构的模型并提供了易于使用的 API。 Transformers 库 模型实现 Transformers 库实现了许多流行的 Transformer 模型例如 BERT、GPT、T5、RoBERTa 等。这些模型经过预训练可以直接用于各种下游任务如文本分类、情感分析、命名实体识别等。 使用和定制 用户可以轻松加载预训练模型进行微调或直接使用。库中提供了丰富的工具和方法用于模型训练、评估和部署。 管道PipelineAPI Transformers 库的 Pipeline API 简化了常见任务的实现过程如文本分类、问答系统、文本生成等。用户只需几行代码即可完成复杂的 NLP 任务。 Transformer 架构在 Hugging Face 的应用 预训练和微调 Hugging Face 提供了大量预训练的 Transformer 模型这些模型在大型数据集上进行预训练并可以在小数据集上进行微调适应特定任务。社区和模型库 Hugging Face Hub 上有成千上万的模型由社区成员和 Hugging Face 提供这些模型可以被下载、使用、评估和分享。集成和部署 Transformers 库支持在各种平台和环境中部署 Transformer 模型包括云服务、边缘设备和移动设备。 通过 Hugging Face研究人员和开发者可以快速地利用 Transformer 架构的强大功能解决各种 NLP 任务并推动 NLP 技术的发展。 三、Transformer架构
原始 Transformer 是基于编码器-解码器架构的该架构广泛用于机器翻译等任务中即将一个单词序列从一种语言翻译成另一种语言。 该架构由两个组件组成 编码器 将一个词元的输入序列转化为一系列嵌入向量通常被称为隐藏状态或上下文。 解码器 利用编码器的隐藏状态逐步生成一个词元的输出序列每次生成一个词元。 而编码器和解码器又由如图所示的几个构建块组成。 Transformer的编码器-解码器架构图中上半部分为编码器下半部分为解码器 Transformer架构的特性
输入的文本会使用第2章中所介绍的技术进行词元化并转换成词元嵌入。由于注意力机制不了解词元之间的相对位置因此我们需要一种方法将词元位置的信息注入输入以便模拟文本的顺序性质。也就是说词元嵌入会与包含每个词元位置信息的位置嵌入进行组合。编码器由一系列编码器层或“块”堆叠而成类似于计算机视觉中叠加卷积层。解码器也是如此由一系列解码器层堆叠而成。编码器的输出被提供给每个解码器层然后解码器生成一个对于序列中下一个最可能的词元的预测。该步骤的输出随后被反馈回解码器以生成下一个词元以此类推直到达到特殊的结束序列EOS词元。假设解码器已经预测了“Die”和“Zeit”。现在它会把这两个作为输入并作为所有编码器的输出来预测下一个词元“fliegt”。在下一步中解码器将“fliegt”作为额外的输入。我们重复这个过程直到解码器预测出EOS词元或我们达到了最大长度。
Transformer架构最初是为序列到序列的任务如机器翻译而设计的但编码器和解码器模块很快就被抽出来单独形成模型。虽然Transformer模型已经有数百种不同的变体但其中大部分属于以下三种类型之一 纯编码器 这些模型将文本输入序列转换为富数字表示的形式非常适用于文本分类或命名实体识别等任务。BERT及其变体例如RoBERTa和DistilBERT属于这类架构。此架构中为给定词元计算的表示取决于左侧词元之前和右侧词元之后上下文。这通常称为双向注意力。 纯解码器 针对像“谢谢你的午餐我有一个……”这样的文本提示这类模型将通过迭代预测最可能的下一个词来自动完成这个序列。GPT模型家族属于这一类。在这种架构中对于给定词元计算出来的表示仅依赖于左侧的上下文。这通常称为因果或自回归注意力。 编码器-解码器 这类模型用于对一个文本序列到另一个文本序列的复杂映射进行建模。它们适用于机器翻译和摘要任务。除了Transformer架构它将编码器和解码器相结合BART和T5模型也属于这个类。 实际上纯解码器和纯编码器架构的应用之间的区别有些模糊不清。例如像GPT系列中的纯解码器模型可以被优化用于传统上认为是序列到序列任务的翻译任务。同样BERT等纯编码器模型也可以应用于通常与编码器-解码器或纯解码器模型相关的文本摘要任务注 。
现在你已经对Transformer架构有了高层次的理解接下来我们将更深入地了解编码器的内部工作原理。 四、编码器
编码器Encoder是 Transformer 架构中的一个核心部分负责将输入序列如文本、图像等转换为表示序列的高级抽象。 编码器的结构 编码器层Encoder Layer 编码器由多个相同的编码器层堆叠而成。每个编码器层包含两个子层 多头自注意力机制Multi-Head Self-Attention 允许编码器在处理每个位置的输入时能够注意到输入序列的其他位置。前馈神经网络Feedforward Neural Network 每个位置的表示经过一个全连接前馈网络对位置上的信息进行处理。每个子层后面通常还有一个残差连接Residual Connection以及层归一化Layer Normalization操作有助于提高模型的训练稳定性和速度。 编码器堆叠 Transformer 中的编码器由多个相同的编码器层堆叠而成通常有几层如论文中的六层编码器。每个编码器层生成的输出作为下一个编码器层的输入形成了一种逐层处理和传递信息的结构。 位置编码Positional Encoding Transformer 模型不使用递归结构如 RNN因此需要一种方式来处理序列中单词或位置的顺序信息。位置编码是一种在输入嵌入中添加位置信息的方法它使得模型能够区分不同位置的单词或子词。 编码器的工作原理 输入表示 编码器接收输入序列的表示通常是词嵌入Word Embeddings的组合可能还包括位置编码。多头自注意力机制 注意力机制Attention Mechanism 允许编码器在处理每个位置的输入时能够依据输入序列中其他位置的重要性来调整自己的表示。多头注意力Multi-Head Attention 在注意力机制中使用多组不同的注意力权重从不同的子空间中学习信息提高了模型对不同表示空间的学习能力。前馈神经网络 全连接层 每个位置的表示通过一个两层的全连接前馈神经网络进行变换。激活函数Activation Function 前馈神经网络通常使用 ReLURectified Linear Unit作为激活函数。残差连接和层归一化 残差连接Residual Connection 通过将每个子层的输入与其输出相加有助于避免梯度消失问题使得模型更容易训练。层归一化Layer Normalization 对每个子层的输出进行归一化处理加速训练并提高模型的泛化能力。 编码器在 Transformer 中的应用 特征提取和表示 编码器负责从输入序列中提取和学习关键的特征表示这些表示可以用于各种 NLP 任务如文本分类、命名实体识别和机器翻译等。模型堆叠和预训练 多层编码器的堆叠以及与解码器的结合使得 Transformer 模型在大规模预训练和微调任务中表现出色成为当前领域的主流模型之一。灵活性和性能 编码器结构的灵活性使得它能够处理不同长度和类型的输入序列同时其并行化处理也显著提高了模型的计算效率。 编码器是 Transformer 架构中负责将输入序列转换为高级抽象表示的关键组件它通过多层编码器层、自注意力机制和前馈神经网络来实现对序列信息的有效编码和表示。 Transformer的编码器由许多编码器层相互堆叠而成。如图所示 编码器层放大图 每个编码器层接收一系列嵌入然后通过以下子层进行馈送处理
●一个多头自注意力层。
●一个全连接前馈层应用于每个输入嵌入。
每个编码器层的输出嵌入尺寸与输入嵌入相同我们很快就会看到编码器堆叠的主要作用是“更新”输入嵌入以产生编码一些序列中的上下文信息的表示。例如如果单词“苹果”附近的单词是“主题演讲”或“电话”那么该单词将被更新为更像“公司”的特性而不是更像“水果”的特性。
这些子层同样使用跳跃连接和层规范化这些是训练深度神经网络的常用技巧。但要想真正理解Transformer的工作原理我们还需要更深入地研究。我们从最重要的构建模块开始自注意力层。 1、自注意力机制
注意力机制是一种神经网络为序列中的每个元素分配不同权重或“注意力”的机制。对文本序列来说元素则为我们在之前遇到的词元嵌入其中每个词元映射为固定维度的向量。例如在BERT中每个词元表示为一个768维向量。自注意力中的“自”指的是这些权重是针对同一组隐藏状态计算的例如编码器的所有隐藏状态。与自注意力相对应的与循环模型相关的注意力机制则计算每个编码器隐藏状态对于给定解码时间步的解码器隐藏状态的相关性。 自注意力的主要思想是不是使用固定的嵌入值来表示每个词元而是使用整个序列来计算每个嵌入值的加权平均值。另一种表述方式是说给定词元嵌入的序列x…x自注意力产生新的嵌入序列其中每个是所有x的线性组合
1nj 左边的flies为飞的意思所以上下文嵌入与表示飞的soars相近。右边的flies为苍蝇的意思所以上下文嵌入与表示昆虫的insect相近。
其中的系数wji称为注意力权重其被规范化以使得∑jwji1。如果想要了解为什么平均词元嵌入可能是一个好主意那么可以这样思考当你看到单词“flies”时会想到什么。也许你会想到令人讨厌的昆虫但是如果你得到更多的上下文比如“time flies like an arrow”那么你会意识到“flies”表示的是动词。同样地我们可以通过以不同的比例结合所有词元嵌入来创建“flies”的表示形式也许可以给“time”和“arrow”的词元嵌入分配较大的权重wji。用这种方式生成的嵌入称为上下文嵌入早在Transformer发明之前就存在了例如ELMo语言模型 。如图展示了这一过程我们通过自注意力根据上下文生成了“flies”的两种不同表示 。 自注意力将原始的词元嵌入顶部更新为上下文嵌入底部从而创建包含了整个序列信息的表示 现在我们看一下注意力权重是如何计算的。 1.1 缩放点积注意力
实现自注意力层的方法有好几种但最常见的是那篇著名的Transformer架构论文 所介绍的缩放点积注意力scaled dot-product attention。要实现这种机制需要四个主要步骤
1将每个词元嵌入投影到三个向量中分别称为query、key和value。
2计算注意力分数。我们使用相似度函数确定query和key向量的相关程度。顾名思义缩放点积注意力的相似度函数是点积并通过嵌入的矩阵乘法高效计算。相似的query和key将具有较大的点积而那些没有相似处的则几乎没有重叠。这一步的输出称为注意力分数在一个有n个输入词元的序列中将对应着一个n×n的注意力分数矩阵。
3计算注意力权重。点积在一般情况下有可能会产生任意大的数这可能会导致训练过程不稳定。为了处理这个问题首先将注意力分数乘以一个缩放因子来规范化它们的方差然后再通过softmax 进行规范化以确保所有列的值相加之和为1。结果得到一个n×n的矩阵该矩阵包含了所有的注意力权重。 4更新词嵌入。计算完注意力权重之后我们将它们与值向量v…v相乘最终获得词嵌入表示。
1n
我们可以使用一个很赞的库Jupyter 的 BertVizhttps://oreil.ly/eQK3I来可视化以上的注意力权重计算过程。该库提供了一些可用于可视化Transformer模型的不同方面注意力的函数。如果想可视化注意力权重我们可以使用 neuron_view 模块该模块跟踪权重计算的过程以显示如何将query向量和key向量相结合以产生最终权重。由于BertViz需要访问模型的注意力层因此我们将使用BertViz中的模型类来实例化我们的BERT checkpoint然后使用show()函数为特定的编码器层和注意力头生成交互式可视化。请注意你需要单击左侧的“”才能激活注意力可视化。
注意安装 bertvizpip install bertviz
from transformers import AutoTokenizer # 从transformers库中导入AutoTokenizer
from bertviz.transformers_neuron_view import BertModel # 从bertviz库中导入BertModel
from bertviz.neuron_view import show # 从bertviz库中导入show函数# 设置模型检查点
model_ckpt bert-base-uncased# 加载预训练的分词器
tokenizer AutoTokenizer.from_pretrained(model_ckpt)# 加载预训练的BERT模型
model BertModel.from_pretrained(model_ckpt)# 定义要处理的文本
text time flies like an arrow# 可视化BERT模型在给定文本上的注意力权重
show(model, bert, tokenizer, text, display_modelight, layer0, head8)
# 显示BERT模型在第0层、第8个注意力头的注意力权重
运行结果 1.2 query、key和value的神秘面纱
在你第一次接触query、key和value向量的概念时可能会觉得这些概念有点晦涩难懂。这些概念受到信息检索系统的启发但我们可以用一个简单的类比来解释它们的含义。你可以这样想象你正在超市购买晚餐所需的所有食材。你有一份食谱食谱里面每个食材可以视为一个query。然后你会扫描货架通过货架上的标注key以检查该商品是否与你列表中的食材相匹配相似度函数。如果匹配成功那么你就从货架上取走这个商品value。
在这个类比中你只会得到与食材匹配的商品而忽略掉其他不匹配的商品。自注意力是这个类比更抽象和流畅的版本超市中的每个标注都与配料匹配匹配的程度取决于每个key与query的匹配程度。因此如果你的清单包括一打鸡蛋那么你可能会拿走10个鸡蛋、一个煎蛋卷和一个鸡翅。
缩放点积注意力过程的更详细细节可以如下图所示。 缩放点积注意力中的操作 我们将使用PyTorch实现Transformer架构使用TensorFlow实现Transformer架构的步骤与之类似。两个框架中最重要的函数之间的映射关系详见下表 我们需要做的第一件事是对文本进行词元化因此我们使用词元分析器提取输入ID
from transformers import AutoTokenizer# 设置模型检查点使用 BERT 基础版未区分大小写
model_ckpt bert-base-uncased# 定义要处理的文本
text time flies like an arrow# 加载预训练的分词器
tokenizer AutoTokenizer.from_pretrained(model_ckpt)# 使用 tokenizer 对文本进行编码并返回 PyTorch 张量格式的输入
inputs tokenizer(text, return_tensorspt, add_special_tokensFalse)# 打印输入的 token IDs
inputs.input_ids
运行结果
tensor([[ 2051, 10029, 2066, 2019, 8612]])
正如我们之前所看到的那样句子中的每个词元都被映射到词元分析器的词表中的唯一ID。为了保持简单我们还通过设置add_special_tokensFalse来将[CLS]和[SEP]词元排除在外。接下来我们需要创建一些密集嵌入。这里的密集是指嵌入中的每个条目都包含一个非零值。相反我们在之前所看到的独热编码是稀疏的因为除一个之外的所有条目都是零。在PyTorch中我们可以通过使用torch.nn.Embedding层来实现这一点该层作为每个输入ID的查找表
from torch import nn
from transformers import AutoConfig# 从预训练模型的配置中加载配置信息
config AutoConfig.from_pretrained(model_ckpt)# 创建一个 token embeddings 层使用预训练模型的词汇表大小和隐藏层大小
token_emb nn.Embedding(config.vocab_size, config.hidden_size)# 打印 token embeddings 层
token_emb运行结果
Embedding(30522, 768)
在这里我们使用AutoConfig类加载了与bert-base-uncased checkpoint相关联的config.json文件。在Hugging Face Transformers库中每个checkpoint都被分配一个配置文件该文件指定了各种超参数例如vocab_size和hidden_size。在我们的示例中每个输入ID将映射到nn.Embedding中存储的30 522个嵌入向量之一其中每个向量维度为768。AutoConfig类还存储其他元数据例如标注名称用于格式化模型的预测。
需要注意的是此时的词元嵌入与它们的上下文是独立的。这意味着同形异义词拼写相同但意义不同的词如前面例子中的“flies”“飞行”或“苍蝇”具有相同的表示形式。后续的注意力层的作用是将这些词元嵌入进行混合以消除歧义并通过其上下文的内容来丰富每个词元的表示。
现在我们有了查找表通过输入ID我们可以生成嵌入向量
# 使用 token embeddings 层将输入的 token IDs 转换为嵌入向量
inputs_embeds token_emb(inputs.input_ids)# 打印输入嵌入向量的形状
inputs_embeds.size()
运行结果
torch.Size([1, 5, 768])
这给我们提供了一个形状为[batch_sizeseq lenhidden_dim]的张量就像我们在第2章中看到的一样。这里我们将推迟位置编码因此下一步是创建query、key和value向量并使用点积作为相似度函数来计算注意力分数
import torch
from math import sqrt # 定义 query、key 和 value 都为输入的嵌入向量
query key value inputs_embeds# 获取 key 的最后一个维度的大小用于缩放点积注意力分数
dim_k key.size(-1)# 计算注意力分数使用缩放点积注意力
# torch.bmm 是批量矩阵乘法将 query 和 key 的转置相乘并按 sqrt(dim_k) 缩放
scores torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)# 打印注意力分数张量的形状
scores.size()运行结果
torch.Size([1, 5, 5])
这产生了一个5×5矩阵其中包含批量中每个样本的注意力分数。稍后我们将看到query、key和value向量是通过将独立的权重矩阵WQ应用到嵌入中生成的但这里为简单起见我们将它们设为相等。在缩放点积注意力中点积按照嵌入向量的大小进行缩放这样我们在训练过程中就不会得到太多的大数从而可以避免下一步要应用的softmax饱和。
,K,V
torch.bmm()函数执行批量矩阵乘积简化了注意力分数的计算过程其中query和key向量的形状为[batch_sizeseq_lenhidden_dim]。如果我们忽略批处理维度那么我们可以通过简单地转置key张量以使其形状为[hidden dimseq len]然后使用矩阵乘积来收集所有在[seq_lenseq_len]矩阵中的点积。由于我们希望对批处理中的所有序列独立地执行此操作因此我们使用torch.bmm()它接收两个矩阵批处理并将第一个批处理中的每个矩阵与第二个批处理中的相应矩阵相乘。
接下来我们应用softmax
import torch.nn.functional as F# 对注意力分数进行 softmax 操作得到注意力权重
weights F.softmax(scores, dim-1)# 计算注意力权重在最后一个维度上的和
weights_sum weights.sum(dim-1)# 打印注意力权重在最后一个维度上的和
weights_sum
运行结果
tensor([[1., 1., 1., 1., 1.]], grad_fnSumBackward1)
最后将注意力权重与值相乘
# 使用注意力权重 weights 对 value 进行加权求和得到注意力输出
attn_outputs torch.bmm(weights, value)# 打印注意力输出张量的形状
attn_outputs.shape
运行结果
torch.Size([1, 5, 768])
这就是全部了我们已经完成了简化形式的自注意力机制实现的所有步骤请注意整个过程仅涉及两个矩阵乘法和一个softmax因此你可以将“自注意力”视为一种花哨的平均形式。
我们把这些步骤封装成一个函数以便以后我们可以重用它
def scaled_dot_product_attention(query, key, value):# 获取 key 的最后一个维度的大小用于缩放点积注意力分数dim_k query.size(-1)# 计算缩放点积注意力分数scores torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)# 对注意力分数进行 softmax 操作得到注意力权重weights F.softmax(scores, dim-1)# 使用注意力权重对 value 进行加权求和得到注意力输出return torch.bmm(weights, value)我们的注意力机制在query向量和key向量相等的情况下会给上下文中相同的单词分配非常高的分数特别是给当前单词本身query向量与自身的点积总是1。而实际上一个单词的含义将更好地受到上下文中其他单词的影响而不是同一单词甚至自身。以前面的句子为例通过结合“time”和“arrow”的信息来定义“flies”的含义比重复提及“flies”要更好。那么我们如何实现这点
我们可以让模型使用三个不同的线性投影将初始词元向量投影到三个不同的空间中从而允许模型为query、key和value创建一个不同的向量集。 1.3 多头注意力
前面提到我们将query、key和value视为相等来计算注意力分数和权重。但在实践中我们会使用自注意力层对每个嵌入应用三个独立的线性变换以生成query、key和value向量。这些变换对嵌入进行投影每个投影都带有其自己的可学习参数这使得自注意力层能够专注于序列的不同语义方面。
同时拥有多组线性变换通常也是有益的每组变换代表一种所谓的注意力头。多头注意力层如下图所示。但是为什么我们需要多个注意力头原因是一个注意力头的softmax函数往往会集中在相似度的某一方面。拥有多个头能够让模型同时关注多个方面。例如一个头负责关注主谓交互而另一个头负责找到附近的形容词。显然我们没有在模型中手工制作这些关系它们完全是从数据中学习到的。如果你对计算机视觉模型熟悉你可能会发现其与卷积神经网络中的滤波器相似其中一个滤波器负责检测人脸而另一个滤波器负责在图像中找到汽车的车轮。 现在我们来编码实现首先编写一个单独的注意力头的类
class AttentionHead(nn.Module):def __init__(self, embed_dim, head_dim):super().__init__()# 定义线性变换用于生成 query, key 和 valueself.q nn.Linear(embed_dim, head_dim)self.k nn.Linear(embed_dim, head_dim)self.v nn.Linear(embed_dim, head_dim)def forward(self, hidden_state):# 通过线性变换计算 query, key 和 valuequery self.q(hidden_state)key self.k(hidden_state)value self.v(hidden_state)# 计算缩放点积注意力attn_outputs scaled_dot_product_attention(query, key, value)# 返回注意力输出return attn_outputs
这里我们初始化了三个独立的线性层用于对嵌入向量执行矩阵乘法以生成形状为[batch_sizeseq_lenhead_dim]的张量其中head_dim是我们要投影的维数数量。尽管head_dim不一定比词元的嵌入维数embed_dim小但在实践中我们选择head_dim是embed_dim的倍数以便跨每个头的计算能够保持恒定。例如BERT有12个注意力头因此每个头的维数为768/1264。
现在我们有了一个单独的注意力头因此我们可以将每个注意力头的输出串联起来来实现完整的多头注意力层
class MultiHeadAttention(nn.Module):def __init__(self, config):super().__init__()# 获取嵌入维度和注意力头的数量embed_dim config.hidden_sizenum_heads config.num_attention_heads# 计算每个注意力头的维度head_dim embed_dim // num_heads# 创建多个注意力头self.heads nn.ModuleList([AttentionHead(embed_dim, head_dim) for _ in range(num_heads)])# 定义输出的线性变换层self.output_linear nn.Linear(embed_dim, embed_dim)def forward(self, hidden_state):# 对每个注意力头进行前向传播并在最后一个维度上拼接结果x torch.cat([h(hidden_state) for h in self.heads], dim-1)# 通过线性变换层对拼接结果进行变换x self.output_linear(x)# 返回最终的多头注意力输出return x
请注意注意力头连接后的输出也通过最终的线性层进行馈送以生成形状为[batch_sizeseq_lenhidden_dim]的输出张量以适用于下游的前馈网络。为了确认我们看看多头注意力层是否产生了我们输入的预期形状。在初始化MultiHeadAttention模块时我们传递了之前从预训练的BERT模型中加载的配置。这确保我们使用与BERT相同的设置
# 创建 MultiHeadAttention 实例传入配置对象
multihead_attn MultiHeadAttention(config)# 进行前向传播计算多头注意力的输出
attn_output multihead_attn(inputs_embeds)# 打印注意力输出张量的形状
attn_output.size()运行结果
torch.Size([1, 5, 768])
这么做是可行的最后我们再次使用BertViz可视化单词“flies”的两个不同用法的注意力。这里我们可以使用BertViz的head_view()函数通过计算预训练checkpoint的注意力并指示句子边界的位置来显示注意力
# 从 bertviz 导入 head_view 函数
from bertviz import head_view
# 从 transformers 导入 AutoModel 类
from transformers import AutoModel# 加载预训练的 BERT 模型并启用输出注意力
model AutoModel.from_pretrained(model_ckpt, output_attentionsTrue)# 定义两个句子
sentence_a time flies like an arrow
sentence_b fruit flies like a banana# 使用 tokenizer 对两个句子进行编码
viz_inputs tokenizer(sentence_a, sentence_b, return_tensorspt)# 通过模型进行前向传播获取注意力
attention model(**viz_inputs).attentions# 计算第二个句子在输入序列中的起始位置
sentence_b_start (viz_inputs.token_type_ids 0).sum(dim1)# 将输入 IDs 转换为 tokens
tokens tokenizer.convert_ids_to_tokens(viz_inputs.input_ids[0])# 使用 head_view 函数可视化注意力
head_view(attention, tokens, sentence_b_start, heads[8])运行结果 这种可视化展示了注意力权重表现为连接正在被更新嵌入的词元左侧与所有被关注的单词右侧之间的线条。线条的颜色深度表现了注意力权重的大小深色线条代表值接近于1淡色线条代表值接近于0。
在这个例子中输入由两个句子组成[CLS]和[SEP]符号是我们在第2章中遇到的BERT的词元分析器中的特殊符号。从可视化结果中我们可以看到注意力权重最大的是属于同一句子的单词这表明BERT能够判断出它应该关注同一句子中的单词。然而对于单词“flies”我们可以看到BERT已经识别出在第一句中“arrow”是重要的在第二句中“fruit”和“banana”是重要的。这些注意力权重使模型能够根据它所处的上下文来区分“flies”到底应该为动词还是名词
至此我们已经讲述完注意力机制了我们来看一下如何实现编码器层缺失的一部分位置编码前馈神经网络。 2、 前馈层
编码器和解码器中的前馈子层仅是一个简单的两层全连接神经网络但有一点小小的不同它不会将整个嵌入序列处理为单个向量而是独立处理每个嵌入。因此该层通常称为位置编码前馈神经网络。有时候你还会看到它又被称为内核大小为1的1维卷积这种叫法通常来自具有计算机视觉背景的人例如OpenAI GPT代码库就是这么叫的。论文中的经验法则是第一层的隐藏尺寸应为嵌入尺寸的四倍并且最常用的激活函数是GELU。这是大部分容量和记忆发生的地方也是扩展模型时最经常进行缩放的部分。我们可以将其实现为一个简单的nn.Module如下所示
# 定义 FeedForward 类继承自 nn.Module
class FeedForward(nn.Module):def __init__(self, config):super().__init__()# 定义第一个线性层将输入从隐藏层大小映射到中间层大小self.linear_1 nn.Linear(config.hidden_size, config.intermediate_size)# 定义第二个线性层将输入从中间层大小映射回隐藏层大小self.linear_2 nn.Linear(config.intermediate_size, config.hidden_size)# 定义 GELU 激活函数self.gelu nn.GELU()# 定义 Dropout 层防止过拟合self.dropout nn.Dropout(config.hidden_dropout_prob)def forward(self, x):# 通过第一个线性层x self.linear_1(x)# 通过 GELU 激活函数x self.gelu(x)# 通过第二个线性层x self.linear_2(x)# 通过 Dropout 层x self.dropout(x)# 返回输出return x需要注意的是像nn.Linear这样的前馈层通常应用于形状为batch_sizeinput_dim的张量上它将独立地作用于批量维度中的每个元素。这对于除了最后一个维度之外的任何维度都是正确的因此当我们将形状为batch_sizeseq_lenhidden_dim的张量传给该层时该层将独立地应用于批量和序列中的所有词元嵌入这正是我们想要的。我们可以通过传递注意力输出来测试这一点
# 创建一个 FeedForward 类的实例
feed_forward FeedForward(config)# 通过前馈神经网络计算注意力输出
ff_outputs feed_forward(attn_outputs)# 打印前馈神经网络输出的张量尺寸
ff_outputs.size()
运行结果
torch.Size([1, 5, 768])
现在我们已经拥有了创建完整的Transformer编码器层的所有要素唯一剩下的部分是决定在哪里放置跳跃连接和层规范化。我们看看这会如何影响模型架构。 添加层规范化 如前所述Transformer架构使用了层规范化和跳跃连接。前者将批处理中的每个输入规范化为零均值和单位方差。跳跃连接直接将张量传给模型的下一层而不做处理只将其添加到处理的张量中。在将层规范化放置在Transformer的编码器或解码器层中时论文提供了两种选项 后置层规范化 这是Transformer论文中使用的一种结构它把层规范化置于跳跃连接之后。这种结构从头开始训练时会比较棘手因为梯度可能会发散。因此在训练过程中我们经常会看到一个称为学习率预热的概念其中学习率在训练期间从一个小值逐渐增加到某个最大值。 前置层规范化 这是论文中最常见的布局它将层规范化置于跳跃连接之前。这样做往往在训练期间更加稳定并且通常不需要任何学习率预热。 这两种方式的区别如图所示 Transformer编码器层中层规范化的两种方式 这里我们将使用第二种方式因此我们可以简单地将我们的基本构件粘在一起如下所示
# 创建一个 Transformer 编码器层的实例
encoder_layer TransformerEncoderLayer(config)# 打印输入嵌入的形状和通过编码器层后的输出形状
inputs_embeds.shape, encoder_layer(inputs_embeds).size()运行结果
(torch.Size([1, 5, 768]), torch.Size([1, 5, 768]))
在更高层面的术语中自注意力层和前馈层称为置换等变的——如果输入被置换那么层的相应输出将以完全相同的方式置换。
我们已经成功地从头开始实现了我们的第一个Transformer编码器层然而我们设置编码器层的方式存在一个问题它们对于词元的位置是完全不变的。由于多头注意力层实际上是一种精致的加权和因此词元位置的信息将丢失 。
幸运的是有一种简单的技巧可以使用位置嵌入来整合位置信息。我们来看看。 3、位置嵌入
位置编码基于一个简单但非常有效的想法用一个按向量排列的位置相关模式来增强词元嵌入。如果该模式对于每个位置都是特定的那么每个栈中的注意力头和前馈层可以学习将位置信息融合到它们的转换中。
有几种实现这个目标的方法其中最流行的方法之一是使用可学习的模式特别是在预训练数据集足够大的情况下。这与仅使用词元嵌入的方式完全相同但是使用位置索引作为输入而不是词元ID。通过这种方法在预训练期间可以学习到一种有效的编码词元位置的方式。
我们创建一个自定义的Embeddings模块它将输入的input_ids投影到密集的隐藏状态上并结合Position_ids的位置嵌入进行投影。最终的嵌入层是两个嵌入层的简单求和
class Embeddings(nn.Module):def __init__(self, config):super().__init__()# 定义 token 嵌入层self.token_embeddings nn.Embedding(config.vocab_size, config.hidden_size)# 定义位置嵌入层self.position_embeddings nn.Embedding(config.max_position_embeddings,config.hidden_size)# 定义层归一化self.layer_norm nn.LayerNorm(config.hidden_size, eps1e-12)# 定义 dropout 层self.dropout nn.Dropout()def forward(self, input_ids):# 为输入序列创建位置 IDseq_length input_ids.size(1)position_ids torch.arange(seq_length, dtypetorch.long).unsqueeze(0)# 创建 token 嵌入和位置嵌入token_embeddings self.token_embeddings(input_ids)position_embeddings self.position_embeddings(position_ids)# 合并 token 嵌入和位置嵌入embeddings token_embeddings position_embeddings# 进行层归一化embeddings self.layer_norm(embeddings)# 进行 dropoutembeddings self.dropout(embeddings)return embeddings# 实例化 Embeddings 类
embedding_layer Embeddings(config)# 调用 embedding_layer传入 inputs[input_ids]并获取其输出的大小
embedding_layer(inputs[input_ids]).size()
运行结果
torch.Size([1, 5, 768])
我们可以看到嵌入层现在为每个词元创建了一个密集的嵌入。
这种可学习的位置嵌入易于实现并广泛使用除此之外还有其他一些方法 绝对位置表示 Transformer模型可以使用由调制正弦和余弦信号组成的静态模式来编码词元的位置。当没有大量数据可用时这种方法尤其有效。 相对位置表示 通过结合绝对和相对位置表示的想法旋转位置嵌入在许多任务上取得了优秀的结果。GPT-Neo是一个采用旋转位置嵌入的模型的例子。 尽管绝对位置很重要但有观点认为在计算嵌入时周围的词元最为重要。相对位置表示遵循这种直觉对词元之间的相对位置进行编码。这不能仅通过在开头引入新的相对嵌入层来设置因为相对嵌入针对每个词元会因我们对序列的访问位置的不同而不同。对此注意力机制本身通过添加额外项来考虑词元之间的相对位置。像DeBERTa等模型就使用这种表示 。
现在我们把所有内容整合起来通过将嵌入与编码器层结合起来构建完整的Transformer编码器
class TransformerEncoder(nn.Module):def __init__(self, config):super().__init__()# 初始化 Embeddings 层self.embeddings Embeddings(config)# 创建 num_hidden_layers 个 TransformerEncoderLayer并将它们存储在 ModuleList 中self.layers nn.ModuleList([TransformerEncoderLayer(config) for _ in range(config.num_hidden_layers)])def forward(self, x):# 通过嵌入层获取输入的嵌入表示x self.embeddings(x)# 依次通过每一层 Transformer 编码层for layer in self.layers:x layer(x)# 返回编码后的表示return x
我们检查编码器的输出形状
# 实例化 TransformerEncoder 类
encoder TransformerEncoder(config)# 调用 encoder 的 forward 方法传入 inputs[input_ids]并获取其输出的大小
encoder(inputs[input_ids]).size()
运行结果
torch.Size([1, 5, 768])
我们可以看到在每个批量中我们为每个词元获取了一个隐藏状态。这种输出格式使得架构非常灵活我们可以很容易地适应各种应用例如在掩码语言建模中预测缺失的词元或者在问答中预测回答的起始和结束位置。 4、添加分类头
Transformer模型通常分为与任务无关的主体和与任务相关的头。我们将在第4章讲述Hugging Face Transformers库的设计模式时会再次提到这种模式。到目前为止我们所构建的都是主体部分的内容如果我们想构建一个文本分类器那么我们还需要将分类头附加到该主体上。每个词元都有一个隐藏状态但我们只需要做出一个预测。有几种方法可以解决这个问题。一般来说这种模型通常使用第一个词元来进行预测我们可以附加一个dropout和一个线性层来进行分类预测。下面的类对现有的编码器进行了扩展以用于序列分类
class TransformerForSequenceClassification(nn.Module):def __init__(self, config):super().__init__()# 实例化 TransformerEncoder 类self.encoder TransformerEncoder(config)# 定义 dropout 层防止过拟合self.dropout nn.Dropout(config.hidden_dropout_prob)# 定义分类器层将隐藏状态映射到分类标签self.classifier nn.Linear(config.hidden_size, config.num_labels)def forward(self, x):# 将输入传递给 encoder并选择 [CLS] 标记的隐藏状态x self.encoder(x)[:, 0, :]# 应用 dropout 层x self.dropout(x)# 应用分类器层x self.classifier(x)return x
在初始化模型之前我们需要定义希望预测的类数目
# 设置分类标签的数量
config.num_labels 3
# 实例化 TransformerForSequenceClassification 类
encoder_classifier TransformerForSequenceClassification(config)
# 传递输入数据并获取输出的尺寸
encoder_classifier(inputs.input_ids).size()
运行结果
torch.Size([1, 3])
这正是我们一直在寻找的。对于批处理中的每个样本我们会得到输出中每个类别的非规范化logit值。这类似于我们在第2章中使用的BERT模型用于检测推文中的情感。
至此我们对编码器以及如何将其与具体任务的头相组合的部分就结束了。现在我们将注意力双关语转向解码器。