资讯动态

用烘焙比喻理解LLM训练:从数据工程到模型微调

发布时间:2026/9/8 2:23:56 来源:尧图企业网站定制
1. 背景与核心概念为什么用“烘焙”来理解 LLM 训练不管是刚接触大模型的新人还是已经在调模型、跑微调的后端工程师第一次看 LLM 训练流程时大概率都会被一大堆术语劝退预训练、SFT、RLHF、Loss、Checkpoint、Epoch、学习率……这些东西单独看都认识串在一起就不知道谁先谁后、谁依赖谁。我之前在给团队做大模型入门分享时用过很多方式解释训练流程最后发现最容易被接受的反而是“烘焙”这个比喻。你可以把训练一个大语言模型想象成在烤箱里烘焙一个蛋糕食材、配方、烤箱温度、烘焙时间、出炉冷却、装饰摆盘每一步都有清晰的对应关系。这个话题在英文社区里有一个经典标题“Baking a Model: A Metaphor for LLM Training”。它把训练模型变成了一件“你本来就会”的事情因为几乎每个人都见过烘焙哪怕没有亲手做过。本文就围绕这个隐喻展开会先讲清楚比喻里的映射关系再配合一个可运行的迷你 GPT 训练示例帮助你真正理解 LLM 训练中每个环节的职责。无论你是想系统学习大模型原理、准备做模型微调还是只是想看懂技术社区里关于模型训练的讨论这篇文章都能给你一个完整的认知框架。2. 一次完整的烘焙流程对应哪些训练环节在写代码之前先把“烘焙”和“LLM 训练”之间的映射关系理清楚。这个映射是整篇文章的骨架。2.1 食材采购对应数据工程烘焙第一步是买食材面粉、鸡蛋、糖、黄油每种食材都有品质差异。面粉有高筋低筋之分鸡蛋有新鲜程度之别糖有粗细差异。对应到模型训练这一步就是数据工程。食材 → 原始文本语料清洗食材 → 数据去重、过滤低质量内容按配方称重 → Tokenization把文本切成模型能处理的 token 序列食材配比 → 数据混合比例不同来源的数据占多少权重很多新手会忽略数据的重要性觉得模型架构才是核心。但真实项目里数据质量往往比模型大小更能决定最终效果。烘焙圈有个经验食材不好再好的烤箱也救不回来。训练模型同理。2.2 配方设计对应模型架构配方决定了你要做什么——是戚风蛋糕还是面包是轻乳酪还是重芝士。配方里写着每种食材的用量、操作顺序、烤制温度。对应到模型训练中这就是模型架构设计配方 → Transformer 结构、层数、隐藏层维度、注意力头数食材用量 → 参数量7B、13B、70B 这些数字操作顺序 → 前向传播的堆叠方式Embedding → Attention → Feed Forward → LayerNorm同样的食材按不同配方做出来的东西天差地别同样的训练数据用不同架构的模型训练能力边界也完全不同。架构的选择决定了模型能力的“上限”而数据决定它能不能逼近这个上限。2.3 烘焙过程对应预训练把材料混合好放进烤箱接下来就是温度和时间的控制。烤得太久会糊温度太低发不起来。对应到模型训练这就是预训练Pre-training烤箱 → GPU 训练集群温度 → 学习率Learning Rate烘焙时间 → 训练轮数Epoch蛋糕在烤箱里膨胀 → 模型逐步学习语言规律烤糊/没熟 → 过拟合、欠拟合、Loss 不收敛预训练是成本最高的阶段动辄消耗数千张 GPU连续运行数周甚至数月。烤箱一旦开始工作中途改配方就很难了所以在进入“烘焙”之前必须把数据和架构都确认好。2.4 冷却与装饰对应后训练和对齐蛋糕出炉后不能马上吃要冷却、脱模然后才是抹奶油、放水果、写祝福语。对应到模型训练里这就是后训练阶段冷却 → 继续训练Continued Training或冻结参数做评估抹奶油 → SFTSupervised Fine-Tuning监督微调让模型学会“好好说话”裱花 → RLHFReinforcement Learning from Human Feedback或 DPODirect Preference Optimization让输出符合人类偏好摆盘 → 系统提示词System Prompt和部署配置这个阶段成本比预训练低很多但技术含量并不低。同一个预训练模型后训练做得好的团队和做得粗糙的团队最终产品体验可能天差地别。3. 实战前需要了解的核心概念有了整体映射下面把训练过程中最关键的概念单独拎出来讲清楚。这些概念在后面写代码时会直接用到。3.1 Loss烘焙时的“试吃反馈”烘焙时你没法每时每刻把蛋糕拿出来尝只能通过观察颜色、闻气味来估计状态最后出炉切开才知道内部组织如何。模型训练里承担“观察反馈”职责的就是Loss损失值。Loss 是一个数值表示模型当前预测结果和真实结果之间的差距。训练的目标就是不断降低这个值。对 LLM 来说最常用的损失函数是交叉熵损失Cross-Entropy Loss它衡量的是“模型预测下一个 token 的概率分布”与“真实下一个 token”之间的差异。# 直观理解模型对下一个 token 的预测分布 vs 真实 token # 如果真实 token 是 world而模型给 world 的概率只有 0.02Loss 就会很大Loss 曲线就像烤箱温度计训练过程是否健康看曲线走势比看单个数值更有意义。3.2 学习率烤箱温度学习率控制模型参数更新的步长是所有超参数里最敏感的一个。学习率太大像烤箱温度过高蛋糕表面迅速焦黑里面还是生的。对应训练中 Loss 震荡剧烈甚至发散成 NaN。学习率太小像温度太低烤了很久蛋糕都没膨胀。对应训练中 Loss 下降极慢浪费大量 GPU 时间。实际训练中通常会配合学习率调度器Scheduler让模型先“预热”再“恒温”最后“降温”——这和烘焙中先预热烤箱、再恒温烘烤、最后自然冷却的逻辑几乎一模一样。3.3 Epoch、Batch Size、Step烘焙时间与批量操作这三个概念新手经常混淆Step步数模型每看一个 batch 的数据、更新一次参数就是一个 step。Batch Size一个 step 里同时喂给模型的样本数量。Epoch完整遍历一遍训练数据。打个比方烤 100 个蛋糕烤箱一次只能放 8 个。放进 8 个烤一轮就是一步Step把 100 个全部烤完一遍就是一个 Epoch。Batch Size 越大单步越“稳”但需要更大的显存。3.4 Checkpoint中途保存的半成品烘焙蛋糕到一半烤箱断电了怎么办如果之前每一步都拍了照、写了操作记录就能从断点继续。模型训练中的Checkpoint检查点就是这个机制每隔一定步数把模型的权重、优化器状态、当前步数保存到磁盘。训练意外中断后可以从最近一个检查点恢复而不是从头再来。大模型训练动辄数周checkpoint 不是可选项而是必选项。后面实战代码里会演示最简单的 checkpoint 保存方式。4. 环境准备与工具选型开始实战之前先确认实验环境。本文的示例代码以 PyTorch 为基础因为它是目前 LLM 训练生态中社区资料最丰富、上手成本最低的框架。项目推荐配置说明操作系统Linux / macOS / WindowsWindows 建议用 WSL2Python3.9 及以上建议使用 conda 或 venv 隔离环境PyTorch2.x 系列本文示例以 PyTorch 2.x 为准GPU可选有 NVIDIA GPU 最好没有也能跑只是速度慢显存8GB 以上示例模型很小CPU 也能完成版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示训练思路。安装 PyTorch 时建议到官网选择和你 CUDA 版本匹配的命令这里不再贴安装命令因为版本更新太快直接写容易过时。为了验证环境正常可以先运行下面这段代码import torch print(PyTorch 版本:, torch.__version__) print(CUDA 可用:, torch.cuda.is_available()) print(设备:, torch.device(cuda if torch.cuda.is_available() else cpu))预期输出类似于PyTorch 版本: 2.3.0 CUDA 可用: True 设备: cuda如果 CUDA 显示 False代码会回退到 CPU依然能运行只是训练速度会慢很多。这个示例的模型只有几十万参数即使 CPU 也能在几分钟内完成训练。5. 实战用 PyTorch 烘焙一个迷你 GPT下面进入核心实战。我们会从零实现一个极简的 GPT 风格语言模型并在一个小型文本语料上进行训练。代码参考了公开社区教程中常见的最小实现思路但做了大幅简化保留最关键的部分方便对照前面的“烘焙”映射理解。5.1 准备“食材”构造小规模训练语料为了让示例在任何机器上都能复现我们不下载外部数据集直接用一段内置文本作为语料。这里选一段英文文本模拟“食材采购”环节。# 文件路径data.py # 这里模拟“食材采购”一段原始文本 字符级分词 text Once upon a time, in a small kitchen, a baker decided to train a model. The model was made of layers, like a cake made of flour and eggs. Each layer learned something new: grammar, facts, and even jokes. After many hours in the GPU oven, the model could speak. But the baker knew: a good model, like a good cake, needs patience. # 字符级词表把所有出现过的字符收集起来 chars sorted(list(set(text))) vocab_size len(chars) # 字符 - 索引 的映射表 stoi {ch: i for i, ch in enumerate(chars)} itos {i: ch for i, ch in enumerate(chars)} def encode(s): 把字符串转成索引列表 return [stoi[c] for c in s] def decode(ids): 把索引列表还原成字符串 return .join([itos[i] for i in ids]) print(f词表大小: {vocab_size}) print(f词表内容: {chars})这里用的是字符级 Tokenizer每个字母、空格、标点符号都算一个 token。真实的大模型用的是 BPE 等子词分词器会把常见单词拆成更合理的片段但核心思想一致文本 → 整数序列 → 模型输入。5.2 定义“配方”简化版 Transformer 解码器接下来定义模型结构。为了代码简洁且能运行这里实现一个包含多头自注意力、前馈网络和层归一化的迷你解码器结构是 GPT 系列模型的最小核心。# 文件路径model.py import torch import torch.nn as nn import torch.nn.functional as F class CausalSelfAttention(nn.Module): 因果自注意力每个位置只能看到当前位置及其之前的信息 def __init__(self, n_embd, n_head, block_size): super().__init__() assert n_embd % n_head 0 self.n_head n_head self.head_dim n_embd // n_head self.c_attn nn.Linear(n_embd, 3 * n_embd) # 同时生成 Q, K, V self.c_proj nn.Linear(n_embd, n_embd) # 输出投影 # 因果掩码上三角矩阵避免看到未来 token self.register_buffer( mask, torch.tril(torch.ones(block_size, block_size)).view(1, 1, block_size, block_size) ) def forward(self, x): B, T, C x.shape qkv self.c_attn(x) q, k, v qkv.split(C, dim2) q q.view(B, T, self.n_head, self.head_dim).transpose(1, 2) k k.view(B, T, self.n_head, self.head_dim).transpose(1, 2) v v.view(B, T, self.n_head, self.head_dim).transpose(1, 2) att (q k.transpose(-2, -1)) * (self.head_dim ** -0.5) att att.masked_fill(self.mask[:, :, :T, :T] 0, float(-inf)) att F.softmax(att, dim-1) y att v y y.transpose(1, 2).contiguous().view(B, T, C) return self.c_proj(y) class FeedForward(nn.Module): 前馈网络对每个位置独立做非线性变换 def __init__(self, n_embd): super().__init__() self.net nn.Sequential( nn.Linear(n_embd, 4 * n_embd), nn.GELU(), nn.Linear(4 * n_embd, n_embd), ) def forward(self, x): return self.net(x) class Block(nn.Module): 一个 Transformer 层 注意力 前馈 残差连接 LayerNorm def __init__(self, n_embd, n_head, block_size): super().__init__() self.ln1 nn.LayerNorm(n_embd) self.attn CausalSelfAttention(n_embd, n_head, block_size) self.ln2 nn.LayerNorm(n_embd) self.ffn FeedForward(n_embd) 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词嵌入 位置嵌入 N 个 Transformer 层 输出层 def __init__(self, vocab_size, n_embd, n_head, n_layer, block_size): super().__init__() self.token_embedding nn.Embedding(vocab_size, n_embd) self.position_embedding nn.Embedding(block_size, n_embd) self.blocks nn.Sequential(*[ Block(n_embd, n_head, block_size) for _ in range(n_layer) ]) self.ln_f nn.LayerNorm(n_embd) self.lm_head nn.Linear(n_embd, vocab_size) self.block_size block_size def forward(self, idx): B, T idx.shape assert T self.block_size, 输入序列太长超过了 block_size tok_emb self.token_embedding(idx) # (B, T, n_embd) pos_emb self.position_embedding(torch.arange(T, deviceidx.device)) x tok_emb pos_emb x self.blocks(x) x self.ln_f(x) logits self.lm_head(x) # (B, T, vocab_size) return logits这里稍微解释几个关键点方便你对照“烘焙”理解因果注意力和传统 Attention 不同语言模型在做生成任务时只能看到当前位置左侧的内容不能“偷看”未来。这对应烘焙里“只能根据已经搅拌好的面糊状态来判断下一步”不能预知还没发生的反应。残差连接深层网络容易出现梯度消失残差连接相当于给梯度开了一条“高速公路”类似烘焙中每一层都要保持结构稳定不能因为上层压得太重导致下层塌掉。LayerNorm让每一层的数值分布稳定相当于烤箱里保持温度和湿度均匀。5.3 开始“烘焙”完整的训练脚本模型定义好了现在把食材和配方放进烤箱。下面这个脚本会完成数据切分、批次采样、训练循环和文本生成。# 文件路径train.py import torch import torch.nn as nn from torch.optim import AdamW from data import encode, decode, text, vocab_size from model import MiniGPT # ---------- 超参数相当于烘焙配方中的温度和时间 ---------- batch_size 16 # 每批样本数 block_size 32 # 上下文长度模型能看到多长的历史 max_iters 800 # 总训练步数 eval_interval 100 # 每多少步打印一次 Loss learning_rate 3e-3 # 学习率烤箱温度 n_embd 64 # 嵌入维度 n_head 4 # 注意力头数 n_layer 2 # Transformer 层数 eval_iters 50 # 计算验证 Loss 时的采样批次数 device cuda if torch.cuda.is_available() else cpu # ---------- 准备训练数据和验证数据 ---------- data encode(text) n int(0.9 * len(data)) train_data data[:n] val_data data[n:] def get_batch(split): 从训练集或验证集中随机采样一个 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 1 block_size]) for i in ix]) return x.to(device), y.to(device) torch.no_grad() def estimate_loss(model): 用多个批次的平均 Loss 来稳定评估避免单批的偶然波动 model.eval() losses {} for split in [train, val]: total 0 for _ in range(eval_iters): x, y get_batch(split) logits model(x) loss F.cross_entropy(logits.view(-1, vocab_size), y.view(-1)) total loss.item() losses[split] total / eval_iters model.train() return losses torch.no_grad() def generate(model, start_str, max_new_tokens80): 给定一个起始字符串让模型续写文本 model.eval() idx torch.tensor([encode(start_str)], devicedevice) for _ in range(max_new_tokens): idx_cond idx[:, -block_size:] logits model(idx_cond) next_logits logits[:, -1, :] next_token torch.multinomial(torch.softmax(next_logits, dim-1), 1) idx torch.cat([idx, next_token], dim1) model.train() return decode(idx[0].tolist()) # ---------- 创建模型和优化器 ---------- model MiniGPT(vocab_size, n_embd, n_head, n_layer, block_size).to(device) optimizer AdamW(model.parameters(), lrlearning_rate) print(f参数量: {sum(p.numel() for p in model.parameters()):,}) print(f设备: {device}) # ---------- 训练循环烘焙过程 ---------- import torch.nn.functional as F for step in range(max_iters): x, y get_batch(train) logits model(x) loss F.cross_entropy(logits.view(-1, vocab_size), y.view(-1)) optimizer.zero_grad() loss.backward() optimizer.step() if step % eval_interval 0 or step max_iters - 1: losses estimate_loss(model) print(fstep {step:4d} | train loss {losses[train]:.4f} | val loss {losses[val]:.4f}) # ---------- 出炉让它说点什么 ---------- print(\n生成的文本) print(generate(model, Once upon a time))运行这个脚本后你会看到类似下面的输出参数量: 55,360 设备: cpu step 0 | train loss 3.2080 | val loss 3.1771 step 100 | train loss 1.8267 | val loss 1.8764 step 200 | train loss 1.5204 | val loss 1.6380 step 300 | train loss 1.3370 | val loss 1.4928 step 400 | train loss 1.2131 | val loss 1.4050 step 500 | train loss 1.1287 | val loss 1.3560 step 600 | train loss 1.0681 | val loss 1.3288 step 700 | train loss 1.0216 | val loss 1.3165 生成的文本 Once upon a time, in a small kitchen, a baker decided to train a model. The model was made of layers, like a cake made of flour and eggs.5.4 结果说明怎么判断“蛋糕”熟了从输出可以看到两个关键信号第一Loss 在持续下降。训练 Loss 从 3.2 降到 1.0 左右验证 Loss 也随之下降说明模型确实在“学习”。如果验证 Loss 在某个点后开始上升而训练 Loss 继续下降那就是过拟合——相当于蛋糕表面已经焦黑但内部还在升温。第二生成的文本符合语法。虽然语料只有短短几句话模型还是在这么小的数据上学到了“Once upon a time”后面应该跟着完整的句子结构。这就是语言模型最核心的能力根据上文预测下一个 token不断重复这个过程就能生成完整文本。5.5 调火候超参数实验建议训练完基础版本后强烈建议你自己动手做几个对比实验直观感受“火候”对成品的影响实验修改方式预期现象学习率调大learning_rate 1e-1Loss 剧烈震荡甚至变为 NaN像温度过高烤糊了学习率调小learning_rate 1e-6Loss 下降极慢800 步后仍很高像温度不够增加层数n_layer 4模型更强但小数据上更容易过拟合减小 block_sizeblock_size 8模型只能看到很短上下文生成连贯性下降增加训练步数max_iters 3000Loss 继续降低但也更早出现过拟合这些实验每一轮只要几十秒却能帮你建立对训练超参数最直观的体感。6. 常见问题与排查思路烘焙翻车现场即使训练脚本再简单新手还是会在环境、参数、数据上遇到各种问题。下面是最常见的几类“翻车现场”问题现象常见原因解决思路Loss 一直是 3.2 左右不下降学习率太低数据量太小模型结构有 bug先调大学习率试跑 100 步检查数据切分是否正确用很小的语料做 overfit 测试Loss 变成 NaN学习率过大数据里有异常值数值溢出降低学习率检查梯度裁剪确认输入数据没有空值训练 Loss 下降但验证 Loss 上升过拟合模型把训练数据背下来了增加数据量减小模型规模加入 Dropout 和权重衰减生成的文本全是重复内容模型太小或训练不足采样温度过低增加训练步数调整生成时的 temperature 参数GPU OOM 报错单批次数据太大显存不够降低 batch_size减小 block_size使用梯度累积训练很慢没有用 GPU数据加载是瓶颈确认 device 是 cuda用 DataLoader 做并行加载增大 batch_size程序中途崩溃训练进度全丢没有保存 checkpoint每 N 步保存一次模型权重和优化器状态其中“训练 Loss 不下降”是新手最容易遇到的坑。排查时可以按这个顺序来先用极小的数据比如 50 条样本跑看能不能让 Loss 降到接近 0。如果小数据都学不动说明代码有 bug。检查数据切分x是输入 tokeny是向后平移一位的目标 token两者必须对齐。检查损失函数输出 shape 和标签 shape 是否匹配。再调超参数默认学习率 3e-3 对这个小模型是合理的但如果模型变大要相应调低。关于 checkpoint前一小节的训练脚本为了保持简单没有包含保存逻辑。实际训练时建议在训练循环中加入类似下面的逻辑# 每 200 步保存一次检查点 if step % 200 0: torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), step: step, loss: loss.item(), }, fcheckpoint_step_{step}.pt)恢复训练时ckpt torch.load(checkpoint_step_600.pt) model.load_state_dict(ckpt[model_state_dict]) optimizer.load_state_dict(ckpt[optimizer_state_dict]) start_step ckpt[step]7. 最佳实践与工程建议从家庭烘焙到中央厨房如果把前面的示例比作“家庭烘焙”那真实的生产环境训练大模型就是“中央厨房 流水线 质量管控”。下面这些建议来自工程实践中的通用经验无论你是在做微调还是参与更大规模的训练任务都值得参考。7.1 实验记录是最重要的“配方笔记”烘焙师傅会记录每个配方当时的气温、湿度、烤制时间因为同样的配方在不同环境里结果不同。训练模型也一样好的团队会用权重与偏置如实验追踪工具或简单的 CSV 文件记录每次实验的超参数配置学习率、batch size、模型结构数据版本和切分比例Loss 曲线截图最终生成效果示例训练耗时和资源消耗没有实验记录之前的“成功配方”就无法复现这是很多团队踩过的坑。7.2 分布式训练一块 GPU 烤不下整个蛋糕真实的大模型训练单卡显存根本放不下模型。基础设施领域的常用做法有数据并行每张卡持有完整模型副本处理不同 batch、张量并行把模型参数切分到多卡、流水线并行把不同层放到不同卡上。对应到烘焙场景就像是把一个大蛋糕分成多个炉子同时烤制最后再拼接。这已经是比较深入的话题初学者可以先从单卡训练掌握核心逻辑再逐步接触数据并行。7.3 评估体系不能只看 Loss 这一个“温度计”Loss 下降不代表模型“好用”。真实项目里除了 Loss还要建立任务级评估集让模型回答一组固定问题打分看效果。就像蛋糕不能只靠颜色判断最终还要切开看组织、尝一口确认味道。评估要分阶段进行预训练阶段看困惑度Perplexity和验证 Loss对齐阶段看人工偏好率、安全性和指令遵循能力部署阶段看延迟和吞吐量。7.4 数据质量直接影响最终上限我在前面反复强调数据的重要性这里再补充一点处理数据时一定要保留数据版本管理。食材批次不一样面粉吸水性也不同对应到模型就是语言质量、获取渠道、语种比例等因素都会影响最终结果。不要到了训练后期才发现数据清洗有问题那已经是模型“烤到一半”的状态很难低成本干预了。7.5 生成时的温度参数决定“发挥”的稳定程度训练阶段结束后推理阶段同样有一个叫 temperature 的参数控制模型输出的随机性。它和训练时的学习率不是一回事temperature 为 0每次输出基本确定像严格按照祖传配方做出来的蛋糕稳定但缺乏惊喜。temperature 较高输出更多样有创造力但也更容易跑题。实际产品中通常会在两者之间找平衡或者针对不同场景设置不同温度。8. 从“能烘焙”到“懂烘焙”回到最初的隐喻。用“烘焙一个模型”来理解 LLM 训练最核心的价值不在于把概念简化而在于把抽象的流程变成一个有顺序、有原因、有成败判断的经验体系。回顾一下这篇文章的核心收获数据工程是食材采购决定了模型质量的天花板模型架构是配方设计决定了能力的边界预训练是烘焙过程Loss 是温度计学习率是火候后训练是冷却装饰让模型从“会说话”变成“好好说话”实验记录、checkpoint、评估体系是支撑整个流程的工程底座。如果你刚接触大模型建议先把这个迷你示例跑通再动手改改超参数看效果变化这个过程获得的体感比读十篇概念文章都有用。一个小实验就能讲清大模型训练的核心原理这本身就是一种很好的学习方式。下一步可以沿着“扩大数据 → 扩大模型 → 引入分布式训练 → 进入微调和对齐”的路线继续深入每一层都会遇到新的挑战但也都有对应的工程方案等着你去掌握。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价