Transformer 前置知识 07:分词与词嵌入

前面几课,我们一直默认:每个词都是一个向量。可模型拿到的,明明是一串文字。

文字是怎么变成向量的?分两步:先分词,再查表。


第一步:分词

分词(Tokenization),就是把一句话切成一个个小块,每一块叫做一个 token。

最简单的办法有两种:

切法 “我爱吃苹果” 问题
按字切 我 / 爱 / 吃 / 苹 / 果 句子变得很长,一本书就是几十万个 token
按词切 我 / 爱 / 吃 / 苹果 词表太大;遇到没见过的新词(比如”内卷”),就不认识了

两种都不理想。

子词:常见的整块,少见的拆开

所以现代模型用的是子词(Subword):

  • 常见的词保持完整,比如 the、cat、happy 各是一个 token
  • 少见的词拆成几块,比如 unhappiness 可以拆成:

un + happi + ness

这样,词表只要几万个 token,就能拼出几乎任何文字,包括从没见过的新词。

BPE:拆法是怎么定的

常用的方法叫 BPE(Byte Pair Encoding,字节对编码)。思路很简单:

  1. 从单个字母开始
  2. 统计哪两块最常挨在一起,就把它们合并成一块
  3. 重复第 2 步

用 low、lower、lowest 三个词举例:

步骤 合并 low lower lowest
开始 l o w l o w e r l o w e s t
第 1 次 l + o → lo lo w lo w e r lo w e s t
第 2 次 lo + w → low low low e r low e s t

反复合并几万次,就得到了词表。常见的组合会被合并成大块,少见的就停留在小块。

想看真实的分词结果,可以用 OpenAI 的 tiktoken:

1
2
3
4
5
6
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")
ids = enc.encode("unhappiness")
print(ids) # 一串编号
print([enc.decode([i]) for i in ids]) # 每个编号对应的文字块

不同模型的词表不同,拆法也不完全一样。

token ID:编号只是名字

分完词,每个 token 在词表里都有一个编号,叫 token ID。比如”我爱猫”会变成三个编号。

但要注意:

编号只是一个名字,大小没有意义。

7 号和 8 号,并不比 7 号和 100 号更像。就像学号 7 和学号 8 的两个同学,不见得比学号 7 和学号 100 的更像。

所以不能直接把编号送进模型,还需要第二步。

第二步:词嵌入(查表)

词嵌入(Embedding):准备一张大表,叫嵌入矩阵:

  • 行数 = 词表大小(比如 5 万)
  • 列数 = 向量维度(比如 512)
  • 每一行就是一个 token 的向量

拿着编号,去表里取出对应的那一行,就得到了这个 token 的向量。

1
2
3
4
5
6
7
8
import numpy as np

vocab_size, dim = 50000, 512
E = np.random.randn(vocab_size, dim) # 嵌入矩阵(一开始是随机的)

ids = [2769, 4263, 9318] # 示意编号
vectors = E[ids] # 查表:取出 3 行
print(vectors.shape) # (3, 512)

嵌入是学出来的

这张表一开始是随机的,和神经网络的权重一样,在训练中一点点学出来。

学完之后,意思相近的词,向量也会靠得很近:

训练后,意思相近的词聚在一起(示意)

这正是第 1 课讲的:两个向量越像,点积越大。

考考你

词表有 50,000 个 token,每个向量是 512 维,嵌入矩阵一共有多少个参数?

答案: ,约 2560 万个。光是这一张表,就是一个不小的参数量。

小结

  1. 分词:把文字切成 token,现代模型用子词,常见方法是 BPE
  2. 每个 token 有一个编号,但编号大小没有意义
  3. 词嵌入:用编号去嵌入矩阵里取出一行,就是这个 token 的向量
  4. 嵌入矩阵是训练学出来的:意思相近的词,向量也相近

下一课讲位置编码:模型怎么知道每个词排在第几个。