news 2026/10/14 19:47:07

概率语言模型基石:链式法则与马尔可夫假设详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
概率语言模型基石:链式法则与马尔可夫假设详解

做NLP这几年,我越来越觉得,大部分看似复杂的模型问题,最后都能归到“概率怎么拆、假设怎么做”这两件事上。今天要聊的Chain Rule和Markov Assumption,恰恰是概率语言模型最底层的两块基石。如果你正在系统学NLP,或者打算自己实现一个文本生成、拼写纠错、语音识别候选重排的模块,这两个概念几乎是绕不开的。

先说人话版:Chain Rule告诉我们“一句完整的话出现的概率”可以被拆成一串条件概率相乘;Markov Assumption则告诉大家,计算条件概率时别贪心,只依赖最近的一两个词就够了,别把整段历史都背上。这两招组合在一起,构成了整个统计语言模型的起点——从最早的n-gram模型,到今天所有自回归大模型的最底层形式,都跑不出这个框架。

文章后面会给出完整的推导、Python实现、迷惑点排查和避坑经验,尽量让没接触过概率语言模型的同学也能照着做一遍。

1. 概率语言模型到底在算什么

1.1 一句话定义:给句子打分

概率语言模型的目标,用一句话说就是:输入一个句子,输出它出现的概率,也就是计算 P(w₁, w₂, ..., wₙ)。这里的 w 是句子里的词,下标表示顺序。

这个“打分”能力用途极广。拼写纠错时,候选词更正的句子要算概率,选最大的那个;输入法候选排序要考虑哪个句子更像人话;语音识别出一堆候选结果,也要靠语言模型挑出语义上最可能的那个;文本生成更是直接把概率当成采样依据。

你可能会问,为什么非要“概率”?因为概率天然给了我们一个统一的比较尺度:一句话概率大,说明语料里常见、语法语义上更合理;概率小,说明别扭。这个思路最朴素,也最好用。

1.2 直接统计整句话为什么行不通

最直觉的做法是:拿一个巨大的语料库,数一数某个句子出现了多少次,除以总句子数,就是概率。听起来很直接,但实际上完全不可行。

原因在于语料库不可能覆盖所有句子。自然语言的组合空间极大,大部分合法句子在语料库里出现次数是0。就算今天互联网语料再多,你随便写一句“昨晚那只橘猫在窗台上对着月亮发呆了半小时”,很可能全网都找不到完全一致的重复句子。Zipf分布告诉我们,绝大多数句子只出现一次,甚至零次。

所以直接数整句话这条路,本质上被数据稀疏性堵死了。我们需要的是“通过局部统计来逼近整体概率”的做法——这正是Chain Rule和Markov Assumption要解决的问题。

1.3 适合谁来学

如果你是刚入门NLP的学生,这部分内容是课程里最早的“硬骨头”,理解了它,后面看HMM、语言模型评估、机器翻译的噪声信道模型都会顺畅很多。如果你已经上手深度学习,正在用预训练模型做任务,回头补这块知识也不算浪费时间——很多线上模型的bad case,最后追查到底,都是底层条件概率处理不当。

2. 链式法则:把联合概率拆成一道递归式

2.1 先从两个事件看起

概率论里有一个恒等式:

P(A ∩ B) = P(A) × P(B | A)

意思是,A和B同时发生的概率,等于“A发生的概率”乘以“在A发生的条件下B发生的概率”。这不是什么近似,这是条件概率定义的直接推论,对任意事件都成立。

如果再加一个事件C,也能继续拆:

P(A ∩ B ∩ C) = P(A) × P(B | A) × P(C | A, B)

注意最后的因子变成了“在A和B都发生的情况下C发生的概率”。多一个事件,条件里就多一项历史。整个Chain Rule的核心就是这种递归拆分方式:

P(w₁, w₂, ..., wₙ) = P(w₁) × P(w₂ | w₁) × P(w₃ | w₁, w₂) × ... × P(wₙ | w₁, ..., wₙ₋₁)

2.2 从整句拆到局部

