NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
大型语言模型 (LLM) 彻底改变了自然语言处理领域,这些模型展示了先进的功能和复杂的解决方案。这些模型在大量文本数据集上进行训练,在文本生成、翻译、摘要和问答等任务中表现出色。尽管 LLM 功能强大,但它们可能并不总是与特定任务或领域保持一致。
在本教程中,我们将探讨如何通过微调 LLM 来显著提高模型性能、降低训练成本,并获得更准确、更符合上下文的结果。
1、LLM 微调概述
微调 LLM 涉及使用较小的特定领域数据集对预先存在的模型进行额外训练,该模型先前已从大量数据集中获取模式和特征。在“LLM 微调”的背景下,LLM 表示“大型语言模型”,例如 OpenAI 的 GPT 系列。这种方法具有重要意义,因为从头开始训练大型语言模型在计算能力和时间方面都非常耗费资源。利用预训练模型中嵌入的现有知识,可以在数据和计算要求大幅减少的情况下,在特定任务上实现高性能。
以下是 LLM 微调所涉及的一些关键步骤:
- 选择预训练模型:对于 LLM 微调,第一步是仔细选择符合我们所需架构和功能的基本预训练模型。预训练模型是通用模型,已在大量未标记数据上进行训练。
- 收集相关数据集:然后我们需要收集与我们的任务相关的数据集。数据集应以模型可以从中学习的方式进行标记或结构化。
- 预处理数据集:数据集准备好后,我们需要进行一些预处理以进行微调,方法是清理它,将其拆分为训练、验证和测试集,并确保它与我们想要微调的模型兼容。
- 微调:选择预训练模型后,我们需要在预处理的相关数据集上对其进行微调,该数据集更具体到手头的任务。我们选择的数据集可能与特定领域或应用程序相关,从而使模型能够适应并专门针对该上下文。
- 特定任务的适应:在微调过程中,模型的参数会根据新数据集进行调整,帮助其更好地理解和生成与特定任务相关的内容。此过程保留了在预训练期间获得的一般语言知识,同时根据目标领域的细微差别定制模型。
微调 LLM 通常用于自然语言处理任务,例如情绪分析、命名实体识别、摘要、翻译或任何其他理解上下文和生成连贯语言至关重要的应用程序。它有助于利用预训练模型中编码的知识来完成更专业和特定领域的任务。
1.1 微调方法
微调大型语言模型 (LLM) 涉及监督学习过程。在此方法中,使用包含标记示例的数据集来调整模型的权重,从而提高其在特定任务中的熟练程度。现在,让我们深入研究微调过程中采用的一些值得注意的技术。
完全微调(指令微调):指令微调是一种通过对指导其响应查询的示例进行训练来增强模型在各种任务中性能的策略。数据集的选择至关重要,并针对特定任务(例如摘要或翻译)进行量身定制。这种方法称为完全微调,可更新所有模型权重,从而创建具有改进功能的新版本。但是,它需要足够的内存和计算资源,类似于预训练,以处理训练期间梯度、优化器和其他组件的存储和处理。
参数高效微调 (PEFT) 是一种指令微调形式,比完全微调效率高得多。训练语言模型,尤其是完整的 LLM 微调,需要大量的计算资源。内存分配不仅需要用于存储模型,还需要用于训练期间的基本参数,这对简单的硬件来说是一个挑战。PEFT 通过仅更新参数子集来解决这个问题,有效地“冻结”其余参数。这减少了可训练参数的数量,使内存需求更易于管理并防止灾难性遗忘。与完全微调不同,PEFT 保留原始 LLM 权重,避免丢失先前学习的信息。这种方法被证明有利于在针对多个任务进行微调时处理存储问题。有多种方法可以实现参数高效的微调。低秩自适应 LoRA 和 QLoRA 是最广泛使用和最有效的。
1.2 什么是 LoRa?
LoRA 是一种改进的微调方法,其中不是对构成预训练大型语言模型权重矩阵的所有权重进行微调,而是对两个近似于该较大矩阵的较小矩阵进行微调。这些矩阵构成 LoRA 适配器。然后将此微调后的适配器加载到预训练模型中并用于推理。
针对特定任务或用例进行 LoRA 微调后,结果是原始 LLM 保持不变,并出现一个相当小的“LoRA 适配器”,通常占原始 LLM 大小的个位数百分比(以 MB 而不是 GB 为单位)。
在推理过程中,LoRA 适配器必须与其原始 LLM 结合使用。其优势在于许多 LoRA 适配器能够重用原始 LLM,从而在处理多个任务和用例时减少总体内存需求。
1.3 什么是量化 LoRA (QLoRA)?
QLoRA 代表了 LoRA 的更高效的内存迭代。 QLoRA 通过将 LoRA 适配器(较小矩阵)的权重量化为较低精度(例如,4 位而不是 8 位),使 LoRA 更进一步。这进一步减少了内存占用和存储要求。在 QLoRA 中,预训练模型以量化的 4 位权重加载到 GPU 内存中,而 LoRA 中使用的是 8 位。尽管位精度有所降低,但 QLoRA 的有效性仍与 LoRA 相当。
在本教程中,我们将使用 QLoRA 的参数高效微调。
接下来让我们探索如何在单个 GPU 上使用 QLoRA 对自定义数据集上的 LLM 进行微调。
2、设置笔记本。
虽然我们将使用 Kaggle 笔记本进行此演示,但您可以随意使用任何 Jupyter 笔记本环境。Kaggle 每周提供 30 小时的免费 GPU 使用时间,这对于我们的实验来说已经足够了。首先,让我们打开一个新的笔记本,建立一些标题,然后继续连接到运行时。
在这里,我们将选择 GPU P100 作为加速器。你可以随意尝试 Kaggle 或任何其他环境中可用的其他 GPU 选项。
在本教程中,我们将使用 HuggingFace 库来下载和训练模型。要从 HuggingFace 下载模型,我们需要一个访问令牌。如果你已经注册了 HuggingFace,可以从设置部分生成新的访问令牌或使用任何现有的访问令牌。
3、安装所需的库
现在,让我们安装此实验所需的库。
!pip install -q -U bitsandbytes transformers peft accelerate datasets scipy einops evaluate trl rouge_score
让我们了解其中一些库的重要性:
Bitsandbytes
:一个出色的软件包,它提供了一个轻量级的包装器,用于自定义 CUDA 函数,使 LLM 运行得更快——优化器、矩阵乘法和量化。在本教程中,我们将使用此库尽可能高效地加载我们的模型。transformers
:Hugging Face (🤗) 的一个库,为各种自然语言处理任务提供预训练模型和训练实用程序。peft
:Hugging Face (🤗) 的一个库,可实现参数高效的微调。accelerate
:Accelerate 精确地抽象与多 GPU/TPU/fp16 相关的样板代码,其余代码保持不变。datasets
:Hugging Face (🤗) 的另一个库,可轻松访问各种数据集。einops
:一个简化张量运算的库。
加载所需的库:
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
HfArgumentParser,
AutoTokenizer,
TrainingArguments,
Trainer,
GenerationConfig
)
from tqdm import tqdm
from trl import SFTTrainer
import torch
import time
import pandas as pd
import numpy as np
from huggingface_hub import interpreter_login
interpreter_login()
在本教程中,我们不会跟踪我们的训练指标,因此让我们禁用权重和偏差。W&B 平台由一组强大的组件组成,用于监控、可视化数据和模型以及传达结果。要在微调过程中停用权重和偏差,请设置以下环境属性。
import os
# disable Weights and Biases
os.environ['WANDB_DISABLED']="true"
如果你有 Weights and Biases 帐户,请随时启用它并进行试验。
4、加载数据集
有许多数据集可用于微调模型。在本例中,我们将利用 HuggingFace 的 DialogSum 数据集进行微调。DialogSum 是一个广泛的对话摘要数据集,包含 13,460 个对话以及手动标记的摘要和主题。
选择此数据集没有特别的原因。你可以随意使用任何自定义数据集尝试此实验。
让我们执行以下代码以从 HuggingFace 加载上述数据集:
huggingface_dataset_name = "neil-code/dialogsum-test"
dataset = load_dataset(huggingface_dataset_name)
数据集加载完成后,我们可以查看它以了解它包含的内容:
它包含以下字段。
dialog
:对话文本。summary
:人工编写的对话摘要。topic
:人工编写的对话主题/一句话。id
:示例的唯一文件 ID。
5、创建 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,
)
6、加载预训练模型
微软最近开源了 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 位并在微调期间保持它们固定。
7、标记化
现在,让我们配置标记器,结合左填充以优化训练期间的内存使用情况。
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
8、使用零样本推理测试模型
我们将使用一些示例输入来评估上面加载的基础模型:
%%time
from transformers import set_seed
seed = 42
set_seed(seed)
index = 10
prompt = 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}')
从上面的观察中可以看出,与基线摘要相比,该模型在总结对话方面面临挑战。但是,它设法从文本中提取了基本信息,这表明有可能针对手头的特定任务对模型进行微调。
9、预处理数据集
数据集不能直接用于微调。以模型可以理解的方式格式化提示至关重要。参考 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 None
response = 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_prompt
return sample
上述函数可用于将我们的输入转换为提示格式。
现在,我们将使用模型标记器将这些提示处理为标记化提示。
我们的目标是生成具有一致长度的输入序列,这有利于通过优化效率和最小化计算开销来微调语言模型。确保这些序列不超过模型的最大标记限制至关重要。
from functools import partial
# SOURCE https://github.com/databrickslabs/dolly/blob/master/training/trainer.py
def get_max_length(model):
conf = model.config
max_length = None
for 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}")
break
if not max_length:
max_length = 1024
print(f"Using default max length: {max_length}")
return max_length
def 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.py
def 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 sample
print("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_length
dataset = dataset.filter(lambda sample: len(sample["input_ids"]) < max_length)
# Shuffle dataset
dataset = dataset.shuffle(seed=seed)
return dataset
通过利用这些功能,我们的数据集将为微调过程做好准备!
## Pre-process dataset
max_length = get_max_length(original_model)
print(max_length)
train_dataset = preprocess_dataset(tokenizer, max_length,seed, dataset['train'])
eval_dataset = preprocess_dataset(tokenizer, max_length,seed, dataset['validation'])
10、为 QLoRA 准备模型
# 2 - Using the prepare_model_for_kbit_training method from PEFT
# Preparing the Model for QLoRA
original_model = prepare_model_for_kbit_training(original_model)
在这里,使用 prepare_model_for_kbit_training()
函数为 QLoRA 训练准备模型。此函数通过设置必要的配置来初始化 QLoRA 模型。
11、设置 PEFT 进行微调
现在让我们定义 LoRA 配置以微调基础模型。
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
config = LoraConfig(
r=32, #Rank
lora_alpha=32,
target_modules=[
'q_proj',
'k_proj',
'v_proj',
'dense'
],
bias="none",
lora_dropout=0.05, # Conventional
task_type="CAUSAL_LM",
)
# 1 - Enabling gradient checkpointing to reduce memory usage during fine-tuning
original_model.gradient_checkpointing_enable()
peft_model = get_peft_model(original_model, config)
注意秩 (r) 超参数,它定义了要训练的适配器的秩/维度。r 是适配器中使用的低秩矩阵的秩,因此控制训练的参数数量。更高的秩将允许更多的表达能力,但需要权衡计算。
这里的 alpha 是学习权重的缩放因子。权重矩阵按 alpha/r 缩放,因此 alpha 值越高,LoRA 激活的权重也就越大。
一旦一切设置完毕并准备好 PEFT,我们就可以使用 print_trainable_parameters()
辅助函数来查看模型中有多少可训练参数。
print(print_number_of_trainable_model_parameters(peft_model))
12、训练 PEFT 适配器
定义训练参数并创建 Trainer 实例:
output_dir = f'./peft-dialogue-summary-training-{str(int(time.time()))}'
import transformers
peft_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 = False
peft_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 模型添加适配器来准备推理模型。在这里,我们设置 is_trainable=False
,因为计划只用这个 PEFT 模型进行推理。
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
base_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 PeftModel
ft_model = PeftModel.from_pretrained(base_model, "/kaggle/working/peft-dialogue-summary-training-1705417060/checkpoint-1000",torch_dtype=torch.float16,is_trainable=False)
微调通常是一个迭代过程。根据验证和测试集的结果,我们可能需要进一步调整模型的架构、超参数或训练数据,以提高其性能。现在让我们看看如何评估微调后的 LLM 的结果。
13、定性评估模型(人工评估)
现在,让我们使用相同的输入但使用 PEFT 模型进行推理,就像我们之前在第 7 步对原始模型所做的那样。
%%time
from transformers import set_seed
set_seed(seed)
index = 5
dialogue = 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}')
14、定量评估模型(使用 ROUGE 指标)
ROUGE,即以回忆为导向的摘要评估替代研究,是一组指标和软件包,用于评估自然语言处理中的自动摘要和机器翻译软件。这些指标将自动生成的摘要或翻译与参考或一组参考(人工生成的)摘要或翻译进行比较。
现在让我们使用 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 pd
dialogues = 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 evaluate
rouge = 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 模型在百分比方面有显著的改进。
如果你想访问完整的笔记本,请参阅这个存储库。
原文链接:Fine Tune Large Language Model (LLM) on a Custom Dataset with QLoRA
BimAnt翻译整理,转载请标明出处