
过去一年里我被问到最多的一个问题不是哪个模型更好用而是我想认真搞AI该从哪里开始。问的人五花八门写后端服务的、做数据产品的、刚毕业的学生。他们大部分已经能熟练调各家API也跑过几个开源仓库但普遍心里没底——模型训练时loss掉了到底意味着什么为什么别人的代码在3090上跑得飞起我原样照抄就OOM像R1那样先想半天再回答的推理能力底层到底是什么机制这类问题指向同一个核心诉求AI engineering from scratch。不是说让你从零训练一个GPT-4而是把那些被框架、API和现成仓库层层封装起来的东西亲手拆开、亲手搭一遍在脑子里建立起真正的因果模型。这篇东西我想结合自己从纯工程背景转过来、一步步把模型从零搓出来的经历讲清楚这条路到底该怎么走哪些环节最容易被低估哪些坑是我真的踩进去过的。1. 先把从零这件事说清楚它到底指什么from scratch这个词在AI圈里其实被用滥了。有人把从HuggingFace拉一个预训练权重再微调叫从零有人把不用PyTorch、只用NumPy搭网络叫从零还有人把从空文本开始训练一个GPT-2规模模型才叫从零。我倾向于按难度把这条线分成三个台阶。第一层是框架层从零。用PyTorch、TensorFlow搭模型但不用transformers这种把整个架构都封装好的库。你手写Attention、自己定义TransformerBlock、自己写训练循环。这一层你学会了模型内部长什么样。第二层是实现层从零。连自动微分都不用了就用NumPy手推前向和反向传播。你会真正理解反向传播不是一个魔法而是一连串链式法则。这一层学会的是梯度从哪来、为什么更新参数能让loss下降。第三层是数据与管线层从零。自己采集数据、自己写分词器、自己处理序列拼接、自己设计学习率调度。这一层学会的是工程化训练到底在解决什么问题。我的建议很直接如果你目标是成为能独当一面的AI工程师而不是只会用API的调包侠至少要走到第二层和第三层。理由很简单——当你的模型不收敛、loss突然NaN、显存爆炸的时候只有当你对每一层都亲手写过你才有足够的直觉去判断问题出在哪。我见过太多人一遇到问题就去问GPT、去GitHub翻issue不是不行但如果你连梯度在哪个环节爆炸都无从猜测你连该搜什么关键词都不知道。另外要说明一点从零不等于排斥工具。好的从零实践者恰恰是最会用工具的人。你会用PyTorch但你也知道autograd背后在干嘛你会加载transformers的权重但你也知道那个config.json里的每个数字对应网络的哪个部分。工具是加速器不是黑盒。2. 动手前的四块地基数学、Python、数据直觉和实验习惯很多人在从零的道路上死在第一步不是因为他们不够聪明而是因为他们试图一次性把所有东西学完。线性代数从头啃、统计从头啃、微积分从头啃啃到第三章就放弃了。我吃过这个亏所以我想给你划一个最小必要范围。2.1 数学只需要四件事做AI工程你不需要成为数学家但以下四件事必须形成肌肉记忆。矩阵乘法是绝对核心。(B, T, C)形状的张量经过一个线性层变成(B, T, D)这个操作在Transformer里重复了几百遍。你必须能一眼看出两个矩阵能不能乘、结果的形状是什么、三维张量在batch维度上广播时会发生什么。我在带新人的时候发现八成bug都出在形状不匹配上而这些人往往是在脑子里硬算不愿意写出维度。反向传播需要的只是链式法则。不用会解析几百个公式但必须能对一个简单的复合函数手动推导梯度。比如sigmoid的导数是s * (1 - s)softmax配上交叉熵之后梯度恰好是(pred - target)这些结论自己亲手推一遍比背下来有用得多。学习率、初始化、归一化这些概念背后是数值稳定性的直觉。为什么初始化用xavier或者kaiming因为要保证前向传播时方差不爆炸不消失。为什么Transformer里用LayerNorm而不是BatchNorm因为序列长度变化时BatchNorm的统计量会抖。这些事情不需要深刻证明但需要你知道存在这个问题。概率统计只要基础水平交叉熵的含义、采样的概念、期望和方差。后面理解RLHF和推理模型的奖励模型时你会回来补的。2.2 Python和PyTorch别小看张量操作Python语法、NumPy数组、PyTorch张量这三者看似一样其实差别很大。我见过最经典的翻车现场一个人用Python list存了100万个token然后做slicing结果慢得像爬。还有人在GPU上频繁做tensor.cpu().numpy()再转回CUDA来回拷贝把训练速度拖垮一半。从零实践阶段你必须熟练掌握的东西包括广播规则、view/reshape/permute/transpose/contiguous这些操作的语义区别、in-place操作对自动微分的影响。特别是最后一点x 1在PyTorch里可能直接让梯度计算出错因为in-place操作破坏了计算图的节点。这种坑网上查得到但只有自己踩过一次你才会真正长记性。2.3 数据直觉模型的学习对象很多人觉得数据工程是脏活累活没技术含量。但我必须说数据决定了模型能力的上限模型结构只是逼近这个上限的手段。从零训练一个小模型你会无比深刻地体会这一点。举个最常见的例子你用字符级数据训练一个微型GPT它的loss能降到多少、能生成多么像样的文本直接取决于语料的质量和组织方式。同样的模型、同样的训练步数把语料去重、清洗、过滤掉垃圾段落之后生成质量肉眼可见地提升。这种反馈是你在手持大模型API时永远体会不到的。2.4 实验习惯AI工程和普通软件工程不一样普通软件工程希望输出是确定性的——同一个输入同一个输出。但训练模型是随机过程你改了随机种子、换了显卡、甚至换了cuDNN版本结果都会有一点点不同。所以从第一天起就要建立一套实验纪律所有实验固定随机种子至少固定torch.manual_seed和Python的random.seed每次改动只改一个变量记录loss曲线和关键指标训练日志要存成结构化文件包括超参数、数据版本、代码commit号定期存checkpoint不要等OOM了才想起没保存这套习惯帮我省了无数时间。有一次模型在训练中段突然掉loss但下游指标暴跌我靠对比日志发现是数据清洗脚本被同事改了一个正则导致的。没有日志这种问题你查三天都查不出来。3. 第一个里程碑用NumPy手写一个能跑的训练管线这是你从零道路上的第一个大工程。不要一上来就Transformer先把最简单的前馈神经网络用NumPy完整写出来并让它在合成数据上收敛。我当时的做法是做一个二分类或回归任务比如预测y sin(x1) * cos(x2)。网络结构随便一两层隐藏层即可。重点不是模型有多强而是你必须亲自实现以下这些东西# 一个简单的两层MLP前向 import numpy as np def init_params(layers): params [] for i in range(len(layers) - 1): w np.random.randn(layers[i], layers[i1]) * np.sqrt(2.0 / layers[i]) b np.zeros((1, layers[i1])) params.append((w, b)) return params def forward(x, params): h x caches [] for w, b in params[:-1]: z h w b h np.maximum(0, z) # ReLU caches.append((x, z)) x h w, b params[-1] z x w b caches.append((x, z)) return z, caches def mse_loss(pred, target): return np.mean((pred - target) ** 2) def backward(pred, target, caches, params): grads [] m target.shape[0] dz 2 * (pred - target) / m for i in reversed(range(len(params))): x_prev, z caches[i] w, b params[i] dw x_prev.T dz db np.sum(dz, axis0, keepdimsTrue) grads.append((dw, db)) if i 0: da dz w.T dz da * (z 0) # ReLU梯度 grads.reverse() return grads def train(X, y, epochs500, lr0.01): params init_params([X.shape[1], 32, 16, 1]) for epoch in range(epochs): pred, caches forward(X, params) loss mse_loss(pred, y) grads backward(pred, y, caches, params) for (w, b), (dw, db) in zip(params, grads): w - lr * dw b - lr * db if epoch % 50 0: print(fepoch {epoch}, loss {loss:.6f}) return params这段代码跑起来之后你会发现几个教科书上不讲的细节。第一初始化和学习率是强耦合的。我用的是np.sqrt(2.0/layers[i])的He初始化配上lr0.01能稳定收敛。如果初始化换成简单的np.random.randn不做缩放同样的学习率下深层网络很容易出现梯度消失loss降到一定程度就卡死。你亲手试一遍比看十篇初始化论文都管用。第二loss的数值大小是有含义的。比如MSE回归任务里如果target范围是[-1, 1]那么瞎猜输出0的baseline loss大概是1左右。当你看到loss从1.0开始下降你就知道模型是在认真学习了。如果你的网络结构完全没有学习能力loss会一直贴着baseline纹丝不动——这时候问题几乎肯定出在梯度流上。第三你自己的bug只能自己查。没有PyTorch帮你报错维度错了就报维度错梯度错了就默默不收敛。我当时的经验是先做梯度检查。用一个极小的网络对每个参数加一个微小的epsilon用数值差分近似梯度跟你的解析梯度对比。如果两者误差在1e-5左右就说明反向传播写对了。这个习惯后来帮我在更复杂的网络里避开了无数雷。写完这个NumPy版本之后建议你做一次换引擎练习把同样的网络搬到PyTorch里打开自动微分对比两种写法的差别。你会突然理解loss.backward()和optimizer.step()这两行API背后替你做了多少事。从这之后你再用PyTorch写新网络心里就有底了。4. 从玩具到语言模型分词、嵌入和Transformer的工程化解读搞定MLP之后下一步就是语言模型。这一步跨度很大但拆开看其实每块都能理解。我按照从数据到模型的真实顺序来讲。4.1 分词器语言模型的第一道关卡很多人觉得分词是无聊的预处理但分词方案直接决定了模型的词表大小和序列长度的效率。字符级分词最简单但序列太长模型很难学习长距离依赖。单词级分词词表爆炸OOV问题严重。所以现代LLM普遍用Byte Pair EncodingBPE从字符开始不断合并出现频率最高的相邻片段最终得到几千到十几万token的词表。从工程角度你只需要理解BPE的核心逻辑有一个词表一个文本序列被切成一串token ID模型输入层的Embedding矩阵就是vocab_size * embedding_dim那么大。词表大小是显存开销的关键参数之一。我做一个微型GPT时词表从256字符级换到大约5000的BPE显存占用直接翻倍但序列长度可以缩短一半以上整体效果还是BPE更好。4.2 嵌入和位置编码把token变成向量Embedding就是一张查找表。token_id42去查Embedding矩阵的第42行拿到一个向量。这个过程在PyTorch里就是nn.Embedding(vocab_size, embed_dim)。它求解的是语义相近的词在向量空间里距离也近。但自注意力本身不感知顺序——你把猫追狗的token顺序打乱Attention的计算结果完全不变。所以需要位置编码。经典的Transformer用正弦余弦编码但现在LLM基本都用可学习的绝对位置编码或者RoPE旋转位置编码。工程上理解RoPE不用推导全部公式只要知道它是在query和key向量上按维度施加旋转让相对位置信息直接编码进向量而且对长序列有更好的外推性。4.3 自注意力模型理解上下文的核心机制自注意力用一句话概括就是每个token去问其他token我们有多相关然后按相关度加权聚合信息。具体分三步。第一步每个token由输入向量通过三个不同的线性层得到Query、Key、Value。直觉上Query是我在寻找什么Key是我拥有什么Value是我携带的内容。第二步计算Query和所有Key的点积除以sqrt(d_k)防止数值过大再经过softmax得到权重。工程上这一步就是(B, T, C) (B, C, T) - (B, T, T)的矩阵乘法。第三歩用权重去加权求和Value得到每个token的输出。从零实现Attention时最容易被忽略的是causal mask。语言模型只能看当前及之前的token不能偷看未来。实现方式很简单在softmax之前把未来位置的score设为一个极大的负数比如-1e9这样softmax之后权重就是0。如果你忘了这个mask模型在训练时loss会异常地低因为它作弊了——每个位置都能看到正确答案。我当时漏加mask训练loss漂亮得不行结果生成时完全胡言乱语排查了半天。4.4 堆出Transformer块残差、LayerNorm和FFN一个Transformer块就是Attention 残差连接 LayerNorm 前馈网络 残差连接 LayerNorm。这个结构如此稳定以至于从2017年至今几乎所有LLM都还在用它。工程解读如下残差连接解决的是深层网络梯度消失问题。没有残差几十层堆下来梯度根本传不到底层。LayerNorm解决的是训练稳定性问题。它把每个token向量拉回均值为0方差为1避免某些层输出过大导致softmax饱和。前馈网络FFN是参数最多的部分通常是一个两层的MLP中间维度放大到4 * embed_dim。为什么需要它因为Attention本质上是信息交流而FFN是每个token独立进行的思考加工。这张表是我常用的一组微型GPT参数供参考参数取值说明vocab_size1024经过BPE压缩后的词表embed_dim128token嵌入维度num_heads4多头注意力头数num_layers4Transformer块数量context_length256最大序列长度FFN hidden512每块FFN中间层维度total params约3M一个微型但完整的LLM我把这个规模的微型GPT叫做玩具中的战斗机。它在普通CPU上训练几分钟就能出效果在GPU上秒级跑完一个batch但麻雀虽小五脏俱全——Attention、因果mask、训练、采样、生成一个不少。5. 复现一个微型GPT的完整清单与踩坑记录前面讲了很多原理这一节我想直接给你一份可执行的从零复现微型GPT路线图。这条路我走过不止一遍每一步都踩过坑我把最典型的几个写下来。5.1 准备数据与分词选一个体量适中的公开文本语料比如莎士比亚全集或者维基百科抽样。工程上有两点要注意一是控制语料规模不要让预处理就耗尽内存二是要留出验证集不要把所有数据都拿去训练否则你无法判断模型是记忆还是泛化。分词器可以直接用tiktoken或者自己写一个简单的BPE。我在这一步消耗的时间比预想多很多——BPE的merge规则、特殊token比如|endoftext|、批次padding每一个小环节都能出bug。第一次跑通时模型生成的文本是词表里随机token串起来的胡话并且经常出现罕见的单字符token。这是因为训练步数太少模型还在拟合数据分布。5.2 编写模型架构我建议不要复制别人的GPT仓库代码而是自己从nn.Module开始写。按顺序写Embedding、位置编码、单头Attention、多头Attention、TransformerBlock、GPT主体。每写完一块就用形状断言或简单打印检查输出形状不要等到整个模型写完了再debug否则一堆问题叠加在一起根本没法查。我记得第一次写完整个模型前向传播跑通后我高兴了没两秒就撞上反向传播的坑nn.Embedding的token ID必须是torch.long类型如果忘了转换PyTorch报错信息看着像维度问题实际是dtype问题。这种低级错误会反复出现但一旦你的debug经验积累了定位速度会越来越快。5.3 训练循环与超参数微型GPT的训练循环并不复杂但有几个超参数比模型结构更容易决定成败。学习率对微型模型我建议固定1e-3左右配合warmup cosine decay。很多人喜欢贪快直接用大学习率结果loss震荡模型不收敛。我习惯用一个简单规则如果前几百步loss不下降先把学习率除以10试试。批大小batch size直接影响训练稳定性。过小的batch比如4梯度噪声太大过大的batch显存不够。对微型GPT16到64之间的batch比较合适。我的经验是优先保证显存能放下然后在这个范围内选大的。梯度裁剪把梯度范数裁剪到1.0能有效防止偶发的梯度爆炸导致loss变成NaN。这个操作一行代码就写完但缺了它我遇到过两次训练到一半全部变NaN、必须重新跑的惨案。下面是一个核心训练循环的最小骨架optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-2) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max1000) for step, batch in enumerate(dataloader): x, y batch # x: input tokens, y: shifted target tokens logits model(x) # (B, T, vocab_size) loss F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() if step % 100 0: print(fstep {step}, loss {loss.item():.4f}, lr {scheduler.get_last_lr()[0]:.6f})你观察loss曲线时会发现一个典型的pattern前期快速下降然后进入缓慢的平台期。这时候不要慌模型在慢慢学习长尾的语言规律。真正需要警惕的是两种异常loss不降反而升通常是学习率太大或优化器参数设置错误loss降到极低但生成质量极差几乎可以肯定是你用了某种形式的标签泄漏我栽在attention mask上那次就是这样。5.4 采样与生成第一次看到模型在说话训练到一个差不多的程度后写一个采样函数给一个起始token反复取下一个token的分布采样并拼接到序列末尾同时把序列最前面的token丢掉保持长度不变。这段逻辑简单但非常关键因为它让你第一次亲手体验到模型有了一点语言能力。我当时用一个只有几十万参数的字符级模型训练了一晚上采样出来的文本虽然语法不通但已经有了明显的词汇分布特征——高频词合理出现、句子长度和语料类似、偶尔甚至能拼出完整的短语。那一刻的震撼比后来跑通百亿参数模型还要大因为你知道每一个字是怎么从你的代码里长出来的。6. 推理模型的从零之路SFT之后发生了什么现在最新的热点是build a reasoning model from scratch。很多人跑通GPT之后直接跳到这一步结果发现根本无从下手。原因很简单——推理模型不是独立于预训练的另一套东西而是建立在预训练、监督微调SFT和强化学习RL三层之上的。6.1 预训练给了模型基础智力SFT给了对话格式预训练阶段模型只是学会根据前文预测下一个token这时候它像一本百科全书能续写但不会回答你的问题。SFT阶段你用大量人类提问-理想回答的pair去微调模型教会它对话的格式什么时候该解释、什么时候该拒绝、什么时候该一步步思考。工程上SFT并不复杂——用交叉熵loss微调所有参数或者只微调LoRA。真正有价值的是数据构造。如果你想做一个会推理的模型SFT数据里必须包含大量链式思考Chain-of-Thought范例让模型模仿先分解问题、再逐步解决的格式。6.2 强化学习阶段从模仿推理到真的会推理一个完全开放的研究热点是模拟推理格式之后模型是不是真的学会了推理DeepSeek-R1这类模型给了一个令人惊讶的答案当你用强化学习去奖励最终答案正确这个唯一结果时模型会自发涌现出反思、验证、回溯等复杂推理行为。这不需要你显式标注思维链。理解这个机制的直觉很简单SFT教模型长什么样叫推理RL教模型什么样的推理能得分。优化目标是正确率模型发现第一步算错后面就全错了于是自动学会检查发现思路走不下去就自动学会回溯。这些行为不是设计者写进去的是RL从大量试错中筛出来的。RL的工程实现很重。最经典的PPO需要同时维护策略模型、参考模型、奖励模型和价值模型显存开销是微调的好几倍。后来GRPO把价值模型去掉用一组采样序列的平均优势来估计进一步降低训练开销。如果你从零做我建议先不考虑性能用一个小模型在数学题或逻辑题数据集上把GRPO的大体框架跑通策略模型生成若干条候选答案rollout每条答案根据最终正确性得到奖励比如0或1也可以写一个带过程分的小规则用GRPO/PPO更新策略让正确率高的动作获得更大概率这个流程每一步都比SFT重生成阶段需要自己写batch采样、显存管理奖励设计看起来简单但正确答案怎么判断、部分正确怎么给分、边界情况怎么处理都是工程上极其繁琐的细节。我建议至少把基础模型、SFT、采样的流程都跑熟再进RL。6.3 蒸馏、长上下文与部署从论文到产品还要多远就算你复现了一个能推理的小模型离产品还差得很远。业界现在的做法是让大推理模型生成大量高质量思维链再用这些数据去微调小模型这个过程叫蒸馏distillation。蒸馏出来的小模型往往能保留大模型很大一部分推理能力但推理速度提升一个量级。这就是为什么很多开源推理模型参数不大效果却不错。另外一个工程现实是推理模型的序列长度会暴涨。因为要输出思维链输入输出token数可能比普通对话多一个数量级这会直接冲击KV cache的显存和decode的延迟。你在从零阶段不需要解决这些问题但需要意识到推理模型不是一个纯粹的模型问题它是一个系统工程问题。7. 学习路线、资源与我的最后几点建议说了这么多最后给出一个我认为最顺畅的从零路径每一站都有明确的产出物。7.1 六站路线图第一站用NumPy实现MLP跑通前向/反向/训练产出用手推梯度训练的模型。参考微积分基础和前面给的代码即可。第二站用PyTorch重写同样的MLP理解autograd和DataLoader。产出同一模型的两套实现对比。第三站实现单头Attention和causal mask构建微型Transformer训练字符级语言模型。产出能续写文本的微型GPT。第四站引入BPE分词升级到词表更大的模型对比不同词表、不同参数量对loss和生成效果的影响。产出对分词影响有体感。第五站做SFT。找一批问答数据对已经预训练的模型做监督微调观察基座模型和指令模型的差异。产出自己微调过的对话模型。第六站做RL。选一个简单领域数学计算、逻辑推理实现GRPO或PPO训练一个小推理模型。产出从零训练出的推理模型原型。7.2 值得参考的资源Sebastian Raschka的《Build a Large Language Model From Scratch》是一本非常适合这条路的书它的思路跟我上面说的路线高度一致——从数据准备、注意力机制、GPT架构、预训练到微调带着你一行一行把GPT从零搭出来。书里的代码都是PyTorch写的配合官方仓库看进度会快很多。另外Andrej Karpathy的一系列视频尤其是micrograd和nanoGPT那个是我认为性价比最高的免费资源。micrograd用几百行代码把自动微分的核心讲透nanoGPT则是一个极简但完整的GPT训练仓库。我自己的做法是先把micrograd完整读一遍再自己写一份然后把nanoGPT关掉不看了独立照着论文重写一份遇到卡住的地方再回去比对。7.3 几句掏心窝的建议第一不要追求一步到位。很多人一上来就想训一个能通过考试的大型推理模型然后被显存、数据集、训练时间三座大山压垮。从微小的模型做起哪怕它只会讲胡话你学到的东西是一样的。第二调试模型没有银弹。我见过太多人遇到loss不动就怀疑模型写错到处找bug结果只是学习率太小。也见过人疯狂调参却忘了是数据里有太多重复样本。从零动手最大的收获不是你会搭模型而是你学会了系统性地排查问题先检查数据、再检查前向、再跑梯度检查、最后才动超参数。第三从零不是目的理解才是。没有人要求你必须永远手写注意力。当你理解了底层之后大胆去用transformers库、去用分布式训练框架那时候工具是让你更高效而不是让你失去掌控感。这条路很辛苦但在一个所有东西都被包装成一行代码调用的时代能亲手从零捏出一个会说话、会推理的模型那种清晰的掌控感是任何捷径都给不了的。