把这条规则套到句子上,意思就是:一个句子出现的概率,等于第一个词的概率,乘以在第一个词出现后第二个词的概率,再乘以在前两个词出现后第三个词的概率……一直乘到句尾。

这个拆法有什么好处?最关键的是,它把一个“几乎没法直接统计的整句事件”,变成了很多个“局部片段的条件概率”。比如“猫 喜欢 鱼”这个句子:

P(猫, 喜欢, 鱼) = P(猫) × P(喜欢 | 猫) × P(鱼 | 猫, 喜欢)

前两个因子在语料里很容易统计:“猫”出现的次数很多,“喜欢”出现在“猫”后面的情况也比较常见。第三个因子“猫 喜欢 之后出现 鱼”虽然稍微特殊一点,但比统计完整句子“猫喜欢鱼”出现多少次要容易得多——因为前者只需要看“猫 喜欢”后面接什么词。

这就是拆分的力量:把大问题切成小碎片,用小碎片的统计量拼出整体。

2.3 链式法则拆完了,问题并没解决

很遗憾,链式法则是恒等式,它只负责“拆”,不负责“算”。拆到最后,你还是要估计形如 P(wᵢ | w₁, ..., wᵢ₋₁) 这种带完整历史的条件概率。

这里的困难非常具体。假设词表大小是 V,要估计 P(wᵢ | w₁, ..., wᵢ₋₁),理论上需要为每一种历史组合都准备一组概率分布。一个长度为 m 的句子,其可能的历史组合数量高达 V 的 m 次方。哪怕 V 只有一万,m 只有六,V^m 就是 10 的 24 次方——这已经远远超过世界上所有语料的词数总和。

所以光有Chain Rule还不够,它给了我们一根“拆解线索”,但真正让计算可行的,是下面的马尔可夫假设。

3. 马尔可夫假设:用最近的活动窗口换掉全量历史

3.1 核心思想就一句:别管太远的事

马尔可夫假设的核心表述非常朴素:当前状态只依赖于最近的历史,而不是全部历史。放到语言模型上就是:

P(wᵢ | w₁, w₂, ..., wᵢ₋₁) ≈ P(wᵢ | wᵢ₋k, ..., wᵢ₋₁)

其中 k 是窗口大小。k=1 时,当前词只依赖前一个词;k=2 时,依赖前两个词。

这个假设看起来像偷懒,但语言学上是有直觉的。在大多数句子里,一个词怎么接下去,主要受最近一两个词的影响。比如“我 昨天 在 路边 看到 一只——”后面大概率接动物名词,因为最近的信息是“看到一只”;至于句首的“我昨天在路边”,对选词的影响已经弱很多了。

用一个生活化类比:你在群里聊天,判断对方下一句要说什么,往往不需要翻完整段聊天记录,只需要看最近几条消息就够了。马尔可夫假设就是这么个意思。

3.2 一阶、二阶和三阶到底差多少

我们常说的unigram、bigram、trigram,其实对应着不同强度的马尔可夫假设。

模型依赖历史长度条件概率形式参数规模量级
unigram0(不依赖历史)P(wᵢ)V
bigram1P(wᵢ | wᵢ₋₁)V²
trigram2P(wᵢ | wᵢ₋₂, wᵢ₋₁)V³

这里的 n-gram 中的 n 是“上下文窗口 + 1”,bigram 窗口是1,trigram 窗口是2。原理上,窗口越大,模型刻画上下文的能力越强,生成的文本也会更连贯。但代价是参数数量指数级增长,训练数据不够时,大量条件概率根本统计不到,出现严重的零概率问题。

这就是语言模型里最经典的一个权衡:大窗口更准,但更稀疏;小窗口更稳,但更笨。

3.3 马尔可夫假设不是万能药

必须要说清楚,马尔可夫假设是一种简化,不是真理。自然语言里存在大量长距离依赖,最典型的就是“如果……那么……”“虽然……但是……”这类结构。句子前半段的“如果”决定了后半句要用“那么”,中间可能隔了二十个词。如果只用bigram,模型根本抓不住这种关系。

所以真实工程里,要么提高阶数到trigram甚至更高,要么干脆放弃“硬截断窗口”的思路,用神经网络里的注意力机制去动态选择历史——Transformer就是干这个的。但理解马尔可夫假设仍然重要,因为它是“局部性先验”的最经典表达,也是理解注意力机制为什么能奏效的对照组。

4. 两者合体:一个bigram模型从0到1的完整推算

4.1 先造一个小语料,动手算一遍

理论讲完了,还是要落到数字上。假设我们有四句训练语料:

  • 我 喜欢 喝 咖啡
  • 我 喜欢 喝 茶
  • 他 喜欢 吃 苹果
  • 她 喜欢 看 书

现在要计算句子“我 喜欢 喝 咖啡”的概率。

按照链式法则,完整形式是:

P(我) × P(喜欢 | 我) × P(喝 | 我, 喜欢) × P(咖啡 | 我, 喜欢, 喝)

这里的条件历史很长,语料太小,统计不了。应用一阶马尔可夫假设,bigram模型把条件截断到前一个词:

P(我 |) × P(喜欢 | 我) × P(喝 | 喜欢) × P(咖啡 | 喝) × P(| 咖啡)

注意我在句首和句尾都加了特殊符号和。为什么要加?因为句子的第一个词前面其实有一个虚拟的“开头状态”,模型的第一个概率得有个来源,否则没法归一。

4.2 最大似然估计:比例就是概率

训练阶段,所有概率都是用最大似然估计(MLE)算出来的。公式极其简单:

P(wᵢ | wᵢ₋₁) = count(wᵢ₋₁, wᵢ) / count(wᵢ₋₁)

分母是“前一个词出现的总次数”,分子是“前一个词后面跟着当前词的出现次数”。本质上就是一个频率比,符合直觉:前一个词一共出现了多少次,其中多少次后面跟的是当前词,比例自然就是条件概率。

上面四句语料里,统计“喜欢”后面的词,得到:喝出现2次,吃出现1次,看出现1次。“喜欢”总共出现4次。所以:

P(喝 | 喜欢) = 2/4 = 0.5

同理:

P(咖啡 | 喝) = 1/2 = 0.5

因为“喝”后面只跟着两种词:“咖啡”和“茶”,各出现一次。这个例子也能看出,bigram的意义在于它捕捉了“局部搭配”:看到“喜欢”,语言模型知道后面最可能是“喝”,概率0.5;看到“喝”,模型知道后面只有咖啡或茶二选一。

4.3 为什么必须两个假设一块用

单独用链式法则,拆出来的条件概率还是没法算;单独用马尔可夫假设,没有说明怎么从整句概率出发做拆解。只有两者配合,才可以写成:

P(w₁, ..., wₙ) ≈ ∏ᵢ P(wᵢ | wᵢ₋k, ..., wᵢ₋₁)

这个式子就是所有n-gram语言模型的统一表达式。它同时完成了两件事:Chain Rule负责把联合概率拆成可解释的乘积,Markov Assumption负责把乘积里每个条件概率的历史压缩到固定窗口。一个负责“拆”,一个负责“截”,缺一不可。

5. 实操:用Python搭一个可评估的n-gram语言模型

5.1 最小可用代码:统计与概率估计

直接上完整可运行的Python代码,我用最朴素的字典结构,不依赖重型库,方便你一行行看懂。

from collections import defaultdict, Counter def build_bigram(corpus): # corpus 是字符串列表,每行已经按空格分词 bigram_counts = defaultdict(Counter) first_counts = Counter() for line in corpus: tokens = ['<s>'] + line.strip().split() + ['</s>'] first_counts[tokens[0]] += 1 for i in range(len(tokens) - 1): bigram_counts[tokens[i]][tokens[i + 1]] += 1 return first_counts, bigram_counts corpus = [ '我 喜欢 喝 咖啡', '我 喜欢 喝 茶', '他 喜欢 吃 苹果', '她 喜欢 看 书' ] first_counts, bigram_counts = build_bigram(corpus) def bigram_prob(prev, cur): total = sum(bigram_counts[prev].values()) if total == 0: return 0.0 return bigram_counts[prev].get(cur, 0) / total # 测试 print(bigram_prob('我', '喜欢')) # 我后面只跟喜欢,概率1.0 print(bigram_prob('喜欢', '喝')) # 2/4 = 0.5 print(bigram_prob('喝', '咖啡')) # 1/2 = 0.5

