Redian新闻
>
LLM+LoRa微调加速技术原理及基于PEFT的动手实践:一些思考和mt0-large+lora完整案例

LLM+LoRa微调加速技术原理及基于PEFT的动手实践:一些思考和mt0-large+lora完整案例

公众号新闻
来自:老刘说NLP
进NLP群—>加入NLP交流群

如何花费较少的算力成本来进行微调训练,十分重要,当前关于LLaMA、Alpaca、Instruct微调、LoRa微调等多个概念大家讲的很多,最近也在学习,也看到几个有趣的话题(主要参考于(https://github.com/ymcui/Chinese-LLaMA-Alpaca):

首先,来看关于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的完整实践两方面进行了介绍。关于进一步的细节,我们可以熟悉原理后,可以进行动手实践,加深理解。

参考文献

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群—>加入NLP交流群(备注nips/emnlp/nlpcc进入对应投稿群)


加入星球,你将获得:

1. 每日更新3-5篇论文速读
2. 最新入门和进阶学习资料
3. 每日1-3个AI岗位招聘信息

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
关于GPT,人工智能,以及人的一些思考网商银行绿色金融实践:数字化高效激励小微经营者绿色转型LLM底座模型:LLaMA、Palm、GLM、BLOOM、GPT结构对比"𝙇𝙚𝙖𝙙 𝙏𝙝𝙚 𝘾𝙝𝙖𝙧𝙜𝙚"广告#创译挑战基于OpenTelemetry和Jaeger的Go语言链路追踪最佳实践冬季周末的一大享受Sentry 的前端测试实践:从 Enzyme 迁移到 RTL足够惊艳,使用Alpaca-Lora基于LLaMA(7B)二十分钟完成微调,效果比肩斯坦福羊驼两会专访|全国政协委员、飞腾公司副总经理郭御风:本土CPU产业应加速技术路线收敛草书《兰亭序》中科院原院长白春礼最新演讲:关于我国人工智能发展的思考和建议5次延期回复上交所问询函,有何难言之隐?破产重整案才上了央视《法治中国》栏目DPDK技术原理、性能及生态建设元宇宙基础设施——PPIO边缘云在云渲染/云游戏的思考和实践ROG 与 ACRONYM 联合推出幻 X-ACRNM RMT02 二合一笔记本刘如谦新公司浮出水面,仍隐匿运行,正基于PACE开发新型蛋白酶,让蛋白质组编辑成为可能警惕AI灭绝人类!OpenAI CEO等350位专业人士签署声明​;大脑的核桃状结构决定了我们的思考和感觉 | 环球科学要闻从此告别繁琐的模型微调,LLM-Adapters助力NLP任务快速高效微调!“臭豆腐”妙诗一首。。。。。。。Belmont公私校大对比:Belmont Hill vs Belmont High思考、思考、思考不停歇,思维树ToT「军训」LLM我喜欢尝试讲故事的新方法 | 采访反乌托邦动画短片《George-Peterland》导演Christer Wahlberg如何更好地蒸馏ChatGPT模型能力:Lion闭源大型语言模型的对抗性蒸馏模型原理及实验工作介绍我也来一首类似的。。。。。。【雨水】心雨细无声· 写在老爸“七七”之日消息链路拆分最佳实践:钉钉审批异步链路重构【总结】ESC-HFA2023丨FINEARTS-HF研究设计及基线特征重磅发布!非奈利酮或将开启LVEF≥40%心衰人群管理“心”篇章OLAP是一个很卷的赛道,我对目前主流趋势有一些思考lāo dao?láo dao!以 LLM 为核心 LLM@Core:程序员的大语言模型技术指南新冠周报丨约翰·霍普金斯大学即将停止实时 新冠数据更新。其带给我们三个深入思考和经验,将影响很多人的职业发展2023信创实践:信创云炙手可热的新能源行业,如何思考和布局10倍速机会点?自建MongoDB实践:MongoDB 安全加密北大、西湖大学等开源「裁判大模型」PandaLM:三行代码全自动评估LLM,准确率达ChatGPT的94%
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。