如果三年前有人告诉我,我会为了搞懂AI工程而亲手从零训练一个语言模型,再让它学会"推理",我一定觉得他是在开玩笑。但事实是,当我想搞清楚"注意力机制为什么有效""Loss降到多少才算正常""加一层残差到底影响什么"这些问题时,发现所有现成框架都在替我遮住答案。于是我决定走一条笨路:从数据清洗、分词器实现、Transformer骨架、训练调度到评测,全链路自己动手搭一遍。这篇文章就是这条路的完整复盘,适合那些不想只做"调包侠"、想真正理解大语言模型构建与推理模型训练细节的人。
这轮折腾大概持续了两个月,中间翻过车、断电丢过检查点、也看着Loss毫无悬念地发散过。但把这些过程写下来之后,我觉得最值得分享的反而不是什么惊艳的结果,而是那些"原来这一步是这么回事"的瞬间。接下来按实际推进顺序,把整套从零构建AI工程管线的思路、参数和踩坑经验一次说清楚。
1. 为什么我坚持"从零开始"而不是直接调包
1.1 跑通Demo带来的错觉
市面上的大语言模型教程,绝大多数教的是"加载预训练权重,写三行代码,跑一个推理"。我一开始也觉得这就算入门了。但有一次我想调整模型对某个特定领域的长文本理解能力,试了几种办法都没效果,回头一看,我连模型前向传播里数据究竟怎么流动的都没搞明白。这时候才意识到,跑通Demo只是"使用AI",离"做AI工程"还差着十万八千里。
"从零开始"在这个语境里,不是为了证明自己有多硬核,而是为了把黑箱一个一个拆开。当你亲手写过一次自注意力、亲手做过一次分词表的合并、亲手调过一次学习率,后面再遇到问题,你能猜到一个大致的排查方向,而不是只能上网搜"为什么我的模型输出一堆乱码"。
1.2 "从零"到底指的是哪三个层面
第一个层面是数据。真正去构建一个像样的训练语料,而不是拿现成的jsonl跑一遍了事。你需要理解数据规模、清洗规则和分布都会直接影响模型能力。
第二个层面是模型骨架。我选择自己实现一个极简的Transformer,包括embedding、位置编码、注意力掩码、LayerNorm这些组件。哪怕你只是照着论文改写,也会发现那些在框架里"一键完成"的事情背后有大量细节。
第三个层面是训练循环。AdamW的weight decay和Adam的weight decay不是一回事,warmup和余弦退火为什么要配合使用,梯度裁剪的阈值怎么定,这些问题只有自己写训练循环时才会逼着你去面对。
1.3 这条路真正教会我的事
- 显存和训练速度之间的权衡:不是所有地方都值得用更大batch,也不是所有层都应该用float32。
- 验证集应该从第一天就搭好,而不是等模型能跑通了再临时补。
- "模型越大越好"这句话在小规模实验里完全不成立,很多时候有三个亿token的数据,一个120M参数的模型已经能给你非常明确的反馈信号。
这些体会看起来平淡,但都是我用实打实的训练时间和debug时间换来的。
2. 第一版训练管线怎么搭:数据、分词器与一个手搓的Transformer骨架
2.1 语料选择、清洗与规模估算
我先做了一个很朴素的决定:用约2GB的混合文本作为初始语料。中文部分我选了一些公开领域的长文本,英文部分用了百科类子集。清洗规则并不复杂,但每条都来自实际训练中的教训:去掉包含乱码的段落、压缩连续空白符、过滤掉超过2000字的异常长文、把全角符号统一成半角。
至于数据量到底要多少,有个粗略的经验值可以参考。一个120M参数左右的模型,训练大概需要0.5B到1B个token;如果你的模型规模只有20M到30M,那0.1B到0.3B token就够了。
| 模型规模 | 参数量参考 | 建议训练token量 | 单卡训练耗时参考(RTX 4090) |
|---|---|---|---|
| 微型 | 20M | 约100M | 2~3小时 |
| 小型 | 60M | 约300M | 5~8小时 |
| 常规 | 120M | 约600M | 12~20小时 |
| 进阶 | 350M | 约1.5B | 3~5天 |
这个表是我实测下来的节奏,前提是序列长度512、batch size适中、用了混合精度。如果你没有这么多时间,把数据量砍一半也能看到足够明显的训练趋势,只是最终效果会差一些。
2.2 分词器里藏着三个容易忽略的坑
分词器是很多人习惯直接调库跳过的一步,但它的影响比想象中大得多。
第一个坑是词表大小。太小的词表会把词拆得过碎,模型需要学更多组合规则;太大的词表又会让embedding矩阵占用大量显存。我用的是BPE,词表定在32000,对小模型来说是个比较平衡的值。
第二个坑是训练语料和真实语料分布不一致。如果分词器训在英文语料上,却拿中文文本去做训练,会发现同样的token串根本无法覆盖你的输入。我的做法是直接在混合语料上训练分词器,让中文和英文按一定比例都出现在BPE的合并过程里。
第三个坑是特殊token。padding token、eos token、unk token这几个看似不起眼,但漏掉任何一个都会在训练或推理阶段冒出来奇怪的现象,比如模型生成到一半突然结束,或者把所有短句补成一样长导致注意力都被padding位置吸走。
2.3 Transformer骨架:自注意力、前馈与归一化的顺序
这个阶段我写了一个非常精简的Transformer类,核心就是多头注意力加前馈网络。手写注意力部分是整个项目里最值得做的事,因为你会真正理解为什么要有掩码、为什么要做缩放。
class SelfAttention(nn.Module): def __init__(self, hidden_dim, num_heads): super().__init__() self.num_heads = num_heads self.head_dim = hidden_dim // num_heads self.qkv = nn.Linear(hidden_dim, 3 * hidden_dim) self.out_proj = nn.Linear(hidden_dim, hidden_dim) def forward(self, x, mask=None): batch_size, seq_len, _ = x.shape qkv = self.qkv(x).reshape(batch_size, seq_len, 3, self.num_heads, self.head_dim) q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) scores = q @ k.transpose(-2, -1) / (self.head_dim ** 0.5) if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf')) attn_weights = torch.softmax(scores, dim=-1) out = attn_weights @ v out = out.transpose(1, 2).reshape(batch_size, seq_len, -1) return self.out_proj(out)LayerNorm的位置我一直坚持放在残差连接之前,也就是Pre-LN结构。相比Post-LN,Pre-LN在深层的训练中更稳定,尤其我用的是小学习率配合较大warmup的时候,几乎不会出现某些层梯度过大导致整体崩掉的情况。关于这个顺序,几个技术社区一直吵个没完,但我自己的对比实验里,Pre-LN在小模型上的收敛速度确实更友好。
3. 训练中的工程细节:学习率、Loss曲线与评测集设计
3.1 学习率、批量大小与显存预算怎么联动
训练启动阶段,我用的参数是:120M参数模型,batch size 16,序列长度512,AdamW优化器,初始学习率3e-4,warmup steps 1000,然后余弦退火到1e-5以下。
有个很关键的经验:学习率不是单独调出来的,它和batch size、序列长度以及数据规模是绑在一起的。如果你把batch size翻倍,梯度估计更稳,学习率通常可以适当调大一点;如果把序列长度从512涨到1024,每个step的信息量变大,此时如果学习率不变,很容易出现loss震荡。
我试过从3e-4直接拉到6e-4,结果在大概两千步之后loss就开始拉高并且再也回不来。所以对于中小规模模型,3e-4是一个很稳妥的起步值,如果训练曲线太平了,再考虑往上调。
3.2 怎么看Loss曲线:三张图判断训练状态
很多新人面对loss曲线只会问一句"这正常吗"。我的建议是把训练过程拆成三个信号来看。
正常状态:loss平滑下降,前20%的step降得最快,后面慢慢进入平台期。这种情况下不要频繁干扰训练,让它跑完就好。
震荡状态:loss在下降大趋势中来回抖动,抖动幅度超过0.1的时候,要怀疑学习率偏高或数据里有大量污染样本。先检查数据清洗,再考虑降学习率。
发散状态:loss直接往上冲,或者出现NaN。这种情况多半是梯度爆炸或数据里有NaN值。我用的是两个手段:梯度裁剪设到1.0,同时把batch里的异常样本打日志打出来定位。
3.3 评测集:模型不是"背语料"而是"能干活"
我刚开始训练的时候只看loss,结果发现loss降得挺漂亮,但问几个问题模型回答得完全不像话。原因是单纯的语言模型loss低只能说明它对词序列的预测能力还行,不代表它理解语义。于是我从第一天开始就配了一套很小的评测集,大概100条人工构造的任务,包括完形填空、简单指令、事实性问答。
实操上的做法是每个checkpoint跑一遍这100条任务,把回答记录下来,和参考答案并排对比。这个做法很土,但非常有效。你会清楚看到某个阶段模型学会了"把问题复述一遍但不会回答",再过一段时间才学会了"提取关键信息给出答案"。
评测集的价值不只是给你一个分数,而是给你一个随时可以回看的进度记录。有一次我改了一下数据清洗规则,总体loss只降了一点点,但评测集分数从47涨到了61,这种信号如果只看loss是完全发现不了的。
4. 从语言模型到推理模型:思维链数据与轻量RL的实战记录
4.1 推理模型和普通语言模型差在哪里
"推理模型"这个词最近很火,但说白了就是让模型在给出最终答案之前,先输出一段可见的思考过程,并且在这段思考中完成步骤拆分、验算和纠错。普通语言模型看到"三个苹果吃了一个半还剩多少"可能直接输出"1.5个"或"2个",但推理模型会先写"总数3,吃掉1.5,剩余=3-1.5=1.5,验算:1.5+1.5=3,所以答案是1.5"。
这个能力不是靠把模型做大便能自动出现的,而是训练数据的形态发生了根本变化。你需要让模型见过足够多的、带有中间步骤的样本,甚至要让它看到"中途发现错了再改正"的过程。
4.2 构造思维链数据的几个关键细节
我为了让这个120M的小模型具备初步推理能力,专门构造了一套思维链数据,数量不大,只有1000条精修样本,但效果非常明显。构造过程中有三件事是最重要的。
第一是数据格式要固定。我用的是指令问答中间穿插思考过程的模板,每一条都明确切分为问题、思考、答案三部分。
第二是难度要分层。如果全部是"鸡兔同笼"那种难题,模型可能完全摸不到门路;如果全部是三岁小孩都能答的问题,模型也学不到"验证"这一环。我的做法是简单、中等、困难各占三分之一,让模型先学会模仿思考格式,再学会复杂步骤。
第三是必须做答案校验。思维链数据里如果混入了推理错误但答案正确的样本,模型会学到错误的中间逻辑。我每条数据都人工跑了一遍逻辑链,确保思考过程的每一个推导步骤都能推出最终答案。
4.3 用LoRA做SFT,再用DPO强化推理偏好
直接全量微调120M模型不是不行,但我想保留一部分基座模型原本的能力,就采用了LoRA做轻量SFT。关键参数是rank=8,alpha=16,dropout=0.05,只对注意力层的q和v投影做适配。
SFT之后,模型已经能输出带思考过程的回答,但偶尔会思考过长、绕来绕去还跑偏。接着我用了DPO(Direct Preference Optimization)进一步优化,beta取值0.1,构造了约500对偏好数据,偏好对里"正确且简洁的推理过程"作为chosen,"冗长但不严谨的过程"作为rejected。
训练后的结果让我很意外:小模型在50道数学推理题上的正确率,从SFT前的28%,到SFT后的53%,DPO之后到了64%。虽然这个数字放在大模型圈子里不值一提,但证明了"从零构建推理模型"这条路对中小规模模型完全可行。
| 阶段 | 推理题正确率 | 回答平均长度 | 特点 |
|---|---|---|---|
| 基座模型 | 28% | 12字 | 直接给答案,经常出错 |
| SFT后 | 53% | 87字 | 有思考过程,有时过长 |
| DPO后 | 64% | 63字 | 过程精简,正确率提升 |
5. 放大实验后我踩过的三个坑:显存、并行与检查点
5.1 显存不够时先减什么,而不是急着换卡
很多人一上来就想上更大模型,结果显存直接爆掉。我的经验是,先按这个顺序排查:序列长度、batch size、是否开了梯度检查点、是否用了混合精度。
序列长度和注意力计算是平方关系,我从512降到384,显存立刻省出接近四分之一,loss曲线并没有明显变差。batch size降到8之后可以继续训练,只是收敛会稍微慢一点。如果你用的是单卡,梯度检查点也建议打开,虽然每个step会慢一点点,但显存占用可能直接减半。
混合精度基本是标配,用bf16比fp16在小模型上更稳,尤其是我这种从头训练的场景,bf16的指数范围更大,不容易出现梯度溢出。
5.2 多卡并行最容易出问题的三个环节
从单卡切到多卡时,最大的问题反而不是模型代码,而是数据加载和梯度同步。
第一个坑是数据采样重复。多卡环境中如果每个进程都用自己的随机种子,会出现同一份数据被不同卡重复采样,模型等于在同样的样本上翻倍训练。正确做法是在DataLoader层面用DistributedSampler,并且保证每张卡拿到不重叠的数据切片。
第二个坑是梯度平均没对齐。DDP默认会对梯度做平均,但你如果手动改过loss的scale,很容易把梯度扩大或缩小N倍,导致loss曲线在切换多卡后突然变得不平滑。
第三个坑是通信瓶颈。小模型在8卡上训练,很多时候速度提升不是8倍,而是只有3到4倍,原因就是频繁的梯度同步把时间都花在了通信上。解决思路是适当增大batch size、减少step数量,让通信次数降下来。
5.3 日志、检查点与一次让我后悔许久的断电
我必须坦白一件事:有一回训练跑到了3500步,loss已经降到很不错的状态,然后机房断电了。我发现自己根本没有配置自动保存检查点,那一刻整个人是崩溃的。3500步的训练时间、电费、等待,全部重来。
那次之后我把检查点逻辑彻底改了:每500步存一次完整权重,每100步存一次optimizer状态和loss日志,同时把关键指标推到可视化面板。磁盘占用并不大,但对长训练来说,这份保险是必需品。
还有一个小建议:训练日志不要只记录loss,要把学习率、梯度范数、显存占用、每个卡的吞吐量全部记下来。后面排查问题的时候,这些数据比loss本身有用得多。
6. 一份可以照抄的从零路线图,以及我最后悔的三件事
6.1 一台GPU就能跑通的完整项目参数
如果你也想从零训练一个带基础推理能力的模型,我给你一套可以直接参考的配置,这是我跑完一个完整项目后觉得性价比最合适的组合:
- 模型:130M参数,12层Transformer,hidden size 768
- 数据:约300M token,混合中文和英文
- 分词器:BPE,词表32000
- 训练:单卡RTX 4090或类似24GB显存显卡,bf16混合精度,batch size 16,序列长度512
- 总时长:约12到15小时
- 推理增强:1000条思维链样本做LoRA微调,500对偏好样本做DPO
整套流程加起来大约一周的空闲时间就能从零跑到最终评测。我强烈建议你把第一版模型的规模压到这个量级,别一上来就追求7B或13B,因为你在小模型上踩过的每一个坑,到了大模型阶段都会以更昂贵的方式重现。
6.2 按周拆解的时间路径与成本预估
| 阶段 | 时间 | 核心任务 |
|---|---|---|
| 第1-2周 | 数据收集、清洗、分词器训练 | 建好训练语料和评测集 |
| 第3-4周 | Transformer实现、训练循环 | 跑通基座模型的完整训练 |
| 第5-6周 | 思维链数据构造、LoRA微调 | 让模型学会输出推理过程 |
| 第7周 | DPO优化与评测迭代 | 提高推理正确率、控制输出长度 |
算力成本方面,如果你用自己的显卡,主要成本是电费和时间;如果租云GPU,按单卡4090的市场行情算,十几个小时的训练成本相对可控,完全在个人项目可承受的范围内。你真正要付出的其实是调试数据和跑实验的精力,这部分省不掉。
6.3 我最后悔没早点知道的三件事
第一件,验证集真的要从第一天就搭好。我一开始偷懒没有建评测集,等到训练跑了一大半才开始想"怎么定义模型好不好",结果前面所有的checkpoint都没有可比性。
第二件,检查点和日志比模型结构更重要。我再也不想体验断电后一切归零的感觉。你永远不知道训练到什么时候会遇到意外,所以请在最开始就把自动保存做好。
第三件,一次只改一个变量。我有一段时间同时调了学习率、批次大小、数据清洗规则和模型层数,结果loss变差了,我完全不知道是哪一步造成的。后来强制自己每次只动一个参数,训练过程才变成了可控的对照实验。
如果你正在考虑做一个类似的项目,我的体会是:不要急着追求复杂架构,也不要迷信更大的模型。先把一条最朴素的从数据到评测的管线跑通,再慢慢往里加入推理、偏好优化这些东西。那条看似绕远的"从零开始"的路,实际上是最快把AI工程变成自己能力的过程。