5.2 句子打分:log域计算避免下溢

句子概率是多个小于1的数相乘,句子稍长,乘积就会小到超出浮点数能表示的精度范围,直接乘会得到0。工程上通通转成对数相加。

import math def sentence_log_prob(sentence): tokens = ['<s>'] + sentence.strip().split() + ['</s>'] logp = 0.0 for i in range(len(tokens) - 1): p = bigram_prob(tokens[i], tokens[i + 1]) if p <= 0: return float('-inf') # 概率为0,直接判死 logp += math.log(p) return logp print(sentence_log_prob('我 喜欢 喝 咖啡')) print(sentence_log_prob('他 喜欢 喝 咖啡'))

第二句“他 喜欢 喝 咖啡”有没有问题?逐项拆开:P(他|)有,P(喜欢|他)有,P(喝|喜欢)有,P(咖啡|喝)有,全都不为0,所以bigram会给它一个有限的对数概率。尽管这个句子根本没在训练语料里出现过,但bigram靠局部片段就能给出来概率,这正是马尔可夫假设的价值。

但如果算“他 喜欢 喝 书”,到 P(书|喝) 这一步,因为训练语料里“喝”后面只有咖啡和茶,概率就是0,整个句子被判为不可能事件。

5.3 用困惑度横向对比不同阶数的模型

有了打分函数,就要有评估指标。语言模型最常用的指标是困惑度,定义是:

perplexity = exp(- (1/N) × ∑ log P(wᵢ | history))

N是测试语料里的总词数。困惑度越小,说明模型给真实文本打的概率越高,也就是预测能力越强。

下面实现一个完整的困惑度计算函数,同时支持一元、二元和三元模型的简单对比(这里我把n=1/2/3分别映射为不同历史长度)。

def build_ngram(corpus, n): counts = defaultdict(Counter) for line in corpus: tokens = ['<s>'] * (n - 1) + line.strip().split() + ['</s>'] for i in range(len(tokens) - (n - 1)): history = tuple(tokens[i:i + n - 1]) cur = tokens[i + n - 1] counts[history][cur] += 1 return counts def ngram_prob(counts, history, cur): total = sum(counts[history].values()) if total == 0: return 0.0 return counts[history].get(cur, 0) / total def perplexity(corpus, n): counts = build_ngram(corpus, n) total_logp = 0.0 total_tokens = 0 for line in corpus: tokens = ['<s>'] * (n - 1) + line.strip().split() + ['</s>'] for i in range(len(tokens) - (n - 1)): history = tuple(tokens[i:i + n - 1]) cur = tokens[i + n - 1] p = ngram_prob(counts, history, cur) if p <= 0: return float('inf') total_logp += math.log(p) total_tokens += 1 return math.exp(-total_logp / total_tokens) print(f"unigram perplexity: {perplexity(corpus, 1):.2f}") print(f"bigram perplexity: {perplexity(corpus, 2):.2f}") print(f"trigram perplexity: {perplexity(corpus, 3):.2f}")

在这么小的玩具语料上,结果可能反直觉:有时候higher-order的困惑度反而更大,甚至直接变成inf。原因很简单,语料太小,高阶n-gram统计到的组合太稀疏,大量没见过的历史组合导致测试时概率归零。真实场景中,语料越大,高阶模型越占优势。这也说明,n-gram的阶数选择不是越大越好,而是要综合考虑语料量与上下文需求。

6. 一进真实数据就炸的坑:零概率与平滑方法

6.1 零概率是最大的假象

上面代码里有个非常大的隐患:只要某个n-gram组合在训练集里从未出现,测试时概率就是0,句子整体概率直接变成0,对数概率变成负无穷。这种零概率会扼杀所有可能性——哪怕整句话非常通顺,仅仅因为一个局部搭配没在语料里出现过,模型就残忍地给它判死刑。

