目录
第八章:语义搜索和RAG-CSDN博客https://blog.csdn.net/m0_67804957/article/details/145807830当你想到大型语言模型(LLMs)时,多模态可能并不是首先浮现在脑海中的东西。毕竟,它们是语言模型!然而,我们很快就会意识到,如果模型能够处理除文本之外的其他类型数据,它们会更有用。例如,如果一个语言模型能够查看一张图片并回答有关它的问题,这将非常有用。能够处理文本和图像(每种称为一种模态)的模型被称为多模态模型,如下图所示。

我们已经看到了LLMs中出现的各种新兴行为,从泛化能力、推理能力到算术和语言学能力。随着模型变得更大、更智能,它们的技能集也在不断扩大。
能够接收和推理多模态输入的能力可能会进一步增强,并帮助解锁之前无法实现的能力。在实践中,语言并非孤立存在。例如,你的肢体语言、面部表情、调语等都是增强口头表达的沟通方式。
同理,这也适用于LLMs;如果我们能够使它们能够推理多模态信息,它们的能力可能会增加,我们也将能够将它们部署到解决新的问题类型。
在本章中,我们将探索一些具有多模态能力的不同LLMs,以及这对实际用例意味着什么。我们首先探讨图像如何通过原始Transformer技术的改编转换为数值表示。然后,我们将展示如何通过这种Transformer扩展LLMs以包含视觉任务。
一、视觉Transformer(ViT)
在本书的各个章节中,我们已经看到了基于Transformer的模型在多种语言建模任务中的成功,从分类和聚类到搜索和生成建模。因此,研究人员试图将Transformer的部分成功推广到计算机视觉领域也就不足为奇了。
他们提出的方法被称为视觉Transformer(ViT),与之前默认的卷积神经网络(CNN)相比,ViT在图像识别任务上表现出色。就像原始的Transformer一样,ViT用于将非结构化数据(图像)转换为可用于多种任务(如分类)的表示,如下图所示。

ViT依赖于Transformer架构的一个重要组成部分,即编码器。正如我们在第1章中看到的,编码器负责将文本输入转换为数值表示,然后传递给解码器。然而,在编码器能够履行其职责之前,文本输入需要先进行分词,如下图所示。

由于图像并不由单词组成,因此这种分词过程不能用于视觉数据。相反,ViT的作者提出了一种将图像分词为“单词”的方法,从而允许他们使用原始的编码器结构。
假设你有一张猫的图片。这张图片由许多像素组成,假设是512×512像素。每个单独的像素并不能传达太多信息,但当你将像素组合成块时,你逐渐开始看到更多信息。
ViT采用的原理与此类似。它不是将文本分割成词元,而是将原始图像转换为图像块。换句话说,它将图像在水平和垂直方向上切成若干块,如下图所示。

就像我们将文本转换为文本词元一样,我们将图像转换为图像块。图像块的展平输入可以被视为文本中的一段词元。然而,与词元不同的是,我们不能简单地为每个块分配一个ID,因为这些块很少会出现在其他图像中,这与文本的词汇表不同。
相反,这些块通过线性嵌入创建数值表示,即嵌入。然后这些嵌入可以用作Transformer模型的输入。这样,图像块就像词元一样被处理。整个过程如下图所示。

为了说明方便,示例中的图像被切成了3×3的块,但原始实现使用了16×16的块。毕竟,论文的名字叫“一张图像等于16×16个词”。
这种方法的有趣之处在于,一旦嵌入被传递到编码器,它们就会被当作文本词元一样处理。从那一刻起,文本和图像的训练方式没有任何区别。
由于这些相似性,ViT经常被用来使各种语言模型具备多模态能力。最直接的使用方法之一是在嵌入模型训练期间。
二、多模态嵌入模型
在前面的章节中,我们使用嵌入模型来捕捉文本表示的语义内容,例如论文和文档。我们发现,可以利用这些嵌入(或数值表示)来查找相似的文档、执行分类任务,甚至进行主题建模。
正如我们多次看到的,嵌入通常是LLM应用背后的重要驱动力。它们是一种高效的方法,用于捕捉大规模信息,并在信息的海洋中寻找针尖般的关键内容。
尽管如此,到目前为止,我们只探讨了专注于生成文本表示嵌入的纯文本嵌入模型。虽然也有专门用于嵌入图像的嵌入模型,但我们将关注能够同时捕捉文本和视觉表示的嵌入模型。我们在下图中展示了这一点。

