numpy-ml N-gram 平滑模型实战指南:Laplace、Additive/Lidstone 与 Good-Turing 平滑的原理与 NumPy 实现
2026/9/21 2:30:48 网站建设 项目流程
  • 机器学习
  • 人工智能

【免费下载链接】numpy-ml

Machine learning, in numpy

项目地址:https://gitcode.com/gh_mirrors/nu/numpy-ml
点击查看免费下载

N-gram 语言模型是统计自然语言处理的基础组件,其核心难题在于稀疏数据下的概率估计:训练语料中大量合法的 N-gram 从未出现,若直接采用最大似然估计,未出现序列的概率会被错误地估计为零。本文基于 numpy-ml 仓库的 numpy_ml.ngram.rst 文档,系统讲解 Laplace 平滑、Additive/Lidstone 平滑与 Good-Turing 平滑三种经典平滑技术的数学原理,并结合仓库源码 numpy_ml/ngram/ngram.py 与其测试用例,说明它们的 NumPy 实现、调用方式与效果差异。读完本文,你将掌握平滑概率公式的推导、三个模型类的完整 API 用法,以及如何用困惑度(perplexity)评估和比较不同平滑策略。

本文讨论的平滑模型全部收敛于 numpy-ml 的numpy_ml.ngram子模块,其模块结构如下:

  • numpy_ml/ngram/ngram.py:三个模型类与共享基类的完整实现;
  • numpy_ml/ngram/__init__.py:从ngram.py导出全部类;
  • numpy_ml/ngram/README.md:模块概述与效果图;
  • numpy_ml/tests/test_ngram.py:以 NLTK 为基准的对照测试;
  • numpy_ml/plots/ngram_plots.py:平滑效果可视化脚本。

一、为什么需要平滑:稀疏数据下的概率估计问题

在 N-gram 模型中,一个词序列w_i^j = (w_i, w_{i+1}, ..., w_j)(即长度为j - i的 N-gram)的条件概率通常由语料中的经验频次直接估计。然而真实语料永远无法覆盖所有可能的词组合:随着 N 增大,可能的 N-gram 数量呈指数增长,而训练数据只是其中极小一部分。docs/numpy_ml.ngram.rst开篇即点明主题:

处理 N-gram 模型时,平滑(smoothing)指的是调整经验概率估计以应对数据不足的做法。

平滑的本质是"劫富济贫":从高频 N-gram 的概率质量中分出一部分,转移给低频或未出现的 N-gram,避免零概率,同时尽量保持模型的整体概率分布合理。

numpy-ml 在 numpy_ml/ngram/ngram.py 中通过抽象基类NGramBase统一了所有平滑模型的骨架。它接收四个超参数,全部通过hyperparameters字典管理:

参数类型默认值含义
Nint必填语言模型的最大上下文窗口长度(词数),训练时会计算 1, 2, ..., N 阶所有 N-gram
unkboolTrue是否在语言模型中保留<unk>(未知词)标记
filter_stopwordsboolTrue训练前是否过滤停用词
filter_punctuationboolTrue训练前是否过滤标点

NGramBase还提供了一批所有平滑模型共享的核心方法(源码定义):

  • train(corpus_fp, vocab=None, encoding=None):统计语料中 N, N-1, ..., 1-gram 的计数,结果存于self.countsself.n_wordsself.n_tokens
  • completions(words, N):返回给定前缀下所有可能后继词及其对数概率;
  • generate(N, seed_words, n_sentences):用模型采样生成句子(遇到<eol>结束一句);
  • perplexity(words, N)cross_entropy(words, N):评估模型在测试序列上的表现。

三个具体模型MLENGramAdditiveNGramGoodTuringNGram均继承自NGramBase,只实现两个抽象方法log_prob_log_ngram_prob(抽象定义),即"整句对数概率"与"单个 N-gram 对数概率"。这意味着三种平滑技术的差异,最终只体现在_log_ngram_prob这一个函数的概率公式上——这是理解本模块架构的关键。

二、Laplace 平滑:加一平滑的朴素假设

docs/numpy_ml.ngram.rst给出的第一种平滑技术是 Laplace 平滑(也常被称为"加一平滑")。它的假设非常朴素:认为语料中每个 N-gram 实际上都比观测到的计数多出现一次

c(w_{i-n+1}^i)为语料中 N-gramw_{i-n+1}^i的经验计数,|V|为语料中不重复 N-gram 的种类数(词表大小),则条件概率公式为:

p(w_i | w_{i-n+1}^{i-1}) = (1 + c(w_{i-n+1}^i)) / (|V| + Σ_{w_i} c(w_{i-n+1}^i))

即分子加 1,分母加上|V|以保证所有可能的下一词概率之和仍为 1。这样,未出现在语料中的 N-gram 也能获得1 / (|V| + ...)的非零概率。

从文档可以看到,Laplace 平滑对应的模型是AdditiveNGram——它是 Additive(加性)平滑在K = 1时的特例,因此没有独立的模型类,这也解释了为什么文档的Models列表中 Laplace 平滑与 Additive 平滑指向同一个类AdditiveNGram

三、Additive / Lidstone 平滑:一般化的加性平滑

3.1 数学原理

Laplace 平滑强行加 1 过于武断。Additive(Lidstone)平滑将其推广为每个 N-gram 都比实际多出现k,其中k可以是任意非负值,但通常取值在[0, 1]区间(文档定义):

p(w_i | w_{i-n+1}^{i-1}) = (k + c(w_{i-n+1}^i)) / (k·|V| + Σ_{w_i} c(w_{i-n+1}^i))

其中c(a)仍是 N-grama的经验计数,|V|是语料中不重复 N-gram 的种类数。当k = 0时退化为未平滑的最大似然估计;k越大,分配给未见事件(unseen events)的概率质量越多。

3.2AdditiveNGram的源码实现

AdditiveNGram的构造函数(源码)在基类参数之上新增了核心超参数K

参数默认值说明
K1加到每个观测上的伪计数(pseudocount)。K = 1时即 Laplace 平滑;K = 0.5时称为期望似然估计(expected likelihood estimation, ELE),即 Jeffreys-Perks 法则

文档与源码都强调了一个重要的概率论视角:Additive 平滑的估计结果等价于在计数上施加对称 Dirichlet 先验(参数为K)后,后验p(ngram_prob | counts)的期望值。也就是说,K不是拍脑袋的调参量,而是 Dirichlet 先验的强度参数。

其核心概率计算实现于_log_ngram_prob(源码):

def _log_ngram_prob(self, ngram): N = len(ngram) K = self.hyperparameters["K"] counts, n_words, n_tokens = self.counts, self.n_words[1], self.n_tokens[1] ctx = ngram[:-1] num = counts[N][ngram] + K ctx_count = counts[N - 1][ctx] if N > 1 else n_words den = ctx_count + K * n_tokens return np.log(num / den) if den != 0 else -np.inf

对照公式可见:分子是"经验计数 + K",分母是"上下文计数 + K × 词表大小n_tokens"。对于 bigram(N=2),源码 docstring 给出了直观写法:

P(w_i | w_{i-1}) = (A + K) / (B + K·V)

其中A = Count(w_{i-1}, w_i)B = Σ_j Count(w_{i-1}, w_j)V = |{w_j : Count(w_{i-1}, w_j) > 0}|。这等价于假装每一个可能的 N-gram 序列都至少被观察过 K 次

3.3 已知缺陷

文档与源码明确列出了 Additive 平滑的两个问题:

  1. 平等对待每个待预测词:它对所有未见 N-gram 一视同仁地分配概率,忽略了不同 N-gram 之间的差异;
  2. 可能给未见 N-gram 分配过多概率质量:尤其当词表很大时,K·|V|项会显著稀释已见 N-gram 的概率。

这两点正是引入更精细的 Good-Turing 平滑的动机。

四、Good-Turing 平滑:按频率重新分配概率质量

4.1 核心思想与公式

Good-Turing 平滑比 Additive 平滑精细得多。它根据 N-gram 的具体出现频次决定平滑量:将出现r+1次的 N-gram 所占据的一部分概率空间划分出来,分配给只出现r次的 N-gram(文档定义)。

g(x)为语料中出现恰好x次的 N-gram 个数(即"count-of-counts"),N为语料中 N-gram 的总数,则出现r次的 N-gram 的调整计数为:

r* = (r + 1) · g(r + 1) / g(r) p(w_{i-n+1}^i | c(w_{i-n+1}^i) = r) = r* / N

直观理解:高频 N-gram 的g(r+1)g(r)相近,r* ≈ r,调整很小;而低频 N-gram 的计数被显著下调/上调,被腾出的概率质量正好用于那些从未出现的 N-gram(其总概率等于只出现一次 N-gram 的相对占比)。

4.2 大规模计数下的对数线性插值

g(r)在高频区间会变得极其不可靠(大数定律失效,样本稀疏)。numpy-ml 的GoodTuringNGram采用了 Gale 提出的 Simple Good-Turing 方案:当经验估计不可靠时,用一个对数线性(幂律)模型来平滑 count-of-counts。其核心逻辑在_calc_smoothed_counts(源码)中实现,主要步骤为:

  1. 计算未见 N-gram 的总概率p0p0 = NC(1, N) / Σ counts,即只出现一次 N-gram 的相对占比(源码);
  2. 拟合 count 模型:调用_fit_count_models(源码),对每个 N 阶分别做 Church & Gale (1991) 的 averaging transform,然后用 numpy-ml 自带的LinearRegression拟合log(NC) ~ log(r)的对数线性关系:log NC(r) = b + a·log r
  3. 经验值与插值择优:对每个计数C,同时计算经验平滑计数count_emp = (C+1)·NC(C+1)/NC(C)和对数线性插值count_interp,用置信度阈值t = conf·σ判断两者差异是否显著:若|count_interp - count_emp| > t则采用经验值,否则切换到插值(源码)。

GoodTuringNGram新增的唯一超参数是conf(构造函数):

参数默认值说明
conf1.96经验平滑计数标准差的乘子,决定有多少数据点交由对数线性模型平滑。默认值1.96对应 95% 置信区间

另外注意:GoodTuringNGram重写了train方法(源码),在基类完成计数统计后额外调用_calc_smoothed_counts()预计算所有平滑计数并缓存,因此后续概率查询不会重复拟合。

在概率查询端,_log_ngram_prob(源码)对已见 N-gram 使用平滑计数C*

P(ngram) = (1 - p0) · C* / T

其中T为所有平滑计数之和(归一化常数);对未见 N-gram 则从p0中按未见种类数均分,保证概率分布合法。

4.3 基准文献

docs/numpy_ml.ngram.rst在文末列出了两项权威参考文献,也是 Good-Turing 平滑的理论依据:

  1. Chen & Goodman (1998). "An empirical study of smoothing techniques for language modeling". Harvard CSG Technical Report TR-10-98;
  2. Gale & Sampson (1995). "Good-Turing frequency estimation without tears". Journal of Quantitative Linguistics, 2(3), 217-237。

五、训练、评估与生成:完整 API 使用示例

5.1 训练一个平滑 N-gram 模型

所有模型都通过train(corpus_fp, vocab=None, encoding=None)训练,corpus_fp指向一个换行分隔的文本语料文件(基类文档)。可选参数vocab传入numpy_ml.preprocessing.nlp.Vocabulary实例以限定词表,此时词表外单词在unk=True时映射为<unk>unk=False时被删除;encoding支持'utf-8''utf-8-sig''utf-16'等常见编码。

from numpy_ml.ngram import MLENGram, AdditiveNGram, GoodTuringNGram # 训练一个三元模型(N=3),使用 K=0.5(ELE)的 Additive 平滑 model = AdditiveNGram(N=3, K=0.5, unk=True, filter_stopwords=False, filter_punctuation=False) model.train("corpus.txt", encoding="utf-8-sig") # 训练后模型内部保存了 1-gram、2-gram、3-gram 的计数 print(sorted(model.counts[1].items(), key=lambda x: -x[1])[:5]) print(model.n_words) # 每阶 N-gram 的总数 print(model.n_tokens) # 每阶不重复 N-gram 的种类数

5.2 评估:困惑度与交叉熵

NGramBase内置了两个评估方法(源码):

  • cross_entropy(words, N)H(W) = -log p(W) / n,其中nW中 N-gram 的个数,以自然对数(底数 e)计,与"编码 W 所需平均比特数"成正比;
  • perplexity(words, N)PP(W) = exp(H(W))
test_words = ["the", "cat", "sat", "on", "the", "mat"] pp = model.perplexity(test_words, N=3) ce = model.cross_entropy(test_words, N=3)

文档强调:最小化困惑度等价于最大化测试序列在模型下的概率,它也可以理解为语言模型预测下一个词时的平均分支因子(branching factor)。数值越低,模型对真实文本的拟合越好。

5.3 补全与句子生成

completions(words, N)返回给定前缀下所有候选后继词及其对数概率(源码);generate(N, seed_words, n_sentences)则基于这些分布随机采样生成句子,句子以<eol>结束,<bol>用作句首填充(源码)。

# 查看 "the" 之后最可能的 5 个词 comps = sorted(model.completions(["the"], N=3), key=lambda x: -x[1]) print(comps[:5]) # 用三元模型生成 5 个句子 model.generate(N=3, seed_words=["<bol>"], n_sentences=5)

注意generate内部会对平滑概率做再归一化(np.exp(probs) / np.exp(probs).sum(),源码),确保采样分布合法。

六、正确性验证:与 NLTK 的对照测试

numpy-ml 为平滑模型提供了严谨的数值验证。numpy_ml/tests/test_ngram.py定义了以 NLTK 为基准的黄金实现:

  • MLEGold使用nltk.lm.MLE实现未平滑最大似然模型;
  • AdditiveGold使用nltk.lm.Lidstone(order=n, gamma=K)实现 Additive 平滑。

test_mletest_additive两个测试(测试源码)的做法是:用random_paragraph生成 1000 词随机段落写入临时文件,分别用 numpy-ml 实现与 NLTK 黄金实现训练,然后逐条断言两者 N-gram 计数完全一致,并用np.testing.assert_allclose验证对数概率与 NLTK 结果(换算到自然对数底)误差在浮点精度内。测试随机化地取N ∈ [2, 5)K取随机浮点数,覆盖不同参数组合。这份测试直接证明了AdditiveNGram._log_ngram_prob公式与业界标准实现的一致性,也让_log_ngram_probK * n_tokens的分母归一化设计有了可验证的落点。

七、可视化对比:三种平滑策略的效果差异

仓库的绘图脚本 numpy_ml/plots/ngram_plots.py 提供了两种直观的可视化:

  • plot_gt_freqs(fp)(脚本):以对数-对数坐标绘制词频排名分布(rank-probability 曲线),叠加 MLE、simple Good-Turing、Laplace 三种估计。曲线整体越靠上、越平滑,说明低频词的估计越合理——这正是numpy_ml/ngram/img/rank_probs.png展示的内容;
  • compare_probs(fp, N)(脚本):固定语料,将K从 0 扫到 10,观察 Additive 平滑对已见 N-gram(如("<bol>", "the"))与未见 N-gram(如("<bol>", "asdf"))对数概率的影响,输出numpy_ml/ngram/img/add_smooth.png:随着 K 增大,已见 N-gram 的概率被稀释、未见 N-gram 的概率上升。

结合第一节的架构分析可以总结出选型建议:数据充足、追求最大似然时用MLENGram;需要快速给零概率兜底时用AdditiveNGram(K=1 即 Laplace,K=0.5 即 ELE);对概率质量分配精度要求高、且语料规模足够支撑 count-of-counts 统计时,优先选择GoodTuringNGram

结语

N-gram 平滑是统计语言建模中"小处见真章"的经典问题。numpy-ml 用一个抽象基类NGramBase加三个具体子类,把 Laplace、Additive/Lidstone 与 Good-Turing 三种平滑方案的数学公式收敛为各自_log_ngram_prob中短短几行 NumPy 代码,并通过与 NLTK 的对照测试保证了数值正确性。无论你是想深入理解平滑公式的推导,还是需要一个可读、可调试、可二次开发的纯 NumPy 语言模型实现,都可以从 numpy_ml/ngram/ngram.py 与 numpy_ml/tests/test_ngram.py 入手,配合本文的公式与 API 说明快速上手。

  • 机器学习
  • 人工智能

【免费下载链接】numpy-ml

Machine learning, in numpy

项目地址:https://gitcode.com/gh_mirrors/nu/numpy-ml
点击查看免费下载

相关推荐

上一篇:Sinatra请求验证:确保API输入数据安全
下一篇:DeepSpec完全指南:如何训练与评估高效推测解码算法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询