LLM 101 Introducción

Vamos a hacer una serie de preguntas rápidas para ayudar a los alumnos a empezar a trabajar con modelos grandes del lenguaje o LLM (Large Language Model)

¿Que es un modelo grande del lenguaje o LLM del inglés Large Language Model?

Es una red neuronal profunda (con muchas capas) diseñadas para procesar lenguaje natural (NLP del inglés natural language processing). Esto es, entender y generar lenguaje natural escrito. Como todas las redes neuronales, hay que entrenarlas. Una LLM se entrena con mucho texto y “aprende” patrones complejos del lenguaje.

¿Por qué se han puesto de moda?

Hace unos años, unas arquitecturas llamadas Transformer y sobre todo, el entrenamiento masivo con toda la información que hay disponible en Internet ha hecho que los LLM tengan unas prestaciones increíbles en tareas concretas, mas allá de las que originalmente fueron diseñadas. chatGPT (Generative Pre-trained Transformer) de OpenAI y BERT (Bidrectional Encoder Representations form Transformer) de google son las arquitecturas mas famosas. Una descripción de la arquitectura transformer puedes verla aquí

¿Pero un LLM es un programa que ejecuto en mi ordenador?

No exactamente, es un archivo binario que contiene la configuración de la red neuronal después del entrenamiento, esto es, los “pesos” de cada neurona, de cada capa, calculados en el proceso de aprendizaje.
Los modelos pueden tener distintos formatos, generalmente indicados en el nombre. Aunque no hay un estándar de nombrado, se detectan patrones en el nombrado de los modelos. Por ejemplo, SynthIA-7B-v2.0-16k-AWQ es un modelo entrenado con 7B, siete billones de parámetros, en su versión 2.0 y con un tamaño de contexto de 16K. La AWQ indica el formato y la precisión en el cual se han almacenado los “pesos” de cada neurona.

¿Qué es el tamaño del contexto?

Una cosa es el modelo LLM y otra cosa son sus parámetros. El tamaño del contexto es el número máximo de tokens (palabras) que le puedes pasar al LLM. Lo define la arquitectura del LLM y generalmente, a mayor tamaño del contexto, mejor se puede especificar la tarea que le pides al LLM por lo que generará cosas mas precisas. También es cierto que generalmente, implica mas memoria. Es el prompt que le metes a chatGPT para que te responda.

Vale, pero ¿Cómo ejecuto un LLM en mi máquina?

Bien, tienes que cargar el modelo binario en memoria para poder introducirles contexto (el famoso prompt) y obtener lo que genera. Los modelos pueden ser muy grandes por lo que los requisitos hardware son exigentes. Dicho esto, hay librerías en varios lenguajes de programación que te permiten cargar en memoria e interactuar con LLMs mediante un sencillo programa.
Uno de los mejores recursos para trabajar en este campo y en IA en general es la web https:/huggingface.co , que tiene librerías, modelos, datasets, papers, etc. la guía de uso de esta página es un buen primer comienzo.

¿Me puedes poner un ejemplo?

El código python de mas abajo carga un modelo un modelo LLM de código abierto modesto, que se baja de la web https://huggingface.co/ e interacciona con él introduciéndoles prompts (el contexto que hablábamos antes) y obteniendo las respuestas. Realmente trata de predecir la siguiente palabra al prompt.

import time
import torch
import transformers
import accelerate

from transformers import AutoTokenizer, AutoModelForCausalLM

print("Loading model...")
print("transformer version: ", transformers.__version__)
print("accelerate version: ", accelerate.__version__)
timeStart = time.time()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

hugginfacemodelname = "openchat/openchat-3.5-1210"
tokenizer = AutoTokenizer.from_pretrained(hugginfacemodelname)
model = AutoModelForCausalLM.from_pretrained(
    hugginfacemodelname,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
).to(device)

print("Load model time: ", -timeStart + time.time())

while True:
    input_str = input("Enter: ")
    input_token_length = input("Enter length: ")
    if input_str == "exit":
        break
    timeStart = time.time()
    inputs = tokenizer.encode(input_str, return_tensors="pt").to(device)
    outputs = model.generate(
        inputs,
        max_new_tokens=int(input_token_length),
    )
    output_str = tokenizer.decode(outputs[0])
    print(output_str)
    print("Time taken: ", -timeStart + time.time())

El código utiliza la librería python transformers y torch. En primer lugar se indica el modelo LLM a cargar, openchat/openchat-3.5-1210 y se genera el tokenizer que no es sino una herramienta que convierte el texto en tokens (palabras). Para el modelo que hemos especificado, la función “from_pretained” busca y carga el tokenizador correspondiente. Si se lo tiene que bajar, esto puede tardar. A continuación, con AutoModelForCausalLM.from_pretained.., se carga el modelo de lenguaje causal, un tipo de modelo que predice la siguiente palabra en una secuencia.

¿Hay mas tipos de modelos?

Si, en esa librería hay modelos específicos para pregunta-respuesta, para clasificación de imágenes, multimodales, etc. al cual le tienes que indicar qué modelo quieres entrenar.

¿El prompt o la entrada del LLM cómo se articula?

Asociado al concepto de LLM aparece el concepto de prompt engineering o ingeniería de prompts. Es importante destacar que un buen prompt o cadena de prompts puede mejorar mucho la salida de una determinada LLM en una tarea específica. En los últimos años aparece una gran cantidad de métodos para elaborar prompts:
Referencias: https://dl.acm.org/doi/pdf/10.1145/3560815

¿ Puedo entrenar un LLM en lo que yo quiera?

En principio si. Entrenar un LLM desde cero requiere de muchos recursos. La mayoría de los ingenieros hacen un proceso denominado finetunning que no es otra cosa que coger un modelo LLM ya entrado de forma genérica y aplicarles un proceso de entrenamiento con un conjunto de datos específico de la tarea donde quiero usar mi LLM. El resultado es un modelo LLM que tiene el conocimiento general y se ha especializado mediante lo que ha aprendido con los patrones del dataset con el cual has hecho el finetunning. Un ejemplo muy bueno que aparece en los vídeos de formación de google relacionados con LLM, es ver el finetunning similar al proceso de enseñarle a una mascota, por ejemplo un perro, una serie de tareas específicas (buscar droga, personas, órdenes, etc.). Tienes el perro, siendo en este simil, la LLM generalista, y mediante un proceso de entrenamiento (finetunning), obtienes una LLM que es mejor que la generalista en un proceso específico.

Otra técnica muy adecuada es el Retrieval Augmented Generation o RAG, que no es sino mejorar las respuestas de LLM en un dominio específico proporcionándole información específica de ese dominio. Lo que se hace es recuperar información relevante (de fuentes internas o externas) para la pregunta e introducirla en el contexto/prompt junto con la pregunta para obtener mejores respuestas. Realmente todo este proceso de recuperación lo que hace es mejorar el prompt.

Finetunning

En este enlace podemos ver los principios básicos de finetunning:
hay tres aproximaciones al finetuning

  1. Self-supervised learning
  2. Supervised learning
  3. Reinforcement learning

¿Por qué ocupa tanto una LLM?

Una red neuronal almacena pesos calculados en el proceso de entrenamiento, como toda
variable, esos pesos pueden almacenarse en diverso formatos (punto flotante vs entero) y con determinada precisión (64,32,16 bits). La precisión es importante por que es lo que almacena los patrones aprendidos.

¿Qué puedo hacer para reducir el tamaño de una LLM?

Hay un proceso, denominado cuantización, que pasa de una determinada precisión a otra. Si pasas de una precisión en punto flotante de 16 bits a un entero de 8 bits, esto hace que el modelo ocupe menos en el disco duro a costa de perder información/patrones. Es decir, el modelo es algo peor. Hay dos tipos de cuantización, simétrica y asimétrica Mas info: https://myscale.com/blog/es/quantization-for-llms-finetuning/

En el siguiente ejemplo podemos ver un ejemplo básico de finetunning para evaluar sentimiento en comentarios de películas. El ejemplo es muy sencillo pero completo, en primer lugar se evalúa con un pequeño testbench la LLM escogida, una de las mas sencillas, se entrena con un dataset de comentarios etiquetados y se vuelve a evaluar para ver si ha “aprendido”. https://towardsdatascience.com/fine-tuning-large-language-models-llms-23473d763b91

¿Cómo evaluamos un LLM?

Cómo sabemos si una LLM es buena o malo y qué podemos probar para ver si hemos mejorado. Bueno, aquí entra en juego la evaluación de las redes neuronales en general. Es decir, evaluar las prestaciones en base a una tarea específica pasa inherentemente por poder evaluar cómo de bueno son los resultados obtenidos ante prompts especificos.

Dos paradigmas populares, para la evaluación genérica de LLMs:

  1. El tradicional NLP Benchmark approach usando, por ejemplo, MosaicML’s Eval Gauntlet.
  2. Otra técnica muy utilizada es , precisamente, utilizar una LLM como juez.

¿Qué LLM es mejor?

Con la aparición de múltiples LLMs han aparecido numerosas métricas y rankings en diferentes testbench. Aquí puedes ver el ranking de hugging face: link

Herramientas y recursos

https://pinokio.computer/ una WEBUI para instalar y lidiar con modelos de AI.
https://huggingface.co/TheBloke Distintos modelos y parámetros para poder ser ejecutados en máquinas mas modestas
https://github.com/jmorganca/ollama herramienta para ejecutar LLM de forma local y hacerle un wrapper python que te permita adaptar los prompts/salidas.

Terminología:

Cuantización: Reduce la precisión de los números utilizados para representar los pesos en el modelo, pierdes precisión pero el tamaño y la inferencia se acelera sin necesitar tantos recursos.
GPTQ : formato de cuantización
-AWQ : formato de cuantización
-GGUF : formato de modelos que reemplaza a GGML.
Tamaño del contexto: la cantidad de tokens (normalmente palabras ) que un modelo puede considerar en un prompt de una vez cuando genera una respuesta o predicción.
Formatos de los modelos models in open AI book
– Destilación (Distillation) es el proceso de entrenar un modelo pequeño (llamado estudiante) para emular las respuestas de un modelo mas grande (profesor).
-Pruning es el proceso de elminar las salidas no críticas del modelo pequeño o estudiante.
-Quantization es el proceso de reducir la precisión de los pesos del modelo y las activaciones.

Bibliografía

Open source LLM
XGen-7B Technical Report un LLM open source con un prompt de hasta 8K

Creando un nuevo módulo en NS3

[Actualizado a cmake, versión 3.36.1 (documentación oficial ns3.36)]
Crear un nuevo protocolo o algoritmo en NS3 es un paso que implica la creación de un nuevo módulo. Obviamente, a partir del tipo de protocolo o algoritmo a implementar, los pasos a seguir y los protocolos que nos servirán como referencia cambiarán. En cualquier caso, siempre necesitaremos generar un nuevo módulo e incluirlo en la compilación de NS3 para poder hacer simulaciones con posterioridad.

Para crear un nuevo módulo, NS3 nos proporciona un script python, create-module.py, dentro de la carpeta src de nuestra instalación de ns3.
Si por ejemplo queremos crear un nuevo protocolo inalámbrico, pongamos el protocolo weightlessp, si ejecutamos:

$utils/create-module.py weightless
Creating module /home/felix/tools/ns-3-dev-git/contrib/weightless
Successfully created new modules
Run './ns3 configure' to include them in the build

Se nos crea la plantilla dentro de src:

$cd contrib/weightless/
felix@homer-esi:~/tools/ns-3-dev-git/contrib/weightless$ ls
CMakeLists.txt doc examples helper model test

donde vemos las carpetas habituales para meter la documentación, los ejemplos, los asistentes, el modelo propiamente dicho y los test. También me genera el archivo CMakeList.txt que utiliza ns3 para compilar los módulos. Para ver que se incluye correctamente en el proceso de compilación:

$./ns3 configure --enable-example --enable-test
$./ns3 build
$./test.py -s weightlessp
.
PASS: TestSuite weightlessp
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
.

Ahora falta, obviamente, lo difícil, que es programar el modelo propiamente dicho identificando cada una de las partes. Lo más recomendable es estudiar cuidadosamente un protocolo existente de la misma capa que el que queremos programar y copiar su estructura adaptando el comportamiento a la funcionalidad deseada.

Un paseo por la consola de GNU/Linux

La consola de GNU/Linux es una herramienta muy útil para gestionar y administrar el sistema operativo. Es una herramienta que es imprescindible controlar por parte de cualquier profesional de la Informática. Vamos a ver en esta y en posteriores entradas cómo gestionar aspectos básicos del sistema operativo GNU/Linux, concretamente, usaremos una Debian 10.

La consola ejecuta un intérprete de comandos, esto es, un programa que acepta comandos y los ejecuta. El intérprete de comandos que vamos a usar se llama bash. La consola cuando se inicia nos da información de la máquina, usuario y directorio donde se encuentra. En la figura podemos ver cómo se presenta dicha información, generalmente “usuario@nombre-computador: Directorio donde estás$”, despues del dolar ($) podemos empezar a poner órdenes en forma de comandos que se ejecutarán cuando presionemos intro. Esta información es configurable pero lo más habitual es mostrar esa información.

El usuario es el nombre del usuario que está ejecutando los comandos, es importante de cara a qué puede hacer y qué puede ejecutar (temas de permisos que veremos en otra entrada). El nombre del computador también es relevante, ya que la consola se usa para administrar de forma remota otros computadores y se necesita saber dónde estás ejecutando comandos. El directorio donde estás es importante para encontrar los archivos y las rutas relativas, que también veremos mas tarde. El simbolo ~ representa el directorio de trabajo del usuario, que en GNU/Linux está en /home/usuario.
Con esta información, en la figura, nos encontramos con que el usuario alumno, se encuentra en la máquina cuyo nombre es alumno-pc y en el directorio de trabajo suyo por defecto, es decir, el equivalente en Windows a C:\Usuarios\alumno.

Una vez iniciada la consola, podemos empezar a ejecutar comandos, por ejemplo, en la siguiente animación vemos como ejectuamos el comando pwd que le dice a la consola que imprima el directorio donde estas y luego ejecutamos el comando whoami que le dice a la consola que imprima el usuario con el que estamos logeados. Después de introducir el comando o la orden, debemos pulsar la tecla intro para que se ejecute.
Consola

Podemos ver la consola como la caja de herramientas por excelencia de los profesionales de la informática con un montón de herramientas (comandos) para multitud de propósitos. En próximas entradas vamos a ver cómo sentirnos cómodos en la consola explorando cómo ejecutar comandos con opciones, cómo encontrar archivos y las rutas de directorios, cómo ejectuar nuestros propios archivos (e.j archivos python) y gestionar permisos y cómo instalar nuevas herramientas.

Configuración del entorno (VScode) y ejecución de ejemplos en NS3

Actualizado a la versión 3.36 (Mayo 2022)
Mi recomendación antes de nada, una vez instalado NS3 y comprobado que funciona mediante el paso de los correspondientes test (un paso imprescindible), es que configures un entorno de desarrollo adecuado para maximizar tu productividad. Un buen editor de código fuente es Visual Studio Code

En Linux, si abres el editor dentro del directorio de ns3 (en mi caso ns-3.36):

~/tools/ns-allinone-3.36/ns-3.36/$code .

se crea una carpeta .vscode con un archivo c_cpp_properties.json. En ese archivo tenemos que añadir la ruta donde están los .h, el auto completado se habilitará:

{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/build/include/ns3/**",
"/home/felix/tools/ns-allinone-3.36/ns-3.36/build"
],
"defines": [],
"compilerPath": "/usr/bin/clang-11",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-clang-x64",
"compileCommands": "${workspaceFolder}/cmake-cache/compile_commands.json"
}
],
"version": 4
}

Una buena forma de empezar es ejecutar aquellos ejemplos que más te interesen del directorio examples.
Por ejemplo (En negrita los comandos ejecutados):

ns-3.36$ ./ns3 run wifi-simple-adhoc
...
Testing 1 packets sent with receiver rss -80
Received one packet!

Si editas los ejemplos, ahora podrás ver qué métodos y argumentos tiene cada objeto que manipules en tu simulación. Si no lo ves es que tienes las sugerencias deshabilitadas. Puedes habilitarlas siguiendo estas instrucciones

Tus trabajos se harán en el directorio scratch. En el caso de los ejemplos (dentro del directorio examples) que vienen con NS3 no es necesario copiarlos en el directorio scratch y se pueden ejecutar directamente.

El script ns3 es una herramienta de configuración y compilación escrita en python y que es usado en el proyecto NS3 para hacer de interface con el sistema cmake. Se puede usar para configurar y construir el propio NS3.

De momento, nosotros solo nos interesa para compilar y ejecutar nuestros ejemplos. Dentro del directorio de ns3 (~/tools/ns-allinone-3.36/ns-3.36 en mi caso), la sintaxis del comando a ejecutar es:

ns-3.36$./ns3 run ejemplo

Si no es un ejemplo de los que vienen con ns3, buscará ejemplo.cc en el directorio scratch, lo compilará y lo ejecutará.

Topología de los nodos en NS3

Sobre todo en simulaciones que tienen nodos inalámbricos, después de crear los nodos debes posicionarlos para formar tu topología. Topologías habituales en simulaciones son en forma de matriz cuadrada, en línea, en estrella con el nodo que hace de pasarela en el centro, etc.

El emplazamiento de un nodo (X,Y,Z) generalmente se hace con la ayuda de un objeto de la clase MobilityHelper que permite crear escenarios con posiciones donde luego emplazamos nuestros nodos (almacenados en un contenedor del tipo NodeContainer).
Es decir, configuramos una topología concreta con el MobilityHelper y luego instalamos en esa topología los nodos creados y configurados de acuerdo a los requisitos de la simulación.
Una configuración sencilla en matriz 2D sería:

MobilityHelper mobility;
mobility.SetPositionAllocator ("ns3::GridPositionAllocator",
"MinX", DoubleValue (0.0),
"MinY", DoubleValue (0.0),
"DeltaX", DoubleValue (distanceX),
"DeltaY", DoubleValue (distanceY),
"GridWidth", UintegerValue (nodesgridwidth),
"LayoutType", StringValue ("RowFirst"));
mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");

En primer lugar se le indica que vamos a crear puntos de acuerdo a ns3::GridPositionAllocator . Esto es importante por que el resto de argumentos que viene a continuación va en función de este primer argumento. El resto indica las coordenadas iniciales (MinX, MinY) en 0,0 , con una separación determinada, en este caso indicado por distanceX y distanceY, con una anchura indicado por la variable nodesgridwidth empezando por las filas (RowFirst). Por último indicamos que los emplazamientos son fijos “ns3::ConstantPositionMobilityModel”.

En el repositorio de este tutorial podemos ver algunos ejemplos de base para topologías habituales. Los parámetros de entrada son configurables por línea de comandos de cara a permitir la simulación automática de varias topologías.

También en simulaciones inalámbricas, una vez emplazados, es posible que quieras mover tus nodos, para lo cual tendrás que configurar el escenario de acuerdo a tu modelo de movilidad

Otra posibilidad que te permite ns3 es la de leer topologías de un archivo donde has almacenado la posición de algunos nodos.Los inputs readers te permiten este tipo de lecturas ad-hoc y generar topologías personalizadas y/o concretas.