学习链接:Datawhale
本次task我们主要要完成几个新的任务:LoRA微调、vllm加速和多路LLM投票
一步一步来,先启动notebook,注意这次环境变了,记得选用GPU,预装镜像ubuntu22.04-cuda12.1.0-py310-torch2.1.2-tf2.14.0-1.14.0
本次实验文件代码都放在了这个地方:魔搭社区,有需要的可以自取,我们可以在notebook通过控制台直接下载,git代码如下:
git clone https://www.modelscope.cn/datasets/Datawhale/DW2024_Complex_reasoning_ability_assessment_qwen2-7b-lora.git
LoRA微调
LoRA微调的就不作解释了,可以去GitHub或者知乎等地学习,这里给出datawhale的GitHub repository:
https://github.com/datawhalechina/self-llm里面有非常详细的llm教程
在下载了代码文件后,我们进入W2024_Complex_reasoning_ability_assessment_qwen2-7b-lora文件夹打开lora.ipynb文件
首先安装环境,下载模型文件:
!pip install modelscope==1.9.5
!pip install "transformers>=4.39.0"
!pip install streamlit==1.24.0
!pip install sentencepiece==0.1.99
!pip install transformers_stream_generator==0.0.4
!pip install datasets==2.18.0
!pip install peft==0.10.0
!pip install openai==1.17.1
!pip install tqdm==4.64.1
!pip install transformers==4.39.3
!python -m pip install setuptools==69.5.1
!pip install vllm==0.4.0.post1
!pip install nest-asyncio
!pip install accelerate
!pip install tf-keras
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('qwen/Qwen2-7B-Instruct', cache_dir='./', revision='master')
运行到这一步我们重启notebook(别直接给实例关了,点上面那个圈圈或者在顶栏的Kernel-restart Kernal重启内核)
下面几行也都是环境下载和导入文件,我们看process_func(example)函数,Lora
训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 Pytorch
模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids
,将输出文本编码为 labels
,编码之后的结果都是多维的向量。我们首先定义一个预处理函数process_func(example),这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典。
def process_func(example):
MAX_LENGTH = 1800 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer(f"<|im_start|>system\n你是一个逻辑推理专家,擅长解决逻辑推理问题。<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens
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] # 因为eos token咱们也是要关注的所以 补充为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
}
定义LoraConfig:
from peft import LoraConfig, TaskType, get_peft_model
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 比例
)
config
创建peftmodel:
model = get_peft_model(model, config)
config
model.print_trainable_parameters()
自定义Training Arguments参数:
args = TrainingArguments(
output_dir="./output/Qwen2_instruct_lora",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=1,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True
)
接下来几个cell就是模型训练,大概花个十多分钟跑完。然后是微调模型测试和模型合并存储:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModel
mode_path = './qwen/Qwen2-7B-Instruct/'
lora_path = './output/Qwen2_instruct_lora_an/checkpoint-100' # 这里改称你的 lora 输出对应 checkpoint 地址
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path, trust_remote_code=True)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.float16, trust_remote_code=True).eval()
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)
prompt = '''你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下:\n\n### 题目:\n假设您需要构建一个二叉搜索树,其中每个节点或者是一个空的节点(称为"空节点"),或者是一个包含一个整数值和两个子树的节点(称为"数值节点")。以下是构建这棵树的规则:\n\n1. 树中不存在重复的元素。\n2. 对于每个数值节点,其左子树的所有值都小于该节点的值,其右子树的所有值都大于该节点的值。\n3. 插入一个新值到一个"空节点"时,该"空节点"会被一个包含新值的新的数值节点取代。\n4. 插入一个已存在的数值将不会改变树。\n\n请基于以上规则,回答以下选择题:\n\n### 问题:\n选择题 1:\n给定一个空的二叉搜索树,插入下列数字: [5, 9, 2, 10, 11, 3],下面哪个选项正确描述了结果树的结构?\nA. tree(5, tree(2, tree(3, nil, nil), nil), tree(9, tree(10, nil, nil), tree(11, nil, nil)))\nB. tree(5, tree(2, nil, tree(3, nil, nil)), tree(9, nil, tree(10, nil, tree(11, nil, nil))))\nC. tree(5, tree(3, tree(2, nil, nil), nil), tree(9, nil, tree(10, tree(11, nil, nil), nil)))\nD. tree(5, nil, tree(2, nil, tree(3, nil, nil)), tree(9, tree(11, nil, nil), tree(10, nil, nil)))'''
inputs = tokenizer.apply_chat_template([{"role": "user", "content": "你是一个逻辑推理专家,擅长解决逻辑推理问题。"},{"role": "user", "content": prompt}],
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True
).to('cuda')
gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}
with torch.no_grad():
outputs = model.generate(**inputs, **gen_kwargs)
outputs = outputs[:, inputs['input_ids'].shape[1]:]
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# 模型合并存储
new_model_directory = "./merged_model_an"
merged_model = model.merge_and_unload()
# 将权重保存为safetensors格式的权重, 且每个权重文件最大不超过2GB(2048MB)
merged_model.save_pretrained(new_model_directory, max_shard_size="2048MB", safe_serialization=True)
!cp ./qwen/Qwen2-7B-Instruct/tokenizer.json ./merged_model_an/
运行到这里,重启notebook内核不然显存会爆!同样如果后面显存爆了就重启内核试试
vllm加速
vLLM(Virtual Large Language Model)是一个由伯克利大学LMSYS组织开源的大规模语言模型高速推理框架。它的设计目标是在实时应用场景中大幅提升语言模型服务的吞吐量和内存使用效率。vLLM的特点包括易于使用、与Hugging Face等流行工具无缝集成以及高效的性能。
我们打开左侧start_vllm.ipynb文件运行,启动vllm服务,微调后的模型部署到8000端口。
多路vllm投票
上分的另一种思路,分别召回一部分候选集,然后再把这些候选集混合在一起后供后续排序模型使用。通过三次结果推理,将选择答案最多的结果作为最终结果:
def most_frequent_char(char1, char2, char3):
# 创建一个字典来存储每个字符的出现次数
frequency = {char1: 0, char2: 0, char3: 0}
# 增加每个字符的出现次数
frequency[char1] += 1
frequency[char2] += 1
frequency[char3] += 1
# 找到出现次数最多的字符
most_frequent = max(frequency, key=frequency.get)
return most_frequent
改写process函数,三次调用llm,做出现次数统计,最终返回投票数最多的结果:
def process_datas(datas,MODEL_NAME):
results = []
# 送入多线程任务
for data in tqdm(datas, desc="Submitting tasks", total=len(datas)):
problem = data['problem']
for id,question in enumerate(data['questions']):
prompt = get_prompt(problem,
question['question'],
question['options'],
)
# 统一使用llm 三次调用
res,res1,res2 = api_retry(MODEL_NAME, prompt),api_retry(MODEL_NAME, prompt),api_retry(MODEL_NAME, prompt)
# 统一做结果抽取
extract_response,extract_response1,extract_response2 = extract(res),extract(res1),extract(res2)
# 通过投票函数获取最终结果并返回
ans = most_frequent_char(extract_response,extract_response1,extract_response2)
data['questions'][id]['answer'] = ans
results.append(data)
return results
baseline2_evaluate.ipynb
主要代码放在这个文件中,我们点击运行生成新的upload.jsonl文件提交结果产生新分数
还有一个evaluate文件,运行起来会报错,原因是缺少了train文件,训练数据集可以在官网找到,以下是链接:上海科学智能研究院
最后祝大家能打开思路,尝试各种不同方法共同上分。