Cree su propio generador de texto de WhatsApp (y aprenda todo sobre modelos de idiomas)

Un ejemplo práctico de PNL de aprendizaje profundo de principio a fin

Una conversación normal en WhatsApp, pero no todo es lo que parece. La gente es real; El chat es falso. Fue generado por un modelo de lenguaje entrenado en un historial de conversación real. En esta publicación, lo guiaré a través de los pasos para crear su propia versión utilizando el poder de las redes neuronales recurrentes y el aprendizaje de transferencia.

Requisitos

He usado la biblioteca fastai dentro de la herramienta de investigación gratuita de Google para la ciencia de datos, Colab. Esto significa muy poco tiempo (y sin dinero) para configurarlo. Todo lo que necesita para construir su propio modelo es el código establecido en esta publicación (también aquí) y lo siguiente:

  • Un dispositivo para acceder a internet
  • Una cuenta de google
  • Historias de chat de WhatsApp

Discuto algunas teorías y profundizo en algunos de los códigos fuente, pero para más detalles hay varios enlaces a documentos académicos y documentación. Si desea obtener más información, también le recomiendo que eche un vistazo al excelente curso de fastai.

Configuración inicial de Drive y Colab

Primero, vamos a crear un espacio en Google Drive para su computadora portátil. Haga clic en "Nuevo" y asigne un nombre a la carpeta (usé "whatsapp").

Luego vaya a su nueva carpeta, haga clic en "nuevo" nuevamente, abra un cuaderno Colab y asígnele un nombre adecuado.

Finalmente, queremos habilitar una GPU para el portátil. Esto acelerará significativamente el proceso de capacitación y generación de texto (las GPU son más eficientes que las CPU para las multiplicaciones de matrices, el cálculo principal bajo el capó en las redes neuronales).

Haga clic en "Tiempo de ejecución" en el menú superior, luego "Cambiar tipo de tiempo de ejecución" y seleccione "GPU" para el acelerador de hardware.

Datos de WhatsApp

Ahora obtengamos algunos datos. Cuanto más, mejor, por lo que querrá elegir un chat con una historia razonablemente larga. Además, explique lo que le está haciendo a cualquier otra persona involucrada en la conversación y obtenga primero su permiso.

Para descargar el chat, haga clic en las opciones (tres puntos verticales en la parte superior derecha), seleccione "Más", luego "Exportar chat", "Sin medios" y si tiene Drive instalado en su dispositivo móvil, debería tener una opción para guardar en su carpeta recién creada (de lo contrario, guarde el archivo y agregue manualmente a Drive).

Preparación de datos

De vuelta al cuaderno. Comencemos actualizando la biblioteca fastai.

! curl -s https://course.fast.ai/setup/colab | golpetazo

Luego, algunos comandos mágicos estándar y traemos tres bibliotecas: fastai.text (para el modelo), pandas (para la preparación de datos) y re (para expresiones regulares).

## comandos mágicos% reload_ext autoreload% autoreload 2% matplotlib en línea
## importa los paquetes requeridos desde fastai.text import * importa pandas como pd import re

Queremos vincular este portátil a Google Drive para utilizar los datos que acabamos de exportar desde WhatsApp y guardar los modelos que creamos. Para hacerlo, ejecute el siguiente código, vaya al enlace provisto, seleccione su cuenta de Google y copie el código de autorización nuevamente en su computadora portátil.

## Colab google drive cosas de google.colab import drive drive.mount ('/ content / gdrive', force_remount = True) root_dir = "/ content / gdrive / My Drive /" base_dir = root_dir + 'whatsapp /'

Tenemos algo de limpieza que hacer, pero los datos están actualmente en formato .txt. No es ideal. Así que aquí hay una función para tomar el archivo de texto y convertirlo en un marco de datos de pandas con una fila para cada entrada de chat, junto con una marca de tiempo y el nombre del remitente.

