<FrameworkSwitchCourse {fw} />

# 快速标记器的特殊能力 [[快速标记器的特殊能力]]

{#if fw === 'pt'}

<CourseFloatingBanner chapter={6}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/zh-CN/chapter6/section3_pt.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/zh-CN/chapter6/section3_pt.ipynb"},
]} />

{:else}

<CourseFloatingBanner chapter={6}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/zh-CN/chapter6/section3_tf.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/zh-CN/chapter6/section3_tf.ipynb"},
]} />

{/if}

在本节中，我们将仔细研究 🤗 Transformers 中标记器的功能。到目前为止，我们只使用它们来标记输入或将 ID 解码回文本，但是标记器——尤其是那些由 🤗 Tokenizers 库支持的——可以做更多的事情。为了说明这些附加功能，我们将探索如何重现结果 **token-classification** （我们称之为 **ner** ） 和 **question-answering** 我们第一次在[Chapter 1](/course/chapter1)中遇到的管道.

<Youtube id="g8quOxoqhHQ"/>

在接下来的讨论中，我们会经常区分“慢”和“快”分词器。慢速分词器是在 🤗 Transformers 库中用 Python 编写的，而快速版本是由 🤗 分词器提供的，它们是用 Rust 编写的。如果你还记得在[Chapter 5](/course/chapter5/3)中报告了快速和慢速分词器对药物审查数据集进行分词所需的时间的这张表，您应该知道为什么我们称它们为“快”和“慢”：

|               | Fast tokenizer | Slow tokenizer
:--------------:|:--------------:|:-------------:
`batched=True`  | 10.8s          | 4min41s
`batched=False` | 59.2s          | 5min3s

<Tip warning={true}>

⚠️ 对单个句子进行分词时，您不会总是看到相同分词器的慢速和快速版本之间的速度差异。事实上，快速版本实际上可能更慢！只有同时对大量文本进行标记时，您才能清楚地看到差异。

</Tip>

## 批量编码 [[批量编码]]

<Youtube id="3umI3tm27Vw"/>

分词器的输出不是简单的 Python 字典；我们得到的实际上是一个特殊的 **BatchEncoding** 目的。它是字典的子类（这就是为什么我们之前能够毫无问题地索引到该结果中的原因），但具有主要由快速标记器使用的附加方法。

除了它们的并行化能力之外，快速标记器的关键功能是它们始终跟踪最终标记来自的原始文本范围——我们称之为偏移映射.这反过来又解锁了诸如将每个单词映射到它生成的标记或将原始文本的每个字符映射到它内部的标记等功能，反之亦然。让我们看一个例子：

```py
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
example = "My name is Sylvain and I work at Hugging Face in Brooklyn."
encoding = tokenizer(example)
print(type(encoding))
```

如前所述，我们得到一个 **BatchEncoding** 标记器输出中的对象：

```python out
<class 'transformers.tokenization_utils_base.BatchEncoding'>
```

由于 **AutoTokenizer** 类默认选择快速标记器，我们可以使用附加方法 this **BatchEncoding** 对象提供。我们有两种方法来检查我们的分词器是快的还是慢的。我们可以检查 **is_fast** 的属性 **tokenizer** ：

```python
tokenizer.is_fast
```

```python out
True
```

或检查我们的相同属性 **encoding** ：

```python
encoding.is_fast
```

```python out
True
```

让我们看看快速标记器使我们能够做什么。首先，我们可以访问令牌而无需将 ID 转换回令牌：

```py
encoding.tokens()
```

```python out
['[CLS]', 'My', 'name', 'is', 'S', '##yl', '##va', '##in', 'and', 'I', 'work', 'at', 'Hu', '##gging', 'Face', 'in',
 'Brooklyn', '.', '[SEP]']
```

在这种情况下，索引 5 处的令牌是 **##yl** ，它是原始句子中“Sylvain”一词的一部分。我们也可以使用 **word_ids()** 获取每个标记来自的单词索引的方法：

```py
encoding.word_ids()
```

```python out
[None, 0, 1, 2, 3, 3, 3, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11, 12, None]
```

