我们也正在基于Baichun-13B-Base权重,使用Firefly项目代码训练firefly-baichuan-13b模型,目前训了18k步左右,大约消耗了86万条指令数据。目前训练loss下降比较平滑,待训练完毕后,我们也将测试并开源该模型权重。
在本节中,我们将一步一步地详细介绍如何使用Firefly项目对Baichuan-13B进行QLoRA微调,可在一张显卡上进行训练,包括V100和3090。
使用本项目对Baichuan-13B进行训练和推理,主要步骤如下:
-
安装环境。
-
准备训练集。
-
配置训练参数。
-
启动训练。
-
合并权重。
-
模型推理。
我们假定读者具备一定的python编程基础,忽略python、cuda、git等编程环境和工具的安装教程。
首先将Firefly项目代码库clone到本地:
git clone https://github.com/yangjianxin1/Firefly.git
进入项目目录,安装相应的python包:
cd ./Firefly
pip install -r requirements.txt
需要额外注意的点,下面的包务必使用源码安装,以避免不必要的麻烦,且torch版本不要选择2.0,默认使用1.3版本。
pip install git+https:
pip install git+https:
pip install git+https:
pip install git+https:
我们整理并开源了多个较高质量的指令数据集,读者可按需下载。对于中文任务,目前推荐使用moss数据集。数据集下载地址,见Github项目地址。
数据集
|
介绍
|
firefly-train-1.1M |
我们收集了23种常见的NLP任务的数据,并且构造了许多与中华文化相关的数据,如对联、作诗、文言文翻译、散文、金庸小说等。对于每个任务,由人工书写若干种指令模板,保证数据的高质量与丰富度,数据量为115万 |
moss-003-sft-data |
由复旦大学MOSS团队开源的中英文多轮对话数据,包含100万+数据 |
ultrachat |
由清华大学开源的英文多轮对话数据,包含140万+数据 |
WizardLM_evol_instruct_V2_143k |
由WizardLM项目开源的英文指令微调数据集,通过Evol-Instruct方法让指令进化,加强指令的复杂度,以提升模型对复杂指令的遵循能力。包含143k条数据。 |
school_math_0.25M |
由BELLE项目组开源的数学运算指令数据,包含25万条数据。 |
训练数据为jsonl格式,每一行为一个多轮对话,其中conversation字段是必需的,可以根据实际需求添加或删除其他字段。
{
"conversation_id":44,
"category":"Brainstorming",
"conversation":[
{
"human":"怎么能够有效地提升个人能力?",
"assistant":"提升个人能力可以通过以下几个方面实现:\n\n1.
"
},
{
"human":"对于初学者来说,如何快速掌握编程技能?",
"assistant":"以下是几个建议,可以帮助初学者快速掌握编程技能:\n\n1.
学习基础知识:建议先学习编程语言的基础知识和语法,例如变量、数据类型、控制结构、函数等。
可以通过在线课程、编程书籍、视频教程等方式学习。\n\n2. 实践编程:进行实际的编程练习是非常重要的,
可以通过参加编程挑战、编写小项目等方式来进行实践。\n\n3. 寻求反馈:可以寻求他人的反馈和建议,
例如在线编程社区、编程导师等,他们可以帮助你发现问题并及时纠正。\n\n4. 阅读优秀代码:
阅读和分析优秀的代码可以帮助你学习其他程序员的编程思路和技巧,从而提高自己的编程能力。\n\n5.
不断学习和更新:编程技术和工具在不断更新和发展,因此需要不断学习新的知识和技能,
例如学习新的编程语言、框架、工具等,以跟上技术的发展和变化。\n\n总之,学习编程需要耐心和坚持,
并且需要不断地实践和学习。通过以上建议,可以帮助初学者快速掌握编程技能。"
},
{
"human":"除了参加编程挑战和阅读代码,还有哪些方式可以提升自己的编程能力呢?",
"assistant":"除了参加编程挑战和阅读代码,还有以下几种方式可以提升自己的编程能力:
\n\n1. 参与开源项目:加入一个开源项目,可以让你了解到实际的软件开发流程和团队合作方式,"
},
{
"human":"你有没有推荐的编程挑战网站?",
"assistant":"当然,以下是一些值得推荐的编程挑战网站:\n\n1. HackerRank:
HackerRank 是一个全球领先的技术招聘平台,提供数百种编程挑战和比赛,包括算法、数据结构、数学、
SQL、AI 等多个技术领域。\n\n2. LeetCode:LeetCode 是一个在线编程学习和实践平台,
提供数千道编程题目,主要涵盖算法和数据结构。\n\n3. Codeforces:Codeforces 是一个全球知
名的编程竞赛网站,提供数百个比赛和练习题目,主要涵盖算法和数据结构。\n\n4. Codewars:Codewars
是一个在线编程学习和实践平台,提供数千道编程题目,主要涵盖算法、数据结构、面向对象编程等
多个技术领域。\n\n5. Project Euler:Project Euler 是一个非营利性的在线编程挑战平台,
提供一系列数学和计算机科学问题,需要用编程语言进行解答。
\n\n以上这些编程挑战网站可以帮助你提高编程能力,并且可以与其他编程爱好者进行交流和竞争。"
},
{
"human":"在学习编程的过程中,如何避免陷入困境和放弃呢?",
"assistant":"学习编程是一个需要长期坚持和不断学习的过程,以下是一些避免陷入困
境和放弃的方法:\n\n1. 制定合理的学习计划:制定合理的学习计划,包括学习时间、学习内容、目标
等,可以帮助你更好地管理时间和精力,避免学习过程中的松懈和分心。\n\n2. 寻找合适的学习资源:
选择适合自己的学习资源,例如在线课程、编程书籍、视频教程等,可以让你更好地了解和掌握编程知
识和技能。\n\n3. 寻求帮助和支持:在学习过程中,遇到问题和困难是很正常的,可以寻求他人的帮助
和支持,例如参加编程社区、找到编程导师等。\n\n4. 进行实践和项目:实践和项目是学习编程的重要
组成部分,可以帮助你更好地了解和掌握编程技能,同时也可以提高学习的兴趣和动力。\n\n5. 坚持并保
持兴趣:坚持学习和保持兴趣是学习编程的关键。可以通过参加编程社区、参加编程竞赛、与其他编程爱
好者交流等方式来保持兴趣和动力。\n\n总之,学习编程需要耐心和坚持,并需要不断学习和实践。
通过以上方法可以帮助你避免陷入困境和放弃。"
}
],
}
读者也可以使用自己的数据进行模型训练,只需要将数据整理成上述指定的格式即可。
在项目的data/dummy_data.jsonl文件中,我们存放了若干条调试数据,读者可以使用该数据进行代码调试。
本项目中的所有训练参数配置,均存储在train_args目录下,方便统一管理。我们以微调Baichuan-13B为例子,其训练参数的配置文件路径为train_args/qlora/baichuan-13b-sft-qlora.json,读者可以根据自身的硬件条件,对文件中的训练参数进行修改。
训练参数的详细说明如下:
-
output_dir:训练输出目录,存储checkpoint、tokenizer、tensorboard等
-
model_name_or_path:预训练模型的本地目录,或者在huggingface上的模型名称。
-
train_file:训练数据集路径。可以使用data/dummy_data.jsonl进行debug,或者指定为本地的训练文件。
-
num_train_epochs:训练的轮次。如果数据量足够大,一般建议只训一个epoch。
-
per_device_train_batch_size:每张显卡的batch size。
-
gradient_accumulation_steps:梯度累计步数。global batch=num_gpus * per_device_train_batch_size * gradient_accumulation_steps。
-
gradient_checkpointing:如果显存捉襟见肘,可以开启。以时间换空间,模型不缓存激活状态,会进行两次forward计算,以节省显存,我们默认开启。
-
learning_rate:学习率。全量参数微调的时候,建议小一些,1e-5或5e-6。qlora训练时,根据模型大小的不同,建议设置为2e-4或1e-4。
-
max_seq_length:训练时的最大长度。按照自己的设备进行设置,越长需要占用越多显存。
-
logging_steps:每隔多少步打印一次train loss,结果会打印到日志中,也会保存在tensorboard中。
-
save_steps:每隔多少步保存一次模型。
-
save_total_limit:output_dir目录中最多保存多少个checkpoint,超出则会将最旧的删除。
-
lr_scheduler_type:学习率变化策略。
-
warmup_steps:warm up步数。学习率经过多少步,增长到指定的数值。
-
optim:优化器。如果是全量参数微调,建议使用adamw_hf。如果是qlora微调,建议使用paged_adamw_32bit。
-
seed:随机种子,用于复现实验结果。
-
fp16:使用使用fp16混合精度。V100建议开启。
-
bf16:使用使用fp16混合精度。A100建议开启。
-
lora_rank:qlora矩阵的秩。一般设置为8、16、32、64等,在qlora论文中作者设为64。越大则参与训练的参数量越大,一般来说效果会更好,但需要更多显存,。
-
lora_alpha: qlora中的缩放参数。一般设为16、32即可。
-
lora_dropout: lora权重的dropout rate。
在训练Baichuan-13B时,我们的训练配置如下,读者可按需调整:
{
"output_dir": "output/firefly-baichuan-13b",
"model_name_or_path": "baichuan-inc/Baichuan-13B-Base",
"train_file": "./data/moss-003-sft-data.jsonl",
"num_train_epochs": 1,
"per_device_train_batch_size": 6,
"gradient_accumulation_steps": 2,
"learning_rate": 1e-4,
"max_seq_length": 900,
"logging_steps": 300,
"save_steps": 500,
"save_total_limit": 1,
"lr_scheduler_type": "constant_with_warmup",
"warmup_steps": 3000,
"lora_rank": 64,
"lora_alpha": 16,
"lora_dropout": 0.05,
"gradient_checkpointing": true,
"disable_tqdm": false,
"optim": "paged_adamw_32bit",
"seed": 42,
"fp16": true,
"report_to": "tensorboard",
"dataloader_num_workers": 5,
"save_strategy": "steps",
"weight_decay": 0,
"max_grad_norm": 0.3,
"remove_unused_columns": false
}
model_name_or_path可以是huggingface的模型仓库的名称,也可以是本地的模型路径。如果是huggingface的模型仓库名称,训练脚本会自动下载该模型的权重、tokenizer和代码等。如果访问速度较慢,建议先将模型下载到本地,训练时指定本地模型路径。
如果发生OOM,可以缩小max_seq_length、per_device_train_batch_size等参数来缓解。也可以设gradient_checkpointing=true,可以大幅降低显存占用,但训练速度会变慢一些,我们默认开启该参数。
执行如下脚本,即可启动训练,其中num_gpus指的是训练的显卡数。global batch = per_device_train_batch_size * gradient_accumulation_steps * num_gpus。
torchrun --nproc_per_node={num_gpus} train_qlora.py --train_args_file train_args/qlora/
baichuan-13b-sft-qlora.json
我们在V100上进行训练,对于7B的模型,每个step大约15秒左右,对于13B的模型,每个step大约30秒左右。
为了降低保存checkpoint的时间成本,提高训练效率,在训练中,我们只保存adapter的权重,不保存合并后的模型权重。
当训练结束时,我们需要手动将adapter与base model进行权重合并。adapter的权重会保存到output_dir设置的目录,执行script目录下的merge_lora.py脚本即可得到合并后的模型权重。
注意,对于baichuan等模型,由于其自定义了模型结构和tokenizer,并且其代码并未合并到transformers代码库中。所以合并权重之后,需要将其huggingface模型仓库中的python文件也复制到合并权重的目录中,否则加载合并模型进行推理时会出错。
权重合并的脚本如下,请根据实际的base model以及adapter的保存路径,修改save_path、adapter_name_or_path、model_name_or_path等参数即可。
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
"""
使用该脚本,将lora的权重合并到base model中
"""
def merge_lora_to_base_model():
model_name_or_path = 'baichuan-inc/baichuan-13B-Base'
adapter_name_or_path = 'output/firefly-baichuan-13b/checkpoint-20000'
save_path = 'checkpoint/firefly-baichuan-13b'
tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path,
trust_remote_code=True
)
model = AutoModelForCausalLM.from_pretrained(
model_name_or_path,
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
device_map='auto'
)
model = PeftModel.from_pretrained(model, adapter_name_or_path)
model = model.merge_and_unload()
tokenizer.save_pretrained(save_path)
model.save_pretrained(save_path)
if __name__ == '__main__':
merge_lora_to_base_model()
权重合并之后,我们就可以使用该模型进行推理了。项目提供了单轮对话和多轮对话的脚本,详见script/chat目录。该脚本可以兼容本项目训练的所有模型。
生成脚本中的top_p、repetition_penalty、temperature、do_sample等参数对模型的生成效果影响较大,可按照自己的使用场景进行调试修改。
在推理阶段,模型的解码方式对模型的生成效果的影响也非常大。目前主要有Greedy Search、Beam Search、Top-K Sampling、Top-P Sampling(Nucleus Sampling)、Contrastive Search等解码方式。
目前主流模型大多使用Top-P Sampling,该算法具有一定的随机性,能提高生成结果的丰富度,降低重复输出,本项目也使用该方式。Contrastive Search也值得一试,是一种确定性的解码算法。
解码方式值得专门写一篇文章进行介绍,如果大家有兴趣,我们后续也可以对其进行介绍。
单轮对话:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
"""
单轮对话,不具有对话历史的记忆功能
"""
def main():
model_name = 'YeungNLP/firefly-baichuan-13b'
max_new_tokens = 500
top_p = 0.9
temperature = 0.35
repetition_penalty = 1.0
device = 'cuda'
input_pattern = '<s>{}</s>'
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
device_map='auto'
).to(device).eval()
tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True,
use_fast=False if model.config.model_type == 'llama' else True
)
text = input('User:')
while True:
text = text.strip()
text = input_pattern.format(text)
input_ids = tokenizer(text, return_tensors="pt", add_special_tokens=False).input_ids.to(device)
with torch.no_grad():
outputs = model.generate(
input_ids=input_ids, max_new_tokens=max_new_tokens, do_sample=True,
top_p=top_p, temperature=temperature, repetition_penalty=repetition_penalty,
eos_token_id=tokenizer.eos_token_id
)
outputs = outputs.tolist()[0][len(input_ids[0]):]
response = tokenizer.decode(outputs)
response = response.strip().replace(text, "").replace('</s>', "").replace('<s>', "").strip()
print("Firefly:{}".format(response))
text = input('User:')
if __name__ == '__main__':
main()
多轮对话:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
def main():
model_name = 'YeungNLP/firefly-baichuan-13b'
device = 'cuda'
max_new_tokens = 500
history_max_len = 1000
top_p = 0.9
temperature = 0.35
repetition_penalty = 1.0
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
device_map='auto'
).to(device).eval()
tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True,
use_fast=False if model.config.model_type == 'llama' else True
)
history_token_ids = tokenizer('<s>', return_tensors="pt").input_ids
user_input = input('User:')
while True:
user_input = '{}</s>'.format(user_input)
user_input_ids = tokenizer(user_input, return_tensors="pt", add_special_tokens=False).input_ids
history_token_ids = torch.concat((history_token_ids, user_input_ids), dim=1)
model_input_ids = history_token_ids[:, -history_max_len:].to(device)
with torch.no_grad():
outputs = model.generate(
input_ids=model_input_ids, max_new_tokens=max_new_tokens, do_sample=True, top_p=top_p,
temperature=temperature, repetition_penalty=repetition_penalty, eos_token_id=tokenizer.eos_token_id
)
model_input_ids_len = model_input_ids.size(1)
response_ids = outputs[:, model_input_ids_len:]
history_token_ids = torch.concat((history_token_ids, response_ids.cpu()), dim=1)
response = tokenizer.batch_decode(response_ids)
print("Firefly:" + response[0].strip().replace('</s>', ""))
user_input = input('User:')
if __name__ == '__main__':
main()