
在自然语言处理领域理解语言模型的基本构建单元是掌握现代大语言模型LLM工作原理的关键起点。Andrej Karpathy 在其著名的教学材料中提出的 Bigram 语言模型虽然结构简单却完美展示了语言模型的核心思想基于已有上下文预测下一个最可能的词元Token。这种基于统计的模型不仅是深度学习语言模型的雏形更是理解词嵌入Embedding、概率计算和序列生成等核心概念的理想入口。对于刚接触语言模型的开发者来说直接从 Transformer 或 BERT 等复杂模型开始往往会陷入细节困境。而 Bigram 模型只需要基础的 Python 和概率知识就能让开发者直观感受到语言模型如何从数据中学习规律并生成连贯的文本。本文将基于 Karpathy 的教学思路从零实现一个完整的 Bigram 语言模型并深入分析每个组件的设计原理和实际应用。1. 理解 Bigram 语言模型的核心机制1.1 什么是 Bigram 模型Bigram二元语法模型是一种基于马尔可夫假设的统计语言模型它假设每个词的出现概率只依赖于前一个词。这种简化虽然忽略了长距离依赖关系但在计算效率和模型可解释性方面具有明显优势。从数学角度Bigram 模型通过条件概率来建模文本序列。给定一个词序列 $w_1, w_2, ..., w_n$整个序列的概率可以表示为 $$P(w_1, w_2, ..., w_n) P(w_1) \times P(w_2|w_1) \times P(w_3|w_2) \times ... \times P(w_n|w_{n-1})$$在实际应用中Bigram 模型通过统计语料库中词对共现的频率来估计这些条件概率。例如在英文文本中natural 后面出现 language 的概率会明显高于出现 computer 的概率。1.2 Bigram 与现代语言模型的关系虽然现代大语言模型如 GPT 系列基于更复杂的 Transformer 架构但 Bigram 模型体现的核心思想依然适用语言模型本质上是学习词元之间的条件概率分布。Bigram 可以看作是 N-gram 模型家族中最简单的成员而 Transformer 则可以视为能够捕捉任意长度依赖关系的广义 N-gram 模型。理解 Bigram 模型有助于建立以下关键概念的直观认识Tokenization词元化将原始文本转换为模型可处理的离散单元概率计算基于统计或神经网络估计序列可能性文本生成通过采样概率分布逐步生成新文本1.3 Karpathy 教学方法的独特价值Andrej Karpathy 作为深度学习领域的知名专家其教学风格以从第一性原理出发著称。他倡导通过最小可工作实现来理解复杂系统的核心机制这种方法特别适合语言模型的学习渐进式复杂度从 Bigram 开始逐步引入更复杂的模型结构代码透明度每个数学概念都有对应的 Python 实现可视化分析通过直观的图表展示模型内部工作机制实践导向强调动手实现而非纯理论学习2. 环境准备与数据预处理2.1 基础环境配置实现 Bigram 语言模型只需要基础的 Python 科学计算环境。以下是推荐的环境配置# 必需的核心库 import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import matplotlib.pyplot as plt from collections import Counter import re # 检查环境版本 print(fPyTorch 版本: {torch.__version__}) print(fCUDA 可用: {torch.cuda.is_available()}) # 设置随机种子保证结果可复现 torch.manual_seed(42) np.random.seed(42)对于深度学习初学者建议使用 Google Colab 或本地 Jupyter Notebook 环境这样可以交互式地观察每个步骤的输出结果。如果使用本地环境确保安装 PyTorch 1.9 版本以获得最佳兼容性。2.2 文本数据准备与词元化词元化Tokenization是将原始文本转换为模型可处理数字序列的关键步骤。对于 Bigram 模型我们采用字符级词元化这简化了词汇表管理且更适合教学演示。def simple_tokenizer(text): 简单的字符级词元化器 # 获取所有唯一字符作为词汇表 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)} # 编码函数字符串 - 索引列表 encode lambda s: [stoi[c] for c in s] # 解码函数索引列表 - 字符串 decode lambda l: .join([itos[i] for i in l]) return encode, decode, vocab_size, chars # 示例文本数据使用莎士比亚作品片段 text To be, or not to be, that is the question: Whether tis nobler in the mind to suffer The slings and arrows of outrageous fortune, Or to take arms against a sea of troubles # 应用词元化 encode, decode, vocab_size, chars simple_tokenizer(text) print(f词汇表大小: {vocab_size}) print(f词汇表字符: {.join(chars)}) print(f编码示例: {encode(hello)}) print(f解码示例: {decode(encode(hello))})字符级词元化的优势在于词汇表较小通常几十到几百个字符避免了复杂的子词分割算法。但缺点是序列长度较长且难以捕捉词汇语义信息。2.3 训练数据构建Bigram 模型需要将文本序列转换为输入-目标对其中输入是当前词元目标是下一个词元。def create_bigram_dataset(text, encode_fn): 创建 Bigram 训练数据 # 将文本编码为索引序列 data torch.tensor(encode_fn(text), dtypetorch.long) # 构建输入-目标对 inputs data[:-1] # 除最后一个词元外的所有词元 targets data[1:] # 除第一个词元外的所有词元 print(f输入序列长度: {len(inputs)}) print(f前10个输入-目标对:) for i in range(10): print(f输入: {inputs[i].item()} ({decode([inputs[i].item()])}) - 目标: {targets[i].item()} ({decode([targets[i].item()])})) return inputs, targets inputs, targets create_bigram_dataset(text, encode)这种滑动窗口式的数据构建方法是序列建模的基础在更复杂的 RNN 和 Transformer 模型中也会以类似形式出现。3. 实现 Bigram 语言模型3.1 模型架构设计Bigram 语言模型的核心是一个查找表Lookup Table将每个输入词元映射为下一个词元的概率分布。在深度学习框架中这可以通过嵌入层Embedding Layer后接线性层来实现。class BigramLanguageModel(nn.Module): def __init__(self, vocab_size): super().__init__() # 每个词元直接映射到下一个词元的对数概率 self.token_embedding_table nn.Embedding(vocab_size, vocab_size) def forward(self, idx, targetsNone): # idx 和 targets 都是形状为 (B, T) 的整数张量 # B: batch size, T: time steps (序列长度) # 获取词元嵌入对数概率 logits self.token_embedding_table(idx) # (B, T, vocab_size) 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这个实现虽然简单但包含了语言模型的所有核心组件嵌入层、前向计算、损失函数和生成逻辑。3.2 模型初始化与验证在训练前我们需要验证模型的基本功能是否正常# 初始化模型 model BigramLanguageModel(vocab_size) print(f模型参数数量: {sum(p.numel() for p in model.parameters())}) # 创建批量数据这里批量大小为1 batch_size 1 sequence_length 10 batch_inputs inputs[:sequence_length].unsqueeze(0) # 形状: (1, 10) batch_targets targets[:sequence_length].unsqueeze(0) # 形状: (1, 10) # 前向传播测试 logits, loss model(batch_inputs, batch_targets) print(f初始损失: {loss.item():.4f}) # 生成测试 start_context torch.tensor([[encode(T)[0]]]) # 以 T 开头 generated model.generate(start_context, max_new_tokens20) generated_text decode(generated[0].tolist()) print(f初始生成结果: {generated_text})初始阶段由于模型尚未训练生成的文本通常是随机字符的混乱组合损失值也相对较高。3.3 训练循环实现Bigram 模型的训练过程相对简单主要涉及梯度下降优化def train_model(model, inputs, targets, num_epochs1000, learning_rate0.1): 训练 Bigram 模型 # 将数据转换为批量形式 batch_inputs inputs.unsqueeze(0) # (1, seq_len) batch_targets targets.unsqueeze(0) # (1, seq_len) # 优化器 optimizer torch.optim.AdamW(model.parameters(), lrlearning_rate) # 记录训练过程 losses [] for epoch in range(num_epochs): # 前向传播 logits, loss model(batch_inputs, batch_targets) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() losses.append(loss.item()) if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss.item():.4f}) return losses # 执行训练 losses train_model(model, inputs, targets) # 绘制损失曲线 plt.plot(losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss) plt.show()Bigram 模型的训练通常收敛很快因为参数数量较少且模型结构简单。损失值的下降表明模型正在学习训练文本中的统计规律。4. 模型评估与文本生成4.1 生成质量分析训练完成后我们可以评估模型的文本生成能力def evaluate_model(model, encode, decode, start_chars[T, W, O]): 评估模型生成质量 for start_char in start_chars: if start_char not in chars: continue start_idx torch.tensor([[encode(start_char)[0]]]) generated model.generate(start_idx, max_new_tokens50) generated_text decode(generated[0].tolist()) print(f以 {start_char} 开头的生成结果:) print(generated_text) print(- * 50) evaluate_model(model, encode, decode)良好的 Bigram 模型应该能够生成与训练文本风格相似的连贯字符序列。由于我们使用莎士比亚文本训练生成的文本应该包含类似的词汇和句式结构。4.2 概率分布可视化理解模型学到的概率分布有助于洞察其内部工作机制def visualize_probabilities(model, chars, encode, decode): 可视化词元转移概率 # 获取嵌入权重即Bigram概率 weights model.token_embedding_table.weight.detach() # 可视化热力图 plt.figure(figsize(12, 10)) plt.imshow(weights, cmapviridis) plt.colorbar() plt.xlabel(下一个词元) plt.ylabel(当前词元) plt.title(Bigram 概率分布热力图) # 设置刻度标签 plt.xticks(range(len(chars)), chars) plt.yticks(range(len(chars)), chars) plt.tight_layout() plt.show() # 分析特定字符的转移概率 char_to_analyze t if char_to_analyze in chars: char_idx encode(char_to_analyze)[0] probs F.softmax(weights[char_idx], dim-1) # 按概率排序 sorted_indices torch.argsort(probs, descendingTrue) print(f字符 {char_to_analyze} 后最可能出现的字符:) for i in range(min(5, len(chars))): idx sorted_indices[i].item() print(f {chars[idx]}: {probs[idx]:.3f}) visualize_probabilities(model, chars, encode, decode)概率热力图可以直观展示模型学到的字符间转移规律比如空格后通常跟字母元音后可能跟辅音等英语拼写规则。4.3 困惑度计算困惑度Perplexity是语言模型的常用评估指标衡量模型对测试数据的预测能力def calculate_perplexity(model, inputs, targets): 计算模型在测试数据上的困惑度 with torch.no_grad(): logits, loss model(inputs.unsqueeze(0), targets.unsqueeze(0)) perplexity torch.exp(loss) return perplexity.item() test_perplexity calculate_perplexity(model, inputs, targets) print(f测试困惑度: {test_perplexity:.2f})困惑度越低表示模型对数据的建模越好。对于 Bigram 模型困惑度值通常较高因为其有限的上下文窗口难以捕捉语言的复杂结构。5. 常见问题与调试技巧5.1 训练不收敛问题排查Bigram 模型虽然简单但实践中仍可能遇到训练问题问题现象可能原因检查方法解决方案损失值居高不下学习率设置不当尝试不同学习率使用 0.01-0.1 范围的学习率生成文本重复单一字符模型陷入局部最优检查梯度是否消失调整初始化或使用更优优化器不同运行结果差异大随机种子未固定设置 torch.manual_seed()固定所有随机种子保证可复现性def debug_training_issue(): 训练问题调试示例 # 检查梯度 model_debug BigramLanguageModel(vocab_size) logits, loss model_debug(inputs.unsqueeze(0), targets.unsqueeze(0)) loss.backward() # 检查梯度范数 total_norm 0 for p in model_debug.parameters(): if p.grad is not None: param_norm p.grad.norm() total_norm param_norm.item() print(f梯度范数: {total_norm}) # 如果梯度太小可能是学习率问题 if total_norm 1e-5: print(梯度可能消失尝试增大学习率) debug_training_issue()5.2 词元化相关问题字符级词元化虽然简单但在处理真实文本时可能遇到问题def handle_special_characters(text): 处理特殊字符和大小写 # 统一小写处理 text text.lower() # 处理特殊字符和标点 text re.sub(r[^\w\s], , text) # 将标点替换为空格 text re.sub(r\s, , text) # 合并多个空格 return text # 测试处理效果 original_text Hello, World! This is a test. processed_text handle_special_characters(original_text) print(f原始文本: {original_text}) print(f处理后: {processed_text})对于更复杂的应用场景建议使用成熟的词元化库如 Hugging Face Tokenizers它们能更好地处理子词分割和特殊字符。5.3 内存和性能优化当处理大规模文本时需要考虑内存使用和计算效率def optimize_training(data_size10000): 训练过程优化示例 # 1. 使用更小的数据类型 inputs_optimized inputs.to(torch.int16) # 如果词汇表小于32768 # 2. 批量训练 batch_size 32 seq_length 64 # 创建小批量数据 def create_batches(data, batch_size, seq_length): num_batches data.size(0) // (batch_size * seq_length) data data[:num_batches * batch_size * seq_length] data data.view(batch_size, -1) return data batched_data create_batches(inputs, batch_size, seq_length) print(f批量数据形状: {batched_data.shape}) # 3. 梯度累积模拟更大批量 accumulation_steps 4 # 每 accumulation_steps 步执行一次参数更新 optimize_training()6. 扩展方向与进阶应用6.1 从 Bigram 到更复杂模型理解 Bigram 模型后可以自然过渡到更先进的语言模型架构N-gram 模型扩展上下文窗口到 3-5 个词元神经网络语言模型使用 MLP 或 RNN 捕捉更长距离依赖Transformer 模型基于自注意力机制的现代架构class TrigramLanguageModel(nn.Module): 三元语法模型示例 def __init__(self, vocab_size, embedding_dim64): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim) self.fc nn.Linear(2 * embedding_dim, vocab_size) # 连接前两个词元的嵌入 def forward(self, idx, targetsNone): # 假设 idx 形状为 (B, T)其中 T 2 emb1 self.embedding(idx[:, :-1]) # 第一个到倒数第二个词元 emb2 self.embedding(idx[:, 1:]) # 第二个到最后一个词元 # 连接相邻词元的嵌入 combined torch.cat([emb1, emb2], dim-1) logits self.fc(combined) # 其余逻辑与 Bigram 类似 # ...6.2 应用于实际任务Bigram 模型虽然简单但可以应用于一些实际场景文本自动补全基于输入前缀预测后续内容拼写检查识别低概率字符序列作为潜在错误数据压缩利用概率分布进行算术编码def text_autocomplete(model, encode, decode, prefix, max_length20): 文本自动补全功能 context torch.tensor([encode(prefix)], dtypetorch.long) completion model.generate(context, max_new_tokensmax_length) return decode(completion[0].tolist()) # 测试自动补全 prefix To be completion text_autocomplete(model, encode, decode, prefix) print(f自动补全结果: {completion})6.3 集成到现代 NLP 流程在现代 NLP 项目中Bigram 级别的统计信息仍然有用数据预处理分析文本统计特征基线模型作为复杂模型的性能基准特征工程为机器学习模型提供统计特征理解 Bigram 模型为学习词嵌入Embedding概念提供了坚实基础。在现代深度学习中嵌入层是表示离散特征的核心组件其思想直接源于这种简单的查找表方法。通过这个完整的 Bigram 语言模型实现我们不仅掌握了基础统计语言模型的原理还建立了向更复杂模型扩展的坚实基础。这种从第一性原理出发的学习方法确保了对后续高级概念的深入理解。