天天看點

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

作者:零度AI

by April 12, 2023 by Sebastian Raschka

本文介紹了流行的大型語言模型(LLM)參數高效微調方法:字首調優,擴充卡,以及LLaMA-擴充卡。

在快速發展的人工智能領域,有效、高效地利用大型語言模型越來越重要。

參數高效微調處于這個追求的前沿,它使研究者和實踐者能夠重複使用預訓練模型,同時最小化它們的計算和資源占用。這也使我們能夠在更廣泛的硬體上訓練AI模型,包括計算能力有限的裝置,如筆記本電腦,智能手機和物聯網裝置。最後,随着對環境可持續性的關注度不斷提高,參數高效微調減少了訓練大規模AI模型所需的能源消耗和碳排放。

本文解釋了微調的廣義概念,并讨論了像字首調優和擴充卡這樣的流行參數高效替代方案。最後,我們将研究最近的LLaMA-擴充卡方法,并看看我們如何在實踐中使用它。

微調大型語言模型

自從GPT-2和GPT-3以來,我們已經看到,預訓練于通用文本語料庫的生成性大型語言模型(LLM)能夠進行上下文學習,這意味着如果我們想讓預訓練的LLM執行特定的或新的任務,這些任務并非LLM明确訓練過,我們并不需要進一步訓練或微調預訓練的LLM。相反,我們可以直接通過輸入提示提供目标任務的幾個示例,如下面的示例所示。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

在上下文學習對于直接通路大型語言模型(LLM)受限的情況下是一種有價值且使用者友好的方法,例如通過API或使用者界面與LLM進行互動。 然而,如果我們能夠通路LLM,通常使用來自目标領域的資料對其進行适應和微調會得到更好的結果。那麼,我們如何将模型适應到目标任務呢?下圖概述了三種正常方法。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

上述方法都與生成式(解碼器風格)模型如GPT以及以嵌入為重點的(編碼器風格)模型如BERT相容。與這三種方法相對的是,上下文學習隻适用于生成模型。值得強調的是,當我們微調生成模型時,我們處理并建構它們建立的嵌入,而不是生成的輸出文本。

特征基礎方法

在基于特征的方法中,我們加載一個預訓練的LLM并将其應用于我們的目标資料集。在這裡,我們特别感興趣的是為訓練集生成輸出嵌入,我們可以将其作為輸入特征來訓練分類模型。雖然這種方法特别适用于像BERT這樣以嵌入為重點的模型,但我們也可以從生成式的GPT風格模型中提取嵌入(在我們的部落格文章中可以找到一個例子)。

然後,分類模型可以是邏輯回歸模型,随機森林,或者XGBoost —— 任何我們喜歡的。 (然而,根據我的經驗,像邏輯回歸這樣的線性分類器在這裡表現最好。)

從概念上,我們可以用以下代碼來說明基于特征的方法:

model = AutoModel.from_pretrained("distilbert-base-uncased")

# ...
# tokenize dataset
# ...

# generate embeddings
@torch.inference_mode()
def get_output_embeddings(batch):
    output = model(
        batch["input_ids"],
        attention_mask=batch["attention_mask"]
    ).last_hidden_state[:, 0]
return {"features": output}

dataset_features = dataset_tokenized.map(
  get_output_embeddings, batched=True, batch_size=10)

X_train = np.array(imdb_features["train"]["features"])
y_train = np.array(imdb_features["train"]["label"])

X_val = np.array(imdb_features["validation"]["features"])
y_val = np.array(imdb_features["validation"]["label"])

X_test = np.array(imdb_features["test"]["features"])
y_test = np.array(imdb_features["test"]["label"])

# train classifier
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression()
clf.fit(X_train, y_train)

