MiniMind 项目让你体验亲手训练一个能聊天、能算数、能调用工具的 LLM 的乐趣。文章详细拆解了 MiniMind 项目的每一块积木,包括数据准备、模型架构、训练流程、强化学习以及评估实战效果。通过 MiniMind 项目,开发者可以深入了解大模型训练的各个环节,即使是小白也能轻松上手,体验大模型的魅力。

三块钱、两小时、一张消费级显卡,你能训练出一个能聊天、能算数、能调用工具的 LLM。这不是科幻。本文带你拆解 MiniMind 项目的每一块积木。


项目信息

  • GitHub:jingyaogong/minimind(18k+ Stars)
  • 模型参数:64M(GPT-3 的 1/2700)
  • 训练成本:~3 元
  • 训练时间:~2 小时

前言

用乐高自己拼出一架飞机,远比坐在头等舱里飞行更让人兴奋。

MiniMind 项目让"人人都能训一个 LLM"这件事变成了每个开发者都能动手的教程。从零手写了完整的训练代码,主线模型只有64M 参数——相当于 GPT-3 的 1/2700——却覆盖了从预训练到强化学习的完整 LLM 训练流程。项目所有核心算法代码均从 0 使用 PyTorch 原生实现,不依赖 transformers、trl、peft 等第三方框架的高层抽象接口。


一、数据:LLM 的燃料


训练一个模型,第一个问题是"喂什么"。MiniMind 的数据体系分为四个层次:词表训练数据、预训练数据、指令微调数据和强化学习数据。每一层的目标和格式都不一样。

图片

图1:MiniMind 主线训练数据组成与推荐组合示意图——从预训练到 RL 的全套数据体系

1.1 词表(Tokenizer)——语言的最小单位

大模型看不懂"你好",它只看得懂数字。Tokenizer 的作用就是把文本变成一串数字(token id)。MiniMind 选择了一个非常小的词表:6400个 token。相比之下,主流开源模型的词表要大得多:

Tokenizer 模型词表大小来源
Yi64,00001万物(中国)
Qwen2151,643阿里云(中国)
ChatGLM151,329智谱AI(中国)
Mistral32,000Mistral AI(法国)
Llama 3128,000Meta(美国)
MiniMind6,400自定义 BPE

为什么做这么小?因为模型本身只有 64M 参数——如果词表太大,Embedding 层就会占掉大半个模型的参数预算。6400 的词表 + 768 维,Embedding 层只占约 5M 参数,比例合理。如果换成 128K 词表,光是 Embedding 就要 128K × 768 ≈ 98M 参数——比整个模型还大。

MiniMind 使用自训练的BPE + ByteLevel分词器(基于 trainer/train_tokenizer.py),而非直接复用 Llama/Qwen 的分词器。这保证词表彻底针对中文优化,但也意味着它与其他模型的 tokenizer 不兼容。

关键决策: 6400 词表的中文编解码效率弱于 Qwen2 等偏中文友好的 tokenizer,但能显著压缩 embedding 层和输出层的参数占比。从实际使用效果看,没有明显带来生僻词解码失败的问题。

BPE(Byte Pair Encoding)的核心思想很简单:从单个字符开始,反复合并出现频率最高的相邻字节对,直到达到目标词表大小。

// 假设语料中只有 4 个单词,我们想训练一个很小的词表语料:”低  low  lowest  newer” // Step 1:初始词表 = 所有独立字符 + 空格词表:[”低”, ”l”, ”o”, ”w”, ”e”, ”r”, ”s”, ”t”, ” ”](9 个 token) // Step 2:统计所有相邻字节对的频率”l”+”o” → ”low” 中出现 1 次,”lowest” 中出现 1 次 = 2 次(频率最高)”o”+”w” → ”low” 中出现 1 次,”lowest” 中出现 1 次 = 2 次”w”+”e” → ”lowest” 中出现 1 次,”newer” 中出现 1 次 = 2 次 // Step 3:找到频率最高的对”lo”,合并为新 token词表:[”低”, ”l”, ”o”, ”w”, ”e”, ”r”, ”s”, ”t”, ” ”, ”lo”] // Step 4:重复合并过程(假设 50 次合并后)词表:[”低”, ”l”, ”o”, ”w”, ”e”, ”r”, ”s”, ”t”, ” ”,        ”lo”, ”ow”, ”low”, ”we”, ”er”, ”ne”, ”new”, ”lowest”, ...] // 编码时:贪心最长匹配”lowest” → tokenize(”lowest”) = [”low”, ”est”]  ← 优先用长 token
// MiniMind tokenizer 对 4 个样本的压缩率测试样本 1(中文): 字符数 76 → Tokens 46  → 压缩率 1.65 字符/token样本 2(中文): 字符数 212 → Tokens 134 → 压缩率 1.58 字符/token样本 3(英文): 字符数 964 → Tokens 227 → 压缩率 4.25 字符/token样本 4(混合): 字符数 397 → Tokens 154 → 压缩率 2.58 字符/token // 对比:Qwen2 的 151K 词表在中文上压缩率约 1.8~2.0 字符/token// MiniMind 的 6400 词表在中文上约 1.5~1.7——差距不大// 但词表大小差了 23 倍,embedding 层参数少了 23 倍

理解这个压缩率很实用:当你说训练时 max_seq_len=340,实际的文本长度大约是 340 × 1.6 ≈ 544 个中文字符。而 max_seq_len=768 则对应约 1228 个中文字符。

MiniMind 的词表里有 36 个特殊 token,定义了模型的所有行为边界:

// ① 对话控制符(3个)<|im_start|>  <|im_end|>  <|endoftext|> // ② 工具调用标记(4个)  // ③ 思考标记(2个)——类似 DeepSeek-R1 的思维链  // ④ 视觉/多模态预留(8个)<|vision_start|>  <|vision_end|>  <|image_pad|>  <|video_pad|> ... // ⑤ 未来功能预留(14个 buffer token)<|buffer1|> ~ <|buffer14|>

这里有一个设计细节:为什么预留 buffer token?因为词表一旦训练好就没有回头路了——如果后续想加新的特殊 token,就得重新训练整个 tokenizer,这意味着模型权重也得重练。预留 buffer token 就是为了给未来的功能扩展留下余地,这是很多项目容易忽略的"接口预留"设计。

1.2 预训练数据——模型的"通识教育"

预训练数据的格式是最简单的:一段段纯文本,没有对话标记,没有特殊格式。模型的任务就是从这些文本中学会语言规律、知识和"常识"。

版本文件大小说明
完整版pretrain_t2t.jsonl10GB完整训练 minimind-3(推荐 max_seq_len≈380)
轻量版pretrain_t2t_mini.jsonl1.2GB快速复现(推荐 max_seq_len≈768)
// 一条预训练数据——纯文本,模型只需要做词语接龙{”text”: ”如何才能摆脱拖延症?治愈拖延症并不容易,但以下建议可能有所帮助。”}{”text”: ”清晨的阳光透过窗帘洒进房间,桌上的书页被风轻轻翻动。”}{”text”: ”Transformer 通过自注意力机制建模上下文关系,是现代大语言模型的重要基础结构。”}

1.3 SFT 数据——教模型"怎么说话"

预训练让模型知道"苹果是一种水果",但不会让它知道"用户问’苹果是什么’时,你应该回答’苹果是一种水果’"。SFT 就是干这个的。

版本文件大小说明
完整版sft_t2t.jsonl14GB含 Tool Call + Reasoning 数据
轻量版sft_t2t_mini.jsonl1.6GB快速复现,已混入 Tool Call 样本

SFT 数据采用多轮对话格式,包含了 Tool Calling 数据和 Reasoning 数据:

// 标准对话格式{  ”conversations”: [    {”role”: ”user”, ”content”: ”你好”},    {”role”: ”assistant”, ”content”: ”你好!我是 MiniMind,有什么可以帮助你的?”}  ]} // Tool Call 数据格式{  ”conversations”: [    {”role”: ”system”, ”content”: ” # Tools ...”,     ”tools”: ”[{/”name/”:/”calculate_math/”,/”description/”:/”.../”}]”},    {”role”: ”user”, ”content”: ”帮我算一下 256 乘以 37 等于多少”},    {”role”: ”assistant”, ”content”: ””,     ”tool_calls”: ”[{/”name/”:/”calculate_math/”,/”arguments/”:{/”expression/”:/”256 * 37/”}}]”},    {”role”: ”tool”, ”content”: ”{/”result/”:/”9472/”}”},    {”role”: ”assistant”, ”content”: ”256 乘以 37 等于 9472。”}  ]}

1.4 强化学习数据

数据集大小用途
dpo.jsonl53MBDPO 训练,chosen/rejected 偏好对
rlaif.jsonl24MBRLAIF 训练,PPO/GRPO/CISPO
agent_rl.jsonl86MBAgentic RL,多轮 Tool-Use
agent_rl_math.jsonl18MB数学 Tool-Use(带最终校验目标)
// DPO 数据格式:chosen/rejected 偏好对{  ”chosen”: [    {”content”: ”Q”, ”role”: ”user”},    {”content”: ”good answer”, ”role”: ”assistant”}  ],  ”rejected”: [    {”content”: ”Q”, ”role”: ”user”},    {”content”: ”bad answer”, ”role”: ”assistant”}  ]}

二、模型架构:积木怎么搭


MiniMind 的架构对齐了 Qwen3 的生态——标准的 Transformer Decoder-Only,加入了一系列现代 LLM 标配的改进。

2.1 Dense 架构(MiniMind-3)

图片

图2:MiniMind-3 的整体网络结构——标准的 Decoder-Only 架构

参数说明
参数量64MGPT-3 的 1/2700
词表大小6400自定义 BPE 分词器
最大序列长度32768支持长上下文
RoPE theta1e6旋转位置编码 base
层数 (n_layers)8模型深度
隐藏维数 (d_model)768每个 token 的表示维度
KV heads4GQA 的 key-value 头数
Q heads8Query 头数

Pre-Norm + RMSNorm:在每一层的输入做归一化,而不是输出。RMSNorm 在 LayerNorm 基础上去掉均值计算,更快。对于小模型而言,这种选择能有效缓解训练初期梯度不稳定问题。

SwiGLU 激活函数:GLU 变体 + Swish 激活。相比传统 ReLU,SwiGLU 引入了门控机制,让网络可以学习"哪些信息通过、哪些阻断"。

GQA(Grouped Query Attention):8 个 Q 头共享 4 个 KV 头。相比 MHA 少了一半的 KV 投影参数,推理时 KV Cache 大小也减半。

RoPE 的核心思想非常优雅:不把位置信息"加"到 token 的 embedding 上,而是通过旋转矩阵来"旋转"Q 和 K 向量。

直观理解:假设每个 token 的 Q 和 K 向量是 4 维的。位置 0 的向量不做旋转,位置 1 旋转 10°,位置 2 旋转 20°……当位置 5 的 Q(旋转了 50°)去"注意"位置 3 的 K(旋转了 30°)时,角度差是 20°,而这个角度差恰好编码了距离差 2。

数学上:

Q_m = R(m) · Q,   K_n = R(n) · KAttention(Q_m, K_n) = Q^T · R(n-m) · K// 两个位置 m 和 n 的注意力只取决于相对位置 n-m

为什么这样好?

  • 天然支持相对位置——注意力只关心"距离差"。比如"我在北京吃烤鸭"和"烤鸭在北京我吃",吃→烤鸭的依赖关系在位置编码中是一致的
  • 可以衰减远端——距离太远的 token 旋转角度差太大,内积自然变小
  • 计算零成本——旋转是固定的编码操作,不增加可学习参数

MiniMind 训练时的最大序列长度是 32768(32K),但通过 YaRN 算法,可以在推理时处理远超训练时长度的文本。

核心问题:RoPE 的旋转频率分"高频"和"低频"。

  • 高频分量(快速旋转的维度)对应短程依赖——两个 token 只差 1 步时,高频分量能区分它们
  • 低频分量(慢速旋转的维度)对应长程依赖——两个 token 差 1000 步时,只有低频分量还能敏感地区分它们

当训练时最长 32K,但推理时要求 64K 时,低频分量的旋转角度翻倍了,模型看到从未见过的角度组合,导致注意力计算混乱。

YaRN 的解法:

// Step 1: 高频分量不动(短程依赖最需要精度)// Step 2: 低频分量做”温度缩放”——降低旋转频率,适应更长的范围 // 打个比方:// 训练时:低频分量 1 圈走 10000 步(覆盖 32K 范围)// 推到 64K 时:把低频分量变成 1 圈走 20000 步// 这样同样走 64K 步,角度变化和训练时 32K 差不多

MiniMind 实测表现:32K→48K 流畅;32K→64K 勉强可用;超过 64K 后质量明显下降。

2.2 “宽而浅"还是"深而窄”?

在决定 64M 参数的分配时,作者面临一个经典问题:是让模型"矮胖"(dim 更大、层数更少)还是"瘦高"(dim 小、层数多)?

MobileLLM 论文的核心观察是:在参数量固定时,深度往往比宽度更重要。30~42 层的狭长结构通常会优于 12 层的矮胖结构。但"窄"也有下限——当 d_model < 512 时,词嵌入维度过窄带来的劣势会明显放大。

综合考虑后,MiniMind-3 选择了dim=768, layers=8的折中方案。768 的维度保证了每个 token 的表达能力够丰富,8 层的深度也足以学习到层级化的语言模式。

2.3 MoE 架构(MiniMind-3-MoE)

图片

图3:MiniMind MoE 结构——4 experts / top-1 routing

MoE 和 Dense 版本的基本参数相同,差异在于用 MoE 前馈层替换了标准 FFN:

指标DenseMoE
总参数量64M198M(看起来更大)
实际激活64M64M(和 Dense 一样)

|
如果所有 token 都涌向同一个专家,其他专家就白训了。

// 极端情况:一批处理 4 个 tokenToken ”我” → 专家 0(得分 0.61)Token ”爱” → 专家 0(得分 0.55)Token ”中” → 专家 0(得分 0.72)Token ”国” → 专家 0(得分 0.48)// 4 个 token 全部去了专家 0,负载极度不均衡!

MiniMind 的解法:Auxiliary Loss(辅助负载均衡损失)

核心思想:让每个专家被选中的概率 ≈ 1/专家数。

# 计算每个专家被选中的实际频率load = F.one_hot(topk_idx, num_experts).float().mean(0)# 如上例:load = [1.0, 0.0, 0.0, 0.0] # 计算每个专家的平均路由得分scores_mean = scores.mean(0) # Aux Loss = sum(load × scores_mean) × num_experts × coefaux_loss = (load * scores_mean).sum() * 4 * 5e-4# = (1.0 × 0.36 + ...) × 4 × 5e-4 = 0.00072 # 当负载均衡时:load=[0.25,0.25,0.25,0.25]# aux_loss = 0.25 × 4 × 5e-4 = 0.0005# 负载越不均衡,aux_loss 越大

训练脚本中的实际损失计算:

# 训练循环中的损失计算res = model(input_ids, labels=labels)loss = res.loss + res.aux_loss # 日志输出示例# loss: 3.2541, logits_loss: 3.2534, aux_loss: 0.0007# aux_loss 通常比主损失小 3-4 个数量级

MiniMind 的完整 MoE 前向代码(简单直接,但不设 expert capacity):

def forward(self, x):    batch_size, seq_len, hidden_dim = x.shape    x_flat = x.view(-1, hidden_dim)     scores = F.softmax(self.gate(x_flat), dim=-1) # 路由打分    topk_weight, topk_idx = torch.topk(scores, k=1) # top-1 选择     y = torch.zeros_like(x_flat)   # 遍历每个专家,找到分配给它的 token    for i, expert in enumerate(self.experts):        mask = (topk_idx == i)        if mask.any():            token_idx = mask.any(dim=-1).nonzero().flatten()            weight = topk_weight[mask].view(-1, 1)            y.index_add_(0, token_idx,                         (expert(x_flat[token_idx]) * weight).to(y.dtype))        elif self.training:  # 维护计算图:确保没被选中的专家也有梯度路径            y[0, 0] += 0 * sum(p.sum() for p in expert.parameters())

注意那个 elif self.training 分支——即使一个专家在本 batch 没被选到,也强制做虚拟前向来维持计算图连通,防止专家参数"饿死"。

一个 batch 有 32×340=10880 个 token,Dense vs MoE 的处理差异:

// Dense 模型:一个 FFN,一次 forward┌────────────────────────────────┐│  input (10880, 768)            ││  → gate_proj(768, 2048)        ││  → activation (SiLU)           ││  → up_proj(768, 2048)          ││  → down_proj(2048, 768)        ││  总计算:3 次大矩阵乘,GPU > 90%  │└────────────────────────────────┘ // MoE 模型(4 experts/top-1):需要分桶┌────────────────────────────────┐│  input (10880, 768)            ││  → 分桶:                      ││    专家 0: 2800 token          ││    专家 1: 3200 token          │  ← 数据不再连续│    专家 2: 2400 token          ││    专家 3: 2480 token          ││  → 依次调用 4 次 expert forward  │  ← 4 次 kernel 启动│  → 加权组合 y.index_add_        ││  总计算:4 次小矩阵乘 + 4 次 kernel 启动│  GPU 利用率:~50%               │└────────────────────────────────┘

不是计算量变大了,而是计算变得不连续了。训练一个 batch 的 MoE 约等于 Dense 的 1.5 倍时间。

# 启动 MoE 预训练torchrun --nproc_per_node 1 trainer/train_pretrain.py /  --use_moe 1 --batch_size 16 --accumulation_steps 8 /  --learning_rate 5e-4 --max_seq_len 340 /  --data_path ../dataset/pretrain_t2t_mini.jsonl # MoE SFTtorchrun --nproc_per_node 1 trainer/train_full_sft.py /  --use_moe 1 --batch_size 8 --accumulation_steps 4 /  --learning_rate 1e-4 --max_seq_len 1024 /  --data_path ../dataset/sft_t2t_mini.jsonl

专家数量甜点配置:

  • 2 experts → 容量不够,和 Dense 差不多
  • 4 experts → 容量 ×4,速度 ×1.5,性价比最高
  • 8 experts → 容量 ×8,速度 ×3+,kernel 调度开销飙升
  • 16 experts → 大部分时间花在 kernel 启停上,不划算

aux_loss_coef 选择:

  • 太小(<1e-5)→ 负载不均衡,有些专家永远学不到东西
  • 太大(>1e-2)→ 路由被迫均匀,专家无法专业化,MoE 优势消失
  • MiniMind 默认 5e-4,是安全起点

模型版本对比:

模型参数量词表层数dim备注
minimind-364M64008768Dense
minimind-3-moe198M-A64M640087684 experts/top-1

三、训练流程:从原始文本到能聊天的模型


3.1 第一阶段:预训练(Pretrain)——“词语接龙”

一句话理解:让模型学会"词语接龙"——给你前面 N 个 token,你预测第 N+1 个是什么。这个阶段不涉及对话、不涉及指令——就是单纯地"读了大量文本后,学会了语言规律和知识"。

损失函数:语言建模(LM Loss),对所有 token 做交叉熵。

假设模型有一行训练文本:“我的宠物是一只猫”。Tokenize 后变成了 6 个 token:[我, 的, 宠物, 是, 一只, 猫]。

Step 1:构造输入和目标输入:[我, 的, 宠物, 是, 一只]   ← 前 5 个 token目标:[的, 宠物, 是, 一只, 猫]   ← 后 5 个 token(向右偏移一位) Step 2:模型在每个位置输出一个概率分布位置 1:输入”我” → 正确目标”的”的预测概率为 0.02(刚开始非常低)位置 2:输入”我的” → 正确目标”宠物”的预测概率为 0.015位置 3:输入”我的宠物” → 正确目标”是”概率 0.03位置 4:输入”我的宠物是” → 正确目标”一只”概率 0.01位置 5:输入”我的宠物是一只” → 正确目标”猫”概率 0.02 Step 3:Loss 计算Loss = -[log(0.02) + log(0.015) + log(0.03) + log(0.01) + log(0.02)] / 5     = 4.03 // 当模型训练充分后,”我的宠物是”之后”一只猫”的概率接近 1// 此时的 Loss 接近 0:-log(0.95) ≈ 0.05
# train_pretrain.py 的核心逻辑简化版model = MiniMind(config)optimizer = AdamW(model.parameters(), lr=5e-4) for epoch in range(num_epochs):    for batch in dataloader:        input_ids = batch[”input_ids”] # shape: [batch_size, seq_len]        logits = model(input_ids) # [B, S, vocab_size]   # 构造标签:向右偏移一位        labels = input_ids[:, 1:]        pred = logits[:, :-1, :]   # 计算交叉熵损失        loss = F.cross_entropy(            pred.reshape(-1, vocab_size),            labels.reshape(-1)        )         loss.backward()        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)        optimizer.step()        scheduler.step()        optimizer.zero_grad() torch.save(model.state_dict(), ”out/pretrain_768.pth”)

关键细节:

  • 每个 token 位置独立做预测,互不影响
  • 模型使用 Causal Attention Mask(因果掩码),确保位置 i 只能看到位置 ≤ i 的 token
  • 序列中每个位置都产生梯度信号,所以一个 batch 就有 batch_size × seq_len 个训练样本

训练命令:

# cd trainer 目录下执行torchrun --nproc_per_node 1 train_pretrain.py# 或python train_pretrain.py# 训练后输出权重:out/pretrain_768.pth

图片

图4:768dim 配置在预训练阶段的 Loss 曲线——Loss 从约 11 一路下降到约 1.3

训练技巧:

  • 学习率预热(Warm-up):前若干步从 0 线性增加到目标学习率
  • 余弦退火(Cosine Annealing):后期按余弦曲线衰减到接近 0
  • 梯度裁剪(Gradient Clipping):限制梯度最大范数(max_norm=1.0)
  • 权重衰减(Weight Decay):对非归一化参数施加 L2 正则化

预训练后的简单测试:

💬: 为什么天空是蓝色的🧠: 天空之所以看起来是蓝色的,主要是因为太阳光进入大气层后,     短波长的蓝光更容易被空气分子散射... 💬: 解释什么是机器学习🧠: 机器学习是人工智能的一个重要分支,它通过数据训练模型,     使系统能够自动学习规律...

3.2 第二阶段:监督微调(SFT)——“学会对话”

一句话理解:让模型从"会接话"变成"会回答问题"。MiniMind 当前的 SFT 数据达 14GB,本身就带有 mid-training 性质的持续强化。

训练方式:同样是语言建模 Loss,但只对 assistant 部分的 token 计算 Loss。用户输入的 token 不参与 Loss 计算。

torchrun --nproc_per_node 1 train_full_sft.py# 或python train_full_sft.py# 训练后输出权重:out/full_sft_768.pth
// 原始对话用户:给我写一首关于月亮的诗助理:床前明月光... // 送入模型的 token 序列<|im_start|>user/n给我写一首关于月亮的诗<|im_end|><|im_start|>assistant/n床前明月光...<|im_end|> // Loss Mask:1 表示计算 Loss,0 表示忽略[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0]

假设有两组 SFT 数据:

  • 数据 A:用户问"1+1等于几?“,助理回答"1+1=2”
  • 数据 B:相同问题,但助理回答"我也不知道"

如果不做 Loss Mask,模型会学到"1+1等于几?"之后,"1+1=2"和"我也不知道"都要有高概率——这显然不对。

有了 Loss Mask,用户部分 Mask=0,模型只从 assistant 的回答中学。"好"回答的 token 被强化,"差"回答的 token 也被强化——数据质量决定了最终效果。

# train_full_sft.py 的核心逻辑for batch in dataloader:    input_ids = batch[”input_ids”] # [B, S]    labels = batch[”labels”] # [B, S](-100 表示忽略的 token)    attention_mask = batch[”attention_mask”]     logits = model(input_ids, attention_mask)   # 只有在 labels != -100 的位置才计算 loss    loss = F.cross_entropy(        logits.view(-1, vocab_size),        labels.view(-1),        ignore_index=-100 # 关键!-100 的 token 不参与 loss    )

图片

图5:768dim 配置在 SFT 阶段的 Loss 曲线——从约 1.5 下降到约 1.2

SFT 后的测试效果:

💬: 解释什么是机器学习🧠: 机器学习是人工智能的核心技术之一,通过算法让计算机从数据中学习规律,     并持续改进预测或决策效果... 💬: 推荐一些中国的美食🧠: 例如北京烤鸭、兰州拉面、四川火锅、广东早茶、小笼包和麻婆豆腐等,     这些美食分别代表了不同地区的风味特点...

3.3 知识蒸馏(KD,可选)——“向老师学习”

让 MiniMind 向大模型(Teacher)学习。MiniMind 实现了两种方式:

黑盒蒸馏:等价于对 teacher 生成答案做监督微调。学生只看到"老师说了什么"。

白盒蒸馏:额外让学生拟合教师模型在 token 分布层面的偏好。学生学到的不仅是"标准答案",还包括教师在候选 token 之间的相对倾向。

假设教师模型看到"北京的简称是什么?":

教师模型的 softmax 输出(概率分布):”京” 的概率:0.80   ← 老师最确定的答案”京郊” 的概率:0.10  ← 老师有点犹豫”北京” 的概率:0.05”冀” 的概率:0.03其他词:0.02 // 黑盒蒸馏看到的是:{”京”: 目标概率 1.0}// 白盒蒸馏看到的是:{”京”: 0.80, ”京郊”: 0.10, ”北京”: 0.05, ”冀”: 0.03, ...}

为什么白盒更好?黑盒蒸馏让学生只学到"京"这个硬目标,可能过拟合;白盒蒸馏让学生看到教师的完整知识分布——“京"后面有 0.10 概率跟"郊”,这不是错误,而是语言中的合理不确定性。

白盒蒸馏公式中,T 用来调节教师分布的"锐利程度":

