资讯动态

从零实现迷你LLM:Transformer架构与训练全流程解析

发布时间:2026/10/27 8:29:12 来源:尧图企业网站定制
1. 从零开始理解 LLM为什么值得深入学习很多人第一次接触 LLM脑子里冒出来的问题往往是“这玩意儿到底怎么工作的”。我刚开始也是这个状态看着各种论文和博客里满屏的 Transformer、Attention、RLHF感觉每个字都认识连在一起就不知道在说什么。后来踩了不少坑也动手写过一些简化版的实现才慢慢把这条链路串起来。这篇内容就是把我自己学习 LLM 的过程整理出来从最核心的架构原理到训练流程里的关键环节再到实际动手时可以复现的代码思路尽量说人话让刚入门的朋友也能跟上。LLM也就是大语言模型本质上是一个用海量文本训练出来的概率模型。你给它一段话它预测下一个词最可能是什么然后把这个词拼回去再预测下一个循环往复就生成了完整的回答。听起来简单但要让这个“预测下一个词”做到像人一样合理背后涉及的东西非常多。Transformer 架构解决了长距离依赖的问题Attention 机制让模型能动态关注输入的不同部分思维链让模型学会分步推理RLHF 则把人类的偏好注入到模型的行为里。这几个关键词基本覆盖了 LLM 从结构到对齐的核心脉络。这篇文章适合谁看如果你已经用过 ChatGPT 之类的产品想搞清楚它内部到底发生了什么或者你写过一些 Python想自己动手实现一个迷你版的 Transformer又或者你在做 AI 应用开发需要理解模型的边界和能力来源那这篇内容应该能给你一些实在的帮助。我会尽量把每个概念拆开配上类比和可运行的代码片段让你不只是“知道”而是能“讲清楚”并且“写出来”。2. Transformer 架构拆解从 Attention 到完整模型2.1 为什么 RNN 不够用Transformer 解决了什么问题在 Transformer 出现之前处理序列数据主要靠 RNN 和 LSTM。RNN 的思路很直观按顺序读入每个词维护一个隐藏状态把之前的信息传递下去。但问题也很明显序列一长前面的信息传到后面就衰减得差不多了梯度消失和梯度爆炸是家常便饭。LSTM 通过门控机制缓解了一部分问题但本质上还是串行计算没法并行训练效率上不去。Transformer 的核心突破在于两点。第一它完全抛弃了循环结构用 Attention 机制直接建模序列中任意两个位置之间的关系不管距离多远都是一步到位。第二因为不依赖串行计算整个序列可以并行处理训练速度大幅提升。你可以把 RNN 想象成一个人逐字读句子读完最后一个字可能已经忘了开头而 Transformer 像是一眼扫过整个句子同时判断每个词和其他词的关系。这个转变带来的影响是深远的。并行化让模型可以堆到几十层甚至上百层参数量从几百万涨到几千亿这才有了“大”语言模型的基础。没有 Transformer 的并行能力训练 GPT 这种规模的模型在算力上根本不可行。2.2 Self-Attention 的计算过程用矩阵乘法理解核心逻辑Self-Attention 是 Transformer 的灵魂很多人第一次看公式会觉得抽象但拆成矩阵运算其实很清晰。假设输入是一个序列每个词被表示成一个向量整个输入可以写成矩阵 (X)形状是 (n \times d)其中 (n) 是序列长度(d) 是向量维度。Self-Attention 做的是三件事用 (X) 分别乘以三个可学习的权重矩阵 (W_Q)、(W_K)、(W_V)得到 Query、Key、Value 三个矩阵。然后计算 (Q) 和 (K) 的点积除以 (\sqrt{d_k}) 做缩放再经过 softmax 得到注意力权重最后用这些权重对 (V) 加权求和。用公式写就是[ \text{Attention}(Q, K, V) \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V ]为什么要除以 (\sqrt{d_k})因为当维度 (d_k) 很大时点积的结果会变得很大softmax 之后会趋近于 one-hot梯度几乎消失。除以 (\sqrt{d_k}) 相当于把数值拉回一个合理的范围让梯度保持稳定。这个细节在实现的时候很容易忽略但不做缩放训练很容易崩。用 PyTorch 写一个最简版的 Self-Attention 大概是这样import torch import torch.nn as nn import math class SelfAttention(nn.Module): def __init__(self, d_model): super().__init__() self.d_model d_model self.W_q nn.Linear(d_model, d_model) self.W_k nn.Linear(d_model, d_model) self.W_v nn.Linear(d_model, d_model) def forward(self, x): Q self.W_q(x) K self.W_k(x) V self.W_v(x) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_model) attn torch.softmax(scores, dim-1) out torch.matmul(attn, V) return out, attn这段代码可以直接跑输入一个(batch, seq_len, d_model)的张量输出注意力加权后的表示和注意力权重矩阵。注意力权重可以可视化你会看到每个词对其他词的关注程度这在调试模型时非常有用。2.3 Multi-Head Attention为什么要多个头单个 Attention 头只能学到一种关注模式比如可能只关注相邻词或者只关注句法结构。但语言里的关系是多种多样的有的词需要看主语有的需要看修饰语有的需要看上下文的情感倾向。Multi-Head Attention 就是并行跑多个 Self-Attention每个头有自己独立的 (W_Q)、(W_K)、(W_V)最后把多个头的输出拼接起来再经过一个线性层融合。头的数量通常取 8 或 16每个头的维度是 (d_{model} / h)。比如 (d_{model}512)(h8)每个头的维度就是 64。这样总计算量和单头差不多但模型能同时捕捉多种关系。你可以理解为一个人从不同角度观察同一句话有人看语法有人看语义有人看情感最后把意见综合起来。实现上就是把上面的 SelfAttention 复制多份或者用矩阵 reshape 的方式一次性算完。实际工程里为了效率通常会把多个头的计算合并成一个大矩阵运算避免 Python 循环。2.4 位置编码没有循环结构顺序信息怎么保留Transformer 没有循环也没有卷积如果只靠 Attention打乱输入顺序结果是一样的这显然不对。所以需要额外注入位置信息这就是位置编码。原始论文用的是正弦和余弦函数[ PE_{(pos, 2i)} \sin(pos / 10000^{2i/d_{model}}) ] [ PE_{(pos, 2i1)} \cos(pos / 10000^{2i/d_{model}}) ]其中 (pos) 是位置(i) 是维度索引。这样每个位置都有一个唯一的编码向量而且不同位置之间的相对关系可以通过线性变换表示。后来也有可学习的位置嵌入比如 BERT 用的就是可学习的参数。现在很多大模型改用 RoPE旋转位置编码通过旋转矩阵把位置信息编码到 Attention 计算里对长序列的外推能力更好。位置编码的维度必须和词嵌入维度一致这样两者可以直接相加。相加之后每个词的表示既包含语义信息也包含位置信息再送入 Attention 层。2.5 完整 Transformer Block残差、LayerNorm 和前馈网络一个完整的 Transformer Block 不只是 Attention还包括残差连接、LayerNorm 和前馈网络。残差连接把输入直接加到输出上缓解深层网络的梯度消失问题。LayerNorm 对每个样本的特征维度做归一化稳定训练。前馈网络通常是两层全连接中间用 ReLU 或 GELU 激活维度先升后降比如从 512 升到 2048 再降回 512。整个 Block 的计算流程是输入经过 LayerNorm再进 Multi-Head Attention输出和原始输入相加然后再经过 LayerNorm进前馈网络输出再和上一步的结果相加。这种结构可以堆很多层GPT-3 堆了 96 层每层都有 Attention 和前馈网络。写成一个模块大概是这样class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.attn nn.MultiheadAttention(d_model, n_heads, dropoutdropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.ff nn.Sequential( nn.Linear(d_model, d_ff), nn.GELU(), nn.Linear(d_ff, d_model) ) self.dropout nn.Dropout(dropout) def forward(self, x): attn_out, _ self.attn(x, x, x) x self.norm1(x self.dropout(attn_out)) ff_out self.ff(x) x self.norm2(x self.dropout(ff_out)) return x这里用的是 Pre-LN 结构也就是先归一化再进子层和原始论文的 Post-LN 略有不同。Pre-LN 在深层模型里训练更稳定现在的大模型基本都用这种。3. 从 Transformer 到 LLM训练流程与关键环节3.1 预训练让模型学会预测下一个词预训练阶段的目标很简单给模型大量文本让它预测下一个词。比如输入“今天天气真”模型要预测“好”。损失函数就是交叉熵比较预测分布和真实下一个词的 one-hot 分布。虽然目标简单但数据量足够大时模型会学到语法、事实知识、推理能力甚至一些世界常识。预训练的数据通常来自网页、书籍、代码库等清洗和去重非常关键。数据质量差模型学到的就是垃圾。训练时用 Adam 优化器学习率预热后余弦衰减batch size 尽量大梯度累积是常见手段。这个阶段算力消耗极大GPT-3 用了上千张 GPU 训练几个月普通人很难复现但可以下载开源模型继续微调。预训练后的模型叫基座模型它能续写文本但不太会回答问题。你问它“法国的首都是哪里”它可能续写成“法国的首都是哪里这是一个常见问题”而不是直接回答“巴黎”。因为它只学了预测下一个词没学过对话格式。3.2 监督微调教会模型按指令回答监督微调SFT是用人工标注的指令-回答对来训练模型。比如输入“请解释什么是重力”目标输出是一段合理的解释。数据量不用像预训练那么大几千到几万条高质量样本就能显著改变模型行为。关键是数据的多样性和质量覆盖问答、写作、翻译、代码等任务。SFT 的训练损失还是交叉熵但只计算回答部分的损失指令部分不计算。这样模型学会的是“看到这样的指令应该生成这样的回答”。实际做的时候学习率要比预训练小很多通常用 1e-5 到 5e-5训练 2 到 3 个 epoch 就够了训多了容易过拟合。3.3 RLHF用人类偏好对齐模型行为RLHF 分三步。第一步用 SFT 模型生成多个回答让人工标注哪个更好训练一个奖励模型。奖励模型的输入是问题和回答输出一个分数分数越高表示人类越喜欢。第二步用强化学习算法通常是 PPO优化 SFT 模型让它生成的回答能获得更高的奖励分数同时加一个 KL 惩罚项防止模型偏离 SFT 模型太远否则会输出乱七八糟的东西。第三步是迭代用新模型继续生成数据继续训练奖励模型和策略模型。这个过程很复杂训练不稳定是常态超参数敏感。现在也有 DPO 等更简单的方法直接用偏好数据优化模型不需要显式的奖励模型和强化学习效果也不错。RLHF 的核心价值是让模型输出更符合人类期望比如更有帮助、更诚实、更无害。但也要注意奖励模型可能被“钻空子”模型会学会讨好奖励模型而不是真正解决问题这就是所谓的 reward hacking。3.4 思维链让模型学会分步推理思维链Chain-of-Thought不是训练阶段的必须环节而是一种提示技巧。在提问时加上“让我们一步步思考”模型会生成中间推理步骤最后给出答案。对于数学题、逻辑题思维链能显著提升准确率。后来也有研究把思维链数据加入 SFT让模型默认就输出推理过程。思维链的本质是让模型把复杂问题拆成多个简单步骤每一步的预测难度都降低了整体正确率就上去了。你可以理解为让模型“打草稿”而不是直接报答案。现在很多模型在回答前会先输出一段思考过程就是思维链的工程化应用。4. 动手实现一个迷你 LLM从数据到生成4.1 数据准备与分词器训练要自己训一个迷你 LLM第一步是准备数据。可以用公开的文本数据集比如中文维基百科、新闻语料、小说等。数据清洗包括去重、去除乱码、过滤敏感内容。然后训练分词器常用的是 BPE 或 SentencePiece。分词器把文本切成子词单元平衡词表大小和序列长度。用 HuggingFace 的 tokenizers 库训练一个 BPE 分词器大概这样from tokenizers import Tokenizer, models, trainers, pre_tokenizers tokenizer Tokenizer(models.BPE()) tokenizer.pre_tokenizer pre_tokenizers.Whitespace() trainer trainers.BpeTrainer(vocab_size8000, special_tokens[pad, unk, s, /s]) tokenizer.train([data.txt], trainer) tokenizer.save(tokenizer.json)词表大小 8000 到 50000 都常见太小会导致序列过长太大则嵌入矩阵参数量增加。中文场景下词表通常比英文大一些因为汉字组合多。4.2 模型搭建与参数配置迷你 LLM 的配置可以这样(d_{model}256)层数 4 到 6头数 4 到 8前馈维度 1024最大序列长度 256 或 512。参数量大概几百万到几千万单张消费级显卡就能训。嵌入层用nn.Embedding位置编码可以用可学习嵌入输出层把隐藏状态映射到词表大小用交叉熵损失。训练循环里每个 batch 取一段连续文本输入是前 n 个词目标是后 n 个词。用 AdamW 优化器学习率 3e-4warmup 几百步然后余弦衰减。梯度裁剪设 1.0防止梯度爆炸。batch size 根据显存调整不够就用梯度累积。4.3 训练过程监控与调参经验训练时重点看 loss 曲线。如果 loss 震荡厉害可能是学习率太大或者 batch size 太小。如果 loss 不下降检查数据有没有问题或者模型初始化是否合理。验证集 loss 开始上升时说明过拟合了可以加 dropout 或减少训练步数。我自己的经验是小模型训练很快就能看到效果几千步后就能生成通顺的短句。但要注意小模型的知识容量有限不要指望它能回答复杂问题。它的价值在于帮你理解整个流程跑通之后换成更大的模型和数据思路是一样的。4.4 文本生成温度、Top-k 和 Top-p 采样训练完模型生成文本时需要采样策略。贪心解码每次选概率最大的词结果确定但容易重复。温度采样通过除以温度参数调整概率分布的平滑程度温度大于 1 更随机小于 1 更保守。Top-k 只从概率最高的 k 个词里采样Top-p 从累积概率达到 p 的最小词集合里采样。实际用的时候温度 0.7 到 1.0 比较常见Top-p 0.9 左右。代码实现就是在 logits 上做处理def sample(logits, temperature1.0, top_k50, top_p0.9): logits logits / temperature if top_k 0: indices_to_remove logits torch.topk(logits, top_k)[0][..., -1, None] logits[indices_to_remove] -float(Inf) if top_p 1.0: sorted_logits, sorted_indices torch.sort(logits, descendingTrue) cumulative_probs torch.cumsum(torch.softmax(sorted_logits, dim-1), dim-1) sorted_indices_to_remove cumulative_probs top_p sorted_indices_to_remove[..., 1:] sorted_indices_to_remove[..., :-1].clone() sorted_indices_to_remove[..., 0] 0 indices_to_remove sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) logits[indices_to_remove] -float(Inf) probs torch.softmax(logits, dim-1) next_token torch.multinomial(probs, num_samples1) return next_token这段代码可以直接用在生成循环里每次拿到 logits 后调用得到下一个 token拼接到输入后面继续生成。5. 常见问题与排查技巧实录5.1 训练不收敛或 loss 震荡怎么办训练不收敛的原因很多按优先级排查先看数据确保输入和目标对齐没有乱码或空样本再看学习率太大容易震荡太小下降慢可以试试 1e-4 到 1e-3 之间然后看初始化嵌入层和线性层用正态分布初始化标准差 0.02 左右最后看梯度打印梯度范数如果爆炸就加梯度裁剪如果消失就检查网络深度和激活函数。Loss 震荡还有一个常见原因是 batch size 太小梯度噪声大。可以增大 batch size 或者用梯度累积模拟大 batch。另外学习率预热也很重要前几百步从 0 线性增加到设定值能显著稳定训练。5.2 生成重复文本怎么解决模型生成重复文本通常是因为训练不足或者采样策略太保守。先检查训练 loss 是否降到合理范围如果 loss 还很高说明模型没学好。如果 loss 正常但生成重复可以调高温度或者用 Top-k、Top-p 采样避免总是选概率最高的词。也可以在训练数据里增加多样性减少重复模式。另一个技巧是加重复惩罚对已经生成的词降低其 logits。但惩罚系数要适中太大可能导致语句不通顺。我一般从 1.1 开始试根据效果调整。5.3 显存不够用的优化手段显存不够时首先减小 batch size用梯度累积保持等效 batch。其次用混合精度训练fp16 或 bf16 能省一半显存。还可以用梯度检查点牺牲计算时间换显存。如果还不行就减小模型维度或层数或者用 LoRA 等参数高效微调方法只训练少量参数。推理阶段可以用量化把模型权重转成 int8 或 int4显存占用大幅下降速度也有提升。HuggingFace 的 bitsandbytes 库支持 8bit 和 4bit 量化几行代码就能加载大模型。5.4 常见问题速查表问题现象可能原因排查方向解决方法Loss 不下降学习率太小、数据有问题检查数据对齐、打印梯度调大学习率、清洗数据Loss 震荡学习率太大、batch 太小观察 loss 曲线降低学习率、增大 batch生成重复训练不足、采样太保守检查 loss、调整采样调高温度、用 Top-p显存不足batch 太大、模型太大查看显存占用减小 batch、混合精度过拟合训练太久、数据太少对比训练和验证 loss加 dropout、早停注意调参时每次只改一个变量否则无法判断是哪个改动起了作用。记录每次实验的配置和结果方便回溯。5.5 实操心得少走弯路的几个建议第一不要一上来就训大模型。先用小模型和小数据跑通全流程确认每个环节都正确再逐步放大。第二数据质量比数据量重要清洗干净的一万条数据可能比脏的十万条更有效。第三多看 loss 曲线和生成样例不要只看最终指标。第四善用开源工具和预训练模型没必要所有东西都从头写。第五保持耐心训练模型经常需要反复调试一次成功是少数。我自己在实现迷你 LLM 的时候最大的坑是位置编码和 Attention 的维度对不上导致训练完全没效果。后来打印了每一层的输入输出形状才发现是 reshape 的时候搞错了维度顺序。所以强烈建议在关键步骤加形状检查能省很多时间。这个方向后续还可以往很多地方扩展比如加入 LoRA 做高效微调用 DPO 替代 RLHF或者把模型部署成 API 服务。每一步深入下去都有不少细节可以挖后面有机会再单独展开聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