NLP 課程文件

從舊的標記器訓練新的標記器

Hugging Face's logo
加入 Hugging Face 社群

並獲取增強型文件體驗

開始

從舊的標記器訓練新的標記器

Ask a Question Open In Colab Open In Studio Lab

如果語言模型在您感興趣的語言中不可用,或者您的語料庫與語言模型訓練所用的語料庫有很大差異,您很可能需要從頭開始重新訓練模型,使用適應您的資料的標記器。這將需要在您的資料集上訓練新的標記器。但這究竟意味著什麼呢?當我們在 第 2 章 中首次考察標記器時,我們看到大多數 Transformer 模型使用 *子詞標記化演算法*。為了識別哪些子詞是有趣的,並且在當前語料庫中出現頻率最高,標記器需要仔細檢視語料庫中的所有文字——這個過程我們稱之為 *訓練*。控制這種訓練的確切規則取決於所使用的標記器型別,我們將在本章後面介紹三種主要演算法。

⚠️ 訓練標記器與訓練模型不同!模型訓練使用隨機梯度下降來使每個批次的損失略微減小。它本質上是隨機的(這意味著您必須設定一些種子才能在兩次執行相同訓練時獲得相同的結果)。訓練標記器是一個統計過程,它試圖識別哪些子詞最適合給定的語料庫,用於選擇它們的精確規則取決於標記化演算法。它是確定性的,這意味著在使用相同演算法對相同語料庫進行訓練時,您始終會獲得相同的結果。

構建語料庫

🤗 Transformers 中有一個非常簡單的 API 可用於使用與現有標記器相同的特徵訓練新的標記器:AutoTokenizer.train_new_from_iterator()。為了演示其工作原理,假設我們想要從頭開始訓練 GPT-2,但使用英語以外的語言。我們的首要任務是收集該語言的訓練語料庫。為了提供每個人都能理解的示例,我們這裡不會使用俄語或漢語等語言,而是使用一種專業化的英語:Python 程式碼。

The 🤗 Datasets 庫可以幫助我們構建 Python 原始碼的語料庫。我們將使用常用的 load_dataset() 函式下載並快取 CodeSearchNet 資料集。該資料集是為 CodeSearchNet 挑戰 建立的,包含來自 GitHub 上開源庫的數百萬個函式,涵蓋多種程式語言。在這裡,我們將載入該資料集的 Python 部分

from datasets import load_dataset

# This can take a few minutes to load, so grab a coffee or tea while you wait!
raw_datasets = load_dataset("code_search_net", "python")

我們可以檢視訓練集,以檢視我們可以訪問哪些列

raw_datasets["train"]
Dataset({
    features: ['repository_name', 'func_path_in_repository', 'func_name', 'whole_func_string', 'language', 
      'func_code_string', 'func_code_tokens', 'func_documentation_string', 'func_documentation_tokens', 'split_name', 
      'func_code_url'
    ],
    num_rows: 412178
})

我們可以看到資料集將文件字串與程式碼分開,並建議對兩者進行標記化。在這裡,我們只使用 whole_func_string 列來訓練我們的標記器。我們可以透過索引到 train 集中檢視其中一個函式的示例

print(raw_datasets["train"][123456]["whole_func_string"])

這將列印以下內容

def handle_simple_responses(
      self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
    """Accepts normal responses from the device.

    Args:
      timeout_ms: Timeout in milliseconds to wait for each response.
      info_cb: Optional callback for text sent from the bootloader.

    Returns:
      OKAY packet's message.
    """
    return self._accept_responses('OKAY', info_cb, timeout_ms=timeout_ms)

我們需要做的第一件事是將資料集轉換為文字列表的 *迭代器*——例如,文字列表的列表。使用文字列表將使我們的標記器能夠更快地執行(在文字批次上訓練,而不是逐個處理文字),如果我們想要避免將所有內容都載入到記憶體中,它應該是一個迭代器。如果您的語料庫非常龐大,您將想要利用 🤗 Datasets 不會將所有內容都載入到 RAM 中,而是將資料集的元素儲存在磁碟上的這一事實。

執行以下操作將建立每個包含 1,000 個文字的文字列表,但會將所有內容都載入到記憶體中

# Don't uncomment the following line unless your dataset is small!
# training_corpus = [raw_datasets["train"][i: i + 1000]["whole_func_string"] for i in range(0, len(raw_datasets["train"]), 1000)]

使用 Python 生成器,我們可以避免 Python 在實際需要之前將任何內容載入到記憶體中。要建立這樣的生成器,您只需將方括號替換為圓括號

training_corpus = (
    raw_datasets["train"][i : i + 1000]["whole_func_string"]
    for i in range(0, len(raw_datasets["train"]), 1000)
)

這行程式碼不會獲取資料集的任何元素;它只是建立一個可以在 Python for 迴圈中使用的物件。文字只會在您需要時載入(即,當您處於需要它們的 for 迴圈步驟時),並且一次只加載 1,000 個文字。這樣,即使您正在處理龐大的資料集,也不會耗盡所有記憶體。

生成器物件的問題是它只能使用一次。因此,與這會給我們兩次返回前 10 個數字的列表不同

gen = (i for i in range(10))
print(list(gen))
print(list(gen))

我們只獲得一次,然後得到一個空列表

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]

這就是為什麼我們定義一個返回生成器的函式

def get_training_corpus():
    return (
        raw_datasets["train"][i : i + 1000]["whole_func_string"]
        for i in range(0, len(raw_datasets["train"]), 1000)
    )


training_corpus = get_training_corpus()

您還可以使用 yield 語句在 for 迴圈內定義生成器

def get_training_corpus():
    dataset = raw_datasets["train"]
    for start_idx in range(0, len(dataset), 1000):
        samples = dataset[start_idx : start_idx + 1000]
        yield samples["whole_func_string"]

這將生成與之前完全相同的生成器,但允許您使用比列表推導更復雜的邏輯。

訓練新的標記器

現在我們已經以文字批次的迭代器形式獲得了語料庫,我們就可以訓練新的標記器了。為此,我們首先需要載入我們想要與模型配對的標記器(這裡為 GPT-2)

from transformers import AutoTokenizer

old_tokenizer = AutoTokenizer.from_pretrained("gpt2")

儘管我們要訓練一個新的分詞器,但這樣做是一個好主意,可以避免從頭開始。這樣,我們就不需要指定任何關於分詞演算法或我們要使用的特殊標記的資訊;我們的新分詞器將與 GPT-2 完全相同,唯一改變的是詞彙表,它將由我們語料庫上的訓練決定。

首先,讓我們看看這個分詞器將如何處理一個示例函式。

example = '''def add_numbers(a, b):
    """Add the two numbers `a` and `b`."""
    return a + b'''

tokens = old_tokenizer.tokenize(example)
tokens
['def', 'Ġadd', '_', 'n', 'umbers', '(', 'a', ',', 'Ġb', '):', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo',
 'Ġnumbers', 'Ġ`', 'a', '`', 'Ġand', 'Ġ`', 'b', '`', '."', '""', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

這個分詞器有一些特殊符號,比如 ĠĊ,分別表示空格和換行符。正如我們所看到的,這效率不高:分詞器會為每個空格返回單個標記,而它可以將縮排級別組合在一起(因為在程式碼中會有四到八個空格的集合非常常見)。它還以一種奇怪的方式分割了函式名,不習慣看到帶有 _ 字元的單詞。

讓我們訓練一個新的分詞器,看看它是否解決了這些問題。為此,我們將使用 train_new_from_iterator() 方法

tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)

如果您的語料庫非常大,這條命令可能需要一些時間,但對於這個 1.6 GB 文字資料集來說,它非常快(在配備 12 核 AMD Ryzen 9 3900X CPU 的機器上只需 1 分鐘 16 秒)。

請注意,AutoTokenizer.train_new_from_iterator() 僅在您使用的分詞器是“快速”分詞器時才有效。正如您將在下一節中看到的那樣,🤗 Transformers 庫包含兩種型別的分詞器:一些完全用 Python 編寫,而另一些(快速的)則由 🤗 Tokenizers 庫支援,該庫是用 Rust 程式語言編寫的。Python 是資料科學和深度學習應用程式最常用的語言,但當任何東西都需要並行化才能快速執行時,它必須用另一種語言編寫。例如,模型計算的核心矩陣乘法是用 CUDA 編寫的,CUDA 是針對 GPU 最佳化的 C 庫。

用純 Python 訓練一個全新的分詞器會非常慢,這就是我們開發 🤗 Tokenizers 庫的原因。請注意,就像您不需要學習 CUDA 語言就可以在 GPU 上對一批輸入執行模型一樣,您也不需要學習 Rust 才能使用快速分詞器。 🤗 Tokenizers 庫為許多內部呼叫 Rust 程式碼片段的方法提供了 Python 繫結;例如,並行化新分詞器的訓練或,正如我們在 第 3 章 中看到的那樣,對一批輸入進行分詞。

