Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

Open In Colab

使用 Hugging Face 生态系统 (TRL) 微调视觉语言模型 (Qwen2-VL-7B)

作者:Sergio Paniego

🚨 警告:此 Notebook 资源密集型,需要大量的计算能力。如果您在 Colab 中运行此 Notebook,它将使用 A100 GPU。

在本食谱中,我们将演示如何使用 Hugging Face 生态系统,特别是 Transformer 强化学习库 (TRL) 来微调视觉语言模型 (VLM)

🌟 模型和数据集概述

我们将基于 ChartQA 数据集微调 Qwen2-VL-7B 模型。该数据集包含各种图表类型的图像以及问题-答案对,非常适合增强模型的视觉问答能力。

📖 更多资源

如果您对更多 VLM 应用感兴趣,请查看:

fine_tuning_vlm_diagram.png

1. 安装依赖项

让我们开始安装微调所需的必要库!🚀

!pip install  -U -q git+https://github.com/huggingface/transformers.git git+https://github.com/huggingface/trl.git datasets bitsandbytes peft qwen-vl-utils wandb accelerate
# Tested with transformers==4.47.0.dev0, trl==0.12.0.dev0, datasets==3.0.2, bitsandbytes==0.44.1, peft==0.13.2, qwen-vl-utils==0.0.8, wandb==0.18.5, accelerate==1.0.1

我们还需要安装早期版本的 PyTorch,因为最新版本存在一个问题,目前阻止此 Notebook 正确运行。您可以在此处了解有关此问题的更多信息,并在问题解决后考虑更新到最新版本。

!pip install -q torch==2.4.1+cu121 torchvision==0.19.1+cu121 torchaudio==2.4.1+cu121 --extra-index-url https://download.pytorch.org/whl/cu121

登录 Hugging Face 以上传您微调的模型!🗝️

您需要使用您的 Hugging Face 帐户进行身份验证,才能直接从此 Notebook 保存和共享您的模型。

from huggingface_hub import notebook_login

notebook_login()

2. 加载数据集 📁

在本节中,我们将加载 HuggingFaceM4/ChartQA 数据集。此数据集包含图表图像以及相关的问题和答案,非常适合用于视觉问答任务的训练。

接下来,我们将为 VLM 生成系统消息。在本例中,我们希望创建一个充当图表图像分析专家并根据图表图像提供简洁答案的系统。

system_message = """You are a Vision Language Model specialized in interpreting visual data from chart images.
Your task is to analyze the provided chart image and respond to queries with concise answers, usually a single word, number, or short phrase.
The charts include a variety of types (e.g., line charts, bar charts) and contain colors, labels, and text.
Focus on delivering accurate, succinct answers based on the visual information. Avoid additional explanation unless absolutely necessary."""

我们将数据集格式化为聊天机器人结构以进行交互。每次交互将包含系统消息,后跟图像和用户查询,最后是查询的答案。

💡有关此模型的更多使用技巧,请查看模型卡片

def format_data(sample):
    return [
        {
            "role": "system",
            "content": [{"type": "text", "text": system_message}],
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "image": sample["image"],
                },
                {
                    "type": "text",
                    "text": sample["query"],
                },
            ],
        },
        {
            "role": "assistant",
            "content": [{"type": "text", "text": sample["label"][0]}],
        },
    ]

出于教育目的,我们将仅加载数据集中每个拆分的 10%。但是,在实际用例中,您通常会加载整个样本集。

from datasets import load_dataset

dataset_id = "HuggingFaceM4/ChartQA"
train_dataset, eval_dataset, test_dataset = load_dataset(dataset_id, split=["train[:10%]", "val[:10%]", "test[:10%]"])

让我们看一下数据集的结构。它包括图像、查询、标签(即答案)和我们将丢弃的第四个特征。

train_dataset

现在,让我们使用聊天机器人结构格式化数据。这将使我们能够为模型适当地设置交互。

train_dataset = [format_data(sample) for sample in train_dataset]
eval_dataset = [format_data(sample) for sample in eval_dataset]
test_dataset = [format_data(sample) for sample in test_dataset]
train_dataset[200]

3. 加载模型并检查性能!🤔

现在我们已经加载了数据集,让我们开始加载模型并使用数据集中的样本评估其性能。我们将使用 Qwen/Qwen2-VL-7B-Instruct,这是一个能够理解视觉数据和文本的视觉语言模型 (VLM)。

如果您正在探索替代方案,请考虑以下开源选项:

此外,您可以查看排行榜,例如 WildVision ArenaOpenVLM Leaderboard,以查找性能最佳的 VLM。

Qwen2_VL architecture

import torch
from transformers import Qwen2VLForConditionalGeneration, Qwen2VLProcessor

model_id = "Qwen/Qwen2-VL-7B-Instruct"

接下来,我们将加载模型和分词器,为推理做准备。

model = Qwen2VLForConditionalGeneration.from_pretrained(
    model_id,
    device_map="auto",
    torch_dtype=torch.bfloat16,
)

processor = Qwen2VLProcessor.from_pretrained(model_id)

为了评估模型的性能,我们将使用数据集中的一个样本。首先,让我们看一下此样本的内部结构。

train_dataset[0]

我们将使用不带系统消息的样本来评估 VLM 的原始理解能力。以下是我们将使用的输入:

train_dataset[0][1:2]

现在,让我们看一下与样本对应的图表。您能根据视觉信息回答查询吗?

>>> train_dataset[0][1]["content"][0]["image"]

让我们创建一个方法,该方法将模型、处理器和样本作为输入,以生成模型的答案。这将使我们能够简化推理过程并轻松评估 VLM 的性能。

from qwen_vl_utils import process_vision_info


def generate_text_from_sample(model, processor, sample, max_new_tokens=1024, device="cuda"):
    # Prepare the text input by applying the chat template
    text_input = processor.apply_chat_template(
        sample[1:2], tokenize=False, add_generation_prompt=True  # Use the sample without the system message
    )

    # Process the visual input from the sample
    image_inputs, _ = process_vision_info(sample)

    # Prepare the inputs for the model
    model_inputs = processor(
        text=[text_input],
        images=image_inputs,
        return_tensors="pt",
    ).to(
        device
    )  # Move inputs to the specified device

    # Generate text with the model
    generated_ids = model.generate(**model_inputs, max_new_tokens=max_new_tokens)

    # Trim the generated ids to remove the input ids
    trimmed_generated_ids = [out_ids[len(in_ids) :] for in_ids, out_ids in zip(model_inputs.input_ids, generated_ids)]

    # Decode the output text
    output_text = processor.batch_decode(
        trimmed_generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False
    )

    return output_text[0]  # Return the first decoded output text
# Example of how to call the method with sample:
output = generate_text_from_sample(model, processor, train_dataset[0])
output

虽然模型成功检索了正确的视觉信息,但它在准确回答问题方面存在困难。这表明微调可能是增强其性能的关键。让我们继续进行微调过程!

移除模型并清理 GPU

在我们继续在下一节中训练模型之前,让我们清除当前变量并清理 GPU 以释放资源。

import gc
import time


def clear_memory():
    # Delete variables if they exist in the current global scope
    if "inputs" in globals():
        del globals()["inputs"]
    if "model" in globals():
        del globals()["model"]
    if "processor" in globals():
        del globals()["processor"]
    if "trainer" in globals():
        del globals()["trainer"]
    if "peft_model" in globals():
        del globals()["peft_model"]
    if "bnb_config" in globals():
        del globals()["bnb_config"]
    time.sleep(2)

    # Garbage collection and clearing CUDA memory
    gc.collect()
    time.sleep(2)
    torch.cuda.empty_cache()
    torch.cuda.synchronize()
    time.sleep(2)
    gc.collect()
    time.sleep(2)

    print(f"GPU allocated memory: {torch.cuda.memory_allocated() / 1024**3:.2f} GB")
    print(f"GPU reserved memory: {torch.cuda.memory_reserved() / 1024**3:.2f} GB")


clear_memory()

4. 使用 TRL 微调模型

4.1 加载用于训练的量化模型 ⚙️

接下来,我们将使用 bitsandbytes 加载量化模型。如果您想了解有关量化的更多信息,请查看这篇博客文章这篇

from transformers import BitsAndBytesConfig

# 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 = Qwen2VLForConditionalGeneration.from_pretrained(
    model_id, device_map="auto", torch_dtype=torch.bfloat16, quantization_config=bnb_config
)
processor = Qwen2VLProcessor.from_pretrained(model_id)

4.2 设置 QLoRA 和 SFTConfig 🚀

接下来,我们将为我们的训练设置配置 QLoRA。QLoRA 能够有效地微调大型语言模型,同时与传统方法相比,显著减少了内存占用。与标准 LoRA 通过应用低秩近似来减少内存使用量不同,QLoRA 通过量化 LoRA 适配器的权重,更进一步地降低了内存需求并提高了训练效率,使其成为优化模型性能而不牺牲质量的绝佳选择。

>>> from peft import LoraConfig, get_peft_model

>>> # Configure LoRA
>>> peft_config = LoraConfig(
...     lora_alpha=16,
...     lora_dropout=0.05,
...     r=8,
...     bias="none",
...     target_modules=["q_proj", "v_proj"],
...     task_type="CAUSAL_LM",
... )

>>> # Apply PEFT model adaptation
>>> peft_model = get_peft_model(model, peft_config)

>>> # Print trainable parameters
>>> peft_model.print_trainable_parameters()
trainable params: 2,523,136 || all params: 8,293,898,752 || trainable%: 0.0304

我们将使用监督式微调 (SFT) 来改进我们的模型在手头任务上的性能。为此,我们将使用 SFTConfig 类从 TRL 库中定义训练参数。SFT 允许我们提供标记数据,帮助模型学习根据接收到的输入生成更准确的响应。这种方法确保模型根据我们的特定用例进行定制,从而在理解和响应视觉查询方面获得更好的性能。

from trl import SFTConfig

# Configure training arguments
training_args = SFTConfig(
    output_dir="qwen2-7b-instruct-trl-sft-ChartQA",  # Directory to save the model
    num_train_epochs=3,  # Number of training epochs
    per_device_train_batch_size=4,  # Batch size for training
    per_device_eval_batch_size=4,  # Batch size for evaluation
    gradient_accumulation_steps=8,  # Steps to accumulate gradients
    gradient_checkpointing=True,  # Enable gradient checkpointing for memory efficiency
    # Optimizer and scheduler settings
    optim="adamw_torch_fused",  # Optimizer type
    learning_rate=2e-4,  # Learning rate for training
    lr_scheduler_type="constant",  # Type of learning rate scheduler
    # Logging and evaluation
    logging_steps=10,  # Steps interval for logging
    eval_steps=10,  # Steps interval for evaluation
    eval_strategy="steps",  # Strategy for evaluation
    save_strategy="steps",  # Strategy for saving the model
    save_steps=20,  # Steps interval for saving
    metric_for_best_model="eval_loss",  # Metric to evaluate the best model
    greater_is_better=False,  # Whether higher metric values are better
    load_best_model_at_end=True,  # Load the best model after training
    # Mixed precision and gradient settings
    bf16=True,  # Use bfloat16 precision
    tf32=True,  # Use TensorFloat-32 precision
    max_grad_norm=0.3,  # Maximum norm for gradient clipping
    warmup_ratio=0.03,  # Ratio of total steps for warmup
    # Hub and reporting
    push_to_hub=True,  # Whether to push model to Hugging Face Hub
    report_to="wandb",  # Reporting tool for tracking metrics
    # Gradient checkpointing settings
    gradient_checkpointing_kwargs={"use_reentrant": False},  # Options for gradient checkpointing
    # Dataset configuration
    dataset_text_field="",  # Text field in dataset
    dataset_kwargs={"skip_prepare_dataset": True},  # Additional dataset options
    # max_seq_length=1024  # Maximum sequence length for input
)

training_args.remove_unused_columns = False  # Keep unused columns in dataset

4.3 训练模型 🏃

我们将使用 Weights & Biases (W&B) 记录我们的训练进度。让我们将 Notebook 连接到 W&B,以捕获训练期间的基本信息。

import wandb

wandb.init(
    project="qwen2-7b-instruct-trl-sft-ChartQA",  # change this
    name="qwen2-7b-instruct-trl-sft-ChartQA",  # change this
    config=training_args,
)

我们需要一个整理器函数,以便在训练过程中正确检索和批量处理数据。此函数将处理数据集输入的格式化,确保它们为模型正确结构化。让我们在下面定义整理器函数。

👉 查看 TRL 官方示例 scripts 以获取更多详细信息。

# Create a data collator to encode text and image pairs
def collate_fn(examples):
    # Get the texts and images, and apply the chat template
    texts = [
        processor.apply_chat_template(example, tokenize=False) for example in examples
    ]  # Prepare texts for processing
    image_inputs = [process_vision_info(example)[0] for example in examples]  # Process the images to extract inputs

    # Tokenize the texts and process the images
    batch = processor(
        text=texts, images=image_inputs, return_tensors="pt", padding=True
    )  # Encode texts and images into tensors

    # The labels are the input_ids, and we mask the padding tokens in the loss computation
    labels = batch["input_ids"].clone()  # Clone input IDs for labels
    labels[labels == processor.tokenizer.pad_token_id] = -100  # Mask padding tokens in labels

    # Ignore the image token index in the loss computation (model specific)
    if isinstance(processor, Qwen2VLProcessor):  # Check if the processor is Qwen2VLProcessor
        image_tokens = [151652, 151653, 151655]  # Specific image token IDs for Qwen2VLProcessor
    else:
        image_tokens = [processor.tokenizer.convert_tokens_to_ids(processor.image_token)]  # Convert image token to ID

    # Mask image token IDs in the labels
    for image_token_id in image_tokens:
        labels[labels == image_token_id] = -100  # Mask image token IDs in labels

    batch["labels"] = labels  # Add labels to the batch

    return batch  # Return the prepared batch

现在,我们将定义 SFTTrainer,它是 transformers.Trainer 类的包装器,并继承其属性和方法。当提供 PeftModel 对象时,此类通过正确初始化 PeftConfig 对象来简化微调过程。通过使用 SFTTrainer,我们可以高效地管理训练工作流程,并确保为我们的视觉语言模型提供流畅的微调体验。

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    data_collator=collate_fn,
    peft_config=peft_config,
    tokenizer=processor.tokenizer,
)

开始训练模型!🎉

trainer.train()

让我们保存结果 💾

trainer.save_model(training_args.output_dir)

5. 测试微调后的模型 🔍

现在我们已经成功微调了视觉语言模型 (VLM),现在是评估其性能的时候了!在本节中,我们将使用 ChartQA 数据集中的示例来测试模型,以了解它在回答基于图表图像的问题方面的表现。让我们深入了解并探索结果!🚀

让我们清理 GPU 内存以确保最佳性能 🧹

clear_memory()

我们将使用与之前相同的管道重新加载基础模型。

model = Qwen2VLForConditionalGeneration.from_pretrained(
    model_id,
    device_map="auto",
    torch_dtype=torch.bfloat16,
)

processor = Qwen2VLProcessor.from_pretrained(model_id)

我们将训练后的适配器附加到预训练模型。此适配器包含我们在训练期间进行的微调调整,使基础模型能够利用新知识,而无需更改其核心参数。通过集成适配器,我们可以增强模型的功能,同时保持其原始结构。

adapter_path = "sergiopaniego/qwen2-7b-instruct-trl-sft-ChartQA"
model.load_adapter(adapter_path)

我们将使用之前模型最初难以正确回答的数据集中的样本。

train_dataset[0][:2]
>>> train_dataset[0][1]["content"][0]["image"]
output = generate_text_from_sample(model, processor, train_dataset[0])
output

由于此样本来自训练集,因此模型在训练期间遇到过它,这可能被视为一种作弊形式。为了更全面地了解模型的性能,我们还将使用未见过的样本对其进行评估。

test_dataset[10][:2]
>>> test_dataset[10][1]["content"][0]["image"]
output = generate_text_from_sample(model, processor, test_dataset[10])
output

模型已成功学习如何按照数据集中指定的查询做出响应。我们实现了我们的目标!🎉✨

💻 我开发了一个示例应用程序来测试模型,您可以在此处找到它。您可以轻松地将其与另一个以预训练模型为特色的 Space 进行比较,该 Space 可在此处获得。

from IPython.display import IFrame

IFrame(src="https://sergiopaniego-qwen2-vl-7b-trl-sft-chartqa.hf.space", width=1000, height=800)

6. 比较微调后的模型与基础模型 + Prompting 📊

我们已经探索了微调 VLM 如何成为使其适应我们特定需求的有价值的选择。另一种要考虑的方法是直接使用 Prompting 或实施 RAG 系统,这在另一个食谱中有所介绍。

微调 VLM 需要大量的数据和计算资源,这可能会产生费用。相比之下,我们可以尝试 Prompting,看看是否可以在没有微调开销的情况下获得类似的结果。

让我们再次清理 GPU 内存以确保最佳性能 🧹

>>> clear_memory()
GPU allocated memory: 0.02 GB
GPU reserved memory: 0.27 GB

🏗️ 首先,我们将按照与之前相同的管道加载基线模型。

model = Qwen2VLForConditionalGeneration.from_pretrained(
    model_id,
    device_map="auto",
    torch_dtype=torch.bfloat16,
)

processor = Qwen2VLProcessor.from_pretrained(model_id)

📜 在本例中,我们将再次使用之前的样本,但这次我们将包含系统消息,如下所示。此添加有助于为模型情境化输入,从而可能提高其响应准确性。

train_dataset[0][:2]

让我们看看它的表现如何!

text = processor.apply_chat_template(train_dataset[0][:2], tokenize=False, add_generation_prompt=True)

image_inputs, _ = process_vision_info(train_dataset[0])

inputs = processor(
    text=[text],
    images=image_inputs,
    return_tensors="pt",
)

inputs = inputs.to("cuda")

generated_ids = model.generate(**inputs, max_new_tokens=1024)
generated_ids_trimmed = [out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)]

output_text = processor.batch_decode(
    generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)

output_text[0]

💡 正如我们所看到的,模型使用预训练模型以及附加的系统消息生成了正确的答案,而无需任何训练。根据具体的用例,这种方法可以作为微调的可行替代方案。

7. 继续学习之旅 🧑‍🎓️

为了进一步提高您在多模态模型方面的理解和技能,请查看以下资源:

这些资源将帮助您加深多模态学习的知识和技能。

< > 在 GitHub 上更新