目录
关于填充(padding)和截断(truncation)的所有信息
前言
在本教程中,我们将探讨如何使用 Transformers来预处理数据,主要使用的工具称为tokenizer。 tokenizer可以与特定的模型关联的tokenizer类来创建,也可以直接使用AutoTokenizer类来创建。
正如我在素轻:HuggingFace | 一起玩预训练语言模型吧中写到的那样,tokenizer首先将给定的文本拆分为通常称为tokens的单词(或单词的一部分,标点符号等,在中文里可能就是词或字,根据模型的不同拆分算法也不同)。然后tokenizer能够将tokens转换为数字,以便能够构建张量并输入到模型中。当然了,大多数预训练语言模型都需要额外的tokens才能作为一次正常的输入(例如,BERT中的[CLS]),这些都会由tokenizer自动完成。
要自动下载在特定模型在预训练或微调期间使用的vocab,可以使用from_pretrained()方法:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
基本用法
PreTrainedTokenizer有很多方法,但是你需要记住的唯一方法是它的__call__:你只需要将文本序列提供给tokenizer对象即可:
encoded_input = tokenizer("Hello, I'm a single sentence!")
print(encoded_input)
# {'input_ids': [101, 138, 18696, 155, 1942, 3190, 1144, 1572, 13745, 1104, 159, 9664, 2107, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
返回的是一个字典,里面的列表包含了int类别的数据。其中:
- “input_id”是对应于文本序列中每个token的索引(在vocab中的索引);
- “attention_mask”是对应于注意力机制的计算,各元素的值为0或1,如果当前token被mask或者是只是用来作为填充的元素,那么其不需要进行注意力机制的计算,其值为0;
- “token_type_ids”是对应于不同的文本序列,例如在NSP(BERT及某些语言模型中的“Next Sentence Prediction”)任务中需要输入两个文本序列。
当然,如果输入的是索引,tokenizer可以进行反向解码:
tokenizer.decode(encoded_input["input_ids"])
# "[CLS] Hello, I'm a single sentence! [SEP]"
上面解码的[CLS]字符就是大多数预训练语言模型会自动加入的特殊token。tokenizer会自动添加了模型期望的一些特殊token。但是并非所有模型都需要特殊token。例如,如果我们使用gpt2-medium来创建tokenizer,那么解码后的文本序列不会有特殊的token了。你可以通过传递add_special_tokens = False来禁用加入特殊token(仅当你自己添加了这些特殊token时才建议这样做)。
如果要处理多个文本序列,则可以通过将它们作为列表输入到tokenizer:
batch_sentences = ["Hello I'm a single sentence",
"And another sentence",
"And the very very last one"]
encoded_inputs = tokenizer(batch_sentences)
print(encoded_inputs)
# {'input_ids': [[101, 8667, 146, 112, 182, 170, 1423, 5650, 102],
# [101, 1262, 1330, 5650, 102],
# [101, 1262, 1103, 1304, 1304, 1314, 1141, 102]],
# 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0]],
# 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1]]}
当然了,在神经网络中,我们常常是通过一个batch的形式来作为一次输入,这个时候你可能想要:
- 如果必要,将每个文本序列填充到最大的长度;
- 如果必要,将每个文本序列截断到模型可以接受的最大长度;
- 返回张量。
将文本序列列表提供给tokenizer时,可以使用以下选项来完成所有这些操作(即设置padding=True, truncation=True, return_tensors="pt"):
注:这里是以PyTorch的tensor形式返回,如果需要以TensorFlow tensor形式返回,只需定义为:return_tensors="pt"。具体参见原文档。
batch = tokenizer(batch_sentences, padding=True, truncation=True, return_tensors="pt")
print(batch)
# {'input_ids': tensor([[ 101, 8667, 146, 112, 182, 170, 1423, 5650, 102],
# [ 101, 1262, 1330, 5650, 102, 0, 0, 0, 0],
# [ 101, 1262, 1103, 1304, 1304, 1314, 1141, 102, 0]]),
# 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0]]),
# 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 0, 0, 0, 0],
# [1, 1, 1, 1, 1, 1, 1, 1, 0]])}
在输出中可以看到,如果是填充的元素,其attention_mask中对应的位置即为0。
预处理文本序列对
有时你需要为模型提供一对文本。例如,如果你要分类成对的两个句子是否相似,或者是在阅读理解的问题中,需要同时输入上下文和问题。对于BERT模型,输入表示如下:[CLS]序列A [SEP]序列B [SEP]。
您可以通过将两个文本序列作为两个模型期望的参数来提供(这不是一个列表,因为两个文本序列的列表将会被模型理解为一个batch中的两个单个文本序列,就像我们之前看到的那样):
encoded_input = tokenizer("How old are you?", "I'm 6 years old")
print(encoded_input)
# {'input_ids': [101, 1731, 1385, 1132, 1128, 136, 102, 146, 112, 182, 127, 1201, 1385, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
可以看到这里面token_type_ids的值就是上面所解释的那样。
同样地,如果我们对获得的token ID进行解码,则会看到已正确文本序列对:
tokenizer.decode(encoded_input["input_ids"])
# "[CLS] How old are you? [SEP] I'm 6 years old [SEP]"
如果要处理的是文本序列对列表,则应将它们作为两个列表提供给tokenizer:第一个所有文本序列的列表和第二个所有文本序列的列表:
batch_sentences = ["Hello I'm a single sentence",
"And another sentence",
"And the very very last one"]
batch_of_second_sentences = ["I'm a sentence that goes with the first sentence",
"And I should be encoded with the second sentence",
"And I go with the very last one"]
encoded_inputs = tokenizer(batch_sentences, batch_of_second_sentences)
print(encoded_inputs)
# {'input_ids': [[101, 8667, 146, 112, 182, 170, 1423, 5650, 102, 146, 112, 182, 170, 5650, 1115, 2947, 1114, 1103, 1148, 5650, 102],
# [101, 1262, 1330, 5650, 102, 1262, 146, 1431, 1129, 12544, 1114, 1103, 1248, 5650, 102],
# [101, 1262, 1103, 1304, 1304, 1314, 1141, 102, 1262, 146, 1301, 1114, 1103, 1304, 1314, 1141, 102]],
#'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
#'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}
当然你仍然可以反向解码:
for ids in encoded_inputs["input_ids"]:
print(tokenizer.decode(ids))
# [CLS] Hello I'm a single sentence [SEP] I'm a sentence that goes with the first sentence [SEP]
# [CLS] And another sentence [SEP] And I should be encoded with the second sentence [SEP]
# [CLS] And the very very last one [SEP] And I go with the very last one [SEP]
关于填充(padding)和截断(truncation)的所有信息
三个参数padding
,truncation
和max_length
将做进一步的介绍
- padding用于填充。它的参数可以是布尔值或字符串:
- True或”longest“:填充到最长序列(如果你仅提供单个序列,则不会填充);
- “max_length”:用于指定你想要填充的最大长度,如果max_length=Flase,那么填充到模型能接受的最大长度(这样即使你只输入单个序列,那么也会被填充到指定长度);
- False或“do_not_pad”:不填充序列。如前所述,这是默认行为。
2. truncation用于截断。它的参数可以是布尔值或字符串:
- 如果为True或“only_first”,则将其截断为max_length参数指定的最大长度,如果未提供max_length = None,则模型会截断为模型接受的最大长度。如果提供的是一对文本序列,则只会截断这一对中的第一个文本序列(因为参数“only_first”),如果参数是“only_second”,则只会截断这一对中的第二个文本序列;
- “longest_first”截断为max_length参数指定的最大长度,如果max_length = None,则截断到模型接受的最大长度;
- False或“do_not_truncate”不截断序列。如前所述,这是默认行为。
下表总结了设置填充和截断的推荐方法:
Pre-tokenized输入
tokenizer还接受pre-tokenized的输入。在命名实体识别(NER)或词性标记(POS)中计算标签并提取预测时,此功能特别有用。
注意:Pre-tokenized并不意味着输入已被tokenized(如果是这种情况,则无需将它们传递给tokenizer了),而只是拆分为多个单词(这通常是subword tokenization算法(如BPE)的第一步)。例如,在下面这个代码中,输入的token为5个,而实际上输入的token索引为9个,这说明了其内部仍需要进行tokenized。
如果要使用pre-tokenized的输入,则在将输入传递给tokenizer时只需设置is_split_into_words=True即可。例如:
encoded_input = tokenizer(["Hello", "I'm", "a", "single", "sentence"], is_split_into_words=True)
print(encoded_input)
# {'input_ids': [101, 8667, 146, 112, 182, 170, 1423, 5650, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
请注意,除非传递add_special_tokens = False,否则tokenizer仍会添加特殊token的ID(如果适用)
当然,你可以和前面一样输入文本序列对之类的,或者添加填充和截断的参数。
batch = tokenizer(batch_sentences,
batch_of_second_sentences,
is_split_into_words=True,
padding=True,
truncation=True,
return_tensors="pt")