构建和玩耍!配备LLM的您自己的V&L模型!

构建和玩耍!

开发LLM集成GIT视觉语言模型。

本文摘要:

  • 解释了由Microsoft开发的视觉语言模型GIT。
  • 使用PyTorch和Hugging Face的Transformers将GIT的语言模型替换为大型语言模型(LLMs)。
  • 介绍了如何使用LoRA对GIT-LLM模型进行微调。
  • 测试和讨论开发的模型。
  • 调查GIT的图像编码器嵌入的“图像嵌入”是否与“文本嵌入”在同一空间中指示特定字符。

大型语言模型(LLM)正在显示出越来越多的价值。将图像纳入LLMs使它们作为视觉语言模型更加有用。在本文中,我将解释一个名为GIT-LLM的模型的开发,它是一个简单但功能强大的视觉语言模型。有些部分,如代码解释,可能会感觉有点乏味,所以可以直接跳到结果部分。我进行了各种实验和分析,所以我想您会喜欢看到我能够取得的成果。

该实现已公开提供,请随意尝试。

GitHub – turingmotors/heron

通过在GitHub上创建一个帐户为turingmotors/heron做出贡献。

github.com

将GIT转换为LLM

让我们深入探讨这个技术博客的主题。

什么是GIT?

Generative Image-to-text Transformer(GIT)是由Microsoft提出的一种视觉语言模型。

arXiv: https://arxiv.org/abs/2205.14100Code: https://github.com/microsoft/GenerativeImage2Text

它的架构非常简单。它将从图像编码器提取的特征向量转换为可以像文本一样处理的向量,使用投影模块。然后,将这些向量输入到语言模型中,以为图像生成标题或执行问答。该模型可以以类似的方式处理视频。

这张图片引用自“GIT: A Generative Image-to-text Transformer for Vision and Language”

尽管它很简单,但如果您查看“Paper with code”上的排行榜,您会发现它在许多任务中排名很高。

https://paperswithcode.com/paper/git-a-generative-image-to-text-transformer

最初,GIT使用强大的模型(如CLIP)作为其图像编码器,并从头开始训练语言模型部分。然而,在本文中,我尝试使用强大的LLM并进行微调。在这里,我将该模型称为“GIT-LLM”。

使用Hugging Face的Transformers进行LLM

我将使用Hugging Face的Transformers库来开发GIT-LLM。Transformers是一个用于处理机器学习模型的Python库。它提供了许多最先进的预训练模型,您可以立即进行推理。它还提供了训练和微调模型的工具。我相信Transformers对最近LLM衍生模型的发展做出了重要贡献。几乎所有可用的LLMs都可以使用Transformers处理,并且许多从它们派生的多模态模型在开发和微调中使用Transformers作为基础。

这是使用Transformers模型的最简单代码。您可以通过使用AutoModel和AutoTokenizer轻松尝试LLMs。

from transformers import AutoModelForCausalLM, AutoTokenizermodel_name = "facebook/opt-350m"model = AutoModelForCausalLM.from_pretrained(model_name).to("cuda")tokenizer = AutoTokenizer.from_pretrained(model_name)prompt = "Hello, I'm am conscious and"input_ids = tokenizer(prompt, return_tensors="pt").to("cuda")sample = model.generate(**input_ids, max_length=64)print(tokenizer.decode(sample[0]))# Hello, I'm am conscious and I'm a bit of a noob. I'm looking for a good place to start.

让我们来检查一下OPT模型保存的参数。打印由AutoModelForCausalLM创建的模型。

OPTForCausalLM(  (model): OPTModel(    (decoder): OPTDecoder(      (embed_tokens): Embedding(50272, 512, padding_idx=1)      (embed_positions): OPTLearnedPositionalEmbedding(2050, 1024)      (project_out): Linear(in_features=1024, out_features=512, bias=False)      (project_in): Linear(in_features=512, out_features=1024, bias=False)      (layers): ModuleList(        (0-23): 24 x OPTDecoderLayer(          (self_attn): OPTAttention(            (k_proj): Linear(in_features=1024, out_features=1024, bias=True)            (v_proj): Linear(in_features=1024, out_features=1024, bias=True)            (q_proj): Linear(in_features=1024, out_features=1024, bias=True)            (out_proj): Linear(in_features=1024, out_features=1024, bias=True)          )          (activation_fn): ReLU()          (self_attn_layer_norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)          (fc1): Linear(in_features=1024, out_features=4096, bias=True)          (fc2): Linear(in_features=4096, out_features=1024, bias=True)          (final_layer_norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)        )      )    )  )  (lm_head): Linear(in_features=512, out_features=50272, bias=False))

