CS336 Lecture_01
这节课介绍了分词器 Tokenization 的作用和分类
Tokenization
intro_to_tokenization
分词化就是获取通常表示为 Unicode 字符串的原始文本,并将其本质上转化为一组整数,每个整数代表一个 token
我们需要一个程序 Tokenizer,可以将字符串编码为 token,再将它们解码回字符串
词汇表大小就是 token 值的数量,也就是整数范围的数量
注意点:空格也是 token 的一部分
压缩率 = 原始字节数 / 编码后的 token 数
character_tokenizer
基于字符的分词,一个 Unicode 字符串就是一个 Unicode 字符序列,并且每个字符都可以转化为一个整数,称为码点(code point)
这种分词方法的问题:词汇表非常大(基本上是为每个字符平均分配一个槽位),而有些字符出现频率远高与其他字符,对"预算"的利用不是很有效
# 每个字符都可以转化为一个码点,通过 `ord`
assert ord("a") == 97
assert ord("🌍") == 127757
# 还可以通过 `chr` 转化回去
assert chr(97) == "a"
assert chr(127757) == "🌍"
byte_tokenizer
Unicode 字符串可以用字节序列来表示,因为每个字符串都可以直接转化为字节
转化为字节后,所有的索引值都在 0~255 之间,因为一个字节一共有 2^8=256 个可能的值
词汇表较小,但是压缩比为 1,效率很低
# 一些 Unicode 字符用一个字节表示(b 表示 byte)
assert bytes("a", encoding="utf-8") == b"a"
# 其他的字符需要多个字节表示
assert bytes("🌍", encoding="utf-8") == b"\xf0\x9f\x8c\x8d"
word_tokenizer
既然我们不能给每一个 token 分配一个字符或一个字节,也许有些 token 可以代表很多字节,有些 token 可以代表很少字节
于是想到将字符串分割成一个片段序列,给每个 token 分配一个整数
问题在于不知道词汇表的上限,对于一个新的输入,就可能得到一个以前从未见过的片段,而新词必须获得 UNK 标记
bpe_tokenizer
基本思想:不是预先设定好如何分割的概念,而是在原始文本上训练分词器
越常见的字符序列尝试表示为一个 token,罕见的序列表示为多个 token
BPE算法:先将字符串转换为字节序列,然后重复地、连续地合并最常见的相邻 token 对(并在词汇表中加入,从 256 开始)
def train_bpe(string: str, num_merges: int) -> BPETokenizerParams:
indices = list(map(int, string.encode("utf-8")))
merges: dict[tuple[int, int], int] = {}
vocab: dict[int, bytes] = {x: bytes([x]) for x in range(256)}
for i in range(num_merges):
# 计数
counts = defaultdict(int)
for index1, index2 in zip(indices, indices[1:]):
counts[(index1, index2)] += 1
# 查找
pair = max(counts, key=counts.get)
index1, index2 = pair
# 合并
new_index = 256 + i
merges[pair] = new_index
vocab[new_index] = vocab[index1] + vocab[index2]
indices = merge(indices, pair, new_index)
return BPETokenizerParams(vocab=vocab, merges=merges)
更多推荐
所有评论(0)