Создавайте и играйте! Ваша собственная модель V&L, оснащенная LLM!

Создавайте и играйте! Модель V&L с LLM!

Разработка интегрированных моделей GIT с языковыми моделями.

Краткое изложение данной статьи:

  • Объяснение модели GIT, языковой модели компании Microsoft.
  • Замена языковой модели GIT на большие языковые модели (LLM) с использованием PyTorch и Transformers от Hugging Face.
  • Введение в технику настройки моделей GIT-LLM с использованием LoRA.
  • Тестирование и обсуждение разработанных моделей.
  • Исследование, указывают ли “Вложения изображений”, встроенные с помощью кодировщика изображений GIT, на конкретные символы в том же пространстве, что и “Текстовые вложения”.

Большие языковые модели (LLM) все больше проявляют свою ценность. Включение изображений в LLM делает их еще более полезными в качестве моделей языка зрения. В этой статье я объясню разработку модели под названием GIT-LLM, простой, но мощной модели языка зрения. Некоторые части, такие как объяснения кода, могут показаться немного скучными, поэтому не стесняйтесь переходить прямо к разделу с результатами. Я провел различные эксперименты и анализы, поэтому думаю, что вам будет интересно узнать, что мне удалось достичь.

Реализация доступна публично, поэтому попробуйте ее.

GitHub – turingmotors/heron

Внести вклад в разработку turingmotors/heron, создав учетную запись на GitHub.

github.com

Преобразование GIT в LLM

Давайте углубимся в основную тему этого технического блога.

Что такое GIT?

Generative Image-to-text Transformer, или GIT, является моделью языка зрения, предложенной компанией Microsoft.

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

Его архитектура довольно проста. Она преобразует векторы признаков, извлеченные из кодировщика изображений, в векторы, которые могут быть обработаны как текст с помощью модуля проекции. Затем эти векторы вводятся в языковую модель для создания подписей к изображениям или выполнения вопросно-ответных операций. Модель также может обрабатывать видео аналогичным образом.

This figure is cited from “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”.

Использование LLM с библиотекой Transformers от Hugging Face

Я буду использовать библиотеку Transformers от Hugging Face для разработки GIT-LLM. Transformers – это библиотека на языке Python для работы с моделями машинного обучения. Она предлагает множество современных предварительно обученных моделей, на которых вы можете сразу же выполнять вывод. Она также предоставляет инструменты для обучения и настройки моделей. Я считаю, что Transformers внесла значительный вклад в развитие последних производных LLM. Почти все доступные LLM могут быть обработаны с помощью Transformers, а многие мультимодальные модели, производные от них, используют Transformers в качестве основы для разработки и настройки.

Вот простейший код для использования модели из Transformers. Вам будет легко попробовать LLM, используя AutoModel и AutoTokenizer.

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 в основном представляет часть “Трансформер” модели языка. Если, например, вы хотите выполнять задачи, такие как классификация текста, вы будете использовать только эту часть. Класс 〇〇ForCausalLM предназначен для генерации текста, применения классификатора для подсчета токенов к векторам после их обработки с помощью Трансформера. Расчет потерь также выполняется в методе forward этого класса. embed_positions обозначает позиционное кодирование, которое добавляется к project_in.

Использование GIT с Transformers

Я попробую это на основе официальной страницы документации GIT. Также я буду обрабатывать изображения, поэтому буду использовать Processor, который также включает в себя Tokenizer.

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# Предобработка текстапромпт = "Что это?"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, которые являются модулями, преобразующими изображения в векторы-подсказки. В то время как языковая модель использует декодер для OPT и энкодер для GIT, это указывает только на разницу в том, как строится маска внимания. Могут быть небольшие различия в слое трансформера, но их функции существенно одинаковы. GIT использует название “энкодер”, потому что он использует уникальную маску внимания, которая применяет внимание ко всем признакам изображения и использует маску причинности для текстовых признаков.

Рассмотрим связи модели;

Упрощенная архитектура модели GIT (изображение, созданное автором)

Информация об изображении обрабатывается GitVisionModel и GitProjection для соответствия вложениям текста. После этого она вводится вместе с вложениями текста в “трансформерные” слои языковой модели. Хотя есть незначительные различия, часть, связанная с языковой моделью, разрабатывается почти одинаковым образом.

Маска внимания в 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, поэтому я сделал его совместимым с ViT, обученным с помощью CLIP. 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. Регулировка размерности MLP, специфичная для OPT        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.

класс 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, вы увидите, что OPT первоначально помещается в self.model во время инициализации super. Если вы измените это имя переменной экземпляра, у вас окажется два OPT, что может нагрузить память.

Расширение LoRA

Для эффективной донастройки LLM я буду использовать библиотеку под названием Parameter-Efficient Fine-Tuning (PEFT). Поскольку она разработана Hugging Face, она интегрируется настолько плавно с Transfors. Хотя в PEFT есть различные методы, на этот раз я собираюсь провести некоторые эксперименты, используя широко распространенный подход, называемый низкоранговой адаптацией (LoRA).

Моделям можно применять LoRA всего за несколько строк, если они поддерживают PEFT.

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 применяется к v_proj само-внимания в ViT.

(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 вы найдете добавленные полносвязные слои, такие как lora_A и lora_B. Модуль Linear, преобразованный в LoRA, является классом Linear, наследующимся от Linear и LoraLayer в PyTorch. Это относительно уникальный модуль, так что те, кто интересуется подробностями, могут посмотреть его реализацию.

Обратите внимание, что модели, созданные с использованием PEFT, по умолчанию сохраняют только часть LoRA. Хотя есть метод для сохранения с помощью метода merge_and_unload, вам может понадобиться сохранить все модели, сохраняемые в середине обучения с помощью Trainer. Одним из подходов к этому является перегрузка метода _save_checkpoints Trainer’а, но чтобы избежать хлопот, на этот раз я справился, просто извлекая исходную часть модели, содержащуюся в 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, мультимодальный набор данных для настройки инструкций, разработанный Shanghai AI Lab. Настройка инструкций является методом, который дает впечатляющие результаты даже с ограниченным количеством данных. Кажется, что 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")        # Настройка соответствующего процессора для каждой модели        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,        )        # batch size 1 -> unbatch        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 или видео, также могут быть обработаны аналогичным образом, что делает его универсальным набором данных для целей валидации.

Образец данных выглядит следующим образом:

Изображение цитируется из данных в M3IT.

Название к этой картинке следующее:

##Инструкция: Напишите краткое описание изображения, описывающее его основные компоненты, отношения между ними и любые заметные детали. ##Вопрос: ##Ответ: Мужчина с красным шлемом на маленьком мопеде на грунтовой дороге.

Для набора данных COCO, который предназначен для заголовков, часть вопроса оставлена пустой.

Давайте углубимся в операции процессора. Он нормализует изображения и токенизирует текст. Входные данные, меньшие, чем max_length, также дополняются. Обработанные данные, возвращаемые процессором, являются словарем, содержащим:

  • input_ids: Массив токенизированного текста.
  • attention_mask: Маска для токенизированного текста (с заполнением 0).
  • pixel_values: Массив нормализованных изображений, также преобразованных в формат Channel-first.

Эти имена ключей соответствуют аргументам функции forward модели, поэтому их не следует изменять. Наконец, input_ids непосредственно передаются в ключ с именем labels. Функция forward модели 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

Визионный энкодер и LLM, использованные для этого исследования:

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

Обучение использует набор данных COCO и длится 5 эпох.

Вот модули, обучаемые во время каждого эксперимента:

  • Proj: GIT Projection. Инициализируется случайным образом, поэтому всегда обучается.
  • LoRA: Запрос, ключ и значение само-внимания в языковой модели были применены.
  • OPT: Все слои были обучены.
  • ViT: Все слои были обучены.
  • Head: Последний lm_head OPT был обучен.

(Примечание: Хотя LoRA может быть применена к ViT, но чтобы не усложнять эксперименты, она на этот раз не включена.)

На этой фигуре показана потеря обучения. Проект, LoRA, OPT, ViT и Head в легенде - это обучаемые модули, объясненные выше. (Рисунок, сделанный автором)

Как показано на графике потери обучения, явно видно, что некоторые группы показывают плохие результаты. Такое происходит, когда OPT включен в обучение. Хотя все эксперименты проводились в довольно похожих условиях, могут потребоваться более детальные настройки, такие как скорость обучения, при тонкой настройке языковой модели. Результаты, за исключением моделей, в которых OPT включен в обучение, будут рассмотрены далее.

На этой фигуре показана потеря обучения без полных результатов тонкой настройки. Проект, LoRA, OPT, ViT и Head в легенде - это обучаемые модули, объясненные выше. (Рисунок, сделанный автором)
На этой фигуре показана потеря валидации. Проекция, LoRA, OPT, ViT и Head в легенде - это обученные модули, объясненные выше. (фигура создана автором)

Как обучение, так и потеря валидации уменьшились больше всего с моделью Проекции+LoRA. Подстройка окончательного слоя Head показала практически идентичные результаты. Если также обучать ViT, потеря кажется немного выше, и результаты кажутся нестабильными. Даже при добавлении LoRA во время обучения ViT потеря все еще имеет тенденцию быть высокой. Для донастройки с этими данными, кажется, использование предварительно обученной модели ViT без обновления ее параметров дает более стабильные результаты. Эффективность LoRA была признана в различных местах, и из этого эксперимента явно видно, что добавление LoRA к LLM улучшило как обучение, так и потерю валидации.

Рассмотрим результаты вывода на некоторых тестовых данных:

Примеры результатов GIT-OPT. Изображения взяты из набора данных M3IT, а текстовые результаты созданы моделью автора

При обучении OPT сам по себе результаты так же низки, как и результат потери, что делает модель безмолвной. Кроме того, при обучении ViT вывод имеет семантический смысл, но описывает нечто совершенно отличное от данного изображения. Однако другие результаты, в некоторой степени, улавливают особенности изображений. Например, первое изображение упоминает “кошку” и “банан”, а второе идентифицирует “дорожный знак”. Сравнивая результаты с и без LoRA, последний имеет тенденцию повторно использовать похожие слова, но использование LoRA кажется делает его немного более естественным. Обучение слоя Head дает интересные результаты, например, вместо “еды” для первого изображения используется “игра”. Хотя в этих результатах есть некоторые неестественные элементы, можно сделать вывод, что обучение успешно захватывает особенности изображений.

Эксперимент 2: Сравнение моделей масштаба в миллиарды

Для условий донастройки в предыдущих экспериментах использовалась немного меньшая модель языка, OPT-350m. Теперь намерение – переключить модель языка на модель 7B. Не ограничиваясь только OPT, будут также представлены более сильные LLMs, LLaMA и MPT.

Интеграция этих двух моделей может быть выполнена похожим образом, как с OPT. Отсылаясь к функциям forward моделей LlamaModel и MPTModel, объедините векторы проецированных изображений с текстовыми токенами и измените маску с маски внимания Текущего внимания на маску Внимания GIT. Одно замечание: для MPT маска – это не (0, -бесконечность), а (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 account → Settings → Access Token.

Параметры обучения остаются неизменными, используя набор данных COCO и длительность в 3 эпохи. Исходя из результатов эксперимента 1, модули для донастройки были выбраны Проекция + LoRA.

Давайте взглянем на результаты.

На этой фигуре показана потеря обучения (фигура создана автором)
На этой фигуре показана потеря валидации (фигура создана автором)

Изучая потерю, ясно, что модели, использующие LLaMA2 и MPT в качестве LLM, показывают более удовлетворительное снижение. Давайте также рассмотрим результаты вывода.

Пример результатов GIT-LLM. Изображения цитируются из набора данных M3IT, а текстовые результаты были созданы моделью автора

Что касается первого изображения, для всех моделей выражения кажутся более естественными по сравнению с OPT-350m. Здесь нет странных выражений типа “банан с бананом”, что подчеркивает силу LLM. Что касается второго изображения, все еще есть некоторые трудности с фразами “светофор” или “здание”. Для таких сложных изображений, возможно, потребуется обновление модели ViT.

Наконец, давайте проведем вывод на изображениях, которые стали популярными с помощью GPT-4.

Пример результатов GIT-LLM. Изображение цитируется отсюда, а текстовые результаты были созданы моделью автора

Хотя ожидались гладкие ответы, так как используется LLM, результаты довольно просты. Возможно, это связано с тем, что модель обучалась только на COCO.

Эксперимент 3. Увеличение данных

Учитывая неудовлетворительные результаты предыдущего эксперимента, было решено включить в обучение данные, отличные от COCO. Набор данных M3IT, который в настоящее время используется, является довольно полным и может обрабатывать значительное количество данных в том же формате, что и COCO.

Эта таблица цитируется из таблицы 3 «M3IT: большой набор данных для многоязыковой настройки инструкций с несколькими режимами»

Предполагается использовать данные из этого источника, исключая категории «китайский» и «видео». Исходный набор данных для обучения COCO содержал 566 747 элементов данных. Путем объединения его с дополнительными источниками этот объем увеличился до 1 361 650. Хотя размер примерно удвоился, верится, что набор данных стал более качественным благодаря увеличению разнообразия задач.

Обработка нескольких наборов данных Pytorch может быть легко выполнена с использованием ConcatDataset.

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 эпохи, и для настройки проекции и LoRA использовалась модель LLaMA2, аналогично эксперименту 2.

Поскольку в этот раз нет потери для сравнения, давайте сразу перейдем к результатам вывода.

Пример результатов GIT-LLaMA2. Изображения цитируются из набора данных M3IT, а текстовые результаты были созданы моделью автора
Пример результатов GIT-LLaMA2. Изображения цитируются из набора данных M3IT, а текстовые результаты были созданы моделью автора
Примеры результатов GIT-LLaMA2. Картинки цитируются из набора данных M3IT, а текстовые результаты созданы моделью автора

Вместе с решением простых задач модель теперь справляется с более сложными вызовами. Добавление наборов данных для задач, более сложных, чем просто описания, позволило значительно расширить возможности. Было удивительно достичь такого уровня точности всего за 1 эпоху обучения.

Давайте проверим это на следующем примере изображения. В связи с увеличением разнообразия в наборе данных, способ представления вопросов был незначительно изменен.

Примеры результатов GIT-LLaMA2. Картинка цитируется отсюда, а текстовые результаты созданы моделями автора

Хотя описание “Зонтик” всё ещё странное, кажется, что становится лучше. Чтобы улучшить результаты дальше, необходимо увеличить количество эпох обучения, добавить больше типов или объемов наборов данных и использовать более мощные ViT или LLM. Тем не менее, впечатляет, что такая модель может быть разработана всего за полдня с использованием вычислительных и данных ресурсов.

Бонусный эксперимент. Превратилось ли изображение в слова?

Давайте еще раз взглянем на структуру GIT.

Упрощенная модельная архитектура GIT-LLM (изображение, созданное автором)

Как показано на рисунке, после извлечения функций с помощью визуального кодировщика, изображения обрабатываются наравне с векторизованным текстом с помощью визуальной проекции. Другими словами, возможно, что визуальная проекция преобразует векторы изображений в векторы текста. Было проведено исследование, чтобы увидеть, как выглядят векторы после визуальной проекции.

Хотя есть возможность использовать Head, чтобы вернуть вектор после проекции обратно в текст, было обнаружено, что даже векторы, векторизованные с помощью модуля Embedding, не могут быть возвращены обратно в исходный текст с использованием этого метода. Поэтому векторы, близкие к векторам текста до ввода в LLM, должны быть присвоены соответствующему слову. Все токены, зарегистрированные в токенизаторе, были векторизованы с использованием модуля Embedding, и тот, у которого наивысшая косинусная схожесть, был определен как целевое слово.

Изображение, используемое для этого эксперимента, изображает кошку.

Картинка цитируется из набора данных 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 и Projection.

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)

Были вычислены скалярные произведения этих векторов и векторов слов, и результаты с максимальным значением были декодированы в соответствующий идентификатор токена.

# Скалярное произведениеnearest_token = out_vit[0] @ token_id_to_features.T# Индекс максимального значения соответствует соответствующему токену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 = 14IMAGE_HEIGHT = 468IMAGE_WIDTH = 640y_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]    # Первый токен - это токен bos, поэтому он исключается    word = decoded_text[index + 1]    # Для различения слов по цвету    plt.annotate(word, (x, y), size=7, color="blue")plt.show()plt.clf()plt.close()
Изображение, созданное автором

Часто встречающиеся слова закодированы цветом. Результаты кажутся указывать, что они не просто проецируются на смысловые слова. В то время как слово «Кот» может быть наложено на изображение кота, придавая ему некоторую значимость, его значение остается неясным.

Неопределенные результаты этого эксперимента могут быть связаны с принудительным выбором слова с высокой косинусной сходностью. В любом случае, подход не сводится к простому проецированию слов и созданию образцов изображений. Векторы, извлеченные из изображений, преобразуются с помощью Visual Projection в векторы в пространстве токенов, которые, кажется, сохраняют некоторое сходство в значении, функционируя как загадочные образцы. Возможно, лучше не углубляться в это дальше.

Заключение

В этой технической блог-статье я представил метод интеграции LLM в модель визуального языка GIT. Кроме того, были проведены различные эксперименты с использованием разработанных моделей. В ходе работы были достигнуты успехи и неудачи, и я хотел бы продолжать проводить эксперименты с моделями визуального языка для накопления понимания. Пожалуйста, рассмотрите эту статью как справочный материал и будьте вдохновлены для создания своих собственных моделей визуального языка и исследования их потенциала.

This is an illustrated image of GIT-LLM created using Stable Diffusion. (Image made by the author)