拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零训练大模型全流程实战:数据准备、预训练、SFT、DPO与评估

从零训练大模型全流程实战:数据准备、预训练、SFT、DPO与评估

1. 从零训练大模型到底在练什么

很多人第一次听到“从零训练一个大模型”,脑子里浮现的画面是几十号人围着一堆显卡忙活大半年。这个印象不算错,但也不全对。真实情况是,从零训练这四个字里,“零”的定义决定了你后面所有工作的量级。如果你说的“零”是指连模型结构都自己设计、词表自己造、数据从互联网上生爬,那确实是一个团队级别的工程;但如果你说的“零”是指基于开源基座模型,走完数据构建、继续预训练、SFT、偏好对齐、评估这一整条链路,那一个人或者一个小团队完全可以在有限资源下跑通全流程。

我自己走过一遍这条路,踩过的坑比想象中多得多。这篇文章不打算给你画大饼,也不打算只讲概念。我会把数据准备、预训练、SFT、DPO/RLHF、评估这五个阶段拆开,每个阶段告诉你实际要做什么、为什么这么做、参数怎么定、哪里容易翻车。适合有一定Python和深度学习基础、想系统了解大模型训练全流程的人,也适合已经在做微调但没走过完整链路的从业者。

先说一个核心认知:大模型训练的本质是分布对齐。你手里的数据决定了模型学到的分布,你的训练目标决定了模型往哪个方向偏移。预训练是让模型学会“世界长什么样”,SFT是让模型学会“人希望我怎么回答”,DPO/RLHF是让模型学会“哪种回答更好”。三个阶段的目标不同,数据格式不同,训练策略也不同。很多人微调效果不好,根本原因不是参数没调对,而是数据分布和训练目标不匹配。

下面这张表是我自己总结的五个阶段核心对比,先建立一个全局观:

阶段目标数据量级典型算力关键指标
数据准备构建高质量语料1B~10T tokensCPU为主去重率、困惑度
预训练学习语言与世界知识10B~1T tokens多卡GPULoss、PPL
SFT学会指令跟随10K~1M条单机多卡指令遵循率
DPO/RLHF对齐人类偏好5K~100K对单机多卡胜率、KL散度
评估验证能力边界基准集推理卡准确率、鲁棒性

这张表不是让你照搬,而是让你知道每个阶段大概在什么量级上工作。接下来我逐个拆解。

2. 数据准备:决定模型上限的隐形工程

2.1 数据来源与采集策略

数据是大模型的天花板,这句话已经被说烂了,但真正做的时候你会发现,采集策略比数据量更重要。我见过太多人一上来就爬了几百G文本,结果清洗完只剩不到10G能用。问题出在源头:没有按目标能力反推数据需求。

如果你要做的是通用中文大模型,数据来源大概分这几类:

  • 网页文本:覆盖面广,但噪声极大,需要严格清洗
  • 书籍与论文:质量高,但版权和格式转换是问题
  • 代码仓库:对逻辑推理和结构化输出有帮助
  • 百科与问答:事实性强,适合知识类任务
  • 对话数据:直接用于SFT阶段,预训练阶段少量混入即可

我的经验是,预训练阶段的数据配比大概是:网页60%、书籍15%、代码15%、百科问答10%。这个比例不是固定的,如果你的模型偏重代码能力,代码比例可以拉到30%以上。但要注意,代码数据过多会让模型在自然语言生成上变得“机械”,输出风格偏向代码注释。

采集的时候有一个容易被忽略的点:时间戳和来源标记。每一条数据都要记录它从哪里来、什么时候抓的、原始URL是什么。这不是为了合规审计,而是为了后续做数据消融实验。当你发现模型在某类任务上表现差时,你可以快速定位是不是某批数据出了问题。

2.2 清洗与去重的实操细节

清洗这一步,我踩过最大的坑是过度清洗。早期我用了一套非常激进的规则,把包含特殊符号、长度异常、重复字符的文本全删了,结果训练出来的模型在标点使用和长文本生成上表现很差。后来才明白,噪声本身也是分布的一部分,你要去掉的是有害噪声,不是所有非常规文本。

