大语言模型是怎么工作的?通俗解释版

点击查看目录

我相信你也会同意,现在已经无法忽视生成式 AI(Generative AI,简称 GenAI)了。关于大语言模型(Large Language Models,LLMs)的新闻铺天盖地。你很可能已经用过 ChatGPT,甚至一直把它开着当助手用。

但很多人心中有一个基本疑问:这些模型看上去“很聪明”,这种“聪明”到底是从哪儿来的?

这篇文章就是想用简单的方式、尽量不涉及复杂数学,来解释文本生成模型是如何工作的,帮助你把它们当作计算机算法来理解,而不是神奇魔法。

大语言模型到底在做什么?

我们先来澄清一个很多人对大语言模型的误解。很多人以为这些模型是“会聊天”、“会答题”的,但实际上,它们唯一真正擅长的事情是:

👉 给它一段文字,它就根据上下文“猜”下一个词(准确说是“Token”)是什么。

所以,我们可以从“Token”这个核心概念开始揭开 LLM 的神秘面纱。

什么是 Token?

Token(词元)是大语言模型处理文本的最小单位。

你可以大致把它当成是“词”,但实际上,Token 既可以是一个字母、一个词,也可以是一段词根、甚至是空格或标点。LLM 的目标是尽可能高效地表示文本,所以不会总是按单词来切分。

一个语言模型的全部 Token 列表就叫它的“词汇表”(vocabulary)。这个词汇表是通过一种叫做 BPE(字节对编码) 的算法,从大量语料中训练出来的。

举个例子,开源的 GPT-2 模型就使用了一个包含 50,257 个 Token 的词汇表。

每个 Token 都有一个唯一编号,模型会用一个叫做 tokenizer(分词器) 的工具,把你输入的文字转换成 Token 编号列表。你也可以在 Python 中尝试这个过程:

pip install tiktoken

然后在 Python 中运行以下代码:

import tiktoken
encoding = tiktoken.encoding_for_model("gpt-2")

encoding.encode("The quick brown fox jumps over the lazy dog.")
# 输出: [464, 2068, 7586, 21831, 18045, 625, 262, 16931, 3290, 13]

encoding.decode([464])    # 'The'
encoding.decode([2068])   # ' quick'
encoding.decode([13])     # '.'

可以看到:

  • Token 464 表示 “The”
  • Token 2068 表示 " quick"(注意前导空格也包括进去了)
  • Token 13 表示句号 “.”

由于分词是算法决定的,有时候你会发现一些奇怪的现象:

encoding.encode('The')    # [464]
encoding.encode('the')    # [1169]
encoding.encode(' the')   # [262]

同一个词,根据是否有大小写、是否有前导空格,会被编码成不同的 Token。

此外,使用频率低的词不会单独占用一个 Token,而是被拆成多个 Token。例如:

encoding.encode("Payment")      # [19197, 434]
encoding.decode([19197])        # 'Pay'
encoding.decode([434])          # 'ment'

预测下一个 Token

正如前面提到的,语言模型的任务就是预测下一个 Token 是什么

假设你已经输入了:

['The', ' quick', ' brown', ' fox']

然后运行伪代码:

predictions = get_token_predictions(['The', ' quick', ' brown', ' fox'])

这一步会返回一份概率分布表,告诉你在这个上下文下,每个 Token 作为下一个词的概率是多少。

以 GPT-2 为例,这个返回值就是一个包含 50,257 个浮点数 的列表,每个数表示对应 Token 出现在下一个位置的概率。

你可以想象,像 “jumps” 这样的词很可能被赋予较高概率,而像 “potato” 这样的无关词概率则接近 0。

为了能做出这种预测,模型必须经过一个训练过程:它读了大量的文本,从中学习“哪些词通常跟随哪些词”。

最终它构建了一个复杂的数据结构,能根据输入 Token 序列预测下一个 Token 的概率。

这和你原来想象的一样吗?现在你是不是开始觉得,这其实并没有那么神秘了?

生成一段完整的文字

因为语言模型每次只能预测一个下一个 token(词元),所以如果我们想要它生成一整段句子,唯一的办法就是通过循环多次调用模型,每次生成一个新的 token,直到长度足够。

每一次循环中,模型都会根据当前的输入预测下一个 token 的概率分布,然后从中选择一个 token,添加到输入序列的末尾,再继续下一轮预测。这就像接力一样,模型每次生成一个词,然后拿这个词继续接着往下写。

我们来看一段更完整的伪代码(Python 风格):

