蒸餾和權重量化在生產推論加速方面非常有效,但在某些情況下,你可能還需要嚴格控制模型的記憶體佔用。例如,如果產品經理突然決定我們的文字助手需要佈署在移動裝置上,那麼我們需要意圖分類別器佔用盡可能少的儲存空間。為了完善我們的壓縮方法概述,讓我們看如何透過識別和移除網路中最不重要的權重來縮小模型中的引數量。

深度神經網路中的稀疏性

剪枝的主要思想是在訓練期間逐漸移除權重連線(甚至可能是神經元),使模型變得越來越稀疏。剪枝後的模型非零引數量更少,可以儲存在緊湊的稀疏矩陣格式中。剪枝還可以與量化結合,以獲得更進一步的壓縮效果。

權重剪枝方法

從數學角度看,大多數權重剪枝方法的工作原理是計算重要性分數矩陣A,然後按重要性選擇前k%的權重:

Top_k A_ij = {
    1 如果 S_ij 在前 k%
    0 否則
}

實際上,k成為控制模型稀疏度(即值為零的權重比例)的新超引數。k值越低,矩陣越稀疏。從這些分數中,我們可以定義一個掩碼矩陣A,在前向傳播過程中掩蓋權重W_ij,從而有效地建立一個稀疏的啟用網路:

a_i = ∑_k W_ik * M_ik * x_k

這種方法的核心在於,透過識別並移除網路中貢獻較小的權重,可以顯著減少模型大小,同時保持大部分效能。剪枝是一種強大的技術,特別適用於在資源受限環境中佈署大型模型。

結合知識蒸餾、模型量化和權重剪枝,我們可以建立既快速又小巧的模型,適合在各種環境中佈署,從雲伺服器到移動裝置。這些技術不僅可以獨立使用,還可以組合使用以獲得更好的結果,為Transformer模型在生產環境中的高效佈署提供了強大的工具箱。

效能最佳化技術的綜合比較

透過我們的實驗,可以清楚地看到不同最佳化技術的效果。知識蒸餾幫助我們建立了一個更小、更快的模型,而不會顯著影響準確性。量化進一步減少了模型大小和推理時間,特別是當與ONNX Runtime結合時。最後,權重剪枝提供了另一種減少模型大小的方法,特別適合儲存空間嚴格受限的情況。

在實際應用中,選擇哪種最佳化技術取決於你的具體需求和約束:

  • 如果延遲是主要關注點,ONNX+量化可能是最佳選擇
  • 如果模型大小是主要限制,剪枝+量化可能更合適
  • 如果你需要在不同平台間的可移植性,ONNX提供了最佳的框架相容性

無論選擇哪種方法,這些最佳化技術都能讓Transformer模型在生產環境中執行得更快、更高效,使AI助手等應用能夠在各種硬體上流暢執行,同時保持高品質的預測結果。

權重剪枝:讓模型瘦身的藝術

權重剪枝是模型壓縮的關鍵技術之一,核心思想是移除神經網路中不重要的連線,同時保持模型的效能。在實作剪枝方法時,我們需要考慮三個關鍵問題:

  1. 應該移除哪些權重?
  2. 如何調整剩餘權重以獲得最佳效能?
  3. 如何以計算高效的方式進行網路剪枝?

這些問題直接影響剪枝過程中評分矩陣 A 的計算方式。讓我們從最早也是最流行的剪枝方法之一開始:幅度剪枝(Magnitude Pruning)。

幅度剪枝:根據權重大小的選擇

幅度剪枝的核心概念非常直觀:根據權重的絕對值大小計算重要性分數,具體來説,評分矩陣 A = |W_ij| (1 ≤ i, j ≤ n),然後透過 A = Top_k(A) 生成二進位遮罩。在實際應用中,幅度剪枝通常採用迭代方式進行:

  1. 訓練模型以學習哪些連線較重要
  2. 剪除重要性最低的權重
  3. 重新訓練稀疏模型
  4. 重複此過程直到達到所需的稀疏度

這種方法的一個主要缺點是計算成本高昂:每次剪枝步驟都需要將模型訓練至收斂。為瞭解決這個問題,通常採用漸進式增加稀疏度的策略,從初始稀疏度 s_i(通常為零)逐步增加到最終值 s_f,經過 N 個步驟:

s_t = s_f + (s_i - s_f) * (1 - (t - t_0)/(N*Δt))^3

其中 t ∈ {t_0, t_0 + Δt, ..., t_0 + N*Δt}

這個公式描述了稀疏度排程器的工作方式。每隔 Δt 步更新一次二進位遮罩 A,這允許被遮罩的權重在訓練過程中重新啟用,從而還原因剪枝導致的任何潛在精確度損失。立方因子意味著權重剪枝率在早期階段最高(當冗餘權重數量較大時),然後逐漸減小。

幅度剪枝的一個問題是它主要針對純監督學習設計,其中每個權重的重要性直接與任務相關。然而,在遷移學習中,權重的重要性主要由預訓練階段決定,因此幅度剪枝可能會移除對微調任務重要的連線。針對這個問題,Hugging Face 研究人員提出了一種稱為移動剪枝(Movement Pruning)的自適應方法。

移動剪枝:適應性更強的剪枝方法

移動剪枝的基本理念是在微調過程中逐漸移除權重,使模型變得越來越稀疏。與幅度剪枝不同,移動剪枝的關鍵創新點在於權重和分數都是在微調過程中學習得到的。

分數不再直接從權重派生(如幅度剪枝),而是像其他神經網路引數一樣透過梯度下降學習。這意味著在反向傳播中,我們還會追蹤損失 L 相對於分數 $S_ij$ 的梯度。一旦學習了分數,就可以使用 $A = Top_k(A)$ 生成二進位遮罩。

移動剪枝的直觀理解是:從零移動最多的權重是最重要的保留物件。換句話説,正權重在微調過程中增加(負權重則相反),這等同於説分數隨著權重遠離零而增加。這種行為與幅度剪枝不同,幅度剪枝選擇離零最遠的權重作為最重要的權重。

上圖展示了兩種剪枝方法的根本差異。左側是幅度剪枝,它保留絕對值最大的權重(圖中藍色區域),無論它們是增加還是減少;右側是移動剪枝,它保留那些在訓練過程中"移動"最多的權重(即正權重變得更正,負權重變得更負)。這種差異反映了兩種方法對"重要性"的不同理解。

這兩種剪枝方法之間的差異也體現在剩餘權重的分佈上。幅度剪枝產生兩個權重叢集,而移動剪枝產生更平滑的分佈。

這個圖表顯示了兩種剪枝方法保留的權重分佈差異。幅度剪枝(MaP)產生明顯的雙峰分佈,因為它保留了最大的正權重和最小的負權重;而移動剪枝(MvP)則產生更平滑的分佈,因為它關注的是權重的變化方向而非絕對大小。這種差異對模型的泛化能力和穩定性有重要影響。

值得注意的是,目前 Transformers 函式庫並不直接支援剪枝方法。不過,有一個名為 Neural Networks Block Movement Pruning 的函式庫實作了許多這些思想,如果記憶體約束是一個問題,建議檢視該函式庫。

模型壓縮的多維度策略

最佳化 Transformer 模型以在生產環境中佈署涉及兩個維度的壓縮:延遲和記憶體佔用。從微調模型開始,我們可以應用蒸餾、量化和透過 ORT(ONNX Runtime)進行最佳化,顯著減少這兩個方面的開銷。特別是,在 ORT 中進行量化和轉換以最小的努力提供了最大的收益。

雖然剪枝是減少 Transformer 模型儲存大小的有效策略,但目前的硬體並未針對稀疏矩陣運算進行最佳化,這限制了此技術的實用性。然而,這是一個活躍的研究領域,相信這些限制在不久的將來可能會得到解決。

實際應用

上述所有技術都可以適用於其他任務,如問答系統、命名實體識別或語言建模。如果你發現自己難以滿足延遲要求,或者模型消耗了所有計算預算,建議嘗試這些最佳化方法。

在實際專案中,我發現結合多種最佳化技術通常能達到最佳效果。例如,先使用知識蒸餾減小模型大小,然後應用量化降低推理時的計算需求,最後透過專門的執行時最佳化框架進一步提升效能。這種多維度最佳化策略能在保持模型準確性的同時,大幅提升佈署效率。

面對標註資料稀缺的挑戰

在資料科學專案中,最常見的問題之一就是:有標註資料嗎?通常回答是"沒有"或"只有一點",但客戶仍然期望你的機器學習模型能夠表現良好。由於在非常小的資料集上訓練模型通常不會產生好結果,一個明顯的解決方案是標註更多資料。然而,這需要時間與成本高昂,特別是當每個標註都需要領域專業知識來驗證時。

幸運的是,有幾種方法非常適合處理少量或無標註資料的情況!你可能已經熟悉其中一些方法,如零樣本學習或少樣本學習,如 GPT-3 令人印象深刻的能力,只需幾十個例子就能執行各種各樣的任務。

一般來説,最佳方法取決於任務、可用資料量以及已標註資料的比例。下圖所示的決策樹可以幫助我們選擇最合適的方法。

這個決策樹提供了根據資料情況選擇適當技術的框架。它首先考慮是否有標註資料,然後根據資料量、領域特性和時間資源進行細分。例如,即使只有少量標註樣本也能使用少樣本學習;而完全沒有標註資料時,可以考慮零樣本學習方法。這種系統化的方法能幫助開發者在資源有限的情況下做出最佳技術選擇。

讓我們逐步走過這個決策樹:

  1. 你是否有標註資料? 即使只有少量標註樣本也能對最佳方法產生影響。如果完全沒有標註資料,可以從零樣本學習開始。

從無到有:零樣本與少樣本學習

在標註資料極其稀缺的情況下,零樣本和少樣本學習提供了強大的解決方案。這些方法依賴於預訓練模型中已經編碼的知識,透過巧妙的提示或少量範例來引導模型執行特定任務。

當面對完全沒有標註資料的情況時,我發現零樣本學習特別有用。透過設計恰當的任務描述和提示,模型能夠利用其預訓練知識進行推理。而當有少量標註資料時,少樣本學習則能顯著提升模型效能。

在實踐中,我經常結合不確定性感知的自訓練與資料增強技術,進一步提升模型在標註稀缺情況下的表現。這種方法特別適用於文字分類別等任務,透過生成高品質的合成樣本來擴充訓練資料。

模型壓縮與低資源學習的結合

在資源受限的生產環境中,我們往往同時面臨兩個挑戰:模型過大與標註資料不足。這時,結合模型壓縮技術與低資源學習方法可以產生協同效應。

例如,在一個實際專案中,我採用了以下策略:首先使用零樣本或少樣本學習建立基線模型,然後透過知識蒸餾將大型預訓練模型的知識轉移到更小的架構中,最後應用量化和剪枝進一步最佳化模型大小和推理速度。

這種組合方法不僅解決了標註資料稀缺的問題,還確保了最終模型能夠在資源受限的環境中高效執行。在許多案例中,這種方法能夠在保持接近原始效能的同時,將模型大小減少 75% 以上,推理速度提升 3-5 倍。

