别再死记硬背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

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

更多推荐