Download scripts/gen_gpt_notebook.py from yusiwen/dl-from-scratch: direct link, hf CLI and curl.
- Browser
- Download file 7.42 kB
-
https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_gpt_notebook.py
- Command line
-
hf download hf://yusiwen/dl-from-scratch/scripts/gen_gpt_notebook.py
-
curl -L -o gen_gpt_notebook.py https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_gpt_notebook.py
7.42 kB
| #!/usr/bin/env python3 | |
| """Generate GPT notebook.""" | |
| import nbformat as nbf | |
| nb = nbf.v4.new_notebook() | |
| nb.metadata = { | |
| "kernelspec": { | |
| "display_name": "Python 3", | |
| "language": "python", | |
| "name": "python3", | |
| }, | |
| "language_info": {"name": "python", "version": "3.12.0"}, | |
| } | |
| cells = [] | |
| def md(source): | |
| cells.append(nbf.v4.new_markdown_cell(source)) | |
| def code(source): | |
| cells.append(nbf.v4.new_code_cell(source)) | |
| # ── Title ── | |
| md("""\ | |
| # GPT: Decoder-Only Transformer | |
| Autoregressive language model with Causal Self-Attention and KV Cache. | |
| """) | |
| # ── Background ── | |
| md("""\ | |
| ## 背景 | |
| GPT(Generative Pre-trained Transformer)是 decoder-only 的 Transformer 架构。 | |
| 与 BERT 的双向注意力不同,GPT 使用 **因果掩码(causal mask)**——每个 token | |
| 只能看到自己和之前的位置,从而实现自回归生成。 | |
| 训练目标:给定 tokens [t₁, t₂, ..., tₙ],预测 [t₂, t₃, ..., tₙ₊₁]。 | |
| """) | |
| # ── Math ── | |
| md("""\ | |
| ## 数学原理 | |
| ### 自回归语言建模 | |
| $$P(x_1, ..., x_n) = \\prod_{i=1}^n P(x_i \\mid x_1, ..., x_{i-1})$$ | |
| 训练损失(每个位置预测下一个 token): | |
| $$\\mathcal{L} = -\\sum_{i=1}^{n-1} \\log P(x_{i+1} \\mid x_1, ..., x_i)$$ | |
| ### 缩放点积注意力 + 因果掩码 | |
| $$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right) V$$ | |
| 因果掩码将上三角位置设为 $-\\infty$,保证位置 $i$ 只能 attend 到 $j \\leq i$。 | |
| """) | |
| # ── Architecture ― | |
| md("""\ | |
| ## 架构 | |
| ``` | |
| Token Embed → +Pos Embed → [DecoderBlock × N] → LayerNorm → LM Head → logits | |
| DecoderBlock: | |
| Causal Self-Attention → Add + LayerNorm → FFN → Add + LayerNorm | |
| ``` | |
| """) | |
| # ── Imports ── | |
| code("""\ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| from torch.utils.data import DataLoader, Dataset | |
| from datasets import load_dataset | |
| from nlp.gpt.tokenizer import WordTokenizer | |
| from nlp.gpt.model import GPT | |
| from utils.device import get_device | |
| device = get_device() | |
| print(f"Device: {device}") | |
| """) | |
| # ── Tokenizer ── | |
| code("""\ | |
| # 构建词级分词器 | |
| print("Loading text8 and building vocabulary...") | |
| ds = load_dataset("afmck/text8", split="train") | |
| raw = ds[0]["text"] | |
| words = raw.lower().split() | |
| chunk_words = 32 | |
| raw_chunks = [" ".join(words[i:i + chunk_words]) for i in range(0, len(words), chunk_words)] | |
| tokenizer = WordTokenizer(vocab_size=5000) | |
| tokenizer.build_vocab(raw_chunks) | |
| print(f"Vocabulary size: {tokenizer.vocab_size}") | |
| """) | |
| # ── Dataset ── | |
| code("""\ | |
| class TextDataset(Dataset): | |
| def __init__(self, texts, tokenizer, max_len=64): | |
| self.examples = [] | |
| for text in texts: | |
| tokens, mask = tokenizer.encode(text, max_len) | |
| self.examples.append({ | |
| "input_ids": torch.tensor(tokens, dtype=torch.long), | |
| "attention_mask": torch.tensor(mask, dtype=torch.long), | |
| }) | |
| def __len__(self): | |
| return len(self.examples) | |
| def __getitem__(self, idx): | |
| return self.examples[idx] | |
| sentences = [s.strip() for s in raw_chunks if len(s.strip()) > 5] | |
| sentences = sentences[:15000] # 用 15K 子集控制训练时间 | |
| print(f"Training chunks: {len(sentences):,}") | |
| dataset = TextDataset(sentences, tokenizer, max_len=64) | |
| loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=0) | |
| """) | |
| # ── Model ── | |
| code("""\ | |
| model = GPT( | |
| vocab_size=tokenizer.vocab_size, | |
| d_model=256, n_heads=4, n_layers=4, max_len=64, | |
| ).to(device) | |
| print(f"Parameters: {model.num_params():,}") | |
| """) | |
| # ── Training ── | |
| md("""\ | |
| ## 训练 | |
| > ⏱ 预估耗时:**15 epoch × ~45s/epoch ≈ 11 分钟**(M4 Max, batch_size=64) | |
| > 如果觉得太久,可以把下面 `NUM_EPOCHS` 改小到 5 先看趋势。 | |
| """) | |
| code("""\ | |
| NUM_EPOCHS = 15 # ← 可调 | |
| LR = 1e-3 | |
| GRAD_CLIP = 1.0 | |
| criterion = nn.CrossEntropyLoss() | |
| optimizer = optim.AdamW(model.parameters(), lr=LR) | |
| loss_history = [] | |
| ppl_history = [] | |
| for epoch in range(1, NUM_EPOCHS + 1): | |
| model.train() | |
| total_loss = 0.0 | |
| num_batches = 0 | |
| for batch in loader: | |
| input_ids = batch["input_ids"].to(device) | |
| labels = input_ids[:, 1:].contiguous() | |
| inputs = input_ids[:, :-1].contiguous() | |
| optimizer.zero_grad() | |
| logits, _ = model(inputs) | |
| loss = criterion(logits.view(-1, logits.size(-1)), labels.view(-1)) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP) | |
| optimizer.step() | |
| total_loss += loss.item() | |
| num_batches += 1 | |
| avg_loss = total_loss / num_batches | |
| perplexity = math.exp(avg_loss) | |
| loss_history.append(avg_loss) | |
| ppl_history.append(perplexity) | |
| print(f"Epoch [{epoch:2d}/{NUM_EPOCHS}] Loss: {avg_loss:.4f} PPL: {perplexity:.2f}") | |
| """) | |
| # ── Loss curve ── | |
| md("""\ | |
| ## Loss 曲线 | |
| """) | |
| code("""\ | |
| import matplotlib.pyplot as plt | |
| from utils.device import get_device | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) | |
| ax1.plot(loss_history, marker='o') | |
| ax1.set_xlabel("Epoch"); ax1.set_ylabel("Loss"); ax1.set_title("Training Loss") | |
| ax1.grid(True) | |
| ax2.plot(ppl_history, marker='o', color='orange') | |
| ax2.set_xlabel("Epoch"); ax2.set_ylabel("Perplexity"); ax2.set_title("Perplexity") | |
| ax2.grid(True) | |
| plt.tight_layout(); plt.show() | |
| """) | |
| # ── Generation ── | |
| md("""\ | |
| ## 文本生成 | |
| 训练完成后,尝试用不同 temperature 生成文本,观察随机性变化。 | |
| """) | |
| code("""\ | |
| def generate_text(model, tokenizer, prompt, max_new_tokens=30, temperature=0.8, top_k=40): | |
| tokens, _ = tokenizer.encode(prompt, max_len=64) | |
| input_ids = torch.tensor([tokens], dtype=torch.long) | |
| with torch.no_grad(): | |
| output = model.generate( | |
| input_ids, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| top_k=top_k, | |
| bad_tokens_ids=[tokenizer.sep_id, tokenizer.pad_id, tokenizer.cls_id], | |
| ) | |
| full_text = tokenizer.decode(output[0].tolist()) | |
| return full_text.strip() | |
| prompts = [ | |
| "the meaning of life is", | |
| "the future of artificial intelligence is", | |
| ] | |
| for prompt in prompts: | |
| print(f"\\nPrompt: {prompt}") | |
| print("-" * 50) | |
| for temp in [0.3, 0.8, 1.2]: | |
| output = generate_text(model, tokenizer, prompt, temperature=temp) | |
| print(f" t={temp:.1f}: {output}") | |
| """) | |
| # ── Temperature comparison ── | |
| md("""\ | |
| ### Temperature 的作用 | |
| - **t=0.3**:生成保守,几乎总是选概率最高的 token | |
| - **t=0.8**:适度随机,结果多样但不散乱 | |
| - **t=1.2**:高随机性,可能偏离主题 | |
| 读者可以回到训练 cell 改 `NUM_EPOCHS` 跑更久,观察更多 epoch 后生成质量的提升。 | |
| """) | |
| # ── Thinking questions ── | |
| md("""\ | |
| ## 思考题 | |
| 1. 如果去掉 causal mask,训练时每个 token 都能看到未来,模型会学到什么?推理时还能用吗? | |
| 2. KV Cache 为什么能加速推理?推理时每步的复杂度从 $O(T^2)$ 降到了多少? | |
| 3. Temperature 越高生成越随机还是越确定?从 softmax 的公式角度解释。 | |
| 4. top-k 采样和 temperature 缩放各自的优缺点是什么?能同时用吗? | |
| 5. 把 `num_epochs` 从 15 改到 25,PPL 会降到多少?试试看。 | |
| """) | |
| nb.cells = cells | |
| out = "nlp/gpt/gpt.ipynb" | |
| with open(out, "w") as f: | |
| nbf.write(nb, f) | |
| print(f"Generated {out}") | |