模型壓縮技術如權重剪枝、量化和執行時最佳化,以及處理標註資料稀缺的方法如零樣本和少樣本學習,共同構成了現代深度學習實踐的重要工具箱。透過深入理解這些技術的原理和適用場景,我們能夠在資源受限的情況下依然構建高效、高效能的深度學習系統。

隨著硬體技術的進步,特別是針對稀疏矩陣運算的最佳化,這些方法的效能還將進一步提升。同時,預訓練模型的不斷發展也使零樣本和少樣本學習變得更加強大。在這個快速發展的領域中,持續關注最新研究進展並靈活運用各種技術組合,將是應對實際挑戰的關鍵。

標籤不足時的機器學習決策略

在實際應用中,我們經常面臨標籤資料不足的挑戰。無論是開發自動化客服系統、內容分類別器還是問題標記工具,取得大量標記資料往往成本高昂與耗時。在這種情況下,我們需要根據現有資源做出明智的決策。

標籤數量決定模型選擇

根據可用標籤的數量,我們可以採取不同的策略:

  1. 有大量標籤資料:如果有充足的標記資料,可以直接使用標準的微調方法,這是最直接也最有效的方式。

  2. 有少量標籤資料:當只有少數標記樣本時,關鍵問題是「是否有未標記資料可用」:

    • 有未標記資料:可以先在領域資料上微調語言模型,或使用更複雜的方法如無監督資料增強(UDA)或不確定性感知自訓練(UST)

    • 無未標記資料:可以採用少量樣本學習(few-shot learning)或使用預訓練語言模型的嵌入向量進行最近鄰搜尋

在實務中,選擇合適的方法需要考慮多種因素,而不僅是標籤數量。我在處理不同專案時發現,領域特性、資料品質以及問題複雜度同樣會影響最終決策。

GitHub問題自動標記:實際案例分析

為了展示如何在標籤有限的情況下構建有效模型,我們將解決一個常見的支援團隊面臨的問題:根據問題描述自動為GitHub問題增加標籤。這些標籤可能定義問題型別、相關產品或負責處理的團隊,自動化這一過程可以顯著提高工作效率。

在這個案例中,我們將使用一個流行的開放原始碼專案——Transformers的GitHub問題作為資料集。

取得與準備資料

首先,我們需要取得GitHub問題資料。我們可以使用GitHub REST API查詢Issues端點來取得所有問題的JSON資料。每個問題包含多個欄位,包括狀態、建立者、標題、內容以及標籤等訊息。

import pandas as pd
dataset_url = "https://git.io/nlp-with-transformers"
df_issues = pd.read_json(dataset_url, lines=True)
print(f"DataFrame shape: {df_issues.shape}")

這段程式碼使用pandas載入了GitHub問題的JSON資料。read_json函式的lines=True引數表示每行是一個獨立的JSON物件。輸出顯示資料集包含9930個問題記錄,每個記錄有26個欄位。

接下來我們需要處理標籤資料。原始資料中,標籤是一個包含多個JSON物件的列表,每個物件包含標籤的中繼資料。我們只需要標籤名稱:

df_issues["labels"] = (df_issues["labels"]
.apply(lambda x: [meta["name"] for meta in x]))
df_issues[["labels"]].head()

這段程式碼將每個問題的標籤資料從複雜的JSON物件列表簡化為只包含標籤名稱的列表。apply函式對每行資料應用一個lambda函式,該函式從每個標籤中繼資料中提取名稱欄位。輸出顯示前5個問題的標籤,可以看到多個問題沒有標籤(空列表)。

分析標籤分佈

瞭解標籤分佈是重要的一步,這有助於我們評估資料集的平衡性並確定需要處理的挑戰:

df_issues["labels"].apply(lambda x : len(x)).value_counts().to_frame().T

這段程式碼計算每個問題擁有的標籤數量,並統計各標籤數量的問題數。結果顯示大多數問題有0或1個標籤,少數問題有2個或更多標籤。具體來説,6440個問題沒有標籤,3057個問題有1個標籤,305個問題有2個標籤,以此類別推。

接下來,我們看最常見的標籤型別:

df_counts = df_issues["labels"].explode().value_counts()
print(f"Number of labels: {len(df_counts)}")
# 顯示前8個最常見的標籤類別
df_counts.to_frame().head(8).T

這段程式碼首先使用explode()方法將標籤列表展開,使每個標籤成為單獨的一行,然後計算每個標籤的出現頻率。結果顯示資料集中共有65個不同的標籤,其中"wontfix"和"model card"是最常見的標籤。這種高度不平衡的分佈在實際問題中很常見,需要特別處理。

標籤篩選與標準化

為了簡化問題,我們可以選擇只關注一部分標籤,並對標籤名稱進行標準化處理:

label_map = {"Core: Tokenization": "tokenization",
"New model": "new model",
"Core: Modeling": "model training",
"Usage": "usage",
"Core: Pipeline": "pipeline",
"TensorFlow": "tensorflow or tf",
"PyTorch": "pytorch",
"Examples": "examples",
"Documentation": "documentation"}

def filter_labels(x):
    return [label_map[label] for label in x if label in label_map]

df_issues["labels"] = df_issues["labels"].apply(filter_labels)
all_labels = list(label_map.values())

這段程式碼建立了一個對映字典,將原始標籤名稱對映到更簡潔、標準化的形式。然後定義一個函式來過濾標籤,只保留對映中存在的標籤。最後,對每個問題的標籤列表應用這個過濾函式,並儲存所有標準化後的標籤列表。這樣做有幾個好處:

  1. 簡化了標籤名稱,使其更易於理解和處理
  2. 只關注對我們任務最相關的標籤
  3. 統一了標籤的表達方式,消除了不一致性

我們來看過濾後的標籤分佈:

df_counts = df_issues["labels"].explode().value_counts()
df_counts.to_frame().T

過濾後的標籤分佈顯示"tokenization"和"new model"成為最常見的標籤,分別有106和98個問題。整體來看,所有標籤的分佈相對平衡,這對我們的分類別任務是有利的。

區分有標籤和無標籤資料

在處理少量標籤問題時,未標記的資料也是寶貴資源。我們可以建立一個新列來區分有標籤和無標籤的問題:

df_issues["split"] = "unlabeled"
mask = df_issues["labels"].apply(lambda x: len(x)) > 0
df_issues.loc[mask, "split"] = "labeled"
df_issues["split"].value_counts().to_frame()

這段程式碼首先將所有問題標記為"unlabeled",然後建立一個遮罩(mask)來識別標籤列表非空的問題,並將這些問題標記為"labeled"。結果顯示9489個問題沒有標籤,只有441個問題有標籤。這種極度不平衡的情況正是我們需要專門技術來處理的典型場景。

合併標題和內容

為了提高分類別效果,我們將問題的標題和內容合併為單一文欄位:

df_issues["text"] = (df_issues
.apply(lambda x: x["title"] + "\n\n" + x["body"], axis=1))

這段程式碼為每個問題建立一個新的"text"列,將標題和內容用兩個換行符連線起來。這種方式既保留了原始格式,又將所有相關訊息合併為單一文字,便於模型處理。標題通常包含問題的核心訊息,而正文提供更多細節,兩者結合可以提供更全面的上下文。

移除重複資料預處理的最後一步是檢查並移除重複項:

len_before = len(df_issues)
df_issues = df_issues.drop_duplicates(subset="text")
print(f"Removed {(len_before-len(df_issues))/len_before:.2%} duplicates.")

這段程式碼使用drop_duplicates方法移除文字完全相同的記錄,並計算移除的重複項佔原始資料集的百分比。移除重複資料是重要的預處理步驟,可以避免相同資料在訓練和測試集中同時出現,導致模型評估結果過於樂觀。

標籤不足問題的解決方案

在確認我們只有441個帶標籤的問題後,我們面臨典型的標籤不足情境。根據我們之前討論的決策樹,我們有幾種可能的解決方案:

1. 利用未標記資料的策略

當有大量未標記資料(9489個問題)和少量標記資料(441個問題)時,我們可以:

  • 領域適應預訓練:先在所有GitHub問題上微調語言模型,再使用標記資料訓練分類別器
  • 半監督學習:使用如UDA(無監督資料增強)或UST(不確定性感知自訓練)等技術,讓模型從未標記資料中學習
  • 自訓練:先用標記資料訓練基礎模型,然後用該模型對未標記資料進行預測,將高置信度的預測增加到訓練集中

2. 少量樣本學習方法

如果我們只使用標記資料,可以採用:

  • Few-shot learning:使用預訓練語言模型的上下文學習能力,僅需少量範例
  • 最近鄰搜尋:使用預訓練模型的嵌入向量,透過相似度比對找到最相似的已標記問題

3. 資料增強技術

為擴充有限的標記資料,我們可以:

  • 同義詞替換:用同義詞隨機替換文字中的詞語
  • 回譯:將文字翻譯成另一種語言,再翻譯回來,產生表達相同意思的不同句子
  • EDA (簡易資料增強):結合隨機插入、刪除、替換和交換操作

實施策略:GitHub問題標記器

在這個具體案例中,我認為結合使用未標記資料和少量樣本學習是最佳策略。具體來説,我會建議以下步驟:

  1. 首先使用所有GitHub問題(包括未標記的)對預訓練語言模型進行領域適應微調
  2. 然後使用標記資料實施少量樣本學習,建立多標籤分類別器
  3. 最後透過自訓練技術,逐步擴充訓練資料

這種組合方法能夠充分利用未標記資料提供的領域知識,同時透過少量樣本學習解決標籤稀疏問題。

多標籤分類別的特殊考量

值得注意的是,GitHub問題標記是一個多標籤分類別問題,一個問題可能同時屬於多個類別(如同時是"tokenization"和"pytorch"相關問題)。這與多類別分類別(每個樣本只屬於一個類別)有所不同,需要特殊處理:

  1. 損失函式選擇:應使用二元交叉熵而非分類別交叉熵
  2. 評估指標:需要使用如F1分數、精確率、召回率等適合多標籤任務的指標
  3. 閾值調整:預測時需要為每個標籤設定合適的閾值

擴充套件應用與侷限性

本文討論的方法主要適用於文字分類別任務,對於更複雜的任務如命名實體識別、問答系統或摘要生成,可能需要額外的資料增強或專門技術。然而,標籤不足決策樹的核心思想是通用的:根據可用標籤數量和未標記資料的可用性選擇合適的策略。

在實際應用中,我發現技術選擇只是成功的一部分。資料品質、問題定義的清晰度以及對業務需求的理解同樣重要。最好的模型也無法彌補糟糕的問題定義或不一致的標記標準。

標籤不足情境下的機器學習是一個快速發展的領域,隨著預訓練語言模型的進步,我們處理這類別問題的能力也在不斷提升。透過結合領域知識、靈活選擇技術策略,以及持續迭代改進,我們能夠在有限標籤條件下構建高效的文字分類別系統。

深入理解多標籤分類別:以GitHub問題標籤系統為例

在處理真實世界的文字分類別問題時,我們經常會遇到多標籤分類別的情況,即一個文字可能同時屬於多個類別。這比傳統的單標籤分類別要複雜得多,尤其是當我們只有少量已標記資料時。本文將以構建GitHub問題自動標籤系統為例,展示如何處理這類別挑戰。

