由于神经网络模型不能直接处理文本,因此我们需要先将文本转换为数字,这个过程被称为编码 (Encoding),其包含两个步骤:
- 使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens;
- 将所有的 token 映射到对应的 token ID。
分词策略
根据切分粒度的不同,分词策略可以分为以下几种:
按词切分 (Word-based)
特点
- 以空格、标点符号为界,将文本分割为单词。
- 每个单词被视为一个独立的 token。
优点:
- 实现简单,分词器只需根据空格或标点分割。
缺点:
- 对于形态变化的词(如
run
和running
)无法识别它们之间的关系。 - 分词表会很大,可能导致内存占用过多。
- 遇到分词表中没有的词(Out-Of-Vocabulary,OOV),分词器会用特殊的
[UNK]
token 表示,影响模型效果。
例子
直接利用 Python 的 split()
函数按空格进行分词,其默认分隔符为空格:
tokenized_text = "Jim Henson was a puppeteer".split() print(tokenized_text)
结果:
['Jim', 'Henson', 'was', 'a', 'puppeteer']
问题:词形变化和未知词
- 词形变化问题:
- 示例:
dog
和dogs
、run
和running
,被分词器视为不同的 token,无法识别它们的词根关系。
- 示例:
- 未知词问题:
- 如果单词不在词汇表中(Out-Of-Vocabulary, OOV),分词器会用
[UNK]
(Unknown Token)表示。 - 问题:
[UNK]
会丢失单词原始的语义信息。- 如果分词策略不够好,句子中会有大量
[UNK]
。
- 如果单词不在词汇表中(Out-Of-Vocabulary, OOV),分词器会用
词表(Vocabulary)
什么是词表?
词表: 一个映射字典,将 token 映射到数字 ID(从 0 开始)。
- 示例:
{'Jim': 100, 'Henson': 101, 'was': 102, 'a': 103, 'puppeteer': 104}
词表的作用
- 将 token 转换为对应的数字 ID。
- 神经网络只能处理数字,不能直接理解字符串。
遇到 OOV 的处理
- 如果分词结果中出现词表中没有的单词(如
puppeteering
),分词器会用[UNK]
替代。
按字符切分 (Character-based)
这种策略把文本切分为字符(字母)而不是词语,这样就只会产生一个非常小的词表,并且很少会出现词表外的 tokens。
可以使用 Python 的内置函数 list()
将输入字符串转换为字符列表。即
但是从直觉上来看,字符本身并没有太大的意义,因此将文本切分为字符之后就会变得不容易理解。这也与语言有关,例如中文字符会比拉丁字符包含更多的信息,相对影响较小。此外,这种方式切分出的 tokens 会很多,例如一个由 10 个字符组成的单词就会输出 10 个 tokens,而实际上它们只是一个词。
因此现在广泛采用的是一种同时结合了按词切分和按字符切分的方式——按子词切分 (Subword tokenization)。
按子词切分(Subword Tokenization)
子词切分是一种分词策略,能够将输入文本分解为更小的单元(子词或子字符串),其主要特点如下:
核心概念
-
高频单词直接保留,例如单词
do
不会被切分。 -
低频单词会被分解为多个子词(subword)。
-
示例:
tokenization
被分解为token
和ization
。-
子词带有特殊标记(如
<w>
或##
),表示这是子词的一部分。
-
优点
1. 解决 OOV(Out-of-Vocabulary)问题:
- 即使一个单词在词表中不存在,也可以通过子词组合得到。
- 例如,
ization
可能是低频词,但可以通过token
和ization
的组合表示。
2. 减小词表大小:
- 子词分词只需一个小词表即可覆盖大部分文本,避免了巨大的内存开销。
3. 保留词义:
- 子词保留了部分语义信息,例如
token
和ization
分别表示单词的前缀和后缀。
例子
我们可以使用 Hugging Face 的 Transformers 库中的分词器(Tokenizer)实现子词切分。例如,WordPiece
或 Byte Pair Encoding (BPE)
是常用的子词分词算法。
代码如下:
from transformers import AutoTokenizer
# 加载预训练分词器
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
# 测试文本
sample_text = "Let's do tokenization!"
# 使用分词器进行子词切分
tokens = tokenizer.tokenize(sample_text)
# 打印结果
print("Original Text:", sample_text)
print("Subword Tokens:", tokens)
运行结果:
Original Text: Let's do tokenization!
Subword Tokens: ['let', "'", 's', 'do', 'token', '##ization', '!']
代码解析
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
加载 BERT 模型对应的分词器,默认使用 WordPiece 分词算法。
结果解释
let
和's
被分解为两个独立的 token。tokenization
被分解为两个子词:token
和##ization
。##
表示这是一个后续子词,属于前一个单词的一部分。
文本编码与解码
本内容展示了 Hugging Face 的 AutoTokenizer
对文本进行编码和解码的过程,分别涉及以下关键步骤。
编码过程
如前所述,文本编码 (Encoding) 过程包含两个步骤:
- 分词:使用分词器按某种策略将文本切分为 tokens;
- 映射:将 tokens 转化为对应的 token IDs。
下面我们
编码示例
1. 首先使用 BERT 分词器来对文本进行分词。
代码如下:
from transformers import AutoTokenizer
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# 输入文本
sequence = "Using a Transformer network is simple"
# 编码 - 分词
tokens = tokenizer.tokenize(sequence)
print(tokens) # 打印分词后的 tokens
输出:
['using', 'a', 'transform', '##er', 'network', 'is', 'simple']
分词细节:
- 使用子词分词策略(如 WordPiece)。
- 将低频单词(如
Transformer
)分解为transform
和##er
。 ##
表示子词为前一个 token 的一部分。
2. 然后,我们通过 convert_tokens_to_ids()
将切分出的 tokens 转换为对应的 token IDs。
代码如下:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
输出:
[7993, 170, 13809, 23763, 2443, 1110, 3014]
每个 token 被映射为一个整数 ID,表示其在词表中的索引。
3. 还可以通过 encode()
函数将这两个步骤合并,并且 encode()
会自动添加模型需要的特殊 token,例如 BERT 分词器会分别在序列的首尾添加 [CLS] 和 [SEP]:
# 使用 encode() 方法直接编码文本
sequence_ids = tokenizer.encode(sequence)
print(sequence_ids)
输出:
[101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]
新增的特殊 token:
101
:表示 [CLS](分类起始符)。102
:表示 [SEP](分隔符)。
注意,上面这些只是为了演示。在实际编码文本时,最常见的是直接使用分词器进行处理,这样不仅会返回分词后的 token IDs,还包含模型需要的其他输入。例如 BERT 分词器还会自动在输入中添加 token_type_ids
和 attention_mask.
4. BERT 的完整编码信息
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]
}
字段说明:
input_ids
:文本对应的 token IDs,包含 [CLS] 和 [SEP]。token_type_ids
:区分不同段落的标记,BERT 多段输入时用到。attention_mask
:掩码标记,1 表示有效的 token,0 表示 padding。
解码过程
文本解码 (Decoding) 与编码相反,负责将 token IDs 转换回原来的字符串。注意,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分为多个 token 的单词。
代码:
decoded_string = tokenizer.decode([7993, 170, 13809, 23763, 2443, 1110, 3014])
print(decoded_string)
输出:
Using a transformer network is simple
解码带特殊 token 的 ID:
decoded_string = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102])
print(decoded_string)
输出:
[CLS] Using a Transformer network is simple [SEP]
总结
-
编码过程:
tokenize()
分词,将文本拆分为子词 token。convert_tokens_to_ids()
将 token 转换为对应的 token IDs。encode()
是以上两步的封装,并自动添加特殊 token(如 [CLS] 和 [SEP])。
-
解码过程:
decode()
将 token IDs 转换回原始文本,支持忽略特殊 token。
-
重要参数:
input_ids
:模型的实际输入。attention_mask
:表示 token 的有效性。token_type_ids
:区分输入段落的标记。