真实语料远比你想象的稀疏。即使几十亿词的语料,still会有大量合理但罕见的搭配没被收录。比如“喝 空气”你大概率没见过,但小说里完全可能写出这种句子。零概率不是“这张牌不存在”,而是“我们没统计到这张牌”。

解决这个问题的统称叫平滑(Smoothing),核心思想是:给那些没见过的组合也留一点概率,把见过的高频组合的概率稍微往下让一让。

6.2 几种平滑方案怎么选

最简单的方案是加一平滑,也叫拉普拉斯平滑:

P(wᵢ | wᵢ₋₁) = (count(wᵢ₋₁, wᵢ) + 1) / (count(wᵢ₋₁) + V)

V是词表大小。分子加1,分母加V,目的是保证整个概率分布加起来仍然等于1。这个方法简单,但坏处也很明显:会把过多概率分配给没见过的组合,尤其是在词表很大时,每个高频词的条件概率被稀释得厉害。

更好的方案是插值平滑:把高阶和低阶模型线性混合,比如:

P_interp(wᵢ | wᵢ₋₂, wᵢ₋₁) = λ₃ × P_ml(wᵢ | wᵢ₋₂, wᵢ₋₁) + λ₂ × P_ml(wᵢ | wᵢ₋₁) + λ₁ × P_ml(wᵢ)

高阶层没见过的,低阶层也许见过;低阶层也没见过的,unigram兜底。这在实践里非常有效。

更高级的还有回退(backoff)和Kneser-Ney平滑。Kneser-Ney的核心思想很有意思:估算一个词的“一元概率”时,看的不是它出现多少次,而是它能搭配多少种不同上下文。比如“好”这个字很常见,但真正重要的是它出现在多少种不同历史后面。这个思路比朴素的词频更贴近语言实际,也是业界效果最好的传统平滑方法之一。对初学者,我建议先把加一平滑和插值平滑吃透,再进阶Kneser-Ney。

6.3 其他高频坑:分词、OOV、语料分布

除了零概率,还有几个坑我实际踩过,值得提前说。

第一是分词不一致。训练语料用了一个分词器,测试语料换了另一个分词器,结果同一个词被切开或拼上,所有n-gram组合全变了。看起来模型性能骤降,实则是数据不对齐。

第二是未登录词(OOV)。测试时遇到一个训练词表里没有的词,直接导致概率为0。工程上通常会把低频词统一替换成特殊符号 ,然后给 留一个概率空间。这样虽然模型说不出这个生词的具体概率,但至少不会被一个陌生词击穿整个句子。

第三是语料分布偏移。你用新闻语料训练模型,然后拿去给聊天对话排序,效果一定差。n-gram模型本质上是把训练集的统计规律原样复述出来,没有泛化到完全不同风格的文本上。这个限制在深度学习时代依然存在,只是被大规模预训练缓解了一部分。

7. 从统计语言模型到深度学习:这套底层逻辑还活着

7.1 自回归模型本质上就是链式法则

很多人一上来就学Transformer,会以为大规模语言模型和传统统计语言模型是两码事。但如果打开任何一个自回归语言模型,比如最典型的GPT系列,它的训练目标本质上还是:

P(x₁, x₂, ..., x_T) = ∏ P(x_t | x₁, ..., x_{t-1})

这就是链式法则,一字不差。区别只在于,n-gram模型用马尔可夫假设把历史截断到最近k个词,而神经网络模型用隐藏状态(或者注意力矩阵)把“历史”压缩成了一个固定维度的向量,理论上能记住更长的依赖。

马尔可夫假设之所以在深度时代依然有价值,是因为“局部性”这个先验在语言中真实存在。即便Transformer已经能捕捉长距离依赖,实际应用中仍然会给它位置编码,让它知道词序;仍然会用滑动窗口式注意力,本质上仍然是“看近不看远”的归纳偏置。

7.2 teacher forcing 的数学含义

训练基于RNN或Transformer的语言模型时,有一个几乎人人都在用但少有人细想的操作叫teacher forcing。它的意思是:训练时,预测第t个词的时候,输入的是真实的前一个词,而不是模型自己生成的词。

