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.

 

Programación Lógica

La programación lógica está inspirada en la lógica de primer orden. El lenguaje más conocido es Prolog y si no lo conocéis os recomiendo que le echéis un ojo. Es una forma de programar bastante diferente a la habitual y aunque para cosas genéricas pueden ser más “cómodos” otros paradigmas de programación para ciertas ramas de la I.A. facilita el desarrollo de forma espectacular.

En programación lógica por lo general hay tres tipos de sentencias:

  • Hechos (facts): Serían los conocimientos de los que se parte. Son sentencias del estilo “Juan es padre de Pepe” que se escribiría como algo así: padre(juan, pepe).
  • Reglas (rules): Calculan nuevos hechos estableciendo relaciones entre ellos.Para ello se utilizan variables. Por ejemplo “Si X es padre de Y e Y es padre de Z entonces X es abuelo de Z” que en Prolog sería: abuelo(X,Z) :- padre(X,Y), padre(Y,Z).
  • Consultas (queries): Se usan para extraer información. Hay de dos tipos, la primera dice si un hecho es verdadero o falso. Por ejemplo “¿Es padre Juan de Pepe?” Que se escribiría (depende el intérprete de Prolog) padre(juan, pepe). El otro tipo son las que tiene que completar un hecho. Por ejemplo “¿Quien es el padre de Pepe?” padre(X, pepe).

Si queréis jugar con él sin tener que instalar nada podéis probar aquí.

Guardando el conocimiento

Supongo que ya os imaginareis como va a aprender este sistema. Convertiremos los datos a un conjunto de hechos y reglas. Para obtener la respuesta de la base de datos usaremos consultas.

¿Qué ventajas tiene este sistema?. Que no solo “aprende” datos, si no que también las relaciones entre ellos y la forma de calcularlas.

En el siguiente ejemplo vemos qué se define el concepto de abuelo y en lugar de añadir abuelo(juan, javi) añadimos la regla genérica para calcularlo.

padre(juan, jose). # juan padre de jose
padre(jose, javi). # jose pare de javi
abuelo(X,Y):-padre(X,Z), padre(Z,Y).

Probamos la query:

abuelo(juan, X)

Se comporta igual que si hubiéramos introducido el dato directamente abuelo(juan, javi).

Otra ventaja de la programación lógica es que es comprensible por los seres humanos, bueno al menos por algunos de ellos.

Usando Prolog desde Javascript

La cosa se complicó en este punto. Si quería que Chispas pudiera usar prolog necesitaba que se pudiera interpretar desde JS. El problema es que no encontré ningún intérprete de programación lógica que encajara con mi idea. O no tenían una sintaxis que me convenciera o no funcionaban bien cuando las reglas se complicaban o lo hacían pero eran muy lentos. Al final el que más me gustaba era el de la página que os he enlazado antes. El único problema es que la web era monolítica y tendría que separar la parte del interprete, encapsularlo y dotarle de una forma de introducir las reglas y leer los resultados que fuera amigable para el desarrollador. Pero eso era fácil de resolverlo programado. Al final el resultado podéis verlo aquí.