LLM 課程文件

從舊分詞器訓練新分詞器

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 程式碼。

🤗 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()

您也可以在 `for` 迴圈中使用 `yield` 語句定義您的生成器

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 的文字資料集來說,它的速度非常快(在 AMD Ryzen 9 3900X CPU 上,12 核,僅需 1 分 16 秒)。

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

純 Python 訓練一個全新的分詞器會極其緩慢,這就是我們開發 🤗 Tokenizers 庫的原因。請注意,就像您不需要學習 CUDA 語言就能在 GPU 上對一批輸入執行模型一樣,您也不需要學習 Rust 就能使用快速分詞器。🤗 Tokenizers 庫為許多方法提供了 Python 繫結,這些方法在內部呼叫 Rust 中的一段程式碼;例如,用於並行化新分詞器的訓練,或者正如我們在第 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', 'ĊĠĠĠĠ']

除了表示縮排的標記外,這裡我們還可以看到表示雙重縮排的標記:`ĊĠĠĠĠĠĠĠ`。`class`、`init`、`call`、`self`和`return`等特殊 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()` 方法時實際發生了什麼。

< > 在 GitHub 上更新

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