En los últimos años, he trabajado principalmente con modelos de idiomas grandes, capacitación, ajuste, indicación, etc., ya que esto fue muy solicitado en el mercado y por los usuarios. Pero creo que los LLM que funcionan principalmente en el texto es solo el comienzo de Genai. En cierto punto, todos querrán IA físicadonde los modelos pueden ver, escuchar, sentir y razonar de una manera humana más arraigada.
Así que comencemos con la multimodalidad. En este cuaderno, presento LLAVA, una arquitectura capaz de interpretar tanto imágenes como texto para generar respuestas multimodales.
En este tutorial, vamos a utilizar un componente de peso más ligero adecuado para ejecutar el cuaderno en un entorno de nivel gratuito como Google Colab.
Los componentes que vamos a usar son:
1minte⃣ Clip-vit b/32 Como el codificador de la imagen
2minte⃣ Tinyllama-1.1b Como el modelo de idioma
3️⃣ a Adaptador MLP de 2 capas Para unir los dos
Configuración
Antes de que podamos sumergirnos en el código, configuremos nuestro entorno.
Primero instalemos la biblioteca de conjuntos de datos.
!pip install -U datasets
Ahora necesitamos importar los paquetes requeridos de abrazar Face y Pytorch. Estas importaciones proporcionan modelos y utilidades previamente capacitados para el procesamiento multimodal.
import json
from pathlib import Path
import requests
import safetensors
import torch
from datasets import load_dataset
from huggingface_hub import hf_hub_download
from PIL import Image
from transformers import (
AutoConfig,
AutoTokenizer,
LlamaTokenizer,
LlavaConfig,
LlavaForConditionalGeneration,
LlavaProcessor,
Seq2SeqTrainer,
Seq2SeqTrainingArguments,
)
from transformers.models.clip.modeling_clip import CLIPVisionModel
from transformers.models.clip.image_processing_clip import CLIPImageProcessor
Descargar componentes del modelo previamente capacitado
Nuestro modelo de Llava estará compuesto por:
El hf_hub_download es un centro que estamos explorando para recuperar pesos previamente capacitados:
vision_backbone_name = "openai/clip-vit-base-patch32"
text_backbone_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
_ = hf_hub_download(
vision_backbone_name, filename="pytorch_model.bin", local_dir="/content"
)
_ = hf_hub_download(
text_backbone_name, filename="model.safetensors", local_dir="/content"
)
Modelo
Instanciar un nuevo modelo de Llava
Ahora instanciamos un nuevo modelo de Llava. Como se explicó anteriormente, un modelo LLAVA está compuesto por dos partes, un codificador visual y un decodificador textual que acabamos de descargar.
vision_config = AutoConfig.from_pretrained(vision_backbone_name).vision_config
text_config = AutoConfig.from_pretrained(text_backbone_name)
Especificamos los modelos Backbone en la configuración de Llava. Luego instanciamos el modelo real con LlavaForConditionalGeneration(llava_config).
llava_config = LlavaConfig(vision_config=vision_config, text_config=text_config)
model = LlavaForConditionalGeneration(llava_config).cuda()
model
Realizar algunas operaciones quirúrgicas
Anteriormente, dijimos que podríamos construir un modelo LLAVA comenzando a partir de un codificador de imagen previamente capacitado y un LLM previamente capacitado. ¡Hagamos exactamente eso!
El modelo de Llava original se inicializa desde un Clip-vit l/14 y un Vicuna v1.5 7b. Para hacer las cosas más manejables con los recursos proporcionados por el plan gratuito de Google Colab, usaremos un Clip-vit b/16 y un Tinyllama 1.1b.
El único componente que entrenaremos es un adaptador MLP de 2 capas entre ellos.
Para usar los modelos Clip y Tinyllama, necesitamos cargar sus pesos previamente capacitados. Pero estos pesos pueden venir en diferentes formatos como .Safetensores o .bin. La función Load_Weights se maneja esto para nosotros. Verifica el tipo de archivo y llama a la función de carga correcta.
def load_weights(path_to_weights: str):
if path_to_weights.endswith(".safetensors"):
return load_safetensors_weights(path_to_weights)
elif path_to_weights.endswith(".bin"):
return load_bin_weights(path_to_weights)
else:
raise ValueError(f"Unsupported weights file: {path_to_weights}")
def load_bin_weights(path_to_weights: str):
return torch.load(path_to_weights, weights_only=True)
def load_safetensors_weights(path_to_weights: str):
return safetensors.torch.load_file(path_to_weights)
vision_backbone_state_dict = load_weights("/content/pytorch_model.bin")
text_backbone_state_dict = load_weights("/content/model.safetensors")
Inyectar los pesos de la columna vertebral de la visión en el modelo 💉
Las siguientes líneas cargan los pesos en la parte de visión del modelo. Establecimos estricto = falso Para ser flexible, ya que nos permite omitir cualquier peso que no coincida perfectamente con la estructura esperada del modelo.
incompatible_keys = model.vision_tower.load_state_dict(
vision_backbone_state_dict, strict=False
)
assert len(incompatible_keys.missing_keys) == 0, (
f"Missing keys in state dict: {incompatible_keys.missing_keys}"
)
incompatible_keys.unexpected_keys
Inyectar los pesos de la columna vertebral en el modelo 💉
La misma lógica que antes, pero también para el modelo de texto.
incompatible_keys = model.language_model.load_state_dict(
text_backbone_state_dict, strict=True
)
Congelar los componentes previamente capacitados ❄️
Ahora queremos congelar los modelos visuales y de texto de la columna vertebral, porque no queremos actualizar sus pesas mientras entrenamos.
Solo entrenaremos el pequeño adaptador (el MLP que conecta la visión y el lenguaje), que es mucho más ligero y más rápido para entrenar.
_ = model.vision_tower.requires_grad_(False)
_ = model.language_model.requires_grad_(False)
# Then we define a helper function to count model parameters
def count_parameters(model, trainable_only=False):
return sum(
p.numel()
for p in model.parameters()
if not trainable_only or p.requires_grad
)
print(f"Total parameters: {count_parameters(model)}")
print(f"Trainable parameters: {count_parameters(model, trainable_only=True)}")
Procesador
Antes de alimentar algún texto a nuestro modelo, necesitamos convertir palabras en números. Para esto se necesita el tokenizador.
tokenizer = LlamaTokenizer.from_pretrained(
text_backbone_name, additional_special_tokens=["<image>", "<pad>"]
)
tokenizer.pad_token_id = 32001
A continuación se muestra el formato que usaremos para chatear con nuestro modelo LLAVA.
La primera parte es la llamada aviso del sistemaque contiene pautas generales sobre cómo el modelo debe responder al usuario.
La segunda parte es una plantilla Jinja (básicamente código) que determina cómo la conversación se representa en función de alguna entrada estructurada (ver ejemplo a continuación).
LLAVA_CHAT_TEMPLATE = (
"A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions. "
"{% for message in messages %}{% if message['role'] == 'user' %}USER: {% else %}ASSISTANT: {% endif %}{% for item in message['content'] %}{% if item['type'] == 'text' %}{{ item['text'] }}{% elif item['type'] == 'image' %}<image>{% endif %}{% endfor %}{% if message['role'] == 'user' %} {% else %}{{eos_token}}{% endif %}{% endfor %}"
)
tokenizer.chat_template = LLAVA_CHAT_TEMPLATE
sample_messages = [
{
"content": [
{
"index": 0,
"text": None,
"type": "image"
},
{
"index": None,
"text": "\nWhat potential activities might be popular at this location?",
"type": "text"
}
],
"role": "user"
},
{
"content": [
{
"index": None,
"text": (
"At this location, with a sandy path leading to the ocean where multiple boats, including "
"sailboats, are moored, popular activities might include boating, sailing, swimming, and "
"beachcombing. Additionally, the sandy path and shoreline provide an ideal setting for leisurely "
"strolls and picnics, while the ocean view offers a serene environment for relaxation and "
"photography. Depending on the specific area and available facilities, other water sports such as "
"kayaking, paddleboarding, and snorkeling could also be prevalent."
),
"type": "text"
}
],
"role": "assistant"
}
]
Aplicemos la plantilla de chat a nuestras muestras.
tokenizer.apply_chat_template(
sample_messages, tokenize=False, add_generation_prompt=False
)
En este punto, hemos configurado nuestro tokenizer y descargamos el modelo de visión. Los reunimos en un unificado procesador.
processor = LlavaProcessor(
image_processor=CLIPImageProcessor.from_pretrained(vision_backbone_name),
tokenizer=tokenizer,
patch_size=model.config.vision_config.patch_size,
)
processor.chat_template = LLAVA_CHAT_TEMPLATE
Desde que agregamos tokens especiales como <image> y <pad> a nuestro tokenizador antes, el modelo necesita ajustar su vocabulario para entenderlos también
model.resize_token_embeddings(len(tokenizer), pad_to_multiple_of=8)
Conjunto de datos
Vamos a descargar el conjunto de datos que vamos a usar de abrazar la cara.
El conjunto de datos que contiene muestras de parejas de texto de imagen está disponible públicamente y se puede encontrar aquí.
train_dataset = load_dataset(
"HuggingFaceH4/llava-instruct-mix-vsft", split="train", streaming=True
)
¿Cómo son nuestros ejemplos de entrenamiento?
next(iter(train_dataset))
¿Cómo construimos un lote de ejemplos?
La siguiente función toma ejemplos de texto de imagen sin procesar y los convierte en entradas listas para el modelo. Formatea los mensajes utilizando la plantilla de chat, procesa tanto el texto como la imagen con el LlavaProcessor Definimos anteriormente y crea etiquetas de entrenamiento adecuadas mientras ignoramos el relleno.
def get_data_collator(processor, ignore_index):
def collate_examples(examples):
# Extract texts and images from the raw examples
texts = []
images = []
for example in examples:
messages = example["messages"]
text = processor.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
texts.append(text)
images.append(example["images"][0])
# Process the inputs (tokenize text and transform images)
batch = processor(texts, images, return_tensors="pt", padding=True)
# Create labels
labels = batch["input_ids"].clone()
if processor.tokenizer.pad_token_id is not None:
labels[labels == processor.tokenizer.pad_token_id] = ignore_index
batch["labels"] = labels
return batch
return collate_examples
# NOTE: this does a bit more than a collate function should...
Capacitación
Finalmente definamos los argumentos de entrenamiento, incluidos el tamaño del lote, la tasa de aprendizaje, los pasos totales y el uso de precisión mixta (FP16) para la velocidad. También evitamos guardar puntos de control para mantener las cosas ligeras. Entonces envolvemos todo en un Seq2SeqTrainerPasando en el modelo, el conjunto de datos y nuestro colador personalizado para entradas de texto de imagen.
args = Seq2SeqTrainingArguments(
output_dir="/content/training_output",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
max_steps=350,
lr_scheduler_type="cosine_with_min_lr",
lr_scheduler_kwargs={"min_lr": 2e-5},
warmup_ratio=0.05,
logging_strategy="steps",
logging_steps=5,
fp16=True,
remove_unused_columns=False, # Important!
optim="adamw_torch",
report_to="none",
save_strategy="no", # let's not save the checkpoint to disk, otherwise it'll take 5 mins
)
trainer = Seq2SeqTrainer(
model=model,
args=args,
data_collator=get_data_collator(
processor, ignore_index=model.config.ignore_index,
),
train_dataset=train_dataset,
)
trainer.train()
Inferencia
Para tener en cuenta que para asegurarse de que la inferencia funcione como se esperaba, debe usar modelos más pesados y entrenar durante más tiempo.
Usaremos esta imagen para inferencia:
conversation = [
{
"content": [
{
"type": "image"
},
{
"text": "\nWhat is represented in the image?",
"type": "text"
}
],
"role": "user"
}
]
En este bloque de celdas como ejemplo, cargamos una imagen de una URL y formatamos una conversación utilizando la plantilla de chat. El procesador convierte ambos en tensores. Luego movemos la entrada al dispositivo del modelo y generamos una respuesta, permitiendo que el modelo describe la imagen en función de la solicitud del usuario.
image_url = "https://llava-vl.github.io/static/images/monalisa.jpg"
inputs_for_generation = processor(
images=Image.open(requests.get(image_url, stream=True).raw),
text=processor.apply_chat_template(conversation, add_generation_prompt=True),
return_tensors="pt",
)
inputs_for_generation = inputs_for_generation.to(device=model.device)
output = trainer.model.generate(
**inputs_for_generation, max_new_tokens=200, do_sample=False
)
print(processor.decode(output[0], skip_special_tokens=True))
Extensiones y mejoras
- Use un codificador de imagen más grande (por ejemplo Clip-vit grande) y LLM (por ejemplo Llama 3.1 8B)
- Entrena por más tiempo. El modelo tarda un tiempo en descubrir cómo seguir las instrucciones en presencia de características de imagen.
- Siga el procedimiento de capacitación en varias etapas adoptado por el Llava original
- Etapa 1: Prerreining para la alineación de características -> Entrenar el modelo en datos de instrucciones de vuelta única, donde se le pide que describa brevemente la imagen. El codificador de imágenes y LLM están congelados
- Etapa 2: ajuste de extremo a extremo -> Entrenar el modelo en datos de instrucciones múltiples giros. Solo el codificador de la imagen está congelado
Demostración de trabajo: huggingface.co/spaces/badayvedat/llava
Conclusión
Creo que este pequeño proyecto es interesante para comprender mejor cómo funcionan los modelos multimodales como Llava. Incluso si utilizamos modelos más pequeños, la idea principal es la misma: combinar la visión y el lenguaje en un solo sistema que pueda comprender las imágenes y hablar sobre ellos.
Por supuesto, los resultados obtenidos en este ejemplo de juguete no son realmente buenos; Hay mucho espacio para mejorar. Pero hacer que Llava funcione en un entorno con recursos limitados es bastante desafiante
Seguirme TDS ¡Si te gusta este artículo! 😁
💼 LinkedIn ️ | 🐦 X (Twitter) | 💻 Sitio web