並獲取增強型文件體驗
開始使用
快速分詞器的特殊能力
在本節中,我們將更深入地瞭解 🤗 Transformers 中分詞器的功能。到目前為止,我們只使用它們來分詞輸入或將 ID 解碼迴文本,但分詞器——尤其是那些由 🤗 Tokenizers 庫支援的分詞器——可以做更多的事情。為了說明這些附加功能,我們將探索如何重現我們第一次在第 1 章中遇到的token-classification(我們稱為ner)和question-answering管道的結果。
在接下來的討論中,我們將經常區分“慢速”和“快速”分詞器。“慢速”分詞器是指在 🤗 Transformers 庫內部用 Python 編寫的分詞器,而“快速”版本是由 🤗 Tokenizers 提供的分詞器,它們是用 Rust 編寫的。如果你還記得第 5 章中那個報告快速和慢速分詞器分詞藥物評論資料集所需時間的表格,你應該就能理解我們為什麼稱它們為快速和慢速了。
| 快速分詞器 | 慢速分詞器 | |
|---|---|---|
batched=True | 10.8 秒 | 4 分 41 秒 |
batched=False | 59.2 秒 | 5 分 3 秒 |
⚠️ 當分詞單個句子時,你不會總能看到相同分詞器的慢速和快速版本之間速度的差異。事實上,快速版本可能反而更慢!只有在同時並行分詞大量文字時,你才能清楚地看到差異。
批次編碼
分詞器的輸出不是一個簡單的 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類預設選擇快速分詞器,因此我們可以使用此BatchEncoding物件提供的附加方法。我們有兩種方法可以檢查我們的分詞器是快速還是慢速。我們可以檢查tokenizer的is_fast屬性。
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 對您是否有意義。
Token 分類管道內部
在第 1 章中,我們首次嘗試應用 NER——其中任務是識別文字的哪些部分對應於人、地點或組織等實體——使用 🤗 Transformers 的pipeline()函式。然後,在第 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,標記S的分數)"max",其中每個實體的分數是該實體中標記的最大分數(因此對於“Hugging Face”,它將是 0.98879766,“Face”的分數)"average",其中每個實體的分數是構成該實體的單詞的分數的平均值(因此對於“Sylvain”,與"simple"策略沒有區別,但“Hugging Face”的分數將為 0.9819,“Hugging”的分數為 0.975,“Face”的分數為 0.98879)
現在讓我們看看如何在不使用pipeline()函式的情況下獲得這些結果!
從輸入到預測
首先,我們需要對輸入進行分詞並將其傳遞給模型。這與第 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])我們有一個包含 1 個序列 19 個標記的批次,模型有 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-PER分配給S標記。
有了這個對映,我們就可以(幾乎完全)重現第一個管道的結果——我們可以只獲取未分類為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'}]這與我們之前得到的結果非常相似,只有一個例外:管道還提供了有關原始句子中每個實體的start和end資訊。這就是我們的偏移量對映將發揮作用的地方。要獲取偏移量,我們只需要在將分詞器應用於輸入時設定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 庫中分詞器的最後一個功能:當我們將輸入截斷到給定長度時處理溢位標記。