ARTICLE DETAIL

资讯详情

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

从零构建小型语言模型:数据、分词、训练到推理的完整实践

从零构建小型语言模型:数据、分词、训练到推理的完整实践 AI engineering from scratch字面意思就是从零开始做AI工程。最近我花了三个月把一个小型语言模型从数据处理、分词、到训练、推理完整跑通这篇文章就是这个项目的全过程记录和经验沉淀。它不是三天上手大模型的速成教程而是一个把黑盒拆开的实践复盘希望对想亲手构建大语言模型的人有参考价值。这篇文章最适合两类人一类是长期调用现成API做应用的开发者想知道模型内部到底是什么样另一类是刚入门深度学习的同学想通过一个完整项目把Transformer、BPE、训练循环这些抽象概念串成一条线。如果你只是想要一个能跑的demo市面上有大量更省事的方案但如果你想理解它为什么能跑、为什么有时候跑不好从零构建是我目前走过的路径里最快的一条——尽管它看起来最慢。先说一个总体判断从零构建语言模型核心链路只有五步——数据准备、分词器、模型架构、训练循环、推理评估。链路本身不复杂复杂的是每一步都有大量细节。任何一个环节出问题最后都表现为两个症状loss不降或者生成结果像胡话。下面我按实际执行顺序把每一步的做法、取舍和踩过的坑完整写出来。1. 项目整体设计为什么非要从零开始1.1 从零开始的真实含义很多人听到从零构建大语言模型第一反应是这得需要多少张显卡是不是要复现GPT-4。其实真正的from scratch并不是这个意思。它不意味着从晶体管开始造计算机也不意味着不能用深度学习框架。它指的是不加载任何预训练权重不把Transformer当黑盒直接调用而是用基础的张量操作把模型的关键组件亲手实现出来并独立完成从文本数据到可运行模型的完整闭环。我在这个项目里训练的目标模型参数量在8000万左右大约是GPT-2最小版本的二分之一。它肯定比不了生产级大模型但它具备一个语言模型所有的核心要素词表映射、位置编码、自注意力、前馈网络、语言建模头。训练完成后它可以根据一段开场白生成上下文连贯的文本完全够用来验证原理。特别说明一下边界用PyTorch不等于不from scratch。PyTorch解决的是自动微分和GPU调度这是框架的本职工作。真正需要自己动手的是注意力计算、层归一化、位置编码、训练循环这些模型层面的逻辑。我给自己定的标准是不用from_pretrained加载任何现成权重不直接使用torch.nn.Transformer这个封装好的模块其余好用的轮子可以合理借用。这个边界很重要否则容易陷入为了从零而从零的低效连数据加载器都要自己写纯属浪费精力。1.2 收益与代价的真实权衡为什么明知道有现成API和开源模型还要自己从零搭一个我的答案很朴素为了把概念变成直觉。举一个亲身经历。做这个项目之前我一直不太理解推理时的temperature参数网上的解释翻来覆去就一句控制随机性。为什么温度高就随机为什么大家推荐0.8而不是0.5这些问题在文档里找不到答案。直到自己实现了softmax和采样代码才彻底看清temperature的本质就是在softmax之前把logits整体除以一个系数。系数大于1logits之间的差距被压缩概率分布变平低概率token被抽中的机会变大系数小于1差距被放大模型更倾向于选择最高概率token。就这么一个除法构成了发散与保守的全部秘密。这样的顿悟我在做这个项目的三个月里经历了不下十次。每次都是同一个模式——文档里一句带过的概念亲手实现一遍之后突然就通了。这是使用现成API永远无法获得的体验因为API背后全是黑盒。当然代价也很实在。不算设备损耗纯研发时间大概三个月。这三个月中大量时间不是在写代码而是在排查为什么loss降不下去为什么生成内容全是重复。如果直接用现成API这些问题根本不会出现因为你根本看不到模型内部发生了什么。但也正因为如此一旦API行为异常你只能干瞪眼。从零构建本质上是把黑盒变成透明盒付出的时间换回来的是调试能力这个交换我觉得非常值。2. 动手前的全局设计先看全景再动手2.1 从数据到推理的完整技术链路我在写第一行代码之前花了两天时间画全局结构把整个项目拆成五个环节然后才动手。这是整个项目里我认为最重要的一步。第一环数据准备。语言模型本质上是在做看完前文预测下一个token的任务。你喂给它的文本质量直接决定学习效果的天花板。我在项目里用了公开的英文语料经过清洗去重后大约有5000万token。对这个规模的模型来说这个数据量勉强够用想再提升效果就得加数据。第二环分词器训练。文本不能直接进入模型得先切分成token。我训练了一个词表大小32000的BPE分词器。分词器的质量会直接影响模型训练效率和最终生成质量但很多人容易忽略这一步直接沿用开源分词器后患不小。第三环模型架构。这是核心包括嵌入层、多头自注意力、前馈网络、层归一化和残差连接。能真正理解Transformer的原理这一环就算通了。第四环训练循环。选择优化器、设计学习率调度、计算损失、梯度裁剪、保存检查点。训练稳定性是这个环节最大的主题。第五环推理评估。模型训练完后怎么让它生成文本怎么评估效果。采样策略的不同会带来完全不同的生成风格这一步直接决定最终能不能用。这五环是一个完整闭环。我一开始最大的失误是急着写模型代码直到开训前几天才发现数据环节还没处理好分词器也没训练最后花了两周紧急补课。强烈建议你先沿着这五环把方案完整写下来再动键盘。磨刀不误砍柴工在这个项目里体现得淋漓尽致。2.2 技术选型与避坑建议我的选型如下可以参考深度学习框架PyTorch 2.1。生态最成熟资料最多遇到问题几乎都能搜到答案。分词器HuggingFace tokenizers库。提供高效的BPE、WordPiece训练接口比自己实现快得多也能方便地转成PyTorch模型可用的格式。实验日志TensorBoard。本地就能跑不依赖账号服务适合个人项目。硬件单张24GB显存的消费级显卡。模型配置参考GPT-2 small的尺寸做了缩减——6层Transformer层、8个注意力头、隐藏维度512、词表32000。参数量约8000万。选型有一个核心原则周边工具能用成熟库就用核心逻辑必须自己写。分词器、数据加载器这些环节用现成的没问题但自注意力、位置编码、前馈层、训练循环这些模型的灵魂部分一定要亲手实现。如果所有东西都用封装好的模块做完整个项目你只会得到一个能跑的错觉换一个场景立刻露怯。3. 核心细节解析与实操要点3.1 数据准备一切模型的起点数据的重要性我再怎么强调都不过分。我第一次训练时贪图方便直接拿未清洗的网页文本当语料结果模型生成效果非常差句子语法勉强通顺语义却乱七八糟。后来花几个晚上认真清理数据同样的参数生成质量立刻上了一个大台阶。清洗数据时我做了几件事删除重复文本块。大规模语料里重复内容极多会让模型花费大量算力去记忆高频重复片段生成时也容易复读。去除乱码和残余HTML标签。网页文本里经常夹杂这类噪声严重影响学习效率。过滤过短的碎片。长度太短的文本缺少上下文对训练的贡献非常有限。统一换行和空格。把各种空白符规范成标准形式减少无意义扰动。数据量方面8000万参数模型配5000万token语料是我实际采用的比例。经验法则是token数量最好远大于参数量否则很容易过拟合。如果纯粹练手500万token也能学会基本语法但要到能生成连贯语义的程度数据量还是越多越好。还有一个特别容易踩的坑训练集和验证集的切分。我第一次直接用了随机切分导致同一篇文档的内容同时出现在训练集和验证集里验证loss虚低模型看起来泛化很好实际换个新文本就露馅。正确做法是按文档整体切分保证验证集里的内容模型在训练时完全没见过。3.2 分词器文本到token的转换分词器解决的是一个很基础的问题模型只能处理离散的token ID文本必须先被切分成token。你可以把分词器理解成输入法里的词库——词库好不好直接决定打字快不快、准不准。分词器的词表大小和训练语料决定了模型能认识哪些语言单位。BPE算法训练过程可以通俗理解成一段合并游戏先把所有文本拆到最细的字符GPT系列用字节统计相邻字符对出现的频率不断合并频率最高的对直到词表达到预设大小。在这个过程中常见词会逐渐合并成完整token生僻字词则保留在字节级别这样能保证不会出现遇到没见过的字就崩溃的问题。我用tokenizers库训练BPE目标词表32000。训练完成后有一个检查动作很关键分别拿中文和英文文本做tokenize查看平均被切分的token数量。如果中英混合语料比例失衡分词器会对某一种语言产生明显偏置常见语言被切得很碎模型学起来事倍功半。我在做中英混合实验时就踩过这个坑后来调整了语料比例才解决。3.3 模型架构Transformer最小实现Transformer的核心是自注意力机制。这里用个生活类比帮你理解自注意力就像会议室里的讨论每个参会者token发言前都要快速听一遍其他人的发言判断谁跟自己当前的话题最相关然后从这些人身上获取关键信息来组织自己的发言。这样一个token就能看到整个序列的上下文而不是只局限于自己。具体到计算层面每个token会生成三个向量Query我想获取什么Key我能提供什么Value我实际的内容。每个token拿自己的Query去和序列中所有Key做点积匹配得到注意力分数再经过softmax转成权重用这个权重对所有Value做加权求和得到最终的上下文表示。从零实现时有几个工程细节千万注意。第一点积注意力必须要除以sqrt(d_k)也就是向量维度的平方根。这个缩放的作用是稳定方差否则点积结果太大过softmax之后梯度极其容易消失。第二多头注意力要把计算拆成多个头每个头在不同表示子空间学习不同关联模式最后拼接。第三位置编码。自注意力本身没有顺序概念必须把位置信息注入进去。我选用了可学习位置嵌入简单有效。模型整体结构是token嵌入加位置嵌入再接6层Transformer块最后接一个输出投影层把隐状态映射回词表大小。每个Transformer块内部采用GPT-2的pre-norm结构先层归一化再做多头注意力然后残差连接再层归一化前馈网络残差连接。pre-norm对训练稳定性帮助很大我在项目里对比过换成post-norm在3e-4学习率下很容易发散pre-norm就稳得多。3.4 训练循环稳定压倒一切训练循环里最有玄机的部分不是前向和反向传播而是优化器与学习率策略的设计以及两者之间的配合。优化器我推荐AdamW。相比AdamAdamW把权重衰减从梯度更新过程中分离出来解决了L2正则与Adam更新规则之间的耦合问题实践下来收敛更稳、泛化更好。权重衰减系数我设为0.1这在预训练场景里是比较常见的设置。学习率策略是重中之重。总训练步数10万采用warmup加余弦退火的组合。warmup阶段让学习率从极低数值线性上升到峰值给模型一个缓冲期否则参数在训练初期剧烈震荡后期很难收敛。峰值学习率设为3e-4warmup步数2000之后按余弦曲线衰减到接近0。训练后期学习率变小参数更新幅度收窄模型能做更精细的调整。实际跑起来的loss走势大致是初始loss约10.8前2000步warmup阶段快速下降到7左右5000步左右降到3.510万步结束时稳定在2.7到2.9之间。这个数值可以换算成困惑度perplexity大约exp(2.8)约等于16.4。意思是在词表32000的模型里模型平均每次预测时把不确定性缩小到了16个token以内。对于这个体量的小模型属于正常水平。梯度裁剪也建议加上把全局梯度范数限制在1.0防止训练后期偶发的梯度爆炸把loss直接打飞。裁剪本身不复杂但能显著提升训练稳定性。3.5 推理与采样策略训练完成的模型推理时是一个token一个token地往外生成。输入今天天气模型预测下一个token的概率分布按策略采样出一个token拼到输入末尾再预测下一个循环往复直到遇到结束符或达到长度上限。不同采样策略的效果差异非常大贪婪解码每次选概率最高的token结果确定性强但容易重复、死板。温度采样把logits除以temperature再做softmax然后从分布中采样。温度越高分布越平、越随机越低越保守。top-k采样从概率最高的k个token中采样防止极低概率的token捣乱。top-p采样核采样从累计概率超过p的最小token集合中采样比top-k更灵活。我最终采用了温度0.8加top-p 0.9的组合实测下来兼顾流畅性和多样性。如果生成内容太重复把温度调到0.9如果语义飘了就把温度降到0.6。这些参数没有绝对最优需要根据具体场景反复试。一个小技巧是准备一组固定prompt每次调整完采样参数后跑一遍同样输入对比效果。4. 实操过程从零跑通一个小型语言模型4.1 环境准备与硬件配置我的实际环境是这样的操作系统Ubuntu 22.04GPU单张24GB显存显卡消费级即可深度学习框架PyTorch 2.1CUDA 11.8Python版本3.10如果你的显卡显存不到16GB完全不用慌。可以把模型配置缩到4层、隐藏维度256或者把序列长度降到128照样能跑通全流程。我第一次跑起来用的就是一块8GB显存的旧卡把batch size降到8、序列长度降到128一样完整跑完了训练和生成。这个项目强调的是理解原理不是比拼规模用小配置跑通流程的价值远大于用大配置死磕显存。4.2 分步实现代码骨架参考我按模块拆分代码每个文件职责单一方便定位问题。核心文件四个train_tokenizer.py训练BPE分词器导出tokenizer.jsondataset.py读取语料切分训练集和验证集使用分词器tokenize后按固定长度切成序列model.py定义模型结构train.py训练循环和checkpoint保存model.py的核心结构可以参考这段简化代码import torch import torch.nn as nn class SelfAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() assert d_model % n_heads 0 self.n_heads n_heads self.head_dim d_model // n_heads self.wq nn.Linear(d_model, d_model) self.wk nn.Linear(d_model, d_model) self.wv nn.Linear(d_model, d_model) self.wo nn.Linear(d_model, d_model) def forward(self, x): B, T, C x.shape q self.wq(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) k self.wk(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) v self.wv(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) attn (q k.transpose(-2, -1)) / (self.head_dim ** 0.5) mask torch.triu(torch.ones(T, T, devicex.device), diagonal1).bool() attn attn.masked_fill(mask, float(-inf)) attn torch.softmax(attn, dim-1) out attn v out out.transpose(1, 2).contiguous().view(B, T, C) return self.wo(out)这里有一个我特别想强调的细节因果掩码causal mask。语言模型生成时只能看当前token之前的token绝不能看到未来的token否则就是作弊。上三角掩码的作用就是实现这个限制把未来位置全部置为负无穷softmax之后这些位置的注意力权重自然变成0。我第一次写的时候忘了加掩码训练loss初始值异常低根本降不下去查了好几天才定位到问题。训练循环的大致结构for step in range(total_steps): x, y next_batch() logits model(x) loss F.cross_entropy(logits.view(-1, vocab_size), y.view(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() if step % 100 0: log_loss(step, loss.item())不要小看这个看似简单的循环。大模型训练失败的大部分原因就藏在数据没排好、学习率没调好、梯度爆炸没防住这些一眼看不出来的细节里。每100步打印一次loss能帮你尽早发现问题。我习惯在loss出现异常时立刻停住排查而不是先让它继续跑着看。4.3 训练监控与效果评估训练过程中我主要关注三个指标训练loss、验证loss、固定prompt的生成示例。每5000步保存一次checkpoint同时用一批固定的prompt做生成测试。注意固定的prompt很重要不同阶段跑同样的输入对比才有意义。从第2000步开始生成的句子还基本是单词的随机组合到第10000步语法结构开始成型到第6万步以上单段文本已经相当连贯只是长文偶尔会跑题。验证loss的评估用困惑度。我最后的验证困惑度约16和训练loss基本匹配没有严重过拟合。如果发现训练loss持续下降、验证loss却不降反升要高度警惕过拟合先检查数据集是否被污染再考虑增加数据量或加大dropout。数据加载还有个细节要用随机打乱的batch顺序并在每个epoch结束后重新打乱。虽然语言模型任务里顺序不那么敏感但固定顺序会导致模型学到第N步的样本是某一类文本这种虚假规律影响泛化。5. 常见问题与排查技巧实录5.1 训练不收敛或loss震荡这是新手最常遇到的问题表现是loss不降或者降了一点又开始反复横跳。排查顺序很重要我按优先级排列先看loss初始值是否正常。词表32000的随机初始化模型交叉熵初始应当在log(32000)约10.37附近。如果初始值远低于这个数说明模型在抄近路最常见的原因就是因果掩码没加对模型看到未来的token了。这个bug最隐蔽因为loss看起来很好但模型学到的全是假规律。如果初始正常但loss就是不降先尝试把学习率调小一个数量级看有没有反应。如果loss震荡得像心电图多半是学习率太大、warmup不足或者batch size太小。加大batch size能有效平滑梯度更新方向。5.2 显存不足显存溢出的解法从轻到重排列减小batch size配合梯度累积。比如batch size设为8、梯度累积4步等效于batch size 32的效果显存占用却只是原来的四分之一。使用混合精度训练显存占用可以接近减半。但新手容易踩坑loss缩放、梯度裁剪的顺序不能乱建议第一个项目先跑通全流程再加混合精度优化。降低序列长度。序列长度对显存的影响是平方级的因为注意力矩阵是T乘T的规模。缩减模型维度。隐藏维度从512降到384效果立竿见影。显存不足时不要硬扛优先用梯度累积它对训练质量的影响最小。5.3 生成内容全是重复训练正常的模型也可能输出重复文本特别是在贪婪解码模式下。先别急着怪模型检查采样参数。如果已经用了温度采样还是重复可以把温度调高到0.9甚至1.0也可以叠加重复惩罚项在采样时对那些出现过一次的token的得分做一点惩罚。如果无论怎么调采样参数都重复问题就要回到训练数据上。语料里重复短语过多会形成一个陷阱模型发现输出高频重复内容是最安全的路径生成时自然会倾向于复读。这时要清洗数据、增加语料多样性。5.4 中文效果特别差如果你用中文语料需要注意BPE分词器在中英混合训练时的失衡问题。中文的token切分粒度和英文差异很大如果中文比例过高分词器会把英文切得特别碎或者反过来。建议中英语料分开训练分词器或者严格控比例然后在评估阶段分别用中英文测试token数量是否均衡。我把遇到的主要问题整理成了一张速查表症状优先排查项常用解法loss初始值异常低因果掩码、数据泄露检查mask、检查数据切分loss不降学习率、warmup调小学习率、增加warmup步数loss震荡剧烈学习率、batch size调小学习率、增大batch或梯度累积显存溢出batch size、序列长度梯度累积、混合精度、缩模型生成内容重复采样参数、数据质量提高温度、重复惩罚中文效果差分词器词表分配中英分开或平衡语料比例6. 从基础模型到推理模型6.1 推理能力从何而来最近从零构建推理模型成了社区的热门话题。所谓推理模型指的是在给出最终答案之前会先生成一段思考过程的模型类似现在大家熟悉的那些擅长数学、代码、逻辑题的模型。推理能力不是凭空跳出来的。它的底层基础恰恰是语言模型本身已经学会了逻辑表达的基本规律。构建推理模型通常分三步走先有一个基础LLM具备流畅的语言生成能力。收集或合成包含思维链的问答数据在基础模型上做监督微调让模型学会先思考、再回答的输出模式。再用强化学习比如GRPO这类算法继续优化推理路径用规则奖励比如最终答案是否正确来引导模型产生有效的中间推理步骤。从我实际操作的体会来说前面每一步都是后一步的前提。如果基础模型还在语法勉强正确、语义经常跑偏的阶段直接上推理训练效果会非常差。推理能力是锦上添花不是雪中送炭。先让模型把话说流畅再教它把话说严谨顺序不能反。6.2 下一步从语言模型到推理模型的尝试完成基础项目后我的下一步计划是做一个小规模的推理模型实验。具体思路是收集一批带思维链的数学题数据在已有的小模型上做指令微调然后实现一个简化版的强化学习训练流程用最终答案是否正确作为奖励信号让模型自己逐步学会生成更有用的思考过程。这个实验还在推进中目前遇到的典型问题是小模型的推理能力上限确实有限需要反复调试奖励函数和数据格式才能看到进展。等这轮做完我会专门再写一篇记录那份经验。做这个实验的初衷很简单——构建推理模型from scratch这条路我也想完整走一遍而不是停留在调用别人开放的推理接口。7. 几个让我印象最深的教训从零构建AI工程真正的难点从来不是某个算法的数学推导而是把所有环节串起来、让系统稳定运行的能力。数据、分词、模型、训练、推理每一环单独拿出来都有大量现成教程但把它们组装成一个能跑通的项目靠的是工程判断力和踩坑经验。我印象最深的几个教训值得单独列出不要在数据没清洗干净的时候就急着调模型。数据噪声会掩盖模型本身的问题让你误判模型不行。先跑通最小闭环再扩展规模。我第一次跑通时模型只有3000万参数、数据只有2000万token但全流程跑完只用了两天。后面加参数、加数据都是在验证过的框架上替换数字。每个环节都要有日志。loss、验证集困惑度、采样结果、显存占用、每个epoch的训练时间全部记录下来。后面排查问题时这些数据能帮你快速缩小范围。最后分享一个我反复用到的小技巧动手前先给自己设定一个最小完成标准。我的标准是——哪天能够向一个完全不懂AI的人解释清楚输入一句话之后我的模型内部发生了哪些计算最终输出了那句话。有了这个标准整个项目就不会迷失在想做得更大想用更新的架构这些诱惑里。做完一个最小闭环永远比做一半的宏大设想更有价值。做完之后再回头扩展每一步都有底气。
返回列表