Estamos en un momento en el que los chatbots y los asistentes virtuales parecen surgir como setas, y si tienes una empresa y no tienes algún proyecto de chatbots parece que te estés quedando atrás. Los chatbots se basan en técnicas de Procesamiento del Lenguaje Natural (NLP, Natural Language Processing) para entender lo que les dice. Pero cuando a alguien le preguntas "¿y cómo funciona?" te suelen contestar un "pues con inteligencia artificial", y si tratas de rascar una respuesta más válida, te miran con cara rara como si fueses un bicho raro y repiten como un mantra "pues con inteligencia artificial, ¿qué no entiendes?".
Todo ello crea un halo de misticismo y una sensación de que hay unos avances en inteligencia artificial brutales, y te deja pensando que cualquier día te despiertas y tu tostadora tratará de matarte mientras tu Google Home chilla "¡muerte a los seres de carne!". Y bueno, no te quedas tranquilo del todo. Y por eso es mejor entender qué es la inteligencia artificial y cómo los chatbots entienden el lenguaje de los humanos.
Por cierto, se me escaparán muchos términos en inglés, pero es que es un tema técnico y la terminología común es en inglés, si yo os digo "declaración y propósito" en lugar de "utterance e intent", seguramente cuando vayáis a leer sobre el tema no os enteraréis de nada. Lo siento de antemano, pero mejor ceñirse a la terminología técnica.
Utterances e intents
Son nombres raros, pero conceptos sencillos. Una utterance es una frase dicha en lenguaje natural, es decir, tal y como la dice una persona. Un intent vendría a ser la intención de dicha frase, lo vemos mejor con esta tabla, que será el ejemplo que veremos a lo largo de todo el artículo:
Una cosa a entender es que cuando tienes un chatbot, tienes una lista de intents, que son las cosas que puede realizar tu chatbot, y para cada intent has entrenado con una o varias utterances. Pero puede suceder que el usuario diga una frase que no se corresponde a nada que el chatbot sepa hacer, y en esas situaciones la inteligencia artificial debe ser capaz de suponer que no es ninguno de los intents, para poder responder al usuario algo como "Lo siento, no entiendo tu pregunta" o similares.
Tokenization
Antes de entrenar, cada una de las frases debe descomponerse en una lista ordenada de palabras. A este proceso se le llama "tokenization". Durante este proceso se puede decidir si los signos de puntuación son retirados o no, dependiendo de cómo queramos trabajar con las frases. Supongamos que nuestro método funciona sin signos de puntuación, entonces nuestro ejemplo anterior quedaría así:
Este proceso que puede parecer sencillo en castellano, se vuelve más arduo en otros idiomas. Pensad sin ir más lejos el inglés la frase "I'm here but I don't see you", la descomposición tiene que romper las contracciones: [I, am, here, but, I, do, not, see, you]. Este proceso se hace especialmente complicado en idiomas como japonés o chino. En japonés hay dos silabarios, hiragana y katakana, en los que cada símbolo respresenta a una sílaba, y además están los kanjis en los que cada símbolo es un idiograma que representa una idea. Para hacer este proceso en japonés, es necesario tener un proceso de conversión de kanjis y de hiragana hacia katakana.
Normalización
Es un proceso opcional pero útil en muchos casos, y básicamente consiste en pasar a minúsculas y opcionalmente eliminar símbolos especiales como las tildes, diéresis y demás.
Stemming y Lemmatization
Ahora que tenemos las palabras, tenemos un problema con palabras derivadas, conjugaciones y similares. Por ejemplo "viajando", "viajé", "viajaré", de todas esas formas entendemos que el verbo es "viajar". El lema sería saber calcular la palabra de base con significado existente en el diccionario, en este caso "viajar", y a la acción de calcular ese lema se le llama "lemmatization". Por desgracia este proceso es duro de hacer porque implica tener un diccionario con todas las formas diferentes que pueden tener las palabras, y eso implica mucho trabajo, mucho espacio, memoria y tiempo de búsqueda.
Pero además del lema, existe el concepto de raíz. La raíz de las palabras "viajando", "viajé" y "viajaré" es "viaj". A la acción de buscar esta raíz se le llama "stemming". La ventaja del stemming es que hay algoritmos para hacerlo, que aunque no son perfectos, son suficientemente buenos.
Aquí tendríamos nuestro ejemplo tras aplicar stemming:
Features y Frecuencia
A cada uno de las diferentes raíces que hemos calculado la llamaremos "feature". Además podemos calcular la frecuencia que tienen nuestras features en el modelo que queremos entrenar.
Ahora para cada frase podemos calcular un vector de features, en el que cada posición representa a una feature, y si esa frase la tiene ponemos un 1 (o un true) y si no la tiene pondremos un 0 (o un false). Nuestros vectores de features de los datos de entrenamiento quedaría así:
Clasificación
Ahora es cuando empieza el proceso. ¿Qué sucede cuando alguien le dice algo al bot? Pues que esa frase debe ser clasificada, es decir, se debe decidir si se corresponde con alguno de los intents de nuestro modelo, y no solamente eso, sino que nos debe decir qué seguridad tiene de ello, a ser posible mediante una probabilidad.
Por cierto, aprovecho para decir que en inteligencia artificial una clasificación es cuando dado un input debemos calcular el output dentro de un ámbito discreto, es decir, conocemos de antemano una lista finita de posibles outputs. Pero también existen las regresiones dónde dado un input se calcula el output dentro de un ámbito contínuo, valores numéricos por ejemplo.
Para ello siempre la fase que entra pasará por las fases anteriores: tokenization, normalization, stemming y finalmente obtener un vector de features. ¿Qué sucede cuando la frase contiene palabras cuya raíz no es ninguna de las features que tenemos? Pues que esas palabras se descartan.
Supongamos que al bot le decimos "Dime cómo te haces llamar". El proceso sería el siguiente:
Naive Bayes Classifier
Muchas veces al lidiar con la clasificación la gente opta por algo llamado Naive Bayes Classifier o Clasificador Bayesiano Ingenuo. Esto se suele hacer porque es un método que no require entrenamiento, y es meramente estadístico, con lo cual es rápido de calcular. Por desgracia sus resultados en el ámbito de chatbots no son muy buenos que digamos.
El método es sencillo, para cada intent se hace el siguiente cálculo: se cuentan en cuántas de las observaciones del intent está presente cada feature, y ese número se divide entre el número de observaciones y se calcula el logaritmo neperiano. En nuestra frase "dime cómo te haces llamar" vemos que la feature "com" está presente en una observación, así que el valor sería ln(1/3) = -1.09861.
Hacemos lo mismo para cada feature, y sumamos los resultados. El total nos da -4.39444.
Ahora elevamos e a ese resultado (0.01235), lo multiplicamos por el número de observaciones de ese intent (3) y dividimos entre el número total de observaciones de nuestro modelo (6) y el total nos da 0.006175.
En el caso del intent llamar, hacer las mismas operaciones nos lleva al resultado 0.
Inteligencia Artificial
Aquí es dónde llega la magia. La inteligencia artificial la hay de muchas formas y colores, pero hoy vamos a hablar del perceptrón.
Suponed que tenéis una función f que no sabéis cuál es, pero sí sabéis varios x diferentes (x1, x2, x3,...) y cuánto vale f(x) para cada uno (y1, y2, y3,...). Y os piden que hagáis una función g que se comporte lo más parecido posible a la función f.
Lo primero que hay que preguntarse es, ¿cómo sé yo si dos g diferentes que me invente, cuál es la que mejor se aproxima a f? Haciendo una función que me diga cuál es el error, un simple número que cuanto más bajo mejor. Esta función la encontraréis con el nombre de función de coste, loss o error. El objetivo de una inteligencia artificial es calcular esa función g, y para saber lo bien que lo está haciendo necesita esa función de coste porque es la que va a usar para estimar si va por el buen o mal camino.
Y aquí es dónde entra en juego el perceptrón:
El perceptrón no es más que una función que dado un vector entrante, multiplica cada término por un número distinto llamado peso y al resultado le suma un número llamado bias. Cuando ha calculado esto, aplica una función llamada función de activación. Si nuestra función f admitía 3 parámetros, f(x1, x2, x3), eso significa que tendremos 3 pesos w1, w2 y w3, y el bias, y nuestro resultado será activación(x1*w1 + x2*w2 + x3*w3 + bias).
Si combinamos varios perceptrones en forma de red, tendremos muchísimos pesos y bias por el medio a calcular, pero la función resultante puede ser cada vez más compleja.
El problema está en, ¿y cómo calculo esos pesos y ese bias de mi modelo? Podríamos hacerlo a mano, empiezas poniendo varios al azar, miras el resultado y aplicas la función de coste, y luego vas haciendo cambios y calculas de nuevo la función de coste y así poco a poco vas viendo si vas bien o mal... Pero eso es un tostón. Y aquí es dónde viene en nuestra ayuda algo llamado optimizador. Un optimizador es el verdadero cerebro de una inteligencia artificial y es el que va a calcular esos pesos y bias por nosotros, usando la función de coste para guiarse. ¿Y cómo lo hace? Bueno, os podría hablar del descenso de gradiente, y cómo es un problema bastante complejo en el que trabajó incluso Newton, pero vamos a simplificarlo un poco porque no es el objetivo. Pero quedémonos conque tiene que buscar el mínimo de la función de coste y para ello hace cambios en los números. Y el tamaño de cada cambio que puede hacer se denomina learning rate. Y esto es muy importante, porque sucede lo siguiente:
Si el learning rate es muy bajo, calculará muy bien el mínimo, pero llevará mucho tiempo. Si el learning rate es muy alto, puede suceder que no encuentre un mínimo satisfactorio y pasaría que no aprendería. Así que hay que encontrar el learning rate adecuado para que obtenga una solución suficientemente buena en un tiempo aceptable.
Logistic Regression Classifier
Entendido lo anterior, ¿cómo lo aplicamos a nuestro problema? Bien, suponed que para cada intent tenéis un perceptrón. No hace falta más la verdad. Cada uno de esos perceptrones responderá a una simple pregunta: ¿qué probabilidad hay de que la frase que dice nuestro usuario, que es este vector de unos y ceros que representan features, se corresponda con las frases que tiene ese intent? Y con eso puedes calcular los pesos, y el modelo quedaría así:
La segunda y tercera columna son los pesos para cada uno de los intents. ¿Por qué resulta que son los mismos pero negados? Porque solamente hay 2 intents, y las features no se solapan, así que las que son buenas para un intent, son malas para el otro en la misma cantidad.
La cuarta columna es nuestro vector de features, la quinta columna es la segunda multiplicada por la cuarta, y la sexta columna es la tercera multiplicada por la cuarta. Resulta que nuestra frase nos devuelve el número 4,823266 para el intent "nombre" y -4,82327 para el intent "lugar". Pero recordad que hay algo llamado función de activación: en nuestro caso elegimos la sigmoide, pero podría haber sido perfectamente la tangente hiperbólica. Y los resultados son que está un 99,2% seguro de que el intent es "nombre".
¿Y cómo se yo si una IA es mejor o peor que otras para hacer chatbots?
Bueno, no hay mucho material al respecto, pero hay un paper de base de la Universidad de Munich: workshop.colips.org/wochat/@sigdial2017/documents/SIGDIAL22.pdf
El paper propone probar con 3 corpus diferentes, es decir, con 3 conjuntos de utterances/intents, y para cada utterance te dice si es un dato para entrenar o un dato para probar, de forma que el sistema de NLU verá los datos para entrenar, y luego debe calcular los resultados para datos que no ha visto nunca.
Y estos son los resultados que hay de esa evaluación para diferentes sistemas de NLU. A destacar que todos son en la nube excepto RASA y NLP.js. Esto es importante porque si vuestra empresa maneja datos delicados, normalmente no querréis mandar las conversaciones de vuestros usuarios a Microsoft, Google, IBM, SAP u otra empresa de terceros.
Extracción de entidades
Solamente con saber el intent basta para según qué problemas, pero para otros no es suficiente y hay que hacer algo llamado extracción de entidades. ¿Qué significa eso? Suponed que queréis hacer un chatbot para gestionar viajes, y para poder dar opciones al usuario necesitáis tres datos: la ciudad de origen, la ciudad de destino y la fecha. Está claro que esos datos no van en el intent. Pensad en la frase "quiero viajar de Madrid a Londres mañana", el intent sería "viajar", pero necesitáis la entidad ciudad de origen "Madrid", ciudad de destino "Londres" y fecha "mañana".
La extracción de entidades se suele hacer con reglas (expresiones regulares) más que con inteligencia artificial. Una de las maneras más sencillas es simplemente tener una lista de palabras, por ejemplo en el problema anterior la lista de ciudades, aunque ya véis que no bastaría porque un usuario puede decir "quiero viajar de Madrid a Londres" o puede decir "quiero viajar a Londres desde Madrid", así que normalmente hay más reglas por detrás.
Slot Filling
¿Qué pasa cuando necesitas obligatoriamente unas entidades y el usuario no te las dice todas? Pues que tienes que preguntar las que faltan. A este proceso se le llama slot filling. En el ejemplo anterior el usuario podría decir "quiero viajar mañana a Londres", y el bot tendría que reaccionar preguntando "¿Desde dónde viajar?".
Pues a mí me habían hablado de algo llamado PoS
PoS significa Part of Speech. ¿Recordáis en lengua cuando en una oración teníamos que saber sujeto, predicado, complemento del verbo, adverbios,...? Pues eso es exactamente lo mismo. Por ejemplo:
Habíamos visto en la parte de entidades que era complejo saber, dada una lista de ciudades, si una ciudad en concreto es el origen o el destino. Si miramos el gráfico anterior vemos que el PoS nos relaciona Madrid con "desde" y podemos inducir de ello que es el origen, y nos relaciona Londres con "hasta" y podemos inducir de ello que es el destino.
¿Y qué es un n-grama?
¡Pero si yo he mencionado n-gram en todo el artículo! Pero ya que preguntas, son secuencias de n palabras consecutivas. Por ejemplo en la frase "me gustan mucho las lentejas" podrías calcular 4 bigramas [(me, gustan), (gustan, mucho), (mucho, las), (las, lentejas)], 3 trigramas [(me, gustan, mucho), (gustan, mucho, las), (mucho, las, lentejas)]... etc.
Esto es muy útil para saber la frecuencia con la que varias palabras van juntas, para calcular modelos de Markov. ¿Sabéis la predicción de palabras en móviles por ejemplo? Pues se basan en esto.
También son útiles en criptografía, pero eso es otra historia.
¿Y el word2vec del que se habla tanto?
Bueno, NLP es un ámbito muy grande con muchos temas de estudio. word2vec propone representar las palabras como vectores en un espacio n-dimensional en el que cada dimensión representa a una cierta característica de la información. Pero esto no se suele usar en chatbots, porque normalmente no hay corpus suficiente como para analizar y crear los vectores. Pero por ejemplo analizando libros o documentación sí.
Estos vectores tienen unas propiedades muy curiosas, como que por ejemplo se pueden sumar o restar, y.. bueno, recordad que cada dimensión era una característica. Suponed la palabras "Rey" y "Reina", sus vectores serían muy similares, lo mismo que las palabras "hombre" y "mujer". Pues resulta que puedo hacer esta operación vectorial: "Rey - hombre + mujer" y obtengo un vector, que resulta que será el mismo o el más cercano a "Reina".
Lo mismo si educase mi modelo con países, ciudades y datos geopolíticos, si hago "París - Francia + España" me daría el vector de Madrid.
Conclusión
La inteligencia artificial no es magia, y tampoco es un Terminator. Es lo que es y sirve para lo que sirve, y en el caso del NLU para chatbots el resultado es bastante satisfactorio. Además, según tienes un chatbot funcionando, cada vez tienes más información de qué piden los usuarios y cómo lo piden, y puedes reentrenarlo para que cada vez sea capaz de responder a más y más cosas y hacer más y más acciones.