目前主流的自然語言處理任務流程中,資料標註和 Tokenization 是至關重要的環節。本文首先介紹如何使用 Prodigy 工具進行高效的文字分類別資料標註,並利用 spaCy 框架訓練文字分類別模型。接著,我們將探討 Hugging Face Tokenizers 函式庫,這是一個高效能的 Tokenization 工具,尤其擅長處理 subword tokenization,並能與最新的 Transformer 模型相容。文章將比較使用少量標籤和完整資料集訓練模型的差異,並提供程式碼範例,展示如何使用這些工具和技術構建高效的 NLP 應用。

自然語言處理任務二:文字分類別

在完成命名實體識別(NER)任務後,我們接著進行第二個NLP任務:文字分類別。文字分類別是NLP中非常常見的應用,包括新聞應用程式將新聞文章分類別到根據主題的類別、電子郵件應用程式的垃圾郵件/非垃圾郵件功能,以及Facebook和其他社交平台上的真實新聞/假新聞分類別模型。

資料集準備

作為回顧,AG News訓練資料集中的所有文章已經被分類別到四個類別之一:商業(Business)、科學技術(Sci_Tech)、體育(Sports)和世界(World)。我們可以直接使用這些標籤來使用spaCy訓練文字分類別模型。然而,在現實世界中,您很少會擁有像這樣預先註解的資料集。相反,您通常需要從頭開始註解您的資料。

資料預處理與分割

為了展示如何輕鬆地註解資料並透過微調大型預訓練語言模型來生成文字分類別模型,我們將使用Prodigy從頭開始註解幾個示例。首先,我們需要準備一個CSV檔案,其中包含文字片段,列名為“text”。我們將使用AG News資料集中的標題和描述(而非僅僅是描述)作為文字片段,並在Prodigy中註解這些。

# 準備用於文字分類別的資料
textcat = data.copy()
textcat["text"] = textcat["title"] + str(" ") + textcat["description"]
textcat["label"] = textcat["class_name"]
textcat.drop(columns=["class_index","title","description","class_name"], inplace=True)

# 分割資料集為訓練集和評估集
textcat_train, textcat_eval = train_test_split(textcat, test_size=0.2, random_state=2020, stratify=textcat.label)

# 儲存資料集到CSV檔案
textcat_train.to_csv(cwd + '/data/ag_dataset/textcat/raw/train_prodigy_textcat_train_with_labels.csv', index=False)
textcat_eval.to_csv(cwd + '/data/ag_dataset/textcat/raw/train_prodigy_textcat_eval.csv', index=False)
textcat_train = textcat_train.text
textcat_train.to_csv(cwd + '/data/ag_dataset/textcat/raw/train_prodigy_textcat_train_without_labels.csv', index=False)

使用Prodigy進行資料註解

我們將使用Prodigy食譜textcat.manual來手動註解類別。這個食譜允許我們手動註解適用於文字的類別。我們使用--label標誌設定標籤,並使用--exclusive標誌將標籤指定為互斥的;換句話說,一個示例只能有一個正確的類別,而不是多個類別/標籤。

$ python -m prodigy textcat.manual <dataset> <source> --label Business,Sci_Tech,Sports,World --exclusive

內容解密:

  1. textcat.manual食譜的使用:這個命令啟動Prodigy的文字分類別註解介面,允許使用者為文字分配特定的類別標籤。
  2. --label--exclusive標誌:這些標誌確保了使用者只能為每個文字選擇一個類別,實作了類別之間的互斥性。
  3. 資料集的分割:透過將資料集分割為訓練集和評估集,我們可以評估模型的效能並進行必要的調整。

微調預訓練語言模型

在完成資料註解後,我們可以使用這些註解來微調我們的預訓練語言模型,從而實作良好的文字分類別效能。這一過程與我們在NER任務中使用的技術相似,透過利用預訓練模型的先驗知識,我們可以顯著提高模型在特定任務上的表現。

使用 Prodigy 進行文字分類別註解

在進行文字分類別任務時,首先需要對資料進行註解。Prodigy 提供了一個直觀的使用者介面來進行文字分類別註解,如 Figure 3-12 所示。

註解資料

使用 Prodigy 的文字分類別註解介面,我們可以對數百個文字進行註解。完成註解後,透過點選介面左上角的軟碟圖示來儲存這些註解。雖然更多的註解資料可以提高模型的效能,但幾百個註解應該足以建立一個基本的文字分類別模型。

將註解資料轉換為 spaCy 的 JSON 格式

一旦註解準備就緒,我們可以使用 data-to-spacy Prodigy 配方將這些註解輸出為 spaCy 的 JSON 格式。這個過程需要指定輸出路徑、語言(在我們的例子中是 “en”)、文字分類別資料集名稱(使用 --textcat 標誌),以及 --textcat-exclusive 標誌,因為我們希望將我們的類別視為互相排斥的。

$ python -m prodigy data-to-spacy <output> \
--lang en --textcat ag_data_textcat --textcat-exclusive

將 JSON 格式的資料轉換為二進位制格式

接下來,我們需要將 JSON 格式的訓練資料轉換為 spaCy 訓練所需的二進位制格式。

$ python -m spacy convert <path-to-json> <path-for-binary-output>

內容解密:

  • python -m spacy convert:這條命令用於將 JSON 格式的資料轉換為 spaCy 可以使用的二進位制格式。
  • <path-to-json>:指定 JSON 檔案的路徑。
  • <path-for-binary-output>:指定轉換後的二進位制檔案的輸出路徑。

準備評估資料集

我們還需要將 textcat_eval 資料集從 CSV 格式轉換為 JSON 格式,以便在 spaCy 中使用。首先,使用 db-in Prodigy 配方將 CSV 檔案匯入 Prodigy。

$ python -m prodigy db-in dataset in_file

然後,使用 data-to-spacy 配方將匯入的資料匯出為 spaCy 的 JSON 格式。

$ python -m prodigy data-to-spacy <output> \
--lang en --textcat ag_data_textcat_eval --textcat-exclusive

使用 spaCy 訓練文字分類別模型

現在,我們已經準備好了訓練資料和評估資料,可以開始使用 spaCy 訓練文字分類別模型了。我們將訓練兩個模型:一個使用 Prodigy 註解的少量標籤,另一個使用完整的標籤資料集。

生成組態檔案

首先,生成一個組態檔案,用於指定訓練引數,包括使用根據 transformer 的模型(RoBERTa)和進行遷移學習。

# 生成組態檔案
config_file_path_output = cwd + "/data/ag_dataset/textcat/config_final.cfg"
python -m spacy init config "$config_file_path_output" --lang en \
--pipeline textcat_multilabel --optimize efficiency --gpu --force

內容解密:

  • python -m spacy init config:這條命令用於生成 spaCy 的組態檔案。
  • --lang en:指定語言為英語。
  • --pipeline textcat_multilabel:指定管道為多標籤文字分類別。
  • --optimize efficiency:最佳化組態以提高效率。
  • --gpu:使用 GPU 加速訓練。
  • --force:強制覆寫現有的組態檔案。

開始訓練模型

使用生成的組態檔案,我們可以開始訓練文字分類別模型。

# 使用 Prodigy 註解訓練模型
import spacy

內容解密:

  • import spacy:匯入 spaCy 函式庫,用於載入和使用訓練好的模型。

訓練文字分類別模型:從少量標籤到完整資料集

本章節將探討如何利用spaCy訓練文字分類別模型,從使用少量標籤的情況到完整利用96,000個原始標籤的情況。

使用少量標籤訓練文字分類別模型

首先,我們使用Prodigy註解的少量標籤來訓練文字分類別模型。相關路徑設定如下:

annots_path = "data/ag_dataset/textcat/annotations/binary/"
output_path = cwd + "/models/ag_dataset/textcat/few_labels"
train_path = cwd + annots_path + "train_few_labels"
dev_path = cwd + annots_path + "eval"

內容解密:

  • annots_path:儲存註解資料的路徑。
  • output_path:訓練完成的模型輸出路徑。
  • train_path:訓練資料的路徑。
  • dev_path:驗證資料的路徑。

接下來,透過以下命令啟動訓練流程:

python -m spacy train "$config_file_path_output" \
--output "$output_path" --paths.train "$train_path" \
--paths.dev "$dev_path" --gpu-id 0 --training.max_epochs 30 --verbose

內容解密:

  • python -m spacy train:使用spaCy進行模型訓練的命令。
  • --output "$output_path":指定模型輸出的路徑。
  • --paths.train "$train_path":指定訓練資料的路徑。
  • --paths.dev "$dev_path":指定驗證資料的路徑。
  • --gpu-id 0:指定使用GPU的ID為0進行訓練。
  • --training.max_epochs 30:設定最大訓練輪次為30。

