5003.大模型-架构-transformer-3.tokenizer

由于神经网络模型不能直接处理文本,因此我们需要先将文本转换为数字,这个过程被称为编码 (Encoding),其包含两个步骤:

  1. 使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens;
  2. 将所有的 token 映射到对应的 token ID。

分词策略

根据切分粒度的不同,分词策略可以分为以下几种:

按词切分 (Word-based)

例如直接利用 Python 的 split() 函数按空格进行分词:

1
2
3
4
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)

['Jim', 'Henson', 'was', 'a', 'puppeteer']

这种策略的问题是会将文本中所有出现过的独立片段都作为不同的 token ,从而产生巨大的词表。而实际上很多词是相关的,例如 “dog” 和 “dogs”、“run” 和 “running”,如果给它们赋予不同的编号就无法表示出这种关联性。

词表就是一个映射字典,负责将 token 映射到对应的 ID(从 0 开始)。神经网络模型就是通过这些 token ID 来区分每一个 token。

当遇到不在词表中的词时,分词器会使用一个专门的 [UNK] token 来表示它是 unknown 的。显然,如果分词结果中包含很多 [UNK] 就意味着丢失了很多文本信息,因此一个好的分词策略,应该尽可能不出现 unknown token。

按字符切分 (Character-based)

character_based_tokenization

这种策略把文本切分为字符而不是词语,这样就只会产生一个非常小的词表,并且很少会出现词表外的 tokens

但是从直觉上来看,字符本身并没有太大的意义,因此将文本切分为字符之后就会变得不容易理解。这也与语言有关,例如中文字符会比拉丁字符包含更多的信息,相对影响较小。此外,这种方式切分出的 tokens 会很多,例如一个由 10 个字符组成的单词就会输出 10 个 tokens,而实际上它们只是一个词。

因此现在广泛采用的是一种同时结合了按词切分和按字符切分的方式——按子词切分 (Subword tokenization)。

按子词切分 (Subword)

高频词直接保留,低频词被切分为更有意义的子词。例如 “annoyingly” 是一个低频词,可以切分为 “annoying” 和 “ly”,这两个子词不仅出现频率更高,而且词义也得以保留。下图展示了对 “Let’s do tokenization!“ 按子词切分的结果:
bpe_subword

可以看到,“tokenization” 被切分为了 “token” 和 “ization”,不仅保留了语义,而且只用两个 token 就表示了一个长词。这种策略只用一个较小的词表就可以覆盖绝大部分文本,基本不会产生 unknown token。尤其对于土耳其语等黏着语,几乎所有的复杂长词都可以通过串联多个子词构成。

加载与保存分词器

分词器的加载与保存与模型相似,使用 Tokenizer.from_pretrained()Tokenizer.save_pretrained() 函数。例如加载并保存 BERT 模型的分词器:

1
2
3
4
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
tokenizer.save_pretrained("./models/bert-base-cased/")

同样地,在大部分情况下我们都应该使用 AutoTokenizer 来加载分词器:

1
2
3
4
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenizer.save_pretrained("./models/bert-base-cased/")

调用 Tokenizer.save_pretrained() 函数会在保存路径下创建三个文件:

  • special_tokens_map.json:映射文件,里面包含 unknown token 等特殊字符的映射关系;
  • tokenizer_config.json:分词器配置文件,存储构建分词器需要的参数;
  • vocab.txt:词表,一行一个 token,行号就是对应的 token ID(从 0 开始)。

编码与解码文本

前面说过,文本编码 (Encoding) 过程包含两个步骤:

  • 分词:使用分词器按某种策略将文本切分为 tokens;
  • 映射:将 tokens 转化为对应的 token IDs。

下面我们首先使用 BERT 分词器来对文本进行分词:

1
2
3
4
5
6
7
8
9
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)
['using', 'a', 'transform', '##er', 'network', 'is', 'simple']

可以看到,BERT 分词器采用的是子词切分策略,它会不断切分词语直到获得词表中的 token,例如 “transformer” 会被切分为 “transform” 和 “##er”。

然后,我们通过 convert_tokens_to_ids() 将切分出的 tokens 转换为对应的 token IDs:

1
2
3
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 13809, 23763, 2443, 1110, 3014]

还可以通过 encode() 函数将这两个步骤合并,并且 encode() 会自动添加模型需要的特殊 token,例如 BERT 分词器会分别在序列的首尾添加 [CLS][SEP]

1
2
3
4
5
6
7
8
9
10
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
sequence_ids = tokenizer.encode(sequence)

print(sequence_ids)

[101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]

其中 101 和 102 分别是 [CLS][SEP] 对应的 token IDs。

注意,上面这些只是为了演示。在实际编码文本时,最常见的是直接使用分词器进行处理,这样不仅会返回分词后的 token IDs,还包含模型需要的其他输入。例如 BERT 分词器还会自动在输入中添加 token_type_idsattention_mask:

1
2
3
4
5
6
7
8
9
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenized_text = tokenizer("Using a Transformer network is simple")
print(tokenized_text)

{'input_ids': [101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

文本解码 (Decoding) 与编码相反,负责将 token IDs 转换回原来的字符串。注意,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分为多个 token 的单词。下面我们通过 decode() 函数解码前面生成的 token IDs

1
2
3
4
5
6
7
8
9
10
11
12
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)

decoded_string = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102])
print(decoded_string)

Using a transformer network is simple
[CLS] Using a Transformer network is simple [SEP]

解码文本是一个重要的步骤,在进行文本生成、翻译或者摘要等 Seq2Seq (Sequence-to-Sequence) 任务时都会调用这一函数。

-------------本文结束感谢您的阅读-------------