教师原始 logits:[2.0, 0.5, -1.0, -2.0]T=1.0 时的 softmax:[0.72, 0.22, 0.04, 0.02]  ← 默认T=2.0 时的 softmax:[0.46, 0.31, 0.
torchrun --nproc_per_node 1 train_lora.py# 合并 LoRA 权重cd scripts && python convert_model.py

3.5 Tool Calling & 自适应思考

当前 Tool Calling 能力已并入 SFT 主线数据,不再单独维护独立的 reason 权重。通过 chat_template 和 open_thinking 开关控制思考能力:

# 测试 Tool Callpython eval_toolcall.py --weight full_sft # 测试思考能力python eval_llm.py --load_from ./minimind-3 --open_thinking 1

测试效果:

💬: 现在几点了?🧠: {”name”: ”get_current_time”,                ”arguments”: {”timezone”: ”Asia/Shanghai”}}📞 [Tool Calling]: get_current_time✅ [Tool Called]: {”datetime”: ”2026-03-15 17:18:22”, ”timezone”: ”Asia/Shanghai”}🧠: 现在是2026年3月15日17时18分22秒。

四、强化学习:让模型学会"做对的事"


SFT 只能让模型学会"模仿"——它看到好的回答就学着说。但什么是"好",SFT 没法让模型自己判断。强化学习(RL)就是来解决这个问题的。

MiniMind 最大的亮点就在这里——从PO(Policy Optimization)算法的统一视角出发,系统性地实现了四种 RL 算法。

4.1 PO 算法的统一视角——所有 RL 算法的"最小公倍数"

核心理念:所有 PO 算法都只是在优化同一个期望目标——区别仅在于三个核心组件(策略项、优势项、正则项)的不同实例化方式。

// 所有 PO 算法的共同目标函数J_PO = E[ Φ(r_t, A_t) - h(KL_t) ] // 符号含义:// r_t = π_θ / π_old  → 概率比,新策略 vs 旧策略// A_t                  → 优势函数,相比基线的超额表现// KL_t                  → KL 散度,新策略偏离参考模型的程度

四种算法对比:

算法策略项 f(r_t)优势项 g(A_t)正则项模型数
DPOlog r_w - log r_l无显式优势隐含在 β1
PPOmin(r_t·A_t, clip(r_t)·A_t)Critic + GAEβ·E[KL]2
GRPOmin(r_t·A_t, clip(r_t)·A_t)组内归一化β·KL_t1
CISPOclip®·A_t·log π_θ组内归一化β·KL_t1

4.2 DPO(Direct Preference Optimization)

核心思想:不需要训练独立的奖励模型,直接用"偏好对"(chosen vs rejected)来优化策略。

假设偏好数据:问题"中国的首都是哪里?"

  • chosen(好回答):“中国的首都是北京”
  • rejected(差回答):“中国的首都是上海”
Step 1:计算两个回答的概率π_ref(chosen) = -3.2   ← 参考模型给好回答的对数概率π_ref(rejected) = -4.1  ← 参考模型给差回答的概率π_θ(chosen) = -2.8      ← 策略模型给好回答的概率,更高了π_θ(rejected) = -5.0    ← 策略模型给差回答的概率,更低了 Step 2:计算偏好分数β = 0.1chosen_ratio = -2.8 - (-3.2) = 0.4       ← 正数,偏好好回答rejected_ratio = -5.0 - (-4.1) = -0.9    ← 负数,疏远差回答preference_score = 0.1 × (0.4 - (-0.9)) = 0.13 Step 3:转为 LossL_DPO = -log(σ(0.13)) = 0.631// σ(0.13) ≈ 0.532// 训练推进后,preference_score 越来越大,Loss → 0

如果偏好数据反了(差的当 chosen),preference_score 会变成负数,模型学到错误偏好。这就是为什么 DPO 对数据质量极其敏感——"垃圾进,垃圾出"在偏好对齐中格外真实。

torchrun --nproc_per_node 1 train_dpo.py# 输出:out/dpo_768.pth

DPO 的局限:不做在线探索,更多用于价值对齐。对"能不能做对题"的智力能力提升有限。

4.3 PPO(Proximal Policy Optimization)

核心思想:用"旧策略"和"新策略"的概率比来把控更新幅度——变化太大就裁剪,确保每步更新都是"小心试探"。

PPO 需要两个网络:Actor(策略网络)和 Critic(价值网络)。

假设模型回答:“北京是中国的首都”(4 个 token)。

// Step 1: Actor 生成回答,Critic 打分token:    ”北京”    ”是”     ”中国” ”的首都”π_old:      0.01     0.05     0.02     0.001π_θ:       0.02     0.08     0.03     0.003r_t:       2.0      1.6      1.5      3.0     ← 概率比V(s_t):    0.5      0.6      0.7      0.8     ← Critic 估值// Step 2:计算 Advantage(GAE)A_1 = 1.0 + 0.99×0.6 - 0.5 = 1.094  ← 好于预期!A_2 = 1.0 + 0.99×0.7 - 0.6 = 1.093A_3 = 1.0 + 0.99×0.8 - 0.7 = 1.092A_4 = 1.0 + 0.99×0.0 - 0.8 = 0.200// Step 3:Clip 机制(ε=0.2,范围 [0.8, 1.2])// token ”北京”:r_t=2.0 > 1.2,被 clip 到 1.2// 无 clip 目标:2.0 × 1.094 = 2.188// clip 后目标:1.2 × 1.094 = 1.313// PPO 取 min,用 1.313

为什么 Clip 这么重要?没有 Clip:token “北京” 每轮概率翻倍,几轮后就成为"必选词",模型崩溃。Clip 把每轮更新限制在 1.2 倍以内——这就是 PPO 名字里"Proximal"(近端)的含义。

// PPO 训练流程Actor 生成一个回答 → Reward Model 打分 → Critic 评估基础分数→ Advantage = 实际得分 - 基础分数→ Actor 向 Advantage 为正的方向更新,用 clip 裁剪过大更新
torchrun --nproc_per_node N train_ppo.py

图片

图6:PPO 训练阶段优化走势——reward 提升缓慢,双网络联合优化复杂度高

PPO 的问题:双网络 + Reward Model,显存占用约为单网络方法的 1.5~2 倍。训练初期 Critic 估计不准会影响 Actor 梯度方向。

4.4 GRPO(Group Relative Policy Optimization)

核心思想:去掉 Critic 网络!同一个 prompt 生成 N 个回答,用组内平均作为"基准"。GRPO 是 DeepSeek-R1 使用的算法。

同一个问题:“计算 25 × 4 + 10 = ?”,N=4 个回答:

回答 1:等于 110    ← 正确!Reward = 1.0回答 2:等于 100    ← 错误(忘记先乘除后加减),Reward = 0.1回答 3:等于 95     ← 错误,Reward = 0.0回答 4:等于 110    ← 正确,Reward = 1.0 // Step 1:组内统计组均值 μ = (1.0 + 0.1 + 0.0 + 1.0) / 4 = 0.525组标准差 σ = 0.477 // Step 2:Advantage 计算(组内归一化)A_1 = (1.0 - 0.525) / 0.477 = 0.996  ← 超越平均水平A_2 = (0.1 - 0.525) / 0.477 = -0.891 ← 低于平均水平A_3 = (0.0 - 0.525) / 0.477 = -1.101 ← 远低于A_4 = (1.0 - 0.525) / 0.477 = 0.996  ← 超越平均水平

关键差异:

// PPO 的 Advantage:依赖 Critic 的估值质量PPO:  A_t = R_t + γ·V(s_{t+1}) - V(s_t) // GRPO 的 Advantage:只依赖组内回答的排序GRPO: A_ij = (R_ij - μ_i) / (σ_i + ε)

如果 4 个回答全是 1.0(退化组):μ=1.0, σ=0.0 → A 全为 0,没有学习信号。

对比 PPO 的优势:

  • 少了一个 Critic 网络,显存大幅降低
  • 组内归一化更稳定,不容易出现极端 Advantage
  • 单网络架构训练更稳定
torchrun --nproc_per_node N train_grpo.py# 输出:out/grpo_768.pth

图片

图7:GRPO 训练阶段——reward 更稳定上升到 4 左右,Policy Loss 下降平稳

4.5 CISPO(Clipped Importance Sampling Policy Optimization)

CISPO 没有重新发明框架,而是抓住了 PPO/GRPO 里一个长期让人别扭的问题——ratio 被 clip 之后,梯度流直接就被硬截断了。

对比 GRPO 和 CISPO 的梯度:

假设某个 token 的 r_t = 2.0(超出 clip 范围),A_t = 1.0 // GRPO 的梯度L_GRPO = min(2.0, 1.2) = 1.2∂L_GRPO/∂θ = 0  ← 因为 r_t > 1.2,梯度为 0!// 即使这个 token 是好词,也不更新了——浪费信息 // CISPO 的梯度L_CISPO = 1.2 · log π_θ(”北京”|...)∂L_CISPO/∂θ = 1.2 · ∂(log π_θ)/∂θ  ← 梯度不为 0!// 即使 r_t 被裁剪为 1.2,梯度仍然通过 log π_θ 传导// 好信号继续更新参数,只是幅度被限制了

CISPO 在训练初期效果更明显——当 30% 的 token 被 clip 时,GRPO 损失 30% 的梯度,而 CISPO 全部保留。

CISPO 可直接作为 GRPO 的 loss 变体,只需在 train_grpo.py 中设置 loss_type=“cispo”。

4.6 Agentic RL——“让模型学会用工具”

场景:模型在多轮工具调用场景下的强化学习。比如模型先调用计算器计算 256×37,拿到结果后再做出回答。奖励是"延迟整轮结算"的。

用户问:“帮我计算 (156 + 289) × 37 的结果”

[turn 1] user: ”帮我计算 (156 + 289) × 37 的结果”[turn 2] assistant:  {”name”: ”calculate_math”,                ”arguments”: {”expression”: ”156 + 289”}}[turn 3] tool 返回: {”result”: ”445”}[turn 4] assistant:  {”name”: ”calculate_math”,                ”arguments”: {”expression”: ”445 * 37”}}[turn 5] tool 返回: {”result”: ”16465”}[turn 6] assistant:  (156 + 289) × 37 = 445 × 37 = 16465

奖励计算(整条轨迹结算):

gt = ”16465”  // ground truth R_answer    = +5.0  // 最终回答正确R_tool     = +1.0  // 工具调用参数格式正确R_format   = +1.0  // 标签闭合R_rm       = +0.5  // Reward Model 综合质量评分R_unfinish =  0.0  // 轨迹完整执行 R(τ) = 5.0 + 1.0 + 1.0 + 0.5 - 0.0 = 7.5

关键:如果模型不调用工具而是直接猜,R_answer=0, R_tool=0,R(τ) 可能变成负数——模型被惩罚后学会"不会的地方一定要调用工具"。

训推分离架构:

# 方式1:默认使用 torch 做 rollouttorchrun --nproc_per_node N train_agent.py # 方式2:使用 sglang 做 rollout(高性能推理引擎)python -m sglang.launch_server --model-path ./minimind-3 /  --attention-backend triton --host 0.0.0.0 --port 8998python train_agent.py --rollout_engine sglang /  --sglang_base_url http://localhost:8998

图片

图8:Agentic RL 训练阶段——在 Tool Use 场景下效果显著

测试结果对比——数学 Tool Use 正确率:

模型权重通过数正确率
full_sft(SFT 后)12/2060%
agent(RL 后)17/2085%

效果提升 25 个百分点!Agent 模型学会了更强的工具调用与利用能力。


五、评估与实战效果


5.1 RL 模型主观对比

基于 minimind-3 (64M),对比三种权重的表现:

模型类型
[A] minimind-364M, SFT
[B] minimind-364M, GRPO
[C] minimind-364M, Agent-CISPO

测试:请介绍一下自己

  • [A] 我是一个AI助手,不能替代任何专业人士…
  • [B] 我非常愿意与你分享…我发现了一些神奇的物品…我的祖先的花园里,有一个小小的庙宇…(废话文学上线)
  • [C] 作为AI,我并非人类,但我可以模拟人类的某些特质…(出现了"共舞"高频复读)

测试:鲁迅的《狂人日记》是如何批判封建礼教的?

  • [A] 鲁迅的《狂人日记》是其作品中对封建礼教的批判…(有条理但重复)
  • [B] 鲁迅的《狂人日记》是中国古典四大名著之一…(事实错误!GRPO 引入新幻觉)
  • [C] 鲁迅是中国现代文学史上第一位作家…(错得更离谱)

5.2 对齐税(Alignment Tax)

现象:RL 后模型在 Tool Use 上大幅提升,但在通用问答上出现"废话文学"和"幻觉"。

这就是对齐税——优化目标变窄之后,模型越来越擅长"迎合当前奖励定义",却不等于整体能力无损提升。

具体表现:

  • reward hacking: 模型学会产生"看起来高级但空洞"的回答来赚奖励分
  • 能力漂移: Tool Use 变强了,通用对话反而更啰嗦、更敢编

建议:

  • 目标是 Tool Use、轻量多步调用 → 使用 agent 权重
  • 更看重日常聊天、知识问答的事实性 → full_sft 仍是更稳妥的选择

5.3 与其他小模型的横向对比

综合评分(GPT-5.4 Thinking 评估,满分 100):

排名模型参数量总分
🥇minimind-3-moe0.2B (A64M)68
🥈chatlm-mini0.2B62

| 🥉 | minimind-
MiniMind 的数据规模远小于其他模型,且训练比例偏向中文,英文表现不佳。这类选择题评测仅供娱乐——但有趣的是,minimind-3-exam 版本(仅做少量 LoRA 格式对齐后)在所有任务上平均提升约 2.9 个百分点,说明对小模型而言,瓶颈未必在知识本身,而可能在于输入格式没有对齐。


六、快速上手


# 第0步:安装git clone --depth 1 https://github.com/minimindcd minimind && pip install -r requirements.txt # 第1步:下载数据(ModelScope / HuggingFace)# 下载 pretrain_t2t_mini.jsonl + sft_t2t_mini.jsonl 到 ./dataset/ # 第2步:预训练cd trainer && python train_pretrain.py # → out/pretrain_768.pth # 第3步:监督微调python train_full_sft.py # → out/full_sft_768.pth # 第4步:测试对话cd .. && python eval_llm.py --weight full_sft

训练开销参考(单卡 RTX 3090):

阶段minimind-3 (64M)minimind-3-moe (198M)
预训练 (mini)~1.21h / 1.57元~1.69h / 2.20元
SFT (mini)~1.10h / 1.43元~1.54h / 2.00元
合计~2.31h / 3元~3.23h / 4.2元

完整的推理生态:

  • Transformers 格式 ✓
  • llama.cpp(GGUF)✓
  • vLLM ✓
  • Ollama:ollama run minimind-3
  • SGLang ✓
  • OpenAI API 兼容服务 ✓
  • Streamlit WebUI ✓

七、总结


读完整个 MiniMind 项目,最打动人的不是技术有多先进,而是设计哲学有多清晰——大道至简。

  • 数据: 四层体系,每一层做不同的事
  • 模型: 6400 词表 + 768 dim + 8 层 + GQA + SwiGLU + RoPE,不多也不少
  • 训练: 预训练 → SFT → RL,三个阶段目标完全不同,缺一不可
  • 强化学习: 一个统一框架下的四种算法,覆盖从价值对齐到工具增强

如果你也想动手训一个,克隆仓库、下载数据、跑 train_pretrain.py、跑 train_full_sft.py——全程不需要改一行代码。等你跑通了,再回去读代码,你会发现每一行都有它的道理。

MiniMind 不是要跟 Llama 3 竞争,它要做的是让每个开发者都能亲手摸到大模型的每一根骨头。三块钱两小时训出来的 64M 模型当然不可能跟 405B 的巨兽比——但它属于你,你从零亲手训练出来的。

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线科技企业深耕十二载,见证过太多因技术卡位而跃迁的案例。那些率先拥抱 AI 的同事,早已在效率与薪资上形成代际优势,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在大模型的学习中的很多困惑。我们整理出这套 AI 大模型突围资料包

  • ✅ 从零到一的 AI 学习路径图
  • ✅ 大模型调优实战手册(附医疗/金融等大厂真实案例)
  • ✅ 百度/阿里专家闭门录播课
  • ✅ 大模型当下最新行业报告
  • ✅ 真实大厂面试真题
  • ✅ 2026 最新岗位需求图谱

所有资料 ⚡️ ,朋友们如果有需要 《AI大模型入门+进阶学习资源包》下方扫码获取~
在这里插入图片描述

① 全套AI大模型应用开发视频教程

(包含提示工程、RAG、LangChain、Agent、模型微调与部署、DeepSeek等技术点)
在这里插入图片描述

② 大模型系统化学习路线

作为学习AI大模型技术的新手,方向至关重要。 正确的学习路线可以为你节省时间,少走弯路;方向不对,努力白费。这里我给大家准备了一份最科学最系统的学习成长路线图和学习规划,带你从零基础入门到精通!
在这里插入图片描述

③ 大模型学习书籍&文档

学习AI大模型离不开书籍文档,我精选了一系列大模型技术的书籍和学习文档(电子版),它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础。
在这里插入图片描述

④ AI大模型最新行业报告

2025最新行业报告,针对不同行业的现状、趋势、问题、机会等进行系统地调研和评估,以了解哪些行业更适合引入大模型的技术和应用,以及在哪些方面可以发挥大模型的优势。
在这里插入图片描述

⑤ 大模型项目实战&配套源码

学以致用,在项目实战中检验和巩固你所学到的知识,同时为你找工作就业和职业发展打下坚实的基础。
在这里插入图片描述

⑥ 大模型大厂面试真题

面试不仅是技术的较量,更需要充分的准备。在你已经掌握了大模型技术之后,就需要开始准备面试,我精心整理了一份大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余

图片

以上资料如何领取?

在这里插入图片描述

为什么大家都在学大模型?

最近科技巨头英特尔宣布裁员2万人,传统岗位不断缩减,但AI相关技术岗疯狂扩招,有3-5年经验,大厂薪资就能给到50K*20薪!

图片

不出1年,“有AI项目经验”将成为投递简历的门槛。

风口之下,与其像“温水煮青蛙”一样坐等被行业淘汰,不如先人一步,掌握AI大模型原理+应用技术+项目实操经验,“顺风”翻盘!
在这里插入图片描述
在这里插入图片描述

这些资料真的有用吗?

这份资料由我和鲁为民博士(北京清华大学学士和美国加州理工学院博士)共同整理,现任上海殷泊信息科技CEO,其创立的MoPaaS云平台获Forrester全球’强劲表现者’认证,服务航天科工、国家电网等1000+企业,以第一作者在IEEE Transactions发表论文50+篇,获NASA JPL火星探测系统强化学习专利等35项中美专利。本套AI大模型课程由清华大学-加州理工双料博士、吴文俊人工智能奖得主鲁为民教授领衔研发。

资料内容涵盖了从入门到进阶的各类视频教程和实战项目,无论你是小白还是有些技术基础的技术人员,这份资料都绝对能帮助你提升薪资待遇,转行大模型岗位。
在这里插入图片描述
在这里插入图片描述

以上全套大模型资料如何领取?

在这里插入图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