我的清洗流程大概是这样:

  1. 基础过滤:去掉HTML标签、乱码、控制字符
  2. 长度过滤:保留50~100000字符的文本,太短的没信息量,太长的可能是拼接错误
  3. 重复检测:用MinHash+LSH做近似去重,阈值设在0.8左右
  4. 质量打分:用一个小模型对文本流畅度打分,低于阈值的丢弃
  5. 敏感内容过滤:这个不用多说,必须做

去重这块我要多说一句。精确去重不够,近似去重才是关键。网页上大量内容是转载和改写的,精确哈希根本抓不到。我用的是MinHash加LSH,先做shingling(按n-gram切分),再算Jaccard相似度。实测下来,这一步能去掉30%以上的冗余数据,对训练效率提升非常明显。

# 近似去重核心逻辑示意 from datasketch import MinHash, MinHashLSH def deduplicate(texts, threshold=0.8): lsh = MinHashLSH(threshold=threshold, num_perm=128) minhashes = {} for i, text in enumerate(texts): m = MinHash(num_perm=128) for shingle in get_shingles(text, k=5): m.update(shingle.encode('utf8')) lsh.insert(f"doc_{i}", m) minhashes[f"doc_{i}"] = m # 后续查询重复项并移除 return list(set(texts))

注意:去重阈值不要设太高,0.9以上会漏掉很多改写内容;也不要设太低,0.6以下会把正常的不同文本误删。0.75~0.85是我实测比较稳的区间。

2.3 数据格式与Tokenization

数据清洗完,下一步是Tokenization。这里有一个关键决策:用现成的tokenizer还是自己训一个。如果你是基于开源模型做继续预训练,直接用原模型的tokenizer,不要换。换tokenizer意味着embedding层要重新训练,代价太大。如果你是从零开始,那可以考虑用SentencePiece或BPE在你自己数据上训一个,词表大小一般在32000~128000之间。

我自己的选择是:中文为主的数据,词表设50000左右;中英混合的,设80000~100000。词表太小会导致长词被切碎,序列变长,训练变慢;词表太大则embedding参数过多,小数据集上容易过拟合。

Tokenize完之后,把所有数据打包成固定长度的序列。这里有个技巧:不要按文档边界截断,而是把所有token拼起来再按max_length切。这样能保证每个训练样本都是满的,不会浪费算力。但要注意在文档之间插入EOS token,让模型知道边界。

# 数据打包示意 def pack_sequences(tokenized_docs, max_length=2048): all_tokens = [] for doc in tokenized_docs: all_tokens.extend(doc) all_tokens.append(EOS_TOKEN_ID) sequences = [] for i in range(0, len(all_tokens), max_length): seq = all_tokens[i:i+max_length] if len(seq) == max_length: sequences.append(seq) return sequences

3. 预训练:让模型学会世界知识

3.1 模型结构选型与参数设定

预训练阶段,模型结构的选择基本决定了你后面的路好不好走。目前主流就三条路:Decoder-only、Encoder-Decoder、Encoder-only。做生成任务,选Decoder-only;做理解任务,Encoder-only够用;Encoder-Decoder适合翻译和摘要这类序列到序列的任务。

我假设你要做的是通用生成模型,那就走Decoder-only路线。结构上直接用Transformer的变体,但有几个参数需要你根据算力做取舍:

参数小规模(1B以下)中规模(1B~7B)大规模(7B以上)
层数12~2424~3232~80
隐藏维度768~15362048~40964096~8192
注意力头数12~1616~3232~64
词表大小32000~5000050000~8000080000~128000
上下文长度1024~20482048~40964096~32768

这张表是经验值,不是铁律。关键原则是:在算力允许范围内,优先加深而不是加宽。深层模型在复杂推理上表现更好,但训练难度也更大,需要残差连接和归一化策略配合。

还有一个容易忽略的点:位置编码。绝对位置编码在长文本上外推能力差,现在主流用RoPE(旋转位置编码)或者ALiBi。RoPE实现简单,外推性也不错,我推荐优先用RoPE。

3.2 训练目标与Loss计算

预训练的目标函数就是标准的自回归语言建模:给定前n个token,预测第n+1个token。Loss用交叉熵,公式很简单:

$$L = -\frac{1}{N}\sum_{i=1}^{N} \log P(x_i | x_{<i})$$

