在機器學習專案中,有效管理和處理資料集至關重要。Hugging Face Hub 提供了一個便捷的平台,方便分享和存取資料集。本文將逐步說明如何使用 huggingface-cli 將資料集上傳至 Hugging Face Hub,並探討如何為程式碼資料訓練客製化的 Tokenizer。由於程式碼具有獨特的結構和語法,使用通用 Tokenizer 可能無法達到最佳效能。因此,針對程式碼資料訓練專用的 Tokenizer 變得相當重要。文章將深入 BPE 演算法的原理,並提供 Python 程式碼範例,示範如何使用 Transformers 函式庫訓練和評估 Tokenizer 的效能。同時,也將探討 GPT-2 Tokenizer 的位元級別處理機制,以及如何利用這個特性來處理程式碼資料。

將資料集上傳到Hugging Face Hub

將我們的資料集上傳到Hugging Face Hub有幾個好處:

  • 可以輕易地從訓練伺服器存取它。
  • 可以展示串流資料集如何與Hub上的資料集無縫協作。
  • 可以與社群(包括讀者)分享。

首先,我們需要透過執行以下命令登入我們的Hugging Face帳戶:

$ huggingface-cli login

然後,我們可以建立新的資料集並上傳壓縮的JSON檔案。為了簡化流程,我們會為訓練集和驗證集分別建立兩個儲存函式庫。

建立和上傳資料集的步驟

  1. 使用huggingface-cli repo create命令建立兩個新的資料集儲存函式庫:一個用於訓練集,另一個用於驗證集。
  2. 將壓縮的JSON檔案複製到對應的本地儲存函式庫中。
  3. 提交更改並將檔案上傳到Hub。
$ huggingface-cli repo create --type dataset --organization transformersbook codeparrot-train
$ huggingface-cli repo create --type dataset --organization transformersbook codeparrot-valid

$ git clone https://huggingface.co/datasets/transformersbook/codeparrot-train
$ git clone https://huggingface.co/datasets/transformersbook/codeparrot-valid

