ARTICLE DETAIL

资讯详情

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

从零构建Decoder-Only语言模型:数据管线、训练策略与推理优化实战

从零构建Decoder-Only语言模型:数据管线、训练策略与推理优化实战 按理说现在这个阶段真正需要从零开始做 AI 工程的场景越来越少框架和现成模型已经把门槛压得极低。但反过来说我见过太多只会调包、跑通 Demo 就以为自己在做 AI 的人遇到一个稍微偏门的问题就彻底抓瞎。这篇文章想聊的是我自己从零开始重走一遍 AI 工程链路之后的体会从数据怎么变成张量到梯度怎么回传再到一个 Decoder-Only 的小模型如何一字一句地学会说话最后这些问题又如何反哺到真实工程里。如果你正在纠结要不要自己造轮子或者想搞懂大模型背后那些不依赖框架也能讲清楚的底层逻辑这篇内容应该能给你一些不一样的参考。1. 为什么值得从零开始先搞清楚你要补的是哪块短板先别急着写代码。我得先把从零开始做 AI 工程这件事的边界划清楚不然你很容易做着做着就跑偏。1.1 从零开始不等于放弃所有现成工具很多人一听from scratch第一反应是把 PyTorch、HuggingFace 全卸了手撸矩阵乘法。我的看法完全不同手算导数可以但你没必要手写 cuDNN。真正的从零开始指的是你自己能独立回答出下面这几个问题一个 token 经过 Embedding 之后到底经历了哪些变换为什么训练的时候要算交叉熵如果换一个损失函数模型还能收敛吗梯度消失是什么导致的为什么残差连接能压住它推理的时候temperature到底改了哪个分布top-p又是怎么截断的这些问题靠现成框架是问不出来的。框架把一切都封装成了输入张量输出张量你感觉不到中间发生了什么。所以我把从零开始重新定义为在保持使用现代工具的前提下逐个环节手写核心逻辑直到能解释清楚每个关键步骤背后的数学动机和工程取舍。这才是性价比最高的学习路线。1.2 这套路线最适合谁我用下来觉得下面三类人最应该走一遍这条链路只会调包的算法工程师。简历上写着精通 Transformer但被问到位置编码为什么用正弦函数时答不上来。你缺的不是新模型而是补底层逻辑。想转行做 AI 工程的开发者。有工程基础但对机器学习的训练机制没有完整认知需要一条从数据到部署的全链路视野。想深入理解开源模型源码的人。看 LLaMA、Mistral 源码时似懂非懂因为对基础组件的实现细节不熟。自己手写一遍之后再看源码就像看老熟人。反之如果你的目标只是快速交付业务模型不想深究内部原理那这套路线确实不一定适合你。但从我个人经验来看哪怕只为了在面试或项目评审中把方案讲清楚底层逻辑都是值得花的。1.3 我的预估周期与项目规划我自己走完一遍包含数据准备、训练到 1 亿参数的小模型、部署推理服务大约花了 3 到 4 周每天保证 2 到 3 小时。如果你只是想理解并手写一个微型 Transformer千万参数量级1 到 2 周就够。建议按这个节奏拆阶段阶段核心任务里程碑第一阶段数据管线与张量基础能批量喂数据理解 shape 变化第二阶段手写注意力与前馈网络前向传播跑通Loss 能下降第三阶段训练循环与损失函数模型能生成看起来像话的内容第四阶段推理优化与接口封装能部署成一个简单的 HTTP 服务目标不是训练出 GPT-4而是把整条工程链路里的每一个环节都摸一遍。你会发现瓶颈往往不在模型本身而在数据清洗、训练稳定性、推理延迟这些脏活累活上。2. 数据如何变成损失值一整套系统的运行逻辑我们总说深度学习就是数据驱动但很少有人把数据到损失到梯度的这条链条完整讲清楚。这条链路就是你理解 AI 工程的骨架。2.1 从原始文本到整数序列假设你手头有一批英文技术文档想拿来训练一个语言模型。第一步不是建模型而是把文本变成数字。流程大概是清洗去掉 HTML 标签、控制字符统一换行格式。分词用 BPEByte Pair Encoding或 SentencePiece 把文本切成子词单元。这一步决定了你的词表大小我常用 8k 到 32k 的规模来保持训练速度。映射每个子词对应一个唯一的整数 ID。切块把长文本切成固定长度的序列比如 512 或 1024 个 token作为训练样本。这一步里最容易翻车的是训练集和推理时的分词不一致。很多人图方便训练时用简单空格切分部署时换成了 BPE模型效果立刻崩掉。分词器是训练的一部分不是预处理的一次性工具它必须跟着模型一起保存、一起部署。2.2 为什么所有东西最后都是交叉熵做语言模型本质上是在做下一个 token 的预测。模型拿到前 512 个 token输出一个词表大小的概率分布然后我们拿真实的下一个 token 去算损失。交叉熵损失公式为L -log( p(y_true) )也就是模型给正确 token 分配的概率取负对数。如果模型给正确 token 的概率是 0.9那损失就是 0.105如果概率只有 0.1损失就是 2.30。最小化交叉熵其实就是让模型越来越擅长把概率集中在正确的下一个 token 上。我在从零开始阶段最深的体会是不要小看这个公式。整个模型的训练目标都凝聚在这么一行简单公式里。后续的label_smoothing把真实概率从 1 摊到其他 token 上防止模型过于自信、损失权重调整都是在这条公式上做文章。2.3 梯度回传反向传播的直觉理解模型前向算出一个损失值怎么把它变成参数的更新方向靠的就是反向传播。你不需要手写每一层梯度但必须理解链式法则在这里扮演的角色损失对参数的梯度 从损失到该参数的路径上所有局部梯度的乘积。参数变大还是变小取决于梯度方向。把梯度下降类比成下山你站在山坡上当前参数位置梯度告诉你最陡的下坡方向学习率决定你每一步迈多大。步子太大容易直接冲下山谷步子太小半天走不到山脚——这就是为什么学习率一直是训练里最敏感的超参数。我建议初学者无论如何都要用 NumPy 手写一个两层的 MLP把dLoss/dW1一步步算出来。这个过程不需要多复杂但能让你彻底摆脱反向传播是玄学的错觉。做完之后再回来用 PyTorch 的loss.backward()你会感觉自己是在指挥一支知根知底的队伍而不是在黑盒里碰运气。2.4 你真正应该盯住的中间量训练过程中不是只看 loss 就够了。我习惯在每次打印 log 时同时记录这几项grad norm梯度范数调试梯度消失/爆炸的第一手指标。正常训练中grad norm 应该在稳定范围内波动如果直接掉到接近 0大概率是梯度消失了。learning rate 实际生效值配合 warmup 和 decay确认它确实按照 schedule 变化。token 级别的困惑度perplexityexp(loss)比 loss 更直观。从 5000 慢慢降到 100你会看到模型越来越懂的过程。这一整套从数据到梯度的链条就是我理解的 AI 工程的地基。下面我们进入模型侧看一个 Decoder-Only 的模型具体是怎么搭起来的。3. 手写一个最小可用的 Decoder-Only 模型这里我不打算贴一个几百行的完整文件那样反而看不清重点。我建议你按下面 4 个组件去拆分实现每个组件都能单独验证。3.1 组件一Token Embedding 与位置编码Embedding 其实就是一个查找表输入 ID 查出一个固定维度的向量。在 PyTorch 里这一行就够了import torch.nn as nn embed_dim 256 vocab_size 8192 token_embedding nn.Embedding(vocab_size, embed_dim)但位置编码值得多说几句。Transformer 本身没有顺序概念所以需要把位置信息塞进去。两种主流做法正弦函数固定编码pos维度的正弦/余弦值好处是不限制序列长度外推到更长序列也不会太难看。可学习位置编码把位置 ID 也当成一个 Embedding 去学短序列上效果通常更好但遇到超长序列需要扩展否则就要做插值。我自己的经验是小模型、短序列场景直接用可学习位置编码就够了简单且省心。只有当你确实需要处理超出训练长度很多倍的序列时才认真考虑旋转位置编码RoPE这类方案。3.2 组件二多头自注意力机制自注意力是 Transformer 的心脏。公式很多人背得熟Attention(Q, K, V) softmax(Q K^T / sqrt(d_k)) V但手写一遍才会意识到几个细节Q K^T之后为什么要除以sqrt(d_k)因为如果不做缩放点积的数值会随着维度增大而变大送进 softmax 之后梯度会变非常小。这个缩放项是数值稳定性的关键。多头是什么不是让你算多个不同的注意力而是把同一个输入投影到多组 Q/K/V分别算注意力最后拼接起来。每个头可以关注不同的模式有的头关注句法关系有的头关注相邻 token有的头关注全局主题。手写实现多头注意力的关键是把 shape 处理好。我习惯将输入从[batch, seq_len, embed_dim]reshape 成[batch, n_heads, seq_len, head_dim]这样每个头独立计算最后再 reshape 回去。3.3 组件三前馈网络与残差连接注意力层之后还有一个两层 MLP这已经是标准配置了。但真正让深层网络能训练起来的是残差连接x x Dropout(Attention(LayerNorm(x))) x x Dropout(FFN(LayerNorm(x)))关于 LayerNorm 和残差连接的顺序有多种变体Post-LN、Pre-LN。我试验下来小规模训练用 Pre-LN 更稳它把 LayerNorm 放在子层输入端可以有效缓解深层梯度不稳定的问题。如果你从零开始训练一个 8 层以上的模型直接选 Pre-LN 能少踩很多坑。3.4 组件四输出头与损失计算最后一层就是线性映射把隐藏维度映回词表大小然后拿去和真实 token 算交叉熵。到这里一个最小模型就完整了from torch.nn import functional as F logits model(token_ids) # [batch, seq_len, vocab_size] loss F.cross_entropy( logits.view(-1, vocab_size), targets.view(-1) )注意这里的targets是输入序列右移一位的结果——也就是token_ids[:, 1:]。这一点必须理解清楚模型看到的输入是第 1 到第 511 个 token要预测的目标是第 2 到第 512 个 token。组件拆分是这个项目所有后续实验的基础。代码一旦能跑通、loss 确实往下降就算成功了一大半。接下来就是训练环节。4. 训练循环之外如何让你的模型会说话会推理很多人以为训练就是一个 for 循环把数据喂进去调loss.backward()和optimizer.step()就完事了。真正上手之后你才会发现训练策略对最终效果的影响不亚于模型架构本身。4.1 学习率策略不是随便选的我从零开始做过对比固定学习率在 1e-3 附近loss 波动到让人怀疑模型坏了切成 warmup cosine decay 之后曲线立刻稳下来。推荐一个我常用的配置Warmup 步数占总共训练步数的 1% 到 5%让参数先在小步慢跑中适应数据分布避免一开始就冲出悬崖。峰值学习率小模型可以用 1e-3 到 3e-4 之间模型越大峰值越低。Decaycosine 降到峰值的 10% 左右。这样可以保证后期用很小的步长做精细收敛。如果你用 AdamW还有一个几乎没人提但很重要的细节weight decay 通常只作用在非 LayerNorm/Bias 参数上。PyTorch 里做参数分组时我会手动把这两类参数单独分出来否则训练会变得不稳定。4.2 推理能力从哪里来标题相关热搜里有build a reasoning model from scratch我在做这个项目时也认真想过这个问题。一个语言模型不是天生会推理的它是通过在大量数据上做下一词预测慢慢涌现出逻辑一致性的。如果你想在训练中人为加强推理能力有一个低成本的做法在训练数据中混入带有推理中间步骤的样本。比如数学题按问题 - 分步解题 - 答案的格式整理模型在预测每一步的过程中本质上就是在模仿先思考后给出结论的模式。还可以在推理阶段加入思维链提示词请一步步分析这个问题最后给出答案。但需要注意的是提示词只在模型有能力生成中间推理过程时才有意义。如果模型太小、训练数据里没见过这种格式你让它一步步思考它也只能硬编。所以我最终的结论是推理能力是数据分布决定的提示词只是把它引导出来。4.3 采样策略temperature 和 top-p 到底改了啥训练结束后生成阶段通过采样策略来控制随机性。这个比想象中更容易被误解temperature把 logits 除以 temperature 后再做 softmax。t0.1时分布非常尖锐几乎等同于贪心t1.0是原始分布t1.0让概率拉平输出越发散。top-p核采样从概率最高的 token 开始累加直到累计概率超过 p然后在保留的 token 集合里重新归一化采样。p0.9是我觉得最实用的平衡点。这两个参数一个管分布锐度一个管候选集合大小它们不是一回事但可以组合使用。我见过太多人只调 temperature效果不理想就怀疑模型坏了其实换成 top-p 之后立刻正常。4.4 训练到一半怎么判断要不要停不要只看 train loss。我自己项目里最有效的一组观察指标是在固定的验证集上看 loss 是否还在降。人工生成几个固定 prompt 的样本观察输出质量。每隔一定步数保存 checkpoint方便回溯。只盯着 train loss 训练很容易过拟合到数据噪声里而不自知。验证集 loss 开始回升、train loss 还在降这就是过拟合的典型信号。对小模型来说最好的应对就是加大数据量或调低模型容量而不是盲目加正则化。5. 训练踩坑全记录我遇到的 4 个经典问题与完整排查链路这一章我决定不直接甩解决方案而是还原我自己踩坑时的排查过程。你会看到AI 工程里 Debug 的能力往往比建模能力更值钱。5.1 问题一loss 完全不下降卡在固定值附近现象训练跑了 2000 步loss 纹丝不动比如一直卡在 5.5 附近。排查链路先算随机初始化的预期 loss。如果词表大小是 8192均匀分布下交叉熵应该是log(8192) ≈ 9.01。如果 loss 卡在 9 附近说明模型基本没学到东西但在正常初始化范围内。检查是否把 logits 和 label 对齐了。最常见的是shift逻辑写错导致模型在预测自己——这样 loss 虽然低但实际上没意义。检查学习率。如果用1e-4训一个小模型收敛会慢到肉眼无法感知但不至于完全不降。如果梯度出现了 NaN就要看下一步。简化数据。先用 1000 条干净的样本跑排除数据噪声影响。我最常遇到的是第 2 步——对齐错误。确认logits和targets的移位方向正确这个坑就消灭了一半。5.2 问题二训练过程中突然出现 NaN现象loss 正常降了几个 epoch突然某一步变成 NaN之后所有参数都是 NaN。排查链路加梯度裁剪grad clip是短期缓解手段但不是根因方案。检查 learning rate 是否过大。尤其是 Adam 的eps设置不当加上极低 learning rate 时容易数值爆炸。重点检查数据里有异常值比如超长 token 序列、2048 长度的样本没截断。长序列会让激活值累积变大。用torch.autograd.detect_anomaly()定位第一次出现 NaN 的位置。长期方案是在数据预处理里严格限制最大序列长度并在模型计算中适时使用layer_norm稳定数值。NaN 一旦出现不要急着调参先找到根因再动训练配置。5.3 问题三loss 在降但生成内容全是重复的现象模型输出的内容永远在重复同一个词或短语比如 The cat the cat the catloss 却已经很低。排查链路这是一个训练目标和生成目标的错配问题。训练时用 teacher forcing用真实 token 作为下一输入而推理时第一个错误 token 会被当作后续输入错误传入后不断被放大最终掉入重复循环。常用的缓解手段采样时把 top-p 调小限制候选集。对已生成的 token 做重复惩罚repetition penalty。在训练数据中减少重复文本占比。如果模型本身很小可能没有足够容量学到不重复的隐式约束。这时候适当增大模型比改代码更有效。5.4 问题四验证集 loss 与训练集 loss 差距越来越大现象train loss 降得很漂亮验证集 loss 却在上升。排查链路确认验证集是否切分正确有没有交叉污染。检查模型容量是否过大参数数量远超数据量。用 dropout 和 weight decay 做正则化但不要一开始就加很大。数据增强在语言模型中作用有限最有效的还是数据量。很多时候验证集 loss 偏高不代表泛化差有可能验证集包含了一些难度偏高的样本。我会查看细分的类别 loss而不是只看一个汇总数这样能更精准定位数据问题。6. 从模型到系统推理优化和工程化的关键动作模型训练出来只是一个起点。真正的 AI 工程是把模型变成一个稳定、快速、可观测的服务。这一步的细节比训练本身更容易被低估。6.1 推理性能优化先看瓶颈在哪拿到一个训练好的模型我先做一次延迟剖析profiling搞清楚时间到底花在哪个环节。对一个解码器模型来说常见瓶颈有三个Prefill处理输入 promptGPU 算力密集一般不是太大问题。Decode逐个生成 token每生成一个 token 都要跑一遍整层网络显存带宽和矩阵乘法都开始吃紧。采样与重复惩罚如果加了复杂的采样逻辑CPU 和 GPU 之间频繁同步延迟会凭空增加。我通常的优化顺序是启用 KV Cache。这一步能把解码复杂度从O(n^2)降到O(n)是最收益提升最大的优化。批量推理batching。算力没吃满时把多个请求拼成一个 batch 推理吞吐量直线上升。使用半精度FP16/BF16。该省显存的地方省该省时间的地方快。但要注意和损失计算的数值稳定性配合。如果你做的模型比较小1B 以下CPU 推理配合 ONNX Runtime 或 OpenVINO 也可能够用还能省下 GPU 成本。这个取舍完全看业务场景没有标准答案。6.2 推理服务的接口设计部署接口时我的习惯是提供一个符合生产需求的接口{ prompt: Explain the difference between AI and ML., max_new_tokens: 256, temperature: 0.7, top_p: 0.9, seed: 42 }接口返回里除了生成的文本还建议带上tokens_generated、elapsed_ms、finish_reason等字段。这些东西不仅方便调用方排查问题还能省下你将来做监控时的大量时间。6.3 模型服务的可观测性上线之后你需要的不是看起来能跑而是出问题知道哪里出问题。我至少会监控三块延迟分布P50/P95/P99不只看平均。失败率超时、非法请求、生成中断的比例。输入输出长度分布上下文变长占用显存和时间都会变化。这些都是工程基本盘。做 AI 工程久了你会发现训练世界上比较少见把推理稳定地跑到生产环境里才是每天都在打交道的活。7. 学习路径与资源清单按顺序走效率翻倍最后一部分给准备动手的你一份我实际用过的资源路径。它能帮你少走不少弯路。7.1 我的推荐学习顺序如果你是从有一定编程基础、但对 ML 训练细节不熟的状态起步建议按这个顺序Python NumPy 基础确保能处理批量矩阵运算。深度学习入门理解 MLP、激活函数、反向传播。可选李沐的《动手学深度学习》中英文都有配合代码实战效果极佳。Transformer 架构先读论文《Attention Is All You Need》再找一篇带逐行代码实现的博客对照着看最后自己动手写一遍。训练一个微型语言模型用 Shakespeare 或开源的小数据集训练一个 1000 万到 1 亿参数的模型跑通生成流程。扩展阅读可以参考《The Annotated Transformer》这类资源或者找一本类似《Build a Large Language Model from Scratch》的书籍来横纵对比。我看这本书时的最大收获是它把所有组件拆成了可逐步验证的小项目每个章节都能独立跑通非常适合跟着做一遍。动手做一个小项目比如做一个领域问答机器人或文档摘要工具覆盖数据清洗、微调、评估、部署全流程。我不建议一上来就啃大模型的论文合集没有代码实践支撑你会很快被公式劝退。跟着能跑的项目走比跟着理论走更容易坚持下来。7.2 容易踩的路线坑结合自身经历三个弯路提醒贪多求全看了十几篇论文但一行代码没写。动手写代码之后很多抽象概念会自动理清。跳过数据环节直接下载别人的数据集不做任何清洗和探索。真实项目里 70% 的时间都在整理数据早做晚做都是做。只做正向实验不做负向对照我后来才养成的习惯是不仅记录这样能收敛也要记录那样会发散。这些负向经验在调参时反而更值钱。7.3 关于从零开始的心态最后说点掏心窝的话。从零开始做 AI 工程最难的不是数学也不是代码而是忍受初期看不到效果的挫败感。你写了三天代码模型还在输出乱码这是极其正常的。只要你确认数据管线没错、loss 确实在降就值得再给它一到两天的耐心。当你在某个早晨第一次看到模型输出一句语法通顺、逻辑基本合理的话时那种我亲手把它造出来了的感觉是直接调用大模型 API 永远体会不到的。这也是我坚持把这套路线写完的原因。希望你也能在这一轮从零开始里找到属于自己的工程手感。
返回列表