/ 什么是系列
什么是N-Gram Model
从语言模型的链式法则出发,解释 N-Gram 如何用最近 N-1 个 token 的频次预测下一个 token,以及数据稀疏、平滑、回退、插值和它与 Transformer 的关系。
**N-Gram Model(N 元语法模型)**是一类经典的统计语言模型。它的核心思想很直接:
用前面最近的 N - 1 个词或 token,去预测下一个词或 token 出现的概率。
例如一句话:
我 喜欢 吃 苹果不同的 N 对应不同的上下文长度:
- Unigram,
1-gram:不看上下文,只看每个词自身出现的频率; - Bigram,
2-gram:用前 1 个词预测下一个词; - Trigram,
3-gram:用前 2 个词预测下一个词; - 4-gram:用前 3 个词预测下一个词。
一句话概括:N-Gram 是一种非参数化、固定窗口、基于离散频次查表的自回归语言模型。
它和现代大语言模型一样,都在做“预测下一个 token”。区别在于:N-Gram 靠短窗口频次统计,Transformer 靠长上下文神经网络计算。
1. 从语言模型说起#
语言模型想回答的问题是:
一句话出现的概率是多少?比如:
我 喜欢 吃 苹果可以写成:
P(我, 喜欢, 吃, 苹果)根据概率链式法则,整句话的概率可以拆成一连串条件概率:
P(w1, w2, ..., wT)
= P(w1) * P(w2 | w1) * P(w3 | w1, w2) * ... * P(wT | w1, ..., wT-1)也就是:每个词的概率都依赖它前面已经出现的所有词。
如果真的完整保留所有历史,统计空间会非常大。对于稍长一点的句子,几乎不可能在训练语料中找到完全相同的历史上下文。N-Gram 的做法是引入一个近似假设:
P(wt | w1, ..., wt-1)
≈ P(wt | wt-N+1, ..., wt-1)也就是说,下一个词只依赖最近的 N - 1 个词。
这个假设通常叫做 N - 1 阶马尔可夫假设。它牺牲了长距离上下文,换来了可统计、可存储、可推理的模型。
2. 不同 N 到底看什么#
假设句子是:
今天 天气 很 好2.1 Unigram#
Unigram 完全不考虑词序:
P(今天 天气 很 好)
= P(今天) * P(天气) * P(很) * P(好)因此在 Unigram 看来,下面两句话可能拥有相同概率:
今天 天气 很 好
好 很 天气 今天只要词的集合和出现次数一样,它就无法区分顺序是否合理。
2.2 Bigram#
Bigram 每一步只看前一个词:
P(今天 天气 很 好)
= P(今天) * P(天气 | 今天) * P(很 | 天气) * P(好 | 很)它已经能够学习局部搭配,比如:
天气 很
很 好但它仍然看不到更远的上下文。
2.3 Trigram#
Trigram 每一步看前两个词:
P(今天 天气 很 好)
= P(今天) * P(天气 | 今天) * P(很 | 今天, 天气) * P(好 | 天气, 很)相比 Bigram,Trigram 能利用更长一点的局部结构。例如它不仅知道 很 -> 好 常见,也可以知道 天气 很 -> 好 更合理。
3. N-Gram 概率怎么估计#
N-Gram 的训练本质上是计数。
对于一个上下文 h 和下一个词 w,最大似然估计可以写成:
P(w | h) = Count(h, w) / Count(h)如果是 Bigram:
P(wt | wt-1) = Count(wt-1, wt) / Count(wt-1)如果是 Trigram:
P(wt | wt-2, wt-1) = Count(wt-2, wt-1, wt) / Count(wt-2, wt-1)这里的 Count(...) 表示某个 token 序列在训练语料中出现的次数。
举个 Bigram 的例子。假设训练语料是:
我 喜欢 吃 苹果
我 喜欢 吃 香蕉
我 喜欢 打 篮球我们想估计:
P(吃 | 喜欢)统计 喜欢 后面出现过什么:
喜欢 吃
喜欢 吃
喜欢 打所以:
P(吃 | 喜欢) = Count(喜欢, 吃) / Count(喜欢) = 2 / 3
P(打 | 喜欢) = Count(喜欢, 打) / Count(喜欢) = 1 / 3因此 Bigram 模型看到 我 喜欢 之后,更倾向于继续生成 吃。
4. 句子开始和结束也要建模#
真实实现里通常会给句子加上特殊 token:
<BOS> 我 喜欢 吃 苹果 <EOS>其中:
<BOS>表示句子开始;<EOS>表示句子结束。
这样模型不仅能学到句子内部的转移概率,也能学到:
P(我 | <BOS>)
P(<EOS> | 苹果)对于 Bigram,整句话概率可以写成:
P(我 喜欢 吃 苹果)
= P(我 | <BOS>)
* P(喜欢 | 我)
* P(吃 | 喜欢)
* P(苹果 | 吃)
* P(<EOS> | 苹果)这也解释了为什么 N-Gram 可以生成文本:只要从 <BOS> 开始,不断根据当前上下文采样下一个 token,直到采到 <EOS> 即可。
5. 为什么 N 不能无限增大#
直觉上,N 越大,模型看到的上下文越多,预测应该越准。但在统计语言模型里,N 变大很快会遇到两个问题。
5.1 N 太小:上下文不足#
Bigram 只看前一个词。比如看到:
银行 利率它无法利用更长的上下文:
中央银行决定降低利率
商业银行决定提高存款利率如果只靠最近一个词,模型很难区分不同语义场景。
5.2 N 太大:数据稀疏严重#
如果使用 10-gram,模型需要统计很多很长的连续片段:
我 昨天 晚上 和 朋友 一起 去了 上海 外滩这样的完整序列在训练语料中可能一次都没有出现。于是模型无法直接估计它的概率。
组合数量也会快速膨胀。假设词表大小是 V,理论上的 N-Gram 组合数是:
V^N如果词表有 50,000 个词:
Bigram: 50,000^2 = 2,500,000,000
Trigram: 50,000^3 = 125,000,000,000,000真实语料只能覆盖其中很小一部分。所以传统统计语言模型常见的是 Bigram、Trigram、4-gram、5-gram,很少直接把 N 设得特别大。
6. 未见过的 N-Gram 怎么办#
最大似然估计有一个致命问题:未见过的组合概率为 0。
假设训练语料里从未出现过:
喜欢 榴莲那么 Bigram 会得到:
P(榴莲 | 喜欢) = 0一旦句子里有一个 N-Gram 概率为 0,整句话概率就会变成 0:
P(sentence) = p1 * p2 * ... * 0 * ... * pT = 0这显然不合理。未见过不代表不可能,只代表训练语料没有覆盖到。因此 N-Gram 模型通常必须使用 smoothing(平滑)。
6.1 加一平滑#
最直观的方法是给每个候选词都加 1 次伪计数:
P(w | h) = (Count(h, w) + 1) / (Count(h) + V)其中:
h是历史上下文;w是下一个 token;V是词表大小。
这样所有未出现过的组合都会得到一个非零概率。
不过加一平滑通常过于粗糙:它会从高频事件里拿走太多概率,分给大量低概率甚至不合理的事件。
6.2 更常见的平滑方法#
经典统计语言模型里更成熟的方法包括:
- Good-Turing smoothing;
- Katz backoff;
- Jelinek-Mercer interpolation;
- Kneser-Ney smoothing。
其中 Kneser-Ney smoothing 通常被认为是 N-Gram 语言模型里非常强的平滑方法。它不只看一个词出现了多少次,还会考虑这个词出现在多少种不同上下文后面。
例如 of San Francisco 可能很常见,但 Francisco 作为任意上下文后的泛化预测并不一定应该很高;Kneser-Ney 会更谨慎地分配这类概率。
7. Backoff 和 Interpolation#
当高阶 N-Gram 没见过时,有两种常见思路:回退和插值。
7.1 Backoff:高阶没有就退到低阶#
假设我们想计算:
P(苹果 | 我, 喜欢, 吃)如果 4-gram:
我 喜欢 吃 苹果没有出现过,可以退回 Trigram:
P(苹果 | 喜欢, 吃)如果 Trigram 也没有出现,就继续退回 Bigram:
P(苹果 | 吃)再没有,就退回 Unigram:
P(苹果)这叫 backoff(回退)。它的直觉是:优先相信更具体的上下文;如果没有统计证据,就使用更粗粒度但更稳定的低阶模型。
7.2 Interpolation:不同阶一起投票#
插值不是只在高阶缺失时才退回低阶,而是始终把不同阶模型加权融合:
P(wt)
= lambda3 * P(wt | wt-2, wt-1)
+ lambda2 * P(wt | wt-1)
+ lambda1 * P(wt)其中:
lambda1 + lambda2 + lambda3 = 1它的好处是更稳:即使高阶统计存在,也不会完全忽略低阶统计带来的全局信息。
8. N-Gram 如何生成文本#
以 Bigram 为例,当前词是:
我模型查表得到下一个词分布:
P(喜欢 | 我) = 0.5
P(想 | 我) = 0.3
P(今天 | 我) = 0.2然后从这个分布中采样。假设采到 喜欢,模型继续查看:
P(w | 喜欢)如果采到 吃,再继续查看:
P(w | 吃)直到生成:
我 喜欢 吃 苹果这个过程和现代大模型的自回归生成形式很像:
prefix -> next token distribution -> sample/select token -> new prefix -> next token distribution区别在于分布从哪里来:
- N-Gram:来自离散频次表;
- Transformer:来自神经网络前向计算。
9. 一个简单 Python 实现#
下面是一个极简 Bigram 模型:
from collections import Counter, defaultdict
sentences = [
["<BOS>", "我", "喜欢", "吃", "苹果", "<EOS>"],
["<BOS>", "我", "喜欢", "吃", "香蕉", "<EOS>"],
["<BOS>", "我", "喜欢", "打", "篮球", "<EOS>"],
]
bigram_counts = defaultdict(Counter)
for sentence in sentences:
for current_word, next_word in zip(sentence[:-1], sentence[1:]):
bigram_counts[current_word][next_word] += 1
def next_word_probability(current_word: str) -> dict[str, float]:
counts = bigram_counts[current_word]
total = sum(counts.values())
if total == 0:
return {}
return {
word: count / total
for word, count in counts.items()
}
print(next_word_probability("喜欢"))输出大致是:
{"吃": 0.6666666666666666, "打": 0.3333333333333333}这段代码没有平滑,也没有回退,只展示最核心的计数和归一化过程。
10. 和 Transformer 大语言模型的关系#
N-Gram 和 Transformer 都可以写成自回归形式:
P(x1, ..., xT) = product over t of P(xt | x<t)但二者计算条件概率的方式完全不同。
| 模型 | 条件概率怎么来 | 上下文 | 泛化能力 | 推理形式 |
|---|---|---|---|---|
| N-Gram | 频次统计表 | 固定 N - 1 个 token | 弱,几乎只能记忆离散片段 | 查表 |
| Transformer | 神经网络参数化计算 | 上下文窗口内的 token | 强,可通过 embedding 和 attention 泛化 | 前向计算 |
N-Gram 可以写成:
P(xt | x<t) ≈ P(xt | xt-N+1, ..., xt-1)Transformer 更接近:
P(xt | x<t) = Softmax(f_theta(x<t))其中 f_theta 是一个带参数的神经网络。
这带来几个关键差异:
- N-Gram 的上下文是硬截断的,超过
N - 1个 token 的历史会被直接丢弃; - Transformer 可以在上下文窗口内对很多历史 token 分配不同注意力权重;
- N-Gram 对未见过的组合几乎没有语义泛化能力;
- Transformer 可以把相似词、相似短语、相似结构映射到相近表示空间;
- N-Gram 训练主要是计数,Transformer 训练是大规模梯度优化。
所以不能简单把 N-Gram 的 N 等同于大模型的 context length。4-gram 只看最近 3 个 token;而 8K context 的 Transformer 理论上可以让当前 token 使用前面 8K 范围内的信息。
11. 今天 N-Gram 还有什么用#
虽然 N-Gram 已经不是主流大语言模型架构,但它仍然很实用。
11.1 文本去重#
可以用 word n-gram 或 character n-gram 判断两个文档是否相似。
例如:
大模型推理性能优化
大语言模型推理性能优化二者可以通过字符 3-gram、MinHash、Jaccard 相似度等方法判断是否近似重复。
11.2 训练数据污染检测#
在评测大模型时,可以检查 benchmark 题目是否以相同或近似 N-Gram 的形式出现在训练集中。
这类方法不能证明模型一定“背过答案”,但可以作为污染风险分析的基础特征。
11.3 生成重复检测#
很多解码器支持禁止重复 N-Gram。例如 Hugging Face 里常见的参数:
no_repeat_ngram_size=3表示已经生成过的连续 3-token 片段不能再次生成。这对减少循环复读很有帮助。
11.4 轻量候选模型#
在一些 speculative decoding 或输出验证场景中,可以使用非常轻量的统计模型快速给出候选 token,再交给主模型验证。
现代系统更常使用小型神经网络作为 draft model,但 N-Gram 作为极轻量候选模型仍然有工程价值。
11.5 语音识别和资源受限场景#
传统 ASR 系统里,N-Gram 语言模型长期扮演重要角色:
声学模型 + 发音词典 + N-Gram 语言模型在资源受限、低延迟、固定领域的场景中,N-Gram 依然可能是一个足够简单、可解释、可控的选择。
12. 优点和缺点#
| 维度 | 优点 | 缺点 |
|---|---|---|
| 训练 | 主要是计数,速度快 | 高阶统计需要大量语料 |
| 推理 | 查表即可,延迟低 | 表可能很大 |
| 可解释性 | 每个概率都有频次来源 | 很难表达语义抽象 |
| 上下文 | 局部搭配建模直接 | 长距离依赖能力弱 |
| 泛化 | 固定领域短语效果可能很好 | 未见过组合需要平滑和回退 |
如果任务依赖固定短语、局部搭配、低延迟推理,N-Gram 仍然有价值。如果任务需要语义理解、长上下文推理、跨表达泛化,Transformer 会更合适。
13. 一句话理解#
N-Gram Model 的核心可以概括为:
根据最近 N - 1 个 token 的历史出现频率,预测下一个 token。例如 Trigram 就是:
看最近两个词,然后统计语料中这两个词后面最常跟什么词。它和现代大模型都属于自回归语言建模,但 N-Gram 靠的是 短窗口频次查表,Transformer 靠的是 长上下文神经网络计算。