ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

datawhale:Happy-LLM 打卡-Task03:2.3搭建一个Transformer(tokenizer分词器、Embedding 层token嵌入、位置编码、输出头)

datawhale:Happy-LLM 打卡-Task03:2.3搭建一个Transformer(tokenizer分词器、Embedding 层token嵌入、位置编码、输出头)

前言:

该部分[1]主要就是对“输入、输出”部分一些细节的补充了,一段话如何变成token矩阵然后输入Task02打卡中的结构里运算,再如何转换回一段话。主要涉及到tokenizer分词器、Embedding 层(token嵌入)、位置编码、输出头等。

1tokenizer分词器

核心就是把自然语言序列通过分词、查表的方式,转化为token id序列。

首先来说明一下什么是“token”,根据国家数据局的定义[2]:

词元(Token)是大模型处理信息的最小信息单元,具有智能时代可计量、可定价、可交易的特征。

也就是说,一段话要被拆分为一串token才能被大模型处理。tokenizer分词器的作用就正是把一段话拆分为一串token,然后再转化为token id序列,以便于大模型处理。

分词器一般是一个相对独立的组件,与神经网络本身关系不是太大,使用如下所示:

from transformers import AutoTokenizer # 导入分词器 tokenizer = AutoTokenizer.from_pretrained("./") # 编码(意思是把自然语言序列转化为token id 序列) full_ids = tokenizer.encode(text)

具体而言,就是两步,先分词,再查表转化为索引,过程如下图:

tokenizer分词器本身是一个比较大、独立的知识概念了,,可以从用jieba库做分词词云讲起,,在此不进行赘述,仅进行举例说明。

以qwen2.5-1.5B的词表为例,采用了Byte-level BPE(BBPE)分词器[3]。

基础 BBPE 子词的词表的总大小为:151643

专用控制特殊 token:22 个(对话标记 <|im_start|>/<|im_end|>、结束符 <|endoftext|> 等)

【第1步】原始 Unicode 文本,也就是人类可读的输入
原文:我爱学习。 I love learning.
字符总数:22 个
字符列表:['我', '爱', '学', '习', '。', ' ', 'I', ' ', 'l', 'o', 'v', 'e', ' ', 'l', 'e', 'a', 'r', 'n', 'i', 'n', 'g', '.']

【第2步】编码为 UTF-8 字节流,是分词器处理的对象形式(1个中文汉字占3个字节,英文/数字/符号占1个字节)
字节总数:32 个
字节十进制:[230, 136, 145, 231, 136, 177, 229, 173, 166, 228, 185, 160, 227, 128, 130, 32, 73, 32, 108, 111, 118, 101, 32, 108, 101, 97, 114, 110, 105, 110, 103, 46]
字节十六进制:['0xe6', '0x88', '0x91', '0xe7', '0x88', '0xb1', '0xe5', '0xad', '0xa6', '0xe4', '0xb9', '0xa0', '0xe3', '0x80', '0x82', '0x20', '0x49',
'0x20', '0x6c', '0x6f', '0x76', '0x65', '0x20', '0x6c', '0x65', '0x61', '0x72', '0x6e', '0x69', '0x6e', '0x67', '0x2e']

【第3步】通过字节映射、BPE 合并,得到词表原生Token
分词后 Token 数量:8 个
词表原生 Token 列表:['æĪij', 'çα', 'åŃ¦ä¹ł', 'ãĢĤ', 'ĠI', 'Ġlove', 'Ġlearning', '.']
这里的 Token 是「字节映射后的字符串」,不是原始中文,和 vocab.json 里的 key 一致

【第4步】每个 Token 的详细对照如下(原生形式 → 还原文本 → 对应字节)
Token[ 0] 原生词表: 'æĪij' 还原文本: '我' 对应字节: [230, 136, 145]
Token[ 1] 原生词表: 'çα' 还原文本: '爱' 对应字节: [231, 136, 177]
Token[ 2] 原生词表: 'åŃ¦ä¹ł' 还原文本: '学习' 对应字节: [229, 173, 166, 228, 185, 160]
Token[ 3] 原生词表: 'ãĢĤ' 还原文本: '。' 对应字节: [227, 128, 130]
Token[ 4] 原生词表: 'ĠI' 还原文本: ' I' 对应字节: [32, 73]
Token[ 5] 原生词表: 'Ġlove' 还原文本: ' love' 对应字节: [32, 108, 111, 118, 101]
Token[ 6] 原生词表: 'Ġlearning' 还原文本: ' learning' 对应字节: [32, 108, 101, 97, 114, 110, 105, 110, 103]
Token[ 7] 原生词表: '.' 还原文本: '.' 对应字节: [46]

【第5步】原生 Token 映射为数字 ID
ID 序列:[35946, 99242, 100134, 1773, 358, 2948, 6832, 13]

