文章目錄
- 一. 常見微調分類
- 1.1 全量微調(FFT:Full Fine-tuning)
- 1.2 參數高效微調(PEFT:Parameter-Efficient Fine-Tuning)
- 1.3 指令微調(IFT:Instructional Fine-tuning)
- 1.3.1 Hard prompt
- 1.3.2 Soft prompt
- 二. 常見微調方法
- 2.1 Prefix-tuning
- 2.2 Prompt-tuning
- 2.3 P-tuning v1
- 2.4 P-tunning v2
- 2.4 Lora
一. 常見微調分類
1.1 全量微調(FFT:Full Fine-tuning)
??全量微調是對預訓練模型的所有參數進行微調,即預訓練模型的所有層和參數均被會更新和優化,從而適應目標任務的需求。需要注意,與預訓練一樣,全量微調需要足夠的內存和計算來存儲和處理訓練過程中的所有梯度、優化器和其它需要更新的部分。全量微調一般可以獲得更好的模型性能。
??這種微調方法通常適用于任務和預訓練模型之間存在較大差異的情況,或者任務需要模型具有高度靈活性和自適應能力的情況。
1.2 參數高效微調(PEFT:Parameter-Efficient Fine-Tuning)
??為了降低微調時的資源成本,及提升微調效率。研究人員提出了PEFT方法。旨在通過最小化微調參數的數量和計算復雜度,來提高預訓練模型在新任務上的性能。常見PEFT方法可分為以下三類:
-
Freeze method : 凍結模型的一些layers,只更新非凍結layers的模型參數,從而降低微調參數的數量與計算復雜度。
-
Additive method : 向預訓練模型添加新的層或在某些層上拼接。然后微調時只更拼接網絡結構的參數。
-
Reparametrization-based method : 重新參數化方法,些方法的思想是使用低秩表征來最小化可訓練參數的數量。
注: PEFT的Freeze method 可理解為與全量微調對應的部分微調,但Additive method與Reparametrization-based method是否可歸為部分微調,這個主要取決于考慮的模型主體是PLM還是微調后的整體模型,以PLM為基準,這兩個方法就不算是部分微調。為了不把文章寫的復雜,這里沒有列出部分微調這個分類。
1.3 指令微調(IFT:Instructional Fine-tuning)
??FFT與PEFT是在從參數更新的層面上來考慮模型的微調,還是有一種提高模型在各種任務上表現的策略是指令微調。這涉及到使用示例來訓練機器學習模型,展示模型應該如何響應查詢。用于微調大型語言模型的數據集必須符合你的指令目的。使用指令微調時,其模型參數的更新可以是FFT或PEFT。與IFT相關的概念有CoT,in-context learning等, 這篇blog主要把重點放到參數重新上,所以這些概念不做展開。
??例如,如果你想提高模型的摘要能力,你應該構建一個包含摘要指令和相應摘的數據集。在翻譯任務中,應包含“請將下面的文本翻譯成英文”等指令。這些提示有助于讓模型以新的專業方式“思考”,并服務于特定任務。
?? 上面例子中“請將下面的文本翻譯成英文”及類似的“請對下文做出摘要”等提示,我們統稱Hard prompt, 與之對應的是Soft prompt。
1.3.1 Hard prompt
??是一個人類可理解的文本字符串,可由任何語言文字組成,直接拼接在模型原始輸入之前。從模型角度來看,hard prompt 并沒有對PLM做任何的修改,也沒沒有增加新的參數,所以使用hard prompt微調時,本質是更新PLM本身的參數。因此受PLM參數化的限制 。
?? 舉個例子,在情感分類任務中,我們要判斷“《功夫》這個電影是周星弛主演的,無厘頭式的幽默,我很喜歡。”這句話的情感極性,我們怎么讓一個語言模型來判斷這個極性呢?我們可以如下圖這樣在輸入后面接入一個引導的句子“這個電影太[MASK]了”,這個引導的句子里明顯缺少了一個形容詞,我們讓模型把這個容易詞自動補全,我們就可以根據這個形容詞來判斷極性了。
從這里我們可以看出,如果說finetune是為了使用預訓練模型適配下游任務,那prompt就是為了使下游任務適配預訓練模型。這種下游任務適配預訓練模型的做法可大幅減少預訓練模型在下游任務的應用成本,甚至做到不對預訓練模型做修改就能使用。
1.3.2 Soft prompt
??通常是在向量空間優化出來的prompt,直接面向模型,人類無法直接理解。且,soft prompt 通常會在PLM模型外部添加相關的網絡結構做為prompt的參數,因此,soft prompt 的微調不會更新PLM模型的參數。
??舉個例子,如prefix tuning 中,在autoregressive 任務中,在PML所有layer之前拼接一個prefix,在微調時,只更新prefix的參數。
二. 常見微調方法
2.1 Prefix-tuning
論文地址:https://arxiv.org/abs/2101.00190
??prefix-tuning的中文表達是前綴微調,在實現時,prefix-tuning是在PLM前部拼接一小段可學習的向量(virtual tokens)作為Prefix(這里的“前部”是指PLM網絡結構每一層的前部,拼接后,PLM網絡的層數是不變的), 且在微調模型的過程中只優化拼接的Prefix,而不需要優化整個模型的參數(訓練的時候只更新Prefix部分的參數,而PLM中的其他部分參數不更新)。對于不同的任務和模型結構需要不同的Prefix:
- 在autoregressive LM 前添加prefix: z = [ P R E F I X ; x ; y ] z=[PREFIX;x;y] z=[PREFIX;x;y]
- 在encoder和decoder前添加prefix: z = [ P R E F I X ; x ; P R E F I X ′ ; y ] z=[PREFIX;x;{PREFIX}^{'};y] z=[PREFIX;x;PREFIX′;y]
可以參考下圖來理解。
??在原始論文的4.2 節中有表述,直接更新Prefix的參數會導致訓練不穩定和性能下降的情況,所以在Prefix前增加了一層MLP, 此MLP與Prefix同row dimension相同,columns dimension 不同。訓練之后,MLP直接舍棄,只保留Prefix的參數即可。
??在原始論文的 7.2 節中,對prefix的拼接進行了消融實驗,實驗證明,PLM的所有層均拼接prefix能取得更好的結果。因此,我們提到prefix-tuning算法,默認是所有層都增加prefix。
??看到這里,如果沒有基礎的話,可能還不清楚prefix到底是怎么回事兒,這里我畫了一幅圖展開一下。
??從上圖中,我們可以看出,當一個PreTrainModel在使用Prefix算法微調時,其實是在PreTrainModel之外增加了一個PrefixEncoder模塊,這個模塊用來生成Prefix, 而這個Prefix本質就是一個學習后的Embedding。PrefixEncoder會生成多個Prefix (past_key_values_1, past_key_values_2, …), 至于具體生成多少,取決于PreTrainModel的層數,這個層數指的是decoder的層數(以gpt類模型為例),如上圖,這個層數為24。Prefix在傳入decoder后,在attention函數中被拼接到當前hidden_states之前。
PrefixEnocder 源碼(出自huggingface PEFT庫)
class PrefixEncoder(torch.nn.Module):def __init__(self, config):super().__init__()self.prefix_projection = config.prefix_projectiontoken_dim = config.token_dimnum_layers = config.num_layersencoder_hidden_size = config.encoder_hidden_sizenum_virtual_tokens = config.num_virtual_tokensif self.prefix_projection and not config.inference_mode:# Use a two-layer MLP to encode the prefixself.embedding = torch.nn.Embedding(num_virtual_tokens, token_dim)self.transform = torch.nn.Sequential(torch.nn.Linear(token_dim, encoder_hidden_size),torch.nn.Tanh(),torch.nn.Linear(encoder_hidden_size, num_layers * 2 * token_dim),)else:self.embedding = torch.nn.Embedding(num_virtual_tokens, num_layers * 2 * token_dim)def forward(self, prefix: torch.Tensor):if self.prefix_projection:prefix_tokens = self.embedding(prefix)past_key_values = self.transform(prefix_tokens)else:past_key_values = self.embedding(prefix)return past_key_values
注: 上文的prefix其實是屬于soft prompt 也稱為 continuous prompt,與之對應的是 hard prompt 亦稱為 discrete prompt。下面對hard prompt 和 soft prompt 分別做簡單介紹。
2.2 Prompt-tuning
論文地址: https://arxiv.org/abs/2104.08691
?? 該方法可以看作是Prefix Tuning的簡化版本,它給每個任務定義了自己的Prompt,然后拼接到數據上作為輸入,具體的理解可參考下圖。
?? 從上圖我們可以看出,prompt tuning的encoder相比于prefix 的 encoder少了一個兩層的MLP結構,只有一個embedding方法。拼接的方式是直接與原始PreTrainModel的Embedding進行拼接,然后進入到PreTrainModel的decoder stack中。沒有其它操作。相比于prefix的每個encoder都進行拼接,需要訓練的參數規則減少了10倍以上(現在的模型encoder動則幾十層)。
下面是hugging face peft庫中的源碼, 核心代碼就幾行:
class PromptEmbedding(torch.nn.Module):def __init__(self, config, word_embeddings):super().__init__()total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodulesself.embedding = torch.nn.Embedding(total_virtual_tokens, config.token_dim)if config.prompt_tuning_init == PromptTuningInit.TEXT and not config.inference_mode:# 此處為初始化self.enbedding的參數,省略...def forward(self, indices):# Just get embeddingsprompt_embeddings = self.embedding(indices)return prompt_embeddings
-
多任務同時訓練
?? 論文中指出,Prompt tuning 不必像fine tuning那樣,對每個下游任務都使用全量模型做一次微調,有幾個下游任務就保存幾份模型參數,推理時也要使用特定的模型副本對相應的任務做推理。在Prompt tuning中,所有的任務均可以一起進行微調,Prompt tuning 只會為每個任務保存一個prompt。推理時,一個batch中可以有多個下游任務的數據。如下圖所示。
-
性能參數規模逼近全量微調
?? 原始論文的Introduction段落闡述了隨著模型參數規模的增加,Prompt Tuning 的效果會逼近全量微調。具體表現見下圖。
-
prompt 長度的影響
?? 論文的3.2節,闡述了prompt長度的影響,prompt的長度在20左右時的表現已經不錯(超過20之后,繼續增加Prompt token長度,對模型的性能提升不明顯),同樣的,這個gap也會隨著模型參數規模的提升而減小(即對于超大規模模型而言,即使 Prompt token 長度很短,對性能也不會有太大的影響)。具體表現見下圖。
-
Prompt 參數初始化
?? 在論文的3.2節中探討了 Prompt token 的初始化方法和長度對于模型性能的影響。通過消融實驗結果發現,與隨機初始化和使用樣本詞匯表初始化相比,Prompt Tuning采用“class label”初始化模型的效果更好。不過隨著模型參數規模的提升,這種gap最終會消失。具體表現見下圖
-
Prompt Ensembing
?? 論文的第6節,提出了 Prompt Ensembling,也就是在一個Batch中同時訓練同一個任務的不同 prompt(即采用多種不同方式詢問同一個問題),這樣相當于訓練了不同模型,比模型集成的成本小很多。模型規模很大時,有很大的優勢。
?? 原始論文中主要是在T5上做了相關實驗,且最主要的性能如果要接近FFT,參數規模足夠大是前提,包括embedding參數的初始化,也與參數規模有關,因些使用此方法時,要注意所使用的模型的參數規模。甜品級及以下的模型使用就不要使用這個方法了。
2.3 P-tuning v1
論文地址:https://arxiv.org/abs/2103.10385 v2
該方法的核心思想是使用可微的virtual token替換discrete tokens,且僅加入到輸入層,并使用prompt encoder(BiLSTM+MLP)對virtual token進行編碼學習。下圖是論文中的框架示意圖。
-
P-Tuning 算法原理 (出自論文2.2節)
?? 給定 M M M為一個預訓練模型,hidden size為 h h h, 詞表大小為 [ V ] [V] [V], { x i , y i } \{x_i,y_i\} {xi?,yi?} 為已經標注好的一個NLU數據集,其中 x 0 : n = { x 0 , x 1 , . . . , x n } x_{0:n} = \{x_0,x_1,...,x_n\} x0:n?={x0?,x1?,...,xn?}為一個由離散token組成的輸入, y ∈ Y y \in Y y∈Y 為標簽集合。我們的目標是估計 f M ( x ) = p ^ ( y ∣ x ) f_{M(x)}=\hat{p}(y|x) fM(x)?=p^?(y∣x)的分類條件概率,其中 M M M的參數經過微調或凍結。
?? Prompt最初是以離散token的形式提出的(Schick and Schütze,2020)。設 [ D i ] [D_i] [Di?]為離散porompt的token。每個prompt都可以描述為一個模板 T = { [ D 0 : i ] , x , [ D ( i + 1 ) : j ] , y , [ D ( j + 1 ) : k ] } T =\{[D_{0:i}],x,[D_{(i+1):j}],y,[D_{(j+1):k}]\} T={[D0:i?],x,[D(i+1):j?],y,[D(j+1):k?]},它可以將token數據(包括輸入 x x x 和標簽 y y y)組織成一個文本token序列,這樣就可以將任務重新表述為 對輸入文本的空白地方進行填空。例如,對于預測一個國家首都的任務(LAMA-TREx P36),prompt可以是“[INPUT] 的首都是 [LABEL]”。對于一段標注數據“(英國,倫敦)”,重新表述的文本將是“英國的首都是 [MASK]。”,其中“[MASK]”應該預測為給定的標簽“倫敦”。離散prompt和離散數據一起映射到輸入嵌入中:
{ e ( D 0 ) . . . e ( D i ) , e ( x 0 ) , . . . , e ( x n ) , . . . , e ( D k ) } \{e(D_0)...e(D_i), e(x_0), ..., e(x_n), ..., e(D_k)\} {e(D0?)...e(Di?),e(x0?),...,e(xn?),...,e(Dk?)}
根據預訓練的嵌入層, e ∈ R ∣ V ∣ × d e \in R|V |×d e∈R∣V∣×d 。
?? 這種離散提示往往極不穩定,可能不是反向傳播的最佳選擇。因此,我們提出了 P-Tuning,它使用連續prompt嵌入來改進和穩定prompt。令 [ P i ] [P_i] [Pi?] 為第 i i i 個連續提示嵌入。P-Tuning 的提示模板如下:
T = { [ P 0 : i ] , x , [ P ( i + 1 ) : j ] , y , [ P ( j + 1 ) : k ] } T = \{[P_{0:i}],x,[P_{(i+1):j}],y,[P_{(j+1):k}]\} T={[P0:i?],x,[P(i+1):j?],y,[P(j+1):k?]}
P-Tuning 利用額外的嵌入函數 f : [ P i ] → h i f : [P_i] → h_i f:[Pi?]→hi?將模板映射到
{ h 0 , . . . , h i , e ( x ) , h i + 1 , . . . , h j , e ( y ) , h j + 1 , . . . , h k } \{h_0,...,h_i,e(x),h_{i+1},...,h_j,e(y),h_{j+1},...,h_k\} {h0?,...,hi?,e(x),hi+1?,...,hj?,e(y),hj+1?,...,hk?}最后,我們更新embeddings { P i } i = 1 k \{P_i\}^k_{i=1} {Pi?}i=1k? 以優化任務損失函數(在p-tunning中,embedding 會被更新)。值得注意的是,我們還可以將離散prompt與連續的prompt連接起來( 如下圖中input embedding 即包含 e ( x ) e(x) e(x)也包含 h i h_i hi? ),這種方法效果更好,并在我們的整個實驗中都采用了這種方法。P-Tuning 適用于凍結和微調語言模型。
關于上面幾個公式可以參考下圖來理解:
-
Prompt Encoder(出自論文2.3節)
??在上述框架中,我們使用映射函數 f f f 將可訓練嵌入 { P i } \{P_i\} {Pi?} 映射到模型輸入 { h i } \{h_i\} {hi?}。直覺是,與使用獨立的可學習嵌入相比,使用映射函數可以更方便地對不同提示嵌入之間的依賴關系進行建模。在我們的實現中,我們使用輕量級神經網絡來制定函數 f f f。具體來說,我們嘗試使用長短期記憶 (LSTM) 網絡、多層感知器 (MLP) 和第 3 節中的恒等映射函數。??這一段落關于Prompt Encoder的說明,我感覺略顯籠統,通過這段話我并沒有看出PromptEncoder到底是怎么實現的,于是乎我又翻閱了GPT understands, too 的v1版本,在v1版本里對PromptEncoder有較詳細的描述,且我又在清華的官網查了p-tuning的實現代碼,在當前時間點(2024/06/20),清華的官方實現的代碼與v1版本中的描述是相同的。所以下面按v1版本中的描述補充一下理解。
Prompt Encoder 由一個使用ReLU做為激活函數的雙層MLP及BiLSTM組成,對于任意時刻的 h i h_i hi?有:
h i = M L P ( [ h i → : h i ← ] = M L P ( [ L S T M ( h 0 : i ) : L S T M ( h i : m ) ] ) \begin{aligned} h_i &= MLP([ \mathop{h_i} \limits ^{\rightarrow}: \mathop{h_i} \limits ^{\leftarrow}] \\ &= MLP([LSTM(h_{0:i}):LSTM(h_{i:m})]) \end{aligned} hi??=MLP([hi?→?:hi?←?]=MLP([LSTM(h0:i?):LSTM(hi:m?)])?下面貼上清華官網的P-tuning PromptEncoder代碼:
import torch
import torch.nn as nnclass PromptEncoder(torch.nn.Module):def __init__(self, template, hidden_size, tokenizer, device, args):super().__init__()self.device = deviceself.spell_length = sum(template)self.hidden_size = hidden_sizeself.tokenizer = tokenizerself.args = args# ent embeddingself.cloze_length = templateself.cloze_mask = [[1] * self.cloze_length[0] # first cloze+ [1] * self.cloze_length[1] # second cloze+ [1] * self.cloze_length[2] # third cloze]self.cloze_mask = torch.LongTensor(self.cloze_mask).bool().to(self.device)self.seq_indices = torch.LongTensor(list(range(len(self.cloze_mask[0])))).to(self.device)# embeddingself.embedding = torch.nn.Embedding(len(self.cloze_mask[0]), self.hidden_size).to(self.device)# LSTMself.lstm_head = torch.nn.LSTM(input_size=self.hidden_size,hidden_size=self.hidden_size // 2,num_layers=2,dropout=self.args.lstm_dropout,bidirectional=True,batch_first=True)self.mlp_head = nn.Sequential(nn.Linear(self.hidden_size, self.hidden_size),nn.ReLU(),nn.Linear(self.hidden_size, self.hidden_size))print("init prompt encoder...")def forward(self):input_embeds = self.embedding(self.seq_indices).unsqueeze(0)output_embeds = self.mlp_head(self.lstm_head(input_embeds)[0]).squeeze()return output_embeds
- 結論
相同參數規模下,如果進行全參數微調,Bert在NLU任務上的效果,超過GPT很多;但是在P-Tuning下,GPT可以取得超越Bert的效果。如下圖。
2.4 P-tunning v2
原始論文:P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks
論文地址:https://arxiv.org/abs/2110.07602
之前的Prompt Tuning和P-Tuning v1 等方法存在兩個主要的問題:
第一, 缺乏模型參數規模和任務通用性。
- 缺乏規模通用性:Prompt Tuning論文中表明當模型規模超過100億個參數時,提示優化可以與全量微調相媲美。但是對于那些較小的模型(從100M到1B),提示優化和全量微調的表現有很大差異,這大大限制了提示優化的適用性。
- 缺乏任務普遍性:盡管Prompt Tuning和P-tuning在一些 NLU 基準測試中表現出優勢,但提示調優對序列標記任務(即序列標注)的有效性尚未得到驗證。
第二,缺少深度提示優化,在Prompt Tuning和P-tuning中,連續提示只被插入transformer第一層的輸入embedding序列中,在接下來的transformer層中,插入連續提示的位置的embedding是由之前的transformer層計算出來的,這可能導致兩個可能的優化挑戰。
- 由于序列長度的限制,可調參數的數量是有限的。
- 輸入embedding對模型預測只有相對間接的影響。
這些問題在P-tuning v2得到了改進。
P-Tuning v2主要是基于P-tuning和prefix-tuning技術,引入Deep Prompt Encoding和Multi-task Learning等策略進行優化的。
如上圖(b)所示,不同層的提示被作為前綴token添加。P-Tuning v2的這種做法帶來了兩個顯著優點:
- 更多的可調任務特定參數:P-Tuning v2擁有更多的可調任務特定參數(從0.01%到0.1%-3%),這不僅增加了每個任務的處理能力,而且在參數效率上更優。
- 深層提示對模型預測的直接影響:添加到更深層的提示對模型預測有更直接的影響。
為了達到最佳性能,P-Tuning v2在優化和實現方面有一些有用的細節:(原始論文3.3節)
重參數化(Reparameterization):先前的研究通常使用多層感知機(MLP)這樣的重參數化編碼器來轉換可訓練向量,但對于NLU任務來說MLP的有效性取決于任務和數據集。對于某些數據集(如RTE和CoNLL04),MLP帶來了明顯的改進;但對于其他數據集(例如,BoolQ和CoNLL12),MLP對結果的影響微小甚至負面。
提示詞長度(Prompt Length): 提示詞長度在P-Tuning v2中扮演著關鍵角色。研究發現,通常簡單的分類任務更傾向于較短的Prompt(少于20個);而復雜的序列標注任務則更適合較長的Prompt(大約100個)。不同的NLU任務通常在不同的提示詞長度下達到最佳性能。
多任務學習(Multi-task Learning): 多任務學習通過共享連續提示,在針對個別任務進行微調之前,聯合優化多個任務。多任務學習對P-Tuning v2來說不是必選項,但可以通過提供更好的初始化來進一步提升性能(Gu等人,2021)。
分類頭(Classification Head): 使用語言建模頭(language modeling head)來預測類別化標簽(Verbalizers)一直是P-Tuning的核心操作,但在完整數據設置下這不是必需的,且與序列標注不兼容。P-Tuning v2改為在BERT中的token之上應用一個隨機初始化的分類頭(見上圖b)。
2.4 Lora
todo ..