ARTICLE DETAIL

资讯详情

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

从零构建大模型:手写Transformer与AI工程实战全指南

从零构建大模型:手写Transformer与AI工程实战全指南 1. 为什么选从零这条路拆掉框架这层滤镜再看AI工程先讲个真实场景。去年有个朋友来找我说想学大模型我给他推荐了一条从零构建的路线。他第一反应是现在transformers库三行代码就能加载一个GPT模型我为什么要费劲去手写注意力机制我说你装好GPT的轮子跟真正驾驭GPT是两回事。他不太服气直到他在自己的任务上调不动参数——加数据涨点不显著改了学习率loss直接发散他对着报错信息完全无从下手。那一刻他才意识到框架替你屏蔽了太多实现细节但也把你的理解一并屏蔽掉了。ai-engineering-from-scratch这个标题想表达的态度其实很直白不做只调包的应用摆渡人而是亲手把AI系统从第一行代码开始重新搭一遍。这里的from scratch不是标榜我不用任何库、不用任何工具而是指核心机制必须自己复现。你可以用PyTorch做张量运算但自注意力的QKV矩阵、位置编码、残差连接、训练循环每一个环节都得从数学定义推到你自己的代码里。这条路适合谁一类是想转AI工程方向的开发者之前主要做业务系统对模型的理解停留在输入输出黑盒另一类是已经在用现成框架做微调、部署的工程师遇到了框架给不了的调优和排错问题还有一类是纯粹想搞懂大模型原理的学习者。无论哪种情况当你能亲手写出一套能训练、能推理的完整Transformer时你再看任何开源模型的架构图、论文里的公式、训练日志里的曲线认知都会完全不同。一个很重要的体会从零构建AI系统最大的回报不是我复现了GPT这个结果而是排查问题时的底气。你亲手写过前向传播就知道梯度从哪个环节开始消失你亲手算过参数量就知道显存为什么会爆炸你亲手处理过词元化就能解释为什么提示词里加空格会让模型表现不稳定。这些能力在框架文档里永远学不到。我把这一整轮实践拆成了七个阶段下面按我实际推进的顺序展开。每一段都会讲清楚为什么这么设计和我踩过的坑而不是只给结论。2. 真正的从零起点先定义什么叫做一个可用的AI系统很多人一听到从零构建大模型脑子里浮现的是从CPU设计、操作系统底层开始造轮子的画面。我在GitHub上见过不少半途而废的项目原因几乎都是同一个起点定得太高第一个星期的挫败感直接劝退。from scratch的科学含义不是从晶体管开始而是从一个约定好的最小边界开始。我的约定是这样的底层张量运算、自动求导可以使用PyTorch或JAX因为重新发明自动微分没有认知增量但模型架构、数据管道、训练循环、推理采样逻辑全部手写不使用任何预训练权重不加载任何他人的模型参数每一步训练都从随机初始化开始。为什么这样划分因为AI工程的核心知识壁垒恰恰在架构设计、数据配比、训练策略和推理优化这一层。框架只是载体。你学会了这个边界内的所有机制把PyTorch换成MindSpore或者纯NumPy迁移成本是可控的反过来你就算背得滚瓜烂熟PyTorch的API不理解内部机制换一个场景依然抓瞎。在这个边界内一个最小可用AI系统至少需要具备四样东西词元化器把原始文本切成模型能读取的词元序列对应真实系统里的分词与词典管理生成模型主体一个完整的前向传播网络能把词元序列映射到下一个词元的概率分布对应真实的Transformer架构训练循环损失函数、反向传播、参数更新、评估逻辑对应真实系统里的实验流程推理接口给定一段前缀文本能逐个采样生成后续词元对应真实系统里的部署服务。我一开始犯的错误是把顺序搞反了。那时候我急着想看模型训练出来的效果跳过词元化设计直接拿现成的分词器用结果后面做数据处理时对词元边界完全没有掌控力。后来老老实实按词元化器→嵌入层→单层注意力→多层Transformer→训练循环→推理的顺序走了一遍每个环节配一个可运行的脚本进度反而快得多。关于参考资料我强烈推荐《Build a Large Language Model from Scratch》这本书的做法——它采用的是逐层复现小规模实验的路线跟我在实践中摸索出的方法论非常接近。网上能找到不少二手笔记和拆解文章但如果你认真走这条路线买一本正版支持一下作者价值远超一杯咖啡的钱。还有一点必须想清楚你从零复现的目标是什么如果你幻想在消费级硬件上从头训练出ChatGPT级别的模型那我劝你换个项目。个人项目从零构建的合理目标是做出一套理解通透、能端到端运行、规模虽小但五脏俱全的系统。以目前开源社区的数据和工具链训练一个GPT-2级别的小型生成模型在一张消费级显卡上是可以完成的任务但你需要学会跟算力约束做交易。这一点我会在后面单独用一节讲。3. 核心组件手写实录词元化、嵌入与自注意力的工程化理解3.1 词元化器先解决文本怎么切的问题词元化是整条链路里最不起眼、但最容易被忽视的模块。它的任务简单说就一句话把一串字符切成一堆整数ID。但切法不同模型学到的语义边界完全不同。最原始的词元化是字符级——每个字符一个ID。优点是词表极小几十个就够缺点是序列太长语义单元被打碎。最常见的词元化是词级——每个单词一个ID但自然语言里词形变化太多《猫》和《猫的》是两个完全不同的ID数据稀疏得可怕。现代大模型几乎都采用子词级别的词元化其中最经典的是BPE。手工实现一个小型BPE是这个阶段最好的工程练习。核心逻辑其实不复杂把训练文本拆成单个字符序列每个字符是一个初始词元统计相邻词元对的共现频率每次把频率最高的一对相邻词元合并成一个新词元重复第二步直到词表达到预设大小。举个例子文本里经常出现ing这个片段。初始时是字符i、n、g三个ID通过几轮合并in会先变成inin再和g合并成ing。最终模型看到的是一个整体词元而不是三个字母的组合。这一步工程实践踩过的坑是合并顺序不可逆。BPE一旦合并完成没有回退机制所以你要尽早决定词表大小。词表太小词元化粒度粗模型表达能力受限词表太大嵌入层参数暴涨数据量不够时容易过拟合。经典GPT-2的词表是50257个词元这个数字后来被很多模型沿用原因是它把常见的英文子词数字特殊符号基本覆盖住了不会过度膨胀。3.2 嵌入层与位置编码给词元一个可学习的地址词元化之后每个词元是一个整数ID。嵌入层的任务就是把整数ID映射成一个稠密向量。这一步本身很直观——就是一个可学习的查询表权重维度是词表大小 × 嵌入维度。但这里有个很关键的设计问题注意力机制本身是对位置不敏感的。你交换序列里任意两个词元的位置注意力计算的结果在数学上完全一样因为注意力只看词元之间的相似度不看谁先谁后。为了让模型感知顺序必须显式注入位置信息。最初的Transformer论文用的是正弦余弦位置编码——一组固定的三角函数值好处是未经训练就能捕捉相对位置关系且能外推到比训练时更长的序列。后来很多模型改为可学习的位置嵌入——把位置编号当成另一个词表去查询好处是训练阶段能更好地适应数据分布。GPT系列用的就是可学习方式并约定最大序列长度超过部分直接截断或报错。工程层面的细节是嵌入层权重要不要给一个缩放的初始化你直接用一个标准差为0.02的正态分布初始化通常会比默认的均匀分布更稳定。这个小参数在前向传播里看不太出来但在深层模型里会通过残差连接逐层累积值得在一开始就统一好。3.3 自注意力机制模型内部的开会机制自注意力是整个Transformer的引擎值得掰开揉碎讲清楚。如果你开过项目周会你就理解自注意力在干什么。会议里有三个角色QQuery——你想了解什么信息的提问者KKey——每个人出示的名牌VValue——每个人实际提供的汇报内容。每个参会词元都会生成自己的Q、K、V然后做两两匹配拿自己的Q去点乘别人的K得到我该关注谁的分数再用softmax把分数变成权重最后按权重取所有人的V。这个过程本质上就是让序列里每个位置都能动态地决定要从序列其他地方聚合哪些信息。工程实现时有四个细节决定性能也决定你能不能把模型训稳缩放因子1/√d_kQ和K的点积结果会随维度增大而增大一旦数值过大softmax的梯度会区域饱和学习效率骤降。除以√d_k让点积方差维持在1量级这是Transformer能顺利训练的关键细节之一。因果掩码causal mask生成任务里一个位置只能看到它之前的词元否则就是作弊。实现方式是把注意力分数矩阵的上三角部分填充为一个极大的负数通常用-1e9经过softmax后这些位置的权重趋近于零。多头拆分不是所有词元之间的注意力权重都一样重要。多头机制把嵌入维度切成多个子空间每个头负责不同类型的关注模式——有的头关注句法关系有的头关注指代消解有的头关注共现统计。多头合在一起表达力远超单个头。内存瓶颈注意力分数矩阵的形状是序列长度 × 序列长度。序列长度1024时是百万级元素序列长度8192时是六千万级。这就是为什么长文本模型必须引入稀疏注意力或者FlashAttention这类优化否则显存先爆。如果你从零手写注意力千万别一上来就写多头并行版本。先写单头、无掩码的最简版本在小数据集上把前向传播跑通再逐步加因果掩码、多头、残差。每加一个组件就验证一次输出形状不然报错信息会让你欲哭无泪。3.4 残差连接、层归一化与前馈网络让网络变深的三兄弟深度网络训练困难早已是旧问题。残差连接解决的是梯度信号衰减每一层的输出都额外加上本层输入让梯度至少有一条直通路可以回溯。没有残差的Transformer堆到六层就开始明显显现训练不稳定的迹象而我见过加了残差的版本能稳稳堆到二十层以上。层归一化则解决内部协变量偏移每个词元向量的特征在层间传递时均值和方差会漂移。LayerNorm的思路是沿着特征维度做标准化——对每个词元向量算均值和方差然后缩放平移。这与BatchNorm完全不同BatchNorm跨样本归一化在变长序列的NLP任务里不仅语义上不自然训练时还依赖足够的batch大小。LayerNorm则无此问题每个词元独立处理。前馈网络是每个位置的独立思考时间注意力完成的是词元间信息交换前馈网络是对聚合后的表征做非线性变换。经典实现是两层全连接中间用一个GELU激活函数——它是ReLU的平滑版本在某些位置的梯度行为比ReLU更稳定这也是大模型普遍选择它的原因。我曾经为了减少代码量把残差连接直接写成x x sublayer(x)跳过了LayerNorm。结果模型在训练早期loss下降挺快到了中后期开始剧烈震荡怎么调学习率都压不住。后来对比正确实现才发现LayerNorm的归一化不是辅助是深层网络稳定性的核心构件之一。这种认知只有自己踩过一遍才会真正吸收。4. 数据与算力约束下的现实对策算不清账之前别急着烧钱4.1 数据从哪来比例怎么配真正的从零训练数据工程占的精力绝对不比模型架构少。你手写一个Transformer可能花两周但准备一份可靠的数据集可能需要一个月。开源社区目前比较可靠的数据源有几类The Pile一个多领域混合的英文语料、RedPajama按LLaMA配方重制的开源数据、FineWeb质量筛选过的网页抓取数据。中文场景可以补充一些高质量的开源语料但务必注意版权与使用条款。数据配比直接影响模型行为。纯网页文本训练出来的模型写代码能力会明显偏弱反之掺入过多代码则可能让模型在自然语言对话里显得机械。我在一次实验里把代码数据的比例调高到25%结果模型在开放式写作任务里频繁输出缩进和分号。后来回退到15%以内情况才恢复正常。这个比例因目标场景而异但一次只动一个变量、其他保持固定的原则一定要坚持。数据质量比数量更重要这句话我强调多少次都不为过。脏数据里最常见的三类问题重复文本同一段落出现几十次模型会把它们当成高频模式学习、编码错乱HTML转义符残留、乱码字符、语言分布偏斜你以为是中文语料清洗后发现有大量英文或代码片段。我习惯在训练前对数据集做一次全面体检统计重复占比、字符集分布、样本长度直方图。如果重复率超过5%训练出来的模型极容易在生成时出现整段复读现象。4.2 算力约束下的选型小模型吃透流程大模型吃透效果这是最容易让人眼高手低的环节。网上铺天盖地都是几百B参数模型的新闻但那些模型训练一次的成本以千万美元计不在个人项目的讨论范围内。个人算力方案的现实分界线大概是这样的8GB左右显存的消费级显卡适合训练参数量在1亿以下的模型例如GPT-2 Small级别的124M参数24GB显存的显卡如RTX 3090/4090量级可到3.5亿参数左右的模型但要配合混合精度和梯度累积只有CPU或云平台免费额度的话建议把目标定为几十M参数的玩具模型重点是把流程跑通。以GPT-2 Small为例来算一笔账124M参数以FP32全量训练时模型参数一份、梯度一份、Adam的一阶和二阶矩各一份光优化器状态就需要约2GB显存。再加上前向传播的激活值、中间缓冲区一张8GB显存卡勉强能跑但batch size会被压得比较难受。用混合精度FP16/BF16能显著降低显存压力换来的是训练稳定性的下降——你得更小心地处理梯度缩放。我自己的经验策略是在8GB卡上先用GPT-2级别的小模型把全流程跑通包括数据管道、训练、评估、推理等所有机制都熟练了再租云GPU去跑更大规模的实验。这个顺序能帮你省下大量云账单。不要抱有我直接上一个7B模型就能出好效果的侥幸新手在7B模型上的第一轮结局通常是显存爆炸然后花几天时间学各种并行策略最后发现核心原理还没弄明白。如果你想用MacBook或者CPU环境体验完整链路也不是不行只是要把期望值调低训练时长从小时级变成天级。做好断点续训机制——每隔一定步数保存一次checkpoint这个习惯在任何规模下都值得养成。我早期吃过一次大亏训练跑了18个小时中途断电因为没存checkpoint全部白费。自那以后每隔几百步保存checkpoint成了肌肉记忆。另一个实用技巧是用微缩数据调试代码。先在几十条样本上跑通前向、反向和更新再去全量数据上训练。很多人忽略了这一步直接全量开跑结果第一天就发现损失恒为NaN浪费一整天。先用小数据验证代码逻辑的正确性再上规模的思路在任何深度学习项目里都适用。5. 训练循环里那些文档不会写的事损失曲线、学习率与过拟合的对话5.1 训练循环的骨架与常见反模式训练循环本身代码量不大取batch输入模型计算交叉熵损失反向传播更新参数。但很多从零项目死在这段看似简单的代码上。先说说损失函数。语言模型的标准目标就是交叉熵对每个位置模型输出的分布要尽可能贴近真实的下一个词元。工程实现时通常把目标词元序列向左偏移一位——输入我要预测爱输入我爱要预测你。这个偏移处理写错一个维度训练信号就全乱了。我遇到过一个隐蔽性极强的bug损失在下降但生成质量一塌糊涂。排查了两天才发现我在构造标签时没有屏蔽填充位置模型在填充符上学到了一堆无意义模式。解决办法是在损失计算时引入ignore_index让填充位置不参与梯度计算。这个细节在框架文档里只是一行参数但在你的手写代码里就是模型好坏的天壤之别。5.2 损失曲线的三种典型形状与应对策略训练语言模型的头几百步loss曲线通常会呈现三种形态平稳下降理想状态继续训练即可震荡下降但幅度较大可能是batch size太小或者学习率偏高。可以尝试加大batch或梯度累积、降低学习率一上来就NaN通常是数值问题。排查顺序先是学习率是否过大再是输入数据是否含NaN或异常值然后是混合精度下的梯度溢出。关于学习率的设置我的经验是先看曲率再定起点。先用一个读数为3e-4的学习率跑几百步观察如果loss在40步内出现明显震荡降到1e-4如果下降缓慢上调到6e-4。warmup也很关键——前几百步学习率从零线性爬升到目标值防止初始更新步子太大导致早期不稳定。训练后半程用余弦退火逐渐衰减学习率通常比固定的学习率收敛更顺滑。5.3 过拟合、欠拟合与验证集的真正作用在消费级算力上训练小模型最常见的困境恰恰是过拟合训练loss一路降到1.0附近验证集loss却停在3.0不再下降。这说明模型在背诵训练集而不是学习泛化规律。解决手段按优先级排序减模型规模→加数据→加正则dropout/权重衰减→降低训练轮次。欠拟合则相反——训练和验证loss都很高此时优先检查模型容量是否足够、数据是否有信息量、学习率是否过低。不要一上来就盲目加层数先确认数据和训练配置没问题。训练过程中必须坚持记录三类日志loss训练/验证、PPL困惑度loss的指数、当前上下文采样结果每隔固定步数打印模型生成的一段文本。前两个指标告诉你拟合状态第三个指标告诉你模型的学习方向有没有跑偏。我见过loss正常下降、生成文本却全是不成词片段的情况正是靠采样输出才及时发现了词元化配置的边界bug。关于梯度裁剪很多人嫌麻烦不设。但对语言模型梯度范数时不时会飙到几十甚至上百不加裁剪的后果就是loss曲线出现尖刺随后模型陷入坏局部最优。设置max_grad_norm1.0这种常规值并不会明显拖慢收敛却能让训练稳定很多。这是一个投入产出比极高的操作。6. 从next token预测到会推理的模型进阶路线的实践拆解6.1 为什么预训练模型不会回答问题你费了好大劲从零训出一个能流畅续写的模型然后输入中国的首都是什么它给你续写出一堆北京是中国的首都也是……表现不错。但当你输入1加1等于几时它可能输出1加1等于几这是一个非常有趣的数学问题……。原因很简单预训练的目标是预测下一个词元训练数据是互联网上的自然文本模型学会的是这段话之后最可能接什么而不是如何精准回答问题。这个差异只有在转向对话和推理场景时才会暴露。6.2 指令微调SFT把生成能力掰向回答问题要让模型学会回答问题你需要构造一批指令-回答对在预训练模型基础上继续训练。一个关键的工程细节是损失遮罩只计算回答部分的loss不计算指令部分的loss。因为指令部分是输入去预测它是没有意义的反而会让模型偏离听指令的行为模式。指令数据的质量直接决定微调效果。我见过的失败案例大多死在数据看起来多但多样性不足上。3000条高质量、任务类型分布均匀的指令数据效果常常好过3万条同质化严重的数据。任务类型要囊括知识问答、摘要、改写、翻译、代码生成、数学推理、对话多轮等。6.3 从指令微调到推理能力一个量变到质变的过程单靠SFT模型能会说话但还不会深思熟虑。推理能力怎么来目前主流答案之一是推理轨迹数据——让人或更强的模型写出完整的思维链chain-of-thought让模型模仿先想后答的过程。近期开源社区对build a reasoning model from scratch的讨论热度很高很多研究者在小规模模型上复现推理能力的尝试给了后来者很大参考。核心思路并不神秘用SFT让模型学会输出带思维链的长答案关键是思维链要有真实的中间推理步骤而不是形式化的让我们一步步分析再用偏好类方法DPO/GRPO这类比PPO更轻量的对齐方法强化正确的推理路径弱化错误路径推理能力的涌现本质上依赖模型在预训练阶段积累了足够丰富的知识关联思维链只是把这些关联显式地组织起来。在算力有限的情况下构建一个小型推理模型的现实路线是蒸馏拿一个已具备推理能力的开源大模型在特定任务集上生成带思维链的样本用这些样本来做SFT。这种方式比自己从零训练推理能力强得多成本也低得多。我在这条路径上实践过一个几百M参数的模型经过几万条推理样本的微调后确实表现出一定程度的多步思考虽然和前沿模型不能相提并论但作为理解推理机制的教学样本已经足够说明问题。6.4 在受限算力下怎么判断我的模型真的会推理了不要用记忆题来测推理。比如问三大火炉城市是哪几个模型答对了也可能是记住了语料。更好的测试方式是构造出训练分布之外的问题看模型能否用已有知识进行组合推理。例如给模型一个自定义的规则苹果叫A香蕉叫B请回答A加B等于什么如果模型能正确套用规则说明它具备一定的符号操作能力而非纯粹的统计续写。我还习惯把模型想不到的情况记录下来作为下一轮数据增强的素材。真正能提升推理能力的数据恰恰是那些让模型初次答错、但通过纠错能学会的样本。这跟人类学习的逻辑是一致的。7. 这一轮实践教会我的以及希望你能避开的路从零把AI工程走完整一圈之后最大的收获不是复现了某个模型架构而是建立了一套完整的调试直觉。现在看到训练日志里的异常曲线我脑子里会自动浮现出可能出问题的环节清单数据管道、词元化边界、损失函数、学习率、梯度状态、数值精度。这种哪一层出了问题的定位能力靠读文档学不来只能在亲手构建系统的过程中养成。如果你决定走这条路我把自己的教训浓缩成几条建议一条是先跑通再优化。别在第一次实验里就追求完美架构和最优超参数。先在一个极小数据集上跑通全流程哪怕产出结果烂到不忍直视也要确认每一个环节都是通的。再逐步拉大模型和数据规模。第二条是一次只改一个变量。从零训练涉及到太多可以调的东西层数、头数、嵌入维度、学习率、batch size、数据配比。如果你同时改三个出了任何问题你都说不清是哪个改动引起的。这个原则我自己违反了很多次每次付出代价后才长记性。第三条是checkpoint和日志规范从第一天就建立。训练跑一个小时之后你会无比感激当初那个每500步自动保存一次模型的自己。而一套包含时间戳、数据版本、超参数、验证指标的日志规范能让你在十天之后回顾实验时不会对着数字发呆。最后建议你选择一个真正感兴趣的垂直场景来练习比如用你自己的博客文章、领域语料或特定格式的数据做一个小型生成模型。目标越具体你在数据清洗和评估上的动力就越足。抽象的我要训练一个大模型会让人迷失而我要做一个能续写我博客风格的模型会让每一步都有明确的验证标准。这条路不短但每一步的回报都是实打实的能力积累。如果你卡在哪个环节——词元化器输出不对、注意力维度搞混、训练曲线诡异——欢迎你在评论区把报错信息和训练日志贴出来我尽量给出排查方向。
返回列表