def generate_text(prompt, num_tokens, hyperparameters):
    tokens = tokenize(prompt)
    for i in range(num_tokens):
        predictions = get_token_predictions(tokens)
        next_token = select_next_token(predictions, hyperparameters)
        tokens.append(next_token)
    return ''.join(tokens)
  • generate_text() 函数接受一个用户的输入提示(prompt),比如一句话或者一个问题。
  • tokenize() 是一个辅助函数,它会用如 tiktoken 这样的工具把文本转成 Token 编号的列表。
  • for 循环中,每轮会调用 get_token_predictions(),也就是实际调用 AI 模型,让它预测下一个 token 的概率。
  • 然后通过 select_next_token() 这个函数,从这些概率中选出一个 token 来作为输出。

这个选 token 的函数可以用多种策略:

  • 最简单的是选择概率最高的那个(在机器学习中称为“贪婪选择”/greedy selection);
  • 更聪明的方法是用随机性来加点变化:根据概率分布用随机数决定哪个 token 被选中,这样同一个 prompt 可以生成不一样的内容,增加“创造力”。

为了进一步控制生成结果的风格,我们可以用一些**超参数(hyperparameters)**来影响 token 的选择过程。这些超参数是通过参数传给 generate_text() 的:

  • 比如 temperature(温度)参数:它会影响模型对“冷门”词的选择倾向。温度越高,概率分布会“拉平”,选择一些低概率 token 的可能性就会增加,生成结果就更有想象力。
  • 还有 top_ptop_k,用于控制模型考虑的“最可能的几个 token”,从中再做选择。

一旦选定了下一个 token,就会进入下一轮循环,把这个新 token 加入输入中,继续预测下一个 token,直到生成足够长度的内容。

注意:这个过程不懂得“句子”或“段落”的概念,它只是一个词接一个词地往下预测。所以生成的文本可能会在句子中间突然结束。为了解决这个问题,num_tokens 参数可以设置为“最多生成几个 token”,而不是固定数量。你还可以设置当生成出句号 token(如 .)时就自动结束。

如果你读到这里并理解了上述内容,恭喜你!你已经掌握了 LLM 的基本工作机制。如果你还想更进一步了解,那么接下来我们会进入稍微技术一点的内容(但依然尽量避免复杂数学)。

模型是如何训练的?

谈到模型是怎么训练出来的,其实很难完全避开数学。不过这里我会用一个非常简单直观的例子来帮助你理解这个过程。

因为语言模型的核心任务是“预测接下来可能出现的 token”,所以最基础的训练方式,就是去统计在训练语料中连续出现的 token 对(也就是“前一个词+下一个词”的组合),并建立一个概率表。

我们从一个简单的例子开始:假设模型的词汇表中只有以下 5 个 token:

['I', 'you', 'like', 'apples', 'bananas']

为了简化,我们这里不考虑空格和标点符号。

训练语料由三句话组成:

  • I like apples
  • I like bananas
  • you like bananas

现在我们可以构建一个 5×5 的表格,行表示“前一个 token”,列表示“后一个 token”。我们统计每个组合在语料中出现的次数:

I you like apples bananas
I 2
you 1
like 1 2
apples
bananas

这个表表示:

  • “I like” 出现了 2 次;
  • “you like” 出现了 1 次;
  • “like apples” 出现了 1 次;
  • “like bananas” 出现了 2 次。

接下来我们把这张“频次表”转换成“概率表”。方法是:每一行中出现的次数加总后,按比例转换成概率。

例如,“like” 这一行有两种后续词:

  • “apples” 出现 1 次;
  • “bananas” 出现 2 次;

总共 3 次,所以:

  • “apples” 的概率是 1/3 ≈ 33.3%;
  • “bananas” 的概率是 2/3 ≈ 66.7%。

于是最终的概率表如下(空格表示概率为 0%):

I you like apples bananas
I 100%
you 100%
like 33.3% 66.7%
apples 25% 25% 25% 25%
bananas 25% 25% 25% 25%

你可能注意到了,“apples” 和 “bananas” 的那两行看上去很奇怪 —— 因为在我们的训练数据中,它们后面没有再跟任何词,这就是训练数据中的“空洞”。

为了让模型不会在遇到这些 token 时“卡住”,我采用了一种折中办法:给每个可能的下一个 token 都分配了相等的概率(即平均分配)。虽然这样做不一定准确,但至少能保证模型不会直接“失效”。

关于“训练数据空洞”的思考

这个例子中的“空洞”很明显,在真实的大模型中可能不容易察觉。但即使是使用了超大规模语料的数据集,也还是可能存在一些训练不足的区域。

这些“稀疏区域”的预测质量通常比较差,虽然看起来语句没问题,但可能会出现事实错误、逻辑不通等现象 —— 这就是所谓的 幻觉(hallucination),即模型生成的内容“像真的,但其实是假的”。

模拟实现:如何用这张概率表生成预测?

现在你已经有了这个概率表,那么语言模型内部的 get_token_predictions() 函数其实就可以非常简单地实现了:

def get_token_predictions(input_tokens):
    last_token = input_tokens[-1]
    return probabilities_table[last_token]

你传入一串 token(比如用户的 prompt),模型只会关注最后一个 token,然后返回它在概率表中对应的一行。

例如输入:

['you', 'like']

函数就会读取 “like” 这一行,告诉你:

  • “apples” 的概率是 33.3%
  • “bananas” 的概率是 66.7%

那么在上一个伪代码中 select_next_token() 函数就有 1/3 的概率选中 “apples”,2/3 的概率选中 “bananas”。

如果模型选择了 “apples”,生成的句子就是:

you like apples

这是一句训练数据中没有的句子,但它却是非常合理的。模型仅凭过去学到的词序关系,就能“合成”出一个看似全新的输出。

你现在应该能理解:模型的“创造力”其实是通过把已学过的词汇和模式巧妙地拼接在一起而来,这种方式虽然没有真正的“思考”,但效果却很惊艳。

上下文窗口(Context Window)

在上一节中,我用来训练迷你语言模型的方法,属于一种叫做马尔可夫链(Markov chain)的技术。

这种方法的一个核心问题是:它**只考虑上一个 token(词元)**来做预测。也就是说,在选择下一个 token 时,之前出现的所有内容都会被“遗忘”。因此,我们可以说这种方法的上下文窗口是 1 个 token —— 实在太小了。

当上下文窗口这么小,模型就像失忆症一样,每次预测都只能看到最近的一个词,很难保持上下文的一致性或连贯性。

如果增大上下文窗口呢?

一个办法是:扩展概率表,使用更长的 token 序列作为上下文

比如:

  • 若使用 2 个 token 做上下文(窗口大小为 2),就需要在概率表中添加所有可能的两词组合;
  • 用我前面举的例子(5 个 token),就需要再添加 5×5=25 行,再加上原本的 5 行,一共 30 行;
  • 模型训练时要学会每三个 token 的组合关系,才能预测下一个 token;
  • 运行时,每次都取最后两个 token 作为上下文,用来查找对应的概率分布。

不过,2 个 token 作为上下文依然远远不够。想要生成更自然、上下文更一致的文本,我们需要更长的上下文窗口。否则,模型无法让新生成的 token 和之前的内容保持一致或有逻辑联系。

那我们需要多大窗口呢?

如果窗口从 2 提高到 3,那么就需要 5×5×5 = 125 个组合,表格就要增加 125 行。但效果仍然很有限。

如果用马尔可夫链来实现 GPT-2 的上下文窗口?

GPT-2 的上下文窗口是 1024 个 token

如果我们用马尔可夫链来实现这样的模型,就意味着我们要准备一个超大的概率表,每一行都代表一串最多 1024 个 token 的组合。

举个例子,如果词汇表中只有 5 个 token,那么可能的 1024 长度的组合数量是:

>>> pow(5, 1024)
55626846462680034577255817933310101605480399...(后面还有上百位)

这是一个天文数字级别的行数,光是存这个表都需要超出地球所有硬盘的总容量!

而且这还只是考虑长度为 1024 的组合。实际上我们还要支持长度为 1、2、3……1023 的组合,因为用户的输入可能并没有那么长。那整个表的规模会更大,完全不可行。

马尔可夫链方法的极限

随着模型的发展:

  • GPT-3 的上下文窗口提升到 2048;
  • GPT-3.5 提升到了 4096;
  • GPT-4 起步是 8192,后来又提升到 32K,甚至是 128K(没错,是 12.8 万个 token);
  • 最新的研究中,甚至出现了支持 上百万 token 上下文窗口 的模型。

这意味着模型已经能够理解和关联更大段落之间的语义,生成的内容也更加连贯、一致。

马尔可夫链是一种很棒的思维工具,它让我们理解“预测下一个 token”的基本逻辑。但它不适合构建真正的大语言模型,因为它在上下文长度、存储空间和可扩展性方面有根本性的瓶颈。

为了处理大规模上下文,我们需要更智能、更高效的机制 —— 而这正是神经网络和 Transformer 架构真正擅长的领域。

从马尔可夫链到神经网络

显然,我们已经无法继续依赖“概率表”的方式了 —— 因为如果想支持较大的上下文窗口,那么这张表将变得大得离谱,根本无法存在于内存中。

那我们该怎么办?

答案是:用一个函数代替表格。这个函数不再存储所有可能性,而是通过算法“算”出一个近似的概率分布 —— 这正是神经网络擅长的事情。

神经网络是一种特殊类型的函数,它:

  • 接收一些输入(比如 token);
  • 对这些输入进行复杂的计算;
  • 输出一个结果 —— 在语言模型中,就是下一个 token 的概率分布。