資料特徵探索與理解

在開始構建分類別器前,我們需要了解資料的特點。我們的資料集包含了GitHub上的問題報告,每個問題可以有多個標籤,如pytorchtokenization等。首先,讓我們看文字長度分佈情況:

import numpy as np
import matplotlib.pyplot as plt

(df_issues["text"].str.split().apply(len)
.hist(bins=np.linspace(0, 500, 50), grid=False, edgecolor="C0"))
plt.title("Words per issue")
plt.xlabel("Number of words")
plt.ylabel("Number of issues")
plt.show()

這段程式碼計算了每個GitHub問題描述的單詞數量,並繪製了一個直方圖來視覺化文字長度分佈。透過使用NumPy的linspace函式,我們建立了從0到500之間的50個等間距點作為直方圖的分箱邊界,這樣可以更精確地觀察資料分佈。grid=False移除了背景網格線,而edgecolor="C0"則為直方圖條形增加了藍色邊框,使其更加清晰。

分析結果顯示,我們的資料集呈現了典型的長尾分佈特徵——大多數問題描述較短,但也有一些問題包含超過500個單詞。這些較長的文字通常包含錯誤訊息和程式碼片段。考慮到大多數Transformer模型的上下文大小為512個標記或更多,截斷少數長文字不太可能對整體效能產生顯著影響。

構建訓練集與驗證集

多標籤分類別問題的一個特殊挑戰是無法保證所有標籤在訓練和測試集中的平衡分佈。為解決這個問題,我可以使用Scikit-multilearn函式庫,它專門為多標籤分類別任務設計。

首先,我們需要將標籤轉換為模型可以處理的格式。這裡使用Scikit-learn的MultiLabelBinarizer類別,它將標籤名稱列表轉換為二值向量,用0表示缺失標籤,用1表示存在的標籤:

from sklearn.preprocessing import MultiLabelBinarizer

mlb = MultiLabelBinarizer()
mlb.fit([all_labels])
mlb.transform([["tokenization", "new model"], ["pytorch"]])

這段程式碼展示瞭如何使用MultiLabelBinarizer將文字標籤轉換為二進位矩陣。首先,我們用所有可能的標籤(all_labels)訓練這個轉換器,這樣它就能學習從標籤名稱到ID的對映。然後,我們用它來轉換兩個標籤集:第一個包含"tokenization"和"new model",第二個只有"pytorch"。

轉換結果是一個二維陣列,每行代表一個樣本,每列代表一個可能的標籤。第一行有兩個1(對應"tokenization"和"new model"),第二行只有一個1(對應"pytorch")。這種表示方法使得多標籤分類別問題變得可處理,因為我們現在可以為每個標籤訓練一個二元分類別器。

為了建立平衡的訓練/測試分割,我們使用Scikit-multilearn的iterative_train_test_split()函式,它迭代地建立分割以實作標籤平衡:

from skmultilearn.model_selection import iterative_train_test_split

def balanced_split(df, test_size=0.5):
    ind = np.expand_dims(np.arange(len(df)), axis=1)
    labels = mlb.transform(df["labels"])
    ind_train, _, ind_test, _ = iterative_train_test_split(ind, labels, test_size)
    return df.iloc[ind_train[:, 0]], df.iloc[ind_test[:,0]]

這個函式封裝了iterative_train_test_split()的功能,使其能夠直接應用於Pandas DataFrame。由於該函式期望一個二維特徵矩陣,我們需要使用np.expand_dims()為可能的索引增加一個維度。

函式接收一個DataFrame和測試集大小引數,然後回傳兩個DataFrame:訓練集和測試集。這種分割方式確保了標籤在兩個集合中的分佈盡可能相似,這對於多標籤分類別任務尤為重要,因為我們需要確保每個標籤在訓練和測試中都有足夠的樣本。

現在我們可以使用這個函式將資料分成監督和非監督資料集,並為監督部分建立平衡的訓練、驗證和測試集:

from sklearn.model_selection import train_test_split

df_clean = df_issues[["text", "labels", "split"]].reset_index(drop=True).copy()
df_unsup = df_clean.loc[df_clean["split"] == "unlabeled", ["text", "labels"]]
df_sup = df_clean.loc[df_clean["split"] == "labeled", ["text", "labels"]]

np.random.seed(0)
df_train, df_tmp = balanced_split(df_sup, test_size=0.5)
df_valid, df_test = balanced_split(df_tmp, test_size=0.5)

這段程式碼首先建立一個乾淨的DataFrame,只包含我們需要的列:文字、標籤和分割訊息。然後,根據"split"列的值將資料分為監督(標記)和非監督(未標記)部分。

接著,我們設定隨機種子以確保結果可重現,並使用先前定義的balanced_split()函式將監督資料進一步分割。首先,我們將監督資料分成訓練集和臨時集,比例為50/50。然後,我們再次將臨時集分成驗證集和測試集,也是50/50。這樣,我們就得到了三個集合:訓練、驗證和測試,它們在標籤分佈上盡可能平衡。

最後,我們將所有分割整合到一個DatasetDict中,方便後續處理:

from datasets import Dataset, DatasetDict

ds = DatasetDict({
    "train": Dataset.from_pandas(df_train.reset_index(drop=True)),
    "valid": Dataset.from_pandas(df_valid.reset_index(drop=True)),
    "test": Dataset.from_pandas(df_test.reset_index(drop=True)),
    "unsup": Dataset.from_pandas(df_unsup.reset_index(drop=True))})

這段程式碼利用Hugging Face的datasets函式庫建立了一個DatasetDict物件,它包含了我們所有的資料分割。from_pandas()方法使我們能夠直接從Pandas DataFrame載入每個分割。在轉換之前,我們使用reset_index(drop=True)重置索引,以確保資料集正確載入。

這種組織方式使我們能夠輕鬆地對資料集進行標記化,並且Hugging Face的Trainer API整合,這在後續使用Transformer模型時非常有用。

建立訓練切片:研究資料量對效能的影響

為了深入研究標記資料量對分類別器效能的影響,我們需要建立不同大小的訓練集切片。這將幫助我們瞭解各種方法在極少標記資料的情況下的表現。我們將從每個標籤僅8個樣本開始,逐步增加到完整的訓練集:

np.random.seed(0)
all_indices = np.expand_dims(list(range(len(ds["train"]))), axis=1)
indices_pool = all_indices
labels = mlb.transform(ds["train"]["labels"])

train_samples = [8, 16, 32, 64, 128]
train_slices, last_k = [], 0

for i, k in enumerate(train_samples):
    # 分割出必要的樣本以填補到下一個切片大小的差距
    indices_pool, labels, new_slice, _ = iterative_train_test_split(
        indices_pool, labels, (k-last_k)/len(labels))
    last_k = k
    if i==0: train_slices.append(new_slice)
    else: train_slices.append(np.concatenate((train_slices[-1], new_slice)))

# 增加完整資料集作為最後一個切片
train_slices.append(all_indices)
train_samples.append(len(ds["train"]))
train_slices = [np.squeeze(train_slice) for train_slice in train_slices]

這段程式碼建立了一系列訓練集切片,從小到大遞增。我們首先設定隨機種子並初始化變數。all_indices包含所有訓練樣本的索引,indices_pool初始時等於all_indices,但會在迭代過程中減少。labels是訓練集中所有樣本的二進位標籤矩陣。

在迴圈中,我們使用iterative_train_test_split()函式從indices_pool中分割出足夠的樣本,以填補當前切片大小與下一個目標大小之間的差距。對於第一個切片,我們直接增加新分割的樣本;對於後續切片,我們將新樣本與前一個切片連線起來,這樣每個切片都是前一個的超集。最後,我們增加完整的訓練集作為最後一個切片。

這種迭代方法只能近似地分割樣本到所需的大小,因為在給定的分割大小下,並不總是能找到完全平衡的分割:

print("Target split sizes:")
print(train_samples)
print("Actual split sizes:")
print([len(x) for x in train_slices])

輸出結果:

Target split sizes:
[8, 16, 32, 64, 128, 223]
Actual split sizes:
[10, 19, 36, 68, 134, 223]

在後續分析中,我們將使用指定的分割大小作為標籤,而不是實際大小。這些訓練切片將幫助我們評估不同方法在各種資料量下的表現。

實作樸素貝葉斯基線模型

在開始任何NLP專案時,建立強基線模型總是一個好主意。這有兩個主要原因:

  1. 根據正規表示式、手工規則或非常簡單模型的基線可能已經能很好地解決問題。在這些情況下,沒有理由使用Transformer等大型模型,後者在生產環境中通常更複雜與維護成本更高。

  2. 基線提供了探索更複雜模型時的快速檢查點。例如,如果你訓練了BERT-large並在驗證集上獲得了80%的準確率,你可能會認為這是一個困難的資料集。但如果你知道簡單的邏輯迴歸模型能獲得95%的準確率呢?這會讓你產生懷疑並促使你除錯模型。

因此,讓我們從訓練一個基線模型開始。對於文字分類別,樸素貝葉斯分類別器是一個很好的基線,因為它非常簡單、訓練迅速,並且對輸入的擾動相當穩健。

Scikit-learn的樸素貝葉斯實作本身不支援多標籤分類別,但我們可以使用Scikit-multilearn函式庫將問題轉換為一對多分類別任務,即為L個標籤訓練L個二元分類別器。首先,讓我們使用多標籤二值化器在訓練集中建立一個新的label_ids列:

def prepare_labels(batch):
    batch["label_ids"] = mlb.transform(batch["labels"])
    return batch

ds = ds.map(prepare_labels, batched=True)

這個函式使用之前定義的mlb(MultiLabelBinarizer)將文字標籤轉換為二進位矩陣,並將結果儲存在一個新的label_ids列中。我們使用map()函式一次性處理整個資料集,batched=True引數使其能夠批次處理資料,提高效率。

為了測量分類別器的效能,我們將使用微平均和巨集平均F1分數,前者跟蹤頻繁標籤的效能,後者不考慮頻率地跟蹤所有標籤的效能。由於我們將評估每個模型在不同大小訓練切片上的表現,我們建立一個defaultdict來儲存每個切片的分數:

from collections import defaultdict

macro_scores, micro_scores = defaultdict(list), defaultdict(list)

這裡我們建立了兩個defaultdict物件,它們的預設值是空列表。macro_scores將儲存每個模型在每個訓練切片上的巨集平均F1分數,而micro_scores將儲存微平均F1分數。這種資料結構使我們能夠輕鬆地將不同模型的效能增加到相應的列表中,以便後續比較和視覺化。

現在,我們準備訓練我們的基線模型!以下是訓練模型並在不同大小的訓練集上評估分類別器的程式碼:

from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import classification_report
from skmultilearn.problem_transform import BinaryRelevance
from sklearn.feature_extraction.text import CountVectorizer

for train_slice in train_slices:
    # 取得訓練切片和測試資料
    ds_train_sample = ds["train"].select(train_slice)
    y_train = np.array(ds_train_sample["label_ids"])
    y_test = np.array(ds["test"]["label_ids"])
    
    # 使用簡單的計數向量化器將文字編碼為標記計數
    count_vect = CountVectorizer()
    X_train_counts = count_vect.fit_transform(ds_train_sample["text"])
    X_test_counts = count_vect.transform(ds["test"]["text"])

