使用QLoRA对Llama 2进行微调是我们常用的一个方法,但是在微调时会遇到各种各样的问题,所以在本文中,将尝试以详细注释的方式给出一些常见问题的答案。这些问题是特定于代码的,大多数注释都是针对所涉及的开源库以及所使用的方法和类的问题。 导入库对于大模型,第一件事是又多了一些不熟悉的Python库。
我们必须首先安装accelerate, peft, bitsandbytes, transformers和trl。除了transformers,其他的库都很陌生。 transformers是这里最古老的库,PyPI上最早的版本(2.0.0)可以追溯到2019年。它是huggingface发布的库,可以快速访问文本,图像和音频(从hugs的API下载)的机器学习模型。它还提供训练和微调模型的功能,并可以HuggingFace模型中心共享这些模型。库没有像Pytorch或Tensorflow那样从头开始构建神经网络的抽象层和模块,它提供了专门针对模型进行优化的训练和推理api。transformer是用于LLM微调的关键Python库之一,因为目前大部分的LLM都是可以通过它来加载使用。
我们继续分析导入: torch是我们很熟悉的深度学习库,这里我们不需要torch的那些低级功能,但是它是transformers和trl的依赖,在这里我们需要使用torch来获取dtypes(数据类型),比如torch.Float16以及检查GPU的工具函数。 load_dataset所做的就是加载数据集,但是它从HuggingFace数据集中心下载到本地。所以这是一个在线加载程序,但它既高效又简单,只需要一行代码。
因为模型很多所以transformer库提供了一组称为Auto classes的类,这些类给出了预训练模型的名称/路径,它可以自动推断出正确的结构并检索相关模型。这个AutoModelForCausalLM是一个通用的Auto类,用于加载用于因果语言建模的模型。 对于transformers,HuggingFace提供了两种类型的语言建模,因果和掩码掩蔽。因果语言模型包括;GPT-3和Llama,这些模型预测标记序列中的下一个标记,以生成与输入数据语义相似的文本。AutoModelForCausalLM类将从模型中心检索因果模型,并加载模型权重,从而初始化模型。from_pretrained()方法为我们完成了这项工作。 model_name = 'NousResearch/Llama-2-7b-chat-hf' model = AutoModelForCausalLM.from_pretrained( model_name, device_map=device_map ) AutoTokenizer是对文本数据进行标记化。它提供了一种无需显式指定标记器类就可以初始化和使用不同模型的标记器的方便的方法。它也是一个通用的Auto类,所以它可以根据提供的模型名称或路径自动选择适当的标记器。标记器将输入文本转换为标记,这些标记是NLP模型使用的基本文本单位。它还提供了额外的功能,如填充、截断和注意力掩码等。AutoTokenizer简化了为NLP任务对文本数据进行标记的过程。我们可以看到在下面初始化AutoTokenizer,后面我们会使用SFTTrainer将初始化的AutoTokenizer作为参数。
BitsAndBytesConfig,前面已经说了我们使用bitsandbytes进行量化。transformer库最近添加了对bitsandbytes的全面支持,因此使用BitsandBytesConfig可以配置bitsandbytes提供的任何量化方法,例如LLM.int8、FP4和NF4。 将量化配置传递给AutoModelForCausalLM初始化器,这样在加载模型权重时就会直接使用量化的方法。
TrainingArguments非常简单。它用于存储SFTTrainer的所有训练参数。SFFTrainer接受不同类型的参数,TrainingArguments帮助我们将所有相关的训练参数组织到一个数据类中保持代码的整洁和有组织。 还有一些很好的工具类可以与TrainingArguments一起使用,比如HfArgumentParser可以为TrainingArguments创建一个参数解析器,这对CLI应用程序很有用。
在完成微调之后,我们将使用pipeline进行推理。可以选择各种管道任务的列表,像“图像分类”,“文本摘要”等。还可以为任务选择要使用的模型。为了定制也可以添加一个参数来进行某种形式的预处理,如标记化或特征提取。
从transformer导入的最后一个内容是logging。这是一个日志系统,这在调试代码时非常有用。 logging.set_verbosity(logging.CRITICAL) 从peft库中导入的LoraConfig数据类是一个配置类,它主要存储初始化LoraModel所需的配置,LoraModel是PeftTuner的一个实例。我们将此配置传递给SFTTrainer,它将使用该配置初始化适当的tuner。
PeftModel,一旦我们使用一种peft方法(如LoRA)进行微调,就需要将LoRA适配器权重保存到磁盘并在使用时将它们加载回内存。PEFT模块微调的权重,与基本模型权重是分开。使用PeftModel,还可以选择将将base_model权重与新微调的适配器权重合并(调整),这样就得到了一个完整的新模型。 PeftModel.from_pretrained()从内存中加载适配器权重,merge_and_unload()方法将它们与base_model合并。
最后一个导入是SFTTrainer。SFTTrainer是transformer Trainer类的子类。Trainer是一个功模型训练的泛化API。SFTTrainer在此基础上增加了对参数微调的支持。有监督的微调步骤是训练因果语言模型(如Llama)用于下游任务(如指令遵循)的关键步骤。 SFTTrainer支持PEFT,因此我们将与LoRA一起使用SFTTrainer。然后,SFTTrainer将使用LoRA执行监督微调。然后我们可以运行训练器(train())并保存权重(save_pretrained())。 #Initialize the SFTTrainer object trainer = SFTTrainer( model=model, train_dataset=dataset, peft_config=peft_config, dataset_text_field='text', max_seq_length=max_seq_length, tokenizer=tokenizer, args=training_arguments, packing=packing, ) # Train model trainer.train()
# Save trained model trainer.model.save_pretrained(new_model) 对于引用,我们也总结了一张图片: 训练参数现在我们知道了需要哪些库来调优Llama 2(或任何LLM),也知道了这些库中需要的类,并且了解了这些类的功能。下面就是对前面导入的参数的介绍。 模型和数据集名称:
model_name、dataset_name和new_model。这些名称遵循HuggingFace模型及其hub上的数据集名称的格式。 birushuo 给一个名字“NousResearch/ Llama-2-7b-chat-hf”这个名字的第一部分NousResearch是一个研究机构,也就是它HuggingFace账户的名称,第二部分是模型名称lama-2 - 7b-chat-hf。模型命名的建议是给模型提供描述性的名称,包括有用的信息,如独特的模型名称(lama-2),关键参数信息(7b),以及一些关于模型如何工作的其他有用信息(chat-hf)。我们在new_model名称llama-2-7b-miniguanaco中看到了同样的规则,这是我们分配给微调模型的名称。这里附加了在miniguanaco上进行微调的数据集的名称。 QLoRA 参数:
我们将使用的参数是r (lora_r)、lora_alpha和lora_dropout。这些参数对于LoRA来说是最重要的,要理解其中的原因,必须深入了解LoRA的论文,我们只做简单的总结: 在神经网络中,反向传播算法计算期望值和实际值之间的误差,然后用这个误差来计算delta,这是神经网络中权重对e的贡献。如果你有一个神经网络的初始权值W0那么对于误差e,我们计算delta_W0 =∆W。然后使用∆W来更新权重W0 +∆W,以减小误差e。LoRA提出将∆W分解为两组低秩矩阵A和B,使W0 +∆W = W0 + BA。而不是使用完整的∆W更新,我们使用较小的低秩更新矩阵BA,这就是我们如何实现相同效率和更低的计算需求。如果∆W的大小为(d × k) (W0的大小),则我们将∆W分解为两个矩阵:B和A,维度分别为(d × r)和(r × k),其中r为秩。 LoraConfig中的参数r (lora_r)是决定更新矩阵BA形状的秩。根据论文可以设置一个小的秩,并且得到很好的结果。当我们更新W0时,可以通过使用缩放因子α来控制BA的影响,这个缩放因子作为学习率。比例因子是我们的第二个参数(lora_alpha)。最后设置lora_dropput,这是正则化的典型dropput。 BitsandBytes参数:
我们正在使用一种称为QLoRA的量化版本的LoRA,这意味着我们希望在LoRA微调中使用量化,将量化应用于我们前面提到的更新权重(以及其他可以量化的操作)。 参数use_4bit(第6行)设置为True,以使用高保真的4位微调,这是后来在QLoRA论文中引入的,以实现比LLM.int8论文中引入的8位量化更低的内存要求。 设置bnb_4bit_compute_dtype(第9行),这是执行计算的数据类型(float16)。也就是说虽然将权重通过4位量化存储,但计算还是发生在16位或32位。 使用bnb_4bit_quant_type(第12行),nf4,根据QLoRA论文,nf4显示了更好的理论和经验性能。 参数use_nested_quant设置为False,并将其传递给bnb_4bit_use_double_quant。模型在第一次量化之后启用第二次量化,从而为每个参数额外节省0.4位。 上面一些参数都是QLoRA的论文提供。 在本文中我们选择NF4量化FP16 (float16)精度进行计算后,我们应该对Colab T4 GPU (16 GB VRAM)没有内存限制。我们做个简单的计算:如果使用Llama-2-7B(70亿params)和FP16(没有量化),我们得到7B × 2字节= 14 GB(所需的VRAM)。使用4位量化,我们得到7B × 0.5字节= ~ 4gb(所需的VRAM)。 训练参数:
Output_dir(第6行):这是设置存储模型预测和检查点的位置,还包括日志 num_train_epochs(第9行):训练的轮次。 fp16和bf16(第12行和第13行):我们将它们都设置为false,因为我们不会使用混合精度训练,因为已经有QLoRA了。 per_device_train_batch_size和per_device_eval_batch_size(第16行和第19行):将它们都设置为4。有足够的内存,可以设置更高的批处理大小(>8),这将加快训练速度。 Gradient_accumulation_steps(第22行):“梯度累积”指的是在实际更新模型权重之前执行的向前和向后传递的次数(更新步骤)。在每一次向前和向后传递期间,梯度被计算并累积在一批数据上。在累积指定步数的梯度之后,然后执行反向传递,计算这些步骤的平均梯度并相应地更新模型权重。这种方法有助于有效地模拟更大批大小,它减少了每次向前和向后传递的内存需求。 max_gradient_norm(第25行):如果梯度的范数(幅度)超过某个阈值(由max_grad_norm参数指定),则梯度裁剪缩小梯度。如果梯度范数大于max_grad_norm,则梯度将按比例缩小,如果梯度规范已经低于max_grad_norm,则不应用缩放。建议从max_grad_norm的较高值开始,然后在多个训练迭代中慢慢缩小它。 learning_rate(第28行):AdamW的学习率。AdamW是流行的Adam优化器的一个变体。它结合了Adam优化器和权重衰减正则化的技术。 weight_decay(第31行):权重衰减,也称为L2正则化或权重正则化,是机器学习和深度学习中常用的一种正则化技术,用于防止模型对训练数据的过拟合。它的工作原理是在损失函数中添加一个惩罚项。我们使用AdamW和权重衰减是有意义的,因为权重衰减在微调期间特别有用,因为它有助于防止过拟合,并确保模型适应新任务,同时保留预训练中的一些知识。 optim(第34行):使用AdamW优化器,“paged_adamw_32bit”似乎是AdamW优化器的一个特定实现或变体,我们找到任何关于他的信息,所以如果你有关于这方面的信息,请在评论中留下,谢谢! lr_scheduler_type(第37行):通常我们在深度学习模型的训练期间使用学习率调度器,以随时间调整学习率。 warmup_ratio(第40行):这里我们将“warmup_ratio”设置为0.03。由于每个epoch有250个训练步骤,热身阶段将持续到前8步(250的3%),在此期间,学习率将从0线性增加到指定的初始值2e-4。热身阶段通常用于稳定训练,防止梯度爆炸,并允许模型开始有效地学习。 group_by_length(第44行):这个参数设置为True,会加快了训练速度。当group_by_length设置为True时,它将训练数据集中大致相同长度的样本分组到同一批中。这意味着具有相似长度的序列被分组在一起,减少了所需的填充。也就是说批将具有更相似长度的序列,这将最小化所应用的填充量。当批处理具有一致的大小时GPU处理通常更有效,从而缩短训练时间,这是从LSTM时代就开始的一个加速技巧。 save_steps和logging_steps(第47行和第50行):这里将两个参数都设置为25,以控制记录训练信息和保存检查点的间隔步骤。 SFTTrainer参数:
最后参数是特定于SFTTrainer的。 max_seq_length:将max_seq_length设置为None允许我们不施加最大序列长度限制,我们不想截断或填充它们到固定长度,因此将max_seq_length设置为None允许我们使用数据中存在的全部序列长度。 packing:根据文档,ConstantLengthDataset使用这个参数来打包数据集的序列。在ConstantLengthDataset上下文中将packing设置为False可以在处理多个简短示例时提高效率,我们的数据集就是这种情况。通过将packing设置为False,允许ConstantLengthDataset将多个短示例打包到单个输入序列中,有效地组合它们。这减少了对大量填充的需求,并提高了内存使用和计算的效率。 加载数据集、基本模型和标记器 device_map = {'': 0}
# Load dataset (you can process it here) dataset = load_dataset(dataset_name, split='train')
# Load tokenizer and model with QLoRA configuration compute_dtype = getattr(torch, bnb_4bit_compute_dtype)
bnb_config = BitsAndBytesConfig( load_in_4bit=use_4bit, bnb_4bit_quant_type=bnb_4bit_quant_type, bnb_4bit_compute_dtype=compute_dtype, bnb_4bit_use_double_quant=use_nested_quant, )
# Check GPU compatibility with bfloat16 if compute_dtype == torch.float16 and use_4bit: major, _ = torch.cuda.get_device_capability() if major >= 8: print('=' * 80) print('Your GPU supports bfloat16: accelerate training with bf16=True') print('=' * 80)
# Load base model model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map=device_map ) model.config.use_cache = False model.config.pretraining_tp = 1
# Load LLaMA tokenizer tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) tokenizer.add_special_tokens({'pad_token': '[PAD]'}) tokenizer.pad_token = tokenizer.eos_token tokenizer.padding_side = 'right'
# Load LoRA configuration peft_config = LoraConfig( lora_alpha=lora_alpha, lora_dropout=lora_dropout, r=lora_r, bias='none', task_type='CAUSAL_LM', ) 第5行加载数据集。然后在第9行使用gettr函数将compute_dtype设置为torch.float16。在第10行,初始化BitsandBytesConfig。 在第17行,我们使用torch.cuda.get_device_capability()函数检查GPU与bfloat16的兼容性。该函数返回支持cuda的GPU设备的计算能力。计算能力表示GPU支持的版本和特性。该函数返回一个由两个整数组成的元组,(major, minor),表示GPU的主要和次要计算能力版本。主要版本表示该计算能力的主要版本,次要版本表示该计算能力的次要版本。例如,如果函数返回(8,0),则表示GPU的计算能力为8.0版本,次要是0。如果GPU是bfloat16兼容的,那么我们将compute_dtype设置为torch.Bfloat16,因为Bfloat16比float16更好的精度。 然后就是使用AutoModelForCausalLM.from_pretrained加载基本模型,在第31行设置了model.config。 use_cache为False,当启用缓存时可以减少变量。禁用缓存则在执行计算的顺序方面引入了一定程度的随机性,这在微调时非常有用。 在第32行设置了model.config.pretraining_tp = 1这里的tp代表张量并行性,根据这里的Llama 2的提示: 设置model.config. pretraining_tp = 1不等于1的值将激活更准确但更慢的线性层计算,这应该更好地匹配原始概率。 然后就是使用model_name加载Llama标记器。如果你看一下NousResearch/ lama-2的文件,你会注意到有一个tokenizer. model文件。使用model_name, AutoTokenizer可以下载该标记器。 在第36行,调用add_special_tokens({' pad_token ': ' [PAD] '})这是另一个重要代码,因为我们数据集中的文本长度可以变化,批处理中的序列可能具有不同的长度。为了确保批处理中的所有序列具有相同的长度,需要将填充令牌添加到较短的序列中。这些填充标记通常是没有任何含义的标记,例如<pad>。 在第37行,我们设置tokenizer. pad_token = tokenizer. eos_token。将pad令牌与EOS令牌对齐,并使我们的令牌器配置更加一致。两个令牌(pad_token和eos_token)都有指示序列结束的作用。设置成一个简化了标记化和填充逻辑。 在第38行,设置填充边,将填充边设置为右可以修复溢出问题。 最后在第41行,我们初始化了LoraConfig: 训练
这里新加的一个参数是第27行的dataset_text_field= ' text '。dataset_text_field参数用于指示数据集中哪个字段包含作为模型输入的文本数据。它使datasets 库能够基于该字段中的文本数据自动创建ConstantLengthDataset,简化数据准备过程。 HuggingFace生态系统是一个紧密结合的库生态系统,它在后台为你自动化了很多工作。 推理
第6行,管道初始化。然后在第7行使用管道,传递使用第5行提示符构造的输入文本。我们使用<s>来指示序列的开始,而添加[INST]和[/INST]作为控制令牌来指示用户消息的开始和结束。 用适配器权重重新加载基本模型
在第2行使用AutoModelForCausalLM.from_pretrained来(重新)加载基本模型。我们将在没有任何量化配置的情况下执行此操作,因为我们不需要对其进行微调,只是想将其与适配器合并。还在第13行重新加载标记器,并进行与之前在第13 - 14行中所做的相同的修改。 保存最后我们将刚刚经过微调的模型及其标记器保存到本地或者上传到HuggingFace。
总结peft,tramsformers等库简化了我们对于大模型开发的工作流程,并且不需要很多的专业知识也可以对大模型进行微调。但是要得到一个好的模型是一个漫长的过程,就像我们上面的代码一样,看似简单实则复杂,不仅要了解方法的原理,还要通过查看论文了解每一个参数的含义。 本文是一个良好的开端,因为可以把我们在这里学到的大部分东西应用到微调任何LLM的任务中。关于微调Llama 2,我们的流程已经介绍完毕了,但是我们如何才能正确地评估我们的微调性能?能否在不花费太多的情况下调整更大的模型(70B) ?使用更大的数据集?模型怎么部署呢?我们会在后续的文章中进行介绍。 作者:Ogban Ugot |
|