N-Gram 语言模型实战从 Bigram 到 4-Gram 的 Python 实现与平滑策略对比语言模型是自然语言处理中的基础组件它能够预测给定上下文条件下下一个词出现的概率。N-Gram 模型作为经典的语言模型之一因其简单高效的特点在实际应用中仍然发挥着重要作用。本文将带您从零开始实现一个支持 1-4 Gram 的 Python 语言模型类并深入分析三种常见平滑策略的效果差异。1. N-Gram 模型核心原理N-Gram 模型基于马尔可夫假设认为当前词的概率仅依赖于前 N-1 个词。这种简化使得模型计算变得可行同时也保留了语言中重要的局部依赖关系。1.1 概率计算公式对于 N-Gram 模型句子概率可分解为P(w₁,w₂,...,wₙ) ≈ ∏ P(wᵢ|wᵢ₋ₙ₊₁,...,wᵢ₋₁)不同阶数的 N-Gram 模型具有不同特性模型类型依赖长度计算复杂度数据稀疏性Unigram0O(V)低Bigram1O(V²)中Trigram2O(V³)高4-Gram3O(V⁴)极高1.2 数据稀疏问题随着 N 的增大模型会遇到严重的零概率问题。例如在英文中# 假设语料库中从未出现过 the quick brown fox jumps p_jumps model.prob(jumps, [the, quick, brown, fox]) # 返回0这种现象会导致任何包含未见过 N-Gram 的句子概率为0模型无法区分合理但罕见的句子与完全不合理的句子2. Python 实现完整 N-Gram 类下面我们实现一个支持 1-4 Gram 的灵活语言模型类from collections import defaultdict import math import json class NGramModel: def __init__(self, n2, smoothingnone): self.n n self.smoothing smoothing self.counts defaultdict(lambda: defaultdict(int)) self.vocab set() self.total_words 0 def train(self, corpus): 训练模型统计N-Gram出现次数 for sentence in corpus: tokens [s]*(self.n-1) sentence [/s] self.total_words len(sentence) for i in range(len(tokens)-self.n1): context tuple(tokens[i:iself.n-1]) word tokens[iself.n-1] self.counts[context][word] 1 self.vocab.add(word) def prob(self, word, context): 计算条件概率P(word|context) context tuple(context[-(self.n-1):]) # 确保上下文长度正确 context_count sum(self.counts[context].values()) # 应用平滑策略 if self.smoothing add_one: return (self.counts[context][word] 1) / (context_count len(self.vocab)) elif self.smoothing good_turing: # 简化版Good-Turing平滑 N1 sum(1 for v in self.counts[context].values() if v 1) discount (N1 / len(self.vocab)) if context_count else 1.0 return (self.counts[context][word] * discount N1/len(self.vocab)) / (context_count N1) else: # 无平滑 return self.counts[context][word] / context_count if context_count else 0 def perplexity(self, test_corpus): 计算测试集的困惑度 log_prob 0 total_words 0 for sentence in test_corpus: tokens [s]*(self.n-1) sentence [/s] total_words len(sentence) 1 # 包括结束符 for i in range(len(tokens)-self.n1): context tokens[i:iself.n-1] word tokens[iself.n-1] p self.prob(word, context) log_prob math.log(p) if p 0 else float(-inf) return math.exp(-log_prob / total_words) if total_words 0 else float(inf)提示实际使用时建议添加更多错误检查和优化如处理未知词、缓存计算结果等。3. 平滑技术对比分析3.1 加一平滑 (Add-One Smoothing)最基础的平滑方法为所有可能的 N-Gram 计数加1Pₐₑ(w|h) (count(h,w)1) / (count(h)V)特点实现简单计算高效对小概率事件过度平滑当V很大时概率质量被过度分配给未见事件3.2 Good-Turing 平滑基于频率的频率来估计未见事件的概率Pₐₑ(w|h) (count(h,w)* P₀) / (count(h)N₁)其中count* 是经过折扣的计数N₁ 是出现一次的 N-Gram 数量P₀ N₁ / V优势对罕见事件处理更合理自动适应数据分布3.3 Kneser-Ney 平滑 (简化版)考虑词作为 continuation 的概率Pₖₙ(w|h) max(count(h,w)-d,0)/count(h) λ(h)Pₖₙ(w)其中d 是折扣系数 (通常0.75)λ(h) 是归一化因子Pₖₙ(w) 是 w 作为 continuation 的概率4. 实战评估中文语料对比实验我们使用中文小说片段构建小型测试语料# 示例语料 corpus [ [今天, 天气, 真好], [今天, 我们, 去, 公园], [明天, 可能, 下雨], [明天, 我们, 在家] ] # 训练不同平滑策略的Bigram模型 models { no_smooth: NGramModel(n2, smoothingnone), add_one: NGramModel(n2, smoothingadd_one), good_turing: NGramModel(n2, smoothinggood_turing) } for name, model in models.items(): model.train(corpus) # 测试句子 test_sentence [明天, 天气, 如何]不同平滑策略下的概率对比上下文预测词无平滑加一平滑Good-Turing明天天气0.00.1110.083天气如何0.00.1110.083明天我们0.50.3330.3755. 高阶 N-Gram 实践技巧5.1 混合模型策略结合不同阶数的 N-Gram 可以平衡稀疏性和上下文信息def interpolated_prob(self, word, context): 线性插值混合不同阶数模型 weights [0.1, 0.3, 0.6] # 自定义权重 prob 0 for i in range(min(len(weights), self.n)): partial_context context[-(i):] prob weights[i] * self._get_ngram_prob(word, partial_context) return prob5.2 内存优化技巧对于大规模语料可以使用以下优化使用 trie 树存储 N-Gram概率的对数空间计算剪枝低频 N-Gramdef prune_model(self, min_count2): 剪枝低频N-Gram for context in list(self.counts.keys()): self.counts[context] { k:v for k,v in self.counts[context].items() if v min_count } if not self.counts[context]: del self.counts[context]在实际项目中N-Gram 模型常与其他技术结合使用。例如在搜索引擎中Bigram 模型可以改善查询建议的质量在语音识别中Trigram 模型能有效减少识别错误率。