为什么这样做?因为训练目标函数是最大化如下对数似然:

L = ∑ log P(x_t | x₁, ..., x_{t-1})

这个函数里的条件历史,用的是真实前文 x₁, ..., x_{t-1}。这不就是最大似然估计在链式法则下的直接应用吗?模型每一步都在学着给“真实前文条件下出现真实当前词”赋予更高概率。生成时模型换成自回归采样,自己的预测输出被当作下一时刻的输入,原理上跟n-gram一个路数:用已知的前文去推下一个词。

想通这一点,你会对“模型训练和生成之间的exposure bias问题”理解得更扎实:训练时一直喂真实历史,生成时历史里开始混入模型自己的错误,分布不再匹配,这正是很多文本生成模型越跑越偏的一个原因。

7.3 关于实践的一个个人体会

我刚学NLP那阵,一直盯着各种花哨模型,觉得n-gram和马尔可夫假设太老、太简单。真到了上手做项目,用n-gram搭一个轻量候选排序模块,发现效果稳得超出预期。反而是后来用复杂模型时,经常因为忽略概率基础,在数据处理和训练目标上埋雷。

我还会偶尔回来看一遍Chain Rule和马尔可夫假设。每次都有点新体会。最近一个是:如果你能把链式法则和马尔可夫假设用最简单的话给同事讲明白,那你对语言模型就已经建立起了整体直觉。很多线上模型的问题,最后定位到根因都很朴素——编码不对齐、条件概率没归一化、某个零概率没兜底。这些都不是模型架构层面的问题,而是对上篇文章里这两块基石的掌握程度问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 19:46:31

PHPCMS整站模板部署与安全迁移实战指南

简介&#xff1a;一套以PHPCMS为基础构建的整站前后台模板&#xff0c;适合使用PHPMySQL搭建大型网站或进行二次开发的开发者参考。资源涵盖前台页面与后台管理布局&#xff0c;模板机制将页面样式与程序逻辑分离&#xff0c;便于按实际需求替换HTML/CSS并扩展文章、图片、下载…

作者头像 李华
网站建设 2026/10/14 19:45:51

高职质检员转型品质管理的6大核心能力与实施路径

1. 高职学历质检专员转型品质管理分析的可行性路径我见过太多质检岗位的同事在职业发展上遇到瓶颈&#xff0c;特别是高职学历背景的同行。从重复性检测工作转向需要综合分析的品质管理岗位&#xff0c;确实存在知识结构和思维模式的差异。但根据我辅导过的37位成功转型案例来看…

作者头像 李华
网站建设 2026/10/14 19:45:24

怎么判断文章是不是AI写的?aigc检测原理和论文降AI率方法一次看懂!

怎么判断文章是不是AI写的&#xff1f;aigc检测原理和论文降AI率方法一次看懂&#xff01; 想知道一篇文章是不是AI写的&#xff0c;现在大家都会丢进aigc检测里测一下。可同一篇文章换个平台测&#xff0c;结果就不一样&#xff0c;自己写的论文也可能被判出很高的AI率。 检…

作者头像 李华
网站建设 2026/10/14 19:44:53

Windows 11 环境下 BAT 脚本无法执行的排查与修复指南

前言 在 Windows 11 的日常使用和系统运维中&#xff0c;.bat&#xff08;批处理&#xff09;脚本因其轻量、便捷的特性&#xff0c;依然是自动化任务和环境配置的重要工具。然而&#xff0c;不少用户在 Win11 环境下会遇到 bat 文件无法运行的问题。具体表现通常为&#xff1a…

作者头像 李华
网站建设 2026/10/14 19:44:45

【ESP32笔记】SDIO读写TF卡

本节目标&#xff1a;分清 SDIO 与 SPI 两种接法&#xff0c;学会用 FATFS 挂载 TF 卡&#xff0c;并用标准 C 文件 API 完成读写一、本节要解决的问题 ESP32 自带几 MB Flash&#xff0c;装完程序后剩下的空间有限&#xff0c;而且数据不方便拿到电脑上。遇到记录日志、存图片…

作者头像 李华