chatbot-periodismo

TL; DR:

En tan solo 23 líneas de código se puede construir, en javascript, un sistema NLP para detectar el intent de una frase utilizando un stemmer de la librería Natural, y la librería brain.js para la red neuronal. Este NLP daría los siguientes resultados comparándolo con otras herramientas comerciales, siguiendo el paper SIGDIAL22 que propone utilizar 3 corpus diferentes para medirlo:

El código se puede ver en el sandbox que está al final del artículo.

Introducción

A la hora de desarrollar un chatbot, una de las piezas fundamentales es el NLP (Natural Language Processing), en concreto cómo conseguir entender qué es lo que quiere el usuario de las múltiples acciones que sabe realizar nuestro chatbot.

Esta parte tan fundamental, normalmente se convierte en una “caja negra” servida por un proveedor como pueden ser DialogFlow (Google), Microsoft LUIS o IBM Watson. Sobre estas “cajas negras” hay desconocimiento de lo que sucede dentro o cómo lo hacen, y esto normalmente causa miedo a implementar uno, o a creer que la implementación realizada sea correcta.

La estructura de este artículo es primero ver la teoría, para más tarde ver el ejemplo práctico con código y cómo medirlo.

Técnicas

Cuando hablamos de NLP, estamos hablando de un área enorme de la Inteligencia Artificial, con multitud de técnicas como LSTM, word2vec, PoS (Part Of Speech),…

En nuestro caso lo vamos a hacer con un clasificador multiclase utilizando una red neuronal. Pero vamos paso por paso.

Qué es un clasificador multiclase

Dentro de NLP hay multitud de ramas y técnicas. En nuestro caso lo que queremos implementar es un clasificador multiclase. Pero, ¿qué significa esto?

Un clasificador es, dentro de la inteligencia artificial, aquella que nos sirve para dado un input clasificarlo hacia la clase (o label) que más se identifique con ese input, siendo estas clases algo discreto y finito… es decir, que es algo enumerable, por ejemplo los colores del arco iris. Si fuese contínuo, por ejemplo que la respuesta fuese un número real entre 0 y 1, entonces ya no sería una clasificación sino una regresión.

Cuando nuestro clasificador tiene dos clases se denomina binario, por ejemplo si tuviésemos un clasificador entrenado para diferenciar entre mails que son spam de los que no lo son, sería binario, dado que hay dos clases: “spam” y “no spam”.

Cuando nuestro clasificador tiene varias clases, se denomina multiclase.

Cómo funciona un clasificador multiclase

Hay varias formas de implementarlo, pero vamos a explicar cómo hacerlo con redes neuronales. El concepto fundamental es entender el perceptrón:

Perceptrón

Un perceptrón no es más que dado un vector de inputs, cada uno de los inputs se multiplica por un número real que se denomina “peso”, y cada input tendrá el suyo. Además, hay un concepto llamado “bias”. Cada input se multiplica por su peso y todos ellos se suman y se les suma el bias, y finalmente ese resultado se pasa a una función llamada “activación”. Con lo cual en cada perceptrón la cantidad de variables que la IA debe calcular es igual al número de inputs (los pesos) más uno (el bias).

En un clasificador multiclase tendremos el mismo escenario, pero en lugar de una salida habrá varias, con lo cual serían tantos perceptrones como clases. Con lo cual la cantidad de variables a calcular será (n+1) * c, siendo n el número de inputs y c el número de clases.

Qué son las clases en un NLP

En un NLP el input es la frase que dice el usuario, pero, ¿y las clases? Las clases son lo que se denomina “intents”, es decir, las acciones que sabe llevar a cabo el bot o a las que sabe responder. Supongamos este ejemplo:

Habría dos clases: Saludar y Viajar. A cada una de las frases se les llama “utterance”.

Cómo construir el input de un NLP

En el ejemplo anterior tenemos diversas frases y los intents con los que se relacionan. Ahora ya sabemos las clases, que serán los intents. Pero, ¿cómo calcular nuestro input? Nuestro input de la red neuronal estará compuesto por “features”, es decir diversas características de la frase que podemos cuantificar de una manera numérica. Vamos a por la manera más fácil de verlo, supongamos que nuestras features son las palabras de cada frase:

En este caso tendríamos 13 features diferentes, una por palabra. Cada línea azul representaría un peso hacia la clase “Saludar”, mientras que cada línea verde representaría un peso hacia la clase “Viajar”. ¿Cómo cuantificar en número el input? La primera y más intuitiva de las maneras es: Si la feature (palabra) está presente en la frase entonces vale 1, y si no lo está vale 0. Por ejemplo la frase “Me quiero ir de vacaciones” tendría los siguientes inputs:

Antes de programar: ¿cómo medir?

Antes de empezar a programar, ¿cómo mediremos que nuestro NLP está actuando de manera correcta? ¿que acierta los intents?

Para ello nos vamos a basar en el siguiente paper, SIGDIAL22: http://www.aclweb.org/anthology/W17-5522

Este paper propone, para probar en inglés, la utilización de 3 corpus diferentes: uno del chatbot de transporte de Munich, otro del foro Ask Ubuntu y otro sobre Web Applications. Ejemplos de frases e intents de cada uno:

Chatbot: i want to go marienplatz (FindConnection); when is the next train in muncher freiheit? (DepartureTime)

AskUbuntu: What software can I use to view epub documents? (Software Recommendation); What does my computer do when I click ‘Shut Down’? (Shutdown computer)

WebApplication: Alternative to Facebook (Find Alternative); How do I delete my Facebook account? (Delete Account)

Para poder medir, he creado la librería evaluate-nlp que se usará durante el ejercicio, y que lo que contiene son los corpus antes mencionados así como las métricas ya obtenidas de otros proveedores.

Importante: cada corpus trae frases y sus intents, pero además una variable que nos indica si es para entrenar o no (training). Las frases con las que se prueba la eficacia del NLP no son las mismas que con las que se entrena.

Tokenizer

Lo primero es construir nuestro tokenizer. Lo haremos primero de la manera más simple, que es cortando el string con split utilizando la expresión regular que nos da los whitespaces.

Aquí tenéis el ejemplo usando codesandbox, que nos permitirá ver el código y a la vez hacer una API para probar nuestro progreso. En la API podemos agregar el query param text con el texto que queramos tokenizar, por ejemplo: https://73l4owjvjq.sse.codesandbox.io?text=esto%20es%20una%20prueba

Programando la Red Neuronal

Para programar la red neuronal hemos escogido brain.js, que nos permite hacer clasificadores de una manera bastante fácil, y con buena performance. Pero podría ser reemplazado por tensorflow.js, pero eso haría el código más complejo, aunque se conseguiría el mismo resultado.

Empezaremos con una función sencilla que nos crea la red neuronal:

El cambio con respecto a cómo la crearía por defecto, es que por defecto crea dos capas ocultas, es decir, calcula features derivadas de las primeras, y eso implica identificar clasificaciones más complejas además de necesitar más computación. Por ahora vamos a dejarlo tal y cómo lo hemos dibujado en puntos anteriores: el input conectado al output y calcular los pesos.

Ahora necesitaremos saber cómo entrenar nuestra red neuronal. Aquí lo complejo es cómo es el tipo de input que le gusta a brain.js, que es un array de objetos con este formato:

Así que primero construiremos una función “utteranceToFeatures” que dado un texto (un utterance) nos construye el objeto de features tal y cómo está en el input. El método chain no es más que un método que recibe un input como primer parámetro y varias funciones, y encadena las funciones, así que en este caso esto sería lo mismo que escribir featuresToDict(tokenize(str)):

Para ello construiremos una función “train” que recibe una red neuronal de brain.js para entrenar, los inputs (xs) y los outputs (ys) ordenados, y devolverá la red ya entrenada. Ojo, es un método asíncrono:

Por último, una función “predict” que dada una red neuronal y un input, nos devuelva la predicción:

Así que tendríamos nuestra primera versión del NLP, en la que el código del NLP ocupa tan sólo 13 líneas de código, vamos a probar qué tal funciona:

Si pintamos los resultados en una gráfica, vemos lo siguiente:

Bueno, no está mal para ser una primera iteración, aun está de último en el global pero porque va muy mal todavía en Web Application… pero vemos que para el corpus de Chatbot ya puntúa mejor que Dialogflow, y para el de Ask Ubuntu ya se sitúa en la mitad.

Trim y minúsculas

¿Cuál sería el primer paso para mejorarlo? Bueno, por un lado, a veces al calcular los tokens nos sale uno en blanco, el cual hay que eliminar. Por otro lado, ni siquiera hemos pasado a minúsculas, así que “Developer” no es lo mismo que “developer” para nuestro sistema. Así que implementamos el pasar a minúsculas y el trim, y los añadimos a nuestra cadena de cálculo de las features:

Los nuevos resultados:

Vemos que se ha mejorado en Web Application pero no en Chatbot ni en Ask Ubuntu. El motivo es sencillo: en los dos primeros las frases ya venían en minúsculas. Por otro lado, ¡ya hemos conseguido superar en el global tanto a DialogFlow como a RASA!

Mejorando la Red Neuronal

Lo siguiente que podemos hacer es mejorar la red neuronal. Vamos a hacer tres cambios: para empezar vamos a cambiar la función de activación. Por defecto la función de activación es la sigmoide. Sin embargo para nuestro problema van a funcionar mejor la tangente hiperbólica o la leaky ReLU.

Por otro lado, vamos a cambiar el umbral de error, que por defecto es 0.005, lo vamos a reducir a 0.00005. Por último, el learning rate por defecto es de 0.3, vamos a reducirlo a 0.1.

Los nuevos resultados:

Vemos que empata ya en primera posición en Chatbot, sigue en la mitad de la tabla en Ask Ubuntu, y empieza a posicionarse en el medio en Web Applicaation. El global ya pasa a estar en mitad de la tabla por encima de DialogFlow, RASA y Recast. Pero con una diferencia: ¡todavía no se ha tenido en cuenta el idioma! En ningún momento hemos tenido en cuenta si el idioma es inglés, español, o qué idioma es, y sin embargo ya está en mitad de la tabla.

Aplicando stemmer

Como hemos dicho al final del punto anterior, todavía ni siquiera hemos tenido en cuenta el idioma utilizado. Hay dos métodos principales: el cálculo del lema o de la raíz. El lema significaría calcular la palabra principal de la familia, por ejemplo de “eres” el lema sería “ser”. El problema del cálculo del lema es que requiere tener diccionarios, con el consiguiente espacio en memoria. Por el otro lado, el cálculo de la raíz, se puede hacer aproximado utilizando algoritmos, usualmente que eliminan y cambian posibles terminaciones para calcular así la raíz. No es perfecto, pero es suficientemente bueno.

Esto de usar stemmers no es algo nuevo dentro del mundo de IT, por ejemplo los gestores documentales los utilizan para hacer búsquedas de documentos, en el caso de lucene y solr el código java de estos es Open Source.

En nuestro caso al ser javascript, utilizaremos el PorterStemmer que viene en la librería Natural. Así que usaremos el PorterStemmer para, de cada token que calculamos una vez pasado a minúsculas, sustituirlo por su raíz:

Y los resultados quedarían así:

Como vemos, nuestro método se posicionaría como primero en el corpus de Chatbot empatado con Recast, primero en el corpus Ask Ubuntu, segundo en el corpus de Web Application empatado con LUIS y solamente superado por Watson, y quedaría primero en la puntuación global, en tan solo 23 líneas de código.

Por Jesús Seijas de la Fuente

Enfocado en proyectos de IA, principalmente IA conversacional y Visión por Computador. Autor de NLP.js, un conjunto de librerías NLP para AI conversacional, capaz de hacer clasificación, con normalizadores, tokenizadores y lematizadores para 41 idiomas, que es capaz de entrenar y clasificar en el navegador y móvil sin conexión a internet, e integrado con BERT. La red neuronal está diseñada para el problema de clasificación de PNL en términos de precisión y rendimiento.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *