為視覺語言多模態模型進行偏好最佳化

HuggingFace發表於2024-07-16

為視覺語言多模態模型進行偏好最佳化

訓練模型使得它能夠理解並預測人類偏好是一項比較複雜的任務。諸如 SFT (Supervised finetuning) 的傳統的方法一般都需要耗費較大成本,因為這些演算法需要對資料打上特定的標籤。而偏好最佳化 (Preference Optimization) 作為一種替代選項,通常可以簡化這一過程,併產出更準確的結果。透過對候選回答的對比和排序,而不是賦予固定的標籤,偏好最佳化使得模型能更高效地捕捉人類偏好中的細微差別。

偏好最佳化已經在大語言模型中廣泛使用了,但現在,它也可以用在視覺語言模型 (VLM) 上。得益於 TRL 的開發,現在我們可以 使用 TRL 對 VLM 進行直接偏好最佳化 (Direct Preference Optimization)。本文將會介紹使用 TRL 和 DPO 對視覺語言模型進行訓練的全過程。

偏好資料集

進行偏好最佳化,首先我們需要有一個能體現使用者偏好的資料集。在雙項選擇的設定下,相應的資料一般包含一個提示詞 (Prompt) 和兩個候選回答,兩個回答中一個被記為選中 (chosen),另一個被記為淘汰 (rejected)。模型將要去學習著給出選中的回答,而不是被淘汰的那個。下圖就是一個例子:

圖片來自 openbmb/RLAIF-V-Dataset 資料集

圖片來自 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.5PaliGemma 這樣的模型上的 (可參考這篇文章: 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_procdataloader_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

結果

訓練需要幾小時的時間。當訓練完成後,我們可以看看訓練相關指標的變化曲線:

Learning curves

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
AMBER_2 Are there two ships in this image? Yes No
AMBER_111 Is the ground uneven in this image? No Yes
AMBER_7 Is there one shovel in this image? Yes No

你也可以自己找些例子來測試一下這個模型!

🤗 Space: HuggingFaceH4/compare_idefics-8b-dpo

微調 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

相關文章