【Transformer系列】通俗易懂理解Token分词(经验版)

重要说明:本文从网上资料整理而来,仅记录博主学习相关知识点的过程,侵删。

关于Token分词的相关概念,请参考另一篇博客:【Transformer系列】通俗易懂理解Token分词(概念版)

一、参考资料

huggingface/tokenizers 代码仓库:https://github.com/huggingface/tokenizers

Hugging Face 的 Transformers 库快速入门(二):模型与分词器

常识| 大模型收费计量单位之Token概念

Pytorch Transformer Tokenizer常见输入输出实战详解

二、Token相关介绍

1. Token简介

在NLP领域,Token 是指模型处理的基本数据单位。它可以是单词、字符、短语甚至图像片段、声音片段等。例如,一句话会被分割成多个 Token,每个标点符号也会被视为单独的 Token。

2. 分词策略

常见的分词策略,有按词切分 (Word-based)、按字符切分 (Character-based)、按子词切分 (Subword)三种方法。

中文示例:今天天气很好
英文示例:Let’s do tokenization!

2.1 按词切分 (Word-based)

按词切分就是基于空格的 Token 化

["今天", "天气", "很好"]
tokenized_text = "let's do tokenization".split()
print(tokenized_text)

在这里插入图片描述

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

2.2 按字符切分 (Character-based)

按字符切分就是基于字符的 Token 化

["今", "天", "天", "气", "候", "很", "好"]

在这里插入图片描述

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

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

2.3 按子词切分 (Subword)

在这里插入图片描述

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

3. Tokenizer 工具

具体的分词效果,可以通过 OpenAI 官方的 Tokenizer 工具查看:https://platform.openai.com/tokenizer

在这里插入图片描述

4. Tokenizer 文件

Hugging Face中Tokenizer 文件,如下图所示:

在这里插入图片描述

5. 模板(Template)

LLM 大模型学习必知必会系列(五):数据预处理(Tokenizer分词器)、模板(Template)设计以及LLM技术选型

每种模型都有其特定的输入格式,在小模型时代,这种输入格式比较简单:

[CLS]杭州是个好地方[SEP]

[CLS] 代表句子的起始,[SEP] 代表句子的终止。在BERT中,[CLS] 的索引是101,[SEP] 的索引是102。

在LLM时代,base模型的格式和上述的差不多,但chat模型的格式要复杂的多,比如千问chat模型的template格式是:

<|im_start|>system
You are a helpful assistant!
<|im_end|>
<|im_start|>user
How are you?<|im_end|>
<|im_start|>assistant

其中 You are a helpful assistant! 是system字段,How are you? 是用户问题,其他的部分都是template的格式。

system字段是chat模型必要的字段,这个字段会以命令方式提示模型在下面的对话中遵循怎么样的范式进行回答,比如:

“You are a helpful assistant!”
“下面你是一个警察,请按照警察的要求来审问我”
“假如你是一个爱哭的女朋友,下面的对话中清扮演好这个角色”

复杂的template有助于模型识别哪部分是用户输入,哪部分是自己之前的回答,哪部分是给自己的要求

三、示例代码

Transformer中的Tokenizer分词器使用学习

完整的文本编码 (Encoding) 过程,大致包含两个步骤:

  1. **分词:**使用分词器按某种策略将文本切分为 tokens;
  2. **映射:**将 tokens 转化为对应的 token IDs。
# 安装tokenizers
pip install tokenizers 

# 安装 transformers
pip install transformers

1. 加载分词器

transformers.AutoTokenizer.from_pretrained

通常使用 AutoTokenizer 类加载分词器。

from transformers import AutoTokenizer

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

print(tokenizer.vocab_size)

输出结果

28996

也可以使用 BertTokenizer 类加载Bert分词器。

from transformers import BertTokenizer

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

2. 文本分词

transformers.PreTrainedTokenizer.tokenize

使用 BERT 分词器来对文本进行分词。

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

输出结果

['Using', 'a', 'Trans', '##former', 'network', 'is', 'simple', '.']

可以看到,BERT 分词器采用的是子词 (subword) 切分策略:它会不断切分词语直到获得词表中的 token,例如 “Transformer” 会被切分为 “Trans” 和 “##former”。

3. 文本编码(Encode)

transformers.PreTrainedTokenizer.convert_tokens_to_ids

encode 编码,用于将Token序列转化为模型可处理的数值形式。具体来说,它会将每个Token的ID转化为一个固定长度的向量(通常是词嵌入向量),并将这些向量拼接起来形成一个二维数组。这个二维数组就是模型最终的输入。

通过 convert_tokens_to_ids 将切分出的 tokens 转换为对应的 token IDs。

token_ids = tokenizer.convert_tokens_to_ids(tokens)
print(token_ids)

输出结果

[7993, 170, 13809, 23763, 2443, 1110, 3014, 119]

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

token_ids = tokenizer.encode(sequence, add_special_tokens=True)
print(token_ids)

输出结果

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

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

