简介

本文详细解析了大语言模型核心组件——注意力机制的工作原理与实现方法。文章从简化版自注意力机制入手,逐步讲解注意力得分计算、softmax归一化和上下文向量生成过程,并通过代码示例展示了完整的实现流程。注意力机制解决了长序列建模中信息丢失的问题,使模型能够关注输入序列中不同位置的重要性差异,是理解LLM的关键所在。


之前我学们习了如何准备输入文本以训练 LLM。这包括将文本拆分为单个单词和子词 token,这些 token 可以被编码为向量,即所谓的嵌入,以供 LLM 使用。本节我们将关注 LLM 架构中的重要组成部分,即注意力机制。

注意力机制是一个复杂的话题,因此。我们将注意力机制作为独立模块来研究,重点关注其内部的工作原理。我们将编写与自注意力机制相关的 LLM 的部分,以观察其实际运作并创建一个生成文本的模型。

上图中展示的这些不同的注意力变体是逐步构建的,其目标是实现一个简单且高效的多头注意力机制,以便以后可以将其整合到我们将编写的 LLM 架构中。

首先我们将从注意力机制的简化版本开始,了解全局的概念。

然后我们从简化的注意力机制转向真正的自注意力机制,然后我们将添加在当前 LLM 的解码器中使用因果自注意力机制。最后我们将实现多头注意力机制。

长序列建模的问题

开发自注意力机制是为了解决其他类型的神经网络的一个缺点。在深入了解自注意力机制之前(这是大语言模型的核心),让我们先探讨一下缺乏注意力机制的架构存在哪些问题。

假设我们想要开发一个将一种语言翻译成另一种语言的翻译模型,如下图所示,不能只是简单地逐词翻译文本,因为源语言和目标语言的语法结构往往存在差异。

为了解决逐词翻译的局限性,通常使用包含两个子模块的深度神经网络,即所谓的编码器(encoder)和解码器(decoder)。编码器的任务是先读取并处理整个文本,然后解码器生成翻译后的文本。

介绍 Transformer 架构时,我们已经简要讨论过编码器-解码器网络。在 Transformer 出现之前,循环神经网络(RNN)是最流行的用于语言翻译的编码器-解码器架构。

循环神经网络(RNN)是一种神经网络类型,其中前一步的输出会作为当前步骤的输入,使其非常适合处理像文本这样的序列数据。如果您不熟悉 RNN 的工作原理,不必担心,您无需了解 RNN 的详细机制也可以参与这里的讨论;这一节学习的重点更多是编码器-解码器架构的总体概念。

在编码器-解码器架构的 RNN 网络中,输入文本被输入到编码器中,编码器按顺序处理文本内容。

在每个步骤中,编码器会更新其隐状态(即隐藏层的内部值),试图在最终的隐状态中捕捉整个输入句子的含义,如下图所示。

随后,解码器使用该最终隐状态来开始逐词生成翻译句子。解码器在每一步也会更新其隐状态,用于携带生成下一个词所需的上下文信息。

我们无法真正查看完整输入的句子,只能拥有中间的状态 ,可能会导致信息的丢失,从而导致翻译的效果不理想。因此自注意力机制理念就是,在生成文本时,对于每个步骤,无论是翻译文本还是回答问题。我们都可以在输入中获得信息,我们可以查看整个文本的输入,获取文本的信息。

通过注意力机制捕捉数据依赖关系

在自注意力机制中,“self”指的是该机制通过关联同一输入序列中的不同位置来计算注意力权重的能力。它评估并学习输入内部各部分之间的关系和依赖性,例如句子中的单词或图像中的像素。这与传统注意力机制不同,传统机制关注的是两个不同序列间的关系,例如序列到序列模型中,注意力可能存在于输入序列和输出序列之间。

如果我们有一个翻译任务,我们正在翻译下一个 token。 在翻译下一个 token 之前,我们不止是查看前一个 token,还可以查看所有其他输入 token,可以访问用户输入的所有 token。但是更重要的是,我们要给他选择性的访问权限。以下图中文本翻译为例:

  1. 当句子被翻译输出“you” 时,他会查看所有输入的句子。
  2. 但是在每个输入的 token 中,对当前的要翻译的 token 的输出有不同的重要性。
  3. 例如 “Kannst” 具有中等重要性,“du” 具有较高重要性,其他的单词则具有较小重要性。
  4. 在类似的情况下 LLM 会自动执行这种操作, 对输入的 token 分配不同的权重。 这些者重要性权重就是所谓的注意力分数。

自注意力机制是一种允许输入序列中的每个位置在计算序列表示时关注同一序列中所有位置的机制。自注意力机制是基于 Transformer 架构的当代大语言模型(如 GPT 系列模型)的关键组成部分。本文将实现注意力机制。

之前我们已经对输入的数据做了处理,将用户输入的文本转换为对应的向量。现在我们假设我们处理完之后的数据直接进入到自注意力模块。

