Press "Enter" to skip to content

使用 LLaVA 创建您的 Vision 聊天助手

使用开源的LLaVA模型来开始使用多模态对话模型。

Izabela Kraus在Unsplash上的照片

介绍

大型语言模型证明了其自身具有革命性的技术。已经开发出了许多利用它们能力的应用程序,而更多的应用即将推出。大型语言模型最有趣的应用之一是将其部署为智能助手,能够帮助人类用户完成各种任务。使用指令调整和从人类反馈中进行强化学习(RLHF)训练的聊天模型已经显示出非常有前途的能力,可以遵循人类指令并执行分配的任务。然而,它们在仅限于语言任务上的适用性是有限的。

多模态对话模型旨在释放大型语言模型的能力,以解决需要结合自然语言和其他模态才能解决的问题。尤其是自从向GPT-4V引入视觉能力以来,视觉语言模型受到越来越多的关注。通过在GPT-4中增加图像理解能力,增强了GPT-4的自然语言能力,产生了一个强大的聊天助手,可以帮助用户完成既需要视觉又需要语言理解的任务。虽然GPT-4V的视觉能力令人印象深刻,但闭源模型限制了对这一令人惊叹的技术进行研究和实验的潜力。幸运的是,出现了一些开源模型,以方便获取和透明的方式为社区带来了视觉语言模型的能力。这些模型也延续了开源大型语言模型对计算和内存效率的增强趋势,这是一个重要的特性,因为它促进了这些模型的广泛采用。

在本教程中,我将介绍使用“大型语言和视觉助手”(LLaVA)模型创建图像聊天助手的过程,该模型介绍在Visual Instruction Tuning论文中。我首先简要介绍LLaVA模型及其改进,然后讨论使用官方存储库提供的代码实现图像聊天助手的简单代码。然后,我将展示一些我制作的示例,展示该模型的能力和局限性。

LLaVA

LLaVA模型首先在Visual Instruction Tuning论文中介绍,然后在Improved Baselines with Visual Instruction Tuning(也称为LLaVA-1.5)中进一步改进。它的思想是从图像中提取视觉嵌入,并像对待来自语言标记的嵌入一样,在大型语言模型中进行处理。直观地说,我们可以将图像描述为语言模型用来生成其答案的“单词”。为了选择正确的“单词”,该模型使用预训练的CLIP视觉编码器提取视觉嵌入,然后将它们投影到语言模型的词嵌入空间中。后者的操作是通过视觉语言连接器完成的,该连接器在第一篇Visual Instruction Tuning论文中最初选择为简单的线性层,然后在Improved Baselines with Visual Instruction中被更具表现力的多层感知器(MLP)所取代。该模型的架构如下所示。

LLaVA模型的架构。在LLaVA中,投影W是一个简单的线性层,而在LLaVA-1.5中是一个多层感知机(MLP)。图片来自论文《Visual Instruction Tuning》。

该方法的一个优点是,通过使用预训练的视觉编码器和预训练的语言模型,只需要从头开始学习轻量级的视觉-语言连接器模块。具体而言,LLaVA的训练分为两个阶段:

  • 特征对齐的预训练:预训练的视觉编码器和语言模型都被冻结,只有视觉-语言连接器的权重被更新。所有的训练样本都是由文本-图像对组成的单轮对话。这一阶段的目的是训练视觉-语言连接器将视觉编码器的嵌入与语言模型的文本嵌入对齐。
  • 使用视觉指令进行微调:在这一阶段,只有视觉编码器的权重被冻结,而视觉-语言连接器和语言模型一起进行微调。模型通过基于图像的指令跟随任务进行微调。值得注意的是,其中一些数据是通过使用仅使用语言的GPT4从图像的标题和实体边界框的坐标创建指令跟随样本而生成的。

视觉聊天机器人实现

使用提供在官方存储库中提供的代码创建视觉聊天机器人非常简单。该存储库还提供了可以用于解析输入的标准化聊天模板。遵循训练中使用的正确格式对于模型生成的答案质量至关重要。具体的模板取决于所使用的语言模型。使用预训练的Vicuna语言模型的LLaVA-1.5的模板如下:

一个好奇用户和一个人工智能助手之间的对话。助手回答用户问题时提供了有用、详细和礼貌的答案。用户:<im_start><image><im_end> 用户的提示助手:助手的回答用户:另一个提示

前几行是模型使用的一般系统提示。特殊词元<im_start>、<image>和<im_end>用于指示将表示图像的嵌入放置的位置。

聊天机器人可以通过一个简单的Python类来定义。

class LLaVAChatBot:    def __init__(self,                 model_path: str = 'liuhaotian/llava-v1.5-7b',                 device_map: str = 'auto',                 load_in_8_bit: bool = True,                 **quant_kwargs) -> None:        self.model = None        self.tokenizer = None        self.image_processor = None        self.conv = None        self.conv_img = None        self.img_tensor = None        self.roles = None        self.stop_key = None        self.load_models(model_path,                         device_map=device_map,                         load_in_8_bit=load_in_8_bit,                         **quant_kwargs)    def load_models(self, model_path: str,                    device_map: str,                    load_in_8_bit: bool,                    **quant_kwargs) -> None:        """加载模型、处理器和分词器。"""        quant_cfg = BitsAndBytesConfig(**quant_kwargs)        self.model = LlavaLlamaForCausalLM.from_pretrained(model_path,                                                           low_cpu_mem_usage=True,                                                           device_map=device_map,                                                           load_in_8bit=load_in_8_bit,                                                           quantization_config=quant_cfg)        self.tokenizer = AutoTokenizer.from_pretrained(model_path,                                                       use_fast=False)        vision_tower = self.model.get_vision_tower()        vision_tower.load_model()        vision_tower.to(device='cuda')        self.image_processor = vision_tower.image_processor        disable_torch_init()    def setup_image(self, img_path: str) -> None:        """加载并处理图像。"""        if img_path.startswith('http') or img_path.startswith('https'):            response = requests.get(img_path)            self.conv_img = Image.open(BytesIO(response.content)).convert('RGB')        else:            self.conv_img = Image.open(img_path).convert('RGB')        self.img_tensor = self.image_processor.preprocess(self.conv_img,                                                          return_tensors='pt'                                                          )['pixel_values'].half().cuda()    def generate_answer(self, **kwargs) -> str:        """从当前对话生成一个答案。"""        raw_prompt = self.conv.get_prompt()        input_ids = tokenizer_image_token(raw_prompt,                                          self.tokenizer,                                          IMAGE_TOKEN_INDEX,                                          return_tensors='pt').unsqueeze(0).cuda()        stopping = KeywordsStoppingCriteria([self.stop_key],                                            self.tokenizer,                                            input_ids)        with torch.inference_mode():            output_ids = self.model.generate(input_ids,                                             images=self.img_tensor,                                             stopping_criteria=[stopping],                                             **kwargs)        outputs = self.tokenizer.decode(            output_ids[0, input_ids.shape[1]:]        ).strip()        self.conv.messages[-1][-1] = outputs        return outputs.rsplit('</s>', 1)[0]    def get_conv_text(self) -> str:        """返回完整对话文本。"""        return self.conv.get_prompt()    def start_new_chat(self,                       img_path: str,                       prompt: str,                       do_sample=True,                       temperature=0.2,                       max_new_tokens=1024,                       use_cache=True,                       **kwargs) -> str:        """以新图像开始新的对话。"""        conv_mode = "v1"        self.setup_image(img_path)        self.conv = conv_templates[conv_mode].copy()        self.roles = self.conv.roles        first_input = (DEFAULT_IM_START_TOKEN + DEFAULT_IMAGE_TOKEN +                       DEFAULT_IM_END_TOKEN + '\n' + prompt)  # f"{self.roles[0]}: {prompt}")        self.conv.append_message(self.roles[0], first_input)        self.conv.append_message(self.roles[1], None)        if self.conv.sep_style == SeparatorStyle.TWO:            self.stop_key = self.conv.sep2        else:            self.stop_key = self.conv.sep        answer = self.generate_answer(do_sample=do_sample,                                      temperature=temperature,                                      max_new_tokens=max_new_tokens,                                      use_cache=use_cache,                                      **kwargs)        return answer    def continue_chat(self,                      prompt: str,                      do_sample=True,                      temperature=0.2,                      max_new_tokens=1024,                      use_cache=True,                      **kwargs) -> str:        """继续现有的对话。"""        if self.conv is None:            raise RuntimeError("找不到现有的对话。请使用`start_new_chat`方法开始一个新的对话。")        self.conv.append_message(self.roles[0], prompt)        self.conv.append_message(self.roles[1], None)        answer = self.generate_answer(do_sample=do_sample,                                      temperature=temperature,                                      max_new_tokens=max_new_tokens,                                      use_cache=use_cache,                                      **kwargs)        return answer

如果你熟悉transformers库,你会发现许多常见功能,并且所执行的操作应该很容易理解。让我们快速了解上面定义的LLaVAChatBot类的方法。

  • load_models:此方法加载语言模型、分词器和图像处理器,使用BitsAndBytes库指定的参数进行量化。此代码与Hugging Face transformers模型使用的from_pretrained方法相似。BitsAndBytes允许将模型量化为8位或4位,以减少GPU内存要求。
  • setup_image:它从本地路径或URL加载图像,并使用图像处理器将其转换为张量。
  • generate_answer:此方法返回模型对所提供图像的当前对话的回答。LLaVa模型的generate方法与Hugging Face transformers模型的generate方法类似。
  • get_conv_text:此方法返回到目前为止对话的原始文字。
  • start_new_chat:这是聊天机器人的两个主要方法之一,用于与模型开始新的聊天。它通过传递图像和初始提示来创建新的对话。它使用存储库中定义的模板设置对话,模板格式在前一节中讨论。
  • continue_chat:另一个主要方法,用于继续关于图像的现有对话。

我在此Colab笔记本中提供了完整的代码,以便你可以自己尝试。

示例

在本节中,我收集了一些在测试模型功能时获得的有趣示例。本文中的所有示例都是使用具有8位量化的llava-v1.5–7b模型创建的。

首先,让我们看看模型描述和理解图像的能力。下面我要求模型描述一张描绘白虎的图片。

Photo by Joshua J. Cotten on Unsplash
一个好奇的用户与一个人工智能助手的对话。助手以有帮助、详细和礼貌的方式回答用户的问题。用户:请详细描述图片。助手:图像中有一只白虎躺在一块岩石表面上,可能是石墙或岩石。虎似乎在自己的环境中感到轻松和舒适。虎的位置覆盖了整个图片的相当大部分,从左边到几乎整个右边。岩石表面为场景提供了自然而崎岖的背景。

模型似乎能够详细描述图像,并能够推理图像的主题,准确评估白虎是一种罕见现象。

接下来,我测试了模型是否能够为用户提供执行各种任务的可行指导。这是将“字幕”模型与真正的视觉聊天助手区分开来的特点。例如,模型能否根据图像中存在的成分提供食谱?结果如下所示。

Photo by ThermoPro on Unsplash

一个好奇的用户和一个人工智能助手之间的对话。助手对用户的问题给出了有用、详细和礼貌的回答。用户: <im_start><image><im_end>我能用这些食材做什么菜?助手: 根据桌子上展示的食材,你可以做一道美味的烤晚餐。大块的牛肉可以通过调味,在烤箱中烹饪,而胡萝卜、土豆和洋葱等蔬菜可以与肉一起烤。你还可以用土豆、胡萝卜和洋葱做一道配菜,与烤晚餐一起食用。一碗酱料的存在表明你可以用它作为蘸酱或者增强烤肉和蔬菜的味道。总的来说,这些食材的组合可以提供一顿丰盛美味的餐食,与家人和朋友一起享用。

用户: 你建议的食谱可以包括在素食饮食中吗?

助手: 我建议的食谱,其中包括大块的牛肉和蔬菜,不适合素食饮食。牛肉是这道菜中的主要蛋白质来源,不适合避免食用肉类的素食者。但你仍然可以使用桌子上的蔬菜和酱料制作一道适合素食者的餐食。例如,你可以用胡萝卜、土豆和洋葱做一道蔬菜烤,然后搭配土豆泥或者素食酱料。这样,你可以享受到菜肴的味道和口感,而不用加入肉类。

用户: 你能推荐一个只需对展示的食材进行最小修改的素食食谱吗?

助手: 当然可以!这里有一个可以使用桌子上的食材制作的素食食谱:烤蔬菜拼盘配酱料。

食材:
– 胡萝卜
– 土豆
– 洋葱
– 橄榄油
– 盐
– 胡椒
– 香草(例如百里香、迷迭香或欧芹)
– 酱料(可选)

做法:
1. 预热烤箱至400°F(200°C)。
2. 清洗并去皮胡萝卜、土豆和洋葱,切成一口大小的块状。
3. 在一个大碗中,将蔬菜与橄榄油、盐和胡椒混合均匀。
4. 将蔬菜均匀地摆放在覆盖了烘焙纸的烤盘上。
5. 将蔬菜放入预热好的烤箱中烤20-25分钟,直到它们变得柔软并略微变褐色。
6. 如有需要,可以通过将番茄、大蒜和香草等食材混合制作酱料。这个酱料可以用作蘸酱或者增强烤蔬菜的味道。
7. 将烤蔬菜拼盘与土豆泥或者素食酱料一起食用。

这个素食食谱可以让你享受到烤蔬菜的味道和口感,而不含肉类。酱料可以根据个人口味自由调整,这道菜可以与各种配菜一起食用,例如土豆泥或素食酱料。

模型似乎对图片中的食材有很好的理解,也能将单词联系到更抽象的概念上(例如有肉的餐食不适合素食)。

这些建议看起来中肯,虽然有点普遍,但能找出可以改变的当前元素,以赋予更具复古风格。

让我们继续介绍一些实用的东西,可以帮助用户日常活动。这个模型可以帮助撰写广告或列表吗?

Eugene Chystiakov在Unsplash上的照片
一个好奇的用户和人工智能助手之间的聊天。助手对用户的问题给出了有用、详细和礼貌的回答。用户: <im_start><image><im_end>为这把椅子写一份广告吧。助手: 介绍一款现代时尚的黑色木制椅子,完美适用于任何现代环境。该椅子采用时尚的黑色座椅和木制底座,与任何背景形成鲜明对比。其极简主义设计使其成为任何房间的多功能补充,无论是用作餐椅、边桌还是客厅中的亮点。椅子坚固耐用,可经受日常使用,其简洁线条和时尚外观为任何房间增添了一丝优雅。立即用这款时尚实用的椅子升级您的生活空间。</s></s>用户: 现在为同一把椅子写一份面向咖啡馆的广告。助手: 注意咖啡馆老板和设计爱好者!推出黑色和木质椅子,为您的咖啡厅座位增添一抹时尚。这款时尚实用的椅子与任何背景形成鲜明对比,其极简主义设计使其成为任何咖啡厅环境的多功能选择,无论是用作餐椅、边桌还是休息区的亮点。椅子坚固耐用,可经受日常使用,其简洁线条和时尚外观为任何房间增添一丝优雅。立即用这款时尚实用的椅子升级您的咖啡厅氛围。</s></s>

该模型能够准确指出椅子的特点,并创建出有吸引力的广告,根据需要正确定制答案。

最后一个测试是看看已经证明对大型语言模型有益的提示技巧是否也适用于LLaVA。下面是尝试零-shot连续思考提示的示例。

Arnold Middelkoop在Unsplash上的照片
一个好奇的用户和人工智能助手之间的聊天。助手对用户的问题给出了有用、详细和礼貌的回答。用户: <im_start><image><im_end>如果把图中所示的鱼扔到水中,它会浮起来吗?请逐步思考。助手: 图中所示的鱼是一尊大型白色的鱼雕塑,放置在水域附近的红砖人行道上。如果将其扔入水中,这尊雕塑不会浮起来,因为它是一个固体的、不活动的物体。由于其构造和尺寸的特点,它会沉入水底。鱼的放置在地面上以及它的缺乏浮力使得它无法在水中浮起。</s></s>用户: 用什么材料可以制作一个漂浮在水上的鱼雕塑?助手: 要制作一个可以漂浮在水上的鱼雕塑,可以使用轻便浮力材料,如聚苯乙烯发泡板或聚乙烯泡沫。这些材料常用于制作救生衣、浮力装置和渔漂等浮力物体。通过塑造泡沫为所需的鱼形状,它就可以漂浮在水面上,模拟真实鱼的行为。</s></s>

该模型能够解释为什么这条鱼不会浮起来,并正确识别它不是真正的鱼。它还遵循了在给出最终答案之前逐步思考的指导,为对比,下面是没有“请逐步思考”提示的对话示例。

一个好奇的用户和一个人工智能助手之间的对话。助手对用户的问题给予了有帮助、详细和礼貌的回答。

用户: <im_start><image><im_end>如果把图像中所示的鱼扔进水里,它会浮起来吗?
助手: 不,如果将图像中所示的鱼扔进水里,它不会浮起来。它是一个大的、白色的、石头鱼雕塑,不是真正的鱼,没有天然鱼类的浮力特性。

尽管最后的答案仍然是正确的,但这种情况下给出的解释要少得多,答案是句子开头给出的。由于LLaVA使用的生成式语言模型是因果关系的,这意味着所提供的答案不依赖于后续的解释。有趣的是,进行更多的实验来测试Chain of Thought(CoT)是否能够一般地提高视觉语言模型在类似于Large Language Models中观察到的复杂推理任务中的性能。

限制

尽管非常强大,LLaVA也有其限制。例如,该模型在训练中只使用一个图像来进行每次的对话,因此无法处理需要与多个图像进行交互的更复杂的对话。然而,需要注意的是,这种限制不是模型的固有特性,可以通过在训练期间包含多图像对话数据来加以改善。

在测试过程中,我注意到该模型(至少是1.5 7b模型的8位量化版本)在计算图像中的元素数量和处理数字任务方面存在困难。以下是一个例子。

一个好奇的用户和一个人工智能助手之间的对话。助手对用户的问题给予了有帮助、详细和礼貌的回答。
用户: <im_start><image><im_end>停车场内有多少辆汽车?
助手: 停车场内停放了几辆汽车。

用户: 列出由汽车占用的停车位号码
助手: 停车位号码为66、67、68、69、70和71的停车位被汽车占用。

该模型,像所有的大型语言模型一样,也容易产生幻觉,并且可以很容易地通过适当的提示被欺骗而犯错误。例如,如果提供一张沙漠的图片,然后问图片中的鱼是什么物种,它就会产生沙漠鱼的幻觉。

一个好奇的用户和一个人工智能助手之间的对话。助手对用户的问题给予了有帮助、详细和礼貌的回答。
用户: <im_start><image><im_end>告诉我这张图片中鱼的物种是什么。
助手: 图片中的鱼是一种沙漠鱼,它是一种适应生活在沙漠环境中的物种。

结论

LLaVA在图像语言理解方面显示出令人印象深刻的能力。它代表了多模态开源图像语言模型的明显进步。LLaVA的最大优点之一是它的训练和微调过程轻量级。例如,LLaVA 1.5 13b的完整训练只需要120万条数据,大约在一个8-A100节点上用了1天时间。这使得它适合在特定领域进行微调,以获得一位专家助手,就像在LLaVA-Med:在一天内为生物医学训练一个大型语言与视觉助手中所做的那样。

为聊天助手添加图像功能扩展了这些模型的应用领域,将其革命潜力带到更复杂、更细微的任务中。将图像特征视为语言标记也扩展了与仅文本语言模型一起使用的所有高级提示技术的可能性。例如,通过检索与对话相关的文本和图像,可以扩展“检索增强生成”的功能。实际上,使用CLIP共享的图像-文本嵌入空间,可以从输入的文本或图片开始检索外部文档和外部图像!

扩展模型能力的另一个有趣方向在LLaVA-Interactive: 用于图像交流、分割、生成和编辑的一体化演示中介绍。其主要思想是结合图像语言聊天模型、文本到图像生成模型和其他视觉模型(如图像分割模型),以获得能够处理多模态输入和生成多模态输出的助手。

总之,LLaVA标志着开源多模态生成模型的重要一步,这些模型展示出令人印象深刻的能力,吸引了很多关注。随着开源模型的更广泛采用,我相信我们很快将见证这些强大模型在新应用领域的迅速增长。

感谢您的阅读!如果您想自己尝试代码,可以查看这个Colab笔记本

Leave a Reply

Your email address will not be published. Required fields are marked *