使用LoRA和Hugging Face高效训练大语言模型

在本文中,我们将展示如何使用 大语言模型低秩适配 (low-rank adaptation of large language models,lora)  技术在单 gpu 上微调 110 亿参数的 flan-t5 xxl 模型。在此过程中,我们会使用到 hugging face 的 transformers、accelerate 和 peft 库。
快速入门: 轻量化微调 (parameter efficient fine-tuning,peft)
peft 是 hugging face 的一个新的开源库。使用 peft 库,无需微调模型的全部参数,即可高效地将预训练语言模型 (pre-trained language model,plm) 适配到各种下游应用。
注意: 本教程是在 g5.2xlarge aws ec2 实例上创建和运行的,该实例包含 1 个 nvidia a10g。
1. 搭建开发环境
在本例中,我们使用 aws 预置的 pytorch 深度学习 ami,其已安装了正确的 cuda 驱动程序和 pytorch。在此基础上,我们还需要安装一些 hugging face 库,包括 transformers 和 datasets。运行下面的代码就可安装所有需要的包。
# install hugging face libraries!pip install git+https://github.com/huggingface/peft.git!pip install transformers==4.27.1 datasets==2.9.0 accelerate==0.17.1 evaluate==0.4.0 bitsandbytes==0.37.1 loralib --upgrade --quiet# install additional dependencies needed for training!pip install rouge-score tensorboard py7zr  
2. 加载并准备数据集
这里,我们使用 samsum 数据集,该数据集包含大约 16k 个含摘要的聊天类对话数据。这些对话由精通英语的语言学家制作。
{  id: 13818513,  summary: amanda baked cookies and will bring jerry some tomorrow.,  dialogue: amanda: i baked cookies. do you want some?jerry: sure!amanda: i'll bring you tomorrow :-)}  
我们使用 datasets 库中的 load_dataset() 方法来加载 samsum 数据集。
from datasets import load_dataset# load dataset from the hubdataset = load_dataset(samsum)print(ftrain dataset size: {len(dataset['train'])})print(ftest dataset size: {len(dataset['test'])})# train dataset size: 14732# test dataset size: 819  
为了训练模型,我们要用 transformers tokenizer 将输入文本转换为词元 id。
from transformers import autotokenizer, automodelforseq2seqlmmodel_id=google/flan-t5-xxl# load tokenizer of flan-t5-xltokenizer = autotokenizer.from_pretrained(model_id)  
在开始训练之前,我们还需要对数据进行预处理。生成式文本摘要属于文本生成任务。我们将文本输入给模型,模型会输出摘要。我们需要了解输入和输出文本的长度信息,以利于我们高效地批量处理这些数据。
from datasets import concatenate_datasetsimport numpy as np# the maximum total input sequence length after tokenization.# sequences longer than this will be truncated, sequences shorter will be padded.tokenized_inputs = concatenate_datasets([dataset[train], dataset[test]]).map(lambda x: tokenizer(x[dialogue], truncation=true), batched=true, remove_columns=[dialogue, summary])input_lenghts = [len(x) for x in tokenized_inputs[input_ids]]# take 85 percentile of max length for better utilizationmax_source_length = int(np.percentile(input_lenghts, 85))print(fmax source length: {max_source_length})# the maximum total sequence length for target text after tokenization.# sequences longer than this will be truncated, sequences shorter will be padded.tokenized_targets = concatenate_datasets([dataset[train], dataset[test]]).map(lambda x: tokenizer(x[summary], truncation=true), batched=true, remove_columns=[dialogue, summary])target_lenghts = [len(x) for x in tokenized_targets[input_ids]]# take 90 percentile of max length for better utilizationmax_target_length = int(np.percentile(target_lenghts, 90))print(fmax target length: {max_target_length})  
我们将在训练前统一对数据集进行预处理并将预处理后的数据集保存到磁盘。你可以在本地机器或 cpu 上运行此步骤并将其上传到 hugging face hub。
def preprocess_function(sample,padding=max_length):    # add prefix to the input for t5    inputs = [summarize:  + item for item in sample[dialogue]]    # tokenize inputs    model_inputs = tokenizer(inputs, max_length=max_source_length, padding=padding, truncation=true)    # tokenize targets with the `text_target` keyword argument    labels = tokenizer(text_target=sample[summary], max_length=max_target_length, padding=padding, truncation=true)    # if we are padding here, replace all tokenizer.pad_token_id in the labels by -100 when we want to ignore    # padding in the loss.    if padding == max_length:        labels[input_ids] = [            [(l if l != tokenizer.pad_token_id else -100) for l in label] for label in labels[input_ids]        ]    model_inputs[labels] = labels[input_ids]    return model_inputstokenized_dataset = dataset.map(preprocess_function, batched=true, remove_columns=[dialogue, summary, id])print(fkeys of tokenized dataset: {list(tokenized_dataset['train'].features)})# save datasets to disk for later easy loadingtokenized_dataset[train].save_to_disk(data/train)tokenized_dataset[test].save_to_disk(data/eval)  
3. 使用 lora 和 bnb int-8 微调 t5
除了 lora 技术,我们还使用 bitsanbytes llm.int8() 把冻结的 llm 量化为 int8。这使我们能够将 flan-t5 xxl 所需的内存降低到约四分之一。
训练的第一步是加载模型。我们使用 philschmid/flan-t5-xxl-sharded-fp16 模型,它是 google/flan-t5-xxl 的分片版。分片可以让我们在加载模型时不耗尽内存。
from transformers import automodelforseq2seqlm# huggingface hub model idmodel_id = philschmid/flan-t5-xxl-sharded-fp16# load model from the hubmodel = automodelforseq2seqlm.from_pretrained(model_id, load_in_8bit=true, device_map=auto)  
现在,我们可以使用 peft 为 lora int-8 训练作准备了。
from peft import loraconfig, get_peft_model, prepare_model_for_int8_training, tasktype# define lora configlora_config = loraconfig( r=16, lora_alpha=32, target_modules=[q, v], lora_dropout=0.05, bias=none, task_type=tasktype.seq_2_seq_lm)# prepare int-8 model for trainingmodel = prepare_model_for_int8_training(model)# add lora adaptormodel = get_peft_model(model, lora_config)model.print_trainable_parameters()# trainable params: 18874368 || all params: 11154206720 || trainable%: 0.16921300163961817  
如你所见,这里我们只训练了模型参数的 0.16%!这个巨大的内存增益让我们安心地微调模型,而不用担心内存问题。
接下来需要创建一个 datacollator,负责对输入和标签进行填充,我们使用 transformers 库中的 datacollatorforseq2seq 来完成这一环节。
from transformers import datacollatorforseq2seq# we want to ignore tokenizer pad token in the losslabel_pad_token_id = -100# data collatordata_collator = datacollatorforseq2seq(    tokenizer,    model=model,    label_pad_token_id=label_pad_token_id,    pad_to_multiple_of=8)  
最后一步是定义训练超参 ( trainingarguments)。
from transformers import seq2seqtrainer, seq2seqtrainingargumentsoutput_dir=lora-flan-t5-xxl# define training argstraining_args = seq2seqtrainingarguments(    output_dir=output_dir,  auto_find_batch_size=true,    learning_rate=1e-3, # higher learning rate    num_train_epochs=5,    logging_dir=f{output_dir}/logs,    logging_strategy=steps,    logging_steps=500,    save_strategy=no,    report_to=tensorboard,)# create trainer instancetrainer = seq2seqtrainer(    model=model,    args=training_args,    data_collator=data_collator,    train_dataset=tokenized_dataset[train],)model.config.use_cache = false # silence the warnings. please re-enable for inference!  
运行下面的代码,开始训练模型。请注意,对于 t5,出于收敛稳定性考量,某些层我们仍保持 float32 精度。
# train modeltrainer.train()  
训练耗时约 10 小时 36 分钟,训练 10 小时的成本约为 13.22 美元。相比之下,如果 在 flan-t5-xxl 上进行全模型微调 10 个小时,我们需要 8 个 a100 40gb,成本约为 322 美元。
我们可以将模型保存下来以用于后面的推理和评估。我们暂时将其保存到磁盘,但你也可以使用 model.push_to_hub 方法将其上传到 hugging face hub。
# save our lora model & tokenizer resultspeft_model_id=resultstrainer.model.save_pretrained(peft_model_id)tokenizer.save_pretrained(peft_model_id)# if you want to save the base model to call# trainer.model.base_model.save_pretrained(peft_model_id)  
最后生成的 lora checkpoint 文件很小,仅需 84mb 就包含了从 samsum 数据集上学到的所有知识。
4. 使用 lora flan-t5 进行评估和推理
我们将使用 evaluate 库来评估 rogue 分数。我们可以使用 peft 和  transformers 来对 flan-t5 xxl 模型进行推理。对 flan-t5 xxl 模型,我们至少需要 18gb 的 gpu 显存。
import torchfrom peft import peftmodel, peftconfigfrom transformers import automodelforseq2seqlm, autotokenizer# load peft config for pre-trained checkpoint etc.peft_model_id = resultsconfig = peftconfig.from_pretrained(peft_model_id)# load base llm model and tokenizermodel = automodelforseq2seqlm.from_pretrained(config.base_model_name_or_path, load_in_8bit=true, device_map={:0})tokenizer = autotokenizer.from_pretrained(config.base_model_name_or_path)# load the lora modelmodel = peftmodel.from_pretrained(model, peft_model_id, device_map={:0})model.eval()print(peft model loaded)  
我们用测试数据集中的一个随机样本来试试摘要效果。
from datasets import load_datasetfrom random import randrange# load dataset from the hub and get a sampledataset = load_dataset(samsum)sample = dataset['test'][randrange(len(dataset[test]))]input_ids = tokenizer(sample[dialogue], return_tensors=pt, truncation=true).input_ids.cuda()# with torch.inference_mode():outputs = model.generate(input_ids=input_ids, max_new_tokens=10, do_sample=true, top_p=0.9)print(finput sentence: {sample['dialogue']}{'---'* 20})print(fsummary:{tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=true)[0]})  
不错!我们的模型有效!现在,让我们仔细看看,并使用 test 集中的全部数据对其进行评估。为此,我们需要实现一些工具函数来帮助生成摘要并将其与相应的参考摘要组合到一起。评估摘要任务最常用的指标是 rogue_score,它的全称是 recall-oriented understudy for gisting evaluation。与常用的准确率指标不同,它将生成的摘要与一组参考摘要进行比较。
import evaluateimport numpy as npfrom datasets import load_from_diskfrom tqdm import tqdm# metricmetric = evaluate.load(rouge)def evaluate_peft_model(sample,max_target_length=50):    # generate summary    outputs = model.generate(input_ids=sample[input_ids].unsqueeze(0).cuda(), do_sample=true, top_p=0.9, max_new_tokens=max_target_length)    prediction = tokenizer.decode(outputs[0].detach().cpu().numpy(), skip_special_tokens=true)    # decode eval sample    # replace -100 in the labels as we can't decode them.    labels = np.where(sample['labels']!= -100, sample['labels'], tokenizer.pad_token_id)    labels = tokenizer.decode(labels, skip_special_tokens=true)    # some simple post-processing    return prediction, labels# load test dataset from distktest_dataset = load_from_disk(data/eval/).with_format(torch)# run predictions# this can take ~45 minutespredictions, references = [], []for sample in tqdm(test_dataset):    p,l = evaluate_peft_model(sample)    predictions.append(p)    references.append(l)# compute metricrogue = metric.compute(predictions=predictions, references=references, use_stemmer=true)# print resultsprint(frogue1: {rogue['rouge1']* 100:2f}%)print(frouge2: {rogue['rouge2']* 100:2f}%)print(frougel: {rogue['rougel']* 100:2f}%)print(frougelsum: {rogue['rougelsum']* 100:2f}%)# rogue1: 50.386161%# rouge2: 24.842412%# rougel: 41.370130%# rougelsum: 41.394230%  
我们 peft 微调后的 flan-t5-xxl 在测试集上取得了 50.38% 的 rogue1 分数。相比之下,flan-t5-base 的全模型微调获得了 47.23 的 rouge1 分数。rouge1 分数提高了 3%。
令人难以置信的是,我们的 lora checkpoint 只有 84mb,而且性能比对更小的模型进行全模型微调后的 checkpoint 更好。


爬虫工程师是干什么的
浅谈光栅尺传感器的用途及工作原理
vivo S17系列全新发布 后置智慧柔光环来了
工业4.0是什么?有哪些特点技术支柱是什么?
涂料测试露点仪TQC /SHEEN
使用LoRA和Hugging Face高效训练大语言模型
共享飞机很烧钱开不起 一万五一小时
苹果市值首次突破3万亿美元
采用FPGA器件EP1C3T144C6芯片和VHDL实现频率测量计的设计
LED照明驱动IC U6237D的主要特征
新颖的彩灯控制电路
国家电网今年将新建充电桩7.8万个,较去年同比增长35.5倍
一款黑科技的产物,智能家居魔镜已正式亮相
央行数字货币对于银行商业逻辑会有影响吗
轮速传感器故障处理
一言不合就趴窝?聊聊电动汽车的剩余里程的那些事!
自动化仪表安装完成后,电磁流量计该如何调试
如何修改Sony VGP-SP2扬声器
中国城市SUV市场的开创者是本田CR-V?不,三菱欧蓝德才是真正的鼻祖!
先进的金属增材制造如何用于关键核心部件的制造