😝 一直在更新,更多的大模型学习和面试资料已经上传带到CSDN的官方了,有需要的朋友可以扫描下方二维码免费领取【保证100%免费】👇👇

在这里插入图片描述

通过自注意力机制关注输入的不同部分

现在我们将深入了解自注意力机制的内部工作原理,并从零开始学习如何实现它。自注意力机制是基于 Transformer 架构的所有大语言模型的核心。一旦掌握了它的基本原理,你就攻克了大语言模型实现中最困难的部分之一。

由于自注意力机制对于初次接触的读者可能显得较为复杂,我们将在下一小节中首先介绍一个简化版的自注意力机制。随后,我们将实现带有可训练权重的自注意力机制,这种机制被用于大语言模型(LLM)中。

简化的自主力机制

举例来说,假设输入文本为 “Your journey starts with one step”。在这个例子中,序列中的每个元素(如 )对应一个 d 维的嵌入向量,用于表示特定的 token,例如 “Your”。这些输入向量显示为 3 维的嵌入向量。

在自注意力机制中,我们的目标是为输入序列中的每个元素} 计算其对应的上下文向量 。上下文向量可以被解释为一种增强的嵌入向量,它包含所有的输入信息。所有的输入,它们都参与生成这里的输出向量。每个输入的重要性不同。可以看到下图中的 值是不同的。

在自注意力机制中,上下文向量起着关键作用。它们的目的是通过整合序列中所有其他元素的信息(如同一个句子中的其他词),为输入序列中的每个元素创建丰富的表示,正如上图所示。

这对大语言模型至关重要,因为模型需要理解句子中各个词之间的关系和关联性。之后我们将添加可训练的权重,以帮助大语言模型学习构建这些上下文向量,用于执行生成下一个词的任务。

下面代码是一个张量,以上图中的 “Your journey starts with one step” 为输入。这里的每一行都是与该 token 对应的嵌入向量,为了简单起见,我们假设每单单词都是一个 token(这取决与分词器的算法)。

import torch

inputs = torch.tensor(
  [[0.43, 0.15, 0.89], # Your     (x^1)
   [0.55, 0.87, 0.66], # journey  (x^2)
   [0.57, 0.85, 0.64], # starts   (x^3)
   [0.22, 0.58, 0.33], # with     (x^4)
   [0.77, 0.25, 0.10], # one      (x^5)
   [0.05, 0.80, 0.55]] # step     (x^6)
)

我们从输入到这个上下文向量,我们有中间权重,但我们不知道如何计算,因此,我们将一步一步的计算它。将计算过程分解为更小的步骤,一获得上下文向量。

实现自注意力机制的第一步是计算中间值 ω,即注意力得分。如下图所示。(请注意,图中展示的输入张量值是截断版的,例如,由于空间限制,0.87 被截断为 0.8。在此截断版中,单词 “journey” 和 “starts” 的嵌入向量可能会由于随机因素而看起来相似)。

上图展示了如何计算查询 token 与每个输入 token 之间的中间注意力得分。我们通过计算查询 与每个其他输入 token 的点积来确定这些得分:

#第二个输入 token 用作查询向量

query = inputs[1]                                               #A
attn_scores_2 = torch.empty(inputs.shape[0])
for i, x_i in enumerate(inputs):
    attn_scores_2[i] = torch.dot(x_i, query)
print(attn_scores_2)

接下来,我们对先前计算的每个注意力分数进行归一化。

attn_weights_2_tmp = attn_scores_2 / attn_scores_2.sum()

print("Attention weights:", attn_weights_2_tmp)
print("Sum:", attn_weights_2_tmp.sum())

如输出所示,现在注意力权重的总和为 1:

在实践中,更常见且更推荐使用 softmax 函数来进行归一化。这种方法更擅长处理极端值,并且在训练过程中提供了更有利的梯度特性。以下是用于归一化注意力分数的 softmax 函数的基础实现。

defsoftmax_naive(x):
return torch.exp(x) / torch.exp(x).sum(dim=0)

attn_weights_2_naive = softmax_naive(attn_scores_2)

print("Attention weights:", attn_weights_2_naive)
print("Sum:", attn_weights_2_naive.sum())

从输出中可以看到,softmax 函数可以实现注意力权重的归一化,使它们的总和为 1:

此外,softmax 函数确保注意力权重始终为正值。这使得输出可以被解释为概率或相对重要性,其中较高的权重表示更重要。

注意,这种简单的 softmax 实现(softmax_naive)在处理较大或较小的输入值时,可能会遇到数值不稳定性问题,例如上溢或下溢。因此,实际操作中,建议使用 PyTorchsoftmax 实现,它经过了充分的性能优化:

attn_weights_2 = torch.softmax(attn_scores_2, dim=0)

print("Attention weights:", attn_weights_2)
print("Sum:", attn_weights_2.sum())

可以看到,它与我们之前实现的 softmax_naive 函数产生的结果相同。

现在我们已经计算出了归一化的注意力权重,接下来可以执行下图所示的最后一步:通过将嵌入后的输入 token, 与相应的注意力权重相乘,再将所得向量求和来计算上下文向量 。

我们再次获得输入,然后对于每个输入,我们将其 分别与每个输入的注意力权重相乘。 然后我们将所有这些向量相加,就得到了上下文向量。

再次强调一下上下文向量的思想是:结合了所有输入向量的信息,具有更高注意力权重的向量在输出中发挥更大的作用。

query = inputs[1] # 2nd input token is the query

context_vec_2 = torch.zeros(query.shape)
for i,x_i in enumerate(inputs):
    context_vec_2 += attn_weights_2[i]*x_i

print(context_vec_2)
为所有输入的 token 计算注意力权重

之前我们做的是关注其中一个输入作为查询,然后计算查询与其他所有输入让之间的注意力权重。然后我们根据第二个输入查询上下文向量,就是我们图片中展示的 ,这里是针对第二个查询的上下文向量。它包含虽有有关的输入信息。

接下来我们将在这里计算所有其他输入注意力权重,上图是一个 6 * 6 的矩阵,它适用与每个输入 ,我们有六个输入,计算每个输入与其他所有输入的注意力权重。这就是自注意力(与输入的序列相关)。 我们沿用之前的三个步骤(如下图所示),只是对代码做了一些修改,用于计算所有的上下文向量,而不仅仅是第二个上下文向量 。

我们要计算这里的这些值,其中每个 ij 代表这里的每个单元格。 我们实现一个双重 for 循环,计算每个输入和每个输入之间的点积 。

attn_scores = torch.empty(6, 6)

for i, x_i in enumerate(inputs):
for j, x_j in enumerate(inputs):
       attn_scores[i, j] = torch.dot(x_i, x_j)

print(attn_scores)

在上述代码中,我们使用了 Python 中的 for 循环来计算所有输入对的注意力得分。然而,for 循环通常较慢,我们可以通过矩阵乘法实现相同的结果。

 attn_scores = inputs @ inputs.T

接下来开始执行步骤 2,我们现在对每一行进行归一化处理,使得每一行的值之和为 1。attn_scores 是注意力分数,attn_weights 我们称为注意力权重。

attn_weights = torch.softmax(attn_scores, dim=1)
attn_weights

在使用 PyTorch 时,像 torch.softmax 这样的函数中的 dim 参数指定了将在输入张量中的哪个维度上进行归一化计算。通过设置 dim=-1,我们指示 softmax 函数沿着 attn_scores 张量的最后一个维度进行归一化操作。如果 attn_scores 是一个二维张量(例如,形状为 [行数, 列数]),则 dim=-1 将沿列方向进行归一化,使得每一行的值(沿列方向求和)之和等于 1。

在继续执行第 3 步之前,我们先简单验证一下每一行的总和是否确实为 1:

row_2_sum = sum([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])
print("Row 2 sum:", row_2_sum)
print("All row sums:", attn_weights.sum(dim=-1))

输出:

Row 2 sum: 1.0
All row sums: tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000])

在第 3 步也是最后一步中,我们使用这些注意力权重通过矩阵乘法的方式来并行计算所有的上下文向量:

all_context_vecs = attn_weights @ inputs
print(all_context_vecs)

nsor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000])


在第 3 步也是最后一步中,我们使用这些注意力权重通过矩阵乘法的方式来并行计算所有的上下文向量:

```plaintext
all_context_vecs = attn_weights @ inputs
print(all_context_vecs)

以上内容完成了对简化自注意力机制的代码演示。在后面的文章,我们将添加可训练的权重,使大语言模型能够从数据中学习并提升其在特定任务上的性能。

读者福利:如果大家对大模型感兴趣,这套大模型学习资料一定对你有用

对于0基础小白入门:

如果你是零基础小白,想快速入门大模型是可以考虑的。

一方面是学习时间相对较短,学习内容更全面更集中。
二方面是可以根据这些资料规划好学习计划和方向。

包括:大模型学习线路汇总、学习阶段,大模型实战案例,大模型学习视频,人工智能、机器学习、大模型书籍PDF。带你从零基础系统性的学好大模型!

😝 一直在更新,更多的大模型学习和面试资料已经上传带到CSDN的官方了,有需要的朋友可以扫描下方二维码免费领取【保证100%免费】👇👇

在这里插入图片描述

👉AI大模型学习路线汇总👈

大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

👉大模型实战案例👈

光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

在这里插入图片描述

👉大模型视频和PDF合集👈

观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

在这里插入图片描述

在这里插入图片描述

👉学会后的收获:👈

• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

👉获取方式:

😝 一直在更新,更多的大模型学习和面试资料已经上传带到CSDN的官方了,有需要的朋友可以扫描下方二维码免费领取【保证100%免费】👇👇
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