
大型语言模型 (LLM) 彻底改变了自然语言处理领域,它展示了先进的功能和复杂的解决方案。这些模型在广泛的文本数据集上进行训练,在文本生成、翻译、摘要和问答等任务中表现出色。尽管它们很强大,但 LLM 可能并不总是与特定的任务或领域保持一致。
在本教程中,我们将探讨微调 LLM 如何显著提高模型性能、降低训练成本并实现更准确和特定于上下文的结果。
什么是LLM微调?
微调 LLM 涉及对预先存在的模型进行额外训练,该模型以前使用较小的特定领域数据集从广泛的数据集中获取模式和特征。在“LLM 微调”的上下文中,LLM 表示“大型语言模型”,例如 OpenAI 的 GPT 系列。这种方法具有重要意义,因为从头开始训练大型语言模型在计算能力和时间方面都是高度资源密集型的。利用嵌入在预训练模型中的现有知识,可以在特定任务上实现高性能,同时大大减少数据和计算要求。
以下是 LLM 微调中涉及的一些关键步骤:
选择预训练模型:对于 LLM 微调,第一步是仔细选择符合我们所需架构和功能的基本预训练模型。预训练模型是在大量未标记数据上训练的通用用途模型。
收集相关数据集:然后我们需要收集与我们的任务相关的数据集。数据集应以模型可以从中学习的方式进行标记或结构化。
预处理数据集:数据集准备就绪后,我们需要通过清理数据集、将其拆分为训练集、验证集和测试集,并确保它与我们想要微调的模型兼容,从而进行一些预处理以进行微调。
微调:选择预训练模型后,我们需要在预处理的相关数据集上对其进行微调,该数据集更具体地针对手头的任务。我们将选择的数据集可能与特定领域或应用程序相关,从而允许模型适应和专门化该上下文。
特定任务的适配:在微调过程中,模型的参数会根据新数据集进行调整,帮助模型更好地理解和生成与特定任务相关的内容。此过程保留了在预训练期间获得的一般语言知识,同时根据目标域的细微差别定制模型。
微调 LLM 通常用于自然语言处理任务,例如情感分析、命名实体识别、摘要、翻译或任何其他理解上下文和生成连贯语言至关重要的应用程序。它有助于利用预先训练模型中编码的知识来执行更专业和特定于领域的任务。
微调方法
微调大型语言模型 (LLM) 涉及监督学习过程。在这种方法中,使用包含标记样本的数据集来调整模型的权重,从而提高其在特定任务中的熟练程度。现在,让我们深入研究微调过程中采用的一些值得注意的技术。
完全微调(指令微调):指令微调是一种策略,通过在指导模型响应查询的示例上训练模型来增强模型在各种任务中的性能。数据集的选择至关重要,并且根据特定任务(例如摘要或翻译)量身定制。这种方法称为完全微调,可更新所有模型权重,从而创建具有改进功能的新版本。但是,它需要足够的内存和计算资源,类似于预训练,以处理训练期间梯度、优化器和其他组件的存储和处理。
参数高效微调 (PEFT) 是一种指令微调形式,比完全微调效率高得多。训练语言模型,尤其是用于完整的 LLM 微调,需要大量的计算资源。内存分配不仅需要存储模型,还需要在训练期间用于基本参数,这对简单的硬件提出了挑战。。这减少了可训练参数的数量,使内存需求更易于管理,并防止灾难性遗忘。与完全微调不同,PEFT 保留了原始的 LLM 权重,避免了先前学习的信息丢失。事实证明,这种方法有助于在对多个任务进行微调时处理存储问题。有多种方法可以实现参数高效微调。低秩自适应 LoRA 和 QLoRA 是使用最广泛和最有效的。
PEFT通过仅更新参数的子集来解决这个问题,有效地“冻结”了其余参数
什么是LoRa?
LoRA 是一种改进的微调方法,它不是微调构成预训练大型语言模型权重矩阵的所有权重,而是微调近似于这个较大矩阵的两个较小矩阵。这些矩阵构成了 LoRA 适配器。然后,将此微调的适配器加载到预训练模型中并用于推理。
在针对特定任务或用例进行 LoRA 微调后,结果是原始 LLM 保持不变,并且出现了一个相当小的“LoRA 适配器”,通常代表原始 LLM 大小的个位数百分比(以 MB 而不是 GB 为单位)。
在推理过程中,LoRA 适配器必须与其原始 LLM 结合使用。其优势在于许多 LoRA 适配器能够重用原始 LLM,从而在处理多个任务和用例时降低总体内存需求。
什么是量化 LoRA (QLoRA)?
QLoRA 代表了 LoRA 内存效率更高的迭代。QLoRA 将 LoRA 的权重(较小的矩阵)量化为更低的精度(例如,4 位而不是 8 位),使 LoRA 更进一步。这进一步减少了内存占用和存储要求。在 QLoRA 中,预训练模型以量化的 4 位权重加载到 GPU 内存中,而 LoRA 中使用的是 8 位。尽管比特精度有所降低,但 QLoRA 仍保持着与 LoRA 相当的有效性水平。
在本教程中,我们将对 QLoRA 使用参数高效微调。
现在,让我们探讨如何在单个 GPU 上使用 QLoRA 在自定义数据集上微调 LLM。
设置记事簿
安装所需的库
加载数据集
创建 Bitsandbytes 配置
加载预训练模型
代币化
使用零样本推理测试模型
预处理数据集
为 QLoRA 准备模型
设置 PEFT 以进行微调
训练PEFT适配器
定性评估模型(人工评估)
定量评估模型(使用 ROUGE 指标)
1. 设置记事本。
虽然我们将使用 Kaggle 笔记本进行此演示,但请随意使用任何 Jupyter 笔记本环境。Kaggle 每周提供 30 小时的免费 GPU 使用时间,这对于我们的实验来说已经足够了。首先,让我们打开一个新笔记本,建立一些标题,然后继续连接到运行时。

