开源 AI 食谱文档

微调 LLM 以生成波斯语产品目录(JSON 格式)

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

Open In Colab

微调 LLM 以生成波斯语产品目录(JSON 格式)

作者:Mohammadreza Esmaeiliyan

在本笔记本中,我们尝试在不增加复杂性的情况下微调大型语言模型。该模型已针对客户级 GPU 上的使用进行了优化,以生成波斯语产品目录并以 JSON 格式生成结构化输出。它对于从伊朗平台上(如 BasalamDivarDigikala 等)具有用户生成内容的产品的非结构化标题和描述中创建结构化输出特别有效。

您可以在 我们的 HF 账户 上看到使用此代码微调的 LLM。此外,最快的开源推理引擎之一 Vllm 也被用于推理。

让我们开始吧!

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM

peft 库,即参数高效微调,旨在更高效地微调 LLM。如果我们像所有神经网络一样传统地打开和微调网络的上层,这将需要大量的处理和大量的 VRAM。借助最新论文中开发的方法,该库已实现 LLM 的高效微调。在此处阅读有关 peft 的更多信息:Hugging Face PEFT

设置超参数

# General parameters
model_name = "NousResearch/Llama-2-7b-chat-hf"  # The model that you want to train from the Hugging Face hub
dataset_name = "BaSalam/entity-attribute-dataset-GPT-3.5-generated-v1"  # The instruction dataset to use
new_model = "llama-persian-catalog-generator"  # The name for fine-tuned LoRA Adaptor
# LoRA parameters
lora_r = 64
lora_alpha = lora_r * 2
lora_dropout = 0.1
target_modules = ["q_proj", "v_proj", "k_proj"]

LoRA(低秩自适应)通过构建低秩矩阵并将其添加到每个模型层来存储权重变化。此方法仅打开这些层进行微调,而无需更改原始模型权重或进行长时间的训练。生成的权重是轻量级的,并且可以多次生成,从而可以使用加载到 RAM 中的 LLM 微调多个任务。

在此处阅读有关 LoRA 的信息:Lightning AI。有关其他高效训练方法,请参阅 Hugging Face Docs on Performance TrainingSFT Trainer Enhancement

# QLoRA parameters
load_in_4bit = True
bnb_4bit_compute_dtype = "float16"
bnb_4bit_quant_type = "nf4"
bnb_4bit_use_double_quant = False

QLoRA(量化低秩自适应)是一种高效的微调方法,它通过使用 4 位量化使大型语言模型能够在较小的 GPU 上运行。此方法保留了 16 位微调的完整性能,同时减少了内存使用量,从而可以在单个 48GB GPU 上微调参数高达 650 亿的模型。QLoRA 结合了 4 位 NormalFloat 数据类型、双重量化和分页优化器,以有效地管理内存。它允许使用低秩适配器微调模型,从而显着增强了 AI 模型开发的可访问性。

在此处阅读有关 QLoRA 的信息:Hugging Face

# TrainingArguments parameters
num_train_epochs = 1
fp16 = False
bf16 = False
per_device_train_batch_size = 4
gradient_accumulation_steps = 1
gradient_checkpointing = True
learning_rate = 0.00015
weight_decay = 0.01
optim = "paged_adamw_32bit"
lr_scheduler_type = "cosine"
max_steps = -1
warmup_ratio = 0.03
group_by_length = True
save_steps = 0
logging_steps = 25

# SFT parameters
max_seq_length = None
packing = False
device_map = {"": 0}

# Dataset parameters
use_special_template = True
response_template = " ### Answer:"
instruction_prompt_template = '"### Human:"'
use_llama_like_model = True

模型训练

# Load dataset (you can process it here)
dataset = load_dataset(dataset_name, split="train")
percent_of_train_dataset = 0.95
other_columns = [i for i in dataset.column_names if i not in ["instruction", "output"]]
dataset = dataset.remove_columns(other_columns)
split_dataset = dataset.train_test_split(
    train_size=int(dataset.num_rows * percent_of_train_dataset), seed=19, shuffle=False
)
train_dataset = split_dataset["train"]
eval_dataset = split_dataset["test"]
print(f"Size of the train set: {len(train_dataset)}. Size of the validation set: {len(eval_dataset)}")
# Load LoRA configuration
peft_config = LoraConfig(
    r=lora_r,
    lora_alpha=lora_alpha,
    lora_dropout=lora_dropout,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=target_modules,
)

LoraConfig 对象用于在使用 Peft 库时配置模型的 LoRA(低秩自适应)设置。这可以帮助减少需要微调的参数数量,从而加快训练速度并降低内存使用量。以下是参数的细分

  • r:LoRA 中使用的低秩矩阵的秩。此参数控制低秩自适应的维度,并直接影响模型的适应能力和计算成本。
  • lora_alpha:此参数控制低秩自适应矩阵的缩放因子。较高的 alpha 值可以提高模型学习新任务的能力。
  • lora_dropout:LoRA 的 dropout 率。这有助于防止微调期间的过拟合。在本例中,它设置为 0.1。
  • bias:指定是否将偏置项添加到低秩矩阵。在本例中,它设置为“none”,这意味着不会添加偏置项。
  • task_type:定义模型正在微调的任务类型。此处,“CAUSAL_LM”表示任务是因果语言建模任务,该任务预测序列中的下一个单词。
  • target_modules:指定模型中将应用 LoRA 的模块。在本例中,它设置为 ["q_proj", "v_proj", 'k_proj'],它们是模型注意力机制中的查询、值和键投影层。
# Load QLoRA configuration
compute_dtype = getattr(torch, bnb_4bit_compute_dtype)

bnb_config = BitsAndBytesConfig(
    load_in_4bit=load_in_4bit,
    bnb_4bit_quant_type=bnb_4bit_quant_type,
    bnb_4bit_compute_dtype=compute_dtype,
    bnb_4bit_use_double_quant=bnb_4bit_use_double_quant,
)

此代码块配置了使用 BitsAndBytes (bnb) 的设置,BitsAndBytes 是一个为 PyTorch 模型提供高效内存管理和压缩技术的库。具体来说,它定义了模型权重将如何在 4 位精度下加载和量化,这对于减少内存使用量并可能加快推理速度非常有用。

  • load_in_4bit:一个布尔值,确定是否以 4 位精度加载模型。
  • bnb_4bit_quant_type:指定要使用的 4 位量化类型。此处,它设置为 4 位 NormalFloat (NF4) 量化类型,这是 QLoRA 中引入的一种新的数据类型。此类型在信息论上对于正态分布的权重是最佳的,为量化模型以进行微调提供了一种有效的方法。
  • bnb_4bit_compute_dtype:设置用于涉及量化模型的计算的数据类型。在 QLoRA 中,它设置为 “float16”,这通常用于混合精度训练,以平衡性能和精度。
  • bnb_4bit_use_double_quant:此布尔参数指示是否使用双重量化。将其设置为 False 意味着仅使用单量化,这通常更快,但可能精度稍低。

为什么我们有两种数据类型(quant_type 和 compute_type)?QLoRA 采用两种不同的数据类型:一种用于存储基本模型权重(此处为 4 位 NormalFloat),另一种用于计算操作(16 位)。在前向和后向传递期间,QLoRA 将权重从存储格式反量化为计算格式。但是,它仅计算 LoRA 参数的梯度,这些参数使用 16 位 bfloat。这种方法确保仅在必要时才解压缩权重,从而在训练和推理阶段都保持低内存使用率。

# Load base model
model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config, device_map=device_map)
model.config.use_cache = False
# Set training parameters
training_arguments = TrainingArguments(
    output_dir=new_model,
    num_train_epochs=num_train_epochs,
    per_device_train_batch_size=per_device_train_batch_size,
    gradient_accumulation_steps=gradient_accumulation_steps,
    optim=optim,
    save_steps=save_steps,
    logging_steps=logging_steps,
    learning_rate=learning_rate,
    weight_decay=weight_decay,
    fp16=fp16,
    bf16=bf16,
    max_steps=max_steps,
    warmup_ratio=warmup_ratio,
    gradient_checkpointing=gradient_checkpointing,
    group_by_length=group_by_length,
    lr_scheduler_type=lr_scheduler_type,
)
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"  # Fix weird overflow issue with fp16 training
if not tokenizer.chat_template:
    tokenizer.chat_template = "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}"

关于聊天模板,我们将简要解释一下,为了理解模型训练期间用户和模型之间对话的结构,创建了一系列保留短语来分隔用户的消息和模型的响应。这确保了模型准确地理解每条消息的来源,并保持对话结构的意识。通常,遵守聊天模板有助于提高预期任务的准确性。但是,当微调数据集和模型之间存在分布偏移时,使用特定的聊天模板可能会更有帮助。有关更多阅读材料,请访问 Hugging Face 博客关于聊天模板的文章

def special_formatting_prompts(example):
    output_texts = []
    for i in range(len(example["instruction"])):
        text = f"{instruction_prompt_template}{example['instruction'][i]}\n{response_template} {example['output'][i]}"
        output_texts.append(text)
    return output_texts


def normal_formatting_prompts(example):
    output_texts = []
    for i in range(len(example["instruction"])):
        chat_temp = [
            {"role": "system", "content": example["instruction"][i]},
            {"role": "assistant", "content": example["output"][i]},
        ]
        text = tokenizer.apply_chat_template(chat_temp, tokenize=False)
        output_texts.append(text)
    return output_texts
if use_special_template:
    formatting_func = special_formatting_prompts
    if use_llama_like_model:
        response_template_ids = tokenizer.encode(response_template, add_special_tokens=False)[2:]
        collator = DataCollatorForCompletionOnlyLM(response_template=response_template_ids, tokenizer=tokenizer)
    else:
        collator = DataCollatorForCompletionOnlyLM(response_template=response_template, tokenizer=tokenizer)
