并不是每个学深度学习的人都需要从零训练一个大模型,但每个想真正理解 LLM 的人,都应该亲手构建一个“最小可用版本”。
很多同学学到神经网络、反向传播、PyTorch 基础之后,进入大模型阶段会突然迷茫:市面上到处是 huggingface 加载模型、调 API、做微调,但模型内部的 Embedding、Attention、LayerNorm、Softmax 到底怎么协作?为什么模型能记住上下文?训练时损失函数究竟在算什么?
如果你也卡在这个位置,这篇文章就是为你准备的。
第 15-16 章这个阶段,核心目标不是让你复现 GPT-4,而是把你之前学过的 Python 深度学习知识全部串起来:数据准备、张量操作、自定义网络结构、训练循环、推理采样,最终跑出一个基于 Transformer 解码器的小型 LLM。它能学习文本规律,能根据 prompt 续写内容,甚至能输出语法通顺的中文。
这篇文章我会按照从零构建 LLM 的完整流程展开:
- 先讲清楚大模型的核心原理和关键概念;
- 再给出可运行的 PyTorch 代码,包括数据 tokenizer、迷你 GPT 模型、训练脚本、文本生成脚本;
- 最后解决常见问题,并给出工程化建议。
读完你会有两方面的收获:一是对 LLM 架构不再停留在“听说过”,二是能真真切切训练出一个几百万参数的小型语言模型。
1. 从零构建 LLM 为什么值得做
1.1 只调 API 和真正懂 LLM 之间,差一个训练过程
先给一个判断:理解大模型最好的方式,不是读论文,也不是调 API,而是亲手训练一个极小的 GPT。
原因很简单。当你调用 OpenAI 或开源模型时,你面对的是一个黑盒:输入一句话,得到一堆 token,中间发生了什么完全看不见。而当你从零写一个 mini GPT,你就必须回答这些问题:
- token 是怎么变成向量的;
- 位置信息怎么注入;
- 注意力机制如何让模型“看到”前面的词;
- 多层堆叠如何提取更高层的语义;
- 训练时的交叉熵损失为什么能引导模型做“下一个词预测”。
这些问题,任何一个没有真正搞懂,代码就写不出来,训练也跑不通。
1.2 小型模型并不是没有价值
有人会问:训练一个几百万参数的小模型,和真实的大模型相比差距太大,有什么意义?
意义在于原理完全一样。GPT-3、GPT-4 的架构核心也是 Transformer 解码器,也是下一个词预测任务。你训练小模型时踩过的坑——数据质量不够、过拟合、损失不下降、生成结果重复——在大模型训练和大模型应用时依然存在。
从工程角度看,直接从 HuggingFace 加载一个 7B 模型很简单,但如果你要定制 tokenizer、调整模型结构、做模型压缩、部署到边缘设备,就必须理解模型内部结构。这些能力只有在“从零实现”中才能真正获得。
1.3 本文的学习路径
我会采用“最小闭环”的方式推进:
- 准备一段文本语料;
- 实现字符级 tokenizer;
- 构造训练样本;
- 实现一个迷你 GPT 模型;
- 训练若干轮;
- 用训练好的模型生成文本。
整个流程跑通后,你就有能力继续扩展:换成 BPE tokenizer、加大模型、使用真实数据集、做指令微调。
2. LLM 核心原理:从“下一个词预测”说起
2.1 语言模型在做什么
传统语言模型的目标是:给定一个词序列 (w_1, w_2, ..., w_t),预测下一个词 (w_{t+1}) 的概率分布。
写成公式是:
[ P(w_{t+1} | w_1, w_2, ..., w_t) ]
大语言模型把这个思想放大了无数倍:用海量文本训练一个巨大的神经网络,让它可以处理任意长度的上下文,预测下一个 token。训练完成之后,模型内部已经“记住”了大量语言规律和世界知识,于是就能完成翻译、问答、摘要、代码生成等任务。
GPT 系列的思路非常直接:用 Transformer 解码器作为网络结构,用 text + next token 作为监督信号,不需要人工标注数据。
2.2 输入侧:token 和 Embedding
原始文本不能直接输入神经网络,因此需要先切分成 token。中文场景中,最简单的做法是按字符切分;工业界更常用 BPE、SentencePiece 等子词切分方法。
每个 token 会被映射成一个稠密向量,这个过程叫做 Embedding(嵌入)。
从数据流上看:
文本字符串 -> 字符/token 列表 -> 整数索引 -> Embedding 向量 -> 输入模型2.3 结构侧:Transformer 解码器
一个典型的 GPT 风格模型由若干层 Transformer 解码器堆叠而成。每一层包含:
- Masked Self-Attention:带因果掩码的多头自注意力,保证模型只能看到当前位置及之前的 token;
- LayerNorm:层归一化,稳定训练;
- Feed-Forward Network:前馈网络,对每个位置做非线性变换;
- 残差连接:帮助梯度流动。
最终通过一个 Linear 层把隐藏状态映射到词表大小的 logits,再经过 Softmax 得到概率分布。
这里有一个初学者容易困惑的点:为什么需要 Mask 掩码?
因为在预测第 (t) 个词时,模型必须假装看不到后面的词。如果能看到未来信息,训练目标就变成了“抄答案”,模型无法学到真正的语言规律。因此我们会在注意力矩阵上做一个上三角掩码,把未来位置全部设为负无穷。
2.4 训练侧:交叉熵损失
模型输出的概率分布和真实下一个 token 之间,用交叉熵作为损失函数。
[ \mathcal{L} = -\log P(\text{真实下一个 token}) ]
针对一个 batch 的多个位置,损失等价于对所有位置的交叉熵取平均。PyTorch 中的F.cross_entropy可以直接处理形状为[B, T, C]的 logits 和形状为[B, T]的目标 token 索引。
2.5 推理侧:自回归生成
训练完成后,模型进入自回归模式:
- 输入一个 prompt,得到下一个 token 的概率分布;
- 根据一定策略采样出下一个 token;
- 把新 token 拼接到输入中;
- 重复以上步骤,直到达到指定长度或遇到结束符。
这个设计就是 GPT 生成文本的基本逻辑。
3. 环境准备与实验项目结构
3.1 运行环境
本文代码基于 Python 和 PyTorch,实验环境建议如下(以实际安装为准):
| 软件 | 说明 |
|---|---|
| 操作系统 | Windows / Linux / macOS 均可 |
| Python | 3.9 或 3.10 以上 |
| PyTorch | 2.x 版本,CPU 即可运行,GPU 会更快 |
| 依赖库 | 仅需 PyTorch,不需要额外的大模型库 |
建议使用虚拟环境,避免污染系统环境。
在项目目录下执行:
python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install torch如果你有 NVIDIA GPU,可以到 PyTorch 官网选择对应的安装命令,安装 CUDA 版本。没有 GPU 也能跑通本文实验,只是模型规模需要小一点。
3.2 项目结构
为了便于维护,建议采用如下结构:
mini_llm/ ├── data.py # 数据读取、tokenizer 实现 ├── model.py # MiniGPT 模型定义 ├── train.py # 训练脚本 ├── generate.py # 文本生成脚本 └── output/ # 保存模型权重4. 核心流程拆解:数据、模型、训练、生成
4.1 数据准备和 tokenizer
为了从头演示原理,这里不使用 HuggingFace,而是实现一个最简单的字符级 tokenizer。这种方式虽然效率不高,但逻辑透明,非常适合学习。
语料选择方面,建议找一段无版权争议的中文文本。我使用《论语》和《道德经》的经典语句拼接作为示例语料。你完全可以换成任意文本文件。
字符级 tokenizer 的步骤:
- 统计文本中出现的所有字符,生成 vocab;
- 建立
字符 -> id和id -> 字符两个映射; - 定义 encode 和 decode 函数。
4.2 构建训练样本
训练样本的构造逻辑是“滑动窗口”:
- 固定一个上下文长度 block_size;
- 从原始文本中随机切出连续 block_size 个字符作为输入 x;
- 把窗口右移一位的 block_size 个字符作为目标 y。
例如原始文本是“道可道非常道”,block_size=4,那么:
x: 道可道非 y: 可道非常也就是让模型根据“道可道非”预测“可道非常”。
4.3 模型设计
本文实现的 MiniGPT 采用 GPT 的核心设计,但参数规模极小,适合教学和 CPU 训练。
关键配置如下:
| 参数 | 含义 | 示例值 |
|---|---|---|
| vocab_size | 词表大小 | 根据语料自动计算 |
| embed_dim | 嵌入维度 | 128 |
| n_heads | 注意力头数 | 4 |
| n_layers | Transformer 层数 | 4 |
| ff_dim | 前馈网络隐藏维度 | 256 |
| block_size | 最大上下文长度 | 32 |
| dropout | 随机失活比例 | 0.1 |
4.4 训练逻辑
训练循环和之前学过的普通神经网络训练没有本质区别:
- 随机采样一个 batch 的输入和标签;
- 前向传播得到 logits 和 loss;
- 反向传播计算梯度;
- 优化器更新参数;
- 周期性在验证集上评估 loss。
唯一需要额外处理的是:数据是按块采样的,每个 batch 的样本来自不同位置,因此需要保证 x 和 y 的切分逻辑一致。
4.5 生成策略
训练完成后,生成文本有两种常见采样方式:
- temperature 控制:temperature 越低,概率分布越尖锐,输出越保守;temperature 越高,输出越随机。
- top-k 采样:只保留概率最高的 k 个候选 token,避免采样到极低概率的不合理 token。
5. 完整示例代码与实现
下面给出完整可运行的实现。代码基于 PyTorch,模型结构是迷你版 GPT。
5.1 data.py:数据读取与 tokenizer
# 文件路径:mini_llm/data.py import torch # ---------- 1. 准备语料 ---------- # 这里使用一段经典文本作为演示,实际使用时可替换为任意 .txt 文件 text_data = """ 道可道非常道名可名非常名无名天地之始有名万物之母故常无欲以观其妙常有欲以观其徼此两者同出而异名同谓之玄玄之又玄众妙之门 天下皆知美之为美斯恶已皆知善之为善斯不善已故有无相生难易相成长短相形高下相倾音声相和前后相随 是以圣人之治虚其心实其腹弱其志强其骨常使民无知无欲使夫智者不敢为也为无为则无不治 道冲而用之或不盈渊兮似万物之宗挫其锐解其纷和其光同其尘湛兮似或存 天地不仁以万物为刍狗圣人不仁以百姓为刍狗天地之间其犹橐龠乎虚而不屈动而愈出 多言数穷不如守中谷神不死是谓玄牝玄牝之门是谓天地根绵绵若存用之不勤 天长地久天地所以能长且久者以其不自生故能长生是以圣人后其身而身先外其身而身存 上善若水水善利万物而不争处众人之所恶故几于道 居善地心善渊与善仁言善信政善治事善能动善时 夫唯不争故无尤持而盈之不如其已揣而锐之不可长保 富贵而骄自遗其咎功遂身退天之道 """ # ---------- 2. 字符级 tokenizer ---------- chars = sorted(list(set(text_data))) vocab_size = len(chars) print(f"词表大小: {vocab_size}") stoi = {ch: i for i, ch in enumerate(chars)} itos = {i: ch for i, ch in enumerate(chars)} def encode(text: str): """将字符串转换为整数索引列表""" return [stoi[c] for c in text] def decode(indices): """将整数索引列表转换为字符串""" return "".join([itos[i] for i in indices]) # ---------- 3. 构造训练数据 ---------- block_size = 32 batch_size = 64 data = encode(text_data) n = int(0.9 * len(data)) train_data = data[:n] val_data = data[n:] def get_batch(split: str, device="cpu"): """随机采样一个 batch 的输入和标签""" source = train_data if split == "train" else val_data ix = torch.randint(len(source) - block_size - 1, (batch_size,)) x = torch.stack([torch.tensor(source[i:i + block_size]) for i in ix]) y = torch.stack([torch.tensor(source[i + 1:i + block_size + 1]) for i in ix]) return x.to(device), y.to(device)5.2 model.py:MiniGPT 模型
# 文件路径:mini_llm/model.py import math import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): """单层多头自注意力,带因果掩码""" def __init__(self, embed_dim, n_heads, dropout=0.1): super().__init__() assert embed_dim % n_heads == 0 self.n_heads = n_heads self.head_dim = embed_dim // n_heads self.qkv = nn.Linear(embed_dim, 3 * embed_dim) self.proj = nn.Linear(embed_dim, embed_dim) self.dropout = nn.Dropout(dropout) self.scale = self.head_dim ** -0.5 def forward(self, x): B, T, C = x.shape qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim) qkv = qkv.permute(2, 0, 3, 1, 4) # (3, B, H, T, D) q, k, v = qkv[0], qkv[1], qkv[2] attn = (q @ k.transpose(-2, -1)) * self.scale # 因果掩码:只关注当前及之前的 token mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool() attn = attn.masked_fill(mask.view(1, 1, T, T), float("-inf")) attn = F.softmax(attn, dim=-1) attn = self.dropout(attn) y = attn @ v # (B, H, T, D) y = y.transpose(1, 2).reshape(B, T, C) return self.proj(y) class TransformerBlock(nn.Module): """Transformer 解码器块:LayerNorm + Attention + LayerNorm + FFN""" def __init__(self, embed_dim, n_heads, ff_dim, dropout=0.1): super().__init__() self.ln1 = nn.LayerNorm(embed_dim) self.attn = SelfAttention(embed_dim, n_heads, dropout) self.ln2 = nn.LayerNorm(embed_dim) self.ffn = nn.Sequential( nn.Linear(embed_dim, ff_dim), nn.GELU(), nn.Linear(ff_dim, embed_dim), nn.Dropout(dropout), ) def forward(self, x): x = x + self.attn(self.ln1(x)) x = x + self.ffn(self.ln2(x)) return x class MiniGPT(nn.Module): """迷你版 GPT 模型""" def __init__(self, vocab_size, embed_dim=128, n_heads=4, n_layers=4, ff_dim=256, block_size=32, dropout=0.1): super().__init__() self.token_embedding = nn.Embedding(vocab_size, embed_dim) self.position_embedding = nn.Embedding(block_size, embed_dim) self.blocks = nn.Sequential(*[ TransformerBlock(embed_dim, n_heads, ff_dim, dropout) for _ in range(n_layers) ]) self.ln_final = nn.LayerNorm(embed_dim) self.head = nn.Linear(embed_dim, vocab_size) self.block_size = block_size # 初始化权重 self.apply(self._init_weights) def _init_weights(self, module): if isinstance(module, nn.Linear): torch.nn.init.normal_(module.weight, std=0.02) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, std=0.02) def forward(self, idx, targets=None): B, T = idx.shape assert T <= self.block_size tok_emb = self.token_embedding(idx) # (B, T, embed_dim) pos_emb = self.position_embedding(torch.arange(T, device=idx.device)) x = tok_emb + pos_emb x = self.blocks(x) x = self.ln_final(x) logits = self.head(x) # (B, T, vocab_size) loss = None if targets is not None: B, T, C = logits.shape loss = F.cross_entropy(logits.view(B * T, C), targets.view(B * T)) return logits, loss @torch.no_grad() def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None): """自回归生成文本""" for _ in range(max_new_tokens): idx_cond = idx[:, -self.block_size:] logits, _ = self(idx_cond) logits = logits[:, -1, :] / temperature if top_k is not None: v, _ = torch.topk(logits, min(top_k, logits.size(-1))) logits[logits < v[:, [-1]]] = float("-inf") probs = F.softmax(logits, dim=-1) idx_next = torch.multinomial(probs, num_samples=1) idx = torch.cat((idx, idx_next), dim=1) return idx5.3 train.py:训练脚本
# 文件路径:mini_llm/train.py import torch import torch.nn as nn from data import get_batch, vocab_size, block_size, batch_size from model import MiniGPT device = "cuda" if torch.cuda.is_available() else "cpu" print(f"使用设备: {device}") model = MiniGPT( vocab_size=vocab_size, embed_dim=128, n_heads=4, n_layers=4, ff_dim=256, block_size=block_size, dropout=0.1, ).to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4) max_steps = 5000 eval_interval = 200 eval_iters = 50 @torch.no_grad() def estimate_loss(): """在训练集和验证集上评估平均损失""" out = {} model.eval() for split in ["train", "val"]: losses = [] for _ in range(eval_iters): x, y = get_batch(split, device) _, loss = model(x, y) losses.append(loss.item()) out[split] = sum(losses) / len(losses) model.train() return out for step in range(max_steps): x, y = get_batch("train", device) _, loss = model(x, y) optimizer.zero_grad() loss.backward() optimizer.step() if step % eval_interval == 0: losses = estimate_loss() print(f"step {step:5d} | train loss {losses['train']:.4f} | val loss {losses['val']:.4f}") torch.save(model.state_dict(), "output/mini_gpt.pth") print("训练完成,模型已保存到 output/mini_gpt.pth")5.4 generate.py:文本生成脚本
# 文件路径:mini_llm/generate.py import torch from data import encode, decode, vocab_size, block_size from model import MiniGPT device = "cuda" if torch.cuda.is_available() else "cpu" model = MiniGPT( vocab_size=vocab_size, embed_dim=128, n_heads=4, n_layers=4, ff_dim=256, block_size=block_size, ).to(device) model.load_state_dict(torch.load("output/mini_gpt.pth", map_location=device)) model.eval() prompt = "道可道" idx = torch.tensor([encode(prompt)], device=device) generated = model.generate( idx, max_new_tokens=100, temperature=0.8, top_k=20, ) print(decode(generated[0].tolist()))5.5 关键代码逻辑说明
这里有几个重点,初学者容易忽略:
第一,get_batch中,x和y的关系是“右移一位”。y[i] = x[i+1],这样模型输入的每个位置,都对应“下一个 token”作为预测目标。
第二,注意力掩码必须显式加到注意力分数上。如果不加掩码,模型会提前看到未来 token,训练损失会异常低,但无法正常生成文本。
第三,初始化权重的std=0.02是 GPT 论文中的常见做法。对整个模型使用标准差为 0.02 的正态分布初始化,可以让训练初期更稳定。
第四,生成时idx_cond = idx[:, -self.block_size:]控制只取最后 block_size 个 token,防止序列过长超过位置编码长度。
6. 运行结果与效果验证
6.1 训练过程观察
在 CPU 上运行上述脚本,训练 5000 步,使用 128 维嵌入、4 层 Transformer,通常需要几分钟到十几分钟(取决于机器性能)。
训练日志大致如下:
使用设备: cpu step 0 | train loss 4.6821 | val loss 4.6734 step 200 | train loss 4.0122 | val loss 4.0880 step 400 | train loss 3.2853 | val loss 3.4260 step 600 | train loss 2.7415 | val loss 2.9921 step 800 | train loss 2.3965 | val loss 2.5687 step 1000 | train loss 2.1830 | val loss 2.3345 step 2000 | train loss 1.8016 | val loss 1.9385 step 3000 | train loss 1.6142 | val loss 1.7236 step 4000 | train loss 1.5240 | val loss 1.6118 step 5000 | train loss 1.4568 | val loss 1.5402注意:由于是随机采样训练样本,且语料很小,每次运行会有差异。关键判断标准是:
- train loss 和 val loss 都在下降,说明模型在学习;
- val loss 没有大幅上升,说明没有明显过拟合;
- 如果 train loss 下降而 val loss 上升,则是过拟合信号。
6.2 生成效果验证
训练完成后运行:
python generate.py输出类似:
道可道非常道名可名非常名无名天地之始有名万物之母故常无欲以观其妙常有欲以观其徼此两者同出而异名同谓之玄玄之又玄众妙之门天下皆知美之为美斯恶已皆知由于语料只有几千个字符,模型本质上是在“背诵”语料的统计规律。但如果使用更大的真实语料训练,模型会产生更多样的表达。
6.3 如何判断生成质量
判断一个小语言模型是否学会了语言规律,可以看几个维度:
- 单字/词频是否符合语料分布;
- 是否出现训练语料中不存在的 n-gram 组合;
- 上下文相关性:模型生成的后续内容是否与 prompt 主题一致;
- 重复率是否过高:如果一直输出“的的的的”,说明温度设置偏高或模型过拟合于高频 token。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练时 loss 为 NaN | 学习率过大;数据出现异常 token;注意力分数溢出 | 检查学习率、检查输入序列是否包含未登录字符 | 调低学习率,检查 tokenizer 映射完整性 |
| train loss 下降但 val loss 上升 | 模型过拟合 | 对比两个 loss 的差距 | 增加数据量,增加 dropout,减小模型规模 |
| 生成的文本全是重复字符 | temperature 过高或过低;模型容量不足 | 尝试不同 temperature 值 | 将 temperature 设为 0.6-1.0,top_k 设为 10-50 |
| 输入中文报错“KeyError” | tokenizer 词表不包含该字符 | 打印 stoi 字典检查 | 重新统计语料字符,保证 encode 时使用同一份映射 |
| GPU 显存不足 | batch_size 或 block_size 过大,模型嵌入维度太高 | 查看 GPU 显存使用情况 | 减小 batch_size 或 block_size |
| 生成结果与 prompt 无关 | 模型未充分训练;上下文长度太短 | 观察训练 loss 是否还在下降 | 增加训练步数,或换用更大的语料 |
| 只有第一个 token 是 prompt,后面全部停止 | 模型输出的 token id 超出 decode 范围 | 检查词表映射 | 确认 itos 覆盖所有 token id |
8. 最佳实践与工程建议
8.1 数据质量和规模优先于模型技巧
从上面的代码可以看出,小模型也能学会语料中的统计规律,但生成效果好坏很大程度取决于语料质量。如果你想让模型生成更有意义的中文,建议使用更大、更干净、更多样化的语料,而不是急于调大模型。
8.2 训练过程要保留检查点
建议每训练若干步保存一次模型权重,而不是只在最后保存。训练中断时可以从最近的检查点继续,节省时间。检查点文件建议包含:模型参数、优化器状态、当前 step、超参数配置。
8.3 日志记录是调试的基础
实际工程中,训练循环里的print并不够用。建议使用tensorboard或wandb记录 loss、学习率、梯度范数等指标。梯度范数突然变大,往往意味着训练不稳定。
8.4 通过配置化控制超参数
不要把超参数写死在代码里。可以使用config.py或 YAML 配置文件管理:
# 文件路径:mini_llm/config.yaml model: embed_dim: 128 n_heads: 4 n_layers: 4 ff_dim: 256 block_size: 32 dropout: 0.1 train: batch_size: 64 learning_rate: 0.0003 max_steps: 5000 eval_interval: 200 save_path: output/mini_gpt.pth8.5 生成时要设置随机种子
调试过程中,如果每次生成结果都不稳定,很难判断改动是否有效。在训练和生成前设置随机种子:
import torch torch.manual_seed(42)8.6 从字符级 tokenizer 到子词 tokenizer
本文使用字符级 tokenizer,是为了教学直观。实际大模型项目中,字符级会导致序列过长,并且无法有效表示词义。进阶方向是学习 BPE、WordPiece 或 SentencePiece。理解本文思路后,建议尽快切换到中文 BPE tokenizer,这会明显提升模型效果。
8.7 设备选择与训练效率
CPU 上训练小模型完全可行,但如果你想尝试更大的模型,务必使用 GPU。注意设置:
pin_memory=True加速数据加载;torch.no_grad()包裹验证和生成过程;- 混合精度训练(AMP)可以显著减少显存占用。
8.8 警惕训练集和验证集泄漏
切分数据时,文本是按字符随机切分,不是按“句子”切分。这样可以避免验证集出现训练集中完全相同的片段,但掩盖了模型有效泛化能力的问题。更严谨的做法是按段落或文档切分,不过这会让数据量变少,本文示例是从教学角度做了简化。
9. 总结与下一步学习方向
这一圈走下来,你已经亲手完成了一个最小闭环:从语料准备、tokenizer 构建、Transformer 解码器实现,到训练、验证和文本生成。这个过程对理解 LLM 的价值,远大于简单调用现成模型。
接下来,可以从这几个方向继续深入:
第一,把 tokenizer 换成 BPE 或 SentencePiece,并用真实中文语料训练一个大一点的模型,观察生成效果的变化。
第二,研究位置编码的方案。本文用的是可学习绝对位置编码,现代模型还常用 RoPE、ALiBi 等相对位置编码,后者对长文本更好。
第三,理解指令微调和 RLHF。从预训练模型到 ChatGPT,中间还有指令微调和人类反馈对齐的过程。先掌握基础架构,再去理解这些高级技术会轻松很多。
第四,把模型部署到实际应用场景。训练一个模型不难,难的是如何平衡效果、速度和成本。浮点数精度、量化、模型蒸馏、推理加速,都是实际工程中必须要面对的问题。
最后提醒一句:如果你是第一次接触大模型,不要追求一步到位复现 GPT-3。建议先完整运行本文代码,把每个模块的输入输出打印出来,仔细看 attention 矩阵的形状变化。等你能看懂每一行代码在做什么,你对 LLM 的理解就已经超过绝大多数只会调 API 的同学了。