transformers重要组件(模型与分词器)

1、模型: 

from transformers import AutoModelcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)

除了像之前使用 AutoModel 根据 checkpoint 自动加载模型以外,我们也可以直接使用模型对应的 Model 类,例如 BERT 对应的就是 BertModel

from transformers import BertModelmodel = BertModel.from_pretrained("bert-base-cased")

注意,在大部分情况下,我们都应该使用 AutoModel 来加载模型。这样如果我们想要使用另一个模型(比如把 BERT 换成 RoBERTa),只需修改 checkpoint,其他代码可以保持不变。

加载模型

所有存储在 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.jsontokenizer_config.json 和 vocab.txt

保存模型

保存模型通过调用 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),其包含两个步骤:

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

分词策略

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

  • 按词切分 (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] token 来表示它是 unknown 的。显然,如果分词结果中包含很多 [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。尤其对于土耳其语等黏着语,几乎所有的复杂长词都可以通过串联多个子词构成。

加载与保存分词器

分词器的加载与保存与模型相似,使用 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 开始)。

编码与解码文本

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

  1. 分词:使用分词器按某种策略将文本切分为 tokens;
  2. 映射:将 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] 和 [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] 和 [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) 任务时都会调用这一函数。

5.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 = "I've 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_fn=<AddmmBackward0>)

这里我们通过 [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 = "I've been waiting for a HuggingFace course my whole life."tokenized_inputs = tokenizer(sequence, return_tensors="pt")
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_fn=<AddmmBackward0>)

可以看到,分词器输出的结果中不仅包含 token IDs(input_ids),还会包含模型需要的其他输入项。前面我们之所以只输入 token IDs 模型也能正常运行,是因为它自动地补全了其他的输入项,例如 attention_mask 等,后面我们会具体介绍。

由于分词器自动在序列的首尾添加了 [CLS] 和 [SEP] token,所以上面两个例子中模型的输出是有差异的。因为 DistilBERT 预训练时是包含 [CLS] 和 [SEP] 的,所以下面的例子才是正确的使用方法。

Padding 操作

按批输入多段文本产生的一个直接问题就是:batch 中的文本有长有短,而输入张量必须是严格的二维矩形,维度为 (batch size,sequence length),即每一段文本编码后的 token IDs 数量必须一样多。例如下面的 ID 列表是无法转换为张量的:

batched_ids = [[200, 200, 200],[200, 200]
]

我们需要通过 Padding 操作,在短序列的结尾填充特殊的 padding token,使得 batch 中所有的序列都具有相同的长度,例如:

padding_id = 100batched_ids = [[200, 200, 200],[200, 200, padding_id],
]

模型的 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_fn=<AddmmBackward0>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
tensor([[ 1.5694, -1.3895],[ 1.3374, -1.2163]], grad_fn=<AddmmBackward0>)

问题出现了,使用 padding token 填充的序列的结果竟然与其单独送入模型时不同!

这是因为模型默认会编码输入序列中的所有 token 以建模完整的上下文,因此这里会将填充的 padding token 也一同编码进去,从而生成不同的语义表示。

