莱莱 发表于 2024-8-18 21:11:40

AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [

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 等。
[*]这些模型经过预训练,可以直接用于各种下游任务,如文天职类、情感分析、命名实体辨认等。

[*] 利用和定制:

[*]用户可以轻松加载预训练模型,进行微调或直接利用。
[*]库中提供了丰富的工具和方法,用于模型训练、评估和部署。

[*] 管道(Pipeline)API:

[*]Transformers 库的 Pipeline API 简化了常见任务的实现过程,如文天职类、问答系统、文本天生等。
[*]用户只需几行代码即可完成复杂的 NLP 任务。

Transformer 架构在 Hugging Face 的应用


[*]预训练和微调: Hugging Face 提供了大量预训练的 Transformer 模型,这些模型在大型数据集上进行预训练,并可以在小数据集上进行微调,适应特定任务。
[*]社区和模型库: Hugging Face Hub 上有成千上万的模型,由社区成员和 Hugging Face 提供,这些模型可以被下载、利用、评估和分享。
[*]集成和部署: Transformers 库支持在各种平台和环境中部署 Transformer 模型,包罗云服务、边缘设备和移动设备。
通过 Hugging Face,研究职员和开辟者可以快速地利用 Transformer 架构的强大功能,解决各种 NLP 任务,并推动 NLP 技能的发展。

三、Transformer架构

原始 Transformer 是基于编码器-解码器架构的,该架构广泛用于机器翻译等任务中,即将一个单词序列从一种语言翻译成另一种语言。
   该架构由两个组件构成:


[*]编码器
将一个词元的输入序列转化为一系列嵌入向量,通常被称为隐藏状态或上下文。


[*]解码器
利用编码器的隐藏状态,逐步天生一个词元的输出序列,每次天生一个词元。
而编码器息争码器又由如图所示的几个构建块构成。
   https://img-blog.csdnimg.cn/direct/1e72cdee2789445d93191e331d8c76d8.jpeg   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): 前馈神经网络通常利用 ReLU(Rectified Linear Unit)作为激活函数。

[*]残差连接和层归一化:

[*]残差连接(Residual Connection): 通过将每个子层的输入与其输出相加,有助于制止梯度消失问题,使得模型更容易训练。
[*]层归一化(Layer Normalization): 对每个子层的输出进行归一化处理,加快训练并进步模型的泛化本领。

编码器在 Transformer 中的应用


[*]特性提取和表现: 编码器负责从输入序列中提取和学习关键的特性表现,这些表现可以用于各种 NLP 任务,如文天职类、命名实体辨认和机器翻译等。
[*]模型堆叠和预训练: 多层编码器的堆叠以及与解码器的联合,使得 Transformer 模型在大规模预训练和微调任务中表现出色,成为当前领域的主流模型之一。
[*]灵活性和性能: 编码器结构的灵活性使得它能够处理差别长度和范例的输入序列,同时其并行化处理也显著进步了模型的计算服从。
编码器是 Transformer 架构中负责将输入序列转换为高级抽象表现的关键组件,它通过多层编码器层、自注意力机制和前馈神经网络来实现对序列信息的有效编码和表现。
Transformer的编码器由许多编码器层相互堆叠而成。如图所示
   https://img-blog.csdnimg.cn/direct/47074fbbb20e4129a48e4fa9a2983451.jpeg   编码器层放大图    每个编码器层吸取一系列嵌入,然后通过以下子层进行馈送处理:
●一个多头自注意力层。
●一个全连接前馈层,应用于每个输入嵌入。
每个编码器层的输出嵌入尺寸与输入嵌入相同,我们很快就会看到编码器堆叠的主要作用是“更新”输入嵌入,以产生编码一些序列中的上下文信息的表现。比方,假如单词“苹果”附近的单词是“主题演讲”或“电话”,那么该单词将被更新为更像“公司”的特性,而不是更像“水果”的特性。
这些子层同样利用跳跃连接和层规范化,这些是训练深度神经网络的常用技巧。但要想真正理解Transformer的工作原理,我们还需要更深入地研究。我们从最紧张的构建模块开始:自注意力层。

1、自注意力机制

注意力机制是一种神经网络为序列中的每个元素分配差别权重或“注意力”的机制。对文本序列来说,元素则为我们在之前遇到的词元嵌入(此中每个词元映射为固定维度的向量)。比方,在BERT中,每个词元表现为一个768维向量。自注意力中的“自”指的是这些权重是针对同一组隐藏状态计算的,比方编码器的所有隐藏状态。与自注意力相对应的,与循环模型相干的注意力机制则计算每个编码器隐藏状态对于给定解码时间步的解码器隐藏状态的相干性。
 https://latex.csdn.net/eq?x_%7B1%7D%5E%7B%27%7D%2C...x_%7Bn%7D%5E%7B%27%7D%2Cx_%7Bf%7D%5E%7B%27%7D
自注意力的主要思想是,不是利用固定的嵌入值来表现每个词元,而是利用整个序列来计算每个嵌入值的加权平均值。另一种表述方式是说,给定词元嵌入的序列x,…,x,自注意力产生新的嵌入序列,此中每个是所有x的线性组合:
1nj
https://latex.csdn.net/eq?x_%7Bi%7D%5E%7B%27%7D%3D%5Csum_%7Bj%3D1%7D%5E%7Bn%7Dw_%7Bji%7Dx_%7Bj%7D
左边的flies为飞的意思,所以上下文嵌入与表现飞的soars相近。右边的flies为苍蝇的意思,所以上下文嵌入与表现昆虫的insect相近。
此中的系数wji称为注意力权重,其被规范化以使得∑jwji=1。假如想要相识为什么平均词元嵌入大概是一个好主意,那么可以这样思考,当你看到单词“flies”时会想到什么。也许你会想到令人讨厌的昆虫,但是假如你得到更多的上下文,比如“time flies like an arrow”,那么你会意识到“flies”表现的是动词。同样地,我们可以通过以差别的比例联合所有词元嵌入来创建“flies”的表现形式,也许可以给“time”和“arrow”的词元嵌入分配较大的权重wji。用这种方式天生的嵌入称为上下文嵌入,早在Transformer发明之前就存在了,比方ELMo语言模型。如图展示了这一过程,我们通过自注意力根据上下文天生了“flies”的两种差别表现。
   https://img-blog.csdnimg.cn/direct/841fa53d70c748f3822aa90721f1afa2.jpeg   自注意力将原始的词元嵌入(顶部)更新为上下文嵌入(底部),从而创建包含了整个序列信息的表现    现在我们看一下注意力权重是怎样计算的。

1.1 缩放点积注意力
实现自注意力层的方法有好几种,但最常见的是那篇著名的Transformer架构论文 所先容的缩放点积注意力(scaled dot-product attention)。要实现这种机制,需要四个主要步调:
1)将每个词元嵌入投影到三个向量中,分别称为query、key和value。
2)计算注意力分数。我们利用相似度函数确定query和key向量的相干程度。顾名思义,缩放点积注意力的相似度函数是点积,并通过嵌入的矩阵乘法高效计算。相似的query和key将具有较大的点积,而那些没有相似处的则几乎没有重叠。这一步的输出称为注意力分数,在一个有n个输入词元的序列中,将对应着一个n×n的注意力分数矩阵。
3)计算注意力权重。点积在一样寻常环境下有大概会产生任意大的数,这大概会导致训练过程不稳定。为了处理这个问题,起首将注意力分数乘以一个缩放因子来规范化它们的方差,然后再通过softmax 进行规范化,以确保所有列的值相加之和为1。结果得到一个n×n的矩阵,该矩阵包含了所有的注意力权重https://latex.csdn.net/eq?w_%7Bji%7D。
https://latex.csdn.net/eq?x_%7Bi%7D%5E%7B%27%7D%3D%5Csum_%7Bj%7D%5E%7B%7Dw_%7Bji%7D%5E%7B%7Dv_%7Bj%7D
4)更新词嵌入。计算完注意力权重之后,我们将它们与值向量v,…,v相乘,终极获得词嵌入表现。
1n
我们可以利用一个很赞的库,Jupyter 的 BertViz(https://oreil.ly/eQK3I),来可视化以上的注意力权重计算过程。该库提供了一些可用于可视化Transformer模型的差别方面注意力的函数。假如想可视化注意力权重,我们可以利用 neuron_view 模块,该模块跟踪权重计算的过程,以显示怎样将query向量和key向量相联合以产生终极权重。由于BertViz需要访问模型的注意力层,因此我们将利用BertViz中的模型类来实例化我们的BERT checkpoint,然后利用show()函数为特定的编码器层和注意力头天生交互式可视化。请注意,你需要单击左侧的“+”才能激活注意力可视化。
(注意安装 bertviz:pip 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_mode="light", layer=0, head=8)
# 显示BERT模型在第0层、第8个注意力头的注意力权重 运行结果:
https://img-blog.csdnimg.cn/direct/d3d780e02a584e6da4c7c7bc37b88ea3.png
https://img-blog.csdnimg.cn/direct/c66b6f25c02d417db63d0529ba8ae0f5.png

1.2 query、key和value的神秘面纱
在你第一次打仗query、key和value向量的概念时,大概会以为这些概念有点艰涩难懂。这些概念受到信息检索系统的启发,但我们可以用一个简单的类比来表明它们的含义。你可以这样想象,你正在超市购买晚餐所需的所有食材。你有一份食谱,食谱里面每个食材可以视为一个query。然后你会扫描货架,通过货架上的标注(key),以检查该商品是否与你列表中的食材相匹配(相似度函数)。假如匹配成功,那么你就从货架上取走这个商品(value)。
在这个类比中,你只会得到与食材匹配的商品,而忽略掉其他不匹配的商品。自注意力是这个类比更抽象和流通的版本:超市中的每个标注都与配料匹配,匹配的程度取决于每个key与query的匹配程度。因此,假如你的清单包罗一打鸡蛋,那么你大概会拿走10个鸡蛋、一个煎蛋卷和一个鸡翅。
缩放点积注意力过程的更详细细节可以如下图所示。
   https://img-blog.csdnimg.cn/direct/16f9d2719eac466e9aeebfa007b6d72d.jpeg   缩放点积注意力中的操纵    我们将利用PyTorch实现Transformer架构,利用TensorFlow实现Transformer架构的步调与之类似。两个框架中最紧张的函数之间的映射关系详见下表
https://img-blog.csdnimg.cn/direct/062328f31d8f466db7c6efe76d16445a.jpeg
我们需要做的第一件事是对文本进行词元化,因此我们利用词元分析器提取输入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_tensors="pt", add_special_tokens=False)

# 打印输入的 token IDs
inputs.input_ids 运行结果:
tensor([[ 2051, 10029,2066,2019,8612]]) 正如我们之前所看到的那样,句子中的每个词元都被映射到词元分析器的词表中的唯一ID。为了保持简单,我们还通过设置add_special_tokens=False来将和词元排除在外。接下来,我们需要创建一些密集嵌入。这里的密集是指嵌入中的每个条目都包含一个非零值。相反,我们在之前所看到的独热编码是稀疏的,因为除一个之外的所有条目都是零。在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() 这给我们提供了一个形状为的张量,就像我们在第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() 这产生了一个5×5矩阵,此中包含批量中每个样本的注意力分数。稍后我们将看到,query、key和value向量是通过将独立的权重矩阵WQ应用到嵌入中天生的,但这里为简单起见,我们将它们设为相等。在缩放点积注意力中,点积按照嵌入向量的大小进行缩放,这样我们在训练过程中就不会得到太多的大数,从而可以制止下一步要应用的softmax饱和。
,K,V
torch.bmm()函数执行批量矩阵乘积,简化了注意力分数的计算过程,此中query和key向量的形状为。假如我们忽略批处理维度,那么我们可以通过简单地转置key张量以使其形状为,然后利用矩阵乘积来网络所有在矩阵中的点积。由于我们盼望对批处理中的所有序列独立地执行此操纵,因此我们利用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([], grad_fn=<SumBackward1>) 末了将注意力权重与值相乘:
# 使用注意力权重 weights 对 value 进行加权求和,得到注意力输出
attn_outputs = torch.bmm(weights, value)

