ftp怎么上传网站,中国工商建设标准化协会网站,福州网站设计哪家好,科技苑1 模型
除了像之前使用 AutoModel 根据 checkpoint 自动加载模型以外#xff0c;我们也可以直接使用模型对应的 Model 类#xff0c;例如 BERT 对应的就是 BertModel#xff1a;
from transformers import BertModel
model BertModel.from_pretrained(bert-base-ca…1 模型
除了像之前使用 AutoModel 根据 checkpoint 自动加载模型以外我们也可以直接使用模型对应的 Model 类例如 BERT 对应的就是 BertModel
from transformers import BertModel
model BertModel.from_pretrained(bert-base-cased)注意在大部分情况下我们都应该使用 AutoModel 来加载模型。这样如果我们想要使用另一个模型比如把 BERT 换成 RoBERTa只需修改 checkpoint其他代码可以保持不变。
1.1 加载模型
所有存储在 HuggingFace Model Hub 上的模型都可以通过 Model.from_pretrained() 来加载权重参数可以像上面一样是 checkpoint 的名称也可以是本地路径预先下载的模型目录例如
from transformers import BertModelmodel BertModel.from_pretrained(./models/bert/)Model.from_pretrained() 会自动缓存下载的模型权重默认保存到 ~/.cache/huggingface/transformers我们也可以通过 HF_HOME 环境变量自定义缓存目录。 由于 checkpoint 名称加载方式需要连接网络因此在大部分情况下我们都会采用本地路径的方式加载模型。 部分模型的 Hub 页面中会包含很多文件我们通常只需要下载模型对应的 config.json 和 pytorch_model.bin以及分词器对应的 tokenizer.json、tokenizer_config.json 和 vocab.txt。 1.2 保存模型
保存模型通过调用 Model.save_pretrained() 函数实现例如保存加载的 BERT 模型
from transformers import AutoModelmodel AutoModel.from_pretrained(bert-base-cased)
model.save_pretrained(./models/bert-base-cased/)这会在保存路径下创建两个文件
config.json模型配置文件存储模型结构参数例如 Transformer 层数、特征空间维度等pytorch_model.bin又称为 state dictionary存储模型的权重。
简单来说配置文件记录模型的结构模型权重记录模型的参数这两个文件缺一不可。我们自己保存的模型同样通过 Model.from_pretrained() 加载只需要传递保存目录的路径。
2 分词器
由于神经网络模型不能直接处理文本因此我们需要先将文本转换为数字这个过程被称为编码 (Encoding)其包含两个步骤
使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens将所有的 token 映射到对应的 token ID。
2.1 分词策略
根据切分粒度的不同分词策略可以分为以下几种 按词切分 (Word-based) 例如直接利用 Python 的 split() 函数按空格进行分词 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] \texttt{[UNK]} [UNK] token 来表示它是 unknown 的。显然如果分词结果中包含很多 [UNK] \texttt{[UNK]} [UNK] 就意味着丢失了很多文本信息因此一个好的分词策略应该尽可能不出现 unknown token。 按字符切分 (Character-based) 这种策略把文本切分为字符而不是词语这样就只会产生一个非常小的词表并且很少会出现词表外的 tokens。 但是从直觉上来看字符本身并没有太大的意义因此将文本切分为字符之后就会变得不容易理解。这也与语言有关例如中文字符会比拉丁字符包含更多的信息相对影响较小。此外这种方式切分出的 tokens 会很多例如一个由 10 个字符组成的单词就会输出 10 个 tokens而实际上它们只是一个词。 因此现在广泛采用的是一种同时结合了按词切分和按字符切分的方式——按子词切分 (Subword tokenization)。 按子词切分 (Subword) 高频词直接保留低频词被切分为更有意义的子词。例如 “annoyingly” 是一个低频词可以切分为 “annoying” 和 “ly”这两个子词不仅出现频率更高而且词义也得以保留。下图展示了对 “Let’s do tokenization!“ 按子词切分的结果 可以看到“tokenization” 被切分为了 “token” 和 “ization”不仅保留了语义而且只用两个 token 就表示了一个长词。这种策略只用一个较小的词表就可以覆盖绝大部分文本基本不会产生 unknown token。尤其对于土耳其语等黏着语几乎所有的复杂长词都可以通过串联多个子词构成。
2.2 加载与保存分词器
分词器的加载与保存与模型相似使用 Tokenizer.from_pretrained() 和 Tokenizer.save_pretrained() 函数。例如加载并保存 BERT 模型的分词器
from transformers import BertTokenizertokenizer BertTokenizer.from_pretrained(bert-base-cased)
tokenizer.save_pretrained(./models/bert-base-cased/)同样地在大部分情况下我们都应该使用 AutoTokenizer 来加载分词器
from transformers import AutoTokenizertokenizer 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 开始。
2.3 编码与解码文本
文本编码 (Encoding) 过程包含两个步骤
分词使用分词器按某种策略将文本切分为 tokens映射将 tokens 转化为对应的 token IDs。
下面我们首先使用 BERT 分词器来对文本进行分词
from transformers import AutoTokenizertokenizer AutoTokenizer.from_pretrained(bert-base-cased)sequence Using a Transformer network is simple
tokens tokenizer.tokenize(sequence)print(tokens)# [Using, a, Trans, ##former, network, is, simple]可以看到BERT 分词器采用的是子词切分策略它会不断切分词语直到获得词表中的 token例如 “transformer” 会被切分为 “transform” 和 “##er”。
然后我们通过 convert_tokens_to_ids() 将切分出的 tokens 转换为对应的 token IDs
ids tokenizer.convert_tokens_to_ids(tokens)print(ids)# [7993, 170, 13809, 23763, 2443, 1110, 3014]还可以通过 encode() 函数将这两个步骤合并并且 encode() 会自动添加模型需要的特殊token例如 BERT 分词器会分别在序列的首尾添加 [CLS] \texttt{[CLS]} [CLS] 和 [SEP] \texttt{[SEP]} [SEP]
from transformers import AutoTokenizertokenizer 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] \texttt{[CLS]} [CLS] 和 [SEP] \texttt{[SEP]} [SEP] 对应的 token IDs。
注意上面这些只是为了演示。在实际编码文本时最常见的是直接使用分词器进行处理这样不仅会返回分词后的 token IDs还包含模型需要的其他输入。例如 BERT 分词器还会自动在输入中添加token_type_ids 和 attention_mask
from transformers import AutoTokenizertokenizer 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
from transformers import AutoTokenizertokenizer 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) 任务时都会调用这一函数。
3 处理多段文本
现实场景中我们往往会同时处理多段文本而且模型也只接受批 (batch) 数据作为输入即使只有一段文本也需要将它组成一个只包含一个样本的 batch例如
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence Ive been waiting for a HuggingFace course my whole life.tokens tokenizer.tokenize(sequence)
ids tokenizer.convert_tokens_to_ids(tokens)
# input_ids torch.tensor(ids), This line will fail.
input_ids torch.tensor([ids])
print(Input IDs:\n, input_ids)output model(input_ids)
print(Logits:\n, output.logits)# Input IDs:
# tensor([[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607,
# 2026, 2878, 2166, 1012]])
# Logits:
# tensor([[-2.7276, 2.8789]], grad_fnAddmmBackward0)这里我们通过 [ids] 构建了一个只包含一段文本的 batch更常见的是送入包含多段文本的 batch
batched_ids [ids, ids, ids, ...]注意上面的代码仅作为演示。实际场景中我们应该直接使用分词器对文本进行处理例如对于上面的例子
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence Ive been waiting for a HuggingFace course my whole life.tokenized_inputs tokenizer(sequence, return_tensorspt)
print(Inputs Keys:\n, tokenized_inputs.keys())
print(\nInput IDs:\n, tokenized_inputs[input_ids])output model(**tokenized_inputs)
print(\nLogits:\n, output.logits)# Inputs Keys:
# dict_keys([input_ids, attention_mask])# Input IDs:
# tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
# 2607, 2026, 2878, 2166, 1012, 102]])# Logits:
# tensor([[-1.5607, 1.6123]], grad_fnAddmmBackward0)可以看到分词器输出的结果中不仅包含 token IDsinput_ids还会包含模型需要的其他输入项。前面我们之所以只输入 token IDs 模型也能正常运行是因为它自动地补全了其他的输入项例如 attention_mask 等。 由于分词器自动在序列的首尾添加了 [CLS] \texttt{[CLS]} [CLS] 和 [SEP] \texttt{[SEP]} [SEP] token所以上面两个例子中模型的输出是有差异的。因为 DistilBERT 预训练时是包含 [CLS] \texttt{[CLS]} [CLS] 和 [SEP] \texttt{[SEP]} [SEP] 的所以下面的例子才是正确的使用方法。 3.1 Padding 操作
按批输入多段文本产生的一个直接问题就是batch 中的文本有长有短而输入张量必须是严格的二维矩形维度为 ( batch size , sequence length ) (\text{batch size}, \text{sequence length}) (batch size,sequence length)即每一段文本编码后的 token IDs 数量必须一样多。例如下面的 ID 列表是无法转换为张量的
batched_ids [[200, 200, 200],[200, 200]
]模型的 padding token ID 可以通过其分词器的 pad_token_id 属性获得。下面我们尝试将两段文本分别以独立以及 batch 的形式送入到模型中
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence1_ids [[200, 200, 200]]
sequence2_ids [[200, 200]]
batched_ids [[200, 200, 200],[200, 200, tokenizer.pad_token_id],
]print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
print(model(torch.tensor(batched_ids)).logits)# tensor([[ 1.5694, -1.3895]], grad_fnAddmmBackward0)
# tensor([[ 0.5803, -0.4125]], grad_fnAddmmBackward0)
# tensor([[ 1.5694, -1.3895],
# [ 1.3374, -1.2163]], grad_fnAddmmBackward0)问题出现了使用 padding token 填充的序列的结果竟然与其单独送入模型时不同 这是因为模型默认会编码输入序列中的所有 token 以建模完整的上下文因此这里会将填充的 padding token 也一同编码进去从而生成不同的语义表示。 因此在进行 Padding 操作时我们必须明确告知模型哪些 token 是我们填充的它们不应该参与编码。这就需要使用到 Attention Mask 了。
3.2 Attention Mask
Attention Mask 是一个尺寸与 input IDs 完全相同且仅由 0 和 1 组成的张量0 表示对应位置的 token 是填充符不参与计算。当然一些特殊的模型结构也会借助 Attention Mask 来遮蔽掉指定的 tokens。
对于上面的例子如果我们通过 attention_mask 标出填充的 padding token 的位置计算结果就不会有问题了
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence1_ids [[200, 200, 200]]
sequence2_ids [[200, 200]]
batched_ids [[200, 200, 200],[200, 200, tokenizer.pad_token_id],
]
batched_attention_masks [[1, 1, 1],[1, 1, 0],
]print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
outputs model(torch.tensor(batched_ids), attention_masktorch.tensor(batched_attention_masks))
print(outputs.logits)# tensor([[ 1.5694, -1.3895]], grad_fnAddmmBackward0)
# tensor([[ 0.5803, -0.4125]], grad_fnAddmmBackward0)
# tensor([[ 1.5694, -1.3895],
# [ 0.5803, -0.4125]], grad_fnAddmmBackward0)正如前面强调的那样在实际使用时我们应该直接使用分词器对文本进行处理它不仅会向 token 序列中添加模型需要的特殊字符例如 [CLS] , [SEP] \texttt{[CLS]},\texttt{[SEP]} [CLS],[SEP]还会自动生成对应的 Attention Mask。
目前大部分 Transformer 模型只能接受长度不超过 512 或 1024 的 token 序列因此对于长序列有以下三种处理方法
使用一个支持长文的 Transformer 模型例如 Longformer 和 LED最大长度 4096设定最大长度 max_sequence_length 以截断输入序列sequence sequence[:max_sequence_length]。将长文切片为短文本块 (chunk)然后分别对每一个 chunk 编码。
3.3 直接使用分词器
在实际使用时我们应该直接使用分词器来完成包括分词、转换 token IDs、Padding、构建 Attention Mask、截断等操作。例如
from transformers import AutoTokenizercheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)sequences [Ive been waiting for a HuggingFace course my whole life., So have I!
]model_inputs tokenizer(sequences)
print(model_inputs)# {input_ids: [
# [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
# [101, 2061, 2031, 1045, 999, 102]],
# attention_mask: [
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1]]
# }可以看到分词器的输出包含了模型需要的所有输入项。对于 DistilBERT 模型就是 input IDsinput_ids和 Attention Maskattention_mask。
Padding 操作通过 padding 参数来控制
padding“longest” 将序列填充到当前 batch 中最长序列的长度padding“max_length”将所有序列填充到模型能够接受的最大长度例如 BERT 模型就是 512。
from transformers import AutoTokenizercheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)sequences [Ive been waiting for a HuggingFace course my whole life., So have I!
]model_inputs tokenizer(sequences, paddinglongest)
print(model_inputs)model_inputs tokenizer(sequences, paddingmax_length)
print(model_inputs)# {input_ids: [
# [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
# [101, 2061, 2031, 1045, 999, 102, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
# }# {input_ids: [
# [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102, 0, 0, 0, 0, 0, 0, 0, 0, ...],
# [101, 2061, 2031, 1045, 999, 102, 0, 0, 0, ...]],
# attention_mask: [
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, ...],
# [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]]
# }截断操作通过 truncation 参数来控制如果 truncationTrue那么大于模型最大接受长度的序列都会被截断例如对于 BERT 模型就会截断长度超过 512 的序列。此外也可以通过 max_length 参数来控制截断长度
from transformers import AutoTokenizercheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)sequences [Ive been waiting for a HuggingFace course my whole life., So have I!
]model_inputs tokenizer(sequences, max_length8, truncationTrue)
print(model_inputs)# {input_ids: [
# [101, 1045, 1005, 2310, 2042, 3403, 2005, 102],
# [101, 2061, 2031, 1045, 999, 102]],
# attention_mask: [
# [1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1]]
# }分词器还可以通过 return_tensors 参数指定返回的张量格式设为 pt 则返回 PyTorch 张量tf 则返回 TensorFlow 张量np 则返回 NumPy 数组。例如
from transformers import AutoTokenizercheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)sequences [Ive been waiting for a HuggingFace course my whole life., So have I!
]model_inputs tokenizer(sequences, paddingTrue, return_tensorspt)
print(model_inputs)model_inputs tokenizer(sequences, paddingTrue, return_tensorsnp)
print(model_inputs)# {input_ids: tensor([
# [ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
# 2607, 2026, 2878, 2166, 1012, 102],
# [ 101, 2061, 2031, 1045, 999, 102, 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, 1, 1],
# [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# }# {input_ids: array([
# [ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662,
# 12172, 2607, 2026, 2878, 2166, 1012, 102],
# [ 101, 2061, 2031, 1045, 999, 102, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0]]),
# attention_mask: array([
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# }实际使用分词器时我们通常会同时进行 padding 操作和截断操作并设置返回格式为 Pytorch 张量这样就可以直接将分词结果送入模型
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint distilbert-base-uncased-finetuned-sst-2-english
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModelForSequenceClassification.from_pretrained(checkpoint)sequences [Ive been waiting for a HuggingFace course my whole life., So have I!
]tokens tokenizer(sequences, paddingTrue, truncationTrue, return_tensorspt)
print(tokens)
output model(**tokens)# {input_ids: tensor([
# [ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
# 2607, 2026, 2878, 2166, 1012, 102],
# [ 101, 2061, 2031, 1045, 999, 102, 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, 1, 1],
# [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])}# tensor([[-1.5607, 1.6123],
# [-3.6183, 3.9137]], grad_fnAddmmBackward0)在 paddingTrue, truncationTrue 设置下同一个 batch 中的序列都会 padding 到相同的长度并且大于模型最大接受长度的序列会被自动截断。
3.4 编码句子对
除了对单段文本进行编码以外batch 只是并行地编码多个单段文本对于 BERT 等包含“句子对”预训练任务的模型它们的分词器都支持对“句子对”进行编码例如
from transformers import AutoTokenizercheckpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)inputs tokenizer(This is the first sentence., This is the second one.)
print(inputs)tokens tokenizer.convert_ids_to_tokens(inputs[input_ids])
print(tokens)# {input_ids: [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
# token_type_ids: [0, 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, 1]}# [[CLS], this, is, the, first, sentence, ., [SEP], this, is, the, second, one, ., [SEP]]此时分词器会使用 [SEP] \texttt{[SEP]} [SEP] token 拼接两个句子输出形式为“ [CLS] sentence1 [SEP] sentence2 [SEP] \texttt{[CLS] sentence1 [SEP] sentence2 [SEP]} [CLS] sentence1 [SEP] sentence2 [SEP]”的 token 序列这也是 BERT 模型预期的“句子对”输入格式。
返回结果中除了前面我们介绍过的 input_ids 和 attention_mask 之外还包含了一个 token_type_ids 项用于标记哪些 token 属于第一个句子哪些属于第二个句子。如果我们将上面例子中的 token_type_ids 项与 token 序列对齐
[[CLS], this, is, the, first, sentence, ., [SEP], this, is, the, second, one, ., [SEP]]
[ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]就可以看到第一个句子“ [CLS] sentence1 [SEP] \texttt{[CLS] sentence1 [SEP]} [CLS] sentence1 [SEP]”所有 token 的 type ID 都为 0而第二个句子“sentence2 [SEP]”对应的 token type ID 都为 1。 如果我们选择其他模型分词器的输出不一定会包含 token_type_ids 项例如 DistilBERT 模型。分词器只需保证输出格式与模型预训练时的输入一致即可。 实际使用时我们不需要去关注编码结果中是否包含 token_type_ids 项分词器会根据 checkpoint 自动调整输出格式例如
from transformers import AutoTokenizercheckpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)sentence1_list [First sentence., This is the second sentence., Third one.]
sentence2_list [First sentence is short., The second sentence is very very very long., ok.]tokens tokenizer(sentence1_list,sentence2_list,paddingTrue,truncationTrue,return_tensorspt
)
print(tokens)
print(tokens[input_ids].shape)# {input_ids: tensor([
# [ 101, 2034, 6251, 1012, 102, 2034, 6251, 2003, 2460, 1012, 102, 0,
# 0, 0, 0, 0, 0, 0],
# [ 101, 2023, 2003, 1996, 2117, 6251, 1012, 102, 1996, 2117, 6251, 2003,
# 2200, 2200, 2200, 2146, 1012, 102],
# [ 101, 2353, 2028, 1012, 102, 7929, 1012, 102, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0]]),
# token_type_ids: tensor([
# [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),
# attention_mask: tensor([
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
# [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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# }
# torch.Size([3, 18])可以看到分词器成功地输出了形式为“ [CLS] sentence1 [SEP] sentence2 [SEP] \texttt{[CLS] sentence1 [SEP] sentence2 [SEP]} [CLS] sentence1 [SEP] sentence2 [SEP]”的 token 序列并且将三个序列都 padding 到了相同的长度。
4 添加 Token
实际操作中我们还经常会遇到输入中需要包含特殊标记符的情况例如使用 ENT_START \texttt{ENT\_START} ENT_START 和 ENT_END \texttt{ENT\_END} ENT_END 标记出文本中的实体。由于这些自定义 token 并不在预训练模型原来的词表中因此直接运用分词器处理就会出现问题。
例如直接使用 BERT 分词器处理下面的句子
from transformers import AutoTokenizercheckpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)sentence Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.
print(tokenizer.tokenize(sentence))# [two, [, en, ##t, _, start, ], cars, [, en, ##t, _, end, ], collided, in, a, [, en, ##t, _, start, ], tunnel, [, en, ##t, _, end, ], this, morning, .]由于分词器无法识别 [ENT_START] \texttt{[ENT\_START]} [ENT_START] 和 [ENT_END] \texttt{[ENT\_END]} [ENT_END]因此将它们都当作未知字符处理例如“[ENT_END]”被切分成了 ‘[’, ‘en’, ‘##t’, ‘_’, ‘end’, ‘]’ 六个 token。
此外一些领域的专业词汇例如使用多个词语的缩写拼接而成的医学术语同样也不在模型的词表中因此也会出现上面的问题。此时我们就需要将这些新 token 添加到模型的词表中让分词器与模型可以识别并处理这些 token。
4.1 添加新 token
Transformers 库提供了两种方式来添加新 token分别是
add_tokens() 添加普通 token参数是新 token 列表如果 token 不在词表中就会被添加到词表的最后。
checkpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)num_added_toks tokenizer.add_tokens([new_token1, my_new-token2])
print(We have added, num_added_toks, tokens)# We have added 2 tokens为了防止 token 已经包含在词表中我们还可以预先对新 token 列表进行过滤
new_tokens [new_token1, my_new-token2]
new_tokens set(new_tokens) - set(tokenizer.vocab.keys())
tokenizer.add_tokens(list(new_tokens))add_special_tokens() 添加特殊 token参数是包含特殊 token 的字典键值只能从 bos_token, eos_token, unk_token, sep_token, pad_token, cls_token, mask_token, additional_special_tokens 中选择。同样地如果 token 不在词表中就会被添加到词表的最后。添加后还可以通过特殊属性来访问这些 token例如 tokenizer.cls_token 就指向 cls token。
checkpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)special_tokens_dict {cls_token: [MY_CLS]}num_added_toks tokenizer.add_special_tokens(special_tokens_dict)
print(We have added, num_added_toks, tokens)assert tokenizer.cls_token [MY_CLS]# We have added 1 tokens我们也可以使用 add_tokens() 添加特殊 token只需要额外设置参数 special_tokensTrue
checkpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)num_added_toks tokenizer.add_tokens([[NEW_tok1], [NEW_tok2]])
num_added_toks tokenizer.add_tokens([[NEW_tok3], [NEW_tok4]], special_tokensTrue)print(We have added, num_added_toks, tokens)
print(tokenizer.tokenize([NEW_tok1] Hello [NEW_tok2] [NEW_tok3] World [NEW_tok4]!))# We have added 2 tokens
# [[new_tok1], hello, [new_tok2], [NEW_tok3], world, [NEW_tok4], !]特殊 token 的标准化 (normalization) 与普通 token 有一些不同比如不会被小写。 这里我们使用的是不区分大小写的 BERT 模型因此分词后添加的普通 token [NEW_tok1] \texttt{[NEW\_tok1]} [NEW_tok1] 和 [NEW_tok2] \texttt{[NEW\_tok2]} [NEW_tok2] 都被处理为了小写而添加的特殊 token [NEW_tok3] \texttt{[NEW\_tok3]} [NEW_tok3] 和 [NEW_tok4] \texttt{[NEW\_tok4]} [NEW_tok4] 则保持大写。 对于前面的例子很明显实体标记符 [ENT_START] \texttt{[ENT\_START]} [ENT_START] 和 [ENT_END] \texttt{[ENT\_END]} [ENT_END]属于特殊 token因此按添加特殊 token 的方式进行
from transformers import AutoTokenizercheckpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)num_added_toks tokenizer.add_tokens([[ENT_START], [ENT_END]], special_tokensTrue)
# num_added_toks tokenizer.add_special_tokens({additional_special_tokens: [[ENT_START], [ENT_END]]})
print(We have added, num_added_toks, tokens)sentence Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.print(tokenizer.tokenize(sentence))# We have added 2 tokens
# [two, [ENT_START], cars, [ENT_END], collided, in, a, [ENT_START], tunnel, [ENT_END], this, morning, .]可以看到分词器成功地将 [ENT_START] \texttt{[ENT\_START]} [ENT_START] 和 [ENT_END] \texttt{[ENT\_END]} [ENT_END] 识别为 token并且保持大写。
4.2 调整 embedding 矩阵 向词表中添加新 token 后必须重置模型 embedding 矩阵的大小也就是向矩阵中添加新 token 对应的 embedding这样模型才可以正常工作将 token 映射到对应的 embedding。 调整 embedding 矩阵通过 resize_token_embeddings() 函数来实现例如对于前面的例子
from transformers import AutoTokenizer, AutoModelcheckpoint bert-base-uncased
tokenizer AutoTokenizer.from_pretrained(checkpoint)
model AutoModel.from_pretrained(checkpoint)print(vocabulary size:, len(tokenizer))
num_added_toks tokenizer.add_tokens([[ENT_START], [ENT_END]], special_tokensTrue)
print(After we add, num_added_toks, tokens)
print(vocabulary size:, len(tokenizer))model.resize_token_embeddings(len(tokenizer))
print(model.embeddings.word_embeddings.weight.size())# Randomly generated matrix
print(model.embeddings.word_embeddings.weight[-2:, :])# vocabulary size: 30522
# After we add 2 tokens
# vocabulary size: 30524
# torch.Size([30524, 768])# tensor([[-0.0325, -0.0224, 0.0044, ..., -0.0088, -0.0078, -0.0110],
# [-0.0005, -0.0167, -0.0009, ..., 0.0110, -0.0282, -0.0013]],
# grad_fnSliceBackward0)可以看到在添加 [ENT_START] \texttt{[ENT\_START]} [ENT_START] 和 [ENT_END] \texttt{[ENT\_END]} [ENT_END] 之后分词器的词表大小从 30522 增加到了 30524模型 embedding 矩阵的大小也成功调整为了 [30524x768]。 在默认情况下新添加 token 的 embedding 是随机初始化的。 我们尝试打印出新添加 token 对应的 embedding新 token 会添加在词表的末尾因此只需打印出最后两行。如果你多次运行上面的代码就会发现每次打印出的 [ENT_START] \texttt{[ENT\_START]} [ENT_START] 和 [ENT_END] \texttt{[ENT\_END]} [ENT_END] 的 embedding 是不同的。
5 Token embedding 初始化
如果有充分的语料对模型进行微调或者继续预训练那么将新添加 token 初始化为随机向量没什么问题。但是如果训练语料较少甚至是只有很少语料的 few-shot learning 场景下这种做法就存在问题。研究表明在训练数据不够多的情况下这些新添加 token 的 embedding 只会在初始值附近小幅波动。换句话说即使经过训练它们的值事实上还是随机的。
5.1 直接赋值
在很多情况下我们需要手工初始化新添加 token 的 embedding这可以通过直接对 embedding 矩阵赋值来实现。例如我们将上面例子中两个新 token 的 embedding 都初始化为全零向量
import torchwith torch.no_grad():model.embeddings.word_embeddings.weight[-2:, :] torch.zeros([2, model.config.hidden_size], requires_gradTrue)
print(model.embeddings.word_embeddings.weight[-2:, :])# tensor([[0., 0., 0., ..., 0., 0., 0.],
# [0., 0., 0., ..., 0., 0., 0.]], grad_fnSliceBackward0)注意初始化 embedding 的过程并不可导因此这里通过 torch.no_grad() 暂停梯度的计算。
现实场景中更为常见的做法是使用已有 token 的 embedding 来初始化新添加 token。例如对于上面的例子我们可以将 [ENT_START] 和 [ENT_END] 的值都初始化为“entity” token 对应的 embedding。
import torchtoken_id tokenizer.convert_tokens_to_ids(entity)
token_embedding model.embeddings.word_embeddings.weight[token_id]
print(token_id)with torch.no_grad():for i in range(1, num_added_toks1):model.embeddings.word_embeddings.weight[-i:, :] token_embedding.clone().detach().requires_grad_(True)
print(model.embeddings.word_embeddings.weight[-2:, :])
9178# tensor([[-0.0039, -0.0131, -0.0946, ..., -0.0223, 0.0107, -0.0419],
# [-0.0039, -0.0131, -0.0946, ..., -0.0223, 0.0107, -0.0419]],
# grad_fnSliceBackward0)因为 token ID 就是 token 在 embedding 矩阵中的索引因此这里我们直接通过 weight[token_id] 取出“entity”对应的 embedding。 可以看到最终结果符合我们的预期[ENT_START] 和 [ENT_END] 被初始化为相同的 embedding。
5.2 初始化为已有 token 的值
更为高级的做法是根据新添加 token 的语义来进行初始化。例如将值初始化为 token 语义描述中所有 token 的平均值假设新 t o k e n t i token \ t_i token ti 的语义描述为 w i , 1 , w i , 2 , . . . , w i , n w_{i, 1}, w_{i, 2}, ..., w_{i, n} wi,1,wi,2,...,wi,n, 的语义描述为 t i t_i ti 的 embedding 为 E ( t i ) 1 n ∑ j 1 n E ( w i , j ) E(t_i) \frac{1}{n}\sum _{j1} ^n E(w_{i, j}) E(ti)n1∑j1nE(wi,j)
这里 E E E 表示预训练模型的 embedding 矩阵。对于上面的例子我们可以分别为 [ENT_START] 和 [ENT_END] 编写对应的描述然后再对它们的值进行初始化
descriptions [start of entity, end of entity]with torch.no_grad():for i, token in enumerate(reversed(descriptions), start1):tokenized tokenizer.tokenize(token)print(tokenized)tokenized_ids tokenizer.convert_tokens_to_ids(tokenized)new_embedding model.embeddings.word_embeddings.weight[tokenized_ids].mean(axis0)model.embeddings.word_embeddings.weight[-i, :] new_embedding.clone().detach().requires_grad_(True)
print(model.embeddings.word_embeddings.weight[-2:, :])# [end, of, entity]
# [start, of, entity]
# tensor([[-0.0340, -0.0144, -0.0441, ..., -0.0016, 0.0318, -0.0151],
# [-0.0060, -0.0202, -0.0312, ..., -0.0084, 0.0193, -0.0296]],
# grad_fnSliceBackward0)可以看到这里成功地将 [ENT_START] 的 embedding 初始化为“start”、“of”、“entity”三个 token 的平均值将 [ENT_END] 初始化为“end”、“of”、“entity”的平均值。