1. 先搞清楚 Bigram 模型到底解决什么问题如果你刚开始接触语言模型可能会被 GPT、LLM 这些大词吓到。但 Karpathy 讲的 Bigram 模型其实是一个最基础的起点它只根据前一个词预测下一个词。比如“今天天气”后面最可能接“不错”而不是“吃饭”。这种模型虽然简单但能帮你理解语言模型最核心的统计思想——概率分布。我一般会建议新手先从这个模型入手因为它的代码量小依赖少能在普通笔记本上跑起来。你不用纠结 GPU、显存或者复杂的分布式训练就能看到语言生成的基本过程。而且Bigram 模型是理解更复杂模型如 GPT的必经之路如果你连“基于上一个词预测”都搞不清楚直接跳进注意力机制、Transformer 架构很容易迷失在细节里。这个模型最适合两类人一是刚入门 NLP 的学生或开发者想亲手实现一个能生成文本的模型二是已经有经验但想重新梳理语言模型基础的人。如果你属于这两类花 10 分钟跟着跑一遍比看十篇概念文章更有用。2. 环境准备只要 Python 和几个基础库就行Bigram 模型不需要高大上的环境。你甚至不用装 PyTorch 或 TensorFlow——Karpathy 的示例通常只用 NumPy 和标准库。但为了完整复现我建议先准备这些Python 3.6别用太老的版本避免语法兼容问题。NumPy处理矩阵和概率计算。可选Matplotlib如果你想看损失曲线或概率分布。文本文件准备一小段英文或中文文本作为训练数据。比如莎士比亚的几行诗或者自己写的一段话。安装命令就一行pip install numpy matplotlib如果你的环境已经有这些可以直接跳过。我一般会先创建一个干净目录把训练文本和代码放一起避免路径问题bigram_demo/ ├── train.txt # 训练文本 └── bigram.py # 模型代码3. 数据预处理从原始文本到模型能读懂的数字语言模型不能直接处理文字所以第一步是把文本转成数字。具体流程如下3.1 读入文本并构建词表假设你的train.txt内容是hello world hello ai你要先收集所有出现的字符包括空格和换行然后给每个字符分配一个数字 ID。比如chars sorted(list(set(text))) # 去重后排序 vocab_size len(chars) stoi {ch: i for i, ch in enumerate(chars)} # 字符到数字 itos {i: ch for i, ch in enumerate(chars)} # 数字到字符这样hello world 可能变成[12, 3, 5, 5, 8, 0, 9, 8, 10, 5, 4]具体数字取决于你的词表。3.2 构建训练对 (X, Y)Bigram 的核心是用前一个字符预测后一个字符。所以要把文本拆成 (input, target) 对。例如输入 h目标 e输入 e目标 l输入 l目标 l输入 l目标 o注意这里的目标永远是输入的下一个字符。代码实现时X 是文本中除最后一个字符外的所有字符Y 是除第一个字符外的所有字符。3.3 注意文本长度和编码问题如果文本太长内存可能不够。新手建议先用 1000 字符以内的文本测试。如果是中文需要按字分词而不是按词因为 Bigram 模型粒度太粗处理词级任务效果不好。4. 模型核心概率计算和损失函数Bigram 模型本质上是一个概率分布表。行表示当前字符列表示下一个字符表格值表示“给定当前字符下一个字符是某字符的概率”。具体实现分三步4.1 统计共现频率遍历整个文本数一数每个字符后面跟着其他字符的次数。比如 a 后面出现了 10 次 b5 次 c那么 P(b|a) 10/15, P(c|a) 5/15。用代码实现就是import numpy as np counts np.zeros((vocab_size, vocab_size), dtypeint) for char1, char2 in zip(X, Y): idx1, idx2 stoi[char1], stoi[char2] counts[idx1, idx2] 14.2 频率转概率直接用频率会有零概率问题比如训练数据里没出现过的组合。所以通常加一个平滑项Laplace smoothingprobs (counts 1) / (counts.sum(axis1, keepdimsTrue) vocab_size)这样即使某个组合没出现过概率也不会是零。4.3 计算损失函数损失函数衡量模型预测的分布与真实分布的差距。Bigram 常用负对数似然negative log likelihoodloss 0 for char1, char2 in zip(X, Y): idx1, idx2 stoi[char1], stoi[char2] p probs[idx1, idx2] loss -np.log(p) loss / len(X) # 平均损失损失越小说明模型预测越准。5. 文本生成如何从模型里采样出新句子训练完概率表后就可以生成文本了。采样过程如下选一个起始字符比如随机选或固定为空格。根据当前字符对应的概率分布随机选择下一个字符。比如当前字符是 a就按 P(*|a) 的概率选 b 或 c。把新字符作为当前字符重复步骤 2直到生成长度满足要求。代码示例def generate(seed_char, length100): current stoi[seed_char] output [seed_char] for _ in range(length-1): next_probs probs[current] next_char np.random.choice(chars, pnext_probs) output.append(next_char) current stoi[next_char] return .join(output)注意采样是随机的所以每次结果可能不同。如果文本数据小生成结果可能重复训练文本的片段如果数据多样会看到更自然的组合。6. 扩展思考从 Bigram 到现代 LLM 的桥梁Bigram 模型虽然简单但已经包含了语言模型的核心要素。理解它之后你就能自然过渡到更复杂的模型N-gramBigram 只看前一个词N-gram 看前 N-1 个词。N 越大上下文信息越多但参数呈指数增长。神经网络语言模型用神经网络代替概率表能自动学习特征避免手工平滑。GPT本质上是基于 Transformer 的神经语言模型但能处理长距离依赖而不是仅限前几个词。我建议在跑通 Bigram 后做两个实验换不同来源的文本比如科技新闻和小说看生成风格如何变化。尝试 Trigram看前两个词感受模型复杂度和生成质量的关系。7. 常见问题排查7.1 生成文本全是乱码或重复检查训练文本是否太短或多样性不足。确认概率计算有没有除零错误加平滑项可避免。采样时是否正确使用了概率分布别用成了均匀随机。7.2 损失不下降或为 NaN数据预处理时确认 X 和 Y 的长度一致且对齐。概率计算时检查分母是否可能为零。对数计算前确保概率值不在零附近可加极小值 epsilon 防止 log(0)。7.3 内存或速度问题词表太大时如中文字符集概率表会很大。可先限制词表大小比如只保留高频字符。如果文本很长分批统计频率而不是一次性加载全部数据。8. 总结为什么值得花 10 分钟亲手实现Bigram 模型的价值不在于它能生成多好的文本而在于它用最少的代码展示了语言模型的本质。通过亲手实现你会记住语言模型是概率分布的学习。训练数据决定了模型的行为边界。采样生成是随机过程不是确定性输出。这些概念在 GPT、LLM 时代依然适用。当你后面遇到“注意力机制”“位置编码”时可以回想它们都是为了更好地捕捉上下文概率分布。如果你时间紧至少把数据预处理和概率计算这两步跑通。生成部分可以下次补上。但完整走一遍流程你会对“语言模型到底在学什么”有更踏实的感觉。