NLP Course documentation

快速標記器的特殊能力

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

快速標記器的特殊能力

Ask a Question Open In Colab Open In Studio Lab

在本節中,我們將仔細研究 🤗 Transformers 中標記器的功能。到目前為止,我們只使用它們來標記輸入或將 ID 解碼迴文本,但是標記器——尤其是那些由 🤗 Tokenizers 庫支持的——可以做更多的事情。為了說明這些附加功能,我們將探索如何重現結果 token-classification (我們稱之為 ner ) 和 question-answering 我們第一次在Chapter 1中遇到的管道.

在接下來的討論中,我們會經常區分“慢”和“快”分詞器。慢速分詞器是在 🤗 Transformers 庫中用 Python 編寫的,而快速版本是由 🤗 分詞器提供的,它們是用 Rust 編寫的。如果你還記得在Chapter 5中報告了快速和慢速分詞器對藥物審查數據集進行分詞所需的時間的這張表,您應該知道為什麼我們稱它們為“快”和“慢”:

Fast tokenizer Slow tokenizer
batched=True 10.8s 4min41s
batched=False 59.2s 5min3s

⚠️ 對單個句子進行分詞時,您不會總是看到相同分詞器的慢速和快速版本之間的速度差異。事實上,快速版本實際上可能更慢!只有同時對大量文本進行標記時,您才能清楚地看到差異。

批量編碼

分詞器的輸出不是簡單的 Python 字典;我們得到的實際上是一個特殊的 BatchEncoding 目的。它是字典的子類(這就是為什麼我們之前能夠毫無問題地索引到該結果中的原因),但具有主要由快速標記器使用的附加方法。

除了它們的並行化能力之外,快速標記器的關鍵功能是它們始終跟蹤最終標記來自的原始文本範圍——我們稱之為偏移映射.這反過來又解鎖了諸如將每個單詞映射到它生成的標記或將原始文本的每個字符映射到它內部的標記等功能,反之亦然。讓我們看一個例子:

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 標記器輸出中的對象:

<class 'transformers.tokenization_utils_base.BatchEncoding'>

由於 AutoTokenizer 類默認選擇快速標記器,我們可以使用附加方法 this BatchEncoding 對象提供。我們有兩種方法來檢查我們的分詞器是快的還是慢的。我們可以檢查 is_fast 的屬性 tokenizer

tokenizer.is_fast
True

或檢查我們的相同屬性 encoding

encoding.is_fast
True

讓我們看看快速標記器使我們能夠做什麼。首先,我們可以訪問令牌而無需將 ID 轉換回令牌:

encoding.tokens()
['[CLS]', 'My', 'name', 'is', 'S', '##yl', '##va', '##in', 'and', 'I', 'work', 'at', 'Hu', '##gging', 'Face', 'in',
 'Brooklyn', '.', '[SEP]']

在這種情況下,索引 5 處的令牌是 ##yl ,它是原始句子中“Sylvain”一詞的一部分。我們也可以使用 word_ids() 獲取每個標記來自的單詞索引的方法:

encoding.word_ids()
[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) 標記等任務中的標記。我們還可以使用它來屏蔽來自屏蔽語言建模中來自同一單詞的所有標記(一種稱為全詞掩碼)。

一個詞是什麼的概念很複雜。例如,“I’ll”(“I will”的縮寫)算一兩個詞嗎?它實際上取決於分詞器和它應用的預分詞操作。一些標記器只是在空格上拆分,因此他們會將其視為一個詞。其他人在空格頂部使用標點符號,因此將其視為兩個詞。

✏️ 試試看!從bert base cased和roberta base檢查點創建一個標記器,並用它們標記“81s”。你觀察到了什麼?ID這個詞是什麼?

同樣,有一個 sentence_ids() 我們可以用來將標記映射到它來自的句子的方法(儘管在這種情況下, token_type_ids 分詞器返回的信息可以為我們提供相同的信息)。

