Skip to content
Published on

打造属于自己的 GPT — 用 nanoGPT 从零开始训练语言模型

分享
Authors
Build Your Own GPT

引言

ChatGPT、Claude、Gemini —— 我们每天使用的 AI,其核心都是 GPT(Generative Pre-trained Transformer) 架构。但你亲手做过一个吗?

在这个系列里,我们会以 Andrej Karpathy 的 nanoGPT 为基础,用家里的 GPU 把一个语言模型从零训练出来。它是大模型的缩小版,但原理和 GPT-4 完全相同。

为什么要自己动手做?

  • 只读论文只能理解 30%,亲自写代码则能理解 90%
  • 我们有 GPU 服务器,可以真正拿来训练(GB10 128GB!)
  • 简历上写"从零训练过 LLM 的经验" —— 一个差异化亮点
  • 读 AI 论文时,会产生"啊,就是这里!"这样的直觉

GPT 架构核心

GPT 是纯解码器(Decoder-only)Transformer。核心可以归纳为 3 点:

1. 分词(Tokenization)

这是把文本转换成数字的第一步。

# Character-level 分词器(最简单)
text = "hello world"
chars = sorted(list(set(text)))
# chars = [' ', 'd', 'e', 'h', 'l', 'o', 'r', 'w']

stoi = {ch: i for i, ch in enumerate(chars)}  # char → int
itos = {i: ch for i, ch in enumerate(chars)}  # int → char

encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

print(encode("hello"))  # [3, 2, 4, 4, 5]
print(decode([3, 2, 4, 4, 5]))  # "hello"

实际的 GPT 使用 BPE(Byte-Pair Encoding) 分词器:

import tiktoken
enc = tiktoken.get_encoding("gpt2")
tokens = enc.encode("让我们自己搭建一个 GPT!")
print(tokens)  # [171, 120, 230, 168, 245, ...]
print(len(tokens))  # ~15 tokens

2. Self-Attention(核心中的核心)

学习"每个 token 应该在多大程度上关注其他所有 token"。

import torch
import torch.nn as nn
import torch.nn.functional as F

class SelfAttention(nn.Module):
    def __init__(self, embed_dim, head_dim):
        super().__init__()
        self.query = nn.Linear(embed_dim, head_dim, bias=False)
        self.key = nn.Linear(embed_dim, head_dim, bias=False)
        self.value = nn.Linear(embed_dim, head_dim, bias=False)

    def forward(self, x):
        B, T, C = x.shape
        q = self.query(x)  # (B, T, head_dim)
        k = self.key(x)    # (B, T, head_dim)
        v = self.value(x)  # (B, T, head_dim)

        # Attention scores
        weights = q @ k.transpose(-2, -1)  # (B, T, T)
        weights = weights * (C ** -0.5)     # Scale

        # Causal mask — 未来的 token 不可见!
        mask = torch.tril(torch.ones(T, T))
        weights = weights.masked_fill(mask == 0, float('-inf'))

        weights = F.softmax(weights, dim=-1)
        out = weights @ v  # (B, T, head_dim)
        return out

核心直觉:在预测 "The cat sat on the ___" 中的空白处时,会给 "cat" 和 "sat" 分配较高的 attention weight。

3. Transformer Block

class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, num_heads):
        super().__init__()
        head_dim = embed_dim // num_heads
        self.heads = nn.ModuleList([
            SelfAttention(embed_dim, head_dim)
            for _ in range(num_heads)
        ])
        self.proj = nn.Linear(embed_dim, embed_dim)
        self.ffn = nn.Sequential(
            nn.Linear(embed_dim, 4 * embed_dim),
            nn.GELU(),
            nn.Linear(4 * embed_dim, embed_dim),
        )
        self.ln1 = nn.LayerNorm(embed_dim)
        self.ln2 = nn.LayerNorm(embed_dim)

    def forward(self, x):
        # Multi-Head Attention + Residual
        attn_out = torch.cat([h(self.ln1(x)) for h in self.heads], dim=-1)
        x = x + self.proj(attn_out)
        # Feed-Forward + Residual
        x = x + self.ffn(self.ln2(x))
        return x

完整的 GPT 模型

