paint-brush
alpaca-lora:尝试自制的大型语言模型经过@ishootlaser
5,219 讀數
5,219 讀數

alpaca-lora:尝试自制的大型语言模型

经过 Wei18m2023/10/16
Read on Terminal Reader

太長; 讀書

大型语言模型 (LLM) 正在彻底改变软件开发,增强用户与 LangChain 和 Semantic Kernel 等工具的交互。他们可以在内容创建的各个阶段提供协助并简化复杂的流程。然而,对法学硕士提供商的依赖、内容审查和定制选项的担忧导致人们开始寻找开源替代方案。本文探讨了一种训练您自己的 LLM、alpaca-lora 的微调方法,提供了对过程、挑战和潜在解决方案的见解,特别是在 V100 GPU 等硬件上成功实现微调的方法。我们的目标是创建能够产生连贯且与上下文相关的回答的法学硕士,同时避免迅速重复。
featured image - alpaca-lora:尝试自制的大型语言模型
Wei HackerNoon profile picture
0-item


自 ChatGPT 发布以来, 大型语言模型(LLM) 已成为软件开发领域的流行语。它与人类进行自然对话的能力只是冰山一角。通过 LangChain 或 Semantic Kernel 等工具的增强,法学硕士有可能彻底改变用户与软件交互的方式。换句话说,法学硕士可以在功能和数据源之间创造协同效应,并提供更高效、更直观的用户体验。


例如,许多人已经在使用基于人工智能的内容创建工具来制作下一个病毒视频。典型的视频制作流程包括脚本、后勤、故事板、编辑和营销等。为了简化流程,法学硕士可以帮助内容创作者在编写剧本时进行研究,购买拍摄道具,根据剧本生成故事板(可能需要稳定的扩散来生成图像),促进编辑过程,并编写引人注目的标题/用于吸引社交媒体上的观看次数的视频描述。 LLM 是协调所有这些的核心,但是将 LLM 合并到软件产品中时可能存在几个问题:


  1. 如果我使用OpenAI的API,我会变得过于依赖这个服务吗?如果他们提高价格怎么办?如果他们改变服务可用性怎么办?


  2. 我不喜欢 OpenAI 审查内容或对某些用户输入提供非建设性反馈的方式。 (或者反过来:我不喜欢 OpenAI 审查制度如何忽略我的用例中敏感的某些内容。)


  3. 如果我的客户更喜欢私有云或本地部署,我有哪些ChatGPT替代方案?


  4. 我只是想掌控。我需要定制法学硕士,而且我想要便宜的。


正是出于这些担忧,我想知道是否可以有一个与 OpenAI 的 GPT 模型相当的开源模型。幸运的是,优秀的开源社区已经分享了一些非常有前途的解决方案。我决定尝试alpaca-lora ,这是一种用于训练自己的 LLM 的参数高效微调方法。这篇博文讨论了这个过程、我遇到的问题、我如何解决这些问题以及接下来会发生什么。如果您也想使用该技术来培训自己的法学硕士,我希望这些信息能有所帮助。


让我们开始!


内容概述

  • 什么是 LLaMA、羊驼毛和 LoRA?
  • 微调实验
    • 快速扫描源码
    • 第一次尝试
    • 第一次观察
    • 第二次尝试和(某种程度上)成功
    • 超越7B
  • 概括


什么是 LLaMA、羊驼毛和 LoRA?

LLaMA是 Meta AI 的基础语言模型的集合。这些自回归模型的参数数量从 7B 到 65B 不等,只能用于研究目的。 Meta 最近发布了LLaMA 2 ,它可以在商业上使用,但有一些限制。 LLaMA 模型系列的独特之处在于它的训练语料库和性能。 Meta 指出 LLaMA 是在公开数据集上进行训练的。在大多数基准测试中,其 13B 变体的性能可与 GPT-3(175B 型号)相媲美。


alpaca是斯坦福大学在LLaMA的基础上建立一个遵循指令的LLM的尝试。它的工作原理如下:


  1. 要求语言模型生成指令-输入-响应三元组。

  2. 使用生成的数据微调 LLaMA。


经过微调后,LLaMA 能够遵循用户的指令并做出相应的响应。


以下是普通 LLaMA 响应和微调 LLaMA 响应之间的比较:

 { "instruction": "Answer user's question.", "input": "Can you put a medium voltage circuit breaker in a low voltage switchgear? Why? Explain in detail.", "vanilla LLaMA": { "answer": "Yes, you can put a medium voltage circuit breaker in a low voltage switchgear. The medium voltage circuit breaker can be put in a low voltage switchgear because the medium voltage circuit breaker can be put in a low voltage switchgear because the medium voltage circuit breaker can be put in a low voltage switchgear because the medium voltage circuit breaker can be put....(repetition continues)" }, "fine-tuned LLaMA": { "answer": "No, it is not possible to put a medium voltage circuit breaker in a low voltage switchgear. This is because low voltage switchgear is designed to handle voltages up to 1 kV, while medium voltage switchgear is designed to handle voltages from 1 kV to 38 kV. The circuit breakers, contacts, and other components in low voltage switchgear are not suitable for the higher voltages and currents of medium voltage switchgear. Using a medium voltage circuit breaker in a low voltage switchgear could result in damage to the components and could pose a safety hazard." } }


正如您所看到的,微调极大地改变了 LLM 的行为。普通模型陷入了重复循环。虽然微调模型没有产生 100% 正确的响应,但至少它的答案是响亮的“不”。微调是产生可用的 LLM 的必要步骤。在许多情况下,部署经过微调的开源法学硕士就足够了。然而,在一些定制的业务用例中,最好在特定领域的数据集上微调模型。


羊驼最大的缺点是它的资源需求。其 GitHub 页面指出:

简单来说,微调 7B 模型需要大约 7 x 4 x 4 = 112 GB 的 VRAM。


这超出了 A100 80GB GPU 可以处理的 VRAM。我们可以使用LoRA绕过 VRAM 要求。


LoRA 的工作原理如下:

  1. 选择模型中的一些权重,例如变压器模型中的查询投影权重 $W_q$。将适配器权重添加(是的,算术加法)到所选权重。
  2. 冻结原来的模型,只训练增加的权重。


增加的重量具有一些特殊的性质。受这篇论文的启发,Edward Hu 等人。表明对于原始模型权重 $W_o\in R^{d \times k}$,您可以为下游任务生成微调权重 $W_o'=W_o+BA$,其中 $B\in R^{d \times r}$ 、 $A \in R^{r \times k}$ 和 $r\ll min(d, k)$ 是适配器权重的“内在等级”。为适配器权重设置适当的 $r$ 非常重要,因为较小的 $r$ 会降低模型性能,而较大的 $r$ 会增加适配器权重大小,而不会按比例提高性能。


该技术类似于截断 SVD,它通过将矩阵分解为几个较小的矩阵并仅保留几个最大的奇异值来近似矩阵。假设 $W_o\in R^{100 \times 100}$,完整的微调将改变 10,000 个参数。 LoRA 微调 $r=8$ 会将微调权重分解为 2 部分,$B\in R^{100 \times 8}$ 和 $A\in R^{8 \times 100}$,每个部分部分包含800个参数(总共1600个参数)。可训练参数的数量减少了6.25倍。


使用 LoRA 对模型进行改造后,我们得到的模型可训练权重只有约 1%,但其性能在某些领域得到了极大的提高。这将使我们能够在 RTX 4090 或 V100 等更易于访问的硬件上训练 7B 或 13B 模型。


微调实验

我在华为云上使用 GPU 加速的 VM 实例( p2s.2xlarge ,8vCPU,64GB RAM,1x V100 32GB VRAM)进行了实验。已知 V100 不支持 bfloat16 数据类型,其张量核心不支持 int8加速度。这两个限制会减慢混合精度训练速度并导致混合精度训练期间数值溢出。我们将记住这一点以供以后讨论。

