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

资讯详情

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

跟着 Qwen2.5-7B 走一遍真实推理:理解 Transformer 如何生成下一个 token

跟着 Qwen2.5-7B 走一遍真实推理:理解 Transformer 如何生成下一个 token 学习 Transformer、调 LLM 参数时我们常听到这些词token、embedding、hidden state、LM Head、logits、softmax。每个词单独查都能找到解释放在一起却容易分不清谁先谁后也难把它们与模型的一次回答对应起来。这篇文章从一次真实的Qwen2.5-7B 推理出发。我把一个实际问题交给已经训练好的模型记录它运行时的编号、向量、候选分数和生成轨迹再沿着这些真实数据走一遍从输入到下一个 token 的过程理解 Transformer 怎样参与 LLM 生成。这次输入的问题是问题实践中 LLM 的 temperature 该怎么设置设置为 0 就能防止幻觉吗 回答我们暂时不评价模型的回答先观察它怎样开始回答。实测条件2026 年 10 月 2 日Qwen2.5-7B 基础模型RTX 3090BF16bfloat1616 位浮点格式PyTorch 2.8.0、Transformers 4.57.1使用 eager attention未量化、未套聊天模板、未额外添加特殊 token。模型处于推理模式权重不更新。下面的编号、向量、分数和生成轨迹来自这次运行概率表由保存的真实分数计算。它是一种常见的decoder-only 自回归 LLM根据已有内容预测下一 token把选出的 token 接回已有内容再预测下一个。「自回归」说的就是这个循环。1. Tokenizer 把文字变成编号模型计算时用的不是直接可读的汉字而是token ID。Tokenizer分词编码器按它的词表和编码规则把文字转换为一串整数编号。Token是模型处理和生成的单位可以对应一个词、词片段、标点或字节片段不能假定「一个汉字就是一个 token」。上面的输入实际得到25 个 token[86119, 5122, 107933, 444, 10994, 43589, 9315, 220, 110801, 43918, 11319, 43918, 17714, 220, 15, 58230, 109, 26232, 104431, 100320, 99353, 101037, 94432, 102104, 5122]挑几个位置看看位置从 0 开始计数输入位置Token ID可读片段086119问题3444L带前导空格410994LM69315temperature带前导空格24末位置5122这里的LLM被拆成了两个 tokentemperature则是一个。词表vocabulary保存 token 与 ID 的对应关系ID 只是编号9315 并不比 5122「更有意义」。这次输入里的「 就」还被拆成两个字节片段对应 ID 58230 和 109。单独解码时可能出现拼起来才能还原完整文字。因此逐个 token 的显示结果与整个序列的解码结果需要分开看。1.1 先分清25 个输入 token 怎样接出新内容这25 个 token 是已经给定的输入上下文不是回答的长度预算。首次运行时把这段已知输入一起送入模型计算各位置的表示再用末位置的结果预测并选出第一个新 token。随后把新 token 接回已有内容历史长度成为 26。根据这段历史再预测第二个新 token长度成为 27继续循环。回答可以超过 25 个 token直到达到停止条件或可用的长度预算。先带着这条主线读下去处理已有输入 → 选出一个新 token → 把它接回历史 → 继续预测。接下来逐步看这些位置怎样参与计算。2. Embedding 把编号变成向量编号可以查表却还不能直接承担模型的连续数值计算。Embedding嵌入为每个 token ID 取出一条训练得到的数字向量。这次每条向量有3584 个坐标。下面只展示前四个四舍五入到六位小数省略号代表其余坐标并不表示这是一条四维向量。9315带空格的 temperature → [ 0.026245, -0.015442, 0.020874, 0.026245, …] 5122末位置的冒号 → [-0.011597, -0.003036, 0.021240, 0.009399, …]这些数字是模型学到的表示我们不需要给每个坐标人为贴上「颜色」「天气」这样的语义标签。本次使用已经训练好的权重推理过程中查表和计算不重新学习这些向量。25 个位置各有一条向量整体形状就是[1, 25, 3584]1 个样本、25 个位置、每个位置 3584 个坐标。模型还需要位置信息。这次 Qwen 使用RoPE旋转位置编码把位置引入 attention 的匹配过程。小框RoPE 怎样让模型感知位置Attention 会用 Qquery查询向量与 Kkey键向量计算上下文位置之间的匹配。RoPE 按 token 所在的位置旋转 Q、K 中成对的坐标。可以把每对坐标想成平面上的一支小箭头位置编号决定旋转角度不同坐标对使用不同旋转频率。这样Q 与 K 做点积时匹配分数就能体现相对位置信息例如两个 token 相隔多少个位置。向量的维数保持不变。在本次 Qwen 中RoPE 旋转 Q、K不旋转 V也不直接给 embedding 加位置向量。RoPE 原论文、Qwen2 的实现3. Transformer 把上下文融入表示现在有了每个 token 的初始向量但我们还需要结合上下文同一个词放在不同问题里后面适合接的内容可能完全不同。这些向量会经过多层Transformer block模块。先认识四种操作的职责Attention注意力从允许访问的上下文位置读取并加权组合信息。因果掩码让当前位置不能读取后面的位置。FFNMLP前馈网络对每个位置的表示进一步做非线性变换。Residual connection残差连接把进入子层的表示与子层结果相加在原有表示上更新信息。Normalization归一化按架构规定调整表示的数值尺度具体形式和位置因模型而异。3.1 25 个位置一起计算通过注意力结合上下文Embedding 为各位置独立查出初始向量。进入 attention 后各位置会读取可见位置的信息因此这 25 个 token 会在同一段上下文里发生关联。本次使用因果注意力位置从 0 开始编号当前位置可以读取的位置00自身60–6自身及前面的 6 个位置24末位置0–24全部 25 个输入位置因为输入内容已经给定同一层可以并行计算这些位置因果掩码仍限制每个位置可以读取谁。接着这一层的全部结果送入下一层按层继续计算。这里的上下文融合包含 attention 的加权汇总不是把 25 条 embedding 简单相加成一条。这次模型有28 层 Transformer。各位置在每层使用该层共享的权重计算但得到的结果随各自内容与可见上下文变化。输入末位置虽然只是一个冒号它的最终表示仍能受到前面整个问题影响。Qwen2 的因果注意力与层间计算这些中间或最终的数字表示叫hidden state隐藏状态。名字里的「隐藏」不代表里面存着一句人类可读的想法它仍是一组数。常规下一 token 生成主要读取末位置的最终表示此前位置的信息已经通过网络计算影响了它。把前面出现的 Q、K 与 **Vvalue值向量**放回 attention它们都由表示投影得到。Q 与 K 用来计算注意力权重再按权重组合 V。Attention head指多组并行的注意力计算后面还会遇到另一个名字里带 head 的模块。4. LM Head 把隐藏状态变成候选分数第一次接触 LM Head可以先看 4.1已经了解它的职责可以直接看 4.2 的实际计算。4.1 先认识 LM Head模型的输出打分层经过 Transformer我们得到的是隐藏状态也就是融合了上下文的数字向量。但用户最终看到的是文字还需要把这条向量与「下一步可以输出哪些 token」接起来。**LM 是 language model 的缩写LM Head 是语言模型的输出头。**可以先把它理解为一个输出打分层接收隐藏状态为各个候选 token 算出一个分数。比如面对当前问题它会给「温度」「temperature」「实践中」等候选分别打分。这里尚未选定任何一个 token后面的采样或贪心解码才会根据这些分数决定这一步输出谁。对照前面的 embedding更容易看清两端的职责模块接收什么产出什么Embedding一个 token ID一条输入向量LM Head一条最终隐藏状态向量各候选编号的分数这些分数叫logits是未归一化的原始分数还不是概率。「LM Head」与「attention head」也分别位于输出投影和注意力计算中同样带 head职责并不相同。4.2 看真实计算3584 个坐标怎样变成 152064 个分数先看输入。实际送入 LM Head 的末位置向量已经过最终归一化完整长度仍是3584。用h表示这条完整向量下面只展示它的前四个坐标[-1.5546875, -1.6171875, 0.57421875, -1.421875, …]这条向量属于输入末位置「回答」的冒号。融合前面的上下文后它用于预测冒号后面该接哪个 token。再看权重。这次 Qwen 的 LM Head 是一个无偏置的线性层使用训练得到的权重矩阵W。按 PyTorch 的存储方式它有152064 行、3584 列行索引从 0 开始每行对应一个输出候选编号列对应隐藏状态的坐标。Qwen2 输出头定义、PyTorch Linear 的权重形状对候选编号i取出W的第i行把它与h的对应坐标相乘再把全部乘积相加候选 i 的分数 h[0] × W[i,0] h[1] × W[i,1] … h[3583] × W[i,3583]这就是这里所说的线性投影也可以理解为一次点积**3584 个坐标参与计算得到这个候选的一个分数。**例如 ID 104273 对应「温度」它对应的那一行权重与完整h做乘加本次得到的分数为15.75。对全部 152064 行执行这套计算就得到 152064 个分数。实际实现通过矩阵运算一起计算各项推理过程中权重W固定随当前上下文变化的是输入h。一条隐藏状态 h3584 个坐标 ↓ 与 LM Head 的各行权重做乘加 一条 logits 向量152064 个分数 ↓ 按分数所在的编号查 tokenizer 可以查看对应的候选 token 片段本次 tokenizer 的长度为 151665模型输出维度则为 152064两项口径不同。这里先完整计算模型的输出再解释 tokenizer 能映射的编号下一节的概率计算也按完整模型输出维度进行。这次末位置分数最高的三项是候选 tokenToken ID原始分数温度10427315.75temperature无前导空格3455815.3125实践中10793314.37515.75 的含义就是「温度」这个候选的原始分数。Logits 可以为正也可以为负不要求加起来等于 115.75 不是概率分数为 0 也不表示「不可能」。下一节才会把整组分数变成概率。一次前向中25 个输入位置都经过这个输出头所以完整输出是[1, 25, 152064]。这 25 行分别对应各输入位置之后的下一 token 分数当前要接着整段输入作答就读取第 24 行也就是末位置那一行从中选出一个新 token。一次计算出 25 行分数并不等于已经生成了 25 个回答 token。最后核验这条连接取实际记录的完整 LM Head 输入再应用同一个输出头结果与模型返回的末位置 logits最大绝对差异为 0。前面的四个坐标只是展示片段核验使用全部 3584 个坐标不能仅凭那四个数算出 15.75。到这里模型已经给候选打完分。接下来我们再看 temperature、softmax 和采样怎样利用这组分数。5. Temperature 与 softmax 把分数变成概率Softmax把一组分数转换成非负、总和为 1 的概率。正温采样中temperature温度参数先缩放分数再由 softmax 计算概率。下文用 T 表示 temperature温度参数因此T0.5就是设置temperature0.5。表格行中的「温度」「temperature」则是模型可能生成的候选文字。比较同一组 logits 时T1 不做这项缩放低于 1 会让分布更集中高于 1 则让它更平。固定刚才的完整 logits不截断候选得到候选T0.5T1T2「温度」61.79%24.26%0.89%「temperature」25.76%15.67%0.72%「实践中」3.95%6.13%0.45%其余候选合计约8.50%53.94%97.94%每列都在完整的152064 个输出项上计算。它不是只对前三名归一化也不是整段答案的正确率。排序保持不变「温度」的概率优势却明显变化了。计算口径表格把保存的同一份 BF16 logits 转为 FP64双精度浮点数后计算参考概率显示两位百分比模型前向仍是 BF16。原 FP32单精度概率在 T1、T2 的求和检查中超过了预设的2×10⁻⁶误差阈值。下面的实际采样沿用原 FP32 路径。现在可以定位 temperature 了它处理的是LM Head 已经产生的候选分数。网络内部的 attention 也用 softmax但它在上下文位置之间分配权重这里的 softmax 则在输出 token 之间分配概率。6. 过滤候选再决定这次选谁Top_k保留分数排名靠前的固定数量候选top_p按累计概率阈值保留高概率候选集合。过滤之后还要对保留项重新归一化。这次 T1、top_k2 只留下「温度」和「temperature」最终概率约为60.77% 和 39.23%已经不同于截断前的 24.26% 和 15.67%。T1、top_p0.9 则保留296 个候选编号它并不是保留 90% 的词表。Sampling采样按最终概率随机选出一个 token。概率最高的项不必每次都被选中。Seed随机种子用于控制采样的随机数过程不负责判断候选内容是否有事实依据。本次实际采样设置为 T1、top_k0关闭、top_p1seed20261002使用 FP32 概率在 CPU 上抽取。第一步选中的是第二名「temperature」ID 34558。另一条路径是greedy decoding贪心解码直接选择处理后分数最高的项也叫取argmax。这次单独执行的贪心分支第一步选中最高分「温度」ID 104273。支持 temperature0 的服务通常用特殊分支进入贪心它不执行除以零也不需要先算 softmax 才能找出最大分数。具体参数和处理顺序要看引擎实现。7. 追加 token回到下一轮第一步选出的 ID 接回原有的 25 个编号成为下一步的条件。下面把采样分支的实际轨迹与历史长度放在一起看本次没有使用缓存每步将完整前缀送入模型预测第几个新 token本步前缀长度选出的 token 与 ID累计新增文字125temperature34558temperature226空格220temperature327用于100751temperature 用于428控制100359temperature 用于控制第一步只需已有的 25 个输入第二步还需要刚生成的「temperature」第三步又加入刚生成的空格。这些新 token 也会经过 embedding、Transformer 与 LM Head参与后续预测。生成阶段按先后继续是因为后一个新 token 的条件包含前一个实际选出的 token。[外链图片转存中…(img-vwxgd4D3-1791035394673)]相同起点的贪心分支则依次选择[104273, 32665, 9909, 34558]得到贪心温度参数temperature 采样temperature 用于控制只有第一步共享同一组 logits。选出的 token 不同后续前缀就不同模型会重新计算相应分数。Tokenizer.decode将编号序列还原为文字它不负责选择编号。流式输出里的一块文字也可能包含多个 token不能直接当作模型的一次选择。生成会在遇到适用的EOSend-of-sequence序列结束标记或其他停止条件时结束。max_new_tokens限制新增 token 的数量模型与服务还会限制可用的上下文长度。本次设置max_new_tokens4两个分支都新增 4 个 token最终历史共 29 个它们因这个人为上限停止没有生成 EOS。上面只是回答的开头不能据此评出回答质量或幻觉率。**用 KV cache 时还要重跑全部历史吗**首次处理已知输入称为prefill接着进入逐 token 生成的decode 阶段。使用 KV cache 时保存此前各层 attention 的 keyvalue后续通常只把刚生成的一个 token 作为新输入结合缓存中的历史继续计算。因此送入模型的新编号可以只有一个逻辑上下文仍包含原输入与已生成内容。Hugging Face 缓存说明本次正文轨迹未使用缓存。另做的 BF16 缓存对照中两步选择相同但 logits 未通过原定数值容差不能写成缓存前后完全一致。这里的 decode 阶段指继续生成与tokenizer.decode的编号转文字是两个用法。8. 把概念放回同一条链路[外链图片转存中…(img-PcdHmPQZ-1791035394673)]这条链路对应的实际形状是阶段本次形状或结果输入编号[1, 25]Embedding[1, 25, 3584]28 层 Transformer 与最终归一化之后[1, 25, 3584]LM Head 输出[1, 25, 152064]取末位置进行下一 token 选择152064 个分数 → 1 个 ID形状相同不代表数字相同冒号的初始 embedding与融合上下文后的最终隐藏状态已经是不同的向量。除了前面的输出头重算本次还核对了首层 attention、MLP 的两次残差相加重算与记录的最大绝对差异均为0捕获的首层注意力权重中未来位置的最大权重为0。这些检查分别对应具体环节不能扩展成整个推理系统都已获得一致性保证。9. 带着这条链路去看 temperature最值得记住的是三个区别**隐藏状态是表示logits 是分数softmax 后才是概率。**LM Head 连接表示与候选分数解码过程决定这一步最终选择谁。temperature 在常见生成接口中调整输出端的分布。降低它会让本来较高分的候选更容易被选中候选所表达的内容有没有事实依据还需要另外判断。下一篇《temperature 设为 0就不会产生幻觉了吗从解码机制到生产选值》会从这里继续零温保证什么哪些错误它改变不了生产系统又该怎样通过评测选择参数。我是程工造Agent做 Agent 系统与 RAG 的工程师记录有数据、有验证过程的工程实践。下一篇继续讨论temperature 设为 0为什么仍然可能产生幻觉以及生产中怎样选值。更多复盘见知乎专栏「Agent 工程手记」。
返回列表