- 机器学习
- 人工智能
【免费下载链接】numpy-ml
Machine learning, in numpy
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字典管理:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
N | int | 必填 | 语言模型的最大上下文窗口长度(词数),训练时会计算 1, 2, ..., N 阶所有 N-gram |
unk | bool | True | 是否在语言模型中保留<unk>(未知词)标记 |
filter_stopwords | bool | True | 训练前是否过滤停用词 |
filter_punctuation | bool | True | 训练前是否过滤标点 |
NGramBase还提供了一批所有平滑模型共享的核心方法(源码定义):
train(corpus_fp, vocab=None, encoding=None):统计语料中 N, N-1, ..., 1-gram 的计数,结果存于self.counts、self.n_words、self.n_tokens;completions(words, N):返回给定前缀下所有可能后继词及其对数概率;generate(N, seed_words, n_sentences):用模型采样生成句子(遇到<eol>结束一句);perplexity(words, N)与cross_entropy(words, N):评估模型在测试序列上的表现。
三个具体模型MLENGram、AdditiveNGram、GoodTuringNGram均继承自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:
| 参数 | 默认值 | 说明 |
|---|---|---|
K | 1 | 加到每个观测上的伪计数(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 平滑的两个问题:
- 平等对待每个待预测词:它对所有未见 N-gram 一视同仁地分配概率,忽略了不同 N-gram 之间的差异;
- 可能给未见 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(源码)中实现,主要步骤为:
- 计算未见 N-gram 的总概率
p0:p0 = NC(1, N) / Σ counts,即只出现一次 N-gram 的相对占比(源码); - 拟合 count 模型:调用
_fit_count_models(源码),对每个 N 阶分别做 Church & Gale (1991) 的 averaging transform,然后用 numpy-ml 自带的LinearRegression拟合log(NC) ~ log(r)的对数线性关系:log NC(r) = b + a·log r; - 经验值与插值择优:对每个计数
C,同时计算经验平滑计数count_emp = (C+1)·NC(C+1)/NC(C)和对数线性插值count_interp,用置信度阈值t = conf·σ判断两者差异是否显著:若|count_interp - count_emp| > t则采用经验值,否则切换到插值(源码)。
GoodTuringNGram新增的唯一超参数是conf(构造函数):
| 参数 | 默认值 | 说明 |
|---|---|---|
conf | 1.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 平滑的理论依据:
- Chen & Goodman (1998). "An empirical study of smoothing techniques for language modeling". Harvard CSG Technical Report TR-10-98;
- 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,其中n是W中 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_mle与test_additive两个测试(测试源码)的做法是:用random_paragraph生成 1000 词随机段落写入临时文件,分别用 numpy-ml 实现与 NLTK 黄金实现训练,然后逐条断言两者 N-gram 计数完全一致,并用np.testing.assert_allclose验证对数概率与 NLTK 结果(换算到自然对数底)误差在浮点精度内。测试随机化地取N ∈ [2, 5)、K取随机浮点数,覆盖不同参数组合。这份测试直接证明了AdditiveNGram._log_ngram_prob公式与业界标准实现的一致性,也让_log_ngram_prob中K * 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
相关推荐
numpy-ml 中的 MLENGram:无平滑 N-gram 语言模型原理、源码与实战指南
numpy ml 中的 MLENGram:无平滑 N gram 语言模型原理、源码与实战指南 本文基于 numpy ml 仓库的 ngram 模块文档( doc
机器学习人工智能3步终极指南:AdGuard浏览器扩展如何彻底改变你的上网体验
3步终极指南:AdGuard浏览器扩展如何彻底改变你的上网体验 AdGuard浏览器扩展是一款完全免费且开源的广告拦截工具,它不仅能屏蔽烦人的广告,更能全方位保
前端网络安全基于 NumPy 实现隐马尔可夫模型:numpy-ml MultinomialHMM 原理、推理与 Baum-Welch 训练实战指南
基于 NumPy 实现隐马尔可夫模型:numpy ml MultinomialHMM 原理、推理与 Baum Welch 训练实战指南 隐马尔可夫模型(Hidd
机器学习人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考