現在,讓我們探討如何構建一個能處理瑞士主要語言的多語言命名實體識別系統。我們將使用德語、法語、義大利語和英語的資料集,並利用XLM-RoBERTa模型來實作跨語言遷移。

資料準備與預處理

首先,我們需要載入並準備各語言的資料集。對於瑞士的應用場景,我們將建立一個包含四種主要語言的綜合資料集:

from datasets import load_dataset, concatenate_datasets

# 載入各語言資料集
languages = ["de", "fr", "it", "en"]
datasets = {}

for lang in languages:
    datasets[lang] = load_dataset("xtreme", name=f"PAN-X.{lang}")
    print(f"已載入 {lang} 資料集,包含 {len(datasets[lang]['train'])} 個訓練樣本")

這段程式碼透過迴圈載入了四種語言的PAN-X資料集,並將它們儲存在字典中以便後續處理。對於每種語言,我們列印出訓練集的大小,這有助於我們瞭解各語言資料的分佈情況。載入資料是構建任何機器學習系統的第一步,特別是在多語言環境中,瞭解各語言資料量的分佈對於後續的模型訓練和評估至關重要。

接下來,我們需要對資料進行預處理,為模型訓練做準備:

from transformers import AutoTokenizer

# 載入XLM-RoBERTa分詞器
model_checkpoint = "xlm-roberta-base"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

# 定義預處理函式
def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(
        examples["tokens"], 
        truncation=True, 
        is_split_into_words=True,
        max_length=128
    )
    
    labels = []
    for i, label in enumerate(examples["ner_tags"]):
        word_ids = tokenized_inputs.word_ids(batch_index=i)
        previous_word_idx = None
        label_ids = []
        
        for word_idx in word_ids:
            if word_idx is None or word_idx == previous_word_idx:
                label_ids.append(-100)  # 特殊標記或子詞片段
            else:
                label_ids.append(label[word_idx])
            previous_word_idx = word_idx
            
        labels.append(label_ids)
    
    tokenized_inputs["labels"] = labels
    return tokenized_inputs

這段程式碼首先載入了XLM-RoBERTa的分詞器,然後定義了一個用於處理文字和標籤對齊的函式。這個函式完成了兩項關鍵任務:

  1. 將文字標記(tokens)轉換為模型能理解的ID序列
  2. 將實體標籤(ner_tags)與分詞後的標記對齊

特別注意的是處理子詞標記(subword tokens)的方法:當一個詞被分解為多個子詞時,我們只將第一個子詞的標籤保留為原始標籤,其餘子詞的標籤設為-100,這樣這些標記在計算損失時會被忽略。這是處理子詞標記化與序列標籤任務的標準做法。

資料集處理與標籤對映

為了讓模型能夠正確理解標籤,我們需要建立標籤到ID的對映,並對所有語言的資料集應用預處理:

# 取得標籤列表
labels = datasets["en"]["train"].features["ner_tags"].feature.names
id2label = {i: label for i, label in enumerate(labels)}
label2id = {label: i for i, label in enumerate(labels)}

# 對所有語言資料集應用預處理
tokenized_datasets = {}
for lang, ds in datasets.items():
    tokenized_datasets[lang] = ds.map(
        tokenize_and_align_labels,
        batched=True,
        remove_columns=ds["train"].column_names
    )

這段程式碼首先從英語資料集中提取了標籤列表,並建立了標籤與ID之間的雙向對映。這些對映將用於模型訓練和預測過程中的標籤轉換。

然後,我們對每種語言的資料集應用之前定義的預處理函式,將原始資料轉換為模型可用的格式。map函式允許我們批次處理資料,提高效率,而remove_columns引數確保我們只保留模型需要的列。

模型訓練與評估策略

在多語言環境中,有多種訓練和評估策略。一種常見的方法是在一種語言上訓練,然後在其他語言上進行零樣本評估。另一種方法是在所有語言的混合資料上訓練,以提高模型的跨語言能力。

以下是一個在英語上訓練並在其他語言上評估的範例:

from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer
import numpy as np
from datasets import load_metric

# 載入模型
model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    num_labels=len(labels),
    id2label=id2label,
    label2id=label2id
)

# 定義評估指標
metric = load_metric("seqeval")