print("Training accuracy", clf.score(X_train, y_train))
print("Validation accuracy", clf.score(X_val, y_val))
print("test accuracy", clf.score(X_test, y_test)           

微調I —— 更新輸出層

一個與上述特征基礎方法相關的流行方法是微調輸出層(我們将這種方法稱為微調I)。與特征基礎方法相似,我們保持預訓練LLM的參數不變。我們隻訓練新添加的輸出層,這類似于在嵌入特征上訓練邏輯回歸分類器或小型多層感覺機。

在代碼中,這将如下所示:

model = AutoModelForSequenceClassification.from_pretrained(
    "distilbert-base-uncased",
     num_labels=2  # suppose target task is a binary classification task
) 

# freeze all layers
for param in model.parameters():
    param.requires_grad = False

# then unfreeze the two last layers (output layers)
for param in model.pre_classifier.parameters():
    param.requires_grad = True

for param in model.classifier.parameters():
    param.requires_grad = True

# finetune model
lightning_model = CustomLightningModule(model)

trainer = L.Trainer(
    max_epochs=3,
    ...
)

trainer.fit(
  model=lightning_model,
  train_dataloaders=train_loader,
  val_dataloaders=val_loader)

# evaluate model
trainer.test(lightning_model, dataloaders=test_loader)           

從理論上講,由于我們使用相同的當機背景模型,這種方法在模組化性能和速度方面應該與基于特征的方法表現相當。然而,由于基于特征的方法使預計算和存儲訓練資料集的嵌入特征稍微容易一些,是以在特定的實際場景中,基于特征的方法可能更為友善。

微調II —— 更新所有層

盡管最初的BERT論文(Devlin等人)報告說,隻微調輸出層可以得到與微調所有層相當的模組化性能,但微調所有層顯著更昂貴,因為涉及更多的參數。例如,一個BERT基礎模型大約有110百萬個參數。然而,一個用于二分類的BERT基礎模型的最後一層隻有大約1500個參數。此外,BERT基礎模型的最後兩層占據了60,000個參數 —— 這隻占總模型大小的約0.6%。

我們的結果将根據我們的目标任務和目标領域與模型預訓練的資料集的相似程度而變化。但在實踐中,微調所有層幾乎總是能得到更好的模組化性能。

是以,當優化模組化性能時,使用預訓練LLM的黃金标準是更新所有層(這裡稱為微調II)。從概念上看,微調II與微調I非常相似。唯一的差別是我們不當機預訓練LLM的參數,而是同時微調它們:

model = AutoModelForSequenceClassification.from_pretrained(
    "distilbert-base-uncased",
     num_labels=2  # suppose target task is a binary classification task
) 

# don't freeze layers
# for param in model.parameters():
#    param.requires_grad = False

# finetune model
lightning_model = LightningModel(model)

trainer = L.Trainer(
    max_epochs=3,
    ...
)

trainer.fit(
  model=lightning_model,
  train_dataloaders=train_loader,
  val_dataloaders=val_loader)

# evaluate model
trainer.test(lightning_model, dataloaders=test_loader)
           

如果你對一些實際結果感到好奇,上面的代碼片段被用來使用預訓練的DistilBERT基礎模型訓練電影評論分類器: 基于特征的方法和邏輯回歸:83%的測試精度 微調I,更新最後2層:87%的精度 微調II,更新所有層:92%的精度。 這些結果與通常的經驗法則一緻,即微調更多層通常會得到更好的性能,但代價也會增加。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

參數高效微調

在前面的章節中,我們了解到微調更多層通常會帶來更好的結果。現在,上述實驗是基于DistilBERT模型的,這是一個相對較小的模型。那麼,如果我們想微調隻能勉強适應GPU記憶體的更大模型,例如,最新的生成式LLM呢?我們當然可以使用上述的基于特征的或微調I的方法。但假設我們想獲得類似于微調II的模組化品質呢?

多年來,研究人員開發了幾種技術\來微調LLM,以在隻需訓練少量參數的同時獲得高模組化性能。這些方法通常被稱為參數高效微調技術(PEFT)。

下圖總結了一些最廣泛使用的PEFT技術。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

最近引起轟動的一種PEFT技術是LLaMA-Adapter,這是為Meta的流行的LLaMA模型提出的,然而,雖然LLaMA-Adapter是在LLaMA的背景下提出的,但這個想法是不針對特定模型的。

為了了解LLaMA-Adapter是如何工作的,我們必須回顧一下兩種相關的技術,稱為字首調整和擴充卡 —— LLaMA-Adapter結合并擴充了這兩種想法。

是以,在本文的剩餘部分,我們将讨論各種提示修改的概念,以了解字首調整和擴充卡方法,然後我們将更仔細地看一下LLaMA-Adapter。

提示調整和字首調整

原始的提示調整概念是指改變輸入提示以達到更好模組化結果的技術。例如,假設我們有興趣将英語句子翻譯成德語。我們可以以各種不同的方式向模型詢問,如下圖所示。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

現在,上圖所示的概念被稱為硬提示調整,因為我們直接改變的是不可微的離散輸入令牌。

與硬提示調整相反,軟提示調整将輸入令牌的嵌入與可通過反向傳播優化以改善目标任務模組化性能的可訓練張量連接配接起來。

提示調整的一種特定形式是字首調整。字首調整的想法是将一個可訓練的張量添加到每個變壓器塊,而不僅僅是輸入嵌入,如在軟提示調整中。下圖說明了正常變壓器塊和帶字首的變壓器塊之間的差別。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

請注意,上圖中的"全連接配接層"是指一個小的多層感覺器(兩個全連接配接層之間有一個非線性激活函數)。這些全連接配接層将軟提示嵌入到與變壓器塊輸入相同次元的特征空間中,以確定連接配接的相容性。 使用(Python)僞代碼,我們可以說明正常變壓器塊和帶字首的變壓器塊之間的差別:

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

根據原始字首調整論文,字首調整在僅需訓練0.1%的參數的情況下,達到了與微調所有層相當的模組化性能——實驗基于GPT-2模型。此外,在許多情況下,字首調整甚至超過了所有層的微調,這可能是因為涉及的參數較少,有助于減少在較小目标資料集上的過拟合。

最後,為了闡明在推理過程中使用軟提示:在學習一個軟提示後,我們必須在執行我們微調模型的特定任務時,将其作為字首提供。這使得模型可以根據特定任務定制其響應。此外,我們可以有多個軟提示,每個提示對應一個不同的任務,并在推理過程中提供适當的字首,以實作特定任務的最佳結果。

擴充卡

原始的擴充卡方法與前述的字首調整有些相關,因為它們也在每個變壓器塊中添加了額外的參數。然而,擴充卡方法并沒有在輸入嵌入之前添加字首,而是在兩個地方添加了擴充卡層,如下圖所示。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

對于更喜歡(Python)僞代碼的讀者,擴充卡層可以寫成如下形式:

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

請注意,擴充卡的全連接配接層通常相對較小,并且有類似于自動編碼器的瓶頸結構。每個擴充卡塊的第一個全連接配接層将輸入投影到低維表示上。第二個全連接配接層将輸入投影回輸入次元。這怎麼可能是參數有效的呢?例如,假設第一個全連接配接層将1024維的輸入投影到24維,第二個全連接配接層将其投影回1024維。這意味着我們引入了1,024 x 24 + 24 x 1,024 = 49,152個權重參數。相比之下,将1024維的輸入重新投影到1024維空間的單個全連接配接層将有1,024 x 1024 = 1,048,576個參數。

根據原始擴充卡論文,使用擴充卡方法訓練的BERT模型達到了與完全微調的BERT模型相當的模組化性能,而隻需要訓練3.6%的參數。

現在,問題是擴充卡方法與字首調整的比較如何。基于原始字首調整論文,當調整模型參數總數的0.1%時,擴充卡方法的表現稍遜于字首調整方法。然而,當使用擴充卡方法調整模型參數的3%時,該方法與調整模型參數0.1%的字首調整相持平。是以,我們可能會得出結論,字首調整方法是這兩種方法中更有效的一種。

擴充字首調整和擴充卡:LLaMA-Adapter

擴充了字首調整和原始擴充卡方法的思想,研究人員最近提出了LLaMA-Adapter,這是一個用于LLaMA(LLaMA是Meta的流行GPT替代品)的參數有效的微調方法。

像字首調整一樣,LLaMA-Adapter方法在嵌入輸入之前添加了可調的提示張量。值得注意的是,在LLaMA-Adapter方法中,字首是在嵌入表中學習和維護的,而不是外部提供的。模型中的每個變壓器塊都有自己獨特的學習字首,允許在不同的模型層進行更定制化的适應。

此外,LLaMA-Adapter引入了一個零初始化的注意機制,與之配合的是門控機制。這種所謂的零初始化注意力和門控的動機是,擴充卡和字首調整可能會通過引入随機初始化的張量(字首提示或擴充卡層)打亂預訓練LLM的語言知識,導緻微調不穩定和初期訓練階段的損失值較高。

與字首調整和原始擴充卡方法相比,另一個差別是LLaMA-Adapter隻向最頂層的變壓器層添加可學習的适應提示,而不是所有的變壓器層。作者們認為,這種方法可以更有效地調整聚焦于更進階語義資訊的語言表示。

雖然LLaMA擴充卡方法的基本思想與字首調整(前置可調軟提示)有關,但在實作上有一些額外的、微妙的差異。例如,隻有自注意輸入的鍵和值序列通過可調的軟提示進行了修改。然後,根據門控因子(在訓練開始時設為零),是否使用字首修改的注意力。這個概念在下面的可視化中進行了說明。

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

在僞代碼中,我們可以這樣表達:

【效率】大模型高效微調:從字首調優到LLaMA-擴充卡

簡而言之,LLaMA-Adapter與正常的字首調整的差別在于,LLaMA-Adapter隻修改頂部(即,前幾個)變壓器塊,并引入了一個穩定訓練的門控機制。雖然研究人員特别用LLaMA進行了實驗,但他們提出的Adapter方法是一個通用的方法,也可以應用于其他類型的LLM(如GPT)。

使用LLaMA-Adapter方法,研究人員能夠在僅1小時内(使用八個A100 GPU)在由52k指令對組成的資料集上微調一個70億參數的LLaMA模型。此外,微調後的LLaMA-Adapter模型在問答任務上超過了本研究中比較的所有其他模型,而隻需要微調1.2 M參數(擴充卡層)。

如果你想檢視LLaMA-Adapter方法,你可以在這裡找到基于GPL許可的LLaMA代碼的原始實作。

另外,如果你的使用場景與GPL許可不相容,GPL許可要求你将所有衍生作品在類似的許可下開源,可以檢視Lit-LLaMA GitHub倉庫。Lit-LLaMA是在Apache許可的nanoGPT代碼上的LLaMA的可讀實作實,這個許可證有更少的限制性條款。

具體來說,如果你對使用LLaMA-Adapter方法微調LLaMA模型感興趣,你可以從Lit-LLaMA GitHub倉庫運作以下腳本:

python finetune_adapter.py           

(倉庫位址見文末)

總結

微調預訓練的大型語言模型(LLMs)是一種有效的方法,可以定制這些模型以滿足特定的業務需求,并使它們與目标領域的資料保持一緻。這個過程涉及到使用一個與所需領域相關的較小資料集來調整模型參數,使模型能夠學習領域特定的知識和詞彙。

然而,由于LLMs是“大型”的,更新Transformer模型中的多個層次可能非常昂貴,是以研究人員開始開發參數高效的替代方案。

在這篇文章中,我們讨論了幾種正常LLM微調機制的參數高效替代方案。特别是,我們讨論了通過字首調整添加可調整的軟提示和插入額外的擴充卡層。

最後,我們讨論了最近非常流行的LLaMA-Adapter方法,該方法通過字首調整添加可調整的軟提示,并引入額外的門控機制來穩定訓練。

如果你想在實踐中試試這個,可以檢視:https://github.com/Lightning-AI/lit-llama