尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零构建大语言模型:Tokenizer与Transformer全流程实战解析

从零构建大语言模型:Tokenizer与Transformer全流程实战解析 1. 为什么我决定从零开始而不是调包直接用1.1 从零构建的真实动机不是为了造轮子而是为了捅破黑盒两年前我接手一个内部项目需要做一套轻量的文本生成服务。当时第一反应是直接上开源模型但调研完发现所有开箱即用的方案都有同一个问题当模型输出不符合预期时我只能调prompt、调参数却根本不知道问题出在哪一层。是分词的问题是注意力掩码写错了是训练数据的分布不对还是位置编码的锅这种手里有锤子但不知道钉子在哪的感觉最终逼我下了一个决心——从零手写一套完整的AI工程链路。这个决心落到行动上就是一个命名为ai-engineering-from-scratch的项目用自己的代码从数据清洗、Tokenizer构建、模型结构设计、训练循环到推理服务完整地走一遍大语言模型的构建过程。这篇文章就是我跑完整个项目的完整经验总结核心是分享那些代码库里不会写、文档里不会讲的细节哪些环节最容易出隐蔽bug、显存和效果之间的真实权衡、训练过程中哪些指标值得盯、以及最后怎么把模型变成能用的服务。适合的人群很明确已经会用PyTorch但还想搞懂模型内部机理的人以及做AI工程但长期只是调用框架、想脱离套娃开发的人。1.2 设定合理的构建目标不是复刻ChatGPT而是跑通全流程很多人一听从零构建语言模型第一反应是那得几百张卡、几T数据。这是最大的误解。我的目标定得很务实构建一个参数规模在1亿以内、能通过自回归方式生成通顺文本的GPT风格模型在单张消费级显卡上完成训练最终能通过对话指令完成简单问答。这个目标对应到具体数据上就是训练数据量控制在15-30GB级别清洗后的文本词表大小控制在8000-16000区间上下文长度先按256-512个token设计。听起来不大但足以把所有核心工程环节完整走一遍。后面如果想往更大规模扩展整个代码路径不需要推翻重来只是数据和算力按比例放大的问题。1.3 动手之前需要的基础储备先说清楚起点免得大家走弯路。我是基于以下技术栈做的Python 3.10 PyTorch 2.x深度学习的底子只需要到知道什么是反向传播、什么是Loss、会写Dataset类这个程度。Transformer结构部分最好先对注意力机制有一个概念级理解不了解也没关系我在写模型结构那一段时会直接把每一层的维度变化都拆开讲。硬件方面我实测下来一张显存16G以上的显卡是底线24G会比较舒服。我主用的一张NVIDIA RTX 409024G显存后面所有性能数据都是以这个环境为基准得到的结果。没有4090的用云上的A100或者租卡也完全可以后文我会给出单卡和多卡训练时各自需要注意的配置差异。2. 从零构建的整体架构把一个大问题拆成四个可落地的模块2.1 数据管道语料到训练样本的完整闭环整个项目我用了一个很朴素的原则每个环节的输入输出都是标准格式模块与模块之间通过文件协议解耦。数据管道的第一层是拿到原始文本后做清洗、去重、过滤第二层是把清洗后的文本按行切分、按序列长度截断、构建成训练样本。训练样本的格式就是一个简单的自回归任务给定前n个token预测第n1个token所以每一条样本就是一段token序列PyTorch的Dataset只需要返回input_ids和labels两个字段。第三个层是把样本包装成PyTorch的DataLoader里面需要处理的关键项目包括对齐batch size、设置随机种子保证可复现性、以及处理padding时对应的注意力掩码。2.2 Tokenizer单独开发、离线构建的分词系统我最初犯的错误是直接把现成的分词器加载来用后来发现生产级别的Tokenizer和模型强耦合很难替换。所以在从零构建的过程中我把Tokenizer单独拆出来做采用的是BPEByte Pair Encoding算法从头实现训练merges、编码encode、解码decode三条路径。这样做的直接收益是词表大小完全由我控制语种适配可以按需调整后面要做继续预训练时也能平滑引入新词。2.3 模型核心Decoder-only Transformer的标准形态模型的主干用的是经典的Decoder-only结构token embedding 多层Transformer解码层 输出头。每一层Transformer解码层内部包含自注意力Self-Attention、前馈网络FFN和两个归一化层LayerNorm残差连接贯穿始终。细节上和主流开源模型略有区别的地方我后面会专门用一节说清楚包括我为什么改用了RMSNorm、旋转位置编码和SwiGLU激活函数——这些改动让训练稳定性明显提升收益远高于实现成本。2.4 训练与采样前向、反向、生成的完整链路训练环节包含完整的自回归交叉熵损失函数、AdamW优化器、warmup cosine退火的调度器并且做了混合精度训练和梯度累积。推理环节则是单独的生成脚本支持贪心解码和采样解码top-k、top-p、温度控制。工程实现上我把训练和推理分成两个入口文件模型结构共享这样可以避免训练没问题一上推理就崩的尴尬——这个问题我在第6章会详细讲。2.5 模块间的接口设计用文件和数据规范解耦整个项目的运行流程是这样的原始文本 →preprocess.py→ 清洗后的纯文本文件清洗后文本 →train_tokenizer.py→ tokenizer.json 词表文件清洗后文本 tokenizer →build_dataset.py→ 训练样本文件二进制格式训练样本 →train.py→ model checkpointcheckpoint →inference.py→ 生成文本每两个模块之间只有文件接口没有内存级耦合。这个设计给我带来了一个额外红利任何一步出问题都可以直接从对应环节的产物文件去排查而不需要从头跑一遍。我相信很多项目后期都死在牵一发动全身上这种弱耦合的设计思路值得借鉴。3. 数据集与Tokenizer真正决定模型上限的两个隐形工程3.1 语料选择为什么我放弃了现成的标准数据集很多人会直接用网上打包好的开源语料但实际跑下来这些数据有两个问题一是和你要做的场景不完全匹配二是清洗成本反而更高。我最终的数据构成是三份混合一份是开源的中文百科类数据一份是爬取并清洗后的技术类文章还有一份是自带的通用文本数据。总清洗后体量约26GB。选择这个组合的原因很简单我希望模型能同时具备常识知识和技术问答两类能力。这两种数据在来源、格式、句子长度分布上差异很大正好能测试数据管道对异质数据的处理能力。如果你只做一个垂直领域模型数据构成可以更纯粹但混合数据更能暴露出工程上的坑。3.2 数据清洗细节去重、过滤、长度截断的完整规则清洗规则我按照实测效果迭代了三版最终这一版最稳精确去重对整行文本做hash去重去掉完全重复的内容这个环节大约去掉5%的数据。近似去重这个比较关键。单纯hash去重挡不住同一篇文章改个标题、删改几个段落的情况我实现了一个滑动窗口的MinHash近似去重窗口大小设为20个字符把相似度阈值设在0.75以上。这一步做下来又去掉了约3%的低质量数据。规则过滤把所有纯数字行、无意义符号行、URL行、过短行少于20字全部剔除。语言过滤用语言识别模型检测筛掉非目标语言的长文本段落处理多语种混杂的页面很管用。清洗完成后的数据质量直接决定了模型的下限。我的切身感受是数据清洗环节不管投入多少时间都不算多因为模型后期表现出的奇怪错误—比如文风突变、突然蹦出乱码—绝大多数都能追溯到清洗环节的遗漏。3.3 手写BPE的完整过程从训练merge到编码解码这部分是很多人的卡点我按代码逻辑梳理一遍。BPE的思想不复杂从一个基础词表出发通常是单个字符或字节统计语料中相邻词对的出现频率每次把最高频的词对合并成一个新词重复这个步骤直到词表达到目标大小。训练阶段的具体逻辑初始化词表把每个字符或字节作为一个独立的token。统计频率遍历语料统计每个相邻token对的共现次数。合并找到出现次数最多的pair合并为一个新token更新语料中的token序列。循环执行2-3直到词表达到设定大小我最终用的是12000。编码阶段就是一个应用过程把输入文本切分成字符序列然后按照训练得到的merges顺序依次合并得到最终的token ID序列。解码阶段是编码的逆过程每个token ID都对应一个字符串片段把片段拼接起来就是原文。这里有个必须提醒的坑合并顺序极其重要。训练时是按频率从高到低依次合并但在编码时如果不按照同样的优先级顺序从左到右执行会得到完全不同的分词结果直接影响训练和推理的一致性。我在这个点上踩过深坑表现出的症状是训练loss正常下降但生成的文本偶尔会出现低级的字词碎块——因为推理时用的分词结果和训练时的分布不一致。3.4 词表大小与序列长度的权衡显存视角下的实测结论先说结论我前后试了8000/12000/16000三档词表以12000为最优平衡点。8000太小很多专业术语被切碎token序列长度变长计算效率反而下降16000虽然分词粒度更合理但embedding矩阵的参数量直接多出约3000万在小模型上所占比例不小。上下文长度方面我默认用512。有时会有人问是否直接上2048——在小于1B的模型上长上下文会把大量参数容量消耗在位置信息上实际效果远没有理论上那么好。512作为起点设置成可配置项等数据和训练都稳定后再尝试扩展这才是性价比最高的前进路径。4. 模型结构设计与超参数配置从GPT-2出发的工程决策4.1 基线结构embedding、多头注意力、FFN的维度拆解我的基线模型是标准的GPT-2风格结构具体参数如下表参数项数值模型层数n_layer12注意力头数n_head8隐藏维度n_embd768词表大小vocab_size12000上下文长度block_size512参数量约8600万这个量级的选择逻辑是参数量约8600万embedding层12000 x 768约920万参数占整体约10%——这个比例是健康的因为真正决定模型表现的是Transformer主体部分。每层Transformer解码层的维度流动是这样输入token序列[batch, 512]经过embedding变成[batch, 512, 768]经过多头自注意力后维度不变经过FFN时先升到3072再降回768最后残差连接保持输出维度一致。LayerNorm放在注意力之前pre-norm结构这是现代GPT模型稳定训练的关键设计。4.2 我做的三个关键改动RoPE、RMSNorm、SwiGLU这三个改动让模型训练的稳定性和最终效果都上了一个台阶。从绝对位置编码换成RoPE旋转位置编码。GPT-2用的是可学习的绝对位置编码而RoPE的核心思想是通过旋转矩阵把位置信息注入到query和key中。从效果上看RoPE有更好的外推能力训练中看到的最大收益是在512上下文上训练完后可以直接在比训练长度更长的序列上推理效果衰减明显更慢。从LayerNorm换成RMSNorm。LayerNorm做了均值减法加方差归一化RMSNorm只做方差归一化。少一步均值计算简化后的归一化机制在小模型上反而效果更稳定。我的实测是改用RMSNorm后训练loss曲线更容易收敛很少出现后期震荡。从ReLU换成SwiGLU。SwiGLU是门控线性单元的变体本质上是两个线性变换的逐元素乘积再经过激活。这个改动直接提升的是模型表达能力等价于在相同参数量下获得更强的非线性拟合能力。4.3 为什么这些改动能提升训练稳定性直觉层面的理解这三个改动本质上都在解决同一个问题模型训练早期梯度不稳定。梯度不稳定直接导致的典型症状是loss一开始正常下降突然一步飙升到正常值的几十倍然后永远回不来。RNN时代的经验是学习率调小一点但Transformer时代有了更好的解法用RoPE让位置信息不再随序列长度累积误差用RMSNorm让每层输入的尺度更稳定用SwiGLU缓解深层网络里的梯度消失问题。这三件事做得越到位对学习率、warmup等超参数的敏感性就越低代码就越鲁棒——这才是真正的工程价值。在项目初期这类结构选型上的成本是最值得投入的。4.4 训练稳定性与模型容量的实际权衡当然任何改动都有代价。RoPE的实现比简单加法复杂一些需要把query和key拆成两半做旋转RMSNorm省了均值计算但多了一个可学习的缩放参数SwiGLU把FFN的参数量提升了1.5倍左右两个线性层加门控。对8000万参数的小模型来说这些额外开销完全可控但对更大规模模型参数量翻倍带来的显存和计算成本就需要单独评估。我的建议是如果你完全复刻我这个配置先别动结构跑通一个最小实验当你已经理解每层维度变化后再尝试逐个替换成RoPE、RMSNorm、SwiGLU每次只改一个变量感受差异。这样对整个模型结构的理解会比直接拿来就跑要深得多。5. 训练落地的完整流程从单机调试到多卡训练5.1 训练目标与Loss计算自回归的交叉熵目标训练目标用一个公式就能说清楚对每个序列位置t模型预测第t1个token的概率分布然后和真实的第t1个token计算交叉熵损失。所有位置的loss取平均就是这一条样本的loss。这个目标不需要任何人工标注语料本身就是标签这也是语言模型能大量吃数据的原因。实际代码里有一个容易出错的地方输入是[batch, seq_len]的token序列但每个位置要预测的是下一个token所以标签应该是输入序列右移一位。我实现时直接构造了input_ids[:, :-1]和labels[:, 1:]这样对齐简单也不容易出错。5.2 优化器与调度器AdamW warmup cosine退火的标准配置优化器用的是AdamW权重衰减设为0.1。学习率的配置遵循业界成熟经验warmup步数1000步总训练步数约20000步占比5%峰值学习率3e-4对于8000万参数模型这个量级是实测稳定区间cosine退火到峰值学习率的10%关于warmup的原理简单说就是训练初期模型参数是随机的梯度方向噪声很大如果一上来就用大学习率容易把参数推到一个坏的局部区域先小步走等梯度方向稳定后再加速是提高训练成功率的标准操作。5.3 显存优化的实际手段混合精度与梯度累积的必知组合显存优化是我在这个项目里投入时间最多的环节之一。实测结果很直观纯FP32训练batch size 16已经是极限再大直接OOM。混合精度训练FP16batch size可以拉到32显存占用约降35%-40%。再加上梯度累积gradient_accumulation_steps4等效batch size可以到128单卡即可获得足够稳定的梯度估计。梯度累积是显存不够时最直接的穷人版大batch方案。它把一次大batch的前向后向拆成多次小batch分别计算梯度累积到一个缓冲里再做一步参数更新。注意这里有一个隐藏的环节混合精度训练下梯度最好以FP32格式累积否则小batch的梯度在FP16下会被截断成0导致深层参数更新停滞。5.4 训练过程的监控Loss曲线怎么看、什么时候该停我训练时的最优监控组合训练loss看大趋势是否持续下降正常的曲线应该整体平滑下行中间有小的锯齿波动。验证loss留出1%的数据做验证集每500步算一次。训练loss和验证loss的gap如果有持续扩大的趋势说明开始过拟合。梯度范数这是一个容易被忽视的指标。如果梯度范数突然爆炸超过正常值的50倍以上即使loss还不高也要立即介入否则下一步更新可能把参数推到NaN区域。我在训练到约13000步时观察到验证loss开始平台期训练loss还在微降判断为过拟合迹象就停止了训练。这个步数比原计划的20000步提前了约7000步节省了大量时间——这也是实时盯监控的价值。5.5 单卡与双卡的成本实测我记录了一组实际的训练成本数据环境都是4090配置单卡4090双卡4090DDP混合精度FP16FP16每卡batch size3216梯度累积44有效batch size128128每1000步耗时约38分钟约21分钟总训练步数13000步13000步总耗时约8.2小时约4.5小时峰值显存约21G约19G双卡DDP分布式数据并行只需要在原有训练代码上加入初始化进程组、包装模型的少量代码即可加速比接近1.8倍这个收益非常值得。如果未来继续扩大模型和数据规模直接往多节点扩展即可。6. 推理阶段的重重意外生成乱码、复读机与温度参数6.1 从训练到推理的断层加载checkpoint时的常见坑训练阶段模型用的是完整的训练模式输出带dropout、带缓存推理阶段必须切换为eval模式。很多人看着模型训练得不错一换成推理接口就发现生成内容质量崩了主要原因就是dropout没关或者KV缓存逻辑没有正确实现。我在推理实现里启用了KV Cache生成每个token时把之前的key和value缓存下来避免每一步重复计算前面的注意力。这个优化对推理速度的影响是数量级的未开缓存时生成100个token可能需要10秒级延迟开启后降到1秒以内。KV Cache的正确实现需要仔细处理新token和缓存序列的拼接这是从零实现推理最容易被忽略的工程细节。6.2 解码策略的选择贪心、温度、top-k与top-p的配合生成文本的解码策略直接影响输出质量。贪心解码每次取概率最大的token的问题在测试环境中暴露得很清楚生成的文本高度可预测但缺乏变化句子容易陷入机械式重复。我最终使用的是采样解码具体参数是一组经过大量筛选的经验组合温度temperature0.7top-k40top-p0.9温度的作用本质上是改变softmax输出的概率分布形状温度越低分布越尖锐输出越保守温度高于1.0时分布变平输出越随机。0.7这个值在保持通顺和内容多样性之间的平衡最好。top-k和top-p的作用是截断概率分布防止采样到那些完全没有意义的低概率token。6.3 复读机问题的真正原因与对策模型生成一段时间后开始不断重复同一段内容这是所有语言模型都会遇到的问题。排查后发现原因分两层第一层是解码层的问题贪心解码和固定参数采样都容易陷入概率循环。解决方法是加入重复惩罚系数repetition penalty 1.1对已经生成过的token的概率进行惩罚这个手段简单有效。第二层是模型层的问题模型本身训练时间不够或者数据中重复模式过多导致模型把重复当成了某种安全输出。这种只能靠调整训练数据或增加训练步数来改善。对大多数场景修复解码层的惩罚项是最快速、收益最高的。我在生成脚本里同时实现了这两层对策并保留了统计接口来量化重复率方便调参时对比。6.4 评估模型水平的土办法不依赖Benchmark的快速检测标准Benchmark对8000万参数的小模型来说可能过于严苛我更推荐一套实用的快速评估组合流畅度测试让模型写一段短文听读起来通顺与否重点观察是否有明显语法错误。知识测试问简单百科类问题看是否给出正确信息错误是否属于一本正经胡说八道。重复率测试生成200个token检测n-gram重复占比。稳定性测试同样prompt跑5次观察输出结果的差异度——如果差异过大说明采样参数没调好。这套测试我每次模型迭代后都跑一遍最快10分钟能完成比攒一堆评测集再跑更符合小模型快速迭代的节奏。7. 从复读机到能对话SFT与偏好优化的最短路径7.1 继续预训练什么时候做、怎么做基础模型训练完成后如果发现场景数据占比小效果不理想可以进入**继续预训练Continue Pretraining**阶段。做法很简单用场景内的高质量数据在基础模型基础上继续跑一遍预训练但学习率降为基础阶段的十分之一步数也少一个量级。我在这步踩过一个典型坑继续预训练步数太多直接把模型训忘了之前学会的通用能力大幅退化。解决方法是每训练几百步就做一次通用能力评测一旦发现通用能力明显退化就立即停止。数据和步数的平衡点没有固定公式只能靠监控曲线和评测结果实时调整。7.2 指令微调SFT让模型学会回答问题的最短路径预训练模型的输出风格是续写要让它变成对话助手需要做指令微调SFT。训练数据格式是一组指令-回答对{instruction: 请介绍一下人工智能的起源, output: 人工智能的概念最早可以追溯到...}我在实际筛选数据时有两个经验一是指令的多样性比数量更重要哪怕只有5000条指令只要覆盖了各种问法效果也好过重复的5万条二是回答内容的质量优先回答太短的、不完整的、有事实错误的对模型伤害极大。SFT训练时只需要对回答部分计算loss指令部分的loss置为0不参与反向传播。这样模型会学会跟着指令走而不是把指令和回答都当成连续文本来学习。7.3 DPO偏好优化RLHF的轻量替代方案如果要让模型进一步学会什么回答更好传统做法是RLHF基于人类反馈的强化学习但实现复杂、训练不稳定。我选用了DPODirect Preference Optimization直接偏好优化——不需要额外训练奖励模型只需要准备偏好数据对同一问法下一个更优回答和一个更差回答。DPO调优后模型在回答质量上提升明显更倾向于给出结构完整的回答而不是罗列关键词。从工程角度看DPO比RLHF简单一个数量级效果却已经覆盖掉大部分需求场景是目前小模型对齐最值得优先尝试的方案。7.4 关于从零构建推理模型的思考reasoning model不是新范式最近关于从零构建推理模型reasoning model from scratch的讨论很多。我个人的观点是推理模型不是什么全新的模型结构它的核心突破更多体现在训练方法论上——用高质量的思维链数据让模型在回答前先生成内部推理步骤再给出最终答案。从工程实现上看这条路径完全可以建立在我前面构建的基础模型之上只需要准备一批问题-推理过程-最终答案的思维链数据做SFT让模型学会输出推理链条再用DPO优化推理步骤的质量就能实现基础模型向推理模型的转变。这个路径说明了一个关键结论AI工程的核心竞争力不在模型结构本身而在于数据构造和训练方法的有机结合。从零构建的价值也恰恰体现在这里——当你掌握全链路后任何新方法论都能快速落地成实际能力。8. 踩坑记录与成本实测我的第一手数据8.1 六个隐蔽bug的排查链路整个项目下来最消耗时间的是下面六个问题我按排查过程整理成表给后来人少走弯路现象根本原因排查链路训练早期loss变成NaN混合精度下梯度上溢先关闭FP16确认是否消失再检查loss缩放因子设置最终定位到学习率偏高训练loss正常生成却是乱码推理时的tokenizer和训练不一致对比训练和推理加载的tokenizer配置发现是tokenizer文件被覆盖生成文本每句话都重复开头位置编码未正确缓存KV逐层打印注意力权重发现KV Cache拼接时把位置信息弄乱SFT后模型回答嗯啊等无意义词指令数据质量差回答过短检查训练数据发现大量短回答清洗后剔除短文本问题消失验证loss还在下降生成质量却变差训练数据和验证数据分布偏移抽样对比训练集和验证集文本发现验证集中混入了过多噪声数据多卡训练速度不升反降DDP的batch size设置过小检查发现每卡batch size降到位数之后通信开销占比过高8.2 硬件与时间成本实测从零跑一个模型到底要花多少钱很多人在工程立项时都会被问从零训练一个模型要花多少钱实测数据如下端到端从零训练到SFT完的整体流程含数据清洗、预处理、基础模型训练、SFT共消耗约12小时GPU时间。以云上4090每小时25-35元的价格折算单次完整流程成本在300-450元之间如果只用实验室自有的单卡则成本近似于零主要为电费。数据清洗和Tokenizer构建约占项目总时间的40%但几乎不消耗GPU主要成本是人工写的过滤规则。如果你只用开源预训练模型做微调成本可以控制在3小时约100元左右。但两者的差别在于从零构建留给后续所有迭代的掌控感和自由度是在黑盒上微调无法获得的。这笔账算下来从零做一遍是非常值得的对长期工程能力的积累作用不可替代。8.3 给新手的最短可行路径一份明确的路线图如果让我回到起点重新走一遍我会建议新手按下面这个顺序推进能有效避免时间浪费先做一次小规模完整验证用100MB数据、1000步训练把从数据到推理的完整链路跑通确认每个模块没有逻辑错误后再放大规模。这一步是基础中的基础跳过了再回头调bug的成本会成倍增长。数据清洗和Tokenizer同时并行开发这两个模块互不依赖可以分别推进能在缩短整体进度的同时减少等待时间。在单卡上把模型和训练跑稳定后先不要着急引入多卡把显存优化、监控指标、评估流程全都确定下来再做多卡扩展。基础模型跑通后再做SFT最后做DPO。这个顺序如果颠倒遇到问题时将很难定位。8.4 关于参考书籍与资料的一句话建议市面上关于从零构建大语言模型的中文系统资料确实不多。前些时候看到《Build a Large Language Model (From Scratch)》这本书的评价很高内容覆盖了从数据准备到模型训练的完整路径我在项目推进过程中也参考了其中的部分思路很值得作为对照资料阅读。我的工程代码和这本书的配套代码可以配合起来看一个是教学级的最小实现一个是更偏工程级的完整落地两者结合能少踩很多坑。最后再分享一个我在整个项目结束后最深的体会从零构建一遍之后你再看任何开源模型的技术报告视角会发生根本变化。别人写的每一行超参数配置、每个结构细节在你眼里都不再是黑盒设置而变成了一个个你可以解释、可以调整、甚至可以说这里我不同意的工程决策。这种掌控感才是ai-engineering-from-scratch这个项目带给我的最大收益。
返回列表