因此,在进行 Padding 操作时,我们必须明确告知模型哪些 token 是我们填充的,它们不应该参与编码。这就需要使用到 Attention Mask 了,在前面的例子中相信你已经多次见过它了。

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_mask=torch.tensor(batched_attention_masks))
print(outputs.logits)
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward0>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
tensor([[ 1.5694, -1.3895],[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)

正如前面强调的那样,在实际使用时,我们应该直接使用分词器对文本进行处理,它不仅会向 token 序列中添加模型需要的特殊字符(例如 [CLS],[SEP]),还会自动生成对应的 Attention Mask。

目前大部分 Transformer 模型只能接受长度不超过 512 或 1024 的 token 序列,因此对于长序列,有以下三种处理方法:

  1. 使用一个支持长文的 Transformer 模型,例如 Longformer 和 LED(最大长度 4096);
  2. 设定最大长度 max_sequence_length 以截断输入序列:sequence = sequence[:max_sequence_length]
  3. 将长文切片为短文本块 (chunk),然后分别对每一个 chunk 编码。在后面的快速分词器中,我们会详细介绍。

直接使用分词器

正如前面所说,在实际使用时,我们应该直接使用分词器来完成包括分词、转换 token IDs、Padding、构建 Attention Mask、截断等操作。例如:

from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've 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 IDs(input_ids)和 Attention Mask(attention_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 = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, padding="longest")
print(model_inputs)model_inputs = tokenizer(sequences, padding="max_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 参数来控制,如果 truncation=True,那么大于模型最大接受长度的序列都会被截断,例如对于 BERT 模型就会截断长度超过 512 的序列。此外,也可以通过 max_length 参数来控制截断长度:

from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, max_length=8, truncation=True)
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 = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")
print(model_inputs)model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
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 = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
print(tokens)
output = model(**tokens)
print(output.logits)
{'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_fn=<AddmmBackward0>)

在 padding=True, truncation=True 设置下,同一个 batch 中的序列都会 padding 到相同的长度,并且大于模型最大接受长度的序列会被自动截断。

编码句子对

除了对单段文本进行编码以外(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] token 拼接两个句子,输出形式为“[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]”所有 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,padding=True,truncation=True,return_tensors="pt"
)
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]”的 token 序列,并且将三个序列都 padding 到了相同的长度。

5.4 添加 Token

实际操作中,我们还经常会遇到输入中需要包含特殊标记符的情况,例如使用 [ENT_START] 和 [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] 和 [ENT_END] ,因此将它们都当作未知字符处理,例如“[ENT_END]”被切分成了 '[''en''##t''_''end'']' 六个 token。

此外,一些领域的专业词汇,例如使用多个词语的缩写拼接而成的医学术语,同样也不在模型的词表中,因此也会出现上面的问题。此时我们就需要将这些新 token 添加到模型的词表中,让分词器与模型可以识别并处理这些 token。

添加新 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_tokeneos_tokenunk_tokensep_tokenpad_tokencls_tokenmask_tokenadditional_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_tokens=True

    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_tokens=True)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] 和 [NEW_tok2] 都被处理为了小写,而添加的特殊 token [NEW_tok3] 和 [NEW_tok4] 则保持大写。

对于前面的例子,很明显实体标记符 [ENT_START] 和 [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_tokens=True)
# 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] 和 [ENT_END] 识别为 token,并且保持大写。

调整 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_tokens=True)
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_fn=<SliceBackward0>)

可以看到,在添加 [ENT_START] 和 [ENT_END] 之后,分词器的词表大小从 30522 增加到了 30524,模型 embedding 矩阵的大小也成功调整为了 30524×768。

在默认情况下,新添加 token 的 embedding 是随机初始化的。

我们尝试打印出新添加 token 对应的 embedding(新 token 会添加在词表的末尾,因此只需打印出最后两行)。如果你多次运行上面的代码,就会发现每次打印出的 [ENT_START] 和 [ENT_END] 的 embedding 是不同的。

初始化为已有 token 的值

descriptions = ['start of entity', 'end of entity']with torch.no_grad():for i, token in enumerate(reversed(descriptions), start=1):tokenized = tokenizer.tokenize(token)print(tokenized)tokenized_ids = tokenizer.convert_tokens_to_ids(tokenized)new_embedding = model.embeddings.word_embeddings.weight[tokenized_ids].mean(axis=0)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_fn=<SliceBackward0>)

可以看到,这里成功地将 [ENT_START] 的 embedding 初始化为“start”、“of”、“entity”三个 token 的平均值,将 [ENT_END] 初始化为“end”、“of”、“entity”的平均值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/669775.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何利用边缘计算网关进行机床数据采集,以提高数据采集的效率和准确性-天拓四方