$ cd codeparrot-train
$ cp ../codeparrot/*.json.gz .
$ rm ./file-000000000183.json.gz
$ git add .
$ git commit -m "Adding dataset files"
$ git push

$ cd ../codeparrot-valid
$ cp ../codeparrot/file-000000000183.json.gz .
$ mv ./file-000000000183.json.gz ./file-000000000183_validation.json.gz
$ git add .
$ git commit -m "Adding dataset files"
$ git push

內容解密:

  • huggingface-cli repo create用於在Hub上建立新的資料集儲存函式庫。
  • --type dataset指定儲存函式庫型別為資料集。
  • --organization transformersbook指定儲存函式庫所屬的組織。
  • git clone用於將遠端儲存函式庫克隆到本地。
  • 將JSON檔案複製到本地儲存函式庫,並使用git add .git commitgit push將更改上傳到Hub。

這樣,我們就成功地將資料集上傳到了Hugging Face Hub,並可以透過串流技術存取它。

建立Tokenizer

現在我們已經收集並載入了大型資料集,接下來看看如何有效地處理資料以供給模型使用。在之前的章節中,我們使用了與模型配套的分詞器(Tokenizer)。這是因為這些模型是在特定的預處理流程下進行預訓練的,使用相同的分詞器可以確保輸入模型的資料分佈一致。

然而,當我們訓練一個新模型時,使用為其他資料集準備的分詞器可能會不是最優選擇。以下是一些使用現有分詞器可能遇到的問題:

為何需要自定義Tokenizer

  1. 不同資料特性的適應性:不同的資料集可能具有不同的語言特性、詞彙分佈等,使用現有的分詞器可能無法最佳地捕捉新資料集的特性。
  2. 詞彙表的不匹配:如果現有分詞器的詞彙表與新資料集的詞彙分佈不匹配,可能會導致未知詞(unknown tokens)的增加,影響模型的效能。

因此,當我們從頭開始訓練一個新模型時,建立一個適合特定資料集的分詞器是非常重要的。接下來的章節將探討如何為我們的程式碼資料集建立一個合適的分詞器。

自定義Python程式碼的分詞器(Tokenizer)訓練與實作

在自然語言處理(NLP)任務中,分詞器(Tokenizer)扮演著至關重要的角色。分詞器的品質直接影響模型的效能,尤其是在處理特定領域的文字,如程式碼時。本篇文章將探討如何為Python程式碼訓練一個自定義的分詞器,以滿足特定的需求。

為何需要自定義分詞器?

現有的分詞器,如T5和CamemBERT的分詞器,都是針對特定的語料函式庫進行訓練的。例如,T5的分詞器在訓練過程中使用了大量的停用詞過濾,因此對於某些常見的英文單詞如“sex”可能會將其拆分成多個子詞(subwords)。同樣地,CamemBERT的分詞器主要針對法語文字進行訓練,因此對於英文單詞如“being”也可能無法正確處理。

from transformers import AutoTokenizer

def tok_list(tokenizer, string):
    input_ids = tokenizer(string, add_special_tokens=False)["input_ids"]
    return [tokenizer.decode(tok) for tok in input_ids]

tokenizer_T5 = AutoTokenizer.from_pretrained("t5-base")
tokenizer_camembert = AutoTokenizer.from_pretrained("camembert-base")

print(f'T5 tokens for "sex": {tok_list(tokenizer_T5, "sex")}')
print(f'CamemBERT tokens for "being": {tok_list(tokenizer_camembert, "being")}')

內容解密:

  1. 匯入必要的函式庫:使用transformers函式庫中的AutoTokenizer來載入預訓練的分詞器。
  2. 定義tok_list函式:該函式接受一個分詞器和一個字串,傳回該字串被分詞後的結果列表。
  3. 載入T5和CamemBERT的分詞器:使用from_pretrained方法分別載入T5和CamemBERT的預訓練分詞器。
  4. 測試分詞結果:列印出T5分詞器對“sex”和CamemBERT分詞器對“being”的分詞結果。

訓練自定義分詞器的必要性

由於現有的分詞器可能無法滿足特定任務的需求,因此訓練一個自定義的分詞器是必要的。訓練分詞器的過程與訓練模型不同,它不涉及反向傳播或權重的調整,而是建立一個從文字到整數列表的最佳對映。

分詞器模型

一個分詞器通常由四個步驟組成:標準化(normalization)、預分詞(pretokenization)、分詞器模型(tokenizer model)和後處理(postprocessing)。其中,分詞器模型是可以根據資料進行訓練的部分。常見的分詞演算法包括BPE(Byte Pair Encoding)、WordPiece和Unigram。

BPE演算法

BPE從單個字元開始,逐步合併最常出現的字元對,直到達到預定的詞彙大小。

Unigram演算法

Unigram則是從包含所有單詞和潛在子詞的初始詞彙開始,逐步刪除或拆分不太有用的標記,直到達到目標詞彙大小。

衡量分詞器的效能

評估分詞器的效能具有挑戰性,可以透過以下指標進行衡量:

  • 子詞豐富度(Subword fertility):平均每個單詞被拆分成多少個子詞。
  • 連續單詞比例:被拆分成至少兩個子詞的單詞比例。
  • 覆寫率指標:未知單詞或罕見標記的比例。

此外,還需要考慮分詞器對拼寫錯誤或噪音的魯棒性。

為Python程式碼建立自定義分詞器

由於Python程式碼具有特定的語法和結構,直接使用自然語言的分詞器可能並不合適。例如,空白符在Python中具有重要的語義意義,因此保留空白符是必要的。

from transformers import AutoTokenizer

python_code = r"""def say_hello():
    print("Hello, World!")
    # Print it
    say_hello()
"""

tokenizer = AutoTokenizer.from_pretrained("gpt2")
print(tokenizer(python_code).tokens())

內容解密:

  1. 匯入GPT-2分詞器:GPT-2使用的是根據位元組級別的分詞器,可以保留空白符。
  2. 定義Python程式碼範例:提供一段簡單的Python函式範例。
  3. 測試GPT-2分詞器:列印出GPT-2分詞器對Python程式碼的分詞結果。

建構高效能的Tokenizer:原理與實作解析

在處理自然語言或程式碼時,Tokenizer扮演著至關重要的角色。Python內建的tokenize模組能夠將Python程式碼字串分割成有意義的單元,如程式碼操作、註解、縮排和反縮排等。然而,這種根據Python的預先標記化(pretokenization)方法通常較為緩慢,並且受限於Python全域直譯器鎖(GIL)。另一方面,Transformers函式庫中的大多數Tokenizer是由Tokenizers函式庫提供,並且是以Rust語言編寫。

為何選擇Rust Tokenizers?

Rust Tokenizers在訓練和使用的速度上遠遠超過Python實作。考慮到語料函式庫的規模,我們很可能會選擇使用這些Rust Tokenizers。下面讓我們深入瞭解Tokenizer的工作原理。

瞭解GPT-2 Tokenizer的內部運作

首先,我們觀察GPT-2 Tokenizer的正規化(normalization)過程:

print(tokenizer.backend_tokenizer.normalizer)
None

結果顯示GPT-2 Tokenizer不進行任何正規化,直接處理原始的Unicode輸入。

接下來,看看預先標記化(pretokenization)的結果:

print(tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(python_code))

輸出結果包含許多特殊的符號,如ĠĊ,以及伴隨每個token的數字。這些數字代表了原始字串中每個token對應的位置。例如,單詞hello對應到原始字串中的第8到13個字元。

內容解密:

  1. pre_tokenize_str方法將輸入的Python程式碼進行預先標記化處理。
  2. 輸出結果中的每個tuple包含兩個元素:第一個是token本身,第二個是該token在原始字串中的起始和結束位置。
  3. 這些位置資訊對於理解token與原始輸入之間的對應關係至關重要。

位元級別處理:為何及如何?

GPT-2 Tokenizer採用位元級別(byte-level)處理,這意味著它直接操作位元組而非Unicode字元。每個Unicode字元由1到4個位元組組成,取決於字元本身。這種方法的優點在於,雖然Unicode字元集龐大(143,859個字元),但位元組字母表僅包含256個元素。因此,可以將任何Unicode字串表示為由這256個值組成的更長的字串。

程式碼範例:觀察字元的位元組表示

a, e = u"a", u"€"
byte = ord(a.encode("utf-8"))
print(f'`{a}` is encoded as `{a.encode("utf-8")}` with a single byte: {byte}')
byte = [ord(chr(i)) for i in e.encode("utf-8")]
print(f'`{e}` is encoded as `{e.encode("utf-8")}` with three bytes: {byte}')

輸出結果:

`a` is encoded as `b'a'` with a single byte: 97
`€` is encoded as `b'\xe2\x82\xac'` with three bytes: [226, 130, 172]

內容解密:

  1. 程式碼展示瞭如何將Unicode字元編碼為UTF-8位元組序列。
  2. 簡單字元如a被編碼為單一位元組,而複雜字元如則被編碼為三個位元組。
  3. 這種表示方法使得模型能夠以統一的方式處理所有Unicode字元。

建構詞彙表的策略

直接使用256個位元組值作為詞彙表雖然簡單,但會導致輸入序列被分割成許多小片段,增加模型的計算負擔。另一方面,若使用完整的Unicode字元集作為詞彙表,又會導致詞彙表過於龐大。

BPE演算法:中庸之道

BPE(Byte Pair Encoding)演算法透過逐步合併最頻繁出現的位元組對來擴充詞彙表,從而在詞彙表大小和輸入序列長度之間取得平衡。這種方法使得模型能夠高效地表示常見的字串模式。

訓練Tokenizer:從零開始的技術解析

Byte-Pair Encoding(BPE)演算法的原理與應用

Byte-Pair Encoding(BPE)是一種由Philip Gage於1994年提出的資料壓縮技術,最初用於對位元組進行操作。在自然語言處理(NLP)領域,BPE演算法被廣泛應用於子詞(subword)分割。與其名稱所暗示的不同,標準的BPE演算法通常對Unicode字串進行操作,而非位元組。

BPE演算法的核心概念

  1. 基礎詞彙的建立:BPE演算法首先將輸入的文字分解為個別的位元組或字元。對於GPT-2 Tokenizer而言,它首先將256個可能的位元組值對映到Unicode字串,以便標準的BPE演算法能夠處理這些字串。

  2. 合併最常出現的符號對:BPE演算法的核心步驟是重複合併最常出現的相鄰符號對,直到達到預定的詞彙大小。這種方法使得模型能夠有效地處理罕見詞彙和新詞。

GPT-2 Tokenizer中的BPE實作

GPT-2 Tokenizer使用BPE演算法,並對其進行了一些修改以適應位元組層級的輸入。具體來說,它首先將輸入的位元組對映到Unicode字元,然後再應用BPE演算法。

from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode
byte_to_unicode_map = bytes_to_unicode()
unicode_to_byte_map = dict((v, k) for k, v in byte_to_unicode_map.items())
base_vocab = list(unicode_to_byte_map.keys())
print(f'Size of our base vocabulary: {len(base_vocab)}')
print(f'First element: `{base_vocab[0]}`, last element: `{base_vocab[-1]}`')

內容解密:

  • 這段程式碼演示瞭如何取得GPT-2 Tokenizer中使用的位元組到Unicode字元的對映表。
  • bytes_to_unicode()函式傳回一個字典,將0到255的位元組值對映到對應的Unicode字元。
  • 透過反轉這個字典,我們可以得到Unicode字元到位元組值的對映。
  • 最後,程式碼列印出基礎詞彙的大小以及第一個和最後一個元素。

訓練自定義的Tokenizer

為了使Tokenizer更好地適應特定的領域(如Python程式碼),我們可以重新訓練它。訓練過程包括指定目標詞彙大小、準備輸入字串的迭代器,並呼叫train_new_from_iterator()方法。

訓練步驟

  1. 指定目標詞彙大小:決定新的Tokenizer應該具有多少個詞彙。

  2. 準備輸入資料:提供一個迭代器,該迭代器能夠產生用於訓練Tokenizer的輸入字串列表。

  3. 呼叫訓練方法:使用Transformers函式庫提供的train_new_from_iterator()方法來訓練新的Tokenizer。

# 假設我們已經有了一個tokenizer例項和一個corpus
tokenizer.train_new_from_iterator(corpus, vocab_size=52000)

內容解密:

  • 這段程式碼展示瞭如何使用Transformers函式庫訓練一個新的Tokenizer。
  • corpus是一個迭代器,提供用於訓練的輸入字串。
  • vocab_size引數指定了新的Tokenizer的詞彙大小。