## función para analizar el archivo de extracción de WhatsApp def parse_file (text_file): '' 'Convertir el archivo de texto de registro de chat de WhatsApp en un marco de datos de Pandas' '' # alguna expresión regular para tener en cuenta los mensajes que ocupan varias líneas '^ (\ d \ d \ / \ d \ d \ / \ d \ d \ d \ d. *?) (? = ^^ \ d \ d \ / \ d \ d \ / \ d \ d \ d \ d | \ Z) ', re.S | re.M) con abierto (archivo_texto) como f: data = [m.group (1) .strip (). replace (' \ n ',' ') para m en pat.finditer (f.read ())]
remitente = []; mensaje = []; datetime = [] para la fila en los datos: # la marca de tiempo es anterior al primer guión datetime.append (row.split ('-') [0])
        # el remitente está entre am / pm, guión y dos puntos try: s = re.search ('- (. *?):', row) .group (1) sender.append (s) excepto: sender.append ('' ) # el contenido del mensaje es posterior al primer intento de dos puntos: message.append (row.split (':', 1) [1]) excepto: message.append ('') df = pd.DataFrame (zip (datetime, remitente, mensaje), columnas = ['marca de tiempo', 'remitente', 'texto']) # excluye cualquier fila donde el formato no coincida # formato de marca de tiempo apropiado df = df [df ['marca de tiempo']. str.len () == 17] df ['marca de tiempo'] = pd.to_datetime (df.timestamp, format = '% d /% m /% Y,% H:% M')
    # eliminar eventos no asociados con un remitente df = df [df.sender! = ''] .reset_index (drop = True) return df

Vamos a ver cómo funciona. Cree la ruta a sus datos, aplique la función a la exportación del chat y observe el marco de datos resultante.

## ruta al directorio con su archivo ruta = Ruta (base_dir)
## analizar el extracto de WhatsApp, reemplace chat.txt con su ## extracto nombre de archivo df = parse_file (ruta / 'chat.txt')
## mira el resultado df [205: 210]

¡Perfecto! Este es un pequeño fragmento de conversación entre mi encantadora esposa y yo. Una de las ventajas de este formato es que puedo crear fácilmente una lista de nombres de participantes en minúsculas, reemplazando cualquier espacio con guiones bajos. Esto ayudará más tarde.

## lista de participantes de la conversación participantes = lista (df ['remitente']. str.lower (). str.replace ('', '_'). únicos ()) participantes

En este caso, solo hay dos nombres, pero funcionará con cualquier número de participantes.

Finalmente, debemos pensar en cómo queremos que este texto se alimente a nuestra red modelo. Normalmente, tendríamos varios fragmentos de texto independientes (por ejemplo, artículos de Wikipedia o reseñas de IMDB), pero lo que tenemos aquí es un flujo continuo de texto. Una conversación continua. Eso es lo que creamos. Una cadena larga, que incluye los nombres de los remitentes.

## concatena nombres y texto en una cadena texto = [(df ['remitente']. str.replace ('', '_') + '' + df ['texto']). str.cat (sep = ' ')]
## muestra parte del texto de la cadena [0] [8070: 8150]

Se ve bien. Estamos listos para convertir esto en un alumno.

Creación del alumno

Para usar la API fastai ahora necesitamos crear un DataBunch. Este es un objeto que luego puede usarse dentro de un alumno para entrenar un modelo. En este caso, tiene tres entradas clave: los datos (divididos en conjuntos de capacitación y validación), etiquetas para los datos y el tamaño del lote.

Datos

Para dividir el entrenamiento y la validación, escojamos un punto en algún lugar en el medio de nuestra larga cadena de conversación. Fui por el primer 90% para entrenamiento, el último 10% para validación. Luego podemos crear un par de objetos TextList y verificar rápidamente que se vean igual que antes.

## encuentra el índice para el 90% a través de la cadena larga split = int (len (text [0]) * 0.9)
## crear listas de texto para tren / tren válido = lista de texto ([texto [0] [: división]]) válido = lista de texto ([texto [0] [división:]])
## vistazo rápido al tren de texto [0] [8070: 8150]

Vale la pena profundizar un poco más en esta TextList.

Es, más o menos, una lista de texto (con solo un elemento en este caso), pero echemos un vistazo rápido al código fuente para ver qué más hay.

