Comparativa entre LLaVA 1.5 y BakLLaVA 1

Vamos enfrentar, de manera muy informal, dos modelos multimodales código libre. LLaVA 1.5 contra BakLLaVA 1 y por extensión dos modelos de lenguaje: Vicuna 13B, usado por LLaVA y Mistral 7B, que es el que usa BakLLaVA.

Ambos modelos han sido cuantizados a 5 bits y se ejecutan usando le funcionalidad llava de llama.cpp. Los modelos los puedes encontrar en estos links: LLaVA y BakLLaVA

Puedes ver la explicación del combate en vídeo en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Imagen 1

Describe la imagen con detalle

Prompt: describe the image in detail

LLaVA – 1.5 (Vicuna 13B)

The image is a painting of a unicorn standing in a snowy field. The unicorn is positioned in the center of the scene, with its long horns prominently displayed. The field is covered in snow, giving the scene a wintry atmosphere.

In addition to the unicorn, there are several birds scattered throughout the scene, some flying in the sky and others perched on branches or the ground. The birds are of various sizes and are spread out across the field, adding to the overall beauty of the painting.

BakLLaVA – 1 (Mistral 7B)

The image features a painting of a unicorn standing in a snowy field. The unicorn appears to be walking through the snow, with its majestic figure covering a significant portion of the field.

In the background, there are a few trees scattered around the area, adding to the wintry atmosphere. The painting captures the beauty and grace of the unicorn in its natural habitat, creating a unique and captivating scene.

En mi opinión BakLLaVA escribe y describe mucho mejor, entiende claramente que se le pide y como se analiza una pintura. LLaVA en cambio se pone a hablar de unos pájaros que no hay por ningún lado

Imagen 2

Antes de empezar a ver las respuesta a las preguntas debo aclarar una cosa de esta imagen. Tras una ardua investigación consistente en ampliar la foto y buscar imágenes de «taxis USA» en Google puedo afirmar con bastante seguridad que ambos vehículos son taxis. Ya veréis el porqué aclaro esto.

Dibuja una caja alrededor del hombre

Prompt: draw a box around the man

LLaVA – 1.5 (Vicuna 13B)

[0.37, 0.11, 0.58, 0.36]

BakLLaVA – 1 (Mistral 7B)

[0.34, 0.1, 0.62, 0.43]

Los recortes son muy parecidos, centrándose en la parte superior del hombre, quizás porque la tabla y la ropa colgando los confunda.

¿Cuántas cosas amarillas hay en la foto?

Prompt: how many yellow things are in the photo?

LLaVA – 1.5 (Vicuna 13B)

There are two yellow things in the photo: a yellow taxi cab and a yellow SUV.

BakLLaVA – 1 (Mistral 7B)

There are two yellow things in the photo.

Ambas respuestas son casi correctas, olvidan el suéter del hombre. LLaVA da más información, aunque no es errónea es ambigua: el SUV es amarillo pero también es un taxi y se podría interpretar que no.

¿Qué cosas amarillas hay en la foto?

Prompt: what yellow things are in the photo?

LLaVA – 1.5 (Vicuna 13B)

In the photo, there are two yellow taxis.

BakLLaVA – 1 (Mistral 7B)

There are two yellow things in the photo: a van and a taxi.

En este caso paso lo contrario BakLLaVA da más información pero me temo que es incorrecta, no es una furgoneta.

¿Por qué la foto es graciosa?

Prompt: why the photo is funny?

LLaVA – 1.5 (Vicuna 13B)

The photo is funny because it shows a man ironing clothes while sitting in the back of a yellow car, which is an unusual and unconventional setting for ironing. Typically, ironing is done in a more controlled environment, such as a laundry room or a designated ironing area. The man’s choice to iron clothes in the back of a car on a busy street adds a humorous and unexpected element to the scene.

BakLLaVA – 1 (Mistral 7B)

The photo is funny because it shows a man hanging clothes, specifically a pair of jeans, on a clothesline outside a yellow SUV in the middle of a city street. This is an unusual and unexpected sight, as people typically do laundry or hang clothes in a home or a dedicated laundry facility, not while standing on a busy street. The man’s action contrasts with the typical urban scene, which involves vehicles, pedestrians, and city life, making it a humorous and unconventional sight that catches the viewer’s attention.

La explicación de ambos es muy parecida, la de BakLLaVA me parece mejor escrita pero comete un error: dice jeans pero es una camisa

Imagina una historia que explique la imagen

Prompt: imagine a story to explain the image

LLaVA – 1.5 (Vicuna 13B)

In the image, a man is standing in the back of a yellow SUV, which is driving down a busy city street. He is holding a blue shirt and appears to be ironing it while the car is in motion. This unusual scene captures the attention of onlookers, as it is not common to see someone ironing clothes in the back of a moving vehicle. The man’s actions might be a result of a time constraint or a unique way of multitasking, but it is essential to prioritize safety and avoid distractions while driving.

BakLLaVA – 1 (Mistral 7B)

The image shows a man hanging clothes, specifically a pair of jeans, on a clothesline attached to the back of a yellow SUV parked in a parking lot. This scene could be a part of a story where the man is a busy professional, possibly a real estate agent or a salesperson, who is on his way to a meeting or a presentation. He has found a way to utilize his time efficiently by combining his daily commute with tasks such as doing laundry or taking care of his personal errands. The clothesline on the back of the vehicle is a clever solution to save space and time while attending to his responsibilities.

En ambos casos la explicación es parecida (y poco imaginativa) de que tiene prisa y hace multitasking. Me da la sensación de que BakLLaVA escribe mejor, pero LLaVA es más preciso y acierta más, por ejemplo reconoce que esta planchando una camisa y no unos jeans.

Imagen 3

Lee el texto en la foto

Prompt: Read the text in the photo