我们可以看到分词器的特殊标记 **[CLS]** 和 **[SEP]** 被映射到 **None** ，然后每个标记都映射到它起源的单词。这对于确定一个标记是否在单词的开头或两个标记是否在同一个单词中特别有用。我们可以依靠 **##** 前缀，但它仅适用于类似 BERT 的分词器；这种方法适用于任何类型的标记器，只要它是快速的。在下一章中，我们将看到如何使用此功能将每个单词的标签正确应用于命名实体识别 (NER) 和词性 (POS) 标记等任务中的标记。我们还可以使用它来屏蔽来自屏蔽语言建模中来自同一单词的所有标记（一种称为全词掩码）。

<Tip>

一个词是什么的概念很复杂。例如，“I'll”（“I will”的缩写）算一两个词吗？它实际上取决于分词器和它应用的预分词操作。一些标记器只是在空格上拆分，因此他们会将其视为一个词。其他人在空格顶部使用标点符号，因此将其视为两个词。

✏️ 试试看！从bert base cased和roberta base检查点创建一个标记器，并用它们标记“81s”。你观察到了什么？ID这个词是什么？

</Tip>

同样，有一个 **sentence_ids()** 我们可以用来将标记映射到它来自的句子的方法（尽管在这种情况下， **token_type_ids** 分词器返回的信息可以为我们提供相同的信息）。

最后，我们可以将任何单词或标记映射到原始文本中的字符，反之亦然，通过 **word_to_chars()** 或者 **token_to_chars()** 和 **char_to_word()** 或者 **char_to_token()** 方法。例如， **word_ids()** 方法告诉我们 **##yl** 是索引 3 处单词的一部分，但它是句子中的哪个单词？我们可以这样发现：

```py
start, end = encoding.word_to_chars(3)
example[start:end]
```

```python out
Sylvain
```

正如我们之前提到的，这一切都是由快速标记器跟踪每个标记来自列表中的文本跨度这一事实提供支持的抵消.为了说明它们的用途，接下来我们将向您展示如何复制结果 **token-classification** 手动管道。

<Tip>

✏️ 试试看！创建您自己的示例文本，看看您是否能理解哪些标记与单词 ID 相关联，以及如何提取单个单词的字符跨度。对于奖励积分，请尝试使用两个句子作为输入，看看句子 ID 是否对您有意义。

</Tip>

## 在令牌分类管道内 [[在令牌分类管道内]]

在[Chapter 1](/course/chapter1)我们第一次尝试使用 NER——任务是识别文本的哪些部分对应于个人、地点或组织等实体——使用 🤗 Transformers **pipeline()** 功能。然后，在[Chapter 2](/course/chapter2)，我们看到了管道如何将从原始文本中获取预测所需的三个阶段组合在一起：标记化、通过模型传递输入和后处理。前两步 **token-classification** 管道与任何其他管道相同，但后处理稍微复杂一些 - 让我们看看如何！