大多數 Transformer 模型都提供了一個快速分詞器(有一些例外,您可以 在這裡 檢視),AutoTokenizer API 始終為您選擇快速分詞器(如果可用)。在下一節中,我們將看看快速分詞器的一些其他特殊功能,這些功能對於標記分類和問答等任務非常有用。但是,在深入瞭解這些功能之前,讓我們嘗試使用我們全新的分詞器來處理之前的示例。

tokens = tokenizer.tokenize(example)
tokens
['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
 'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

這裡我們再次看到了表示空格和換行符的特殊符號 ĠĊ,但我們也可以看到我們的分詞器學習了一些非常特定於 Python 函式語料庫的標記:例如,有一個 ĊĠĠĠ 標記表示縮排,一個 Ġ""" 標記表示開始文件字串的三引號。分詞器還正確地將函式名拆分為 _。這是一個相當緊湊的表示;相比之下,對同一個示例使用純英文分詞器將得到一個更長的句子。

print(len(tokens))
print(len(old_tokenizer.tokenize(example)))
27
36

讓我們看另一個例子。

example = """class LinearLayer():
    def __init__(self, input_size, output_size):
        self.weight = torch.randn(input_size, output_size)
        self.bias = torch.zeros(output_size)

    def __call__(self, x):
        return x @ self.weights + self.bias
    """
tokenizer.tokenize(example)
['class', 'ĠLinear', 'Layer', '():', 'ĊĠĠĠ', 'Ġdef', 'Ġ__', 'init', '__(', 'self', ',', 'Ġinput', '_', 'size', ',',
 'Ġoutput', '_', 'size', '):', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'weight', 'Ġ=', 'Ġtorch', '.', 'randn', '(', 'input', '_',
 'size', ',', 'Ġoutput', '_', 'size', ')', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'bias', 'Ġ=', 'Ġtorch', '.', 'zeros', '(',
 'output', '_', 'size', ')', 'ĊĊĠĠĠ', 'Ġdef', 'Ġ__', 'call', '__(', 'self', ',', 'Ġx', '):', 'ĊĠĠĠĠĠĠĠ',
 'Ġreturn', 'Ġx', 'Ġ@', 'Ġself', '.', 'weights', 'Ġ+', 'Ġself', '.', 'bias', 'ĊĠĠĠĠ']

除了對應於縮排的標記之外,這裡我們還可以看到一個表示雙重縮排的標記:ĊĠĠĠĠĠĠĠ。諸如 classinitcallselfreturn 之類的特殊 Python 詞語每個都被標記為一個標記,我們可以看到,除了在 _. 上分割之外,分詞器還正確地分割了駝峰命名法:LinearLayer 被標記為 ["ĠLinear", "Layer"]

儲存分詞器

為了確保我們以後可以使用它,我們需要儲存我們新的分詞器。與模型一樣,這可以透過 save_pretrained() 方法完成

tokenizer.save_pretrained("code-search-net-tokenizer")

這將建立一個名為 code-search-net-tokenizer 的新資料夾,其中將包含分詞器需要重新載入的所有檔案。如果您想與同事和朋友共享此分詞器,您可以透過登入您的帳戶將其上傳到 Hub。如果您在筆記本中工作,有一個方便的函式可以幫助您完成此操作

from huggingface_hub import notebook_login

notebook_login()

這將顯示一個視窗,您可以在其中輸入您的 Hugging Face 登入憑據。如果您不在筆記本中工作,只需在您的終端中鍵入以下行

huggingface-cli login

登入後,您可以透過執行以下命令推送您的分詞器

tokenizer.push_to_hub("code-search-net-tokenizer")

這將在您的名稱空間中建立一個名為 code-search-net-tokenizer 的新儲存庫,其中包含分詞器檔案。然後,您可以使用 from_pretrained() 方法從任何地方載入分詞器

# Replace "huggingface-course" below with your actual namespace to use your own tokenizer
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")

您現在已準備好從頭開始訓練語言模型,並根據您的任務對其進行微調!我們將在 第 7 章 中介紹這一點,但首先,在本章的其餘部分,我們將仔細研究快速分詞器,並詳細探討呼叫 train_new_from_iterator() 方法時實際發生的事情。

© . This site is unofficial and not affiliated with Hugging Face, Inc.