# 打印注意力输出张量的形状
attn_outputs.shape 运行结果:
torch.Size() 这就是全部了,我们已经完成了简化形式的自注意力机制实现的所有步调!请注意,整个过程仅涉及两个矩阵乘法和一个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函数通常会集中在相似度的某一方面。拥有多个头能够让模型同时关注多个方面。比方,一个头负责关注主谓交互,而另一个头负责找到附近的形容词。显然,我们没有在模型中手工制作这些关系,它们完满是从数据中学习到的。假如你对计算机视觉模型熟悉,你大概会发现其与卷积神经网络中的滤波器相似,此中一个滤波器负责检测人脸,而另一个滤波器负责在图像中找到汽车的车轮。
https://img-blog.csdnimg.cn/direct/587ff9d1c2874545b9153c66a2d7763c.jpeg
现在我们来编码实现,起首编写一个单独的注意力头的类:
class AttentionHead(nn.Module):
    def __init__(self, embed_dim, head_dim):
      super().__init__()
      # 定义线性变换,用于生成 query, key 和 value
      self.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 和 value
      query = 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 这里我们初始化了三个独立的线性层,用于对嵌入向量执行矩阵乘法,以天生形状为的张量,此中head_dim是我们要投影的维数数目。尽管head_dim不一定比词元的嵌入维数(embed_dim)小,但在实践中,我们选择head_dim是embed_dim的倍数,以便跨每个头的计算能够保持恒定。比方,BERT有12个注意力头,因此每个头的维数为768/12=64。
