ARTICLE DETAIL

资讯详情

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

从零搭建AI工程能力:从微型Transformer训练到应用落地的完整实践

从零搭建AI工程能力:从微型Transformer训练到应用落地的完整实践 1. 为什么我坚持从零开始搭建AI工程能力先交代一下背景。我做了大概六年的软件开发之前主攻的是后端和分布式系统跟AI的关系最多是调调第三方API、把别人的模型包一层HTTP服务。直到去年接了一个内部项目需要在一个完全离线的环境里做一个轻量级的文本分析功能不能调用任何云服务商业模型又不允许直接部署到客户现场——这才把我逼上了从零开始的路。那段时间我翻遍了网络上的资料从Attention Is All You Need这篇论文开始到各种开源仓库再到后来索性自己动手写了一个小型Transformer训练了一把。整个过程跌跌撞撞但收获非常大。这篇文章不是教程式的手把手教学更像是把我从零搭建AI工程能力的过程拆开揉碎讲清楚每一步做决策的逻辑、踩过的坑以及现在回头看哪些弯路其实可以避免。1.1 从API调用到理解底层的跨越很多做工程的朋友跟我一开始一样觉得做AI应用就等于调API。做个聊天机器人用ChatGPT做个分类任务用BERT的fine-tune接口再不行就上AutoML。这套流程在业务验证阶段完全没问题但一旦涉及以下几个场景你就必须深入到底层模型需要部署在私有化、离线、或有合规要求的环境中项目对模型体积、推理延迟、内存占用有硬性指标你需要对模型行为做精细控制比如修改采样策略、约束输出格式你需要在有限的算力下训练或微调一个针对特定领域的小模型你希望理解模型为什么输出这个结果而不是盲人摸象当这些需求出现时会用API这个技能就变得非常苍白。你不仅需要读懂别人的模型代码还需要自己动手搭建数据处理流程、训练循环、评估体系甚至要调整模型架构本身。1.2 从零搭建项目的基本框架数据、模型、训练、评估我给自己定的目标是不依赖任何现成的预训练模型从数据清洗到模型实现到训练推理全部自己走一遍。这听起来工作量巨大但从零的价值恰恰在这里——当你亲手实现了一个哪怕很小的Transformer再回头去看那些开源大模型的文档会发现很多过去晦涩的概念变得非常清晰。整个项目的基本框架我从一开始就确立了就是传统机器学习的四件套数据、模型、训练、评估。每走一步都要把为什么这样做记下来这样才能沉淀出可复用的工程经验。数据部分我的选择是自己爬取并清洗了一批中文语料。具体来说涵盖了技术博客、产品文档、百科条目等总计约2GB的文本。这个过程让我意识到数据清洗的工作量远超预期后面我会专门聊这一块。模型部分我实现了一个约1.5亿参数150M的Transformer解码器结构上参考了GPT-2的设定。训练部分用PyTorch自己写了训练循环没有用HuggingFace的Trainer这一点后面会解释为什么。评估部分我针对文本生成文本分类简单推理这三个场景分别设计了评测集和指标。1.3 适合人群与前置要求什么人适合读这篇文章我建议是已经在用AI的API做开发、有一定Python和基础深度学习概念知道Loss、梯度、反向传播大概是怎么回事的工程师。如果你完全没写过代码那读起来会比较吃力如果你已经是能熟练训练模型的研究员那这篇文章的很多内容对你来说偏基础但工程化踩坑的部分或许还能带来一点参考价值。另外前置的硬件条件我建议至少有一张显存不低于8GB的GPU。云服务器租用也可以但注意训练过程的网络稳定性很关键。我自己是在一张24GB显存的Tesla T4上完成的后续会讲到这个配置下能做什么规模的事。2. 第一个里程碑实现一个微型语言模型当我把数据处理完之后面临一个选择直接下载一个开源预训练模型然后做领域微调还是自己从零训练一个小模型。前者时间成本低后者学习价值高且有部署灵活性。我最后决定两条腿走路自己训练了一个小模型同时也用开源模型做了对比实验。这里先讲自己训练的路线因为它的工程难度更集中、更能暴露问题。2.1 选择最小可运行架构从Transformer到迷你GPT我最初也想直接上Llama的架构但仔细看完论文和代码后发现对于从零学习的阶段最合理的起点是GPT-2的架构设定一个仅解码器的Transformer使用LayerNorm和残差连接注意力头部是因果自注意力。为什么选这个结构因为它简单到没有争议所有核心概念——token嵌入、位置编码、多头注意力、前馈网络——都裸暴露在你面前。我的具体配置如下层数12层隐藏维度768注意力头数12词表大小32,000中文用BPE还是字级别后面单独讲最大序列长度512总参数量约150M这个配置在24GB显存上可以比较舒服地训练batch size开个几十没问题。如果你的显存小一点可以把隐藏维度降到512层数降到8层效果依然可玩。2.2 手写Tokenizer从文本到token序列Tokenizer是我在这个项目里第一个原来如此的瞬间。过去用BERT和GPT我以为Tokenizer就是一个分词函数直到自己实现才发现这里面的设计决策非常深。我一开始图省事用逐字切分的方式。中文嘛字就是天然的最小单位。这个方案实现起来非常简单词表只需要几千个字再加一些特殊符号但训练出来的模型让我很失望——生成的句子看起来字与字之间毫无语法感长一点的句子经常逻辑混乱。后来我换成了BPEByte Pair Encoding算法。原理不复杂从字符级开始反复统计并合并出现频率最高的相邻token对直到达到预定的词表大小。这一轮改造让生成质量有了质的提升模型学会了人工智、深度学这类常用词组的强关联关系。我自己的实现大约200行Python但因为用了大量正则和字典操作速度较慢处理2GB数据花了将近一天的时间。建议读者不要重复造这个轮子。直接用HuggingFace的tokenizers库或者SentencePiece性能好很多而且底层算法和我手工实现的BPE在原理上是一致的。关键是理解BPE的工作方式而不是用代码重新实现一遍。2.3 训练循环与损失函数设计有了数据、模型和Tokenizer接下来就是训练循环。很多人第一次训练语言模型时会很天真地以为Loss降下去了模型就好了但实际上训练过程有非常多的细节在影响最终效果。标准的自回归语言模型训练目标很简单给定前文预测下一个token用交叉熵损失。我用的优化器是AdamW学习率采用余弦退火cosine annealing初始学习率设为3e-4并带了一个包含500步的warmup阶段。权重衰减倒是设得不大因为对这个规模的模型过拟合还没那么严重。这里有一个我认为很重要的设计梯度累积。我单卡T4的显存能塞下的batch size其实不大但语言模型训练通常需要足够大的总批大小才能稳定。我通过梯度累积把有效batch size扩大到了相当于原来4倍的量级训练的稳定性肉眼可见地提高了。还有一个很多人容易忽略的问题数据加载的shuffle策略。如果是按文件顺序读取语料模型会学到非常严重的局部话题漂移——前面的对话和后面的段落风格割裂。我采用了样本级别的shuffle保证每个batch里的文本片段来自语料库的不同位置这个改动让Loss的曲线平滑了很多。2.4 从零训练一个能说人话的模型需要什么配置直接给结论在我上面提到的1.5亿参数配置下用大约20亿token的语料训练一个epoch在T4单卡上大概需要跑3天左右。我实际只跑了约0.6个epoch就得到了一个勉强能接话的模型——生成短句基本通顺但一开始写长段落就完全跑偏。后来加了更多数据并训练满一个epoch效果才到了一个可以接受的下限。必须坦率地说这个效果和ChatGPT完全没法比连很多开源小模型都比不上。但作为一次学习实践它的价值不在好用而在用最小的规模让你理解语言模型的训练全流程。如果你也想复现我建议把目标设定为让模型能产出语法正确、主题相关的短文本而不是追求生成质量上的惊喜。3. 让模型学会推理从预测下一个词到引导逻辑模型能接话之后我开始思考一个更深层的问题怎么让模型做一些看起来需要推理的任务而不仅仅是靠统计关联续写。这里我把推理用引号是因为目前的大模型本质上是概率模型所谓的推理其实是训练和推理策略共同作用的结果。3.1 为什么下一个词预测能涌现推理能力这件事要从语言模型的本质说起。当你给模型输入如果今天是周三那么后天是它并不是在做严格的数学推算而是在根据训练数据中的大量类似语料估计条件概率分布。当训练数据足够多时这类模式被反复强化模型在概率上就更倾向于输出正确答案。我做了很多次递归实验来确定这个结论用不同数量、不同风格的数据训练同样架构的模型发现推理能力的涌现和参数量、数据量呈明显的相关关系而不是某个单独的因素决定。这个理解对工程实践的指导意义在于**如果你想提升一个任务上的推理能力与其调参不如先看看训练数据里有多少类似的高质量样例。**很多人在微调时忽略了这一点构造了一堆格式单调的问题结果模型在测试集上表现不错一到真实场景就露馅。3.2 从Prompt到Chain-of-Thought不修改权重也能提升推理既然模型的输出是概率采样那么通过改变输入的提示词Prompt就可以显著影响输出质量。这就是Prompt Engineering的出发点也是我后续把大量时间投入的方向。具体到推理类任务最有价值的技巧是让模型分步骤思考。举个例子你问模型你有5个苹果吃掉了2个又买了3个现在有几个它可能直接猜一个答案。但如果你在提示词里加入请一步一步思考先算剩余的苹果数量再算增加后的数量模型生成正确结果的概率会大幅提升。我自己做了一个对比实验在一个包含200道小学数学应用题的自建评测集上直接提问的准确率约41%加入一步一步思考提示约67%在提示词中给出一个示例few-shot cot约78%这说明在模型权重不变的情况下仅仅通过提示词引导推理能力就能提升近一倍。对于工程落地来说这是性价比最高的手段。3.3 结构化输出的工程实现推理类任务在真实产品中通常不是直接输出一段话就完事的。你需要让模型输出JSON格式的结果或者在一个固定Schema下填空。这个需求在工程上非常常见但也是LLM最容易出问题的地方。我的做法是三层保险第一层在提示词里严格定义输出格式并给出一个示例。第二层在解码阶段使用logit处理器把与JSON语法无关的token的概率置为接近0强制模型只能输出合法的JSON结构字符。第三层在应用层再套一层解析如果JSON解析失败就重新调用一次模型最多重试三次。第三层这个兜底策略必不可少。即便用了前两层模型在长输出时依然可能出现字段缺失或字符串转义错误的情况。实测下来加了logit处理器和重试机制后稳定率从约82%提升到了99%以上。3.4 用评估集量化推理水平没有评测你根本不知道自己的改动是朝对的方向前进。我的做法是维护了一个多任务的评测集每个任务包含100到200条不等的测试数据覆盖三种类型常识问答如为什么夏天冰淇淋容易化数学推理小学级别的应用题逻辑推断条件推理和排除法题评测指标上对于可以明确判对错的题数学和逻辑直接用准确率对于开放性问答我使用了一个简单但可靠的方法人工打分1到5分制每轮改动后随机抽取20条来做对比。虽然人工打分有主观性但当你是要比较两个版本模型的效果差异时它已经足够可靠。这个评估体系后来帮了大忙——最典型的例子是我某个版本的模型训练Loss下降了但评测准确率反而掉了。如果没有评估集我可能会被Loss下降这个假象蒙蔽上线一个有问题的模型。4. 构建AI应用时的工程化陷阱当你把模型训练出来、效果还算凑合之后真正让人头疼的阶段才刚开始——把它变成工程上可用、可维护、可靠的服务远比训练模型本身更磨人。这一节我把踩过的坑集中复盘一下。4.1 数据质量决定模型上限这句话在各种教程里都被反复强调但我还是想用自己的教训来现身说法。我第一版训练数据是从网上批量爬的清洗过程写了正则、去除了明显乱码但做得很粗。训练出来的模型有一个非常诡异的毛病生成的文章经常在一段话中间突然插入完全不相关的知识点。排查了好几天最后定位到是训练数据里混入了大量百家号式的低质内容——标题可能讲人工智能文章前三分之一讲编程后半段直接跳到了养生。语言模型是把整篇文本当作一个序列来学的这种主题割裂的数据会严重误导模型的上下文建模。后来我重写了数据清洗流程加了三级过滤第一级是格式和长度过滤剔除纯乱码和超短片段第二级是内容质量过滤用规则匹配的方式剔除那些明显的营销号文本第三级是语义一致性粗检查比如计算相邻段落的关键词重叠度。这一轮清洗下来数据量减少了30%但模型生成质量的提升明显高于数据量的减少。4.2 过拟合与泛化小模型的死记硬背vs真正理解小模型因为容量有限非常容易走死记硬背的捷径。我在一个专门做文本分类的微调实验里发现了特别典型的现象验证集准确率很高但一旦遇到训练集里没见过的说话方式几乎百分之百失败。举个例子我微调了一个情感分类模型训练数据里很常见的表达是太棒了非常满意这类直白词。但测试时用户说的是这个产品的设计让我觉得设计师还挺用心的模型直接判成了中性。解决方案有两个方向一是增加数据多样性同一个语义用多种句式表达甚至做简单的数据增强比如把主动句改写为被动句二是给模型设置一个拒答或不确定的出口让它在低置信度时不要强行分类而是回复信息不足无法判断。后者在真实产品中有非常大的工程价值因为用户的自由输入永远超出你的想象。4.3 显存管理与批处理调优训练和推理阶段的显存管理是每个从API走过来的工程师最容易懵的地方。第一次自己写训练循环大概每跑一个batch就崩溃一次要么OOM要么中途显存占用持续上升。教训有几个首先PyTorch的默认行为会累积计算图如果不调用optimizer.zero_grad()显存会随着迭代次数线性增长直到崩溃。所以zero_grad必须放在每个batch开始的位置。其次model.eval()和torch.no_grad()必须在推理时一起使用否则即使你只是做预测显存也会因为构建了计算图而膨胀。第三学会启用torch.cuda.amp混合精度训练这在T4上的加速效果很显著显存占用也能减少不少。还有一个很多人不知道的小技巧在模型定义时把不需要的中间变量显式删除。比如在实现多头注意力时attention_weights这个变量如果不释放会一直残留在显存里。用del删除并调用torch.cuda.empty_cache()可以释放碎片但这个操作别太频繁否则反而降低性能。4.4 多阶段训练策略预训练、指令微调的对齐从零训练好一个基座模型之后你会发现它虽然能续写文本但并不听话——你让它翻译它可能给你编故事你让它分类它可能跟你聊人生观。这就是基座模型和助手模型的差别。解决这个问题需要做指令微调Instruction Tuning用大量指令期望输出的配对数据继续训练模型让模型学会服从指令。我构造了一个相对简单但有效的做法把任务分为三类问答、摘要、分类并为每类写了20到30个不同的指令模板再配合生成的训练数据构建了约10万条指令数据。这里的关键经验是指令微调的数据格式一定要统一而且要覆盖足够多的同义指令表达方式。如果训练时只有请翻译以下内容测试时用户说帮我翻一下模型大概率就会不知所措。我把指令数据做了大幅度的同义改写确保模型学到的是理解意图而不是匹配句子模式。5. 从模型到产品最小可用的AI系统集成模型本身只是一部分真正到产品层面需要考虑的维度突然多了很多部署方式、请求并发、延迟控制、失败兜底、可观测性。这一节我尽量把最有通用性的部分讲透。5.1 服务化部署从notebook到API训练阶段你可以在notebook里慢慢调试但产品化必须把模型封装成服务。我选择的是基于FastAPI搭一个简单的推理服务用PyTorch的TorchServe做模型管理。这个选型的逻辑很简单Easy。部署时遇到的最坑问题是首请求延迟。模型第一次加载到显存、做torch.compile或CUDA图优化可能需要几十秒但产品层不可能容忍这个等待。我的解决方案是服务启动时预加载模型并用一个假的输入预热一次推理确保所有CUDA内核都完成编译然后才标记服务为就绪状态。用K8s的readiness probe配合这个逻辑就避免了实际请求打到一个还在准备的节点上。另外单条请求的推理延迟也很关键。我为了降低延迟做了三件事开启混合精度推理、把小batch的请求拼接成一个大batch服务端整理优化、对输出做了截断上限。最终把单次推理延迟从约1.2秒降到了约400毫秒这个量级对多数内部工具类场景已经够用。5.2 面向业务的评估体系很多时候模型的离线指标和用户体验之间存在鸿沟。我的一个分类模型离线准确率93%但实际用户反馈经常答非所问。后来发现原因在于离线测试集的样本分布和线上完全不同。离线测试集里每个类别的样本数差不多但线上80%的请求集中在两个高频类别。解决这个问题的方式是建立一套面向业务的评估体系把线上真实流量记录下来按周抽样人工标注成评测集然后持续把模型输出和标注结果做对比。这听起来笨拙但我必须强调这是所有评估手段里最有效的一层校验它可能帮你发现各种你在离线场景根本想不到的问题。与此同时我还建立了一套轻量级的模型行为监控统计每条请求的响应置信度、生成长度、响应时间以及失败率。一旦某个指标偏移超过阈值就触发告警。这让你能在用户大规模投诉之前就发现问题。5.3 为AI系统设计兜底机制AI系统默认是概率性的意味着无论你怎么优化它都会有出错的时候。我强烈建议在做面向业务的设计时默认假设模型不可靠然后为之设计兜底机制。我在自己的产品里定了一组兜底策略低置信度时明确告知用户我无法确定答案而不是胡编任务失败时提供固定规则的可替代方案比如关键词匹配的简单问答库用户可干预允许用户对模型输出进行纠错并把纠错结果反馈到数据积累中这样的设计在工程上多花的功夫并不多但产品体验的提升非常明显。用户最反感的是模型一本正经地给出错误的答案而坦诚表示不知道反而更容易获得信任。5.4 多模型协作与Agent的基本范式随着项目深入单一模型越来越难覆盖所有需求。我开始尝试多模型协作一个轻量模型负责意图识别路由给不同的专用模型一个模型负责草稿生成另一个模型负责纠错和润色。这种架构目前来看已经是AI应用的主流形态了。我用的编排逻辑并不复杂第一步意图识别模型把请求分成若干类别第二步根据类别路由到对应的处理模型比如翻译走翻译模型、摘要走摘要模型第三步如果生成结果质量不达标触发一个修改模型对原文和初稿进行迭代优化。整体上就是一个非常简陋的Agent实现。这部分的经验是不要一上来就追求复杂的Agent自动规划先让各模型各司其职用规则串联起来。等规则越来越复杂、分支越来越多之后再考虑引入大模型做动态规划。否则前期调试的复杂度会让你寸步难行。6. 我从这个项目里踩过的坑与建议说完了主要的技术路线最后必须单独把踩坑经历拎出来。有些坑是教程不会写的但对工程实践影响极大。6.1 生成质量与幻觉问题语言模型在生成事实性内容时经常会出现一本正经的胡说八道。我最初以为这是模型规模不够的问题但后来用了更大的开源模型发现幻觉依然存在。于是我把注意力转向引用来源要求模型在回答事实性问题时必须附带引用的文档片段否则拒绝回答。这个策略的工程落地需要配合检索RAG。我搭了一个简单的检索库先根据用户问题召回相关文档片段再把这些片段作为上下文提供给模型同时告诉模型如果没有依据就回答不知道。实测下来幻觉率显著降低但代价是回答速度变慢——每次请求都要多一次向量检索和文档拼接。我的最终建议是如果你的场景是事实性问答RAG不是可选项而是必选项。6.2 训练稳定性问题从零训练语言模型的过程中Loss曲线突然飙高是家常便饭。我遇到过一个让我熬夜两天的bug训练到大约70%的时候Loss从2.1突然跳到6.8而且再也降不回来。排查了一圈最后发现是数据管道里有一个概率极小的并发问题导致某个batch的文本数据被意外截断产生了一堆垃圾输入。这种问题没有银弹只有两类手段能改善一是给数据管道加一层健康检查每个batch输入模型之前校验序列长度和数据格式二是周期性保存检查点checkpoint并且在发现异常时能快速回滚到最近的健康状态。我后来把检查点保存频率设成了每500步一次虽然磁盘占用变大但心理安全感完全不同。6.3 时间和资源的分配策略从零训练一个模型非常耗钱耗时如果一个劲地埋头训练很容易一周只跑了两三个实验迭代效率极低。我的现实经验是用几分钟级别的冒烟测试来验证数据管道的正确性用小规模比如十分之一的数据量跑一个完整epoch估算整体训练时间在确认数据没问题之后再启动长训练任务更关键的是别把长训练和实验迭代串行长训练跑着的同时并行准备下一个实验的数据、写评估脚本、整理Bug清单。一台GPU的算力是有限的但你的分析和沉淀可以不占算力。6.4 最终建议如果让我给一个想从零开始做AI工程的工程师列一个建议清单大概是这样的先定目标是用一个月的业余时间搞懂原理还是用三个月做出产品目标不同路线规划差异巨大。别贪大不要一上来就想复现ChatGPT先做一个能跑的玩具级别的完整链路更重要。重视数据管道很多模型的性能问题都要追溯到数据准备阶段花大力气做数据清洗是值得的。建立评估没有评估体系的训练等同于盲飞。兜底思想贯穿始终任何时候都要问自己如果这个模型出错了系统能不能兜住我在实际做这个项目时最大的体会是从零这个词的重量。它不是一个浪漫的宣言而是一种把每一层遮蔽都剥开、让每个概念都经受实践检验的笨功夫。但恰恰是这种笨功夫让我在后来面对任何AI工程问题时不再恐慌和无从下手。这篇文章写的就是这段笨功夫的完整记录。希望对你也有参考价值。
返回列表