这样,自然语言序列就转化为了token序列、再到进一步的token id序列。由此可见,Transformer 神经网络部分,输入的就是一串整数(token id序列)。

  • 注1:2017 原始论文《Attention Is All You Need》里的 Transformer模型,在机器翻译实验中统一使用 BPE(Byte Pair Encoding,字节对编码)分词器。
  • 注2:Transformer 架构本身不绑定任何分词器,分词器是独立的预处理模块,可以独立得到或者复用。
  • 注3:BBPE,字节级字节对编码,BPE的增强变体,在UTF-8 原始字节流上进行操作,不会出现未知 token,所有文字、符号、生僻字、emoji 都能拆成字节子词。BPE类分词器是大语言模型最常用的分词方法。
  • 注4:有时候,,也会把神经网络中进行计算的向量、token id等称为“token”、“token向量”等等,习惯性叫法,同时也算是对应的关系“token-token id-token向量-token向量1-token向量2......”,从“token向量1”开始就经过了注意力机制的信息融合,变成动态量了。

2 Embedding 层(token嵌入)

核心就是把token id转化为token向量、把token id序列转化为token矩阵。

而Embedding 层就是⼀个存储固定⼤⼩词典、可学习的嵌⼊向量查找表。根据token id查找对应的向量。

在模型训练好以后,Embedding 层是进行静态嵌入,每一个token id对应着一个唯一确定的向量。

之后通过注意力机制不断进行信息融合,才是动态的“上下文嵌入”。“token-token id-token向量-token向量1-token向量2......”,“token向量”还是静态的嵌入,从token向量1开始,经过了注意力机制的信息融合,才算是动态的上下文嵌入了。

3 位置编码(Positional Encoding)

位置编码是为了解决注意力机制无法区分位置顺序的问题。

Transformer模型使用的是绝对位置编码,现在的大模型大多采用旋转位置编码(RoPE)等其他方式。

3.1 为什么需要

首先,自然语言需要区分位置。“我爱学习”和“学习爱我”显然含义不同。同时,同一个词在不同位置也需要区分,“苹果手机上放了一个苹果”。

其次,为什么“注意力机制无法区分位置顺序”。

注意力权重计算阶段,是计算一个序列A的token i与序列B的token j的权重,是q与k两个向量做内积,得到相似度,进而归一化得到注意力权重,而与i、j的下标无关。也就是得到了("我", "爱", "学习")三个元素之间的权重关系,一个3*3的矩阵。

加权求和阶段,得到信息融合后新的token向量,序列A的token i的注意力输出,也就是token i',就是对序列B每个token的注意力权重与值加权求和,token j 的注意力权重与值绑定,改变顺序,不影响token i' 的计算结果。

多义词上,得到了("苹果", "手机", "上", "放", "了一个", "苹果"),在首次融合的时候,第一个“苹果”与最后一个“苹果”的词向量相同,都是根据“苹果”token的token id (104167)得到对应的静态嵌入向量,因此与其他token计算得到的注意力权重相同,最后计算得到融合后新的token向量也相同。但这就有问题了,显然第一个“苹果”后面跟着“手机”,是指代了品牌名称,这两个“苹果”应该有区别,而且是与其所在位置有关,也就是与其所处的上下文有关。即:

  • 对于任意一个 token,第 1 个位置和第 100 个位置的 token,只要词向量内容相同,对它的注意力贡献就完全相同;
  • 相邻的两个 token 和相隔很远的两个 token,在注意力计算中没有天然的权重差异,不存在 “近的更重要” 的默认规则。

由此可见,尽管形式上被组织成有序的“矩阵”,但是实际看到的是无序的“集合”,是在感知 token 内容集合,所以需要引入位置编码。

3.2 绝对位置编码的实现

绝对位置编码,通过给每个位置索引分配一个唯一的、可被模型识别的标识向量,将其与原始词向量逐元素相加,让输入的每一行同时包含“语义内容” 和 “位置身份”两部分信息。

绝对位置编码的生成只依赖当前 token 的全局绝对位置序号 i(0,1,2,…,n-1),和其他 token 的位置、和上下文语义都没有关系。即绝对位置编码。本质是一个整数ID(位置索引)到向量的映射。一般分为:正弦余弦绝对位置编码、可学习绝对位置编码。

  • 正弦余弦绝对位置编码: Transformer 原始论文《Attention Is All You Need》中提出的经典方案,没有可训练参数,不是靠学习得到的,其映射由固定的三角函数公式直接定义。具体算法参考教程即可(对不同的token向量,均进行仅与绝对位置序号 i有关的位置编码;而在同一向量编码内部,对于奇偶位置,计算公式不同)。
  • 可学习绝对位置编码:BERT、GPT-2 等模型采用的方案,与Embedding 层同理,是一个可学习的嵌⼊向量查找表。本质是一个可训练的参数矩阵,只不是不是通过token id查找,而是通过位置索引查找对应的向量。

不考虑编码的具体函数形式,绝对位置编码的使用如下:

其中,(谢谢豆包老师解答)

1. 项1:纯内容交互
这就是原本的相似度计算,完全由两个 token 的语义内容决定,和位置无关。没有位置编码时,注意力分数只有这一项。

2. 项2:第i个token的内容 × 第j个token的位置
模型可以学习到:当某个语义的 token 出现在第 j 个位置时,当前 token 对它的注意力该增强还是减弱。
例如:模型可以学到“动词对位于宾语位置的名词注意力更高”这类规律。

