Blog
Proyecto: Desarrollar un Modelo de Lenguaje Neural para la Generación de Textos
- Publicado por: Rafael Fernandez
- Categoría: Uncategorized
Un modelo de lenguaje puede predecir la probabilidad de la siguiente palabra en la secuencia, basándose en las palabras ya observadas en la secuencia. Los modelos de redes neuronales son un método preferido para desarrollar modelos de lenguaje estadístico porque pueden usar una representación distribuida donde diferentes palabras con significados similares tienen una representación similar y porque pueden usar un gran contexto de palabras recientemente observadas al hacer predicciones. En este tutorial, descubrirás cómo desarrollar un modelo de lenguaje estadístico utilizando el Deep Learning en Python. Después de completar este tutorial, tú sabrás:
- Cómo preparar el texto para desarrollar un modelo de lenguaje basado en palabras.
- Cómo diseñar y adaptar un modelo de lenguaje neural con una incrustación aprendida y una capa oculta de LSTM.
- Cómo utilizar el modelo del idioma aprendido para generar un nuevo texto con propiedades estadísticas similares a las del texto fuente.
Vamos a empezar.
Descripción general del tutorial
Este tutorial está dividido en las siguientes partes:
- La República de Platón
- Preparación de datos
- Modelo de entrenamiento de idiomas
- Modelo de uso del lenguaje
La República de Platón
La República de Platón es la obra más famosa del filósofo griego clásico Platón. Está estructurado como un diálogo (por ejemplo, una conversación) sobre el tema del orden y la justicia dentro de una ciudad o estado. El texto completo está disponible gratuitamente en el dominio público. Está disponible en el sitio web del Proyecto Gutenberg en varios formatos. Puedes descargar la versión en texto ASCII de todo el libro (o libros) aquí (puede que necesites abrir la URL dos veces):
- Descargar The Republic by Plato.
http://www.gutenberg.org/cache/epub/1497/pg1497.txt
Descarga el texto del libro y colócalo en tu trabajo actual directamente con el nombre de archivo republic.txt. Abre el archivo en un editor de texto y elimina el anverso y reverso. Esto incluye detalles sobre el libro al principio, un largo análisis e información sobre la licencia al final. El texto debe empezar por:
BOOK I.
I went down yesterday to the Piraeus with Glaucon the son of Ariston, …
Y terminar con:
… And it shall be well with us both in this life and in the pilgrimage of a thousand years which we have been describing.
Guarda la versión limpia como republic_clean.txt en tu directorio de trabajo actual. El archivo debe tener unas 15.802 líneas de texto. Ahora podemos desarrollar un modelo de lenguaje a partir de este texto.
Preparación de datos
Comenzaremos por preparar los datos para el modelado. El primer paso es mirar los datos.
Revisión del texto
Abra el texto en un editor y simplemente mire los datos del texto. Por ejemplo, aquí está el primer pedazo de diálogo:
BOOK I.
I went down yesterday to the Piraeus with Glaucon the son of Ariston, that I might offer up my prayers to the goddess (Bendis, the Thracian Artemis.); and also because I wanted to see in what manner they would celebrate the festival, which was a new thing. I was delighted with the procession of the inhabitants; but that of the
Thracians was equally, if not more, beautiful. When we had finished our prayers and viewed the spectacle, we turned in the direction of the city; and at that instant Polemarchus the son of Cephalus chanced to catch sight of us from a distance as we were starting on our way home, and told his servant to run and bid us wait for him.
The servant took hold of me by the cloak behind, and said: Polemarchus desires you to wait.I turned round, and asked him where his master was.
There he is, said the youth, coming after you, if you will only wait.
Certainly we will, said Glaucon; and in a few minutes Polemarchus appeared, and with him Adeimantus, Glaucon’s brother, Niceratus the son of Nicias, and several others who had been at the procession.
Polemarchus said to me: I perceive, Socrates, that you and your companion are already on your way to the city.
You are not far wrong, I said.…
¿Qué crees que tendremos que manejar en la preparación de los datos? Esto es lo que veo de un vistazo rápido:
- Encabezamientos de libros/capítulos (por ejemplo, BOOK I.).
- Mucha puntuación (por ejemplo -, ;-, ?-, y más).
- Nombres extraños (por ejemplo, Polemarchus).
- Algunos monólogos largos que se extienden por cientos de líneas.
- Algunos diálogos citados (e.g. ‘…’).
- Estas observaciones, y más, sugieren formas en que podemos desear preparar los datos del texto.
- La forma específica en que preparamos los datos realmente depende de cómo pretendemos modelarlos, lo que a su vez depende de cómo pretendemos utilizarlos.
Diseño de Modelos de Lenguaje
En este tutorial, desarrollaremos un modelo del texto que podremos utilizar para generar nuevas secuencias de texto. El modelo de lenguaje será estadístico y predecirá la probabilidad de cada palabra dada una secuencia de entrada de texto. La palabra predicha será introducida como entrada para generar a su vez la siguiente palabra. Una decisión clave de diseño es la duración de las secuencias de entrada. Necesitan ser lo suficientemente largos para permitir que el modelo aprenda el contexto de las palabras a predecir. Esta longitud de entrada también definirá la longitud del texto de la semilla usada para generar nuevas secuencias cuando usamos el modelo.
No hay una respuesta correcta. Con tiempo y recursos suficientes, podríamos explorar la capacidad del modelo para aprender con secuencias de entrada de diferentes tamaños. En su lugar, escogeremos una longitud de 50 palabras para la longitud de las secuencias de entrada, de forma algo arbitraria. Podríamos procesar los datos para que el modelo sólo se ocupe de las frases autónomas y rellenar o truncar el texto para cumplir este requisito para cada secuencia de entrada. Tú podrías explorar esto como una extensión de este tutorial.
En cambio, para que el ejemplo sea breve, dejaremos que todo el texto fluya junto y capacitaremos al modelo para predecir la siguiente palabra a través de oraciones, párrafos e incluso libros o capítulos en el texto. Ahora que tenemos un diseño de modelo, podemos ver la transformación del texto en bruto en secuencias de 100 palabras de entrada a 1 palabra de salida, listo para adaptarse a un modelo.
Cargar texto
El primer paso es cargar el texto en la memoria. Podemos desarrollar una pequeña función para cargar todo el archivo de texto en la memoria y devolverlo. La función se llama load_doc() y se lista a continuación. Si se le da un nombre de archivo, devuelve una secuencia de texto cargado.
# cargar doc en la memoria def load_doc(filename): # abrir archivo en modo solo lectura file = open(filename, 'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text
Usando esta función, podemos cargar la versión más limpia del documento en el archivo republic_clean.txt de la siguiente manera:
# cargar documento in_filename = 'republic_clean.txt' doc = load_doc(in_filename) print(doc[:200])
Al ejecutar este fragmento se carga el documento y se imprimen los primeros 200 caracteres como una comprobación de cordura.
BOOK I. I went down yesterday to the Piraeus with Glaucon the son of Ariston, that I might offer up my prayers to the goddess (Bendis, the Thracian Artemis.); and also because I wanted to see in what
Hasta ahora, todo bien. A continuación, limpiemos el texto.
Limpiando el texto
Necesitamos transformar el texto en bruto en una secuencia de tokens o palabras que podamos usar como fuente para entrenar al modelo. Basado en la revisión del texto en bruto (arriba), a continuación se presentan algunas operaciones específicas que realizaremos para limpiar el texto. Es posible que desee explorar más operaciones de limpieza usted mismo como una extensión.
- Sustituir ‘-‘ por un espacio en blanco para que podamos dividir mejor las palabras.
- Dividir las palabras según el espacio en blanco.
- Eliminar todos los signos de puntuación de las palabras para reducir el tamaño del vocabulario (por ejemplo,’¿Qué? Qué’).
- Eliminar todas las palabras que no estén en orden alfabético para eliminar los signos de puntuación independientes.
- Normalizar todas las palabras en minúsculas para reducir el tamaño del vocabulario.
- El tamaño del vocabulario es un gran problema con el modelado del lenguaje.
Un vocabulario más pequeño resulta en un modelo más pequeño que entrena más rápido. Podemos implementar cada una de estas operaciones de limpieza en este orden en una función. Abajo está la función clean_doc() que toma un documento cargado como argumento y devuelve una matriz de tokens limpios.
# Convierte a doc en tokens limpios def clean_doc(doc): # Reemplace '--' con un espacio doc = doc.replace('--', ' ') # dividido en tokens por el espacio en blanco tokens = doc.split() # prepara a regex para el filtrado de caracteres re_punc = re.compile('[%s]' % re.escape(string.punctuation)) # Elimina la puntuación de cada palabra tokens = [re_punc.sub('', w) for w in tokens] # eliminar los tokens restantes que no estén en orden alfabético tokens = [word for word in tokens if word.isalpha()] # en minúsculas tokens = [word.lower() for word in tokens] return tokens
Podemos ejecutar esta operación de limpieza en nuestro documento cargado e imprimir algunos de los tokens y estadísticas como un control de sanidad.
# documento limpio tokens = clean_doc(doc) print(tokens[:200]) print('Total Tokens: %d' % len(tokens)) print('Unique Tokens: %d' % len(set(tokens)))
Primero, podemos ver una bonita lista de tokens que parecen más limpios que el texto crudo. Podríamos quitar los marcadores de capítulos del BOOK I y más, pero este es un buen comienzo.
['book', 'i', 'i', 'went', 'down', 'yesterday', 'to', 'the', 'piraeus', 'with', 'glaucon', 'the', 'son', 'of', 'ariston', 'that', 'i', 'might', 'offer', 'up', 'my', 'prayers', 'to', 'the', 'goddess', 'bendis', 'the', 'thracian', 'artemis', 'and', 'also', 'because', 'i', 'wanted', 'to', 'see', 'in', 'what', 'manner', 'they', 'would', 'celebrate', 'the', 'festival', 'which', 'was', 'a', 'new', 'thing', 'i', 'was', 'delighted', 'with', 'the', 'procession', 'of', 'the', 'inhabitants', 'but', 'that', 'of', 'the', 'thracians', 'was', 'equally', 'if', 'not', 'more', 'beautiful', 'when', 'we', 'had', 'finished', 'our', 'prayers', 'and', 'viewed', 'the', 'spectacle', 'we', 'turned', 'in', 'the', 'direction', 'of', 'the', 'city', 'and', 'at', 'that', 'instant', 'polemarchus', 'the', 'son', 'of', 'cephalus', 'chanced', 'to', 'catch', 'sight', 'of', 'us', 'from', 'a', 'distance', 'as', 'we', 'were', 'starting', 'on', 'our', 'way', 'home', 'and', 'told', 'his', 'servant', 'to', 'run', 'and', 'bid', 'us', 'wait', 'for', 'him', 'the', 'servant', 'took', 'hold', 'of', 'me', 'by', 'the', 'cloak', 'behind', 'and', 'said', 'polemarchus', 'desires', 'you', 'to', 'wait', 'i', 'turned', 'round', 'and', 'asked', 'him', 'where', 'his', 'master', 'was', 'there', 'he', 'is', 'said', 'the', 'youth', 'coming', 'after', 'you', 'if', 'you', 'will', 'only', 'wait', 'certainly', 'we', 'will', 'said', 'glaucon', 'and', 'in', 'a', 'few', 'minutes', 'polemarchus', 'appeared', 'and', 'with', 'him', 'adeimantus', 'glaucons', 'brother', 'niceratus', 'the', 'son', 'of', 'nicias', 'and', 'several', 'others', 'who', 'had', 'been', 'at', 'the', 'procession', 'polemarchus', 'said']
También tenemos algunas estadísticas sobre el documento limpio. Podemos ver que hay poco menos de 120.000 palabras en el texto limpio y un vocabulario de poco menos de 7.500 palabras. Esto es más bien pequeño y los modelos que encajan en estos datos deberían ser manejables en hardware modesto.
Total Tokens: 118684 Unique Tokens: 7409
Guardar texto limpio
Podemos organizar la larga lista de tokens en secuencias de 50 palabras de entrada y 1 palabra de salida. Es decir, secuencias de 51 palabras. Podemos hacer esto iterando sobre la lista de tokens desde el token 51 en adelante y tomando los 50 tokens anteriores como una secuencia, luego repitiendo este proceso hasta el final de la lista de tokens. Transformaremos los tokens en cadenas separadas por espacios para su posterior almacenamiento en un archivo. El código para dividir la lista de tokens limpios en secuencias con una longitud de 51 tokens se muestra a continuación.
# organizar en secuencias de tokens length = 50 + 1 sequences = list() for i in range(length, len(tokens)): # seleccionar secuencia de tokens seq = tokens[i-length:i] # convertir en una línea line =''.join(seq) # acumular sequences.append(line) print('Total Sequences: %d'% len(sequences))
La ejecución de esta pieza crea una larga lista de líneas. Imprimiendo las estadísticas en la lista, podemos ver que tendremos exactamente 118.633 patrones de entrenamiento que se ajustan a nuestro modelo.
Total Sequences: 118633 [php] # guardar tokens en un archivo, un diálogo por línea def save_doc(lines, filename): data ='\n'.join(lines) file = open(filename,'w') file.write(data) file.close()
Podemos llamar a esta función y guardar nuestras secuencias de entrenamiento en archivo republic_sequences.txt.
# guardar secuencias en un archivo out_filename ='republic_sequences.txt' save_doc(sequences, out_filename)
Echa un vistazo al archivo con tu editor de texto. Verás que cada línea se desplaza a lo largo de una palabra, con una nueva palabra al final para ser predicha; por ejemplo, aquí están las primeras tres líneas en forma truncada:
book i i ... catch sight of i i went ... sight of us i went down ... of us from ...
Ejemplo completo
Enlazando todo esto, el listado completo de códigos se proporciona a continuación
import string import re # cargar doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # convertir a doc en tokens limpios def clean_doc(doc): # Sustituir - -> por un espacio ' '. doc = doc.replace('--','') # dividido en tokens por espacio en blanco tokens = doc.split() # preparar a regex para el filtrado de caracteres re_punc = re.compile('[%s]'% re.escape(string.punctuation)) # eliminar la puntuación de cada palabra tokens = [re_punc.sub('', w) for w in tokens] # eliminar los tokens restantes que no estén en orden alfabético tokens = [word for word in tokens if word.isalpha()] # minúsculas tokens = [word.lower() for word in tokens] return tokens # guardar tokens en un archivo, un diálogo por línea def save_doc(lines, filename): data ='\n'.join(lines) file = open(filename,'w') file.write(data) file.close() # documento de carga in_filename ='republic_clean.txt' doc = load_doc(in_filename) print(doc[:200]) # documento limpio tokens = clean_doc(doc) print(tokens[:200]) print('Total Tokens: %d'% len(tokens)) print('Unique Tokens: %d'% len(set(tokens))) # organizar en secuencias de tokens length = 50 + 1 sequences = list() for i in range(length, len(tokens)): # seleccionar secuencia de tokens seq = tokens[i-length:i] # convertir en una línea line =''.join(seq) # acumular sequences.append(line) print('Total Sequences: %d'% len(sequences)) # guardar secuencias en un archivo out_filename ='republic_sequences.txt' save_doc(sequences, out_filename)
Ahora debería tener los datos de formación almacenados en el fichero republic_sequences.txt en su directorio de trabajo actual. A continuación, veamos cómo ajustar un modelo de lenguaje a estos datos.
Modelo de entrenamiento
Ahora podemos formar un modelo de lenguaje estadístico a partir de los datos preparados. El modelo que entrenaremos es un modelo de lenguaje neural. Tiene algunas características únicas:
- Utiliza una representación distribuida para las palabras, de modo que diferentes palabras con significados similares tendrán una representación similar.
- Aprende la representación al mismo tiempo que aprende el modelo.
- Aprende a predecir la probabilidad de la siguiente palabra usando el contexto de las últimas 100 palabras.
Específicamente, usaremos una capa de incrustación para aprender la representación de palabras, y una red neuronal recurrente de memoria a corto plazo (LSTM) para aprender a predecir palabras basadas en su contexto. Empecemos por cargar nuestros datos de entrenamiento.
Secuencias de carga
Podemos cargar nuestros datos de entrenamiento usando la función load_doc() que desarrollamos en la sección anterior. Una vez cargados, podemos dividir los datos en secuencias de entrenamiento separadas mediante la división basada en nuevas líneas. El fragmento de abajo cargará el archivo de datos republic_sequences.txt del directorio de trabajo actual.
# cargar doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar in_filename ='republic_sequences.txt' doc = load_doc(in_filename) lines = doc.split('\n')
A continuación, podemos codificar los datos de entrenamiento.
Codificación de secuencias
La capa de incrustación de palabras espera que las secuencias de entrada estén compuestas de enteros. Podemos mapear cada palabra de nuestro vocabulario a un entero único y codificar nuestras secuencias de entrada. Más tarde, cuando hacemos predicciones, podemos convertir la predicción en números y buscar sus palabras asociadas en el mismo mapeo. Para hacer esta codificación, usaremos la clase Tokenizer en la API de Keras.
Primero, el Tokenizer debe ser entrenado en todo el conjunto de datos de entrenamiento, lo que significa que encuentra todas las palabras únicas en los datos y asigna a cada uno un entero único. A continuación, podemos utilizar el ajuste Tokenizer para codificar todas las secuencias de entrenamiento, convirtiendo cada secuencia de una lista de palabras a una lista de números enteros.
# enteros codifican secuencias de palabras tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) sequences = tokenizer.texts_to_sequences(lines)
Podemos acceder al mapeo de palabras a números enteros como un atributo del diccionario llamado índice de palabras en el objeto Tokenizer. Necesitamos saber el tamaño del vocabulario para definir la capa de incrustación más adelante. Podemos determinar el vocabulario calculando el tamaño del diccionario de mapeo.
A las palabras se les asignan valores de 1 al número total de palabras (por ejemplo, 7.409). La capa de incrustación necesita asignar una representación vectorial para cada palabra de este vocabulario desde el índice 1 hasta el índice más grande y, dado que la indexación de matrices es cero, el índice de la palabra al final del vocabulario será de 7.409; esto significa que la matriz debe tener 7.409 + 1 de longitud. Por lo tanto, al especificar el tamaño del vocabulario a la capa de incrustación, lo especificamos como 1 más grande que el vocabulario real.
# tamaño del vocabulario vocab_size = len(tokenizer.word_index) + 1
Entradas y salidas de secuencia
Ahora que hemos codificado las secuencias de entrada, necesitamos separarlas en elementos de entrada (X) y salida (y). Podemos hacer esto con el corte de la matriz. Después de separarnos, necesitamos una codificación en caliente de la palabra de salida. Esto significa convertirlo de un número entero a un vector de valores 0, uno por cada palabra del vocabulario, con un 1 para indicar la palabra específica en el índice de las palabras valor entero.
Esto es para que el modelo aprenda a predecir la distribución de probabilidad para la siguiente palabra y la verdad básica de la cual aprender es 0 para todas las palabras excepto la palabra real que viene después. Keras proporciona el to_categorical() que puede ser usado para codificar en caliente las palabras de salida para cada par de secuencias de entrada y salida.
Finalmente, necesitamos especificar a la capa de incrustación qué tan largas son las secuencias de entrada. Sabemos que hay 50 palabras porque diseñamos el modelo, pero una buena manera genérica de especificar es usar la segunda dimensión (número de columnas) de la forma de los datos de entrada. De este modo, si se modifica la longitud de las secuencias al preparar los datos, no es necesario modificar este código de carga de datos; es genérico.
# se separan en entrada y salida sequences = array(sequences) X, y = sequences[:,:-1], sequences[:,-1] y = to_categorical(y, num_classes=vocab_size) seq_length = X.shape[1]
Modelo de ajuste
Ahora podemos definir y adaptar nuestro modelo lingüístico a los datos de la formación. La incorporación aprendida necesita saber el tamaño del vocabulario y la longitud de las secuencias de entrada, tal y como se ha discutido anteriormente. También tiene un parámetro para especificar cuántas dimensiones se utilizarán para representar cada palabra. Es decir, el tamaño del espacio vectorial incrustado.
Los valores comunes son 50, 100 y 300. Usaremos 50 aquí, pero consideremos probar valores más pequeños o más grandes. Usaremos dos capas ocultas de LSTM con 100 celdas de memoria cada una. Más celdas de memoria y una red más profunda pueden lograr mejores resultados.
Una densa capa totalmente conectada con 100 neuronas se conecta a las capas ocultas del LSTM para interpretar las características extraídas de la secuencia. La capa de salida predice la siguiente palabra como un solo vector del tamaño del vocabulario con una probabilidad para cada palabra del vocabulario. Se utiliza una función de activación softmax para asegurar que las salidas tengan las características de probabilidades normalizadas.
# definir el modelo def define_model(vocab_size, seq_length): model = Sequential() model.add(Embedding(vocab_size, 50, input_length=seq_length)) model.add(LSTM(100, return_sequences=True)) model.add(LSTM(100)) model.add(Dense(100, activation='relu')) model.add(Dense(vocab_size, activation='softmax')) # compilar red model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # resumir el modelo definido model.summary() plot_model(model, to_file='model.png', show_shapes=True) return model
Se imprime un resumen de la red definida como un cheque de cordura para asegurarnos de que hemos construido lo que queríamos.
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 50, 50) 370500 _________________________________________________________________ lstm_1 (LSTM) (None, 50, 100) 60400 _________________________________________________________________ lstm_2 (LSTM) (None, 100) 80400 _________________________________________________________________ dense_1 (Dense) (None, 100) 10100 _________________________________________________________________ dense_2 (Dense) (None, 7410) 748410 ================================================================= Total params: 1,269,810 Trainable params: 1,269,810 Non-trainable params: 0 _________________________________________________________________
Un gráfico del modelo definido se guarda en un archivo con el nombre model.png.