class MicroGPT(nn.Module):
    def __init__(self, vocab_size, embed_dim=384, num_heads=6,
                 num_layers=6, block_size=256):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, embed_dim)
        self.pos_emb = nn.Embedding(block_size, embed_dim)
        self.blocks = nn.Sequential(*[
            TransformerBlock(embed_dim, num_heads)
            for _ in range(num_layers)
        ])
        self.ln_f = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, vocab_size)

    def forward(self, idx, targets=None):
        B, T = idx.shape
        tok_emb = self.token_emb(idx)          # (B, T, embed_dim)
        pos_emb = self.pos_emb(torch.arange(T)) # (T, embed_dim)
        x = tok_emb + pos_emb

        x = self.blocks(x)
        x = self.ln_f(x)
        logits = self.head(x)  # (B, T, vocab_size)

        loss = None
        if targets is not None:
            loss = F.cross_entropy(
                logits.view(-1, logits.size(-1)),
                targets.view(-1)
            )
        return logits, loss

    def generate(self, idx, max_new_tokens):
        for _ in range(max_new_tokens):
            logits, _ = self(idx[:, -256:])  # block_size 限制
            probs = F.softmax(logits[:, -1, :], dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            idx = torch.cat([idx, next_token], dim=1)
        return idx

模型大小对比:

模型参数量层数训练时间(1 GPU)
我们的 MicroGPT10M6约 30 分钟
GPT-2 Small124M12约数天
GPT-3175B96数千 GPU-天
GPT-4~1.8T(估计)??

实践:用 Shakespeare 训练

数据准备

# 在 spark01(GB10 128GB)上运行
cd ~/nanoGPT
python3 data/shakespeare_char/prepare.py
# → train: 1,003,854 tokens / val: 111,540 tokens

开始训练

python3 train.py config/train_shakespeare_char.py \
  --device=cuda \
  --max_iters=5000 \
  --eval_interval=500 \
  --log_interval=100

生成结果(5000 次迭代后)

ROMEO:
What say you to this? Let me not stay a whit;
And yet I feel the thing I have forgot
To take upon the honour of my word.

从零生成了莎士比亚风格的文本!

下一步:韩语 GPT

  1. 训练韩语分词器(SentencePiece BPE)
  2. 收集 Namuwiki / 新闻数据
  3. 训练 MicroGPT 500M(spark01 128GB)
  4. 通过 LoRA 微调 打造对话模型

系列路线图

主题状态
第 1 期用 nanoGPT 理解 GPT(本文)
第 2 期制作韩语分词器🔜
第 3 期训练 500M 韩语 GPT🔜
第 4 期用 RLHF 打造对话模型🔜
第 5 期从零开始的图像生成模型(DDPM)🔜

📝 小测验 — 打造属于自己的 GPT(点击查看!)

Q1. GPT 是 Encoder 还是 Decoder 结构? ||Decoder-only Transformer||

Q2. Self-Attention 中 Causal Mask 的作用是什么? ||阻止看到未来的 token —— 为了自回归生成,用下三角矩阵进行掩码||

Q3. 计算 Attention score 时为什么要除以 sqrt(d_k)? ||内积值变大会让 softmax 变得极端,导致 gradient vanishing —— 通过缩放来稳定||

Q4. BPE 分词器和 Character-level 分词器的优缺点是什么? ||BPE:词表效率高(序列更短),但实现复杂。Character-level:简单,但序列变长,长距离依赖更难学习||

Q5. Residual Connection(残差连接)在 Transformer 中为什么重要? ||为了防止深层网络中 gradient 消失,把原始输入加回去 —— 提升训练稳定性和收敛速度||

Q6. Feed-Forward Network 为什么要先扩展 4 倍再压缩回去? ||为非线性变换获取更强的表达能力 —— 在更宽的空间中提取特征后,再压缩回原始维度||

Q7. 我们的 MicroGPT 参数量大约是多少,和 GPT-2 Small 相差几倍? ||约 10M,与 GPT-2 Small(124M)相差约 12 倍||

Q8. 仅通过 next token prediction 训练,为什么就能理解语言? ||要准确预测下一个 token,就必须内在地学会语法、语义、上下文乃至世界知识 —— 压缩即是理解||

📚 参考资料 & GitHub 参考

原始代码

原始论文

视频讲座

📖 相关系列 & 推荐文章