news 2026/6/1 18:43:58

别再死记硬背N-Gram公式了!用Python从零实现一个能‘打分’的句子生成器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死记硬背N-Gram公式了!用Python从零实现一个能‘打分’的句子生成器

别再死记硬背N-Gram公式了!用Python从零实现一个能‘打分’的句子生成器

自然语言处理(NLP)听起来高深莫测?其实它的核心思想往往简单得令人惊讶。想象一下,当你和朋友聊天时,对方刚说完"今天天气真",你脑海中会本能地预测下一个词可能是"好"而不是"香蕉"——这种预测能力正是N-Gram模型的本质。本文将带你用Python从零构建一个会"创作"还能"打分"的N-Gram引擎,让抽象的语言模型理论变成看得见摸得着的代码实践。

1. 拆解N-Gram:为什么说它是语言预测的"直觉大师"

N-Gram模型的核心在于一个简单却深刻的观察:词语的出现不是随机的,而是受前面词语影响的。就像我们不会说"喝汽车"而会说"开汽车",语言中存在隐形的搭配规则。这种规则不需要理解语义,纯粹基于统计规律:

  • Unigram(1元):只考虑单词本身频率(如"的"出现概率高)
  • Bigram(2元):考虑前一个词的影响("苹果"后出现"手机"的概率>"香蕉")
  • Trigram(3元):扩展到前两个词的上下文("人工"+"智能"后很可能接"技术")
# 直观理解Bigram概率计算 corpus = "我喜欢苹果 苹果很甜".split() # 分词后的语料 bigrams = [("我","喜欢"), ("喜欢","苹果"), ("苹果","苹果"), ("苹果","很"), ("很","甜")] # 计算P(苹果|喜欢) = "喜欢 苹果"出现次数 / "喜欢"出现次数 p_apple_given_like = bigrams.count(("喜欢","苹果")) / corpus.count("喜欢") print(f"P(苹果|喜欢) = {p_apple_given_like}") # 输出1.0

这个例子揭示了一个关键现象:N值越大,模型对上下文的捕捉越精细,但数据稀疏问题也越严重。比如Trigram需要更多训练数据才能获得可靠统计。

2. 工程蓝图:构建会"创作"的N-Gram引擎

让我们设计一个具有完整生命周期的N-Gram系统:

  1. 语料预处理层

    • 文本清洗(去除标点、统一大小写)
    • 分词处理(英文按空格,中文需分词器)
    • 添加起始/结束标记(<s></s>
  2. 模型训练层

    • 统计词频(建立词库)
    • 计算N-Gram条件概率
    • 平滑处理(应对未见词组合)
  3. 应用功能层

    • 句子生成(基于概率采样)
    • 通顺度打分(计算句子概率)
    • 交互式测试(输入前缀补全句子)
class NGramGenerator: def __init__(self, n=2): self.n = n self.ngrams = defaultdict(Counter) self.start_token = "<s>" self.end_token = "</s>" def train(self, corpus): for sentence in corpus: tokens = [self.start_token] + sentence.split() + [self.end_token] for i in range(len(tokens)-self.n+1): context = tuple(tokens[i:i+self.n-1]) next_word = tokens[i+self.n-1] self.ngrams[context][next_word] += 1

关键细节:defaultdict(Counter)这种嵌套数据结构能高效存储层级化的N-Gram统计量。例如ngrams[("我","喜欢")]["苹果"]=3表示"我喜欢苹果"出现3次。

3. 概率的艺术:从统计表到句子生成

单纯的统计表只是冰冷的数字,如何让它"活"起来?核心在于加权随机采样——让高频组合有更高选中概率,同时保留一定的随机性:

def generate_sentence(self, max_len=20): current = (self.start_token,) result = [] for _ in range(max_len): next_word = random.choices( list(self.ngrams[current].keys()), weights=list(self.ngrams[current].values()) )[0] if next_word == self.end_token: break result.append(next_word) current = tuple(list(current[1:]) + [next_word]) if self.n > 1 else () return " ".join(result)

实际运行示例(训练《红楼梦》语料后):

生成结果1: "老太太笑道 这个丫头" 生成结果2: "宝玉听了 不觉滴下泪来" 生成结果3: "凤姐儿忙问道 你瞧瞧这个"

为什么需要平滑技术?当遇到未见过的新词组合时,直接概率为零会导致模型失效。加一平滑(Laplace)是最简单的解决方案:

def get_probability(self, context, word): total = sum(self.ngrams[context].values()) + len(self.vocab) # 加词汇表大小 count = self.ngrams[context].get(word, 0) + 1 # 加一平滑 return count / total

4. 给句子"打分":量化语言流畅度的秘密

判断"今天天气真好"比"天气好今天真"更通顺,N-Gram通过计算句子概率实现这点。采用对数概率避免数值下溢:

def score_sentence(self, sentence): tokens = [self.start_token] + sentence.split() + [self.end_token] log_prob = 0.0 for i in range(len(tokens)-self.n+1): context = tuple(tokens[i:i+self.n-1]) next_word = tokens[i+self.n-1] prob = self.get_probability(context, next_word) log_prob += math.log(prob) if prob > 0 else -float('inf') return log_prob

测试对比(数值越小越好):

"人工智能改变世界"得分: -12.34 "世界改变工智能力人"得分: -48.72

5. 实战优化:让玩具模型变身实用工具

基础版存在三个明显缺陷:

  1. 内存效率低:全量存储N-Gram表
  2. 生成质量不稳定:可能陷入重复循环
  3. 长文本失效:概率连乘导致数值爆炸

解决方案:

  • 采用Trie树压缩存储
  • 引入温度参数控制随机性
  • 使用束搜索(Beam Search)优化生成
def beam_search_generate(self, beam_width=3, max_len=15): beams = [([self.start_token], 0.0)] # (tokens, score) for _ in range(max_len): new_beams = [] for tokens, score in beams: context = tuple(tokens[-(self.n-1):]) if self.n > 1 else () for next_word, prob in self.ngrams[context].items(): new_score = score + math.log(prob) new_beams.append((tokens + [next_word], new_score)) # 保留top-k beams = sorted(new_beams, key=lambda x: x[1], reverse=True)[:beam_width] return " ".join(beams[0][0][1:-1]) # 去除起止标记

优化后的生成示例:

原始随机生成: "的 的 的 的 的" # 陷入重复 束搜索生成: "昨夜西风凋碧树 独上高楼" # 保持连贯性

6. 超越基础:现代NLP中的N-Gram变体

虽然深度学习崛起,但N-Gram思想仍在进化:

  • 缓存模型(Cache LM):混合近期词语的局部统计
  • 类别N-Gram:先对词语聚类(如"北京→[城市]")
  • 神经N-Gram:用神经网络预测条件概率
# 混合模型示例(结合Bigram和缓存) class CachedNGram: def __init__(self, base_ngram, cache_weight=0.3): self.base = base_ngram self.cache = [] self.cache_weight = cache_weight def update_cache(self, word): self.cache.append(word) if len(self.cache) > 10: # 缓存窗口 self.cache.pop(0) def get_probability(self, context, word): base_prob = self.base.get_probability(context, word) cache_prob = self.cache.count(word) / len(self.cache) if self.cache else 0 return (1-self.cache_weight)*base_prob + self.cache_weight*cache_prob

这种混合模型能捕捉"最近常提某话题"的语言现象,比如聊天机器人会更倾向于重复用户刚用过的词汇。

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

UABEA:为什么每个Unity开发者都需要这个跨平台资源编辑器?

UABEA&#xff1a;为什么每个Unity开发者都需要这个跨平台资源编辑器&#xff1f; 【免费下载链接】UABEA c# uabe for newer versions of unity 项目地址: https://gitcode.com/gh_mirrors/ua/UABEA 你是否曾经遇到过这样的情况&#xff1a;想要修改游戏中的一张纹理&a…

作者头像 李华
网站建设 2026/6/1 18:26:58

终极指南:如何在不同音乐平台间无缝迁移歌单

终极指南&#xff1a;如何在不同音乐平台间无缝迁移歌单 【免费下载链接】lx-music-desktop 一个基于 Electron 的音乐软件 项目地址: https://gitcode.com/GitHub_Trending/lx/lx-music-desktop 你是否曾经因为更换音乐平台而不得不放弃多年精心收藏的歌单&#xff1f;…

作者头像 李华
网站建设 2026/6/1 18:25:56

Redis 哨兵模式底层原理与自动故障转移全流程

文章目录前言一、 哨兵集群的核心架构与三大常态监控二、 主观下线&#xff08;SDOWN&#xff09;与客观下线&#xff08;ODOWN&#xff09;1. 主观下线&#xff08;Subjectively Down, SDOWN&#xff09;2. 客观下线&#xff08;Objectively Down, ODOWN&#xff09;三、 领头…

作者头像 李华