随着大语言模型(LLM)技术的飞速发展,越来越多的应用开始渗透到我们的工作和日常生活中。从智能助手、自动翻译到内容生成,LLM 已经成为推动人工智能发展的关键技术之一。这些技术不仅影响着企业和科研领域,还在教育、医疗、金融等多个行业引发了深远的变革。
LLM背后的原理到底是什么?如何理解日常应用中(提示词工程)的现象与本质?在这篇文章里,我们一起来看看。
如果你希望从零开始通过编码的方式去深入学习一个类GPT的大模型如何实现,强烈建议学习这个开源项目:https://github.com/skindhu/Build-A-Large-Language-Model-CN
词嵌入(Embedding)
LLM一般无法直接处理原始的文本数据,因为文本属于离散数据,它与实现和训练神经网络所需的数学运算并不兼容。因此,我们需要一种方法将文本数据中的词(token)转换为数学上的连续值向量,这个转换过程就称为嵌入(Embedding)。在一个多维向量中,其中的每个数值都记录了某方面的信息,维度越多所包含的信息则越多。最小的 GPT-2 模型(117M 和 125M 参数)使用 768 维的嵌入大小,而最大的 GPT-3 模型(175B 参数)则使用 12288 维的嵌入大小。
那到底应该如何将一段段原始的文本语料变成一个嵌入向量?
以 GPT-3 为例,它的训练语料体量达到惊人的 45TB,涵盖了海量的文本数据。在正式训练之前,这些原始文本需要经过一个关键的预处理步骤:分词。分词的目标是将连续的文本切分为单词、子词和特殊字符的组合,统称为 token。分词不仅能使模型更高效地处理文本,还能通过细粒度的拆分捕获更丰富的语言信息.
每个大语言模型(LLM)都有一个专属的词汇表(vocabulary),词汇表中包含了模型能够识别和使用的所有 token。分词后的文本会根据词汇表将每个 token 映射为一个唯一的 token ID,这些 ID 是模型处理文本的基础。
通过这种分词和映射,庞大的自然语言被高效地转换为模型可以直接处理的向量表示,为后续的训练过程奠定了基础。下图展示了这个流程。
而分词是一项复杂而精巧的过程,尤其在处理像 GPT 这样的高性能大语言模型时,其目标是将自然语言切分为模型可处理的基本单元(token)。GPT 采用了一种称为字节对编码(Byte Pair Encoding,BPE) 的分词方法,这种方法以其高效性和灵活性在大语言模型中被广泛应用。
字节对编码通过统计训练语料中的词频,迭代地将最常见的字符或子词组合成新的子词单元。最终,BPE 不仅能有效减少词汇表的大小,还能灵活应对不同语言和未见过的单词,确保模型在各种语言场景下都能实现高效的分词和文本处理。下面通过一个示例简要介绍字节对编码的原理:
假设有句子: "The cat drank the milk because it was hungry"
- 初始化: BPE会先将句子中每个字符视为一个单独的token
['T', 'h', 'e', ' ', 'c', 'a', 't', ' ', 'd', 'r', 'a', 'n', 'k', ' ', 't', 'h', 'e', ' ', 'm', 'i', 'l', 'k', ' ', 'b', 'e', 'c', 'a', 'u', 's', 'e', ' ', 'i', 't', ' ', 'w', 'a', 's', ' ', 'h', 'u', 'n', 'g', 'r', 'y']
- 统计最常见的字节对: BPE算法会在这些token中找到出现频率最高的“字节对”(即相邻的两个字符),然后将其合并为一个新的token。例如这里最常见的字节对时('t', 'h'),因为它在单词"the"和"that"中出现频率较高
- 合并字节对: 根据统计结果,我们将最常见的字节对('t', 'h')合并为一个新的token,其它类似
['Th', 'e', ' ', 'c', 'a', 't', ' ', 'dr', 'a', 'nk', ' ', 'th', 'e', ' ', 'm', 'i', 'l', 'k', ' ', 'be', 'c', 'a', 'u', 'se', ' ', 'it', ' ', 'wa', 's', ' ', 'hu', 'n', 'gr', 'y']
- 重复上述步骤: 重复步骤2和3,得到最终的token序列
['The', ' ', 'cat', ' ', 'drank', ' ', 'the', ' ', 'milk', ' ', 'because', ' ', 'it', ' ', 'was', ' ', 'hungry']
在完成分词后,我们会将生成的 token 序列映射为模型可处理的token ID序列,这些 token ID 会进一步被转化为嵌入向量(embedding vectors)。嵌入层会将离散的 token ID 映射到一个高维连续空间中,以捕捉 token 的语义信息。
Transformer中的注意力模型
要了解大语言模型的原理,注意力机制一个绕不开的话题,而且占有非常大的权重。在注意力机制出现之前,深度神经网络(特别是循环神经网络 RNN)被广泛用于处理自然语言处理(NLP)任务。然而,它们在序列建模中存在一些明显的局限性。
假设我们有一个长句子: "The cat, who was sitting on the windowsill, jumped down because it saw a bird flying outside the window."
我们的任务是要预测句子最后的内容,即要理解"it"指的是"the cat"而不是"the windowsill"或其它内容。对于RNN来说,这个任务是有难度的,原因如下:
- 长距离依赖问题: 在 RNN 中,每个新输入的词会被依次传递到下一个时间步。随着句子长度增加,模型的隐状态会不断被更新,但早期信息(如“the cat”)会在层层传播中逐渐消失。因此,模型可能无法在“it”出现时有效地记住“the cat”是“it”的指代对象。
- 梯度消失问题 :RNN 在反向传播中的梯度会随着时间步的增加逐渐减小,这种“梯度消失”使得模型很难在长句中保持信息的准确传播,从而难以捕捉到长距离的语义关联。
为了弥补 RNN 的这些不足,注意力机制 被引入。它的关键思想是在处理每个词时,不仅依赖于最后的隐藏状态,而是允许模型直接关注序列中的所有词 。这样,即使是较远的词也能在模型计算当前词的语义时直接参与。在上例中,注意力机制如何帮助模型理解“it”指代“the cat”呢?接下来我们来详细介绍注意力机制。
注意力机制的核心特点是,在一个语句中,每个token需要关注其它所有token与自己的相关性,这个相关性使用一个数值类型的注意力权重来表示,值越高表示相关性越高,这种直接的关注能力让模型能够高效捕捉长距离依赖关系。
那注意力得分是如何计算出来的呢?还是拿 "The cat drank the milk because it was hungry"
举例,我们一步步来拆解。
- 首先我们还是对原始语句进行token拆分,假设结果如下:
['The', ' ', 'cat', ' ', 'drank', ' ', 'the', ' ', 'milk', ' ', 'because', ' ', 'it', ' ', 'was', ' ', 'hungry']
- 将每个token转换为Embedding向量(为了解释的可读性,我们暂时忽略掉token转 token ID的过程, token ID是个整数,易读性差)
-
The
-> Embedding向量E1
-
cat
-> Embedding向量E2
-
drank
-> Embedding向量E3
-
……
- 添加位置编码
由于注意力机制不保留词的顺序信息(但是顺序信息很重要),GPT模型会为每个词添加位置编码,这可以帮助模型理解词在序列中的相对位置。
-
例如:
E1 + Pos1
,E2 + Pos2
,……
- 为每个Token生成Q、K、V向量
-
Q向量(查询向量) :查询向量代表了这个词在寻找相关信息时提出的问题。
-
K向量(键向量) :键向量代表了一个单词的特征,或者说是这个单词如何"展示"自己,以便其它单词可以与它进行匹配。
-
V向量(值向量) :值向量携带的是这个单词的具体信息,也就是当一个单词被"注意到"时,它提供给关注者的内容。
更通俗的理解: 想象我们在图书馆寻找一本书( Q向量
),我们知道要找的主题( Q向量
),于是查询目录( K向量
),目录告诉我哪本书涉及这个主题,最终我找到这本书并阅读内容( V向量
),获取了我需要的信息。
具体生成Q、K、V向量的方式主要通过线性变换:
-
Q1 = W_Q * (E1 + Pos1)
-
K1 = W_K * (E1 + Pos1)
-
V1 = W_V * (E1 + Pos1)
-
依次类推,为所有token生成
Q
,K
,V
向量,其中W_Q
,W_K
和W_V
是Transformer训练出的权重(每一层不同)
- 计算每个token的上下文向量
- 针对每一个目标token,Transformer会计算它的
Q向量
与其它所有的token的K向量
的点积,以确定每个词对当前词的重要性(即相似度分数)
- 例如对于词
cat
的Q向量 Q_cat
,模型会计算:
score_cat_the = Q_cat · K_the
--- 与the
的语义相关度score_cat_drank = Q_cat · K_drank
--- 与drank
的语义相关度score_cat_it = Q_cat · K_it
--- 与it
的语义相关度- 依此类推,得到
cat
与句子中其它所有token的相似度分数[score_cat_the、score_cat_drank、socre_cat_it、……]
归一化相似度分数
使用softmax函数将这些分数归一化为注意力权重,使它们转化为概率分布(总和为1),这些权重决定了模型在当前步骤中应该多大程度地关注每个词
- 例如:
attention_wights = softmax([score_cat_the、score_cat_drank、socre_cat_it、……])
得到[α_1、α_2、α_3、……、α_n]
- 注意:在实际应用中,注意力分布概率往往是稀疏的,即大部分注意力集中在少数几个
token
上
- 计算上下文向量
- 针对每个token,使用注意力权重对所有词的`V向量进行加权求和,生成当前词的上下文向量
-
例如
context_cat = α_1 * V1 + α_2 * V2 + ... + α_n * Vn
-
这个上下文向量包含了
Prompt
中所有token的信息,但其中最重要的信息由那些具有较高注意李权重的token来提供
- 多头注意力的堆叠
上述的流程是针对一个语句中的每个token去计算它的上下文向量,但其实Transformer中针对token的上下文向量会并行计算多次,每个并行计算模块称为一个注意力头。而上述第 4 步用于计算
K、V、Q向量
的矩阵
W
也会拆分多不同的注意力头中。使用多头注意力的原因如下:
- 多样化关注 :多个注意力头允许模型在不同的子空间中独立计算注意力得分,每个注意力头可以专注于不同的语义特征,如短距离依赖、长距离依赖、语法结构、语义关系等,这样模型就能从多个角度捕捉到输入序列中的丰富信息。
- 并行计算: 通过多个注意力头,模型能够同时处理输入序列中的不同部分,避免了逐层处理的瓶颈。
- 假设我们有一句复杂的句子:
The cat sat on the mat because it was soft, but then it moved because it was hungry.
-
单一注意力头可能得问题:如果只有一个注意力头,模型可能会在处理代词“it”时遇到困难,因为“it”在句子中有不同的指代对象(“mat”和“cat”)。单一头可能无法同时捕捉到这些复杂的指代关系。
-
多头注意力的优势:多个注意力头可以同时处理不同的语义层次。某些头可能专注于短距离依赖关系,如“it”和“mat”之间的关系;而另一些头可能关注长距离依赖关系,如“it”和“cat”之间的关系。这样,模型能够更准确地理解句子中的复杂结构,并生成更符合语境的输出。
- 基于最终的
MultiHeadOutput
预测下一个词
- 在生成任务中,GPT基于当前的上下文向量以及当前词来预测下一个词的概率分布(线性层 + softmax),并根据temprature值来预测下一个词。
- 例如当前一个词是
cat
,GPT结合当前的上下文向量以及cat这个词来预测下一个词是drank
- temprature是为了增加采样的随机性,提升GPT的创造能力
- 逐步生成后续token
- 新生成的token会被加入到
prompt
中,更新后的序列重新执行前面的流程来生成下一个token,这一过程重复执行,直到生成完整的响应。
注意: 与普通的自注意力机制不同,GPT其实使用的是称为Causual Attention的因果注意力机制,它与普通自注意力机制的区别是它限制每个 token
只能关注到它之前的token,不能看到未来的token。这种机制确保了模型在生成时不会泄漏未来信息,保持生成过程的因果性(即当前 token
的生成只依赖于过去的信息)
- 基于优质的指令数据微调来实现对话能力
Transormer模型本质上是一个token预测模型,要实现ChatGPT这种对话能力,还需要通过一批优质的对话数据对模型进行微调:
- 准备好用于微调的数据集合
- 使用上述对话数据集对预训练好的GPT模型进行微调,预训练的数据集已经学习好了语言模型的基础能力,包括语法、词汇、语言结构,可以相对准确的预测下一个token。而微调则是利用特定领域的数据来让模型适应某些特定的任务。
- 微调默认情况下会调整所有权重,但由于权重已经经过预训练,大多数情况下,微调只会对预训练权重进行微小调整,而不是大幅度改变。这种方式能够让模型保持原有的语言生成能力,同时使其在特定任务上表现得更好。
- 冻结部分权重的微调,一般冻结低层(往往是学习到的基础语言特征),对高层的权重进行调整。一般用于加速训练,或者数据量较小,全权重微调可能导致过拟合的情况下使用。
- GPT的训练任务是通用的语言建模任务,主要是根据已有上下文预测下一个token,这种训练让模型具备了基础的语言生成能力。但是ChatGPT的对话任务不仅要求生成连贯的文本,还要求模型理解对话上下文、维护多轮对话的一致性、处理指令性任务,并在某些场景下具备情感理解能力。为了解决这些更复杂的需求,模型必须对大量的对话数据进行全模型微调,以调整所有的权重,使模型不仅在语言生成上有良好的表现,也能在对话上下文中生成更加相关和符合语境的回复。
通过以上的步骤,就可以生成一个类ChatGPT的一个对话大模型。了解了大致原理,我们再来思考日常使用ChatGPT时需要注意的一些问题,是不是已经有了解释。
日常应用中的现象与本质
1. GPT为什么对Prompt的长度有限制?
-
自注意力机制的计算复杂度: 在Transformer中,自注意力机制是模型的核心,它需要每个token与序列中的其它token计算语义相关度(计算点积),对于一个长度为
N
的Prompt,计算复杂度为O(N^2)
,随着Prompt长度的增加,计算量会急剧增加。 -
内存资源限制 :计算注意力时,需要将整个Prompt的Embedding、Q/K/V向量都存储在内存中,而且这些数据之间的相似度计算也需要占用大量内存。Prompt越长,所需的内存也越多。
-
训练数据特点 :在GPT训练过程中,模型的参数都是针对一定的输入长度进行优化的,如果训练时的最大长度是4k tokens,那么模型的所有参数(例如位置编码、层的权重等)都是根据这个长度调整的。在推理阶段,如果输入长度超过了这个范围,模型将无法正确地处理这些超出部分。
2. 当通过一个比较长的Prompt来设计一个复杂的任务时,为何模型生成的质量往往不好(不稳定)?
-
信息衰减: 当我们通过一个长Prompt去设计一个复杂的任务时,虽然自注意力可以捕捉长距离依赖,但在非常长的序列中,模型可能会因为上下文太过复杂而难以保持对相关信息的有效关注(注意力打分后转换的概率分布往往是稀疏的),这会导致生成质量下降。
-
在较长的Prompt中,模型的注意力往往会集中在距离较近或上下文关联更强
token
上,而对距离较远的token
,注意力得分可能会被极大稀释,导致信息衰减。 -
信息噪声累积 :在长Prompt中,有些是有价值的信息,有些是次要或背景信息,但是
prompt
中的每个token都会计算与其它所有token
的相关度,当信息量过大时,可能会导致高价值信息被稀释,最终降低生成结果的质量。
3. 如何提升模型生成质量的稳定性?
- 信息排列方式优化 :GPT总是结合前面的语句序列去预测下一个token,并在计算注意力时更关注距离较近的token,对远距离的token可能存在信息衰减的问题。我们在设计Prompt时,可以把更重要的信息放在尾部,而一些次要信息放在头部。例如我们经常把对任务输出格式的约束放在尾部(要求模型遵守),而任务的背景放在头部。
- 控制信息量 :避免在Prompt中堆砌过多的信息,尤其是重复或细枝末节的内容,保持对Prompt的简洁有助于模型更好的理解和生成相关内容。
- 复杂任务的合理拆分 :将一个复杂的任务合理拆分成多个子任务,逐个执行,每个子任务的执行结果(或其它信息)作为上下文作为下一个子任务的输入,由于子任务的价值信息密度高,GPT在计算注意力时更容易聚焦,导致生成的质量更稳定。
结语
随着 LLM 技术的广泛应用,掌握其基础原理和实现方法将成为每一位 AI 从业者必备的技能。通过学习和研究大语言模型,我们不仅能更好地理解当前的技术发展,还能为未来的创新和突破奠定基础。这里推荐一个开源项目:从零开始编码实现一个类GPT的大模型。它从零开始通过编码的方式带我们了解如何准备和清理训练数据、分词、词嵌入、Transformer架构的实现、模型精调、实现指令遵循等,对于大模型的理解非常有帮助。