快速扫描源码

finetune.pygenerate.py是该项目的核心。第一个脚本微调 LLaMA 模型,第二个脚本使用微调后的模型与用户聊天。我们先看一下finetune.py的主要流程:


  1. 加载预先训练的大型基础模型
model = LlamaForCausalLM.from_pretrained( base_model, # name of a huggingface compatible LLaMA model load_in_8bit=True, torch_dtype=torch.float16, device_map=device_map, )


  1. 加载模型的标记器
tokenizer = LlamaTokenizer.from_pretrained(base_model) tokenizer.pad_token_id = ( 0 # unk. we want this to be different from the eos token ) tokenizer.padding_side = "left" # Allow batched inference


  1. 基于训练模板,使用tokenizegenerate_and_tokenize_prompt两个函数准备模型输入。


  2. 使用 Huggingface 的PEFT创建 LoRA 适配模型

config = LoraConfig( r=lora_r, # the lora rank lora_alpha=lora_alpha, # a weight scaling factor, think of it like learning rate target_modules=lora_target_modules, # transformer modules to apply LoRA to lora_dropout=lora_dropout, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, config)


  1. 创建训练器实例并开始训练
trainer = transformers.Trainer( model=model, train_dataset=train_data, eval_dataset=val_data, args=transformers.TrainingArguments( ...


这很简单。


最后,该脚本生成一个包含检查点、适配器权重和适配器配置的模型文件夹。


接下来我们看一下generate.py的主要流程


  1. 负载模型和适配器重量
model = LlamaForCausalLM.from_pretrained( base_model, device_map={"": device}, torch_dtype=torch.float16, ) model = PeftModel.from_pretrained( model, lora_weights, device_map={"": device}, torch_dtype=torch.float16, )


  1. 指定生成配置
generation_config = GenerationConfig( temperature=temperature, top_p=top_p, top_k=top_k, num_beams=num_beams, **kwargs, ) generate_params = { "input_ids": input_ids, "generation_config": generation_config, "return_dict_in_generate": True, "output_scores": True, "max_new_tokens": max_new_tokens, }


  1. 定义流式和非流式生成模式的函数:
 if stream_output: # streaming ... # Without streaming with torch.no_grad(): generation_output = model.generate( input_ids=input_ids, generation_config=generation_config, return_dict_in_generate=True, output_scores=True, max_new_tokens=max_new_tokens, ) s = generation_output.sequences[0] output = tokenizer.decode(s) yield prompter.get_response(output)


  1. 启动 Gradio 服务器来测试模型:
 gr.Interface( ...


第一次尝试

该项目的README.md指出,以下微调设置可产生 LLaMA 7B,其性能可与斯坦福羊驼毛相媲美。在hushingface 上分享了“官方”羊驼-劳拉体重


 python finetune.py \ --base_model='decapoda-research/llama-7b-hf' \ --num_epochs=10 \ --cutoff_len=512 \ --group_by_length \ --output_dir='./lora-alpaca' \ --lora_target_modules='[q_proj,k_proj,v_proj,o_proj]' \ --lora_r=16 \ --micro_batch_size=8


然而,根据我的经验,它并没有产生可用的模型。在 V100 上运行它会遇到以下显示停止问题:


  1. 使用load_in_8bit加载模型会导致数据类型错误。
  2. 绑定脚本将导致 PEFT 生成无效的适配器。无效的适配器不会对原始 LLaMA 模型进行任何更改,只会产生乱码。
  3. decapoda-research/llama-7b-hf模型显然使用了错误的分词器。它的pad token、bos token和eos token与LLaMA的官方tokenizer不同。
  4. 如前所述,V100 缺乏对 int8/fp16 混合训练的适当支持。这会导致意外行为,例如training loss = 0.0eval loss = NaN


在深入研究并浪费了大量虚拟机时间后,我发现了必要的更改,以便在单个 V100 上进行训练。

 ... # do not use decapoda-research/llama-7b-hf as base_model. use a huggingface LLaMA model that was properly converted and has a correct tokenizer, eg, yahma/llama-7b-hf or huggyllama/llama-7b. # decapoda-research/llama-7b-hf is likely to cause overflow/underflow on V100. train loss goes to 0 and eval loss becomes NaN. using yahma/llama-7b-hf or huggyllama/llama-7b somehow mitigates this issue model = LlamaForCausalLM.from_pretrained( base_model, load_in_8bit=True, # only work for 7B LLaMA. On a V100, set True to save some VRAM at the cost of slower training; set False to speed up training at the cost of more VRAM / smaller micro batch size torch_dtype=torch.float16, device_map=device_map, ) ... # comment out the following line if load_in_8bit=False model = prepare_model_for_int8_training(model) ... # set legacy=False to avoid unexpected tokenizer behavior. make sure no tokenizer warning was raised during tokenizer instantiation tokenizer = LlamaTokenizer.from_pretrained(base_model, legacy=False) ... # the following binding script results in invalid adapter. simply comment them out old_state_dict = model.state_dict model.state_dict = ( lambda self, *_, **__: get_peft_model_state_dict( self, old_state_dict() ) ).__get__(model, type(model)) ... # if load_in_8bit=True, need to cast data type during training with torch.autocast('cuda'): trainer.train(resume_from_checkpoint=resume_from_checkpoint)


进行这些更改后,这些训练参数会产生一个可用的模型。

 { "args": [ "--base_model", "yahma/llama-7b-hf", "--num_epochs=10", "--data_path", "yahma/alpaca-cleaned", "--output_dir", "./your output dir", "--cutoff_len=512", "--lora_target_modules=[q_proj,k_proj,v_proj,o_proj]", "--lora_r=16", "--micro_batch_size=2", "--batch_size=16", "--wandb_project", "your wandb project name" ], }


请注意,训练脚本不仅保存“最佳”模型(评估损失最低的检查点),还保存几个检查点。我使用 wandb 记录训练/评估损失以进行进一步分析。


第一次观察

我向微调模型提出了一些随机问题。它会产生一些合理的反应。


这是一个例子:

说明:认真回答用户的问题。


输入:当您架出开关柜断路器卡车时会发生什么?


应对措施:当开关柜断路器车“摇出”时,意味着该车内的所有断路器均已分闸,且该车处于不带电状态。这通常在卡车保养、修理或检查时进行。一旦断路器被拉出,卡车就可以安全地工作,并且在完成必要的维护或修理后可以重新通电。


此示例查询 LLM 的内部知识。该模型似乎对开关设备了解很多。然而,当我尝试实证测试它的推理能力时,它产生了一个非常奇怪的结果:


操作说明:


根据以下有关项目的信息,回答问题。 Chinese-Llama-2项目旨在增强大语言模型Llama-2的中文理解、生成、翻译能力。通过LoRA微调、全参数指令微调、二次预训练等方法的应用,我们诚邀您下载使用相关数据集、训练指南和模型参数。


输入:该项目使用了哪些方法?


回应:Chinese-Llama-2项目旨在增强Llama-2大语言模型中文的理解、生成、翻译能力。通过LoRA微调、全参数指令微调、二次预训练等方法的应用,我们诚邀您下载并使用相关数据集、训练指南和模型参数。</s>


正如您所看到的,该模型喜欢重复提示。我将其称为提示重复器模型,其他开发人员也在回购问题中报告了这种行为。只有通过更精心设计的提示,它回答问题的能力才会变得更好。这种行为不是我们在生产系统中想要的,因为我们无法保证跨不同模型的即时有效性。模型必须对提示不太敏感。我们希望以某种方式提高该法学硕士的表现。


在下一次会议中,我将讨论导致这个问题的原因以及如何改进微调结果。

第二次尝试和(某种程度上)成功

以下是我尝试改善微调结果的三件事:


  1. 掩盖提示的损失(有助于避免提示重复)

  2. 关闭group-by-length选项(有助于提高性能,使损失曲线看起来更平滑)

  3. 不要相信评估损失曲线。使用训练损失较低的检查点,即使其评估损失可能高于“最佳”检查点。 (有助于提高性能,因为 eval 损失不是这里最好的矩阵)


我们来一一解释一下这3点。

掩盖提示损失

我一直在寻找快速重复的原因,直到找到这篇文章官方的 lora Weights 提交消息。他们建议在损失计算中应排除提示。基本上,您不想鼓励模型输出提示标记。在训练期间屏蔽提示不会鼓励模型重复提示标记。下图解释了这一点:在 3 次训练运行中, stoic-star-6是唯一一次在训练期间没有屏蔽提示的运行。因此,它的训练损失在开始时会更高。我怀疑如果a)计算损失时没有屏蔽提示, b)训练不充分,模型将更有可能重复提示而不是遵循指示。


在源代码中,丢失屏蔽是通过将提示标记设置为-100来完成的:

索引设置为-100的标记将被忽略(屏蔽),仅针对标签为[0, ..., config.vocab_size]标记计算损失。


关闭group-by-length选项

group-by-length选项允许 Huggingface 的Trainer将相似长度的输入分组为批次。这有助于在填充输入序列时节省 VRAM 使用量。然而,它会大大减少单个批次内的样本方差。在训练过程中,我们通常更喜欢将模型暴露于各种训练样本中。将按group-by-length设置为False可减少样本变异。它还会导致训练过程中损失波动(例如,连续两个batch的填充长度分别为10和50。较短的batch损失较低,较长的batch损失较高。这会导致损失曲线振荡,如图所示以下)。


另一方面,由于group-by-length减少了批量样本变化,我怀疑模型性能也会受到影响。下图比较了有或没有group-by-length训练损失。很明显,启用了group-by-length运行peach-violet-19平均损失较高。


不要相信评估损失曲线

我观察到训练损失和评估损失在我的所有运行中都存在差异。


这是一个例子:

在此示例中,最佳检查点出现在epoch=2左右。如果您按照原始存储库的建议加载最佳检查点,您很可能会获得上一会话中提到的提示重复器模型。


通过在训练接近结束时加载模型检查点,我得到了一个性能更高的模型。我问了与之前相同的问题,模型返回了正确答案:


操作说明:

根据以下有关项目的信息,回答问题。 Chinese-Llama-2项目旨在增强大语言模型Llama-2的中文理解、生成、翻译能力。通过LoRA微调、全参数指令微调、二次预训练等方法的应用,我们诚邀您下载使用相关数据集、训练指南和模型参数。


输入:该项目使用了哪些方法?


回应:Chinese-Llama-2项目采用了LoRA微调、全参数指令微调、二次预训练等方法。</s>


但从评估损失来看,这个模型应该表现得更差。是什么导致了这种困境?

我认为评估损失并不是衡量大型语言模型性能的良好矩阵。 LLaMA 使用CrossEntropyLoss进行训练和评估损失:


 # modelling_llama.py from transformers library ... # forward function under LlamaForCausalLM class if labels is not None: # Shift so that tokens < n predict n shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, self.config.vocab_size), shift_labels.view(-1))


在评估集上进行测试时,模型可能会用不同的措辞产生相同的答案:

 { "evaluation prompt": "What is 1 + 3?" "evaluation answer": "4." "prediction answer": "The answer is 4." }


两个答案都是正确的,但如果预测答案与评估答案不完全匹配,评估损失将会很高。在这种情况下,我们需要一个更好的评估矩阵来衡量模型的性能。我们稍后会担心适当的评估。现在,我们假设最好的模型是训练损失最低的模型。

超越7B

我尝试在 V100 上微调 13B 模型。虽然 V100 可以处理 7B 模型上的 int8 和 fp16 训练,但它根本无法处理 13B 模型上的 int8 训练。如果load_int_8bit = True ,13B 模型将产生training_loss = 0.0 。我们可以使用一些调试工具来理解为什么会发生这种情况(剧透警告:这是由上溢/下溢引起的)。


我使用 Huggingface 的DebugUnderflowOverflow工具在训练期间检查参数。在第一次前向传递中,它检测到 inf/nan 值:

更具体地说, DebugUnderflowOverflowLlamaDecoderLayer的第二个输入中捕获负无穷大值,如下图所示。第二个输入是attention_mask 。我更深入地研究了一下,发现attention_mask对于填充元素应该有非常大的负值。巧合的是,负无穷大值位于每个序列的开头。这一观察使我相信负无穷值应该发生在这一层。进一步的调查还表明,无穷大值不会在接下来的几层中导致更多的无穷大值。因此, LlamaDecoderLayer的溢出很可能不是训练损失异常的根本原因。



接下来,我检查了每一层的输出。很明显,最后一层的输出已经溢出,如下图所示。我认为这是由于 int-8 权重的精度有限造成的(或者float16的范围有限。 bfloat16很可能可以避免这个问题)。



为了解决溢出问题,我在训练时使用了float16。 V100 没有足够的 VRAM 来训练 13B 模型,除非使用一些技巧。 Hugging Face DeepSpeed提供了多种方法(例如 CPU 卸载)来减少训练 VRAM 使用量。但最简单的技巧是在训练开始之前调用model.gradient_checkpointing_enable()来启用梯度检查点。


梯度检查点以训练速度为代价换取更少的 VRAM 使用。通常,在前向传递期间,计算激活并将其存储在内存中以供在后向传递期间使用。这会占用额外的内存。然而,使用梯度检查点,不是在前向传递期间存储激活,而是在后向传递期间重新计算它们,从而节省 VRAM。这是一篇关于此技术的好文章


我能够使用 float16 和启用梯度检查点来训练 Llama 13B:

 python finetune.py \ --base_model=yahma/llama-13b-hf \ --num_epochs=10 \ --output_dir 'your/output/dir' \ --lora_target_modules='[q_proj,k_proj,v_proj,o_proj]' \ --cutoff_len=1024 \ --lora_r=16 \ --micro_batch_size=4 \ --batch_size=128 \ --wandb_project 'alpaca_lora_13b' \ --train_on_inputs=False


13B模型可以处理一些高级任务,例如名称实体识别。我使用示例提示进行测试,这是 13B 模型的准确响应:

一切都好!这是一个令人兴奋的开始。该模型允许我们使用 LangChain 创建复杂的应用程序。


目前,我们仍然缺少自动模型评估的工具。我们可以使用语言模型评估工具在许多测试用例上评估我们的模型,甚至创建我们自己的测试用例。 Hugging Face 的 Open LLM 排行榜也使用了同样的工具。虽然评估是法学硕士发展的一个重要方面,但本文仅关注培训过程。我可能会在以后的文章中讨论评估。


概括

在本文中,我们介绍了大型基础模型 (LFM) 的概念以及几种使 LFM 表现理想的微调方法。然后我们重点介绍了 LoRA(一种用于微调 LFM 的参数高效方法),并解释了微调代码以及性能改进技术。最后,我们更进一步,成功在 V100 GPU 上训练了 Llama 13B 模型。虽然13B模型训练遇到了一些问题,但我们发现这些问题是由硬件限制造成的,并提出了解决方案。最后,我们得到了一个经过微调的、有效的法学硕士,但我们还没有定量评估法学硕士的表现。



关于作者


你好呀!我叫魏。我是一名专注的问题解决者、 ABB的高级人工智能专家和分析项目负责人,以及机器学习 Google 开发专家。我拥有明尼苏达大学双城分校机械工程硕士学位和伊利诺伊大学厄巴纳-香槟分校机械工程学士学位。


我的技术堆栈专注于 Python / C# 编程、计算机视觉、机器学习、算法和微服务,但我也有广泛的兴趣,例如游戏开发 (Unity)、前端/后端开发、技术领导力、修补单板计算机和机器人。


我希望这篇文章能够以某种方式帮助人们。感谢您的阅读,祝您解决问题愉快!