你是不是也遇到过这种情况:刚接触大模型时,看论文、看源码、看视频,到处都在说预训练、微调、SFT、RLHF,感觉每个词都认识,但连在一起就不知道整个流程到底在干什么。然后你去看训练框架的文档,里面全是数据预处理、优化器、学习率调度、检查点保存……越看越乱,最后只能在别人的代码里改改参数,跑起来也不知所以然。
这篇文章想换一个思路,用“烘焙”来做比喻,把 LLM 训练的完整链路重新讲一遍。“Baking a Model” 这个说法在英文技术社区里并不少见,它不是玩梗,而是因为大模型训练和烘焙在工程逻辑上有很多高度对应的地方:都是非线性过程、都极度依赖经验、最终质量都取决于原料和过程控制,而不是单点“魔法”。
读完这篇文章,你应该能收获三样东西:
- 一张 LLM 训练全流程的“心智地图”:预训练、微调、对齐分别对应烘焙的哪个阶段,它们为什么不能跳步。
- 一份可以照着跑的完整代码示例:用 Hugging Face 生态训练一个真正的语言模型,从数据处理到推理验证。
- 一套判断训练过程是否健康的经验指标:什么时候 loss 下降是正常的、什么时候该停、什么时候模型“坏了”。
文章会从比喻入手,但不会停留在比喻。每个环节都会给出技术定义、关键代码和实际工程建议,毕竟比喻只是脚手架,真正要建立的还是技术直觉。
1. 为什么“烘焙”是理解 LLM 训练的最佳比喻
先给结论:用烘焙比喻大模型训练,最大的价值不是让概念变得“可爱”,而是让初学者看清单个操作在整个流程中的位置。
很多人学 LLM 训练的难点在于:每个单独的技术点都有大量资料,但很少有人讲清楚它们之间的前后依赖。比如你知道数据清洗很重要,但不知道为什么预训练阶段的数据清洗和微调阶段完全不同;你知道学习率很关键,但不理解为什么训练后期要下降,为什么同样一个超参数在预训练和微调里作用差别那么大。
这些问题用烘焙类比会立刻清晰起来。
想象你在做一款面包,整个流程是:
- 挑选小麦、面粉、水、酵母、盐,并称量。
- 把原料混合揉成面团。
- 第一次发酵,让面团膨胀。
- 排气、整形,决定最终形状。
- 第二次发酵。
- 入炉烘烤,控制温度和湿度。
- 出炉冷却、切片、包装。
现在把它映射到 LLM 训练上:
| 烘焙步骤 | LLM 训练对应阶段 | 关键技术 |
|---|---|---|
| 挑选原料、称量 | 数据收集与清洗 | 数据质量、去重、过滤、tokenization |
| 揉面 | 数据组织与批次构建 | 采样策略、batch size、序列长度 |
| 第一次发酵 | 预训练(pretraining) | 自监督学习、语言建模损失、学习率调度 |
| 排气、整形 | 模型结构设计与初始化 | transformer 架构、参数量、初始化方式 |
| 第二次发酵 | 继续预训练 / domain adaptation | 领域数据继续训练、增量预训练 |
| 入炉烘烤 | 监督微调(SFT) | 指令数据、对话数据、交叉熵损失 |
| 出炉冷却、切片 | 对齐 / RLHF / 部署前评估 | 奖励模型、PPO、评测集验证 |
| 试吃、调整配方 | 迭代实验 | 实验管理、超参数搜索、回滚 |
这张对照表有两点值得展开。
第一,烘焙的每个阶段都不能跳步。你不可能不发酵就直接烤,也不可能在面团还没成型时就裱花。同样,LLM 训练中跳过预训练直接微调会效果很差,因为模型根本不具备语言基础能力;反过来,你也不能指望预训练直接产出能聊天的助手,因为预训练只是“学会了字词规律”,还没有“学会听从指令”。
第二,原料质量决定了上限,工艺只决定你能多接近这个上限。面包好不好吃,最关键的其实是面粉和发酵状态,而不是最后那几分钟烤箱温度;模型最终效果的上限,同样由数据质量和数据分布决定,超参数调优只是让你逼近这个上限。这个认知非常重要,因为很多新手把大量时间花在调学习率上,却不愿意花时间检查数据质量,方向就反了。
所以,要理解 LLM 训练,不要从“训练”本身开始,而要从“原料”开始。接下来各节就按这个顺序展开。
2. 原料准备:数据收集、清洗与 Tokenization
在烘焙里,面粉的蛋白质含量、新鲜度、吸水性,直接决定了面团能不能揉出筋、面包能不能膨胀。在模型训练里,数据就是面粉。
但数据不能直接扔给模型。原始文本要先经过一条流水线:
- 收集:从网页、书籍、代码仓库、学术论文等来源采集文本。
- 清洗:去 HTML 标签、去重复段落、去低质量内容、过滤有害信息。
- 去重:计算文本相似度,去除近似重复的数据。重复数据会严重干扰训练。
- 分词(Tokenization):把文本切分成 token 序列,这一步相当于“把面粉过筛”。
- 构建训练样本:将 token 序列截断或拼接成固定长度,组装成 batch。
其中Tokenization 是初学者最容易忽略、但影响极其深远的一步。模型看到的世界不是字符,不是单词,而是 token 的序列。同一个词在中文和英文里被切分的方式完全不同;tokenizer 词表大小、切分粒度直接影响训练效率和最终效果。
在实际代码里,用 Hugging Face 生态处理数据通常长这样:
# 文件路径:prepare_data.py from datasets import load_dataset, concatenate_datasets from transformers import AutoTokenizer import numpy as np # 1. 加载原始数据集 # 这里用中文维基作为演示数据,实际项目请根据任务替换 dataset = load_dataset("wikipedia", language="zh", date="20240101", trust_remote_code=True) # 2. 清洗:去掉空文本和过短的内容 def clean_text(example): text = example["text"] if text is None: return {"text": ""} text = text.replace("\n", " ").strip() return {"text": text} dataset = dataset.map(clean_text, num_proc=8) dataset = dataset.filter(lambda x: len(x["text"]) > 50) # 3. 加载 tokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") # 4. 切分文本并截断/拼接为固定长度 block_size = 512 def tokenize_function(examples): return tokenizer(examples["text"], truncation=False) tokenized_dataset = dataset.map(tokenize_function, batched=True, num_proc=8, remove_columns=["text"]) def group_texts(examples): concatenated = sum(examples["input_ids"], []) total_length = (len(concatenated) // block_size) * block_size result = { "input_ids": [concatenated[i: i + block_size] for i in range(0, total_length, block_size)], "attention_mask": [ [1] * block_size for _ in range(0, total_length, block_size) ], } return result lm_dataset = tokenized_dataset.map(group_texts, batched=True, num_proc=8) print(lm_dataset["train"][0].keys())这段代码做了什么?
- 加载原始语料。
- 过滤掉短文本,因为短文本往往信息量低或属于没实际内容的模板页面。
- 使用中文 BERT 的 tokenizer 把文本切成子词片段。
- 把所有 token 首尾拼接后,按固定长度切成块,每一块就是一条训练样本。
最后一步group_texts是实践中一个经典技巧。直接按段落切分会导致大多数样本长度不足,造成计算浪费;拼接后再切,可以最大化每个训练样本的利用率。
2.1 数据质量检查清单
很多新手训练完模型发现效果差,第一反应是模型结构有问题,实际原因却在数据。这里给一份经验清单:
- 重复率:用 MinHash 等算法做近似去重。重复数据会让模型记忆而不是泛化。
- 语言分布:混合多语言数据时,各语言占比要和目标使用场景匹配,而不是随意混合。
- 污染检测:确保评测集没有出现在训练集里,否则评测结果虚高。
- 质量过滤:参考 C4、Gopher 论文的过滤规则,用启发式规则(长度、符号密度、语言识别)筛掉低质量页面。
3. 揉面与发酵:预训练阶段到底在做什么
有了原料,下一步是揉面。在 LLM 训练里,预训练就是“把数据中的语言规律揉进模型参数里”。
这一步的技术定义是:在超大规模无标注文本上,以自监督方式训练 Transformer 模型,最常用的目标函数是自回归语言建模损失(Causal Language Modeling Loss),也就是让模型根据前面的 token 预测下一个 token。
用公式表达就是:
L = - (1/N) * Σ log P( token_i | token_1, token_2, ..., token_{i-1} )模型看到一句话前 511 个 token,预测第 512 个 token 是什么。训练数据里有正确答案,所以这是标准的监督信号——只是“标签”来自文本本身,因此叫自监督。
很多人问:为什么预测下一个词就能学到知识?
这个问题其实和“为什么揉面能形成面筋”一样。面筋不是加进去的,是揉出来的——反复折叠、拉伸让蛋白质分子重新排列。语言模型也一样,预测下一个词看起来是一个非常简单的任务,但要在海量文本上反复做这件事,模型就不得不学会词法、句法、事实关联、逻辑推理等大量能力,因为这些能力都能帮助降低预测损失。
这就是“涌现”的朴素解释:能力不是被显式编程进去的,而是在足够大的模型上、足够多的文本上,通过预测下一个词这个简单任务被迫发展出来的。
3.1 分布式训练:单卡烤不动
预训练阶段的数据量通常是 TB 级,单张 GPU 根本装不下。工程上需要分布式训练,常见策略有:
- 数据并行(Data Parallelism):每张卡持有完整模型副本,处理不同 batch,梯度同步。
- 张量并行(Tensor Parallelism):把单个 Transformer 层的矩阵拆分到多张卡上。
- 流水线并行(Pipeline Parallelism):把不同层放到不同卡上,数据像流水线一样流过。
- ZeRO / DeepSpeed 优化:把优化器状态、梯度、参数分片存储,降低显存占用。
这已经是训练框架层面的问题了。对初学者来说,不需要立刻掌握全部细节,但要明白:预训练依赖大规模分布式并行和极高的数据工程能力,这正是个人开发者几乎不可能复现的环节。这也是为什么现在业界的标准路径不是从零预训练,而是“加载开源基座模型 + 领域微调”。
代码层面,如果用 Transformers 搭配 Accelerate,可以很直观地看到数据并行的抽象:
# 文件路径:train_pretrain.py # 一个简化的预训练训练循环示例 import torch from torch.utils.data import DataLoader from transformers import AutoModelForCausalLM, get_cosine_schedule_with_warmup from accelerate import Accelerator accelerator = Accelerator() model_path = "your-base-model-path" # 实际项目替换 model = AutoModelForCausalLM.from_pretrained(model_path) train_dataloader = DataLoader(lm_dataset["train"], batch_size=32, shuffle=True) optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5) scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=100, num_training_steps=len(train_dataloader) * 3, ) model, optimizer, train_dataloader, scheduler = accelerator.prepare( model, optimizer, train_dataloader, scheduler ) model.train() global_step = 0 for epoch in range(3): for batch in train_dataloader: batch = {k: v.to(accelerator.device) for k, v in batch.items()} outputs = model(input_ids=batch["input_ids"], labels=batch["input_ids"]) loss = outputs.loss accelerator.backward(loss) optimizer.step() scheduler.step() optimizer.zero_grad() global_step += 1 if global_step % 100 == 0: print(f"step {global_step} loss {loss.item():.4f} lr {scheduler.get_last_lr()[0]:.2e}") # 每个 epoch 结束保存一次检查点 accelerator.save_state(f"./checkpoints/epoch_{epoch}")这里需要强调:labels=batch["input_ids"]这一行的含义是让模型把「预测下一个 token」作为目标,模型内部会自动把标签左移一位。新手经常在这里写错,导致 loss 不为负却学不到东西。
3.2 预训练时期望看到的 loss 变化
预训练过程中,loss 的典型变化模式是:前期快速下降,中期缓慢下降,后期进入平台期。如果 loss 出现以下异常,要立刻停止排查:
- loss 为 NaN:学习率过高或数据里有异常值,立即降低学习率或检查数据。
- loss 不下降:数据 pipeline 有问题,比如 labels 与 input_ids 错位。
- loss 下降后反弹:学习率调度异常,或 batch size 太小导致梯度不稳定。
4. 整形与二次发酵:继续预训练与领域适配
面包第一次发酵后要排气、整形,有时还会加入不同配料。LLM 训练里对应的环节是“二次训练”:在通用基座模型的基础上,用特定领域数据继续训练。
这有几种常见叫法:
- 继续预训练(Continued Pretraining / Domain-Adaptive Pretraining)
- 增量预训练(Incremental Pretraining)
它的目的是让模型更熟悉某个领域的语言风格和知识分布,比如代码、医学、法律、金融。实操上和预训练几乎一样,只是数据从通用语料换成了领域语料,学习率通常调小。
为什么需要这一步?因为通用模型虽然在大量文本上训练过,但领域文本的分布和通用文本差别很大。比如代码数据的特点是结构化强、括号和缩进重要、错误信息频繁出现;法律数据则有大量固定句式,这些分布特征需要在领域数据上继续训练才能被强化。
继续预训练的关键经验:
- 学习率要比预训练更小,通常用预训练的 1/10 甚至更低。
- 混合通用数据,防止“灾难性遗忘”(后面会讲)。
- 这个阶段的评估不是看对话能力,而是看困惑度(Perplexity)和下游任务表现。
5. 入炉烘烤:监督微调(SFT)让模型学会“说话”
面包经过整形后,终于可以入炉了。在 LLM 训练中,监督微调是让模型从“会预测下一个词”变成“会回答用户问题”的关键烘烤步骤。
微调和预训练的区别,用一个表格就能说清楚:
| 维度 | 预训练 | 监督微调(SFT) |
|---|---|---|
| 数据来源 | 海量无标注文本 | 人工标注的指令-回答对 |
| 数据量 | TB 级 | 数千到百万条 |
| 目标 | 学习语言规律和世界知识 | 学会遵循指令、对齐人类偏好 |
| 训练时间 | 数周到数月 | 数小时到数天 |
| 模型状态 | 基座模型 | 指令微调后的助手模型 |
SFT 的数据格式非常重要。现在开源生态里通用的格式是对话模板,例如 ChatML:
<|im_start|>system 你是一个有帮助的助手。<|im_end|> <|im_start|>user 介绍一下大模型训练的基本流程。<|im_end|> <|im_start|>assistant 大模型训练通常包含预训练、微调和对齐三个阶段……<|im_end|>训练时,只有 assistant 回答部分的 token 才参与 loss 计算,system 和 user 部分应该被 mask 掉。这个细节直接决定模型能不能学会“对话”而不是重复整个模板。
用代码实现时,可以用 Transformers 的DataCollatorForCompletionOnlyLM,它专门处理这种“只对回答部分计算损失”的场景:
# 文件路径:train_sft.py from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForCompletionOnlyLM, ) from datasets import Dataset tokenizer = AutoTokenizer.from_pretrained("your-base-model-path") tokenizer.pad_token = tokenizer.eos_token train_data = [ { "conversation": [ {"role": "user", "content": "什么是 Transformer?"}, {"role": "assistant", "content": "Transformer 是一种基于自注意力机制的神经网络架构,最早由 Vaswani 等人于 2017 年提出。"}, ] }, # ... 实际项目请准备更多数据 ] def format_conversation(example): prompt = "" conv = example["conversation"] for i, turn in enumerate(conv): if turn["role"] == "user": prompt += f"<|im_start|>user\n{turn['content']}<|im_end|>\n" else: prompt += f"<|im_start|>assistant\n{turn['content']}<|im_end|>\n" prompt += "<|im_start|>assistant\n" # 这里需要把目标文本也拼接出来,供 DataCollator 计算 completion 位置 response = conv[-1]["content"] return {"prompt": prompt, "completion": response} formatted_data = [format_conversation(item) for item in train_data] dataset = Dataset.from_list(formatted_data) def tokenize_with_response(example): prompt_ids = tokenizer.encode(example["prompt"], truncation=True, max_length=1024) completion_ids = tokenizer.encode(example["completion"], add_special_tokens=False) input_ids = prompt_ids + completion_ids + [tokenizer.eos_token_id] labels = [-100] * len(prompt_ids) + completion_ids + [tokenizer.eos_token_id] return {"input_ids": input_ids, "labels": labels} tokenized_dataset = dataset.map(tokenize_with_response, remove_columns=["prompt", "completion"]) data_collator = DataCollatorForCompletionOnlyLM( response_template="<|im_start|>assistant\n", tokenizer=tokenizer, ) training_args = TrainingArguments( output_dir="./sft_model", per_device_train_batch_size=4, gradient_accumulation_steps=8, learning_rate=2e-5, num_train_epochs=3, logging_steps=10, save_strategy="epoch", report_to="none", ) trainer = Trainer( model=AutoModelForCausalLM.from_pretrained("your-base-model-path"), args=training_args, train_dataset=tokenized_dataset, data_collator=data_collator, ) trainer.train() trainer.save_model("./sft_model_final")这段代码的核心在于labels列表中的-100。PyTorch 的交叉熵损失会把-100位置的 token 忽略掉,这样模型在训练时只需要预测 assistant 的回答部分,而不是去预测用户的提问。
这里的response_template="<|im_start|>assistant\n"是告诉 DataCollator 从哪个标记开始才算 completion,这样它就能自动构造出正确的labels。
很多新手在微调时遇到同一个问题:模型训练完 loss 很低,但对话时只会把用户的问题和回答一起复述出来。这个现象的根源通常就是 label 没有 mask,模型把“预测用户问题”也当成了学习目标。
如果你用的是 LLaMA 系模型,官方没有 ChatML 模板,需要把response_template换成模型对应的 assistant 起始标记,同时把tokenizer.add_special_tokens处理到位。
5.1 微调数据的关键认知
SFT 的数据质量比数据量更重要。业内不少团队的实践经验是:几千条高质量、覆盖多任务的指令数据,效果往往好于几万条低质量、模板雷同的数据。
高质量 SFT 数据应满足:
- 指令多样,覆盖真实用户使用场景。
- 回答准确、信息密度高、逻辑清晰。
- 避免大量“复读机式”的问答对。
- 注意安全性和价值观对齐,不包含违规内容。
6. 出炉与试吃:评估、困惑度与人工评测
面包出炉后要先冷却、切开试吃,才知道火候对不对。LLM 训练也是如此,训练结束时必须做系统评估,而且要在训练过程中就建立评估机制。
6.1 客观指标
- Perplexity(困惑度):语言模型对测试数据的负对数似然的指数形式。困惑度越低,说明模型对文本的预测能力越强。但要注意,困惑度低不代表对话能力强,它只是基础能力的指标。
- Loss(训练损失):监控训练过程用。
- 下游任务指标:比如代码生成的 pass@k、数学推理的准确率、阅读理解 EM/F1 等。
6.2 人工评测
真正的对话质量,必须靠人工评测。实践中通常会设计一个评测集,包含几百到几千条覆盖不同能力的 prompt,然后让多个评测者给模型回答打分,对比不同训练版本的优劣。
现在也有很多自动评测框架可以辅助,但最终人工抽检不可替代,尤其是对安全性和语气这类难以量化的维度。
6.3 一个简单的评估脚本
# 文件路径:evaluate_model.py from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_path = "./sft_model_final" tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto") model.eval() def generate(prompt, max_new_tokens=200): messages = [{"role": "user", "content": prompt}] input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(input_text, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.7, top_p=0.9, ) return tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) test_prompts = [ "用一句话解释什么是注意力机制。", "写一个 Python 函数,计算斐波那契数列。", "大模型训练分为哪几个阶段?", ] for prompt in test_prompts: print(f"Prompt: {prompt}") print(f"Answer: {generate(prompt)}") print("-" * 50)运行后,逐个检查回答是否流畅、准确、符合指令。这里要特别提醒:评估时必须设置temperature和top_p等采样参数,固定评估方式和参数,否则同一 prompt 每次生成结果可能不同,不利于对比版本效果。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练 loss 不下降 | labels 与 input_ids 错位,模型在预测自己的输入而非下一 token | 检查数据预处理,打印一个 batch 的 input_ids 和 labels 对比 | 修正 labels 构造逻辑,或使用 DataCollatorForCompletionOnlyLM |
| loss 变成 NaN | 学习率过高、数据中存在异常值、混合精度训练精度溢出 | 查看 loss 变 NaN 前的日志和当时的输入数据 | 降低学习率;检查数据;关闭 fp16 或改用 bf16 |
| 显存不足(OOM) | batch size 过大、序列过长、优化器状态占用太大 | 查看报错栈,确认是模型参数还是激活值占满显存 | 减小 batch size、开启梯度累积、使用 LoRA/QLoRA、使用 DeepSpeed ZeRO |
| 微调后“复读机”现象 | 没有对 prompt 部分做 label mask | 打印 labels 中的 -100 位置是否正确覆盖 user 部分 | 使用 response_template 自动 mask 回答部分 |
| 灾难性遗忘:微调后通用能力下降 | 微调数据分布和预训练分布差异过大,或学习率过高、训练轮数过多 | 在通用评测集上对比基座模型和微调模型 | 混合 5%-20% 通用数据;降低学习率;控制 epoch;使用参数高效微调 |
| 评测集和训练集重叠 | 数据泄漏,评测结果虚高 | 计算训练集和评测集的 n-gram 重叠率 | 构建评测集时做相似度去重排除训练数据 |
| 生成结果重复单调 | 温度过低、top_p 过小、模型未收敛 | 尝试不同采样参数组合 | 调整 temperature / top_p / repetition_penalty |
这里单说两个高频问题。
灾难性遗忘是微调中最常见的隐性坑。模型在领域数据上训练太久,可能会遗忘预训练阶段学到的通用能力。解决思路不是“尽量多训”,而是“控制训练强度”:降低学习率、减少 epochs、混合通用数据、优先选择 LoRA 这类参数高效微调方法。
OOM 的排查逻辑也要说清楚。很多新手第一反应是把 batch size 调小,但有时真正的问题是优化器状态太大。AdamW 优化器每个参数要维护两份额外状态,单个 float32 参数实际占用显存可能是参数本身的 3 倍以上。这时用 AdamW 的 8-bit 版本或换用 Adafactor,效果立竿见影。
8. 工程最佳实践:从“能跑”到“可维护”
训练模型和做面包一样,最大的坑往往不在配方本身,而在流程管理。下面这几条实践经验,是比任何单一技巧都重要的。
8.1 用 LoRA 降门槛
如果你在单个消费级显卡上做微调,不要直接训练全部参数,优先选择 LoRA。
LoRA 的核心思路是冻结原始权重,只训练一小部分低秩增量矩阵。这能把可训练参数量降低到原来的 1% 甚至更低,显存占用大幅下降。用 PEFT 库实现非常方便:
# 文件路径:train_lora.py from peft import LoraConfig, get_peft_model, TaskType lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, # 低秩矩阵的秩 lora_alpha=32, # 缩放系数 target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.1, ) model = AutoModelForCausalLM.from_pretrained("your-base-model-path") model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 输出示例:trainable params: 4,194,304 || all params: 1,234,567,890 || trainable%: 0.34LoRA 的额外好处是可插拔。训练完的增量矩阵只有几十到几百 MB,可以单独保存、加载、切换,多个 LoRA 模块可以复用到同一个基座模型上,很适合团队内部做多业务场景的模型复用。
8.2 实验管理:每次训练都要“可复现”
面包师傅会记录每次的配方、温度、时间,模型训练更应该如此。实践建议:
- 每次实验记录:数据版本(hash 或 commit id)、代码 commit、超参数、训练日志、评测结果。
- 使用实验管理工具(如 Weights & Biases、MLflow、TensorBoard),或至少用一个统一的实验记录表格。
- 保存检查点时,同时保存 tokenizer 和配置文件,确保模型可以独立加载。
8.3 评估先行:还没训练就要想好怎么评测
这是最有价值的一条建议。很多人训练完模型才开始设计评测,结果发现评测集不完善,只能重新训练。正确做法是:
- 训练开始前先构建评测集。
- 用基座模型跑一遍生成结果作为 baseline。
- 每次微调后在同一批 prompt 上对比。
如果没有 baseline,你无法判断模型是变好了还是变差了。很多“微调后效果变差”的结论,其实是因为根本没有对比对象。
8.4 节省资源的小技巧
- 用梯度累积替代直接放大 batch size。
- 优先用 bf16 而不是 fp16,bf16 在训练稳定性上更优。
- 检查点保存不要每步都存,用
save_strategy="epoch"或按步数间隔保存。 - 训练结束后用
model = model.merge_and_unload()合并 LoRA 权重,部署时零额外开销。
9. 总结:烤箱还是那台烤箱,配方和手艺才是核心
用烘焙来理解 LLM 训练,学到的不是比喻本身,而是流程思维。
- 数据是面粉,决定模型能力的上限。
- 预训练是揉面和发酵,把语言规律揉进参数。
- 继续预训练是整形和二次发酵,让模型适配领域。
- 微调是入炉烘烤,让模型学会对话和指令遵循。
- 评估是试吃,没有评估的训练等于闭着眼睛烤面包。
- 灾难性遗忘是烤过头,OOM 是烤箱塞太满,loss 不降是配方抄错了。
对于正在入门 LLM 训练的同学,我的实战建议是:不要急着从零预训练大模型,也不要上来就追最新的训练框架。先用中文小规模数据跑通一个最小的预训练 + 微调流程,把数据 pipeline、loss 变化、评估方法、领域适配这几个环节全部走一遍,然后再去学习分布式训练和 RLHF。有了完整的流程视角,你再去看任何一篇训练相关的论文或代码,都会清楚它在整个流程里处于哪个位置。
下一步值得深入的方向有三个:一是继续预训练时如何解决灾难性遗忘;二是 SFT 数据配比到底怎么设计才更优;三是 RLHF 中的奖励模型如何评估,它和 SFT 优化目标的差异在哪里。这三块都是通往更高质量助手模型的必经之路,也可以作为你接下来的动手实践主题。
烘焙的关键不是记住一个面包配方,而是理解每一种原料和每一步操作对成品的影响;训练模型也是如此。把这份“配方思维”建立起来,你就已经从“调参侠”迈向了真正理解大模型引擎的开发者。