现在我们有了一个单独的注意力头,因此我们可以将每个注意力头的输出串联起来,来实现完整的多头注意力层:
class MultiHeadAttention(nn.Module):
    def __init__(self, config):
      super().__init__()
      # 获取嵌入维度和注意力头的数量
      embed_dim = config.hidden_size
      num_heads = config.num_attention_heads
      # 计算每个注意力头的维度
      head_dim = embed_dim // num_heads
      
      # 创建多个注意力头
      self.heads = nn.ModuleList(
            
      )
      # 定义输出的线性变换层
      self.output_linear = nn.Linear(embed_dim, embed_dim)

    def forward(self, hidden_state):
      # 对每个注意力头进行前向传播,并在最后一个维度上拼接结果
      x = torch.cat(, dim=-1)
      # 通过线性变换层对拼接结果进行变换
      x = self.output_linear(x)
      # 返回最终的多头注意力输出
      return x 请注意,注意力头连接后的输出也通过终极的线性层进行馈送,以天生形状为的输出张量,以适用于下游的前馈网络。为了确认,我们看看多头注意力层是否产生了我们输入的预期形状。在初始化MultiHeadAttention模块时,我们传递了之前从预训练的BERT模型中加载的配置。这确保我们利用与BERT相同的设置:
# 创建 MultiHeadAttention 实例,传入配置对象
multihead_attn = MultiHeadAttention(config)

# 进行前向传播,计算多头注意力的输出
attn_output = multihead_attn(inputs_embeds)

# 打印注意力输出张量的形状
attn_output.size()
运行结果:
torch.Size() 这么做是可行的!末了,我们再次利用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_attentions=True)

# 定义两个句子
sentence_a = "time flies like an arrow"
sentence_b = "fruit flies like a banana"

# 使用 tokenizer 对两个句子进行编码
viz_inputs = tokenizer(sentence_a, sentence_b, return_tensors='pt')

# 通过模型进行前向传播,获取注意力
attention = model(**viz_inputs).attentions

# 计算第二个句子在输入序列中的起始位置
sentence_b_start = (viz_inputs.token_type_ids == 0).sum(dim=1)

# 将输入 IDs 转换为 tokens
tokens = tokenizer.convert_ids_to_tokens(viz_inputs.input_ids)

# 使用 head_view 函数可视化注意力
head_view(attention, tokens, sentence_b_start, heads=)
运行结果:
https://img-blog.csdnimg.cn/direct/66242091eca940b4a824a59ed9bfc8d7.png
https://img-blog.csdnimg.cn/direct/82b572c9dd75447d8add8253d5b0a819.pnghttps://img-blog.csdnimg.cn/direct/5bfe8abf817643b0a0c89eafec614780.png
这种可视化展示了注意力权重,表现为连接正在被更新嵌入的词元(左侧)与所有被关注的单词(右侧)之间的线条。线条的颜色深度表现了注意力权重的大小,深色线条代表值靠近于1,淡色线条代表值靠近于0。
在这个例子中,输入由两个句子构成,和符号是我们在第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_size,input_dim)的张量上,它将独立地作用于批量维度中的每个元素。这对于除了末了一个维度之外的任何维度都是正确的,因此当我们将形状为(batch_size,seq_len,hidden_dim)的张量传给该层时,该层将独立地应用于批量和序列中的所有词元嵌入,这正是我们想要的。我们可以通过传递注意力输出来测试这一点:
# 创建一个 FeedForward 类的实例
feed_forward = FeedForward(config)

