# Summary [[Andrej Karpathy]] 2019 年发布,用最小代码(几百行)展示 GPT 的核心机制(Transformer + 自回归语言建模)。  1. 本质:GPT is not a complicated model and this implementation is appropriately about 300 lines of code (see `mingpt/model.py`)  2. 就是 Transformer:**All that's going on** is that a sequence of indices feeds into a Transformer, and a probability distribution over the next index in the sequence comes out. `def forward(self, idx, targets=None):` 中体现  3. 高效batching是重点:The majority of the complexity is just being clever with batching (both across examples and over sequence length) for efficiency.   # Cues [[Transformer架构]] [[自回归]] [[temperature]] [[seed 随机种子]] # Notes - [一、项目结构](#%E4%B8%80%E3%80%81%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84) - [二、本质是 Transformer](#%E4%BA%8C%E3%80%81%E6%9C%AC%E8%B4%A8%E6%98%AF%20Transformer) - [1.1 Temperature](#1.1%20Temperature) - [1.2 top_k采样](#1.2%20top_k%E9%87%87%E6%A0%B7) - [三、训练过程](#%E4%B8%89%E3%80%81%E8%AE%AD%E7%BB%83%E8%BF%87%E7%A8%8B) - [3.1 训练参数](#3.1%20%E8%AE%AD%E7%BB%83%E5%8F%82%E6%95%B0) - [3.2 训练过程](#3.2%20%E8%AE%AD%E7%BB%83%E8%BF%87%E7%A8%8B) - [3.3 正则化与优化器配置](#3.3%20%E6%AD%A3%E5%88%99%E5%8C%96%E4%B8%8E%E4%BC%98%E5%8C%96%E5%99%A8%E9%85%8D%E7%BD%AE) - [效果](#%E6%95%88%E6%9E%9C) - [四、实践](#%E5%9B%9B%E3%80%81%E5%AE%9E%E8%B7%B5) - [4.1 示例感受](#4.1%20%E7%A4%BA%E4%BE%8B%E6%84%9F%E5%8F%97) - [任务类型](#%E4%BB%BB%E5%8A%A1%E7%B1%BB%E5%9E%8B) - [训练结果](#%E8%AE%AD%E7%BB%83%E7%BB%93%E6%9E%9C) - [损失变化(Loss)](#%E6%8D%9F%E5%A4%B1%E5%8F%98%E5%8C%96%EF%BC%88Loss%EF%BC%89) - [准确率](#%E5%87%86%E7%A1%AE%E7%8E%87) - [验证](#%E9%AA%8C%E8%AF%81) - [4.2 实践项目 (动手训练模型)](#4.2%20%E5%AE%9E%E8%B7%B5%E9%A1%B9%E7%9B%AE%C2%A0(%E5%8A%A8%E6%89%8B%E8%AE%AD%E7%BB%83%E6%A8%A1%E5%9E%8B)) - [4.2.1 加法器任务`adder.py`](#4.2.1%20%E5%8A%A0%E6%B3%95%E5%99%A8%E4%BB%BB%E5%8A%A1%60adder.py%60) - [4.2.2 文本生成任务 `chargpt`](#4.2.2%20%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90%E4%BB%BB%E5%8A%A1%20%60chargpt%60) - [五、待尝试](#%E4%BA%94%E3%80%81%E5%BE%85%E5%B0%9D%E8%AF%95) ## 一、项目结构 1. GPT 模型训练和推理 - 支持从头训练或加载预训练模型 2. 多种模型规格 - 从 nano (3层) 到 xl (48层) 3. 字符级和 [[BPE]] 分词 - 支持文本生成任务 | 优先级 | 文件 | 作用 | | ----- | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | | 🔴 阅读 | `model.py` | GPT 模型定义 | | 🔴 阅读 | `trainer.py` | 训练流程<br>- 训练循环的标准流程<br>- [[优化器 optimizer]] 配置(权重衰减策略)<br>- Callback 机制 | | 🟢 阅读 | `bpe.py` | 文本编码<br>- [[BPE]] (Byte Pair Encoding) 如何将文本转换为 token 序列<br>- 这是 GPT-2 使用的同款编码器,<br> GPT-2 使用 50,257 个 token 的词汇表 | | 🟡 阅读 | `utils.py` | 配置管理<br>- `CfgNode` [[超参数]]配置系统(灵感来自 [[YACS]])<br>- 如何从命令行或字典更新配置 | | 🟡 动手 | `projects/adder/adder.py` | 完整训练示例 | | | | | ## 二、本质是 Transformer **All that's going on** is that a sequence of indices feeds into a Transformer, and a probability distribution over the next index in the sequence comes out. `def forward(self, idx, targets=None):` 中体现 ```Java 输入 → Norm → Multi-Head Attention → Add → Norm → FFN → Add → 输出 ``` 1. `idx` 就是输入的索引序列,形状是 `(batch_size, sequence_length)` 2. Token Embedding: 将索引转换为向量 3. Position Embedding: 添加位置信息 4. Transformer Blocks: 多层自注意力和前馈网络处理 1. 都是 attn 和 mlpf 都是preNorm[[正则化]] 5. `lm_head` 将隐藏状态映射到词汇表大小, 得到 logits,表示每个位置对下一个 token 的未归一化概率 6. 归一化后得到真正的概率分布 7. 从分布中采样得到下一个索引 `idx_next` 8. 将新索引拼接回序列,[[自回归]]继续生成 ```shell GPT类 ├── 初始化: 构建 Transformer 架构 │ ├── wte: 词嵌入层 │ ├── wpe: 位置嵌入层 │ ├── Block x N层: Transformer块 │ │ ├── LayerNorm1 $\rightarrow$ 自注意力 (CausalSelfAttention) │ │ └── LayerNorm2 $\rightarrow$ 前馈网络 (MLP) │ └── lm_head: 输出投影层 │ ├── forward(): 前向传播 │ ├── 输入token $\rightarrow$ 词嵌入 + 位置嵌入 │ ├── 通过N个Transformer块 │ └── 输出logits (+ 可选loss计算) │ └── generate(): 文本生成 └── 自回归预测下一个token def forward(self, idx, targets=None): # 1. idx 就是输入的索引序列,形状是 (batch_size, sequence_length) device = idx.device b, t = idx.size() pos = torch.arange(0, t, dtype=torch.long, device=device).unsqueeze(0) # shape (1, t) # forward the GPT model itself tok_emb = self.transformer.wte(idx) # 2. Token Embedding: 将索引转换为 Embedding pos_emb = self.transformer.wpe(pos) # 3. Position Embedding: 添加位置信息 Embedding x = self.transformer.drop(tok_emb + pos_emb) for block in self.transformer.h: # 4. Transformer Blocks: 多层自注意力和前馈网络处理 x = x + self.attn(self.ln_1(x)) # pre Norm x = x + self.mlpf(self.ln_2(x)) x = self.transformer.ln_f(x) # 5. lm_head 将隐藏状态映射到词汇表大小, 得到 logits,表示每个位置对下一个 token 的未归一化概率 logits = self.lm_head(x) loss = None if targets is not None: loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1) return logits, loss @torch.no_grad() def generate(self, idx, max_new_tokens, temperature=1.0, do_sample=False, top_k=None): for _ in range(max_new_tokens): idx_cond = idx if idx.size(1) <= self.block_size else idx[:, -self.block_size:] logits, _ = self(idx_cond) logits = logits[:, -1, :] / temperature if top_k is not None: v, _ = torch.topk(logits, top_k) logits[logits < v[:, [-1]]] = -float('Inf') # 6. 归一化后得到真正的概率分布 probs = F.softmax(logits, dim=-1) # 7. 从分布中采样得到下一个索引 idx_next if do_sample: idx_next = torch.multinomial(probs, num_samples=1) else: _, idx_next = torch.topk(probs, k=1, dim=-1) # 8. 将新索引拼接回序列,自回归继续生成 idx = torch.cat((idx, idx_next), dim=1) return idx ``` ### 1.1 LLM decoding 这里的`generate`函数比较简单,进一步可以看[[Transformers]]库的generate函数的所有参数的作用(了解了就知道推理的各种方法了,什么[[temperature]],[[重复性惩罚]],top-k, top-p, [[Beam search]], group_beam_search, 避免[[N-gram]]重复),有助于深入理解[[OpenAI API]] #### Temperature Temperature 通过缩放 logits 来控制概率分布的"尖锐度": `logits = logits[:, -1,:]/ temperature` 1. temperature = 1.0(默认值) - 不改变 logits,保持原始概率分布 - 平衡的随机性 2. temperature < 1.0(例如 0.5) - logits 被放大(除以小于1的数) - 经过 softmax 后,概率分布更加尖锐/集中 - 模型更倾向选择最可能的 token - 输出更确定、保守、重复性高 3. temperature > 1.0(例如 1.5) - logits 被缩小(除以大于1的数) - 经过 softmax 后,概率分布更加平滑/均匀 - 模型会考虑更多可能性较低的 token - 输出更多样、创造性、但可能不连贯 #### top_k采样 1. 是一种采样策略,相当于从 n 个 logits 候选人里截取了 k 个 2. 另一种是 top_p - [[核采样]](0-1),默认 1.0,类似 top_k,但基于累积概率。例如 top_p=0.9 表示只考虑累积概率达到 90% 的 token。[[OpenAI API]] 回顾 `model.py` 中的这些部分: - **Line 29-71**: `CausalSelfAttention` - 注意 `masked_fill` 如何实现因果掩码 - Multi-head 的拆分和合并 - **Line 215-258**: `configure_optimizers` - Weight Decay 的选择性应用策略 - 哪些参数需要正则化,哪些不需要 ## 三、训练过程 `trainer.py` 是一个通用的训练框架,与 GPT 本身无关,可以用于任何 PyTorch 模型。 [[BPE]] `bpe.py` ### 3.1 训练参数 ```@staticmethod def get_default_config(): C = CN() C.device = 'auto' # GPU or CPU C.num_workers = 4 # 数据加载的进程数 C.max_iters = None # 最大训练步数,训练循环由 max_iters 控制,而不是 epoch 数 这是 GPT 训练的常见做法(不强调 "epoch" 概念) 实现无限数据流,不依赖 epoch 概念 C.batch_size = 64 # 批次大小,背一本书,一次背几页 C.learning_rate = 3e-4 # 学习率(GPT-3 论文中的值),在山上每一步跨多大 C.betas = (0.9, 0.95) # Adam 优化器的 beta 参数 [[优化器 optimizer]] C.weight_decay = 0.1 # 权重衰减(GPT-3 风格) C.grad_norm_clip = 1.0 # 梯度裁剪阈值 [[梯度范数]] return C ``` #### 正则化 ##### Pre Norm [[正则化]] ```python class Block(nn.Module): """ an unassuming Transformer block """ def forward(self, x): x = x + self.attn(self.ln_1(x)) x = x + self.mlpf(self.ln_2(x)) return x ``` ##### 优化器配置 问题:模型在训练集上表现很好,但在测试集上表现差(过拟合) 解决方案:[[正则化]] = 限制模型复杂度,防止过拟合 1. [[Weight Decay 权重衰减]]是一种正则化方法,它惩罚过大的权重值。 2. 哪些层需要正则化,哪些层不需要? 1. Bias通常不正则化,它只是一个偏移量(标量或向量),数量少,影响小,不会导致过拟合。 2. Linear 层是模型的主要学习参数,数量多、维度高,容易过拟合。 3. LayerNorm 的 weight 和 bias 是缩放和平移参数,它们的作用是归一化,不是学习特征,通常接近1和 0,不会导致过拟合。 4. Embedding是查找表,不是参数化的变换,每个token 对应一个向量。Weight decay 会让所有embedding 向量缩小,破坏表示 ```shell def configure_optimizers(self, train_config): decay = set() no_decay = set() whitelist_weight_modules = (torch.nn.Linear, ) blacklist_weight_modules = (torch.nn.LayerNorm, torch.nn.Embedding) for mn, m in self.named_modules(): for pn, p in m.named_parameters(): fpn = '%s.%s' % (mn, pn) if mn else pn if pn.endswith('bias'): # 1. Bias通常不正则化,它只是一个偏移量(标量或向量),数量少,影响小,不会导致过拟合 no_decay.add(fpn) elif pn.endswith('weight') and isinstance(m, whitelist_weight_modules): # 2. 这些是模型的主要学习参数数量多、维度高,容易过拟合 decay.add(fpn) elif pn.endswith('weight') and isinstance(m, blacklist_weight_modules): # 3. LayerNorm 的 weight 和 bias 是缩放和平移参数 # 它们的作用是归一化,不是学习特征,通常接近1和 0,不会导致过拟合 # 4. Embedding是查找表,不是参数化的变换 每个token 对应一个向量 # Weight decay 会让所有embedding 向量缩小,破坏表示 no_decay.add(fpn) optimizer = torch.optim.AdamW(optim_groups, lr=train_config.learning_rate, betas=train_config.betas) return optimizer ``` 效果 ```Java # 假设模型结构如下 model = nn.Sequential( nn.Linear(10, 20), # 有 weight 和 bias nn.LayerNorm(20), # 有 weight 和 bias nn.Linear(20, 5), # 有 weight 和 bias ) # 运行 configure_optimizers 后的分组: decay = { '0.weight', # Linear 的 weight ✅ '2.weight', # Linear 的 weight ✅ } no_decay = { '0.bias', # Linear 的 bias ❌ '1.weight', # LayerNorm 的 weight ❌(特殊!) '1.bias', # LayerNorm 的 bias ❌ '2.bias', # Linear 的 bias ❌ } ``` ### 3.2 训练过程 1. 拿到1个 batch 的训练数据,放到 GPU 中 2. 前向传播 3. 反向传播,更新参数 4. 回调一些动作 5. 终止条件 ``` shell def run(self): model, config = self.model, self.config # setup the optimizer self.optimizer = model.configure_optimizers(config) # setup the dataloader train_loader = DataLoader( self.train_dataset, sampler=torch.utils.data.RandomSampler(self.train_dataset, replacement=True, num_samples=int(1e10)), shuffle=False, pin_memory=True, batch_size=config.batch_size, num_workers=config.num_workers, ) model.train() self.iter_num = 0 self.iter_time = time.time() data_iter = iter(train_loader) while True: # 1. 拿到1个 batch 的训练数据,放到 GPU 中 try: batch = next(data_iter) except StopIteration: data_iter = iter(train_loader) batch = next(data_iter) batch = [t.to(self.device) for t in batch] x, y = batch # 2. 前向传播 logits, self.loss = model(x, y) # 3. 反向传播,更新参数 model.zero_grad(set_to_none=True) self.loss.backward() ## 梯度裁剪,限制梯度的全局范数不超过 1.0,GPT-3 论文的标准做法 torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_norm_clip) self.optimizer.step() # 4. 回调一些动作 self.trigger_callbacks('on_batch_end') self.iter_num += 1 tnow = time.time() self.iter_dt = tnow - self.iter_time self.iter_time = tnow # 5. 终止条件 if config.max_iters is not None and self.iter_num >= config.max_iters: break ``` 回调就是[[观察者模式]]: 1. 训练日志记录:不仅记录 loss,还要记录时间、学习率等,[[wandb]] 2. 模型验证与评估:对于生成任务,定期采样查看质量,定期检查模型在验证集上的表现 3. 保存表现最好的模型,防止过拟合:保存[[checkpoint]]时包含 optimizer 状态 4. 记录 iter_num 以便续训 ## 四、实践 ### 4.1 直观例子 | 文件名 | 描述 | |:------------------- |:------------------------------------------ | | `simple_generate.py` | 只是在一个随机参数的模型上演示推理流程(不训练),会生成乱码,**证明训练的重要性** | | `generate_script.py` | 加载预训练 GPT-2 模型并生成文本,类似于调用一个开源的 Qwen | | `demo_script.py` | 最简单的排序任务示例,自己感受完整的训练流程 | #### 任务类型 排序任务`demo_script.py` ```Java input sequence : [[0, 0, 2, 1, 0, 1]] ← 输入的原始序列 predicted sorted: [[0, 0, 0, 1, 1, 2]] ← 模型预测的排序结果 gt sort : [0, 0, 0, 1, 1, 2] ← 真实的正确答案(Ground Truth) matches : True ← 预测是否与真值匹配 ``` #### 训练结果 - 只用了 10000 个样本 - 只训练了 2000 次迭代(~1 分钟) - 模型只有 0.09M 参数(相比 GPT-2 的 124M) ```shell (venv) (base) liuyishou@MacBook-Pro-3 minGPT % python demo_script.py Creating datasets... Example data point: 1 -1 1 -1 0 -1 2 -1 0 -1 1 0 0 0 0 1 1 1 1 1 1 2 Creating model... number of parameters: 0.09M Setting up trainer... running on device cpu Starting training... /Users/liuyishou/usr/projects/learn/minGPT/venv/lib/python3.12/site-packages/torch/utils/data/dataloader.py:684: UserWarning: 'pin_memory' argument is set as true but not supported on MPS now, then device pinned memory won't be used. warnings.warn(warn_msg) iter_dt 0.00ms; iter 0: train loss 1.06646 iter_dt 19.26ms; iter 100: train loss 0.13984 iter_dt 19.41ms; iter 200: train loss 0.05240 iter_dt 18.38ms; iter 300: train loss 0.01114 iter_dt 20.08ms; iter 400: train loss 0.05151 iter_dt 19.12ms; iter 500: train loss 0.02871 iter_dt 18.89ms; iter 600: train loss 0.08735 iter_dt 20.64ms; iter 700: train loss 0.00715 iter_dt 17.61ms; iter 800: train loss 0.01766 iter_dt 19.13ms; iter 900: train loss 0.02940 iter_dt 19.60ms; iter 1000: train loss 0.01233 iter_dt 19.74ms; iter 1100: train loss 0.02167 iter_dt 18.90ms; iter 1200: train loss 0.01647 iter_dt 17.48ms; iter 1300: train loss 0.01915 iter_dt 20.00ms; iter 1400: train loss 0.00056 iter_dt 16.95ms; iter 1500: train loss 0.02157 iter_dt 19.06ms; iter 1600: train loss 0.00426 iter_dt 17.79ms; iter 1700: train loss 0.03464 iter_dt 16.83ms; iter 1800: train loss 0.00071 iter_dt 16.56ms; iter 1900: train loss 0.00308 Evaluating model... train final score: 5000/5000 = 100.00% correct test final score: 5000/5000 = 100.00% correct Testing with a specific sequence... input sequence : [[0, 0, 2, 1, 0, 1]] predicted sorted: [[0, 0, 0, 1, 1, 2]] gt sort : [0, 0, 0, 1, 1, 2] matches : True ``` #### 损失变化(Loss) ```Java iter 0: 1.06646 ← 初始:完全随机 iter 1000: 0.01233 ← 中期:已经学得很好 iter 1900: 0.00308 ← 末期:几乎完美 ``` 说明:模型从"随机猜测"进化到"精确预测" #### 准确率 ```Java # 训练集 100% 正确 → 模型学会了 train final score: 5000/5000 = 100.00% correct ✅ # 测试集也 100% 正确 → 模型没有死记硬背,而是真正理解了排序算法 test final score: 5000/5000 = 100.00% correct ✅ # 如果过拟合,测试集会明显低于训练集。这里两者都是 100%,说明模型学到了通用规律 ``` #### 验证 ```Java 输入: [0, 0, 2, 1, 0, 1] 模型预测: [0, 0, 0, 1, 1, 2] ✅ 正确答案: [0, 0, 0, 1, 1, 2] (Ground Truth(真实值/标准答案/真值)) matches: True ``` 说明:模型在从未见过的具体序列上也能正确排序 ### 4.2 实践项目 (动手训练模型) #### 4.2.1 加法器任务`adder.py` A. projects/adder/ - 推荐首先尝试! - 任务:训练 GPT 学习加法(如 123+456=579) - 难度:⭐⭐ - 收获:理解 GPT 如何学习算术逻辑,体会位置编码和注意力机制的作用 - 这是 GPT-3 论文中提到的经典实验 这个项目会训练一个 GPT 模型来学习 n 位数的加法运算。例如:85 + 50 = 135 可视化理解模型如何学习算术规则 **a) 加法器项目(最简单)** ```bash projects/adder/adder.py ``` #### 4.2.2 文本生成任务 `chargpt` B. projects/chargpt/ - 更有趣但更复杂 - 任务:训练字符级语言模型(比如莎士比亚文本生成) - 难度:⭐⭐⭐ - 收获:理解语言模型如何学习文本风格和结构 **b) 字符级语言模型** ```bash projects/chargpt/chargpt.py ``` - 在文本上训练字符级 LM - 可以下载 tiny-shakespeare 数据集测试 ## 五、待尝试 1. **修改超参数**:尝试改变 `n_layer`、`n_head`、`n_embd` 观察效果 2. **自定义数据集**:用自己的数据训练模型 3. **添加功能**:尝试实现 README 中的 todos 4. 阅读 README 中引用的论文(GPT-1/2/3) 5. **Debug & Visualize**: - 打印 attention weights 可视化注意力模式 - 观察不同层学到了什么 6. 如果想要更现代的实现,可以看 [[nanoGPT]](作者的新版本) - - 对比 minGPT vs nanoGPT 的设计差异 - nanoGPT 更注重性能和可复现性