最後,我們可以將任何單詞或標記映射到原始文本中的字符,反之亦然,通過 word_to_chars() 或者 token_to_chars()char_to_word() 或者 char_to_token() 方法。例如, word_ids() 方法告訴我們 ##yl 是索引 3 處單詞的一部分,但它是句子中的哪個單詞?我們可以這樣發現:

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

正如我們之前提到的,這一切都是由快速標記器跟蹤每個標記來自列表中的文本跨度這一事實提供支持的抵消.為了說明它們的用途,接下來我們將向您展示如何複製結果 token-classification 手動管道。

✏️ 試試看!創建您自己的示例文本,看看您是否能理解哪些標記與單詞 ID 相關聯,以及如何提取單個單詞的字符跨度。對於獎勵積分,請嘗試使用兩個句子作為輸入,看看句子 ID 是否對您有意義。

在令牌分類管道內

Chapter 1我們第一次嘗試使用 NER——任務是識別文本的哪些部分對應於個人、地點或組織等實體——使用 🤗 Transformers pipeline() 功能。然後,在Chapter 2,我們看到了管道如何將從原始文本中獲取預測所需的三個階段組合在一起:標記化、通過模型傳遞輸入和後處理。前兩步 token-classification 管道與任何其他管道相同,但後處理稍微複雜一些 - 讓我們看看如何!

通過管道獲得基本結果

首先,讓我們獲取一個標記分類管道,以便我們可以手動比較一些結果。默認使用的模型是dbmdz/bert-large-cased-finetuned-conll03-english;它對句子執行 NER:

from transformers import pipeline

token_classifier = pipeline("token-classification")
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
[{'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”生成的標記識別為一個位置。我們還可以要求管道將對應於同一實體的令牌組合在一起:

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.")
[{'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()函數的情況下獲得這些結果!

從輸入到預測

首先,我們需要標記我們的輸入並將其傳遞給模型。這是完全按照Chapter 2;我們使用 AutoXxx 類,然後在我們的示例中使用它們:

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:

print(inputs["input_ids"].shape)
print(outputs.logits.shape)
torch.Size([1, 19])
torch.Size([1, 19, 9])

我們有一個包含 19 個標記的 1 個序列的批次,模型有 9 個不同的標籤,因此模型的輸出具有 1 x 19 x 9 的形狀。與文本分類管道一樣,我們使用 softmax 函數來轉換這些 logits到概率,我們採用 argmax 來獲得預測(請注意,我們可以在 logits 上採用 argmax,因為 softmax 不會改變順序):

import torch

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

model.config.id2label 屬性包含索引到標籤的映射,我們可以用它來理解預測:

model.config.id2label
{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-PERS 令牌。

IOB1 vs IOB2 format

了這張地圖,我們已經準備好(幾乎完全)重現第一個管道的結果——我們可以獲取每個未被歸類為的標記的分數和標籤 O

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

這與我們之前的情況非常相似,只有一個例外:管道還為我們提供了有關 startend 原始句子中的每個實體。這是我們的偏移映射將發揮作用的地方。要獲得偏移量,我們只需要設置 return_offsets_mapping=True 當我們將分詞器應用於我們的輸入時:

inputs_with_offsets = tokenizer(example, return_offsets_mapping=True)
inputs_with_offsets["offset_mapping"]
[(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) 作為這裡的抵消。如果我們在示例中抓取相應的切片:

example[12:14]

我們得到了正確的文本跨度,而沒有 ##

yl

使用這個,我們現在可以完成之前的結果:

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)
[{'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 ):

example[33:45]
Hugging Face

為了編寫在對實體進行分組的同時對預測進行後處理的代碼,我們將連續並標記為的實體分組在一起 I-XXX ,除了第一個,可以標記為 B-XXX 或者 I-XXX (因此,當我們得到一個實體時,我們停止對實體進行分組 O ,一種新型實體,或 B-XXX 這告訴我們一個相同類型的實體正在啟動):

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)

我們得到了與第二條管道相同的結果!

[{'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 庫中標記器的最後一個功能:當我們將輸入截斷為給定長度時處理溢出的標記。