实验所用的设备是单卡4090 24GB,在验证阶段使用了KV cache所以总共需要至少21GB的显存,才推荐完成这个微调操作。
在10万左右训练数据集、使用Lora方法、半精度主干网络权重、3 Epoch的情况下,单卡微调时间大约3小时半。
0 原始数据预处理
0.1 构造指令微调数据
这一步主要是将原始数据读取进来,整理并替换为大模型要求的指令微调格式,保存为jsonl方便下次读取。
import json
import pandas
import numpy as np
file_path = "./data/外科问诊数据集.csv"
data = pandas.read_csv(file_path, encoding="GBK", encoding_errors="ignore")
data = data.dropna()
# 构建指令微调数据
dataset = []
for i in range(len(data)):
dataset.append({
"instruction": data["title"][i],
"input": data["ask"][i],
"output": data["answer"][i]
})
print("数据集大小:", len(dataset))
# 保存为jsonl
with open("./data/外科问诊数据集.jsonl", "w", encoding="utf-8") as f:
for i in range(len(dataset)):
f.write(json.dumps(dataset[i], ensure_ascii=False) + "\n")
0.2 分词预处理
from datasets import load_dataset
import torch
from transformers import Qwen2Tokenizer
data_path = "./data/外科问诊数据集.jsonl"
dataset = load_dataset("json", data_files=data_path)["train"]
torch.cuda.empty_cache()
model_path = "/root/lanyun-tmp/hf/Qwen2.5-1.5B-Instruct"
tokenizer = Qwen2Tokenizer.from_pretrained(model_path, local_files_only=True)
def process_func(example):
MAX_LENGTH = 384
instruction = tokenizer(
f"<|im_start|>system\n{example['instruction']}<|im_end|>\n<|im_start|>user\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 = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": torch.LongTensor(input_ids),
"attention_mask": torch.LongTensor(attention_mask),
"labels": torch.LongTensor(labels)
}
k_workers = 20
dataset = dataset.map(process_func, batched=False, num_proc=k_workers)
dataset.save_to_disk("./data/外科问诊_processed")
1 数据读取
到这里就可以直接读取已经分词处理好的数据了,直接按照合理比例进行划分数据集,这里总数据大小是11万左右。
import datasets
data_path = "./data/外科问诊_processed"
dataset = datasets.load_from_disk(data_path)
split_dataset = dataset.train_test_split(test_size=0.2, seed=42)
train_data = split_dataset["train"]
test_data = split_dataset["test"]
print(f"训练集大小:{len(train_data)}")
print(f"测试集大小:{len(test_data)}")
训练集大小:92792
测试集大小:23199
2 配置
import torch, transformers
from transformers import Qwen2Tokenizer, Qwen2ForCausalLM
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType
device = "cuda" if torch.cuda.is_available() else "cpu"
k_workers = 20
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.1 # Dropout 比例
)
有了上面的lora配置,就可以开始加载lora后的模型了
torch.cuda.empty_cache()
# torch.cuda.set_device(0)
model_path = "/root/lanyun-tmp/hf/Qwen2.5-1.5B-Instruct"
tokenizer = Qwen2Tokenizer.from_pretrained(model_path, local_files_only=True)
model = Qwen2ForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.bfloat16,
device_map="auto",
local_files_only=True
)
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
model = get_peft_model(model, lora_config)
model.enable_input_require_grads()
model.print_trainable_parameters()
model.train()
trainable params: 9,232,384 || all params: 1,552,946,688 || trainable%: 0.5945
3 训练
from transformers import TrainingArguments, Trainer
args = TrainingArguments(
output_dir="./output/Qwen2.5_instruct_lora",
per_device_train_batch_size=16,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
dataloader_num_workers=k_workers,
bf16=True,
logging_steps=50,
num_train_epochs=3,
save_steps=100,
learning_rate=1e-4,
optim="adamw_torch_fused",
ddp_find_unused_parameters=False,
label_names=["labels"],
)
trainer = Trainer(
model=model,
args=args,
train_dataset=train_data,
eval_dataset=test_data,
data_collator=transformers.DataCollatorForSeq2Seq(tokenizer, return_tensors="pt"),
compute_metrics=compute_metrics
)
trainer.train()
由于是分步来训练统计loss的,所以会因为数据的分布不同,导致loss出现小范围的抖动,但是总体上在3000个step之后是趋于稳定了,接下来就可以对比baseline测试一下实际效果

为了方便保存和下次读取,可以将lora参数合并
# 合并lora参数
from transformers import Qwen2ForCausalLM, Qwen2Tokenizer
import torch
from peft import PeftModel
model_path = "/root/lanyun-tmp/hf/Qwen2.5-1.5B-Instruct"
model = Qwen2ForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
local_files_only=True
)
model = PeftModel.from_pretrained(model, "./output/Qwen2.5_instruct_lora/checkpoint-4350").merge_and_unload()
tokenizer = Qwen2Tokenizer.from_pretrained(model_path, local_files_only=True)
model.save_pretrained("./output/Qwen2.5_instruct_lora_merged")
tokenizer.save_pretrained("./output/Qwen2.5_instruct_lora_merged")
4 测试
接下来可以和base模型做一个输出对比:
from vllm import LLM, SamplingParams
import datasets, torch
base_model = "/root/lanyun-tmp/hf/Qwen2.5-1.5B-Instruct"
lora_model = "./output/Qwen2.5_instruct_lora_merged"
model = LLM(
model=base_model,
trust_remote_code=True,
tensor_parallel_size=1,
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=512,
repetition_penalty=1.1,
)
data_path = "./data/外科问诊_processed"
dataset = datasets.load_from_disk(data_path)
split_dataset = dataset.train_test_split(test_size=0.2, seed=42)
test_data = split_dataset["test"]
nums = [1, 10, 20, 100, 200, 500, 1000, 2000, 5000, 10000]
inputs = []
for i, num in enumerate(nums):
example = test_data[num]
instruction = example["instruction"]
input_value = example["input"]
text = f"<|im_start|>system\n{instruction}<|im_end|>\n<|im_start|>user\n{input_value}<|im_end|>\n<|im_start|>assistant\n"
print(f"第 {i + 1} 段对话:")
print(text)
output = model.generate([text], sampling_params=sampling_params)
print(output[0].outputs[0].text)
# input_ids = tokenizer(text, return_tensors="pt").input_ids.to(model.device)
inputs.append(text)
print("\n\n")
torch.cuda.empty_cache()
以下是未经过微调的base模型生成的结果:
以下是通过PEFT的模型生成的结果:
可以看出,通过PEFT的模型生成的语句就有点医生的味道了,而不是死板地列出一条条字段。



