目录
AutoResearch项目学习
/    

AutoResearch项目学习

GeminiGeneratedImage7iqwq07iqwq07iqw.png

🎓 autoresearch 项目学习指南(零基础版)

这份指南面向完全没有深度学习/机器学习背景的读者。我会从最基础的概念讲起,用生活化的类比帮你理解代码背后的原理。


第一章:这个项目在做什么?

1.1 一句话总结

让 AI 自己当研究员,自动训练一个"小型 ChatGPT",不断实验、改进、迭代。

想象你有一个超级勤劳的实习生:

  • 你告诉他"试着让这个模型变得更好"
  • 他修改代码 → 训练 5 分钟 → 看结果 → 决定保留还是回滚
  • 你睡觉,他通宵干活
  • 第二天你醒来,他已经跑了 ~100 个实验 🎉

1.2 项目的三个文件

             ┌────────────────┐
  人类编写 → │  program.md    │ → "指挥手册",告诉 AI 怎么做实验
             └────────────────┘
             ┌────────────────┐
  AI 修改  → │  train.py      │ → "实验代码",包含模型和训练逻辑
             └────────────────┘
             ┌────────────────┐
  不可修改 → │  prepare.py    │ → "基础设施",数据准备和评估
             └────────────────┘

第二章:什么是 LLM?先搞懂基本概念

2.1 语言模型 = 超强"接龙"

LLM(Large Language Model)本质上就是一个超级强大的文字接龙机器:

输入: "今天天气真"
模型预测下一个字: "好" (概率 60%), "差" (概率 15%), "热" (概率 10%) ...

ChatGPT、Claude 等都是这样工作的 — 它们一个字一个字地"猜"下一个最合适的字,连起来就是你看到的完整回复。

2.2 "训练"是什么意思?

训练就像教小孩学说话:

  1. 给大量文本(类比:让小孩听大量对话)
  2. 遮住最后一个字,让模型猜(类比:你说"太阳从东边..."让小孩接"升起来")
  3. 猜错了就调整(类比:纠正错误,下次猜得更准)
  4. 重复几百万次

[!IMPORTANT]
这个项目训练的是一个很小的 GPT 模型(~50M 参数),而 ChatGPT 有几千亿参数。但原理完全一样!就像小孩和大人说同一种语言,只是词汇量不同。

2.3 关键指标:val_bpb

项目用 val_bpb(validation bits per byte)来衡量模型好坏:

  • 越低越好(就像高尔夫球,杆数越少越好)
  • 它衡量的是"模型有多擅长预测下一个字符"
  • 典型值大约在 0.9~1.1 之间