def compute_metrics(eval_preds):
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)
    
    # 移除忽略的索引(-100)
    true_labels = [[id2label[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [id2label[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    
    results = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": results["overall_precision"],
        "recall": results["overall_recall"],
        "f1": results["overall_f1"],
        "accuracy": results["overall_accuracy"]
    }

# 設定訓練引數
training_args = TrainingArguments(
    output_dir="./results",
    evaluation_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=False,
)

# 建立訓練器
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_datasets["en"]["train"],
    eval_dataset=tokenized_datasets["en"]["validation"],
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)

# 開始訓練
trainer.train()

這段程式碼展示了模型訓練的完整流程:

  1. 首先,我們載入預訓練的XLM-RoBERTa模型,並設定它用於標記分類別任務,指定標籤數量和標籤對映。

  2. 然後定義評估指標函式,使用seqeval度量來計算序列標籤任務的精確度、召回率、F1分數和準確度。這個函式處理了標籤轉換和-100標記的移除。

  3. 接著設定訓練引數,如學習率、批次大小、訓練輪數等,這些引數對模型效能有重要影響。

  4. 最後建立訓練器並開始訓練過程。在這個例子中,我們使用英語資料進行訓練,這將允許我們後續評估模型在其他語言上的零樣本效能。

跨語言評估

訓練完成後,我們可以評估模型在不同語言上的效能,這是檢驗跨語言遷移能力的關鍵步驟:

# 在各語言上評估模型效能
results = {}
for lang in languages:
    print(f"在 {lang} 語言上評估...")
    eval_results = trainer.evaluate(tokenized_datasets[lang]["validation"])
    results[lang] = eval_results
    print(f"{lang} F1分數: {eval_results['eval_f1']:.4f}")

這段程式碼對每種語言的驗證集進行評估,並收集各語言的效能指標。透過比較不同語言的F1分數,我們可以瞭解模型的跨語言遷移能力。如果模型在未見過的語言上也能獲得良好的效能,這表明XLM-RoBERTa確實具備了強大的跨語言泛化能力。

多語言模型的最佳化技巧

在實際應用中,我們可以採用多種策略來進一步提升多語言NER模型的效能:

混合語言訓練

除了在單一語言上訓練外,我們還可以嘗試在多種語言的混合資料上訓練模型:

# 建立混合語言訓練集
mixed_train = concatenate_datasets([ds["train"] for ds in tokenized_datasets.values()])
mixed_val = concatenate_datasets([ds["validation"] for ds in tokenized_datasets.values()])

# 使用混合資料訓練
mixed_trainer = Trainer(
    model=AutoModelForTokenClassification.from_pretrained(
        model_checkpoint,
        num_labels=len(labels),
        id2label=id2label,
        label2id=label2id
    ),
    args=training_args,
    train_dataset=mixed_train,
    eval_dataset=mixed_val,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)

mixed_trainer.train()

這段程式碼透過合併所有語言的訓練和驗證資料集,建立了混合語言資料集。然後使用這個混合資料集訓練新模型。這種方法可以幫助模型更好地學習語言間的共通特徵,提高在所有目標語言上的效能。混合語言訓練特別適合處理語碼轉換情況,因為模型能夠同時學習多種語言的特徵。

語言平衡與加權

當不同語言的資料量差異很大時,我們可以透過平衡或加權策略來改善效能:

# 計算各語言樣本權重
lang_counts = {lang: len(ds["train"]) for lang, ds in tokenized_datasets.items()}
total_samples = sum(lang_counts.values())
lang_weights = {lang: total_samples / (len(languages) * count) for lang, count in lang_counts.items()}

# 建立加權取樣器
from torch.utils.data import WeightedRandomSampler
import torch

def create_weighted_sampler(dataset, lang_weights, lang):
    weights = torch.ones(len(dataset)) * lang_weights[lang]
    return WeightedRandomSampler(weights, len(dataset))

# 在訓練器中使用加權取樣器
# 注意:實際實作需要自定義DataLoader

這段程式碼展示瞭如何計算各語言的樣本權重,並建立加權取樣器。對於資源豐富的語言,我們給予較低的權重;對於資源較少的語言,給予較高的權重。這種方法可以防止模型過度擬合資源豐富的語言,從而提高在資源較少語言上的效能。

在實際應用中,我們需要自定義DataLoader來使用這些取樣器,這裡只展示了基本概念。

對抗訓練與領域適應

對於更高階的最佳化,我們可以考慮對抗訓練或領域適應技術:

# 對抗訓練範例(概念性程式碼)
class LanguageClassifier(torch.nn.Module):
    def __init__(self, hidden_size):
        super().__init__()
        self.classifier = torch.nn.Linear(hidden_size, len(languages))
        
    def forward(self, features):
        return self.classifier(features)

# 在實際實作中,這需要自定義訓練迴圈來結合NER任務和語言識別的對抗損失

這段程式碼展示了對抗訓練的基本概念。對抗訓練的核心思想是訓練一個語言分類別器,嘗試從模型的特徵表示中識別語言,同時訓練主模型使其特徵表示對語言分類別器具有"欺騙性"。這種方法可以幫助模型學習語言無關的特徵表示,從而提高跨語言遷移能力。

這只是概念性程式碼,實際實作需要更複雜的設計,包括梯度反轉層和自定義訓練迴圈。

實際應用與案例分析

多語言命名實體識別在實際業務中有廣泛應用。以下是一些典型場景:

多語言檔案分析

在瑞士的銀行或保險公司中,客戶檔案可能包含德語、法語、義大利語和英語的混合內容。使用多語言NER模型可以從這些檔案中提取關鍵實體,如客戶名稱、地址和組織,而無需為每種語言維護單獨的模型。

多語言搜尋增強

電子商務平台可以利用多語言NER來識別產品描述中的品牌、型號和特性,從而提供更精準的多語言搜尋體驗。

社交媒體監測

在多語言社交媒體監測中,多語言NER可以識別提及的人物、地點和組織,幫助企業瞭解不同語言社群的品牌感知。

模型佈署與推理

訓練完成後,我們需要將模型佈署到生產環境。以下是一個簡單的推理範例:

from transformers import pipeline

# 建立NER管道
ner_pipeline = pipeline(
    "token-classification", 
    model=trainer.model, 
    tokenizer=tokenizer,
    aggregation_strategy="simple"  # 合併子詞標記
)

# 多語言文字範例
texts = {
    "en": "Jeff Bezos founded Amazon in Seattle.",
    "de": "Angela Merkel war die Bundeskanzlerin von Deutschland.",
    "fr": "Emmanuel Macron est le président de la France.",
    "it": "Leonardo da Vinci ha dipinto la Mona Lisa a Firenze."
}

# 對各語言文字進行推理
for lang, text in texts.items():
    print(f"\n{lang}: {text}")
    results = ner_pipeline(text)
    for entity in results:
        print(f"  {entity['word']} - {entity['entity_group']} ({entity['score']:.4f})")

這段程式碼展示瞭如何使用訓練好的模型建立命名實體識別管道,並對不同語言的文字進行推理。pipeline函式提供了一個簡單的介面,處理了標記化、模型推理和後處理等步驟。

aggregation_strategy="simple"引數指示管道將屬於同一實體的子詞標記合併為一個實體,這對於最終使用者來説更有意義。

對於每種語言的範例文字,我們列印出識別的實體、實體型別和置信度,這展示了模型在不同語言上的表現。

生產環境中的最佳實踐

在將多語言NER模型佈署到生產環境時,有幾個重要考慮因素:

  1. 模型量化與最佳化:可以使用ONNX Runtime或TensorRT等工具將模型量化和最佳化,以減少模型大小並提高推理速度。

  2. 批處理推理:在處理大量文字時,使用批處理可以顯著提高吞吐量。

  3. 語言識別前置處理:在多語言環境中,可以先使用語言識別模型確定文字語言,然後選擇適當的處理流程。

  4. 後處理規則:可以增加特定領域的後處理規則,如合併相鄰實體或過濾低置信度預測。

未來發展與研究方向

多語言NER技術仍在快速發展,以下是一些值得關注的趨勢:

  1. 更大規模的多語言模型:如mT5和BLOOM等模型支援更多語言,並具有更強的跨語言遷移能力。

  2. 低資源語言的改進:研究如何更有效地將知識從資源豐富的語言遷移到低資源語言。

  3. 多模態多語言模型:結合文字、影像和音訊的多模態多語言模型可能為NER帶來新的突破。

  4. 領域適應技術:開發更有效的方法,使通用多語言模型能夠快速適應特定領域。

多語言命名實體識別是一個充滿挑戰和機遇的領域。透過XLM-RoBERTa等多語言Transformer模型,我們能夠構建能夠跨語言工作的強大NER系統,為多語言環境中的訊息提取和理解提供有力支援。隨著技術的不斷進步,我們可以期待這些系統在效能和語言覆寫範圍方面的持續改進。

在這個全球化的時代,打破語言障礙的技術將變得越來越重要,而多語言NLP模型正是實作這一目標的關鍵工具。無論是在瑞士這樣的多語言國家,還是在全球營運的企業中,這些技術都能帶來巨大的價值。

多語言命名實體識別:處理不平衡資料集

在處理多語言應用程式時,我們經常會遇到語言資源不平衡的問題。這種不平衡源於現實世界中某些語言的標記資料較為稀缺,可能是因為缺乏精通該語言的領域工作者。本文將探討如何在這種不平衡的資料環境中建立一個能夠處理多種語言的模型。

建立不平衡的多語言資料集

首先,讓我們建立一個模擬真實世界語言不平衡的資料集。我們將使用PAN-X資料集,並根據四種歐洲語言的使用比例來對資料進行取樣:

from collections import defaultdict
from datasets import DatasetDict, load_dataset

# 定義語言和它們的比例
langs = ["de", "fr", "it", "en"]  # 德文、法文、義大利文和英文
fracs = [0.629, 0.229, 0.084, 0.059]  # 各語言的比例

# 建立一個defaultdict來儲存每種語言的資料集
panx_ch = defaultdict(DatasetDict)

# 根據比例載入和下取樣每種語言的資料
for lang, frac in zip(langs, fracs):
    # 載入單語言語料函式庫
    ds = load_dataset("xtreme", name=f"PAN-X.{lang}")
    
    # 對每個分割進行洗牌和下取樣
    for split in ds:
        panx_ch[lang][split] = (
            ds[split]
            .shuffle(seed=0)
            .select(range(int(frac * ds[split].num_rows))))

這段程式碼建立了一個不平衡的多語言資料集,模擬真實世界中的語言資源分佈情況。我們使用defaultdict來為每種語言儲存一個DatasetDict物件,然後根據預設的比例對每種語言的資料進行下取樣。shuffle()方法確保我們不會在下取樣過程中引入偏差,而select()方法則允許我們根據fracs中的值來選擇相應比例的資料。

檢視資料分佈

讓我們看每種語言在訓練集中的樣本數量:

import pandas as pd

pd.DataFrame({lang: [panx_ch[lang]["train"].num_rows] for lang in langs},
             index=["Number of training examples"])

結果顯示:

defriten
Number of training examples12580458016801180

這個表格清楚地展示了我們資料集中的語言不平衡情況。德語(de)的樣本數量超過了其他所有語言的總和,這正是我們要模擬的真實世界情境。在實際應用中,我們通常會從資源豐富的語言(這裡是德語)開始,然後嘗試將模型的能力遷移到資源較少的語言(法語、義大利語和英語)上,這就是所謂的零樣本跨語言遷移(zero-shot cross-lingual transfer)。

探索資料結構

讓我們檢查德語料函式庫中的一個樣本:

element = panx_ch["de"]["train"][0]
for key, value in element.items():
    print(f"{key}: {value}")

輸出:

langs: ['de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de']
ner_tags: [0, 0, 0, 0, 5, 6, 0, 0, 5, 5, 6, 0]
tokens: ['2.000', 'Einwohnern', 'an', 'der', 'Danziger', 'Bucht', 'in', 'der', 'polnischen', 'Woiwodschaft', 'Pommern', '.']

這個樣本展示了資料集的結構。每個樣本包含三個欄位:langs(表示每個詞的語言)、ner_tags(表示每個詞的命名實體標籤ID)和tokens(實際的詞)。ner_tags中的數字對應於不同的命名實體型別,但這種編碼方式對人類來説不太直觀,我們需要將其轉換為更可讀的格式。

理解資料特徵

讓我們檢查資料集的特徵結構:

for key, value in panx_ch["de"]["train"].features.items():
    print(f"{key}: {value}")

輸出:

tokens: Sequence(feature=Value(dtype='string', id=None), length=-1, id=None)
ner_tags: Sequence(feature=ClassLabel(num_classes=7, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], names_file=None, id=None), length=-1, id=None)
langs: Sequence(feature=Value(dtype='string', id=None), length=-1, id=None)

這裡我們可以看到ner_tags的實際結構是一個ClassLabel特徵序列,包含7個類別。O表示非實體詞,而其他標籤則使用BIO標註方案:B-表示實體的開始,I-表示實體的內部,後面跟著實體型別(PER為人名,ORG為組織,LOC為地點)。例如,B-LOC表示地點實體的開始詞,I-LOC表示地點實體的後續詞。

建立人類可讀的標籤

讓我們將數字標籤轉換為文字標籤,使其更易於理解:

# 取得標籤特徵
tags = panx_ch["de"]["train"].features["ner_tags"].feature
print(tags)

# 建立一個函式來將數字標籤轉換為文字標籤
def create_tag_names(batch):
    return {"ner_tags_str": [tags.int2str(idx) for idx in batch["ner_tags"]]}

# 應用轉換函式
panx_de = panx_ch["de"].map(create_tag_names)

這段程式碼使用了ClassLabel.int2str()方法來將數字標籤轉換為文字標籤。我們建立了一個新的欄位ner_tags_str,其中包含了人類可讀的標籤名稱。map()方法將這個轉換應用到整個資料集,為每個樣本增加新的欄位。

檢視標籤與詞的對齊

現在讓我們看第一個樣本中的詞和標籤是如何對齊的:

de_example = panx_de["train"][0]
pd.DataFrame([de_example["tokens"], de_example["ner_tags_str"]],
             ['Tokens', 'Tags'])

結果顯示:

01234567891011
Tokens2.000EinwohnernanderDanzigerBuchtinderpolnischenWoiwodschaftPommern.
TagsOOOOB-LOCI-LOCOOB-LOCB-LOCI-LOCO

這個表格清楚地展示了詞和它們對應的命名實體標籤。我們可以看到"Danziger Bucht"(但澤灣)被標記為一個地點實體(LOC),“polnischen Woiwodschaft Pommern”(波蘭的波美拉尼亞省)也被標記為地點實體。這個句子的意思是"2,000名居民住在波蘭波美拉尼亞省的但澤灣",標籤看起來很合理,因為但澤灣是波羅的海的一個海灣,而"voivodeship"(省)是波蘭的一個行政區劃單位。

檢查標籤分佈

為了確保資料集中的標籤分佈合理,我們來計算一下每個分割中各類別實體的頻率:

from collections import Counter

split2freqs = defaultdict(Counter)
for split, dataset in panx_de.items():
    for row in dataset["ner_tags_str"]:
        for tag in row:
            if tag.startswith("B"):  # 只計算實體的開始標籤
                tag_type = tag.split("-")[1]
                split2freqs[split][tag_type] += 1

pd.DataFrame.from_dict(split2freqs, orient="index")

結果顯示:

ORGLOCPER
validation268331722893
test257331803071
train536661865810

這個表格顯示了每個分割(訓練集、驗證集和測試集)中各類別實體的頻率分佈。我們可以看到三種實體型別(組織ORG、地點LOC和人名PER)在各個分割中的分佈大致相似,這意味著驗證集和測試集應該能夠很好地衡量我們的命名實體識別器的泛化能力。

多語言Transformer模型

多語言Transformer模型的架構和訓練過程與單語言模型類別似,但預訓練語料函式庫包含了多種語言的檔案。這種方法的一個顯著特點是,儘管模型在訓練過程中沒有接收到明確的語言區分訊息,但產生的語言表示能夠很好地跨語言泛化,適用於各種下游任務。在某些情況下,這種跨語言遷移能力可以產生與單語言模型相當的結果,這避免了為每種語言訓練一個模型的需求。

為了衡量命名實體識別的跨語言遷移進展,CoNLL-2002和CoNLL-2003資料集常被用作英語、荷蘭語、西班牙語和德語的基準。這個基準包含了與PAN-X相同的LOC、PER和ORG類別的新聞文章註解,但它還包含了一個額外的MISC標籤,用於不屬於前三類別的雜項實體。多語言Transformer模型通常以三種不同的方式進行評估:

  1. en:在英語訓練資料上微調,然後在每種語言的測試集上評估。
  2. each:在單語言測試資料上微調和評估,以衡量每種語言的效能。
  3. all:在所有訓練資料上微調,在每種語言的測試集上評估。

我們將採用類別似的評估策略,但首先需要選擇一個模型。最早的多語言Transformer之一是mBERT,它使用與BERT相同的架構和預訓練目標,但在預訓練語料函式庫中增加了來自多種語言的維基百科文章。自那時以來,mBERT已被XLM-RoBERTa(簡稱XLM-R)所取代,這就是我們在本文中將考慮的模型。

XLM-R只使用掩碼語言模型(MLM)作為100種語言的預訓練目標,但與前代模型相比,其預訓練語料函式庫的規模巨大:包括每種語言的維基百科轉儲和來自網路的2.5TB Common Crawl資料。這個語料函式庫比早期模型使用的語料函式庫大了幾個數量級,為緬甸語和斯瓦希里語等低資源語言提供了顯著的訊號提升,因為這些語言的維基百科文章數量有限。

XLM-R的成功很大程度上歸功於RoBERTa的改進,包括更大的批次大小、更多的訓練資料和更長的訓練時間。此外,XLM-R使用SentencePiece作為其分詞器,這是一種語言無關的分詞和去分詞方法,非常適合多語言設定。

在下一部分,我們將探討如何使用XLM-R模型來處理我們的多語言命名實體識別任務,尤其關注如何在資源豐富的語言(德語)上訓練模型,然後將其能力遷移到資源較少的語言(法語、義大利語和英語)上。

多語言模型的優勢

多語言模型在處理跨語言任務時有幾個關鍵優勢:

  1. 資源分享:對於低資源語言,可以從高資源語言中借用知識。
  2. 維護成本降低:維護一個多語言模型比維護多個單語言模型更為經濟高效。
  3. 零樣本遷移:在某些語言上訓練後,模型可以在未見過的語言上表現良好。
  4. 語言不可知的表示:模型學習到的表示捕捉了跨語言的通用語言特性。

然而,這些模型也面臨挑戰,如語言之間的幹擾、詞彙覆寫不均衡以及某些語言特有的語法結構難以建模等。在實際應用中,我們需要根據具體任務和可用資源來權衡使用多語言模型還是單語言模型。

多語言命名實體識別是評估這些模型能力的一個很好的任務,因為它要求模型理解不同語言中的上下文,並能夠識別和分類別各種實體。透過我們構建的不平衡資料集,我們可以測試模型在資源豐富和資源有限的語言之間進行知識遷移的能力。

在下一部分,我們將探討如何使用這些多語言模型來構建一個高效的命名實體識別系統,並評估它在我們的多語言資料集上的表現。

XLM-RoBERTa:多語言自然語言理解的強大工具

XLM-RoBERTa(簡稱XLM-R)是一個為多語言自然語言理解(NLU)任務設計的預訓練模型。正如其名稱所示,它根據RoBERTa的預訓練方法,但擴充套件到支援多種語言。在開發過程中,研究人員對BERT的多個方面進行了改進,特別是完全移除了下一句預測任務。

XLM-R與之前的XLM模型相比有幾個重要的區別:

  1. 移除了語言嵌入(language embeddings)
  2. 直接使用SentencePiece對原始文字進行分詞
  3. 擁有更大的詞彙表:高達25萬個詞元,相較於RoBERTa的5.5萬個

這些特性使XLM-R成為處理多語言自然語言理解任務的絕佳選擇。接下來,我將探討它如何高效地跨語言進行分詞處理。

分詞器的深度解析

XLM-R不使用WordPiece分詞器,而是採用SentencePiece分詞器,該分詞器在包含100種語言的原始文字上進行訓練。為了比較SentencePiece和WordPiece的差異,我們可以使用Transformers函式庫載入BERT和XLM-R的分詞器:

from transformers import AutoTokenizer

bert_model_name = "bert-base-cased"
xlmr_model_name = "xlm-roberta-base"

bert_tokenizer = AutoTokenizer.from_pretrained(bert_model_name)
xlmr_tokenizer = AutoTokenizer.from_pretrained(xlmr_model_name)

透過對一段簡短文字進行編碼,我們可以看到兩種模型在預訓練過程中使用的特殊標記:

text = "Jack Sparrow loves New York!"
bert_tokens = bert_tokenizer(text).tokens()
xlmr_tokens = xlmr_tokenizer(text).tokens()

print("BERT:", bert_tokens)
print("XLM-R:", xlmr_tokens)

輸出結果:

BERT: [CLS] Jack Spa ##rrow loves New York ! [SEP]
XLM-R: <s> ▁Jack ▁Spar row ▁love s ▁New ▁York ! </s>

從上面的結果可以看出兩個模型的主要差異:

  1. BERT使用[CLS][SEP]標記來表示序列的開始和結束,而XLM-R使用<s></s>
  2. XLM-R的分詞方式不同,它使用符號(Unicode U+2581)來標記單詞的開始
  3. BERT使用##字首表示單詞的子片段(如##rrow),而XLM-R則直接分割(如row

這些差異反映了兩種不同的分詞策略和處理方式,特別是針對多語言處理時的設計考量。

分詞器處理流程

分詞不僅是將字元串轉換為整數的單一操作,實際上它是一個完整的處理流程,通常包含四個步驟:

1. 正規化(Normalization)

這一步對原始字元串進行清理操作,使其更加標準化。常見操作包括:

  • 去除空白
  • 移除重音字元
  • Unicode正規化(處理同一字元的不同表示方式)
  • 轉為小寫(如果模型只接受小寫字元)

例如,在正規化後,“Jack Sparrow loves New York!“可能變成"jack sparrow loves new york!"。

2. 預分詞(Pretokenization)

這一步將文字分割成更小的單元,為最終的詞元設定上限。可以將預分詞理解為將文字分割成"單詞”,而最終的詞元將是這些單詞的部分。

對於支援這種處理的語言(英語、德語和許多印歐語系),字元串通常可以根據空格和標點符號分割成單詞。例如,這一步可能將文字轉換為[“jack”, “sparrow”, “loves”, “new”, “york”, “!"]。

然而,對於中文、日語或韓語等語言,將符號分組成語義單位可能是一個非確定性操作,有多種同樣有效的分組方式。在這種情況下,可能最好不要預分詞,而是使用特定語言的函式庫進行預分詞。

3. 分詞模型(Tokenizer model)

一旦輸入文字被正規化和預分詞,分詞器會在單詞上應用子詞分割模型。這是需要在語料函式庫上訓練的流程部分(或者如果使用預訓練的分詞器,則是已經訓練好的部分)。

模型的作用是將單詞分割成子詞,以減少詞彙表的大小並減少未登入詞的數量。存在多種子詞分詞演算法,包括BPE(Byte-Pair Encoding)、Unigram和WordPiece。

例如,我們的範例可能在應用分詞模型後變為[jack, spa, rrow, loves, new, york, !]。此時我們不再有字元串列表,而是整數列表(輸入ID)。

4. 後處理(Postprocessing)

這是分詞流程的最後一步,可以對詞元列表應用一些額外的轉換,例如在輸入序列的開頭或結尾增加特殊標記。

例如,BERT風格的分詞器會增加分類別和分隔標記:[CLS, jack, spa, rrow, loves, new, york, !, SEP]。這個序列(實際上是整數序列)然後可以輸入到模型中。

SentencePiece分詞器的特點

SentencePiece分詞器根據一種稱為Unigram的子詞分割方法,並將每個輸入文字編碼為Unicode字元序列。這一特性對多語言語料函式庫特別有用,因為它使SentencePiece能夠不受重音符號、標點符號的影響,並且能夠處理像日語這樣沒有空格字元的語言。

SentencePiece的另一個特殊功能是將空格分配給Unicode符號U+2581(即▁字元,也稱為四分之一方塊字元)。這使得SentencePiece能夠在不依賴特定語言的預分詞器的情況下,無歧義地對序列進行去分詞。

在前面的例子中,我們可以看到WordPiece已經丟失了"York"和”!“之間沒有空格的訊息。相比之下,SentencePiece在分詞文字中保留了空格,因此我們可以無歧義地轉換回原始文字:

"".join(xlmr_tokens).replace(u"\u2581", " ")
# 輸出: '<s> Jack Sparrow loves New York!</s>'

這段程式碼展示了SentencePiece的一個重要特性:它能夠精確地還原始文字。透過將特殊的Unicode符號\u2581(▁)替換回空格,我們可以重建原始文字,包括保留標點符號與前面單詞之間的緊密連線(如"York!“沒有空格)。這種能力對於需要準確還原文字的應用場景非常重要,比如機器翻譯或文字生成任務。

用於命名實體識別的Transformers模型

現在我們瞭解了SentencePiece的工作原理,讓我們看如何將簡單範例編碼成適合命名實體識別(NER)的形式。首先需要載入帶有標記分類別頭的預訓練模型。但我們不直接從Transformers載入這個頭部,而是自己構建它!透過深入瞭解Transformers API,我們可以透過幾個步驟實作這一點。

在第2章中,我們看到BERT用特殊的[CLS]標記來表示整個文字序列。這個表示然後透過全連線或密集層輸出所有離散標籤值的分佈。

對於命名實體識別這樣的標記分類別任務,情況略有不同。在NER中,我們需要為序列中的每個標記分配一個標籤,而不僅是整個序列。這意味著我們需要一個能夠處理每個標記的分類別頭,而不僅是[CLS]標記。

命名實體識別(NER)是一種序列標記任務,其中模型需要識別文字中的命名實體(如人名、地名、組織名等)並為每個標記分配適當的標籤。這與文字分類別不同,文字分類別只需要為整個文字分配一個標籤。

在BERT等Transformer模型中,文字分類別通常使用特殊的[CLS]標記來表示整個序列,然後透過一個分類別層預測標籤。而對於NER,我們需要使用每個標記的表示來預測其對應的實體標籤。這就是為什麼我們需要一個標記分類別頭,而不僅是一個序列分類別頭。

透過自行構建標記分類別頭,我們可以更好地理解Transformers API的工作原理,並且能夠根據特定需求自定義模型架構。這種靈活性對於處理複雜的NLP任務非常有價值。

多語言命名實體識別的實際應用

XLM-RoBERTa的強大之處在於它能夠在不同語言之間分享知識,使得在低資源語言上的表現也相當出色。這對於需要處理多語言文字的企業和機構非常有價值。

例如,在國際新聞分析、跨語言訊息抽取或全球社交媒體監測等應用中,能夠一致地識別不同語言中的實體是至關重要的。XLM-R模型配合SentencePiece分詞器,為這些任務提供了強大的基礎。

在實際佈署中,我們可以使用XLM-R為基礎,構建一個多語言NER系統,該系統能夠識別多種語言中的人名、地名、組織名和其他實體型別。這種系統可以處理混合語言的文字,並且能夠在新語言上快速適應,只需要少量的標註資料。

多語言模型的另一個優勢是它們可以實作零樣本跨語言遷移。也就是説,我們可以在一種語言(如英語)上訓練模型,然後直接在另一種語言(如西班牙語或中文)上使用它,而無需在目標語言上進行額外訓練。這大減少了為每種語言單獨建立NER系統的工作量。

構建自定義標記分類別頭

要為命名實體識別構建自定義標記分類別頭,我們需要了解Transformers模型的輸出結構,並在此基礎上增加適當的分類別層。

在標準的Transformer編碼器中,模型為每個輸入標記生成一個向量表示。對於BERT-base,這是一個768維的向量,而對於XLM-R-base,也是768維。我們需要將這些向量對映到我們的NER標籤空間。

以下是構建自定義標記分類別頭的基本步驟:

  1. 載入預訓練的Transformer模型(如XLM-R)作為基礎編碼器
  2. 在編碼器的輸出上增加一個線性層,將每個標記的表示對映到標籤空間
  3. 應用適當的啟用函式(通常是softmax)來獲得每個標籤的機率分佈
  4. 使用適當的損失函式(如交叉熵)訓練模型

這種方法允許我們充分利用預訓練模型的語言理解能力,同時為特定的NER任務定製模型。

透過深入瞭解Transformers的內部工作原理,我們可以更靈活地適應各種序列標記任務,而不僅是依賴預定義的模型架構。

XLM-RoBERTa模型結合SentencePiece分詞器,為多語言命名實體識別提供了強大的工具。透過理解這些技術的工作原理,我們可以構建更高效、更準確的多語言NLP系統,克服語言障礙,實作真正的跨語言自然語言理解。

在實際應用中,這種多語言方法不僅可以提高效率,還可以在低資源語言上獲得更好的效能,這對於建立全球化的NLP解決方案至關重要。隨著預訓練模型和分詞技術的不斷發展,我們可以期待多語言NLP系統在未來會變得更加強大和普及。

Transformer架構的命名實體識別實作

在自然語言處理領域中,命名實體識別(Named Entity Recognition,NER)是一項基礎與重要的任務。隨著Transformer架構的興起,NER的效能和準確度有了顯著提升。在實作層面上,根據編碼器的Transformer模型(如BERT)處理NER任務時採用了一種直接而有效的方法。

BERT與編碼器模型在NER中的應用

BERT等編碼器模型在處理NER任務時,會將每個輸入標記(token)的表示向量輸入到相同的全連線層,以輸出該標記對應的實體型別。正因如此,NER通常被歸類別為「標記分類別」(token classification)任務。

整個流程可以概括為:

  1. 輸入文字被分割成標記
  2. 標記透過BERT等編碼器模型獲得上下文化表示
  3. 每個標記的表示向量傳入分類別層
  4. 分類別層為每個標記預測實體類別(如人名、地點、組織等)

子詞處理的挑戰與解決方案

在實際應用中,我們遇到的一個關鍵挑戰是:如何處理被分割成多個子詞的標記?例如,「Christa」這個名字可能被分割成「Chr」和「##ista」兩個子詞,那麼應該將B-PER(人名開始)標籤分配給哪一個子詞呢?

根據BERT論文中的做法,標籤應該分配給第一個子詞(在我們的例子中是「Chr」),而後續的子詞(「##ista」)則被標記為IGN(忽略)。在後處理步驟中,我們可以輕鬆地將第一個子詞的預測標籤傳播到後續子詞。

另一種可能的做法是為「##ista」子詞分配相同的B-PER標籤,但這違反了IOB2格式的規範。IOB2格式要求B(Beginning)標籤只用於實體的第一個標記,而I(Inside)標籤用於實體內的後續標記。

值得注意的是,XLM-R模型的架構根據RoBERTa,而RoBERTa的架構與BERT相同,因此我們在BERT中看到的所有架構特性都適用於XLM-R。

Transformers框架中的模型類別剖析

模型類別的命名與組織方式

Transformers函式庫圍繞著專用於每種架構和任務的類別進行組織。與不同任務相關的模型類別按照<模型名稱>For<任務>的慣例命名,或者當使用AutoModel類別時,則命名為AutoModelFor<任務>

這種方法雖然直觀,但也有其侷限性。想像一下以下場景:你有一個絕妙的想法,想用transformer模型解決一個長期困擾你的NLP問題。你向老闆進行了精心準備的提案,並承諾如果解決了這個問題,可以增加部門收入。老闆被你的演示打動,給了你一週時間來建立概念驗證。

你滿心歡喜地開始工作,啟動GPU並開啟筆記本。你執行from transformers import BertForTaskXY(TaskXY是你想解決的假想任務),突然發現匯入錯誤:ImportError: cannot import name BertForTaskXY。糟糕,Transformers函式庫中沒有針對你的使用案例的BERT模型!你該如何在一週內完成專案?從哪裡開始?

別擔心!Transformers函式庫的設計理念之一就是讓你能夠輕鬆擴充套件現有模型以滿足特定需求。你可以載入預訓練模型的權重,並且可以使用特定任務的輔助函式。這使你能夠以極小的開銷為特定目標建立自定義模型。

模型的身體與頭部

Transformers函式庫如此多功能的關鍵概念是將架構分為「身體」(body)和「頭部」(head)。當我們從預訓練任務切換到下游任務時,需要將模型的最後一層替換為適合該任務的層。這最後一層被稱為模型頭部,是特定於任務的部分。模型的其餘部分被稱為身體,包括標記嵌入和transformer層,這些部分與任務無關。

這種結構也反映在Transformers的程式碼中:模型的身體在BertModelGPT2Model等類別中實作,這些類別回傳最後一層的隱藏狀態。而特定任務的模型,如BertForMaskedLMBertForSequenceClassification,則使用基本模型並在隱藏狀態之上增加必要的頭部。

這種身體和頭部的分離使我們能夠為任何任務建立自定義頭部,並將其安裝在預訓練模型之上。

為標記分類別建立自定義模型

接下來,讓我們透過為XLM-R建立自定義標記分類別頭部的練習,深入瞭解這一過程。由於XLM-R使用與RoBERTa相同的模型架構,我們將使用RoBERTa作為基本模型,但會增加XLM-R特有的設定。

需要説明的是,這是一個教育性練習,目的是向你展示如何為自己的任務建立自定義模型。對於標記分類別任務,Transformers函式庫中已經存在XLMRobertaForTokenClassification類別,你可以直接從函式庫中匯入使用。

首先,我們需要一個資料結構來表示我們的XLM-R NER標記器。我們需要一個設定物件來初始化模型,以及一個forward()函式來生成輸出。讓我們來構建我們的XLM-R標記分類別:

import torch.nn as nn
from transformers import XLMRobertaConfig
from transformers.modeling_outputs import TokenClassifierOutput
from transformers.models.roberta.modeling_roberta import RobertaModel
from transformers.models.roberta.modeling_roberta import RobertaPreTrainedModel

class XLMRobertaForTokenClassification(RobertaPreTrainedModel):
    config_class = XLMRobertaConfig
    
    def __init__(self, config):
        super().__init__(config)
        self.num_labels = config.num_labels
        # 載入模型身體
        self.roberta = RobertaModel(config, add_pooling_layer=False)
        # 設定標記分類別頭部
        self.dropout = nn.Dropout(config.hidden_dropout_prob)
        self.classifier = nn.Linear(config.hidden_size, config.num_labels)
        # 載入並初始化權重
        self.init_weights()
        
    def forward(self, input_ids=None, attention_mask=None, token_type_ids=None,
                labels=None, **kwargs):
        # 使用模型身體取得編碼器表示
        outputs = self.roberta(input_ids, attention_mask=attention_mask,
                              token_type_ids=token_type_ids, **kwargs)
        # 將分類別器應用於編碼器表示
        sequence_output = self.dropout(outputs[0])
        logits = self.classifier(sequence_output)
        # 計算損失
        loss = None
        if labels is not None:
            loss_fct = nn.CrossEntropyLoss()
            loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
        # 回傳模型輸出物件
        return TokenClassifierOutput(loss=loss, logits=logits,
                                   hidden_states=outputs.hidden_states,
                                   attentions=outputs.attentions)

這段程式碼定義了一個用於標記分類別(如NER)的自定義XLM-RoBERTa模型。讓我們逐步解析其關鍵元件:

  1. 設定類別config_class = XLMRobertaConfig確保在初始化新模型時使用標準的XLM-R設定。如果你想更改預設引數,可以透過覆寫設定中的預設設定來實作。

  2. 初始化方法

    • 透過super().__init__(config)呼叫RobertaPreTrainedModel類別的初始化函式,這個抽象類別負責處理權重的初始化或載入預訓練權重。
    • 使用self.roberta = RobertaModel(config, add_pooling_layer=False)載入模型身體。注意add_pooling_layer=False引數確保回傳所有隱藏狀態,而不僅是與[CLS]標記相關的狀態。
    • 設定分類別頭部,包括一個dropout層和一個標準的前饋層。
    • 最後,透過呼叫從RobertaPreTrainedModel繼承的init_weights()方法初始化所有權重,這將載入模型身體的預訓練權重,並隨機初始化標記分類別頭部的權重。
  3. 前向傳播方法

    • 資料首先透過模型身體傳遞。雖然有多個輸入變數,但現在我們只需要input_idsattention_mask
    • 模型身體輸出的隱藏狀態然後透過dropout和分類別層傳遞。
    • 如果在前向傳播中也提供了標籤,我們可以直接計算損失。
    • 最後,我們將所有輸出包裝在TokenClassifierOutput物件中,這使我們能夠以熟悉的命名元組方式存取元素。

透過這種方式,我們成功建立了一個可以用於NER任務的自定義XLM-RoBERTa模型,並且可以利用預訓練權重來提高模型效能。這個例子展示了Transformers框架的靈活性,讓我們能夠輕鬆擴充套件現有模型以滿足特定需求。

在實際應用中,這種自定義能力非常強大,因為它允許我們針對特定任務或領域調整模型架構,而不必從頭開始實作整個模型。特別是對於那些Transformers函式庫尚未直接支援的任務,這種方法提供了一個快速與有效的解決方案。

實際上,在我開發多語言NER系統時,發現能夠自定義模型頭部架構是非常關鍵的,尤其是當處理特定語言或領域的實體時。例如,針對醫療文字的NER任務,我可能需要調整分類別頭部以更好地識別藥物名稱、疾病和症狀等專業實體。透過上述方法,我只需要設計合適的頭部結構,同時保留強大的預訓練語言模型主體,從而達到最佳的效能和效率平衡。

多語言命名實體識別是自然語言處理中的一項關鍵任務,它使我們能夠從不同語言的文字中自動識別和提取實體訊息。透過使用Transformer架構,特別是像BERT和XLM-R這樣的預訓練模型,我們可以構建高效與準確的NER系統。而Transformers函式庫的模組化設計允許我們輕鬆擴充套件和自定義這些模型,以滿足特定的應用需求,無論是處理特殊領域的文字還是支援新的任務型別。

自定義Transformer模型的實作與應用

在自然語言處理領域,命名實體辨識(NER)是一項基礎與重要的任務。透過繼承Hugging Face的預訓練模型架構,我們可以輕鬆建立自己的自定義模型,同時獲得許多實用功能。在這篇文章中,我將帶領大家深入瞭解如何建立、載入和應用自定義的Transformer模型進行多語言命名實體辨識。

自定義模型的優勢與特性

當我們繼承自PreTrainedModel類別時,只需實作兩個主要函式,就能獲得所有Transformer工具的便利性。這種方式的最大優勢在於,我們可以直接使用from_pretrained()方法載入預訓練權重,而不必從頭開始訓練模型。這大幅降低了開發成本,同時保留了自定義模型架構的彈性。

載入自定義模型的完整流程

要載入自定義的Token分類別模型,我們需要進行幾個關鍵步驟,包括準備標籤對映、設定模型引數,以及載入預訓練權重。

準備標籤對映

首先,我們需要建立標籤與索引間的雙向對映:

# 從tags物件中建立雙向對映
index2tag = {idx: tag for idx, tag in enumerate(tags.names)}
tag2index = {tag: idx for idx, tag in enumerate(tags.names)}

這段程式碼建立了兩個字典:index2tag將數字索引對映到對應的NER標籤(如B-PER, I-LOC等),而tag2index則相反,將標籤對映到索引。這種雙向對映在模型預測和評估階段都非常重要,因為模型輸出的是數字索引,但我們需要將其轉換回人類可讀的標籤。

設定模型引數

接下來,我們使用AutoConfig來設定模型設定:

from transformers import AutoConfig

xlmr_config = AutoConfig.from_pretrained(xlmr_model_name,
                                        num_labels=tags.num_classes,
                                        id2label=index2tag, 
                                        label2id=tag2index)

這段程式碼從預訓練的XLM-RoBERTa模型載入設定,同時覆寫了幾個關鍵引數:

  • num_labels:指定了模型輸出的標籤數量(在NER任務中通常是標籤的總數)
  • id2label:提供索引到標籤的對映
  • label2id:提供標籤到索引的對映

AutoConfig類別包含模型架構的藍圖。當我們使用AutoModel.from_pretrained()時,會自動下載關聯的設定案。但若需要修改類別數量或標籤名稱等引數,可以先載入設定並自定義這些引數。

載入模型權重

有了設定後,我們可以載入預訓練權重:

import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
xlmr_model = (XLMRobertaForTokenClassification
             .from_pretrained(xlmr_model_name, config=xlmr_config)
             .to(device))

這段程式碼做了三件事:

  1. 確定使用的裝置(GPU或CPU)
  2. 使用我們自定義的設定載入XLM-RoBERTa模型
  3. 將模型移至適當裝置

值得注意的是,我們並未在自定義模型類別中實作載入預訓練權重的功能,這是透過繼承RobertaPreTrainedModel自動獲得的功能。

驗證模型與標記器的正確性

載入模型後,讓我們使用一個簡單的範例來驗證模型和標記器是否正確初始化:

input_ids = xlmr_tokenizer.encode(text, return_tensors="pt")
pd.DataFrame([xlmr_tokens, input_ids[0].numpy()], index=["Tokens", "Input IDs"])

輸出結果:

0123456789
Tokens▁Jack▁Sparrow▁loves▁New▁York!
Input IDs02176337456155555161723565753382

從這個輸出中,我們可以看到開始標記<s>和結束標記</s>分別被賦予ID 0和2。標記器將句子拆分為子詞,例如「Sparrow」被拆分為「▁Spar」和「row」,每個子詞都有對應的輸入ID。這些ID是模型實際處理的數值。

取得模型預測

接下來,我們將輸入傳遞給模型並提取預測結果:

outputs = xlmr_model(input_ids.to(device)).logits
predictions = torch.argmax(outputs, dim=-1)
print(f"Number of tokens in sequence: {len(xlmr_tokens)}")
print(f"Shape of outputs: {outputs.shape}")

輸出:

Number of tokens in sequence: 10
Shape of outputs: torch.Size([1, 10, 7])

這段程式碼展示了模型的輸出形狀為[1, 10, 7],這代表:

  • 1:批次大小(batch size)
  • 10:序列中的標記數量
  • 7:可能的NER標籤數量

模型為每個標記輸出7個值(對應7個可能的NER標籤),我們取最大值(argmax)來獲得最可能的標籤。

檢視預測結果

讓我們檢視模型對每個標記的預測標籤:

preds = [tags.names[p] for p in predictions[0].cpu().numpy()]
pd.DataFrame([xlmr_tokens, preds], index=["Tokens", "Tags"])

輸出結果:

0123456789
Tokens▁Jack▁Sparrow▁loves▁New▁York!
TagsOI-LOCB-LOCB-LOCOI-LOCOOI-LOCB-LOC

不出所料,使用隨機初始化權重的標記分類別層效果不佳。例如,「Jack」被標記為「I-LOC」(位置內部),而「Spar」和「row」都被標記為「B-LOC」(位置開始)。這些預測明顯不正確,因為「Jack Sparrow」應該是人名(PER),而「New York」應該是位置(LOC)。這説明我們需要在標記資料上微調模型。

建立實用的輔助函式

為了方便後續使用,讓我們將前面的步驟封裝成一個輔助函式:

def tag_text(text, tags, model, tokenizer):
    # 取得包含特殊字元的標記
    tokens = tokenizer(text).tokens()
    # 將序列編碼為ID
    input_ids = xlmr_tokenizer(text, return_tensors="pt").input_ids.to(device)
    # 取得7個可能類別的預測分佈
    outputs = model(input_ids)[0]
    # 取argmax取得每個標記最可能的類別
    predictions = torch.argmax(outputs, dim=2)
    # 轉換為DataFrame
    preds = [tags.names[p] for p in predictions[0].cpu().numpy()]
    return pd.DataFrame([tokens, preds], index=["Tokens", "Tags"])

這個輔助函式整合了我們前面介紹的步驟,使得標記新文字變得簡單。它接收文字、標籤集合、模型和標記器作為輸入,然後回傳一個DataFrame,顯示每個標記及其預測標籤。這種封裝極大地提高了程式碼的可讀性和重用性。

為NER任務準備標記資料

在訓練模型之前,我們需要處理資料集中的文字和標籤。這涉及到標記化和標籤ID的準備工作。

NER任務的標記化挑戰

NER任務的標記化過程比普通文字分類別更複雜,因為我們需要維持標記和標籤之間的對應關係。讓我們透過一個德語例子來説明:

首先,我們收集單詞和標籤作為普通列表:

words, labels = de_example["tokens"], de_example["ner_tags"]

接下來,我們標記每個單詞,並使用is_split_into_words引數告訴標記器我們的輸入序列已經被分割成單詞:

tokenized_input = xlmr_tokenizer(de_example["tokens"], is_split_into_words=True)
tokens = xlmr_tokenizer.convert_ids_to_tokens(tokenized_input["input_ids"])
pd.DataFrame([tokens], index=["Tokens"])

輸出結果(部分):

0123456
Tokens▁2.000▁Einwohnern▁an▁der▁Dan

這個例子展示了標記器如何將「Einwohnern」分割成兩個子詞:「▁Einwohner」和「n」。按照慣例,只有「▁Einwohner」應該與B-LOC標籤關聯,所以我們需要一種方法來遮蔽第一個子詞之後的子詞表示。

使用word_ids處理子詞對映

標記器提供了word_ids()函式,幫助我們解決子詞對映問題:

word_ids = tokenized_input.word_ids()
pd.DataFrame([tokens, word_ids], index=["Tokens", "Word IDs"])

輸出結果(部分):

0123456
Tokens▁2.000▁Einwohnern▁an▁der▁Dan
Word IDsNone011234

word_ids()函式將每個子詞對映到words序列中的相應索引。例如,第一個子詞「▁2.000」被分配索引0,而「▁Einwohner」和「n」都被分配索引1(因為「Einwohnern」是words中的第二個單詞)。特殊標記如<s></s>被對映為None。

處理標籤ID

接下來,我們設定-100作為特殊標記和我們希望在訓練期間遮蔽的子詞的標籤:

previous_word_idx = None
label_ids = []
for word_idx in word_ids:
    if word_idx is None or word_idx == previous_word_idx:
        label_ids.append(-100)
    elif word_idx != previous_word_idx:
        label_ids.append(labels[word_idx])
    previous_word_idx = word_idx

labels = [index2tag[l] if l != -100 else "IGN" for l in label_ids]
index = ["Tokens", "Word IDs", "Label IDs", "Labels"]
pd.DataFrame([tokens, word_ids, label_ids, labels], index=index)

輸出結果(部分):

012345
Tokens▁2.000▁Einwohnern▁an▁der
Word IDsNone01123
Label IDs-10000-10000
LabelsIGNOOIGNOO

這段程式碼實作了一個重要的處理:只有每個單詞的第一個子詞會被賦予實際的標籤ID,而後續子詞則被標記為-100。這是因為在PyTorch中,torch.nn.CrossEntropyLoss類別有一個名為ignore_index的屬性,其值為-100。這個索引在訓練過程中會被忽略,所以我們可以用它來忽略與連續子詞相關的標記。

這種方法確保了模型只學習每個單詞第一個子詞的標籤,而不會嘗試預測後續子詞的標籤,這與NER任務的標註慣例相符。