但实际训练的时候,有几个细节决定成败:

第一,Loss masking。如果你在数据里混入了padding token,计算Loss的时候要把padding位置mask掉,否则模型会学会预测padding,浪费容量。

第二,梯度累积。小显存跑大模型,必须用梯度累积。比如你想用batch size 512,但单卡只能放8,那就累积64步再更新一次。注意,梯度累积等价于大batch,但BatchNorm统计量不会同步更新,不过Transformer一般用LayerNorm,这个问题不大。

第三,学习率调度。预训练阶段用warmup+cosine decay是最稳的。warmup步数一般设总步数的1%~5%,峰值学习率在1e-4到3e-4之间。太大容易发散,太小收敛慢。

# 学习率调度示意 def get_lr(step, warmup_steps, total_steps, peak_lr): if step < warmup_steps: return peak_lr * step / warmup_steps progress = (step - warmup_steps) / (total_steps - warmup_steps) return peak_lr * 0.5 * (1 + math.cos(math.pi * progress))

3.3 分布式训练与显存优化

预训练绕不开分布式。单卡能跑的模型规模有限,7B以上的模型基本都要多卡。分布式策略有三种:数据并行、张量并行、流水线并行。数据并行最简单,每张卡放完整模型,数据切分;张量并行把矩阵乘法切到多卡;流水线并行把不同层放到不同卡。

我自己的经验是:7B以下用数据并行+ZeRO就够,7B以上考虑张量并行+流水线并行。ZeRO是DeepSpeed提出的显存优化技术,把优化器状态、梯度、参数分片到多卡,能大幅降低单卡显存占用。

显存优化还有几个实用技巧:

  • 混合精度训练:用bf16而不是fp16,bf16动态范围大,不容易溢出
  • 梯度检查点:用时间换显存,适合深层模型
  • Offload:把优化器状态放到CPU内存,进一步省显存,但会慢

提示:混合精度训练时,Loss scaling很关键。bf16一般不需要动态scaling,fp16需要。如果你用fp16发现Loss变成NaN,先检查scaling策略。

3.4 预训练阶段的常见翻车点

预训练阶段我遇到过几次比较严重的翻车,这里列出来给你避坑:

Loss突然飙升:大概率是学习率太大或者数据里有异常样本。先降学习率,再检查数据。

模型输出重复:这是典型的“复读机”现象,原因是训练不充分或者数据重复度过高。增加数据多样性,延长训练时间。

显存溢出:先降batch size,再开梯度检查点,最后考虑Offload。不要一上来就改模型结构。

训练速度慢:检查数据加载是不是瓶颈。用prefetch和num_workers加速,把数据预处理放到GPU上做。

4. SFT:让模型学会听人话

4.1 指令数据的构建与格式

预训练完的模型,你问它“今天天气怎么样”,它可能会续写“今天天气怎么样,明天天气怎么样”。因为它只学会了预测下一个词,没学会“回答问题”。SFT就是教它这个。

SFT数据的格式一般是指令-输入-输出三元组。比如:

{ "instruction": "把下面的句子翻译成英文", "input": "今天天气很好", "output": "The weather is nice today." }

但实际构建的时候,格式可以更灵活。我常用的是对话格式,把多轮对话拼成一个序列,用特殊token分隔角色:

<|user|>今天天气怎么样<|assistant|>今天天气很好,适合出门。<|end|>

这种格式的好处是,模型能学会多轮交互,而且推理的时候直接按同样格式拼接就行。

数据量方面,SFT不需要太多数据,但质量要求极高。我自己的经验是,10000~50000条高质量指令数据,就能让模型在常见任务上表现不错。关键是覆盖度:任务类型要全,回答风格要一致,不能有错误示范。

4.2 训练策略与超参选择

SFT的训练策略和预训练有本质区别。预训练是从头学,SFT是在已有知识上做微调。所以:

  • 学习率要小:预训练用1e-4,SFT用1e-5到5e-5
  • 训练轮数要少:1~3个epoch足够,多了容易过拟合
  • Loss只算输出部分:指令部分的Loss要mask掉,只计算模型生成的回答部分的Loss