第三章:数据是怎么准备的?(prepare.py

3.1 总览

prepare.py 做两件事:

  1. 下载训练数据(互联网文本)
  2. 训练分词器(把文字变成数字)

3.2 什么是分词器(Tokenizer)?

计算机不懂文字,只懂数字。分词器就是翻译官:

"Hello world" → [1542, 897]     ← 编码(文字→数字)
[1542, 897]   → "Hello world"   ← 解码(数字→文字)

项目使用 BPE(Byte Pair Encoding) 分词器:

  • 词表大小:8192 个 token(每个 token 可以是一个字、一个词、或者词的一部分)
  • 类比:就像莫尔斯电码,把常用的词用短码表示,不常用的词拆成小片段
# prepare.py 第 30 行 - 关键常量
MAX_SEQ_LEN = 2048       # 模型一次能"看"多少个 token(上下文长度)
TIME_BUDGET = 300        # 训练时间预算:300秒 = 5分钟
VOCAB_SIZE = 8192        # 词表大小:8192 个 token

3.3 数据是什么?

# prepare.py 第 41 行
BASE_URL = "https://huggingface.co/datasets/karpathy/climbmix-400b-shuffle/resolve/main"

数据来自 Karpathy 整理的 climbmix-400b 数据集,是大量混合的互联网文本(网页、书籍、代码等),分成了 6543 个 shard(分片)。

3.4 DataLoader:喂数据的传送带

┌──────────────────────────────────────────────────────┐
│  DataLoader 工作流程                                  │
│                                                       │
│  原始文本 → 分词器编码 → 打包成批次 → 送入 GPU         │
│                                                       │
│  "Hello world"   →  [1542, 897]  → ┐                 │
│  "AI is great"   →  [203, 45, 78] → ├→ 一个批次(Batch)│
│  "Deep learning" →  [92, 1034]    → ┘                 │
└──────────────────────────────────────────────────────┘

关键概念:

  • Batch(批次):一次同时处理多个文本,利用 GPU 并行计算的能力
  • Sequence Length(序列长度):每段文本统一裁剪/填充到 2048 个 token
  • BOS(Begin of Sequence)token:每段文本开头加一个特殊标记,告诉模型"新文本开始了"
# prepare.py 第 276 行 - DataLoader 签名
def make_dataloader(tokenizer, B, T, split, buffer_size=1000):
    # B = batch size(批次大小)
    # T = sequence length(序列长度,即 MAX_SEQ_LEN = 2048)
    # split = "train" 或 "val"(训练集或验证集)

第四章:模型长什么样?(train.py 上半部分)

4.1 GPT 模型结构总览

                    ┌─────────────┐
  Token IDs    →    │  Embedding  │  → 把数字变成向量
  [1542, 897]       └──────┬──────┘
                           ↓
                    ┌──────────────┐
                    │  Block × 8   │  → 8 层 Transformer 堆叠
                    │  ┌────────┐  │
                    │  │Attention│  │  → "看看其他位置的信息"
                    │  └────────┘  │
                    │  ┌────────┐  │
                    │  │  MLP   │  │  → "思考、推理"
                    │  └────────┘  │
                    └──────┬──────┘
                           ↓
                    ┌──────────────┐
                    │   LM Head    │  → 输出每个 token 的概率
                    └──────────────┘
                           ↓
                 概率分布: [0.6, 0.15, 0.1, ...]
                 对应 token: ["好", "差", "热", ...]

4.2 Embedding(嵌入层)—— 把数字变成"含义"

# train.py 第 130 行
"wte": nn.Embedding(config.vocab_size, config.n_embd),

想象每个词是一个人,Embedding 就是给每个人一张身份证(一个向量/数组):

TokenIDEmbedding 向量(简化示例)
"猫"42[0.3, -0.1, 0.8, ...]
"狗"57[0.4, -0.2, 0.7, ...]
"飞机"103[-0.5, 0.9, 0.1, ...]

这里 n_embd = 768 表示每个词的向量长度为 768维(项目中默认是 DEPTH × ASPECT_RATIO = 8 × 64 = 512,再向上对齐到 128的倍数 = 512)

4.3 模型的尺寸由什么决定?

# train.py 第 433-451 行 - 超参数
ASPECT_RATIO = 64       # model_dim = depth × 64
HEAD_DIM = 128          # 每个注意力头的维度
DEPTH = 8               # Transformer 层数(最重要的旋钮!)
DEVICE_BATCH_SIZE = 128 # 每次送入 GPU 的样本数

想象一个管弦乐团:

  • DEPTH = 8:8 组演奏家(8 层 Transformer),每一组都从不同角度"处理"信息
  • n_embd:每个音乐家的技能维度(向量维度)
  • n_head:每组里的演奏家数量(注意力头数)

4.4 Attention(注意力机制)—— 让每个词"看看"其他词

这是 Transformer 最核心的创新!

# train.py 第 61-96 行 - CausalSelfAttention
class CausalSelfAttention(nn.Module):
    def __init__(self, config, layer_idx):
        self.c_q = nn.Linear(...)  # Query  = "我在找什么?"
        self.c_k = nn.Linear(...)  # Key    = "我有什么?"
        self.c_v = nn.Linear(...)  # Value  = "如果你找的是我,这是我的信息"
        self.c_proj = nn.Linear(...)  # 输出投影

直觉类比 —— 图书馆查找:

你走进图书馆想找"如何做蛋糕"的信息:

Query (Q)  = 你的问题:"蛋糕怎么做?"
Key (K)    = 每本书封面的关键词:"烘焙指南"、"量子物理"、"法式甜点"...
Value (V)  = 每本书的内容

注意力分数 = Q 和每个 K 的匹配度:
  "烘焙指南" → 高匹配 → 多看这本书的内容
  "量子物理" → 低匹配 → 几乎忽略
  "法式甜点" → 中等匹配 → 看一点

最终输出 = 根据匹配度加权混合各本书的 Value 内容

"Causal"(因果) 的含义:每个位置只能看它前面的词,不能偷看后面的。因为生成时你是从左到右的,右边还没生成呢!

"今  天  天  气  真  好"
 ↑
 这个位置只能看到 "今"

      ↑
      这个位置能看到 "今 天"

                  ↑
                  这个位置能看到 "今 天 天 气 真"

Sliding Window(滑动窗口注意力)

# train.py 第 40 行
window_pattern: str = "SSSL"  # S=Short(半长), L=Long(全长)

不是每一层都需要看完所有前文。有些层只看最近的一半内容(S),有些看全部(L)。这样能节省计算量。

RoPE(旋转位置编码)

# train.py 第 52-58 行
def apply_rotary_emb(x, cos, sin):
    ...

模型怎么知道"第1个词"和"第100个词"的位置差异?RoPE 通过正弦/余弦函数旋转向量来编码位置信息。类比:就像在一条线上给每个座位编号一样。

Value Embedding(值嵌入)—— 项目的独特技巧

# train.py 第 83-87 行
# Value residual (ResFormer): mix in value embedding with input-dependent gate per head
if ve is not None:
    ve = ve.view(B, T, self.n_kv_head, self.head_dim)
    gate = 2 * torch.sigmoid(self.ve_gate(x[..., :self.ve_gate_channels]))
    v = v + gate.unsqueeze(-1) * ve

这是一个比较新的技巧:除了正常的 Value(来自上一层的计算结果),还额外查一张"备忘录"(直接从词嵌入得到的 Value),两者混合。这有助于信息在深层网络中更好地流动(避免"传话游戏"中信息丢失)。

4.5 MLP(多层感知器)—— "思考"层

# train.py 第 99-109 行
class MLP(nn.Module):
    def __init__(self, config):
        self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=False)   # 扩展到 4 倍宽
        self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=False) # 压缩回去

    def forward(self, x):
        x = self.c_fc(x)
        x = F.relu(x).square()  # 激活函数:ReGLU 的变体(ReLU²)
        x = self.c_proj(x)
        return x

类比:Attention 是"收集信息",MLP 是"消化思考"。

  • 先把信息展开(768 → 3072 维),就像把一个话题展开详细讨论
  • 通过 relu().square() 进行非线性处理(类比:人脑中的"啊哈!"顿悟时刻,不是简单的线性运算)
  • 压缩回来(3072 → 768 维),提炼出核心结论

4.6 Block(Transformer 块)—— 一层完整处理

# train.py 第 112-121 行
class Block(nn.Module):
    def forward(self, x, ve, cos_sin, window_size):
        x = x + self.attn(norm(x), ve, cos_sin, window_size)  # 残差连接
        x = x + self.mlp(norm(x))                               # 残差连接
        return x

注意 x = x + ...,这叫残差连接(Residual Connection)

原始信号 ─────────────────────── + ──→ 输出
              │                   ↑
              └→ [Attention] ─────┘
          
类比:你在背单词,每次不是从零开始,而是"在已知的基础上补充新信息"

norm(x)RMS Norm(归一化),确保数值不会太大或太小,让训练更稳定。

4.7 前向传播:数据流过模型的完整路径

# train.py 第 268-291 行 - GPT.forward()
def forward(self, idx, targets=None, reduction='mean'):
    # idx = 输入的 token ID [B, T],B=批次大小,T=序列长度

    x = self.transformer.wte(idx)  # ① 词嵌入:[B,T] → [B,T,768]
    x = norm(x)
    x0 = x  # 保存初始嵌入(后面要用)

    for i, block in enumerate(self.transformer.h):
        # ② 残差短路:每一层混合"当前信息"和"初始信息"
        x = self.resid_lambdas[i] * x + self.x0_lambdas[i] * x0
        # ③ 值嵌入(隔一层有一次)
        ve = self.value_embeds[str(i)](idx) if str(i) in self.value_embeds else None
        # ④ 通过 Transformer Block(Attention + MLP)
        x = block(x, ve, cos_sin, self.window_sizes[i])

    x = norm(x)  # ⑤ 最终归一化

    # ⑥ 输出层(预测下一个 token 的概率分布)
    logits = self.lm_head(x)          # [B,T,768] → [B,T,8192]
    logits = logits.float()
    logits = 15 * torch.tanh(logits / 15)  # Softcap:防止极端值

    # ⑦ 如果有标准答案,计算损失
    if targets is not None:
        loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
        return loss
    return logits

一图胜千言:

Token IDs    Embedding     8 × Block           LM Head        概率
[42, 57] → [0.3, -0.1..] → [Attn+MLP]×8 → [logits] → softmax → [0.6, 0.1, ...]
                                                                    ↓
                                                            "好" (最可能)

第五章:优化器 —— 模型怎么"学习"的?(train.py 中间部分)

5.1 学习的原理:梯度下降

① 模型预测下一个词 → 预测 "猫" (概率 30%)
② 正确答案其实是 "狗"
③ 计算 loss(损失)= 预测和真实答案的差距
④ 计算梯度 = loss 对每个参数的偏导数("往哪个方向调才能减小误差")
⑤ 更新参数 = 参数 - 学习率 × 梯度

重复几十万次... 模型就越来越准!

类比:你闭着眼睛走下山。

  • 损失(loss) = 你的海拔(越低越好)
  • 梯度 = 你脚下的坡度方向
  • 学习率 = 每一步迈多大
  • 目标 = 找到最低的山谷

5.2 这个项目的优化器:MuonAdamW

这个项目用了两种优化器组合使用,非常前沿:

对象优化器类比
嵌入层、输出层等(1D参数)AdamW经典的、稳健的老司机
Transformer 中间的矩阵参数(2D参数)Muon新一代的、更高效的赛车手

AdamW(简版理解)

# train.py 第 306-314 行
# 不只是跟着梯度走,而是:
# 1. 记住历史方向(momentum,惯性)—— 避免来回震荡
# 2. 根据每个参数的历史变化幅度自适应调整步长
# 3. 权重衰减(weight decay)—— 防止参数变得太大

Muon(项目的亮点之一)

Muon 是一种非常新的优化器,专门用于 2D 矩阵参数。核心思想是对梯度做正交化(Polar Express),这能更高效地利用梯度信息。

[!NOTE]
作为初学者,你只需要知道:Muon 比 AdamW 在矩阵参数上训练更快、更高效。它是 2024-2025 年才出现的前沿技术。

5.3 学习率调度

# train.py 第 518-525 行
WARMUP_RATIO = 0.0     # 前 0% 时间:学习率从 0 升到最大(这里没有 warmup)
WARMDOWN_RATIO = 0.5   # 后 50% 时间:学习率从最大降到 0
学习率
  ↑
1.0 ├─────────────────╲
    │                   ╲
    │                     ╲
0.0 └────────────────────────→ 训练进度
    0%                50%  100%
  
    ←── 正常训练 ──→←─降温阶段─→

为什么?前期大步探索,后期小步微调。就像考试前复习:先快速过一遍内容,最后精细查漏补缺。


第六章:训练循环 —— 整个流程串起来(train.py 下半部分)

6.1 训练循环的伪代码

# train.py 第 543-604 行 - 简化理解版
while 训练时间 < 5分钟:
    # ① 梯度累积(大批量 = 多个小批量的梯度相加)
    for 每个小批次 in 梯度累积步数:
        loss = 模型(输入, 标准答案)      # 前向传播
        loss.backward()                   # 反向传播(计算梯度)
        加载下一个小批次数据
  
    # ② 调整学习率
    根据训练进度调整学习率
  
    # ③ 更新参数
    optimizer.step()        # 用梯度更新模型参数
    model.zero_grad()       # 清空梯度,准备下一轮
  
    # ④ 监控
    打印(当前步数, 损失, 速度, 剩余时间)

# ⑤ 训练结束 → 在验证集上评估
val_bpb = evaluate_bpb(model, tokenizer, batch_size)
print(f"val_bpb: {val_bpb}")

6.2 梯度累积 —— 用小显存实现大批量训练

# train.py 第 438 行
TOTAL_BATCH_SIZE = 2**19  # ~524K 个 token = 目标批量大小
DEVICE_BATCH_SIZE = 128   # GPU 一次只能放 128 个样本

# 自动计算:524288 / (128 × 2048) = 2 次梯度累积
grad_accum_steps = TOTAL_BATCH_SIZE // (DEVICE_BATCH_SIZE * MAX_SEQ_LEN)

类比:你想搬 100 箱货物,但车只能装 50 箱。那就跑两趟,效果一样!

6.3 训练中的关键输出

step 00953 (100.0%) | loss: 3.421 | lrm: 0.00 | dt: 287ms | tok/sec: 1,826,484 | mfu: 39.8% | remaining: 0s
字段含义
step第几步
loss当前训练损失(越低越好)
lrm学习率乘数(0~1)
dt每步耗时
tok/sec每秒处理多少 token
mfu模型 FLOP 利用率(GPU 利用效率,39.8% 算不错)
remaining剩余训练时间

第七章:评估 —— 模型到底行不行?

7.1 bits per byte (BPB)

# prepare.py 第 344-365 行
def evaluate_bpb(model, tokenizer, batch_size):
    ...
    return total_nats / (math.log(2) * total_bytes)

BPB 的直觉:

模型平均需要多少 "比特" 来编码文本中的每个字节

  • 理论最低值 ≈ 0(模型完美预测每个字符)
  • 随机猜 ≈ 8.0(每个字节 8 比特,完全不压缩)
  • 好的小模型 ≈ 0.9~1.1

为什么不直接用 loss?因为不同的词表大小会影响 loss 值,但 BPB 是 词表无关的,这样即使 AI 改变了词表大小,结果仍然可比。


第八章:自动实验循环 —— 一切串起来

8.1 人类 vs AI 的分工

┌────────────────────────────────────────────────────────┐
│                     人类的工作                          │
│                                                         │
│  1. 编写 program.md(指挥手册)                          │
│  2. 启动 AI 助手 + 提示它读 program.md                   │
│  3. 去睡觉 😴                                           │
│  4. 第二天看 results.tsv 里的实验结果                     │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│                     AI 的工作                           │
│                                                         │
│  LOOP:                                                  │
│    1. 想一个改进 idea                                    │
│    2. 修改 train.py                                     │
│    3. git commit                                        │
│    4. uv run train.py > run.log 2>&1  (训练 5 分钟)    │
│    5. 检查 val_bpb                                      │
│    6. 比之前好?→ keep(保留)                            │
│       比之前差?→ git reset(回滚)                       │
│    7. 记录到 results.tsv                                 │
│    8. 回到步骤 1                                         │
└────────────────────────────────────────────────────────┘

8.2 AI 可能会做什么实验?

实验类型具体例子
调超参数调大/调小学习率、改批量大小
改架构加深/减浅模型层数(DEPTH)、改注意力头数
改激活函数ReLU² → GELU, SiLU 等
改优化器设置调 momentum、weight decay
改窗口模式"SSSL" → "SL" → "L"
加新技巧新的归一化方法、不同的初始化策略

第九章:概念速查表

概念一句话解释类比
Token文本的最小单位拼图的一小块
Embedding把 token 变成数字向量给每个词发一张多维身份证
Attention让每个位置看其他位置的信息图书馆里查找相关的书
MLP对收集到的信息做非线性变换大脑的"思考"过程
Residual跳跃连接,保留原始信息背单词时在已知基础上补充
Norm归一化,让数值保持稳定音量调节器
Loss预测和真实答案的差距考试扣了多少分
Gradientloss 对参数的偏导数下山时脚下的坡度方向
Learning Rate每次更新的步长下山时每步迈多大
Batch Size一次处理多少样本一次改几张试卷
Epoch遍历完整个数据集一次课本从头到尾看一遍
val_bpb验证集上的 bits per byte最终考试成绩(越低越好)
Softcap限制 logits 最大值给分数封顶,防止极端预测
RoPE旋转位置编码给每个座位编号
Flash Attention高效注意力计算的 GPU 实现高速公路 vs 乡间小路

第十章:如果你想进一步学习

推荐路径

Level 1(你现在的位置)
  ├─ ✅ 理解项目全貌
  ├─ 📖 3Blue1Brown 的《神经网络》系列视频
  └─ 📖 Andrej Karpathy 的 "Neural Networks: Zero to Hero" YouTube 系列

Level 2
  ├─ 📖 读 "Attention Is All You Need" 论文(Transformer 原始论文)
  ├─ 🔧 尝试修改 train.py 中的超参数,看看效果
  └─ 🔧 亲手运行一次训练(如果有 GPU 的话)

Level 3
  ├─ 📖 学习完整的 nanochat 仓库(本项目的简化版)
  ├─ 🔧 尝试自己写一个 program.md 来引导 AI 实验
  └─ 📖 阅读 Muon 优化器的论文

[!TIP]
即使没有 GPU,阅读代码和理解原理本身就是极好的学习。这个项目代码极简(只有两个 Python 文件),是学习 GPT 训练的绝佳素材。

项目地址:https://github.com/karpathy/autoresearch


标题:AutoResearch项目学习
作者:gitsilence
地址:https://blog.lacknb.cn/articles/2026/04/08/1775629958385.html