ARTICLE DETAIL

资讯详情

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

从零构建大语言模型:数据、训练到推理能力全解析

从零构建大语言模型:数据、训练到推理能力全解析 这篇博客要写的内容我心里有数先跟你聊两句背景再切入正题。这两年 AI 圈的画风变化特别快。前几年大家还在比谁家的 API 调用封装得漂亮、谁的 prompt 写得更花哨到了最近风向突然转向了“从零开始”。无论是《Build a Large Language Model from Scratch》这类书籍被疯狂传阅还是社区里越来越多“build a reasoning model from scratch”的实战分享都在传递同一个信号光会用模型已经不够了越来越多工程师想自己把模型造出来。这篇文章就是围绕“ai-engineering-from-scratch”这个主题把我从零搭建、训练、微调一个可用的 AI 模型过程中的思考、步骤、踩坑记录全部摊开讲清楚。内容会覆盖数据准备、tokenizer 实现、模型结构、训练循环以及最近特别火的推理能力构建CoT 强化学习路线适合已经用过 AI API 但想深入底层、有 Python 基础想动手实践的读者也适合准备系统学习大模型原理的入门者。1. 内容整体设计与思路拆解1.1 为什么“从零”成了新趋势先明确一个概念AI engineering from scratch不等于从零发明神经网络。你不需要重新推导数学公式也不需要复现 GPT-4 那么庞大的架构。这里说的是“从零构建一条自己的 AI 系统链路”——从哪里弄数据、怎么清洗、怎么切词、怎么设计模型结构、怎么让它学会推理整个过程自己掌控而不是打开一个 API 接口传参就完事。我看到很多人在讨论中把“from scratch”误解成“必须完全自己手写所有代码”。实际推敲下来真正合理的理解是在关键链路上不做黑盒依赖。比如你可以用 PyTorch 做张量运算但模型结构要自己搭你可以用 HuggingFace 的数据集加载工具但数据筛选策略得自己定。说白了框架可以用现成的认知链路必须是自己的。这也是为什么我特别推荐拿 Sebastian Raschka 那本Build a Large Language Model from Scratch当路线图参考。它没有停在概念讲解层面而是真的从一段文本开始一步步构建 tokenizer、实现 GPT 架构、训练出能生成文本的模型。很多人误解这本书是“玩具项目”实际动手跑完才会明白它把从零到一的工程链路讲透了数据处理、模型初始化、训练循环、评估、微调每一步都在教你“怎么选”而不是只告诉你“怎么做”。1.2 从零构建能解决什么实际问题我自己过去有段时间陷入一个怪圈调用大模型 API 做应用调不好就怪 prompt 写得不够好再调不好就怪模型版本太老。表面看是工程问题本质是对模型能力边界没有感知。当你亲手训练过一个模型你就会知道 loss 下降的曲线长什么样、数据质量对结果的影响有多大、同一个学习率在不同 batch size 下表现差异有多大。这些感知用熟了之后你再回去做应用层开发判断力完全不一样。从零构建还有实际的经济价值。做实验的时候你在自己的机器上跑一个小模型几十块钱的电费就能跑完几轮实验而如果用 API 反复迭代光试错成本就可能到几百上千块。训练规模再大一点自己做数据处理和分布式训练省下来的费用就更可观。对我这种经常要拿数据测试思路的人这条路径不仅是“学习成本”也是“研发成本”的控制手段。更适合初学者的一点是从零构建能让你真正看懂“推理模型”的组成。最近社区里“build a reasoning model from scratch”之所以火是因为 DeepSeek-R1 这类模型展示了一个事实推理能力不是被神秘地“涌现”出来的而是一条可复制的工程路径——高质量思维链数据做 SFT 冷启动再用规则奖励驱动的强化学习比如 GRPO让模型在探索中自发提升推理长度和准确率。这条路径你在小模型上完全可以复现。1.3 适合谁学、需要什么前置知识我的判断是不需要数学博士背景但需要动手的意愿。上手前最好具备Python 基础到中等水平能读懂类、函数、循环会调试报错。PyTorch 基础知道张量是什么能跑最简单的训练脚本。不会也别慌照着官方教程过一遍 basic 章节就够了。一点线性代数和概率的直觉知道矩阵乘法是啥明白交叉熵是衡量什么的就行。复杂的推导可以边做边补不用一开始全学会。耐心这是最重要的前置条件。第一次训练模型loss 不降、显存爆炸、生成了乱码太正常了没有耐心的人第一周就会放弃。我自己就是从“只会调库、不懂原理”的状态走过来的。下面我把整条实践路线拆开包括我踩过的坑、反复试出来的参数、以及为什么选那些方案希望你能少走弯路。2. 核心细节解析与实操要点2.1 数据预处理Tokenization 与 BPE 的取舍很多人从零做模型第一反应是去搞模型架构实际上第一个拦路虎是数据怎么变成数字。文本不能直接喂进神经网络得先拆成最小单位token再映射成整数索引。这里我首选 BPEByte Pair Encoding字节对编码原因很简单它既能高效压缩常见词又能处理生僻词和未知词。BPE 的核心逻辑听起来挺直白先把文本拆成字节然后反复统计相邻字符对的出现频率把最高频的字符对合并成一个新 token直到达到预设的词汇表大小。实际操作里有两个细节特别容易踩坑训练 tokenizer 的语料要与最终训练语料同源。我先用小规模中文语料训了一个 mini tokenizer然后拿去做英文语料训练结果生成的文本全是乱码。原因是同一套 BPE 规则在不同语言上的分词结果完全不同。后来我统一用混合中英文语料来做问题就消失了。词汇表大小要权衡。词汇表太小比如 512每个 token 表达的信息太碎序列会特别长、训练变慢词汇表太大比如 64K模型要学的 embedding 参数大量增加。我做实验时常用 4K~8K学习效果和训练速度比较平衡。一个实用的数据清洗流程可以这么设计def clean_text(text: str) - str: # 去除 HTML 标签和不可见字符 text re.sub(r[^], , text) text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f], , text) # 统一换行符 text text.replace(\r\n, \n).replace(\r, \n) # 压缩连续空白 text re.sub(r[ \t], , text) # 过滤过长段落通常是抓取的异常页面 if len(text.split( )) 2000: return return text.strip()这套流程看着简单实际效果立竿见影。我一开始偷懒不清洗直接拿爬来的语料训练结果 loss 下降极慢生成内容里夹杂大量乱码符号后来才发现是清洗不够导致的。2.2 模型骨架从 Embedding 到 Transformer Block网上关于 Transformer 原理的文章多到看不完但你自己动手实现时真正需要写出来的核心模块就那几个。我建议用“组装乐高”的心态来理解不要被注意力机制的公式吓到。Embedding 层负责把 token 索引变成向量。这里有个小细节位置编码必须加否则模型分不清“我打你”和“你打我”的区别。我现在常用 RoPE旋转位置编码它在长文本上的外推能力好于最早的绝对位置编码而且实现起来就是一个旋转矩阵操作不算复杂。多头注意力机制是整个模型的灵魂。我自己的理解方式是把它想象成一个“自助图书馆检索系统”每个 token 先拿自己的查询向量Query去跟所有 token 的键向量Key比对算出相关性分数再按分数加权取出对应的值向量Value。在这个机制里你要特别注意因果掩码causal mask——生成任务中当前位置不能看到未来内容所以注意力矩阵的上三角部分必须置成负无穷否则模型会作弊。一个极简的注意力实现大概长这样def causal_attention(q, k, v, maskNone): # q, k, v: [batch, seq_len, heads, head_dim] scores torch.einsum(bhid,bhjd-bhij, q, k) scores scores / math.sqrt(q.shape[-1]) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights torch.softmax(scores, dim-1) return torch.einsum(bhij,bhjd-bhid, weights, v)看到这个代码我要提醒一点千万不要直接用这个版本去训练稍大一点的模型因为复杂度是序列长度的平方。拿它理解原理没问题工程上要用的还是 Flash Attention 这类高效实现。我在从零写模型的时候先用自己的 naive 实现跑通逻辑然后再切到官方优化版这样才能既理解原理又不浪费算力。前馈网络MLP的细节更简单每个 token 的向量经过两三次线性变换加激活函数就行但它的参数量占了整个模型的近三分之二。实际挑参时我发现把隐藏层维度设为 embedding 维度的 4 倍是经济的选择参数量合理效果也不会明显退化。最后把 embedding、注意力、前馈网络组合成一个 Transformer Block再把多个 Block 堆叠起来配上最后的 layer norm 和输出投影层模型主体就完成了。LayerNorm 的位置值得注意现在主流做法是放在注意力计算之前Pre-Norm比放在后面Post-Norm更容易训练稳定。我自己在同样参数下对比过Pre-Norm 的首轮 loss 下降确实更平滑。2.3 损失函数与训练循环模型到底在“学”什么有了模型结构接下来要回答的问题是“模型怎么知道自己说得对不对”。答案就是交叉熵损失给定前面的文本模型预测下一个 token 的概率分布与真实下一个 token 做比较越接近则损失越小。写训练循环的时候有三个细节不能省打乱与批次策略。我一开始把整个语料拼成一个超长序列然后连续切块进模型。这样做会在批次内保留顺序对各次训练不太差但最终效果不如随机采样。随机采样的意思是每个 batch 从语料的不同位置抽取这样模型每次看到的上下文更多样泛化效果更好。缺点是会打断文档的连续性需要张量拼接。梯度累积。显存有限的小机器上这是一个救命技巧。假设你需要有效 batch size 为 16但单次只能放 4 条序列那就跑 4 次前向和反向把梯度累积起来再做一次优化器更新。注意 PyTorch 里要手动把loss除以累积步数否则梯度会按 4 倍算大学习率直接失效。学习率预热warmup。训练刚开始时模型参数是随机状态直接大步更新很容易把参数冲到不稳定的区域。业界惯用的做法是先用 500~2000 步把学习率从接近 0 线性升到目标值之后再根据轮次衰减。我试过从第一步就用固定学习率效果明显更差——特别是用 AdamW 优化器时没热身状态下的方差动辄偏大。这里附一个极简的训练循环骨架方便你看到整体流程optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay0.1) scheduler CosineWarmupScheduler(optimizer, warmup_steps1000, total_steps20000) for step, batch in enumerate(train_loader): logits model(batch[input_ids]) loss cross_entropy(logits, batch[target_ids]) # 梯度累积 loss loss / grad_accum_steps loss.backward() if (step 1) % grad_accum_steps 0: torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad()2.4 训练策略选型预训练 → SFT → RL模型结构是骨架数据训练是血肉。从零开始构建一个有用模型的完整路线我总结为三步第一步预训练Pre-training。目标是让模型学会语言本身。用大规模无标注语料做标准的下一个 token 预测。这一步的产物是一个“会说但不会干活”的模型——它能接出流利的句子但不一定听话也不一定正确。第二步监督微调SFT。准备一批“问题-优质回答”对让模型学会按指令回答。重点在于数据质量远大于数量。几百条高质量问答有时候效果能明显好过几万条噪声数据。这里的窍门是把指令、输入、回答用专门的特殊 token比如|im_start|和|im_end|分隔让模型学到格式边界。第三步对齐与推理增强RL / RLHF 类方法。这一层做的是“告诉模型什么答案好”。对推理类任务来说我特别推荐用规则奖励而不是人工偏好模型。比如做数学题直接核对最终答案是否正确做事实验证直接调用知识库比对。奖励明确信噪比高工程上实现也简单。用 GRPO 这类策略优化算法让模型在探索多个答案时从奖励差异中学习这就是近期“build a reasoning model from scratch”的核心思路。这条路线是一个连续递进的过程不是分开的孤立任务。我之前犯过的错误是跳过 SFT 直接做强化学习结果 RL 阶段模型生成的答案乱写奖励信号完全没法引导白白浪费了好几天计算资源和调整时间。3. 实操过程与核心环节实现3.1 环境准备与数据准备我所有的实验都是在单张 24GB 显存的消费级显卡上完成的RTX 3090 跑起来完全够。如果你显存只有 12GB也不是没法做把模型缩到 1 亿参数以下、序列长度缩短到 512 以内就行。所以给你一份环境清单torch2.0 transformers4.40 datasets2.19 numpy tiktoken # 或者自己实现的 BPE数据方面给个示例流程。我用的是开源中文语料大概 2GB 纯文本。首先按段落切分过滤掉过短和过长的片段然后做语言过滤去除非中英文字符占比过高的行最后把数据切成固定长度块比如 1024 token存成二进制文件。一个重要的经验是把数据缓存在磁盘训练时按索引读取不要每次重启都重新清洗一遍。我一开始没缓存每次调试都要等 20 分钟数据预处理烦得想砸键盘。切块代码很简单# 将 token ids 拼接并切成固定长度 def make_chunks(token_ids, block_size1024): chunks [] for i in range(0, len(token_ids) - block_size, block_size): chunks.append(token_ids[i : i block_size]) return chunks注意这里有个隐藏问题切块从 0 开始的偏移量是固定的如果语料长度不是 block_size 的整数倍就会丢弃尾部数据。简单做法是头部留出 overlap或者直接用 sliding window 做重叠采样。我后来直接保留 128 token 的重叠量有效地利用了数据。3.2 手写 tokenizer、模型与训练代码我不想给你贴一个超大代码块然后就走人这里把最核心的三段代码块拆开讲tokenizer 训练、Transformer 模型主类、训练入口。Tokenzier 训练如果不想手写 BPE 那套合并逻辑可以直接用tiktoken训练但“从零”路线我还是建议自己写一遍 BPE。核心就三步初始化字节词汇表统计相邻对频率迭代合并最高频对直到词汇数量达标。写完之后在语料上跑一遍分词打印几个样例检查切分是否合理。我发现中文语料中BPE 会倾向于合并常见词组比如“人工智能”会成为一个 token这个行为正常不用特意去分词成单字。模型实现GPT 风格的模型结构并不复杂一个高水平的精简实现大概是nn.Embedding做 token embedding一个可学习的或者位置编码模块然后堆叠 N 个 TransformerBlock每个 Block 里包含因果多头注意力和 MLP注意残差连接和 layer norm 的顺序。有一个容易出错的地方是初始化残差层最后的投影层通常初始化为零或者很小权重这能保证初始时整个 Block 近似恒等映射深层堆叠时才不会出现数值爆炸。我前期没注意初始化用默认分布去初始化深层模型跑了几个 step 之后 loss 直接变成 NaN。训练入口使用torch.utils.data.DataLoader从预处理好的二进制文件取 batch每个 iteration 里取两个连续的 block第二个 block 作为第一个的标签也就是把数据右移一位。我通常把 batch_size 设为 8、序列长度 1024正好吃满 24GB 显存。如果内存不够batch_size 减半然后用梯度累积找平即可。最后贴一下训练时的观察项。我不光盯 step loss还会持续记录训练集 loss 与验证集 loss 的差值判断过拟合生成的样例文本每 500 步生成一小段直观感受模型状态学习率当前值确认调度器在正常变化梯度范数如果超过 5说明优化器步长太大或数据有异常3.3 构建推理能力从 CoT 数据到强化学习这是 2025 年之后整个 AI 工程圈最热门的话题也是我花时间最多的地方。从零构建推理能力我认为路径分三段走第一段收集思维链Chain-of-Thought数据。单纯让模型直接给答案它不会“思考”要让模型学会思考你得先给它看“思考过程”。数据格式大概是问题然后是一长段中间推理过程可以自言自语式最后是答案。公开的数学推理数据比如 GSM8K 训练集可以直接用但如果你有自己的垂直领域任务就需要自己构造数据。我的经验是先做 1000~5000 条高质量 CoT 数据做 SFT 冷启动比上来就跑 RL 更稳。冷启动模型至少要能把思考格式写出来否则 RL 阶段根本探索不到高奖励的回答。第二段用规则奖励做强化的启动。对推理任务奖励信号不需要“人类打分”直接用规则判断答案正确得 1 分错误得 0 分如果格式不合法比如没有输出答案:标签甚至可以直接扣分。我还加了一个长度奖励项的变体鼓励模型写出更长的推理步骤但注意权重不要太大不然模型会学成“废话制造机”绕了一大圈最后答案还是错的。第三段用 GRPO 替代 PPO 做迭代更新。我强烈建议直接尝试 GRPOGroup Relative Policy Optimization它是 PPO 的一个简化变体取消了对价值网络的依赖只用同一个 prompt 采样出一组答案通过组内相对奖励计算优势。实现起来少了一个庞大的 critic 模型显存占用和实现复杂度都大幅下降。这也是 DeepSeek-R1 等模型公开的技术路线中用得比较顺的策略。下面这个伪代码展示 GRPO 部分逻辑的尺度帮助你建立直观印象# 对同一个 prompt 采样 group_size 个回答 responses policy.sample(prompt, ngroup_size) rewards [rule_reward(r) for r in responses] baseline mean(rewards) advantages [(r - baseline) for r in rewards] # 再用这些 advantage 做 PPO 风格的策略更新截断3.4 评估与验证训练完怎么知道“行不行”训练完模型最怕的是自我感觉良好但拿不出评估数据。我建了一套三层次评估法自动评估跑一个固定题集比如 200 道数学题统计准确率。训练前测一次训练后测一次看提升幅度。对抗评估故意把问题改写成带噪声的版本错别字、语序打乱、冗余信息看模型推理是否稳健。人工盲测把模型输出和参考答案混在一起让评测者打分。这个对于主观性强的任务特别重要。我观察到的一个真实规律是训练集准确率接近 100%但测试集却只到 70% 左右几乎一定是过拟合了。解决办法不是盲目加数据而是先看验证集上错误的类型。比如如果你的模型只在“计算最后一步”上出错那就去增强最后一步算术的专项数据如果模型容易漏掉问题条件那就去改 prompt 格式让条件更显眼。4. 常见问题与排查技巧实录4.1 训练不收敛或者 loss 是 NaN这是新手最常碰到的“劝退问题”。我总结下来主要有三个原因学习率过高、数据里有异常值、模型初始化不当。学习率过高很好判断loss 前期震荡特别剧烈然后一飞冲天变成 NaN。应对方法首先把目标学习率降到一半再观察几轮。如果仍然不行把 warmup 步数加长例如从 1000 步加到 3000 步让参数逐渐被“带入正轨”。数据异常会直接表现为某个 batch 的 loss 突然从正常值跳到 NaN。排查办法是用一个很小的 batch比如 2 条样本逐条前向计算 loss找出那一条导致异常的数据。我遇到过一种情况数据里混入了超长数字tokenizer 输出的 token 数量超过了模型的最大长度结果位置编码越界。这类问题在数据清洗阶段就应该把它过滤掉。初始化不当的问题在 2.3 节提过核心是残差层的输出全部初始化为接近 0 的权重或者使用小的标准差这样深层网络的初始状态是稳定的训练从一开步就在一个合理的地基上。4.2 显存不够怎么办不要一上来就换大机器。我给你一套由快到慢的排查顺序降低 batch size保持有效 batch 用梯度累积补回来。显存主要被 batch 里的激活值占掉这个方法效果最明显。减少序列长度。从 1024 降到 512显存几乎立刻省一半。对很多任务来说512 长度的上下文已经够用。打开混合精度训练AMP。PyTorch 里一行torch.cuda.amp就能做到前向半精度反向全精度显存直接减少不少速度还能提升。使用梯度检查点。前向时丢弃中间激活反向时再重新计算。注意这会降低约 30% 的运算速度但在杯水车薪的显存压力下是值得的。还有一个容易被忽略的点优化器状态占的显存不比模型小。用 AdamW 的话每个参数要存两份动量模型 10 亿参数optimizer 状态会额外占 8GB。如果显存紧张可以考虑把部分参数冻结比如只训练最后几层或者换用更省内存的优化器如 Adafactor但收敛效果通常不如 AdamW你自己权衡。4.3 生成内容重复、逻辑断裂这种现象的根源通常是采样策略和训练数据两方面的问题。训练数据里如果有大量重复文本比如新闻网站的泛页模型会把“复读”当作合理生成。清理数据时可以用 n-gram 去重统计 8-gram 级别的重复片段把重复率超标的文档直接扔掉。生成阶段的重复则是采样问题。温度参数调低比如 0.7可以降低随机性但过低会陷入贪心循环top-k 和 top-p 过滤掉概率极低的 token能有效减少从长尾里选到无关词。我的经验值组合是温度 0.8、top-k 40、top-p 0.9先跑一遍看效果再微调。要注意的是这些参数不是越大越好需要结合任务来调。开放创作类内容可以稍高一点数学题则大概率降低温度更合适。逻辑断裂问题则基本指向模型本身的容量不够或者 CoT 数据太少。小模型想推理长链条本身就不现实我一般会把任务拆成多步提示让模型每步只做少量推理然后汇总全部结果这比指望一次性输出长链条可靠得多。4.4 数据配比与过拟合的纠偏训练混合数据时你不能简单把不同来源的数据等比拼接。比如你拿了 1GB 维基百科风格文本再加 1GB 代码那么模型的语言能力可能偏“书面化”代码能力却会因为格式学习不到位而表现较差。一个合理的方法是按 token 数量设定采样权重让每种来源的数据在每轮训练中都能得到大致充分的曝光。我在混入推理数据时通常让数学题数据占总 token 的 10%~20%其余留给通用语料这样推理能力不会以牺牲通用能力为代价。过拟合的另一种纠正方向是正则化与早停。小模型在几万条文本上训练多轮很容易把训练集的边角料都背下来。我在训练第 2 轮后验证集 loss 基本不再下降时就会停手省下来的时间去调数据和策略收益高得多。我还试过对 attention 权重做 dropout从 0.0 调到 0.1验证集 loss 确实平稳了一些代价是训练时间稍微变长这个度你自己把握。5. 一些动手实践中才有的心得体会这节内容不是教科书上的是我踩过几次坑之后总结的按重要程度排给你。第一“从零”最大的收获不是造出了模型而是你对 AI 系统的“失控感”消失了。以前我遇到模型输出不对劲只能靠叠 buff 式的 prompt再不行就怀疑模型“笨”。现在我会下意识推断问题是出在数据分布、训练阶段、还是采样策略——这种判断力只有亲手把每个环节都捏过一遍之后才会长出来。第二能跑通和能好用之间差距全在数据和评测上。很多人止步于“loss 降了、能生成文本了”但一个能用的模型要过得了标准题集、扛得住对抗改写、经得起人工盲测。把这些评测工具早早在工程链路里布好比事后补救省力太多。第三想构建推理能力别直接冲 RL。社区里不少“quick start 教程”会直接甩给你一个 GRPO 的训练脚本我劝你别照着跑完就以为自己会了。先老老实实做几百条 CoT 数据把模型冷启动到能按格式思考再用 RL 增强这条路径的稳定性和可解释性都远高于直接硬碰强化学习。第四硬件不够不是放弃的理由而是限制设计的纪律。我见过有人在单卡上硬跑一个几十亿参数的模型结果只能在日志里看 loss 数字连一次生成样例都等不到。与其这样不如把模型缩小到能实时调试的规模把实验迭代的周期压缩到分钟级。可以这样理解训练一次 3 天的模型你想试一个参数都要等 3 天模型缩小 10 倍迭代周期变成几小时反而让你更可能找到正确的方向整体进展更快。如果想在这个路线图上继续延伸我个人的建议方向包括把 SFT 数据构造流程写成一个自动化 pipeline接入自己的业务数据做垂直领域指令微调或者在评估环节引入更严格的对抗测试集专门攻击模型的推理漏洞再进一步也可以读一读 R1 的技术报告看它是如何在强化学习阶段让模型自发涌现出“重新检查”和“反思”行为的并尝试在更小的模型上复现出部分现象。这条路我还在走希望你看了这篇笔记之后能比我少走几个弯路。
返回列表