nlp-with-transformers系列-04_多语言命名实体识别
到本章為止,我們已經(jīng)使用Transformers模型來解決英文語料的NLP任務,但如果我們語料是用Greek, Swahili或者Klingon等語言組成,現(xiàn)在怎么辦? 一種方法是在Hugging Face Hub上搜索合適的預訓練語言模型,并在手頭的任務上對其進行微調。 然而,這些預訓練的模型往往只存在于像德語、俄語或普通話這樣的 "豐富資源 "的語言,這些語言有大量的網(wǎng)絡文本可供預訓練。 當語料庫是多語言的時候,另一個常見的挑戰(zhàn)出現(xiàn)了,在產(chǎn)品化環(huán)境中維護多個單語模型對我們以及工程團隊來說是沒有樂趣的。
幸運的是,有一類多語言Transformers前來救場。 與BERT一樣,這些模型使用遮蔽語言模型作為預訓練目標,在一百多種語言的語料上聯(lián)合訓練的。 通過對多種語言的巨大語料庫進行預訓練,這些多語言Transformers能夠實現(xiàn)零距離的跨語言遷移。 這意味著,在一種語言上經(jīng)過微調的模型可以應用于其他語言,而不需要任何進一步的訓練! 這也使得這些模型非常適合于 “code-switching”,即說話者在一次對話中交替使用兩種或更多的語言或方言。
在本章中,我們將探討如何對一個名為XLM-RoBERTa的單一Transformers模型(在第三章中介紹)進行微調,以便在幾種語言中進行命名實體識別(NER)。 正如我們在第一章中所看到的,NER是一項常見的NLP任務,用于識別文本中的實體,如人物、組織或地點。 這些實體可用于各種應用,如從公司文件中獲得關鍵信息,提高搜索引擎的質量,或只是從語料庫中建立一個結構化數(shù)據(jù)庫。
XLM-RoBERTa 是 RoBERTa 的多語言版本,它在包含 100 種語言的 2.5TB 過濾后的 CommonCrawl 數(shù)據(jù)上進行了預訓練。
在本章中假設我們要為一個位于瑞士的客戶進行NER,有四種國家語言(英語通常作為它們之間的橋梁), 我們首先要為這個問題獲得一個合適的多語言語料庫。
溫馨提示
零樣本遷移或零樣本學習通常是指在一組標簽上訓練一個模型,然后在另一組標簽上對其進行評估的任務。 在Transformers的背景下,零樣本學習也可以指像GPT-3這樣的語言模型在下游任務上被評估,而它甚至沒有被微調過的情況。
數(shù)據(jù)集
在本章中,我們將使用多語言編碼器的跨語言TRansfer評估(XTREME)基準的一個子集,稱為WikiANN或PAN-X。 該數(shù)據(jù)集由多種語言的維基百科文章組成,包括瑞士最常用的四種語言。 德語(62.9%)、法語(22.9%)、意大利語(8.4%)和英語(5.9%)。 每篇文章都用LOC(地點)、PER(人物)和ORG(組織)標簽以 " inside-outside-beginning"(IOB2)的格式進行了注釋。 在這種格式中,B-前綴表示一個實體的開始,而屬于同一實體的連續(xù)標記被賦予I-前綴。 一個O標記表示該標記不屬于任何實體。 例如,下面這句話:
Jeff Dean is a computer scientist at Google in California would be labeled in IOB2 format as shown in Table 4-1.
要在XTREME中加載PAN-X子集之一,我們需要知道哪種數(shù)據(jù)集配置要傳遞給load_dataset()函數(shù)。 每當你處理一個有多個域的數(shù)據(jù)集時,你可以使用get_dataset_config_names()函數(shù)來找出哪些子集可用:
from datasets import get_dataset_config_names
xtreme_subsets = get_dataset_config_names("xtreme")
print(f"XTREME has {len(xtreme_subsets)} configurations")
XTREME has 183 configurations
我們可以看到,那是一個很全的配置! 我們縮小搜索范圍,只尋找以 "PAN "開頭的配置:
panx_subsets = [s for s in xtreme_subsets if s.startswith("PAN")]
panx_subsets[:3]
['PAN-X.af', 'PAN-X.ar', 'PAN-X.bg']
到此為止,我們已經(jīng)確定了PAN-X子集的語法, 每個語言都有一個兩個字母的后綴,似乎是一個ISO 639-1語言代碼。 這意味著,為了加載德語語料庫,我們將de代碼傳遞給load_dataset()的name參數(shù),如下所示:
from datasets import load_dataset
load_dataset("xtreme", name="PAN-X.de")
為了制作一個真實的瑞士語料庫,我們將根據(jù)口語比例對PAN-X的德語(de)、法語(fr)、意大利語(it)和英語(en)語料庫進行采樣。 這將造成語言的不平衡,這在現(xiàn)實世界的數(shù)據(jù)集中是非常常見的,由于缺乏精通該語言的領域專家,獲取少數(shù)語言的標注實例可能會很昂貴。 這個不平衡的數(shù)據(jù)集將模擬多語言應用工作中的常見情況,我們將看到我們如何建立一個對所有語言都有效的模型。
為了跟蹤每一種語言,讓我們創(chuàng)建一個Python defaultdict,將語言代碼作為鍵,將DatasetDict類型的PAN-X語料庫作為值:
from collections import defaultdict
from datasets import DatasetDict
langs = ["de", "fr", "it", "en"]
fracs = [0.629, 0.229, 0.084, 0.059]
# Return a DatasetDict if a key doesn't exist
panx_ch = defaultdict(DatasetDict)
for lang, frac in zip(langs, fracs): # Load monolingual corpus ds = load_dataset("xtreme", name=f"PAN-X.{lang}") # Shuffle and downsample each split according to spoken proportion for split in ds: panx_ch[lang][split] = ( ds[split] .shuffle(seed=0) .select(range(int(frac * ds[split].num_rows))))
在這里,我們使用shuffle()方法來確保我們不會意外地偏離我們的數(shù)據(jù)集拆分,而select()允許我們根據(jù)fracs中的值對每個語料庫進行欠采樣。 讓我們通過訪問Dataset.num_rows屬性來看看我們在訓練集中每個語言有多少個例子:
import pandas as pd
pd.DataFrame({lang: [panx_ch[lang]["train"].num_rows] for lang in langs}, index=["Number of training examples"])
根據(jù)設計,我們在德語中的例子比其他所有語言的總和還要多,所以我們將以德語為起點,對法語、意大利語和英語進行Zeroshot跨語言轉移。 讓我們檢查一下德語語料庫中的一個例子:
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', '.']
與我們之前遇到的數(shù)據(jù)集對象一樣,我們的例子中的鍵對應于Arrow表中的列名,而值則表示每一列中的條目。 特別是,我們看到ner_tags列對應于每個實體與一個類ID的映射。 這樣看起來有點麻煩,接下來我們用熟悉的LOC、PER和ORG標簽創(chuàng)建一個新列。 要做到這一點,首先要注意的是,我們的數(shù)據(jù)集對象有一個特征屬性,指定與每一列相關的基礎數(shù)據(jù)類型:
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特征列表。 讓我們從訓練集中挑出這個特征,如下:
tags = panx_ch["de"]["train"].features["ner_tags"].feature
print(tags)
ClassLabel(num_classes=7, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], names_file=None, id=None)
我們可以使用第二章中遇到的ClassLabel.int2str()方法,在我們的訓練集中為每個標簽創(chuàng)建一個帶有類名的新列。 我們將使用map()方法返回一個dict,其鍵對應于新的列名,其值是一個類名的列表:
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)
現(xiàn)在我們有了人類可讀格式的標簽,讓我們看看訓練集中第一個例子的標記和標簽是如何對齊的:
de_example = panx_de["train"][0]
pd.DataFrame([de_example["tokens"], de_example["ner_tags_str"]], ['Tokens', 'Tags'])
LOC標簽的存在是有意義的,因為句子 "2,000 Einwohnern an der Danziger Bucht in der polnischen Woiwodschaft Pommern "在英語中是指 “波蘭波美拉尼亞省格但斯克灣的2,000名居民”,而格但斯克灣是波羅的海的一個海灣,而 "voivodeship "對應的是波蘭的一個州。
作為快速檢查,我們沒有考慮在標簽中出現(xiàn)任何不尋常的不平衡,讓我們計算每個實體在每個子集中的頻率:
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")
這看起來不錯–PER、LOC和ORG頻率的分布在每個分組中大致相同,因此驗證集和測試集應該能夠很好地衡量我們的NER抽取器的識別能力。 接下來,讓我們看看幾個流行的多語言Transformers,以及如何調整它們來處理我們的NER任務。
多語言Transformers
多語言Transformers涉及類似于單語言Transformers的架構和訓練程序,只是用于預訓練的語料庫由多種語言的文件組成。 這種方法的一個顯著特點是,盡管沒有收到區(qū)分語言的明確信息,但所產(chǎn)生的語言表征能夠在各種下游任務中很好地跨語言進行概括。 在某些情況下,這種進行跨語言轉移的能力可以產(chǎn)生與單語言模型相競爭的結果,這就規(guī)避了為每一種語言訓練一個模型的需要。
為了衡量NER的跨語言轉移的進展,CoNLL2002和CoNLL-2003數(shù)據(jù)集通常被用作英語、荷蘭語、西班牙語和德語的基準。 這個基準由新聞文章組成,其標注的LOC、PER和ORG類別與PAN-X相同,但它包含一個額外的MISC標簽,用于標注不屬于前三組的雜項實體。 多語言Transformers模型通常以三種不同的方式進行評估:
en
在英語訓練數(shù)據(jù)上進行微調,然后在每種語言的測試集中進行評估。
every
在單語測試數(shù)據(jù)上進行微調和評估,以衡量perlanguage的性能。
all
在所有的訓練數(shù)據(jù)上進行微調,在每種語言的測試集上進行評估。
我們將對我們的NER任務采取類似的評估策略,但首先我們需要選擇一個模型來評估。 最早的多語言Transformers之一是mBERT,它使用與BERT相同的架構和預訓練目標,但在預訓練語料庫中加入了許多語言的維基百科文章。 從那時起,mBERT已經(jīng)被XLM-RoBERTa(簡稱XLM-R)所取代,所以這就是我們在本章要考慮的模型。
正如我們在第3章中所看到的,XLM-R只使用MLM作為100種語言的預訓練目標,但與它的前輩相比,它的預訓練語料庫的規(guī)模巨大,因此而與眾不同。 每種語言的維基百科轉儲和2.5TB的網(wǎng)絡通用抓取數(shù)據(jù)。 這個語料庫比早期模型所使用的語料庫要大幾個數(shù)量級,并為像緬甸語和斯瓦希里語這樣只有少量維基百科文章的低資源語言提供了顯著的信號提升。
該模型名稱中的RoBERTa部分是指預訓練方法與單語RoBERTa模型相同。
RoBERTa的開發(fā)者對BERT的幾個方面進行了改進,特別是完全取消了下一句話的預測任務。 XLM-R還放棄了XLM中使用的語言嵌入,使用SentencePiece直接對原始文本進行標記。 除了多語言性質,XLM-R和RoBERTa之間的一個顯著區(qū)別是各自詞匯表的規(guī)模。 250,000個標記對55,000個!
XLM-R是多語言NLU任務的最佳選擇。 在下一節(jié)中,我們將探討它如何在多種語言中有效地進行標記化。
進一步了解Tokenization
XLM-R沒有使用WordPiece標記器,而是使用一個名為SentencePiece的標記器,該標記器是在所有一百種語言的原始文本上訓練出來的。 為了感受一下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()
這里我們看到,XLM-R使用< s>和< /s>來表示一個序列的開始和結束,而不是BERT用于句子分類任務的[CLS]和[SEP]標記。 這些令牌是在標記化的最后階段添加的,我們接下來會看到。
Tokenizer流水線
到目前為止,我們已經(jīng)將標記化作為一個單一的操作,將字符串轉換為我們可以通過模型傳遞的整數(shù)。 這并不完全準確,如果我們仔細觀察,可以發(fā)現(xiàn)它實際上是一個完整的處理流水線,通常由四個步驟組成,如圖4-1所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IFKDItpU-1647525716286)(images/chapter4/image-20220213161546813.png)]
讓我們仔細看看每個處理步驟,并通過 "杰克-斯派洛愛紐約!"這個例子說明它們的效果:
Normalization
這一步對應于你對原始字符串進行的一系列操作,以使其 “更干凈”。 Unicode規(guī)范化是另一種常見的規(guī)范化操作,由許多標記器應用,以處理同一字符經(jīng)常存在各種寫法的事實。 這可以使 "相同 "字符串的兩個版本(即具有相同的抽象字符序列)看起來不同。 像NFC、NFD、NFKC和NFKD這樣的Unicode規(guī)范化方案用標準形式取代了書寫同一字符的各種方式。 規(guī)范化的另一個例子是小寫字母。 如果預期模型只接受和使用小寫字母,那么可以用這種技術來減少它所需要的詞匯量的大小。 經(jīng)過規(guī)范化處理后,我們的例子字符串將看起來像 “Jack Sparrow loves new york!”。
Pretokenization
這一步將文本分割成更小的對象,為訓練結束時的標記提供一個上限。 一個好的方法是,預編碼器將把你的文本分成 “詞”,而你的最終標記將是這些詞的一部分。 對于允許這樣做的語言(英語、德語和許多印歐語系語言),字符串通常可以在空白處和標點符號上被分割成單詞。 例如,這一步可能會改變我們的[“jack”, “sparrow”, “l(fā)oves”, “new”, “york”, “!”]。 然后,在流水線的下一個步驟中,這些詞被更簡單地用字節(jié)對編碼(BPE)或單字算法分割成子字。 然而,分割成 "字 "并不總是一個微不足道的確定操作,甚至不是一個有意義的操作。 例如,在中文、日文或韓文等語言中,在語義單位(如印歐語詞)中對符號進行分組可以是一種非確定性的操作,有幾個同樣有效的分組。 在這種情況下,最好不要對文本進行預編碼,而是使用特定的語言庫進行預編碼。
Tokenizer model
一旦輸入文本被規(guī)范化和預標記化,標記化器就會在單詞上應用一個子詞分割模型。這是流水線的一部分,需要在你的語料庫上進行訓練(如果你使用的是預訓練的標記器,則是已經(jīng)訓練過的)。該模型的作用是將詞分成子詞,以減少詞匯量的大小,并試圖減少詞匯外標記的數(shù)量。存在幾種子詞標記化算法,包括BPE、Unigram和WordPiece。例如,我們運行的例子在應用標記化模型后可能看起來像[jack, spa, rrow, loves, new, york, !] 。請注意,此時我們不再有一個字符串的列表,而是一個整數(shù)的列表(輸入ID);為了保持這個例子的說明性,我們保留了單詞,但去掉了引號以表示轉換。
Postprocessing
這是標記化流水線的最后一步,在這一步中,可以對標記列表進行一些額外的轉換–例如,在輸入的標記索引序列的開頭或結尾添加特殊標記。例如,一個BERT風格的標記器會添加分類和分隔符。[CLS, jack, spa, rrow, loves, new, york, !, SEP]。這個序列(請記住,這將是一個整數(shù)序列,而不是你在這里看到的標記)然后可以被送入模型。
回到我們對XLM-R和BERT的比較,我們現(xiàn)在明白SentencePiece在后處理步驟中添加了< s>和< /s>,而不是[CLS]和[SEP](作為慣例,我們將在圖形說明中繼續(xù)使用[CLS]和[SEP])。讓我們回到SentencePiece標記器,看看它的特殊之處。
SentencePiece Tokenizer
SentencePiece標記器是基于一種稱為Unigram的子詞分割,并將每個輸入文本編碼為Unicode字符序列。這最后一個特點對多語言語料庫特別有用,因為它允許SentencePiece對口音、標點符號以及許多語言(如日語)沒有空白字符的事實不加考慮。SentencePiece的另一個特點是空白字符被分配到Unicode符號U+2581,即▁字符,也叫下四分之一塊字符。這使得SentencePiece能夠在沒有歧義的情況下對一個序列進行去標記,而不需要依賴特定語言的預標記器。例如,在我們上一節(jié)的例子中,我們可以看到WordPiece丟失了 "York “和”!"之間沒有空白的信息。相比之下,SentencePiece保留了標記化文本中的空白,因此我們可以毫無歧義地轉換回原始文本:
"".join(xlmr_tokens).replace(u"\u2581", " ")
'<s> Jack Sparrow loves New York!</s>'
現(xiàn)在我們了解了SentencePiece的工作原理,讓我們看看如何將我們的簡單例子編碼成適合NER的形式。首先要做的是給預訓練的模型加載一個標記分類頭。但我們不是直接從Transformers中加載這個頭,而是自己建立它! 通過深入研究Transformers API,我們只需幾個步驟就可以做到這一點。
命名實體識別的Transformers
在第2章中,我們看到,對于文本分類,BERT使用特殊的[CLS]標記來表示整個文本序列。然后,該表示法通過一個全連接或dense層來輸出所有離散標簽值的分布,如圖4-2所示。
BERT和其他僅有編碼器的變換器對NER采取了類似的方法,只是每個單獨的輸入標記的表示被送入同一個全連接層以輸出標記的實體。由于這個原因,NER經(jīng)常被看作是一個標記分類任務。這個過程看起來像圖4-3中的圖表。
到目前為止,情況還不錯,但是在標記分類任務中,我們應該如何處理子詞呢?例如,圖4-3中的名字 "Christa "被標記為子詞 "Chr "和 “##ista”,那么哪一個子詞應該被賦予B-PER標簽呢?
在BERT的論文中,作者將這個標簽分配給了第一個子詞(在我們的例子中是 “Chr”),而忽略了后面的子詞("##ista")。這就是我們在這里采用的慣例,我們將用IGN來表示被忽略的子詞。我們以后可以在后處理步驟中輕松地將第一個子詞的預測標簽傳播到后面的子詞。我們可以 也可以選擇通過給它分配一個B-LOC標簽的副本來包括 "##ista "子詞的表示,但這違反了IOB2的格式。
幸運的是,我們在BERT中看到的所有架構方面都延續(xù)到了XLM-R,因為它的架構是基于RoBERTa的,與BERT完全相同。接下來我們將看到Transformers是如何通過微小的修改來支持許多其他任務的。
Transformers 模型類的剖析
Transformers 是圍繞每個架構和任務的專用類來組織的。與不同任務相關的模型類是根據(jù)For慣例命名的,當使用AutoModel類時,則是AutoModelFor。
然而,這種方法有其局限性,為了激勵深入了解Transformer API,請考慮以下情況。假設你有一個很好的想法,用一個Transformers 模型來解決一個你想了很久的NLP問題。因此,你安排了一次與老板的會議,并通過一個藝術性的PowerPoint演示文稿,推銷說如果你能最終解決這個問題,就能增加你部門的收入。你豐富多彩的演示和對利潤的談論給你留下了深刻印象,你的老板慷慨地同意給你一個星期的時間來建立一個概念驗證。對結果感到滿意,你馬上開始工作。你啟動了你的GPU并打開了一個筆記本。你執(zhí)行從transformers導入BertForTaskXY(注意,TaskXY是你想解決的假想任務),當可怕的紅色充滿你的屏幕時,你的臉色一下子變了。ImportError: cannot import name BertForTaskXY. 哦,不,沒有適合你的用例的BERT模型! 如果你必須自己實現(xiàn)整個模型,你怎么能在一周內完成這個項目呢!?你甚至應該從哪里開始?
不要驚慌! Transformers 的設計是為了使你能夠為你的特定使用情況輕松地擴展現(xiàn)有的模型。你可以從預訓練的模型中加載權重,并且你可以訪問特定任務的輔助函數(shù)。這讓你可以用很少的開銷為特定目標建立自定義模型。在本節(jié)中,我們將看到我們如何實現(xiàn)我們自己的自定義模型。
主體和頭部
使得Transformers 如此多才多藝的主要概念是將架構分成主體和頭部(正如我們在第一章中看到的)。我們已經(jīng)看到,當我們從預訓練任務切換到下游任務時,我們需要將模型的最后一層替換成適合該任務的一層。這最后一層被稱為模型頭;它是特定任務的部分。模型的其余部分被稱為主體;它包括與任務無關的標記嵌入和Transformers層。這種結構也反映在Transformers代碼中:模型的主體由BertModel或GPT2Model這樣的類來實現(xiàn),它返回最后一層的隱藏狀態(tài)。特定任務的模型,如BertForMaskedLM或BertForSequenceClassification使用基礎模型,并在隱藏狀態(tài)的基礎上添加必要的頭,如圖4-4所示。
正如我們接下來要看到的,這種主體和頭部的分離使我們能夠為任何任務建立一個定制的頭,并將其安裝在一個預訓練的模型之上即可。
為標記分類創(chuàng)建一個自定義模型
讓我們經(jīng)歷一下為XLM-R建立一個自定義的標記分類頭的練習。由于XLM-R使用與RoBERTa相同的模型架構,我們將使用RoBERTa作為基礎模型,但用XLM-R的特定設置進行增強。請注意,這是一個教育性練習,向你展示如何為你自己的任務建立一個自定義模型。對于標記分類,XLMRobertaForTokenClassification類已經(jīng)存在,你可以從Transformers導入。如果你愿意,你可以跳到下一節(jié),直接使用那個。
為了開始工作,我們需要一個數(shù)據(jù)結構來表示我們的XLM-R NER標記器。首先,我們需要一個配置對象來初始化模型,以及一個forward()函數(shù)來生成輸出。讓我們繼續(xù)建立我們的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# Load model body self.roberta = RobertaModel( config, add_pooling_layer = False)# Set up token classification head self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, config.num_labels)# Load and initialize weights self.init_weights() def forward(self, input_ids = None, attention_mask = None, token_type_ids = None, labels = None, ** kwargs): #Use model body to get encoder representations outputs = self.roberta(input_ids, attention_mask = attention_mask,token_type_ids = token_type_ids, * * kwargs)# Apply classifier to encoder representation sequence_output = self.dropout(outputs[0]) logits = self.classifier( sequence_output)# Calculate losses loss = Noneif labels is not None: loss_fct = nn.CrossEntropyLoss() loss =loss_fct(logits.view(-1, self.num_labels), labels.view(-1))# Return model output objectreturn TokenClassifierOutput(loss = loss, logits = logits, hidden_states = outputs.hidden_states, attentions = outputs.attentions)
config_class確保我們在初始化一個新模型時使用標準的XLM-R設置。如果你想改變默認參數(shù),你可以通過覆蓋配置中的默認設置來實現(xiàn)。通過super()方法,我們調用RobertaPreTrainedModel類的初始化函數(shù)。這個抽象類處理初始化或加載預訓練的權重。然后我們加載我們的模型主體,也就是RobertaModel,并用我們自己的分類頭來擴展它,包括一個dropout和一個標準前饋層。注意,我們設置add_pooling_layer=False,以確保所有的隱藏狀態(tài)都被返回,而不僅僅是與[CLS]標記相關的一個。最后,我們通過調用從RobertaPreTrainedModel繼承的init_weights()方法來初始化所有權重,該方法將加載模型主體的預訓練權重,并隨機初始化我們標記分類頭的權重。
唯一要做的就是用forward()方法定義模型在前向傳遞中應該做什么。在前向傳遞過程中,數(shù)據(jù)首先通過模型主體被輸入。有許多輸入變量,但我們現(xiàn)在唯一需要的是input_ids 和 attention_mask。隱藏狀態(tài)是模型主體輸出的一部分,然后通過dropout和分類層進行反饋。如果我們在前向傳遞中也提供標簽,我們可以直接計算損失。如果有一個注意力掩碼,我們需要多做一點工作,以確保我們只計算未掩碼的標記的損失。最后,我們將所有的輸出包在一個TokenClassifierOutput對象中,允許我們訪問前幾章中熟悉的命名元組中的元素。
通過實現(xiàn)一個簡單類的兩個函數(shù),我們就可以建立我們自己的自定義Transformers模型。由于我們繼承了PreTrainedModel,我們可以立即獲得所有有用的Transformer工具,比如from_pretrained()! 讓我們來看看我們如何將預訓練的權重加載到我們的自定義模型中。
加載一個自定義模型
現(xiàn)在我們準備加載我們的標記分類模型。我們需要在模型名稱之外提供一些額外的信息,包括我們將用于標記每個實體的標簽,以及每個標簽與ID的映射,反之亦然。所有這些信息都可以從我們的tags變量中得到,作為一個ClassLabel對象,它有一個names屬性,我們可以用它來導出映射。
index2tag = {idx: tag for idx, tag in enumerate(tags.names)}
tag2index = {tag: idx for idx, tag in enumerate(tags.names)}
我們將把這些映射和tags.num_classes屬性存儲在我們在第三章遇到的AutoConfig對象中。向from_pretrained()方法傳遞關鍵字參數(shù)會覆蓋默認值:
from transformers import AutoConfig
xlmr_config = AutoConfig.from_pretrained(xlmr_model_name, num_labels=tags.num_classes, id2label=index2tag, label2id=tag2index)
AutoConfig類包含了一個模型的架構藍圖。當我們用AutoModel.from_pretrained(model_ckpt)加載一個模型時,與該模型相關的配置文件會自動下載。然而,如果我們想修改諸如 類或標簽名稱,那么我們可以先用我們想定制的參數(shù)加載配置。
現(xiàn)在,我們可以像往常一樣用帶有額外配置參數(shù)的from_pretrained()函數(shù)加載模型權重。注意,我們沒有在我們的自定義模型類中實現(xiàn)加載預訓練的權重;我們通過繼承RobertaPreTrainedModel免費獲得這個功能:
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))
作為一個快速檢查,我們已經(jīng)正確地初始化了標記器和模型,讓我們在已知實體的小序列上測試預測:
input_ids = xlmr_tokenizer.encode(text, return_tensors="pt")
pd.DataFrame([xlmr_tokens, input_ids[0].numpy()], index=["Tokens", "Input IDs"])
正如你在這里看到的,開始和結束標記分別被賦予0和2的ID。
最后,我們需要將輸入傳遞給模型,并通過獲取argmax來提取預測,以獲得每個標記最可能的類別:
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])
這里我們看到對數(shù)的形狀是[batch_size, num_tokens, num_tags],每個標記在七個可能的NER標記中都有一個對數(shù)。通過對序列的枚舉,我們可以很快看到預訓練模型的預測:
preds = [tags.names[p] for p in predictions[0].cpu().numpy()]
pd.DataFrame([xlmr_tokens, preds], index=["Tokens", "Tags"])
不出所料,我們的隨機權重的標記分類層還有很多不足之處;讓我們在一些標記的數(shù)據(jù)上進行微調,使其變得更好!在這之前,讓我們把前面的步驟封裝在以后使用的輔助函數(shù)中。在這樣做之前,讓我們把前面的步驟打包成一個輔助函數(shù),供以后使用:
def tag_text(text, tags, model, tokenizer):# Get tokens with special characters tokens = tokenizer(text).tokens() # Encode the sequence into IDs input_ids = xlmr_tokenizer(text, return_tensors="pt").input_ids.to(device) # Get predictions as distribution over 7 possible classes outputs = model(inputs)[0] # Take argmax to get most likely class per token predictions = torch.argmax(outputs, dim=2) # Convert to DataFrame preds = [tags.names[p] for p in predictions[0].cpu().numpy()] return pd.DataFrame([tokens, preds], index=["Tokens", "Tags"])
在我們訓練模型之前,我們還需要對輸入進行標記化,并準備好標簽。我們接下來會做這個。
將文本標記化以用于NER
現(xiàn)在我們已經(jīng)確定標記器和模型可以對單個例子進行編碼,我們的下一步是對整個數(shù)據(jù)集進行標記,以便我們可以將其傳遞給XLM-R模型進行微調。正如我們在第二章中所看到的,Datasets提供了一種快速的方法,用map()操作對數(shù)據(jù)集對象進行標記化。要實現(xiàn)這一點,請回憶一下,我們首先需要定義一個具有最小簽名的函數(shù):
function(examples: Dict[str, List]) -> Dict[str, List]
其中examples相當于數(shù)據(jù)集的一個片斷,例如panx_de[‘train’][:10]。由于XLM-R標記器為模型的輸入返回了輸入ID,我們只需要用注意力掩碼和標簽ID來增加這些信息,這些標簽編碼了與每個NER標記相關的標記信息。
按照Transformers文檔中的方法,讓我們看看這在我們的單一德語例子中是如何工作的,首先收集單詞和標簽作為普通列表:
words, labels = de_example["tokens"], de_example["ner_tags"]
接下來,我們對每個詞進行標記,并使用is_split_into_words參數(shù)來告訴標記器,我們的輸入序列已經(jīng)被分割成單詞:
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"])
在這個例子中,我們可以看到標記器將 "Einwohnern "分成兩個子詞,"▁Einwohner "和 “n”。由于我們遵循的慣例是只有"▁Einwohner "應該與B-LOC標簽相關聯(lián),我們需要一種方法來掩蓋第一個子詞之后的子詞表示。幸運的是,tokenized_input是一個包含word_ids()函數(shù)的類,可以幫助我們實現(xiàn)這個目標:
word_ids = tokenized_input.word_ids()
pd.DataFrame([tokens, word_ids], index=["Tokens", "Word IDs"])
在這里我們可以看到,word_ids已經(jīng)將每個子詞映射到單詞序列中的相應索引,所以第一個子詞"▁2.000 "被分配到索引0,而"▁Einwohner "和 "n "被分配到索引1(因為 "Einwohnern "是單詞中的第二個單詞)。我們還可以看到,像< s>和< /s>這樣的特殊標記被映射為無。讓我們把-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)
注意事項
為什么我們選擇-100作為屏蔽子詞表示的ID?原因是在PyTorch中,交叉熵損失類torch.nn.CrossEntropyLoss有一個名為ignore_index的屬性,其值為-100。這個指數(shù)在訓練過程中會被忽略,所以我們可以用它來忽略與連續(xù)子詞相關的標記。
就這樣了 我們可以清楚地看到標簽ID是如何與標記對齊的,所以讓我們通過定義一個包含所有邏輯的單一函數(shù),將其擴展到整個數(shù)據(jù)集:
def tokenize_and_align_labels(examples): tokenized_inputs = xlmr_tokenizer(examples["tokens"], truncation=True, is_split_into_words=True) labels = [] for idx, label in enumerate(examples["ner_tags"]): word_ids = tokenized_inputs.word_ids(batch_index=idx) 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
我們現(xiàn)在有了對每個分裂進行編碼所需的所有成分,所以讓我們寫一個我們可以迭代的函數(shù):
def encode_panx_dataset(corpus): return corpus.map(tokenize_and_align_labels, batched=True, remove_columns=['langs', 'ner_tags', 'tokens'])
將這個函數(shù)應用于DatasetDict對象,我們就可以得到每個分割的編碼數(shù)據(jù)集對象。讓我們用它來對我們的德語語料庫進行編碼:
panx_de_encoded = encode_panx_dataset(panx_ch["de"])
現(xiàn)在我們有一個模型和一個數(shù)據(jù)集,我們需要定義一個性能指標。
性能評估
評估NER模型與評估文本分類模型類似,通常報告精度、召回率和F-score的結果。唯一的微妙之處在于,一個實體的所有單詞都需要被正確預測,這樣才能算作正確的預測。幸運的是,有一個叫seqeval的漂亮庫,是為這類任務設計的。例如,給定一些占位的NER標簽和模型預測,我們可以通過seqeval的classification_report()函數(shù)來計算度量:
from seqeval.metrics import classification_report
y_true = [["O", "O", "O", "B-MISC", "I-MISC", "I-MISC", "O"], ["B-PER", "I-PER", "O"]]
y_pred = [["O", "O", "B-MISC", "I-MISC", "I-MISC", "I-MISC", "O"], ["B-PER", "I-PER", "O"]]
print(classification_report(y_true, y_pred))
正如我們所看到的,seqeval期望預測和標簽為列表,每個列表對應于我們驗證集或測試集中的一個例子。為了在訓練過程中整合這些指標,我們需要一個函數(shù)來獲取模型的輸出并將其轉換為seqeval所期望的列表。下面的函數(shù)通過確保我們忽略與后續(xù)子詞相關的標簽ID來完成這個任務:
import numpy as np
def align_predictions(predictions, label_ids): preds = np.argmax(predictions, axis=2) batch_size, seq_len = preds.shape labels_list, preds_list = [], [] for batch_idx in range(batch_size): example_labels, example_preds = [], [] for seq_idx in range(seq_len): # Ignore label IDs = -100 if label_ids[batch_idx, seq_idx] != -100: example_labels.append(index2tag[label_ids[batch_idx] [seq_idx]]) example_preds.append(index2tag[preds[batch_idx][seq_idx]]) labels_list.append(example_labels) preds_list.append(example_preds) return preds_list, labels_list
有了性能指標,我們就可以開始實際訓練模型了。
微調 XLM-RoBERTa
我們現(xiàn)在有了對我們的模型進行微調的所有材料!我們的第一個策略是在PAN-X的德語子集上對我們的基本模型進行微調,然后評估它在法語和意大利語上的零散跨語言表現(xiàn)。我們的第一個策略是在PAN-X的德語子集上微調我們的基礎模型,然后評估它在法語、意大利語和英語上的零起點跨語言性能。像往常一樣,我們將使用變形金剛訓練器來處理我們的訓練循環(huán),所以首先我們需要使用TrainingArguments類來定義訓練屬性:
from transformers import TrainingArguments
num_epochs = 3
batch_size = 24
logging_steps = len(panx_de_encoded["train"]) // batch_size
model_name = f"{xlmr_model_name}-finetuned-panx-de"
training_args = TrainingArguments( output_dir=model_name, log_level="error", num_train_epochs=num_epochs, per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, evaluation_strategy="epoch", save_steps=1e6, weight_decay=0.01, disable_tqdm=False, logging_steps=logging_steps, push_to_hub=True)
在這里,我們在每個歷時結束時評估模型在驗證集上的預測,調整權重衰減,并將save_steps設置為一個大數(shù)字,以禁用檢查點,從而加快訓練速度。
這也是確保我們登錄到Hugging Face Hub的一個好時機(如果你在終端工作,你可以執(zhí)行huggingface-cli login命令)。
from huggingface_hub
import notebook_login notebook_login()
我們還需要告訴Trainer如何在驗證集上計算指標,所以在這里我們可以使用之前定義的align_predictions()函數(shù),以seqeval需要的格式提取預測和標簽,以計算F-score:
from seqeval.metrics import f1_score
def compute_metrics(eval_pred): y_pred, y_true = align_predictions(eval_pred.predictions, eval_pred.label_ids)return {"f1": f1_score(y_true, y_pred)}
最后一步是定義一個數(shù)據(jù)整理器,這樣我們就可以把每個輸入序列填充到一個批次的最大序列長度。Transformers提供了一個 專用于標記分類的數(shù)據(jù)整理器,它將與輸入一起填充標簽。
from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(xlmr_tokenizer)
填充標簽是必要的,因為與文本分類任務不同,標簽也是序列。這里的一個重要細節(jié)是,標簽序列被填充了-100的值,正如我們所看到的,PyTorch損失函數(shù)會忽略這個值。
我們將在本章中訓練幾個模型,所以我們將通過創(chuàng)建model_init()方法來避免為每個訓練者初始化一個新的模型。這個方法會加載一個未訓練過的模型,并在調用train()的開始階段被調用:
def model_init(): return (XLMRobertaForTokenClassification .from_pretrained(xlmr_model_name, config=xlmr_config) .to(device))
現(xiàn)在我們可以將所有這些信息連同編碼的數(shù)據(jù)集一起傳遞給Trainer:
from transformers import Trainer
trainer = Trainer(model_init=model_init, args=training_args, data_collator=data_collator, compute_metrics=compute_metrics, train_dataset=panx_de_encoded["train"], eval_dataset=panx_de_encoded["validation"], tokenizer=xlmr_tokenizer)
然后按如下方式運行訓練循環(huán),并將最終模型推送給Hub:
trainer.train() trainer.push_to_hub(commit_message="Training completed!")
這些F1分數(shù)對于一個NER模型來說是相當不錯的。為了確認我們的模型按預期工作,讓我們在我們的簡單例子的德語翻譯上測試它:
text_de = "Jeff Dean ist ein Informatiker bei Google in Kalifornien"
tag_text(text_de, tags, trainer.model, xlmr_tokenizer)
上圖可以看出模型是有效的! 但是,我們永遠不應該根據(jù)一個單一的例子而對性能過于自信。相反,我們應該對模型的錯誤進行適當和細致的驗證。在下一節(jié)中,我們將探討如何在NER任務中做到這一點。
錯誤分析
在我們深入研究XLM-R的多語言方面之前,讓我們花點時間調查一下我們模型的錯誤。正如我們在第二章中所看到的,在訓練和調試變換器(以及一般的機器學習模型)時,對你的模型進行徹底的錯誤分析是最重要的方面之一。有幾種失敗模式,在這些模式下,可能看起來模型表現(xiàn)良好,而實際上它有一些嚴重的缺陷。訓練可能失敗的例子包括:
-
我們可能不小心掩蓋了太多的標記,也掩蓋了一些我們的標簽,從而得到一個真正有希望的損失下降。
-
compute_metrics()函數(shù)可能有一個錯誤,高估了真實的性能。
-
我們可能會把NER中的零類或O類實體作為一個正常的類,這將嚴重歪曲準確率和F-score,因為它是大多數(shù)人的類,差距很大。
當模型的表現(xiàn)比預期的要差得多時,查看錯誤可以產(chǎn)生有用的見解,并揭示出僅通過查看代碼很難發(fā)現(xiàn)的錯誤。而且,即使模型表現(xiàn)良好,代碼中沒有錯誤,錯誤分析仍然是了解模型的優(yōu)點和缺點的有用工具。當我們在生產(chǎn)環(huán)境中部署模型時,這些方面我們始終需要牢記。
對于我們的分析,我們將再次使用我們所掌握的最強大的工具之一,那就是查看損失最大的驗證例子。我們可以重新使用我們在第二章中為分析序列分類模型而建立的大部分函數(shù),但是我們現(xiàn)在要計算樣本序列中每個標記的損失。
讓我們定義一個我們可以應用于驗證集的方法:
from torch.nn.functional import cross_entropy
def forward_pass_with_label(batch): # Convert dict of lists to list of dicts suitable for data collator features = [dict(zip(batch, t)) for t in zip(*batch.values())] # Pad inputs and labels and put all tensors on device batch = data_collator(features) input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) labels = batch["labels"].to(device) with torch.no_grad(): # Pass data through model output = trainer.model(input_ids, attention_mask) # logit.size: [batch_size, sequence_length, classes] # Predict class with largest logit value on classes axis predicted_label = torch.argmax(output.logits, axis=-1).cpu().numpy() # Calculate loss per token after flattening batch dimension with view loss = cross_entropy(output.logits.view(-1, 7), labels.view(-1), reduction="none")# Unflatten batch dimension and convert to numpy arrayloss = loss.view(len(input_ids), -1).cpu().numpy() return {"loss":loss, "predicted_label": predicted_label}
現(xiàn)在我們可以使用map()將這個函數(shù)應用于整個驗證集,并將所有的數(shù)據(jù)加載到一個DataFrame中進行進一步分析:
valid_set = panx_de_encoded["validation"]
valid_set = valid_set.map(forward_pass_with_label, batched=True, batch_size=32)
df = valid_set.to_pandas()
代幣和標簽仍然是用它們的ID編碼的,所以讓我們把代幣和標簽映射回字符串,以便更容易閱讀結果。對于標簽為-100的填充代幣,我們分配一個特殊的標簽,即IGN,這樣我們就可以在以后過濾它們。我們還通過將損失和預測標簽字段截斷到輸入的長度來擺脫所有的填充物:
index2tag[-100] = "IGN"
df["input_tokens"] = df["input_ids"].apply( lambda x: xlmr_tokenizer.convert_ids_to_tokens(x))
df["predicted_label"] = df["predicted_label"].apply( lambda x: [index2tag[i] for i in x])
df["labels"] = df["labels"].apply( lambda x: [index2tag[i] for i in x])
df['loss'] = df.apply( lambda x: x['loss'][:len(x['input_ids'])], axis=1)
df['predicted_label'] = df.apply( lambda x: x['predicted_label'][:len(x['input_ids'])], axis=1)
df.head(1)
每一列包含每個樣本的標記、標簽、預測標簽等的列表。讓我們通過拆開這些列表來逐一看看這些標記。這些列表。pandas.Series.explode()函數(shù)允許我們在一行中完全做到這一點,它為原始行列表中的每個元素創(chuàng)建一個行。由于一行中的所有列表都有相同的長度,我們可以對所有列進行并行處理。我們還放棄了我們命名為IGN的填充代幣,因為它們的損失反正是零。最后,我們將損失(仍然是numpy.Array對象)轉換成標準的浮點數(shù):
df_tokens = df.apply(pd.Series.explode)
df_tokens = df_tokens.query("labels != 'IGN'")
df_tokens["loss"] = df_tokens["loss"].astype(float).round(2)
df_tokens.head(7)
有了這種形式的數(shù)據(jù),我們現(xiàn)在可以按輸入標記進行分組,并用計數(shù)、平均值和總和對每個標記的損失進行匯總。最后,我們根據(jù)損失的總和對匯總的數(shù)據(jù)進行排序,看看哪些標記在驗證集中積累了最多的損失:
(
df_tokens.groupby("input_tokens")[["loss"]]
.agg(["count", "mean", "sum"])
.droplevel(level=0, axis=1)
# Get rid of multi-level columns
.sort_values(by="sum", ascending=False)
.reset_index()
.round(2)
.head(10)
.T
)
我們可以在這個列表中觀察到幾種模式。
-
空白符號的總損失最高,這并不令人驚訝,因為它也是列表中最常見的符號。然而,它的平均損失要比列表中的其他標記低得多。這意味著模型對它的分類并不費力。
-
像 “in”、“von”、"der "和 "und "這樣的詞出現(xiàn)得相對頻繁。它們經(jīng)常與命名的實體一起出現(xiàn),有時是它們的一部分,這解釋了為什么模型可能會把它們混在一起。
-
括號、斜線和單詞開頭的大寫字母比較少見,但其平均損失相對較高。我們將進一步調查它們。
我們還可以對標簽ID進行分組,看看每一類的損失:
(
df_tokens.groupby("labels")[["loss"]]
.agg(["count", "mean", "sum"])
.droplevel(level=0, axis=1)
.sort_values(by="mean", ascending=False)
.reset_index()
.round(2)
.T
)
我們看到,B-ORG的平均損失最高,這意味著確定一個組織的開始對我們的模型構成了挑戰(zhàn)。
我們可以通過繪制標記分類的混淆矩陣來進一步分解,我們看到一個組織的開始經(jīng)常與隨后的I-ORG標記相混淆:
from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
def plot_confusion_matrix(y_preds, y_true, labels): cm = confusion_matrix(y_true, y_preds, normalize="true") fig, ax = plt.subplots(figsize=(6, 6)) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels) disp.plot(cmap="Blues", values_format=".2f", ax=ax, colorbar=False) plt.title("Normalized confusion matrix") plt.show()
plot_confusion_matrix(df_tokens["labels"], df_tokens["predicted_label"], tags.names
從圖中我們可以看出,我們的模型傾向于混淆B-ORG和IORG實體最多。除此之外,它在對其余實體進行分類時表現(xiàn)得相當好,這一點從混淆矩陣的近對角線性質可以看出。
現(xiàn)在我們已經(jīng)檢查了標記水平上的錯誤,讓我們繼續(xù)看一下具有高損失的序列。在這個計算中,我們將重新審視我們的 "未爆炸 "數(shù)據(jù)框架,通過對每個標記的損失進行加總來計算總損失。要做到這一點,首先讓我們寫一個函數(shù),幫助我們顯示帶有標簽和損失的標記序列。
很明顯,這些樣本的標簽有問題;例如,聯(lián)合國和中非共和國分別被標為一個人!這是不對的。同時,第一個例子中的 "8.Juli "被標記為一個組織。事實證明,PAN-X數(shù)據(jù)集的注釋是通過一個自動過程產(chǎn)生的。這樣的注釋通常被稱為 “銀質標準”(與人類生成的注釋的 "黃金標準 "形成對比),而且毫不奇怪,有些情況下,自動方法未能產(chǎn)生合理的標簽。事實上,這樣的失敗模式并不是自動方法所獨有的;即使人類仔細地注釋數(shù)據(jù),當注釋者的注意力減退或者他們只是誤解了句子,也會發(fā)生錯誤。
我們先前注意到的另一件事是,括號和斜線的損失相對較高。讓我們來看看幾個帶有開頭小括號的序列的例子:
df_tmp = df.loc[df["input_tokens"].apply(lambda x: u"\u2581(" in x)].head(2)
for sample in get_samples(df_tmp): display(sample)
一般來說,我們不會把括號及其內容作為命名實體的一部分,但這似乎是自動提取注釋文件的方式。在其他的例子中,括號里包含了一個地理規(guī)范。雖然這確實也是一個位置,但我們可能希望在注釋中把它與原始位置斷開。這個數(shù)據(jù)集由不同語言的維基百科文章組成,文章標題經(jīng)常在括號中包含某種解釋。例如,在第一個例子中,括號里的文字表明Hama是一個 “Unternehmen”,即英文中的公司。當我們推出這個模型時,這些細節(jié)是很重要的,因為它們可能對模型所在的整個管道的下游性能產(chǎn)生影響。
通過一個相對簡單的分析,我們已經(jīng)發(fā)現(xiàn)了我們的模型和數(shù)據(jù)集的一些弱點。在一個真實的用例中,我們會反復進行這個步驟,清理數(shù)據(jù)集,重新訓練模型,分析新的錯誤,直到我們對性能感到滿意。
在這里,我們分析了單一語言的錯誤,但我們也對跨語言的性能感興趣。在下一節(jié)中,我們將進行一些實驗,看看XLM-R的跨語言轉移的效果如何。
跨語言遷移
現(xiàn)在我們已經(jīng)在德語上對XLM-R進行了微調,我們可以通過Trainer的predict()方法來評估它轉移到其他語言的能力。由于我們計劃評估多種語言,讓我們創(chuàng)建一個簡單的函數(shù),為我們做這件事:
def get_f1_score(trainer, dataset): return trainer.predict(dataset).metrics["test_f1"]
我們可以用這個函數(shù)來檢查測試集的性能,并在一個dict中記錄我們的分數(shù):
f1_scores = defaultdict(dict)
f1_scores["de"]["de"] = get_f1_score(trainer, panx_de_encoded["test"])
print(f"F1-score of [de] model on [de] dataset: {f1_scores['de']['de']:.3f}")F1-score of [de] model on [de] dataset: 0.868
對于一個NER任務來說,這些結果相當不錯。我們的指標在85%左右,我們可以看到該模型在ORG實體上似乎最吃力,可能是因為這些實體在訓練數(shù)據(jù)中最不常見,而且許多組織名稱在XLM-R的詞匯中很罕見。其他語言的情況如何?為了熱身,讓我們看看我們在德語上微調的模型在法語上的表現(xiàn)如何:
text_fr = "Jeff Dean est informaticien chez Google en Californie"
tag_text(text_fr, tags, trainer.model, xlmr_tokenizer)
還不錯! 雖然這兩種語言的名稱和組織都是一樣的,但該模型確實能夠正確標記 "Kalifornien "的法語翻譯。接下來,讓我們通過編寫一個簡單的函數(shù)來量化我們的德語模型在整個法語測試集上的表現(xiàn),該函數(shù)對數(shù)據(jù)集進行編碼并生成分類報告:
def evaluate_lang_performance(lang, trainer):panx_ds = encode_panx_dataset(panx_ch[lang]) return get_f1_score(trainer, panx_ds["test"])
f1_scores["de"]["fr"] = evaluate_lang_performance("fr", trainer)
print(f"F1-score of [de] model on [fr] dataset: {f1_scores['de']['fr']:.3f}") F1-score of [de] model on [fr] dataset: 0.714
雖然我們看到微觀平均指標下降了約15分,但請記住,我們的模型還沒有看到一個貼有標簽的法語例子!一般來說,性能的下降與語言之間的 "距離 "有關。一般來說,性能下降的大小與語言之間的 "距離 "有關。雖然德語和法語被歸類為印歐語系,但從技術上講,它們屬于不同的語系。分別是日耳曼語和羅曼語。
接下來,讓我們評估一下在意大利語上的表現(xiàn)。由于意大利語也是一種羅曼語,我們期望得到一個與法語類似的結果:
f1_scores["de"]["it"] = evaluate_lang_performance("it", trainer)
print(f"F1-score of [de] model on [it] dataset: {f1_scores['de']['it']:.3f}") F1-score of [de] model on [it] dataset: 0.692
事實上,我們的期望得到了F-scores的證實。最后,讓我們來看看英語的表現(xiàn),它屬于日耳曼語系的語言:
f1_scores["de"]["en"] = evaluate_lang_performance("en", trainer)
print(f"F1-score of [de] model on [en] dataset: {f1_scores['de']['en']:.3f}") F1-score of [de] model on [en] dataset: 0.589
令人驚訝的是,我們的模型在英語上的表現(xiàn)最差,盡管我們可能直觀地期望德語比法語更類似于英語。在對德語進行了微調并對法語和英語進行了零點轉移之后,接下來讓我們看看什么時候直接對目標語言進行微調是有意義的。
零樣本遷移何時才能生效?
到目前為止,我們已經(jīng)看到,在德語語料庫上對XLM-R進行微調,可以得到85%左右的F-score,而且不需要任何額外的訓練,該模型就能夠在我們的語料庫中的其他語言上取得適度的表現(xiàn)。問題是,這些結果有多好,它們與在單語語料庫上微調的XLM-R模型相比如何?
在本節(jié)中,我們將通過在越來越大的訓練集上對XLM-R進行微調,來探索法語語料庫的這個問題。通過這種方式跟蹤性能,我們可以確定在哪一點上零點跨語言轉移更有優(yōu)勢,這在實踐中對指導關于是否收集更多標記數(shù)據(jù)的決定很有用。
為了簡單起見,我們將保持對德語語料庫進行微調時的超參數(shù),只是我們將調整TrainingArguments的logging_steps參數(shù),以考慮到訓練集規(guī)模的變化。我們可以用一個簡單的函數(shù)把這一切包起來,該函數(shù)接收一個對應于單語語料庫的DatasetDict對象,通過num_samples對其進行降樣,并對XLM-R進行微調,以返回最佳歷時的度量:
def train_on_subset(dataset, num_samples): train_ds = dataset["train"].shuffle(seed=42).select(range(num_samples)) valid_ds = dataset["validation"] test_ds = dataset["test"] training_args.logging_steps = len(train_ds) // batch_size trainer = Trainer(model_init=model_init, args=training_args, data_collator=data_collator, compute_metrics=compute_metrics, train_dataset=train_ds, eval_dataset=valid_ds, tokenizer=xlmr_tokenizer) trainer.train() if training_args.push_to_hub: trainer.push_to_hub(commit_message="Training completed!") f1_score = get_f1_score(trainer, test_ds) return pd.DataFrame.from_dict( {"num_samples": [len(train_ds)], "f1_score": [f1_score]})
正如我們對德語語料庫的微調一樣,我們也需要將法語語料庫編碼為輸入ID、注意力掩碼和標簽ID:
panx_fr_encoded = encode_panx_dataset(panx_ch["fr"])
接下來,讓我們通過在250個例子的小型訓練集上運行來檢查我們的函數(shù)是否有效:
training_args.push_to_hub = False
metrics_df = train_on_subset(panx_fr_encoded, 250) metrics_df
我們可以看到,在只有250個例子的情況下,法語的微調在很大程度上低于德語的零槍轉移。現(xiàn)在讓我們把訓練集的大小增加到500、1000、2000和4000個例子,以了解性能的提高:
for num_samples in [500, 1000, 2000, 4000]: metrics_df = metrics_df.append( train_on_subset(panx_fr_encoded, num_samples), ignore_index=True)
我們可以通過繪制測試集上的F-scores作為增加訓練集大小的函數(shù),來比較法語樣本的微調與德語的零點跨語言轉移之間的比較:
fig, ax = plt.subplots()
ax.axhline(f1_scores["de"]["fr"], ls="--", color="r")
metrics_df.set_index("num_samples").plot(ax=ax)
plt.legend(["Zero-shot from de", "Fine-tuned on fr"], loc="lower right") plt.ylim((0, 1))
plt.xlabel("Number of Training Samples")
plt.ylabel("F1 Score")
plt.show()
從圖中我們可以看到,在大約750個訓練實例之前,零點轉移一直是有競爭力的,在這之后,對法語的微調達到了與我們對德語微調時類似的性能水平。然而,這個結果是不容忽視的。根據(jù)我們的經(jīng)驗,讓領域專家給幾百個文檔貼上標簽的成本很高,特別是對NER來說,貼標簽的過程很細而且很耗時。
我們可以嘗試最后一種技術來評估多語言學習:同時對多種語言進行微調!讓我們來看看如何進行微調。讓我們來看看我們如何做到這一點。
一次性對多種語言進行微調
到目前為止,我們已經(jīng)看到,從德語到法語或意大利語的零拍跨語言轉移產(chǎn)生了約15點的性能下降。緩解這種情況的一個方法是同時對多種語言進行微調。為了看看我們能得到什么類型的收益,讓我們首先使用 concatenate_datasets()函數(shù),將德語和法語語料庫連接起來:
from datasets import concatenate_datasets
def concatenate_splits(corpora): multi_corpus = DatasetDict() for split in corpora[0].keys(): multi_corpus[split] = concatenate_datasets( [corpus[split] for corpus in corpora]).shuffle(seed=42) return multi_corpus
panx_de_fr_encoded = concatenate_splits([panx_de_encoded, panx_fr_encoded])
對于訓練,我們將再次使用前幾節(jié)的超參數(shù),因此我們可以簡單地更新訓練器中的記錄步驟、模型和數(shù)據(jù)集:
training_args.logging_steps = len(panx_de_fr_encoded["train"]) // batch_size
training_args.push_to_hub = True
training_args.output_dir = "xlm-roberta-base-finetuned-panx-de-fr"
trainer = Trainer(model_init=model_init, args=training_args, data_collator=data_collator, compute_metrics=compute_metrics, tokenizer=xlmr_tokenizer, train_dataset=panx_de_fr_encoded["train"], eval_dataset=panx_de_fr_encoded["validation"])
trainer.train()
trainer.push_to_hub(commit_message="Training completed!")
讓我們來看看該模型在每種語言的測試集上的表現(xiàn):
for lang in langs: f1 = evaluate_lang_performance(lang, trainer) print(f"F1-score of [de-fr] model on [{lang}] dataset: {f1:.3f}")
F1-score of [de-fr] model on [de] dataset: 0.866 F1-score of [de-fr] model on [fr] dataset: 0.868 F1-score of [de-fr] model on [it] dataset: 0.815 F1-score of [de-fr] model on [en] dataset: 0.677
它在法語分詞上的表現(xiàn)比以前好得多,與德語測試集上的表現(xiàn)相當。有趣的是,它在意大利語和英語部分的表現(xiàn)也提高了大約10個百分點。因此,即使增加另一種語言的訓練數(shù)據(jù),也能提高該模型在未見過的語言上的表現(xiàn)。
讓我們通過比較在每種語言上的微調和在所有語料庫上的多語言學習的性能來完成我們的分析。由于我們已經(jīng)對德語語料庫進行了微調,我們可以用train_on_subset()函數(shù)對其余語言進行微調,num_samples等于訓練集的例子數(shù)量。
corpora = [panx_de_encoded]
# Exclude German from iteration
for lang in langs[1:]: training_args.output_dir = f"xlm-roberta-base-finetuned-panx-{lang}" # Fine-tune on monolingual corpus ds_encoded = encode_panx_dataset(panx_ch[lang]) metrics = train_on_subset(ds_encoded, ds_encoded["train"].num_rows) # Collect F1-scores in common dict f1_scores[lang][lang] = metrics["f1_score"][0]# Add monolingual corpus to list of corpora to concatenate corpora.append(ds_encoded)
現(xiàn)在我們已經(jīng)對每種語言的語料庫進行了微調,下一步是將所有的分片串聯(lián)起來,創(chuàng)建一個所有四種語言的多語言語料庫。與之前的德語和法語分析一樣,我們可以使用concatenate_splits()函數(shù)來為我們在上一步生成的語料庫列表上完成這一步驟:
corpora_encoded = concatenate_splits(corpora)
現(xiàn)在我們有了我們的多語言語料庫,我們用訓練器運行熟悉的步驟:
training_args.logging_steps = len(corpora_encoded["train"]) // batch_size
training_args.output_dir = "xlm-roberta-base-finetuned-panx-all"
trainer = Trainer(model_init=model_init, args=training_args, data_collator=data_collator, compute_metrics=compute_metrics, tokenizer=xlmr_tokenizer, train_dataset=corpora_encoded["train"], eval_dataset=corpora_encoded["validation"])
trainer.train()
trainer.push_to_hub(commit_message="Training completed!")
最后一步是在每種語言的測試集上生成訓練器的預測結果。這將使我們深入了解多語言學習的真正效果。我們將在f1_scores字典中收集F-scores,然后創(chuàng)建一個DataFrame,總結我們多語言實驗的主要結果:
for idx, lang in enumerate(langs): f1_scores["all"][lang] = get_f1_score(trainer, corpora[idx]["test"])
scores_data = {"de": f1_scores["de"], "each": {lang: f1_scores[lang][lang] for lang in langs}, "all": f1_scores["all"]}
f1_scores_df = pd.DataFrame(scores_data).T.round(4)
f1_scores_df.rename_axis(index="Fine-tune on", columns="Evaluated on", inplace=True)f1_scores_df
從這些結果中,我們可以得出一些一般性的結論。
- 多語言學習可以帶來顯著的性能提升,尤其是當跨語言轉移的低資源語言屬于類似的語言家族時。在我們的實驗中,我們可以看到德語、法語和意大利語在所有類別中都取得了相似的表現(xiàn),這表明這些語言之間的相似度要高于英語。
- 作為一個一般的策略,把注意力集中在語言家族內的跨語言轉移是一個好主意,特別是在處理像日語這樣的不同文字時。
發(fā)布模型部件
在這一章中,我們已經(jīng)推送了很多微調過的模型到 Hub 上。盡管我們可以使用 pipeline() 函數(shù)在我們的本地機器上與它們進行交互,但 Hub 提供了非常適合這種工作流程的部件。圖4-5是我們的transformersbook/xlm-roberta-base-finetuned-panx-all檢查點的一個例子,你可以看到它在識別一個德語文本的所有實體方面做得很好。
小結
在本章中,我們看到了如何使用一個在100種語言上預訓練過的單一Transformers來處理一個多語言語料庫的NLP任務: XLM-R。盡管我們能夠證明,當只有少量的標記例子可供微調時,從德語到法語的跨語言轉換是有效的的,但如果目標語言與基礎模型被微調的語言有很大不同,或者不是預訓練時使用的100種語言之一,這種良好的性能通常就不會出現(xiàn)。最近的建議,如MAD-X,正是為這些低資源的情況而設計的,由于MAD-X是建立在Transformers之上的,你可以很容易地調整本章的代碼來使用它。
到目前為止,我們看了兩個任務:序列分類和標記分類。這兩個任務都屬于自然語言理解的范疇,即把文本合成為預測。在下一章中,我們將首次看到文本生成,在這里,不僅輸入內容是文本,模型的輸出也是文本。
總結
以上是生活随笔為你收集整理的nlp-with-transformers系列-04_多语言命名实体识别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Educational Codeforc
- 下一篇: 中国计算机学会CCF推荐国际学术会议和期