跳转至

第 4 章:从 n-gram 到神经语言模型

1. 本章要解决的问题

第 3 章里,我们已经回答了一个基础但非常重要的问题:

文本是怎么变成模型可以处理的输入的?

我们已经知道,一句话进入模型之前,通常会经历:

文本 -> token -> token id -> embedding

但这时新的问题马上就来了:

输入已经变成向量了,然后呢?

模型到底想学会什么?

更具体一点说,当我们把一句话喂给模型时,模型训练的目标究竟是什么?

例如给它前文:

I love

模型为什么会倾向于预测:

  • you
  • deep
  • this

而不是一个完全随机的 token?

这就是本章要解决的核心问题。

这一章的任务可以概括成一句话:

我们要建立“语言模型”这件事的最小完整直觉。

这里的“最小完整”很重要。

因为很多人在一开始接触大模型时,会直接把注意力放在:

  • Transformer
  • Attention
  • GPT
  • 预训练

这些现代概念上。

但如果你没有先搞清楚“语言模型本质上到底在预测什么”,后面看到再复杂的结构,都容易只停留在“会背名词”的状态。

所以这一章不急着讲复杂结构,而是先把一条最核心的主线讲明白:

语言模型是什么 -> n-gram 是怎么做的 -> 它为什么不够 -> 神经语言模型为什么出现 -> 为什么后来会走到 Transformer

从全书结构上看,这一章有三个作用:

  • 它承接第 3 章的“输入表示”
  • 它为第 5 章的 Attention 建立问题背景
  • 它也为第 6 章 Transformer 和第 7 章 Mini-GPT 做目标层面的铺垫

如果第 3 章解决的是“文本怎么表示”,那么第 4 章解决的就是:

模型拿到这些表示之后,到底在学习什么预测任务。

2. 你学完后应该会什么

  • 能用自己的话解释什么是语言模型
  • 能理解 next-token prediction 和序列概率建模之间的关系
  • 能写出 Chain Rule,并知道它为什么重要
  • 能理解 unigram、bigram、trigram 的基本思想
  • 能说明 n-gram 语言模型为什么会遇到数据稀疏和泛化差的问题
  • 能理解神经语言模型相对 n-gram 的核心改进
  • 能顺着这条历史脉络,过渡到后面的 Attention 和 Transformer

3. 什么是语言模型

先给一个尽量不绕的定义:

语言模型(Language Model, LM)做的事情是:

给定前文,预测下一个 token 出现的概率。

例如前文是:

I love

一个语言模型可能给出这样的预测:

  • you: 0.35
  • deep: 0.18
  • this: 0.12
  • pizza: 0.05

这意味着什么?

意味着在模型看来,在当前上下文下,某些 token 比另一些 token 更合理、更可能出现。

所以语言模型不是在做“绝对正确答案”的判断,而是在做:

条件概率分布的估计。

3.1 next-token prediction 的直觉

如果把语言模型想得非常朴素一点,它其实像是一个“自动补全系统”。

你给它一句话的前半段,它根据过去看过的大量文本,判断后面最可能接什么。

例如:

  • My name is
  • Today is a
  • The capital of France is

在不同上下文里,模型对“下一个 token”的判断会完全不同。

所以语言模型最核心的能力,不是记住固定答案,而是:

根据上下文动态调整对下一个 token 的概率判断。

3.2 它不仅是在补下一个词,也是在给整个句子建模

虽然训练时我们常说“预测下一个 token”,但这件事背后对应的是更一般的问题:

给整个 token 序列分配一个概率。

例如一个句子:

I love deep learning

如果一个模型觉得这个句子在语言上更自然、更常见,那么它就应该给这个序列更高的概率; 如果一个句子非常奇怪、几乎不符合语言习惯,那么它就应该给更低的概率。

所以:

  • 从训练过程看,它是在一步一步预测下一个 token
  • 从概率建模看,它是在为整个序列建模

这两件事,本质上是同一件事的两个视角。

4. Chain Rule:为什么可以一步一步预测

现在我们来把上面的直觉写成数学形式。

设一个 token 序列是:

\[ x_1, x_2, x_3, \dots, x_T \]

那么这个完整序列的概率可以写成:

\[ P(x_1, x_2, \dots, x_T) = P(x_1) P(x_2|x_1) P(x_3|x_1,x_2) \dots P(x_T|x_1,\dots,x_{T-1}) \]

这就是概率论里的 Chain Rule。

它的意义非常大,因为它告诉我们:

如果你能不断回答“给定前文,下一个 token 的概率是多少”,那么你其实就已经可以给整个句子建模了。

换句话说,语言模型之所以可以训练成“next-token prediction”,并不是拍脑袋定出来的技巧,而是因为:

序列概率本来就可以分解成一连串条件概率。

4.1 一个最小例子

假设一句话是:

I love NLP

那么它的概率可以拆成:

\[ P(\text{I love NLP}) = P(\text{I}) \cdot P(\text{love}|\text{I}) \cdot P(\text{NLP}|\text{I love}) \]

这说明模型训练时其实在做三件事:

  1. 句子开头出现 I 的概率有多大
  2. 已知前面是 I,下一个是 love 的概率有多大
  3. 已知前面是 I love,下一个是 NLP 的概率有多大

于是,一个看起来复杂的句子概率问题,就被拆成了一连串局部预测问题。

这就是现代语言模型训练目标的基础直觉。

5. n-gram:最早期的语言模型怎么做

既然语言模型的目标是估计:

\[ P(x_t | x_1, x_2, \dots, x_{t-1}) \]

那最直接的方法是什么?

最直接的方法当然是:

去统计训练语料里各种上下文和下一个词的共现次数。

这就是传统 n-gram 语言模型的基本思路。

5.1 unigram:完全不看上下文

最简单的语言模型叫 unigram model。

它做了一个非常强的简化:

每个 token 的出现概率只看它自己,不看上下文。

也就是说:

\[ P(x_t | x_1, \dots, x_{t-1}) \approx P(x_t) \]

这当然很粗糙,但它给了我们最早的概率语言模型直觉:

一个词出现得越频繁,它的概率就越大。

例如在一个小语料里:

  • the 出现 100 次
  • cat 出现 10 次
  • banana 出现 2 次

那么 unigram model 就会认为 thecat 更可能出现。

问题是,它完全不知道上下文。

所以在 unigram 看来:

  • I eat 后面接 apple
  • I eat 后面接 Monday

可能都只是看各自的全局词频,而不是看局部语义是否合理。

5.2 bigram:只看前一个 token

bigram model 往前走了一步。

它假设下一个 token 只依赖前一个 token:

\[ P(x_t | x_1, \dots, x_{t-1}) \approx P(x_t | x_{t-1}) \]

例如如果我们想预测:

I am ___

bigram model 在真正做的事情其实是:

只看 am 后面在训练语料里通常跟什么。

如果训练集中:

  • am happy 出现 40 次
  • am tired 出现 25 次
  • am banana 出现 0 次

那么模型自然会倾向于给 happytired 更高概率。

5.3 trigram:看前两个 token

再进一步,trigram model 假设下一个 token 只依赖前两个 token:

\[ P(x_t | x_1, \dots, x_{t-1}) \approx P(x_t | x_{t-2}, x_{t-1}) \]

这比 bigram 更合理,因为语言通常不只依赖前一个词。

例如:

New York ___

如果只看前一个 token York,信息不够完整; 但如果看前两个 token New York,下一个词是 City 的概率就会更明显。

5.4 一般化到 n-gram

所谓 n-gram,本质上就是做一个统一的马尔可夫近似:

\[ P(x_t | x_1, \dots, x_{t-1}) \approx P(x_t | x_{t-n+1}, \dots, x_{t-1}) \]

也就是:

预测下一个 token 时,不看全部历史,只看最近的 n-1 个 token。

于是:

  • unigram:看 0 个历史 token
  • bigram:看 1 个历史 token
  • trigram:看 2 个历史 token

这就是 n-gram 语言模型名字的来源。

6. 一个最小 bigram 例子

为了让这个概念彻底落地,我们来看一个最小语料:

  • I love NLP
  • I love deep learning
  • I enjoy learning

如果我们只做 bigram 统计,那么模型会关心:

  • I 后面出现过什么
  • love 后面出现过什么
  • deep 后面出现过什么

