资讯详情

从数数到预测:用Python手写Bigram语言模型入门

📅 2026/11/2 19:56:02 | 华诺云谱 👁 阅读
从数数到预测:用Python手写Bigram语言模型入门
1. 从“数数”开始理解语言模型很多人第一次听到“语言模型”这四个字脑子里浮现的都是参数量、显卡、训练成本这些大词觉得离自己很远。其实语言模型最朴素的内核用一句话就能说清楚它就是在数数数一个词后面跟着另一个词出现了多少次然后拿这个次数去猜下一个字。这篇文章面向的是刚接触自然语言处理、想搞明白“模型到底在干什么”的读者不管你是写 Python 的、做数据分析的还是纯粹好奇大模型原理的都能顺着这条线走一遍。我会用 Python 从零手写一个 Bigram 模型把“数数”这件事做到能跑、能出结果、能看出问题再一步步引出 N-gram 和马尔可夫假设让你真正理解为什么今天的语言模型是这个样子。先明确一个概念边界。语言模型Language Model的任务不是理解语义而是给一个词序列打概率分或者说预测下一个词。你手机输入法里打字时跳出来的候选词背后就是一个语言模型在工作。它不关心你这句话有没有深意它只关心“在已经出现这几个词的情况下下一个词最可能是哪个”。这个定位非常重要因为后面所有的技术演进从 N-gram 到神经网络再到今天的大模型本质上都在做同一件事只是“数数”的方式越来越聪明。那为什么从“数数”讲起因为这是最原始、最直观、也最容易亲手实现的方式。你不需要任何深度学习框架不需要显卡一台普通笔记本装个 Python 就能跑。而且当你亲手数完词频、算出概率、生成出一段似是而非的句子时你会对语言模型的本质有一个肌肉记忆级别的理解这比看十篇综述都管用。上篇我们就聚焦在这个最朴素的起点上把地基打牢。1.1 语言模型到底在预测什么把话说得更具体一点。假设我们有一句话“今天天气真”语言模型要回答的问题是下一个字是“好”的概率是多少是“差”的概率是多少是“热”的概率是多少它输出的是一组概率分布然后我们通常取概率最高的那个作为预测结果。数学上对于一个词序列 $w_1, w_2, ..., w_n$语言模型要计算的是整个序列的联合概率 $P(w_1, w_2, ..., w_n)$。这个联合概率直接算是不现实的因为可能的序列组合是天文数字。于是就有了链式法则把它拆成条件概率的乘积$P(w_1, w_2, ..., w_n) P(w_1) \cdot P(w_2|w_1) \cdot P(w_3|w_1,w_2) \cdots$。拆完之后问题变成给定前面所有词预测当前词的概率。但“前面所有词”这个条件还是太长了长到没法统计。这就引出了下一个关键角色——马尔可夫假设。1.2 为什么需要马尔可夫假设马尔可夫假设的核心思想特别务实别管那么远的历史了只看最近几个词就够了。具体来说它假设当前词的出现只依赖于前面有限个词而不是全部历史。如果只看前面 1 个词就叫一阶马尔可夫假设对应的模型叫 Bigram二元模型如果看前面 2 个词叫二阶对应 Trigram三元模型看前面 N-1 个词就是 N-gram 模型。这个假设当然是不完美的因为语言里长距离依赖很常见比如“虽然……但是……”这种结构中间隔了很远也有关系。但它的好处是让问题变得可计算。你不需要统计无限长的历史只需要统计有限窗口内的共现次数。这就是为什么 N-gram 能在早期自然语言处理里大行其道也是为什么理解它是理解后续所有模型的基础。你可以把马尔可夫假设理解成一种“近视眼策略”看不清远方但把眼前几步看准了整体也能凑合能用。2. 用 Python 手写一个 Bigram 模型理论说再多不如动手跑一遍。这一章我们从头搭一个 Bigram 语言模型语料就用几句简单的中文目的是把流程走通把每一步在干什么讲清楚。你跟着敲一遍就能得到一个能预测下一个字的模型虽然简陋但五脏俱全。2.1 语料准备与预处理先准备一小段语料。真实项目里语料可能是几 GB 的文本但学习阶段用几句话就够了关键是理解流程。我准备了三句话corpus [ 今天天气真好, 今天天气不错, 明天天气真好, ]预处理这一步在实际项目里非常关键包括分词、去标点、转小写、处理特殊符号等。中文需要分词这里为了简单我们按字来处理也就是把每个汉字当成一个 token。英文的话通常按空格切词。预处理做得好不好直接决定模型质量这是很多人容易忽略的地方。比如标点符号如果不处理模型可能会学到“句号后面跟句号”这种没用的模式。# 按字切分并在句首句尾加上特殊标记 tokens [] for sentence in corpus: chars list(sentence) # s 表示句子开始/s 表示句子结束 tokens.append([s] chars [/s])加上s和/s是为了让模型知道句子从哪里开始、到哪里结束这样生成的时候才知道什么时候该停。这个细节很多教程不讲但实际用起来很重要否则模型生成会停不下来。2.2 统计词频Bigram 的核心就是数数接下来就是核心的“数数”环节。我们要统计每个词后面跟着另一个词出现了多少次。用一个嵌套字典来存外层 key 是前一个词内层 key 是后一个词value 是次数。from collections import defaultdict bigram_count defaultdict(lambda: defaultdict(int)) unigram_count defaultdict(int) for sentence in tokens: for i in range(len(sentence) - 1): prev_word sentence[i] curr_word sentence[i 1] bigram_count[prev_word][curr_word] 1 unigram_count[prev_word] 1跑完这段bigram_count[今天][天]就是“今天”后面跟“天”的次数。你可以打印出来看看会发现“今天”后面跟“天”出现了 2 次跟“明”没出现过。这就是模型学到的全部知识——一张共现次数表。2.3 从次数到概率归一化光有次数还不能直接用来预测因为不同词的总次数不一样没法比较。需要归一化成概率。对于 Bigram条件概率的计算公式是$$P(w_i | w_{i-1}) \frac{Count(w_{i-1}, w_i)}{Count(w_{i-1})}$$用代码实现就是拿 bigram 的次数除以对应前一个词的总次数bigram_prob defaultdict(dict) for prev_word, next_words in bigram_count.items(): total unigram_count[prev_word] for curr_word, count in next_words.items(): bigram_prob[prev_word][curr_word] count / total现在bigram_prob[今天]就是一个概率分布里面每个词的概率加起来等于 1。你可以验证一下bigram_prob[今天][天]应该是 1.0因为“今天”后面只跟过“天”。这个归一化过程看着简单但它是所有概率语言模型的标配操作理解它很重要。2.4 生成第一个句子有了概率表就可以生成句子了。思路是从s开始每次根据当前词的概率分布随机采样下一个词直到采样到/s或者达到最大长度。import random def generate_sentence(start_words, max_len20): sentence [] current start_word for _ in range(max_len): if current not in bigram_prob: break next_words list(bigram_prob[current].keys()) probs list(bigram_prob[current].values()) current random.choices(next_words, weightsprobs, k1)[0] if current /s: break sentence.append(current) return .join(sentence) print(generate_sentence())跑几次你会发现生成的句子基本就是语料里出现过的组合比如“今天天气真好”或者“明天天气不错”。这是因为语料太小模型没有泛化能力。但这恰恰说明了 Bigram 的本质它只能复现见过的模式没见过的基本不会生成。这就是“数数”方法的根本局限也是后面所有更复杂模型要解决的问题。3. N-gram 家族与马尔可夫假设的取舍Bigram 只是 N-gram 家族里最简单的一员。把窗口从 1 扩大到 2、3就得到 Trigram、4-gram。窗口越大模型能捕捉的上下文越长理论上预测越准但代价是数据稀疏问题急剧恶化。这一章我们把 N-gram 家族讲透把马尔可夫假设的取舍讲明白。3.1 从 Bigram 到 Trigram窗口变大的代价Trigram 的条件概率是 $P(w_i | w_{i-2}, w_{i-1})$也就是看前面两个词。代码上只需要把统计的 key 从单个词改成词对trigram_count defaultdict(lambda: defaultdict(int)) bigram_total defaultdict(int) for sentence in tokens: for i in range(len(sentence) - 2): key (sentence[i], sentence[i 1]) curr_word sentence[i 2] trigram_count[key][curr_word] 1 bigram_total[key] 1看起来只是多了一个维度但问题来了语料里“今天天气”后面跟“真”出现了 2 次但“明天天气”后面跟“真”只出现 1 次而“今天天气”后面跟“不”出现 0 次。当某个词对在语料里从没出现过时它的概率就是 0模型就完全无法预测了。这就是数据稀疏问题。窗口越大可能的词对组合越多而语料覆盖的组合比例越低零概率的情况就越普遍。有研究统计过在中等规模语料上Trigram 的零概率情况能占到相当高的比例。所以 N-gram 不是越大越好通常 Trigram 或 4-gram 就是实际应用的上限再大就全是零了。3.2 平滑技术给零概率留条活路解决零概率的标准做法是平滑Smoothing。最简单的思路是给每个可能的组合都加一个很小的数保证没有零概率。这叫加一平滑Add-one Smoothing也叫拉普拉斯平滑。$$P(w_i | w_{i-1}) \frac{Count(w_{i-1}, w_i) 1}{Count(w_{i-1}) V}$$其中 V 是词表大小。加一平滑的问题是它给所有未出现的组合都分配了相同的概率这不太合理因为有些词天生就更可能出现在某些词后面。于是有了更精细的方法比如 Good-Turing 平滑、Kneser-Ney 平滑。Kneser-Ney 是 N-gram 时代公认效果最好的平滑方法它的核心思想是一个词作为“接续词”的多样性比它本身的频率更重要。这个思想后来也影响了很多神经网络模型的设计。3.3 马尔可夫假设的边界在哪里马尔可夫假设说“只看最近几个词”这个假设在什么情况下成立什么情况下会出问题短句、固定搭配、常见表达基本没问题。比如“今天天气”后面跟“真”或“不”看前两个词就够了。但长距离依赖就麻烦了比如“我昨天买的那本书虽然封面有点旧但是内容特别好”这里“书”和“内容”隔了很远但语义上强相关。Bigram 或 Trigram 完全捕捉不到这种关系。这就是 N-gram 的天花板。它用局部统计换来了可计算性但牺牲了长距离建模能力。后来的神经网络语言模型包括 RNN、Transformer本质上都是在解决这个问题怎么在不爆炸计算量的前提下让模型看到更长的上下文。理解了 N-gram 的边界你就理解了为什么会有后面的技术演进。4. 实操中的坑与排查技巧这一章是我自己在写 Bigram 和 N-gram 时踩过的坑以及一些排查问题的思路。这些东西教科书上不一定写但实际动手时一定会遇到。4.1 常见问题速查表问题现象可能原因排查方法解决思路生成句子重复同一段语料太小或概率过于集中打印概率分布看是否只有一个高概率词扩大语料或引入随机性生成句子停不下来没有/s标记或概率为 0检查预处理是否加了结束标记补上结束标记设置最大长度概率全是 0词对从未在语料中出现打印 bigram_count 看是否为空使用平滑技术中文分词错误按字切分导致语义断裂检查分词结果换用 jieba 等分词工具内存爆掉语料太大字典存不下监控内存占用使用 Counter 或数据库存储4.2 几个容易忽略的细节第一个细节是句首句尾标记。很多人写 Bigram 时忘了加s和/s结果模型不知道句子怎么开始、怎么结束生成时要么从随机词开始要么停不下来。加上这两个标记后模型才能学到“句子开头通常是什么词”“句子结尾通常是什么词”。第二个细节是概率归一化的分母。Bigram 的分母是前一个词的总出现次数不是所有词的总次数。这个很容易搞混。如果你用所有词的总次数做分母概率加起来就不等于 1 了模型就错了。写代码时一定要验证一下对任意一个前词它后面所有词的概率之和应该等于 1。第三个细节是随机采样的温度。上面生成句子时用的是random.choices按概率采样。如果你总是取概率最高的词贪心策略生成的句子会很单调容易陷入循环。适当引入随机性生成的句子更多样。这个思路后来在神经网络语言模型里演变成了“温度参数”本质是一样的。4.3 从 Bigram 到实际项目的距离你可能会问学了这个 Bigram 能干什么直接拿去做产品肯定不行但它是一个极好的教学工具和基线模型。在实际项目里你可以用 Bigram 做快速原型验证比如验证数据预处理流程是否通畅、验证评估指标是否合理。它跑得快、代码简单适合用来做 sanity check。如果 Bigram 都跑不出合理结果那大概率是数据或流程有问题而不是模型问题。另外Bigram 的思想在很多地方还在用。比如拼写纠错、输入法候选词、简单的文本分类特征都能看到 N-gram 的影子。它没有过时只是不再是主角了。理解它是为了更好地理解那些更复杂的主角。5. 从 N-gram 到大模型一条清晰的演进线把 Bigram 和 N-gram 搞明白之后再看今天的大语言模型你会发现它们之间有一条清晰的演进线。这条线的主轴就是怎么在有限算力下更聪明地利用更长的上下文。5.1 神经网络语言模型解决了什么N-gram 的两个死穴是数据稀疏和长距离依赖。神经网络语言模型用两个手段解决第一把词映射成稠密向量词嵌入这样即使两个词从没在语料里共现过只要向量相近模型也能泛化第二用循环神经网络或注意力机制来处理变长上下文不再受固定窗口限制。词嵌入这个想法特别关键它把离散的符号变成了连续空间里的点让“相似词”在数学上有了意义。5.2 大模型时代“数数”还在吗有人觉得大模型和 N-gram 是完全不同的东西其实底层逻辑没变还是在预测下一个词只是预测的方式从“查表数数”变成了“神经网络计算”。大模型的参数量可以理解为一种极度压缩的、泛化能力极强的“计数表”。它见过的模式比任何 N-gram 都多而且能通过向量运算泛化到没见过的组合。但它的训练目标和 Bigram 是一样的最大化下一个词的条件概率。所以你在学习大模型时如果觉得概念太抽象不妨回到 Bigram 想一想它就是在做同样的事只是用了更复杂的函数来拟合那个条件概率分布。这个视角能帮你把很多看似高深的概念拉回到地面上。5.3 学习路径建议如果你是从这篇文章入门的我建议的路径是先把 Bigram 手写一遍理解数数和归一化然后扩展到 Trigram理解数据稀疏和平滑接着学词嵌入和简单的神经网络语言模型理解泛化最后再看 Transformer 和大模型。每一步都动手写代码哪怕只是几十行。语言模型这个领域看懂了和写出来了是两回事写出来了和调通了又是两回事。我自己在学的时候光 Bigram 就写了好几遍每次都有新理解。最后分享一个我自己的习惯每学一个新模型我都会拿同一小段语料去跑对比它和 Bigram 的生成结果。这样能直观感受到模型能力的提升也能看清每个技术到底解决了什么问题。这个对比习惯比单纯看论文管用得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