视觉语言模型提示工程指南
视觉语言模型(VLMs)代表了在处理和理解多模态数据方面的一个重大进步,通过结合文本和视觉输入。
与仅处理文本的大语言模型(LLMs)不同,VLMs 是多模态的,使用户能够解决需要同时理解和文本的任务。这种能力打开了一个广泛的应用范围,例如视觉问答(VQA),其中模型根据图像回答问题,以及图像描述生成,涉及为图像生成描述性文本。在这篇博客文章中,我将解释如何提示 VLMs 来完成需要视觉理解的任务,并探讨不同的提示方法。
1、介绍
VLMs 是现有 LLMs 的扩展——特别是它们将视觉作为附加模态进行处理。VLMs 通常使用交叉注意力等技术训练图像和文本表示在同一表示或向量空间中对齐 [1] [2] [3] [4]。这些系统的优点在于你可以通过文本作为方便的接口“查询”或“交互”图像。由于其多模态能力,VLMs 对于弥合文本和视觉数据之间的差距至关重要,开辟了许多文本模型无法解决的应用场景。要深入了解 VLMs 的工作原理,我建议阅读 Sebastian Raschka 的关于多模态 LLMs 的优秀文章 。
在之前的博客中,我简要介绍了 提示 LLMs 的技术。类似于 LLMs,VLMs 也可以使用类似的提示技术——此外还具有通过图像帮助模型更好地理解任务的优势。在这篇博客中,我讨论了适用于 VLMs 的常见提示范式,涵盖了零样本、少量样本和思维链提示。我还探索了其他深度学习方法如何帮助指导 VLMs 提示——例如将对象检测集成到提示策略中。在我的实验中,我使用了 OpenAI 的 GPT-4o-mini 模型,这是一个 VLM。
本博客中的所有代码和资源都可以在这个 GitHub 链接 上找到。
2、数据集
为了撰写这篇博客,我从 Unsplash 下载了 5 张许可宽松使用的照片。具体来说,我使用了以下图片:
- 由 Mathias Reding 在 Unsplash 上的照片
- 由 Josh Frenette 在 Unsplash 上的照片
- 由 sander traa 在 Unsplash 上的照片
- 由 NEOM 在 Unsplash 上的照片
- 由 Alexander Zaytsev 在 Unsplash 上的照片
这些图片的标题是从图片网址中获取的,因为 Unsplash 图片附带了每张图片的相应标题。
3、零样本提示
零样本提示代表了一种情景,在这种情况下,用户只通过系统和用户提示描述任务并通过系统发送图像(或多个图像)进行处理。在这种设置下,VLM 仅依赖任务描述来生成输出。如果我们基于提供给 VLM 的信息量大小对提示方法进行分类,零样本提示提供了最少的信息量。
对于用户而言,这种优势在于,对于大多数任务,精心设计的零样本提示可以通过描述任务以文本形式产生不错的输出。考虑一下这一点的意义:仅仅几年之前,像图像分类或图像描述生成这样的任务需要在一个大型数据集上训练 CNN 或深度学习模型后才能使用。现在,你可以通过利用文本来描述它们来完成这些任务。你只需通过指定和描述你想要分析的内容就可以创建一个现成的图像分类器。在 VLM 出现之前,实现这一点需要收集一个大型的任务特定数据集,训练模型,然后用于推理。
那么我们如何提示它们? OpenAI 支持将图像作为 Base64 编码的 URL 发送到 VLM [2]。一般请求结构如下:
{
"role": "system",
"content": "You are a helpful assistant that can analyze images and provide captions."
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Please analyze the following image:"
},
{
"type": "image_url",
"image_url": {
"url": "data:image/jpeg;base64,{base64_image}",
"detail": "detail"
}
}
]
}
这个结构与使用 OpenAI 提示普通 LLM 的方式基本相同。但关键区别在于添加图像到请求中,这需要将其编码为 Base64 字符串。这里其实很有趣——虽然我在这里只使用一张图像作为输入,但实际上 没有任何限制阻止我同时附加多张图像。
让我们实现辅助函数来进行零样本提示。为了加快实验速度,我并行化了发送到 OpenAI API 的请求。我实现了构造提示和调用模型以获取输出的辅助函数。这包括一个将图像编码为 Base64 字符串的函数,以及用于构造提示并通过 OpenAI API 调用 VLM 的函数。
def encode_image(image_path):
"""
将图像编码为 Base64 字符串。
参数:
image_path (str): 要编码的图像文件路径。
返回:
str: 图像的 Base64 编码字符串,或者如果发生错误则返回 None。
"""
try:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
except Exception as e:
print(f"编码图像时出错: {e}")
return None
def llm_chat_completion(messages, model="gpt-4o-mini", max_tokens=300, temperature=0.0):
"""
使用指定参数调用 OpenAI 的 ChatCompletion API。
参数:
messages (list): 对话的消息列表字典。
model (str): 用于聊天完成的模型。
max_tokens (int, 可选): 响应的最大令牌数。默认值为 300。
temper归因(float,可选):采样温度,用于随机性。默认值为 0.0。
返回:
str 或 None: API 的响应内容,或在发生错误时返回 None。
"""
try:
response = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
temperature=temperature
)
return response.choices[0].message.content
except Exception as e:
print(f"调用 LLM 时出错: {e}")
return None
def build_few_shot_messages(few_shot_prompt, user_prompt="请分析以下图片:", detail="auto"):
"""
从图像-标题对生成少量示例消息。
参数:
few_shot_prompt (dict): 一个字典,将图像路径映射到元数据,包括 "image_caption"。
detail (str, 可选): 包含的图像细节级别。默认值为 "auto"。
返回:
list: 少量示例消息的列表。
"""
few_shot_messages = []
for path, data in few_shot_prompt.items():
base64_image = encode_image(path)
if not base64_image:
continue # 如果编码失败则跳过
caption = data
few_shot_messages.append(
{
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": detail
}
},
]
}
)
few_shot_messages.append({"role": "assistant", "content": caption})
return few_shot_messages
def build_user_message(image_path, user_prompt="请分析以下图片:", detail="auto"):
"""
为分析单个图像创建用户消息。
参数:
image_path (str): 图像文件的路径。
detail (str, 可选): 包含的图像细节级别。默认值为 "auto"。
返回:
dict 或 None: 用户消息字典,或如果图像编码失败则返回 None。
"""
base64_image = encode_image(image_path)
if not base64_image:
return None
return {
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": detail
}
},
]
}
我现在将所有这些组合在一起,并定义一个函数,该函数以图像路径作为输入,以及必要的参数(系统提示、用户提示和其他超参数),调用视觉语言模型并返回为图像生成的描述。
def get_image_caption(
image_path,
few_shot_prompt=None,
system_prompt="你是一个可以帮助分析图像并提供描述的助手。",
user_prompt="请分析以下图片:",
model="gpt-4o-mini",
max_tokens=300,
detail="auto",
llm_chat_func=llm_chat_completion,
temperature=0.0
):
"""
使用 LLM 获取图像的描述。
参数:
image_path (str): 要分析的图像的文件路径。
few_shot_prompt (dict, 可选): 映射图像路径到 {"image_caption": <描述>}。
system_prompt (str, 可选): LLM 的初始系统提示。
user_prompt (str, 可选): LLM 的用户提示。
model (str, 可选): LLM 模型名称(默认为 "gpt-4o-mini")。
max_tokens (int, 可选): 响应中的最大标记数(默认为 300)。
detail (str, 可选): 图像分析的详细程度(默认为 "auto")。
llm_chat_func (callable, 可选): 调用 LLM 的函数。默认为 `llm_chat_completion`。
temperature (float, 可选): 采样温度(默认为 0.0)。
返回:
str 或 None: 生成的描述,或在出现错误时返回 None。
"""
try:
user_message = build_user_message(image_path, detail)
if not user_message:
return None
# 构建消息序列
messages = [{"role": "system", "content": system_prompt}]
# 如果提供了 few-shot 示例,则包含它们
if few_shot_prompt:
few_shot_messages = build_few_shot_messages(few_shot_prompt, detail)
messages.extend(few_shot_messages)
messages.append(user_message)
# 调用 LLM
response_text = llm_chat_func(
model=model,
messages=messages,
max_tokens=max_tokens,
temperature=temperature
)
return response_text
except Exception as e:
print(f"获取描述时出错: {e}")
return None
def process_images_in_parallel(
image_paths,
model="gpt-4o-mini",
system_prompt="你是一个可以帮助分析图像并提供描述的助手。",
user_prompt="请分析以下图片:",
few_shot_prompt=None,
max_tokens=300,
detail="auto",
max_workers=5
):
"""
并行处理一组图像以使用指定模型生成描述。
参数:
image_paths (list): 要处理的图像文件路径列表。
model (str): 用于生成描述的模型(默认为 "gpt-4o")。
max_tokens (int): 生成描述的最大标记数(默认为 300)。
detail (str): 图像分析的详细程度(默认为 "auto")。
max_workers (int): 并行处理使用的线程数(默认为 5)。
返回:
dict: 字典,键为图像路径,值为其对应的描述。
"""
captions = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用 lambda 或 partial 传递其他参数
future_to_image = {
executor.submit(
get_image_caption,
image_path,
few_shot_prompt,
system_prompt,
user_prompt,
model,
max_tokens,
detail
): image_path
for image_path in image_paths
}
# 使用 tqdm 跟踪进度
for future in tqdm(as_completed(future_to_image), total=len(image_paths), desc="处理图像"):
image_path = future_to_image[future]
try:
caption = future.result()
captions[image_path] = caption
except Exception as e:
print(f"处理 {image_path} 时出错: {e}")
captions[image_path] = None
return captions
我们现在运行两个图像的输出,并从零样本提示设置中获得相应的描述。
from tqdm import tqdm
import os
IMAGE_QUALITY = "high"
PATH_TO_SAMPLES = "images/"
system_prompt = """你是一个可以分析图像并提供描述的 AI 助手。
您将被提供一张图像。分析图像的内容、上下文和显著特征。
提供一个简洁的描述,涵盖图像的重要方面。"""
user_prompt = "请分析以下图片:"
image_paths = [os.path.join(PATH_TO_SAMPLES, x) for x in os.listdir(PATH_TO_SAMPLES)]
zero_shot_high_quality_captions = process_images_in_parallel(image_paths, model="gpt-4o-mini", system_prompt=system_prompt, user_prompt=user_prompt, few_shot_prompt=None, detail=IMAGE_QUALITY, max_workers=5)
我们观察到模型为提供的图像生成了详细的描述,涵盖了所有方面的详细信息。
4、少样本提示
少样本提示涉及将任务的示例或演示作为上下文提供给视觉语言模型,以便为模型提供更多参考和上下文以执行任务。
少样本提示的优势在于它可以帮助为任务提供额外的背景,与零样本提示相比。让我们看看这在我们的任务中是如何实际发生的。首先,考虑现有的实现:
def build_few_shot_messages(few_shot_prompt, user_prompt = "请分析以下图片:", detail="auto"):
"""
从图像-标题对生成少量示例消息。
参数:
few_shot_prompt (dict): 一个字典,将图像路径映射到元数据,包括 "image_caption"。
detail (str, 可选): 包含的图像细节级别。默认值为 "auto"。
返回:
list: 少量示例消息的列表。
"""
few_shot_messages = []
for path, data in few_shot_prompt.items():
base64_image = encode_image(path)
if not base64_image:
continue # 如果编码失败则跳过
caption = data
few_shot_messages.append(
{
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": detail
}
},
]
}
)
few_shot_messages.append({"role": "assistant", "content": caption})
return few_shot_messagesturns:
list: A list of few-shot example消息。
"""
few_shot_messages = []
for path, data in few_shot_prompt.items():
base64_image = encode_image(path)
if not base64_image:
continue # 跳过如果编码失败的情况
caption = data
few_shot_messages.append(
{
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": detail
}
},
]
}
)
few_shot_messages.append({"role": "assistant", "content": caption})
return few_shot_messages
我利用3张图像作为few-shot示例提供给视觉语言模型(VLM),然后运行我们的系统。
image_captions = json.load(open("image_captions.json"))
FEW_SHOT_EXAMPLES_PATH = "few_shot_examples/"
few_shot_samples = os.listdir(FEW_SHOT_EXAMPLES_PATH)
few_shot_captions = {os.path.join(FEW_SHOT_EXAMPLES_PATH,k):v for k,v in image_captions.items() if k in few_shot_samples}
IMAGE_QUALITY = "high"
few_shot_high_quality_captions = process_images_in_parallel(image_paths, model = "gpt-4o-mini", few_shot_prompt= few_shot_captions, detail=IMAGE_QUALITY, max_workers=5)
注意使用VLM生成的few-shot示例对应的描述。这些描述与零样本提示设置下生成的描述相比,要简洁得多。让我们看看这些示例的描述看起来如何:
这种差异主要是由于few-shot示例的影响。 Few-shot示例中的描述明显影响了VLM对新图像输出的描述,导致生成的描述更加简洁和简短。这说明了few-shot示例的选择对于塑造VLM行为的强大作用。用户可以通过精心选择few-shot样本来引导模型的输出,使其更接近他们偏好的风格、语气或详细程度。
5、链式思维提示
链式思维(CoT)提示[9]最初是为LLMs开发的,它通过将复杂问题分解成更简单的中间步骤,并鼓励模型在回答之前“思考”,使它们能够解决复杂的任务。这种方法可以不加调整地应用于VLMs。主要的区别在于,VLM可以在CoT推理过程中同时使用图像和文本作为输入。
为了实现这一点,我利用了为few-shot提示选择的3个示例,并使用OpenAI的O1模型(这是一种用于推理任务的多模态VLM)构建它们的CoT跟踪。然后,我将这些CoT推理跟踪用作提示VLM的few-shot示例。例如,对于一张图像,CoT跟踪如下所示:
我为这些图像创建的few-shot CoT跟踪包含每个重要方面详细的推理,以便生成最终的描述。
few_shot_samples_1_cot = """Observations:
可见模糊:前景和部分图像失焦或模糊,表明可能是相机移动或拍摄时主体运动造成的。
高大、华丽的建筑:建筑物有多层楼,装饰性阳台和精致的立面,暗示这是较老或经典的都市建筑。
街道级视角:两侧停满了车的道路或狭窄街道确认这是一个城市环境,具有典型的市内交通和基础设施。
柔和、温暖的光线:阳光以一定角度照射到建筑物上,使立面呈现出温暖的光芒,增强了真实城市场景的感觉,而不是舞台布置。
最终描述:一张城市街道的模糊照片,有建筑物。"""
few_shot_samples_2_cot = """Observations:
高大的沙漠沙丘:景观由大型、起伏的沙丘组成,在干燥、荒凉的环境中。
越野车辆:白色的SUV看起来适合穿越崎岖地形,其大小和离地间隙表明其适应能力。
沙丘上的轮胎痕迹:可见的轨迹显示最近在沙丘上移动,证明车辆正在行驶并导航沙漠路径。
从另一辆车内部拍摄:前景中的仪表盘和挡风玻璃框架表明照片是从乘客或驾驶员的视角拍摄的,可能是跟随或平行于SUV行驶。
最终描述:一辆白色汽车在沙漠道路上行驶。"""
few_shot_samples_3_cot = """Observations:
陡峭的岩壁:陡峭的峡谷墙壁表明是一个崎岖的沙漠景观,两侧有砂岩悬崖。
发光的帐篷:两个未来感十足的帐篷发出柔和的光,表明这是一个夜晚场景,帐篷里有灯光或灯笼。
繁星点点的夜空:头顶的星星表明这是一个户外露营的夜间场景。
单个男性身影:一个男人站在其中一个帐篷附近,表明他可能是露营小组的一员。
最终描述:一个男人站在沙漠中的帐篷旁边。"""
我们为VLM运行CoT提示设置,并获得这些图像的结果:
import copy
few_shot_samples_cot = copy.deepcopy(few_shot_captions)
few_shot_samples_cot["few_shot_examples/photo_1.jpg"] = few_shot_samples_1_cot
few_shot_samples_cot["few_shot_examples/photo_3.jpg"] = few_shot_samples_2_cot
few_shot_samples_cot["few_shot_examples/photo_4.jpg"] = few_shot_samples_3_cot
IMAGE_QUALITY = "high"
cot_high_quality_captions = process_images_in_parallel(image_paths, model = "gpt-4o-mini", few_shot_prompt= few_shot_samples_cot, detail=IMAGE_QUALITY, max_workers=5)
从few-shot CoT设置中获得的输出显示,VLM现在能够在得出最终描述之前将图像分解为中间步骤。这种方法突显了CoT提示在影响VLM执行特定任务行为方面的强大作用。
6、基于对象检测引导的提示
拥有能够直接处理图像并识别各种元素的模型的一个有趣含义是,它开启了无数机会,使VLM能够更高效地执行任务并实现“特征工程”。如果你仔细想想,你会发现你可以提供带有与VLM任务相关的嵌入元数据的图像。
一个例子就是将对象检测作为提示VLM的附加组件。AWS的这篇博客[10]对此进行了详细探讨。在这篇博客中,我利用对象检测模型作为提示VLM管道中的附加组件。
通常情况下,一个目标检测模型是用固定的词汇表训练的,这意味着它只能识别预定义的一组物体类别。然而,在我们的流程中,由于我们无法提前预测图像中会出现哪些物体,因此我们需要一个灵活且能够识别广泛物体类别的目标检测模型。为此,我使用了OWLViT模型[11],这是一个开放词汇的目标检测模型。该模型需要指定要检测的物体的文字提示。
另一个需要解决的挑战是在利用OWLViT模型之前,如何获得对图像中物体的高层次理解,因为该模型需要描述物体的文字提示。这就是视觉语言模型(VLM)发挥作用的地方!首先,我们将图像传递给VLM,并提供一个提示来识别图像中的高层次物体。这些检测到的物体随后将作为文字提示与图像一起传递给OWLViT模型以生成检测结果。接下来,我们在同一张图像上绘制检测框,并将更新后的图像传递给VLM,提示其生成图像标题。推理代码部分改编自[12]。
# 直接加载模型
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
processor = AutoProcessor.from_pretrained("google/owlvit-base-patch32")
model = AutoModelForZeroShotObjectDetection.from_pretrained("google/owlvit-base-patch32")
我使用VLM检测每张图像中存在的物体:
IMAGE_QUALITY = "high"
system_prompt_object_detection = """你被提供了一张图片。你必须识别图像中的所有重要物体,并提供标准化的物体列表。
输出格式如下:
输出:物体_1, 物体_2"""
user_prompt = "从提供的图片中提取物体:"
detected_objects = process_images_in_parallel(image_paths, system_prompt=system_prompt_object_detection, user_prompt=user_prompt, model = "gpt-4o-mini", few_shot_prompt= None, detail=IMAGE_QUALITY, max_workers=5)
detected_objects_cleaned = {}
for key, value in detected_objects.items():
detected_objects_cleaned[key] = list(set([x.strip() for x in value.replace("Output: ", "").split(",")]))
现在将检测到的物体作为文字提示传递给OWLViT模型,以获取图像的预测结果。我实现了一个辅助函数,用于为图像预测边界框,并在原始图像上绘制边界框。
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import torch
def detect_and_draw_bounding_boxes(
image_path,
text_queries,
model,
processor,
output_path,
score_threshold=0.2
):
"""
使用PIL在图像上检测物体并绘制边界框。
参数:
- image_path (str): 图像文件路径。
- text_queries (list of str): 要处理的文字查询列表。
- model: 用于检测的预训练模型。
- processor: 预处理图像和文字查询的处理器。
- output_path (str): 保存带有边界框的输出图像的路径。
- score_threshold (float): 过滤低置信度预测的阈值。
返回:
- output_image_pil: 包含边界框和标签的PIL Image对象。
"""
img = Image.open(image_path).convert("RGB")
orig_w, orig_h = img.size # 原始宽度和高度
inputs = processor(
text=text_queries,
images=img,
return_tensors="pt",
padding=True,
truncation=True
).to("cpu")
model.eval()
with torch.no_grad():
outputs = model(**inputs)
logits = torch.max(outputs["logits"][0], dim=-1) # 形状 (num_boxes,)
scores = torch.sigmoid(logits.values).cpu().numpy() # 转换为概率
labels = logits.indices.cpu().numpy() # 类别索引
boxes_norm = outputs["pred_boxes"][0].cpu().numpy() # 形状 (num_boxes, 4)
converted_boxes = []
for box in boxes_norm:
cx, cy, w, h = box
cx_abs = cx * orig_w
cy_abs = cy * orig_h
w_abs = w * orig_w
h_abs = h * orig_h
x1 = cx_abs - w_abs / 2.0
y1 = cy_abs - h_abs / 2.0
x2 = cx_abs + w_abs / 2.0
y2 = cy_abs + h_abs / 2.0
converted_boxes.append((x1, y1, x2, y2))
draw = ImageDraw.Draw(img)
for score, (x1, y1, x2, y2), label_idx in zip(scores, converted_boxes, labels):
if score < score_threshold:
continue
draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
label_text = text_queries[label_idx].replace("An image of ", "")
text_str = f"{label_text}: {score:.2f}"
text_size = draw.textsize(text_str) # 如果没有字体,删除 "font=font"
text_x, text_y = x1, max(0, y1 - text_size[1]) # 将文本放置在框上方稍高处
draw.rectangle(
[text_x, text_y, text_x + text_size[0], text_y + text_size[1]],
fill="white"
)
draw.text((text_x, text_y), text_str, fill="red") # , font=font)
img.save(output_path, "JPEG")
return img
for key, value in tqdm(detected_objects_cleaned.items()):
value = ["An image of " + x for x in value]
detect_and_draw_bounding_boxes(key, value, model, processor, "images_with_bounding_boxes/" + key.split("/")[-1], score_threshold=0.15)
带有检测物体的图像现在被传递给VLM进行图像标题生成:
IMAGE_QUALITY = "high"
image_paths_obj_detected_guided = [x.replace("downloaded_images", "images_with_bounding_boxes") for x in image_paths]
system_prompt="""你是一个可以帮助分析图像并提供标题的助手。你被提供了一张包含重要物体边界框注释的图像,以及它们的标签。
分析整体图像和提供的边界框信息,并为图像提供适当的标题。""",
user_prompt="请分析以下图像:"
obj_det_zero_shot_high_quality_captions = process_images_in_parallel(image_paths_obj_detected_guided, model = "gpt-4o-mini", few_shot_prompt= None, detail=IMAGE_QUALITY, max_workers=5)
在这个任务中,鉴于我们使用的图像简单性,物体的位置并没有向VLM提供任何重要的信息。然而,目标检测引导提示对于更复杂的任务,例如文档理解,可以成为一种强大的工具,通过目标检测向VLM提供布局信息以进一步处理。此外,语义分割可以作为一种方法,通过提供分割掩码来指导提示。
7、结束语
视觉语言模型(VLM)是AI工程师和科学家解决各种需要结合视觉和文本技能的问题的强大工具。在这篇文章中,我探讨了VLM上下文中的提示策略,以有效地使用这些模型完成图像描述等任务。这绝不是提示策略的详尽或全面列表。随着通用人工智能(GenAI)的进步,越来越明显的是,创意和创新的方法在引导LLM和VLM解决问题方面具有无限潜力。对于有关VLM提示的补充资源,我还推荐Azure的材料 [13]。
原文链接:Prompting Vision Language Models
汇智网翻译整理,转载请标明出处