边缘计算网关集成了数据采集、处理和传输功能的嵌入式设备。它位于传感器和执行器组成的设备层与云计算平台之间&#xff0c;能够实时处理和响应本地设备的数据请求&#xff0c;减轻云平台的压力&#xff0c;提高数据处理的速度和效率。同时&#xff0c;边缘计算网关还可以将处…

跟着cherno手搓游戏引擎【21】shaderLibrary(shader管理类)

前置&#xff1a; ytpch.h&#xff1a; #pragma once #include<iostream> #include<memory> #include<utility> #include<algorithm> #include<functional> #include<string> #include<vector> #include<unordered_map> #in…

ROS笔记一:工作空间和功能包

目录 工作空间 如何创建工作空间&#xff1a; 编译工作空间 设置环境变量 功能包 创建功能包 CMakeLists.txt package.xml 工作空间 ROS的工作空间是用来存放工程文件代码的文件夹 ROS的开发依赖于工作空间&#xff0c;包括编写代码、编译等都是在工作空间下进行的 工作空…

如何在Termux中使用Hexo结合内网穿透工具实现远程访问本地博客站点

文章目录 前言 1.安装 Hexo2.安装cpolar3.远程访问4.固定公网地址 前言 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。 下面介绍在Termux中安装个人hexo博客并结合…

GO语言集成开发 JetBrains GoLand 2023 中文

JetBrains GoLand 2023是一款专为Go语言开发者打造的集成开发环境&#xff08;IDE&#xff09;。它基于IntelliJ IDEA平台&#xff0c;提供了丰富的功能和工具&#xff0c;旨在提高开发效率和质量。GoLand 2023具备强大的Go语言支持&#xff0c;包括语法高亮、自动补全、代码提…

RPA财务机器人之UiPath实战 - 自动化操作Excel进行财务数据汇总与分析之流程建立与数据读取、处理、汇总、分析

一、案例介绍&#xff1a; A公司共有13个开在不同银行的帐户&#xff0c;分别用于不同的业务分部或地区分部收付款。公司总部为了核算每月的收支情况&#xff0c;查看银行在哪个月交易量频繁&#xff0c;需要每月汇总各个银行的帐户借方和贷方金额&#xff0c;并将其净收支&am…

unity 增加系统时间显示、FPS帧率、ms延迟

代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;using UnityEngine;public class Frame : MonoBehaviour {// 记录帧数private int _frame;// 上一次计算帧率的时间private float _lastTime;// 平…

MATLAB Fundamentals>>>Centering and Scaling

MATLAB Fundamentals>Common Data Analysis Techniques>Polynomial Fitting>Centering and Scaling 数据导入 This code sets up the activity. yr 2000:2007 penguins [5.49 7.03 7.73 7.70 9.29 9.21 11.89 10.85] 附加练习 How does the model look?…

Node.js JSON Schema Ajv依赖库逐步介绍验证类型和中文错误提示

在构建应用程序时&#xff0c;数据的有效性是至关重要的。为了确保传入的数据符合预期的格式和规范&#xff0c;我们可以使用 Ajv&#xff08;Another JSON Schema Validator&#xff09;进行验证。在这篇博文中&#xff0c;我们将从头开始学习 Ajv&#xff0c;逐步介绍验证类型…

Unity3D判断屏幕中某个坐标点的位置是否在指定UI区域内

系列文章目录 unity工具 文章目录 系列文章目录前言一、使用rect.Contains()判断1-1、转换坐标1-2、代码如下&#xff1a;1-3、注意事项1-3、测试效果如下 二、使用坐标计算在不在区域内2-1、方法如下&#xff1a;2-2、注意事项 三、使用RectTransformUtility.ScreenPointToLo…

MongoDB从入门到实战之Docker快速安装MongoDB

前言 在上一篇文章中带领带同学们快速入门MongoDB这个文档型的NoSQL数据库&#xff0c;让大家快速的了解了MongoDB的基本概念。这一章开始我们就开始实战篇教程&#xff0c;为了快速把MongoDB使用起来我将会把MongoDB在Docker容器中安装起来作为开发环境使用。然后我这边MongoD…

SQL Server数据库日志查看若已满需要清理的三种解决方案

首先查看获取实例中每个数据库日志文件大小及使用情况&#xff0c;根据数据库日志占用百分比来清理 DBCC SQLPERF(LOGSPACE) 第一种解决方案&#xff1a; 在数据库上点击右键 → 选择 属性 → 选择 文件&#xff0c;然后增加数据库日志文件的文件大小。 第二种解决方案 手动…

宝塔+php+ssh+vscode+虚拟机 远程调试

远程(虚拟机)宝塔 安装扩展 配置文件添加&#xff0c;zend_extension看你虚拟机的具体位置 [Xdebug] zend_extension/www/server/php/74/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so xdebug.modedebug xdebug.start_with_requesttrigger xdebug.client_host&quo…

C/C++ - 容器list

目录 容器特性 list 容器特性 使用场景 构造函数 默认构造函数 填充构造函数 范围构造函数 复制构造函数 大小函数 函数&#xff1a;size 函数&#xff1a;empty​ 函数&#xff1a;max_size​ 增加函数 函数&#xff1a;​push_back​ 函数&#xff1a;push_f…

车位检测,YOLOV8,OPENCV调用

车位检测YOLOV8NANO,opencv调用 车位检测&#xff0c;YOLOV8NANO&#xff0c;训练得到PT模型&#xff0c;然后转换成ONNX&#xff0c;OPENCV的DNN调用&#xff0c;支持C,PYTHON,ANDROID

C#用Array类的FindAll方法和List<T>类的Add方法按关键词在数组中检索元素并输出

目录 一、使用的方法 1. Array.FindAll(T[], Predicate) 方法 &#xff08;1&#xff09;定义 &#xff08;2&#xff09;示例 2.List类的常用方法 &#xff08;1&#xff09;List.Add(T) 方法 &#xff08;2&#xff09;List.RemoveAt(Int32) 方法 &#xff08;3&…

Bytebase 签约 Vianova,助力欧洲城市交通智能平台中 Snowflake 和 PG 的变更自动化及版本控制

在数字化发展的浪潮中&#xff0c;自动化数据库变更管理成为提升产品上线效率、降低人为失误风险的关键工具&#xff0c;同时促进流程的一致性与标准化&#xff0c;确保合规性和变更的可追溯性。近日&#xff0c;数据库 DevOps 团队协同管理工具 Bytebase 签约欧洲交通数据管理…

免费在线绘图工具有哪些好用的?线画图工具是比较好的选择?

据说一张图胜过千言万语。一张好的图片可以帮助你快速表达自己的想法&#xff0c;让自己的想法更直观清晰&#xff0c;无论是产品分析、方案选项还是技术交流。市面上有很多绘图工具。这里有一些好用又免费的工具&#xff0c;绝对会让你在几分钟内坠入爱河。 即时设计 - 可实时…

YUM | 起源 | 发展 | 运行逻辑

介绍 YUM&#xff08;Yellowdog Updater, Modified&#xff09;起源于 Red Hat Linux 发行版 up2date 工具。 最初&#xff0c;up2date 是由 Red Hat 公司提供的用于管理系统更新的工具。然而&#xff0c;社区逐渐对 up2date 出现一些不满&#xff0c;主要是由于其使用体验和…

10英寸安卓车载平板电脑丨ONERugged车载工业平板:解决农业工作效率

农业是人类社会的基石之一&#xff0c;而农业工作效率的提升一直是农民和农业专业人士关注的重要议题。随着技术的不断进步&#xff0c;车载工业平板成为了解决农业工作效率的创新解决方案。本文将探讨车载工业平板如何为农业带来巨大的改变&#xff0c;提高农民的工作效率和农…