在这里,我们将选择 GPU P100 作为加速器。 您可以随意尝试 Kaggle 或任何其他环境中提供的其他 GPU 选项。
在本教程中,我们将使用 HuggingFace 库来下载和训练模型。要从 HuggingFace 下载模型,我们需要一个访问令牌。如果您已经注册了 HuggingFace,则可以从设置部分生成新的访问令牌或使用任何现有的访问令牌。
2. 安装所需的库
现在,让我们安装此实验所需的库。
!pip install -q -U bitsandbytes transformers peft accelerate datasets scipy einops evaluate trl rouge_score
让我们了解其中一些库的重要性。
Bitsandbytes:一个出色的软件包,它为自定义 CUDA 函数提供了一个轻量级包装器,使 LLM 运行得更快——优化器、矩阵乘法和量化。在本教程中,我们将使用此库尽可能高效地加载模型。
transformers:Hugging Face(🤗)的一个库,为各种自然语言处理任务提供预训练模型和训练实用程序。
peft:Hugging Face (🤗) 的一个库,可实现参数高效的微调。
加速:精确地加速抽象,并且只加速与多 GPU/TPU/fp16 相关的样板代码,并保持代码的其余部分不变。
datasets:Hugging Face (🤗) 的另一个库,可轻松访问各种数据集。
einops:简化张量操作的库。
加载所需的库
from datasets import load_datasetfrom transformers import (AutoModelForCausalLM,AutoTokenizer,BitsAndBytesConfig,HfArgumentParser,AutoTokenizer,TrainingArguments,Trainer,GenerationConfig)from tqdm import tqdmfrom trl import SFTTrainerimport torchimport timeimport pandas as pdimport numpy as npfrom huggingface_hub import interpreter_logininterpreter_login()
在本教程中,我们不会跟踪训练指标,因此让我们禁用权重和偏差。 W&B平台是用于监控、可视化数据和模型以及传达结果的强大组件的基本集合。要在微调过程中停用权重和偏差,请设置以下环境属性。
import os# disable Weights and Biasesos.environ['WANDB_DISABLED']="true"
如果您有 Weights and Biases 帐户,请随时启用它并对其进行试验。
3. 加载数据集
许多数据集可用于微调模型。在本例中,我们将利用 HuggingFace 的 DialogSum DataSet 进行微调过程。DialogSum 是一个广泛的对话摘要数据集,包含 13,460 个对话以及手动标记的摘要和主题。
选择此数据集没有具体原因。随意尝试使用任何自定义数据集进行此实验。
让我们执行以下代码,从 HuggingFace 加载上述数据集。
huggingface_dataset_name = "neil-code/dialogsum-test"dataset = load_dataset(huggingface_dataset_name)
加载数据集后,我们可以查看它以了解它包含的内容:

它包含以下字段。
对话:对话的文本。
摘要:对话的人工编写摘要。
主题:人类书面主题/对话的单行。
id:示例的唯一文件 ID。
4. 创建 Bitsandbytes 配置
为了加载模型,我们需要一个配置类来指定我们希望如何执行量化。我们将使用 BitsAndBytesConfig 以 4 位格式加载模型。这将大大减少内存消耗,但会牺牲一些准确性。
compute_dtype = getattr(torch, "float16")bnb_config = BitsAndBytesConfig(load_in_4bit=True,bnb_4bit_quant_type='nf4',bnb_4bit_compute_dtype=compute_dtype,bnb_4bit_use_double_quant=False,)
5. 加载预训练模型
Microsoft 最近开源了 Phi-2,这是一个具有 27 亿个参数的小型语言模型 (SLM)。在这里,我们将使用 Phi-2 进行微调过程。该语言模型表现出卓越的推理和语言理解能力,在基本语言模型中实现了最先进的性能。
现在让我们使用 HuggingFace 的 4 位量化加载 Phi-2。
model_name='microsoft/phi-2'device_map = {"": 0}original_model = AutoModelForCausalLM.from_pretrained(model_name,device_map=device_map,quantization_config=bnb_config,trust_remote_code=True,use_auth_token=True)
该模型使用 bitsandbytes 库中的“BitsAndBytesConfig”以 4 位加载。这是 QLoRA 过程的一部分,该过程涉及将模型的预训练权重量化为 4 位,并在微调期间保持固定。
6. 代币化
现在,让我们配置分词器,结合左填充以优化训练期间的内存使用。
tokenizer = AutoTokenizer.from_pretrained(model_name,trust_remote_code=True,padding_side="left",add_eos_token=True,add_bos_token=True,use_fast=False)tokenizer.pad_token = tokenizer.eos_token
7. 使用零样本推理测试模型
我们将使用一些示例输入来评估上面加载的基本模型。
%%timefrom transformers import set_seedseed = 42set_seed(seed)index = 10prompt = dataset['test'][index]['dialogue']summary = dataset['test'][index]['summary']formatted_prompt = f"Instruct: Summarize the following conversation.\n{prompt}\nOutput:\n"res = gen(original_model,formatted_prompt,100,)#print(res[0])output = res[0].split('Output:\n')[1]dash_line = '-'.join('' for x in range(100))print(dash_line)print(f'INPUT PROMPT:\n{formatted_prompt}')print(dash_line)print(f'BASELINE HUMAN SUMMARY:\n{summary}\n')print(dash_line)print(f'MODEL GENERATION - ZERO SHOT:\n{output}')

从上面的观察结果可以看出,与基线摘要相比,该模型在总结对话方面面临挑战。然而,它设法从文本中提取基本信息,这表明有可能针对手头的特定任务微调模型。
8. 预处理数据集
数据集不能直接用于微调。必须以模型可以理解的方式格式化提示。参考 HuggingFace 模型文档,很明显需要使用下面指定格式的对话和摘要生成提示。