其优势在于,这允许比较多模态表示,因为生成的嵌入位于同一向量空间(下图)。例如,使用这种多模态嵌入模型,我们可以根据输入文本查找图像。如果我们搜索“小狗图片”这样的文本,我们会找到什么样的图像?反之亦然也是可能的。哪些文档与这个问题最相关?

目前存在许多多模态嵌入模型,但最知名且目前使用最广泛的模型是对比语言-图像预训练(CLIP)。
2.1 CLIP:连接文本和图像
CLIP是一种嵌入模型,可以计算图像和文本的嵌入。生成的嵌入位于同一向量空间,这意味着图像的嵌入可以与文本的嵌入进行比较。这种比较能力使得CLIP及类似模型可用于以下任务:
零样本分类
我们可以将图像的嵌入与可能类别的描述的嵌入进行比较,以找出最相似的类别。聚类
将图像和一组关键词进行聚类,以找出哪些关键词属于哪些图像集合。搜索
在数十亿文本或图像中,我们可以快速找到与输入文本或图像相关的内容。生成
使用多模态嵌入驱动图像生成(例如,稳定扩散)。
2.2 CLIP如何生成多模态嵌入?
CLIP的流程其实相当直观。假设你有一个包含数百万图像及其标题的数据集,如我们在下图中所示。

这个数据集可以用来为每一对图像及其标题创建两种表示。为此,CLIP使用一个文本编码器来嵌入文本,以及一个图像编码器来嵌入图像。如下图所示,结果是图像及其对应标题的嵌入。

生成的嵌入对通过余弦相似度进行比较。正如我们在第4章中看到的,余弦相似度是向量之间夹角的余弦值,通过嵌入的点积除以其长度的乘积来计算。
在训练开始时,图像嵌入和文本嵌入之间的相似度会很低,因为它们尚未被优化到同一向量空间。在训练过程中,我们优化嵌入之间的相似度,希望最大化相似图像/标题对的相似度,并最小化不相似图像/标题对的相似度(下图)。

在计算它们的相似度后,模型会更新,然后使用新的数据批次和更新后的表示重新开始这一过程(下图)。这种方法被称为对比学习,我们将在第10章中深入探讨其内部机制,并创建我们自己的嵌入模型。

最终,我们期望一张猫的图像的嵌入与“一张猫的图片”这一短语的嵌入相似。正如我们将在第10章中看到的,为了确保表示尽可能准确,训练过程中还应包括不相关的图像和标题的负样本。建模相似性不仅要知道什么使事物相似,还要知道什么使它们不同和不相似。
三、OpenCLIP
在接下来的例子中,我们将使用开源版本的CLIP,即OpenCLIP。使用OpenCLIP或任何CLIP模型,核心在于两件事:在将输入传递给主模型之前,处理文本和图像输入。
在开始之前,我们先看一个小例子,我们将使用之前见过的一张图片,即通过Stable Diffusion生成的一张小狗在雪地里玩耍的AI生成图像,如下图所示:
from urllib.request import urlopen
from PIL import Image
# 加载一张AI生成的小狗在雪地里玩耍的图片
puppy_path = "https://raw.githubusercontent.com/HandsOnLLM/Hands-On-Large-Language-Models/main/chapter09/images/puppy.png"
image = Image.open(urlopen(puppy_path)).convert("RGB")
caption = "a puppy playing in the snow"

由于我们有这张图片的标题,因此可以使用OpenCLIP为两者生成嵌入。
为此,我们需要加载三个模型:
一个用于对文本输入进行分词的分词器
一个用于预处理和调整图像大小的预处理器
主模型,用于将上述输出转换为嵌入
from transformers import CLIPTokenizerFast, CLIPProcessor, CLIPModel
model_id = "openai/clip-vit-base-patch32"
# 加载分词器以预处理文本
clip_tokenizer = CLIPTokenizerFast.from_pretrained(model_id)
# 加载处理器以预处理图像
clip_processor = CLIPProcessor.from_pretrained(model_id)
# 主模型,用于生成文本和图像嵌入
model = CLIPModel.from_pretrained(model_id)
加载模型后,预处理输入变得非常简单。我们先从分词器开始,看看预处理输入后会发生什么:
# 对输入进行分词
inputs = clip_tokenizer(caption, return_tensors="pt")
inputs
这将输出一个包含输入ID的字典:
{'input_ids': tensor([[49406, 320, 6829, 1629, 530, 518, 2583, 49407]]),
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}
为了查看这些ID代表什么,我们可以使用convert_ids_to_tokens
函数将它们转换回词元:
# 将输入转换回词元
clip_tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
这将输出以下内容:
['<|startoftext|>',
'a</w>',
'puppy</w>',
'playing</w>',
'in</w>',
'the</w>',
'snow</w>',
'<|endoftext|>']
正如我们之前多次看到的,文本被分割成了词元。此外,我们还看到文本的开头和结尾被标记出来,以将其与潜在的图像嵌入区分开。你可能还会注意到,这里没有[CLS]词元。在CLIP中,[CLS]词元实际上用于表示图像嵌入。
现在我们已经预处理了标题,可以创建嵌入了:
# 创建文本嵌入
text_embedding = model.get_text_features(**inputs)
text_embedding.shape
这将生成一个包含512个值的嵌入,对应于这个单一字符串:
torch.Size([1, 512])
在创建图像嵌入之前,我们需要像文本嵌入一样对图像进行预处理,因为模型期望输入图像具有某些特性,例如大小和形状。
为此,我们可以使用之前创建的处理器:
# 预处理图像
processed_image = clip_processor(
text=None, images=image, return_tensors="pt"
)["pixel_values"]
processed_image.shape
原始图像的大小为512×512像素。注意,预处理将图像的大小调整为224×224像素,因为这是模型期望的大小:
torch.Size([1, 3, 224, 224])
让我们可视化预处理的结果,如图9-13所示:
import torch
import numpy as np
import matplotlib.pyplot as plt
# 准备图像以便可视化
img = processed_image.squeeze(0)
img = img.permute(*torch.arange(img.ndim - 1, -1, -1))
img = np.einsum("ijk->jik", img)
# 可视化预处理后的图像
plt.imshow(img)
plt.axis("off")

为了将这个预处理后的图像转换为嵌入,我们可以像之前一样调用模型,并查看返回的形状:
# 创建图像嵌入
image_embedding = model.get_image_features(processed_image)
image_embedding.shape
>> 返回形状
torch.Size([1, 512])
注意,生成的图像嵌入的形状与文本嵌入的形状相同。这很重要,因为它允许我们比较它们的嵌入,看看它们是否相似。
我们可以使用这些嵌入来计算它们的相似度。为此,我们首先需要对嵌入进行归一化,然后计算点积以得出相似度分数:
# 归一化嵌入
text_embedding /= text_embedding.norm(dim=-1, keepdim=True)
image_embedding /= image_embedding.norm(dim=-1, keepdim=True)
# 计算它们的相似度
text_embedding = text_embedding.detach().cpu().numpy()
image_embedding = image_embedding.detach().cpu().numpy()
score = np.dot(text_embedding, image_embedding.T)
score
这将返回以下分数:
array([[0.33149648]], dtype=float32)
我们得到的相似度分数为0.33。由于我们不知道模型认为低相似度和高相似度的分界线,因此很难解释这个分数。相反,我们可以通过扩展示例,添加更多图像和标题来进一步说明,如图9-14所示。

从结果来看,0.33的分数确实较高,因为与其他图像的相似度相比,这个分数明显更高。
使用sentence-transformers加载CLIP
sentence-transformers
库实现了一些基于CLIP的模型,这使得创建嵌入变得更加简单。只需要几行代码即可:from sentence_transformers import SentenceTransformer, util # 加载与SBERT兼容的CLIP模型 model = SentenceTransformer("clip-ViT-B-32") # 编码图像 image_embeddings = model.encode(images) # 编码标题 text_embeddings = model.encode(captions) # 计算余弦相似度 sim_matrix = util.cos_sim( image_embeddings, text_embeddings )
四、让文本生成模型具备多模态能力
传统上,文本生成模型正如你所期望的那样,是用于解释文本表示的模型。像Llama 2和ChatGPT这样的模型在推理文本信息并以自然语言回应方面表现出色。
然而,它们仅限于其训练所使用的模态,即文本。正如我们在多模态嵌入模型中看到的那样,加入视觉能力可以增强模型的能力。
在文本生成模型的情况下,我们希望它能够对某些输入图像进行推理。例如,我们可以给它一张披萨的图片,然后问它包含哪些配料。你也可以给它一张埃菲尔铁塔的图片,然后问它是什么时候建造的,或者它位于哪里。这种对话能力在下图中进一步得到了说明。

为了弥合这两个领域的差距,人们尝试为现有的模型引入多模态形式。其中一种方法被称为BLIP-2:用于统一视觉-语言理解和生成的引导式语言-图像预训练2。BLIP-2是一种易于使用且模块化的技术,允许为现有的语言模型引入视觉能力。
4.1 BLIP-2:弥合模态差距
从零开始创建一个多模态语言模型需要大量的计算能力和数据。我们需要使用数十亿的图像、文本和图像-文本对来创建这样的模型。可想而知,这并不容易实现!
与其从零开始构建架构,BLIP-2通过构建一个名为查询Transformer(Q-Former)的“桥梁”来弥合视觉与语言之间的差距,连接一个预训练的图像编码器和一个预训练的语言模型(LLM)。
通过利用预训练模型,BLIP-2只需要训练这座“桥梁”,而无需从零开始训练图像编码器和LLM。它充分利用了现有的技术和模型!这座桥梁在下图中有所展示。

为了连接这两个预训练模型,Q-Former模仿了它们的架构。它包含两个共享注意力层的模块:
一个图像Transformer,用于与冻结的视觉Transformer交互以提取特征;
一个文本Transformer,可以与LLM交互。
Q-Former的训练分为两个阶段,每个模态一个,如下图所示。

在第一步中,使用图像-文档对来训练Q-Former以表示图像和文本。这些配对通常是图像的标题,正如我们在训练CLIP时所看到的那样。
图像被输入到冻结的ViT中以提取视觉嵌入。这些嵌入被用作Q-Former的ViT的输入。标题被用作Q-Former的文本Transformer的输入。
有了这些输入,Q-Former随后在三个任务上进行训练:
图像-文本对比学习
该任务尝试对齐图像和文本嵌入对,以最大化它们之间的互信息。图像-文本匹配
一个分类任务,用于预测图像和文本对是否为正(匹配)或负(不匹配)。基于图像的文本生成
训练模型根据从输入图像中提取的信息生成文本。
这三个目标共同优化,以改进从冻结的ViT中提取的视觉表示。从某种意义上说,我们试图将文本信息注入到冻结的ViT的嵌入中,以便我们可以在LLM中使用它们。BLIP-2的第一步在下图中有所展示。

在第二步中,第一步得到的可学习嵌入现在包含了与相应文本信息相同维度空间的视觉信息。这些可学习嵌入随后被传递给LLM。从某种意义上说,这些嵌入作为软视觉提示,使LLM能够基于Q-Former提取的视觉表示进行条件化。
它们之间还有一个全连接的线性层,以确保可学习嵌入具有LLM期望的相同形状。将视觉转换为语言的第二步在下图中有所展示。

将这些步骤结合起来,使得Q-Former能够在相同的维度空间中学习视觉和文本表示,这些表示可以用作LLM的软提示。因此,LLM将以类似于你在提示LLM时提供上下文的方式获得有关图像的信息。整个BLIP-2的详细过程在下图中有所展示。

自BLIP-2以来,许多其他视觉LLM已经发布,它们的过程类似,例如LLaVA(一个使文本LLM具备多模态能力的框架)或Idefics 2(基于Mistral 7B LLM的高效视觉LLM)。尽管这两种视觉LLM的架构不同,但它们都将预训练的类似CLIP的视觉编码器与文本LLM连接起来。这些架构的目标是将输入图像的视觉特征投影到语言嵌入中,以便它们可以用作LLM的输入。与Q-Former类似,它们试图弥合图像与文本之间的差距。
五、预处理多模态输入
现在我们已经了解了 BLIP-2 是如何创建的,这种模型有许多有趣的用例,不仅限于为图像生成标题、回答视觉问题,甚至还可以进行提示。
在我们探讨一些用例之前,让我们先加载模型并探索如何使用它:
from transformers import AutoProcessor, Blip2ForConditionalGeneration
import torch
# 加载处理器和主模型
blip_processor = AutoProcessor.from_pretrained("Salesforce/blip2-opt-2.7b")
model = Blip2ForConditionalGeneration.from_pretrained(
"Salesforce/blip2-opt-2.7b",
torch_dtype=torch.float16
)
# 将模型发送到 GPU 以加速推理
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
提示:通过 model.vision_model
和 model.language_model
,我们可以看到我们加载的 BLIP-2 模型分别使用了哪种 ViT(视觉 Transformer)和生成模型。
我们加载了组成完整流程的两个组件:一个处理器和一个模型。处理器可以类比为语言模型中的分词器。它将非结构化的输入(如图像和文本)转换为模型通常期望的表示形式。
5.1 预处理图像
让我们先探索处理器对图像做了什么。我们先加载一张超跑的图片,用于说明:
# 加载超跑的图片
car_path = "https://raw.githubusercontent.com/HandsOnLLM/Hands-On-Large-Language-Models/main/chapter09/images/car.png"
image = Image.open(urlopen(car_path)).convert("RGB")
image
这张图片的尺寸是 520×492 像素,这通常是一个不常见的格式。那么让我们看看我们的处理器会对它做什么:
# 预处理图像
inputs = blip_processor(image, return_tensors="pt").to(device, torch.float16)
inputs["pixel_values"].shape
>> 这给出了以下形状:
torch.Size([1, 3, 224, 224])
结果是一个 224×224 大小的图像。这比我们最初拥有的图像小得多!这也意味着所有原始不同形状的图像都将被处理成正方形。因此,输入非常宽或高的图像时要小心,因为它们可能会被扭曲。
5.2 预处理文本
让我们继续探索处理器对文本的处理。首先,我们可以访问用于分词输入文本的分词器:
blip_processor.tokenizer
这给出了以下输出:
GPT2TokenizerFast(name_or_path='Salesforce/blip2-opt-2.7b', vocab_size=50265, model_max_length=1000000000000000019884624838656, is_fast=True, padding_side='right', truncation_side='right', special_tokens={'bos_token': '</s>', 'eos_token': '</s>', 'unk_token': '</s>', 'pad_token': '<pad>'}, clean_up_tokenization_spaces=True), added_tokens_decoder={
1: AddedToken("<pad>", rstrip=False, lstrip=False, single_word=False, normalized=True, special=True),
2: AddedToken("</s>", rstrip=False, lstrip=False, single_word=False, normalized=True, special=True),
}
BLIP-2 模型在这里使用了 GPT2Tokenizer。正如我们在第 2 章中探讨的那样,分词器处理输入文本的方式可能会有很大差异。
为了探索 GPT2Tokenizer 的工作方式,我们可以用一个简短的句子来尝试。我们先将句子转换为 token ID,然后再将它们转换回 token:
# 预处理文本
text = "Her vocalization was remarkably melodic"
token_ids = blip_processor(image, text=text, return_tensors="pt")
token_ids = token_ids.to(device, torch.float16)["input_ids"][0]
# 将 input ids 转换回 token
tokens = blip_processor.tokenizer.convert_ids_to_tokens(token_ids)
>> 输出tokens为
['</s>', 'Her', 'Ġvocal', 'ization', 'Ġwas', 'Ġremarkably', 'Ġmel', 'odic']
当我们检查这些 token 时,你可能会注意到一些 token 开头有一个奇怪的符号,即 Ġ 符号。这实际上本应是一个空格。然而,一个内部函数会将某些代码点的字符向上移动 256 位,使其可打印。因此,空格(代码点 32)变成了 Ġ(代码点 288)。
我们将它们替换为下划线以便于说明:
# 将空格 token 替换为下划线
tokens = [token.replace("Ġ", "_") for token in tokens]
tokens
>> 这给出了一个更美观的输出:
['</s>', 'Her', '_vocal', 'ization', '_was', '_remarkably', '_mel', 'odic']
输出表明,下划线表示单词的开头。这样,由多个 token 组成的单词就可以被识别出来。
5.3 用例 1:图像字幕生成
像 BLIP-2 这样的模型最直接的用途是为你的数据中的图像生成字幕。你可能是一家商店,想要为其服装生成描述,或者你可能是一位摄影师,没有时间手动标记 1000 多张婚礼照片。
字幕生成的过程与处理过程密切相关。图像被转换为模型可以读取的像素值。这些像素值被传递给 BLIP-2,以转换为 LLM 可以用来决定合适字幕的软视觉提示。
让我们以超跑的图像为例,使用处理器将其转换为预期形状的像素:
# 加载超跑的 AI 生成图像
image = Image.open(urlopen(car_path)).convert("RGB")
# 将图像转换为输入并进行预处理
inputs = blip_processor(image, return_tensors="pt").to(device, torch.float16)
image
下一步是使用 BLIP-2 模型将图像转换为 token ID。之后,我们可以将 ID 转换为文本(生成的字幕):
# 生成要传递给解码器(LLM)的图像 ID
generated_ids = model.generate(**inputs, max_new_tokens=20)
# 从图像 ID 生成文本
generated_text = blip_processor.batch_decode(
generated_ids, skip_special_tokens=True
)
generated_text = generated_text[0].strip()
generated_text
generated_text
包含字幕:
an orange supercar driving on the road at sunset
这似乎是这张图像的完美描述!
图像字幕生成是学习这个模型的一个很好的方式,然后再进入更复杂的用例。你可以自己尝试几张图像,看看它在哪些方面表现良好,在哪些方面表现不佳。特定领域的图像,如特定卡通角色或虚构创作的图片,可能会失败,因为该模型主要在公共数据上进行训练。
让我们以一个有趣的例子结束这个用例,即罗夏克测试(Rorschach test)的图像,如下图所示。这是旧心理实验的一部分,用于测试个体对墨迹的感知。某人在这种墨迹中看到的内容据说可以揭示一个人的性格特征。这是一种相当主观的测试,但正因为如此,它才更有趣!

让我们以上图图像作为输入:
# 加载罗夏克图像
url = "https://upload.wikimedia.org/wikipedia/commons/7/70/Rorschach_blot_01.jpg"
image = Image.open(urlopen(url)).convert("RGB")
# 生成字幕
inputs = blip_processor(image, return_tensors="pt").to(device, torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=20)
generated_text = blip_processor.batch_decode(
generated_ids, skip_special_tokens=True
)
generated_text = generated_text[0].strip()
generated_text
像之前一样,当我们检查 generated_text
变量时,我们可以看到字幕:
a black and white ink drawing of a bat
我完全可以理解模型会用这样的描述来为这张图像生成字幕。由于这是罗夏克测试,你认为它对模型来说意味着什么?
5.4 用例 2:多模态聊天式提示
尽管字幕生成是一个重要的任务,但我们可以将其用例扩展得更远。在前面的例子中,我们展示了从一种模态(视觉,图像)到另一种模态(文本,字幕)的转换。
与其遵循这种线性结构,我们可以通过执行所谓的视觉问答来同时呈现两种模态。在这个特定的用例中,我们给模型提供了一张图像以及一个关于该特定图像的问题,让模型回答。模型需要同时处理图像和问题。
为了演示,我们先从超跑的图片开始,让 BLIP-2 描述这张图片。为此,我们首先需要像之前几次那样预处理图像:
# 加载超跑的 AI 生成图像
image = Image.open(urlopen(car_path)).convert("RGB")
为了执行视觉问答,我们需要给 BLIP-2 提供的不仅仅是图像,还有提示。如果没有提示,模型将像之前一样生成字幕。我们将要求模型描述我们刚刚处理的图像:
# 视觉问答
prompt = "Question: Write down what you see in this picture. Answer:"
# 处理图像和提示
inputs = blip_processor(image, text=prompt, return_tensors="pt").to(device, torch.float16)
# 生成文本
generated_ids = model.generate(**inputs, max_new_tokens=30)
generated_text = blip_processor.batch_decode(
generated_ids, skip_special_tokens=True
)
generated_text = generated_text[0].strip()
generated_text
>> 这给出了以下输出:
A sports car driving on the road at sunset
它正确地描述了图像。然而,这是一个相当简单的例子,因为我们的问题本质上是要求模型生成一个字幕。相反,我们可以以聊天的方式提出后续问题。
为此,我们可以给模型我们之前的对话,包括它对问题的回答。然后我们再问一个后续问题:
# 聊天式提示
prompt = "Question: Write down what you see in this picture. Answer: A sports car driving on the road at sunset. Question: What would it cost me to drive that car? Answer:"
# 生成输出
inputs = blip_processor(image, text=prompt, return_tensors="pt").to(device, torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=30)
generated_text = blip_processor.batch_decode(
generated_ids, skip_special_tokens=True
)
generated_text = generated_text[0].strip()
generated_text
>> 这给出了以下回答:
$1,000,000
100 万美元相当具体!这表明 BLIP-2 更具聊天式行为,允许进行一些有趣的对话。
最后,我们可以通过创建一个交互式聊天机器人来使这个过程更顺畅,使用 ipywidgets
,这是 Jupyter 笔记本的一个扩展,允许我们创建交互式的按钮、输入文本等:
from IPython.display import HTML, display
import ipywidgets as widgets
def text_eventhandler(*args):
question = args[0]["new"]
if question:
args[0]["owner"].value = ""
# 创建提示
if not memory:
prompt = " Question: " + question + " Answer:"
else:
template = "Question: {} Answer: {}."
prompt = " ".join(
[
template.format(memory[i][0], memory[i][1])
for i in range(len(memory))
]
) + " Question: " + question + " Answer:"
# 生成文本
inputs = blip_processor(image, text=prompt, return_tensors="pt")
inputs = inputs.to(device, torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=100)
generated_text = blip_processor.batch_decode(
generated_ids,
skip_special_tokens=True
)
generated_text = generated_text[0].strip().split("Question")[0]
# 更新记忆
memory.append((question, generated_text))
# 赋值到输出
output.append_display_data(HTML("<b>USER:</b> " + question))
output.append_display_data(HTML("<b>BLIP-2:</b> " + generated_text))
output.append_display_data(HTML("<br>"))
# 准备小部件
in_text = widgets.Text()
in_text.continuous_update = False
in_text.observe(text_eventhandler, "value")
output = widgets.Output()
memory = []
# 显示聊天框
display(
widgets.VBox(
children=[output, in_text],
layout=widgets.Layout(display="inline-flex", flex_flow="column-reverse"),
)
)
似乎我们可以继续对话并提出许多问题。通过这种聊天式的方法,我们本质上创建了一个可以对图像进行推理的聊天机器人!
六、总结
在本章中,我们探索了多种方法,通过弥合文本和视觉表示之间的差距,使大型语言模型(LLMs)具备多模态能力。我们首先讨论了用于视觉的 Transformer,这些模型可以将图像转换为数值表示。这是通过使用图像编码器和 patch embedding 实现的,它们使得模型能够在不同尺度上处理图像。
接着,我们探讨了如何通过 CLIP 创建嵌入模型,将图像和文本都转换为数值表示。我们看到 CLIP 如何通过对比学习将图像和文本嵌入对齐到一个共享空间中,从而实现零样本分类、聚类和搜索等任务。本章还介绍了 OpenCLIP,这是 CLIP 的一个开源版本,非常适合用于多模态嵌入任务。
最后,我们探索了如何使文本生成模型具备多模态能力,并深入研究了 BLIP-2 模型。这些多模态文本生成模型的核心思想是将输入图像的视觉特征投影到文本嵌入中,以便 LLMs 使用。我们看到了这个模型如何用于图像字幕生成和多模态聊天式提示,其中两种模态被结合起来生成响应。总体而言,本章突出了多模态在 LLMs 中的强大能力,并展示了其在图像字幕生成、搜索和聊天式提示等领域的应用。
在本书的第三部分,我们将涵盖训练和微调技术。在第 10 章中,我们将探索如何创建和微调一个文本嵌入模型,这是推动许多语言建模应用的核心技术。这一章将作为训练和微调语言模型的入门。