訓練結果分析

從Example 3-4中可以看到,經過30個epoch的訓練後,模型的F1得分約為83。儘管只使用了少量的標籤進行訓練,這樣的表現仍然相當不錯。

使用完整資料集訓練文字分類別模型

接下來,我們使用完整的96,000個原始標籤來訓練文字分類別模型。相關路徑設定如下:

annots_path = "data/ag_dataset/textcat/annotations/binary/"
output_path = cwd + "/models/ag_dataset/textcat/full_labels"
train_path = cwd + annots_path + "train_full_labels"
dev_path = cwd + annots_path + "eval"

內容解密:

  • 與前面的少量標籤訓練類別似,但輸出的路徑和訓練資料的路徑有所不同,以對應完整資料集。

訓練命令如下:

python -m spacy train "$config_file_path_output" \
--output "$output_path" --paths.train "$train_path" \
--paths.dev "$dev_path" --gpu-id 0 --training.max_epochs 1 --verbose

內容解密:

  • 注意這裡的最大訓練輪次(--training.max_epochs)被設定為1。

完整資料集訓練結果分析

從Example 3-5中可以看到,使用完整的96,000個標籤進行訓練後,模型的F1得分在僅僅一個epoch後就超過了94。這表明隨著訓練資料量的增加,模型的表現有了顯著的提升。

第四章:Tokenization

在深入瞭解自然語言處理(NLP)的基礎部分時,我們首先探討的是Tokenization。在前三章中,我們已經對NLP流程的高層元件有了基本的認識。從本章開始直到第九章,我們將探討現代NLP系統的底層細節。這些核心元件包括:

  • Tokenization
  • Embeddings
  • 模型架構

在之前的章節中,這些步驟都被我們所使用的函式庫(spaCy、transformers和fastai)抽象化了。但現在,我們將試圖瞭解這些函式庫的實際運作方式,以及如何在低層次上修改程式碼,以建立出色的NLP應用程式。

簡易的Tokenizer

在思考深度學習堆積疊的底層元件時,瞭解元件的輸入和輸出是非常有用的。那麼,這裡的輸入和輸出是什麼?輸入是文字,通常以.txt檔案的形式提供,或是讀入Python物件。輸出是一系列的tokens。本章的一個主要主題將是討論什麼是「token」,以及它應該做什麼。

text = open('example.txt', 'r').read()
words = text.split(" ")
tokens = {v: k for k, v in enumerate(words)}
tokens

內容解密:

  1. text = open('example.txt', 'r').read():讀取名為example.txt的檔案內容到text變數中。
  2. words = text.split(" "):將文字內容以空格分割成單詞列表。
  3. tokens = {v: k for k, v in enumerate(words)}:建立一個字典,將單詞對映到它們在列表中的索引。

這樣的簡易Tokenizer建立了一個字典(無論是字面意義還是比喻意義),將單詞對映到數字。這非常有用,因為現在我們有了一個可以輸入到NLP模型的文字表示形式。

token_map = map(lambda t: tokens[t], words)
list(token_map)

內容解密:

  1. token_map = map(lambda t: tokens[t], words):使用map函式將每個單詞替換為其對應的索引。
  2. list(token_map):將對映結果轉換為列表。

雖然這是一個極度簡化的例子,但它說明瞭Tokenizer的基本功能。在實際應用中,我們不會這樣進行Tokenization,因為它很慢,而且不能處理不同語言之間的複雜性。

Tokenizer的精確定義

更準確地說,Tokenizer是一個將字元序列轉換為token序列的程式。Tokenizer作為一種通用工具,在NLP之外也非常有用。無論何時需要解析文字,都可能需要某種形式的Tokenizer。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 文字分類模型微調與 Tokenization 流程

package "Prodigy 資料標註" {
    component [textcat.manual\n手動標註食譜] as manual
    component [類別標籤設定\n(Business/Sports...)] as labels
    component [互斥標籤模式\n--exclusive] as exclusive
}

package "spaCy 資料準備" {
    component [data-to-spacy\nJSON 格式輸出] as data_to_spacy
    component [spacy convert\n二進位制格式] as convert
    component [訓練/評估資料集分割] as split
}

package "模型微調" {
    component [預訓練語言模型] as pretrained
    component [文字分類微調] as finetune
    component [少量標籤 vs 完整資料集] as compare
}