我们将创建一些辅助函数来格式化输入数据集,确保其适用于微调过程。在这里,我们需要将对话摘要(提示-响应)对转换为 LLM 的显式指令。
def create_prompt_formats(sample):"""Format various fields of the sample ('instruction','output')Then concatenate them using two newline characters:param sample: Sample dictionnary"""INTRO_BLURB = "Below is an instruction that describes a task. Write a response that appropriately completes the request."INSTRUCTION_KEY = "### Instruct: Summarize the below conversation."RESPONSE_KEY = "### Output:"END_KEY = "### End"blurb = f"\n{INTRO_BLURB}"instruction = f"{INSTRUCTION_KEY}"input_context = f"{sample['dialogue']}" if sample["dialogue"] else Noneresponse = f"{RESPONSE_KEY}\n{sample['summary']}"end = f"{END_KEY}"parts = [part for part in [blurb, instruction, input_context, response, end] if part]formatted_prompt = "\n\n".join(parts)sample["text"] = formatted_promptreturn sample
上面的函数可以用来将我们的输入转换为提示格式。
现在,我们将使用模型分词器将这些提示处理为标记化提示。
我们的目标是生成长度一致的输入序列,这有利于通过优化效率和最小化计算开销来微调语言模型。必须确保这些序列不会超过模型的最大令牌限制。
from functools import partial# SOURCE https://github.com/databrickslabs/dolly/blob/master/training/trainer.pydef get_max_length(model):conf = model.configmax_length = Nonefor length_setting in ["n_positions", "max_position_embeddings", "seq_length"]:max_length = getattr(model.config, length_setting, None)if max_length:print(f"Found max lenth: {max_length}")breakif not max_length:max_length = 1024print(f"Using default max length: {max_length}")return max_lengthdef preprocess_batch(batch, tokenizer, max_length):"""Tokenizing a batch"""return tokenizer(batch["text"],max_length=max_length,truncation=True,)# SOURCE https://github.com/databrickslabs/dolly/blob/master/training/trainer.pydef preprocess_dataset(tokenizer: AutoTokenizer, max_length: int,seed, dataset):"""Format & tokenize it so it is ready for training:param tokenizer (AutoTokenizer): Model Tokenizer:param max_length (int): Maximum number of tokens to emit from tokenizer"""# Add prompt to each sampleprint("Preprocessing dataset...")dataset = dataset.map(create_prompt_formats)#, batched=True)# Apply preprocessing to each batch of the dataset & and remove 'instruction', 'context', 'response', 'category' fields_preprocessing_function = partial(preprocess_batch, max_length=max_length, tokenizer=tokenizer)dataset = dataset.map(_preprocessing_function,batched=True,remove_columns=['id', 'topic', 'dialogue', 'summary'],)# Filter out samples that have input_ids exceeding max_lengthdataset = dataset.filter(lambda sample: len(sample["input_ids"]) < max_length)# Shuffle datasetdataset = dataset.shuffle(seed=seed)return dataset
通过利用这些功能,我们的数据集将为微调过程做好准备!
# 2 - Using the prepare_model_for_kbit_training method from PEFT# Preparing the Model for QLoRAoriginal_model = prepare_model_for_kbit_training(original_model)
9. 为 QLoRA 准备模型
# 2 - Using the prepare_model_for_kbit_training method from PEFT# Preparing the Model for QLoRAoriginal_model = prepare_model_for_kbit_training(original_model)
在这里,模型是使用 'prepare_model_for_kbit_training()' 函数为 QLoRA 训练准备的。此函数通过设置必要的配置来初始化 QLoRA 的模型。
10. 设置 PEFT 进行微调
现在让我们定义用于微调基本模型的 LoRA 配置。
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_trainingconfig = LoraConfig(r=32, #Ranklora_alpha=32,target_modules=['q_proj','k_proj','v_proj','dense'],bias="none",lora_dropout=0.05, # Conventionaltask_type="CAUSAL_LM",)# 1 - Enabling gradient checkpointing to reduce memory usage during fine-tuningoriginal_model.gradient_checkpointing_enable()peft_model = get_peft_model(original_model, config)
请注意 rank (r) 超参数,它定义了要训练的适配器的排名/维度。r 是适配器中使用的低秩矩阵的秩,因此控制训练的参数数量。更高的排名将允许更多的表现力,但需要权衡计算。
alpha 这里是学习权重的比例因子。权重矩阵按 alpha/r 进行缩放,因此 alpha 值越高,LoRA 激活的权重就越大。
一旦一切设置好并准备好了 PEFT,我们就可以使用 print_trainable_parameters() 辅助函数来查看模型中有多少可训练参数。
print(print_number_of_trainable_model_parameters(peft_model))
11. 训练PEFT适配器
定义训练参数并创建实例。Trainer
output_dir = f'./peft-dialogue-summary-training-{str(int(time.time()))}'import transformerspeft_training_args = TrainingArguments(output_dir = output_dir,warmup_steps=1,per_device_train_batch_size=1,gradient_accumulation_steps=4,max_steps=1000,learning_rate=2e-4,optim="paged_adamw_8bit",logging_steps=25,logging_dir="./logs",save_strategy="steps",save_steps=25,evaluation_strategy="steps",eval_steps=25,do_eval=True,gradient_checkpointing=True,report_to="none",overwrite_output_dir = 'True',group_by_length=True,)peft_model.config.use_cache = Falsepeft_trainer = transformers.Trainer(model=peft_model,train_dataset=train_dataset,eval_dataset=eval_dataset,args=peft_training_args,data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),)
在这里,我们使用了 1000 个训练步骤。对于我们的自定义数据集来说,这似乎已经足够好了。在完成训练步骤之前,我们需要尝试不同的数字。此外,上面使用的超参数可能会因我们尝试微调的数据集/模型而异。这只是为了显示微调的能力。
让我们现在开始训练。训练模型需要一些时间,具体取决于 TrainingArguments 中使用的超参数。
peft_trainer.train()
一旦模型训练成功,我们就可以用它来推理了。现在,让我们通过向原始 Phi-2 模型添加适配器来准备推理模型。在这里,我们之所以进行设置,是因为计划只是使用这个 PEFT 模型进行推理。is_trainable=False
import torchfrom transformers import AutoTokenizer, AutoModelForCausalLMbase_model_id = "microsoft/phi-2"base_model = AutoModelForCausalLM.from_pretrained(base_model_id,device_map='auto',quantization_config=bnb_config,trust_remote_code=True,use_auth_token=True)
eval_tokenizer = AutoTokenizer.from_pretrained(base_model_id, add_bos_token=True, trust_remote_code=True, use_fast=False)eval_tokenizer.pad_token = eval_tokenizer.eos_token
from peft import PeftModelft_model = PeftModel.from_pretrained(base_model, "/kaggle/working/peft-dialogue-summary-training-1705417060/checkpoint-1000",torch_dtype=torch.float16,is_trainable=False)
微调通常是一个迭代过程。根据验证和测试集结果,我们可能需要对模型的架构、超参数或训练数据进行进一步调整,以提高其性能。现在让我们看看如何评估微调 LLM 的结果。
12. 定性评估模型(人工评估)
现在,让我们使用相同的输入,但使用 PEFT 模型执行推理,就像我们之前在步骤 7 中使用原始模型所做的那样。
%%timefrom transformers import set_seedset_seed(seed)index = 5dialogue = dataset['test'][index]['dialogue']summary = dataset['test'][index]['summary']prompt = f"Instruct: Summarize the following conversation.\n{dialogue}\nOutput:\n"peft_model_res = gen(ft_model,prompt,100,)peft_model_output = peft_model_res[0].split('Output:\n')[1]#print(peft_model_output)prefix, success, result = peft_model_output.partition('###')dash_line = '-'.join('' for x in range(100))print(dash_line)print(f'INPUT PROMPT:\n{prompt}')print(dash_line)print(f'BASELINE HUMAN SUMMARY:\n{summary}\n')print(dash_line)print(f'PEFT MODEL:\n{prefix}')

13. 定量评估模型(使用 ROUGE Metric)
ROUGE,即 Recall-Oriented Understudy for Gisting Evaluation,是一组指标和软件包,用于评估自然语言处理中的自动摘要和机器翻译软件。这些指标将自动生成的摘要或翻译与参考文献或一组参考文献(人工生成的)摘要或翻译进行比较。
现在,让我们使用 **ROUGE** 指标来量化模型生成的摘要的有效性。它将摘要与通常由人类创建的“基线”摘要进行比较。虽然它不是一个完美的指标,但它确实表明我们通过微调实现了摘要有效性的整体提高。
为了展示ROUGE指标评估的能力,我们将使用一些样本输入进行评估。
original_model = AutoModelForCausalLM.from_pretrained(base_model_id,device_map='auto',quantization_config=bnb_config,trust_remote_code=True,use_auth_token=True)
import pandas as pddialogues = dataset['test'][0:10]['dialogue']human_baseline_summaries = dataset['test'][0:10]['summary']original_model_summaries = []instruct_model_summaries = []peft_model_summaries = []for idx, dialogue in enumerate(dialogues):human_baseline_text_output = human_baseline_summaries[idx]prompt = f"Instruct: Summarize the following conversation.\n{dialogue}\nOutput:\n"original_model_res = gen(original_model,prompt,100,)original_model_text_output = original_model_res[0].split('Output:\n')[1]peft_model_res = gen(ft_model,prompt,100,)peft_model_output = peft_model_res[0].split('Output:\n')[1]print(peft_model_output)peft_model_text_output, success, result = peft_model_output.partition('###')original_model_summaries.append(original_model_text_output)peft_model_summaries.append(peft_model_text_output)zipped_summaries = list(zip(human_baseline_summaries, original_model_summaries, peft_model_summaries))df = pd.DataFrame(zipped_summaries, columns = ['human_baseline_summaries', 'original_model_summaries', 'peft_model_summaries'])df
import evaluaterouge = evaluate.load('rouge')original_model_results = rouge.compute(predictions=original_model_summaries,references=human_baseline_summaries[0:len(original_model_summaries)],use_aggregator=True,use_stemmer=True,)peft_model_results = rouge.compute(predictions=peft_model_summaries,references=human_baseline_summaries[0:len(peft_model_summaries)],use_aggregator=True,use_stemmer=True,)print('ORIGINAL MODEL:')print(original_model_results)print('PEFT MODEL:')print(peft_model_results)print("Absolute percentage improvement of PEFT MODEL over ORIGINAL MODEL")improvement = (np.array(list(peft_model_results.values())) - np.array(list(original_model_results.values())))for key, value in zip(peft_model_results.keys(), improvement):print(f'{key}: {value*100:.2f}%')

从上述结果中可以看出,与以百分比表示的原始模型相比,PEFT模型有了显着的改进。
如果您想访问完整的笔记本,请参阅下面的存储库。
https://www.kaggle.com/code/dassum/finetune-phi-2-on-custom-dataset
结论
微调大型语言模型 (LLM) 对于寻求优化其运营流程的企业来说至关重要。虽然 LLM 的初始训练传授了广泛的语言理解,但微调过程将这些模型改进为能够处理特定主题并提供更准确结果的专用工具。为不同的任务、行业或数据集定制 LLM 扩展了这些模型的功能,确保了它们在动态数字环境中的相关性和价值。展望未来,LLM 的持续探索和创新,加上改进的微调方法,有望推动更智能、更高效和上下文感知的人工智能系统的发展。
参考链接:
1. https://huggingface.co/microsoft/phi-2
2.https://www.superannotate.com/blog/llm-fine-tuning
3.https://huggingface.co/microsoft/phi-2/discussions/19
4.https://www.microsoft.com/en-us/research/blog/phi-2-the-surprising-power-of-small-language-models/
5.https://ai.stackexchange.com/questions/41485/while-fine-tuning-a-decoder-only-llm-like-llama-on-chat-dataset-what-kind-of-pa
6.https://huggingface.co/docs/peft/conceptual_guides/lora
7.https://github.com/TimDettmers/bitsandbytes

请用个人微信添加