**注意:**实际编码文本时,更为常见的是直接使用分词器进行处理。这样返回的结果中不仅包含处理后的 token IDs,还包含模型需要的其他辅助输入。例如对于 BERT 模型还会自动在输入中添加 token_type_idsattention_mask

输出结果

{'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]}

4. 文本解码(Decode)

transformers.PreTrainedTokenizerBase.convert_tokens_to_string

transformers.PreTrainedTokenizer.convert_ids_to_tokens

文本解码 (Decode) 是Encode编码的逆操作,用于将 token IDs 转化为原来的Token序列。值得注意的是,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分词器分为多个 token 的单词。下面我们尝试通过 decode() 函数解码前面生成的 token IDs:

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

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

输出结果

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

也可以组合 convert_ids_to_tokens()convert_tokens_to_string() 两个函数以实现文本解码。

decode_result = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens([7993, 170, 13809, 23763, 2443, 1110, 3014, 119]))
print(decode_result)

decode_result = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens([7993, 170, 13809, 23763, 2443, 1110, 3014, 119]))
print(decode_result)

输出结果

Using a Transformer network is simple.
Using a Transformer network is simple.

5. 保存分词器

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

调用 Tokenizer.save_pretrained() 函数会创建以下4个文件:

(llama.cpp) root@notebook-1813389960667746306-scnlbe5oi5-79366:/public/home/scnlbe5oi5/Downloads# tree -L 1 ./models/bert-base-cased/
./models/bert-base-cased/
|-- special_tokens_map.json
|-- tokenizer.json
|-- tokenizer_config.json
`-- vocab.txt

special_tokens_map.json

映射文件,包含unknown tokens 等特殊字符的映射关系。

{
  "cls_token": "[CLS]",
  "mask_token": "[MASK]",
  "pad_token": "[PAD]",
  "sep_token": "[SEP]",
  "unk_token": "[UNK]"
}

解释说明

  • [CLS],句子的起始标记;
  • [SEP],句子的终止标记。
  • [UNK],未知词;
  • [PAD],填充词;
  • [MASK],掩码。

tokenizer.json

vocab.txt 词表中的词按顺序生成索引表。模型根据索引表中的索引号编码生成one-hot向量,并与Bert中的 nn.embeding 训练权重矩阵相乘以获得该字符的随机词向量。

vocab.txt 词表示例:

在这里插入图片描述

对应的 tokenizer.json 索引表:

在这里插入图片描述

tokenizer_config.json

分词器配置文件,包含构建分词器需要的参数。

{
  "added_tokens_decoder": {
    "0": {
      "content": "[PAD]",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "100": {
      "content": "[UNK]",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "101": {
      "content": "[CLS]",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "102": {
      "content": "[SEP]",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "103": {
      "content": "[MASK]",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    }
  },
  "clean_up_tokenization_spaces": true,
  "cls_token": "[CLS]",
  "do_lower_case": false,
  "mask_token": "[MASK]",
  "model_max_length": 512,
  "pad_token": "[PAD]",
  "sep_token": "[SEP]",
  "strip_accents": null,
  "tokenize_chinese_chars": true,
  "tokenizer_class": "BertTokenizer",
  "unk_token": "[UNK]"
}

vocab.txt

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

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

vocab.txt是词表文件,每一个 token 占一行,行号就是对应的 token ID(从 0 开始)。不同模型的词表文件会因为设置的规则不同导致内容不同,所以相同的文本序列调用不同模型的tokenizer会产生不同的整数索引。预训练模型与tokenizer必须使用相同的vocab文件和配置文件才能使输入文本序列满足模型的输入要求。

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

6. 处理多段文本

在实际应用中,我们往往需要同时处理大量长度各异的文本。而且所有的神经网络模型都只接受批 (batch) 数据作为输入,即使只输入一段文本,也需要先将它组成只包含一个样本的 batch,然后才能送入模型。

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)

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 只包含一段文本),更多情况下送入的是包含多段文本的 batch:

batched_ids = [ids, ids, ids, ...]

**再次强调:**上面的演示只是为了便于我们更好地理解分词背后的原理。实际应用中,我们应该直接使用 tokenizer 分词器对文本进行处理,例如对于上面的例子:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)

sequence = "I've been waiting for a HuggingFace course my whole life."

tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print("Input IDs:\n", tokenized_inputs["input_ids"])

output = model(**tokenized_inputs)
print("Logits:\n", output.logits)

输出结果

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 等。

6.1 Padding 操作

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

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

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

padding_id = 100

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

每个预训练模型使用的 padding token 的 ID 可能有所不同,可以通过其对应分词器的 pad_token_id 属性获得。下面我们尝试将两段文本分别以独立以及组成 batch 的形式送入到模型中:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)

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>)
We strongly recommend passing in an `attention_mask` since your input_ids may be padded. See https://huggingface.co/docs/transformers/troubleshooting#incorrect-output-when-padding-tokens-arent-masked.
tensor([[ 1.5694, -1.3895],
        [ 1.3374, -1.2163]], grad_fn=<AddmmBackward0>)

问题出现了!在组成 batch 后,使用 padding token 填充的序列的结果出现了问题,与单独送入模型时的预测结果不同。这是因为 Transformer 模型会编码输入序列中的每一个 token 以建模完整的上下文,因此这里会将填充的 padding token 也当成是普通 token 一起编码,从而生成了不同的上下文语义表示。

因此,在进行 Padding 操作的同时,我们必须明确地告诉模型哪些 token 是我们填充的,它们不应该参与编码,这就需要使用到 attention mask。

6.2 Attention masks

Attention masks 是一个与 input IDs 尺寸完全相同的仅由 0 和 1 组成的张量,其中 0 表示对应位置的 token 是填充符,不应该参与 attention 层的计算,而应该只基于 1 对应位置的 token 来建模上下文。

除了标记填充字符位置以外,许多特定的模型结构也会使用 Attention masks 来遮蔽掉一些 tokens。

对于上面的例子,如果我们通过 attention_mask 标出填充的 padding token 的位置,计算结果就不会有问题了:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)

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 masks。

目前大部分 Transformer 模型只能处理长度为 512 或 1024 的 token 序列,如果你需要处理的序列长度大于 1024,有以下两种处理方法:

  • 使用一个支持长文的 Transformer 模型,例如 LongformerLED(最大长度 4096);
  • 设定一个最大长度 max_sequence_length截断输入序列:sequence = sequence[:max_sequence_length]

6.3 直接使用分词器

前面我们介绍了分词、转换 token IDs、padding、构建 attention masks 以及截断等操作。实际上,直接使用分词器就能实现所有的这些操作。

from transformers import AutoTokenizer

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)

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 内的序列填充到当前 batch 中最长序列的长度;
  • padding="max_length":将所有序列填充到模型能够接受的最大长度,例如 BERT 模型就是 512。
from transformers import AutoTokenizer

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)

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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0], [101, 2061, 2031, 1045, 999, 102, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0]]}

截断操作通过 truncation 参数来控制,如果 truncation=True,那么大于模型最大接受长度的序列都会被截断,例如对于 BERT 模型就会截断长度超过 512 的序列。此外,也可以通过 max_length 参数来控制截断长度:

from transformers import AutoTokenizer

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)

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 AutoTokenizer

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)

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, AutoModelForSequenceClassification

model_id = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(model_id)

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 中的序列都会 pad 到相同的长度,并且大于模型最大接受长度的序列会被自动截断。

6.4 编码句子对

在上面的例子中,我们都是对单个序列进行编码(即使通过 batch 处理多段文本,也是并行地编码单个序列),而实际上对于 BERT 等包含“句子对”分类预训练任务的模型来说,都支持对“句子对”进行编码,例如:

from transformers import AutoTokenizer

model_id = "google-bert/bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_id)

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_idsattention_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]”片段所有 tokens 的 token type ID 都为 0,而第二个句子“sentence2 [SEP]”片段对应的 token type ID 都是 1。

如果我们选择其他的预训练模型,分词器的输出不一定会包含 token_type_ids 项(例如 DistilBERT 模型)。分词器的输出格式只需保证与模型在预训练时的输入格式保持一致即可。

实际使用时,我们不需要去关注编码结果中是否包含 token_type_ids 项,分词器会根据 model_id 自动调整适用于对应模型的格式,例如:

from transformers import AutoTokenizer

model_id = "google-bert/bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_id)

sentence1_list = ["This is the first sentence 1.", "second sentence 1."]
sentence2_list = ["This is the first sentence 2.", "second sentence 2."]

tokens = tokenizer(
    sentence1_list,
    sentence2_list,
    padding=True,
    truncation=True,
    return_tensors="pt"
)
print(tokens)
print(tokens['input_ids'].shape)

输出结果

{'input_ids': tensor([[ 101, 2023, 2003, 1996, 2034, 6251, 1015, 1012,  102, 2023, 2003, 1996,
         2034, 6251, 1016, 1012,  102],
        [ 101, 2117, 6251, 1015, 1012,  102, 2117, 6251, 1016, 1012,  102,    0,
            0,    0,    0,    0,    0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 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, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]])}
torch.Size([2, 17])

可以看到分词器成功地输出了形式为“[CLS] sentence1 [SEP] sentence2 [SEP]”的 token 序列,并且将两个 token 序列都 pad 到了相同的长度。

三、相关经验

使用Tokenizer的注意事项

  1. 选择合适的词汇表大小:词汇表大小会直接影响模型的性能和计算效率。如果词汇表过大,会导致模型参数过多,计算量大增;而如果词汇表过小,则可能导致一些重要的信息丢失。
  2. 处理未知词:对于不在词汇表中的未知词,可以通过添加<unk>来处理。此外,还可以通过一些技术手段(如WordPiece等)来减少未知词的数量。
  3. 保持一致性:在训练和推理阶段,应确保使用相同的Tokenizer来进行文本处理,以保证模型输入的一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花花少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值