Ok, una TextList es una clase con un montón de métodos (funciones, cualquier cosa en lo anterior comenzando con "def", todo minimizado). Hereda de una ItemList, en otras palabras, es una especie de ItemList. Por supuesto, vaya y busque una ItemList, pero estoy más interesado en la variable "_procesador". El procesador es una lista con un TokenizeProcessor y un NumericalizeProcessor. Estos suenan familiares en un contexto de PNL:

Tokenizar: procese el texto y divídalo en palabras individuales

Numericalizar: reemplazar esas fichas con números que corresponden a la posición de la palabra en un vocabulario

¿Por qué estoy resaltando esto? Bueno, ciertamente ayuda a comprender las reglas que se utilizan para procesar su texto, y profundizar en esta parte del código fuente y la documentación lo ayudará a hacerlo. Pero, específicamente, quiero agregar mi propia nueva regla. Creo que deberíamos mostrar que los nombres de los remitentes en el texto son similares de alguna manera. Idealmente, me gustaría un token antes de cada nombre de remitente que le diga al modelo "este es un nombre de remitente".

¿Cómo podemos hacer esto? Ahí es donde _procesador es útil. La documentación nos dice que podemos usarlo para pasar un tokenizador personalizado.

Por lo tanto, podemos crear nuestra propia regla y pasarla con un procesador personalizado. Todavía quiero mantener los valores predeterminados anteriores, por lo que todo lo que necesito hacer es agregar mi nueva función a la lista existente de reglas predeterminadas y agregar esta nueva lista a nuestro procesador personalizado.

## nueva regla def add_spk (x: Colección [str]) -> Colección [str]: res = [] para t en x: si t en participantes: res.append ('xxspk'); res.append (t) else: res.append (t) return res
## agrega una nueva regla a los valores predeterminados y pasa el procesador del cliente custom_post_rules = defaults.text_post_rules + [add_spk] tokenizer = Tokenizer (post_rules = custom_post_rules)
procesador = [TokenizeProcessor (tokenizer = tokenizer), NumericalizeProcessor (max_vocab = 30000)]

La función agrega el token 'xxspk' antes de cada nombre.

Antes de procesar: "... huevos, leche Paul_Solomon Ok ..."

Después del procesamiento: "... huevos, leche xxspk paul_solomon xxmaj ok ..."

Tenga en cuenta que he aplicado algunas de las otras reglas predeterminadas, a saber, identificar palabras en mayúscula (agrega 'xxmaj' antes de las palabras en mayúscula) y separar la puntuación.

Etiquetas

Vamos a crear algo llamado modelo de lenguaje. ¿Que es esto? Simple, es un modelo que predice la siguiente palabra en una secuencia de palabras. Para hacer esto con precisión, el modelo necesita comprender las reglas del lenguaje y el contexto. De alguna manera, necesita aprender el idioma.

Entonces, ¿cuál es la etiqueta? Fácil, es la siguiente palabra. Más específicamente, en la arquitectura del modelo que estamos usando, para una secuencia de palabras podemos crear una secuencia objetivo tomando esa misma secuencia de tokens y desplazándola una palabra hacia la derecha. En cualquier punto de la secuencia de entrada, podemos mirar ese mismo punto en la secuencia objetivo y encontrar la palabra correcta para predecir (es decir, la etiqueta).

Secuencia de entrada: "... huevos, leche spkxx paul_solomon xxmaj ..."

Etiqueta / siguiente palabra: "ok"

Secuencia objetivo: "..., leche spkxx paul_solomon xxmaj ok ..."

Hacemos esto usando el método label_for_lm (una de las funciones en la clase TextList anterior).

## tomar train and valid y etiquetar para el modelo de idioma src = ItemLists (ruta = ruta, tren = tren, válido = válido) .label_for_lm ()

Tamaño del lote

Las redes neuronales se entrenan pasando lotes de datos en paralelo, por lo que la entrada final para el grupo de datos es nuestro tamaño de lote. Usamos 48, lo que significa que 48 secuencias de texto se pasan a través de la red a la vez. Cada una de estas secuencias de texto tiene 70 tokens de forma predeterminada.

