天天看點

如何使用Hugging Face微調大語言模型(LLMs)

作者:閃耀之星AK

大語言模型(LLMs)在過去一年取得了顯著進步。從ChatGPT爆發以來,後來逐漸發展了衆多開源大模型LLMs,如Meta AI的Llama 2、Mistrals Mistral & Mixtral模型,TII Falcon等。這些LLMs能夠勝任多種任務,包括聊天機器人、問答和自動摘要,而且無需進行額外的訓練。但是,如果你想為你的應用定制模型,可能需要在你的資料集上對模型進行微調,以獲得比直接使用或訓練更小型模型更高品質的結果。

本文将介紹如何使用Hugging Face的TRL、Transformers架構和資料集來微調開放的大語言模型。我們将按照以下步驟進行:

注意:本文是為了在消費者級别的GPU(24GB)上運作而編寫的,例如NVIDIA A10G或RTX 4090/3090,但也可以輕松地适應在更大的GPU上運作。

一、定義我們的使用場景

  1. 微調LLM時,了解你的使用場景和要解決的問題至關重要。這将幫助你選擇合适的模型,或者幫助你建立一個資料集來微調你的模型。如果你還沒有定義你的使用場景,你可能需要重新思考。并非所有的使用場景都需要微調,建議在微調你自己的模型之前,先評估和嘗試已經微調過的模型或基于API的模型。
  1. 例如,我們将使用以下使用場景:
我們想要微調一個模型,它可以基于自然語言指令生成SQL查詢,然後可以內建到我們的BI工具中。目标是減少建立SQL查詢所需的時間,并使非技術使用者更容易建立SQL查詢。

将自然語言轉換為SQL查詢是一個很好的微調LLM的使用場景,因為它是一個複雜的任務,需要對資料和SQL語言有深入的了解。

二、設定開發環境

首先,我們需要安裝Hugging Face的庫,包括trl、transformers和datasets,以及Pytorch。trl是一個新庫,建立在transformers和datasets之上,它簡化了微調大語言模型的過程,包括rlhf(強化學習從人類回報中學習)和對齊開放LLM。如果你對trl還不太熟悉,不用擔心,它是一個新工具,旨在讓微調過程更加便捷。

# 安裝Pytorch和其他庫
!pip install "torch==2.1.2" tensorboard

# 安裝Hugging Face庫
!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" \

# 從github安裝peft & trl
!pip install git+https://github.com/huggingface/trl@a3c5b7178ac4f65569975efadc97db2f3749c65e --upgrade
!pip install git+https://github.com/huggingface/peft@4a1559582281fc3c9283892caea8ccef1d6f5a4f--upgrade           

如果你的GPU采用的是Ampere架構(如NVIDIA A10G或RTX 4090/3090)或更新版本,你可以利用Flash Attention技術。Flash Attention通過優化注意力機制的計算過程,并采用一些經典技術(如分塊和重新計算)來顯著提高計算速度,并降低記憶體消耗。簡而言之,這項技術可以将訓練速度提升至原來的三倍。想要了解更多詳情,可以通路FlashAttention的官方頁面。

注意:如果你的計算機記憶體不足96GB且擁有大量CPU核心,你可能需要調整MAX_JOBS的數值。在我們的測試中,使用的是g5.2xlarge執行個體,設定了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 Attention可能需要一段時間(大約10到45分鐘)。

我們将利用Hugging Face Hub作為一個遠端模型版本控制服務。這意味着在訓練過程中,我們的模型、日志和相關資訊将自動上傳到Hugging Face Hub。為了使用這項服務,你需要在Hugging Face上注冊一個賬戶。注冊完成後,我們會使用huggingface_hub包中的登入工具來登入你的賬戶,并在本地磁盤上儲存你的通路令牌。

from huggingface_hub import login

login(
  token="", # 在此處添加您的token
  add_to_git_credential=True
)           

三、建立和準備資料集

  1. 一旦您确定微調是正确的解決方案,我們需要準備一個資料集來訓練我們的模型。這個資料集應該是多樣化的任務示範,展示了你想要解決的問題。建立資料集的方法有很多,比如:
  • 利用現有的開源資料集,如Spider
  • 利用大語言模型生成合成資料集,如Alpaca
  • 雇傭人類來建立資料集,如Dolly
  • 結合以上方法,如Orca

每種方法都有其自身的優勢和劣勢,并取決于預算、時間和品質要求。例如,使用現有資料集是最簡單的,但可能不針對你的特定使用場景,而人工建立資料集雖然準确度高,但成本和時間消耗也大。也可以将幾種方法結合起來建立指令資料集,如所示。

在我們的示例中,我們将使用一個名為sql-create-context的現有資料集,它包含了自然語言指令、資料庫模式定義以及相應的SQL查詢樣本。

随着trl的最新版本釋出,我們現在支援流行的指令和對話資料集格式。這意味着我們隻需将資料集轉換為支援的格式之一,trl就會自動處理後續步驟。支援的格式包括:

  • 對話格式
{"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": "..."}]}           
  • 指令格式
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}{"prompt": "<prompt text>", "completion": "<ideal generated text>"}{"prompt": "<prompt text>", "completion": "<ideal generated text>"}           

我們将使用Hugging Face的Datasets庫來加載我們的開源資料集,并将其轉換為對話格式。在這種格式中,我們将在系統消息中包含資料庫模式定義,作為我們助手的資訊。然後,我們将資料集儲存為jsonl檔案,這樣就可以用于微調我們的模型。我們對資料集進行了随機下采樣,隻保留了10,000個樣本。

注意:如果你已經有了一個資料集,比如通過與OpenAI合作獲得的,你可以跳過這一步,直接進行微調。

from datasets import load_dataset

# 将資料集轉換為OAI消息
system_message = """您是SQL查詢翻譯的文本。使用者将用英語向您提問,您将根據提供的SCHEMA生成SQL查詢。
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"]}
    ]
  }

# 從hub加載資料集
dataset = load_dataset("b-mc2/sql-create-context", split="train")
dataset = dataset.shuffle().select(range(12500))

# 将資料集轉換為OAI消息
dataset = dataset.map(create_conversation, remove_columns=dataset.features,batched=False)
# 将資料集拆分為10000個訓練樣本和2500個測試樣本
dataset = dataset.train_test_split(test_size=2500/12500)

print(dataset["train"][345]["messages"])

# 将資料集儲存到磁盤
dataset["train"].to_json("train_dataset.json", orient="records")
dataset["test"].to_json("test_dataset.json", orient="records")           

四、使用trl和SFTTrainer微調大語言模型

現在,我們準備開始微調我們的模型。我們将使用trl中的SFTTrainer來微調我們的模型。它簡化了對開放的大語言模型進行監督式微調的過程。SFTTrainer是transformers庫中Trainer的一個衍生類,它繼承了所有核心功能,如日志記錄、評估和模型檢查點,并增加了一些實用功能,例如:

  • 資料集格式轉換,支援對話和指令格式
  • 僅在資料集完成時進行訓練,忽略掉提示資訊
  • 資料集打包,以提高訓練效率
  • 參數高效微調(PEFT)支援,包括Q-LoRA技術
  • 為對話微調準備模型和标記器(例如添加特殊标記)

在我們的示例中,我們将利用資料集格式轉換、資料集打包和參數高效微調(PEFT)功能。我們将采用QLoRA技術,這是一種通過量化來減少大型語言模型在微調過程中的記憶體占用,同時保持模型性能的方法。

首先,我們将從磁盤加載我們的json格式資料集。

from datasets import load_dataset

# 從磁盤加載jsonl資料
dataset = load_dataset("json", data_files="train_dataset.json", split="train")           

接下來,我們将加載我們的大語言模型。在我們的應用場景中,我們選擇了CodeLlama 7B,這是一個專門為代碼合成和了解訓練的大語言模型。如果你有其他偏好,比如Mistral、Mixtral模型,或者TII Falcon,隻需調整我們的model_id即可輕松切換。我們将使用bitsandbytes工具将模型量化為4位,以減少記憶體需求。

請注意,模型的規模越大,它所需的記憶體就越多。在我們的示例中,我們使用的是7B版本的模型,它可以在24GB記憶體的GPU上進行微調。如果你的GPU記憶體較小,可能需要考慮使用更小的模型。

正确地為訓練聊天/對話模型準備模型和标記器是至關重要的。我們需要向标記器和模型添加新的特殊标記,以教他們對話中的不同角色。在trl中,我們有一個友善的方法,使用setup_chat_format,它向标記器添加特殊标記,例如<|im_start|>和<|im_end|>,以表示對話的開始和結束。調整模型嵌入層的大小以适應新的标記。設定标記器的chat_template,它用于将輸入資料格式化為類似于聊天的格式。預設是來自OpenAI的chatml。

正确配置模型和分詞器以訓練聊天或對話模型非常重要。我們需要向分詞器和模型中添加特殊的标記,比如開始對話的<|im_start|>和結束對話的<|im_end|>,來教會它們在對話中扮演的角色。在trl庫中,我們有一個名為setup_chat_format的便捷方法,它:

  • 向分詞器添加特殊的對話标記,以訓示對話的開始和結束。
  • 調整模型的嵌入層大小,以适應新的标記。
  • 設定分詞器的chat_template,這用于将輸入資料格式化為類似聊天的格式。預設使用的是OpenAI提供的chatml格式。
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' # 以防止警告

# 将聊天模闆設定為OAI chatML,如果您從微調模型開始,請删除
model, tokenizer = setup_chat_format(model, tokenizer)           

SFTTrainer與peft的內建使得使用QLoRA高效調優LLM變得非常簡單。我們隻需要建立我們的LoraConfig并将其提供給訓練器。我們的LoraConfig參數是根據QLoRA論文定義的。

from peft import LoraConfig

# 基于QLoRA論文和Sebastian Raschka實驗的LoRA配置
peft_config = LoraConfig(
        lora_alpha=128,
        lora_dropout=0.05,
        r=256,
        bias="none",
        target_modules="all-linear",
        task_type="CAUSAL_LM",
)           

在開始訓練之前,我們需要定義我們想要使用的超參數(TrainingArguments)。

from transformers import TrainingArguments

args = TrainingArguments(
    output_dir="code-llama-7b-text-to-sql", # 要儲存的目錄和存儲庫ID
    num_train_epochs=3,                     # 訓練周期數
    per_device_train_batch_size=3,          # 訓練期間每個裝置的批量大小
    gradient_accumulation_steps=2,          # 反向/更新前的步驟數
    gradient_checkpointing=True,            # 使用漸變檢查點來節省記憶體
    optim="adamw_torch_fused",              # 使用融合的adamw優化器
    logging_steps=10,                       # 每10步記錄一次
    save_strategy="epoch",                  # 每個epoch儲存檢查點
    learning_rate=2e-4,                     # 學習率,基于QLoRA論文
    bf16=True,                              # 使用bfloat16精度
    tf32=True,                              # 使用tf32精度
    max_grad_norm=0.3,                      # 基于QLoRA論文的最大梯度範數
    warmup_ratio=0.03,                      # 根據QLoRA論文的預熱比例
    lr_scheduler_type="constant",           # 使用恒定學習率排程器
    push_to_hub=True,                       # 将模型推送到Hub
    report_to="tensorboard",                # 将名額報告到Tensorboard
)           

現在,我們已經具備了建立 SFTTrainer 并啟動模型訓練的所有要素。

from trl import SFTTrainer

max_seq_length = 3072 # 資料集模型和打包的最大序列長度

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,  # 我們使用特殊 tokens
        "append_concat_token": False, # 不需要添加額外的分隔符 token
    }
)           

我們可以通過調用 Trainer 執行個體的 train() 方法來啟動模型訓練。這将啟動一個訓練循環,持續 3 個周期。由于我們采用的是參數高效微調方法,我們隻會儲存經過調整的模型權重,而不是整個模型。

# 開始訓練,模型會自動儲存到hub和輸出目錄
trainer.train()

# 儲存模型
trainer.save_model()           

使用Flash Attention進行3個周期的訓練,在一個包含10k個樣本的資料集上,在一台g5.2xlarge上花費了01:29:58的時間。執行個體成本為1,212$/h,這使得總成本僅為1.8$。

# 再次釋放記憶體
del model
del trainer
torch.cuda.empty_cache()           

可選步驟:将 LoRA 擴充卡合并到原始模型中

在使用 QLoRA 時,我們隻訓練擴充卡,而不是整個模型。這意味着在訓練過程中儲存模型時,我們隻儲存擴充卡的權重。如果你希望儲存整個模型,以便更容易地與文本生成推理一起使用,你可以使用 merge_and_unload 方法将擴充卡權重合并到模型權重中,然後使用 save_pretrained 方法儲存模型。這将儲存一個預設模型,可用于推理。

注意:這個過程可能需要超過 30GB 的 CPU 記憶體。

#### COMMENT IN TO MERGE PEFT AND BASE MODEL ####
# from peft import PeftModel, PeftConfig
# from transformers import AutoModelForCausalLM, AutoTokenizer
# from peft import AutoPeftModelForCausalLM

# # Load PEFT model on CPU
# config = PeftConfig.from_pretrained(args.output_dir)
# model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path,low_cpu_mem_usage=True)
# tokenizer = AutoTokenizer.from_pretrained(args.output_dir)
# model.resize_token_embeddings(len(tokenizer))
# model = PeftModel.from_pretrained(model, args.output_dir)
# 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")           

五、測試和評估大語言模型

  1. 訓練完成後,我們需要對模型進行評估和測試。我們将從原始資料集中選取不同的樣本,并通過一個簡單的循環和準确率作為衡量标準來評估模型的表現。

注意:評估生成式 AI(Generative AI)模型并不容易,因為一個輸入可能有多種正确的輸出。

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
)
# load into pipeline
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)           

讓我們加載測試資料集,嘗試生成一個指令。

from datasets import load_dataset
from random import randint

# 加載我們的測試資料集
eval_dataset = load_dataset("json", data_files="test_dataset.json", split="train")
rand_idx = randint(0, len(eval_dataset))

# 樣品測試
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 查詢,并将結果與真實資料進行對比,但這需要更多的準備工作。

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
# 疊代eval資料集并預測
for s in tqdm(eval_dataset.shuffle().select(range(number_of_eval_samples))):
    success_rate.append(evaluate(s))

# 計算精度
accuracy = sum(success_rate)/len(success_rate)

print(f"Accuracy: {accuracy*100:.2f}%")           

我們在評估資料集的 1000 個樣本上進行了測試,準确率達到了 79.50%,整個過程大約花費了 25 分鐘。

這個結果相當不錯,但我們需要謹慎對待這個名額。如果能在真實資料庫上運作這些查詢并比較結果,那将是一個更可靠的評估方法。由于相同的指令可能對應多種正确的 SQL 查詢,我們還可以通過少樣本學習、RAG(Retrieval-Augmented Generation)和自我修複等技術來進一步提升模型的性能。

六、将大語言模型部署到生産環境

現在,你可以将你的大語言模型部署到生産環境中。為了在生産環境中部署開放的大語言模型,我們推薦使用文本生成推理(Text Generation Inference,TGI)。TGI 是一個專門為部署和提供大型語言模型(LLMs)而設計的高性能解決方案。它通過張量并行技術和連續批處理,支援包括 Llama、Mistral、Mixtral、StarCoder、T5 等在内的多種流行開放大語言模型。IBM、Grammarly、Uber、Deutsche Telekom 等公司都在使用文本生成推理。你可以通過多種方式部署你的模型,例如:

  • 使用 Hugging Face 提供的推理端點
  • 自主搭建(DIY)

如果你已經安裝了 Docker,你可以使用以下指令來啟動推理伺服器。

注意:確定你的 GPU 記憶體足夠運作容器。在筆記本中,你可能需要重新開機核心來釋放所有已配置設定的 GPU 記憶體。

%%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           

一旦你的容器啟動,你就可以開始發送推理請求了。

import requests as r
from transformers import AutoTokenizer
from datasets import load_dataset
from random import randint

# 再次加載我們的測試資料集和Tokenizer
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))

# 生成與第一次本地測試相同的提示
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}}

# 向推理伺服器發送請求
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(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")           

完成工作後,别忘了停止你的容器。

!docker stop tgi           

七、總結

随着大型語言模型的發展和 TRL 等工具的普及,現在是企業投資開放大語言模型技術的絕佳時機。針對特定任務微調開放的大語言模型可以顯著提升效率,并為創新和服務品質提升帶來新的可能性。随着技術的日益普及和成本效益的提高,現在是開始利用開放大語言模型的最佳時刻。

八、References

[1]. Llama 2 (https://huggingface.co/meta-llama/Llama-2-70b-chat-hf)

[2]. Mistral(https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2)

[3]. Mixtral (https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1)

[4]. Falcon(https://huggingface.co/tiiuae/falcon-40b)

[5]. TRL(https://huggingface.co/docs/trl/index)

[6]. Transformers(https://huggingface.co/docs/transformers/index)

[7]. datasets(https://huggingface.co/docs/datasets/index)

[8]. FlashAttention (https://github.com/Dao-AILab/flash-attention/tree/main)

[9]. Hugging Face Hub (https://huggingface.co/models)

[10]. Orca: Progressive Learning from Complex Explanation Traces of GPT-4. (https://arxiv.org/abs/2306.02707)

[11]. sql-create-context (https://huggingface.co/datasets/b-mc2/sql-create-context)

[12]. Text Generation Inference (TGI) https://github.com/huggingface/text-generation-inference

繼續閱讀