为什么说它“特殊”?

因为神经网络的计算行为不光由代码控制,还依赖于大量外部定义的参数(parameters)

这些参数在一开始是未知的、随机的,所以模型一开始的输出几乎毫无用处。

训练的过程,就是找到那些最适合的参数组合,使得神经网络的预测结果尽可能地贴近训练数据中的真实情况。

训练过程会反复进行多轮,每一轮对参数进行一点点微调,这个过程叫做反向传播(Backpropagation),它本质上是一个数学优化过程(本文不会深入讨论)。

每次参数更新之后,模型会再次评估它在训练数据上的表现,再基于反馈继续调整参数。这个循环会持续进行,直到模型能在训练集上做出合理的预测为止。

为了帮助你理解神经网络的规模,我们来看几个数据:

  • GPT-2:约 15 亿个参数
  • GPT-3:1750 亿个参数
  • GPT-4:据说达到了 1.76 万亿个参数!

训练这种规模的神经网络,即使使用现代最强的计算资源,也需要花费数周甚至数月。

由于神经网络中隐藏了如此多的参数,而且这些参数全是通过自动化迭代得出的,连开发者自己都很难完全理解模型内部是如何“思考”的

一个训练完成的 LLM 更像是一个黑盒 —— 你看不到它的内部“逻辑”,只能观察它的输入输出。就连训练它的人也很难解释它为什么会给出某个答案。

层(Layers)、Transformer 和注意力机制(Attention)

你也许会好奇:这些神经网络到底在内部做了些什么?它们是如何“把一堆 token 输入,经过参数的计算,然后输出合理的下一个 token 的概率”的?

这就涉及到神经网络的“分层计算”结构。

一个神经网络由多个称为**层(layer)**的计算模块组成。其基本流程是:

  1. 第一层接收输入;
  2. 对输入做一次变换(数学运算);
  3. 把变换后的结果传给下一层;
  4. 重复这个过程,直到最后一层生成输出。

每一层都像一个“加工车间”,把输入“改造”一下,然后再传递下去。

机器学习研究者会设计不同类型的层,用来处理不同的数据类型,比如图像或文本。有些层是通用的,有些则是专门为处理“token 化后的文本”设计的。

目前在大语言模型中,最常用的神经网络架构叫做 Transformer。这种架构非常适合处理序列数据(例如自然语言中的单词序列),它的核心机制是:

注意力机制(Attention)

Attention 能帮助模型理解“在上下文中,哪些 token 比较重要”,并基于这种理解生成更合理的下一个词的概率分布。

最早,这种机制是用在机器翻译中的,用来判断“源语言的句子中,哪些词在翻译中最关键”。它可以模拟人类阅读时“聚焦重点词语”的过程。

今天,Attention 已成为 Transformer 架构的核心能力,使得 LLM 能够从长上下文中捕捉信息,从而生成连贯、逻辑一致的文本。

如果你已经理解了这一部分,那么你就已经初步了解了大语言模型“背后的魔法”其实是数学 + 神经网络 + 算法工程的结合成果。下一步,我们可以探讨这些模型是否真的“具备智能”。

大语言模型真的“有智能”吗?

看到这里,你可能已经开始有自己的判断:LLM(大语言模型)在生成文本时,是否体现出某种“智能”。

就我个人而言,我并不认为 LLM 具有真正的推理能力,或者能够产生原创性的思想。但这并不意味着它们没有价值。

凭借对上下文中 token 的精妙计算,LLM 能够识别出用户提示(prompt)中的模式,并将其与训练时学到的相似模式进行匹配。它生成的文本,虽然大多是从训练数据中“拼拼凑凑”而来,但拼接的方式非常巧妙,看起来像是“自己想出来的”,而且往往很有用。

不过,考虑到 LLM 存在**幻觉(hallucination)**的倾向(即生成内容看起来合理但却是虚假的),我不建议把 LLM 的输出直接交给最终用户使用,除非事先经过人工审核和验证。

那么,未来几个月或几年中出现的更大规模的 LLM,有可能具备真正的智能吗?

我认为,至少在目前 GPT 架构的框架下,还很难达到真正意义上的智能 —— 因为它还有很多根本性的限制。但谁知道呢?也许未来的某些创新,真的能带我们走得更远。

写在最后

感谢你一路读到这里!

我希望这篇文章能够激起你继续深入学习的兴趣,哪怕最后你真的要面对那些“可怕的数学”,因为如果你想彻底理解 LLM 的每一个细节,那是绕不过去的。

如果你准备好面对这些挑战,我强烈推荐 Andrej Karpathy 的视频课程:Neural Networks: Zero to Hero。它是理解神经网络从入门到精通的绝佳起点。

编辑本页