# SFT Loss masking示意 def compute_sft_loss(logits, labels, response_mask): shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() shift_mask = response_mask[..., 1:].contiguous() loss_fct = CrossEntropyLoss(reduction='none') loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) loss = loss * shift_mask.view(-1) return loss.sum() / shift_mask.sum()

还有一个关键点:数据配比。SFT数据里,不同任务类型的比例要控制好。如果翻译数据占80%,模型就会偏向翻译,其他任务表现下降。我一般让通用对话占50%,专项任务各占10%~20%。

4.3 SFT阶段的实操心得

SFT阶段有几个坑,我踩过之后才明白:

第一,不要用预训练的Loss曲线判断SFT效果。SFT的Loss降到1以下很正常,但不代表模型真的好。要看生成质量,不是看Loss。

第二,数据质量比数量重要十倍。我试过用10万条低质量数据,效果不如1万条精标数据。低质量数据里的错误模式会被模型学去,而且很难纠正。

第三,SFT之后模型会变“窄”。预训练模型知识面广,SFT之后在特定任务上强了,但通用能力可能下降。这是灾难性遗忘。缓解方法是混入少量预训练数据,或者用LoRA做微调,只更新部分参数。

注意:SFT阶段如果发现模型输出变得非常模板化,大概率是数据里重复模式太多。检查你的数据,增加多样性。

5. DPO与RLHF:对齐人类偏好

5.1 RLHF的三步走流程

RLHF(基于人类反馈的强化学习)是让模型输出更符合人类偏好的关键步骤。经典RLHF分三步:

  1. SFT:先让模型学会基本指令跟随
  2. 奖励模型训练:收集人类对模型输出的偏好排序,训练一个奖励模型
  3. PPO强化学习:用奖励模型指导SFT模型优化

这个流程很有效,但工程复杂度极高。PPO训练不稳定,超参敏感,奖励模型容易过拟合。我自己的体验是,PPO调参的时间比训练模型本身还长。

奖励模型的训练数据是偏好对:给定同一个prompt,人类标注哪个回答更好。奖励模型的目标是学会给更好的回答打高分。Loss用pairwise ranking loss:

$$L = -\log(\sigma(r(x, y_w) - r(x, y_l)))$$

其中$y_w$是更好的回答,$y_l$是更差的回答。

5.2 DPO:更简单的替代方案

DPO(直接偏好优化)是最近两年非常火的方法,它跳过了奖励模型,直接用偏好数据优化语言模型。数学上,DPO证明了最优策略和奖励函数之间存在解析关系,所以不需要显式训练奖励模型。

DPO的Loss长这样:

$$L_{DPO} = -\log\sigma(\beta \log\frac{\pi_\theta(y_w|x)}{\pi_{ref}(y_w|x)} - \beta \log\frac{\pi_\theta(y_l|x)}{\pi_{ref}(y_l|x)})$$

其中$\pi_\theta$是当前模型,$\pi_{ref}$是参考模型(通常是SFT后的模型),$\beta$控制偏离参考模型的程度。

DPO的优势很明显:训练稳定、实现简单、不需要奖励模型。我实测下来,DPO在大多数场景下效果和RLHF相当,但训练时间少一半以上。

# DPO Loss核心实现 def dpo_loss(policy_logps, ref_logps, beta=0.1): chosen_logps, rejected_logps = policy_logps chosen_ref, rejected_ref = ref_logps pi_logratios = chosen_logps - rejected_logps ref_logratios = chosen_ref - rejected_ref logits = pi_logratios - ref_logratios loss = -F.logsigmoid(beta * logits) return loss.mean()

5.3 DPO/RLHF的数据准备与训练细节

偏好数据的质量直接决定对齐效果。我构建偏好数据的时候,遵循几个原则:

  • 同一个prompt下的两个回答要有明显质量差异,不能模棱两可
  • 偏好标注要一致,不同标注者的标准要统一
  • 数据要覆盖多种任务类型,不能只集中在某一类

数据量方面,5000~20000对偏好数据就能有不错的效果。关键是质量,不是数量。

训练的时候,$\beta$参数很关键。$\beta$太小,模型偏离参考模型太多,可能输出退化;$\beta$太大,模型学不到新东西。我一般从0.1开始试,根据KL散度调整。

