从Bigram模型入门:10分钟理解语言模型核心原理与代码实现

发布时间:2026/7/17 8:48:12
从Bigram模型入门:10分钟理解语言模型核心原理与代码实现 如果你觉得大语言模型很神秘觉得 Transformer、注意力机制这些概念高深莫测那么 Karpathy 的 Bigram 语言模型可能是你最好的入门选择。这个看似简单的模型只用 10 分钟就能讲清楚核心原理却能帮你真正理解现代语言模型是如何思考的。很多人在学习 LLM 时容易陷入一个误区一上来就研究复杂的架构和数学公式结果越学越迷糊。而 Bigram 模型的价值在于它用最精简的方式展示了语言模型最本质的工作机制——预测下一个词。虽然简单但它揭示了 token 处理、概率计算、生成逻辑这些核心概念为理解更复杂的模型打下了坚实基础。本文将带你从零实现一个完整的 Bigram 语言模型不仅讲解原理还会提供可运行的代码示例。无论你是刚入门 NLP 的开发者还是想夯实基础的有经验工程师这篇文章都能让你获得实用的知识。1. Bigram 语言模型到底解决了什么问题在自然语言处理中最核心的任务之一就是让机器能够理解和生成人类语言。Bigram 模型虽然简单但它解决了语言模型最基础的问题基于上下文预测下一个词的概率。传统方法中如果要实现文本生成可能需要依赖复杂的规则引擎或者模板匹配。这种方法的问题在于缺乏灵活性无法处理未见过的语言模式。Bigram 模型引入了统计学习的思路通过分析大量文本数据中的词对共现频率来学习语言的统计规律。举个例子当我们看到人工这个词时下一个词很可能是智能、成本、操作等但不太可能是香蕉、火箭这种不相关的词。Bigram 模型就是通过计算这种条件概率来工作的。虽然现代大语言模型已经发展到使用 Transformer 等复杂架构但它们的核心思想仍然建立在类似 Bigram 的这种概率预测基础上。理解 Bigram 模型就相当于掌握了理解现代 LLM 的钥匙。2. Bigram 模型的核心概念解析2.1 什么是 BigramBigram二元语法指的是由两个连续元素组成的序列。在语言模型中这些元素通常是单词或字符。例如在句子I love programming中Bigram 序列为(I, love)、(love, programming)。Bigram 模型的核心假设是一个词的出现概率只依赖于它前面的一个词。这种假设虽然简化了语言的实际复杂性但为模型提供了可计算的基础。2.2 Tokenization从文本到数字在语言模型处理文本之前需要先将文字转换为模型可以理解的数字形式这个过程就是 Tokenization分词。对于 Bigram 模型我们可以采用字符级或单词级的分词策略。字符级分词将文本拆分成单个字符词汇表较小但序列较长单词级分词将文本拆分成单词词汇表较大但序列较短。在实现简单 Bigram 模型时字符级分词通常是更好的选择因为它能避免未登录词OOV问题。2.3 条件概率与生成逻辑Bigram 模型的核心是条件概率计算P(当前词 | 前一个词)。通过统计训练文本中词对的出现频率我们可以构建一个概率矩阵其中每个单元格表示给定前一个词时当前词出现的概率。生成新文本时模型从起始标记开始根据当前词的分布概率随机选择下一个词然后将新选择的词作为上下文继续生成后续词直到达到指定长度或遇到结束标记。3. 环境准备与工具选择在开始实现 Bigram 模型之前我们需要准备合适的开发环境。虽然 Bigram 模型相对简单但正确的工具选择能让学习和实验过程更加顺畅。3.1 Python 环境配置推荐使用 Python 3.8 版本因为这个版本在机器学习库的兼容性方面表现稳定。你可以通过以下命令检查当前 Python 版本python --version # 或者 python3 --version如果还没有安装 Python建议从官网下载安装包或者使用 conda、pyenv 等版本管理工具。3.2 必要的库安装Bigram 模型的实现主要依赖以下几个库pip install numpy matplotlib torchNumPy用于高效的数值计算和矩阵操作Matplotlib用于可视化训练过程和结果分析PyTorch虽然简单 Bigram 可以用纯 NumPy 实现但使用 PyTorch 能为后续学习更复杂模型打下基础3.3 开发工具建议对于代码编写推荐使用 Jupyter Notebook 进行初步实验和调试然后用 VS Code 或 PyCharm 整理成完整的脚本。Jupyter 的交互式特性特别适合机器学习项目的探索阶段。# 安装 Jupyter pip install jupyter # 启动 Jupyter jupyter notebook4. 从零实现 Bigram 语言模型现在让我们开始动手实现一个完整的 Bigram 语言模型。我们将使用莎士比亚的文本作为训练数据一步步构建模型并生成新的文本。4.1 数据准备与预处理首先我们需要获取训练数据并进行预处理。这里我们使用莎士比亚的作品作为示例import torch import torch.nn as nn import torch.nn.functional as F import matplotlib.pyplot as plt import requests # 下载莎士比亚文本 url https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt response requests.get(url) text response.text print(数据长度:, len(text)) print(前1000个字符:) print(text[:1000])数据预处理阶段我们需要创建字符到索引的映射# 获取所有唯一字符 chars sorted(list(set(text))) vocab_size len(chars) print(唯一字符数:, vocab_size) print(字符列表:, .join(chars)) # 创建字符到索引的映射 stoi {ch: i for i, ch in enumerate(chars)} itos {i: ch for i, ch in enumerate(chars)} # 编码和解码函数 def encode(s): return [stoi[c] for c in s] def decode(l): return .join([itos[i] for i in l]) # 测试编码解码 test_str hello world encoded encode(test_str) decoded decode(encoded) print(f原始: {test_str}) print(f编码: {encoded}) print(f解码: {decoded})4.2 构建训练数据接下来我们将文本数据转换为模型可以训练的格式# 将整个文本编码为张量 data torch.tensor(encode(text), dtypetorch.long) print(数据张量形状:, data.shape) # 划分训练集和验证集 n int(0.9 * len(data)) train_data data[:n] val_data data[n:] # 定义批量大小和上下文长度 batch_size 4 block_size 8 # Bigram 的上下文长度 def get_batch(split): data train_data if split train else val_data ix torch.randint(len(data) - block_size, (batch_size,)) x torch.stack([data[i:iblock_size] for i in ix]) y torch.stack([data[i1:iblock_size1] for i in ix]) return x, y xb, yb get_batch(train) print(输入张量形状:, xb.shape) print(输入样本:) for i in range(batch_size): print(f样本 {i}: {decode(xb[i].tolist())} - {decode(yb[i].tolist())})4.3 实现 Bigram 模型现在我们来实现核心的 Bigram 模型class BigramLanguageModel(nn.Module): def __init__(self, vocab_size): super().__init__() # 每个token直接映射到下一个token的概率 self.token_embedding_table nn.Embedding(vocab_size, vocab_size) def forward(self, idx, targetsNone): # idx 和 targets 都是 (B,T) 形状的张量 logits self.token_embedding_table(idx) # (B,T,C) if targets is None: loss None else: B, T, C logits.shape logits logits.view(B*T, C) targets targets.view(B*T) loss F.cross_entropy(logits, targets) return logits, loss def generate(self, idx, max_new_tokens): # idx 是当前上下文 (B,T) 数组 for _ in range(max_new_tokens): # 获取预测 logits, loss self(idx) # 只关注最后一个时间步 logits logits[:, -1, :] # 变成 (B,C) # 应用softmax获取概率 probs F.softmax(logits, dim-1) # (B,C) # 从分布中采样 idx_next torch.multinomial(probs, num_samples1) # (B,1) # 将采样结果拼接到序列中 idx torch.cat((idx, idx_next), dim1) # (B,T1) return idx # 初始化模型 model BigramLanguageModel(vocab_size) out, loss model(xb, yb) print(模型输出形状:, out.shape) print(初始损失:, loss)4.4 模型训练与优化有了模型结构后我们需要设置训练循环来优化模型参数# 创建优化器 optimizer torch.optim.AdamW(model.parameters(), lr1e-3) # 训练循环 batch_size 32 for steps in range(10000): # 获取一小批数据 xb, yb get_batch(train) # 前向传播 logits, loss model(xb, yb) # 反向传播 optimizer.zero_grad(set_to_noneTrue) loss.backward() optimizer.step() if steps % 1000 0: print(f步骤 {steps}, 损失: {loss.item():.4f}) # 评估模型在验证集上的表现 model.eval() with torch.no_grad(): xb, yb get_batch(val) logits, loss model(xb, yb) print(f验证集损失: {loss.item():.4f})5. 文本生成与效果验证训练完成后最令人兴奋的部分就是使用模型生成新的文本了。让我们看看这个简单的 Bigram 模型能产生什么样的结果。5.1 基础文本生成# 生成新文本 context torch.zeros((1, 1), dtypetorch.long) # 起始字符 generated_chars model.generate(context, max_new_tokens500)[0].tolist() generated_text decode(generated_chars) print(生成的文本:) print(generated_text)5.2 生成质量分析第一次运行可能会发现生成的文本看起来像是随机字符的组合。这是因为基础的 Bigram 模型确实比较简单它只考虑了前一个字符的信息。但是仔细观察你会发现某些字符组合出现的频率更高这反映了训练数据中的统计规律。为了改进生成质量我们可以尝试以下方法# 改进的生成函数可以控制随机性 def generate_with_temperature(model, idx, max_new_tokens, temperature1.0): for _ in range(max_new_tokens): logits, _ model(idx) logits logits[:, -1, :] / temperature probs F.softmax(logits, dim-1) idx_next torch.multinomial(probs, num_samples1) idx torch.cat((idx, idx_next), dim1) return idx # 尝试不同的温度参数 print(温度0.5更保守:) context torch.zeros((1, 1), dtypetorch.long) result generate_with_temperature(model, context, 200, temperature0.5) print(decode(result[0].tolist())) print(\n温度1.5更多样:) context torch.zeros((1, 1), dtypetorch.long) result generate_with_temperature(model, context, 200, temperature1.5) print(decode(result[0].tolist()))5.3 模型性能评估除了观察生成的文本我们还需要定量评估模型的性能def evaluate_model(model, data, num_batches100): model.eval() total_loss 0 with torch.no_grad(): for _ in range(num_batches): xb, yb get_batch(val) _, loss model(xb, yb) total_loss loss.item() return total_loss / num_batches avg_loss evaluate_model(model, val_data) print(f模型平均验证损失: {avg_loss:.4f}) # 计算困惑度Perplexity perplexity torch.exp(torch.tensor(avg_loss)) print(f模型困惑度: {perplexity:.2f})6. 常见问题与解决方案在实现和训练 Bigram 模型过程中你可能会遇到一些典型问题。这里总结了常见问题及其解决方法。6.1 训练不收敛问题问题现象损失值波动很大或者持续不下降。可能原因学习率设置不当梯度爆炸或消失数据预处理有问题解决方案# 调整学习率 optimizer torch.optim.AdamW(model.parameters(), lr1e-4) # 尝试更小的学习率 # 添加梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 检查数据预处理 print(检查数据范围:, data.min(), data.max()) print(检查词汇表大小:, vocab_size)6.2 生成文本质量差问题现象生成的文本没有意义像是随机字符。可能原因模型过于简单无法捕捉语言规律训练数据不足训练轮数不够解决方案# 增加模型复杂度升级到 Trigram 或更高 class TrigramLanguageModel(nn.Module): def __init__(self, vocab_size, embedding_dim64): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim) self.lm_head nn.Linear(embedding_dim * 2, vocab_size) # 考虑前两个字符 def forward(self, idx, targetsNone): B, T idx.shape # 将每两个连续字符作为输入 embeddings self.embedding(idx) # (B,T,embedding_dim) # 组合连续字符的嵌入 combined torch.cat([embeddings[:, :-1], embeddings[:, 1:]], dim-1) logits self.lm_head(combined) # ... 其余部分类似 Bigram 模型6.3 内存不足问题问题现象训练时出现 CUDA out of memory 错误。可能原因批量大小过大序列长度过长模型参数过多解决方案# 减小批量大小 batch_size 16 # 原来是 32 # 减小序列长度 block_size 16 # 原来是 32 # 使用梯度累积模拟大批量 def train_with_gradient_accumulation(model, accumulation_steps4): optimizer.zero_grad() for i in range(accumulation_steps): xb, yb get_batch(train) logits, loss model(xb, yb) loss loss / accumulation_steps # 标准化损失 loss.backward() optimizer.step()7. Bigram 模型的局限性与改进方向虽然 Bigram 模型是理解语言模型的优秀起点但我们需要清楚认识它的局限性并了解如何向更先进的模型演进。7.1 主要局限性上下文窗口过短只考虑前一个词无法捕捉长距离依赖关系数据稀疏问题很多词对在训练数据中可能从未出现生成质量有限无法产生连贯的长文本7.2 向更先进模型的演进Trigram 模型考虑前两个词能捕捉稍长的模式# Trigram 的基本思想 P(词3 | 词1, 词2) count(词1, 词2, 词3) / count(词1, 词2)N-gram 模型通用化到考虑前 N-1 个词神经网络语言模型使用神经网络来学习词表示和上下文关系现代大语言模型基于 Transformer 架构使用自注意力机制捕捉全局依赖7.3 实际应用中的权衡在选择模型时需要考虑以下因素计算资源Bigram 计算简单适合资源受限环境数据量简单模型在小数据上可能表现更好任务复杂度简单任务可能不需要复杂模型可解释性Bigram 的概率解释更直观8. 工程实践与生产环境建议如果要将 Bigram 或类似语言模型应用到实际项目中以下是一些重要的工程实践建议。8.1 模型部署考虑# 模型保存和加载 def save_model(model, path): torch.save({ model_state_dict: model.state_dict(), stoi: stoi, itos: itos, vocab_size: vocab_size }, path) def load_model(path): checkpoint torch.load(path) model BigramLanguageModel(checkpoint[vocab_size]) model.load_state_dict(checkpoint[model_state_dict]) return model, checkpoint[stoi], checkpoint[itos] # 保存模型 save_model(model, bigram_model.pth)8.2 性能优化技巧# 使用 GPU 加速 device cuda if torch.cuda.is_available() else cpu model model.to(device) # 批量生成优化 def batch_generate(model, contexts, max_new_tokens): 批量生成文本提高GPU利用率 model.eval() with torch.no_grad(): for _ in range(max_new_tokens): logits, _ model(contexts) logits logits[:, -1, :] probs F.softmax(logits, dim-1) next_tokens torch.multinomial(probs, num_samples1) contexts torch.cat([contexts, next_tokens], dim1) return contexts8.3 监控与日志在生产环境中完善的监控是必不可少的import logging from datetime import datetime # 设置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(fmodel_training_{datetime.now().strftime(%Y%m%d)}.log), logging.StreamHandler() ] ) # 训练过程中的关键指标记录 def train_with_logging(model, epochs): for epoch in range(epochs): xb, yb get_batch(train) logits, loss model(xb, yb) # 记录指标 if epoch % 100 0: perplexity torch.exp(loss).item() logging.info(fEpoch {epoch}: Loss{loss.item():.4f}, Perplexity{perplexity:.2f}) # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step()通过实现这个完整的 Bigram 语言模型你不仅掌握了语言模型的基本原理还建立了向更复杂模型演进的基础。这种从简单到复杂的学习路径比直接研究现代大语言模型更能建立扎实的理解。Bigram 模型的价值不在于它的实用性而在于它的教学意义。它像是一把钥匙打开了理解现代语言模型的大门。当你真正理解了这个简单模型的工作原理再学习 Transformer、注意力机制等复杂概念时就会有一种原来如此的顿悟感。建议你在理解本文代码的基础上尝试实现 Trigram 模型或者加入简单的神经网络结构亲自体验模型复杂化带来的效果提升。这种动手实践的经验比单纯阅读理论更有价值。