从零学大模型:Token ID 如何变成向量?
在上一篇《文本如何变成 Token ID?》中,How are 经过分词后得到 Token ID 序列 [2437, 389]。但 Token ID 只是词表中的整数编号,不能直接参与 GPT-2 的向量和矩阵计算。
本文先打开 GPT-2 的模型黑盒,了解 Embedding(嵌入)的位置;再说明 Token Embedding 与 Position Embedding 如何组成 Initial Hidden State(初始隐藏状态);最后用 llama.cpp 查看实际参数和计算结果。
GPT-2 内部的整体流程
GPT-2 接收 Token ID 序列,依次经过 Embedding、12 个 Transformer Block 和 LM Head,最终得到 logits:
本文聚焦图中蓝色的 Embedding。为简化整体流程,图中未单独画出 Final LayerNorm;采样位于 GPT-2 模型之外,将在后续文章中介绍。
从 Embedding 到 Initial Hidden State
GPT-2 分别根据 Token ID 和 Position ID 查询两张 Embedding Table,再将查到的向量逐元素相加,得到 Initial Hidden State,也就是第一个 GPT-2 Block 的输入:
图中的符号含义如下:
| 符号 | 含义 | GPT-2 Small / 本文示例 |
|---|---|---|
V |
词表大小 | 50257 |
D |
Hidden / Embedding size | 768 |
C |
最大位置数 | 1024 |
T |
当前输入的 Token 数量 | 2 |
Token Embedding
Token ID 是 Token 在词表中的整数编号。例如:
1 | "How" → 2437 |
数字 2437 的大小不表示 "How" 的语义,也不能把相邻的 Token ID 理解为语义相近。Token ID 在模型中的作用更像数组下标:模型用它从 Token Embedding Table 中取出一个向量。
Embedding Table 与查表
设 Token Embedding Table 为 E_token,它包含 V 行,每行 D 个数。对于 [2437, 389],模型取出编号为 2437 和 389 的两行,按输入顺序组成 [2, 768] 的 Token Embedding:
设第 i 个 Token ID 为 id_i,查表过程可以写成:
1 | X_token[i] = E_token[id_i] |
这个过程称为 Embedding Lookup(Embedding 查表)。
两种 shape 表示
shape(形状)表示 Tensor(张量)各个维度的大小。本文原理部分与 llama.cpp / GGML 使用不同的维度顺序,同一个 Token Embedding 可表示为:
| 表示方式 | 维度顺序 | 通用 shape | How are 示例 |
|---|---|---|---|
| 本文原理部分 | Token 数量、Hidden 维度 | [T, D] |
[2, 768] |
| llama.cpp / GGML | Hidden 维度、Token 数量 | {D, T} |
{768, 2} |
Position Embedding
Token Embedding 只由 Token ID 决定。同一个 Token 无论出现在第几个位置,都会查到同一个 Token Embedding 向量。
但文本顺序会影响含义。例如:
1 | dogs chase cats |
两个句子包含相同的单词,但顺序不同,追逐者和被追逐者也随之交换。
为了让模型知道每个 Token 位于序列中的什么位置,GPT-2 还会查询 Position Embedding Table。对于同一个 Token ID id,它出现在位置 0 和位置 2 时,Token Embedding 相同,但加入的 Position Embedding 不同:
1 | E_token[id] + E_position[0] |
因此,同一个 Token 出现在不同位置时,会得到不同的 Initial Hidden State。
GPT-2 Small 的 Position Embedding Table 包含 1024 个位置,每个位置对应一个 768 维向量,即 [C, D] = [1024, 768]。根据 T 个 Position ID 查表,得到 [T, D] 的 Position Embedding。
How are 是一段没有历史上下文的新输入,因此两个 Token 的位置编号依次是 0 和 1:
| 输入位置 | Token ID | Token | Position ID |
|---|---|---|---|
| 0 | 2437 | How |
0 |
| 1 | 389 | are |
1 |
Position ID 和 Token ID 是两套不同的编号:
- Token ID 用来查询 Token Embedding Table,表示“当前是什么 Token”。
- Position ID 用来查询 Position Embedding Table,表示“当前位于哪里”。
GPT-2 的 Position Embedding 通过训练得到,并使用绝对位置编号。这里的“绝对”表示每个位置使用自己的编号,例如 0、1、2。
Initial Hidden State
Token Embedding 和 Position Embedding 的 shape 相同,GPT-2 将它们逐元素相加:
1 | Token Embedding [T, D] |
对序列中的第 i 个位置:
1 | X_0[i] = E_token[id_i] + E_position[pos_i] |
下面用 3 维向量演示相加过程。数值仅用于说明,不是 GPT-2 的真实参数:
1 | Token Embedding [ 0.20, -0.10, 0.50] |
GPT-2 Small 实际使用 768 维向量。对于包含两个 Token 的 How are,相加前后的 shape 都是 [2, 768]。
为什么选择相加?
相加可以理解为给 Token 向量加上一个由位置决定的偏移。同一个 Token 出现在不同位置时,就会得到不同的输入表示。
两张 Embedding Table 和后续网络会在训练中共同调整,让模型学会利用相加后的表示预测下一个 Token。相加是一种简单的组合方式,同时还能保持向量维度不变。
Embedding 参数从哪里来
Token Embedding Table 和 Position Embedding Table 都是训练得到的模型参数。推理框架从模型文件中加载它们,用于查表。
从训练到查表的过程可以概括为:
1 | 训练得到的两张 Embedding Table |
本文实战使用的模型文件格式是 GGUF。转换为 GGUF 后,GPT-2 中的两个 Embedding 参数对应为:
1 | transformer.wte → token_embd.weight |
GGUF 用元数据保存模型架构和配置,用 Tensor 保存 Embedding Table 等训练参数。
llama.cpp 实战
下面使用 llama.cpp b10435 和 GPT-2 Q8_0 模型,查看 GGUF 中的 Embedding 参数、运行时操作与 shape,以及实际 Tensor 数值。
基础环境准备见《LLM 如何逐个生成 Token?》,gguf-dump 的安装方式见《文本如何变成 Token ID?》。
进入 llama.cpp 目录,设置模型路径,并构建本篇使用的工具:
1 | cd llama.cpp |
查看 GGUF 中的 Embedding 参数
模型文件名中的 Q8_0 表示部分参数采用 Q8_0 量化格式保存,具体 Tensor 的类型仍需查看 GGUF。
使用 gguf-dump 读取模型元数据和两个 Embedding Tensor,再用 jq 只保留本篇需要的字段:
1 | .venv/bin/gguf-dump "$GPT2_MODEL" --json | |
整理后的输出如下(只调整了 JSON 的换行):
1 | { |
这份静态 GGUF 数据验证了前面的模型配置:
token_embd.weight的 GGML shape 是{768,50257},数学语义是[50257,768]。position_embd.weight的 GGML shape 是{768,1024},数学语义是[1024,768]。- Token Embedding Table 使用 Q8_0,Position Embedding Table 使用 F32,说明同一 GGUF 文件中的 Tensor 可以使用不同类型。
查看 Embedding 的计算过程与 shape
gguf-dump 用于查看静态参数,llama-eval-callback 则用于观察运行时 Tensor。下面以 How are 为输入,筛选 Embedding 阶段的三个节点:
1 | ./build/bin/llama-eval-callback \ |
整理后的关键输出如下:
1 | common_debug_cb_eval: embd = (f32) GET_ROWS( |
其中 leaf_5 对应 Position ID 输入。
这三个节点分别对应:
GET_ROWS(token_embd.weight, inp_tokens)根据[2437, 389]查询 Token Embedding。GET_ROWS(position_embd.weight, leaf_5)根据[0, 1]查询 Position Embedding。ADD(embd, pos_embd)把两个{768,2}Tensor 相加,得到{768,2}的inpL。
inpL 就是进入第一个 GPT-2 Block 的 Initial Hidden State。日志还显示,查表结果 embd 的类型是 F32。
查看 Embedding 与 Initial Hidden State 的数值
llama-eval-callback 还会在节点信息后打印 Tensor 数值。下面使用 -A 6 保留每个目标节点后面的六行:
1 | ./build/bin/llama-eval-callback \ |
整理后的数值部分如下:
1 | embd: |
每个 Tensor 都有两行,分别对应序列位置 0 的 "How" 和位置 1 的 " are"。每行实际包含 768 个值,调试输出只展示前 3 个和后 3 个。
不同计算后端的最后几位可能略有差异,但不影响这里的逐元素相加关系。
对照 Embedding 源码
GPT-2 在 load_arch_tensors() 中声明两个参数 Tensor:
1 | tok_embd = create_tensor( |
代入当前 GPT-2 Small 的配置:
1 | tok_embd → {768, 50257} |
在 llama_model_gpt2::graph::graph() 中,Embedding 主路径是:
1 | inpL = build_inp_embd(model.tok_embd); |
其中,通用的 build_inp_embd() 为 Token ID 创建输入 Tensor,再构造查表节点:
1 | inp->tokens = ggml_new_tensor_1d( |
将源码与运行结果对应起来:
1 | inp_tokens {2} |
这里的 ggml_get_rows() 和 ggml_add() 用于构造计算图节点,数值计算在执行计算图时完成。llama-eval-callback 展示的是节点执行后的 Tensor 类型、shape 和部分数值。
小结
本文以 [2437, 389] 为例,说明了 Token ID 如何变成 GPT-2 的输入向量:
- Token ID 只是词表中的整数编号;GPT-2 将它作为下标,查询训练得到的 Token Embedding Table。
- GPT-2 同时根据 Position ID 查询 Position Embedding,为每个 Token 加入位置信息。
- Token Embedding 与 Position Embedding 逐元素相加,得到 Initial Hidden State。