提示:DPO训练时监控KL散度,如果KL超过10,说明模型偏离参考模型太远,需要降低学习率或增大$\beta$。

6. 评估:怎么知道模型到底行不行

6.1 自动评估与基准测试

模型训完了,怎么判断好坏?自动评估是最直接的方式。常用的基准有:

基准考察能力适用阶段
MMLU多任务知识预训练后
HellaSwag常识推理预训练后
GSM8K数学推理SFT后
HumanEval代码生成SFT后
MT-Bench多轮对话DPO后
AlpacaEval指令跟随DPO后

这些基准跑起来不难,但不要只看分数。我见过模型在MMLU上分数很高,但实际对话一塌糊涂。原因是基准测试和真实使用场景有分布差异。

自动评估还有一个坑:数据污染。如果你的训练数据里包含了基准测试的题目,分数会虚高。做评估之前,一定要用n-gram匹配检查训练数据里有没有基准题目。

6.2 人工评估与A/B测试

自动评估不够,人工评估来凑。人工评估一般做** pairwise比较**:给评估者同一个prompt下的两个模型输出,让他们选哪个更好。这种方式比打分更可靠,因为人类对绝对分数不敏感,但对相对好坏很敏感。

我做人工评估的时候,会控制几个变量:

  • 评估者要多样化,不能全是团队成员
  • prompt要覆盖真实场景,不能全是测试集里的
  • 盲测,评估者不知道哪个是哪个模型

A/B测试是上线前的最后一道关。把新模型和旧模型同时部署,让真实用户用,看留存、满意度、任务完成率。这一步能发现很多离线评估发现不了的问题。

6.3 评估阶段的避坑指南

评估阶段有几个坑,我踩过之后总结如下:

第一,不要用训练Loss判断模型好坏。Loss低不代表生成质量高,这是两回事。

第二,不要只用一个基准。不同基准考察不同能力,要组合使用。

第三,不要忽略推理速度。模型再好,推理慢也没法用。评估的时候要测TTFT(首token延迟)和TPOT(每token延迟)。

第四,不要忘了安全评估。模型会不会输出有害内容,会不会被诱导,这些都要测。

7. 全流程串讲与资源规划

7.1 从零到一的完整时间线

把五个阶段串起来,一个完整的训练流程大概是这样:

  1. 数据准备:2~4周,取决于数据源和清洗复杂度
  2. 预训练:1~4周,取决于模型规模和算力
  3. SFT:3~7天,数据量小,训练快
  4. DPO/RLHF:3~7天,和SFT差不多
  5. 评估:1~2周,包括自动评估和人工评估

总计大概2~3个月,这是小团队跑通全流程的时间。如果算力充足,可以压缩到1个月以内。

7.2 算力与成本估算

算力是硬约束。我按7B模型估算一下:

  • 预训练:需要约100~500 GPU天(A100级别),取决于数据量
  • SFT:需要约10~50 GPU天
  • DPO:需要约10~30 GPU天
  • 评估:需要约5~10 GPU天

如果用云算力,按A100每小时2~3美元算,总成本大概在2万~10万美元之间。如果用自己的卡,成本主要是电费和折旧。

提示:小团队可以从1B~3B模型开始练手,算力需求降一个数量级,流程完全一样。

7.3 给小团队和个人的建议

如果你是一个人或者小团队,我建议:

  • 不要一上来就搞7B,从1B开始,跑通流程最重要
  • 数据质量优先,宁可少而精,不要多而杂
  • SFT和DPO是性价比最高的环节,预训练可以基于开源模型做继续预训练
  • 评估要贯穿始终,不要等训完再评估
  • 记录每一次实验,参数、数据、结果都要记,不然回头根本不知道哪个配置有效

我自己走过一遍之后最大的体会是:大模型训练不是算法问题,是工程问题。算法论文里那些公式看起来很优雅,但实际跑起来,80%的时间花在数据处理、显存优化、训练稳定性上。把工程细节做好,比追新算法重要得多。

最后分享一个小技巧:训练之前先跑一个tiny版本。用1%的数据、1%的模型规模,把整个流程跑一遍,确认没有bug再上全量。这一步能帮你省下大量时间和算力。

返回列表