该系列文章用于介绍使用peft库来进行大模型的微调
第一章 使用PEFT对ChatGLM3-6B进行LORA微调
PEFT简介: PEFT(Parameter-Efficient Fine-Tuning)是一个库,用于有效地使大型预训练模型适应各种下游应用程序,而无需微调模型的所有参数,因为它的成本高得令人望而却步。PEFT方法仅微调少量(额外)模型参数 - 显着降低计算和存储成本 - 同时产生与完全微调模型相当的性能。这使得在消费者硬件上训练和存储大型语言模型 (LLM) 变得更加容易。
PEFT 与 Transformers、Diffusers 和 Accelerate 库集成,以更快、更简单的方式加载、训练和使用大型模型进行推理
声明:
本文实现主要参考自:https://github.com/Suffoquer-fang/LuXun-GPT
该库主要实现了一个从普通语句到鲁迅风格语句的转换的一个LORA微调。但其使用版本较老,已经难以复现。本文在其基础上进行了修改,以适应当前最新版本(peft和chatglm3)的微调,并整理到了该库:https://github.com/foolishortalent/AIGC/tree/main/LuXun%20trans%20chatglm。
# int8
bitsandbytes
accelerate
# chatglm
modelscope # 国内用户用这个比较快
protobuf
transformers
icetk
cpm_kernels
torch
#
datasets
peft>=0.7.1
我们这里使用的是ChatGLM3-6B,运行如下代码便会自行从Hugging Face加载模型。
from transformers import AutoTokenizer, AutoModel
model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
国内同学可以通过魔搭社区下载,和从Hugging Face下载的是一样的。
魔搭:https://modelscope.cn/models/ZhipuAI/chatglm3-6b/summary
pip install modelscope
from modelscope import snapshot_download
model_dir = snapshot_download("ZhipuAI/chatglm3-6b", revision = "v1.0.0")
鲁迅说过:“有多少人工,便有多少智能。”
这也是挡在众多大语言模型微调开发者前面的一座大山,但好在有github:)
本博文整理的git库中包含的数据由对参考库中的example_data修改而来。
参考库中luxun_data.jsonl是selected_aug.jsonl经过与随机一个下面指令相结合生成的:
为了减少回答中出现英文的可能性,我们使用中文代替了原模版中的英文单词:
替代前 数据实例:
{"context": "Instruction: 将这句话改写成鲁迅风格的语言\nInput: 虽然打高尔夫、脱钩和参加豪门社交都是炫耀身份的行为,但如此疯狂追求名利地步就算被曝出丑闻,也不为过。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 将这句话改写成鲁迅风格的语言\nInput: 在追逐虚荣和地位的过程中,参加高尔夫球赛、脱离常规、参加豪门社交等活动已成为一种炫耀的方式,但这样的沉迷甚至即使被曝出丑闻也不为过。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 你是一个非常熟悉鲁迅风格的作家,请用鲁迅的风格改写这句话\nInput: 打高尔夫、脱掉脱钩、参加社交活动等的追求地位与名望的行为已经到了疯狂的程度,哪怕发生丑闻也不会有任何过错。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "Instruction: 用鲁迅的风格改写\nInput: 尽管打高尔夫、脱钩、参加豪门社交等活动是一种炫耀身份和社会地位的行为,但如此的沉迷到了这种地步,即使曝出丑闻也不算是过分。\nAnswer: ", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
替代后 数据实例:
{"context": "指令:将这句话改写成鲁迅风格的语言\n语句:虽然打高尔夫、脱钩和参加豪门社交都是炫耀身份的行为,但如此疯狂追求名利地步就算被曝出丑闻,也不为过。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:将这句话改写成鲁迅风格的语言\n语句:在追逐虚荣和地位的过程中,参加高尔夫球赛、脱离常规、参加豪门社交等活动已成为一种炫耀的方式,但这样的沉迷甚至即使被曝出丑闻也不为过。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:你是一个非常熟悉鲁迅风格的作家,请用鲁迅的风格改写这句话\n语句:打高尔夫、脱掉脱钩、参加社交活动等的追求地位与名望的行为已经到了疯狂的程度,哪怕发生丑闻也不会有任何过错。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
{"context": "指令:用鲁迅的风格改写\n语句:尽管打高尔夫、脱钩、参加豪门社交等活动是一种炫耀身份和社会地位的行为,但如此的沉迷到了这种地步,即使曝出丑闻也不算是过分。\n答:", "target": "“打高尔夫”“脱钩”,“豪门社交”的熏灼之状,竟至于斯,则虽报以丑闻,亦不为过。"}
import json
from tqdm import tqdm
import datasets
import transformers
def preprocess(tokenizer, config, example, max_seq_length):
prompt = example["context"]
target = example["target"]
prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
target_ids = tokenizer.encode(
target,
max_length=max_seq_length,
truncation=True,
add_special_tokens=False)
input_ids = prompt_ids + target_ids + [config.eos_token_id]
return {"input_ids": input_ids, "seq_len": len(prompt_ids)}
def read_jsonl(path, max_seq_length, skip_overlength=False):
model_name = "ZhipuAI//chatglm-6b"
tokenizer = transformers.AutoTokenizer.from_pretrained(
model_name, trust_remote_code=True)
config = transformers.AutoConfig.from_pretrained(
model_name, trust_remote_code=True, device_map='auto')
with open(path, "r") as f:
for line in tqdm(f.readlines()):
example = json.loads(line)
feature = preprocess(tokenizer, config, example, max_seq_length)
if skip_overlength and len(feature["input_ids"]) > max_seq_length:
continue
feature["input_ids"] = feature["input_ids"][:max_seq_length]
yield feature
jsonl_path = "lunxun-style-data/luxun_data.jsonl"
save_path = "lunxun-style-data/luxun"
max_seq_length = 500
skip_overlength = False
dataset = datasets.Dataset.from_generator(
lambda: read_jsonl(args.jsonl_path, args.max_seq_length, args.skip_overlength)
)
dataset.save_to_disk(args.save_path)
我们通过preprocess方法来对luxun_data.jsonl中的每一条数据进行token化处理,将语句转化为代表词语编码序号的一系列序号id。最后返回的 **{“input_ids”: input_ids, “seq_len”: len(prompt_ids)}**中,input_ids由prompt_ids、target_ids、config.eos_token_id三部分拼接而成,seq_len则记录prompt_ids的长度。
构建MyTrainingArguments类,用于接收训练所需参数:
构建MyTrainer类,用于训练。实现了父类的compute_loss和save_model的函数。
# coding=utf-8
import sys
sys.path.append("./")
from dataclasses import dataclass, field
import os
from transformers import (
TrainingArguments,
Trainer,
)
@dataclass
class MyTrainingArguments(TrainingArguments):
max_steps: int = field(default=5000)
save_steps: int = field(default=1000)
learning_rate: float = field(default=1e-4)
fp16: bool = field(default=True)
remove_unused_columns: bool = field(default=False)
logging_steps: int = field(default=50)
output_dir: str = field(default="LuXun-lora")
per_device_train_batch_size: int = field(default=4)
gradient_accumulation_steps: int = field(default=2)
dataset_path: str = field(default="lunxun-style-data/luxun")
lora_rank: int = field(default=8)
import torch
class MyTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False):
return model(
input_ids=inputs["input_ids"],
labels=inputs["labels"],
).loss
def save_model(self, output_dir=None, _internal_call=False):
from transformers.trainer import TRAINING_ARGS_NAME
os.makedirs(output_dir, exist_ok=True)
torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
self.model.save_pretrained(output_dir)
通过build_model来构建待训练的peft模型,通过LoraConfig来设置所需训练的Lora参数:
from transformers import HfArgumentParser
from transformers import AutoTokenizer, AutoModel
import datasets
from peft import get_peft_model, LoraConfig, TaskType
from utils import get_data_collator
from training_arguments import MyTrainingArguments, MyTrainer
def build_model(training_args):
print("#> Building model...")
model = AutoModel.from_pretrained(
"ZhipuAI/chatglm3-6b", load_in_8bit=True, trust_remote_code=True, device_map="auto"
)
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.is_parallelizable = True
model.model_parallel = True
model.config.use_cache = (
False # silence the warnings. Please re-enable for inference!
)
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=training_args.lora_rank,
lora_alpha=32,
lora_dropout=0.1,
target_modules=["query_key_value"]
)
model = get_peft_model(model, peft_config)
print("#> Model built.")
print("#> Total Trainable Parameters:", sum(p.numel() for p in model.parameters() if p.requires_grad))
print("#> Total Parameters:", sum(p.numel() for p in model.parameters()), "\n")
return model
def main():
# parse args
training_args = HfArgumentParser(MyTrainingArguments).parse_args_into_dataclasses()[0]
training_args.remove_unused_columns = False
print("#> Loading dataset...")
dataset = datasets.load_from_disk(training_args.dataset_path)
dataset.set_format(
type=dataset.format["type"],
columns=list(dataset.features.keys()),
)
print("#> Dataset loaded.", "Total samples:", len(dataset), "\n")
# build model
model = build_model(training_args)
tokenizer = AutoTokenizer.from_pretrained("ZhipuAI/chatglm3-6b", trust_remote_code=True)
print("#> Start training...")
# start train
trainer = MyTrainer(
model=model,
train_dataset=dataset,
args=training_args,
data_collator=get_data_collator(tokenizer),
)
trainer.train()
model.save_pretrained(training_args.output_dir)
print("#> Training finished.")
print("#> Model saved to:", training_args.output_dir)
if __name__ == "__main__":
main()
utils中的get_data_collator返回一个数据校对器,用于将所有训练数据的token id、标签数据的token id补齐为相同长度。
def get_data_collator(tokenizer: AutoTokenizer):
def data_collator(features: list) -> dict:
len_ids = [len(feature["input_ids"]) for feature in features]
longest = max(len_ids)
input_ids = []
labels_list = []
for ids_l, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
ids = feature["input_ids"]
seq_len = feature["seq_len"]
labels = (
[-100] * (longest-ids_l+seq_len) + ids[seq_len:]
)
ids = [tokenizer.pad_token_id] * (longest - ids_l) + ids
_ids = torch.LongTensor(ids)
labels_list.append(torch.LongTensor(labels))
input_ids.append(_ids)
input_ids = torch.stack(input_ids)
labels = torch.stack(labels_list)
return {
"input_ids": input_ids,
"labels": labels,
}
return data_collator
训练指令:
python lora_finetune.py
from transformers import AutoModel
import torch
from transformers import AutoTokenizer
from peft import PeftModel
import argparse
def generate(instruction, text):
with torch.no_grad():
input_text = f"指令:{instruction}\n语句:{text}\n答:"
ids = tokenizer.encode(input_text)
input_ids = torch.LongTensor([ids]).cuda()
output = peft_model.generate(
input_ids=input_ids,
max_length=500,
do_sample=False,
temperature=0.0,
num_return_sequences=1
)
output = tokenizer.decode(output[0])
answer = output.split("答:")[-1]
return answer.strip()
if __name__ == "__main__":
base_model="ZhipuAI/chatglm3-6b"
lora="LuXun-lora"
instruction="你是一个非常熟悉鲁迅风格的作家,用鲁迅风格的积极正面的语言改写,保持原来的意思:"
model = AutoModel.from_pretrained(base_model, trust_remote_code=True, load_in_8bit=True, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(base_model, trust_remote_code=True)
if args.lora == "":
print("#> No lora model specified, using base model.")
peft_model = model.eval()
else:
print("#> Using lora model:", lora)
peft_model = PeftModel.from_pretrained(model, lora).eval()
torch.set_default_tensor_type(torch.cuda.FloatTensor)
texts = [
"你好",
"有多少人工,便有多少智能。",
"落霞与孤鹜齐飞,秋水共长天一色。",
"我去买几个橘子,你就站在这里,不要走动。",
"学习计算机技术,是没有办法救中国的。",
"我怎么样都起不了床,我觉得我可能是得了抑郁症吧。",
"它是整个系统的支撑架构,连接处理器、内存、存储、显卡和外围端口等所有其他组件。",
"古巴导弹危机和越南战争是20世纪最大、最致命的两场冲突。古巴导弹危机涉及美国和苏联之间的僵局,因苏联在古巴设立核导弹基地而引发,而越南战争则是北方(由苏联支持)和南方(由美国支持)之间在印度支那持续的军事冲突。",
"齿槽力矩是指旋转设备受到齿轮牙齿阻力时施加的扭矩。",
"他的作品包括蒙娜丽莎和最后的晚餐,两者都被认为是杰作。",
"滑铁卢战役发生在1815年6月18日,是拿破仑战争的最后一场重大战役。"
]
for text in texts:
print(text)
print(generate(args.instruction, text), "\n")
你好
您好,有什么事吗?
有多少人工,便有多少智能。
倘说:这便是智能的时代,而已。
落霞与孤鹜齐飞,秋水共长天一色。
秋天的景色,例如落霞,给天空带来美丽的画面,它却也只需要在天空飞翔,在秋水面上飞翔,在所谓美丽天空上飞翔。
我去买几个橘子,你就站在这里,不要走动。
我便咬定:我去买橘子,你站在此处,不要移动。
学习计算机技术,是没有办法救中国的。
倘要说学计算机技术,便只能说:“我国尚未成功”,而已。
我怎么样都起不了床,我觉得我可能是得了抑郁症吧。
因为我要爬不起床来,我才能觉得我是个抑郁症患者。
它是整个系统的支撑架构,连接处理器、内存、存储、显卡和外围端口等所有其他组件。
它诚然是这个系统的关键部分,一切处理器,一切内存,一切存储,都需要通过它来连接和传输,便在于它的连接和传输。
古巴导弹危机涉及美国和苏联之间的僵局,因苏联在古巴设立核导弹基地而引发,而越南战争则是北方(由苏联支持)和南方(由美国支持)之间在印度支那持续的军事冲突。
古巴导弹危机,大抵是古巴导弹的僵局,或者说是美国导弹的僵局;而越南战争,则放任北方支持,或者说是北方军事占领,或者是什么像越南的印度支那般持续军事对峙。
齿槽力矩是指旋转设备受到齿轮牙齿阻力时施加的扭矩。
齿轮的旋转,显然需要一定的扭矩。也许 toothless rotation 才是旋转设备遇到的问题。
他的作品包括蒙娜丽莎和最后的晚餐,两者都被认为是杰作。
他的作品,如以蒙娜丽莎为准,说是艺术杰作;以最后的晚餐为准,则称之為杰作也不為過。
滑铁卢战役发生在1815年6月18日,是拿破仑战争的最后一场重大战役。
滑铁卢战役的胜利,是拿破仑战争中的最后一次胜利, accordingly它也是拿破仑战争中最有意义的战役。
正如先生所言:“倘说:这便是智能的时代,而已。”
数据的优劣决定了模型的优质与否。智能的时代是数据的时代。而像lora、p-tuning等微调技术均可以由peft这类开源库完成。
算法专家的工作在于了解其算法原理,能够搭建模型、处理数据、训练模型、在多卡情况下训练模型、在终端设备上部署模型、优化模型(速度和精度)。