預訓練語言模型的出現,為自然語言處理領域帶來了顯著的進展。然而,在實際應用中,我們常常面臨標註資料不足的挑戰。本文將探討如何在少數標註資料的情況下,利用微調、上下文學習、少樣本學習以及領域適應等技術,提升模型的效能。首先,我們將介紹如何微調預訓練模型,並使用 F1 分數作為評估指標。接著,我們將探討上下文學習和少樣本學習的應用,並以翻譯任務為例進行說明。最後,我們將介紹如何利用未標註資料進行領域適應,並討論無監督資料增強(UDA)和不確定性感知自訓練(UST)等進階技術。
提升模型效能:微調與上下文學習
在處理少數標註資料的情況下,如何有效地訓練模型是一個重要的課題。本章節將探討兩種主要方法:微調預訓練語言模型和使用上下文學習(In-Context Learning)或少樣本學習(Few-Shot Learning)。
微調預訓練模型
首先,我們來看看簡單地微調一個預訓練的BERT模型在我們的資料集上的表現。為此,我們需要計算評估指標,特別是F1分數,以選擇最佳模型。
計算評估指標
from scipy.special import expit as sigmoid
def compute_metrics(pred):
y_true = pred.label_ids
y_pred = sigmoid(pred.predictions)
y_pred = (y_pred > 0.5).astype(float)
clf_dict = classification_report(y_true, y_pred, target_names=all_labels, zero_division=0, output_dict=True)
return {"micro f1": clf_dict["micro avg"]["f1-score"], "macro f1": clf_dict["macro avg"]["f1-score"]}
微調模型
config = AutoConfig.from_pretrained(model_ckpt)
config.num_labels = len(all_labels)
config.problem_type = "multi_label_classification"
for train_slice in train_slices:
model = AutoModelForSequenceClassification.from_pretrained(model_ckpt, config=config)
trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args_fine_tune, compute_metrics=compute_metrics, train_dataset=ds_enc["train"].select(train_slice), eval_dataset=ds_enc["valid"])
trainer.train()
pred = trainer.predict(ds_enc["test"])
metrics = compute_metrics(pred)
macro_scores["Fine-tune (vanilla)"].append(metrics["macro f1"])
micro_scores["Fine-tune (vanilla)"].append(metrics["micro f1"])
plot_metrics(micro_scores, macro_scores, train_samples, "Fine-tune (vanilla)")
內容解密:
- 評估指標計算:我們定義了一個
compute_metrics函式,用於計算模型的F1分數。首先,使用sigmoid函式將模型的預測logits轉換為機率,然後透過設定閾值(0.5)將機率轉換為二元預測結果。接著,使用classification_report函式計算真實標籤和預測標籤之間的F1分數。 - 模型微調:我們遍歷不同的訓練資料切片,對每個切片訓練一個新的分類別模型。使用
Trainer類別進行模型的訓練和評估,並在測試集上進行預測。 - 結果記錄:將每個模型的宏觀F1分數和微觀F1分數記錄下來,並繪製相應的效能曲線圖。
上下文學習與少樣本學習
除了微調預訓練模型外,上下文學習或少樣本學習是另一種有效的方法。這種方法利用語言模型的內在能力,透過在輸入提示中提供少量的示例,來引導模型生成正確的輸出。
示例:翻譯任務
prompt = """Translate English to French: thanks =>"""
這種方法不需要額外的訓練資料,但可以透過提供更多的示例來提高模型的表現。
利用未標註資料
雖然擁有大量高品質的標註資料是訓練分類別器的最佳情況,但未標註資料並非毫無價值。領域適應(Domain Adaptation)是一種中間地帶的方法,即繼續在目標領域的未標註資料上訓練語言模型,以提高其在下游任務上的表現。
領域適應的步驟
- 繼續訓練語言模型:使用未標註的目標領域資料繼續訓練預訓練的語言模型,採用遮蔽語言建模(Masked Language Modeling)的目標。
- 微調分類別器:將適應後的語言模型用於分類別任務,並進行微調。
這種方法的優點在於,未標註資料通常非常豐富,並且適應後的模型可以重用於多個下游任務。
遮蔽語言模型化的技術探討與實作
在進行自然語言處理任務時,遮蔽語言模型(Masked Language Modeling)是一種常見的預訓練方法,能夠有效地提升模型的語言理解能力。這種方法透過隨機遮蔽輸入序列中的部分詞彙,並讓模型預測這些被遮蔽的詞彙,從而增強模型的語言預測能力。
遮蔽語言模型化的實作步驟
首先,我們需要對文字資料進行特殊的分詞處理。在分詞過程中,除了普通的詞彙外,分詞器還會新增特殊的標記,如 [CLS] 和 [SEP],用於分類別和下一句預測任務。在進行遮蔽語言模型訓練時,我們需要確保模型不會學習預測這些特殊的標記。因此,我們在分詞時設定 return_special_tokens_mask=True,以取得一個遮罩來忽略這些特殊標記。
def tokenize(batch):
return tokenizer(batch["text"], truncation=True, max_length=128, return_special_tokens_mask=True)
ds_mlm = ds.map(tokenize, batched=True)
ds_mlm = ds_mlm.remove_columns(["labels", "text", "label_ids"])
內容解密:
tokenize函式:定義了一個分詞函式,用於對文字資料進行分詞處理。該函式使用tokenizer對文字進行截斷處理,最大長度設為 128,並傳回特殊標記的遮罩。return_special_tokens_mask=True:確保分詞結果中包含特殊標記的遮罩,以便在後續處理中忽略這些標記。ds.map(tokenize, batched=True):將tokenize函式應用於資料集ds,並進行批次處理。remove_columns:移除資料集中不需要的欄位,如"labels"、"text"和"label_ids"。
使用資料整理器進行遮蔽語言模型化
為了實作遮蔽語言模型化,我們需要使用資料整理器(Data Collator)來動態地對輸入序列進行遮蔽,並生成對應的標籤。這樣做的好處是,我們不需要在資料集中儲存標籤,並且每次取樣時都能獲得新的遮罩。Transformers 函式庫提供了 DataCollatorForLanguageModeling 類別來實作這一功能。
from transformers import DataCollatorForLanguageModeling, set_seed
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15)
內容解密:
DataCollatorForLanguageModeling:建立了一個資料整理器,用於進行遮蔽語言模型化的處理。mlm_probability=0.15:設定了遮蔽詞彙的機率為 15%,這與 BERT 論文中的設定一致。tokenizer=tokenizer:將分詞器傳遞給資料整理器,用於處理輸入序列。
測試資料整理器的效果
為了驗證資料整理器的效果,我們可以對一個簡單的輸入序列進行測試。
set_seed(3)
data_collator.return_tensors = "np"
inputs = tokenizer("Transformers are awesome!", return_tensors="np")
outputs = data_collator([{"input_ids": inputs["input_ids"][0]}])
pd.DataFrame({
"Original tokens": tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]),
"Masked tokens": tokenizer.convert_ids_to_tokens(outputs["input_ids"][0]),
"Original input_ids": inputs["input_ids"][0],
"Masked input_ids": outputs["input_ids"][0],
"Labels": outputs["labels"][0]
}).T
內容解密:
set_seed(3):設定了隨機種子,以確保結果的可重現性。data_collator.return_tensors = "np":將資料整理器的輸出格式設定為 NumPy 陣列。inputs = tokenizer("Transformers are awesome!", return_tensors="np"):對輸入文字進行分詞處理,並將結果轉換為 NumPy 陣列。outputs = data_collator([{"input_ids": inputs["input_ids"][0]}]):使用資料整理器對輸入序列進行遮蔽語言模型化處理。
微調遮蔽語言模型
在準備好分詞器和資料整理器後,我們可以開始微調遮蔽語言模型。
from transformers import AutoModelForMaskedLM
training_args = TrainingArguments(
output_dir=f"{model_ckpt}-issues-128",
per_device_train_batch_size=32,
logging_strategy="epoch",
evaluation_strategy="epoch",
save_strategy="no",
num_train_epochs=16,
push_to_hub=True,
log_level="error",
report_to="none"
)
trainer = Trainer(
model=AutoModelForMaskedLM.from_pretrained("bert-base-uncased"),
tokenizer=tokenizer,
args=training_args,
data_collator=data_collator,
train_dataset=ds_mlm["unsup"],
eval_dataset=ds_mlm["train"]
)
trainer.train()
trainer.push_to_hub("Training complete!")
內容解密:
AutoModelForMaskedLM.from_pretrained("bert-base-uncased"):載入了預訓練的 BERT 模型,用於遮蔽語言模型化任務。Trainer:建立了一個訓練器,用於微調遮蔽語言模型。data_collator=data_collator:將資料整理器傳遞給訓練器,用於處理輸入序列。
微調分類別模型
在微調完遮蔽語言模型後,我們可以使用微調後的模型來進行分類別任務。
model_ckpt = f'{model_ckpt}-issues-128'
config = AutoConfig.from_pretrained(model_ckpt)
config.num_labels = len(all_labels)
config.problem_type = "multi_label_classification"
for train_slice in train_slices:
model = AutoModelForSequenceClassification.from_pretrained(model_ckpt, config=config)
trainer = Trainer(
model=model,
tokenizer=tokenizer,
args=training_args_fine_tune,
compute_metrics=compute_metrics,
train_dataset=ds_enc["train"].select(train_slice),
eval_dataset=ds_enc["valid"],
)
trainer.train()
pred = trainer.predict(ds_enc['test'])
metrics = compute_metrics(pred)
macro_scores['Fine-tune (DA)'].append(metrics['macro f1'])
micro_scores['Fine-tune (DA)'].append(metrics['micro f1'])
內容解密:
AutoConfig.from_pretrained(model_ckpt):載入了微調後的模型的組態。AutoModelForSequenceClassification.from_pretrained(model_ckpt, config=config):載入了微調後的模型,用於序列分類別任務。Trainer:建立了一個訓練器,用於微調分類別模型。
結果分析
透過比較使用微調後的模型和原始 BERT 模型的結果,我們可以看到,在低資料領域,使用微調後的模型具有明顯的優勢。此外,在具有更多標註資料的情況下,使用微調後的模型也能獲得更好的效能。
plot_metrics(micro_scores, macro_scores, train_samples, "Fine-tune (DA)")
內容解密:
plot_metrics:繪製了微觀和宏觀 F1 分數隨訓練樣本數量的變化曲線。- 結果分析:透過比較曲線,可以看到使用微調後的模型的優勢。
利用未標記資料提升模型效能
在處理機器學習問題時,資料標記是一項耗時耗力的工作。幸運的是,我們可以利用未標記的資料來提升模型的表現。本章將探討幾種利用未標記資料的方法,並介紹一些進階技術。
簡單卻有效的領域適應
領域適應是一種簡單有效的方法,能夠在不需要額外標記資料的情況下提升模型的表現。其核心思想是利用預訓練模型並在特定領域的未標記資料上進行微調。實驗結果表明,即使只有少量的未標記資料,領域適應也能帶來明顯的效能提升。
領域適應的優點
- 不需要額外的標記資料
- 能夠提升模型的表現
- 簡單易行
進階方法
除了領域適應之外,還有一些進階方法能夠更好地利用未標記資料。
無監督資料增強(UDA)
無監督資料增強的核心思想是:模型的預測結果應該對於原資料和經過擾動後的資料保持一致。擾動可以透過標準的資料增強策略(如 token 替換和反向翻譯)來實作。透過最小化原資料和擾動後資料的預測結果之間的 KL 散度,可以實作模型的一致性約束。
此圖示說明瞭 UDA 的訓練過程:
不確定性感知自訓練(UST)
UST 的核心思想是:訓練一個教師模型在標記資料上,然後利用該模型為未標記資料生成偽標籤。接著,訓練一個學生模型在偽標籤資料上,並在訓練完成後將其變成下一輪的教師模型。
UST 的訓練流程如下:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 少數標註資料提升模型效能技術
package "少數標註資料學習技術" {
package "模型微調" {
component [預訓練模型] as pretrain
component [BERT 微調] as finetune
component [F1 評估] as f1
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml
UST 的一個有趣之處在於如何生成偽標籤:透過多次將相同的輸入傳遞給模型並開啟 dropout,可以獲得模型預測的不確定性度量。然後,利用 Bayesian Active Learning by Disagreement(BALD)方法來取樣偽標籤。
內容解密:
- 領域適應是一種簡單有效的方法,能夠利用未標記資料提升模型的表現。
- 無監督資料增強透過最小化原資料和擾動後資料的預測結果之間的 KL 散度,實作模型的一致性約束。
- 不確定性感知自訓練透過迭代地訓練教師模型和學生模型,能夠提升模型的表現。
程式碼範例:
import torch
import torch.nn as nn
import torch.optim as optim
# 定義模型
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
# 定義 UDA 損失函式
def uda_loss(model, labeled_data, unlabeled_data):
# 計算交叉熵損失
ce_loss = nn.CrossEntropyLoss()(model(labeled_data), labeled_data.labels)
# 計算 KL 散度
kl_loss = nn.KLDivLoss()(model(unlabeled_data), model(unlabeled_data + noise))
# 傳回總損失
return ce_loss + kl_loss
# 訓練模型
model = MyModel()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(10):
optimizer.zero_grad()
loss = uda_loss(model, labeled_data, unlabeled_data)
loss.backward()
optimizer.step()
內容解密:
- 程式碼定義了一個簡單的神經網路模型
MyModel。 uda_loss函式計算了 UDA 的損失函式,包括交叉熵損失和 KL 散度。- 在訓練過程中,使用 Adam 最佳化器來最小化 UDA 損失函式。
本章介紹了利用未標記資料提升模型效能的方法,包括領域適應、無監督資料增強和不確定性感知自訓練。這些方法能夠在不同的場景下提升模型的表現,並為讀者提供了實用的工具和技術。未來,我們將繼續探討如何利用 transformer 模型來處理大規模的資料和計算資源。