LLM 工程师入门:生成式 AI 的简易指南
编者按: 大模型发展了近两年,Baihai IDP 公众号也分享了近百篇 LLM 各环节的技术洞察,有前沿探讨、有落地实践、有应用经验。但回头来看,我们似乎从来没有认真、从 0 开始探讨过 LLM 的基本原理。
最近,一些企业客户和伙伴来询问,是否有 LLM 的从 0 到 1 的科普贴。他们说:
“虽然在很多场景中,LLM 都已经渗透入我们的工作生活,但对其内部的运作机制,仍有很多谜团待解决。
在应用落地时,LLMs 这种“黑箱式”的运作模式,不仅使我们难以完全信任这些模型的输出结果,也阻碍了我们对其进一步研究和优化的步伐。如果我们无法理解 LLMs 的工作原理,就很难评估它们的局限性,进而制定出有针对性的解决方案。”
因此,我们把这篇 LLM 基础原理文章推荐给大家。
本文为希望深入了解生成式 AI 的开发者、技术爱好者、AI 落地的领导者和研究者们编写,以通俗易懂的语言,系统地剖析了大语言模型的内部结构和训练流程,从 token、next token predictions,到马尔可夫链、神经网络等核心概念,循序渐进地揭示了 LLM 是如何生成文本的。
作者 | Miguel Grinberg
编译 | 岳扬
毫无疑问,随着大语言模型[1](LLMs)的新闻不断出现在我们的日常生活,生成式人工智能[2](GenAI)已经成为了我们无法忽视的存在。或许你早已体验过 ChatGPT[3] ,甚至把它当作日常生活的小助理了。
面对这场 GenAI 变革,许多人心中都有一个疑问:这些模型表面上的智能(intelligence)究竟源自何处?本文将试图用浅显易懂的语言,不涉及复杂数学公式,来揭秘生成式文本模型的工作原理,让你认识到它们并非魔法,而是计算机算法的产物。
01 What Does An LLM Do?
首先,我要澄清人们对大语言模型工作原理的一个重大误解。人们通常认为,这些模型能够回答我们的问题或与我们进行对话,但实际上,它们所能做的是基于我们提供的文本输入,预测下一个单词(更准确地说,是下一个 token)。
现在,让我们一步步揭开 LLMs 神秘面纱背后的“真面目”,从 token 开始探索。
1.1 Tokens
token 是大语言模型(LLM)处理文本时的基本单元。虽然我们可以简单地将 token 视为单个单词,但 LLM 的目的是以最高效的方式对文本进行编码。因此在很多情况下,token 可能是比单个单词短或长的字符序列。 标点符号和空格同样以 token 的形式存在,它们可以单独表示为一个 token,也可以与其他字符组合。
LLM 所使用的所有 token 构成了它的词汇表(vocabulary),这个词汇表能够用来表达所有可能的文本内容。大语言模型通常采用 BPE(Byte Pair Encoding)[4]算法来根据输入数据集创建 token 词汇表。以 GPT-2 语言模型[5](开源模型,可供深入研究)为例,其词汇表拥有 50,257 个 token。
每个 token 在 LLM 的词汇表中都有一个独一无二的标识符(通常是一个数字编号)。LLM 通过分词器将常规文本字符串转换为一系列 token 编号。 如果您对 Python 有所了解,并且想要尝试对 token 进行操作,可以安装 OpenAI 提供的 tiktoken 软件包:
然后请在 Python 命令行中尝试以下操作:
在本实验中,我们可以观察到,GPT-2 语言模型的词汇表中,token 464 对应的是单词"The",而 token 2068 则对应" quick",这个 token 包括了单词之前的空格。在该模型中,句号由 token 13 表示。
由于 token 是通过算法来决定的,因此我们可能会遇到一些奇特的情况,比如 GPT-2 会将单词"the"的以下三种形式编码为不同的 token:
BPE 算法并不会总是将整个单词直接转化为一个 token。事实上,那些使用频率较低的单词不会被单独表示为一个 token,而是需要通过多个 token 的组合来进行编码。下文这个例子,展示了 GPT-2 模型是如何用一个由两个 token 组成的序列来编码某个单词的:
1.2 Next Token Predictions
如上文所述,语言模型会根据给定文本预测之后可能出现的 token。如果用 Python 伪代码来展示这个过程,下文就演示了如何使用这些模型来预测下一个 token:
这个函数将用户输入的提示词转换成的 token 列表作为模型输入。这里假设每个单词都是一个单独的 token。为了简单起见,此处使用了每个 token 的文字形式,但实际上,每个 token 都是以数字的形式传递给模型的。
该函数返回的是一种特有的数据结构,它为词汇表中的每个 token 分配了一个在输入文本后紧接着出现的概率。如果使用的是 GPT-2 模型,那么返回的将是一个包含 50,257 个浮点数的列表(list),列表中每个数字代表相应 token 紧接着文本内容出现的概率。
在上述案例中,可以设想训练效果良好的语言模型会为"jumps"这个 token 分配一个较高的概率,以接续短语"The quick brown fox"[6]。同样地,如果模型训练得当,那么像"potato"这样的随机单词接在这个短语后面的概率就会低很多,几乎接近于 0。
为了做出合理的预测,语言模型需要经过一个训练过程。在训练期间,模型会学习大量文本内容。训练结束后,模型就能利用它从训练文本中构建的特有数据结构,来计算给定 token 序列的下一个 token 的概率。
这与你的预期是否有所不同?我希望现在这个概念看起来不再那么神秘了。
1.3 生成长文本序列
由于模型只能预测下一个出现的 token,因此要想让它生成完整的句子,就必须在 for 循环中多次运行模型。每一次循环迭代,都会根据返回的概率列表选择一个新的 token。这个新 token 会被加入到下一次循环迭代中模型的输入序列中,如此循环往复,一直持续到生成足够的文本为止。
下面是一个更完整的 Python 伪代码示例,演示了这个过程:
generate_text() 函数需要用户提供提示词内容,比如可以是一个问题。
tokenize() 这个辅助函数负责将用户的提示词转换成一系列 token,这个过程会用到 tiktoken 或类似的库。在 for 循环中,get_token_predictions() 函数负责调用 AI 模型,获取下一个 token 的概率列表,与上文案例中描述的过程相同。
select_next_token() 函数的作用是根据模型给出的下一个 token 的概率列表,从候选 token 中挑选出最合适的 token 来放入输入序列。这个函数可以采取最简单的方法,即选择概率最高的 token ,这在机器学习中被称为“greedy selection”。然而,为了增加生成文本的多样性,该函数通常会采用更高级的策略,使用一个随机数生成器来选择 token,即使是在随机选择 token 的情况下,也会优先选择那些概率较高的 token。通过这种方式,即便是给出相同的输入提示词,模型也能生成不同的文本响应。
为了进一步增加 token 选择过程的灵活性,可以通过调整超参数来改变 LLMs 返回的概率分布。 这些超参数为传递给文本生成函数的参数,能够帮助用户控制 token 选择过程的“greediness”(译者注:模型在选择下一个 token 时所表现出的倾向性,是倾向于选择概率最高的 token(即最可能的 token),还是允许一些不太可能的 token(即概率较低的 token)被选中。)。如果你以前经常使用 LLMs,那么就很可能对 “temperature” 超参数比较熟悉。当 temperature 值较高时,token 的概率分布会被 flattened out(译者注:模型会考虑更多的 token,包括那些概率较低的 token,使得概率分布更加均匀。),这样做的结果是,之前不太可能被选中的 token 现在有更大的机会被选中,从而使生成的文本看起来更具创造性和新颖性。除了 temperature 之外,还有两个超参数 top_p 和 top_k,它们分别用来控制在选择过程中高概率 token 被选中的数量。 通过调整这些超参数,可以进一步影响文本生成的风格和多样性。
一旦模型选定了下一个 token,循环就会继续迭代。此时,模型将接收到一个新的输入序列,这个输入序列的末尾添加了新 token(译者注:上一次迭代选择的 token)。num_tokens 参数决定了循环的迭代次数,也就是生成文本的长度。生成的文本可能会(并且经常)在句子中间结束,因为大语言模型(LLM)并没有句子或段落的概念,它只是逐个处理 token。为了防止生成的文本内容在句子中间就结束了,我们可以将 num_tokens 参数视为一个最大数量值,而不是一个确切的 token 数量。在这种情况下,我们可以在模型生成一个句号 token 时结束循环。
如果你已经阅读到此处,并且理解了前文的所有内容,那么恭喜你,你现在对 LLMs 的工作原理已经有了较为深入的理解。各位读者是否对更多技术细节感兴趣?在下一节中,我将介绍更多技术细节,同时尽量避免提及关于这项技术的复杂数学知识。
02 Model Training
不幸的是,要想不涉及数学知识就讨论模型的训练过程,实在是件不容易的事。接下来,我将首先向大家展示一种非常简单的训练方法。
我们的目标是预测 token 后面可能出现的其他 tokens,因此,训练模型的简单方法就是从训练数据集中提取所有连续的 tokens 对,然后用这些数据来构建一个概率表(table of probabilities)。
让我们用一个简短的词汇表(vocabulary)和数据集(dataset)来做这件事。假设模型的词汇表有以下五个 token :
为了使这个例子简明扼要,我们不把空格和标点符号算作 token。
我们使用以下三个句子组成的训练数据集:
I like apples
I like bananas
you like bananas
我们可以制作一个 5x5 的表格,每个单元格记录的是该行 token 后面跟着该列 token 的频次。表格如下:
该数据集中,“I like” 出现了两次,“you like” 一次,“like apples” 一次,而 “like bananas” 则是两次。
现在我们知道了训练数据集中每对 tokens 的出现频率,就可以推算出它们相互跟随的概率。做法是将表格中每一行的数字转换成概率。比如,表格里“like”这一行,我们看到它后面跟着 “apples” 一次,跟着 “bananas” 两次。这意味着在“like”之后出现 “apples” 的概率是 33.3%,而出现 “bananas” 的概率则是 66.7%。
下面是计算出所有概率的完整表格。那些空白单元格代表的概率自然就是 0%。
对于 “I”、“you” 和 “like” 这些行的概率计算比较轻松、直接,但 “apples” 和 “bananas” 这两行就有点难办了,因为数据集中并没有 “apples” 和 “bananas” 后面跟着其他 token 的情况。这就好比在我们的训练数据中出现了“缺口”。为了确保模型在遇到这种未训练的情况时也能有所输出,我决定将 “apples” 和 “bananas” 后续 token 的概率均匀分配给其他四个 tokens。这种做法可能会导致一些不太自然的输出结果,但至少模型在处理这两个 token 时不会卡壳。
训练数据中的这种“缺口”问题不容小觑。在真正的 LLMs 中,由于训练数据集规模非常庞大,我们不太可能遇到像本文这个简单例子中这么明显的缺口。但是,由于训练数据的覆盖面不足,那些较小的、较难发现的缺口确实是存在的,并且相当常见。 LLMs 在这些训练不足的区域所做的 tokens 预测质量可能会不高,而且这些问题往往不易被察觉。这也是 LLMs 有时会出现“幻觉[7]”(即生成的文本虽然朗朗上口,但可能包含与事实不符的内容或前后矛盾之处。)的原因之一。
借助上面的概率表,你现在可以构思一下 get_token_predictions() 函数的实现方法。以下是用 Python 伪代码表示的一种实现方式:
是不是比想象中还要简单些呢?这个函数可以接收来自用户提示词的 tokens 序列。它提取出这个序列中的最后一个 token ,并找到概率表中对应的那一行。
假如你用 [‘you’, ‘like’] 作为 input tokens 调用这个函数,它会给出 “like” 对应的那一行,这使得 “apples” 有 33.3% 的可能性放入句子的下一部分,而 “bananas” 则有 66.7% 的可能性。根据这些概率值可以得知,上面提到的 select_next_token() 函数在三次中有一次会挑选 “apples”。
当 “apples” 被选为 “you like” 的后续词时,我们得到了句子 “you like apples”。这个句子在训练数据集中并不存在,但它却是完全合理的。希望这个小例子能让您开始意识到,这些模型是如何通过重新利用训练中学到的 patterns(译者注:在数据集中识别和学习的重复出现的 tokens 序列组合。) 和拼接不同的信息片段,来形成看似原创的想法或概念的。
2.1 上下文窗口(The Context Window)
在上一部分,我用来训练那个微型语言模型的方法,称为马尔可夫链[8](译者注:Markov chain,一种数学系统,用于描述一系列可能的事件,其中每个事件的发生概率只依赖于前一个事件。)。
这种技术存在的问题是,它只根据一个 token(输入序列中的最后一个 token)来预测接下来的内容。在此之前的文本在决定如何延续文本内容时并不起作用,因此我们可以认为这种方法的上下文窗口仅限于一个 token,这是非常小的。由于上下文窗口太小,模型会不断“忘记”自己的思路,从一个词跳到下一个词,显得杂乱无章。
为了提高模型的预测能力,我们可以构建一个更大的概率表。如果想使用两个 token 的上下文窗口,就需要在概率表中添加代表所有两个 token 组合的新行。以前文的例子来说,五个 token 将会在概率表增加 25 行两个 token 组合的新行,再加上原有的 5 个单 token 行。这次除了考虑两个 token 的组合外,还需要考虑三个 token 的组合,因此必须再次训练模型。在 get_token_predictions() 函数的每次循环迭代中,如果条件允许,将使用输入序列的最后两个 tokens 来在一个更大的概率表中找到与这两个 tokens 相对应的行。
但是,两个 tokens 的上下文窗口仍然不够。为了让生成的文本不仅在结构上能够自洽,还能在一定程度上具备一些意义,需要一个更大的上下文窗口。如果没有足够大的上下文窗口,新生成的 token 就无法与之前的 tokens 所表达的概念(concepts)或想法(ideas)建立联系。那么我们应该如何是好?将上下文窗口扩展到 3 个 tokens 会在概率表中增加 125 行新内容,但质量可能仍然不尽人意。我们又需要多大的上下文窗口呢?
OpenAI 的开源模型 GPT-2 使用的上下文窗口其大小为 1024 个 tokens。如果我们要用马尔可夫链(Markov chains)实现这样一个大小的上下文窗口,概率表的每一行都需要代表一个长度在 1 到 1024 个 tokens 之间的序列。以前文例子中使用的 5 个 tokens 的词汇表为例,1024 个 token 长度的序列共有 5 种可能的 token 组合。我在 Python 中进行了计算:
>>> pow(5, 1024)55626846462680034577255817933310101605480399511558295763833185422180110870347954896357078975312775514101683493275895275128810854038836502721400309634442970528269449838300058261990253686064590901798039126173562593355209381270166265416453973718012279499214790991212515897719252957621869994522193843748736289511290126272884996414561770466127838448395124802899527144151299810833802858809753719892490239782222290074816037776586657834841586939662825734294051183140794537141608771803070715941051121170285190347786926570042246331102750604036185540464179153763503857127117918822547579033069472418242684328083352174724579376695971173152319349449321466491373527284227385153411689217559966957882267024615430273115634918212890625
这个行数多得吓人!这还只是整个概率表的一小部分,因为我们还需要长度从 1023 个 tokens 到 1 个 token 的所有序列,以确保在模型输入中没有足够 token 时,较短的 token 序列也能被处理。虽然马尔可夫链很有趣,但它们也确实存在很大的可扩展性问题。
而且,1024 个 tokens 的上下文窗口现在也已经不算特别大了。在 GPT-3 中,上下文窗口增加到了 2048 个 tokens,然后在 GPT-3.5 中增加到了 4096 个 token。GPT-4 最开始拥有 8192 个 tokens 的上下文窗口,后来增加到了 32K,再后来又增加到了 128K(没错,128,000 个 tokens !)。现在,具有 1M 或更大上下文窗口的模型也开始出现了,更大的上下文窗口允许模型更好地理解和利用之前输入的文本信息,从而在生成新的文本时保持更高的连贯性和准确性。
总之,马尔可夫链让我们以正确的方式思考文本生成问题,但它们也存在一些重大问题,使得我们无法将其视为一个可行的解决方案。
2.2 从马尔可夫链到神经网络
显然,我们必须放弃使用概率表的想法,因为一个合理上下文窗口的概率表需要的 RAM 大得惊人。我们可以采取的替代方案是用函数(function)来代替表格(table),这个函数是通过算法生成的,而不是存储在一个大表格中。这正是神经网络擅长的领域。
神经网络是一种特殊的函数,它接受 inputs ,对其进行计算,并返回 output 。对于语言模型来说,input 代表提示词转换的 tokens,output 是对下一个 token 的预测概率列表。
我说过神经网络是一种“特殊”的函数,是因为除了函数的逻辑之外,它们对 input 进行的计算还受到一系列外部定义参数的控制。一开始,神经网络的参数是未知的,因此,函数产生的输出是完全无用的。神经网络的训练过程包括寻找参数,找到使函数在对训练数据集进行评估时表现最佳的参数,其假设是,如果函数在训练数据上表现良好,那么它也会在其他数据上表现良好。
在训练过程中,神经网络的参数会通过反向传播算法[9],以极小的增量进行迭代调整。这个过程涉及大量的数学计算,因此在这里不会详细展开。每次调整后,神经网络的预测能力都会略有提升。更新参数后,神经网络会再次评估其在训练数据集上的表现,并根据评估结果进行下一轮的调整。这个过程会一直持续下去,直到神经网络在训练数据集上对下一个 token 的预测达到令人满意的效果。
下面帮助各位读者了解一下神经网络的规模,GPT-2 模型大约有 15 亿个参数,而 GPT-3 将参数数量增加到 1750 亿,GPT-4 据说有大约 1.76 万亿个参数。使用目前这一代硬件来训练这样大规模的神经网络需要相当长的时间,通常是几周甚至几个月。
有趣的是,由于参数数量众多,而且都是通过长时间的迭代过程计算出来的,没有人类干预,因此很难理解模型是如何工作的。训练好的大语言模型就像一个黑盒子,非常难以调试,因为模型的大部分“思维”都隐藏在参数中。即使是那些训练它们的人,也很难解释其内部的工作原理。
2.3 模型层、Transformers 和注意力机制
你可能非常想知道,神经网络函数内部究竟进行了哪些神奇的运算,使得它们在这些经过精心调优的参数的帮助下,能够接收 input tokens 列表,并以某种方式“猜”出下一个 token 的合理概率。
神经网络就像是一套复杂的操作链条⛓,链条的每个链环都被称为一个“模型层”。第一层接收 inputs (译者注:神经网络接收的初始数据。),对其进行某种形式的转换。经过转换的 input 接着进入下一层,再次进行转换。这一过程一直持续到数据最终到达最后一层,并在这里进行最后一次转换,从而产生最终的模型输出。
机器学习专家们设计了各种类型的模型层,它们能够对输入数据执行数学转换。同时,他们还找到了模型层的不同组织方式和分组方法,以达到预期的效果。有些模型层是通用的,适用于多种类型的输入数据;而有些模型层则是专门为处理特定类型的数据而设计的,比如图像数据或大语言模型中的经过分词后的文本。
在大语言模型的文本生成场景中,目前最流行的神经网络架构是 Transformer[10]。使用这种架构设计的 LLM(大语言模型)被称为 GPT,即 Generative Pre-Trained Transformers[11]。
Transformer 模型的独特之处在于它们执行的一种称为 Attention[12] 的模型层计算方式,这使得它们能够从上下文窗口中的 tokens 中推断出它们之间的关系(relationships)和模式(patterns),随后将这些关系和模式反映在下一个 token 的预测概率中。
Attention 机制最初被用于语言翻译场景,是一种找出输入序列中哪些 tokens 对理解其意义最为重要的方法。这种机制使现代翻译器能够通过关注(或集中“注意力”于)重要的单词或 tokens,从根本上“理解”一个句子。
03 Do LLMs Have Intelligence?
您可能已经开始思考,大语言模型(LLMs)在生成文本时是否展现出某种智能了?
我个人并不认为 LLMs 具备推理或创造原创思想的能力,但这并不意味着它们一无是处。由于 LLMs 对上下文窗口中的 tokens 进行了巧妙的计算,所以 LLMs 能够识别出用户提示词中存在的 patterns,并将它们与训练期间学到的类似 patterns 相匹配。 它们生成的文本大部分都是由零碎的训练数据组成的,但它们将单词(实际上是 tokens)拼接在一起的方式非常复杂,在很多情况下,它们生成的模型输出结果既有原创感,又非常有用。
鉴于 LLM 容易产生幻觉,我不会信任未经人工验证的情况下,任何直接将 LLMs 的 output 发送给用户的工作流程。
在未来的几个月或几年里,是否会有更大的 LLM 出现?它们能够实现真正的智能吗?由于 GPT 架构的诸多限制,我觉得这是不太可能实现的,但谁知道呢,也许随着未来创新成果的不断涌现,我们会实现这一目标。
04 The End
感谢您与我一起坚持到最后!我希望本文已经能够激发你的学习兴趣,让你决定继续学习大模型相关知识,并最终面对那些令人畏惧的数学知识,如果您想详细了解每一个细节的话,就无法回避这些数学知识。在这种情况下,我强烈推荐 Andrej Karpathy 的《Neural Networks: Zero to Hero》[13]系列视频。
Thanks for reading!
Hope you have enjoyed and learned new things from this blog!
About the authors
Hi, my name is Miguel. I'm a software engineer, but also tinker with photography and filmmaking when I have time. I was born in Buenos Aires, Argentina, but I lived most of my adult life in Portland, Oregon, USA, the place that comes to mind when I think of home. As of 2018 I'm living in Ireland.
END
本期互动内容 🍻
❓通过阅读本文,你能够理解为什么 LLMs 会产生“幻觉”或者不可靠的输出吗?你认为造成这些问题的原因是什么?
🔗文中链接🔗
[1]https://en.wikipedia.org/wiki/Large_language_model
[2]https://en.wikipedia.org/wiki/Generative_artificial_intelligence
[4]https://en.wikipedia.org/wiki/Byte_pair_encoding
[5]https://github.com/openai/gpt-2
[6]https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
[7]https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence)
[8]https://en.wikipedia.org/wiki/Markov_chain
[9]https://en.wikipedia.org/wiki/Backpropagation
[10]https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)
[11]https://en.wikipedia.org/wiki/Generative_pre-trained_transformer
[12]https://en.wikipedia.org/wiki/Attention_(machine_learning)
[13]https://karpathy.ai/zero-to-hero.html
原文链接:
https://blog.miguelgrinberg.com/post/how-llms-work-explained-without-math
评论