為視覺語言多模態模型進行偏好最佳化
訓練模型使得它能夠理解並預測人類偏好是一項比較複雜的任務。諸如 SFT (Supervised finetuning) 的傳統的方法一般都需要耗費較大成本,因為這些演算法需要對資料打上特定的標籤。而偏好最佳化 (Preference Optimization) 作為一種替代選項,通常可以簡化這一過程,併產出更準確的結果。透過對候選回答的對比和排序,而不是賦予固定的標籤,偏好最佳化使得模型能更高效地捕捉人類偏好中的細微差別。
偏好最佳化已經在大語言模型中廣泛使用了,但現在,它也可以用在視覺語言模型 (VLM) 上。得益於 TRL 的開發,現在我們可以 使用 TRL 對 VLM 進行直接偏好最佳化 (Direct Preference Optimization)。本文將會介紹使用 TRL 和 DPO 對視覺語言模型進行訓練的全過程。
偏好資料集
進行偏好最佳化,首先我們需要有一個能體現使用者偏好的資料集。在雙項選擇的設定下,相應的資料一般包含一個提示詞 (Prompt) 和兩個候選回答,兩個回答中一個被記為選中 (chosen),另一個被記為淘汰 (rejected)。模型將要去學習著給出選中的回答,而不是被淘汰的那個。下圖就是一個例子:
圖片來自 openbmb/RLAIF-V-Dataset 資料集
❔ Question: How many families?
- ❌ Rejected: The image does not provide any information about families.
- ✅ Chosen: The image shows a Union Organization table setup with 18,000 families.
需要注意的是,儘管選中的回答也不是完全正確的 (回答 18000 個家庭還是不對,應該是 18000000),但它也好於那個被淘汰的回答。
本文將使用 openbmb/RLAIF-V-Dataset 作為示例資料集,它包含了超過 83000 條標註的資料。可以透過下面程式碼檢視一下資料集:
>>> from datasets import load_dataset
>>> dataset = load_dataset("openbmb/RLAIF-V-Dataset", split="train[:1%]")
>>> sample = dataset[1]
>>> sample["image"].show()
>>> sample["question"]
'how many families?'
>>> sample["rejected"]
'The image does not provide any information about families.'
>>> sample["chosen"]
'The image shows a Union Organization table setup with 18,000 families.'
我們將要訓練的 VLM 模型需要文字和影像同時作為輸入,所以這裡的第一步還是要對資料集格式進行改造。一條資料應該被結構化成能模擬人機對話的形式。使用者提供一個提示語,其中包含一張圖片和一個問題,然後模型需要能夠給出一個回答。我們用以下程式碼實現格式轉換:
from datasets import features
from transformers import AutoProcessor
processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b", do_image_splitting=False)
def format(example):
# Prepare the input for the chat template
prompt = [
{
"role": "user",
"content": [{"type": "image"}, {"type": "text", "text": example["question"]}],
},
]
chosen = [
{
"role": "assistant",
"content": [{"type": "text", "text": example["chosen"]}],
},
]
rejected = [
{
"role": "assistant",
"content": [{"type": "text", "text": example["rejected"]}],
},
]
# Apply the chat template
prompt = processor.apply_chat_template(prompt, tokenize=False)
chosen = processor.apply_chat_template(chosen, tokenize=False)
rejected = processor.apply_chat_template(rejected, tokenize=False)
# Resize the image to ensure it fits within the maximum allowable
# size of the processor to prevent OOM errors.
max_size = processor.image_processor.size["longest_edge"]
example["image"].thumbnail((max_size, max_size))
return {"images": [example["image"]], "prompt": prompt, "chosen": chosen, "rejected": rejected}
# Apply the formatting function to the dataset,
# remove columns to end up with only "images", "prompt", "chosen", "rejected" columns
dataset = dataset.map(format, remove_columns=dataset.column_names)
# Make sure that the images are decoded, it prevents from storing bytes.
# More info here https://github.com/huggingface/blog/pull/2148#discussion_r1667400478
f = dataset.features
f["images"] = features.Sequence(features.Image(decode=True)) # to avoid bytes
dataset = dataset.cast(f)
完成了格式轉換,我們來看看第一條資料:
>>> dataset[1]
{'images': [<PIL.JpegImagePlugin.JpegImageFile image mode=L size=980x812 at 0x154505570>],
'prompt': 'User:<image>how many families?<end_of_utterance>\n',
'rejected': 'Assistant: The image does not provide any information about families.<end_of_utterance>\n',
'chosen': 'Assistant: The image shows a Union Organization table setup with 18,000 families.<end_of_utterance>\n'}
OK!接下來準備好 GPU,訓練馬上開始。
訓練
我們將使用 Idefics2-8b 作為我們的示例模型,但 TRL 裡的 DPO 也是能用在像 Llava 1.5 和 PaliGemma 這樣的模型上的 (可參考這篇文章: Finetuning Llava 1.5, PaliGemma and others)。不過訓練之前,我們先檢查一下我們的 GPU 視訊記憶體是否夠用:
訓練需要多大的 GPU 視訊記憶體?
一個 80GB VRAM 的 GPU 足夠用來對 Idefics2-8b 進行 DPO 訓練嗎?我們可以先計算一下:
我們用 $ N $ 表示引數的數量,用 $ P $ 表示訓練使用的精度。訓練過程中,下列部分需要共同放入視訊記憶體中:
- 要訓練的模型: $ N \times P $
- 用以防止模型產生偏離的參考模型: 和要訓練的模型一樣大,所以也是 $ N \times P $
- 梯度: 我們對所有引數都進行訓練,所以每個引數都有梯度: $ N \times P $
- 最佳化器的狀態量: 我們使用 AdamW,一個引數會儲存兩個狀態量,所以需要: $ 2 \times N \times P $
Idefics2-8b 有 80 億 (8B) 引數,我們使用 float32
精度,每個引數佔 4 個位元組。所以總的視訊記憶體需求是:
引數來源 | 計算公式 | 視訊記憶體需求 |
---|---|---|
要訓練的模型 | $ 8 \times 10^9 \times 4 $ | 32 GB |
參考模型 | $ 8 \times 10^9 \times 4 $ | 32 GB |
梯度 | $ 8 \times 10^9 \times 4 $ | 32 GB |
最佳化器狀態量 | $ 2 \times 8 \times 10^9 \times 4 $ | 64 GB |
合計 | 160 GB |
這遠超我們前面說的 80GB 視訊記憶體了!幸運的是,我們可以使用量化、LoRA 等技術來大幅度地減少視訊記憶體需求,讓訓練可以進行。接下來我們將介紹這些技術。
量化
量化會降低模型權重和啟用值的精度,但也同時顯著減少記憶體需求。將精度從 float32
改為 bfloat16
,會讓每個引數需要的位元數從 4 位元減少到 2 位元。這一策略不僅能減少記憶體使用,還會顯著加速訓練,確保以最小代價保證足夠高的效能。具體做法如下:
import torch
from transformers import AutoModelForVision2Seq
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b", torch_dtype=torch.bfloat16)
透過如下 bf16=True
的設定, bfloat16
也可以被用在最佳化器上:
from transformers import TrainingArguments
training_args = TrainingArguments(..., bf16=True)
LoRA
LoRA 對引數矩陣進行低秩分解; 在訓練時,固定住原引數矩陣,僅訓練分解出的兩個矩陣。是一種大規模減少 LLM 訓練引數的方法。LoRA 已被整合在了 PEFT 庫裡,使用非常方便:
from transformers import AutoModelForVision2Seq
+ from peft import get_peft_model, LoraConfig
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b")
+ peft_config = LoraConfig(target_modules="all-linear")
+ model = get_peft_model(model, peft_config)
PEFT 像是給原模型進行了一次封裝 (程式碼中稱為 adapter )。訓練時,實際上是這個 adapter 在被訓練,而原有的模型保持不動。我們現在算算 LoRA 幫我們減少了多少要訓練的引數:
>>> model.print_trainable_parameters()
trainable params: 55,348,736 || all params: 8,458,116,848 || trainable%: 0.6543860411799315
它幫我們把要訓練的引數從八十億降到了五千五百萬!差距真大!這將顯著減少視訊記憶體需求。
使用 bfloat16 和 LoRA 後的視訊記憶體需求
現在我們來算算新的視訊記憶體需求:
引數來源 | 計算公式 | 視訊記憶體需求 |
---|---|---|
要訓練的模型 | $ 8 \mathrm{G} \times 2 $ | 16 GB |
參考模型 | $ 8 \mathrm{G} \times 2 $ | 16 GB |
梯度 | $ 55 \mathrm{M} \times 2 $ | 0.1 GB |
最佳化器狀態量 | $ 2 \times 55 \mathrm{M} \times 2 $ | 0.2 GB |
合計 | 32.3 GB |
現在我們僅需 32GB 的視訊記憶體就可以訓練我們的 Idefics2-8b 模型了。這合理多了,用 80GB 視訊記憶體的 GPU 就可以訓練了。
PEFT 文件 和 谷歌這篇關於 LoRA 和 QLoRA 文章 也提供了很多關於視訊記憶體最佳化的幫助指南,讀者感興趣可以閱讀。
訓練時 batch size 怎麼設定?
上述關於視訊記憶體佔用的計算還不算準確,因為實際訓練時,啟用值也需要佔用視訊記憶體。啟用值是神經網路各層的輸出。作為中間產物,它們的視訊記憶體佔用量取決於模型結構和訓練時的 batch size。準確計算這些視訊記憶體需求還是很困難的,我們一般依賴實驗觀察。
若想找到一個合適的 batch size ( per_device_train_batch_size
),你可以先隨便選取一個你認為合適的數值 (比如 64) 然後試著開始訓練。當然這大多數情況下會爆視訊記憶體 (OOM)。如果這樣,你可以減半 batch size,同時將 gradient_accumulation_steps
翻倍,以獲得和原先 batch size 設定相同的效果。反覆重複這一過程,最終當 OOM 不再出現時,你就可以訓練了。我們的實驗引數是: per_device_train_batch_size
設為 2, gradient_accumulation_steps
設為 32。
你還可以使用 gradient_checkpointing
來減少啟用值所需的記憶體。這一技術在計算梯度時,會重新計算一遍前向過程,而不是在前向過程中儲存用於計算梯度的中間結果。需要使用時,設定 gradient_checkpointing=True
即可。
完整訓練程式碼
一切就緒,我們可以開始訓練了。下面是我們的完整訓練程式碼。除了上面提到的部分外,我們還設定了 dataset_num_proc
和 dataloader_num_workers
等引數,用於加速資料預處理。
# dpo_idefics2-8b.py
from datasets import features, load_dataset
from transformers import AutoModelForVision2Seq, AutoProcessor
import torch
from trl import DPOConfig, DPOTrainer
from peft import LoraConfig
def main():
# Load the model and processor
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b", torch_dtype=torch.bfloat16)
processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b", do_image_splitting=False)
# Load the dataset
dataset = load_dataset("openbmb/RLAIF-V-Dataset", split="train")
def format(example):
# Prepare the input for the chat template
prompt = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": example["question"]}]}]
chosen = [{"role": "assistant", "content": [{"type": "text", "text": example["chosen"]}]}]
rejected = [{"role": "assistant", "content": [{"type": "text", "text": example["rejected"]}]}]
# Apply the chat template
prompt = processor.apply_chat_template(prompt, tokenize=False)
chosen = processor.apply_chat_template(chosen, tokenize=False)
rejected = processor.apply_chat_template(rejected, tokenize=False)
# Resize the image to ensure it fits within the maximum allowable
# size of the processor to prevent OOM errors.
max_size = processor.image_processor.size["longest_edge"]// 2
example["image"].thumbnail((max_size, max_size))
return {"images": [example["image"]], "prompt": prompt, "chosen": chosen, "rejected": rejected}
# Apply the formatting function to the dataset
dataset = dataset.map(format, remove_columns=dataset.column_names, num_proc=32)
# Make sure that the images are decoded, it prevents from storing bytes.
# More info here https://github.com/huggingface/blog/pull/2148#discussion_r1667400478
f = dataset.features
f["images"] = features.Sequence(features.Image(decode=True))
dataset = dataset.cast(f)
# Train the model
training_args = DPOConfig(
output_dir="idefics2-8b-dpo",
bf16=True,
gradient_checkpointing=True,
per_device_train_batch_size=2,
gradient_accumulation_steps=32,
num_train_epochs=1,
dataset_num_proc=32, # tokenization will use 32 processes
dataloader_num_workers=32, # data loading will use 32 workers
logging_steps=10,
)
trainer = DPOTrainer(
model,
ref_model=None, # not needed when using peft
args=training_args,
train_dataset=dataset,
tokenizer=processor,
peft_config=LoraConfig(target_modules="all-linear"),
)
trainer.train()
if __name__ == "__main__":
main()
啟動指令碼開始訓練,接下來就等待結果吧 🚀
accelerate launch dpo_idefics2-8b.py
結果
訓練需要幾小時的時間。當訓練完成後,我們可以看看訓練相關指標的變化曲線:
In DPO, we focus on several metrics to assess the quality of the training:
在 DPO 中,為了評估訓練,我們關注這幾個指標:
- 精度 (Accuracy): 在訓練樣本中,模型更願意輸出被選中的回答而不是被淘汰的回答,這個比率有多少。我們可以看到隨著訓練,精度在提升,這是個好的訊號。
- 獎勵 (Rewards): 這一指標與一個回答 (選中或淘汰) 被選中的機率呈正相關,讀者可以參考 DPO 論文 , 第 5 部分。我們希望被選中的回答對應的獎勵高於被淘汰的回答。我們可以透過兩者獎勵的差值 ( reward margin ) 來看: 圖中這一差值逐漸變大, 這也是個好的訊號。
評測
推理程式碼
訓練完成後,我們接下來就要在一些樣本上評測一下了。這會讓我們瞭解模型學習得怎麼樣、預測有效性如何。下面的程式碼可以用來在測試樣本上進行評測:
from transformers import AutoModelForVision2Seq, AutoProcessor
from PIL import Image
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b").to("cuda")
processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b", do_image_splitting=False)
model.load_adapter("HuggingFaceH4/idefics2-8b-dpo-rlaif-v-v0.3") # <-- Load the adapter we've just trained
# Process
user_message = ...
image_path = ...
data = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": user_message}]}]
prompts = processor.apply_chat_template(data, add_generation_prompt=True) # add_generation_prompt=True to end the prompt with "ASSISTANT:"
images = [Image.open(image_path)]
inputs = processor(prompts, images, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
# Generate
generated_ids = model.generate(**inputs, max_new_tokens=500)
response_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response_text)
前面提到的 openbmb/RLAIF-V-Dataset 這個資料集是用來減少大模型幻覺的。但真實訓練效果如何呢?我們可以使用 AMBER benchmark 這個評測基準,該資料集專門被用來評估 VLM 的幻覺情況。我們列出 Idefics2 和 Idefics2+DPO 的結果,並和其它模型對比。
Accuracy | F1 | |
---|---|---|
GPT-4o | 88.8 | 91.6 |
Idefics2+DPO | 85.9 | 89.4 |
Idefics2 | 85.8 | 89.1 |
GPT-4v | 83.4 | 87.4 |
MiniGemini | 82.6 | 87.6 |
LLaVA-NeXT | 81.4 | 85.4 |
QWEN-VL | 81.9 | 86.4 |
LURE | 73.5 | 77.7 |
OPERA | 75.2 | 78.3 |
Less-is-more | 72.4 | 75.8 |
VCD | 71.8 | 74.9 |
總的來看,有點作用!幻覺似乎減少了點。訓練看來還是成功的。
下面我們也列出一些視覺化結果出來:
Image | Question | Idefics2 | Idefics2+DPO |
---|---|---|---|
Are there two ships in this image? | Yes | No | |
Is the ground uneven in this image? | No | Yes | |
Is there one shovel in this image? | Yes | No |
你也可以自己找些例子來測試一下這個模型!
微調 Llava 1.5 和 PaliGemma 等模型
截至本文完稿時,TRL 的 DPO 實現已支援 Idefics2、Llava 1.5 和 PaliGemma,同時 TRL 也在努力支援更多的模型。最簡單的呼叫方法還是使用 TRL 提供的 示例指令碼。例如,如果你想微調 PaliGemma,你可以這樣:
accelerate launch examples/scripts/dpo_visual.py \
--dataset_name HuggingFaceH4/rlaif-v_formatted \
--model_name_or_path google/paligemma-3b-pt-224 \
--per_device_train_batch_size 2 \
--gradient_accumulation_steps 32 \
--dataset_num_proc 32 \
--output_dir dpo_paligemma_rlaif-v \
--bf16 \
--torch_dtype bfloat16 \
--gradient_checkpointing \
--use_peft \
--lora_target_modules=all-linear
更多關於 PaliGemma 微調的資訊可以在 smol-vision 這個專案裡看到。
🚀🚀 好了!你現在已經會使用 DPO 微調 VLM 模型了!我們期待你在社群分享你的模型、資料和獨特見解!
原文連結:
https://hf.co/blog/dpo_vlm 原文作者: Quentin Gallouédec, Shengyi Costa Huang, Merve Noyan, Kashif Rasul
譯者: hugging-hoi2022