英特尔-阿里天池LLM Hackathon
- 项目思路
- 项目背景
- 项目思路
- Lora微调Qwen模型
- 使用ipex_llm推理加速
- Gradio交互
项目名称:医疗问答助手
项目思路
项目背景
在当今医疗领域,智能问答系统正在逐步成为辅助医疗诊断的重要工具。随着自然语言处理技术的发展,基于大模型的问答系统在处理复杂医疗问题时展现出了巨大的潜力。Qwen2-1.5B模型作为一个大型预训练语言模型,拥有强大的语言理解和生成能力,但在特定领域应用时,往往需要进一步的微调和优化。为了提升医疗问答系统的准确性,本项目采用了LoRA(Low-Rank Adaptation)微调方法,并通过ipex_llm框架在指定的CPU平台上进行推理加速。
项目思路
明确了项目需求之后可以将本次项目分为三个部分:Lora微调Qwen模型、使用ipex_llm在CPU上进行推理加速、使用Gradio交互。
Lora微调Qwen模型
我们本次项目的目的是完成一个医疗问答机器人,训练的首先需要收集数据,我们使用github上开源的医疗问答数据集,数据集包含了2.7w条真实的问答数据(github链接有点久远了时间我忘记了,如果有需要可以私信我我发给您)。
Qwen的Lora我们在之前的博客中有提到过,在这里就不细说了,详见Qwen2-1.5B微调+推理
import torch
from datasets import Dataset, load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer
from peft import LoraConfig, TaskType, get_peft_model, PeftModel
dataset = load_dataset("csv", data_files="./问答.csv", split="train")
dataset = dataset.filter(lambda x: x["answer"] is not None)
datasets = dataset.train_test_split(test_size=0.1)
tokenizer = AutoTokenizer.from_pretrained("./Qwen2-1.5B-Instruct", trust_remote_code=True)
def process_func(example):
MAX_LENGTH = 768
input_ids, attention_mask, labels = [], [], []
instruction = example["question"].strip() # query
instruction = tokenizer(
f"<|im_start|>system\n你是医学领域的人工助手章鱼哥<|im_end|>\n<|im_start|>user\n{example['question']}<|im_end|>\n<|im_start|>assistant\n",
add_special_tokens=False,
)
response = tokenizer(f"{example['answer']}", add_special_tokens=False) # \n response, 缺少eos token
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": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
tokenized_ds = datasets['train'].map(process_func, remove_columns=['id', 'question', 'answer'])
tokenized_ts = datasets['test'].map(process_func, remove_columns=['id', 'question', 'answer'])
model = AutoModelForCausalLM.from_pretrained("./Qwen2-1.5B-Instruct", trust_remote_code=True)
config = LoraConfig(target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], modules_to_save=["post_attention_layernorm"])
model = get_peft_model(model, config)
args = TrainingArguments(
output_dir="./law",
per_device_train_batch_size=4,
gradient_accumulation_steps=16,
gradient_checkpointing=True,
logging_steps=6,
num_train_epochs=10,
learning_rate=1e-4,
remove_unused_columns=False,
save_strategy="epoch"
)
model.enable_input_require_grads()
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_ds.select(range(400)),
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
trainer.train()
训练结束得到微调后的权重,打包下载即可。
使用ipex_llm推理加速
导入需要的包
ipex是Intel公司研发优化大语言模型 (LLM) 在其硬件(Intel CPU)上运行而开发的一组扩展库和工具。
import os
import torch
import time
from transformers import AutoTokenizer
from ipex_llm.transformers import AutoModelForCausalLM
from peft import PeftModel
由于实在Cpu推理可以根据核心数设置线程
# 设置OpenMP线程数为8, 优化CPU并行计算性能
os.environ["OMP_NUM_THREADS"] = "8"
# base_model_name = "qwen2chat_int4"
# model = AutoModelForCausalLM.load_low_bit(base_model_name, trust_remote_code=True)
# 加载基础模型和分词器
base_model_name = "Qwen2-1-5B-Instruct" # 替换为你的基础模型名称
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name, trust_remote_code=True)
合并Lora
# 加载LoRA微调后的权重
lora_checkpoint = "./checkpoint-781"
lora_model = PeftModel.from_pretrained(model, lora_checkpoint)
输入Prompt测试
# 定义输入prompt
prompt = "头疼怎么治疗呢"
# 构建符合模型输入格式的消息列表
messages = [{"role": "user", "content": prompt}]
开启推理模式,在这部分其实有一个缺陷,就是合并Lora后的模型推理速度非常慢,大概是普通模型的五倍,欢迎有大佬能指点。
# 使用推理模式,减少内存使用并提高推理速度
with torch.inference_mode():
# 应用聊天模板,将消息转换为模型输入格式的文本
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 将文本转换为模型输入张量,并移至CPU (如果使用GPU,这里应改为.to('cuda'))
model_inputs = tokenizer([text], return_tensors="pt").to('cpu')
st = time.time()
# 生成回答, max_new_tokens限制生成的最大token数
generated_ids = lora_model.generate(model_inputs.input_ids, max_new_tokens=512)
end = time.time()
# 初始化一个空列表,用于存储处理后的generated_ids
processed_generated_ids = []
# 使用zip函数同时遍历model_inputs.input_ids和generated_ids
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids):
# 计算输入序列的长度
input_length = len(input_ids)
# 从output_ids中截取新生成的部分
# 这是通过切片操作完成的,只保留input_length之后的部分
new_tokens = output_ids[input_length:]
# 将新生成的token添加到处理后的列表中
processed_generated_ids.append(new_tokens)
# 将处理后的列表赋值回generated_ids
generated_ids = processed_generated_ids
# 解码模型输出,转换为可读文本
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
打印推理时间和结果
# 打印推理时间
print(f'Inference time: {end-st:.2f} s')
# 打印原始prompt
print('-'*20, 'Prompt', '-'*20)
print(text)
# 打印模型生成的输出
print('-'*20, 'Output', '-'*20)
print(response)
一站式py脚本
import os
import torch
import time
from transformers import AutoTokenizer
from ipex_llm.transformers import AutoModelForCausalLM
from peft import PeftModel
# 设置OpenMP线程数为8, 优化CPU并行计算性能
os.environ["OMP_NUM_THREADS"] = "8"
# 加载基础模型和分词器
base_model_name = "Qwen2-1-5B-Instruct" # 替换为你的基础模型名称
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name, trust_remote_code=True)
# 加载LoRA微调后的权重
lora_checkpoint = "./checkpoint-5000"
lora_model = PeftModel.from_pretrained(model, lora_checkpoint)
# 定义输入prompt
prompt = "头疼怎么治疗呢"
# 构建符合模型输入格式的消息列表
messages = [{"role": "user", "content": prompt}]
# 使用推理模式,减少内存使用并提高推理速度
with torch.inference_mode():
# 应用聊天模板,将消息转换为模型输入格式的文本
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 将文本转换为模型输入张量,并移至CPU (如果使用GPU,这里应改为.to('cuda'))
model_inputs = tokenizer([text], return_tensors="pt").to('cpu')
st = time.time()
# 生成回答, max_new_tokens限制生成的最大token数
generated_ids = lora_model.generate(model_inputs.input_ids, max_new_tokens=512)
end = time.time()
# 初始化一个空列表,用于存储处理后的generated_ids
processed_generated_ids = []
# 使用zip函数同时遍历model_inputs.input_ids和generated_ids
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids):
# 计算输入序列的长度
input_length = len(input_ids)
# 从output_ids中截取新生成的部分
# 这是通过切片操作完成的,只保留input_length之后的部分
new_tokens = output_ids[input_length:]
# 将新生成的token添加到处理后的列表中
processed_generated_ids.append(new_tokens)
# 将处理后的列表赋值回generated_ids
generated_ids = processed_generated_ids
# 解码模型输出,转换为可读文本
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 打印推理时间
print(f'Inference time: {end-st:.2f} s')
# 打印原始prompt
print('-'*20, 'Prompt', '-'*20)
print(text)
# 打印模型生成的输出
print('-'*20, 'Output', '-'*20)
print(response)
Gradio交互
Gradio是一个功能强大的Web交互页面,Gradio的特点是可以非常简单的使用几行代码实现前端的页面,在这里我只是简单的使用了比赛baseline提供的一个简单的Gradio,后续有时间我也会专门补一篇gradio使用教程。
import os
import torch
import time
from transformers import AutoTokenizer
from ipex_llm.transformers import AutoModelForCausalLM
from peft import PeftModel
import gradio as gr
from threading import Event
# 设置OpenMP线程数为8, 优化CPU并行计算性能
os.environ["OMP_NUM_THREADS"] = "8"
# 加载基础模型和分词器
base_model_name = "Qwen2-1-5B-Instruct" # 替换为你的基础模型名称
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name, trust_remote_code=True)
# 加载LoRA微调后的权重
lora_checkpoint = "./checkpoint-781"
lora_model = PeftModel.from_pretrained(model, lora_checkpoint)
# 创建一个停止事件,用于控制生成过程的中断
stop_event = Event()
# 定义用户输入处理函数
def user(user_message, history):
return "", history + [[user_message, None]]
# 定义机器人回复生成函数
def bot(history):
stop_event.clear()
prompt = history[-1][0]
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to('cpu')
print(f"\n用户输入: {prompt}")
print("模型输出: ", end="", flush=True)
start_time = time.time()
with torch.inference_mode():
generated_ids = lora_model.generate(model_inputs.input_ids, max_new_tokens=512)
processed_generated_ids = []
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids):
input_length = len(input_ids)
new_tokens = output_ids[input_length:]
processed_generated_ids.append(new_tokens)
generated_ids = processed_generated_ids
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
history[-1][1] = response
end_time = time.time()
print(f"\n生成完成,用时: {end_time - start_time:.2f} 秒")
return history
def stop_generation():
stop_event.set()
with gr.Blocks() as demo:
gr.Markdown("# Qwen 聊天机器人")
chatbot = gr.Chatbot()
msg = gr.Textbox()
clear = gr.Button("清除")
stop = gr.Button("停止生成")
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
bot, chatbot, chatbot
)
clear.click(lambda: None, None, chatbot, queue=False)
stop.click(stop_generation, queue=False)
if __name__ == "__main__":
print("启动 Gradio 界面...")
demo.queue()
demo.launch(root_path='/dsw-607012/proxy/7860/')
运行代码即可启动本次项目的界面,测试界面如下: