Como hacer un chatbot en español y que te trolee en el intento

Rubén Chaves
Apr 19 · 8 min read

A principios de Febrero se inauguró el canal de proyectos comunitarios en el Slack de Machine Learning Hispano. Como primer proyecto se propuso crear un chatbot en español que interactuara con la comunidad. No se especificaba método, pero dado que los últimos grandes avances en procesamiento de lenguaje natural (NLP por sus siglas en inglés) se han hecho con deep learning, este parecía el método más indicado para cumplir el objetivo.

En este artículo veremos los puntos claves del desarrollo del modelo que está actualmente implementado en el bot AntonIA. En concreto, utilizaremos un tipo de RNN avanazada, un Seq2seq con Luong attention y veremos como con un vocabulario, un dataset pequeño y poco entrenamiento, se puede obtener un chatbot que cree situaciones como esta:

(Este artículo se centra en los conceptos, si quieres ver el código comentado y separado por cada fase del proceso tienes el código aquí)

Aunque por internet circulan muchos tutoriales que te guían a la hora de hacer un chatbot, suelen estar en inglés y nosotros, obviamente, lo queremos en español. Si bien es verdad que la teoría es casi igual, hay algo que no es sustituible, el dataset.

En inglés la comunidad de NLP esta más desarrollada y existen todo tipo de dataset generales y específicos para cada tarea de NLP (sentiment analysis, questiong answering, machine translation, etc). En español son menos las opciones pero aun así la tarea no es imposible. Nuestras opciones son:

  • Transfer Learning: podemos entrenar un modelo no supervisado que cree un modelo de lenguaje en español utilizando un corpus como el de Wikipedia y luego adaptarlo a la tarea de chatbot con un dataset pequeño como tus conversaciones de Whatsapp o las de una canal de Slack.
  • San Google: buscar un dataset que se adapte a la perfección a tu tarea a veces no es posible, pero si encontramos algo lo suficientemente similar, con un poco de creatividad y procesamiento, podremos conseguir nuestro objetivo. Por ejemplo, para nuestro caso, si no tenemos un dataset de conversaciones, nos valdría con uno de guiones o libros, de los que extraeremos las conversaciones buscando las líneas que comienzan con guiones.

Aunque ambas opciones se puede combinar para crear un modelo más competente, nos limitaremos a la segunda opción.

Dataset

Además existen los alineamientos entre los subtítulos entre los idiomas, útil si lo que deseamos es crear un traductor.

De las 213 millones de lineas totales, nos quedamos con menos del 0.25%, unas 500k de lineas, suficiente para crear un modelo bueno, con un vocabulario decente y que obtengamos resultados sin tener que pasar mucho tiempo procesando y entrenando.

Pre-procesing (notebook)

1. Cargar las lineas y formatearlas, para como dijimos antes homogeneizar el dataset y eliminar el mayor ruido posible para facilitar el entrenamiento del modelo, sobre todo para separar los signos de puntuación de las palabras y eliminar caracteres raros. Las varias pruebas que hice con diferentes formas de tratar las strings se pueden resumir en dos:

  • Sencillo: donde solo se separan los signos de puntuación principales, elimino los guiones iniciales (que no aparecían consistentemente) y las comas.
  • Complejo: donde se va más al caso por menor creando tokens especiales para caracteres como el guión al principio del cambio de emisor, se separan más signos de puntuación y no se elimina ninguno.

En contra de lo esperado, el procesamiento sencillo dio mejores resultados, lo que puede ser explicado por la capacidad de las redes neuronales para diferenciar uan cierta cantidad de ruido y de crear su propias features a partir del input. De este modo, ante la duda, es mejor no procesar carácter raros y que sean eliminados a la hora de crear el vocabulario, o descifrados de manera automática por la red.

2. Eliminar las que sean muy largas. En nuestro caso las de más de 20 caracteres son eliminadas, principalmente por dos razones: las RNN pueden dar problemas de estabilidad numérica (exploding/vanishing gradient), y al haber menos secuencias largas, se crearían batches con muchos ceros, desperdiciando cálculos y ralentizando el entrenamiento (mejorable si creamos los batches con las secuencias ordenadas por longitud).

3. Crear un vocabulario, tokenizarlo (convertirlo en un entero) y borrar las palabras que no superen un mínimo de apariciones, de al menos 10 veces en nuestro caso, las cuales introducirían demasiado ruido y se sustituye por el token UNK. Con este token conseguimos un modelo más robusto que permite en el input palabras que no están en el vocabulario e, incluso, podemos obtener mejor rendimiento, al contar con una palabra comodín que cuenta con la suficiente frecuencia para que a la hora de entrenar se le asigne esta función. Por simplicidad, no estamos utilizando lematización (separar las palabras en una raíz y la forma).

Al final nos quedamos con 13758 de 57473 palabras, 24% del total, suficiente para generar al completo el 80% de las lineas. Esto no significa que desperdiciemos el 20% restante, si no que tendrán palabras UNK. Mantener un vocabulario relativamente pequeño, es una buena decisión ya que el tamaño del vocabulario es de lo que más afecta al tamaño del modelo y al tiempo de entrenamiento como veremos en la siguiente sección.

4. Crear los pares de frases contiguas (pregunta-respuesta), aceptando UNK en las entradas pero no en las salidas para condicionar a la red a aprender a no decir UNK(que ya suficientes tonterías dice). También podriamos crear un vocabulario de salida diferente sin UNK, pero de esta forma nos lo ahorramos y nos permitirá usar output embeddings (más detalles luego).

Una cosa interesante que intenté pero no dió resultado para contrarestar el hecho de que en el dataset los cambios de emisor no estan siempre indicados, fue usar un token especial para los guiones del comienzo de las líneas que si marcan estos cambios y luego a la hora de la evaluación usar este token como comienzo de la respuesta (inspirada en la idea de los TL;DR de GPT-2).

Modelo (notebook)

Esquema de Seq2Seq ( de https://jeddy92.github.io/JEddy92.github.io/ts_seq2seq_intro/)

El tamaño del vocabulario es de lo que más afecta al tamaño del modelo y al tiempo de entrenamiento, ya que al estar utilizando embeddings, por cada palabra vamos a crear un vector de iguales dimensiones al output del RNN(n_palabras x hid_dim), a los que le sumamos los parámetros de la capa de salida, que convierte el output del RNN en score para cada palabra (hid_dim x n_palabras). Así, si con 13k palabras tenemos 24M parámetros, si dobláramos el vocabulario, pasaríamos a 39M.

Sin embargo, existe la posibilidad de reducir este número y es que como hemos visto estas matrices tienen dimensiones y funciones muy parecidas, siendo una la transpuesta de la otra. El embedding convierte una palabra en un vector de hid_dim, y la capa de salida convierte el hid_dim en los score de cada palabra. Es por esto que a la capa de salida se la denomina como output embedding, a la cual si hacemos que comparta los parámetros con los del input embedding (se necesita que tengan el mismo tamaño, por eso que el este el UNK token presente en la salida), reduciremos el tamaño del modelo considerablemente. Esto que puede parecer tan complejo se puede resumir en Pytorch con una línea de código:

self.out.weight = self.embedding.weight

Con esta técnica se ahorra en memoria (que no tiempo de computación), paso clave para luego pasar a modelos más grandes como los son Transformer-XL o BERT.

Entrenamiento

- Teacher forcing: normalmente, a cada paso en el decoder, se le introduce como input la palabra generada en el paso anterior. Al comienzo del entrenamiento esta palabra no tiene mucho sentido por lo que es necesario pasar la palabra correcta en vez de la generada a modo de ruedas de entrenamiento. Estas ruedas deben ser eliminadas poco a poco o confundiremos la rápida convergencia del modelo con la efectividad real.

- Graddient clipping: cada vez que los gradientes pasan un umbral se normalizan para evitar el posible underflow y overflow de estos.

Aunque las redes tienen fama de entrenar muy lento, el modelo se entreno por apenas 9 horas, eso si para llegar al modelo final muchos intentos fallaron antes.

Evaluacion

Como vemos usar un dataset de películas hace que tu bot sea un poco dramático.

Si quieres probar el chatbot puedes seguir las instrucciones del repositorio o pasarte por el Slack de MLH y mencionar a AntonIA. Ten en cuenta al hablar con el bot que diferencia las palabras con tildes y sin tilde, la puntución al acabar y al empezar pregunta o exclamacióny que ignora las mayúsculas y los espacios.