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

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

El algoritmo KNN ponderado en Arduino

Ya hemos visto como funciona el algoritmo KNN en Arduino, simplemente busca los vecinos más cercanos y mira cuales es la clase más numerosa entre ellos. Esta estrategia puede tener problemas cuando las distintas categorías están muy mezcladas o en puntos que están en la frontera entre dos categorías. En esos casos un punto puede estar muy cerca de unos pocos vecinos de una clase pero la mayoría de vecinos (más lejana) ser de otra clase.

Una forma de solucionar esto es que no todos los vecinos valgan lo mismo. Los más cercanos valen más que los más lejanos. En el algoritmo KNN tradicional cada vecino vale lo mismo «1», en la versión ponderada cada vecino vale una constante fija w dividida por la distancia (w/dist). Ahora sumamos cada uno de esos valores de cada clase y asignamos la clase que más sume. La constate w se usa para evitar que el resultado sea muy pequeño cuando las distancias son muy grandes.

La confianza también cambia, ahora es el valor de la suma de los vecinos de la clase mayoritaria, dividida entre el total de la suma de los pesos de todos los K vecinos.

Un ejemplo lo podemos ver en la imagen de debajo donde los puntos verdes son clasificados como pertenecientes a la clase azul (X) en lugar de a la clase roja (+) si se usa el algoritmo KNN normal, pero son correctamente clasificados si se usa la versión ponderada.

Para K = 3 los puntos verdes son mal clasificados por KNN

La librería ArduinoKNN no contempla este algoritmo por lo que tendremos que implementarlo nosotros usando esta librería como base. Podéis usar la librería Arduino_KNNw que implementa algunas mejoras sobre

En este caso tenemos que usar la función; classifyWeighted(input, K ,w ); Siendo input el elemento a clasificar (un array), K el número de vecinos que se tendrán en cuenta y w la constate por la que se dividirá la distancia, w = 1 por defecto. Si ya sabemos usar Arduino_KNN (si no aquí podéis leerlo) su uso es idéntico:

int class = myKNN.classifyWeighted(input, 3);
float confidence = myKNN.confidence();

Podéis ver un ejemplo aqui.

Crear un agente con creatividad artificial

La creatividad artística es una de las capacidades humanas que más nos distinguen de los demás seres vivos. Es una de nuestras capacidades mas difíciles de imitar por las inteligencias artificiales. Tanto es así que hasta los más pesimistas pronósticos sobre la destrucción del empleo por parte de las máquinas salvan los empleos creativos. Pero la inteligencias artificiales poco a poco se adentran el el campo de la creatividad construyendo lo que se conoce, con el poco creativo nombre de creatividad artificial.

Es importante señalar que la creatividad artificial se centra en el proceso creativo no en el impulso creativo. Podemos imitar los resultados del proceso creativo humano pero no la necesidad, el contexto o el mensaje de la obra. Los resultados a veces son tan sorprendentes que parecen contener un mensaje o significado mas allá de la propia obra, pero es algo accidental, codificado en los datos con los que se alimentó la obra. Realmente se busca la imitación del arte humano, no la expresión de la máquina.

Elegir el dominio artístico

No es lo mismo crear un libro, que una pintura, una escultura o un vestido. Incluso dentro de cada dominio hay casos diferentes, no es lo mismo escribir una novela que un poema. Ni sigue las mismas reglas una novela romántica que una de misterio e incluso dentro del mismo género hay estilos.

Reunir ejemplos

Necesitamos recopilar obras de arte del dominio. Las necesitamos para alimentar nuestro sistema y extraer sus características y reglas. Hay que tener en cuenta que estas obras son las que definirán las creaciones de nuestro agente hay que reunir obras como las que queremos que produzca

Convertir las obras a un modelo

El modelo es una forma de representar la obra de forma «entendible y manipulable» por nuestro software.

Reglas y algoritmo generador

La creatividad necesita seguir una reglas. Si tiramos letras al azar sobre una página el resultado será muy innovador y único pero su valor como novela es muy pobre. Las reglas nos dicen como generar una obra a partir de una entrada. Pueden estar definidas a mano, aprendidas automáticamente de nuestros ejemplos o una mezcla de ambas. Podemos darnos el lujo de romper alguna regla durante el proceso creativo pero si rompemos todas obtendremos un sin sentido.

siguiente estas reglas el algoritmo puede crear una obra a partir de los datos proporcionados como entrada.

Datos de entrada o semilla

Hace falta definir una «semilla», una entrada al modelo con la que este pueda «tirar del hilo» para generar la obra de arte. La semilla puede ser elegida por un humano, generada al azar o tomar algún valor de algún sitio. En los modelos iterativos (más adelante los vemos) la semilla incluye una obra que es la que vamos a transformar. En la primera iteración se pueden usar datos generados al azar.

Representar el modelo

La representación es la traducción de ese modelo a la obra que nosotros los humanos apreciamos. Es el paso contrario a convertir la obra en modelo

Modelos iterativos

Lo ideal seria que a la primera la obra resultante fuera valida. Pero Actualmente se aprovecha que tenemos un «juez» para convertir el proceso de creación de la obra en uno iterativo. En este proceso se toma la obra generada o mejor dicho su modelo y se usa para realimentar el algoritmo generador de forma iterativa transformando la obra intentando mejorar la puntuación del juez en cada iteración. Este proceso recuerda al de las metaheurísticas.

Juez

El juez actúa valorando la obra creada según las características deseadas. Estas características pueden ser aprendidas o definidas por nosotros. Esta parte del algoritmo se comporta como la función fitness.

Hay algoritmos generadores que en lugar de generar una obra generan varias obras que son pequeñas variaciones de la mima. El juez actúa eligiendo con cual se realimenta el proceso. Que por lo general suele ser la que mejor puntuación obtiene.

En este punto se puede introducir «ayuda» humana que actué como juez, si no en todas las iteraciones cada cierto número de ellas.

Ejemplo

En el caso del generador de textos basado cadena de Markov que vimos en esta entrada del blog tendríamos los siguientes elementos:

  • Los textos que se usan para entrenarla son los ejemplos.
  • El modelo son los tokens que se extraen del texto para entrenar a la red.
  • Los n-gramas que se aprenden son la reglas generadoras.
  • El algoritmo generador es tan sencillo como elegir al azar la palabra siguiente ponderando este azar según las probabilidades indicadas por las reglas (n-gramas)
  • En este caso no hay juez a que es un algoritmo de una única pasada

Vamos a convertir este sistema en iterativo. En lugar de una frase se generaran varias. Un humano indicará cual es la que más le gusta y a partir de esa frase se generaran nuevas frases. Para ello el algoritmo tomará un punto aleatorio de la frase, eliminará todas las palabras a partir de ese punto y continuará creando la el texto usando los n-gramas aprendidos para elegir la siguientes palabras.

Regresión lineal con incertidumbre en Arduino

Vamos a empezar este texto develando el truco que usaremos para representar incertidumbre con la regresión lineal para Arduino y que se basa en emplear la versión con pesos del algoritmo de regresión lineal. La incertidumbre estará representada como valores con una variación de pesos según la certeza que tengamos de su valor. Usaremos el peso como porcentaje de certeza de ese dato

Esta no es la mejor ni la única manera de hacerlo. No hay que olvidar que aquí se trata de hacerlo en algo tan limitado en memoria y potencia como pude ser un Arduino UNO.

Usaremos la librería regressino, en concreto su librería para regresión lineal:

#include <LinearRegression.h>

LinearRegression lr = LinearRegression();

Una forma de incertidumbre es cuando directamente tenemos valores de los que «nos fiamos» menos que de otros. Por ejemplo, porque vienen de dos fuentes distintas. En este caso los datos menos fiables tendrán que tener un peso más bajo que los más fiables para que su influencia sobre el resultado final sea menor.

//datos fuente no fiable
lr.learn(1, 3, 0.5);
lr.learn(2, 5, 0.5);
lr.learn(3, 6, 0.5);

//datos fuente fiable
lr.learn(2, 4, 1);
lr.learn(4, 5, 1);
lr.learn(5, 6, 1);

Pero hay otro caso de incertidumbre, cuando no conocemos el valor del dato con seguridad, lo que conocemos son los valores entre los que está comprendido. Generalmente tenemos dos valores, un mínimo y un máximo o tres valores con uno más probable y un mínimo y un máximo (a veces representados como errores) entre los que ese valor puede variar. En este caso tenemos que definir cómo se distribuye el peso (probabilidad) entre estos valores. Hay que recordar que la suma total de los pesos tiene que ser igual a 1.

Una vez definida la forma en que se distribuye la probabilidad hay que descuartizarla en puntos. La idea es que esto funcione en un Arduino UNO y no podemos trabajar directamente con funciones de probabilidad.

Supongamos que para x = 10 sabemos que el valor de y está comprendido entre 2 y 3. Ahora hay que saber cómo está distribuida la probabilidad entre esos dos valores. Veamos algunas posibilidades:

  • Toda la probabilidad se concentra en cada uno de esos valores por lo tanto el 2 tendría un 50% de certeza y el 3 otro 50%. O lo que es lo mismo un peso de 0.5
lr.learn(10, 2, 0.5);
lr.learn(10, 3, 0.5);
  • La probabilidad se distribuye de forma uniforme por todo el espacio entre esos dos puntos. Para representarlo tómanos varios puntos entre 2 y 3 y les asignamos a todos la misma probabilidad.
lr.learn(10, 2, 0.2);
lr.learn(10, 2.25, 0.2);
lr.learn(10, 2.5, 0.2);
lr.learn(10, 2.75, 0.2);
lr.learn(10, 3, 0.2);
  • El punto central es mucho más probable que los extremos. Un ejemplo de como podemos hacerlo.
lr.learn(10, 2, 0.25);
lr.learn(10, 2.5, 0.5);
lr.learn(10, 3, 0.25);
  • Si por ejemplo queremos simular una distribución en campana de media u y con desviación estándar s
lr.learn(10, u, 0.682);
lr.learn(10, u-s, 0.136);
lr.learn(10, u-(2*s), 0.023);
lr.learn(10, u+s, 0.136);
lr.learn(10, u+(2*s), 0.023);

Regresión lineal con pesos en Arduino

Ya hemos visto varias formas de extender las capacidades de la regresión lineal en Arduino. En este caso vamos a asociar pesos a los valores para que no todos los casos aporten los mismo al resultado final. Pero qué significa «aportar» más al resultado, de forma gráfica podríamos imaginarnos que los puntos de mayor peso atraen más a la recta de la regresión lineal por lo que esta tiende a acercarse más a estos. La utilidad de este sistema es cuando tenemos resultados que por algún motivo valoramos más que otros.

Vamos a modelar el peso como un valor entre 0 y 1. De tal forma que el peso máximo corresponda con 1 y el mínimo con 0. A mayor peso más «aporta» ese valor al resultado. Entre dos valores uno con peso 1 y otro con peso 0.5 el primer cuenta el doble que el segundo. Cuando el peso es 1 no hay diferencia con la regresión lineal sin pesos. Y cuando el peso es cero el valor no va a aportar nada al resultado.

Partimos de la función de regresión lineal que ya vimos en otro post:

void LinearRegression::learn(double x, double y){
    n++;
    meanX = meanX + ((x-meanX)/n);
    meanX2 = meanX2 + (((x*x)-meanX2)/n);
    varX = meanX2 - (meanX*meanX);

    meanY = meanY + ((y-meanY)/n);
    meanY2 = meanY2 + (((y*y)-meanY2)/n);
    varY = meanY2 - (meanY*meanY);

    meanXY = meanXY + (((x*y)-meanXY)/n);

    covarXY = meanXY - (meanX*meanY);

    m = covarXY / varX;
    b = meanY-(m*meanX);
}

Como ya hemos visto en otro post vamos a usar «el truco» que consiste en transformar el valor de x e y que se le pasa a la función que calcula la regresión lineal (learn). Podríamos verlo como que el valor de x se usan para modificar el valor de meanX y meanX2 y el de y para meanY y meanY2. Entonces si w es el peso:

  • Si el peso es 1 x e y no cambian su valor
  • Si es 0 x e y no tienen que afectar a los valores de meanX y meanY para que pase eso el valor de x ha de ser igual que meanX y el de y igual a meanY.
  • Si el peso este entre 0 y 1 el valor ha de componerse con el valor de x e y y el valor de las medias de cada uno.

Para conseguir eso vamos a ponderar el valor de la x y el de meanX según w

x = x*w + meanX*(1-w);
y = y*w + meanY*(1-w);

Solo queda añadir un par de comprobaciones para evitar que el valor pueda ser mayor de 1 o menor de 0:

void LinearRegression::learn(double x, double y, double w){
    if(w >= 1) { 
        learn(double x, double y);
    } else if(w <= 0) {
        return;
    } 

    x = x*w + meanX*(1-w);
    y = y*w + meanY*(1-w);
    learn(double x, double y);
}

Todo esto se puede ver en la librería Regressino.

Por último comentar que hay dos posible optimizaciones que se podrían aplicar si fuera necesario y que nos permiten ahorrar algunos cálculos si hay muchos pesos cercanos a 0 o a 1:

  • Los pesos muy cercanos a 0 se ignoran
  • Los pesos muy cercanos a 1 se tratan como si no tuvieran peso


Hay una pequeña pérdida de precisión pero en el caso de tener muchos datos puede compensar al reducir el tiempo de cálculo.

Regresión lineal segmentada en Arduino

Vamos a seguir viendo «trucos» para aprovechar la regresión lineal. La principal limitación de la regresión lineal es precisamente que es «lineal». Ya hemos visto que se puede utilizar la regresión lineal para calcular otro tipo de regresiones. Ahora vamos a ver como aproximar formas mas complicadas.

Cualquier curva se puede aproximar usando segmentos de linea recta. A mayor número de segmentos mejor aproximación. Por ejemplo debajo podemos ver la aproximación a una curva con forma de campana.

Se puede consguir mejor resultado si cada segmento empieza en el mismo punto que termina el anterior, pero la imagen es más realista respecto al caso que vamos a ver: Usar la regresión lineal para calcular cada uno de esos segmentos de forma independiente.

La forma de trabajar es muy sencilla, dividimos el espacio en varias partes para cada una de las cuales calculamos una regresión lineal. Luego cuando queremos estimar un valor lo primero es ver a que segmento corresponde y usar esa regresión para calcular su valor.

Para elegir como dividir la regresión hay varias opciones:

  • Dividir en segmentos iguales, es la forma más sencilla y entendible. Tiene el problema de que te puedes encontrar huecos sin datos.
  • Dividir cuando haya datos suficientes para asegurarse la calidad del aprendizaje. Se toman segmentos de longitud variable, el unico requisito es que haya suficiente punto de aprendizaje en ese segmento para asegurarse de que hay datos, si no los hay se alarga el segmento hasta que los haya.
  • Se empieza con una solo regresión, luego se divide en dos y se compara el error cuadrático medio entre ambos, si es menor en la version de mas segmentos se repite la operación

Este último punto no muestra algo interesante. Si necesitas comparar que combinación de segmentos es la mejor opción se puede usar el error cuadrático medio. Aquí puedes ver como calcularlo.

Ejemplo

Vamos a usar la librería Regressino en concreto nos vamos a basar en uno de sus ejemplos.

Lo primero es incluir la librería correspondiente y declarar una regresión lineal para cada segmento que queremos crear (en este caso 3)

#include <LinearRegression.h>

LinearRegression lr1 = LinearRegression();
LinearRegression lr2 = LinearRegression();
LinearRegression lr3 = LinearRegression();

El primer segmento va de X = 1 a X = 10, el segundo de X = 11 a X = 20 y el tercero de X = 21 a X = 30.

    Serial.println("Start learn");
    //1-10
    lr1.learn(1,2);  
    lr1.learn(2,3);
    lr1.learn(3,4);
    lr1.learn(6,7);
    lr1.learn(8,9);

    //11-20
    lr2.learn(11,24);  
    lr2.learn(12,26);
    lr2.learn(13,28);
    lr2.learn(16,34);
    lr2.learn(18,38);

    //21-30
    lr3.learn(21,66);  
    lr3.learn(22,69);
    lr3.learn(23,72);
    lr3.learn(26,81);
    lr3.learn(28,87);
    Serial.println("End learn");

Para calcular la estimaciones hay que tener en cuenta esos límites para saber que regresión lineal usar:


    for(int i = 0; i < 31; i++){
      Serial.print("Result (");
      Serial.print(i);
      Serial.print("): ");
      if(i < 11){
        Serial.println(lr1.calculate(i));    
      } else if(i < 21){
        Serial.println(lr2.calculate(i));
      } else {
        Serial.println(lr3.calculate(i));
      }
    }

Como podemos ver es un proceso muy sencillo pero no todo son ventajas.

Problemas

  • Puede dar lugar discontinuidades y saltos bruscos en los puntos donde se produce un cambio de segmento
  • Una de las limitaciones es que para cada valor de X solo puede existir un valor Y. Las rectas calculadas en cada segmento no pueden solaparse en ningun punto.
  • Lo contrario si que puede producirse, que varios valores de X tengan el mismo valor de Y
  • Necesitas tener valores de muestra en todos los segmentos. Si divides los datos en cinco segmentos, te tienes que asegurar de que tienes muestras suficientes para que el algoritmo de regresión calcule una buena aproximación en cada uno de ellos.

Ideas para explorar

  • Hemos usado el algoritmo de regresión lineal para cada segmento, pero podria usarse algún otro de los que hemos visto, incluso distintos en cada segmento.
  • Si hay un hueco sin datos podemos estimar la ecuación del segmento Y = m*X + c usando el punto final de segmento anterior (X1, Y1) y el punto inicial del segmento siguiente (X2, Y2). Para ello m = (Y2-Y1)/(X2-X1) una vez calculada la m podemos calcular la c = Y1 – m*X1. No es una solución ideal y seguramente resulte en una mala aproximación
  • Otra opción pra calcularlo es realizar el aprendizaje con algunos de los puntos del final del segmento anterior y de algunos del inicio del segmento siguiente.
  • Esa misma idea se puede usar para tratar de mejorar el aprendizaje permitiendo que el inicio y final de cada segmento se solapen con los segmento anterior y siguiente.

Problemas de la esperanza y aversión al riesgo y a la pérdida

Usando matemáticas para decidir muchas veces acabas confiando en la esperanza, matemática se entiende. La esperanza no es un beneficio real, es una forma de estimar un posible beneficio, es lo que ganarías de media si se repite infinitas veces el proceso aleatorio.

Supongamos que participas en un sorteo de 1000€, han vendido solo 10 billetes. Así que tu billete tiene una esperanza de 100€….ahora bien. Intenta comprar algo con esos 100€ que según las matemáticas es el potencial beneficio de ese billete. Al final tienes 0€ o 1000€. El uso de la esperanza como un beneficio real es lo que hace que puedan surgir paradojas.

Paradoja de los dos sobres

Imagina que te dan un sobre, llamemosle 1 con una cantidad desconocida de dinero, indiquemosla como X. El sobre 2 tiene o el doble o la mitad que el 1. Es decir o 2X o X/2. Ahora nos piden elegir si cambiamos de sobre o no. ¿Qué es lo más provechoso? Sabemos que el sobre 1 contiene X, pero ¿Cuál es la esperanza del sobre 2?

1/2 * 2X + 1/2 * X/2 = 5X/4

Mientras que tu sobre tiene X dinero el otro sobre tiene una esperanza de 1,25X. Deberíamos coger el sobre 2. Así que eso haces. Ahora te ofrecen volver al sobre 1. Si lo piensas tú has elegido en sobre con la mitad o el doble de dinero que 1, pero eso significa que 1 tiene la mitad (si el 2 tiene el doble) o el doble (si el 2 tiene la mitad) que el tuyo…Un momento ¿Un sobre que tiene el doble o la mitad que el tuyo no es como ha empezado todo esto?. Efectivamente. Y si antes has cambiado, lo lógico sería volver a hacerlo ahora ¿No?. La esperanza es la misma, 1,25 veces lo que tenga tú sobre. Pero si lo cambias estarás en la misma situación que al principio y ya hemos visto que lo mejor era cambiarlo…..así que puedes estar cambiando sobres infinito número de veces.

Paradoja de San Petersburgo

Te ofrecen un juego de tirar una moneda. Si sale cara vuelves a tirar, si sale cruz se acaba la partida. Ganas 2^n euros. Siendo n el número de caras obtenidas. Veamos cual es la esperanza de beneficio. En este caso será la suma de la esperanza de cada tirada. Sabiendo que la probabilidad de ganar en la primera tirada es 1/2 (la probabilidad de salir cara), la de la segunda tirada es 1/2 * 1/2(dos caras seguidas), la tercera 1/21/21/2 y así consegutivamente:

Tirada 1 = 1/2 * 2^1 = 2/2 = 1
Tirada 2 = 1/21/2 * 2^2 = 4/4 = 1 Tirada n = (1/2^n) 2^n = 2^n/2^n = 1

La esperanza es la suma de todas las posibles tiradas….como todas son igual a 1 la esperanza es infinita. Luego te pidan lo que te pidan por participar sale rentable. Aunque te pidan un millón de euros es poco por la esperanza de ganar infinitos.

Aversión a la pérdida

Para este tipo de problemas se plantean varias soluciones, no todas posibles de implementar en código, pero yo voy a optar por usar una inspirada en dos mecanismos de los humanos. La aversión a la perdida o al riesgo. Suenan parecidos pero son distintos.

La aversión a la pérdida se podría definir como que se valora más lo que tienes que lo que puedes ganar. O lo que es lo mismo cuando has de comparar lo que puedes ganar con lo que puedes perder has de multiplicar la perdida por una variable que la incrementa. Además para simular el comportamienti humano esta variable ha de incrementarse conforme aumenta el valor de la cantidad que arriesgamos. Pongamos el caso de el tipico juego de tirar la moneda con una moneda justa (50% de que salga cara y 50% de que salga cruz). Si sale cara ganamos, si sale cruz perdemos. Si las condiciones son que si ponemos 1€ de apuesta podemos ganar 2€ la esperanza nos dice que da igual si jugamos o no:

(0.5 * 2) + (0.5 * 0) = 1

1 que es igual a la apuesta por lo que no hay esperanza de beneficio

Sin embargo con una adversion al riesgo de 1.1 la apuesta se multiplica por ese valor siendo 1.1 por lo que necesitamos una esperanza mayor.

Ya no jugariamos Por supuesto en este caso en que la perdida es tan solo 1€ muchos se plantearan jugar. Sin embargo si fueran 1000€ lo que nos jugamos, aunque el beneficio sea igualmente 1000€ habrá menos gente gente dispuesta a jugar. Esos se debe a que la aversión a la perdida crece con la cantidad que se va perder, no es siemrpe la misma. Para 1€ puede ser 1 pero para 1000€ puedes ser 100. De hecho en caso de cantidades muy pequeñas puede ser menor que 1. Ya que la perdida la consideramos despreciable y eso podría animarnos a correr ese riesgo.

Aversión al riesgo

Por otro lado la aversión al riesgo favorece la elección de las opciones más probables. Si la anterior aversión aplicaba según la cantidad «apostada», está aplica según la probabilidad de ganar. Es menor que 1 y a menor probabilidad menor será.

Siendo Ar la función que devuelve la aversión al riesgo. P la probabilidad y V el valor, la nueva esperanza seria:

Esp = Pi * Vi * Ar(P)

Es importante tener en cuenta que se multiplica el valor y no la probabilidad. Si, ya sé que parece lo mismo, pero las probabilidades han de sumar 1.

El asno de Buridan

Por último un caso especial, no es un gran problema ya que estamos habituados a resolverlo, tan habituados que puede que ni nos demos cuenta de que existe.

La historia original habla de un asno hambriento que situado equidistante a dos pesebres idénticos muere de hambre por no saber por cual decidirse. O lo que en el caso de la I.A. sería un agente enfrentado a dos opciones con la misma esperanza no sabría cual decidir.

Elección irracional

El ejemplo más sencillo es que nos toque diseñar un agente que elige entre cara o cruz. ¿Qué es lo que hacemos habitualmente? Elegirla al azar. Esto es realizar una elección no racional. No hay ningún motivo para elegir una u otra así que dejamos al azar esa elección. Este mismo sistema nos puede servir cuando el valor de la esperanza no nos sirva de guía. Nada nos garantiza elegir la mejor opción pero nos permite no quedarnos congelados sin saber que elegir.

Algoritmos que «olvidan» con el tiempo

Una de las virtudes de las máquinas es que no olvidan. ¿Para qué sirve que lo aprendido pierda valor con el tiempo?. Para aprender datos que pueden cambiar con el tiempo. Por ejemplo hábitos, gustos e intereses. Realmente el concepto de olvidar es que con el tiempo la información aprendida pierda valor.

Supongamos que queremos hacer un algoritmo que valore aplicaciones  según el valor de las votaciones de los usuarios, pero como las aplicaciones van cambiando con cada nueva versión queremos que los votos recientes cuenten más que los votos más antiguos. Es decir que el programa «olvide» el valor de los votos según pasa el tiempo. Para lograr este efecto. Cada voto lo multiplicamos por un valor que disminuye con el tiempo. De esa forma cuanto más tiempo hace que se votó menos valor tiene el voto.

Algunas fórmulas:

W(t) representa el peso del voto V tras t unidades de tiempo transcurridas desde que se voto. Las llamo «unidades de tiempo» porque para las fórmulas da igual que sean segundos, horas, días, días de Júpiter, una unidad de tiempo inventada,….

t son las unidades de tiempo desde que se votó.

V representa el valor del voto.

W(t) es el peso de ese voto en el instante t

k es una constante que determina como de rápido pierde valor el voto.

W(t) = 1-k*t

W(t) = 1 -( k * t)     En rojo k = 0,2 En azul k = 0,5

Para esta función k tiene que ser menos que 1.

La caída del valor el lineal

W(t) = 1/(k*t)

W(t) = 1/(k*t)   K = 2

En este caso k tiene que ser mayor que 1 si es menor aumenta el valor de V en lugar de reducirlo. Cuidado también con valores de t pequeños

W(t) = k^t

Wt) = k^t         En negro k = 0.9 en rojo k = 0.5

En este caso k ha de ser menor que 1. A mayor valor tenga k más rápida será la perdida de valor con el paso del tiempo.

W(t) = e ^ -kt

W(t) = e ^ -kt     En azul  k = 2 en verde k = 1

Basta con que k sea positivo. A mayor valor de k más rápido cae el valor.

Permite ajustar la «vida media» (vm) que seria la cantidad de tiempo en que el valor V valdría la mitad. Para calcular el k necesario para tener esa vida media basta con hacer la siguiente operación: 

k =  ln 2 / vm

Por ejemplo si queremos una vida media de 3 unidades de tiempo:

k = ln 2 / 3 = 0,23104906

Una vez tenemos el valor actualizado de cada voto lo único que hay que hacer es sumarlos.

Como se usa

Ahora que tenemos un función W(t) que nos calcula el peso de cada voto cuando tiene una antiguedad t . Podemos calcular el valor obtenidos de todos los votos como:

Σ Vi * W(ti) / Σ W(ti)

Veamos un ejemplo sencillo, tenemos un voto reciente (t = 0) que le da 4 estrellas mientras que hay otro que le da 5 estrellas de hace tres versiones (t = 3). Consideremos que cada vez que se sube una nueva versión el tiempo avanza una unidad. Para calcular el peso del voto vamos a usar la función: W(t) = e ^ -kt con k=0,23104906 (vida media = 3)

(4 * W(0) + 5 * W(3)) / (W(0) +W(3)) = (4 * 1 + 5 * 0.5) / (1 + 0.5) = 4.33

Si todos los votos valieran lo mismo el valor seria de 4.5. Podemos ver como el voto más reciente «cuenta más».

Un último consejo es que borrar aquellos votos cuyo peso caiga por debajo de un mínimo para reducir los cálculos.

El problema del contexto en la Inteligencia Artificial

Definir el problema del contexto es difícil ya que sus causas son muchas. De forma intuitiva el problema del contexto es que cuando hablas con otra persona tiene claro dónde y cuándo estás, que os rodea y un montón de información cultural y social aprendida. Además tenemos una capacidad más o menos buena para captar las emociones e intenciones de la otra persona y de aprender cómo es su personalidad con el paso del tiempo. Suma a todo esto nuestra habilidad para comprender el lenguaje, los gestos, los símbolos visuales y el entorno junto con nuestra capacidad de abstracción. Sin ser perfectos, muchas veces ocurren malentendidos, ninguna inteligencia artificial se nos acerca si quiera. Vamos a ver algunas de las causas de forma individual.

Los conocimientos y comportamientos aprendidos se deben a nuestra cultura y sociedad. Son muchos más de los que nos pensamos. Son cosas como las normas de comportamiento, frase hechas, «lo que está de moda» o aquel conjunto de diversos conocimientos que llamamos «culturilla general». La unica forma de solucionar este problema es aportar ese conocimiento al agente. Por ejemplo, el refrán: «El que a buen árbol se arrima, buena sombra le cobija» no va a ser correctamente interpretado por ninguna I.A. que no conozca su significado previamente. No es un problema de interpretación de lenguaje natural, es un conocimiento que debe ser aprendido. Lo mismo pasa con las señales de tráfico, por ejemplo.

Otro problema del contexto es que el ser humano es perfectamente consciente del entorno que le rodea. Si por ejemplo tu pareja te dice: «¿Puedes mirar en Internet a que hora pasa el bus?». Y es un martes a las ocho de la mañana seguramente se refiera al bus al trabajo. Si es un sábado y se va al pueblo a ver a sus padres será el bus del pueblo y si es un martes de agosto tiene vacaciones y vais a coger un avión se referirá al bus al aeropuerto. Como humanos no tenemos ningún problema en entender esto. Eso se debe a que integramos la frase en un contexto espacial, temporal y personal. Para que una I.A. haga lo mismo tiene que ser capaz de fusionar varias fuentes de datos: localización, fecha, hora, agenda, costumbres de la persona. Estamos tan acostumbrados a hacerlo de forma automática que nos parece engañosamente facil hacerlo. Sin embargo para un agente saber que fuentes de información son importantes y como fusionarlas es difícil.

Las inteligencias artificiales tampoco son buenas entendiendo figuras como la ironía, el sarcasmo o interpretando las cosas desde el punto de vista de otras personas. No son capaces de las abstracciones necesarias para entender cosas como el arte o incluso distinguir un objeto de su reflejo en un espejo. La incapacidad de ponerse en el lugar de otros causa problemas como que muchos vehículos autónomos causan mareos ya que sus maniobras no están pensadas para la comodidad de los viajeros. En algunos casos sencillos se pueden resolver estos problemas con aprendizaje y realimentación de los humanos.

woman in black shirt facing mirror

Una I.A. no sabria distinguir entre la chica real y la reflejada, diria que en la foto hay dos personas. Photo by Ivan Obolensky on Pexels.com

Saber que señales ignorar y cuáles no. Hace un tiempo iba conduciendo por un calle de dos carriles uno para cada sentido con aparcamientos en batería a los lados. Llegado a un punto uno de los carriles estaba en obras, la solución por la que había optado era que uno de los carriles provisionales pasaba por encima del aparcamiento en batería y el otro usaba el carril que habitualmente circulaba en sentido opuesto. La única señal que había era unas vayas amarillas que más o menos sugerían lo que había que hacer y un cartel de cuidado obras. Tras unos segundo de duda resulto sencillo saber que durante unos metros tendría que hacer caso omiso a las indicaciones viales y circular por encima de las lineas de aparcamiento ya que mi carril estaría ocupado por coches circulando en sentido contrario. El entorno puede estar lleno de señales contradictorias y hay que entender cuáles ignorar en cada momento.

Algunas técnicas que tenemos para tratar de minimizar este problema y que los agentes den la sensación de tener cierta «consciencia» del entorno:

Limitar el ámbito del agente, si haces un agente para consultar sobre mecánica del automóvil nadie se sorprenderá de que por «gato» solo entienda los gatos hidráulicos.

Crear estados o tópicos, es una forma de ampliar el caso anterior. El programador define estados o tópicos que ayudan a crear el contexto. Si por ejemplo el estado es «mecánica del automóvil» es muy probable que gato se refiera al gato hidráulico, pero si el tópico es «mascotas» es más probable que se refiera a un animal. El problema de los tópicos es que al final el número de los mismos está limitado.

Detección de tópicos, en combinación con el punto anterior, es detectar los tópicos de un texto o conversación. Si hablo de un gato saber si me refiero al animal o a la herramienta. Generalmente se extrae relacionándolo con el resto de palabras, si digo «coche», «rueda», «maulla».

Aprendizaje, para adaptarse a cada persona es necesario aprender sus hábitos. Los asistentes actuales ya lo hacen tratando de aprender los hábitos, gustosy costumbres a base de recopilar datos automáticamente,

Interacción con los humanos, a veces la forma más fácil de saber algo es que te lo digan. Es importante la capacidad de preguntar y recibir esa información ya sea de forma natural o a través de una pantalla de configuración.

Detección de emociones, técnicas que van desde el reconocimiento de expresiones faciales a la detección de sentimientos en frases o de ironías.

Extracción de datos del entorno, muchas veces el contexto viene determinado por lo que te rodea. El que agente necesita saber dónde está y que ocurre a su alrededor. Si suena música que música es, si se es viendo una película, cuál es. Si conoce al resto de las personas que pueda haber.

Fusión de datos, no basta con captar todos los datos hay que entenderlos en su conjunto. Para ello hace falta cruzarlos pero también ser capaz de extraer nueva información al cruzar estos datos. Ha de «deducir datos nuevos». Tiene que trabajar con incertidumbre y ser capaz de «rellenar huecos» con lo más probable. También tiene que ser trabajar de descartar los datos que son correctos pero no aplican ene ese momento, que quizás sea lo más difícil.

¿Con todo esto basta? No, o al menos no con el nivel actual de la tecnología. Aún estamos lejos de lograr agentes que entiendan el contexto completamente. Sin embargo con estas técnicas, en algunos casos, los resultados pueden ser sorprendentes, o al menos parecerlo, por eso a veces los asistentes como Siri o Alexa nos sorprenden con su «comprensión» y otras con su estupidez. Aun queda mucho para lograr que la I.A. entienda el contexto correctamente.