在這段程式碼中,我們開始遍歷之前建立的訓練切片。對於每個切片,我們選擇相應的訓練樣本並取得訓練和測試標籤。然後,我們使用CountVectorizer將文字轉換為詞頻矩陣,這是一種簡單但有效的特徵提取方法,它將文字表示為單詞出現次數的向量。

fit_transform()方法在訓練資料上學習詞彙表並轉換文字,而transform()方法僅使用已學習的詞彙表轉換測試資料。這確保了我們不會從測試資料中洩漏訊息到模型中。

在實際應用中,我會繼續訓練樸素貝葉斯分類別器,並評估其效能。這將作為我們比較更複雜模型的基準點。隨著訓練資料量的增加,我們可以觀察效能如何變化,這會提供關於資料效率的寶貴見解。

多標籤分類別的挑戰與解決方案

多標籤分類別比單標籤分類別更具挑戰性,主要原因有:

  1. 標籤不平衡:某些標籤可能非常罕見,導致模型難以學習這些類別的特徵。

  2. 標籤相關性:標籤之間可能存在相關性或依賴關係,忽略這些關係可能會導致次優效能。

  3. 評估複雜性:評估多標籤分類別器需要考慮多個指標,如精確率、召回率、F1分數的微平均和巨集平均版本。

  4. 資料稀缺:當標記資料有限時,每個標籤的訓練例項更少,使學習更加困難。

在我們的GitHub問題標籤系統中,這些挑戰尤為明顯。我們只有223個標記例項用於訓練,而與標籤分佈不均衡。為了應對這些挑戰,我們採用了以下策略:

  1. 使用迭代平衡分割:確保每個標籤在訓練和測試集中都有足夠的代表性。

  2. 建立強基線:從簡單但強大的基線模型開始,如樸素貝葉斯分類別器。

  3. 研究資料效率:透過在不同大小的訓練集上評估模型,瞭解資料量如何影響效能。

  4. 特徵工程:使用適當的文字表示方法,如詞袋模型,捕捉文字中的關鍵訊息。

在下一步的研究中,我可能會探索更先進的方法,如:

  1. 問題轉換方法:除了一對多方法外,還可以使用標籤冪集方法或標籤排序方法。

  2. 演算法適應方法:修改現有演算法以直接處理多標籤問題,如多標籤決策樹。

  3. 整合方法:組合多個分類別器以提高效能,如隨機k-標籤集。

  4. 深度學習方法:利用神經網路捕捉標籤間的複雜關係,如多工學習架構。

  5. 半監督學習:利用未標記資料提高模型效能,特別適用於標記資料有限的情況。

資料量與模型效能的關係

在實際應用中,瞭解資料量與模型效能之間的關係至關重要,這有助於我們做出關於資料收集和模型選擇的明智決策。透過在不同大小的訓練切片上評估模型,我們可以繪製學習曲線,顯示效能如何隨訓練樣本數量的增加而變化。

典型的學習曲線通常呈現對數增長模式:在資料量較少時,增加少量樣本會顯著提高效能;但隨著資料量的增加,收益逐漸減少。這種模式的確切形狀取決於問題的複雜性、模型的容量以及資料的品質。

對於我們的GitHub問題標籤系統,我們可能會發現:

  1. 簡單模型(如樸素貝葉斯)在小資料集上表現相對較好,但隨著資料量增加,其效能提升有限。

  2. 複雜模型(如深度學習模型)在小資料集上可能表現不佳,但隨著資料量增加,其效能提升顯著。

  3. 對於罕見標籤,即使增加總體資料量,效能也可能仍然受限,除非專門增加這些標籤的樣本。

這種分析不僅幫助我們選擇適當的模型,還指導我們的資料收集策略。例如,如果我們發現某些標籤的效能特別差,我們可能需要專門收集這些標籤的更多樣本,而不是簡單地增加總體資料量。

在實際工作中,我經常發現平衡資料品質和數量是關鍵。有時,少量高品質的標記資料比大量噪聲資料更有價值。同樣重要的是確保標記過程的一致性和準確性,因為標記錯誤可能會嚴重影響模型效能。

結論與實踐建議

構建有效的多標籤分類別系統,特別是在標記資料有限的情況下,需要綜合考慮資料處理、模型選擇和評估策略。透過本文的GitHub問題標籤系統案例,我們展示瞭如何系統地處理這類別問題。

從資料探索開始,我們瞭解了文字長度分佈和標籤特性,這為後續處理奠定了基礎。然後,我們使用專門的多標籤分割方法建立了平衡的訓練和測試集,並進一步建立了不同大小的訓練切片,以研究資料量對效能的影響。

在模型方面,我們從樸素貝葉斯分類別器開始,它作為一個強基線,幫助我們理解問題的難度並為更複雜模型提供比較基準。透過在不同大小的訓練集上評估模型,我們可以瞭解資料效率並做出關於資料收集和模型選擇的明智決策。

在實際應用中,我建議採取以下步驟來最佳化多標籤分類別系統:

  1. 投入時間進行徹底的資料探索和清洗,瞭解資料特點和潛在挑戰。

  2. 使用適當的方法建立平衡的訓練和測試集,確保每個標籤都有足夠的代表性。

  3. 從簡單但強大的基線模型開始,逐步探索更複雜的方法。

  4. 研究資料量與效能的關係,指導資料收集和模型選擇決策。

  5. 考慮標籤間的相關性和依賴關係,可能時採用能捕捉這些關係的方法。

  6. 使用多種評估指標全面評估模型效能,特別關注在罕見標籤上的表現。

  7. 在可能的情況下,利用未標記資料提高模型效能,如半監督學習方法。

透過這些策略,即使在標記資料有限的情況下,也能構建出高效的多標籤分類別系統,為使用者提供準確的自動標籤服務,大提高工作效率。 接下來,我們將繼續探討多標籤文字分類別的模型訓練與評估方法。

模型訓練與評估:從樸素貝葉斯到進階方法

上面這段程式碼是建立和訓練多標籤分類別器的核心部分。讓我們詳細解析其工作原理:

  1. BinaryRelevance(classifier=MultinomialNB()):這行建立了一個使用多項式樸素貝葉斯作為基礎分類別器的二元相關性模型。二元相關性是處理多標籤分類別的一種策略,它為每個標籤類別獨立訓練一個二元分類別器。

  2. 模型訓練部分使用了向量化後的訓練資料(X_train_counts)和對應的標籤(y_train)。

  3. 預測與評估部分使用了測試資料生成預測,並透過classification_report計算了各項效能指標。

  4. 最後將巨集平均(macro average)和微平均(micro average)F1分數儲存到對應的字典中,這些指標將用於後續的效能分析和視覺化。

這種計數向量化的方法(bag-of-words)雖然簡單,但值得注意的是它完全忽略了詞序訊息,僅考慮詞頻。

結果視覺化與效能分析

提供的plot_metrics函式是一個實用的視覺化工具,用於繪製模型在不同訓練樣本數量下的表現。這個函式建立了兩個子圖:左側顯示微平均F1分數,右側顯示巨集平均F1分數。當前模型(在這裡是樸素貝葉斯)以實線表示,而其他模型則以虛線顯示,便於比較。

從圖表中我們可以觀察到幾個關鍵趨勢:

  1. 隨著訓練樣本數量的增加,模型的F1分數(微平均和巨集平均)都有所提升。
  2. 由於訓練樣本少,結果存在一定的波動性,這主要是因為每個切片可能有不同的類別分佈。
  3. 對數刻度的x軸幫助我們更好地觀察模型效能隨樣本量增長的趨勢變化。

零標記資料的分類別挑戰

當我們完全沒有標記資料時,零樣本分類別(zero-shot classification)提供了一種解決方案。這種情況在產業應用中相當常見,可能是因為歷史資料缺乏標籤,或者取得標籤成本過高。

零樣本分類別的核心思想是利用預訓練模型而無需針對特定任務進行額外微調。這種方法的可行性源於像BERT這樣的語言模型在大量文字上預訓練時所獲得的語義理解能力。

遮罩語言模型的巧妙應用

一個直觀的零樣本分類別方法是利用遮罩語言模型(masked language model)的預測能力。我們可以建構如下的提示句:

"這個段落的主題是[MASK]。"

由於模型在預訓練過程中已經學習了上下文與主題的關係,它應該能夠為遮罩位置提供合理的主題預測。

作者透過一個有趣的例子説明瞭這一點:假設你有兩個孩子,一個喜歡關於汽車的電影,另一個喜歡關於動物的電影。你想建立一個函式來判斷新電影的主題。使用BERT的fill-mask管道,我們可以:

from transformers import pipeline
pipe = pipeline("fill-mask", model="bert-base-uncased")

movie_desc = "The main characters of the movie madacascar are a lion, a zebra, a giraffe, and a hippo. "
prompt = "The movie is about [MASK]."
output = pipe(movie_desc + prompt)

結果顯示模型預測的最可能標記都與動物相關。我們也可以直接查詢特定標記的機率:

output = pipe(movie_desc + prompt, targets=["animals", "cars"])

對於關於動物的電影描述,“animals"的機率遠高於"cars”;當換成關於變形金剛的描述時,“cars"的機率又高於"animals”,這證實了方法的有效性。

自然語言推理的應用

雖然遮罩語言模型可以用於分類別,但使用更接近分類別任務的模型會有更好的效果。文字蘊含(text entailment)是一個很好的代理任務。

在文字蘊含任務中,模型需要判斷兩段文字之間的關係是蘊含(entailment)、矛盾(contradiction)還是中性(neutral)。這類別模型通常在MNLI或XNLI等資料集上訓練。

每個樣本包含三部分:前提(premise)、假設(hypothesis)和標籤。當假設在前提條件下必然為真時,標籤為蘊含;當假設在前提條件下必然為假時,標籤為矛盾;如果兩者都不適用,則標籤為中性。

這種根據文字蘊含的方法為零樣本分類別提供了更強大的基礎,因為它更接近分類別的本質:判斷文字與特定類別描述之間的關係。

從零樣本到少樣本學習

隨著我們從完全沒有標記資料的情況逐步過渡到有少量標記資料的情況,分類別方法也需要相應調整。下一節將探討如何利用少量標記資料進行高效的文字分類別,以及如何將預訓練模型與少量標記資料結合,實作更好的分類別效能。

這種進階方法對於實際應用尤為重要,因為在大多數現實場景中,我們通常能夠取得少量標記資料,但完全依賴大量標記資料的傳統方法又難以實作。透過結合預訓練模型的知識和少量標記資料的特定任務訊息,我們可以構建更加高效和準確的分類別系統。

title: “零標籤資料的自然語言處理:從零樣本分類別到模型選擇” date: 2025

零樣本學習與少量標籤的NLP技術實戰

在實際的NLP專案中,我們常面臨一個嚴峻的挑戰:缺乏足夠的標籤資料。無論是因為標註成本高昂,還是專案時間緊迫,有時我們必須在極少甚至沒有標籤的情況下構建有效的文字分類別模型。這篇文章將探討如何在這種資源受限的情境中應對挑戰,從零樣本學習到少量標籤的有效利用。

