Entropía

Supongo que la entropía os sonara de la termodinámica y así es. Cuando Shannon desarrollo la teoría de la información llego a una formula similar a la que describe la entropía en los sistema físicos. He aquí la formula:

H(x) = – Σ p(xi) * log2(p(xi))

Es decir, si tenemos una variable x con distintos valores posibles, para cada valor (xi) cuya probabilidad es p(xi) la entropía de ese valor es:

 p(xi) * log2(p(xi))

Y la entropía total es la suma de la entropía de cada valor. A veces a cada posible valor se le llama signo.

Por ejemplo supongamos un semáforo que se pasa el 45% del tiempo en rojo, el 45% en verde y el 10% restante en ambar. La entro pía de cada señal seria:

x p(x) Entropia Formula
verde 0,45 -0.156 log2(0,45)*0.45 
rojo 0.45 -0.156 log2(0,45)*0.45
ambar 0.1 -0.1 log2(0.1)*0.1

Entropia =  -0,412

¿Como se usa esto y qué mide?

Mide la cantidad de bits que «aporta» un dato o lo que es lo mismo, la cantidad de información que transmite. Para entenderlo mejor tomando el ejemplo más simple, un bit con dos valores (0,1) pero cambiando la probabilidad de cada uno, respetando que la suma de ambas siempre es 1 (P(1) = 1 – P(0)):

P(0) P(1) Entropia Formula
0 1 0 -(log2(0)*0 + log2(1)*1)
0.1 0.9 0.46 -(log2(0.1)*0.1 + log2(0.9)*0.9)
0.2 0.8 0.72 -(log2(0.2)*0.2 + log2(0.8)*0.8)
0.3 0.7 0.88 -(log2(0.3)*0.3 + log2(0.7)*0.7)
0.4 0.6 0.97 -(log2(0.4)*0.4 + log2(0.6)*0.6)
0.5 0.5 1 -(log2(0.5)*0.5 + log2(0.5)*0.5)

Para el primer caso que es que el bit siempre sea 1 el valor aportado es 0. ya que no aporta ninguna información. en resumen si sabemos que un signo es más probable que otro aporta menos información que si ambos son equiprobables. 

¿Para qué nos sirve todo esto?. De forma muy simple, cuando haya que elegir entre varias propiedades, la entropía puede ayudarnos a elegir que parámetro escoger

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.

Elegir estructura (chasis) para un robot casero

Cuando me planteo hacer cualquier pequeño robot que se mueva siempre me encuentro con el problema de que no tengo más que una ligera idea mecánica. Hay montones de modelos caseros y no es fácil elegir cual se ajusta mejor a tus objetivos y capacidades. Así que voy a intentar explicar rápidamente que tipos de configuraciones son las más típicas en robots caseros. Aviso que es una clasificación basada en mi experiencia en robótica, por lo que es bastante informal.

Ruedas:

Vamos a empezar por que zapatos le calzamos, los tipos de ruedas mas usados.

  • Ruedas motrices: Son las que aportan movimiento al vehículo, van conectadas directamente al motor y al girar este estas giran moviendo el vehículo.
  • Ruedas directrices: son las que giran para dirigir el vehículo. Por ejemplo en un coche convencional son las que giran al girar el volante, vamos, las delanteras. A su vez pueden ser motrices por ejemplo en un 4×4.
  • Ruedas fijas: Son ruedas que giran libremente solo hacia delante y hacia atrás. Pueden ser motrices.
  • Ruedas locas: Se puede decir que estas se dejan llevar, son ruedas que ademas de girar sobre si mismas en el eje horizontal también permiten libre movimiento en el vertical. Aportan simplemente estabilidad evitando que el vehículo vuelque sin oponer muchas resistencia a los giros. Son típicas en los robots de tres ruedas. Un ejemplo son las ruedas de los carritos de supermercado. Otro tipo muy usado son una esfera que rota libremente en cualquier dirección.Un ejemplo lo tenéis en los antiguos ratones mecánicos o en las cabezas de los bolis Bic.
  • Omnidireccionales: Al igual que las anteriores son ruedas que pueden desplazarse en ambos ejes pero que a su vez pueden ser motrices. Ejemplos son las omni wheel o algunos robot que se desplazan sobre esferas. Tiene la ventaja de que pueden desplazarse en cualquier dirección, pero suelen ser complicados de realizar. Un caso especial y que generalmente queda bastante llamativo son los robots bola. Son bolas con el robot por dentro. El problema que les veo es que es complicado colocar sensores externos.

Configuraciones:

Una vistos los tipos de zapatos vamos a ver las configuraciones más usadas:

  • Dos ruedas motrices, una o dos ruedas locas: Habitual en los kits de iniciación. Es una configuración sencilla y barata. Dos ruedas motrices de gran tamaño a los lados con una rueda loca delante o detrás solo para evitar que esa parte toque el suelo. Es fácil de programar. Cambiarlo de dirección es tan solo hacer girar cada rueda en un sentido, lo que facilita enormemente los cálculos. Su punto débil es que las ruedas local pueden trabarse muy fácilmente ante los obstáculos.
  • Cuatro ruedas motrices: Distribuidas dos a dos como en un coche, solo que ninguna es directriz. Gira como el caso anterior haciendo girar las dos ruedas de un lateral en un sentido y las otras dos del otro en otro. Lo normal es que lleve cuatro motores lo que tiene la ventaja de que cada rueda puede girar a una velocidad diferente, lo cual suena muy bien pero es más difícil de aprovechar de lo que parece y si no se hace bien puede ocurrir que se entorpezcan entre ellas. No es mucho más complicado de programar que el anterior, pero a cambio si que requiere más electrónica y un montaje más complicado.
  • Dos ruedas motrices, dos ruedas fijas: A menos que vaya a ir la mayor parte del tiempo en linea recta o sobre raíles es una mala idea. La única ventaja sobre las ruedas locas del primer caso es que las fijas permiten moverse en terrenos más abruptos
  • Dos orugas: La forma correcta de hacer el caso anterior. La programación y electrónica es la de un vehículo de dos ruedas motrices sin ruedas de dirección. La complejidad viene en la parte de las orugas que mecánicamente es mucho más complicado que simplemente poner unas ruedas. En suelo liso posiblemente sea la opción que más batería consume.
  • Dos ruedas motrices: Es como el primer caso pero sin rueda loca. La idea es que si las ruedas son grandes y el cuerpo pequeño este no tocara el suelo. Es importante distribuir bien el peso a lo largo del eje de la ruedas.
  • Dos ruedas fijas motrices y dos directrices: Idéntica a cualquier coche de los que hay por la calle (también se conoce como configuración Ackerman). Requiere al menos un motor para las ruedas motrices y un servo para girar la directrices, aunque a veces se reemplaza por un motor a cambio de perder precisión en el giro que pasa a ser todo o nada sin poder elegir una posición intermedia. Posiblemente sea el más complicado para calcular los movimientos que tiene que hacer para llegar a un punto.
  • Dos ruedas fijas motrices y una directriz: La idea es muy parecida a la anterior. El resultado es un triciclo, con una rueda directriz. Al igual que la configuración Ackerman requiere al menos un motor para las ruedas motrices y un servo para girar la directriz. Su ventaja respecto al anterior modelo es que permite giros mucho mas cerrados

Distribuir el peso:

Un consejo para distribuir el peso. Lo más cerca del suelo y centrado posible. La distribución del peso afecta a muchas más cosas de las que parecen en un principio. Por ejemplo a la adherencia de las rueda, al centro de giro o a la estabilidad del vehículo.

Lo que más suele pesar son las baterías, así que su colocación es fundamental. Contra más bajas se pongan más estabilidad tendrá el vehículo y más difícil será que vuelque. Hay que tratar de colocarlas lo mas centradas posibles respecto respecto al centro de giro del vehículo y respecto a los ejes. Descompensar un lado puede traer problemas en los giros desplazando el centro de giro o restando adherencia a las ruedas.

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 corazón del aprendizaje máquina

Decenas de algoritmos de aprendizaje que van desde la sencilla regresión lineal a cosas tan complejas como las redes neuronales y casi todos (seguro que hay alguno que no solo por fastidiar) se basan en una misma y sencilla idea. Reducir al mínimo el error total de la función de aprendizaje.

Vamos poco a poco. tenemos una función f(x) que recibe una entrada de valores x = [x0,…,xn] y que tiene unos parámetros p = [p0,….,pi] que configuran la respuesta de la función. Esta función es la que aprende, el proceso de aprendizaje consiste en adaptar los parámetros p para aproximar la salida a una deseada.

Voy a ir al caso del aprendizaje supervisado que es el más sencillo de entender. Hay un conjunto de ejemplos de entrada x cada uno con una «respuesta» Y = [Y0,…,Um] asociada (la respuesta correcta). Lo ideal es que cuando a f se le pasa x devuelva una «respuesta» y = [y0,…,ym] esa respuesta sea lo más parecida posible a Y.

f(x) = y ≈ Y

Esto se ha de cumplir con todos los ejemplos que hay. Ajustamos p para que minimice la suma de los errores de cada muestra de aprendizaje.

min Σ error(y, Y)

  • f es la función que pretende usarse para aprender el modelo.
  • p son los parámetros de f cuyo valor se módica para adaptarse al modelo. Este cambio de valor es la parte que se aprende.
  • x son los valores que recibimos como entrada de los cuales nuestro algoritmo tiene que calcular y
  • Y es el valor real asociado a esos parámetros
  • y es la aproximación calculada por nuestra función f

Para calcular el error se pueden usar diversas funciones. Igual que métodos para aproximar los valores de p. Sin embargo el algoritmo será algo parecido a este:

Inicializamos p
Para cada x
y = f(p, x)
e = error(Y,y)
aproximar(p, e)

El error total no siempre se calcula sumando los errores de cada muestra, pero es algo habitual.

A veces no es tan sencillo y para cada muestra x no hay un valor Y con el que comparar los resultados. El valor puede ser una propiedad de todos los resultados obtenidos. Por ejemplo muchos algoritmos de «clustering» se basan en minimizar la distancia interna entre elementos del cluster y maximizar la distancia externa entre los clusters elegidos.

Todo esto se puede complicar bastante y cambiar algunos detalles, pero en el fondo de todo algoritmo de aprendizaje hay una función de optimización que trata de reducir el error.

Convertir de números a palabras. Cardinales

Pasar de números a palabras es una tarea bastante sencilla. En esta entrada vamos a ver los cardinales y dejaremos los ordinales para otra.

Los números en castellano repiten la forma de nombrarse cada tres dígitos: Unidades, decenas, centenas. Si sabes decir los números del 0 al 999 sabes decir todos los demás sólo hay que añadir detrás el valor correspondiente: mil, millón, billón, trillón,… Hay que recordar que en español el mil se repite entre millón, billón, trillón,…

Cómo la forma se repiten el primer paso será separar el número en bloques de tres dígitos y escribir su valor correspondiente. En español hay dos maneras válidas de componer el texto que representa a estos tres dígitos:

centenas + » «+decenas+»i»+unidades

centenas + » «+decenas+» y «+unidades

Nosotros usaremos la segunda por ser la más frecuente. Aunque los números menores de 30 se realizan con la primera forma.

Centenas:

100 ciento *
200 doscientos *
300 trescientos *
400 cuatrocientos *
 500 quinientos *
600 seiscientos *
700 setecientos *
800 ochocientos *
900 novecientos *

Decenas:

10 dieci*
20 veinti*
30 treinta y *
40 cuarenta y *
50 cincuenta y *
60 sesenta y *
70 setenta y *
80 ochenta y *
90 noventa y *

Unidades:

1 uno
2 dos
3 tres
4 cuatro
5 cinco
6 seis
7 siete
8 ocho
9 nueve

Excepciones:

0 cero
10 diez
11 once
12 doce
13 trece
14 catorce
15 quince
20 veinte
100 cien

Con esto ya podemos decir todos los números. Cada tres números hay que añadir un indicador de valor:

*.000 * mil
*.000.000 * millones
*.000.000.000 * mil (millones)
*.000.000.000.000 * billones
*.000.000.000.000.000 * mil (billones)
*.000.000.000.000.000.000 * trillones

Aquí nos encontramos con las excepciones cuando X es 1

1.000 mil
1.000.000 un millón
1.000.000.000 mil millones
1.000.000.000.000 un billón
1.000.000.000.000.000 mil billones
1.000.000.000.000.000.000 un trillón

Parece muy complicado pero son unas pocas reglas muy sencillas de codificar.

De palabras a número

Como acabamos de ver nuestro sistema de nombrar los números se basa en repetir bloques de tres números y añadir un modificador de valor (miles, millones,…). El proceso de transformar texto a número consiste en:

  1. iniciar la variable number a 0
  2. ver si el texto empieza por alguna de las palabras de las tablas anteriores
    1. Si la palabra representa centenas, decena o unidades se sumar el valor correspondiente a number.
    2. Si la palabra es un modificador de valor se multiplica por el valor que representa
  3. eliminar palabras encontrado del principio del texto
  4. volver al punto 2 hasta que se termine el texto o no se encuentren coincidencias

Antes de tratar el texto y para facilitarnos el procesado pasamos el texto a minúsculas, eliminamos de la cadena, las tildes, espacios y reemplazamos las «y» por «i».

«Veintidós mil trecientos quince»

Primero tratamos la cadena;

«veintidosmiltrecientosquince»

Buscamos la primera coincidencia

veinti -> number +20
dos -> number +2
mil -> number *1000
trescientos -> number +300
quince -> number +15

number = 22315

Podéis ver le código aquí.

De número a palabras

Para hacer el paso de número a palabras hay que dividir el numero en bloques de tres dígitos y tratar cada uno de forma independiente usando las tablas del principio

123456

123 – «ciento» + «veinti» + «tres»

456 – «cuatrocientos» + «cincuenta» + «y» + «seis»

Añadimos el modificador de valor correspondiente

«ciento veintitres» + «mil»

«cuatrocientos cincuenta y seis»

Unimos las cadenas:

«ciento veintitres mil» + «cuatrocientos cincuenta y seis»

Resultado:

ciento veintitres mil cuatrocientos cincuenta y seis

Queda un último paso que es reemplazar las excepciones, por ejemplo «uno mil» por «mil» o «veintitres» por «veintitrés» (como en este caso):

ciento veintitrés mil cuatrocientos cincuenta y seis

El código aquí.

Género

¿Género? Si, algunos valores pueden tener género y son casos que hay que tener en cuenta. En concreto las centenas y la terminación «uno».

doscientas, trescientas, cuatrocientas, una, veintiuna, …

Decimales

Cuando hay decimales se indica con el separador: «coma», «punto», «con» y se lee el número decimal como si fuera otro número. Cumplen las mismas reglas, a excepción de que son femeninos. Si no se pone indicador de valor significa que es el número leído desde la coma.

«Con tres milésimas» = ,003

«Con tres» = ,3

Para los decimales el texto se separa en dos partes, la entera y la decimal, y se procesan por separado. La parte decimal se procesa igual que la entera. Pero al final hay que añadirle las unidades, que dependerán del número de dígitos.

0,0X décimas
0,0X centésimas
0,00X milésimas
0,00000X millonésima

Es importante tener en cuenta que los primeros números pueden ser ceros que no afectan a la forma de escribirlo, pero si al valor que se añade al final:

,3  = «tres décimas»

,03  = «tres centésimas»

,003  = «tres milésimas»

Cómo JavaScript es tan «particular» con los decimales se ha optado por usar dos enteros, uno representa la parte decimal y otro el número por el que hay que dividirla.

No sólo números

En los números no sólo hay números, hay otros símbolos a tener en cuenta. Separadores de miles, separador decimal, símbolo de menos, monedas, unidades, símbolo de %.

 

 

 

 

Generar frases de forma automática a partir de plantillas

Ya hemos visto otras formas de generar frases. Esta forma de generar frases consiste en tener unas plantillas de las que sólo algunas partes son variables.

Por ejemplo:

Al abrir el cofre encuentras un ${adjetivo} ${objeto} para ${acción}

Al usar una plantilla las frases generadas tienen sentido y es fácil integrarlas con otras generadas de la misma forma. Los textos generados no tienen tanta variedad como con otros métodos pero te aseguras el sentido de los mismos.

Hay un curioso proyecto en inglés que partiendo de una base de datos de palabras en inglés te permite generar un frase a partir de un aplantilla en la cual solo indicas {{an_ adjetive}}, {{noum}} y el lo rellena con palabras al azar de la base de datos (de términos terroríficos al parecer)

Por ejemplo:

This is {{ an_adjective }} {{ noun }}. => This is a wicked cadaver.

En castellano el principal problema que tiene este método son  distintas flexiones que tiene las palabras y la necesidad de que las distintas palabras de la frase tengan concordancia entre ellas. «Caperucita rojo» es claramente incorrecta. Así que nosotros tenemos que aportar género y número para nombres, complementos, adjetivos. Y en el caso de los verbos persona, tiempo y modo. Para solucionar este problema hay tres opciones:

  • Limitar las opciones, generamos frases solo con un género, número, tiempo y modo.
  • Crear diccionarios diferentes para cada opción. Por ejemplo para los nombres se crearían cuatro diccionarios: femenino-singular, femenino-plural, masculino-singular, masculino-plural.
  • Usar un algoritmo que modifique las flexiones de cada palabra Algo de eso ya hemos visto en este blog. Es un algoritmo al que le pasas una palabra y la forma de la misma que quieres obtener y te la transforma. Ya hemos visto algo así.

En JS generar este tipo de frases es muy sencillo, preparamos una plantilla.

‘Al abrir el cofre encuentras un ${objeto} ${adjetivo} para ${acción}`

Crearemos una lista de opciones:


var adjetivos = ["épico","dorado","brillante"];
var objetos = ["mazo", "mandoble", "arco"];
var acciones = ["matar dragones", "rescatar princesas", "luchar batallas"];

Ahora usaremos una función para elegir uno de ellos al azar:

function selectOne(items){
return items[Math.floor(Math.random() * items.length)];
}

Y finamente otra que rellena la plantilla:

function template(objeto, adjetivo, accion){
return `Al abrir el cofre encuentras un ${objeto} ${adjetivo} para ${accion}`
}

Vamos con el ejemplo de funcionamiento:

template(selectOne(adjetivos), selectOne(objetos), selectOne(acciones));

Ejemplo de resultados:

  • «Al abrir el cofre encuentras un brillante arco para luchar batallas»
  • «Al abrir el cofre encuentras un épico arco para matar dragones»
  • «Al abrir el cofre encuentras un dorado mazo para matar dragones»

Este sistema para generar frases tiene la ventaja de que estas tienen sentido pero tiene la desventaja de que resultan repetitivas y si se usa varias veces en seguida se percibe el patrón.

Un mejora necesaria seria usar un algoritmo de flexiones para encontrar el género y número del objeto y adecuar el del adjetivo. Imaginaros que uno de los objetos es «espada», el adjetivos debería ser femenino.

 

 

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.

 

Generar frases de forma automática a partir de textos

Ya hemos visto, sin entrar en detalles, un sistema para generar frases en la entrada sobre los bots y las respuestas en lenguaje natural.

La solución más común para generar texto de forma automática es usar cadenas de Markov. Explicándolo pronto y mal consiste en tener un grafo para cada palabra, esa palabra está conectada con el resto de las posibles palabras que pueden seguirla. Cada enlace tiene asociada la probabilidad de que esa sea la siguiente palabra. Además de palabras hay que tener en cuenta dos nodos especiales: «inicio de frase» y «fin de frase». El primer nodo es por el que se empieza para formar una frase y al llegar al segundo se termina la frase.

La forma de usar este grafo es sencilla. Se parte del nodo «inicio de frase». El siguiente nodo se elige al azar, cada nodo tiene una probabilidad distinta de ser elegido. Una vez un nodo es elegido se repite la operación en ese nodo hasta llegar al nodo de «final de la frase».

Generar estos grafos a mano seria demasiado trabajo, por eso estos grafos se crean fácilmente a partir de textos. Tan sencillo como recorrer los textos e ir contando para cada palabra cual es la siguiente. Por ejemplo para la palabra «perro» tomando como base del aprendizaje las siguientes frases:

  • El perro ladró a la bicicleta.
  • El perro lamió la mano de su dueño
  • El perro olisqueó la comida antes de comerla
  • El perro olisqueó la prenda antes de seguir el rastro
  • Como el perro y el gato

El resultado serían:

GrafoPerro

Olisqueó tiene el doble de probabilidades de ser elegida como siguiente palabra que las demás.

El problema que vemos aquí es con las palabras muy habituales como «y». Detrás de «y» puede ir cualquier cosa que no tenga ninguna relación con las palabras anteriores de la frase. ¿Como evitamos eso?. En lugar de generar el grafo de una sola palabra lo generamos de dos o tres. En nuestro caso si lo generamos de dos palabras quedaría así:

El problema de usar más de una palabra es que el tamaño de los textos de aprendizaje ha de ser grande o se repetirán siempre las mismas frases. Vamos a añadir una frase más para que no todas empiecen por «el perro»

  • Al perro le gusta jugar con la pelota

El resultado

 

GrafoPerron

Con más palabras de profundidad las frases tienen más sentido, pero su variedad se resiente. Al final hay que buscar un equilibrio.

Este sistema sirve para generar frases que a veces tienen sentido o que por lo menos dan la sensación de que quieren decir algo. Es importante buscar textos similares para generar grafos que mantengan el sentido. Si por ejemplo mezclamos textos de gatos, animales, con gatos, herramienta, puede ser divertido el resultado pero difícilmente tendrá sentido. Al final el resultado de estas herramientas es más artístico que practico y el texto generado difícilmente tendrá sentido más allá de una frase, aunque si los textos están bien elegidos tendrás la sensación de que quiere decir «algo».

Un ejemplo muy sencillo de usar es esta aplicación cuyo uso es muy sencillo, basta con preparar los textos de ejemplo y pasarlos al generador:

python markov.py gen <name> <count>

  • Name es el nombre del fichero que contiene esos textos
  • Count es el nivel de profundidad de los nodos (el numero de palabras que tiene en cuenta)

Una de la ventajas de este sistema es que solo depende del idioma de los textos usado como fuente, el algoritmo funciona igual en todos los idiomas.

Reemplazar caracteres

Una de la funciones más habituales de analizar texto como lenguaje natural es el reemplazar caracteres: quitar tildes, cambiar mayúsculas por minúsculas, reemplazar signos de puntuación, etc. Su utilidad es simplificar el texto para facilitar el análisis, la búsqueda en diccionarios o la separación en tokens.

Hay muchas funciones para reemplazar caracteres, desde el remplace más simple hasta cosas más complejas como expresiones regulares. ¿Para qué inventar uno nuevo?. Por sencillez y velocidad. El método que utilizamos aquí es muy simple, no permite reemplazos complicados como las expresiones regulares, solo remplazar un carácter por otro, pero a cambio es capaz de realizar todos los reemplazos en una sola iteración al texto.

Su funcionamiento es sencillo, se crea un hashmap donde el carácter a remplazar es la clave y el carácter que lo reemplaza el valor. Luego se recorre el texto caracter a caracter y se consulta en el hashmap si existe un valor para el carácter actual, si es así se remplaza, si no se deja el carácter tal cual.

Para usarlo basta con añadir los caracteres, se pueden añadir de uno en uno o varios a la vez.

replace.add(«A»,»a»);

replace.add(«Aåáäâ», «a»);

También se puede reemplazar un carácter por varios.

replace.add(«/»,» dividido por «);

No así al revés, no se pueden reemplazar varios caracteres.

Si se reemplaza por una cadena vacía será como borrar el carácter del texto

replace.add(«:», «»);

Una vez configurado el diccionario basta con llamar al método replace con el texto a tratar, como resultado devolverá el texto con los reemplazos realizados.

text = replace.replace(text);

En el repositorio de la libreria se puede ver un ejemplo de funcionamiento. En él se crea un conversor a «ninini» (léase con ritmo de «chincha pincha»). Para ello se transforman todas las vocales a letras «i».

En el ejemplo se puede ver que no bastaria solo con reemplazar los caracteres hay casos como «qii» y «gii» que necesitan un tratamiento posterior. Por eso es importante elegir bien que métodos de reemplazo a usar y el coste en tiempo de los mismos.

Finalmente hay una funcionalidad más que aporta esta librería. Si se fija un valor para la propiedad default cuando no se encuentre un carácter en el hashmap, en lugar de dejarlo igual, lo reemplazará por el valor de default. Por ejemplo en este caso eliminará todos los caracteres no

replace.setDefault(«»);