这很简单。初始embed_tokens的输入维度和最终lm_head的输出维度都是50,272,表示训练该模型时使用的标记数量。让我们验证分词器的词汇表大小:

print(tokenizer.vocab_size)# 50265

包括bos_token、eos_token、unk_token、sep_token、pad_token、cls_token和mask_token等特殊标记,它预测了来自50,272种标记类型的下一个词的概率。

通过查看实现,您可以了解这些模型是如何连接的。一个简单的图表可以表示如下的流程:

OPT的简化模型架构(作者制作的图片)

结构和数据流非常简单。不同语言模型之间,〇〇Model和〇〇ForCausalLM具有类似的框架。〇〇Model类主要代表语言模型的“Transformer”部分。例如,如果您想执行文本分类等任务,只需使用这部分。〇〇ForCausalLM类用于文本生成,在使用Transformer处理向量后,将这些向量应用于一个标记计数的分类器。损失的计算也在这个类的forward方法中完成。embed_positions表示位置编码,它被添加到project_in中。

使用GIT和Transformers

我将根据GIT的官方文档页面尝试一下。由于我还将处理图像,所以我将使用一个同时包含Tokenizer的Processor。

from PIL import Imageimport requestsfrom transformers import AutoProcessor, AutoModelForCausalLMmodel_name = "microsoft/git-base-coco"model = AutoModelForCausalLM.from_pretrained(model_name)processor = AutoProcessor.from_pretrained(model_name)# 下载和预处理图像url = "http://images.cocodataset.org/val2017/000000039769.jpg"image = Image.open(requests.get(url, stream=True).raw)pixel_values = processor(images=image, return_tensors="pt").pixel_values# 预处理文本prompt = "这是什么?"inputs = processor(            prompt,            image,            return_tensors="pt",            max_length=64        )sample = model.generate(**inputs, max_length=64)print(processor.tokenizer.decode(sample[0]))# 两只猫在沙发上睡觉

鉴于输入图像产生了输出“两只猫在沙发上睡觉”,看起来工作得很好。

让我们也来看一下模型的结构:

GitForCausalLM(  (git): GitModel(    (embeddings): GitEmbeddings(      (word_embeddings): Embedding(30522, 768, padding_idx=0)      (position_embeddings): Embedding(1024, 768)      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)      (dropout): Dropout(p=0.1, inplace=False)    )    (image_encoder): GitVisionModel(      (vision_model): GitVisionTransformer(        ...      )    )    (encoder): GitEncoder(      (layer): ModuleList(        (0-5): 6 x GitLayer(          ...        )      )    )    (visual_projection): GitProjection(      (visual_projection): Sequential(        (0): Linear(in_features=768, out_features=768, bias=True)        (1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)      )    )  )  (output): Linear(in_features=768, out_features=30522, bias=True))

虽然有点长,但是如果你将其分解开来,其实也非常简单。在GitForCausalLM中,有一个GitModel,其中包含以下模块:

  • embeddings (GitEmbeddings)
  • image_encoder (GitVisionModel)
  • encoder (GitEncoder)
  • visual_projection (GitProjection)
  • output (Linear)

与OPT相比的主要区别在于GitVisionModel和GitProjection的存在,它们是将图像转换为类似提示的向量的确切模块。虽然语言模型使用Decoder来进行OPT,使用Encoder来进行GIT,但这只是表示注意力掩码的构造方式不同。变压器层可能有轻微的差异,但它们的功能基本相同。GIT使用Encoder的名称,因为它使用了一种独特的注意力掩码,该掩码将注意力应用于图像的所有特征,并对文本特征使用因果掩码。

观察模型的连接:

GIT的简化模型架构(图片由作者制作)

图像信息由GitVisionModel和GitProjection处理,以匹配文本的嵌入。之后,将其与文本的嵌入一起输入到语言模型的“Transformer”层中。虽然有细微的差异,但与语言模型相关的部分几乎以相同的方式进行开发。

GIT的注意力掩码

通常的语言模型和GIT语言模型的架构几乎相同,但是应用注意力掩码的方式不同。

对于语言模型,在预测未来标记时,会应用注意力掩码以不看过去的标记。这是一种称为“因果注意力”的方法,对应于下图的左侧。第一列标记仅引用自身,确保不会对后续单词应用自注意力。第二列对前两个单词应用自注意力,第三个单词及以后的单词变为0。这样的掩码使其能够有效地训练以预测下一个单词。

