標記器(Tokenizer)
標記器(Tokenizer)是 NLP 管道的核心組件之一。它們有一個目的:將文本轉換為模型可以處理的數據。模型只能處理數字,因此標記器(Tokenizer)需要將我們的文本輸入轉換為數字數據。在本節中,我們將確切地探討標記化管道中發生的事情。
在 NLP 任務中,通常處理的數據是原始文本。這是此類文本的示例
Jim Henson was a puppeteer
但是,模型只能處理數字,因此我們需要找到一種將原始文本轉換為數字的方法。這就是標記器(tokenizer)所做的,並且有很多方法可以解決這個問題。目標是找到最有意義的表示——即對模型最有意義的表示——並且如果可能的話,找到最小的表示。
讓我們看一下標記化算法的一些示例,並嘗試回答您可能對標記化提出的一些問題。
基於詞的(Word-based)
想到的第一種標記器是基於詞的(word-based).它通常很容易設置和使用,只需幾條規則,並且通常會產生不錯的結果。例如,在下圖中,目標是將原始文本拆分為單詞併為每個單詞找到一個數字表示:
有多種方法可以拆分文本。例如,我們可以通過應用Python的split()
函數,使用空格將文本標記為單詞:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
還有一些單詞標記器的變體,它們具有額外的標點符號規則。使用這種標記器,我們最終可以得到一些非常大的“詞彙表”,其中詞彙表由我們在語料庫中擁有的獨立標記的總數定義。
每個單詞都分配了一個 ID,從 0 開始一直到詞彙表的大小。該模型使用這些 ID 來識別每個單詞。
如果我們想用基於單詞的標記器(tokenizer)完全覆蓋一種語言,我們需要為語言中的每個單詞都有一個標識符,這將生成大量的標記。例如,英語中有超過 500,000 個單詞,因此要構建從每個單詞到輸入 ID 的映射,我們需要跟蹤這麼多 ID。此外,像“dog”這樣的詞與“dogs”這樣的詞的表示方式不同,模型最初無法知道“dog”和“dogs”是相似的:它會將這兩個詞識別為不相關。這同樣適用於其他相似的詞,例如“run”和“running”,模型最初不會認為它們是相似的。
最後,我們需要一個自定義標記(token)來表示不在我們詞彙表中的單詞。這被稱為“未知”標記(token),通常表示為“[UNK]”或”<unk>“。如果你看到標記器產生了很多這樣的標記,這通常是一個不好的跡象,因為它無法檢索到一個詞的合理表示,並且你會在這個過程中丟失信息。製作詞彙表時的目標是以這樣一種方式進行,即標記器將盡可能少的單詞標記為未知標記。
減少未知標記數量的一種方法是使用更深一層的標記器(tokenizer),即基於字符的(character-based)標記器(tokenizer)。
基於字符(Character-based)
基於字符的標記器(tokenizer)將文本拆分為字符,而不是單詞。這有兩個主要好處:
- 詞彙量要小得多。
- 詞彙外(未知)標記(token)要少得多,因為每個單詞都可以從字符構建。
但是這裡也出現了一些關於空格和標點符號的問題:
這種方法也不是完美的。由於現在表示是基於字符而不是單詞,因此人們可能會爭辯說,從直覺上講,它的意義不大:每個字符本身並沒有多大意義,而單詞就是這種情況。然而,這又因語言而異;例如,在中文中,每個字符比拉丁語言中的字符包含更多的信息。
另一件要考慮的事情是,我們的模型最終會處理大量的詞符(token):雖然使用基於單詞的標記器(tokenizer),單詞只會是單個標記,但當轉換為字符時,它很容易變成 10 個或更多的詞符(token)。
為了兩全其美,我們可以使用結合這兩種方法的第三種技術:子詞標記化(subword tokenization)。
子詞標記化
子詞分詞算法依賴於這樣一個原則,即不應將常用詞拆分為更小的子詞,而應將稀有詞分解為有意義的子詞。
例如,“annoyingly”可能被認為是一個罕見的詞,可以分解為“annoying”和“ly”。這兩者都可能作為獨立的子詞出現得更頻繁,同時“annoyingly”的含義由“annoying”和“ly”的複合含義保持。
這是一個示例,展示了子詞標記化算法如何標記序列“Let’s do tokenization!”:
這些子詞最終提供了很多語義含義:例如,在上面的示例中,“tokenization”被拆分為“token”和“ization”,這兩個具有語義意義同時節省空間的詞符(token)(只需要兩個標記(token)代表一個長詞)。這使我們能夠對較小的詞彙表進行相對較好的覆蓋,並且幾乎沒有未知的標記
這種方法在土耳其語等粘著型語言(agglutinative languages)中特別有用,您可以通過將子詞串在一起來形成(幾乎)任意長的複雜詞。
還有更多!
不出所料,還有更多的技術。僅舉幾例:
- Byte-level BPE, 用於 GPT-2
- WordPiece, 用於 BERT
- SentencePiece or Unigram, 用於多個多語言模型
您現在應該對標記器(tokenizers)的工作原理有足夠的瞭解,以便開始使用 API。
加載和保存
加載和保存標記器(tokenizer)就像使用模型一樣簡單。實際上,它基於相同的兩種方法: from_pretrained()
和 save_pretrained()
。這些方法將加載或保存標記器(tokenizer)使用的算法(有點像建築學(architecture)的模型)以及它的詞彙(有點像權重(weights)模型)。
加載使用與 BERT 相同的檢查點訓練的 BERT 標記器(tokenizer)與加載模型的方式相同,除了我們使用 BertTokenizer
類:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
如同 AutoModel
,AutoTokenizer
類將根據檢查點名稱在庫中獲取正確的標記器(tokenizer)類,並且可以直接與任何檢查點一起使用:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
我們現在可以使用標記器(tokenizer),如上一節所示:
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 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]}
保存標記器(tokenizer)與保存模型相同:
tokenizer.save_pretrained("directory_on_my_computer")
我們在Chapter 3中將更多地談論token_type_ids
,稍後我們將解釋 attention_mask
鍵。首先,讓我們看看 input_ids
如何生成。為此,我們需要查看標記器(tokenizer)的中間方法。
編碼
將文本翻譯成數字被稱為編碼(encoding).編碼分兩步完成:標記化,然後轉換為輸入 ID。
正如我們所見,第一步是將文本拆分為單詞(或單詞的一部分、標點符號等),通常稱為標記(token)。有多個規則可以管理該過程,這就是為什麼我們需要使用模型名稱來實例化標記器(tokenizer),以確保我們使用模型預訓練時使用的相同規則。
第二步是將這些標記轉換為數字,這樣我們就可以用它們構建一個張量並將它們提供給模型。為此,標記器(tokenizer)有一個詞彙(vocabulary),這是我們在實例化它時下載的部分 from_pretrained()
方法。同樣,我們需要使用模型預訓練時使用的相同詞彙。
為了更好地理解這兩個步驟,我們將分別探討它們。請注意,我們將使用一些單獨執行部分標記化管道的方法來向您展示這些步驟的中間結果,但實際上,您應該直接在您的輸入上調用標記器(tokenizer)(如第 2 部分所示)。
標記化
標記化過程由標記器(tokenizer)的tokenize()
方法實現:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
此方法的輸出是一個字符串列表或標記(token):
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
這個標記器(tokenizer)是一個子詞標記器(tokenizer):它對詞進行拆分,直到獲得可以用其詞彙表表示的標記(token)。transformer
就是這種情況,它分為兩個標記:transform
和 ##er
。
從詞符(token)到輸入 ID
輸入 ID 的轉換由標記器(tokenizer)的convert_tokens_to_ids()
方法實現:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
這些輸出一旦轉換為適當的框架張量,就可以用作模型的輸入,如本章前面所見。
✏️ 試試看! 在我們在第 2 節中使用的輸入句子(“I’ve been waiting for a HuggingFace course my whole life.”和“I hate this so much!”)複製最後兩個步驟(標記化和轉換為輸入 ID)。檢查您獲得的輸入 ID 是否與我們之前獲得的相同!
解碼
解碼(Decoding) 正好相反:從詞彙索引中,我們想要得到一個字符串。這可以通過 decode()
方法實現,如下:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
請注意, decode
方法不僅將索引轉換回標記(token),還將屬於相同單詞的標記(token)組合在一起以生成可讀的句子。當我們使用預測新文本的模型(根據提示生成的文本,或序列到序列問題(如翻譯或摘要))時,這種行為將非常有用。
到現在為止,您應該瞭解標記器(tokenizer)可以處理的原子操作:標記化、轉換為 ID 以及將 ID 轉換回字符串。然而,我們只是刮到了冰山一角。在下一節中,我們將採用我們的方法來克服它的限制,並看看如何克服它們。