零樣本學習的表現評估

在評估零樣本學習管道的表現時,我發現即使在樣本量超過50的情況下,零樣本方法在微觀和巨集觀F1分數上依然表現優異。微觀F1分數的結果表明,基準模型在常見類別上表現良好,而零樣本管道則在這些類別上更為出色,因為它不需要任何學習樣本。

值得注意的是,雖然我們討論的是無標籤情境,但在評估過程中仍然使用了驗證集和測試集。這是為了展示不同技術的效果並使結果具有可比性。即使在實際應用中,收集少量標記樣本進行快速評估也是合理的。關鍵在於我們並未使用資料來調整模型引數,而只是調整了一些超引數。

如果你在自己的資料集上難以獲得良好結果,可以嘗試以下幾種方法來改進零樣本管道:

  1. 零樣本管道對標籤名稱非常敏感。如果標籤名稱缺乏意義或難以與文字建立聯絡,管道很可能表現不佳。可以嘗試使用不同的名稱,或平行使用多個名稱並在額外步驟中聚合它們。

  2. 另一個可以改進的是假設的形式。預設形式是 hypothesis="This is example is about {}" ,但你可以向管道傳遞任何其他文字。根據具體使用案例,這可能會提升效能。

接下來,讓我們轉向有少量標記樣本可用於訓練模型的情境。

處理少量標籤資料的策略

在大多數NLP專案中,你至少能夠取得一些標記樣本。這些標籤可能直接來自客戶或跨公司團隊,或者你可能決定親自標註一些樣本。即使在前面的零樣本方法中,我們也需要一些標記樣本來評估零樣本方法的效果。

在這一部分,我將探討如何充分利用這些寶貴的少量標記樣本。首先來看一種稱為資料增強的技術,它能幫助我們擴充有限的標記資料。

文字資料增強技術

一種簡單但有效的方法是應用資料增強技術,從現有樣本生成新的訓練樣本。這在電腦視覺領域是常見策略,其中影像會被隨機擾動而不改變資料的含義(例如,稍微旋轉的貓仍然是貓)。

對於文字,資料增強較為棘手,因為擾動單詞或字元可能完全改變含義。例如,“大象比老鼠重嗎?“和"老鼠比大象重嗎?“僅透過單詞交換就有了完全相反的答案。然而,如果文字包含多個句子(比如GitHub問題描述),這類別轉換引入的噪聲通常不會影響標籤。

實際應用中,常用的文字資料增強技術主要有兩類別:

回譯法(Back translation)

將源語言文字翻譯成一個或多個目標語言,然後再翻譯回源語言。回譯法對於資源豐富的語言或不包含太多領域特定詞彙的語料函式庫效果最佳。

詞元擾動(Token perturbations)

給定訓練集中的文字,隨機選擇並執行簡單轉換,如隨機同義詞替換、單詞插入、交換或刪除。

下表展示了這些轉換的例子:

增強方法句子
原句Even if you defeat me Megatron, others will rise to defeat your tyranny
同義詞替換Even if you kill me Megatron, others will prove to defeat your tyranny
隨機插入Even if you defeat me Megatron, others humanity will rise to defeat your tyranny
隨機交換You even if defeat me Megatron, others will rise defeat to tyranny your
隨機刪除Even if you me Megatron, others to defeat tyranny
回譯(德語)Even if you defeat me, others will rise up to defeat your tyranny

回譯可以使用M2M100等機器翻譯模型實作,而NlpAug和TextAttack等函式庫提供了各種詞元擾動方案。在這裡,我將專注於使用同義詞替換,因為它實作簡單與能夠傳達資料增強的核心理念。

我使用NlpAug中的ContextualWordEmbsAug增強器,利用DistilBERT的上下文詞嵌入進行同義詞替換。讓我們從一個簡單的例子開始:

from transformers import set_seed
import nlpaug.augmenter.word as naw
set_seed(3)
aug = naw.ContextualWordEmbsAug(model_path="distilbert-base-uncased",
device="cpu", action="substitute")
text = "Transformers are the most popular toys"
print(f"Original text: {text}")
print(f"Augmented text: {aug.augment(text)}")

輸出結果:

Original text: Transformers are the most popular toys
Augmented text: transformers'the most popular toys

在這個例子中,我使用了DistilBERT模型來生成上下文相關的同義詞替換。透過設定隨機種子確保結果可重現,並指定CPU作為運算裝置。增強器將單詞"are"替換為撇號,生成了一個新的合成訓練樣本。這種輕微的變化可以幫助模型學習更健壯的特徵,而不會改變文字的基本含義。

我們可以將這種增強包裝在一個簡單的函式中:

def augment_text(batch, transformations_per_example=1):
    text_aug, label_ids = [], []
    for text, labels in zip(batch["text"], batch["label_ids"]):
        text_aug += [text]
        label_ids += [labels]
        for _ in range(transformations_per_example):
            text_aug += [aug.augment(text)]
            label_ids += [labels]
    return {"text": text_aug, "label_ids": label_ids}

這個函式接受一批文字和標籤,並為每個原始樣本建立指定數量的增強版本。它保留原始樣本,然後增加增強版本,同時複製對應的標籤。這樣,如果設定transformations_per_example=1,資料集大小會增加一倍;設定為2則增加三倍(原始樣本加兩個增強版本)。

現在,當我們將此函式傳遞給map()方法時,可以透過transformations_per_example引數生成任意數量的新樣本。在訓練樸素貝葉斯分類別器時,只需在選擇樣本切片後增加一行程式碼:

ds_train_sample = ds_train_sample.map(augment_text, batched=True,
                                     remove_columns=ds_train_sample.column_names).shuffle(seed=42)

這行程式碼將資料增強應用於訓練樣本,移除原始列名(因為我們的函式回傳新的列名),並使用固定的隨機種子進行打亂以確保結果可重現。這個簡單的資料增強步驟可以顯著提升模型效能,尤其是在訓練樣本有限的情況下。

實驗結果顯示,少量的資料增強可以將樸素貝葉斯分類別器的F1分數提高約5個點。一旦我們有約170個訓練樣本,它在巨集觀分數上就超過了零樣本管道。這表明資料增強是處理少量標籤場景的有效策略。

使用嵌入向量作為查詢表

大模型語言(如GPT-3)在處理資料有限的任務時表現卓越。原因在於這些模型學習了文字的有用表示,這些表示編碼了多個維度的訊息,如情感、主題、文字結構等。因此,大模型語言的嵌入向量可用於開發語義搜尋引擎、查詢相似檔案或評論,甚至分類別文字。

在這一部分,我將建立一個文字分類別器,其模式類別似於OpenAI API的分類別端點。這個方法遵循三步流程:

  1. 使用語言模型嵌入所有標記文字
  2. 在儲存的嵌入向量上執行最近鄰搜尋
  3. 聚合最近鄰的標籤以獲得預測

當需要對新文字進行分類別時,系統會使用相同的模型嵌入它,然後在儲存的嵌入中查詢最相似的文字。最後,根據最相似文字的標籤進行預測。

這種根據嵌入的方法有幾個顯著優勢:

  1. 它不需要微調大型模型,因此計算效率高
  2. 它可以有效地處理少量標籤資料
  3. 它利用了預訓練模型中已經編碼的豐富語義訊息
  4. 隨著新標記資料的增加,系統可以輕鬆更新和擴充套件

在實踐中,這種方法在標籤資料極為有限的情況下特別有用。它允許我們利用大模型語言的強大表示能力,而無需進行昂貴的微調過程。此外,由於它基本上是一個最近鄰查詢,新類別的增加非常簡單,只需增加帶有新標籤的嵌入向量即可。

這種根據嵌入的查詢表方法在需要快速佈署、資源有限或標籤資料稀缺的場景中,提供了一種實用與高效的文字分類別解決方案。它結合了預訓練語言模型的強大語義理解能力和最近鄰演算法的簡單直觀性,為處理少量標籤的NLP任務提供了一條有效途徑。

綜合策略:從無標籤到少量標籤的實用框架

在處理標籤資料極少或完全沒有的NLP任務時,我建議採用一個階段性的方法:

  1. 評估零樣本能力:首先嘗試零樣本學習方法,特別是當標籤名稱具有明確語義時。調整標籤描述和假設形式,可能會顯著提升效能。

  2. 建立小型評估集:無論選擇何種方法,都值得投入時間標註一小部分資料(每類別約20-50個樣本)作為評估集,以便比較不同方法的效果。

  3. 應用資料增強:如果有少量標記資料,使用資料增強技術擴充訓練集。結契約義詞替換和回譯等方法,可以有效增加資料多樣性。

  4. 利用嵌入向量:對於少量標籤場景,考慮使用大模型語言的嵌入向量結合最近鄰搜尋。這種方法計算效率高,與能充分利用預訓練模型的語義理解能力。

  5. 迭代改進:隨著更多標籤資料的取得,定期重新評估各種方法的效果,並根據需要調整策略。

在實際專案中,我發現混合策略通常效果最佳。例如,可以使用零樣本方法快速啟動專案,同時開始收集標記資料。隨著標記資料的積累,可以逐步過渡到根據嵌入的方法或微調預訓練模型。

處理少量或無標籤資料的NLP任務確實充滿挑戰,但透過合理運用這些技術,我們可以構建出在實際應用中表現良好的文字分類別系統。關鍵在於理解每種方法的優勢和侷限性,並根據具體專案需求選擇合適的策略組合。

隨著NLP技術的不斷發展,我們在少量標籤場景下的能力也在持續提升。大模型語言的出現尤其改變了這一領域的格局,使我們能夠透過零樣本和少樣本學習解決過去需要大量標記資料的問題。透過靈活運用這些技術,即使在資源受限的情況下,我們也能構建出高效的NLP解決方案。

鄰近點嵌入檢索:少量標籤資料的強大解決方案

在機器學習專案中,我們常面臨標籤資料不足的困境。當只有少量標記資料時,傳統的監督式學習方法往往效果不佳。這時,向量嵌入結合最近鄰搜尋的方法就顯得特別有價值。

這種技術之所以強大,主要在於它不需要模型微調就能有效利用少量的標記資料點。關鍵在於選擇一個適合的預訓練模型,理想情況下這個模型應該在與目標資料集相似的領域上進行過訓練。

鄰近點數量的重要性

在實施最近鄰嵌入檢索時,校準要搜尋的鄰居數量至關重要:

  • 鄰居太少:結果可能帶有噪音,不穩定
  • 鄰居太多:可能混入鄰近但不相關的群組

這種平衡對於最終分類別效果有決定性影響。

使用預訓練模型生成文字嵌入向量

由於我們需要將文字轉換為向量表示,讓我們使用預訓練的語言模型來完成這項工作。雖然GPT-3只能透過OpenAI API存取,但我們可以使用GPT-2的變體來測試這種技術。具體來説,我將使用一個在Python程式碼上訓練過的GPT-2變體,這樣它能更好地捕捉GitHub問題中的上下文。

文字嵌入處理的關鍵挑戰

在使用Transformer模型如GPT-2時,一個主要挑戰是:模型會為每個標記(token)回傳一個嵌入向量。例如,對於句子"I took my dog for a walk”,我們會得到多個嵌入向量。但我們真正需要的是整個句子(或GitHub問題)的單一嵌入向量。

為解決這個問題,我們可以使用池化(pooling)技術。最簡單的池化方法之一是取標記嵌入的平均值,即平均池化(mean pooling)。使用平均池化時,我們需要確保不將填充標記納入平均值計算,這可以透過注意力遮罩(attention mask)來處理。

實作文字嵌入功能

讓我們來實作這個功能,首先載入GPT-2分詞器和模型,定義平均池化操作,並將整個過程封裝在一個簡單的embed_text()函式中:

import torch
from transformers import AutoTokenizer, AutoModel

model_ckpt = "miguelvictor/python-gpt2-large"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
model = AutoModel.from_pretrained(model_ckpt)

def mean_pooling(model_output, attention_mask):
    # 提取標記嵌入
    token_embeddings = model_output[0]
    
    # 計算注意力遮罩
    input_mask_expanded = (attention_mask
                          .unsqueeze(-1)
                          .expand(token_embeddings.size())
                          .float())
    
    # 對嵌入求和,但忽略被遮罩的標記
    sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
    sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
    
    # 回傳平均值作為單一向量
    return sum_embeddings / sum_mask

def embed_text(examples):
    inputs = tokenizer(examples["text"], padding=True, truncation=True,
                      max_length=128, return_tensors="pt")
    with torch.no_grad():
        model_output = model(**inputs)
    pooled_embeds = mean_pooling(model_output, inputs["attention_mask"])
    return {"embedding": pooled_embeds.cpu().numpy()}

這段程式碼實作了幾個關鍵功能:

  1. 首先載入了一個針對Python程式碼訓練的GPT-2模型
  2. mean_pooling函式處理模型輸出,將每個標記的嵌入向量合併成一個單一向量
  3. 注意力遮罩確保填充標記不參與平均計算,這是處理變長文字的關鍵
  4. embed_text函式將整個過程封裝起來,接收文字批次,回傳嵌入向量
  5. torch.no_grad()確保在推理過程中不計算梯度,節省記憶體

現在我們可以為每個資料集生成嵌入向量。注意,GPT風格的模型沒有填充標記,因此在批次處理之前需要增加一個。我們可以簡單地重用結束標記(end-of-string token)作為填充標記:

tokenizer.pad_token = tokenizer.eos_token
embs_train = ds["train"].map(embed_text, batched=True, batch_size=16)
embs_valid = ds["valid"].map(embed_text, batched=True, batch_size=16)
embs_test = ds["test"].map(embed_text, batched=True, batch_size=16)

使用FAISS建立高效向量搜尋引擎

現在我們擁有了所有嵌入向量,需要建立一個系統來搜尋它們。我們可以編寫一個函式,計算新文字嵌入與訓練集中現有嵌入之間的相似度(例如餘弦相似度)。但更好的選擇是使用Datasets內建的FAISS索引結構。

FAISS(Facebook AI Similarity Search)是一個專為高效向量搜尋而設計的函式庫,可以將其視為嵌入向量的搜尋引擎。我們可以使用add_faiss_index()函式將現有欄位增加到FAISS索引中,或者使用add_faiss_index_from_external_arrays()載入新的嵌入向量。

讓我們使用前者將訓練嵌入增加到資料集中:

embs_train.add_faiss_index("embedding")

嘗試最近鄰檢索

現在我們可以透過呼叫get_nearest_examples()函式執行最近鄰查詢。該函式回傳最接近的鄰居以及每個鄰居的比對分數。我們需要指定查詢嵌入以及要檢索的最近鄰數量。讓我們試一下,看哪些檔案與一個範例最接近:

i, k = 0, 3  # 選擇第一個查詢和3個最近鄰
rn, nl = "\r\n\r\n", "\n"  # 用於移除文字中的換行符,使顯示更緊湊

query = np.array(embs_valid[i]["embedding"], dtype=np.float32)
scores, samples = embs_train.get_nearest_examples("embedding", query, k=k)

print(f"QUERY LABELS: {embs_valid[i]['labels']}")
print(f"QUERY TEXT:\n{embs_valid[i]['text'][:200].replace(rn, nl)} [...]\n")
print("="*50)
print(f"Retrieved documents:")

for score, label, text in zip(scores, samples["labels"], samples["text"]):
    print("="*50)
    print(f"TEXT:\n{text[:200].replace(rn, nl)} [...]")
    print(f"SCORE: {score:.2f}")
    print(f"LABELS: {label}")

執行上述程式碼,我們得到了令人驚喜的結果:透過嵌入查詢獲得的三個檔案都具有相同的標籤,從標題就能看出它們非常相似。查詢和檢索到的檔案都圍繞著增加新的高效Transformer模型。

最佳引數選擇:k和m的最佳化

現在的問題是:k的最佳值是多少?同樣,我們應該如何聚合檢索檔案的標籤?例如,我們是應該檢索三個檔案並分配所有至少出現兩次的標籤?還是應該檢索20個檔案並使用所有至少出現5次的標籤?

讓我們系統地研究這個問題:我們將嘗試多個k值,然後對每個k變化標籤分配閾值m(其中m < k)。我們將記錄每種設定的巨集觀(macro)和微觀(micro)效能,以便稍後決定哪種執行效果最佳。

我們可以使用get_nearest_examples_batch()函式,它接受一批查詢,而不是迴圈遍歷驗證集中的每個樣本:

def get_sample_preds(sample, m):
    return (np.sum(sample["label_ids"], axis=0) >= m).astype(int)

def find_best_k_m(ds_train, valid_queries, valid_labels, max_k=17):
    max_k = min(len(ds_train), max_k)
    perf_micro = np.zeros((max_k, max_k))
    perf_macro = np.zeros((max_k, max_k))
    
    for k in range(1, max_k):
        for m in range(1, k + 1):
            _, samples = ds_train.get_nearest_examples_batch("embedding",
                                                          valid_queries, k=k)
            y_pred = np.array([get_sample_preds(s, m) for s in samples])
            clf_report = classification_report(valid_labels, y_pred,
                                           target_names=mlb.classes_, 
                                           zero_division=0, output_dict=True)
            perf_micro[k, m] = clf_report["micro avg"]["f1-score"]
            perf_macro[k, m] = clf_report["macro avg"]["f1-score"]
    
    return perf_micro, perf_macro

這段程式碼實作了兩個關鍵函式:

  1. get_sample_preds:根據閾值m決定哪些標籤應該被分配給樣本
  2. find_best_k_m:系統地測試不同的k和m值組合,評估每種組合的效能
  3. 函式使用F1分數作為評估指標,分別記錄微觀和巨集觀層面的效能
  4. 微觀F1考慮所有例項的總體效能,而巨集觀F1則是各類別F1分數的平均值

視覺化k和m的效能

讓我們檢查使用所有訓練樣本時的最佳值,並視覺化所有k和m設定的分數:

valid_labels = np.array(embs_valid["label_ids"])
valid_queries = np.array(embs_valid["embedding"], dtype=np.float32)
perf_micro, perf_macro = find_best_k_m(embs_train, valid_queries, valid_labels)

fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True)
ax0.imshow(perf_micro)
ax1.imshow(perf_macro)
ax0.set_title("micro scores")
ax0.set_ylabel("k")
ax1.set_title("macro scores")

for ax in [ax0, ax1]:
    ax.set_xlim([0.5, 17 - 0.5])
    ax.set_ylim([17 - 0.5, 0.5])
    ax.set_xlabel("m")

plt.show()

從視覺化結果可以看出一個明顯的模式:對於給定的k值,選擇過大或過小的m都會導致次優結果。當選擇m/k比率約為1/3時,能達到最佳效能。讓我們找出整體最佳的k和m值:

k, m = np.unravel_index(perf_micro.argmax(), perf_micro.shape)
print(f"Best k: {k}, best m: {m}")

結果顯示,當我們選擇k=15和m=5時,效能最佳。換句話説,當我們檢索15個最近鄰並分配至少出現5次的標籤時,分類別效果最好。

在不同訓練集大小下評估嵌入查詢效能

現在我們有了一個良好的方法來找到嵌入查詢的最佳引數,可以像之前對樸素貝葉斯分類別器那樣,透過訓練集的不同切片來評估效能。在切片資料集之前,我們需要刪除索引,因為無法像資料集那樣切片FAISS索引:

embs_train.drop_index("embedding")
test_labels = np.array(embs_test["label_ids"])
test_queries = np.array(embs_test["embedding"], dtype=np.float32)

for train_slice in train_slices:
    # 從訓練切片建立Faiss索引
    embs_train_tmp = embs_train.select(train_slice)
    embs_train_tmp.add_faiss_index("embedding")
    
    # 使用驗證集取得最佳k、m值
    perf_micro, _ = find_best_k_m(embs_train_tmp, valid_queries, valid_labels)
    k, m = np.unravel_index(perf_micro.argmax(), perf_micro.shape)
    
    # 在測試集上取得預測
    _, samples = embs_train_tmp.get_nearest_examples_batch("embedding",
                                                        test_queries,
                                                        k=int(k))
    y_pred = np.array([get_sample_preds(s, m) for s in samples])
    
    # 評估預測
    clf_report = classification_report(test_labels, y_pred,
                                    target_names=mlb.classes_, 
                                    zero_division=0, output_dict=True)
    macro_scores["Embedding"].append(clf_report["macro avg"]["f1-score"])
    micro_scores["Embedding"].append(clf_report["micro avg"]["f1-score"])

plot_metrics(micro_scores, macro_scores, train_samples, "Embedding")

這段程式碼展示瞭如何評估嵌入查詢方法在不同訓練資料量下的表現:

  1. 首先刪除現有的FAISS索引,以便能夠切片資料集
  2. 對於每個訓練集切片:
    • 建立一個新的FAISS索引
    • 使用驗證集找出最佳的k和m值
    • 對測試集進行預測
    • 評估效能並記錄結果
  3. 最後繪製效能指標,顯示嵌入查詢方法如何隨訓練樣本數量變化而表現

嵌入查詢方法的優勢與特點

透過實驗結果可以看出,嵌入查詢方法在微觀分數上與之前的方法競爭力相當,同時只有兩個"可學習"引數:k和m。這種方法的主要優勢包括:

  1. 無需模型微調:不需要耗時的模型訓練過程
  2. 引數少:只需調整k(檢索的鄰居數量)和m(標籤分配閾值)
  3. 靈活性高:可以輕鬆適應新增的標籤和類別
  4. 低資源需求:特別適合標籤資料有限的情境
  5. 易於實作:實作簡單,不需要複雜的模型架構

這種方法在處理多標籤分類別問題時特別有效,尤其是當我們只有少量標記資料時。它利用了預訓練模型的強大表示能力,結合最近鄰搜尋的簡單性,提供了一個高效與有效的解決方案。

實際應用建議

在實際應用這種方法時,我建議考慮以下幾點:

  1. 選擇合適的預訓練模型:盡量選擇在與目標領域相似資料上預訓練的模型
  2. 嵌入向量的維度:較高維度的嵌入可能捕捉更多訊息,但也增加計算成本
  3. 池化策略的選擇:除了平均池化,還可以嘗試最大池化或注意力池化
  4. k和m的比例:實驗表明m/k比例約為1/3效果較好,但最佳值可能因資料集而異
  5. 索引更新策略:在實時系統中,考慮定期更新FAISS索引以納入新資料

在資源有限的情況下,向量嵌入結合最近鄰搜尋提供了一個強大的解決方案,能夠有效利用少量標記資料實作良好的分類別效能。這種方法不僅實用,而與實作簡單,是處理少量標籤問題的絕佳選擇。

對於需要快速原型設計或資源有限的專案,這種方法可能是比複雜的深度學習模型更實用的選擇。它結合了預訓練模型的力量和最近鄰演算法的簡單性,提供了一個平衡與有效的解決方案。

在技術選型時,我們應該根據資料量、計算資源和效能需求來選擇合適的方法。當標籤資料極少時,向量嵌入與最近鄰搜尋的組合往往能提供最佳價效比。

領域相關性與模型選擇的重要性

在機器學習應用中,選擇合適的模型和方法往往取決於特定的領域和任務特性。我在處理GitHub問題分類別任務時發現,零樣本管道的效能與其訓練資料的領域相關性密切相關。由於GitHub資料集包含大量可能未在模型訓練中出現的程式碼,這會顯著影響模型表現。

相較之下,對於像評論情感分析這類別更常見的任務,零樣本管道可能表現得更好。嵌入向量的品質同樣高度依賴於模型及其訓練資料。在實際測試中,我嘗試了多種模型,包括:

  • sentence-transformers/stsb-roberta-large (專為句子嵌入最佳化)
  • microsoft/codebert-base (針對程式碼訓練)
  • dbernstein/roberta-python (針對Python檔案訓練)

有趣的是,在這個特定使用案例中,經過Python程式碼訓練的GPT-2模型表現最佳。這再次證明瞭領域適配性的重要性。一旦建立評估管道,只需替換模型檢查點名稱即可快速測試不同模型,這是一個實用的工程技巧。

接下來,讓我們比較這種簡單的嵌入技術與在有限資料上直接微調Transformer模型的效果差異。

FAISS:高效相似度搜尋的秘密武器

FAISS的工作原理與優勢

FAISS (Facebook AI Similarity Search) 是處理大規模向量相似度搜尋的強大工具。當我們從文字搜尋轉向嵌入向量搜尋時,傳統的搜尋加速方法不再適用,這就是FAISS發揮作用的地方。

在文字搜尋中,我們通常使用倒排索引,將詞語對映到檔案。這就像書末的索引:每個詞都對應到它出現的頁面(或檔案)。但這種方法僅適用於離散物件,如單詞,而非連續的向量空間。由於每個檔案可能都有獨特的向量表示,我們需要尋找相似而非完全比對的向量。

理論上,要找到與查詢向量最相似的向量,需要將查詢向量與資料函式庫中的每個向量進行比較。對於本章的小型資料集來説這不是問題,但擴充套件到數千甚至數百萬條目時,每次查詢都需要等待相當長的時間。

FAISS的分割槽技術

FAISS透過資料分割槽解決了這個問題。如果只需將查詢向量與資料函式庫的子集進行比較,我們可以顯著加速處理過程。但如何確定搜尋哪個分割槽?隨機分割槽顯然不是最佳解決方案。

FAISS採用k-means聚類別將資料集分組,按相似性將嵌入向量分成多個群組。每個群組都有一個中心向量(質心),代表該群組所有成員的平均值。

FAISS索引結構:灰點代表增加到索引的資料點,粗黑點是透過k-means聚類別找到的群集中心,彩色區域代表屬於一個群集中心的區域

有了這種分組,搜尋n個向量變得更容易:

  1. 首先搜尋k個質心,找到與查詢最相似的質心(k次比較)
  2. 然後在該群組內搜尋(n/k個元素比較)

這將比較次數從n減少到k + n/k。關鍵問題是:k的最佳值是多少?

  • 如果k太小,每個群組仍包含許多樣本需要比較
  • 如果k太大,我們需要搜尋太多質心

尋找函式f(k) = k + n/k的最小值,我們得到k = √n。對於n = 2^20的情況,最小值出現在k = 2^10 = 1,024處,這正是理論預期的位置。

FAISS的進階功能

除了透過分割槽加速查詢外,FAISS還提供了其他強大功能:

  • 支援GPU加速,進一步提升效能
  • 提供多種向量壓縮方案,解決記憶體問題
  • 針對不同使用場景的最佳化索引選擇

Facebook的CCMatrix語料函式庫建立是FAISS的一個大型應用案例。研究人員使用多語言嵌入來查詢不同語言中的平行句子。這個龐大的語料函式庫隨後用於訓練M2M100,一個能夠在100種語言之間直接翻譯的大型機器翻譯模型。

微調Transformer模型處理少量標籤

從預訓練模型開始

如果我們有標記資料,也可以嘗試最直接的方法:微調預訓練的Transformer模型。在這個部分,我將使用標準BERT檢查點作為起點。

對於許多應用來説,從預訓練的BERT類別模型開始是個不錯的選擇。但如果你的語料函式庫領域與預訓練語料函式庫(通常是維基百科)有顯著差異,建議探索Hugging Face Hub上的其他模型。很可能已經有人在你的領域預訓練了模型!

準備資料

我們首先載入預訓練的分詞器,對資料集進行分詞,並去除訓練和評估不需要的列:

import torch
from transformers import (AutoTokenizer, AutoConfig,
AutoModelForSequenceClassification)

model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)

def tokenize(batch):
    return tokenizer(batch["text"], truncation=True, max_length=128)

ds_enc = ds.map(tokenize, batched=True)
ds_enc = ds_enc.remove_columns(['labels', 'text'])

這段程式碼首先匯入必要的函式庫,然後指定使用"bert-base-uncased"模型作為基礎。接著定義了一個分詞函式,它將文字截斷到最大長度128,並使用BERT的分詞器進行處理。最後,我們將這個函式應用到整個資料集,並移除了原始的’labels’和’text’列,因為分詞後我們不再需要它們。

多標籤損失函式期望標籤型別為float,因為它也允許使用類別機率而非離散標籤。因此,我們需要更改label_ids列的型別:

ds_enc.set_format("torch")
ds_enc = ds_enc.map(lambda x: {"label_ids_f": x["label_ids"].to(torch.float)},
                   remove_columns=["label_ids"])
ds_enc = ds_enc.rename_column("label_ids_f", "label_ids")

由於Arrow的型別格式限制,我們不能直接逐元素修改列格式。這段程式碼採用了一個變通方法:先將資料集格式設定為PyTorch格式,然後建立一個新列"label_ids_f”,將原始標籤轉換為浮點數型張量。接著刪除原始列,並將新列重新命名為原始列名,完成型別轉換。

設定訓練引數

由於資料集規模有限,我們很可能會快速過擬合訓練資料,所以設定load_best_model_at_end=True,並根據微F1分數選擇最佳模型:

from transformers import Trainer, TrainingArguments

training_args_fine_tune = TrainingArguments(
    output_dir="./results", num_train_epochs=20, learning_rate=3e-5,
    lr_scheduler_type='constant', per_device_train_batch_size=4,
    per_device_eval_batch_size=32, weight_decay=0.0,
    evaluation_strategy="epoch", save_strategy="epoch",logging_strategy="epoch",
    load_best_model_at_end=True, metric_for_best_model='micro f1',
    save_total_limit=1, log_level='error')

這段程式碼設定了訓練引數。我們設定了20個訓練週期,使用恆定學習率3e-5,訓練批次大小為4,評估批次大小為32。每個週期都會進行評估並儲存模型,但只保留表現最佳的一個模型版本。關鍵是我們使用’micro f1’作為選擇最佳模型的指標,這對於不平衡的多標籤分類別問題特別有用。

評估指標計算

我們需要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"]}

這個函式用於計算模型的評估指標。由於模型回傳的是logits值,我們首先使用sigmoid函式將它們正規化到0-1範圍,然後使用0.5作為閾值將其二值化。接著使用scikit-learn的classification_report計算詳細的分類別指標,並回傳微平均和巨集平均F1分數。微平均F1考慮所有樣本的總體表現,而巨集平均F1則是各類別F1分數的簡單平均。

訓練與評估模型

現在我們可以開始訓練了。對於每個訓練集切片,我們從頭開始訓練一個分類別器,在訓練迴圈結束時載入表現最佳的模型,並儲存在測試集上的結果:

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)")

這段程式碼首先設定模型設定,指定標籤數量並將問題型別設為多標籤分類別。然後,對每個訓練集切片(不同的樣本數量),我們從預訓練檢查點建立一個新模型,設定Trainer物件,訓練模型,並在測試集上評估效能。結果被儲存在字典中,最後繪製效能曲線。

微調結果分析

從實驗結果可以看出,當我們能夠存取約64個範例時,簡單微調vanilla BERT模型就能達到有競爭力的結果。但在此之前,行為有些不穩定,這再次是由於在小樣本上訓練模型時,某些標籤可能不平衡分佈所致。

這種現象在我處理實際專案中經常遇到 - 當訓練資料極少時,模型表現會有較大波動,這時候資料品質和分佈比數量更重要。特別是在多標籤分類別任務中,確保每個類別都有足夠與平衡的樣本尤為關鍵。

少量標籤學習的關鍵洞見

透過上述實驗,我歸納出處理少量標籤情景的幾點關鍵策略:

  1. 領域適配性優先:選擇與目標領域相近的預訓練模型比通用模型更有效,即使後者規模更大。

  2. 資料效率技術:像FAISS這樣的高效相似度搜尋工具可以最大化有限標籤的價值,特別是在需要處理大規模向量時。

  3. 模型選擇策略:在極少量標籤情況下(<50樣本),根據嵌入的方法往往優於直接微調;而當樣本增加時,微調方法的優勢開始顯現。

  4. 評估指標選擇:對於不平衡的多標籤問題,微F1通常是比巨集F1更可靠的模型選擇指標,因為它考慮了標籤頻率。

  5. 過擬合防護:在少量資料上訓練時,早停和模型選擇策略至關重要,應根據驗證集效能而非訓練集效能選擇模型。

在實際應用中,我發現將這些技術與領域知識相結合,可以在資源有限的情況下構建出高效的分類別系統。特別是對於技術文字分類別,結合程式碼特定的預訓練模型與高效的向量搜尋技術,往往能達到出人意料的好結果。

在下一個部分,我們將探討如何利用未標記資料進一步提升模型效能,這對於實際應用場景尤為重要,因為取得標記資料通常比取得未標記資料困難得多。

少量標籤資料的機器學習挑戰

在機器學習領域,取得大量高品質的標籤資料往往是一項艱鉅任務。無論是時間成本、專業知識需求還是財務考量,這些因素都使得標籤資料的取得變得困難。然而,我們並非完全束手無策。過去幾年,我目睹了許多創新技術的興起,這些技術能夠有效地從少量甚至零標籤資料中學習。

本文將探討三種關鍵策略:使用提示進行語境內學習和少樣本學習、利用無標籤資料進行領域適應,以及微調語言模型。這些方法不僅能幫助我們克服資料限制,還能在資源有限的情況下實作高效的模型訓練。

使用提示的語境內學習和少樣本學習

提示學習的概念與應用

在之前的討論中,我們看到可以使用像BERT或GPT-2這樣的語言模型,透過提示和解析模型的標記預測來適應監督任務。這種方法與傳統的增加特定任務頭並調整模型引數的方法不同。雖然這種方法不需要任何訓練資料,但似乎無法利用我們可能擁有的標籤資料。不過,有一個折衷方案,稱為語境內學習或少樣本學習。

以英語到法語的翻譯任務為例。在零樣本正規化中,我們可能構建如下提示:

Translate English to French:
thanks =>

這有望促使模型預測"merci"一詞的標記。透過增加幾個範例到提示中,模型的表現可以顯著提升。GPT-3論文的一個有趣發現是,大模型語言能夠有效地從提示中呈現的範例中學習。

這段説明瞭提示學習的基本概念:不需要重新訓練模型,而是透過精心設計的提示來引導預訓練模型執行特定任務。這種方法的優勢在於不需要大量訓練資料,但缺點是如果沒有良好的提示設計,效果可能不佳。在實際應用中,我發現提示的設計至關重要,一個好的提示可以顯著提高模型的表現。

大型模型與語境學習能力

研究者發現,隨著模型規模的擴大,它們在使用語境內範例方面的能力也會變得更強,從而帶來顯著的效能提升。雖然GPT-3規模的模型在生產環境中使用具有挑戰性,但這是一個令人興奮的新興研究領域。已有人構建了有趣的應用,例如自然語言命令列,可以透過GPT-3將自然語言命令解析為命令列指令。

使用標籤資料的另一種方法是建立提示和所需預測的範例,並繼續在這些範例上訓練語言模型。一種名為ADAPET的新方法使用這種方法,在各種任務上的表現優於GPT-3,透過生成的提示調整模型。Hugging Face研究人員的最新工作表明,與微調自定義頭相比,這種方法可能更加資料高效。

這裡討論了模型規模與語境學習能力的關係,以及實際應使用案例子。當處理大模型語言時,我觀察到這種能力確實隨著模型引數量的增加而提升。在實踐中,即使無法使用GPT-3這樣的超大模型,也可以透過精心設計的提示和少量範例,讓中小型模型展現出不錯的少樣本學習能力。

利用無標籤資料

雖然擁有大量高品質的標籤資料是訓練分類別器的最佳情況,但這並不意味著無標籤資料毫無價值。只需想我們使用的大多數模型的預訓練:即使它們主要在來自網際網路的不相關資料上訓練,我們仍可以將預訓練權重用於各種文字的其他任務。這是NLP中遷移學習的核心思想。

自然地,如果下游任務具有與預訓練文字類別似的文字結構,遷移效果會更好。因此,如果我們能將預訓練任務更接近下游目標,就有可能改善遷移效果。

領域適應的價值

讓我們以具體使用案例來思考:BERT是在BookCorpus和英文維基百科上預訓練的,而包含程式碼和GitHub問題的文字在這些資料集中絕對是小眾。如果從頭開始預訓練BERT,我們可以在GitHub上所有問題的爬蟲上進行。然而,這將非常昂貴,而與BERT學到的許多語言方面對GitHub問題仍然有效。

那麼,在從頭重新訓練和直接使用模型進行分類別之間,有沒有折衷方案?有的,這就是領域適應(在第7章中我們也看到了問答的領域適應)。我們可以在來自我們領域的資料上繼續訓練語言模型,而不是從頭重新訓練。在這一步中,我們使用預測遮罩詞的經典語言模型目標,這意味著我們不需要任何標籤資料。之後,我們可以將適應後的模型作為分類別器載入並進行微調,從而利用無標籤資料。

這段解釋了領域適應的概念和價值。在實際工作中,我經常使用這種方法來提升模型在特定領域的表現。例如,當處理醫療文字時,通用BERT模型的效果往往不如在醫療文獻上做過領域適應的模型。領域適應的美妙之處在於,與標籤資料相比,無標籤資料通常豐富可得,與適應後的模型可用於多種任務。

微調語言模型

現在讓我們看微調預訓練語言模型所需的步驟。在這一部分,我們將使用遮罩語言建模在資料集的無標籤部分上微調預訓練的BERT模型。為此,我們只需要兩個新概念:標記化資料時的額外步驟和特殊的資料整理器。

標記化處理

除了文字中的普通標記外,標記器還會向序列增加特殊標記,如用於分類別和下一句預測的[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"])

這段程式碼定義了一個標記化函式,它將文字轉換為模型可以處理的標記,並回傳特殊標記的遮罩。這對於遮罩語言建模很重要,因為我們只想預測實際文字中的標記,而不是特殊標記。truncation=Truemax_length=128確保文字被截斷到適當的長度,這對於批次處理很重要。

資料整理器的使用

要開始遮罩語言建模,還缺少在輸入序列中遮罩標記並在輸出中擁有目標記的機制。我們可以設定一個函式,遮罩隨機標記並為這些序列建立標籤。但這會使資料集的大小翻倍,因為我們也會在資料集中儲存目標序列,而與這意味著我們每個訓練週期都會使用相同的序列遮罩。

一個更優雅的解決方案是使用資料整理器。資料整理器是建立資料集和模型呼叫之間橋樑的函式。從資料集中抽樣一個批次,資料整理器準備批次中的元素以餵給模型。在最簡單的情況下,它只是將每個元素的張量連線成一個單一的張量。在我們的案例中,我們可以使用它來即時進行遮罩和標籤生成。這樣我們不需要儲存標籤,而與每次抽樣時都會得到新的遮罩。

這個任務的資料整理器叫做DataCollatorForLanguageModeling。我們用模型的標記器和我們想要遮罩的標記比例透過mlm_probability引數初始化它。我們將使用這個整理器來遮罩15%的標記,這遵循BERT論文中的程式:

from transformers import DataCollatorForLanguageModeling, set_seed

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,
                                              mlm_probability=0.15)

資料整理器是處理批次資料的關鍵元件。在這裡,DataCollatorForLanguageModeling特別用於遮罩語言建模任務,它會隨機遮罩15%的輸入標記,並為這些遮罩標記建立標籤。這種動態遮罩方法比靜態遮罩更有效,因為它允許模型在每個訓練週期中看到不同的遮罩模式。

讓我們快速看一下資料整理器的實際效果,瞭解它實際上做了什麼。為了在DataFrame中快速展示結果,我們將標記器和資料整理器的回傳格式切換為NumPy:

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

輸出結果顯示,對應於感嘆號的標記已被遮罩標記替換。此外,資料整理器回傳了一個標籤陣列,對於原始標記為-100,對於遮罩標記為標記ID。包含-100的條目在計算損失時被忽略。讓我們將資料整理器的格式切換回PyTorch:

data_collator.return_tensors = "pt"

這段程式碼展示了資料整理器如何工作。它將一個簡單句子中的感嘆號替換為[MASK]標記,並建立相應的標籤。標籤陣列中,值為-100的位置表示不參與損失計算(即原始未遮罩的標記),而其他值則是遮罩標記的原始標記ID。這種機制使模型能夠學習預測被遮罩的標記。

開始模型訓練

有了標記器和資料整理器,我們就可以開始微調遮罩語言模型了。我們像往往常一樣設定TrainingArguments和Trainer:

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!")

這段程式碼設定了訓練引數並初始化了Trainer物件。我們使用AutoModelForMaskedLM載入BERT模型,這是專為遮罩語言建模設計的。訓練引數指定了輸出目錄、批次大小、日誌策略等。特別注意的是,我們使用無標籤資料集(ds_mlm["unsup"])進行訓練,而使用標籤資料集的訓練部分(ds_mlm["train"])進行評估。這是因為在遮罩語言建模中,我們不需要標籤,但仍想在有標籤的資料上評估模型效能。

評估訓練效果

我們可以存取trainer的日誌歷史來檢視模型的訓練和驗證損失。所有日誌都儲存在trainer.state.log_history中,作為一個字典列表,我們可以輕鬆載入到Pandas DataFrame中。由於訓練和驗證損失是在不同步驟記錄的,因此DataFrame中有缺失值。為此,我們在繪製指標之前刪除缺失值:

df_log = pd.DataFrame(trainer.state.log_history)
(df_log.dropna(subset=["eval_loss"]).reset_index()["eval_loss"]
 .plot(label="Validation"))
df_log.dropna(subset=["loss"]).reset_index()["loss"].plot(label="Train")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend(loc="upper right")
plt.show()

這段程式碼將訓練日誌轉換為DataFrame,並繪製訓練和驗證損失隨時間的變化。dropna(subset=["eval_loss"])dropna(subset=["loss"])確保我們只使用有相應損失值的行。透過這個圖表,我們可以觀察模型是否隨著訓練而改進,以及是否存在過擬合問題。

從結果來看,訓練和驗證損失都顯著下降。這表明我們的領域適應過程是成功的,模型已經更好地適應了目標領域的文字特性。下一步是檢查這種改進是否能在微調分類別器時也帶來效能提升。

領域適應的實際應用價值

在實際工作中,領域適應是一種極為實用的技術,尤其是在處理特定領域的文字時。例如,當我在處理醫療文字或金融報告等專業領域的NLP任務時,通常會先在相關領域的無標籤資料上進行領域適應,然後再進行特定任務的微調。

這種方法的優勢在於:

  1. 充分利用了大量易於取得的無標籤資料
  2. 使模型更好地理解領域特定的語言和術語
  3. 為多個下游任務提供了更好的基礎模型
  4. 在標籤資料有限的情況下,能顯著提升模型效能

值得注意的是,領域適應的效果通常與原始預訓練領域和目標領域的差異程度成正比。如果差異很大(例如從一般文字到程式碼或專業學術論文),領域適應帶來的改進通常更為顯著。

少量標籤學習的綜合策略

在實際應用中,我通常會結合多種方法來解決少量標籤問題:

  1. 領域適應:首先在無標籤資料上進行領域適應,使模型更好地理解目標領域的語言特性。

  2. 資料增強:透過同義詞替換、回譯等技術擴充有限的標籤資料。

  3. 少樣本學習:使用提示工程和範例學習,讓模型從少量範例中學習任務模式。

  4. 半監督學習:結合標籤和無標籤資料,使用自訓練或一致性正則化等技術。

  5. 主動學習:人工智慧選擇最有價值的資料點進行標註,最大化標註效率。

這種綜合策略能夠在資源有限的情況下,最大程度地提升模型效能。

在機器學習領域,標籤資料的稀缺是一個普遍挑戰,但透過本文討論的方法,我們可以有效地應對這一挑戰。語境內學習和少樣本學習允許模型從有限的範例中學習,而領域適應則能充分利用豐富的無標籤資料。這些技術不僅在學術研究中表現出色,在實際應用中也證明瞭其價值。

隨著大模型語言的發展,這些方法的效果將進一步提升,使我們能夠在更廣泛的場景中佈署高效的NLP解決方案,即使在標籤資料有限的情況下也能取得良好的效果。關鍵是要根據具體問題和可用資源,靈活選擇和組合這些方法,以達到最佳效果。