本期的任务是对原项目进行微调——baseline02 微调方案
1、微调介绍(为什么要微调)
1.1 大语言模型微调
大模型微调(Fine-tuning)是一种技术,通过在预训练的大型语言模型上使用特定数据集进行进一步训练,使模型能够更好地适应特定任务或领域。
其核心原理在于,机器学习模型只能代表其训练数据的逻辑和理解。对于未见过的数据样本,模型可能无法准确识别或理解。对于大型模型而言,它们虽然能够处理广泛的语言信息并进行流畅的对话,但在特定场景下可能无法提供准确的答案。
例如,一个通用的大型语言模型虽然包含丰富的语言信息,但在医药领域的特定问题上可能表现不佳。如果需要一个能够准确回答患者问题的医药应用,就需要为这个通用模型提供大量新的医药数据进行学习和理解。比如,当患者询问“布洛芬能否与感冒药同时服用?”时,为了确保模型能够给出正确的回答,我们需要对基础模型进行微调。
1.2 微调模型对于本次比赛有什么意义
-
上下文理解提升:微调过程中使用的特定数据集可以帮助模型更好地理解特定任务的上下文,从而在推理时能够考虑到更多的相关信息和细节。
-
性能优化:微调可以针对特定任务优化模型的性能,使其在处理该任务时达到更高的准确率和更低的错误率。这对于需要高可靠性和准确性的推理任务尤为重要。
-
减少数据需求:对于一些数据稀缺的领域或任务,微调可以在相对较少的数据量下实现较好的性能提升,因为模型已经具备了大量的通用语言知识。
-
适应性增强:微调使模型能够更好地适应特定用户或场景的需求,提供更加个性化和定制化的推理服务。
2、LoRA微调
LoRA介绍
LoRA(Low-Rank Adaptation)微调是一种高效的模型微调技术,特别适用于大型预训练语言模型的适应性调整。LoRA的核心思想是通过引入低秩矩阵来调整模型的权重,从而在不显著增加模型参数数量的情况下,实现对模型的微调。
LoRA 的优势
- 可以针对不同的下游任务构建小型 LoRA 模块,从而在共享预训练模型参数基础上有效地切换下游任务。
- LoRA 使用自适应优化器(Adaptive Optimizer),不需要计算梯度或维护大多数参数的优化器状态,训练更有效、硬件门槛更低。
- LoRA 使用简单的线性设计,在部署时将可训练矩阵与冻结权重合并,不存在推理延迟。
- LoRA 与其他方法正交,可以组合。
LoRA原理
代码介绍
!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
下载模型文件,这里我们下载魔搭社区的Qwen2-7B-Instruct模型参数。
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('qwen/Qwen2-7B-Instruct', cache_dir='./', revision='master')
接下来需要加载微调的环境
from datasets import Dataset
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, GenerationConfig
导入文件使用(an文件)
# 将JSON文件转换为CSV文件
df = pd.read_json('an.json')
ds = Dataset.from_pandas(df)
加载tokenlizer
tokenizer = AutoTokenizer.from_pretrained('./qwen/Qwen2-7B-Instruct', use_fast=False, trust_remote_code=True)
tokenizer
数据格式化
Lora
训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 Pytorch
模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids
,将输出文本编码为 labels
,编码之后的结果都是多维的向量。我们首先定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典:
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
}
调用process_func
函数处理数据,并进行decode
数据检查
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
tokenized_id
tokenizer.decode(tokenized_id[0]['input_ids'])
tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"])))
打印模型结构
import torch
model = AutoModelForCausalLM.from_pretrained('./qwen/Qwen2-7B-Instruct', device_map="auto",torch_dtype=torch.bfloat16)
model
定义LoraConfig
LoraConfig
这个类中可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看源码。
-
task_type
:模型类型 -
target_modules
:需要训练的模型层的名字,主要就是attention
部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。 -
r
:lora
的秩,具体可以看Lora
原理 -
lora_alpha
:Lora alaph
,具体作用参见Lora
原理
Lora
的缩放是啥嘞?当然不是r
(秩),这个缩放就是lora_alpha/r
, 在这个LoraConfig
中缩放就是4倍。
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
使用 get_peft_model()
函数创建一个 PeftModel
。
它需要一个基本模型(您可以从 Transformers
库加载)和 LoraConfig
,其中包含如何配置模型以使用 LoRA 进行训练的参数。
model = get_peft_model(model, config)
config
model.print_trainable_parameters()
自定义 TrainingArguments 参数
TrainingArguments
这个类的源码也介绍了每个参数的具体作用,当然大家可以来自行探索,这里就简单说几个常用的。
-
output_dir
:模型的输出路径 -
per_device_train_batch_size
:顾名思义batch_size
-
gradient_accumulation_steps
: 梯度累加,如果你的显存比较小,那可以把batch_size
设置小一点,梯度累加增大一些。 -
logging_steps
:多少步,输出一次log
-
num_train_epochs
:顾名思义epoch
-
gradient_checkpointing
:梯度检查,这个一旦开启,模型就必须执行model.enable_input_require_grads()
,这个原理大家可以自行探索
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
)
模型合并存储
这里将lora微调后的模型融入到原模型中
# 模型合并存储
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/
3、vllm加速
3.1 vllm介绍
vLLM(Virtual Large Language Model)是一个由伯克利大学LMSYS组织开源的大规模语言模型高速推理框架。它的设计目标是在实时应用场景中大幅提升语言模型服务的吞吐量和内存使用效率。vLLM的特点包括易于使用、与Hugging Face等流行工具无缝集成以及高效的性能。
3.2 vllm服务启动
接下来请打开start_vllm.ipynb,执行后我们通过vllm的类openai接口成功将微调后的模型部署到8000端口。
3.3 vllm api调用
我们改写了baseline中的call_qwen_api代码,该文调用本地类openai的qwen微调模型接口。
def call_qwen_api(MODEL_NAME, query):
# 这里采用dashscope的api调用模型推理,通过http传输的json封装返回结果
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="sk-xxx", # 随便填写,只是为了通过接口参数校验
)
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
# {'role':'system','content':'你是一个解决推理任务的专家,你需要分析出问题中的每个实体以及响应关系。然后根据问题一步步推理出结果。并且给出正确的结论。'},
{"role": "user", "content": query}
]
)
return completion.choices[0].message.content