- ??????第1章 對大型語言模型的介紹
- 第2章 分詞和嵌入
- 第3章 解析大型語言模型的內部機制
- 第4章 文本分類
- 第5章 文本聚類與主題建模
- 第6章 提示工程
- 第7章 高級文本生成技術與工具
- 第8章 語義搜索與檢索增強生成
- 第9章 多模態大語言模型
- 第10章 構建文本嵌入模型
- 第12章 微調生成模型
在第四章中,我們使用了預訓練模型對文本進行分類。我們直接使用了未經任何修改的預訓練模型。這可能會讓你產生疑問:如果我們對模型進行微調會發生什么?
如果有充足的數據,微調通常能產生性能最佳的模型。在本章中,我們將探討幾種微調BERT模型的方法和應用:
- 《監督式分類》展示了微調分類模型的通用流程;
- 《少樣本分類》將介紹SetFit方法——一種通過少量訓練樣本高效微調高性能模型的技術;
- 《基于掩碼語言建模的繼續預訓練》探討了如何對預訓練模型進行持續訓練;
- 《命名實體識別》研究了基于詞元級別的分類任務。
我們將專注于非生成式任務,生成式模型相關內容將在第十二章中討論。
監督式分類
在第4章中,我們通過利用預訓練的表示模型探索了監督式分類任務。這些模型要么是經過訓練以預測情感(任務特定模型),要么是用于生成嵌入表示(嵌入模型),如圖11-1所示。
?
圖11-1. 在第4章中,我們使用了預訓練模型執行分類任務,但未更新其權重。這些模型被保持為"凍結"狀態
這兩個模型均保持凍結(不可訓練)狀態,以展示利用預訓練模型進行分類任務的潛力。嵌入模型采用了獨立的可訓練分類頭(classifier)來預測電影評論的情感極性。在本節中,我們將采取類似方法,但允許同時對基礎模型和分類頭進行訓練更新。如圖11-2所示,我們將不再使用獨立的嵌入模型,而是通過微調預訓練BERT模型來創建任務專用模型(類似于第2章中的實現方式)。與嵌入模型方法相比,這次我們將表示模型(representation model)和分類頭作為統一架構進行聯合微調。
?
圖11-2. 與"凍結"架構不同,我們會對預訓練BERT模型和分類頭都進行訓練。反向傳播過程將從分類頭開始,經過BERT模型(進行傳播)
為此,我們不是凍結模型,而是使其在整個訓練過程中保持可訓練狀態并更新參數。如圖11-3所示,我們將采用一個預訓練的BERT模型,并在其基礎上添加一個作為分類頭的神經網絡結構。這兩部分組件都將通過微調過程進行優化,以適配具體的分類任務需求。
?
圖11-3. 任務特定模型的架構。它包含一個預訓練模型(例如BERT),并附加了一個專用于該任務的分類頭
實際應用中,這意味著預訓練BERT模型和分類頭會共同更新。它們并非獨立運行,而是通過相互學習實現更精準的特征表征。
微調預訓練的BERT模型
我們將使用第4章中使用的相同數據集來微調模型,即包含來自爛番茄的5,331條正面和5,331條負面電影評論的爛番茄數據集。
from datasets import load_dataset#?Prepare data?and splits
tomatoes = load_dataset("rotten_tomatoes")
train_data, test_data = tomatoes["train"], tomatoes["test"]
我們分類任務的第一步是選擇要使用的基礎模型。我們選用了"bert-base-cased",該模型在英文維基百科以及包含未發表書籍的大型數據集上進行了預訓練[1]。我們需要預先定義想要預測的標簽數量,這是構建于預訓練模型之上的前饋神經網絡所必需的步驟:
from transformers import AutoTokenizer, AutoModelForSequenceClassification#?Load?model and tokenizer
model_id =?"bert-base-cased"
model = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=2
)
tokenizer = AutoTokenizer.from_pretrained(model_id)?
接下來,我們將對數據進行分詞處理:
from transformers import DataCollatorWithPadding# Pad to?the?longest sequence?in?the?batch
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
def preprocess_function(examples):"""Tokenize?input data"""return tokenizer(examples["text"], truncation=True)
# Tokenize?train/test?data
tokenized_train = train_data.map(preprocess_function, batched=True)
tokenized_test = test_data.map(preprocess_function, batched=True)
在創建Trainer之前,我們需要準備一個特殊的DataCollator。DataCollator是一個幫助我們構建數據批次的工具類,同時它還允許我們應用數據增強技術。
在分詞過程中(如第9章所示),我們會通過填充(padding)手段使輸入文本形成統一長度的表征。為此,我們使用DataCollatorWithPadding來實現這一功能。
當然,如果沒有定義一些評估指標(metrics),完整的示例代碼就略顯遺憾了:
import numpy?as?np
from datasets import load_metricdef compute_metrics(eval_pred):"""Calculate F1?score"""logits, labels =?eval_predpredictions = np.argmax(logits, axis=-1)load_f1 = load_metric("f1")f1 = load_f1.compute(predictions=predictions,?references=labels)["f1"]return?{"f1": f1}
1 ?Jacob Devlin et al.?“BERT:?Pre-training?of?deep bidirectional?transformers?for?language?understanding.” arXiv?preprint arXiv:1810.04805?(2018).
通過compute_metrics,我們可以定義任意數量的感興趣的指標,這些指標可以在訓練過程中打印輸出或記錄日志。這在訓練過程中特別有用,因為它可以幫助檢測模型的過擬合行為。接下來我們將實例化我們的訓練器(Trainer):
from transformers import TrainingArguments, Trainer# Training arguments?for parameter?tuning
training_args = TrainingArguments("model",learning_rate=2e-5,per_device_train_batch_size=16,per_device_eval_batch_size=16,num_train_epochs=1,weight_decay=0.01,save_strategy="epoch",report_to="none"
)# Trainer which?executes?the?training process
trainer = Trainer(model=model,args=training_args,train_dataset=tokenized_train,eval_dataset=tokenized_test,tokenizer=tokenizer,data_collator=data_collator,compute_metrics=compute_metrics,
)
TrainingArguments類定義了需要調整的超參數,例如學習率和訓練輪次(rounds)。Trainer用于執行訓練過程。
最后,我們可以訓練模型并進行評估:
trainer.evaluate()
{'eval_loss': 0.3663691282272339,
'eval_f1': 0.8492366412213741,
'eval_runtime': 4.5792,
'eval_samples_per_second': 232.791,
'eval_steps_per_second': 14.631,
'epoch':?1.0}
我們的F1分數達到了0.85,明顯高于第四章中使用的任務特定模型的0.80。這表明自行微調模型比直接使用預訓練模型更具優勢,而整個訓練過程僅需花費我們幾分鐘時間。
凍結層
為了進一步展示訓練整個網絡的重要性,接下來的示例將演示如何使用 Hugging Face Transformers 凍結網絡的特定層。
我們將凍結主 BERT 模型,并僅允許更新通過分類頭(classification head)。這將是一個很好的對比,因為我們除凍結特定層外,其他設置均保持不變。
首先,讓我們重新初始化模型,以便從頭開始訓練:
#?Load?model and tokenizermodel = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=2
)tokenizer = AutoTokenizer.from_pretrained(model_id)
我們預訓練的BERT模型包含許多可以凍結的層。審視這些層有助于深入了解網絡結構,并明確哪些部分可能需要凍結:
#?Print?layer names
for name, param in model.named_parameters():
print(name)
bert.embeddings.word_embeddings.weight
bert.embeddings.position_embeddings.weight
bert.embeddings.token_type_embeddings.weight
bert.embeddings.LayerNorm.weight
bert.embeddings.LayerNorm.bias
bert.encoder.layer.0.attention.self.query.weight
bert.encoder.layer.0.attention.self.query.bias
...
bert.encoder.layer.11.output.LayerNorm.weight
bert.encoder.layer.11.output.LayerNorm.bias
bert.pooler.dense.weight
bert.pooler.dense.bias
classifier.weight
classifier.bias
有12個(編號0至11)編碼器塊,每個包含注意力頭、密集網絡和層歸一化模塊。我們在圖11-4中進一步詳細說明了該架構,以展示所有可能被凍結的部分。此外,我們還添加了分類頭模塊。
?
圖11-4. 帶有額外分類頭的BERT基礎架構
我們可以選擇僅凍結某些層以加快計算速度,同時仍允許主模型從分類任務中學習。通常,我們希望被凍結的層后面跟著可訓練層。
我們將像第2章中那樣,凍結除分類頭之外的所有部分。
for name, param in model.named_parameters():# Trainable classification?headif name.startswith("classifier"):param.requires_grad = True# Freeze everything elseelse:param.requires_grad =?False
如圖11-5所示,我們凍結了除前饋神經網絡(即分類頭)之外的所有組件。
?
圖11-5. 我們對所有編碼器模塊和嵌入層進行完全凍結,使得BERT模型在微調過程中不會學習新的表示
由于我們已成功凍結了除分類頭之外的所有組件,現在可以開始訓練模型了:
from transformers import TrainingArguments, Trainer# Trainer which?executes?the?training process
trainer = Trainer(model=model,args=training_args,train_dataset=tokenized_train,eval_dataset=tokenized_test,tokenizer=tokenizer,data_collator=data_collator,compute_metrics=compute_metrics,
)trainer.train()
你可能會注意到訓練速度變得快了很多。這是因為我們僅訓練了分類頭(classification head),與微調整個模型相比,這種做法為我們帶來了顯著的速度提升:
trainer.evaluate()
{'eval_loss': 0.6821751594543457,
'eval_f1': 0.6331058020477816,
'eval_runtime': 4.0175,
'eval_samples_per_second': 265.337,
'eval_steps_per_second': 16.677,
'epoch':?1.0}
在評估模型時,我們只得到了0.63的F1分數,明顯低于原來的0.85分。與其凍結幾乎所有層,不如嘗試按照圖11-6的示意,將編碼器塊10之前的所有層進行凍結,觀察這對性能會產生什么影響。這樣做的主要優勢在于既能減少計算量,又能讓更新信息繼續通過預訓練模型的部分結構傳遞:
?
圖11-6. 我們凍結了BERT模型的前10個編碼器塊。其余部分均可訓練,并將進行微調
#?Load?modelmodel_id =?"bert-base-cased"
model = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=2
)tokenizer = AutoTokenizer.from_pretrained(model_id)
# Encoder block 11 starts?at?index?165?and
# we freeze everything before?that block
for index, (name, param) in enumerate(model.named_parameters()):if index?<?165:param.requires_grad =?False# Trainer which?executes?the?training process
trainer = Trainer(model=model,args=training_args,train_dataset=tokenized_train,eval_dataset=tokenized_test,tokenizer=tokenizer,data_collator=data_collator,compute_metrics=compute_metrics,
)trainer.train()
訓練結束后,我們評估結果:
trainer.evaluate()
{'eval_loss'?: 0.40812647342681885,
'eval_f1'?: 0.8,
'eval_runtime'?: 3.7125,
'eval_samples_per_second'?: 287.137,
'eval_steps_per_second'?: 18.047,
'epoch'?:?1.0}
我們的F1分數達到了0.8,這比之前凍結所有層時的0.63有了顯著提升。這表明,盡管我們通常希望盡可能多地訓練網絡層,但在計算資源有限的情況下,適當減少訓練層數仍然可以達到預期效果。
為了進一步驗證這種效應,我們測試了分階段凍結編碼器塊并進行微調的效果(如圖11-7所示)。實驗結果表明,僅訓練前五個編碼器塊(紅色垂直線標注位置)已能接近完全訓練所有編碼器塊的性能水平。
?
圖11-7. 凍結特定編碼器塊對模型性能的影響。隨著訓練塊數量的增加,模型性能有所提升,但該提升趨勢在初期階段即趨于穩定
在訓練多個輪次(epoch)時,凍結(freeze)與不凍結模型在訓練時間和資源消耗上的差異通常會變得更加顯著。因此,建議你通過實踐找到適合自身需求的平衡點。
少樣本分類
少樣本分類是監督分類中的一項技術,其特點是通過僅使用少量帶標簽的示例來訓練分類器以學習目標標簽。當面臨分類任務但缺乏充足現成可用的標注數據時,這項技術尤為有效。換言之,該方法允許我們為每個類別標注少量高質量數據點用于模型訓練(如圖11-8所示)。這種通過少量標注數據訓練模型的理念,充分體現了少樣本分類的核心優勢。
?
圖 11-8. 在少樣本分類中,我們僅使用少量詞元數據點進行學習。
SetFit:僅需少量訓練樣本的高效微調框架
為了實現少樣本文本分類,我們提出了一種名為 SetFit 的高效框架。該框架基于 sentence-transformers 的架構構建,能夠生成高質量的文本表示,并在訓練過程中動態更新這些表示。實驗表明,僅需少量標注樣本,SetFit 即可與基于大規模標注數據集微調類似 BERT 模型的方法相媲美(如前文案例所示)。
SetFit 的核心算法包含以下三個步驟:
1.訓練數據采樣
基于同類(in-class)和異類(out-class)標注數據的篩選,生成正(相似)、負(不相似)樣本對。
2 ?Lewis Tunstall et al.?“Efficient?few-shot?learning?without?prompts.” arXiv?preprint arXiv:2209.11055?(2022).
2.微調嵌入
基于先前生成的訓練數據對預訓練的嵌入模型進行微調
3.訓練分類器
在嵌入模型上方添加一個分類頭(classification head),并使用先前生成的訓練數據對其進行訓練
在微調嵌入模型之前,我們需要生成訓練數據。該模型要求訓練數據是句子對的樣本,包含正例(相似)和反例(不相似)兩種類型。然而,當處理分類任務時,我們的輸入數據通常并沒有被這樣標注。
例如,假設我們擁有圖11-9所示的分類訓練數據集,該數據集將文本分為兩類:關于編程語言的文本和關于寵物的文本。
?
圖11-9. 兩類數據:編程語言相關文本與寵物相關文本
在步驟1中,SetFit通過基于同類別選樣和跨類別選樣的方式生成所需數據(如圖11-10所示)。例如,當我們擁有16條關于體育的句子時,可以通過計算16 * (16 – 1) / 2 = 120生成詞元為正樣本對的配對。我們也可以通過收集不同類別之間的配對來生成負樣本對。
?
圖11-10 步驟1:采樣訓練數據。我們假設同一類別內的句子具有相似性并構建正樣本對,而不同類別間的句子則構成負樣本對
在第2步中,我們可以使用生成的句子對來微調嵌入模型。這種方法利用了名為對比學習的技術來微調預訓練的BERT模型。如第10章所述,對比學習能夠通過相似(正例)和不相似(反例)句子對的訓練,學習到準確的句子嵌入表示。
由于這些句子對已在之前的步驟中生成,我們可以直接用于微調SentenceTransformers模型。雖然我們之前已討論過對比學習方法,但為了便于回顧,在圖11-11中再次展示了該方法的具體實現流程。
?
圖11-11. 步驟2:微調SentenceTransformers模型。通過對比學習,模型從正負句子對中學習嵌入表示
微調此嵌入模型的目標是使其能夠生成針對分類任務調整后的嵌入。通過微調嵌入模型,類別之間的相關性及其相對含義將被提煉并融入到生成的嵌入中。
在第三步中,我們為所有句子生成嵌入向量,并將其作為分類器的輸入。我們可以使用微調后的SentenceTransformers模型將句子轉換為可供特征使用的嵌入表示。分類器通過學習這些微調后的嵌入表示,能夠準確預測未見過的句子。圖11-12展示了這最后一步的實現過程。
?
圖11-12 步驟3:訓練分類器。該分類器可以是任何scikit-learn模型或分類頭。
將所有步驟整合后,我們得到了一套高效優雅的處理框架,可在每個類別僅有少量標注樣本時完成文本分類任務。該方法巧妙運用了我們已標注數據的特點——盡管其形式與理想標注方式存在差異。圖11-13通過三個步驟的示意圖,全面概述了整個流程:
首先,基于同類樣本和跨類樣本選擇策略生成句對;
其次,使用生成的句對對預訓練的SentenceTransformer模型進行微調;
最后,通過微調后的模型對句子進行嵌入表征,并在其上訓練分類器實現類別預測
?
圖11-13. SetFit的三個主要步驟
少樣本分類的微調
我們之前訓練時使用的數據集包含約8,500條電影評論。然而,在少樣本設置下,我們將每個類別僅抽取16個樣本。當只有兩個類別時,相比之前使用的8,500條電影評論,我們現在僅有32個文檔用于訓練!
from setfit import sample_dataset# We simulate a few-shot setting by sampling?16?examples per?class
sampled_train_data = sample_dataset(tomatoes["train"], num_samples=16)
在完成數據采樣后,我們選擇一個預訓練的SentenceTransformer模型進行微調。官方文檔提供了預訓練SentenceTransformer模型的概覽,我們從中選用了"sentence-transformers/all-mpnet-base-v2"。該模型在MTEB(大規模多任務嵌入評估)排行榜上表現優異,該排行榜展示了嵌入模型在各類任務中的性能表現。
from setfit import SetFitModel#?Load?a pretrained SentenceTransformer model
model = SetFitModel.from_pretrained("sentence-transformers/all-mpnet-base-v2")
在加載預訓練的SentenceTransformer模型后,我們可以開始定義SetFitTrainer。默認情況下,系統會選擇邏輯回歸模型作為待訓練的分類器。與我們在Hugging Face Transformers中的操作類似,可以通過訓練器定義并調整相關參數。例如,我們將num_epochs設為3,這樣對比學習將會進行三個輪次:
from setfit import TrainingArguments as SetFitTrainingArguments
from setfit import Trainer as?SetFitTrainer#?Define?training arguments
args = SetFitTrainingArguments(num_epochs=3, # The number of epochs?to?use for contrastive?learningnum_iterations=20 ?# The number of?text?pairs?to?generate
)args.eval_strategy = args.evaluation_strategy# Create?trainer
trainer = SetFitTrainer(model=model,args=args,train_dataset=sampled_train_data,eval_dataset=test_data,metric="f1"
)
我們只需調用train即可啟動訓練循環。此時應該會看到以下輸出:
#?Training?loop
trainer.train()
***** Running training?*****
Num unique?pairs?=?1280
Batch?size =?16
Num epochs?=?3
Total optimization steps?=?240
請注意,輸出內容中提到為微調SentenceTransformer模型生成了1,280個句子對。默認情況下,我們的每個樣本會生成20組句子配對(即20×32=680組)。由于每個樣本需要生成正向和反向兩個方向的配對,因此需要將基數乘以2,即680×2=1,280個句子對。考慮到最初僅有32個標注句子作為起點,最終生成了1,280個句子對,這一成果真是了不起!
當我們沒有明確指定分類頭時,默認使用邏輯回歸。如果希望自行指定分類頭,可以通過在SetFitTrainer中配置以下模型實現:
#?Load?a SetFit model from Hub
model = SetFitModel.from_pretrained(
"sentence-transformers/all-mpnet-base-v2",?use_differentiable_head=TΓue,
head_params={"out_features":?num_classes},
)
# Create?trainer
trainer = SetFitTrainer(
model=model,
...
)
此處,num_classes 表示需要預測的類別數量.
接下來,我們評估模型以了解其性能表現:
# Evaluate?the model on?our?test?data
trainer.evaluate()
{'f1': 0.8363988383349468}
僅憑32份標注文檔,我們就獲得了0.85的F1分數。考慮到該模型是在原始數據的極小子集上訓練的,這一表現已經非常出色!此外,在第二章中,我們通過在全數據的嵌入表示上訓練邏輯回歸模型,取得了相同的性能。因此,這一流程展示了花費時間標注少量實例的潛力。
SetFit不僅可以執行少樣本分類任務,還支持在完全沒有標簽的情況下使用(即零樣本分類)。SetFit會從標簽名稱生成合成示例以模擬分類任務,然后用這些合成示例訓練SetFit模型。例如,如果目標標簽是"happy(開心)"和"sad(悲傷)",則生成的合成數據可能是"這個示例是開心的"和"這個示例是悲傷的"。
基于掩碼語言模型的持續預訓練
在之前的示例中,我們使用了預訓練模型并對其進行微調以執行分類任務。這一過程包含兩個步驟:首先進行模型的預訓練(這一步已由我們完成),隨后針對特定任務進行微調。我們通過圖11-14展示了該過程的示意圖。
?
圖11-14. 要在目標任務(例如分類任務)上微調模型,我們可以選擇從頭開始預訓練BERT模型,或者使用已有的預訓練模型
這種兩步法廣泛應用于眾多場景。但當面對特定領域數據時存在局限性——預訓練模型通常基于維基百科等通用數據進行訓練,可能無法適配您特定領域的詞匯。
與其采用這種兩步法,我們可以在中間增加一個環節:對已預訓練的BERT模型進行繼續預訓練(continued pretraining)。換言之,我們可以繼續使用掩碼語言建模(MLM)任務訓練BERT模型,只不過改用領域內數據。這類似于從通用BERT模型到專注醫療領域的BioBERT模型,再到針對藥物分類任務微調的BioBERT模型的演進過程。
這將更新子詞表示,使其更適應模型之前未見過的詞匯。如圖11-15所示,這一過程展示了額外步驟如何更新掩碼語言建模任務。在預訓練的BERT模型上繼續進行預訓練已被證實能夠提升模型在分類任務中的性能,這是微調流程中值得加入的關鍵步驟3。
?
圖11-15 與采用兩步法不同,我們可以增加一個額外步驟——在目標任務的微調之前繼續對預訓練模型進行預訓練。請注意,在圖1中掩碼被填充了抽象概念,而在圖2中則填充了電影特定概念。
無需從頭開始訓練整個模型,我們可以直接在預訓練基礎上繼續訓練,隨后再針對分類任務進行微調。這種方法還能幫助模型更好地適應特定領域,甚至掌握某個組織的內部術語。企業可能采用的模型傳承關系如圖11-16所示。
3 ?Chi Sun et?al.?“How to?fine–tune?GERT?for?text?classification?”?Chinese?Computational Linguistics:?18th???China National?Conference, CCL 2019, Kunming, China,?October?18-20, 2019,?proceedings?18.?Springer?International Publishing, 2019.
在本示例中,我們將演示如何應用第二步并繼續預訓練已經過預訓練的BERT模型。我們使用最初啟用的相同數據集,即Rotten Tomatoes影評數據。
首先加載我們迄今為止使用的"bert-base-cased"模型,并對其進行MLM(掩碼語言建模)任務的準備:
from transformers import AutoTokenizer, AutoModelForMaskedLM#?Load?model for masked language modeling?(MLM)
model = AutoModelForMaskedLM.from_pretrained("bert-base-cased")
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
我們需要對原始句子進行分詞。由于這不是一個監督任務,我們還將移除標簽:
def preprocess_function(examples):return tokenizer(examples["text"], truncation=True)#?Tokenize?data
tokenized_train = train_data.map(preprocess_function, batched=True)
tokenized_train = tokenized_train.remove_columns("label")
tokenized_test = test_data.map(preprocess_function, batched=True)
tokenized_test = tokenized_test.remove_columns("label")
之前,我們使用的是DataCollatorWithPadding,它會動態填充接收到的輸入。
現在我們將改用另一種數據整理器,由它來為我們執行詞元的掩碼處理。通常有兩種實現方式:詞元掩碼(token masking)和全詞掩碼(whole-word masking)。在詞元掩碼方法中,我們會隨機掩碼句子中15%的詞元,這可能會導致單詞的部分字符被掩碼。如圖11-17所示,若要實現整個單詞的掩碼,我們可以采用全詞掩碼方法。
?
通常來說,預測整個單詞比預測詞元(tokens)更為復雜,這會使模型在訓練過程中需要學習更準確、更精確的表征,從而表現更佳。然而,這種方式往往需要更多時間才能收斂。在本示例中,我們使用DataCollatorForLanguageModeling采用詞元掩碼(token masking)以實現更快的收斂速度。不過,通過將DataCollatorForLanguageModeling替換為DataCollatorForWholeWordMask,我們可以改用全詞掩碼(whole-word masking)。最后,我們將給定句子中被掩碼詞元的概率設置為15%(對應參數mlm_probability)。
from transformers import DataCollatorForLanguageModeling#?Masking?Tokens
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=True,mlm_probability=0.15
)
接下來,我們將創建用于運行MLM任務的Trainer,并指定某些參數:
# Training arguments?for parameter?tuning
training_args = TrainingArguments("model",learning_rate=2e-5,per_device_train_batch_size=16,per_device_eval_batch_size=16,num_train_epochs=10,weight_decay=0.01,save_strategy="epoch",report_to="none"
)# Initialize?Trainer
trainer = Trainer(model=model,args=training_args,train_dataset=tokenized_train,eval_dataset=tokenized_test,tokenizer=tokenizer,data_collator=data_collator
)
有幾個參數值得注意。我們訓練20個輪次,并保持任務周期簡短。你可以嘗試調整學習率和權重衰減,以確定它們是否有助于模型的微調。在開始訓練循環之前,我們首先要保存預訓練的分詞器。由于分詞器在訓練過程中不會更新,因此無需在訓練后保存。不過,在繼續預訓練后,我們將保存模型:
# Save pre-trained tokenizer
tokenizer.save_pretrained("mlm")
#?Train model
trainer.train()
#?Save updated model
model.save_pretrained("mlm")
這為我們提供了mlm文件夾中的一個更新后的模型。為了評估其性能,我們通常需要在多種任務上對模型進行微調。不過出于我們的目的,可以通過運行一些掩碼任務來測試模型是否通過持續訓練學習了新知識。具體方法是在繼續預訓練之前加載我們的預訓練模型。使用句子"What a horrible [MASK]!",模型將預測[MASK]位置應該出現的單詞:
from transformers import pipeline#?Load?and?create predictions
mask_filler = pipeline("fill-mask", model="bert-base-cased")
preds = mask_filler("What a?horrible?[MASK]!")
#?Print results
for pred?in?preds:print(f">>> {pred["sequence"]}")
>>> What a?horrible?idea!
>>> What a?horrible?dream!
>>> What a?horrible?thing!
>>> What a?horrible?day!
>>> What a?horrible?thought!
輸出結果展示了諸如“想法”、“夢想”和“天”這樣的概念,這些顯然是有意義的。接下來,讓我們看看更新后的模型會預測出什么:
#?Load?and?create predictionsmask_filler = pipeline("fill-mask", model="mlm")
preds = mask_filler("What a?horrible?[MASK]!")
#?Print results
for pred?in?preds:print(f">>> {pred["sequence"]}")
>>> What a?horrible?movie!
>>> What a?horrible?film!
>>> What a?horrible?mess!
>>> What a?horrible?comedy!
>>> What a?horrible?story!
一部糟糕透頂的電影、影片、混亂局面等,都清楚地表明相較于預訓練模型,當前模型對我們輸入的數據存在更嚴重的偏向性。
下一步應當是在本章開頭進行的分類任務上對這個模型進行微調。只需按如下方式加載模型即可開始使用:
from transformers import AutoModelForSequenceClassification# Fine-tune for classification
model = AutoModelForSequenceClassification.from_pretrained("mlm", num_labels=2)
tokenizer = AutoTokenizer.from_pretrained("mlm")
命名實體識別
在本節中,我們將深入探討針對命名實體識別(NER)任務微調預訓練BERT模型的具體過程。與文檔級分類不同,該方法能夠對單個詞元(token)或詞語進行細粒度分類,涵蓋人物、地點等實體類別。這種特性在涉及敏感數據的去標識化與匿名化任務中尤為重要。
雖然NER與本章開頭討論的文檔分類任務具有相似性,但兩者在數據預處理和分類邏輯上存在顯著差異。由于本任務關注詞語級別的實體識別而非文檔整體分析,我們需要對原始數據進行特殊處理以適配這種細粒度的建模需求。圖11-18直觀展示了該模型在詞語級別進行實體識別的工作原理.
?
圖11-18. 對BERT模型進行微調以用于命名實體識別(NER),可實現人物或地點等命名實體的檢測。
對預訓練BERT模型進行微調時采用的架構與我們在文檔分類中所觀察到的架構相似。然而,其分類方法存在根本性轉變——該模型不再依賴詞元嵌入的聚合或池化操作,轉而直接對序列中的每個詞元進行獨立預測。需要特別說明的是,我們的詞級分類任務并非對完整詞語進行分類,而是針對共同構成這些詞語的基礎詞元進行細粒度分類。圖11-19直觀展示了這種詞元級別的分類機制。
?
圖11-19. 在BERT模型的微調過程中,系統會對單個詞元(token)進行分類,而非對單詞或整個文檔進行分類
為命名實體識別準備數據
在本示例中,我們將使用英文版的CoNLL-2003數據集。該數據集包含多種類型的命名實體(人物、組織、地點、雜項以及無實體標注),共有約14,000個訓練樣本[4]。
# The?CoNLL-2003?dataset for NER
dataset = load_dataset("conll2003", trust_remote_code=TΓue)
在為本文尋找可用數據集時,我們還發現了另外幾個值得分享的優質資源。WNUT_17數據集專注于識別新興實體和罕見實體,這類實體往往更難以被有效檢測。此外,tner/mit_movie_trivia和tner/mit_restaurant這兩個數據集極具趣味性:前者用于檢測演員、情節、原聲音樂等電影相關實體,后者則專注于識別設施、菜肴、菜系等餐飲類實體5。
讓我們通過一個例子來檢查數據的結構:
example = dataset["train"][848]
{'id':?'848',
'tokens':?['Dean',
'Palmer',
'hit',
'his',
'30th',
'homer',
'for',
'the',
'Rangers',
'?.'],
'pos_tags':?[22, 22, 38, 29,?16,?21,?15,?12,?23,?7],
'chunk_tags':?[11, 12, 21,?11,?12,?12,?13,?11,?12, 0],
'ner_tags':?[1, 2, 0,?0,?0,?0,?0,?0,?3,?0]}
該數據集為句子中的每個單詞提供了標簽。這些標簽可在ner_tags鍵下找到,其對應以下可能的實體類型:
4 ?Erik F. Sang and?Fien?De Meulder.?“Introduction?to?the?CoNLL-2003?shared?task:?Language-independent?named entity recognition.” arXiv?preprint cs/0306050?(2003).
5 ?Jingjing Liu et al.?“Asgard:?A?portable?architecture?for?multilingual?dialogue?systems.”?2013 IEEE?International?Conference?on Acoustics, Speech and Signal Processing. IEEE,?2013.
label2id =?{"O": 0,?"B-PER": 1,?"I-PER": 2,?"B-ORG":?3,?"I-ORG":?4,"B-LOC": 5,?"I-LOC": 6,?"B-MISC": 7,?"I-MISC":?8
}
id2label = {index: label for label,?index?in?label2id.items()}
{'O':?0,
'B-PER':?1,
'I-PER':?2,
'B-ORG':?3,
'I-ORG': 4,
'B-LOC':?5,
'I-LOC':?6,
'B-MISC':?7,
'I-MISC': 8}
這些實體對應于特定類別:人物(PER)、組織(ORG)、地點(LOC)、雜項實體(MISC)和非實體(O)。需注意,這些實體詞元前會添加前綴符號:B(表示短語起始)或 I(表示短語內部)。若兩個連續詞元屬于同一短語,則該短語起始詞元使用 B,后續詞元使用 I 表明它們同屬一個整體而非獨立實體。
圖11-20進一步闡釋了此規則。如圖所示,由于"Dean"是短語起始詞元,"Palmer"是結束詞元,因此"Dean Palmer"整體被識別為人物實體,而單獨的"Dean"和"Palmer"并不構成獨立的人物實體。
?
圖11-20 通過用同一實體標示短語的起始和結束位置,我們可以識別整個短語中的實體
我們的數據已經過預處理并拆分為單詞,但尚未轉換為詞元。為此,我們將使用本章貫穿始終的預訓練模型(即bert-base-cased)的分詞器對其進行進一步分詞處理:
from transformers import AutoModelForTokenClassification#?Load tokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")#?Load?model
model = AutoModelForTokenClassification.from_pretrained("bert-base-cased",num_labels=len(id2label),id2label=id2label,label2id=label2id
)
讓我們來看一下分詞器將如何處理我們的示例:
#?Split?individual?tokens?into sub-tokens
token_ids = tokenizer(example["tokens"], is_split_into_words=True)["input_ids"]
sub_tokens = tokenizer.convert_ids_to_tokens(token_ids)
['[CLS]',
'Dean',
'Palmer',?'hit',
'his',
'30th',
'home',
'##r',
'for',
'the',
'Rangers',?'?'
.?,
'[SEP]']
正如我們在第2章和第3章所學,分詞器會添加[CLS]和[SEP]詞元。需要注意的是,單詞"homer"被進一步拆分為"home"和"##r"兩個子詞。由于我們擁有的是詞級別的標注數據而非子詞級別的標注數據,這給我們帶來了一些挑戰。這個問題可以通過在分詞過程中將標簽與其對應的子詞進行對齊來解決。
以標注為B-PER(表示人物)的單詞"Maarten"為例,當通過分詞器處理時,該詞會被拆分為"Ma"、"##arte"和"##n"三個子詞。我們不能簡單地將B-PER實體應用于所有子詞,因為這會錯誤地表示這三個子詞代表三個獨立的人物。當實體被拆分為多個子詞時,第一個子詞應使用B(表示起始)標簽,后續子詞應使用I(表示內部)標簽。
因此,"Ma"將被詞元為B-PER表示短語的開始,而"##arte"和"##n"則使用I-PER標簽表明它們屬于同一個短語。這種對齊過程如圖11-21所示
?
圖11-21. 分詞輸入的標簽對齊過程
我們創建了一個名為align_labels的函數,該函數將對輸入進行分詞,并在分詞過程中將這些詞元與其更新后的標簽進行對齊:
def align_labels(examples):token_ids = tokenizer(examples["tokens"],truncation=True,is_split_into_words=True)labels = examples["ner_tags"]updated_labels =?[]for index, label in enumerate(labels):#?Map?tokens?to?their respective wordword_ids = token_ids.word_ids(batch_index=index)previous_word_idx =?Nonelabel_ids =?[]for word_idx in word_ids:# The?start?of a?new wordif word_idx?!= previous_word_idx:previous_word_idx = word_idxupdated_label =?-100 if word_idx is None else label[word_idx]label_ids.append(updated_label)# Special?token?is?-100elif word_idx is Nonelabel_ids.append(-100)# If the?label?is B-XXX we?change?it?to I-XXXelse:updated_label = label[word_idx]if updated_label %?2?==?1:updated_label +=?1label_ids.append(updated_label)updated_labels.append(label_ids)token_ids["labels"] = updated_labelsreturn token_idstokenized = dataset.map(align_labels, batched=True)
在我們的示例中,請注意,額外的標簽(-100)已被添加到[CLS]和[SEP]詞元中:
#?Difference between original and updated?labels
print(f"Original: {example["ner_tags"]}")
print(f"Updated: {tokenized["train"][848]["labels"]}")
Original:?[1, 2, 0,?0,?0,?0,?0,?0,?3,?0]
Updated:?[-100, 1, 2, 0,?0,?0,?0,?0,?0,?0,?3,?0,?-100]
現在我們已經完成了分詞和標簽對齊工作,就可以開始著手定義評估指標了。這與我們之前處理單個文檔單一預測的評估方式不同,現在每個文檔會生成多個針對每個詞元的預測結果。我們將使用Hugging Face的evaluate工具包來構建compute_metrics函數,該函數能夠實現基于詞元級別的性能評估:
import evaluate#?Load sequential evaluation
seqeval = evaluate.load("seqeval")def compute_metrics(eval_pred):# Create predictionslogits, labels =?eval_predpredictions = np.argmax(logits, axis=2)true_predictions =?[]true_labels =?[]#?Document-level?iterationfor prediction, label in zip(predictions,?labels):# Token-level?iterationfor token_prediction, token_label in zip(prediction, label):# We?ignore special?tokensif token_label?!=?-100:true_predictions.append([id2label[token_prediction]])true_labels.append([id2label[token_label]])results = seqeval.compute(predictions=true_predictions, references=true_labels)return {"f1":?results["overall_f1"]}
命名實體識別的微調
我們即將完成配置。現在需要替換原來的DataCollatorWithPadding,改用適用于token級別分類的數據整理器,即DataCollatorForTokenClassification:
from transformers import DataCollatorForTokenClassification# Token-classification DataCollator
data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
至此我們已經完成模型加載,后續步驟與本章前面介紹的訓練流程類似。我們將定義一個具有可調參數的訓練器,并創建Trainer實例:
我們隨后對所創建的模型進行評估:
# Evaluate?the model on?our?test?data
trainer.evaluate()
最后,讓我們保存模型并將其集成到推理流水線中。這樣我們可以審查特定數據,手動檢查推理過程的具體行為,并驗證輸出結果是否符合預期:
# Training arguments?for parameter?tuning
training_args = TrainingArguments("model",learning_rate=2e-5,per_device_train_batch_size=16,per_device_eval_batch_size=16,num_train_epochs=1,weight_decay=0.01,save_strategy="epoch",report_to="none"
)# Initialize?Trainer
trainer = Trainer(model=model,args=training_args,train_dataset=tokenized["train"],eval_dataset=tokenized["test"],tokenizer=tokenizer,data_collator=data_collator,compute_metrics=compute_metrics,
)trainer.train()
我們隨后對所創建的模型進行評估:
# Evaluate?the model on?our?test?datatrainer.evaluate()
最后,讓我們保存模型并將其集成到推理流水線中。這樣我們可以審查特定數據,手動檢查推理過程的具體行為,并驗證輸出結果是否符合預期:
from transformers import pipeline
#?Save our fine-tuned model
trainer.save_model("ner_model")
#?Run?inference on?the fine-tuned model
token_classifier = pipeline("token-classification",model="ner_model",
)token_classifier("My name is Maarten.")
[{'entity':?'B-PER',
'score': 0.99534035,
'index': 4,
'word':?'Ma',
'start':?11,
'end':?13},
{'entity':?'I-PER',
'score': 0.9928328,
'index':?5,
'word':?'##arte',
'start':?13,
'end':?17},
{'entity':?'I-PER',
'score': 0.9954301,
'index':?6,
'word':?'##n',
'start':?17,
'end':?18}]
在句子"My name is Maarten"中,單詞"Maarten"及其子詞被正確識別為人名!
本章小結
在本章中,我們探討了針對特定分類任務微調預訓練表示模型的多項任務。我們首先演示了如何微調預訓練的BERT模型,并通過凍結其架構中的某些層擴展了示例。
我們嘗試了一種名為SetFit的少樣本分類技術,該技術通過使用少量標注數據對預訓練嵌入模型及其分類頭進行聯合微調。僅使用少量標注數據點,該模型的性能與我們在前幾章探討的模型相當。
隨后,我們深入研究了繼續預訓練(continued pretraining)的概念,即以預訓練的BERT模型為起點,使用不同數據進行持續訓練。其底層機制——掩碼語言建模——不僅用于創建表示模型,還可用于模型的繼續預訓練。
最后,我們研究了命名實體識別任務,該任務涉及從非結構化文本中識別特定實體(如人名和地名)。與先前示例相比,此類分類是在詞級別(而非文檔級別)上完成的。
在下一章中,我們將繼續探索語言模型微調領域,但將聚焦于生成式模型。通過兩步流程,我們將研究如何微調生成式模型以正確遵循指令,進而優化其符合人類偏好的表現。