LLaVA – 1.5 (Vicuna 13B)

The image displays a computer screen with a form open, asking for personal information. The form includes fields for name, surename, and acceptance of terms. There is also a send button at the bottom of the form, which indicates that the user can submit their information by clicking on it.

BakLLaVA – 1 (Mistral 7B)

In the photo, there is a pop-up window on a computer screen with the title » Fill in next form » above a form field. Below the form field, there is a button labeled » Send » along with some text. The rest of the form fields are not visible in the image.

En este caso LLaVA lee más campos correctamente (solo se deja el título), además queda claro que entiende mejor la funcionalidad de cada elemento de la imagen.

Escribe el código HTML para crear el formulario de la imagen

Prompt: Write HTML code to create the form in the image

LLaVA – 1.5 (Vicuna 13B)

To create the form in the image, you can use the following HTML code:

»’html

<form>
<label for=»name»>Name:</label>
<input type=»text» id=»name» name=»name» required>
<label for=»surname»>Surname:</label>
<input type=»text» id=»surname» name=»surname» required>
<label for=»email»>Email:</label>
<input type=»email» id=»email» name=»email» required>
<label for=»accept_terms»>Accept Terms:</label><input type=»checkbox» id=»accept_terms» name=»accept_terms» required>
<button type=»submit»>Send</button>
</form>

»’

This code creates a simple form with four input fields: name, surname, email, and accept terms. The «required» attribute is added to each input field to ensure that the user must fill in the information before submitting the form. The «type» attribute is set to «text» for the name and surname fields, and «email

BakLLaVA – 1 (Mistral 7B)

1. The top part of the form:

<form action=»/form_processor.php» method=»post» accept-terms=»yes» name=»form_name» id=»form_id» style=»display: flex; flex-direction: column; align-content: center; background-color: pink; border: 10px solid white; padding: 10px; gap: 10px; width: 500px; height: 700px; font-size: 16px; font-family: Arial, Helvetica, san-serif; text-align: center; overflow: hidden; resize: both; scroll: no;>

2. The label and input fields for the name and e-mail address:

<div class=»form-group» id=»form-group-1″ style=»gap: 10px; width: 500px;>

<label for=»surname» style=»font-size: 16px; font-family: Arial, Helvetica,

Aquí tenemos un problema habitual en estos modelos. Han reconocido que es un formulario pero han caído en una trampa. No es el formulario habitual y eso hace que se confundan. Ambos modelos añaden el email., BakLLaVA se olvida del surname. Debo decir que el código, incompleto, de BakLLaVA me gusta más.

Conclusiones

Creo que este combate lo gana LLaVA. Mientras que BakLLaVA genera textos mejor escritos y con más detalle, seguramente gracias a usar Mistral, su respuestas son peores. Su comprensión de la imagen es menor que con LLaVA y no hay que extrañarse. En lo de entrenar modelos de lenguaje la experiencia es un punto y LLaVA es la segunda versión. En BakLLaVA creo que son conscientes de esto y ya han anunciado que están preparando una nueva versión.

Combinar varios modelos de lenguaje para aumentar su funcionalidad

Vamos a ver como usar juntos varios pequeños modelos de lenguaje que aprendimos a crear en este otro post.

Básicamente hay tres arquitecturas:

LLM en linea

En este caso el prompt pasa por el primer modelo que lo trata y lo usa para generar el prompt que pasa al segundo modelo. Es un sistema muy habitual y ya lo hemos visto en otros post: Autotune para escritores, Integrar tus documentos en tus conversaciones con un chatbot Si queréis ver un ejemplo de funcionamiento podéis recurrir a ellos.

Esta arquitectura es útil para añadir las ventajas de varios modelos. Por ejemplo GPT4 lo usa para crear prompts para DALL-E 3. Si le pides que cree una imagen puedes ver como genera varios prompts para usarlos con DALL-E 3

Varios LLM en paralelo

Esta arquitectura se basa en tener varios modelos que pueden recibir tu prompt simultáneamente pero solo se tiene en cuenta la respuesta de uno de ellos. Hay dos formas de montar este sistema:

Selección de la mejor respuesta

En este caso podemos usar varios modelos de lenguaje o varias instancias del mismo modelo. Se parte de un único prompt que se pasa en paralelo a estos modelos. La respuestas son juzgadas ya sea por otra IA o por una algoritmo más convencional para quedarse con la mejor de todas.

En nuestro caso partimos del modelo que ya entrenamos en otro post. Este modelo esta pensado para controlar la domótica de una casa, se le pasa una petición del usuario en formato texto y la respuesta llega en forma de comando que indica que aparato de la casa hay que apagar o encender. El problema que tiene es que no siempre devuelve el comando correcto, por eso vamos a usar tres instancias el mismo y a quedarnos con el comando que devuelvan dos al menos dos de ellas (el típico sistema de votación).

Para ello usaremos el comando batched de llama.cpp que permite ejecutar en paralelo el mismo prompt sobre un único modelo de lenguaje, aunque con la pega de que divide el contexto entre cada una de las instancias. En este caso pasaremos el prompt a 3 instancias:

./batched ./ggml-onoff-256x8x16-f32.gguf \
    "<s> [ORDEN] apaga la luz del salon [COMANDO]" 3
...
sequence 0:

<s> 
  [ORDEN] apaga la luz del salon 
  [COMANDO] LUZ_SL OFF
 </s

sequence 1:

<s> 
  [ORDEN] apaga la luz del salon 
  [COMANDO] LUZ_SL ON
 </s>

sequence 2:

<s> 
  [ORDEN] apaga la luz del salon 
  [COMANDO] LUZ_SL ON
 </s>

Podemos ver que responde dos comandos LUZ_SL ON y uno LUZ_SL OFF por lo tanto elegiríamos LUZ_SL ON

