Transformer
模型 2017 年出自于 Google Brain 研究小组 Ashish Vaswani 等人发布的论文《Attention is all you need》中,是一种在自然语言处理(NLP)及其他序列到序列(Seq2Seq)任务中广泛使用的深度学习模型框架。
以下是小组各成员的贡献,名单顺序随机。Jakob 建议以 self-attention 取代 RNN,并开始努力评估这一想法。Ashish 与 Illia 一起设计并实现了第一个 Transformer 模型,并在这项工作中的各个方面起着至关重要的作用。Noam 提出了 scaled dot-product attention, multi-head attention 和参数无关的位置表示,并成为涉及几乎每个细节的另一个人。Niki 在我们原始的代码库和 tensor2tensor 中设计、实现、调优和评估了无数模型变体。Llion 还尝试了新的模型变体,负责我们的初始代码库以及高效的推理和可视化。Lukasz 和 Aidan 花了无数漫长的时间来设计和实现 tensor2tensor 的各个部分,以取代我们之前的代码库,从而大大改善了结果并极大地加速了我们的研究。
Transformer
架构最初旨在用于训练语言翻译模型,然而,2018 年 OpenAI 团队发现,Transformer
架构是字符预测的关键解决方案。一旦对整个互联网数据进行了训练,该模型就有可能理解任何文本的上下文,并连贯地完成任何句子,就像人类一样。
Transformer
模型由两部分组成:编码器 (Encoder) 和解码器 (Decoder)。一般来说,仅编码器 (encoder-only) 架构擅长从文本中提取信息,用于分类和回归等任务,而仅解码器 (decoder-only) 模型专门用于生成文本。例如,专注于文本生成的 ChatGPT 属于仅解码器 (decoder-only) 模型的范畴。
让我们在训练模型时浏览一下架构的关键思想。
下图是类似 ChatGPT 的纯解码器 (decoder-only) 转换器架构的训练过程:
- 首先,我们需要一系列输入的字符作为训练数据,这些输入被转换为 vector embedding 格式。
- 接下来,将位置编码添加到 vector embedding 中,以捕捉序列中每个字符的位置。
- 随后,该模型通过一系列计算操作处理这些输入嵌入,最终为给定的输入文本生成可能的下一个字符的概率分布。
- 模型将预测结果与训练数据集中的实际后续字符进行对比,相应地调整概率或“权重”。
- 最后,模型通过迭代优化这一过程,不断更新其参数,以提高未来预测的准确性。
Tokenization (令牌化) 是 Transformer 模型的第一步:
它将输入的句子转换成数字表示的格式。
Tokenization 是将文本分割成更小的单元,称为 tokens(令牌),这些可以是单词、次单词、短语或字符。因为将短语分解成更小的部分有助于模型识别文本的底层结构,并更有效地处理它。
例如:
Chapter 1: Building Rapport and Capturing
上面的句子可能会被切割为如下的 token :
Chapter, , 1, :, , Building, , Rap, port, , and, , Capturing
这些 token 会被转换为相应的数字:
[26072, 220, 16, 25, 17283, 23097, 403, 220, 323, 220, 17013, 220, 1711]
如您所见,数字 220 表示空格。有很多方法可以将字符标记为整数,对于示例的数据集,我们将使用 tiktoken 库。
为了演示,我将使用一个包含 460k 个字符的小型教科书数据集 sales-textbook.txt(来自 Hugging Face )进行训练。
我们的训练数据中包含 3771 个不同字符的词汇量 (Vocab size),用于标记我们的数据集的最大数字是 100069 (max_token_value),它映射到一个单词 Clar 。
一旦有了标记化映射,我们就可以为数据集中的每个字符找到相应的整数索引。将使用这些分配的整数索引作为前进的 token,而不是在与模型交互时使用整个单词。
首先,让我们构建一个包含词汇表中所有字符的查找表。本质上,这个表由一个充满随机初始化数字的矩阵组成。
我们最大令牌数 (max_token_value) 为 100069 ,考虑到维度为 64(原始论文使用 512 个维度,表示为 d_model),得到的查找表变成了一个 100069 × 64 的矩阵,这被称为 Token Embedding Look-up Table (令牌嵌入查找表)。
陈述如下:
Token Embedding Look-Up Table:
0 1 2 3 4 ... 59 60 61 62 63
0 0.625765 0.025510 0.954514 0.064349 -0.502401 ... -0.403228 -0.274928 1.473840 0.068826 1.332708
1 -0.497006 0.465756 -0.257259 -1.067259 0.835319 ... -1.464924 -0.557690 -0.693927 -0.325247 1.243933
2 1.347121 1.690980 -0.124446 -1.682366 1.134614 ... 1.219090 0.097527 -0.978587 -0.432050 -1.493750
3 1.078523 -0.614952 -0.458853 0.567482 0.095883 ... 1.080655 -2.215207 0.203201 -1.115814 -1.258691
4 0.814849 -0.064297 1.423653 0.261726 -0.133177 ... 1.313207 -0.334949 0.149743 1.306531 -0.046524
... ... ... ... ... ... ... ... ... ... ... ...
100064 -0.898191 -1.906910 -0.906910 1.838532 2.121814 ... 1.228242 0.368963 1.058280 0.406413 -0.326223
100065 1.354992 -1.203096 -2.184551 -1.745679 -0.005853 ... -0.487176 -0.421959 0.490739 -1.056457 2.636806
100066 -0.436116 0.450023 -1.381522 0.625508 0.415576 ... -0.797004 0.144952 -0.279772 1.522029 -0.629672
100067 0.147102 0.578953 -0.668165 -0.011443 0.236621 ... -1.744829 0.637790 -1.064455 1.290440 -1.110520
100068 0.415268 -0.345575 0.441546 -0.579085 1.110969 ... -0.404213 -0.012741 1.333426 0.372255 0.722526
[100,069 rows x 64 columns]
其中每一行表示一个字符(由其标记号索引),每一列表示一个维度。
目前,您可以将“维度”视为角色的特征或方面。在我们的例子中,我们指定了64个维度,这意味着我们将能够以64种不同的方式理解一个字符的文本含义,例如将其归类为名词、动词、形容词等。
假设,现在我们有一个 context_length 为 16 的训练输入示例,即:
" . By mastering the art of identifying underlying motivations and desires, we equip ourselves with "
现在,我们通过使用每个 tokenized character (or word) 的整数索引查找嵌入表 (embedding table) 来检索其嵌入向量 (input embeddings)。因此,我们得到了它们各自的 input embeddings :
[ 627, 1383, 88861, 279, 1989, 315, 25607, 16940, 65931, 323, 32097, 11, 584, 26458, 13520, 449]
在 transformer 架构中,多个输入序列同时并行处理,通常称为批处理。让我们将 batch_size 设置为 4 。因此,我们将同时处理四个随机选择的句子作为我们的输入。
Input Sequence Batch:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 627 1383 88861 279 1989 315 25607 16940 65931 323 32097 11 584 26458 13520 449
1 15749 311 9615 3619 872 6444 6 3966 11 10742 11 323 32097 13 3296 22815
2 13189 315 1701 5557 304 6763 374 88861 7528 10758 7526 13 4314 7526 2997 2613
3 323 6376 2867 26470 1603 16661 264 49148 627 18 13 81745 48023 75311 7246 66044
[4 rows x 16 columns]
每一行代表一个句子,每列是该句子第 0 到第 15 位的字符。
结果,我们现在有一个表示 4 个批次 16 个字符输入的矩阵。这个 tokens 矩阵的形状是 (batch_size,context_length) = [4, 16] 。
简而言之,我们之前将 input embedding 查找表定义为大小为 100069 × 64 的矩阵。下一步是将我们的输入序列矩阵映射到这个 embedding matrix (嵌入矩阵) 上,以获得我们的 input embedding 。
input embedding 矩阵的形状是 (batch_size, context_length, d_model) = [4, 16, 64] 。
位置编码是 transformer 架构中最具挑战性的概念,这里我们仅介绍原始的位置编码。
总结一下位置编码解决了什么问题:
- 希望每个 token 都包含一些关于它在句子中位置的信息。
- 希望该模型将彼此靠近的单词视为“接近”,将距离较远的单词视为“遥远”。
- 希望 Positional Encoding 能够表示模型可以学习的模式。
Positional Encoding 描述了实体在序列中的位置,以便为每个位置分配一个唯一的表示。
Positional Encoding 是添加到每个 token 的 input embedding 中的另一个数字向量。位置编码是正弦波和余弦波,其频率根据标记字符的位置而变化。
在原始论文中,介绍的计算位置编码的方法是:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中 pos 是位置,i 是从 0 到 dmodel / 2 。dmodel 是我们在训练模型时定义的模型维度(在我们的例子中是 64,在原始论文中他们使用 512)。
事实上,这个 Positional Encoding 矩阵是一个常量。
让我们来看看 Positional Encoding 矩阵:
Position Embedding Look-Up Table:
0 1 2 3 4 ... 59 60 61 62 63
0 0.000000 1.000000 0.000000 1.000000 0.000000 ... 1.000000 0.000000 1.000000 0.000000 1.000000
1 0.841471 0.540302 0.681561 0.731761 0.533168 ... 1.000000 0.000178 1.000000 0.000133 1.000000
2 0.909297 -0.416147 0.997480 0.070948 0.902131 ... 1.000000 0.000356 1.000000 0.000267 1.000000
3 0.141120 -0.989992 0.778273 -0.627927 0.993253 ... 1.000000 0.000533 1.000000 0.000400 1.000000
4 -0.756802 -0.653644 0.141539 -0.989933 0.778472 ... 1.000000 0.000711 1.000000 0.000533 1.000000
5 -0.958924 0.283662 -0.571127 -0.820862 0.323935 ... 0.999999 0.000889 1.000000 0.000667 1.000000
6 -0.279415 0.960170 -0.977396 -0.211416 -0.230368 ... 0.999999 0.001067 0.999999 0.000800 1.000000
7 0.656987 0.753902 -0.859313 0.511449 -0.713721 ... 0.999999 0.001245 0.999999 0.000933 1.000000
8 0.989358 -0.145500 -0.280228 0.959933 -0.977262 ... 0.999998 0.001423 0.999999 0.001067 0.999999
9 0.412118 -0.911130 0.449194 0.893434 -0.939824 ... 0.999998 0.001600 0.999999 0.001200 0.999999
10 -0.544021 -0.839072 0.937633 0.347628 -0.612937 ... 0.999997 0.001778 0.999998 0.001334 0.999999
11 -0.999990 0.004426 0.923052 -0.384674 -0.097276 ... 0.999997 0.001956 0.999998 0.001467 0.999999
12 -0.536573 0.843854 0.413275 -0.910606 0.448343 ... 0.999996 0.002134 0.999998 0.001600 0.999999
13 0.420167 0.907447 -0.318216 -0.948018 0.855881 ... 0.999995 0.002312 0.999997 0.001734 0.999998
14 0.990607 0.136737 -0.878990 -0.476839 0.999823 ... 0.999995 0.002490 0.999997 0.001867 0.999998
15 0.650288 -0.759688 -0.968206 0.250154 0.835838 ... 0.999994 0.002667 0.999996 0.002000 0.999998
[16 rows x 64 columns]
我们将 position embedding 的数字可视化,可以得到下图:
每条垂直线是我们从 0 到 64 的维度;每一行代表一个字符。这些值在 -1 和 1 之间,因为它们来自正弦和余弦函数。较暗的颜色表示值更接近 -1,较亮的颜色更接近 1 ,绿色表示介于两者之间的值。
我们将 position embedding 跟 inupt embedding 简单的相加,就获得了 Final Input Embedding 矩阵。该矩阵的形状是:(batch_size, context_length, d_model) = [4, 16, 64] 。
注:现在大多数开源的 LLMs 基本上采用的是 RoPE
(旋转位置编码,Rotary Positional Encoding) 。
Transformer Block 是一个如下三层组成的 stack:
一个 masked multi-head attention 、两个 normalization layers (归一化层) 和一个 feed-forward network (前馈网络)。
masked multi-head attention 是一组 self-attentions,每一个都被称为一个 head 。那么,让我们先看看自关注机制。
为了进行注意力计算,Q, K, V的计算公式: