别再死记硬背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系统:
-
语料预处理层
- 文本清洗(去除标点、统一大小写)
- 分词处理(英文按空格,中文需分词器)
- 添加起始/结束标记(
<s>和</s>)
-
模型训练层
- 统计词频(建立词库)
- 计算N-Gram条件概率
- 平滑处理(应对未见词组合)
-
应用功能层
- 句子生成(基于概率采样)
- 通顺度打分(计算句子概率)
- 交互式测试(输入前缀补全句子)
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. 实战优化:让玩具模型变身实用工具
基础版存在三个明显缺陷:
- 内存效率低 :全量存储N-Gram表
- 生成质量不稳定 :可能陷入重复循环
- 长文本失效 :概率连乘导致数值爆炸
解决方案:
- 采用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
这种混合模型能捕捉"最近常提某话题"的语言现象,比如聊天机器人会更倾向于重复用户刚用过的词汇。
更多推荐
所有评论(0)