微调大模型

源代码来自:老牛同学

使用开源大模型 Qwen2-0.5B 的示例,实现了一个基于微调和 RAG(Retrieval-Augmented Generation)的文本分类助手。以下是各部分的详细解释:

1. 引入必要库

import json
import pandas as pd
import torch
from datasets import Dataset
from modelscope import AutoTokenizer
from swanlab.integration.huggingface import SwanLabCallback
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq
import os
import swanlab

这部分代码引入了主要用于微调、训练和生成文本的库,包括 transformerspeft(主要用于 LoRA 微调)、datasets(用于处理数据集),以及 swanlab 用于回调和日志记录。

2. 设置路径和设备

BASE_DIR = 'D:\\ModelSpace\\Qwen2'
device = 'cuda' if torch.cuda.is_available() else 'cpu'

设置模型的根目录和设备名称。设备名称判断系统是否支持 CUDA(GPU 加速),如果不支持则使用 CPU。

在mac中使用mps加速

device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")

3. 数据集格式转换函数

def dataset_jsonl_transfer(origin_path, new_path):
messages = []
with open(origin_path, "r", encoding="utf-8") as file:
for line in file:
data = json.loads(line)
text = data["text"]
catagory = data["category"]
output = data["output"]
message = {
"input": f"文本:{text},分类选项列表:{catagory}",
"output": output,
}
messages.append(message)
with open(new_path, "w", encoding="utf-8") as file:
for message in messages:
file.write(json.dumps(message, ensure_ascii=False) + "\n")

dataset_jsonl_transfer 函数用于将原始 JSON 数据转换成微调所需的数据格式。原始文件的每行包含一个 JSON 对象,函数将其读取、重构并保存成新的 JSONL 格式文件,每行包含一个示例数据。

4. 数据预处理函数

def process_func(example):
MAX_LENGTH = 384
instruction = tokenizer(f"<|im_start|>system\n你是一个文本分类领域的专家...{example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False)
response = tokenizer(f"{example['output']}", add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
if len(input_ids) > MAX_LENGTH:
input_ids, attention_mask, labels = input_ids[:MAX_LENGTH], attention_mask[:MAX_LENGTH], labels[:MAX_LENGTH]
return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}

process_func 函数将数据处理成大模型可以接受的格式,包含 input_idsattention_masklabels。这里模拟了一个对话输入,用户提问,助手返回分类输出。如果序列长度超出最大限制 MAX_LENGTH,进行截断。

5. 加载模型和分词器

model_dir = os.path.join(BASE_DIR, 'Qwen2-0.5B')
tokenizer = AutoTokenizer.from_pretrained(model_dir, use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, device_map=device, torch_dtype=torch.bfloat16)
model.enable_input_require_grads()

加载模型和分词器,将 bfloat16 用于精度,以减少 GPU 占用量。model.enable_input_require_grads() 开启梯度检查点支持,以节省内存。

6. 加载和处理数据集

train_jsonl_new_path = os.path.join(BASE_DIR, 'train.jsonl')
test_jsonl_new_path = os.path.join(BASE_DIR, 'test.jsonl')

if not os.path.exists(train_jsonl_new_path):
dataset_jsonl_transfer(train_dataset_path, train_jsonl_new_path)
if not os.path.exists(test_jsonl_new_path):
dataset_jsonl_transfer(test_dataset_path, test_jsonl_new_path)

train_df = pd.read_json(train_jsonl_new_path, lines=True)
train_ds = Dataset.from_pandas(train_df)
train_dataset = train_ds.map(process_func, remove_columns=train_ds.column_names)

检查并转换数据集,将其加载为 Dataset 格式,并通过 process_func 处理成可用于训练的数据格式。

7. LoRA 配置与应用

config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", ...],
inference_mode=False,
r=8,
lora_alpha=32,
lora_dropout=0.1,
)

model = get_peft_model(model, config)

设置并应用 LoRA(Low-Rank Adaptation)配置,用于高效微调。LoRA 能通过添加低秩矩阵在不改变原模型参数的情况下更新模型,适合大模型的微调。

8. 训练参数与 Trainer 初始化

args = TrainingArguments(
output_dir=os.path.join(BASE_DIR, 'output', 'Qwen2-0.5B'),
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=2,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True,
report_to="none",
)

swanlab_callback = SwanLabCallback(project="Qwen2-FineTuning", experiment_name="Qwen2-0.5B")

trainer = Trainer(
model=model,
args=args,
train_dataset=train_dataset,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
callbacks=[swanlab_callback],
)

配置训练参数并创建 Trainer 实例,指定保存路径、batch 大小、梯度累积步数、日志记录频率、学习率等。SwanLabCallback 用于将训练过程发送至 SwanLab 进行实时监控。

9. 训练模型

trainer.train()

调用 .train() 开始训练。

10. 模型推理与评估

def predict(messages, model, tokenizer):
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
generated_ids = model.generate(model_inputs.input_ids, max_new_tokens=512)
generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)]
return tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

predict 函数实现了模型推理功能,将输入转成模型格式后生成输出文本。

11. 测试集上的推理

test_df = pd.read_json(test_jsonl_new_path, lines=True)[:10]
test_text_list = []

for index, row in test_df.iterrows():
instruction = row['你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项列表,请输出文本内容的正确分类']
input_value = row['input']

messages = [{"role": "system", "content": f"{instruction}"}, {"role": "user", "content": f"{input_value}"}]
response = predict(messages, model, tokenizer)
messages.append({"role": "assistant", "content": f"{response}"})

result_text = f"{messages[0]}\n\n{messages[1]}\n\n{messages[2]}"
test_text_list.append(swanlab.Text(result_text, caption=response))

swanlab.log({"Prediction": test_text_list})
swanlab.finish()

在测试集上对模型进行评估,将预测结果和输入对比,并将输出文本记录到 SwanLab