ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从零开始AI工程:构建、训练与部署的完整实战

从零开始AI工程:构建、训练与部署的完整实战 从零开始做 AI 工程我到底经历了什么我一直觉得AI 圈最缺的不是会调库的人而是敢从零开始把一件事真正想明白的人。所以当有人问我ai-engineering-from-scratch到底是什么意思我都会直接告诉他这不是一个项目而是一种态度是你愿意从数学公式、数据清洗、权重更新这些最底层的东西一路堆起来最终亲手造出一个能跑、能看、能用的 AI 系统。这篇博文我就想把自己从头做 AI 工程的完整思路、实操步骤和踩坑记录分享出来给那些准备动手却不知道从哪里开始的人也给那些已经在路上但总觉得根基不稳的人。我不会讲太玄乎的理论也不会只丢给你一堆框架调用我会告诉你我是怎么选方向的、怎么拆任务的、怎么一步步验证自己想法的。适合看这篇内容的人有三类第一类是刚入门 AI 但不想只会用现成 API 的人第二类是已经在做算法但缺乏工程化思维的人第三类是纯粹对从零构建一个 AI 系统感兴趣、想通过完整项目理解全链路的人。无论你是哪一类我希望你看完之后能获得一种感觉原来从零开始搞 AI 工程真的可以按图索骥一步一步落地。1. 内容整体设计与思路拆解1.1 为什么我选择从零开始而不是直接调库刚开始接触 AI 工程的时候主流的声音其实是拿来主义数据不够用现成的、模型不够用预训练的、部署不够用平台的。我也确实走过这条路但时间久了发现一个问题——当项目出现奇怪的 bug或者模型效果怎么调都不对的时候我连排查的方向都没有因为我只知道怎么用不知道为什么能跑。这就是典型的框架依赖症。我下定决心从零做一个完整的小型语言模型项目不是为了重复造轮子而是为了把黑盒子打开看看里面每一个齿轮是怎么咬合的。从零开始听起来像是在做重复劳动但它的价值恰恰在于把表面上会变成骨子里懂。比如大家都知道 Transformer 有自注意力机制但只有当你亲手实现 Q、K、V 三个矩阵的乘法反向传播时你才会明白为什么维度要这样设、为什么缩放因子这么重要。我做的项目不是拿一个现成的大模型来微调而是从数据构造、词表构建、模型结构定义、训练循环到推理部署全部自己搭建一遍每一步都记录清楚。这种做法的优点非常直接核心逻辑完全掌控出问题能快速定位缺点是前期投入很大开发周期比直接调库多出三到五倍。但我想说的是这个多出来的三到五倍恰恰是最有价值的部分。你在调库的时候遇到奇怪现象很多时候只能搜索别人的经验而从零开始你会发现自己能够直接推导出问题出在哪一层。就比如我项目里遇到过一个 loss 不下降的问题后来定位到是学习率预热策略写错了如果你只是调用别人封装好的 Trainer可能根本不会去想预热策略对收敛的影响只会一味调大 batch size。1.2 核心需求解析从标题到技术选型的推演ai-engineering-from-scratch这个标题给我的第一个暗示是必须有一个完整的工程闭环不能只做模型训练一个点。所以我拆解出的核心需求包括四块数据链路、模型构建、训练闭环、推理与评估。每一块都不是孤立的数据链路决定模型能学到什么模型构建决定学习能力的上限训练闭环决定模型能否收敛到预期效果推理与评估决定这个模型到底能不能用。这四个环节串起来才算是一个完整的 AI 工程。基于这四个需求我对技术选型做了几个关键决策。数据方面我选择从公开语料中抽取并做一个小的清洗管道而不是直接下载打包好的数据集因为我想把真实世界的数据有多脏这个体验完整走一遍。模型方面以小型 GPT 风格架构为原型参数量控制在 1500 万左右这样单张消费级显卡就能训练还能保证在合理时间内看到结果镜像我搭的是 tokenizer 训练、模型初始化、数据加载三部分都从零写入的代码库。训练方面我选了 AdamW 优化器配余弦退火和线性预热而不是直接用默认参数因为在从头搭建的场景下每一项配置影响都能被清晰观察这本身就是学习的一部分。选型逻辑中最重要的一个原则是限制产生创造力。当我把模型的规模限制在一个很小的范围内时我就必须认真思考模型容量够不够数据质量是不是比数据数量更关键哪些技巧是真正能带来提升的哪些只是锦上添花这种思考方式远比无脑堆积资源要深刻得多。2. 核心细节解析与实操要点2.1 从零构建数据管道的三个关键环节很多人一上来就急着建模这是最要命的错误。我在这个项目里把数据环节拆成了三步每一步都有明确产出。第一步是采集与清洗我写了一个从原始网页文本中提取段落的脚本去重、去广告、去乱码把原始文本切成可以用于训练的干净片段。这一步的难点不是写正则表达式而是要知道什么样的文本对模型训练是有害的重复度过高的文本会让模型产生复读机倾向包含大量乱码的文本会干扰 tokenizer 的学习还有那些特殊符号很多的对话记录会拉偏分布。第二步是构建词表这一步的选择决定了模型的表达能力。我自己实现了一个 BPEByte Pair Encoding训练器从零开始训练一个词表而不是加载现成的词表文件。BPE 的核心思想是不断合并高频共现的字符对和子词对最终形成一层从字节到语义单元的映射。我在 5000 万字符的语料上训练了一个词表大小约 8000 的 BPE并通过观察词表质量来反向调整语料的清洗策略。比如我发现语料里如果有很多重复的日期格式tokenizer 就会把一整个日期学成一个 token这其实浪费了词表容量。第三步是构建采样器与批次生成器。很多教学代码这一步只做一个随机抽样打乱顺序但我在实操中发现如果直接把所有样本拼成长序列再切块会出现截断导致语义不完整的问题。我后来采用的做法是先把文档拆成较短的句子片段再把相邻片段拼接到接近目标序列长度最后用特殊的分隔符隔开不同文档。这个小细节在训练时能让 loss 下降更平滑也能减少模型学到跨文档无意义拼接的概率。这里我给你一个参数配置参考如果目标是训练一个序列长度为 512 的小模型那么批次大小可以设为 16每批大约包含 8192 个 token。针对单张消费级显卡这个体量是相对安全的。如果你显存更大可以适当增大 batch size但不要忽略梯度累加策略——每步实际更新时用的梯度是多个小批次累加的结果这样可以稳定训练过程。2.2 模型代码实现的四个核心模块从零搭建模型代码我不会一上来就写一个 giant transformer而是先拆成四个模块嵌入层、多头注意力、前馈网络和残差连接结构。嵌入层相对简单就是把 token 索引映射为向量再叠加可学习的位置编码。我推荐使用可学习位置编码因为对小模型来说它比三角函数式的位置编码更容易优化也更好理解。我项目里用的嵌入维度是 256序列长度是 512因此位置编码的参数是 512 乘 256。多头注意力是实现时最容易出错也最关键的部分。我建议把 Q、K、V 的线性变换合并成一个大的线性层然后拆分到多个头这样计算效率更高也更方便调试。每个头的维度我设为 64一共 4 个头注意力头数乘每头维度等于总嵌入维度。具体实现里有个小细节很考验功底注意力权重要除以根号下 head_dim这个缩放因子在标准实现中是必须的否则点积结果过大会导致 softmax 进入饱和区梯度变得非常小。前馈网络部分我用了两层线性变换加 GELU 激活函数。相比传统的 ReLUGELU 在 Transformer 里的表现通常更平滑但要注意初始化标准差要合适。如果初始化太大很早就出现梯度爆炸如果太小整个模型在前几十步几乎不动。我这里给的初始化经验是标准差设为 0.02 乘模型维度的平方根加权调整实际效果比较稳定。残差连接和层归一化要特别注意先归一化还是先残差的问题。我选择的是 Pre-Norm 结构也就是先做层归一化再做注意力或前馈计算最后加残差。这种结构在深层模型中更容易稳定训练而且在小模型上也能看到明显的收敛优势。如果你用的是 Post-Norm就必须配合更精细的学习率调节和 warmup否则训练后期容易发散。每次训练结束我会打印每一层参数的梯度范数。如果梯度范数突然变成 NaN 或者超过 10那百分之百是中间计算出现了数值溢出。把梯度过大的层单独拉出来检查是定位模型代码 bug 的最快路径。3. 实操过程与核心环节实现3.1 用 5 分钟搭出最小可运行训练循环我建议你搭建训练循环时从最简单的最小闭环开始也就是读取一个 batch算一次前向算一次反向更新一次参数。不要一开始就加入复杂的学习率调度、梯度裁剪、EMA指数移动平均等机制先把主线跑通再逐步加配置。我项目里的最小训练循环包括四个函数加载数据、前向计算、损失计算、优化器更新。损失函数我用的是交叉熵其中要注意把所有非填充位置的 token 都计入损失忽略填充部分否则模型会不断学习预测如何输出填充符浪费学习能力。同时我设置了一个梯度累积步数每 8 个微批次做一次参数更新这样 batch size 的表观值可以做得较大精度也能得到保证。第一轮迭代我建议只训练大约 100 步目标不是为了看到完美的模型效果而是为了确认 loss 在下降、显存没有溢出、tokenizer 生成的样本可以被模型正常消费。我第一次跑的时候就踩了坑数据的序列长度明明是 512但数据加载器在最后一个 batch 没有做 padding 对齐导致模型输入维度不一致报错信息还很隐蔽。后来我在数据加载器里加了一个统一的 padding 函数让所有序列动态对齐到最长长度这个问题才解决。一个值得记录的具体配置是我设置的最大学习率是 3e-4权重衰减设为 0.1线性预热步数为 500之后按余弦退火衰减到 1e-5。选择这些参数不是拍脑袋而是参考了小规模语言模型训练的常见组合并在我的数据量和模型规模下做了少量调整。要注意的一点是小模型通常不怕较高的学习率但大词表和稀疏数据场景下调大学习率极易导致 loss 震荡所以如果你复现时发现曲线不稳定优先降低学习率至 1e-4 左右而不是调大 batch。3.2 从零训练一个 1500 万参数模型的完整记录在当前软硬件环境下我用一张显存 12GB 的消费级显卡训练这个 1500 万参数模型总训练 token 数约为 1 亿训练时间大约一个晚上。为了让训练过程更可追踪我记录下每 500 步的 loss 值和学习率并保存 checkpoint。整个训练过程的总 loss 曲线呈现快速下降后缓慢收敛的形态但在大约第 3000 步附近出现过一次小的 loss 回升跳变。那次跳变的根因是数据加载器在某个 epoch 边界没有重新洗牌导致连续多个 batch 的数据分布非常接近模型过拟合了局部模式等在边界处切换到新数据分布时 loss 直接上升。理解这类现象非常重要因为在小规模训练中数据顺序的影响比大模型训练更明显。此后我把每个 epoch 结束后的数据文件随机打乱顺序并在拼接时重新生成随机片段问题就再也没有出现过。在模型结构上我使用了两层 Transformer block每层 4 个头隐藏维度 256词表大小 8000。我计算的参数量大约是 1500 万其中嵌入层占了很大比例因为有 8000 乘 256 的参数。如果想在这个规模下继续提升效果最有效的做法不是增加层数而是扩大词表或嵌入维度因为当前瓶颈主要在于嵌入表征的容量。训练结束后我测试了模型的生成效果给定一句人工智能的未来在于模型能够给出语义基本连贯的续写这说明它对语言分布已经有了一定的学习能力。对于一个只有 1500 万参数的模型来说这个结果是合理的毕竟它的容量不足以产生复杂的知识和推理能力但它已经完全掌握了词语搭配、句法结构等基础模式。3.3 推理部署与效果评估的关键细节从零训练完模型只完成了一半工作你还需要把模型变成可以被服务的东西。这一步我用的是一个简单的文本生成接口输入 prompt 后经过 tokenizer 编码、模型前向计算、采样解码最终返回生成的文本。采样时我用了 top-k 采样加温度控制温度设为 0.8top-k 设为 50。在这个配置下生成结果既不太保守也不太发散。评估阶段我除了看生成样例还计算了困惑度perplexity在验证集上大约得到 80 左右。对一个小模型来说这个数字是可接受的但如果想让它更低就需要成倍增加训练数据量或者扩大模型。要特别注意的是困惑度是衡量模型对数据分布拟合程度的指标并不等同于下游任务的效果。如果你做的是分类、抽取等具体任务还需要专门构造评估集。推理性能也是一个不能忽略的环节。我这个模型在 CPU 上生成 100 个 token 大约需要 5 秒在 GPU 上只要 0.5 秒。如果想把推理速度进一步优化可以考虑 batch 推理、int8 量化或 KV Cache。我在项目里实现了简单的 KV Cache使生成时不必重新计算之前的键值对生成的显存占用下降约 30%速度也有明显提升。这里有一个很容易犯的错误如果实现 KV Cache 时没有正确管理缓存长度长文本生成到后期会出现索引错误建议每一步生成都显式更新缓存长度。4. 常见问题与排查技巧实录4.1 训练 loss 不下降应该如何快速定位我遇到的第一类典型问题是 loss 不下降具体表现是训练了好几百步loss 一直在初始值附近震荡或者下降得非常缓慢。排查思路要分三条线走第一检查数据标签是否对齐比如 token 移位导致模型预测目标错误这是非常隐蔽的 bug第二检查学习率是否过低或过高过低会出现能下降但很难看过高会出现 loss 震荡甚至发散第三检查初始化是否合理如果 embedding 初始化太大模型一开始就处于一个很不稳定的状态优化器往往很难把它拉回来。还有一种非常容易被忽视的情况模型的输出层没有和词表大小匹配。我记得有一次我在改词表大小后忘记同步修改模型输出维度导致代码跑出的 loss 完全异常最后是通过逐层打印张量形状才定位到的。我的建议是把模型前向过程中所有关键张量的形状打印出来从输入 token 到最终 logits每一步检查是否符合预期。这个习惯能帮你省下非常多调试时间。如果 loss 曲线一直不降还有一个可能是你使用了过大的梯度累积步数导致参数更新频率太低模型收敛极慢。我建议把梯度累积步数先设为 1观察 baseline 表现再逐步增大。否则你很容易陷入看起来 loss 在动但实际上是在累积多个 batch 的噪声信号的错觉。4.2 显存不足或训练崩溃的排查经验显存不足在小规模项目中也会出现特别是当你使用了较长的序列长度或较大的 batch size 时。我的建议是先按显存的一半规划 batch size再逐步往上加同时用 gradient checkpointing 技术减少中间激活的存储。在 12GB 显存上我的 1500 万参数模型本来可以轻松运行但我在第一次尝试时因为设置了过大的 batch size 和过长的序列长度直接 OOM。后来我把 batch size 降到 16序列长度保持 512问题就解决了。训练崩溃是另一个常见问题最常见的表现是 loss 变成 NaN。我从自己的实践中总结了四个排查点一是学习率过大二是数据中存在异常值三是某一层梯度爆炸四是混合精度下出现数值下溢。我的做法是先在纯 float32 下训练几十步确认没有 NaN 之后再尝试混合精度这样定位问题会非常高效。如果一旦发现某一步 loss 变为 NaN我会立即减小学习率并检查最后一个 batch 的数据是否有空值或极大值。我试过在小模型上用 bfloat16 混合精度训练相比 float32 损失了一点精度但速度提升约 30%。如果你的显卡不支持 bfloat16建议老老实实用 float32因为 float16 在小模型上的动态范围太小容易出现梯度消失。这个坑我在一次快速实验里踩得很好让我彻底明白硬件支持混合精度和模型适合混合精度是两回事。4.3 生成质量差、复读或者语义混乱怎么办训练完成后生成质量不理想是非常普遍的现象尤其是小模型。复读问题我看到最多的原因是训练数据中有大量重复片段。比如我从语料里清洗的时候没有去除连续重复的段落模型就学到了不断重复最近见过的内容的模式。解决方法是提升数据清洗强度把连续重复超过三次的 n-gram 筛掉同时在生成的时候把重复惩罚系数设高一些像 repetition_penalty 设为 1.2 左右效果立竿见影。语义混乱则更可能和上下文长度不足有关。模型只有 512 的序列长度它能看到的信息是有限一旦输入太长前面信息会被忘掉。缓解方案是训练时随机截取长文档的连续片段并且在推理时只使用最近的一部分上下文。另外如果模型生成的句子语法正确但逻辑不通最根本原因还是模型容量和训练数据不足这需要从更大的模型和更高质量的数据方向来解而不是靠调参能解决的。还有一个容易踩的细节生成时温度设置太高或者太低都会导致问题。太高模型会逐渐漂移说话毫无逻辑太低模型会走向极端保守变成一个复读机。我前文给出的 0.8 温度加 top-k 50 的组合是在我这个小模型上经过几次生成对比之后选出来的你可以用它作为起点再根据你的场景微调。当你真正理解了温度、top-k、top-p 每个参数对生成分布的影响你就能像一个调音师一样让模型在不同场景下表现出不同风格。4.4 一套排查速查表值得直接收藏我把从零构建 AI 工程遇到的高频问题整理成一张速查表方便你在卡住的时候快速对号入座。这张表不是网上能随便搜到的废话合集而是我在实操中真实遇到并解决的场景每一行都有针对性。现象可能原因排查方向训练 loss 恒为初始值数据标签错位 / 输出层维度不匹配检查 token 对齐、打印 logits 形状loss 下降到一半突然升高数据顺序问题 / 学习率调度异常检查 epoch 边界数据洗牌、打印学习率loss 变成 NaN学习率过大 / 数据异常 / 梯度爆炸降低学习率、纯 float32 验证、检查梯度范数生成结果频繁复读训练数据重复严重 / 采样温度过低清洗 n-gram 重复、提高温度、增加重复惩罚生成语义混乱上下文长度不足 / 模型容量不足缩短输入、增大模型或数据量显存不足batch size 过大 / 序列过长降低 batch size、开启梯度检查点推理速度慢没有缓存 KV / 实时计算重复实现 KV Cache、批量推理参数更新无效梯度累积步数过大 / 优化器配置错误检查累积步数、打印参数更新前后的差值5. 工程扩展从玩具模型走向真实应用5.1 数据规模与模型规模同时放大的策略如果你已经成功复现了小型模型的全流程下一步就是如何扩展到更大规模。我想提醒你的是增加模型参数量和增加数据量不应该是同步进行的而是遵循一个阶梯式原则先固定数据规模增加模型参数量观察是否出现欠拟合再固定模型规模增加数据量观察是否出现瓶颈。这样做的好处是你能明确知道当前系统性能的短板究竟在哪里。我在 1500 万参数模型的基础上尝试过扩大到 6000 万参数同样保持 1 亿 token 的数据量结果提升非常有限。这说明 1 亿 token 的数据对 6000 万参数是不够的。反过来我在保持 1500 万参数不变时把数据扩大到 5 亿 token困惑度有了显著下降。这个实验告诉我对小模型来说数据量的优先级远高于参数量。扩数据的同时要格外注意数据处理管道的吞吐能力。我在首次扩容时发现数据加载成了瓶颈GPU 经常处于等待状态。解决办法是采用多进程数据加载预取配合内存映射文件把数据加载时间降到每步可以忽略不计。如果你不想折腾这些工程优化也可以先用一个简单的数据管道跑通扩容实验但你要有心理准备数据管道不优化GPU 利用率通常只有 20% 到 30%非常浪费。5.2 将模型接入实际业务场景的落地思考从零训练一个模型不代表你永远不用预训练模型而是在需要用的时候你能做出更明智的决策。以我经验来看如果你的业务场景很垂直、数据量很大、且有独特的语言模式从零训练小型专用模型是值得考虑的因为你可以完全控制数据分布还能避免外部模型带来的数据合规风险如果你的业务场景是通用型任务那么基于开源大模型做微调会更高效。我在项目里做了一个简单的文本分类头把从零训练的模型接在一个客服工单自动打标场景下测试。由于任务比较单一只用了模型的最后一层隐藏状态接一个线性分类器准确率就已经接近使用通用大模型 prompt 的水平而且推理成本低了一个数量级。这个实验让我确信从零构建模型在特定场景下确实可以成为一套性价比极高的方案。当然从零训练模型也意味着你要负责整个生命周期包括数据更新、模型重训、效果监控、版本回滚。我建议即使是最小的项目也要把 checkpoint 管理器、实验记录、评估脚本这些工程基建搭好否则等模型迭代到第五版第六版时你会发现自己连哪个参数组合产生了哪个效果都说不清楚。我个人的习惯是每跑完一次实验就更新一份 markdown 格式的实验日志里面记录数据版本、模型配置、训练曲线、生成样例。这个习惯帮我避免了很多重复劳动也让我在写项目文档的时候事半功倍。如果你打算走得更远这套日志习惯一定要趁早养成。6. 一些没人告诉你的经验与心得从零开始做 AI 工程这件事技术本身是公开的论文是公开的代码也有无数开源实现可以借鉴真正拉开差距的是那些没人明确教过你的经验。我想用最后这一部分多说几句真心话。第一不要害怕写看起来幼稚的代码。我第一次写注意力机制的实现时为了验证矩阵乘法结果是否正确我用一个很小的随机张量手算了前向和反向整个过程非常笨拙但它让我彻底理解了张量形状的变化。这种理解在以后调试复杂模型时是巨大的财富。很多人觉得写复杂代码才算厉害但我觉得把基础代码写到滴水不漏才是真正的工程能力。第二训练一个模型就像养一盆花环境稳定比拼命施肥更重要。我踩过最大的坑就是想同时改进所有模块结果每次改动一多模型效果反而变差连排查都不知道从哪开始。正确的节奏是每次只改变一个变量比如只改学习率调度、只改数据清洗策略并且记录它对 loss 和生成效果的影响。这样你的项目演进是有迹可循的而不是一团乱麻。第三我强烈建议你在动手之前想清楚评价标准。很多人从零构建模型只顾着让 loss 下降却没想清楚最终要解决什么问题。我在项目中途调整过评价标准从困惑度更低改成生成文本在特定领域的准确率更高这个转变看似微小却直接导致我重新设计了数据采样策略。如果你的目标是生成流畅对话那评价标准就应该是对话质量如果目标是分类准确那评价标准就应该是标注数据上的指标。想清楚了你会少走很多弯路。最后说一个我在多次实操中得到的体会从零起步进度永远比想象中慢但每一步都是扎实的。当你第一次看到自己亲手训练的模型生成出连贯的文本时那种成就感绝不是调用 API 能比的。这个项目虽然只是万里长征的第一步但它会让你建立起一种判断力知道哪些技术决策是可靠的哪些只是表面热闹。无论你接下来是继续深耕模型架构还是转向分布式训练又或者是做模型部署和产品化这次的从零开始都会成为你技术判断力里最坚实的一块基石。
返回列表