Seleción del mejor modelo a partir del prompt

Es el mismo esquema que el modelo anterior pero «al revés» primero pasamos por el selector que elige a que modelo enviar el prompt.

En este ejemplo vamos a tener tres modelos. Uno de ellos es el que controla la domótica, otro que responde el tiempo que hace (este no existe realmente pero para el caso da igual) y un último que actúa como discriminador eligiendo a que modelo se le pasa cada prompt.

Vamos a ver como entrenamos el modelo discriminador. Para ellos crearemos un fichero de entrenamiento con un millón de prompts (medio millón de cada modelo). Con el entrenaremos un modelos al que le pensamos el prompt del usuario y nos dice que modelo es el más adecuado para ocuparse de él: LLM1 (domótica) o LLM2 (tiempo)

Un ejemplo del fichero de entrenamiento:

<s>
  [ORDEN] oye activa  led de la salon
  [COMANDO] LLM1
 </s>
<s>
  [ORDEN] hey como es el dia  en la calle
  [COMANDO] LLM2
 </s>

El comando usado para entrenar este modelo:

./train-text-from-scratch  \
    --vocab-model ./models/llama-2-13b.Q4_K_M.gguf  \
    --ctx 64 --embd 256 --head 8 --layer 16  \
    --checkpoint-in chk-select-256x16x32.gguf  \
    --checkpoint-out chk-select-256x16x32.gguf  \
    --model-out ggml-select-256x16x32-f32.gguf  \
    --train-data "sentencias2LLM.txt" \
    -t 6 -b 64 --seed 1 --adam-iter 1024 

Vamos ha probarlo

./main -m ggml-select-256x16x32-f32.gguf  -r  "</s>" \
    -p "<s>\n [ORDEN] apaga la luz del salon\n [COMANDO]"
....
 <s> 
  [ORDEN] apaga la luz del salon
  [COMANDO] LLM1
 </s>


./main -m ggml-select-256x16x32-f32.gguf  -r "</s>" \
    -p "<s>\n [ORDEN] que tiempo hace\n [COMANDO]"
...
 <s>
  [ORDEN] que tiempo hace
  [COMANDO] LLM2
 </s>

Puede ver todo esto en funcionamiento en el siguiente vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Entrena desde cero tu propio modelo de lenguaje sin tener ni idea de que estás haciendo

¿Quién ha dicho que hace falta saber para entrenar un modelo de lenguaje?

Para entender mejor este post dispones de este Google Colab dónde puedes ver el código y el siguiente video de mi canal de YouTube:

Haz click para ver el vídeo en YouTube

Si usamos llama.cpp podemos entrenar fácilmente un pequeño modelo de lenguaje usando train-text-from-scratch ellos mismos incluyen un ejemplo para entrenarlo usando textos de Shakespeare.

wget https://raw.githubusercontent.com/brunoklein99/deep-learning-notes/master/shakespeare.txt
./train-text-from-scratch \
    --vocab-model ../models/ggml-vocab-llama.gguf \
    --ctx 64 --embd 256 --head 8 --layer 16 \
    --checkpoint-in  chk-shakespeare-256x16-LATEST.gguf \
    --checkpoint-out chk-shakespeare-256x16-ITERATION.gguf \
    --model-out ggml-shakespeare-256x16-f32-ITERATION.gguf \
    --train-data "shakespeare.txt" \
    -t 6 -b 16 --seed 1 --adam-iter 256 \
    --no-checkpointing

Para probar el modelo:

./main -m ggml-shakespeare-256x8x16-f32.gguf

En lugar de usar a Shakespeare vamos a usar algo más de nuestra lengua, El Quijote. Para ello vamos a descargarlo desde aqui.

!wget https://gist.githubusercontent.com/jsdario/6d6c69398cb0c73111e49f1218960f79/raw/8d4fc4548d437e2a7203a5aeeace5477f598827d/el_quijote.txt
./train-text-from-scratch \
    --vocab-model ../models/ggml-vocab-llama.gguf \
    --ctx 64 --embd 256 --head 8 --layer 16 \
    --checkpoint-in  chk-quijote-256x16-LATEST.gguf \
    --checkpoint-out chk-quijote-256x16-ITERATION.gguf \
    --model-out ggml-quijote-256x16-f32-ITERATION.gguf \
    --train-data "el_quijote.txt" \
    -b 16 --seed 1 --adam-iter 256 \
    --no-checkpointing

Si simplemente entrenamos con estos datos veremos que el resultado es horrible, no parece ni castellano. ¿Cuál es el problema?. El tokenizador, su función es dividir el texto en tokens…algo asi como palabras (no exactamente) pero este texto esta lleno de «tildes raras», tildes que realmente no lo son. Puedes probas a abrir el archivo y buscar «á» y verás que no encuentra ninguna, aunque tu puedas ver varias a simple vista. Estos caracteres nos rompen el proceso de tokenizar palabras por lo que el resultado se llena de una jerigonza extraña.

¿De dónde sale este tokenizador? En este caso usamos el que viene con llama.cpp pero podemos usar el de cualquier modelo de lenguaje si lo tenemos en formato .gguf. Esto nos ahorra tener que entrenar el nuestro propio y podemos usar un modelo que sabemos que soporta datos como los de nuestro dataset.

Para solucionarlo vamos a quitar todos las tildes «raras», diéresis y reemplazaremos la ñ por la n. Así nos evitaremos problemas durante la tokenización. Hemos desbloqueado un nuevo logro: Limpiar los datos antes de tratar de procesarlos.

Podéis ver el código usado para limpiar el modelo en el Google Colab punto 2.B

Con esto el modelo genera palabras correctas y algo más parecido al castellano (del Quijote, eso sí)

Sin embargo estos modelos tienen un problema ¡No paran!. Se ponen a generar texto y nunca llegan al punto final (literalmente les pasa eso).