package "Hugging Face Tokenizers" {
    component [Subword Tokenization] as subword
    component [BPE / WordPiece] as algorithm
    component [高效能處理] as performance
}

manual --> labels
labels --> exclusive
exclusive --> data_to_spacy

data_to_spacy --> convert
convert --> split
split --> pretrained

pretrained --> finetune
finetune --> compare

subword --> algorithm
algorithm --> performance
performance --> finetune

note right of exclusive
  每個文字只能
  有一個類別
end note

note right of subword
  處理 OOV 詞彙
  與 Transformer 相容
end note

@enduml

此圖示展示了Tokenizer在NLP流程中的位置。

在深度學習實踐者中,我們感興趣的Tokenizer型別通常不會給我們解析樹。我們想要的是能夠讀取文字並生成一系列one-hot向量的Tokenizer。

實作與新想法

在瞭解了輸入和輸出之後,讓我們直接進入實作,然後再看看這個領域的一些新想法,並以更低層次的細節進行檢視。

我們認為,有兩個用於Tokenization的工具優於大多數其他工具——spaCy的Tokenizer和Hugging Face Tokenizers函式庫。

Hugging Face Tokenizers:新一代的 Tokenization 工具

在自然語言處理(NLP)的領域中,Tokenization 是基礎且關鍵的一步。不同的 Tokenization 方法會直接影響模型的表現和效率。Hugging Face 的 Tokenizers 函式庫是一個以 Rust 編寫的高效能工具,提供了 Python 和 JavaScript 的繫結,專為深度學習和 NLP 任務設計。

為什麼選擇 Hugging Face Tokenizers?

相較於 spaCy 的 Tokenizer,Hugging Face Tokenizers 更加現代化,並且專注於最新的研究成果和演算法。某些模型,如 BERT,需要特定的 Tokenization 方法,因此 Hugging Face Tokenizers 提供了與這些模型的相容性。

Hugging Face Tokenizers 的一大特點是其高效能。它充分利用多核心處理器,能夠在不到一分鐘的時間內處理大規模的資料集(GB 級別)。這對於非學術界的 NLP 任務來說,是相當龐大的資料量。

Tokenization 的流程

Hugging Face Tokenizers 將 Tokenization 的任務分解為幾個可管理的步驟:

  1. Normalizer:對輸入的字串進行初始轉換,例如小寫轉換、去除空白、Unicode 正規化等。

  2. PreTokenizer:決定如何預先分割原始字串,例如根據空白或特定字元進行分割。

  3. Model:負責子詞(subtoken)的發現和生成,這部分是可訓練的,並且依賴於輸入的資料。

  4. Post-Processor:提供高階的建構功能,以相容某些根據 Transformer 的 SOTA 模型,例如 BERT 需要在 tokenized sentence 周圍加上 [CLS][SEP] tokens。

  5. Decoder:將 tokenized 輸入映射回原始字串,解碼器的選擇通常與 PreTokenizer 相關。

  6. Trainer:為每個模型提供訓練能力。

安裝 Hugging Face Tokenizers

安裝 Hugging Face Tokenizers 非常簡單,只需執行以下指令:

pip install tokenizers

或者,你也可以在 GitHub 倉函式庫中的 requirements.txtenvironment.yml 檔案中找到相關的安裝設定。

Subword Tokenization

Subword Tokenization 是一種折衷的方法,旨在平衡詞彙量和資訊保留。考慮字串 “cat” 和 “cats”,一個有效的 subtokenization 是 [cat, ##s],其中 ##s 表示字首子詞。這種方法的優勢在於,它能夠保留詞彙的語義資訊,同時避免了大規模詞彙表的開銷。

import tokenizers

內容解密:

這段程式碼匯入了 tokenizers 函式庫,為後續使用 Hugging Face Tokenizers 的功能做好準備。這裡沒有具體的 tokenization 操作,但它展示瞭如何在 Python 環境中使用該函式庫。

Subword Tokenization 有其優缺點。一方面,它減少了詞彙表的大小,從而減小了 embedding 矩陣的規模;另一方面,一個單詞可能會被分割成多個 tokens,這可能會影響模型的輸入長度限制。

圖示:Subword Tokenization 的概念

此圖示展示了不同 tokenization 方法之間的差異,包括 character-based 和 word-level tokenization。Subword Tokenization 試圖在這兩者之間取得平衡,既保留了語義資訊,又控制了詞彙表的大小。