大型语言模型(LLM)在去年取得了很大进展。我们从没有 ChatGPT 的竞争对手变成了一整套 LLM 动物园,包括 Meta AI 的 Llama 2、Mistrals Mistral 和 Mixtral 模型、TII Falcon 等等。 这些 LLM 可用于各种任务,包括聊天机器人、问答、总结,无需任何额外培训。但是,如果要为应用程序自定义模型。您可能需要对数据进行微调,以获得更高质量的结果,而不是通过训练更小的模型来提示或节省成本,更高效的模型。
这篇博文将引导您了解如何在 2024 年使用 Hugging Face TRL、Transformer 和数据集微调开放 LLM。在博客中,我们将:
注意:本博客旨在在消费级 GPU (24GB) 上运行,例如 NVIDIA A10G 或 RTX 4090/3090,但可以轻松适应在更大的 GPU 上运行。
1. 定义我们的用例
在微调 LLM 时,了解您的用例和想要解决的任务非常重要。这将帮助您选择正确的模型或帮助您创建数据集来微调模型。如果您尚未定义用例。您可能想回到绘图板。 我想提一下,并非所有用例都需要微调,始终建议在微调自己的模型之前评估和试用已经微调的模型或基于 API 的模型。
例如,我们将使用以下用例:
我们想要微调一个模型,该模型可以基于自然语言指令生成 SQL 查询,然后可以将其集成到我们的 BI 工具中。目标是减少创建 SQL 查询所需的时间,并使非技术用户更容易创建 SQL 查询。
文本转 SQL 可能是微调 LLM 的一个很好的用例,因为它是一项复杂的任务,需要大量关于数据和 SQL 语言的(内部)知识。
2. 搭建开发环境
我们的第一步是安装 Hugging Face Libraries 和 Pytorch,包括 trl、transformer 和数据集。如果您还没有听说过 trl,请不要担心。它是 transformer 和数据集之上的新库,可以更轻松地微调、rlhf、对齐开放 LLM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Install Pytorch & other libraries !pip install "torch==2.1.2" tensorboard # Install Hugging Face libraries !pip install --upgrade \ "transformers==4.36.2" \ "datasets==2.16.1" \ "accelerate==0.26.1" \ "evaluate==0.4.1" \ "bitsandbytes==0.42.0" \ # "trl==0.7.10" # \ # "peft==0.7.1" \ # install peft & trl from github !pip install git+https://github.com/huggingface/trl@a3c5b7178ac4f65569975efadc97db2f3749c65e --upgrade !pip install git+https://github.com/huggingface/peft@4a1559582281fc3c9283892caea8ccef1d6f5a4f --upgrade |
如果您使用的是具有 Ampere 架构的 GPU(例如 NVIDIA A10G 或 RTX 4090/3090)或更新版本,则可以使用 Flash 注意。Flash Attention 是一种对注意力计算进行重新排序的方法,并利用经典技术(平铺、重新计算)来显着加快速度,并将序列长度从二次减少到线性的内存使用量。The TL;博士;将训练速度提高 3 倍。如需了解更多信息,请访问FlashAttention。
注意:如果您的计算机的 RAM 少于 96GB 且 CPU 内核很多,请减少MAX_JOBS
的数量。在 g5.2xlarge
上,我们使用了 4
。
1 2 3 4 |
import torch; assert torch.cuda.get_device_capability()[0] >= 8, 'Hardware not supported for Flash Attention' # install flash-attn !pip install ninja packaging !MAX_JOBS=4 pip install flash-attn --no-build-isolation |
安装 flash-attn 可能需要相当长的时间(10-45 分钟)。
我们将使用 Hugging Face Hub 作为远程模型版本控制服务。这意味着我们将在训练期间自动将模型、日志和信息推送到中心。您必须为此在 Hugging Face 上注册。在您拥有帐户后,我们将使用huggingface_hub
软件包中登录我们的帐户并将我们的令牌(访问密钥)存储在磁盘上。
1 2 3 4 5 6 |
from huggingface_hub import login login( token="", # ADD YOUR TOKEN HERE add_to_git_credential=True ) |
3. 创建并准备数据集
一旦确定微调是正确的解决方案,我们就需要创建一个数据集来微调我们的模型。数据集应该是要解决的任务的一组多样化的演示。有几种方法可以创建此类数据集,包括:
每种方法都有自己的优点和缺点,取决于预算、时间和质量要求。例如,使用现有数据集是最简单的,但可能无法根据您的特定用例进行定制,而使用人工可能是最准确的,但可能既耗时又昂贵。也可以结合几种方法来创建指令数据集,如 Orca: Progressive Learning from Complex Explanation Traces of GPT-4 中所示。
在我们的示例中,我们将使用一个名为 sql-create-context 的现有数据集,其中包含自然语言指令、架构定义和相应 SQL 查询的示例。
在trl
最新版本中,我们现在支持流行的指令和对话数据集格式。这意味着我们只需要将数据集转换为支持的格式之一,其余的trl
将负责。这些格式包括:
- 对话格式(conversational format)
1 2 3 |
{"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]} {"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]} {"messages": [{"role": "system", "content": "You are..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]} |
- 指令格式(instruction format)
1 2 3 |
{"prompt": "<prompt text>", "completion": "<ideal generated text>"} {"prompt": "<prompt text>", "completion": "<ideal generated text>"} {"prompt": "<prompt text>", "completion": "<ideal generated text>"} |
在我们的示例中,我们将使用 🤗 Datasets 库加载开源数据集,然后将其转换为对话格式,其中我们将架构定义包含在助手的系统消息中。然后,我们将数据集保存为 jsonl 文件,然后我们可以使用它来微调我们的模型。我们将数据集随机抽样到仅 10,000 个样本。
注意:此步骤可能因您的用例而异。例如,如果您已经有一个数据集,例如使用 OpenAI,您可以跳过此步骤并直接进入微调步骤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
from datasets import load_dataset # Convert dataset to OAI messages system_message = """You are an text to SQL query translator. Users will ask you questions in English and you will generate a SQL query based on the provided SCHEMA. SCHEMA: {schema}""" def create_conversation(sample): return { "messages": [ {"role": "system", "content": system_message.format(schema=sample["context"])}, {"role": "user", "content": sample["question"]}, {"role": "assistant", "content": sample["answer"]} ] } # Load dataset from the hub dataset = load_dataset("b-mc2/sql-create-context", split="train") dataset = dataset.shuffle().select(range(12500)) # Convert dataset to OAI messages dataset = dataset.map(create_conversation, remove_columns=dataset.features,batched=False) # split dataset into 10,000 training samples and 2,500 test samples dataset = dataset.train_test_split(test_size=2500/12500) print(dataset["train"][345]["messages"]) # save datasets to disk dataset["train"].to_json("train_dataset.json", orient="records") dataset["test"].to_json("test_dataset.json", orient="records") |
4. 使用trl
和SFTTrainer
微调LLM
我们现在准备微调我们的模型。我们将使用来自trl
的SFTTrainer 来微调我们的模型。这使得SFTTrainer
监督微调开放 LLM 变得直接。该SFTTrainer
是来自transformers
的Trainer
的子类库,支持所有相同的功能,包括日志记录、评估和检查点,但添加了额外的生活质量功能,包括:
- 数据集格式,包括对话和指令格式
- 仅针对完成情况进行训练,忽略提示
- 打包数据集以实现更高效的训练
- PEFT(参数高效微调)支持,包括 Q-LoRA
- 准备用于对话微调的模型和分词器(例如,添加特殊令牌)
在我们的示例中,我们将使用数据集格式、打包和 PEFT 特征。作为 peft 方法,我们将使用 QLoRA 技术来减少微调期间大型语言模型的内存占用,而不会因使用量化而牺牲性能。如果您想了解有关 QLoRA 及其工作原理的更多信息,请查看使用位沙字节、4 位量化和 QLoRA 博客文章使 LLM 更易于访问。
现在,让我们开始吧!🚀 让我们从磁盘加载 json 数据集。
1 2 3 4 |
from datasets import load_dataset # Load jsonl data from disk dataset = load_dataset("json", data_files="train_dataset.json", split="train") |
接下来,我们将加载我们的 LLM。对于我们的用例,我们将使用 CodeLlama 7B。CodeLlama 是一个经过训练的 Llama 模型,用于一般代码合成和理解。 但是我们可以通过更改变量model_id
轻松地将模型换成另一个模型,例如 Mistral 或 Mixtral 模型、TII Falcon 或任何其他 LLM。我们将使用 bitsandbytes 将我们的模型量化为 4 位。
注意:请注意,模型越大,所需的内存就越多。在我们的示例中,我们将使用 7B 版本,它可以在 24GB GPU 上进行调整。如果您的 GPU 较小。
正确地准备用于训练聊天/对话模型的模型和分词器至关重要。我们需要向分词器和模型添加新的特殊令牌,以教他们在对话中的不同角色。在 trl
我们有一个方便的setup_chat_format方法:
- 向分词器添加特殊令牌,例如
<|im_start|>
和<|im_end|>
,指示对话的开始和结束。 - 调整模型嵌入层的大小以容纳新标记。
- 设置
chat_template
分词器 ,用于将输入数据格式化为类似聊天的格式。默认值chatml
来自 OpenAI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import torch from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig from trl import setup_chat_format # Hugging Face model id model_id = "codellama/CodeLlama-7b-hf" # or `mistralai/Mistral-7B-v0.1` # BitsAndBytesConfig int-4 config bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16 ) # Load model and tokenizer model = AutoModelForCausalLM.from_pretrained( model_id, device_map="auto", attn_implementation="flash_attention_2", torch_dtype=torch.bfloat16, quantization_config=bnb_config ) tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.padding_side = 'right' # to prevent warnings # # set chat template to OAI chatML, remove if you start from a fine-tuned model model, tokenizer = setup_chat_format(model, tokenizer) |
SFTTrainer
支持与peft
的本机集成,这使得使用 QLoRA 等 LLM 的高效调整变得非常容易。我们LoraConfig
只需要创建我们的并将其提供给培训师。我们LoraConfig
的参数是根据 qlora 论文和 sebastian 的博客文章定义的。
1 2 3 4 5 6 7 8 9 10 11 |
from peft import LoraConfig # LoRA config based on QLoRA paper & Sebastian Raschka experiment peft_config = LoraConfig( lora_alpha=128, lora_dropout=0.05, r=256, bias="none", target_modules="all-linear", task_type="CAUSAL_LM", ) |
在开始训练之前,我们需要定义要使用的超参数(TrainingArguments
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from transformers import TrainingArguments args = TrainingArguments( output_dir="code-llama-7b-text-to-sql", # directory to save and repository id num_train_epochs=3, # number of training epochs per_device_train_batch_size=3, # batch size per device during training gradient_accumulation_steps=2, # number of steps before performing a backward/update pass gradient_checkpointing=True, # use gradient checkpointing to save memory optim="adamw_torch_fused", # use fused adamw optimizer logging_steps=10, # log every 10 steps save_strategy="epoch", # save checkpoint every epoch learning_rate=2e-4, # learning rate, based on QLoRA paper bf16=True, # use bfloat16 precision tf32=True, # use tf32 precision max_grad_norm=0.3, # max gradient norm based on QLoRA paper warmup_ratio=0.03, # warmup ratio based on QLoRA paper lr_scheduler_type="constant", # use constant learning rate scheduler push_to_hub=True, # push model to hub report_to="tensorboard", # report metrics to tensorboard ) |
现在,我们拥有了创建 SFTTrainer
模型并开始训练模型的所有构建块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from trl import SFTTrainer max_seq_length = 3072 # max sequence length for model and packing of the dataset trainer = SFTTrainer( model=model, args=args, train_dataset=dataset, peft_config=peft_config, max_seq_length=max_seq_length, tokenizer=tokenizer, packing=True, dataset_kwargs={ "add_special_tokens": False, # We template with special tokens "append_concat_token": False, # No need to add additional separator token } ) |
我们可以通过在 Trainer
实例上调用train()
方法来开始训练模型。这将启动训练循环并训练我们的模型 3 个周期。由于我们使用的是PEFT方法,因此我们只会保存自适应的模型权重,而不是完整的模型。
1 2 3 4 5 |
# start training, the model will be automatically saved to the hub and the output directory trainer.train() # save model trainer.save_model() |
使用 Flash Attention 对 3 个 epoch 的训练,数据集为 10k 个样本,在 g5.2xlarge
实例成本1.212$/h
,使我们的总成本仅为1.8$
。
1 2 3 4 |
# free the memory again del model del trainer torch.cuda.empty_cache() |
可选:将 LoRA 适配器合并到原始模型中
使用 QLoRA 时,我们只训练适配器,而不是完整模型。这意味着在训练期间保存模型时,我们只保存适配器权重,而不是完整模型。如果要保存完整模型,以便更轻松地与文本生成推理一起使用,则可以使用 merge_and_unload
方法将适配器权重合并到模型权重中,然后使用 save_pretrained
方法保存模型。这将保存一个默认模型,该模型可用于推理。
注意:您可能需要> 30GB CPU 内存。
1 2 3 4 5 6 7 8 9 10 11 12 |
#### COMMENT IN TO MERGE PEFT AND BASE MODEL #### # from peft import AutoPeftModelForCausalLM # # Load PEFT model on CPU # model = AutoPeftModelForCausalLM.from_pretrained( # args.output_dir, # torch_dtype=torch.float16, # low_cpu_mem_usage=True, # ) # # Merge LoRA and base model and save # merged_model = model.merge_and_unload() # merged_model.save_pretrained(args.output_dir,safe_serialization=True, max_shard_size="2GB") |
5. 测试和评估 LLM
训练完成后,我们要评估和测试我们的模型。我们将从原始数据集中加载不同的样本,并使用简单的循环和准确性作为我们的指标,在这些样本上评估模型。
注意:评估生成式 AI 模型并非易事,因为 1 个输入可以有多个正确的输出。如果您想了解有关评估生成模型的更多信息,请查看使用 Langchain 和 Hugging Face 的实例评估 LLM 和 RAG 博客文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import torch from peft import AutoPeftModelForCausalLM from transformers import AutoTokenizer, pipeline peft_model_id = "./code-llama-7b-text-to-sql" # peft_model_id = args.output_dir # Load Model with PEFT adapter model = AutoPeftModelForCausalLM.from_pretrained( peft_model_id, device_map="auto", torch_dtype=torch.float16 ) tokenizer = AutoTokenizer.from_pretrained(peft_model_id) # load into pipeline pipe = pipeline("text-generation", model=model, tokenizer=tokenizer) |
让我们加载我们的测试数据集,尝试生成一条指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from datasets import load_dataset from random import randint # Load our test dataset eval_dataset = load_dataset("json", data_files="test_dataset.json", split="train") rand_idx = randint(0, len(eval_dataset)) # Test on sample prompt = pipe.tokenizer.apply_chat_template(eval_dataset[rand_idx]["messages"][:2], tokenize=False, add_generation_prompt=True) outputs = pipe(prompt, max_new_tokens=256, do_sample=False, temperature=0.1, top_k=50, top_p=0.1, eos_token_id=pipe.tokenizer.eos_token_id, pad_token_id=pipe.tokenizer.pad_token_id) print(f"Query:\n{eval_dataset[rand_idx]['messages'][1]['content']}") print(f"Original Answer:\n{eval_dataset[rand_idx]['messages'][2]['content']}") print(f"Generated Answer:\n{outputs[0]['generated_text'][len(prompt):].strip()}") |
好!我们的模型能够根据自然语言指令生成 SQL 查询。让我们在测试数据集的全部 2,500 个样本上评估我们的模型。注意:如上所述,评估生成模型并非易事。在我们的示例中,我们使用基于真实 SQL 查询生成的 SQL 的准确性作为我们的指标。另一种方法是自动执行生成的 SQL 查询,并将结果与实际情况进行比较。这将是一个更准确的指标,但需要更多的工作来设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from tqdm import tqdm def evaluate(sample): prompt = pipe.tokenizer.apply_chat_template(sample["messages"][:2], tokenize=False, add_generation_prompt=True) outputs = pipe(prompt, max_new_tokens=256, do_sample=True, temperature=0.7, top_k=50, top_p=0.95, eos_token_id=pipe.tokenizer.eos_token_id, pad_token_id=pipe.tokenizer.pad_token_id) predicted_answer = outputs[0]['generated_text'][len(prompt):].strip() if predicted_answer == sample["messages"][2]["content"]: return 1 else: return 0 success_rate = [] number_of_eval_samples = 1000 # iterate over eval dataset and predict for s in tqdm(eval_dataset.shuffle().select(range(number_of_eval_samples))): success_rate.append(evaluate(s)) # compute accuracy accuracy = sum(success_rate)/len(success_rate) print(f"Accuracy: {accuracy*100:.2f}%") |
我们在评估数据集中的 1000 个样本上评估了我们的模型,并得到了79.50%
的准确度,这需要 ~25 分钟。
这很好,但如前所述,您需要对这个指标持保留态度。如果我们可以通过对真实数据库运行查询来评估我们的模型并比较结果,那就更好了。因为同一指令可能有不同的“正确”SQL 查询。还有几种方法可以提高性能,方法是使用少样本学习,使用 RAG、自我修复来生成 SQL 查询。
6. 为生产部署 LLM
现在可以将模型部署到生产环境。为了将开放 LLM 部署到生产环境中,我们建议使用文本生成推理 (TGI)。TGI 是用于部署和服务大型语言模型 (LLM) 的专用解决方案。TGI 支持使用张量并行和连续批处理为最流行的开放 LLM(包括 Llama、Mistral、Mixtral、StarCoder、T5 等)生成高性能文本。文本生成推理被 IBM、Grammarly、Uber、Deutsche Telekom 等公司使用。有几种方法可以部署模型,包括:
如果安装了 docker,则可以使用以下命令启动推理服务器。
注意:请确保有足够的 GPU 内存来运行容器。重新启动内核以从笔记本电脑中删除所有已分配的 GPU 内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
%%bash # model=$PWD/{args.output_dir} # path to model model=$(pwd)/code-llama-7b-text-to-sql # path to model num_shard=1 # number of shards max_input_length=1024 # max input length max_total_tokens=2048 # max total tokens docker run -d --name tgi --gpus all -ti -p 8080:80 \ -e MODEL_ID=/workspace \ -e NUM_SHARD=$num_shard \ -e MAX_INPUT_LENGTH=$max_input_length \ -e MAX_TOTAL_TOKENS=$max_total_tokens \ -v $model:/workspace \ ghcr.io/huggingface/text-generation-inference:latest |
容器运行后,可以发送请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import requests as r from transformers import AutoTokenizer from datasets import load_dataset from random import randint # Load our test dataset and Tokenizer again tokenizer = AutoTokenizer.from_pretrained("code-llama-7b-text-to-sql") eval_dataset = load_dataset("json", data_files="test_dataset.json", split="train") rand_idx = randint(0, len(eval_dataset)) # generate the same prompt as for the first local test prompt = tokenizer.apply_chat_template(eval_dataset[rand_idx]["messages"][:2], tokenize=False, add_generation_prompt=True) request= {"inputs":prompt,"parameters":{"temperature":0.2, "top_p": 0.95, "max_new_tokens": 256}} # send request to inference server resp = r.post("http://127.0.0.1:8080/generate", json=request) output = resp.json()["generated_text"].strip() time_per_token = resp.headers.get("x-time-per-token") time_prompt_tokens = resp.headers.get("x-prompt-tokens") # Print results print(f"Query:\n{eval_dataset[rand_idx]['messages'][1]['content']}") print(f"Original Answer:\n{eval_dataset[rand_idx]['messages'][2]['content']}") print(f"Generated Answer:\n{output}") print(f"Latency per token: {time_per_token}ms") print(f"Latency prompt encoding: {time_prompt_tokens}ms") |
太棒了,完成后不要忘记停止容器。
1 |
!docker stop tgi |
结论
大型语言模型和工具TRL的可用性使其成为公司投资开放式LLM技术的理想时机。针对特定任务微调开放 LLM 可以显著提高效率,并为创新和改进服务开辟新的机会。随着可访问性和成本效益的提高,现在是开始使用开放式 LLM 的最佳时机。
感谢您的阅读!如果您有任何问题,请随时在Twitter或LinkedIn上与我联系。
参考连接:https://www.philschmid.de/fine-tune-llms-in-2024-with-trl?WT.mc_id=academic-105485-koreyst