Esto ocurre porque usamos lo que se denominan como «datos no estructurados», se les llama así porqué carecen de una estructura claramente señalizada por signos que las IA puedan aprender del propio texto.

Para jugar con datos estructurados vamos a crear nuestros datos con un algoritmo (además de estructurados son sintéticos al estar generados de forma artificial por un algoritmo). Puedes ver el algoritmo que genera estos datos en el punto 2.C del Google Colab.

La estructura de datos que vamos a usar es la siguiente

...
<s>
 [ORDEN] escucha el led del oficina desenchufala
 [COMANDO] LUZ_DP OFF
</s>
<s>
 [ORDEN]  desconecta la aire el 
 [COMANDO] AC OFF
</s>
<s>
 [ORDEN]  enciende  luz la salon
 [COMANDO] LUZ_SL ON
</s>
...

<s> – Indica inicio del la sentencia
</s> – Indica fin del la sentencia
[ORDEN] – Indica la orden que le damos
[COMANDO] – Indica el comando que el responde

Con esto ya tenemos datos estructurados.

La idea es simular órdenes a un agente que encienda o apague las luces de la casa .

El comando para entrenar el modelo:

./train-text-from-scratch  \
  --vocab-model ./models/ggml-vocab-llama.gguf \
  --ctx 64 --embd 256 --head 16 --layer 32  \
  --checkpoint-in chk-onoff-256x16x32.gguf \
  --checkpoint-out chk-onoff-256x16x32.gguf \
  --model-out ggml-onoff-256x16x32-f32.gguf \ 
  --train-data "sentencias.txt" -b 64 \
  --seed 1 --adam-iter 512 --no-checkpointing

Para mejorar el resultado he duplicado el número de cabezas y capas. Y he ampliado el tamaño del batch para que coincida con el del contexto y que ambos sean suficientes para que quepa cada uno de los ejemplos completo.

El truco para que el modelo termine es indicarle a llama.cpp que </s> significa que ha terminado de generar la sentencia y que pare de generar texto.

./main -m ggml-onoff-256x16x32-f32.gguf  \
  -p "<s>\n [ORDEN] enciende la luz del salon\n [COMANDO]" -r "</s>"

Si el agente ha aprendido correctamente la estructura de los datos los cerrará con el símbolo de fin de sentencia y todo funcionará correctamente.

Usar Stable Diffusion con pocos recursos de hardware

Cuando aparecieron los primeros modelos de texto a imagen requerían un hardware potente y mucha paciencia para obtener resultados que en esa época eran sorprendentes (actualmente los consideraríamos como «malos resultados»). Esos primeros modelos basados en VQGAN requerían varios minutos usando hardware potente, Google Colab mediante, para lograr generar una imagen. Con los modelos de difusión la cosa mejoró, sin embargo seguíamos necesitando GPUs con gran cantidad de memoria RAM. Pero la comunidad de software libre está haciendo un gran esfuerzo por lograr que se ejecute en hardware cada vez más modesto.

¿Hasta qué punto se ha optimizado Stable Diffusion? ¿Podemos usarlo en un ordenador normalito?.

Primero, uso Stable Diffusion porque al ser código libre ha permitido a la gente «trastear» con él, ventajas del Open Source.

Al empezar a mirar opciones he descubierto que hay muchísimos proyectos que tratan de reducir los recursos necesarios para ejecutar SD. Al final me he decantado por el proyecto stable-diffusion.cpp por los siguientes motivos:

  • Es un proyecto real que trata de hacer SD usable con muchas de sus características. Muchos de los otros proyectos son poco más que demos técnicas muy limitadas.
  • Esta en continuo desarrollo.
  • Busca un equilibrio entre consumo de recursos y usabilidad.

Antes de empezar con las pruebas hemos de entender cómo funciona SD. Para ello usaremos el siguiente esquema:

SD consta principalmente de tres partes:

  • Text Encoder: convierte el prompt en embeddings que usa para condicionar el funcionamiento de la Unet
  • Unet: es alimentada con una imagen llena de ruido y su trabajo es eliminarlo, condicionado por el text encoder, para obtener la imagen. En este paso SD no trabaja con la imagen final si no con una descripción en el espacio latente de la misma
  • V.A.E.: su función es convertir la descripción del paso anterior en una imagen.

Este es el funcionamiento para generar una imagen a partir de un prompt. Si queremos generarla cuando una imagen de partida, además del prompt, lo que se hace es que en lugar de pasar a la Unet una imagen llena de ruido se le pasa la imagen de partida y se le añade ruido. De esta manera la imagen influirá sobre el resultado final.

Sabiendo esto vamos a ver que «trucos» usa stable-diffusion.cpp para conseguir optimizar la ejcución de SD:

  • Esta programado en C++ buscando exprimir al máximo el rendimiento del programa
  • Usa un versión cuantizada del modelo. En mis pruebas usar la versión Q4_1 que usa unos 4 bits por parámetro. Mientras que el modelo original usa 32 bits.
  • Usar la versión mini y nano de SD que generan imágenes mas pequeñas. Este truco lo añado yo después de verlo comentado en el proyecto.

Tenéis un vídeo con detalles de la pruebas y de la tabla de resultados en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Para las pruebas he usado los siguientes modelos:

El comando usado ha sido:

./bin/sd -m [modelo] -H 768 -W 768 -p «A sheep with sunglasses riding a motorbike»

Se ha probado con diferentes tamaños de imagen según el modelo empleado:

ModeloDim.ClipUnetVAEMem.Tiempo
1.4 – Q4_151273.80/87.211286.35/1857.9494.51/1768.951952.45199.92
1.5 – Q4_151273.80/87.211286.35/1857.9494.51/1768.951952.45218.39
2.1 nano – Q4_1512211.55/226.211290.15/1669.8294.51/1768.951768.65254.42
2.1 – Q4_1512211.55/226.211290.15/1669.8294.51/1768.951768.65179.76
2.1 – Q4_1768211.55/226.211290.15/3021.5494.51/3848.653848.65543.88
1.4 mini – Q4_125673.80/87.211286.35/1346.9494.51/520.651468.0757.44
2.1 nano – Q4_1128211.55/226.211290.15/1314.9194.51/208.651610.8815.62
2.1 – f325121346.65/1360.932179.92/2553.3394.51/1768.653653.36181.84
2.1 – f327681346.65/1360.932179.92/3897.2494.51/3848.653991.75566.58
Mem., Clip, Unet y VAE indican el tamaño en MB
Clip, Unet y VAE indican el tamaño de: parámetros / total memoria en ejecución
Dim. indica el tamaño en pixeles de la imagen Dim x Dim
Tiempo indica la duración de la creación en segundos

Las pruebas han sido realizadas en un portátil con un procesador AMD Ryzen 7 5800H con 16 GB de memoria RAM.

Como mención honorifica cabe destacar otro proyecto: OnnxStream que logra ejecutar SD en una Raspberry Pi 2 Zero que solo tiene 512MB (¡Y le sobran casi 200 MB!) de memoria RAM

¿Qué son los embeddings y qué tienen que ver con las bases de datos vectoriales?

Últimamente se habla mucho del uso de bases de datos vectoriales combinadas con grandes modelos de lenguaje. ¿Pero que relación tienen?. El punto en común entre ambos son los embeddings. ¿Qué es un embedding?. Un embedding es un vector que representa la descripción del significado de un texto (en este caso) en un espacio de múltiples dimensiones. Este espacio establece una relación entre los diversos significados de los textos, de tal forma que el angulo formado por dos vectores indica lo próximos que sus significados están.

Para calcular lo parecidos que son dos vectores usaremos la similitud coseno (cos_sim):
u ⋅ v /∣u∣∣v∣
Siendo u y v vectores. 1 significa que son idénticos, 0 que no tienen relación y -1 que son contrarios.

Los embeddings son usados en los modelos de lenguaje para representar el texto de forma interna y trabajar con él en forma de vectores (no confundir con los wordvectors, aunque la idea es la misma y están relacionados los wordvectors funcionan a nivel de token…si no sabes lo que es un token quédate con que funcionan a nivel de palabra).

Para estas pruebas usaremos el modelo de lenguaje BERT a través de la librería sentence-transformers.

Primero cargamos las librerías que necesitamos y el modelo

import numpy as np
from sentence_transformers import SentenceTransformer, util

#Cargamos el modelo
sbert_model = SentenceTransformer('all-mpnet-base-v2')

Estas son las sentencias con las que vamos a comparar nuestro texto de búsqueda. Las sentencias han sido obtenidas de la Wikipedia en inglés, en concreto la primera del articulo de mecánica cuántica y las siguientes de la definición de física clásica.

sentences = [
    "a fundamental theory in physics that provides a description of the physical properties of nature at the scale of atoms and subatomic particles.",
    "refers to theories of physics that do not use the quantisation paradigm, which includes classical mechanics and relativity.",
    "physical objects ranging from those larger than atoms and molecules, to objects in the macroscopic and astronomical realm",
    "in the context of general and special relativity, classical theories are those that obey Galilean relativity."
]

Buscamos *Quantum mechanics*, deberia de dar como mayor similitud el primer texto. Fijaros en la trampa de que el segundo incluye *quantisation paradigm* para «engañar» al modelo y ver si es capaz de buscar por «significado» y no solo por «similitud de las palabras»

El resultado:

similarity = tensor([[0.5077]])
similarity = tensor([[0.4847]])
similarity = tensor([[0.4180]])
similarity = tensor([[0.2300]])

Correcto el código más cercano a 1 es el primero.

Una pequeña optimización que se puede hacer si vas a almacenar estos vectores para compararlos varias veces. Podemos normalizar los vectores, esto significa que le damos longitud uno sin modificar la dirección del mismo, podemos usar el producto escalar (dot_score), mucho más rápido que la similitud coseno.

Si partimos de la similitud coseno u ⋅ v /∣u∣∣v∣ Al tener distancia 1 los módulos || son 1 por lo que el divisor desaparece y queda solo el producto escalar u ⋅ v

sentence_embeddings = sbert_model.encode(sentences)

query = "Quantum mechanics"
query_embedding = sbert_model.encode([query], normalize_embeddings = True)

for sentece in sentences:
  sim = util.dot_score(query_embedding, sbert_model.encode([sentece], normalize_embeddings = True))
  print("similarity = ", sim)

¿Pero que tiene esto que ver con las bases e datos vectorariales?. Una base de datos vectorial funciona básicamente como el ejemplo (en realidad usa diferentes operaciones especializadas para diversos casos). Asocia un vector con los datos originales a los que representa y permite buscar a partir de un texto, que es convertido en embedding.

Lo bueno de los embeddings es que no solo puede representar texto, usando modelos multimodales se pueden usar embeddings que provengan de imágenes, audios, vídeos, … Lo que permite cruzar datos heterogéneos.

Esta flexibilidad hace que muchas veces puedan cumplir funciones como búsqueda inteligente de textos, responder preguntas, buscar referencias o datos relacionados que a su vez se pueden combinar con los modelos de lenguaje aportando estos datos al contexto del mismo. Un ejemplo de este eso es su uso en técnicas como RAG descrito en esta entrada del blog

Puedes ver todo esto descrito en el siguiente vídeo de mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Crear audio, sonidos y música, a partir de prompts usando IA

En este post vamos a usar audiocraft la IAs que ha publicado Meta centradas en generar sonidos y música a partir de descripciones de texto (prompts). Para ello usaremos el código publicado por Meta en su repositorio público de Github

Audiogen

Esta IA sirve para generar sonidos a partir de una descripción de los mismos.

En este caso solo disponemos de un modelo:

facebook/audiogen-medium: 1.5B texto a audio

Para esta IA vamos a usar un colab propio creado a partir del README de su repositorio.

En el siguiente vídeo de mi canal de Youtube están descritos los pasos para usarlo:

Haz click para ver el vídeo en mi canal de Youtube

Musicgen

Musicgen permite crear música de dos formas: a partir de una descripción en texto y además de la descripción usando una melodía como base (solo con el modelo musicgen-melody).

En este caso la demo está mucho más trabajada que en el caso anterior y cuanta con una completa interfaz gráfica que permite probar las diferentes opciones. Usaremos el colab que viene incluido en el README de su repositorio.

Contamos con 4 modelos diferentes:

facebook/musicgen-small: 300M, texto a música
facebook/musicgen-medium: 1.5B, texto a música
facebook/musicgen-melody: 1.5B, texto a música y texto+melodia a música
facebook/musicgen-large: 3.3B, texto a música

En el siguiente vídeo de mi canal de Youtube están descritos los pasos para usarlo:

Haz click para ver el vídeo en mi canal de Youtube

¿Qué significan los parámetros de configuración de los LLM?

Cuando empiezas a trastear con los LLM descubres que se pueden configurar diversos parámetros, por desgracia su significado y utilidad muchas veces no esta muy claro, para solucionar eso voy a tratar de explicar los distintos parámetros que se pueden configurar habitualmente en los LLM. Mi intención no es dar una explicación detallada de como funciona cada uno, sino explicar de forma intuitiva su uso. He usado el nombre (o nombres) en inglés más habituales, aunque es posible que se llamen de forma un poco diferente según la librería que uses para ejecutar los modelos. Igualmente, cuales se pueden usar depende de la librería y del modelo de lenguaje que uses. Al final puedes encontrar un vídeo con una explicación más detallada de los parámetros más importantes. Empecemos:

Prompt Template: Establece la plantilla que se usara en la conversación con un cahtbot. En ella se indicara donde se inserta el prompt del usuario. Es importante usar la plantilla adecuada para el modelo de LLM que usamos. en algunos casos la plantilla esta dividida en prefix (prefijo) y (sufix) sufijo.

System Prompt: Es el prompt inicial que se le pasa al LLM en el se le indica como que tiene que hacer.

Context Size: indica el tamaño del contexto en tokens. Es la cantidad de tokens con la que el modelo puede trabajar.

Prompt Batch Size: Es una técnica de optimizan que divide el prompt en bloques del tamaño indicado para alimentar el LLM. Un tamaño demasiado pequeño puede afectar a la calidad del resultado

RoPE-scaling (rotary position embeddings scaling): se refiere a una técnica para aumentar el tamaño del contexto. Indica por cuánto se multiplica el tamaño del mismo.

Number of Tokens to Predict: Números de tokens a generar. ¡Ojo! Eso no quiere decir que la respuesta vaya a tener ese número exacto de tokens. Es posible que sea menor ya que existe un token End-of-Sequence (EOS) que indica que el texto termina ahí. Algunos modelos permiten ignorar este token, aunque no se garantiza que el texto que siga tenga demasiado sentido.

Temperature: Controla «lo aleatorio» que es el proceso para elegir la siguiente palabra. A mayor valor más «creativo» será el texto generado. Si su valor es 0 elegirá siempre el token más probable mientras que valores mayores hará que pueda elegir entre otros tokens menos probables. Se puede usar para equilibrar la coherencia y la creatividad.

Repeat penalty / Presence penalty / Frequency Penalty: se utiliza para evitar que el modelo repita las mismas palabras con demasiada frecuencia en el texto generado. Es un valor que se resta a la probabilidad de elegir un token cada vez que ocurre en el texto generado. Un valor elevado hará que el modelo sea menos propenso a repetir tokens. La diferencia entre presence y frequency es que el primero solo valora si el token está o no, frecuency acumula valor por cada vez que aparece el token
En algunos modelos se pueden usar número negativos para conseguir el efecto contrario.
Suele tener otro parámetro para indicar cuántos de los últimos tokens se tienen en cuenta.

Top K Sampling: es un método que selecciona el siguiente token de de un subconjunto formado por los k tokens más probables. A menor valor tenga más predecible será el texto generado.

Top P Sampling: similar a Top K, con la diferencia de que selecciona el siguiente token de un subconjunto de tokens que juntos tienen una probabilidad acumulada de al menos p.

Tail Free Sampling (TFS): La misma idea que Top P pero más refinada intenta no meter tokens con muy baja probabilidad dentro del subconjunto del tokens entre lo que se elige el siguiente. Su valor es entre 0 y 1. Si el valor es 1, TFS no influye en el resultado (podríamos decir que esta desactivado). Lo habitual son valores entre 0.9 y 0.95.

Classifier-Free Guidance (CFG): Es una técnica, que proviene de los mecanismos de difusión usados en la generación de imágenes, para ayudar a mantener el contenido generado próximo al prompt. En este caso se usa un contexto para guiar la generación, suele ser la última palabra del prompt o el System promtp. También permite prompts negativos con los LLMs

Logit Bias: Permite aumentar/disminuir la probabilidad de ciertos tokens.

RNG Seed / Seed: Establece la semilla con la que se inicializa el generador de números aleatorios. Permite obtener resultados repetibles usando la misma semilla.

Puede ver esto mismo explicado en vídeo haciendo click en la imagen:

Haz click para ver el vídeo en mi canal de Youtube

Clonar tu voz en otro idioma usando bark

Investigando sobre Bark, proyecto del que ya hemos hablado en este blog. Descubrí que hay un proyecto que permite clonar voces para usarlas en Bark.

La idea me entusiasmo, sin embargo surgió un problema, no había un modelo en español sobre el que clonar mi voz. Resumiendo mucho, cuando «clonas» una voz a partir de un breve audio lo que hace el programa es extraer las características de tu voz y aplicarlas a un modelo ya existente. Por eso el resultado de «clonar» es tan dispar según la voz que se trate de replicar. Al no tener un modelo es español tuve que usar uno en inglés, el resultado es una voz inglesa pero «que suena» como la mía.

El proyecto original tiene una notebook en python para ejecutar: clone_voice.ipynb. Está pensado para ejecutarlo en local. Más adelante explico que pasos hacer para ejecutar ese notebook en VSC. Pero he creado una versión que se ejecuta en Google Colab, lo puedes encontrar aqui.

Consejos para grabar tu voz

Es necesario crear un pequeño fichero audio.wav del que poder clonar la voz.

  • Audios de no más de 13 segundos
  • Formato .wav PCM 16 bits (a mí me ha funcionado, es posible que acepte más)
  • Lo recomendado son 9 – 10 segundos
  • Habla claro, tranquilo y vocaliza bien
  • Reduce, en lo posible, los ruidos de fondo
  • Que no haya más de una voz en el audio

Puedes ver un ejemplo de como usar el Google Colab en el siguiente vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Instalar y ejecutar en local

Veamos los pasos para instalarlo en local

git clone https://github.com/serp-ai/bark-with-voice-clone.git
cd bark-with-voice-clone
pip install git+https://github.com/suno-ai/bark.git
git clone https://github.com/gitmylo/bark-voice-cloning-HuBERT-quantizer/  
cd bark-voice-cloning-HuBERT-quantizer
pip install -r requirements.txt
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117  
cd ..

Para clonar una voz debemos usar el cuaderno de Python: clone_voice.ipynb Que se encuentra en el directorio raíz del proyecto.

Si no sabes como ejecutarlo puedes usar Visual Studio Code, el propio IDE te sugerirá que plugins necesitas instalar para ejecutarlo.

Si tienes una tarjeta gráfica que no sea de NVIDIA, que no soporte CUDA o que tenga menos de 12Gb de VRAM puedes ejecutarlo usando exclusivamente CPU (debes disponer de 12 Gb de memoria RAM libres).  Para que todo funcione usando la CPU debes cambiar la siguiente línea:

#device = 'cuda' # or 'cpu'
device = 'cpu'

Debo avisar que ejecutarlos sobre CPU hace que tarde unas dos horas (portátil con AMD Ryzen 7 – 50xx) durante las cuales el ventilador hace ruido de que quiere despegar.

Crea un asistente para consultarle dudas de tus proyectos de software

Uno de los problemas que tengo con copilot y otros asistentes para escribir código es lo mal que se les da crear código para proyectos  grandes. Si quieres hacer un proyecto pequeño o tienes un problema con un algoritmo aislado van genial. Pero cuando tienes un proyecto grande con librerias y métodos ya creados, tiende a ignorarlos y usar otras librerías. Vamos a crearnos un asistente, que no va a generar código pero va a ayudarnos a programar proyectos ya existentes. Le preguntaremos nuestras dudas y el nos responderá.

Ya vimos la arquitectura Retrieval Augmented Generation (RAG) en otro post. Vamos a partir de ese punto. Recapitulo rápidamente. Tenemos un listado de documentos que usaremos para complementar el prompt que crea el usuario. Para ello se usa alguna técnica para extraer información de los documentos a partir del prompt de usuario. Con esto se da un «contexto» que permite al LLM dar una mejor respuesta.

Con la arquitectura anterior ya hecha puede parecer muy sencillo hacer esto . Ponemos el código como documentos y listo. El problema de esto es que es difícil que añadiendo solo el código sea capaz de responderte a tus dudas. Pensar que realmente el LLM no tiene acceso a todo tu código. Solo a una parte, la que se le pasa como contexto.

La forma de mejorar esto es meter otro LLM y un prompt. En mi caso la idea es pedirle que comenté el código. Y el resultado usarlo como documentos sobre lo que usar RAG. Al estar el código comentado en lenguaje escrito es más fácil que responda a las preguntas.

Arquitectura del sistema

De la idea a la práctica

Ya hemos visto la idea, ahora toca ver cómo la convertimos en realidad.

Mi idea era usar Stable Vicuna 13B en mi máquina local como LLM para ambas tareas. Pero su rendimiento es terrible en mi máquina (lo ejecuto sin CPU). Así que para comentar el código use ChatGPT 3.5.

Por desgracia un fichero entero de código rara vez cabe. Para ello dividí el código en trozos. Hay que asegurarse de que no se parte una función por la mitad (lo correcto sería usar alguna librería para ello, yo lo he hecho a mano).

Para ello le pasó el siguiente prompt:

Te voy a pasar fragmentos de código de un mismo proyecto, 
añade comentarios en español explicando su funcionamiento. 
Sigue las siguientes reglas:
1. Comenta las lineas que sean interesantes
2. Comentas la función de cada variable
3. Comenta antes de cada función que es lo que hace
4. Se breve
Por ejemplo:
var f = 0; //variable que indica hasta que numero de fibonacci se calcula
/*Toma un número n como argumento y devuelve un array 
con la secuencia de Fibonacci de longitud n. 
Si n es igual o menor que 0, devuelve un array vacío.*/
function fibonacci(n) {
    if (n <= 0) {
        return [];
    } else if (n === 1) {
        return [0];
    } else if (n === 2) {
        return [0, 1];
    } else {
        // Inicializa el array con los dos primeros elementos de la secuencia  
        var sequence = [0, 1];
        // Itera desde el tercer elemento hasta el n-ésimo elemento de la secuencia
        for (var i = 2; i < n; i++) {
            // Calcula el siguiente número de Fibonacci sumando los dos números anteriores
            var nextNumber = sequence[i - 1] + sequence[i - 2];
            // Agrega el siguiente número a la secuencia
            sequence.push(nextNumber);
        }
        // Devuelve la secuencia completa de Fibonacci
        return sequence;
    }
}
//Calcula el número de fibbonaci f
fibonacci(f);

Ignoró la respuesta y paso luego cada bloque de código. Voy juntando las respuestas en un fichero con el nombre del original pero terminado en .txt. Por ejemplo, si proceso el fichero HolaMundo.cpp obtendré el fichero HolaMundo.cpp.txt. Esto lo hago para poder saber de qué fichero vienen las citas elegidas (es una pista de por donde buscar si la respuesta ni me satisface del todo). Es posible que el fichero obtenido este lleno de errores (comentarios mal cerrados, faltan llaves, cosas así) da igual, lo queremos solo como documentación.

También se puede añadir cualquier documentación. Si está en otro idioma se podría usar ChatGPT para traducirla. Recordar que el algoritmo que elige que fragmentos se añaden al contexto no es tan «listo» como un LLM y hay que ponerle las cosas fáciles.

Con todos los documentos preparados ya podemos añadirlos a nuestro RAG. En mi caso uso GPT4all como ya expliqué en este otro post. Ahora tengo un chatbot al que puedo preguntar sobre mi código.

Resultados

Suficiente, es una herramienta útil, no es perfecta. Unas veces da respuestas sorprendentemente ingeniosas, otras sorprendentemente estúpidas. Lo normal es que sirva de ayuda y si no te da la respuesta te da una buena pista. Si te enfrentas a un proyecto mal documentado (o demasiado grande para estudiarlo en profundidad) puede ayudarte.

Una sorpresa que me he encontrado es lo útil que resultan las citas que incluyen las respuestas. Al saber de qué fichero provienen las citas, te puedes ahorrar mucho tiempo de buscar por el código (por eso mantengo el nombre original del archivo y añado la extensión «.txt»)

Puedes ver un vídeo sobre este proceso en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Integrar tus documentos en las conversaciones con un chatbot – Retrieval Augmented Generation con GPT4All

Sería genial poder introducir nuestros documentos dentro de la información que posee un chatbot, como ChatGPT, para obtener respuestas más personalizadas. Si bien no es posible hacerlo de forma directa hay una manera de incluir la información de estos documentos en lo que le pedimos a nuestros chatbots. Este sistema se conoce como «Retrieval Augmented Generation».

Este sistema funciona con cuatro pasos:

  1. El usuario escribe el prompt y lo envia.
  2. El prompt, en lugar de ir directamente al chatbot, pasa por un sistema que partiendo del texto del prompt y buscando en una base de datos o de documentos, recupera información relevante sobre este prompt.
  3. Incluye los fragmentos más relevantes de estos documentos como contexto del prompt y se envía al chatbot que procesa esta petición.
  4. El chatbot genera una respuesta y la devuelve al usuario

El paso dos se puede realizar de múltiples maneras, algo muy habitual es usar los embeddings de los documentos para realizar una búsqueda por proximidad con el embedding del prompt. El problema que tiene esto es que generar embeddings suele ser muy costoso. Por lo tanto se pueden usar otros sistemas que en muchos casos pueden funcionar mejor. Por ejemplo, si tú ya tienes una base de datos organizada o ya tienes los documentos estructurados, una simplemente búsqueda por palabras relevantes o como tf-idf que son algoritmos clásicos y bien usados pueden dar resultados tan buenos o incluso mejores como una búsqueda usando embeddings.

Se podría pensar que entrenar un chatbot con los datos que tiene nuestros documentos
podría ser una mejor solución. El problema es que entrenar un chatbot puede llevar muchísima cantidad de tiempo, mientras que simplemente tener los documentos y usar algún algoritmo de búsqueda o preprocesar estos para algún algoritmo de búsqueda simple puede ser bastante menos costoso, además de que podemos tenerlo optimizado prácticamente en tiempo real, cosa que si tuvieramos que entrenar un chatbot sería imposible, ya que ahora mismo entrenar un chatbot requiere bastantes horas, incluso días.

Hay que tener cuidado con este sistema, aunque los documentos sigan en tu local, estás enviando parte de los mismos al chatbot, en el caso de que sean chatbots en una nube de terceros. Hay que tomar las medidas adecuadas para evitar filtraciones.

Configuración de documentos locales en GPT4All

Si queremos probar en local esta técnica podemos usar GPT4All siguiendo los siguientes pasos:

  • Descargue e instale la última versión de GPT4All Chat desde el sitio web oficial de GPT4All.
  • Una vez instalado, abra la aplicación y diríjase a la pestaña de Configuración (la rueda dentada).
  • En la sección de Configuración, busque la opción denominada «LocalDocs» y haga clic en ella. A continuación, deberá configurar una colección o carpeta en su computadora que contenga los archivos a los que desea que su chatbot tenga acceso. Puede crear una nueva carpeta o utilizar una existente. Es importante destacar que puede modificar el contenido de esta carpeta en cualquier momento según sus necesidades, estos cambios se reflejaran automáticamente en los datos que se pasan al chatbot.
  • Ahora, puede iniciar una sesión de chat , en la parte superior derecha de la interfaz, encontrará un icono que representa una base de datos.
  • Haga clic en este icono y aparecerá un menú desplegable donde podrá seleccionar la colección que desea que su chatbot conozca durante esa sesión de chat .

Si durante la conversación en el prompt haces referencia al contenido de los documentos en local en el resultado se mostrará una cita a los datos extraídos.

Puedes ver un vídeo con ejemplos en mi canal de Youtube:

Haz click para ver el vídeo