{#if fw === 'pt'}

<Youtube id="0E7ltQB7fM8"/>

{:else}

<Youtube id="PrX4CjrVnNc"/>

{/if}

### 通过管道获得基本结果 [[通过管道获得基本结果]]

首先，让我们获取一个标记分类管道，以便我们可以手动比较一些结果。默认使用的模型是[dbmdz/bert-large-cased-finetuned-conll03-english](https://huggingface.co/dbmdz/bert-large-cased-finetuned-conll03-english);它对句子执行 NER：

```py
from transformers import pipeline

token_classifier = pipeline("token-classification")
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
```

```python out
[{'entity': 'I-PER', 'score': 0.9993828, 'index': 4, 'word': 'S', 'start': 11, 'end': 12},
 {'entity': 'I-PER', 'score': 0.99815476, 'index': 5, 'word': '##yl', 'start': 12, 'end': 14},
 {'entity': 'I-PER', 'score': 0.99590725, 'index': 6, 'word': '##va', 'start': 14, 'end': 16},
 {'entity': 'I-PER', 'score': 0.9992327, 'index': 7, 'word': '##in', 'start': 16, 'end': 18},
 {'entity': 'I-ORG', 'score': 0.97389334, 'index': 12, 'word': 'Hu', 'start': 33, 'end': 35},
 {'entity': 'I-ORG', 'score': 0.976115, 'index': 13, 'word': '##gging', 'start': 35, 'end': 40},
 {'entity': 'I-ORG', 'score': 0.98879766, 'index': 14, 'word': 'Face', 'start': 41, 'end': 45},
 {'entity': 'I-LOC', 'score': 0.99321055, 'index': 16, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

该模型正确地将“Sylvain”生成的每个标记识别为一个人，将“Hugging Face”生成的每个标记识别为一个组织，将“Brooklyn”生成的标记识别为一个位置。我们还可以要求管道将对应于同一实体的令牌组合在一起：

```py
from transformers import pipeline

token_classifier = pipeline("token-classification", aggregation_strategy="simple")
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
```

```python out
[{'entity_group': 'PER', 'score': 0.9981694, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.97960204, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.99321055, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

**aggregation_strategy** 选择将更改为每个分组实体计算的分数。和 **simple** 分数只是给定实体中每个标记的分数的平均值：例如，“Sylvain”的分数是我们在前面的示例中看到的标记分数的平均值 **S** , **##yl** , **##va** ， 和 **##in** .其他可用的策略是：

- `"first"`, 其中每个实体的分数是该实体的第一个标记的分数（因此对于“Sylvain”，它将是 0.993828，标记的分数)

- `"max"`,其中每个实体的分数是该实体中标记的最大分数（因此对于“Hugging Face”，它将是 0.98879766，即“Face”的分数）

- `"average"`, 其中每个实体的分数是组成该实体的单词分数的平均值（因此对于“Sylvain”，与“simple”策略，但“Hugging Face”的得分为 0.9819，“Hugging”得分的平均值为 0.975，“Face”得分为 0.98879）

现在让我们看看如何在不使用pipeline（）函数的情况下获得这些结果！

### 从输入到预测 [[从输入到预测]]

{#if fw === 'pt'}

首先，我们需要标记我们的输入并将其传递给模型。这是完全按照[Chapter 2](/course/chapter2);我们使用 **AutoXxx** 类，然后在我们的示例中使用它们：

```py
from transformers import AutoTokenizer, AutoModelForTokenClassification

model_checkpoint = "dbmdz/bert-large-cased-finetuned-conll03-english"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
model = AutoModelForTokenClassification.from_pretrained(model_checkpoint)

example = "My name is Sylvain and I work at Hugging Face in Brooklyn."
inputs = tokenizer(example, return_tensors="pt")
outputs = model(**inputs)
```

由于我们正在使用 **AutoModelForTokenClassification** 在这里，我们为输入序列中的每个标记获得一组 logits：

```py
print(inputs["input_ids"].shape)
print(outputs.logits.shape)
```

```python out
torch.Size([1, 19])
torch.Size([1, 19, 9])
```

{:else}

首先，我们需要标记我们的输入并将其传递给模型。这是完全按照[Chapter 2](/course/chapter2);我们使用 **AutoXxx** 类，然后在我们的示例中使用它们：

```py
from transformers import AutoTokenizer, TFAutoModelForTokenClassification

model_checkpoint = "dbmdz/bert-large-cased-finetuned-conll03-english"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
model = TFAutoModelForTokenClassification.from_pretrained(model_checkpoint)

example = "My name is Sylvain and I work at Hugging Face in Brooklyn."
inputs = tokenizer(example, return_tensors="tf")
outputs = model(**inputs)
```

于我们正在使用 **AutoModelForTokenClassification** 在这里，我们为输入序列中的每个标记获得一组 logits：

```py
print(inputs["input_ids"].shape)
print(outputs.logits.shape)
```

```python out
(1, 19)
(1, 19, 9)
```

{/if}

我们有一个包含 19 个标记的 1 个序列的批次，模型有 9 个不同的标签，因此模型的输出具有 1 x 19 x 9 的形状。与文本分类管道一样，我们使用 softmax 函数来转换这些 logits到概率，我们采用 argmax 来获得预测（请注意，我们可以在 logits 上采用 argmax，因为 softmax 不会改变顺序）：

{#if fw === 'pt'}

```py
import torch

probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)[0].tolist()
predictions = outputs.logits.argmax(dim=-1)[0].tolist()
print(predictions)
```

{:else}

```py
import tensorflow as tf

probabilities = tf.math.softmax(outputs.logits, axis=-1)[0]
probabilities = probabilities.numpy().tolist()
predictions = tf.math.argmax(outputs.logits, axis=-1)[0]
predictions = predictions.numpy().tolist()
print(predictions)
```

{/if}

```python out
[0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 6, 6, 6, 0, 8, 0, 0]
```

 **model.config.id2label** 属性包含索引到标签的映射，我们可以用它来理解预测：

```py
model.config.id2label
```

```python out
{0: 'O',
 1: 'B-MISC',
 2: 'I-MISC',
 3: 'B-PER',
 4: 'I-PER',
 5: 'B-ORG',
 6: 'I-ORG',
 7: 'B-LOC',
 8: 'I-LOC'}
```

正如我们之前看到的，有 9 个标签： **O** 是不在任何命名实体中的标记的标签（它代表“外部”），然后我们为每种类型的实体（杂项、人员、组织和位置）提供两个标签。标签 **B-XXX** 表示令牌在实体的开头 **XXX** 和标签 **I-XXX** 表示令牌在实体内 **XXX** .例如，在当前示例中，我们希望我们的模型对令牌进行分类 **S** 作为 **B-PER** （一个人实体的开始）和令牌 **##yl** , **##va** 和 **##in** 作为 **I-PER** （在个人实体内）

在这种情况下，您可能认为模型是错误的，因为它给出了标签 **I-PER** 对所有这四个令牌，但这并不完全正确。实际上有两种格式 **B-** 和 **I-** 标签：IOB1和IOB2. IOB2 格式（下面粉红色）是我们介绍的格式，而在 IOB1 格式（蓝色）中，标签以 **B-** 仅用于分隔相同类型的两个相邻实体。我们使用的模型在使用该格式的数据集上进行了微调，这就是它分配标签的原因 **I-PER** 到 **S** 令牌。

<div class="flex justify-center">
<img class="block dark:hidden" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter6/IOB_versions.svg" alt="IOB1 vs IOB2 format"/>
<img class="hidden dark:block" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter6/IOB_versions-dark.svg" alt="IOB1 vs IOB2 format"/>
</div>

了这张地图，我们已经准备好（几乎完全）重现第一个管道的结果——我们可以获取每个未被归类为的标记的分数和标签 **O** ：

```py
results = []
tokens = inputs.tokens()

for idx, pred in enumerate(predictions):
    label = model.config.id2label[pred]
    if label != "O":
        results.append(
            {"entity": label, "score": probabilities[idx][pred], "word": tokens[idx]}
        )

print(results)
```

```python out
[{'entity': 'I-PER', 'score': 0.9993828, 'index': 4, 'word': 'S'},
 {'entity': 'I-PER', 'score': 0.99815476, 'index': 5, 'word': '##yl'},
 {'entity': 'I-PER', 'score': 0.99590725, 'index': 6, 'word': '##va'},
 {'entity': 'I-PER', 'score': 0.9992327, 'index': 7, 'word': '##in'},
 {'entity': 'I-ORG', 'score': 0.97389334, 'index': 12, 'word': 'Hu'},
 {'entity': 'I-ORG', 'score': 0.976115, 'index': 13, 'word': '##gging'},
 {'entity': 'I-ORG', 'score': 0.98879766, 'index': 14, 'word': 'Face'},
 {'entity': 'I-LOC', 'score': 0.99321055, 'index': 16, 'word': 'Brooklyn'}]
```

这与我们之前的情况非常相似，只有一个例外：管道还为我们提供了有关 **start** 和 **end** 原始句子中的每个实体。这是我们的偏移映射将发挥作用的地方。要获得偏移量，我们只需要设置 **return_offsets_mapping=True** 当我们将分词器应用于我们的输入时：

```py
inputs_with_offsets = tokenizer(example, return_offsets_mapping=True)
inputs_with_offsets["offset_mapping"]
```

```python out
[(0, 0), (0, 2), (3, 7), (8, 10), (11, 12), (12, 14), (14, 16), (16, 18), (19, 22), (23, 24), (25, 29), (30, 32),
 (33, 35), (35, 40), (41, 45), (46, 48), (49, 57), (57, 58), (0, 0)]
```

每个元组是对应于每个标记的文本跨度，其中 **(0, 0)** 保留用于特殊令牌。我们之前看到索引 5 处的令牌是 **##yl** ， 其中有 **(12, 14)** 作为这里的抵消。如果我们在示例中抓取相应的切片：


```py
example[12:14]
```

我们得到了正确的文本跨度，而没有 **##** ：

```python out
yl
```

使用这个，我们现在可以完成之前的结果：

```py
results = []
inputs_with_offsets = tokenizer(example, return_offsets_mapping=True)
tokens = inputs_with_offsets.tokens()
offsets = inputs_with_offsets["offset_mapping"]

for idx, pred in enumerate(predictions):
    label = model.config.id2label[pred]
    if label != "O":
        start, end = offsets[idx]
        results.append(
            {
                "entity": label,
                "score": probabilities[idx][pred],
                "word": tokens[idx],
                "start": start,
                "end": end,
            }
        )

print(results)
```

```python out
[{'entity': 'I-PER', 'score': 0.9993828, 'index': 4, 'word': 'S', 'start': 11, 'end': 12},
 {'entity': 'I-PER', 'score': 0.99815476, 'index': 5, 'word': '##yl', 'start': 12, 'end': 14},
 {'entity': 'I-PER', 'score': 0.99590725, 'index': 6, 'word': '##va', 'start': 14, 'end': 16},
 {'entity': 'I-PER', 'score': 0.9992327, 'index': 7, 'word': '##in', 'start': 16, 'end': 18},
 {'entity': 'I-ORG', 'score': 0.97389334, 'index': 12, 'word': 'Hu', 'start': 33, 'end': 35},
 {'entity': 'I-ORG', 'score': 0.976115, 'index': 13, 'word': '##gging', 'start': 35, 'end': 40},
 {'entity': 'I-ORG', 'score': 0.98879766, 'index': 14, 'word': 'Face', 'start': 41, 'end': 45},
 {'entity': 'I-LOC', 'score': 0.99321055, 'index': 16, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

这和我们从第一个管道中得到的一样！

### 分组实体 [[分组实体]]

使用偏移量来确定每个实体的开始和结束键很方便，但该信息并不是绝对必要的。然而，当我们想要将实体组合在一起时，偏移量将为我们节省大量混乱的代码。例如，如果我们想将令牌组合在一起 **Hu** , **##gging** ， 和 **Face** ，我们可以制定特殊的规则，说前两个应该附加，同时删除 **##** ，以及 **Face** 应该添加一个空格，因为它不以 **##** — 但这仅适用于这种特定类型的标记器。我们必须为 SentencePiece 或 Byte-Pair-Encoding 分词器（本章稍后讨论）。

编写另一组规则。使用偏移量，所有自定义代码都消失了：我们可以在原始文本中获取从第一个标记开始到最后一个标记结束的跨度。所以，在令牌的情况下 **Hu** , **##gging** ， 和 **Face** ，我们应该从字符 33（开始 **Hu** ) 并在字符 45 之前结束（结束 **Face** )：

```py
example[33:45]
```

```python out
Hugging Face
```

为了编写在对实体进行分组的同时对预测进行后处理的代码，我们将连续并标记为的实体分组在一起 **I-XXX** ，除了第一个，可以标记为 **B-XXX** 或者 **I-XXX** （因此，当我们得到一个实体时，我们停止对实体进行分组 **O** ，一种新型实体，或 **B-XXX** 这告诉我们一个相同类型的实体正在启动）：

```py
import numpy as np

results = []
inputs_with_offsets = tokenizer(example, return_offsets_mapping=True)
tokens = inputs_with_offsets.tokens()
offsets = inputs_with_offsets["offset_mapping"]

idx = 0
while idx < len(predictions):
    pred = predictions[idx]
    label = model.config.id2label[pred]
    if label != "O":
        # Remove the B- or I-
        label = label[2:]
        start, _ = offsets[idx]

        # Grab all the tokens labeled with I-label
        all_scores = []
        while (
            idx < len(predictions)
            and model.config.id2label[predictions[idx]] == f"I-{label}"
        ):
            all_scores.append(probabilities[idx][pred])
            _, end = offsets[idx]
            idx += 1

        # The score is the mean of all the scores of the tokens in that grouped entity
        score = np.mean(all_scores).item()
        word = example[start:end]
        results.append(
            {
                "entity_group": label,
                "score": score,
                "word": word,
                "start": start,
                "end": end,
            }
        )
    idx += 1

print(results)
```

我们得到了与第二条管道相同的结果！

```python out
[{'entity_group': 'PER', 'score': 0.9981694, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.97960204, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.99321055, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

这些偏移量非常有用的另一个任务示例是问答。深入研究这个管道，我们将在下一节中进行，也将使我们能够了解 🤗 Transformers 库中标记器的最后一个功能：当我们将输入截断为给定长度时处理溢出的标记。