El modelo se compila especificando la pérdida categórica de entropía cruzada necesaria para ajustarse al modelo. Técnicamente, el modelo está aprendiendo una clasificación multiclase y esta es la función de pérdida adecuada para este tipo de problemas. Se utiliza la eficiente implementación de Adam para el descenso de gradiente en mini-batch y se evalúa la precisión del modelo. Finalmente, el modelo se ajusta a los datos de 100 épocas de entrenamiento con un tamaño de lote modesto de 128 para acelerar las cosas. La formación puede durar unas horas en hardware moderno sin GPUs. Puede acelerarla con un tamaño de lote mayor y/o menos épocas de formación.
Durante la formación, verás un resumen del rendimiento, incluida la pérdida y la precisión evaluadas a partir de los datos de formación al final de cada actualización por lotes. Obtendrás resultados diferentes, pero tal vez una precisión de un poco más del 50% de la predicción de la siguiente palabra en la secuencia, lo cual no es malo. No buscamos una precisión del 100% (por ejemplo, un modelo que memorizara el texto), sino un modelo que capte la esencia del texto.
... Epoch 96/100 118633/118633 [==============================] - 265s - loss: 2.0324 - acc: 0.5187 Epoch 97/100 118633/118633 [==============================] - 265s - loss: 2.0136 - acc: 0.5247 Epoch 98/100 118633/118633 [==============================] - 267s - loss: 1.9956 - acc: 0.5262 Epoch 99/100 118633/118633 [==============================] - 266s - loss: 1.9812 - acc: 0.5291 Epoch 100/100 118633/118633 [==============================] - 270s - loss: 1.9709 - acc: 0.5315
Guardar modelo
Al final de la ejecución, el modelo entrenado se guarda en un archivo. Aquí, usamos la API del modelo de Keras para guardar el modelo en el archivo model.h5 en el directorio de trabajo actual. Más tarde, cuando carguemos el modelo para hacer predicciones, también necesitaremos el mapeo de palabras a números enteros. Esto está en el objeto Tokenizer, y también podemos guardarlo usando Pickle.
# guardar el modelo en un archivo model.save('model.h5') # guardar el tokenizador dump(tokenizer, open('tokenizer.pkl','wb'))
Ejemplo Completo
Podemos juntar todo esto; el ejemplo completo para el ajuste del modelo de lenguaje se muestra a continuación.
from numpy import array from pickle import dump from keras.preprocessing.text import Tokenizer from keras.utils.vis_utils import plot_model from keras.utils import to_categorical from keras.models import Sequential from keras.layers import Dense from keras.layers import LSTM from keras.layers import Embedding # cargar doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # definir el modelo def define_model(vocab_size, seq_length): model = Sequential() model.add(Embedding(vocab_size, 50, input_length=seq_length)) model.add(LSTM(100, return_sequences=True)) model.add(LSTM(100)) model.add(Dense(100, activation='relu')) model.add(Dense(vocab_size, activation='softmax')) # compilar red model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # resumir el modelo definido model.summary() plot_model(model, to_file='model.png', show_shapes=True) return model # cargar in_filename ='republic_sequences.txt'doc = load_doc(in_filename) lines = doc.split('\n') # enteros codifican secuencias de palabras tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) sequences = tokenizer.texts_to_sequences(lines) # tamaño del vocabulario vocab_size = len(tokenizer.word_index) + 1 # se separan en entrada y salida sequences = array(sequences) X, y = sequences[:,:-1], sequences[:,-1] y = to_categorical(y, num_classes=vocab_size) seq_length = X.shape[1] # definir modelo model = define_model(vocab_size, seq_length) # modelo adecuado model.fit(X, y, batch_size=128, epochs=100) # guardar el modelo en un archivo model.save('model.h5') # guardar el tokenizador dump(tokenizer, open('tokenizer.pkl','wb'))
Usar el modelo de lenguaje
Ahora que tenemos un modelo de lenguaje entrenado, podemos usarlo. En este caso, podemos utilizarlo para generar nuevas secuencias de texto que tengan las mismas propiedades estadísticas que el texto fuente. Esto no es práctico, al menos no para este ejemplo, pero da un ejemplo concreto de lo que el modelo lingüístico ha aprendido. Comenzaremos por cargar de nuevo las secuencias de entrenamiento.
Cargar datos
Podemos utilizar el mismo código de la sección anterior para cargar las secuencias de texto de los datos de la formación. Específicamente, la función load_doc().
# cargar doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar secuencias de texto limpias in_filename ='republic_sequences.txt'doc = load_doc(in_filename) lines = doc.split('\n')
Necesitamos el texto para poder elegir una secuencia de fuente como entrada al modelo para generar una nueva secuencia de texto. El modelo requerirá 50 palabras como entrada. Más tarde, tendremos que especificar la longitud esperada de la entrada. Podemos determinar esto a partir de las secuencias de entrada calculando la longitud de una línea de los datos cargados y restando 1 para la palabra de salida esperada que también está en la misma línea.
seq_length = len(lines[0].split()) - 1
Modelo de carga
Ahora podemos cargar el modelo desde el archivo. Keras proporciona la función load_model() para cargar el modelo, listo para su uso.
# cargar el modelo model = load_model('model.h5')
También podemos cargar el tokenizador desde un archivo utilizando la API de Pickle.
# load the tokenizer tokenizer = load(open('tokenizer.pkl','rb'))
Estamos listos para usar el modelo cargado.
Generar texto
El primer paso para generar texto es preparar una entrada de semillas. Para ello, seleccionaremos una línea de texto aleatoria del texto de entrada. Una vez seleccionado, lo imprimiremos para que tengamos una idea de lo que se utilizó.
# seleccionar un texto de la semilla seed_text = lines[randint(0,len(lines))] print(seed_text +'\n')
A continuación, podemos generar nuevas palabras, una a la vez. Primero, el texto de la semilla debe ser codificado a números enteros usando el mismo tokenizador que usamos cuando entrenamos el modelo.
encoded = tokenizer.texts_to_sequences([seed_text])[0]
El modelo puede predecir la siguiente palabra directamente llamando a model.predict_classes() que devolverá el índice de la palabra con la mayor probabilidad.
# predecir probabilidades para cada palabra yhat = model.predict_classes(encoded, verbose=0)
Podemos entonces buscar el índice en el mapeo del Tokenizer para obtener la palabra asociada.
out_word ='' for word, index in tokenizer.word_index.items(): if index == yhat: out_word = word break
Entonces podemos añadir esta palabra al texto de la semilla y repetir el proceso. Es importante destacar que la secuencia de entrada va a ser demasiado larga. Podemos truncarlo a la longitud deseada después de que la secuencia de entrada haya sido codificada a números enteros. Keras proporciona la función pad_sequences() que podemos usar para realizar este truncamiento.
encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
Podemos envolver todo esto en una función llamada generate_seq() que toma como entrada el modelo, el tokenizer, la longitud de la secuencia de entrada, el texto de la semilla y el número de palabras a generar. Luego devuelve una secuencia de palabras generadas por el modelo
# generar una secuencia a partir de un modelo de lenguaje def generate_seq(model, tokenizer, seq_length, seed_text, n_words): result = list() in_text = seed_text # generar un número fijo de palabras for _ in range(n_words): # codificar el texto como un número entero encoded = tokenizer.texts_to_sequences([in_text])[0] # truncar secuencias a una longitud fija encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre') # predecir probabilidades para cada palabra yhat = model.predict_classes(encoded, verbose=0) # mapa de predicción de palabra a palabra out_word ='' for word, index in tokenizer.word_index.items(): if index == yhat: out_word = word break # añadir a la entrada in_text +=''+ out_word result.append(out_word) return''.join(result)
Ahora estamos listos para generar una secuencia de nuevas palabras con algún texto semilla.
# generar nuevo texto generated = generate_seq(model, tokenizer, seq_length, seed_text, 50) print(generated)
Juntando todo esto, el listado completo de códigos para generar texto a partir del modelo del idioma aprendido se enumera a continuación.
from random import randint from pickle import load from keras.models import load_model from keras.preprocessing.sequence import pad_sequences # cargar doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # generar una secuencia a partir de un modelo de lenguaje def generate_seq(model, tokenizer, seq_length, seed_text, n_words): result = list() in_text = seed_text # generar un número fijo de palabras for _ in range(n_words): # codificar el texto como un número entero encoded = tokenizer.texts_to_sequences([in_text])[0] # truncar secuencias a una longitud fija encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre') # predecir probabilidades para cada palabra yhat = model.predict_classes(encoded, verbose=0) # mapa de predicción de palabra a palabra out_word ='' for word, index in tokenizer.word_index.items(): if index == yhat: out_word = word break # añadir a la entrada in_text +=''+ out_word result.append(out_word) return''.join(result) # cargar secuencias de texto limpias in_filename ='republic_sequences.txt'doc = load_doc(in_filename) lines = doc.split('\n') seq_length = len(lines[0].split()) - 1 # cargar el modelo model = load_model('model.h5') # cargar el tokenizador tokenizer = load(open('tokenizer.pkl','rb')) # seleccionar un texto de la semilla seed_text = lines[randint(0,len(lines))] print(seed_text +'\n') # generar nuevo texto generated = generate_seq(model, tokenizer, seq_length, seed_text, 50) print(generated)
Al ejecutar el ejemplo, primero se imprime el texto de la semilla.
when he said that a man when he grows old may learn many things for he can no more learn much than he can run much youth is the time for any extraordinary toil of course and therefore calculation and geometry and all the other elements of instruction which are a
Dada la naturaleza estocástica de las redes neuronales, sus resultados específicos pueden variar. Considere la posibilidad de ejecutar el ejemplo unas cuantas veces.
[/php]preparation for dialectic should be presented to the name of idle spendthrifts of whom the other is the manifold and the unjust and is the best and the other which delighted to be the opening of the soul of the soul and the embroiderer will have to be said at
Puedes ver que el texto parece razonable. De hecho, la adición de la concatenación ayudaría a interpretar la semilla y el texto generado. Sin embargo, el texto generado obtiene el tipo correcto de palabras en el orden correcto. Intente ejecutar el ejemplo unas cuantas veces para ver otros ejemplos de texto generado.
Extensiones
En esta sección se enumeran algunas ideas para ampliar el tutorial que tal vez desee explorar.
- Texto de semillas artificiales: Haz a mano o selecciona el texto de la semilla y evalúa cómo el texto de la semilla impacta el texto generado, específicamente las palabras o frases iniciales generadas.
- Simplifiqua el vocabulario: Explora un vocabulario más simple, tal vez con palabras de tallo o palabras de parada eliminadas.
- Limpieza de datos: Considera la posibilidad de usar más o menos limpieza del texto, tal vez dejar en alguna puntuación o tal vez reemplazar todos los nombres de fantasía con uno o un puñado. Evaluar cómo estos cambios en el tamaño del vocabulario afectan al texto generado.
- Modelo Tune: Ajusta el modelo, como el tamaño de la incrustación o el número de celdas de memoria en la capa oculta, para ver si puede desarrollar un modelo mejor.
- Modelo más profundo: Extiende el modelo para que tenga múltiples capas ocultas de LSTM, tal vez con la función "droppout" para ver si puede desarrollar un modelo mejor.
- Desarrollo de Embedded Pre-Formado: Extiende el modelo para usar vectores de Word2Vec ya entrenados para ver si resulta en un mejor modelo.
- Utiliza GloVe Embedding: Utiliza los vectores de inserción de palabras de GloVe con y sin ajuste fino por parte de la red y evalúe cómo afecta al entrenamiento y a las palabras generadas.
- Longitud de secuencia: Explora el entrenamiento del modelo con diferentes secuencias de entrada de longitud, tanto más cortas como más largas, y evalúa cómo afecta a la calidad del texto generado.
- Reduce el alcance: Considera entrenar el modelo en un libro (capítulo) o en un subconjunto del texto original y evalúa el impacto sobre la formación, la velocidad de la formación y el texto resultante generado.
- Modelo de Frases Sabias: Divide los datos en bruto según las frases y coloca cada frase en una longitud fija (por ejemplo, la longitud de frase más larga).
Si exploras alguna de estas extensiones, me encantaría saberlo.
➡ Continúa aprendiendo en nuestro curso de Procesamiento de Lenguaje Natural: