如何花費較少的算力成本來進行微調訓練,,十分重要,,當前關于LLaMA、Alpaca,、Instruct微調、LoRa微調等多個概念大家講的很多,最近也在學習,,也看到幾個有趣的話題(主要參考于(https://github.com/ymcui/Chinese-LLaMA-Alpaca ):
首先,來看關于Instruct微調和LoRa微調
Instruct微調和LoRa微調是兩種不同的技術,。 Instruct微調是指在深度神經網絡訓練過程中調整模型參數的過程,,以優(yōu)化模型的性能,。在微調過程中,使用一個預先訓練好的模型作為基礎模型,,然后在新的數據集上對該模型進行微調,。Instruct微調是一種通過更新預訓練模型的所有參數來完成的微調方法,通過微調使其適用于多個下游應用,。
LoRa微調則是指對低功耗廣域網(LoRaWAN)中的LoRa節(jié)點參數進行微調的過程,,以提高節(jié)點的傳輸效率。在LoRa微調中,,需要了解節(jié)點的硬件和網絡部署情況,,并通過對節(jié)點參數進行微小調整來優(yōu)化傳輸效率。
與Instruct微調相比,,LoRA在每個Transformer塊中注入可訓練層,,因為不需要為大多數模型權重計算梯度,大大減少了需要訓練參數的數量并且降低了GPU內存的要求,。 研究發(fā)現,,使用LoRA進行的微調質量與全模型微調相當,速度更快并且需要更少的計算,。因此,,如果有低延遲和低內存需求的情況,建議使用LoRA微調,。
其次,,我們再來看看為什么會有LLaMA模型和LoRA兩種模型
如上所述,模型的微調方式有很多種,,基于LoRA的微調產生保存了新的權重,,可以將生成的LoRA權重認為是一個原來LLaMA模型的補丁權重 。至于LLaMA 權重,,它則是由Mean公司開源的大模型預訓練權重,。
最后,我們來看看關于詞表擴充,,為什么要擴充詞表,,直接在原版LLaMA上用中文預訓練不行?
本身LLaMA對中文支持不是很好,,大多數相關衍生工作是直接在原版上進行pretrain/finetune的,,從而采取了更大膽的策略——增加中文詞表,可能進一步加劇中文訓練不充分的問題,,但從長遠看是否有利于后續(xù)進一步預訓練就得靠時間檢驗了,,加入詞表是有一定破壞性的,一是破壞原有分詞體系,,二是增加了未訓練的權重,。所以如果不能進行充分訓練的話,,可能會有比較大的問題。如果不是特別專的領域(比如生物醫(yī)學等涉及很多專業(yè)詞匯的領域)沒有太大必要去擴充英文詞表,。
原版LLaMA模型的詞表大小是32K,,其主要針對英語進行訓練(具體詳見LLaMA論文),對多語種支持不是特別理想(可以對比一下多語言經典模型XLM-R的詞表大小為250K),。通過初步統(tǒng)計發(fā)現,,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中的實現 當前,已經出現了很多l(xiāng)ora作為adapter的微調模型,,如Alpaca LoRA,,Chinese-LLaMA-Alpaca等,其在公開時會注明:中文LLaMA/Alpaca LoRA模型無法單獨使用,,需要搭配原版LLaMA模型,,發(fā)布的是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)有效地適應各種下游應用,而不需要對模型的所有參數進行微調,。
對大規(guī)模的PLM進行微調往往成本過高,,在這方面,PEFT方法只對少數(額外的)模型參數進行微調, 基本思想在于僅微調少量 (額外) 模型參數,,同時凍結預訓練 LLM 的大部分參數,,從而大大降低了計算和存儲成本,這也克服了災難性遺忘的問題,,這是在 LLM 的全參數微調期間觀察到的一種現象PEFT 方法也顯示出在低數據狀態(tài)下比微調更好,,可以更好地泛化到域外場景。
例如,,使用PEFT-lora進行加速微調的效果如下,,從中我們可以看到該方案的優(yōu)勢:
例如,其對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的一些優(yōu)勢:
1)一個預先訓練好的模型可以被共享,,并用于為不同的任務建立許多小的LoRA模塊??梢詢鼋Y共享模型,,并通過替換圖中的矩陣A和B來有效地切換任務,大大降低了存儲需求和任務切換的難度,。
2)在使用自適應優(yōu)化器時,,LoRA使訓練更加有效,并將硬件進入門檻降低了3倍,,因為我們不需要計算梯度或維護大多數參數的優(yōu)化器狀態(tài),。相反,我們只優(yōu)化注入的,、小得多的低秩矩陣,。
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:///bigscience/mt0-large,。
模型權重地址:https:///bigscience/mt0-large/tree/main
先看看mt0-large是什么。多任務提示微調(MTF)已被證明可以幫助大型語言模型在zero-shot的環(huán)境下生成新的任務,,但到目前為止,,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,、設定優(yōu)化器和正則項
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=}" )
執(zhí)行訓練日志輸出如下:
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的完整實踐兩方面進行了介紹,。關于進一步的細節(jié),我們可以熟悉原理后,,可以進行動手實踐,,加深理解。
參考文獻 1,、https://zhuanlan.zhihu.com/p/400790006 2,、https://blog.csdn.net/qq_39388410/article/details/121036309 3,、https://github.com/ymcui/Chinese-LLaMA-Alpaca
關于我們 老劉,劉煥勇,,NLP開源愛好者與踐行者,,主頁:https://liuhuanyong.。
就職于360人工智能研究院,、曾就職于中國科學院軟件研究所,。
老劉說NLP,將定期發(fā)布語言資源,、工程實踐,、技術總結等內容,歡迎關注,。
,。