GIT的输入有两种类型的标记:图像标记和文本标记。由于所有图像标记同时使用,并且不用于预测下一个标记,因此因果注意力不适用。另一方面,文本标记仍然需要因果注意力。如图的右侧所示,设计了这样的掩码。对于图像信息的前三行,将应用自注意力以使用所有标记信息。从文本标记开始,向下移动一列会增加可以引用的单词数量。

因果注意力掩码和Git注意力掩码之间的差异(图片由作者制作)

让我们也来检查一下生成GIT掩码的代码。创建GIT掩码的代码片段如下所示:

import torchdef create_git_attention_mask(    tgt: torch.Tensor,    memory: torch.Tensor,) -> torch.Tensor:    num_tgt = tgt.shape[1]    num_memory = memory.shape[1]    # 应用注意力的区域为0,不应用注意力的区域为-inf    top_left = torch.zeros((num_memory, num_memory))    top_right = torch.full(        (num_memory, num_tgt),        float("-inf"),    )    bottom_left = torch.zeros(        (num_tgt, num_memory),    )    # 因果注意力掩码    bottom_right = torch.triu(torch.ones(tgt.shape[1], tgt.shape[1]), diagonal=1)    bottom_right = bottom_right.masked_fill(bottom_right == 1, float("-inf"))        # 连接掩码    left = torch.cat((top_left, bottom_left), dim=0)    right = torch.cat((top_right, bottom_right), dim=0)    # 添加多头的轴    full_attention_mask = torch.cat((left, right), dim=1)[None, None, :]    return full_attention_mask# batch_size, sequence, feature_dimvisual_feature = torch.rand(1, 3, 128)text_feature = torch.rand(1, 4, 128)mask = create_git_attention_mask(tgt=text_feature, memory=visual_feature)print(mask)"""tensor([[[[0., 0., 0., -inf, -inf, -inf, -inf],          [0., 0., 0., -inf, -inf, -inf, -inf],          [0., 0., 0., -inf, -inf, -inf, -inf],          [0., 0., 0., 0., -inf, -inf, -inf],          [0., 0., 0., 0., 0., -inf, -inf],          [0., 0., 0., 0., 0., 0., -inf],          [0., 0., 0., 0., 0., 0., 0.]]]])"""

您将掩码添加到注意力权重中。因此,自注意力发生的部分为0,而不包含在注意力中的部分为-inf。通过将此掩码提供给前向传播,只有文本部分可以进行因果关注。对于视觉语言模型来说,像这样有效地创建和使用掩码非常重要。

连接GIT和OPT

现在,让我们连接GIT和OPT。目标是创建一个如图所示的模型。

GIT-OPT的简化模型体系结构(图像由作者制作)

对于一般的实现,您可以参考modeling_git.py

最重要的部分是GitOPTModel。在其中,需要将视觉编码器连接到LLM。我将解释一些关键组件。

class GitOPTModel(OPTModel):    def __init__(self, config: OPTConfig):        super(GitOPTModel, self).__init__(config)        self.image_encoder = CLIPVisionModel.from_pretrained(config.vision_model_name)        self.visual_projection = GitProjection(config)

在__init__函数中,实例化了各种模块。super初始化了OPTModel。在GIT中,建议使用使用CLIP训练的强大图像编码器,因此我将其与使用CLIP训练的ViT兼容。GitProjection取自原始的GIT实现。

让我们来看看forward函数的内部。实现基于OPTDecoder的前向部分,添加了来自图像编码器的信息。虽然有点冗长,但我在代码中添加了注释,请按照每个步骤进行理解。

class GitOPTModel(OPTModel):    ...    def forward(        self,        input_ids: Optional[torch.Tensor] = None,        attention_mask: Optional[torch.Tensor] = None,        pixel_values: Optional[torch.Tensor] = None,    ) -> BaseModelOutputWithPooling:        seq_length = input_shape[1]        # 1. 使用ViT提取图像特征        visual_features = self.image_encoder(pixel_values).last_hidden_state        # 2. 将ViT提取的特征转换为类似提示的图像嵌入        projected_visual_features = self.visual_projection(visual_features)        # 3. 将标记向量化        inputs_embeds = self.decoder.embed_tokens(input_ids)        # 4. 获取位置编码        pos_embeds = self.embed_positions(attention_mask, 0)        # 5. 针对OPT特定的文本嵌入进行维度调整        inputs_embeds = self.decoder.project_in(inputs_embeds)        # 6. 文本嵌入 + 位置编码        embedding_output = inputs_embeds + pos_embeds        # 7. 连接图像嵌入和文本嵌入        hidden_states = torch.cat((projected_visual_features, embedding_output), dim=1)        # 8. 为文本区域创建因果关注掩码        tgt_mask = self._generate_future_mask(            seq_length, embedding_output.dtype, embedding_output.device        )        # 9. 为GIT创建注意力掩码        combined_attention_mask = self.create_attention_mask(            tgt=embedding_output,            memory=projected_visual_features,            tgt_mask=tgt_mask,            past_key_values_length=0,        )        # 10. 重复通过解码器层,这是语言模型的主要部分        for idx, decoder_layer in enumerate(self.decoder.layers):            layer_outputs = decoder_layer(                hidden_states,                attention_mask=combined_attention_mask,                output_attentions=output_attentions,                use_cache=use_cache,            )            hidden_states = layer_outputs[0]        # 11. 针对OPT特定的MLP进行维度调整        hidden_states = self.decoder.project_out(hidden_states)        # 12. 对齐输出接口        return BaseModelOutputWithPast(            last_hidden_state=hidden_states,            past_key_values=next_cache,            hidden_states=all_hidden_states,            attentions=all_self_attns,        )

虽然看起来可能有些复杂,但如果您逐步进行理解,就会发现它遵循图中所示的流程。实际代码可能会更复杂一些,但首先掌握主要过程将使理解其他部分更容易。这是伪代码,因此有关详细部分,请参考已发布的实现。

最后,让我们简要地了解一下GITOPTForCausalLM部分。

class GitOPTForCausalLM(OPTForCausalLM):    def __init__(        self,        config,    ):        super(GitOPTForCausalLM, self).__init__(config)        self.model = GitOPTModel(config)    def forward(        ...    ) -> CausalLMOutputWithPast:        outputs = self.model(            ...        )        sequence_output = outputs[0]        logits = self.lm_head(sequence_output)        loss = None        if labels is not None:            # 将下一个单词作为任务进行预测            num_image_tokens = self.image_patch_tokens            shifted_logits = logits[:, num_image_tokens:-1, :].contiguous()            labels = labels[:, 1:].contiguous()            loss_fct = CrossEntropyLoss()            loss = loss_fct(shifted_logits.view(-1, self.config.vocab_size), labels.view(-1))        return CausalLMOutputWithPast(            loss=loss,            logits=logits,            ...        )

模型内部的处理很简单。当提供了标签(即在训练过程中),损失计算也在forward中进行。在shifted_logits中,从文本标记的第一个标记到倒数第二个标记中获取了标记。然后使用将标签向右移动一个单词作为正确答案的交叉熵损失进行计算。

需要注意的是,在初始化函数中给分配GitOPTModel的变量命名为self.model。如果查看父类OPTForCausalLM的实现,会发现在super初始化期间首先将OPT放置到self.model中。如果更改此实例变量的名称,将会持有两个OPT,这可能会导致内存开销。

LoRA扩展

为了有效地微调LLM,我将使用一个称为Parameter-Efficient Fine-Tuning(PEFT)的库。由于它是由Hugging Face开发的,它可以与Transfors无缝集成。虽然PEFT中有各种方法,但这次我将使用一种常见的方法,即低秩适应(LoRA)进行一些实验。

如果模型支持PEFT,可以只使用几行代码就可以应用LoRA。

from transformers import AutoModelForCausalLMfrom peft import get_peft_config, get_peft_model, LoraConfigmodel = AutoModelForCausalLM.from_pretrained('microsoft/git-base')peft_config = LoraConfig(    task_type="CAUSAL_LM",    r=8,    lora_alpha=32,    lora_dropout=0.1,    target_modules=["v_proj"])peft_model = get_peft_model(model, peft_config)

target_modules参数指定要转换为LoRA的模块。如果以列表形式提供target_modules,则将其实现为将以每个字符串结尾的模块转换为LoRA。为简单起见,仅将LoRA应用于自注意力模块的“value”(v_proj)。

在模型中,图像编码器部分使用了ViT。请注意,如果像这样指定,ViT的自注意力部分也可能被应用于LoRA。虽然有点繁琐,但是通过指定到关键名称不重叠的部分并将其给予target_modules,可以避免这种情况。

target_modules = [f"model.image_encoder.vision_model.encoder.{i}.self_attn.v_proj" for i in range(len(model.model.decoder))]

结果模型成为PeftModelForCausalLM类的实例。它有一个名为base_model的实例变量,其中保存了转换为LoRA的原始模型。例如,我展示了将LoRA应用于ViT中的自注意力的v_proj的示例。

(self_attn): GitVisionAttention(  (k_proj): Linear(in_features=768, out_features=768, bias=True)  (v_proj): Linear(    in_features=768, out_features=768, bias=True    (lora_dropout): ModuleDict(      (default): Dropout(p=0.1, inplace=False)    )    (lora_A): ModuleDict(      (default): Linear(in_features=768, out_features=8, bias=False)    )    (lora_B): ModuleDict(      (default): Linear(in_features=8, out_features=768, bias=False)    )    (lora_embedding_A): ParameterDict()    (lora_embedding_B): ParameterDict()  )  (q_proj): Linear(in_features=768, out_features=768, bias=True)  (out_proj): Linear(in_features=768, out_features=768, bias=True))

在v_proj Linear内,您会发现添加了全连接层,如lora_A和lora_B。LoRA转换的Linear模块是一个继承自PyTorch的Linear和LoraLayer的同名Linear类。这是一个相对独特的模块,所以对于那些对细节感兴趣的人应该查看其实现。

请注意,使用PEFT创建的模型默认情况下只会保存LoRA部分。虽然有一种使用merge_and_unload方法保存的方法,但您可能希望在训练过程中保存所有正在保存的模型。重载Trainer的_save_checkpoints方法是一种方法,但为了避免麻烦,我这次通过在训练阶段仅获取PeftModel中保存的原始模型部分来处理。

model = get_peft_model(model, peft_config)model.base_model.model.lm_head = model.lm_headmodel = model.base_model.model

我相信有更有效的方法来处理这个问题,所以我仍在研究中。

使用GIT-LLM进行实验

现在让我们使用迄今为止开发的模型进行一些实验。

有关训练配置和其他设置的详细信息,请参阅已发布的实现,因为它们基本上遵循相同的方法。

数据集:M3IT

为了进行实验,我想使用一个将图像与文本配对并且易于集成的数据集。在探索Hugging face的数据集时,我发现了M3IT,这是上海人工智能实验室开发的用于指令调整的多模态数据集。指令调整是一种即使在有限的数据量下也能产生令人印象深刻结果的方法。看起来M3IT专门为指令调整重新注释了各种现有数据集。

https://huggingface.co/datasets/MMInstruction/M3IT

这个数据集很容易使用,所以我决定在以下实验中利用它。

要使用M3IT进行训练,需要创建一个自定义的PyTorch数据集。

class SupervisedDataset(Dataset):    def __init__(        self,        vision_model_name: str,        model_name: str,        loaded_dataset: datasets.GeneratorBasedBuilder,        max_length: int = 128,    ):        super(SupervisedDataset, self).__init__()        self.loaded_dataset = loaded_dataset        self.max_length = max_length        self.processor = AutoProcessor.from_pretrained("microsoft/git-base")        # 为每个模型设置相应的Processor        self.processor.image_processor = CLIPImageProcessor.from_pretrained(vision_model_name)        self.processor.tokenizer = AutoTokenizer.from_pretrained(            model_name, padding_side="right", use_fast=False        )    def __len__(self) -> int:        return len(self.loaded_dataset)    def __getitem__(self, index) -> dict:        # cf: https://huggingface.co/datasets/MMInstruction/M3IT#data-instances        row = self.loaded_dataset[index]        # 创建文本输入        text = f'##Instruction: {row["instruction"]} ##Question: {row["inputs"]} ##Answer: {row["outputs"]}'                # 加载图像        image_base64_str_list = row["image_base64_str"]  # str (base64)        img = Image.open(BytesIO(b64decode(image_base64_str_list[0])))        inputs = self.processor(            text,            img,            return_tensors="pt",            max_length=self.max_length,            padding="max_length",            truncation=True,        )        # 批量大小为1 -> 解除批量化        inputs = {k: v[0] for k, v in inputs.items()}        inputs["labels"] = inputs["input_ids"]        return inputs

在__init__函数中,image_processor和tokenizer对应于各自的模型。传递的loaded_dataset参数应来自MMInstruction/M3IT数据集。

coco_datasets = datasets.load_dataset("MMInstruction/M3IT", "coco")test_dataset = coco_datasets["test"]

对于COCO指令调整数据集,训练、验证和测试之间的拆分与原始数据集相同,分别有566,747、25,010和25,010个图像-文本对。其他数据集,如VQA或Video,也可以类似处理,使其成为一个多用途的验证数据集。

示例数据如下:

该图片引用自M3IT的数据。

该图片的标题如下:

##指令:写出对图片的简洁描述,捕捉主要组成部分、它们之间的关系以及任何值得注意的细节。##问题:##回答:一个戴着红色头盔的男人骑着一辆小摩托车在一条土路上。

对于COCO数据集,用于标题的问题部分留空。

让我们深入了解处理器的操作。它主要对图像进行规范化和文本进行标记化。还会对短于max_length的输入进行填充。处理器返回的处理后的数据是一个包含以下内容的字典:

  • input_ids:标记化文本的数组。
  • attention_mask:标记化文本的掩码(填充为0)。
  • pixel_values:归一化图像的数组,也转换为通道优先。

这些键名对应于模型的前向函数的参数,因此不应更改。最后,input_ids直接传递给名为labels的键。GitOPTForCausalLM的前向函数通过预测下一个词来计算损失,该词偏移一个标记。

实验1:确定微调位置

在GIT模型的研究论文中,解释了使用强大的视觉编码器和采用随机参数的语言模型。这次,由于最终目标是使用7B级的语言模型,将对语言模型应用预训练模型。以下模块将用于微调的检查。其中,GIT Projection作为一个初始化模块,始终包括在内。有些组合可能看起来是多余的,但是在这次试验中,它们被探索而没有太多的考虑。

设置为训练的模块被赋予梯度,而其余模块被修改为不具有梯度。

# 指定要训练的参数(全部训练会增加内存使用量)for name, p in model.model.named_parameters():    if np.any([k in name for k in keys_finetune]):        p.requires_grad = True    else:        p.requires_grad = False

用于此检查的Vision Encoder和LLM是:

  • openai/clip-vit-base-patch16
  • facebook/opt-350m

训练使用COCO数据集,持续5个epochs。

以下是每个实验期间训练的目标模块:

  • Proj:GIT Projection。随机初始化,因此始终进行训练。
  • LoRA:语言模型中自注意力的查询、键和值被应用。
  • OPT:训练所有层。
  • ViT:训练所有层。
  • Head:训练OPT的最终lm_head。

(注意:虽然LoRA可以应用于ViT,但为了避免实验过于复杂,这次没有包括。)

该图显示了训练损失。图例中的Proj,LoRA,OPT,ViT和Head是上面解释的训练模块。(图由作者制作)

如训练损失图所示,显然有些组合效果不佳。这是包括OPT在训练中的情况。尽管所有实验都在相当相似的条件下进行,但是在微调语言模型时可能需要更详细的调整,例如学习率。接下来将检查排除了包含OPT在训练中的模型的结果。

该图显示了没有完全微调结果的训练损失。图例中的Proj,LoRA,OPT,ViT和Head是上面解释的训练模块。(图由作者制作)
该图显示了验证损失。图例中的Proj、LoRA、OPT、ViT和Head是上面解释的训练模块。(图由作者制作)

无论是训练还是验证损失,使用Projection+LoRA模型都能获得最大的降低。微调最后的Head层显示出几乎相同的结果。如果还训练ViT模型,损失似乎会稍高一些,结果似乎不稳定。即使在ViT训练过程中添加LoRA,损失仍然倾向于较高。对于这些数据的微调,似乎使用一个不更新参数的预训练ViT模型会得到更稳定的结果。LoRA的有效性在各个地方得到了确认,从这个实验中可以看出,将LoRA添加到LLM中改进了训练和验证损失。

回顾一下一些测试数据的推断结果:

GIT-OPT的示例结果。图片引用自M3IT数据集,文本结果由作者的模型生成

当训练OPT本身时,结果和损失一样糟糕,使模型无法言表。此外,当训练ViT时,输出具有语义意义,但描述的是与给定图像完全不同的内容。然而,其他结果似乎在一定程度上捕捉到了图像的特征。例如,第一张图片提到了“猫”和“香蕉”,第二张图片识别到了“交通标志”。将结果与不使用LoRA进行比较,后者往往会重复使用相似的词语,但使用LoRA似乎使其稍微更加自然。训练Head层会产生有趣的输出,例如在第一张图片中使用“玩耍”而不是“吃饭”。虽然这些结果中存在一些不自然的元素,但可以推断出训练成功地捕捉到了图像特征。

实验二:比较十亿级模型

在之前的实验中,微调条件使用了一个稍小的语言模型OPT-350m。现在,打算将语言模型切换为一个7B模型。不仅仅满足于OPT,还将引入更强大的LLMs、LLaMA和MPT。

将这两个模型集成可以采用类似于OPT的方式进行。参考LlamaModel和MPTModel的前向函数,将投影的图像向量与文本标记进行组合,并将掩码从因果注意力掩码改为GIT的注意力掩码。需要注意的一点是:对于MPT,掩码不是(0,-inf),而是(False,True)。后续的过程可以类似地实现。

要使用7B级模型与OPT一起使用,只需将模型名称从facebook/opt-350m更改为facebook/opt-6.7b。

对于LLaMA,由于有了LLaMA2,这将是首选模型。要使用这个预训练模型,需要得到Meta和Hugging Face的批准。Hugging Face需要一个帐户,所以确保进行设置。批准通常在几小时内到达。之后,在执行训练的终端上登录Hugging Face。

huggingface-cli login

您可以使用在Hugging Face帐户→设置→访问令牌中创建的令牌登录。

训练参数保持一致,使用COCO数据集,持续3个时期。基于实验1的结果,设置用于微调的模块是Projection + LoRA。

让我们来看看结果。

该图显示了训练损失。(图由作者制作)
这张图显示了验证损失(作者制作的图)

通过审查损失,可以明显看出使用LLaMA2和MPT作为LLM的模型具有更令人满意的减少。让我们也观察推理结果。

GIT-LLMs的示例结果。图片摘自M3IT数据集,文本结果由作者的模型生成

关于第一张图片,对于所有模型而言,表情与OPT-350m相比更加自然。没有像“一个香蕉和一个香蕉”之类的奇怪表情,突显了LLM的优势。对于第二张图片,诸如“一个交通灯”或“一座建筑物”等短语仍然存在一些困难。对于这样的复杂图片,可能需要考虑升级ViT模型。

最后,让我们在与GPT-4一起流行的图像上进行推理。

GIT-LLMs的示例结果。图片摘自这里,文本结果由作者的模型生成

尽管预计使用LLM会得到流畅的回应,但结果却非常简单。这可能是因为该模型仅在COCO上进行了训练。

实验3. 增加数据

鉴于之前实验的令人失望的结果,决定在训练中引入除COCO以外的其他数据。目前使用的M3IT数据集非常全面,并且可以处理与COCO相同格式的大量数据。

这个表格摘自“M3IT: A Large-Scale Dataset towards Multi-Modal Multilingual Instruction Tuning”的表3

打算使用该来源的数据,但排除“中文”和“视频”类别。原始的COCO训练数据集包含了566,747个数据。通过与其他来源的数据结合,这个数字增加到了1,361,650个。尽管大小大致翻了一番,但由于任务多样性的增加,数据集被认为具有更高的质量。

使用ConcatDataset可以轻松处理多个Pytorch数据集。

dataset_list = [    datasets.load_dataset("MMInstruction/M3IT", i) for i in m3it_name_list]train_dataset = torch.utils.data.ConcatDataset([d["train"] for d in dataset_list])

训练进行了1个时期,使用LLaMA2模型对投影和LoRA进行了微调,与实验2类似。

由于这次没有损失可供对比,让我们直接深入推理结果。

GIT-LLaMA2的示例结果。图片摘自M3IT数据集,文本结果由作者的模型生成
GIT-LLaMA2的示例结果。图片摘自M3IT数据集,文本结果由作者的模型生成
GIT-LLaMA2 的示例结果。图片引用自 M3IT 数据集,文字结果由作者的模型生成

除了解决简单的问题,该模型现在还可以处理更复杂的挑战。通过添加用于任务的数据集,而不仅仅是字幕生成,其功能显著增强。令人惊讶的是,只经过 1 个训练时期就能达到如此准确的水平。

让我们用以下示例图像进行测试。鉴于数据集的多样性增加,提出问题的方式略有修改。

GIT-LLaMA2 的示例结果。图片引用自这里,文字结果由作者的模型生成

虽然描述为“雨伞”仍然有些奇怪,但感觉好像有所改善。要进一步改进,需要增加训练时期的数量,添加更多类型或规模的数据集,并利用更强大的 ViT 或 LLM。尽管如此,令人印象深刻的是,在计算和数据资源有限的情况下,这样的模型在仅半天的时间内就能开发出来。

额外实验。图像是否转化为文字了?

让我们再次看一下 GIT 结构。

GIT-LLM 的简化模型架构(作者制作的图像)

如图所示,在视觉编码器进行特征提取后,图像通过视觉投影与向量化文本等同对待。换句话说,视觉投影可能将图像向量转换为文本向量。进行了一项调查,以查看经过视觉投影后的向量的外观。

虽然可以使用头部将投影后的向量还原为文本,但发现即使使用嵌入模块进行向量化的向量也无法通过此方法还原为原始文本。因此,应将与输入 LLM 之前的文本向量密切相似的向量分配为相应的单词。所有在标记器中注册的令牌都使用嵌入模块进行向量化,并确定与其余弦相似度最高的令牌 ID 作为目标单词。

此实验使用的图像是一只猫。

图片引用自 M3IT 数据集。

现在,让我们进行分析(完整的分析可在此处查看)。首先,将所有注册的令牌进行向量化。

coco_datasets = datasets.load_dataset("MMInstruction/M3IT", "coco")test_dataset = coco_datasets["test"]supervised_test_dataset = SupervisedDataset(model_name, vision_model_name, test_dataset, 256)ids = range(supervised_test_dataset.processor.tokenizer.vocab_size)all_ids = torch.tensor([i for i in ids]).cuda()token_id_to_features = model.model.embed_tokens(all_ids)

接下来,提取通过 ViT 和投影可能已转换为单词的图像向量。

inputs = supervised_test_dataset[0] # 任选一个样本pixel_values = inputs["pixel_values"]out_vit = model.model.image_encoder(pixel_values).last_hidden_stateout_vit = model.model.visual_projection(out_vit)

计算这些向量与单词向量的点积,并将具有最大值的结果解码为相关令牌 ID。

# 点积
nearest_token = out_vit[0] @ token_id_to_features.T
# 最大值对应的索引即为相关的token ID
visual_out = nearest_token.argmax(-1).cpu().numpy()
decoded_text = supervised_test_dataset.processor.tokenizer.batch_decode(visual_out)
print(decoded_text)"""['otr', 'eg', 'anto', 'rix', 'Nas', ...]"""

如打印的decoded_text所示,出现了一些不熟悉的单词。由于有些单词重复出现,它们被计数了。

print(pd.Series(decoded_text).value_counts())"""mess        43atura       29せ           10Branch      10Enum         9bell         9worden       7..."""

似乎出现了大量不熟悉的单词。根据位置的不同,它们可能传达了有意义的信息。让我们将这些单词绘制在猫的图像上。

n_patches = 14
IMAGE_HEIGHT = 468
IMAGE_WIDTH = 640
y_list = np.arange(15, IMAGE_HEIGHT, IMAGE_HEIGHT//n_patches)
x_list = np.arange(10, IMAGE_WIDTH, IMAGE_WIDTH//n_patches)
plt.figure()
plt.axis("off")
plt.imshow(np.array(image), alpha=0.4)
for index in np.arange(n_patches ** 2):
    y_pos = index // n_patches
    x_pos = index - y_pos * n_patches
    y = y_list[y_pos]
    x = x_list[x_pos]
    # 第一个token是bos token,所以要排除掉
    word = decoded_text[index + 1]
    # 为了区分不同的单词,使用蓝色进行标注
    plt.annotate(word, (x, y), size=7, color="blue")
plt.show()
plt.clf()
plt.close()
作者制作的图像

频繁出现的单词按颜色进行编码。结果似乎表明它们不仅仅是被投影到有意义的单词上。虽然单词“猫”可能叠加在猫的图像上,使其具有一定的相关性,但其含义仍然不清楚。

这个实验的结果并不确定,可能是因为强制选择具有高余弦相似度的单词。无论如何,这种方法不仅仅涉及将单词进行投射和创建图像提示。从图像中提取的向量通过Visual Projection转换为token空间中的向量,在意义上似乎有些相似,作为神秘的提示。最好不要深入研究。

结论

在这篇技术博文中,我介绍了将LLMs集成到视觉语言模型GIT中的方法。此外,使用开发的模型进行了各种实验。虽然有成功和失败,但我希望继续使用视觉语言模型进行实验,积累见解。请将本文视为参考,并鼓励您创建自己的视觉语言模型,并探索其潜力。

这是作者使用稳定扩散创建的GIT-LLM的插图。(图片由作者制作)