## crear databunch con un tamaño de lote de 48 bs = 48 data = src.databunch (bs = bs)

¡Ahora tenemos nuestros datos! Creemos al alumno.

## crear alumno learn = language_model_learner (datos, AWD_LSTM, drop_mult = 0.3)

Fastai nos da la opción de crear rápidamente un modelo de aprendizaje del idioma. Todo lo que necesitamos son nuestros datos (ya los tenemos) y un modelo existente. Este objeto tiene un argumento 'pretrained' establecido en 'True' por defecto. Esto significa que vamos a tomar un modelo de lenguaje previamente capacitado y ajustarlo a nuestros datos.

Esto se llama aprendizaje de transferencia, y me encanta. Los modelos de idiomas necesitan una gran cantidad de datos para funcionar bien, pero no tenemos ningún lugar lo suficientemente cerca en este caso. Para resolver este problema, podemos tomar un modelo existente, capacitado en cantidades masivas de datos, y ajustarlo a nuestro texto.

En este caso, utilizamos un modelo AWD_LSTM que ha sido previamente entrenado en el conjunto de datos WikiText-103. AWD LSTM es un modelo de lenguaje que utiliza un tipo de arquitectura llamada red neuronal recurrente. Está entrenado en texto, y en este caso ha sido entrenado en una carga completa de datos de Wikipedia. Podemos ver cuánto.

Este modelo ha sido entrenado en más de 100 millones de tokens de 28k artículos de Wikipedia con un rendimiento de vanguardia. ¡Suena como un gran punto de partida para nosotros!

Vamos a tener una idea rápida de la arquitectura del modelo.

aprender.modelo

Desglosaré esto.

  1. Codificador: el vocabulario de nuestro texto tendrá cualquier palabra que se haya usado más de dos veces. En este caso, son 2.864 palabras (la suya será diferente). Cada una de estas palabras se representa usando un vector de longitud 2,864 con un 1 en la posición apropiada y todos ceros en otra parte. La codificación toma este vector y lo multiplica por una matriz de peso para aplastarlo en una incrustación de 400 palabras de longitud.
  2. Celdas LSTM: la incrustación de 400 palabras de longitud se alimenta a una celda LSTM. No entraré en los detalles de la celda, todo lo que necesita saber es que un vector de longitud 400 entra en la primera celda y sale un vector de longitud 1.152. Otras dos cosas que vale la pena señalar: esta celda tiene memoria (está recordando palabras anteriores) y la salida de la celda se retroalimenta y se combina con la siguiente palabra (esa es la parte recurrente), además de ser empujada a la siguiente capa . Hay tres de estas celdas en una fila.
  3. Decodificador: la salida de la tercera celda LSTM es un vector de longitud 400, esto se expande nuevamente en un vector con la misma longitud que su vocabulario (2,864 en mi caso). Esto nos da la predicción para la siguiente palabra, y se puede comparar con la siguiente palabra real para calcular nuestra pérdida y precisión.

Recuerde que este es un modelo pre-entrenado, por lo que siempre que sea posible, los pesos son exactamente como fueron entrenados usando los datos de WikiText. Este será el caso para las celdas LSTM, y para cualquier palabra que esté en ambos vocabulario. Cualquier palabra nueva se inicializa por la media de todas las incrustaciones.

Ahora vamos a ajustarlo con nuestros propios datos para que el texto que genera suene como nuestro chat de WhatsApp y no como un artículo de Wikipedia.

Formación

Primero, vamos a hacer un entrenamiento congelado. Esto significa que solo actualizamos ciertas partes del modelo. Específicamente, solo vamos a entrenar al último grupo de capas. Podemos ver arriba que el último grupo de capas es "(1): LinearDecoder", el decodificador. Todas las incrustaciones de palabras y las celdas LSTM permanecerán iguales durante el entrenamiento, solo se actualizará la etapa final de decodificación.

Uno de los hiperparámetros más importantes es la tasa de aprendizaje. Fastai nos brinda una pequeña herramienta útil para encontrar rápidamente un buen valor.

## ejecutar lr finder learn.lr_find ()
## plot lr finder learn.recorder.plot (skip_end = 15)

La regla de oro es encontrar la parte más empinada de la curva (es decir, el punto de aprendizaje más rápido). 1.3e-2 parece tratarse aquí mismo.

Avancemos y entrenemos por una época (una vez a través de todos los datos de entrenamiento).

## entrenar para una época congelada learn.fit_one_cycle (1, 1e-2, mamás = (0.8,0.7))

Al final de la época, podemos ver la pérdida en los conjuntos de entrenamiento y validación, y la precisión en el conjunto de validación. Estamos prediciendo correctamente el 41% de las siguientes palabras en el conjunto de validación. No está mal.

El entrenamiento congelado es una excelente manera de comenzar con el aprendizaje de transferencia, pero ahora podemos abrir todo el modelo descongelando. Esto significa que el codificador y las celdas LSTM ahora se incluirán en nuestras actualizaciones de capacitación. También significa que el modelo será más sensible, por lo que reducimos nuestra tasa de aprendizaje a 1e-3.

## entrenar durante cuatro ciclos adicionales sin congelar learn.fit_one_cycle (4, 1e-3, mamás = (0.8,0.7))

Precisión hasta 44.4%. Tenga en cuenta que la pérdida de entrenamiento ahora es menor que la validación, eso es lo que queremos ver, y la pérdida de validación ha tocado fondo.

Tenga en cuenta que seguramente encontrará que su pérdida y precisión son diferentes a las anteriores (algunas conversaciones son más predecibles que otras), por lo que le sugiero que juegue con los parámetros (tasas de aprendizaje, protocolo de entrenamiento, etc.) para tratar de obtener El mejor rendimiento de su modelo.

Generación de texto

Ahora tenemos un modelo de idioma, ajustado a su conversación de WhatsApp. Para generar texto, todo lo que necesitamos hacer es configurarlo y comenzará a predecir la siguiente palabra una y otra vez durante el tiempo que usted lo solicite.

Fastai nos brinda un método de predicción útil para hacer exactamente esto, todo lo que tenemos que hacer es darle un poco de texto para que comience, y decirle cuánto tiempo se debe ejecutar. La salida todavía estará en formato tokenizado, así que escribí una función para limpiar el texto e imprimirlo muy bien en el cuaderno.

## función para generar texto def generate_chat (start_text, n_words): text = learn.predict (start_text, n_words, temperature = 0.75) text = text.replace ("xxspk", "\ n"). replace ("\ '" , "\ '"). replace ("n \' t", "n \ 't") text = re.sub (r' \ s ([?.! "] (?: \ s | $)) ' , r '\ 1', texto) para el participante en los participantes: text = text.replace (participante, participante + ":") print (text)

Avancemos y comencemos.

## generar un texto de 200 palabras de longitud generate_chat (participantes [0] + "¿estás bien?", 200)

¡Agradable! Ciertamente se lee como una de mis conversaciones (centrada principalmente en viajar a casa después del trabajo todos los días), el contexto es sostenido (esa es la memoria LSTM en el trabajo) e incluso el texto parece estar adaptado a cada participante.

Pensamientos finales

Terminaré con una palabra de precaución. Al igual que con muchas otras aplicaciones de IA, la generación de texto falso se puede utilizar a escala para fines poco éticos (por ejemplo, difundir mensajes diseñados para dañar en Internet). Lo he usado aquí para proporcionar una forma divertida y práctica de aprender sobre modelos de lenguaje, pero le animo a que piense cómo los métodos descritos anteriormente se pueden usar para otros fines (por ejemplo, como una entrada a un sistema de clasificación de texto) o a otros tipos de datos de secuencia (por ejemplo, composición musical).

Este es un campo emocionante y de rápido movimiento con el potencial de construir herramientas poderosas que crean valor y benefician a la sociedad. Espero que esta publicación te muestre que cualquiera puede participar.