# 微调大型语言模型以生成波斯语产品目录的 JSON 格式

_作者：[Mohammadreza Esmaeiliyan](https://github.com/MrzEsma)_

在这个 Notebook 中，我们尝试对大型语言模型进行微调，且没有添加额外复杂性。该模型已针对客户级别的 GPU 进行优化，用于生成波斯语产品目录并以 JSON 格式生成结构化输出。它特别适用于从伊朗平台（如 [Basalam](https://basalam.com)、[Divar](https://divar.ir/)、[Digikala](https://www.digikala.com/) 等）上的用户生成内容的非结构化标题和描述中创建结构化输出。

你可以在 [我们的 Hugging Face 账户](https://huggingface.co/BaSalam/Llama2-7b-entity-attr-v1) 查看微调后的 LLM。此外，我们还使用了最快的开源推理引擎之一 —— [Vllm](https://github.com/vllm-project/vllm) 来进行推理。

让我们开始吧！

```python
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` 库，或称为参数高效微调（Parameter Efficient Fine-Tuning），旨在更高效地对大型语言模型（LLMs）进行微调。如果像传统神经网络那样打开并微调网络的上层，它将需要大量的计算和显著的 VRAM（显存）。随着近期论文中提出的方法，`peft` 库已被实现，用于高效地对 LLM 进行微调。你可以在这里了解更多关于 `peft` 的信息：[Hugging Face PEFT](https://huggingface.co/blog/peft)。

## 设置超参数

```python
# 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
```

```python
# LoRA parameters
lora_r = 64
lora_alpha = lora_r * 2
lora_dropout = 0.1
target_modules = ["q_proj", "v_proj", 'k_proj']
```

LoRA（低秩适配，Low-Rank Adaptation）通过构建并将低秩矩阵添加到每个模型层中来存储权重变化。这种方法仅打开这些层进行微调，而不改变原始模型权重，也不需要长时间训练。生成的权重非常轻量，可以多次产生，从而允许在将 LLM 加载到 RAM 中后微调多个任务。

你可以在 [Lightning AI](https://lightning.ai/pages/community/tutorial/lora-llm/) 了解更多关于 LoRA 的信息。对于其他高效的训练方法，请参阅 [Hugging Face 性能训练文档](https://huggingface.co/docs/transformers/perf_train_gpu_one) 和 [SFT Trainer 增强](https://huggingface.co/docs/trl/main/en/sft_trainer#enhance-models-performances-using-neftune)。

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

QLoRA（量化低秩适配，Quantized Low-Rank Adaptation）是一种高效的微调方法，通过使用 4 位量化，使大型语言模型能够在更小的 GPU 上运行。该方法在减少内存使用的同时，保持了 16 位微调的全部性能，使得在单个 48GB GPU 上也可以微调最多 650 亿参数的模型。QLoRA 结合了 4 位 NormalFloat 数据类型、双重量化和分页优化器，有效管理内存。它允许对具有低秩适配器的模型进行微调，显著提高了 AI 模型开发的可访问性。

你可以在 [Hugging Face](https://huggingface.co/blog/4bit-transformers-bitsandbytes) 上了解更多关于 QLoRA 的信息。

```python
# 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
```

## 模型训练

```python
# 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)}")
```

```python
# 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']`，即模型注意力机制中的查询（query）、值（value）和键（key）投影层。

通过调整这些参数，`LoraConfig` 有助于在微调过程中高效地应用 LoRA 方法，从而优化计算资源和训练效率。

```python
# 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)** 库的设置，该库提供了高效的内存管理和压缩技术，专为 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 数据类型。这样的做法确保了权重只有在必要时才会解压，从而在训练和推理过程中保持较低的内存使用量。

```python
# Load base model
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map=device_map
)
model.config.use_cache = False
```

```python
# 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
)
```

```python
# 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 博客关于聊天模板](https://huggingface.co/blog/chat-templates)。

```python
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
```

```python
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
```

```python
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,
    processing_class=tokenizer,
    args=training_arguments,
    packing=packing
)
```

`SFTTrainer` 随后被实例化，用于处理模型的监督微调（SFT）。这个训练器专门针对 SFT 进行设计，并包括了一些额外的参数，例如 `formatting_func` 和 `packing`，这些参数通常在标准的训练器类中是找不到的。

**`formatting_func`**：一个自定义函数，用于格式化训练样本，将指令和回复模板组合在一起。
**`packing`**：禁用将多个样本打包成一个序列，这是标准 `Trainer` 类中没有的参数。

```python
# Train model
trainer.train()

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

## 推理

```python
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()
```

```python
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
```

```python
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
```

```python
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)
```

```python
gen_kwargs = {"max_new_tokens": 1024}
generated_texts = generate(model=model, prompt=prompt, kwargs=gen_kwargs)
print(generated_texts)
```

## 合并基础模型

```python
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)
```

在这里，我们将适配器与基础模型合并，并将合并后的模型推送到模型库中。你也可以只推送适配器到模型库，而避免推送沉重的基础模型文件，方法如下：

```python
model.push_to_hub(adapter_model_name)
```

然后，你可以按照以下方式加载模型：

```python
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)

# 加载 Lora 模型
model = PeftModel.from_pretrained(model, adapter_model_name)
```

通过这种方法，你能够高效地加载适配器模型，而不需要加载完整的基础模型，从而减少内存消耗并提高推理速度。

## 用 [Vllm](https://github.com/vllm-project/vllm) 快速推理


`vllm` 库是目前用于大型语言模型（LLM）推理的最快引擎之一。有关可用选项的比较概览，你可以参考这篇博客：[7 Frameworks for Serving LLMs](https://medium.com/@gsuresh957/7-frameworks-for-serving-llms-5044b533ee88)。

在这个示例中，我们对该任务使用了我们微调模型的第一个版本进行推理。

```python
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": "مانتو اسپرت"
}
```


在这篇博客中，你可以阅读关于微调大型语言模型（LLMs）的最佳实践：[Sebastian Raschka 的杂志](https://magazine.sebastianraschka.com/p/practical-tips-for-finetuning-llms?r=1h0eu9&utm_campaign=post&utm_medium=web)。

<EditOnGithub source="https://github.com/huggingface/cookbook/blob/main/notebooks/zh-CN/fine_tuning_llm_to_generate_persian_product_catalogs_in_json_format.md" />