else:
    formatting_func = normal_formatting_prompts
trainer = SFTTrainer(
    model=model,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    peft_config=peft_config,
    formatting_func=formatting_func,
    data_collator=collator,
    max_seq_length=max_seq_length,
    tokenizer=tokenizer,
    args=training_arguments,
    packing=packing,
)

然后实例化 SFTTrainer 以处理模型的监督微调 (SFT)。此训练器专为 SFT 设计,包含额外的参数,例如 formatting_funcpacking,这些参数通常在标准训练器中找不到。formatting_func:一个自定义函数,用于通过组合指令和响应模板来格式化训练示例。packing:禁用将多个样本打包到一个序列中,这不是典型 Trainer 类中的标准参数。

# Train model
trainer.train()

# Save fine tuned Lora Adaptor
trainer.model.save_pretrained(new_model)

推理

import torch
import gc


def clear_hardwares():
    torch.clear_autocast_cache()
    torch.cuda.ipc_collect()
    torch.cuda.empty_cache()
    gc.collect()


clear_hardwares()
clear_hardwares()
def generate(model, prompt: str, kwargs):
    tokenized_prompt = tokenizer(prompt, return_tensors="pt").to(model.device)

    prompt_length = len(tokenized_prompt.get("input_ids")[0])

    with torch.cuda.amp.autocast():
        output_tokens = model.generate(**tokenized_prompt, **kwargs) if kwargs else model.generate(**tokenized_prompt)
        output = tokenizer.decode(output_tokens[0][prompt_length:], skip_special_tokens=True)

    return output
base_model = AutoModelForCausalLM.from_pretrained(new_model, return_dict=True, device_map="auto", token="")
tokenizer = AutoTokenizer.from_pretrained(new_model, max_length=max_seq_length)
model = PeftModel.from_pretrained(base_model, new_model)
del base_model
sample = eval_dataset[0]
if use_special_template:
    prompt = f"{instruction_prompt_template}{sample['instruction']}\n{response_template}"
else:
    chat_temp = [{"role": "system", "content": sample["instruction"]}]
    prompt = tokenizer.apply_chat_template(chat_temp, tokenize=False, add_generation_prompt=True)
gen_kwargs = {"max_new_tokens": 1024}
generated_texts = generate(model=model, prompt=prompt, kwargs=gen_kwargs)
print(generated_texts)

合并到基础模型

clear_hardwares()
merged_model = model.merge_and_unload()
clear_hardwares()
del model
adapter_model_name = "your_hf_account/your_desired_name"
merged_model.push_to_hub(adapter_model_name)

在这里,我们将适配器与基础模型合并,并将合并后的模型推送到 hub 上。您可以只将适配器推送到 hub,从而避免推送沉重的基础模型文件

model.push_to_hub(adapter_model_name)

然后您可以通过这种方式加载模型

config = PeftConfig.from_pretrained(adapter_model_name)
model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path, return_dict=True, load_in_8bit=True, device_map='auto')
tokenizer = AutoTokenizer.from_pretrained(config.base_model_name_or_path)

# Load the Lora model
model = PeftModel.from_pretrained(model, adapter_model_name)

使用 Vllm 进行快速推理

vllm 库是 LLM 最快的推理引擎之一。有关可用选项的比较概述,您可以使用此博客:7 种用于服务 LLM 的框架。在本示例中,我们正在此任务上推理微调模型的版本 1。

from vllm import LLM, SamplingParams

prompt = """### Question: here is a product title from a Iranian marketplace.  \n         give me the Product Entity and Attributes of this product in Persian language.\n         give the output in this json format: {'attributes': {'attribute_name' : <attribute value>, ...}, 'product_entity': '<product entity>'}.\n         Don't make assumptions about what values to plug into json. Just give Json not a single word more.\n         \nproduct title:"""
user_prompt_template = "### Question: "
response_template = " ### Answer:"

llm = LLM(model="BaSalam/Llama2-7b-entity-attr-v1", gpu_memory_utilization=0.9, trust_remote_code=True)

product = "مانتو اسپرت پانیذ قد جلوی کار حدودا 85 سانتی متر قد پشت کار حدودا 88 سانتی متر"
sampling_params = SamplingParams(temperature=0.0, max_tokens=75)
prompt = f"{user_prompt_template} {prompt}{product}\n {response_template}"
outputs = llm.generate(prompt, sampling_params)

print(outputs[0].outputs[0].text)

示例输出

{
    "attributes": {
        "قد جلوی کار": "85 سانتی متر",
        "قد پشت کار": "88 سانتی متر"
    },
    "product_entity": "مانتو اسپرت"
}

在此博客中,您可以阅读有关微调 LLM 的最佳实践 Sebastian Raschka’s Magazine

< > 更新 在 GitHub 上