如何花費較少的算力成本來進行微調訓練,十分重要,當前關于LLaMA、Alpaca、Instruct微調、LoRa微調等多個概念大家講的很多,最近也在學習,也看到幾個有趣的話題。
首先,來看關于Instruct微調和LoRa微調
Instruct微調和LoRa微調是兩種不同的技術。Instruct微調是指在深度神經網絡訓練過程中調整模型參數的過程,以優化模型的性能。在微調過程中,使用一個預先訓練好的模型作為基礎模型,然后在新的數據集上對該模型進行微調。Instruct微調是一種通過更新預訓練模型的所有參數來完成的微調方法,通過微調使其適用于多個下游應用。
LoRa微調則是指對低功耗廣域網(LoRaWAN)中的LoRa節點參數進行微調的過程,以提高節點的傳輸效率。在LoRa微調中,需要了解節點的硬件和網絡部署情況,并通過對節點參數進行微小調整來優化傳輸效率。
與Instruct微調相比,LoRA在每個Transformer塊中注入可訓練層,因為不需要為大多數模型權重計算梯度,大大減少了需要訓練參數的數量并且降低了GPU內存的要求。研究發現,使用LoRA進行的微調質量與全模型微調相當,速度更快并且需要更少的計算。因此,如果有低延遲和低內存需求的情況,建議使用LoRA微調。
其次,我們再來看看為什么會有LLaMA模型和LoRA兩種模型
如上所述,模型的微調方式有很多種,基于LoRA的微調產生保存了新的權重,可以將生成的LoRA權重認為是一個原來LLaMA模型的補丁權重 。至于LLaMA 權重,它則是由Mean公司開源的大模型預訓練權重。
最后,我們來看看關于詞表擴充,為什么要擴充詞表,直接在原版LLaMA上用中文預訓練不行?
本身LLaMA對中文支持不是很好,大多數相關衍生工作是直接在原版上進行pretrain/finetune的,從而采取了更大膽的策略——增加中文詞表,可能進一步加劇中文訓練不充分的問題,但從長遠看是否有利于后續進一步預訓練就得靠時間檢驗了,加入詞表是有一定破壞性的,一是破壞原有分詞體系,二是增加了未訓練的權重。所以如果不能進行充分訓練的話,可能會有比較大的問題。如果不是特別專的領域(比如生物醫學等涉及很多專業詞匯的領域)沒有太大必要去擴充英文詞表。
原版LLaMA模型的詞表大小是32K,其主要針對英語進行訓練(具體詳見LLaMA論文),對多語種支持不是特別理想(可以對比一下多語言經典模型XLM-R的詞表大小為250K)。通過初步統計發現,LLaMA詞表中僅包含很少的中文字符,所以在切詞時會把中文切地更碎,需要多個byte token才能拼成一個完整的漢字,進而導致信息密度降低。
比如,在擴展詞表后的模型中,單個漢字傾向于被切成1個token,而在原版LLaMA中可能就需要2-3個才能組合成一個漢字,顯著降低編解碼的效率。
由于原版LLaMA對中文的支持非常有限,Chinese-LLaMA-Alpaca項目在原版LLaMA的基礎上進一步擴充了中文詞表。在通用中文語料上訓練了基于sentencepiece的20K中文詞表并與原版LLaMA模型的32K詞表進行合并,排除重復的token后,得到的最終中文LLaMA詞表大小為49953。需要注意的是,在fine-tune階段Alpaca比LLaMA多一個pad token,所以中文Alpaca的詞表大小為49954。
為了進一步加深對lora的理解,本文主要從LoRA基本原理及PEFT中的實現、基于mt0-large+lora的完整實踐兩方面進行介紹,供大家一起參考。
一、LoRA基本原理及PEFT中的實現
當前,已經出現了很多lora作為adapter的微調模型,如Alpaca LoRA,Chinese-LLaMA-Alpaca等,其在公開時會注明:中文LLaMA/Alpaca LoRA模型無法單獨使用,需要搭配原版LLaMA模型,發布的是LoRA權重,可以理解為原LLaMA模型上的一個“補丁”,兩者進行合并即可獲得完整版權重。
LoRA的實現原理在于,凍結預訓練模型權重,并將可訓練的秩分解矩陣注入到Transformer層的每個權重中,大大減少了下游任務的可訓練參數數量。直白的來說,實際上是增加了右側的“旁支”,也就是先用一個Linear層A,將數據從 d維降到r,再用第二個Linear層B,將數據從r變回d維。最后再將左右兩部分的結果相加融合,得到輸出的hidden_state。
如上圖所示,左邊是預訓練模型的權重,輸入輸出維度都是d,在訓練期間被凍結,不接受梯度更新。右邊部分對A使用隨機的高斯初始化,B在訓練開始時為零,r是秩,會對△Wx做縮放 α/r。
幸運的是,HuggingFace的PEFT(Parameter-Efficient Fine-Tuning,地址:https://github.com/huggingface/peft)中提供了模型微調加速的方法,參數高效微調(PEFT)方法能夠使預先訓練好的語言模型(PLMs)有效地適應各種下游應用,而不需要對模型的所有參數進行微調。
對大規模的PLM進行微調往往成本過高,在這方面,PEFT方法只對少數(額外的)模型參數進行微調,基本思想在于僅微調少量 (額外) 模型參數,同時凍結預訓練 LLM 的大部分參數,從而大大降低了計算和存儲成本,這也克服了災難性遺忘的問題,這是在 LLM 的全參數微調期間觀察到的一種現象PEFT 方法也顯示出在低數據狀態下比微調更好,可以更好地泛化到域外場景。
例如,使用PEFT-lora進行加速微調的效果如下,從中我們可以看到該方案的優勢:
例如,其對LoRA做了封裝支持,幾步即可使用:
from?peft?import?get_peft_model,?LoraConfig,?TaskType peft_config?=?LoraConfig( ????task_type=TaskType.CAUSAL_LM,? ????inference_mode=False,? ????r=8,? ????lora_alpha=32,? ????lora_dropout=0.1, ????target_modules=['query_key_value'] ) model?=?"加載的模型" model?=?get_peft_model(model,?peft_config) model.print_trainable_parameters()
論文中提到了LoRA的一些優勢:
1)一個預先訓練好的模型可以被共享,并用于為不同的任務建立許多小的LoRA模塊。可以凍結共享模型,并通過替換圖中的矩陣A和B來有效地切換任務,大大降低了存儲需求和任務切換的難度。
2)在使用自適應優化器時,LoRA使訓練更加有效,并將硬件進入門檻降低了3倍,因為我們不需要計算梯度或維護大多數參數的優化器狀態。相反,我們只優化注入的、小得多的低秩矩陣。
3)簡單的線性設計允許在部署時將可訓練矩陣與凍結權重合并,與完全微調的模型相比,在結構上沒有引入推理延遲。
4)LoRA與許多先前的方法是正交的,可以與許多方法結合,如前綴調整。我們在附錄E中提供了一個例子。
1、引入開源組件
”+”表示增加代碼:
??from?transformers?import?AutoModelForSeq2SeqLM +?from?peft?import?get_peft_model,?LoraConfig,?TaskType? ??model_name_or_path?=?"bigscience/mt0-large" ??tokenizer_name_or_path?=?"bigscience/mt0-large"
2、引入lora配置信息
peft_config?=?LoraConfig( ????task_type=TaskType.SEQ_2_SEQ_LM,? ????inference_mode=False,? ????r=8,? ????lora_alpha=32,? ????lora_dropout=0.1 )
3、進行推理
??from?transformers?import?AutoModelForSeq2SeqLM +?from?peft?import?PeftModel,?PeftConfig ??peft_model_id?=?"smangrul/twitter_complaints_bigscience_T0_3B_LORA_SEQ_2_SEQ_LM" ??config?=?PeftConfig.from_pretrained(peft_model_id) ??model?=?AutoModelForSeq2SeqLM.from_pretrained(config.base_model_name_or_path) +?model?=?PeftModel.from_pretrained(model,?peft_model_id) ??tokenizer?=?AutoTokenizer.from_pretrained(config.base_model_name_or_path) ??model?=?model.to(device) ??model.eval() ??inputs?=?tokenizer("Tweet?text?:?@HondaCustSvc?Your?customer?service?has?been?horrible?during?the?recall?process.?I?will?never?purchase?a?Honda?again.?Label?:",?return_tensors="pt") ??with?torch.no_grad(): ??????outputs?=?model.generate(input_ids=inputs["input_ids"].to("cuda"),?max_new_tokens=10) ??????print(tokenizer.batch_decode(outputs.detach().cpu().numpy(),?skip_special_tokens=True)[0]) #?'complaint'
二、基于mt0-large+lora的完整實踐
接下來,我們來使用huggingface-peft庫來進行一個lora的實踐。
首先,在模型方面,我們選用mt0-large模型為例(只有1.2b),進行實驗,模型地址:https://huggingface.co/bigscience/mt0-large。
模型權重地址:https://huggingface.co/bigscience/mt0-large/tree/main
先看看mt0-large是什么。多任務提示微調(MTF)已被證明可以幫助大型語言模型在zero-shot的環境下生成新的任務,但到目前為止,MTF的探索主要集中在英語數據和模型上,將MTF應用于預訓練的多語言BLOOM和mT5模型系列,就產生稱為BLOOMZ和mT0的微調變體。
具體的,總共生產了三種不同尺寸的核心型號:
BLOOMZ-P3 / mT0-P3:在純英語的P3上進行微調的模型。
BLOOMZ / mT0: 在xP3上進行微調的模型,xP3由帶有英語提示的多語言數據集組成。
BLOOMZ-MT / mT0-MT: 在xP3mt上進行模型微調,xP3mt由多語言數據集和機器翻譯的提示語組成。
其次,在任務方面,我們選用金融領域情感分析任務financial_sentiment_analysis,給定一個句子,要求識別出該句子是negative、positive還是neutral三個中的哪一個,其中的數據樣式如下:
{'sentence':?"The?10,000-odd?square?metre?plot?that?Stockmann?has?bought?for? the?Nevsky?Center?shopping?center?is?located?on?Nevsky?Prospect?,? St?Petersburg?'s?high?street?,?next?to?the?Vosstaniya?Square?underground ?station?,?in?the?immediate?vicinity?of?Moscow?Station?.", ?'label':?1, ?'text_label':?'neutral'}
我們可以通過datasests組件進行調用。
1、引入組件并設置參數
from?transformers?import?AutoModelForSeq2SeqLM from?peft?import?get_peft_config,?get_peft_model,?get_peft_model_state_dict,?LoraConfig,?TaskType import?torch from?datasets?import?load_dataset import?os os.environ["TOKENIZERS_PARALLELISM"]?=?"false" from?transformers?import?AutoTokenizer from?torch.utils.data?import?DataLoader from?transformers?import?default_data_collator,?get_linear_schedule_with_warmup from?tqdm?import?tqdm from?datasets?import?load_dataset device?=?"cuda" model_name_or_path?=?"bigscience/mt0-large" tokenizer_name_or_path?=?"bigscience/mt0-large" checkpoint_name?=?"financial_sentiment_analysis_lora_v1.pt" text_column?=?"sentence" label_column?=?"text_label" max_length?=?128 lr?=?1e-3 num_epochs?=?3 batch_size?=?8
2、搭建模型
peft_config?=?LoraConfig(task_type=TaskType.SEQ_2_SEQ_LM,?inference_mode=False,?r=8,?lora_alpha=32,?lora_dropout=0.1) model?=?AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path) model?=?get_peft_model(model,?peft_config) model.print_trainable_parameters()
3、加載數據
dataset?=?load_dataset("financial_phrasebank",?"sentences_allagree") dataset?=?dataset["train"].train_test_split(test_size=0.1) dataset["validation"]?=?dataset["test"] del?dataset["test"] classes?=?dataset["train"].features["label"].names dataset?=?dataset.map( ????lambda?x:?{"text_label":?[classes[label]?for?label?in?x["label"]]}, ????batched=True, ????num_proc=1, )
4、訓練數據預處理
tokenizer?=?AutoTokenizer.from_pretrained(model_name_or_path) def?preprocess_function(examples): ????inputs?=?examples[text_column] ????targets?=?examples[label_column] ????model_inputs?=?tokenizer(inputs,?max_length=max_length,?padding="max_length",?truncation=True,?return_tensors="pt") ????labels?=?tokenizer(targets,?max_length=3,?padding="max_length",?truncation=True,?return_tensors="pt") ????labels?=?labels["input_ids"] ????labels[labels?==?tokenizer.pad_token_id]?=?-100 ????model_inputs["labels"]?=?labels ????return?model_inputs processed_datasets?=?dataset.map( ????preprocess_function, ????batched=True, ????num_proc=1, ????remove_columns=dataset["train"].column_names, ????load_from_cache_file=False, ????desc="Running?tokenizer?on?dataset", ) train_dataset?=?processed_datasets["train"] eval_dataset?=?processed_datasets["validation"] train_dataloader?=?DataLoader( ????train_dataset,?shuffle=True,?collate_fn=default_data_collator,?batch_size=batch_size,?pin_memory=True ) eval_dataloader?=?DataLoader(eval_dataset,?collate_fn=default_data_collator,?batch_size=batch_size,?pin_memory=True)
5、設定優化器和正則項
optimizer?=?torch.optim.AdamW(model.parameters(),?lr=lr) lr_scheduler?=?get_linear_schedule_with_warmup( ????optimizer=optimizer, ????num_warmup_steps=0, ????num_training_steps=(len(train_dataloader)?*?num_epochs), )
6、訓練與評估
model?=?model.to(device) for?epoch?in?range(num_epochs): ????model.train() ????total_loss?=?0 ????for?step,?batch?in?enumerate(tqdm(train_dataloader)): ????????batch?=?{k:?v.to(device)?for?k,?v?in?batch.items()} ????????outputs?=?model(**batch) ????????loss?=?outputs.loss ????????total_loss?+=?loss.detach().float() ????????loss.backward() ????????optimizer.step() ????????lr_scheduler.step() ????????optimizer.zero_grad() ????model.eval() ????eval_loss?=?0 ????eval_preds?=?[] ????for?step,?batch?in?enumerate(tqdm(eval_dataloader)): ????????batch?=?{k:?v.to(device)?for?k,?v?in?batch.items()} ????????with?torch.no_grad(): ????????????outputs?=?model(**batch) ????????loss?=?outputs.loss ????????eval_loss?+=?loss.detach().float() ????????eval_preds.extend( ????????????tokenizer.batch_decode(torch.argmax(outputs.logits,?-1).detach().cpu().numpy(),?skip_special_tokens=True) ????????) ????eval_epoch_loss?=?eval_loss?/?len(eval_dataloader) ????eval_ppl?=?torch.exp(eval_epoch_loss) ????train_epoch_loss?=?total_loss?/?len(train_dataloader) ????train_ppl?=?torch.exp(train_epoch_loss) ????print(f"{epoch=}:?{train_ppl=}?{train_epoch_loss=}?{eval_ppl=}?{eval_epoch_loss=}")
執行訓練日志輸出如下:
100%|████████████████████████████████████████████████████████████████████████████████████████|?255/255?[02:21<00:00,??1.81it/s] 100%|██████████████████████████████████████████████████████████████████████████████████████████|?29/29?[00:07<00:00,??4.13it/s] epoch=0:?train_ppl=tensor(14.6341,?device='cuda:0')?train_epoch_loss=tensor(2.6834,?device='cuda:0')?eval_ppl=tensor(1.0057,?device='cuda:0')?eval_epoch_loss=tensor(0.0057,?device='cuda:0') 100%|████████████████████████████████████████████████████████████████████████████████████████|?255/255?[02:00<00:00,??2.11it/s] 100%|██████████████████████████████████████████████████████████████████████████████████████████|?29/29?[00:05<00:00,??5.66it/s] epoch=1:?train_ppl=tensor(1.7576,?device='cuda:0')?train_epoch_loss=tensor(0.5640,?device='cuda:0')?eval_ppl=tensor(1.0052,?device='cuda:0')?eval_epoch_loss=tensor(0.0052,?device='cuda:0') 100%|████████████████████████████████████████████████████████████████████████████████████████|?255/255?[01:33<00:00,??2.74it/s] 100%|██████████████████████████████████████████████████████████████████████████████████████████|?29/29?[00:04<00:00,??6.23it/s] epoch=2:?train_ppl=tensor(1.3830,?device='cuda:0')?train_epoch_loss=tensor(0.3243,?device='cuda:0')?eval_ppl=tensor(1.0035,?device='cuda:0')?eval_epoch_loss=tensor(0.0035,?device='cuda:0')
7、模型保存
peft_model_id?=?f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}" model.save_pretrained(peft_model_id)
8、模型推理預測
from?peft?import?PeftModel,?PeftConfig peft_model_id?=?f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}" config?=?PeftConfig.from_pretrained(peft_model_id) model?=?AutoModelForSeq2SeqLM.from_pretrained(config.base_model_name_or_path) model?=?PeftModel.from_pretrained(model,?peft_model_id) model.eval() inputs?=?tokenizer(dataset["validation"][text_column][i],?return_tensors="pt") print(dataset["validation"][text_column][i]) print(inputs) with?torch.no_grad(): ????outputs?=?model.generate(input_ids=inputs["input_ids"],?max_new_tokens=10) ????print(outputs) ????print(tokenizer.batch_decode(outputs.detach().cpu().numpy(),?skip_special_tokens=True)) ????
運行實例,例如輸入:
Demand?for?fireplace?products?was?lower?than?expected?,?especially?in?Germany?.
輸出:
{'input_ids':?tensor([[??259,???264,???259,?82903,???332,??1090,?10040,?10371,???639,???259, ?????????19540,??2421,???259,?25505,???259,???261,???259,?21230,???281,?17052, ???????????259,???260,?????1]]),?'attention_mask':?tensor([[1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1,?1]])} tensor([[????0,???259,?32588,?????1]]) ['negative']
總結
本文主要從LoRA基本原理及PEFT中的實現、基于mt0-large+lora的完整實踐兩方面進行了介紹。關于進一步的細節,我們可以熟悉原理后,可以進行動手實踐,加深理解。
編輯:黃飛
?
評論
查看更多