例如:

I 后面的统计可能是:

  • love: 2 次
  • enjoy: 1 次

所以:

\[ P(\text{love}|\text{I}) = \frac{2}{3} \]
\[ P(\text{enjoy}|\text{I}) = \frac{1}{3} \]

再看 love 后面:

  • NLP: 1 次
  • deep: 1 次

于是:

\[ P(\text{NLP}|\text{love}) = \frac{1}{2} \]
\[ P(\text{deep}|\text{love}) = \frac{1}{2} \]

如果现在前文是:

I

那么 bigram 模型会倾向于预测:

  • love
  • enjoy

如果前文是:

love

那么它会倾向于预测:

  • NLP
  • deep

这就是最原始的“根据历史预测下一个 token”。

从这里你也能看出来,n-gram 语言模型本质上很像一张巨大的条件概率统计表。

7. n-gram 为什么不够

n-gram 很重要,因为它第一次让“语言模型”这件事变得具体可算。

但它也有非常明显的问题。

7.1 数据稀疏:没见过就很难办

自然语言组合非常多。

即使语料已经不小,很多上下文组合仍然可能根本没有出现过。

例如训练集中也许出现过:

  • I love apples
  • I love bananas

但没出现过:

  • I love mangoes

如果你用传统 n-gram 统计,很多没见过的组合就会直接拿不到可靠概率,甚至变成 0。

这就是数据稀疏问题。

它不是小毛病,而是语言建模里非常本质的问题:

语言是开放组合的,但纯统计表只能覆盖见过的组合。

7.2 上下文窗口太短

n-gram 的第二个问题是,它只能看固定长度的历史。

例如 trigram 最多只看前两个 token。

这意味着很多更长距离的信息都会丢失。

比如下面这个句子:

The students who studied hard for the exam were happy because they finally passed.

如果只看很短的局部窗口,模型可能无法稳定利用前面更远的位置来判断后面的词。

而真实语言中,长距离依赖非常常见。

7.3 泛化能力差

n-gram 的第三个问题更关键:

它不会“举一反三”。

例如:

  • I like apples
  • I like bananas

这两个上下文在人类看来非常像。

但在 n-gram 的统计表里,它们本质上只是两条独立记录。

模型很难自动学会:

  • applesbananas 都是食物
  • like appleslike bananas 的模式相近
  • 没见过的相似搭配也许仍然合理

也就是说,n-gram 依赖的是“显式出现过没有”,而不是“结构上像不像”。

7.4 smoothing 能缓解,但不能根治

传统语言模型里确实发展出了很多 smoothing 方法,用来缓解“没见过组合概率为 0”的问题。

例如:

  • Laplace smoothing
  • Good-Turing
  • Kneser-Ney

这些方法很重要,但它们主要是在统计层面做修补。

它们能缓解稀疏问题,却不能从根本上解决两个核心限制:

  • 固定窗口太短
  • 无法学习连续空间中的相似性

这就把我们自然带到了下一步:

能不能不用“查统计表”的方式,而是让模型自己学一个更有泛化能力的函数?

8. 从 n-gram 到神经语言模型

神经语言模型出现的核心动机,可以浓缩成一句话:

不要把语言模型做成一张巨大的离散统计表,而要把它做成一个可学习的函数。

这个变化非常关键。

因为一旦你不用“每种上下文单独记一份计数”,而是改成“共享参数的神经网络”,很多 n-gram 做不到的事情就开始变得可能。

8.1 分布式表示:词不再只是孤立编号

在第 3 章我们已经讲过 embedding。

它的意义在这里真正体现出来了。

传统 n-gram 里,一个 token 本质上只是离散符号。

但在神经语言模型里,一个 token 会先被映射成一个连续向量。

于是模型不再只看到:

  • apple 是 id 1521
  • banana 是 id 2088

而是会看到它们各自的 embedding,并且这些向量可能在空间中比较接近。

这就给了模型一种非常重要的能力:

它可以在“相似 token”和“相似上下文”之间共享统计强度。

8.2 参数共享:没见过的组合也能有机会猜对

神经网络的优势不只是把 token 变成向量,更重要的是:

它学的是一个参数化函数。

可以粗略写成:

\[ P(x_t | x_{<t}) = f_\theta(x_{<t}) \]

这里的 \( f_\theta \) 就是由参数 \( \theta \) 控制的神经网络。

这意味着:

  • 不同上下文之间可以共享参数
  • 相似模式可以互相帮助
  • 即使某个具体组合没在训练里完整出现过,模型也可能因为见过很多相似模式而给出合理预测

这就是“泛化能力”提升的来源。

8.3 最小神经语言模型长什么样

最简单的神经语言模型可以这样理解:

  1. 把上下文 token id 映射成 embedding
  2. 把这些 embedding 拼接或汇总
  3. 送入一个神经网络
  4. 输出对整个词表的打分
  5. 经过 softmax,变成下一个 token 的概率分布

写成流程就是:

context ids -> embeddings -> neural network -> logits -> softmax -> next-token probabilities

这和 n-gram 的核心区别在于:

  • n-gram:显式查表、做计数
  • neural LM:隐式建模、共享参数

9. 从 MLP 到 RNN,再到 Transformer

神经语言模型本身也不是一步到位变成今天的 GPT。

它中间经历了几代典型结构。

9.1 MLP 语言模型

比较早期的一类神经语言模型,会取一个固定长度的上下文窗口, 把这些 token 的 embedding 拼接起来,然后送进 MLP。

它已经比 n-gram 更强,因为:

  • 输入不再是离散计数,而是连续表示
  • 参数可以共享
  • 相似上下文可以迁移

但它仍然有一个明显限制:

窗口长度通常还是固定的。

9.2 RNN / LSTM 语言模型

后面出现的 RNN、LSTM、GRU 等序列模型,尝试解决固定窗口的问题。

它们的核心思想是:

把过去的信息压到隐藏状态里,随着序列一步一步往后传。

这样模型理论上可以利用更长的历史,而不必像 n-gram 或固定窗口 MLP 那样只盯着最近几个 token。

不过 RNN 类模型也有自己的问题,比如:

  • 并行训练不方便
  • 长距离依赖仍然难学
  • 序列很长时训练效率和稳定性不理想

9.3 Transformer 语言模型

Transformer 的出现,把语言模型又往前推了一大步。

它最关键的变化是:

模型不再只靠一个递归状态逐步传信息,而是可以通过 Attention 直接查看上下文中的其他位置。

这让它在下面几件事上表现得更强:

  • 更容易建模长距离依赖
  • 更适合并行训练
  • 更适合大规模扩展

这也是为什么现代大语言模型几乎都建立在 Transformer 之上。

从这个角度看,第 5 章和第 6 章其实不是突然换了一个新话题,而是在回答:

既然我们已经知道语言模型需要更强的上下文建模能力,那么 Attention 和 Transformer 是怎么做到的?

10. perplexity:语言模型好不好,怎么衡量

既然语言模型输出的是概率分布,那我们就需要一个指标来衡量:

模型对真实文本的预测到底好不好。

一个经典指标就是 perplexity,通常记作 PPL。

你可以先把它理解成:

模型面对真实下一个 token 时,有多“困惑”。

直觉上:

  • 如果模型总能把高概率分给正确 token,那么 perplexity 会比较低
  • 如果模型总是拿不准,或者经常把高概率给错 token,那么 perplexity 会比较高

所以一般来说:

perplexity 越低越好。

不过要注意两点:

  • perplexity 主要适用于语言模型概率预测场景
  • 它能反映“预测性”,但不直接等于“生成效果一定更自然”

这也是为什么现代 LLM 的评估不会只看 perplexity,还会结合下游任务和人工偏好评估。

11. 从直觉到实现:语言模型训练时到底在干什么

到这里,我们可以把训练流程串起来了。

假设有一句 token 序列:

[x1, x2, x3, x4, x5]

训练时通常会构造这样的输入输出对:

  • 输入:[x1],目标:预测 x2
  • 输入:[x1, x2],目标:预测 x3
  • 输入:[x1, x2, x3],目标:预测 x4
  • 输入:[x1, x2, x3, x4],目标:预测 x5

也就是说,语言模型训练的本质就是:

不断读取前文,然后让模型把真实下一个 token 的概率拉高。

如果你把它和第 2 章的训练循环联系起来,就会发现它其实非常统一:

  • 前向传播:输出每一步对下一个 token 的概率分布
  • 计算 loss:真实 token 的负对数似然
  • 反向传播:更新参数

所以从工程视角看,语言模型并没有脱离你已经学过的训练框架; 它只是把监督信号换成了“下一个 token 是什么”。

12. 最小代码实现:bigram language model

下面给一个最小 bigram 统计实现,只用来帮助你建立直觉。

from collections import defaultdict, Counter

corpus = [
    ["I", "love", "NLP"],
    ["I", "love", "deep", "learning"],
    ["I", "enjoy", "learning"],
]

bigram_counts = defaultdict(Counter)

for sentence in corpus:
    for prev_token, next_token in zip(sentence[:-1], sentence[1:]):
        bigram_counts[prev_token][next_token] += 1

def predict_next(prev_token):
    counter = bigram_counts[prev_token]
    total = sum(counter.values())
    return {token: count / total for token, count in counter.items()}

print(predict_next("I"))
print(predict_next("love"))

可能输出:

{'love': 0.6667, 'enjoy': 0.3333}
{'NLP': 0.5, 'deep': 0.5}

这个代码虽然非常简单,但已经完整体现了 n-gram 的核心思想:

  • 用上下文做 key
  • 用下一个 token 的计数构造条件概率
  • 用统计频率来近似语言规律

如果你继续往前走一步,把“查统计表”换成“embedding + 神经网络”,就进入神经语言模型的世界了。

13. 常见误区

误区 1:语言模型就是在背答案

这不准确。

语言模型学到的是条件概率分布,而不是固定问答对。

看起来像“知道答案”,本质上是它在大量上下文统计和参数共享中学到了什么 token 更可能出现。

误区 2:next-token prediction 太简单,所以学不到复杂能力

这也是一个常见误解。

单步目标看起来简单,但因为训练数据量巨大、上下文丰富、模型参数很多, 所以为了持续把“下一个 token”预测好,模型会被迫学到:

  • 语法
  • 搭配
  • 事实模式
  • 推理痕迹中的统计结构

也就是说,简单目标不代表学到的能力简单。

误区 3:n-gram 和 Transformer 是完全无关的两件事

并不是。

它们都在做语言模型,只是建模条件概率的方式不同。

可以把 n-gram 看成语言模型发展的早期形态, 把神经语言模型和 Transformer 看成在同一目标上的更强实现。

14. 面试问题

Q1:语言模型本质上在做什么?

语言模型本质上是在估计 token 序列的概率。训练时通常表现为:给定前文,预测下一个 token 的条件概率。

Q2:为什么 next-token prediction 可以用于训练整个语言模型?

因为根据 Chain Rule,整个序列的联合概率可以分解成一系列“给定前文预测下一个 token”的条件概率乘积。

Q3:n-gram 的核心问题是什么?

核心问题包括:数据稀疏、上下文窗口太短、泛化能力差。它依赖离散统计,难以利用相似 token 和相似上下文之间的结构信息。

Q4:神经语言模型相比 n-gram 的主要提升是什么?

它通过 embedding 和参数共享,把离散统计表变成可学习函数,因而具有更好的泛化能力,也更容易建模复杂上下文。

Q5:为什么后来语言模型会走向 Transformer?

因为 Transformer 在长距离依赖建模、并行训练效率和大规模扩展性上,比早期方法更有优势。

15. 本章小结

这一章你应该抓住的,不是某个历史名词,而是一条非常稳定的主线:

  • 语言模型的目标是估计序列概率
  • 这可以通过 next-token prediction 来实现
  • n-gram 是最早期的条件概率统计方法
  • 它让语言模型变得可计算,但也暴露了数据稀疏、短窗口和泛化差的问题
  • 神经语言模型通过 embedding 和参数共享,开始真正具备泛化能力
  • Transformer 则是在这条路上继续发展出的更强结构

所以读完这一章之后,你应该已经知道:

后面要学 Attention,并不是因为它“很火”,而是因为我们已经遇到了一个真实的建模问题:

语言模型需要更强的上下文建模能力。

下一章,我们就正式进入这个问题的核心答案之一:

Attention 机制。