3. 项3:第i个token的位置 × 第j个token的内容
和项2对称,模型可以学习到:处于第 i 个位置的 token,倾向于关注某个语义的 token。
例如:“处于句首位置的 token,更关注句尾的标点/结论词”。

4. 项4:纯位置交互
这一项和 token 内容完全无关,只由 i 和 j 两个绝对位置决定。
模型可以学到纯粹的位置偏好,比如“相邻位置的 token 注意力更高”“第0个位置对所有位置的注意力都偏高”等固定位置模式。如果使用“正弦余弦绝对位置编码”,项4的纯位置交互还可以进一步化简为相对距离 i-j 的函数,这也是为什么固定的绝对位置编码能间接捕捉相对位置信息的数学根源。

使得注意力分数同时依赖两个 token 的内容和位置。

以“正弦余弦绝对位置编码”为例进行进一步的阐述:

对于位置索引 i 的token向量,对应的绝对位置编码在第 2k/2k+1 位置上的计算如下:

对于,位置索引 i=0,向量维度d=4,4个位置上分别 k = 0,0,1,1,因此, 绝对位置编码的4个位置上分别为:

3.3 绝对位置编码的原理

位置编码的原理,,即使是绝对位置编码的原理,,也可以单独拿出来仔细研究。。看教程上的可以,可以看这篇 [4]https://zhuanlan.zhihu.com/p/454482273

(主要我自己也看不太懂了已经。。)

4 输出头

输出头因为与输入部分的Embedding 层相关,所以放到这里吧。

对于解码器模块(Decoder),每一层输出的都是新的、信息融合后的多个token向量(token矩阵),不断更新向量表示,让其它包含更丰富的上下文、来自编码器(也叫跨模态)的信息。

解码器最后一层的输出,也是m个token向量(token矩阵,形状 m * d),然后送到输出头。如下图所示。

输出头的线性层:将每个 d 维的 token 向量,投影为一个 V 维(词表的大小)的原始分值(对数几率logits,形状 m * V )。

输出头的softmax层:对最后一维(V 维)做 softmax 运算,把所有分值压缩到 0~1 之间,且总和为 1,得到每个位置上所有 token 的概率分布(形状 m * V )。

序列中第 i 个位置的 V 维向量,就代表 “在该位置上,词表中每个 token 出现的概率”。

具体过程如下图所示:

最后得到了预测概率矩阵P,即对象A的m个位置,每个位置上是对下一个词的预测,表现为对整个词表的概率分布p。

5 预测概率矩阵P的使用

预测概率矩阵P就是Transformer神经网络部分的输出。在不同的Transformer变体下,以及不同阶段,预测概率矩阵P会有不同的用处。

对于训练阶段:

使用完整的矩阵,每一行的概率分布与对应位置的真实 token 标签计算交叉熵损失,用损失反向传播更新整个模型的参数,全程不取出具体 token id。

注意,这里的“对应位置的真实 token 标签 ”,在指的是对应位置的真实标签,是指对应位置的下一个真实token 标签,而非对应位置本位置的真实token 标签。token位置上的标签,是下一个token。

对于推理阶段:

一次生成一个 token,拼到对象A的序列末尾再生成下一个。

仅取矩阵的最后一行(代表下一个词的预测),通过解码策略(贪心搜索 / Beam Search/Top-k 采样等)从概率分布中选出 1 个 token 的 id(整数);

  • 贪心:直接选概率最大的那个 token id
  • 采样:按概率分布随机抽样

这个 token id 就是预测得到的下一个词。

一方面,拼接到对象A的token序列后面,通过词嵌入层(Embedding)查表,转成对应的 静态嵌入token向量,加位置编码继续生成;

另一方面,通过tokenizer分词器解码,得到token id所对应的自然语言,输出呈现。

  • 注1:编码/解码在不同语境下含义不同。对于tokenizer分词器而言,“编码”是把自然语言序列转换为token id序列,“解码”是把token id序列转换为自然语言序列。解码采样,“解码”是从概率分布中获取token id。编码器与解码器...单纯是习惯称呼沿用吧...
  • 注2:实际使用的时候,会使用批次batch,最前面多一个批次维度,原理叙述的时候省略掉了。
  • 注3:输出头的线性层,有时候会与Embedding 层进行权重绑定。核心是让「输入端词嵌入(id→向量)」和「输出端线性层(向量→logits)」共享同一套权重参数,尤其在小模型上,便于节省参数。本质是让 “词→向量” 和 “向量→词” 这一对互逆映射共用同一个语义空间,既大幅节省参数量,又能让语义表示更对齐。具体而言,输出层的权重,直接复用嵌入层权重的转置,嵌入层权重是(V个token id,每个是d维向量,尽管使用的方式是查表...),转置后刚好是(从d维映射到V维)。

[1]GitHub - datawhalechina/happy-llm: 📚 从零开始构建大模型 · GitHub

[2]人民日报 | “Token”中文名定了:词元-国家数据局

[3]https://zhuanlan.zhihu.com/p/13936916587

[4]https://zhuanlan.zhihu.com/p/454482273

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进