赞
踩
该系列文章用于介绍使用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这类开源库完成。
算法专家的工作在于了解其算法原理,能够搭建模型、处理数据、训练模型、在多卡情况下训练模型、在终端设备上部署模型、优化模型(速度和精度)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。