查看全课大纲(第 11 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
n-gram 语言模型(二元 / 三元 / N-Gram)
小象实战讲义 · 自然语言处理
在上一节,我们学习了最简单的词袋模型(Bag-of-Words),它假设句子中的词是独立出现的。然而,语言具有强烈的序列依赖性,例如“我爱北京”之后,更可能接“天安门”而非“鼠标”。本节我们将引入马尔可夫假设,构建能捕捉词序依赖的n-gram语言模型。从二元(bigram)、三元(trigram)到通用的N-gram模型,我们将学习如何基于统计频率来估计一个句子出现的概率,并理解其在语音识别、分词、拼写纠错等任务中的核心作用。
💡 核心导读
- 马尔可夫链与语言模型:理解马尔可夫特性如何将复杂的联合概率分解为易于估计的条件概率链。
- 从 Unigram 到 N-gram:掌握 Bigram、Trigram 模型的定义与概率计算公式,理解 N 的选择对模型性能与数据稀疏性的影响。
- 模型估计与平滑技术:学习如何通过最大似然估计(MLE)从语料库中统计 n-gram 频率,并初步了解平滑技术(如加一平滑)的必要性。
- N-gram 的应用:通过中文分词实例,直观感受如何利用语言模型的概率评分来选择最优的分词路径。
从句子概率到马尔可夫假设
语言模型(Language Model)的核心是计算一个句子(词序列)出现的概率 。在语音识别中,给定声学信号,我们需要从多个可能的文本候选(如 “she studies morphosyntax” 与 “she studies more faux syntax”)中,选择那个在自然语言中更可能出现的句子,即概率更高的那个。
直接估计长序列的联合概率极其困难。根据概率链式法则,我们可以将其分解为一系列条件概率的乘积:
但这要求我们估计 ,其历史上下文 的组合在语料库中可能从未出现,导致数据稀疏。
马尔可夫假设(Markov Assumption)为此提供了解决方案。它假设一个词出现的概率只依赖于其前面有限个词(马尔可夫链中的状态),而非全部历史。这正是马尔可夫特性(Markov Property)在语言建模中的应用。需要区分阶数:一阶马尔可夫假设对应 Bigram,当前词只依赖前一个词;二阶对应 Trigram,依赖前两个词;一般 N-gram 使用前 个词。若保留“前一状态”的说法,应先把状态定义为最近 个词的整体。
基于此,我们得到一系列简化模型:
- Unigram 模型:词完全独立,。这就是上节的词袋模型。
- Bigram(二元)模型:词只依赖于前一个词,。
- Trigram(三元)模型:词依赖于前两个词,。
- N-gram 模型:词依赖于前 N-1 个词。
Bigram 与 Trigram 模型详解
Bigram 模型
在 Bigram 模型中,句子的概率近似为: 其中, 是给定前一个词 时,当前词 出现的条件概率。
为了处理句子边界,我们通常引入特殊的开始符号 <s> 和结束符号 </s>(或 STOP)。例如,句子 “the dog barks” 在 Bigram 模型下表示为:
Trigram 模型
Trigram 模型进一步考虑了前两个词的历史,其概率公式为: 同样需要处理边界。对于句子 “the dog barks STOP”,其 Trigram 概率为: 这里用两个 <s> 作为句子的起始上下文。
参数估计:最大似然估计(MLE)
n-gram 模型的参数,即条件概率 ,可以通过在大型语料库上统计频率来估计(最大似然估计)。
对于 Trigram 概率 ,其 MLE 估计为: 其中 表示在语料库中出现的次数。分子是三元组 (u, v, w) 出现的次数,分母是二元组 (u, v) 出现的次数。
同理,Bigram 概率为:。
Unigram 概率为:,其中 是语料库总词数。
让我们通过一个极简的代码示例来理解频率统计和概率计算。
# 示例:在一个微型语料库上统计 Unigram, Bigram, Trigram 频率
corpus = [
["i", "love", "this", "film", "."],
["this", "actor", "is", "horrible", "."],
["i", "love", "the", "music", "."]
]
# 1. 统计 Unigram 频率
from collections import defaultdict
uni_counts = defaultdict(int)
total_words = 0
for sent in corpus:
for word in sent:
uni_counts[word] += 1
total_words += 1
print("Unigram 频率:")
for word, count in sorted(uni_counts.items()):
print(f" {word}: {count}")
print(f"总词数 N = {total_words}\n")
# 2. 统计 Bigram 频率
bi_counts = defaultdict(int)
for sent in corpus:
for i in range(len(sent)-1):
bigram = (sent[i], sent[i+1])
bi_counts[bigram] += 1
print("Bigram 频率统计:")
for bigram, count in sorted(bi_counts.items()):
print(f" {bigram}: {count}")
# 3. 计算一个 Bigram 概率,例如 P(love | i)
c_i = uni_counts["i"]
c_i_love = bi_counts[("i", "love")]
p_love_given_i = c_i_love / c_i if c_i > 0 else 0.0
print(f"\nP(love | i) = C(i, love) / C(i) = {c_i_love} / {c_i} = {p_love_given_i:.3f}")
# 4. 统计 Trigram 频率并计算概率
tri_counts = defaultdict(int)
for sent in corpus:
for i in range(len(sent)-2):
trigram = (sent[i], sent[i+1], sent[i+2])
tri_counts[trigram] += 1
print("\nTrigram 频率统计:")
for trigram, count in sorted(tri_counts.items()):
print(f" {trigram}: {count}")
# 计算一个 Trigram 概率,例如 P(film | love, this)
c_love_this = bi_counts[("love", "this")]
c_love_this_film = tri_counts[("love", "this", "film")]
p_film_given_love_this = c_love_this_film / c_love_this if c_love_this > 0 else 0.0
print(f"\nP(film | love, this) = C(love, this, film) / C(love, this) = {c_love_this_film} / {c_love_this} = {p_film_given_love_this:.3f}")Unigram 频率:
.: 3
actor: 1
film: 1
horrible: 1
i: 2
is: 1
love: 2
music: 1
the: 1
this: 2
总词数 N = 15
Bigram 频率统计:
('actor', 'is'): 1
('film', '.'): 1
('horrible', '.'): 1
('i', 'love'): 2
('is', 'horrible'): 1
('love', 'the'): 1
('love', 'this'): 1
('music', '.'): 1
('the', 'music'): 1
('this', 'actor'): 1
('this', 'film'): 1
P(love | i) = C(i, love) / C(i) = 2 / 2 = 1.000
Trigram 频率统计:
('actor', 'is', 'horrible'): 1
('i', 'love', 'the'): 1
('i', 'love', 'this'): 1
('is', 'horrible', '.'): 1
('love', 'the', 'music'): 1
('love', 'this', 'film'): 1
('the', 'music', '.'): 1
('this', 'actor', 'is'): 1
('this', 'film', '.'): 1
P(film | love, this) = C(love, this, film) / C(love, this) = 1 / 1 = 1.000数据稀疏与平滑技术
从上面的例子可以看到,在微型语料库中,许多 bigram 和 trigram 只出现了一次。在真实语料库中,绝大多数可能的 n-gram 根本不会出现,导致其 MLE 概率为 0。例如,若 C(u, v) = 0,则任何 都无法计算;若 C(u, v) > 0 但 C(u, v, w) = 0,则 ,这会使整个句子的概率为 0,显然不合理。
平滑技术(Smoothing)就是为了解决零概率问题,将一部分概率质量重新分配给未出现或出现次数少的 n-gram。最简单的平滑方法是加一平滑(Laplace Smoothing),也称为拉普拉斯平滑。
在加一平滑中,我们在计数时给每一个可能的 n-gram 都加 1。对于 Vocabulary 大小为 的语料库,平滑后的 Bigram 概率为:
# —— 重新构建上文用到的微型数据(本节各代码块相互独立,可单独运行)
corpus = [
["i", "love", "this", "film", "."],
["this", "actor", "is", "horrible", "."],
["i", "love", "the", "music", "."]
]
from collections import defaultdict
uni_counts = defaultdict(int)
total_words = 0
for sent in corpus:
for word in sent:
uni_counts[word] += 1
total_words += 1
bi_counts = defaultdict(int)
for sent in corpus:
for a, b in zip(sent, sent[1:]):
bi_counts[(a, b)] += 1
# 演示加一平滑(拉普拉斯平滑)对 Bigram 概率的影响
# 下面直接使用本块统计出的 uni_counts 与 bi_counts,词典 V 取语料中出现的不同词数
V = len(uni_counts) # 词典大小(包含特殊符号)
print(f"词典大小 V = {V}")
# 未平滑的 P(horrible | is)
c_is = uni_counts["is"]
c_is_horrible = bi_counts[("is", "horrible")]
p_unsmoothed = c_is_horrible / c_is if c_is > 0 else 0.0
print(f"未平滑: P(horrible | is) = {c_is_horrible} / {c_is} = {p_unsmoothed:.3f}")
# 加一平滑的 P(horrible | is)
p_smoothed = (c_is_horrible + 1) / (c_is + V)
print(f"加一平滑: P(horrible | is) = ({c_is_horrible}+1) / ({c_is}+{V}) = {p_smoothed:.4f}")
# 对于一个未出现的 Bigram,例如 P(music | is),未平滑概率为0
c_is_music = bi_counts.get(("is", "music"), 0)
p_unsmoothed_new = c_is_music / c_is if c_is > 0 else 0.0
print(f"\n未平滑: P(music | is) = {c_is_music} / {c_is} = {p_unsmoothed_new:.3f}")
# 加一平滑后,它有了一个非零的概率
p_smoothed_new = (c_is_music + 1) / (c_is + V)
print(f"加一平滑: P(music | is) = ({c_is_music}+1) / ({c_is}+{V}) = {p_smoothed_new:.4f}")词典大小 V = 10
未平滑: P(horrible | is) = 1 / 1 = 1.000
加一平滑: P(horrible | is) = (1+1) / (1+10) = 0.1818
未平滑: P(music | is) = 0 / 1 = 0.000
加一平滑: P(music | is) = (0+1) / (1+10) = 0.0909可以看到两件事。第一,语料里真实出现过的 (is, horrible) 未平滑概率是 1.000,加一平滑后被压到 0.1818——平滑不是免费的,它用”降低已见事件的概率”来换”给未见事件留出概率质量”。第二,(is, music) 在语料中从未出现,未平滑为 0,加一平滑后得到 0.0909,零概率问题就此消失。
所以平滑的本质是概率质量再分配,而不是单纯把概率调小;平滑强度越大,已见事件被压得越狠、未见事件分到得也越多。在实际应用中,还有更精细的平滑方法,如 Good-Turing 估计、Kneser-Ney 平滑等,它们在分配概率质量时更加合理。
N-gram 模型的应用:以中文分词为例
N-gram 语言模型是许多 NLP 任务的基础组件。一个经典应用是中文分词。中文文本没有显式的词边界,分词任务就是将连续的字符序列切分成有意义的词序列。
假设我们有一句话“结合成分子”,有两种可能的分词方式:
结合 / 成 / 分子结 / 合成 / 分子
我们可以利用 Bigram 语言模型来评估哪种分词序列更合理。计算每种分词序列的 Bigram 概率(假设已给定词表及相应的 Bigram 概率):
- 序列1:
- 序列2:
比较两个概率乘积的大小,值更大的分词序列被认为更符合语言习惯。在实际系统中(如 Jieba 分词器的默认模式),会使用类似维特比(Viterbi)的动态规划算法,在所有可能的分词路径中,快速找到概率乘积最大的最优路径。
# 伪代码:基于 Bigram 模型的最优分词路径搜索(概念示意)
输入:字符序列 C = c1 c2 ... cn,Bigram 概率表 P(w_i|w_{i-1})
输出:最优分词序列 W*
1. 构建一个有向无环图(DAG),节点代表字符位置,边代表词典中存在的词。
- 例如,对于"结合成分子",边可能包括: (0,2)->"结合", (0,1)->"结", (1,3)->"合成", (2,4)->"成分", (3,5)->"分子"等。
2. 使用动态规划计算从起点(位置0)到每个位置 j 的最大概率及对应路径。
- 令 dp[j] 为到达位置 j 的最大概率。
- dp[j] = max_{i < j} { dp[i] * P( word(i, j) | word(prev_i, i) ) },其中 word(i,j) 是子串 ci...c_{j-1} 对应的词。
3. 回溯得到概率最大的分词序列 W*。N 的选择与模型局限性
N 越大,模型能捕捉的上下文信息越长,理论上越强。但随之而来的是严重的数据稀疏问题:N-gram 的数量随 N 呈指数级增长,导致绝大多数 N-gram 在训练语料中从未出现,概率估计极不可靠。因此,在实际应用中,N 通常取 2(Bigram)或 3(Trigram),很少超过 5。
N-gram 模型是典型的统计语言模型,它完全基于表面的共现频率,缺乏对词义、句法结构的深层理解。例如,它无法理解“狗咬人”和“人咬狗”在语义上的巨大差异,如果两者在语料中频率相近,则会被赋予相似的概率。
今天怎么做:尽管 N-gram 模型在深度学习时代已不再是主流的核心模型,但它因其简单、高效、可解释性强,仍然在许多场景中发挥着作用,例如:
- 作为大型神经语言模型的快速、轻量级回退(back-off)或插值(interpolation)组件。
- 在资源受限的环境(如嵌入式设备)中执行简单的文本生成或评估任务。
- 作为基线模型(baseline)用于评估更复杂模型的性能提升。
在现代预训练语言模型(如 GPT、BERT)中,虽然其核心是神经网络,但训练目标(如语言模型任务)在概念上与 N-gram 模型一脉相承——都是学习预测下一个词(或掩码词)的概率分布,只是神经模型用连续的向量表示和复杂的非线性变换,替代了离散的计数和查表。
📝 动手练一练
- 概念辨析:假设我们有一个 Trigram 语言模型。对于句子 “A B C D”,请写出使用该模型计算其概率 的完整公式(包含开始和结束符号)。
👉 点击查看参考答案
设开始符号为 <s>,结束符号为 </s>。 句子在 Trigram 模型下的概率为: 每个条件概率 都可以通过统计语料库中三元组 和二元组 的频率来估计。
- 手算练习:给定一个微型语料库:
["cat sits", "dog sits", "cat runs"]。请计算: a) 词汇表 (包含<s>和</s>,假设它们已隐含在句子边界)。 b) 采用 加一平滑(Laplace Smoothing) 的 Bigram 语言模型中, 的概率是多少?
👉 点击查看参考答案
首先,将语料库视为三个句子,每个句子前后有隐含边界。 词汇表 = {<s>, </s>, cat, sits, dog, runs},共 |V| = 6 个词。
统计 Bigram 计数(未平滑):
- (
<s>,cat): 2次 (出现在两个“cat”开头的句子) - (
<s>,dog): 1次 - (
cat,sits): 1次 - (
cat,runs): 1次 - (
sits,</s>): 2次 (两个“sits”结尾的句子) - (
dog,sits): 1次 - (
runs,</s>): 1次
计算 :
- (只有
dog sits中的dog)
因此,平滑后 。
本章小结
本节我们深入探讨了 n-gram 语言模型,它是统计自然语言处理的基石。通过引入马尔可夫假设,我们将复杂的句子概率估计问题,分解为估计有限历史上下文下的条件概率问题。Bigram 和 Trigram 模型因其在模型能力与数据稀疏性之间的较好平衡而被广泛使用。我们学习了如何通过最大似然估计从语料库中统计频率来估计模型参数,并认识了处理零概率问题的平滑技术的必要性。最后,我们以中文分词为例,看到了 n-gram 模型在实际任务中的应用。尽管在深度学习时代,n-gram 模型的核心地位已被神经网络取代,但其思想精髓——基于上下文预测词的概率——仍然是现代语言模型的核心任务之一。
📋 行动清单
- 理解并能够推导 Bigram 和 Trigram 模型计算句子概率的公式。
- 掌握基于计数进行 n-gram 概率的最大似然估计方法。
- 理解数据稀疏问题,并能说明加一平滑的基本原理与计算过程。
- 了解 n-gram 模型在中文分词等任务中的应用思路。
—— 小象教研组