# 通过前馈神经网络计算注意力输出
ff_outputs = feed_forward(attn_outputs)

# 打印前馈神经网络输出的张量尺寸
ff_outputs.size() 运行结果:
torch.Size() 现在我们已经拥有了创建完整的Transformer编码器层的所有要素!唯一剩下的部分是决定在那里放置跳跃连接和层规范化。我们看看这会怎样影响模型架构。
   

[*]添加层规范化
如前所述,Transformer架构利用了层规范化和跳跃连接。前者将批处理中的每个输入规范化为零均值和单位方差。跳跃连接直接将张量传给模型的下一层,而不做处理,只将其添加到处理的张量中。在将层规范化放置在Transformer的编码器或解码器层中时,论文提供了两种选项:


[*]后置层规范化
这是Transformer论文中利用的一种结构,它把层规范化置于跳跃连接之后。这种结构从头开始训练时会比较棘手,因为梯度大概会发散。因此,在训练过程中我们常常会看到一个称为学习率预热的概念,此中学习率在训练期间从一个小值逐渐增长到某个最大值。


[*]前置层规范化
这是论文中最常见的结构,它将层规范化置于跳跃连接之前。这样做通常在训练期间更加稳定,并且通常不需要任何学习率预热。
这两种方式的区别如图所示:
   https://img-blog.csdnimg.cn/direct/15ee1c3725ec414bb1f0864b4892249c.jpeg   Transformer编码器层中层规范化的两种方式    这里我们将利用第二种方式,因此我们可以简单地将我们的根本构件粘在一起,如下所示:
# 创建一个 Transformer 编码器层的实例
encoder_layer = TransformerEncoderLayer(config)

# 打印输入嵌入的形状和通过编码器层后的输出形状
inputs_embeds.shape, encoder_layer(inputs_embeds).size()
运行结果:
(torch.Size(), torch.Size()) 在更高层面的术语中,自注意力层和前馈层称为置换等变的——假如输入被置换,那么层的相应输出将以完全相同的方式置换。
我们已经成功地从头开始实现了我们的第一个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, eps=1e-12)
      # 定义 dropout 层
      self.dropout = nn.Dropout()

    def forward(self, input_ids):
      # 为输入序列创建位置 ID
      seq_length = input_ids.size(1)
      position_ids = torch.arange(seq_length, dtype=torch.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)
      # 进行 dropout
      embeddings = self.dropout(embeddings)
      return embeddings
# 实例化 Embeddings 类
embedding_layer = Embeddings(config)

# 调用 embedding_layer,传入 inputs['input_ids'],并获取其输出的大小
embedding_layer(inputs['input_ids']).size() 运行结果:
torch.Size() 我们可以看到嵌入层现在为每个词元创建了一个密集的嵌入。
这种可学习的位置嵌入易于实现并广泛利用,除此之外,还有其他一些方法:
   

[*]绝对位置表示
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() 我们可以看到在每个批量中,我们为每个词元获取了一个隐藏状态。这种输出格式使得架构非常灵活,我们可以很容易地适应各种应用,比方在掩码语言建模中预测缺失的词元,或者在问答中预测答复的起始和竣事位置。

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,并选择 标记的隐藏状态
      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() 这正是我们一直在寻找的。对于批处理中的每个样本,我们会得到输出中每个类别的非规范化logit值。这类似于我们在第2章中利用的BERT模型(用于检测推文中的情感)。
至此我们对编码器以及怎样将其与具体任务的头相组合的部分就竣事了。现在我们将注意力(双关语!)转向解码器。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: AGI 之 【Hugging Face】 的【Transformer】的 [ Transformer 架构 ] / [