Generación de texto con Python y TensorFlow/Keras

G

Introducción

¿Está interesado en utilizar una red neuronal para generar texto? TensorFlow y Keras se pueden usar para algunas aplicaciones asombrosas de técnicas de procesamiento de lenguaje natural, incluida la generación de texto.

En este tutorial, cubriremos la teoría detrás de la generación de texto usando redes neuronales recurrentes, específicamente una red de memoria a corto y largo plazo, implementaremos esta red en Python y la usaremos para generar texto.

Definición de términos

Para empezar, comencemos por definir nuestros términos. Puede resultar difícil entender por qué se ejecutan ciertas líneas de código a menos que tenga una comprensión decente de los conceptos que se están reuniendo.

TensorFlow

TensorFlow es una de las bibliotecas de Machine Learning más utilizadas en Python, y se especializa en la creación de redes neuronales profundas. Las redes neuronales profundas se destacan en tareas como el reconocimiento de imágenes y el reconocimiento de patrones en el habla. TensorFlow fue diseñado por Google Brain y su poder radica en su capacidad para unir muchos nodes de procesamiento diferentes.

Dificultad

Mientras tanto, Keras es una interfaz de programación de aplicaciones o API. Keras hace uso de las funciones y habilidades de TensorFlow, pero agiliza la implementación de las funciones de TensorFlow, lo que hace que la construcción de una red neuronal sea mucho más simple y fácil. Los principios fundamentales de Keras son la modularidad y la facilidad de uso, lo que significa que, si bien Keras es bastante poderoso, es fácil de usar y escalar.

Procesamiento natural del lenguaje

El procesamiento del lenguaje natural (PNL) es exactamente lo que parece, las técnicas utilizadas para permitir que las computadoras comprendan el lenguaje humano natural, en lugar de tener que interactuar con las personas a través de lenguajes de programación. El procesamiento del lenguaje natural es necesario para tareas como la clasificación de documentos de Word o la creación de un chatbot.

Corpus

Un corpus es una gran colección de texto y, en el sentido del Machine Learning, un corpus puede considerarse como los datos de entrada de su modelo. El corpus contiene el texto que desea que aprenda el modelo.

Es común dividir un corpus grande en conjuntos de entrenamiento y prueba, utilizando la mayor parte del corpus para entrenar el modelo y una parte invisible del corpus para probar el modelo, aunque el conjunto de prueba puede ser un conjunto de datos completamente diferente. Por lo general, el corpus requiere un procesamiento previo para que sea apto para su uso en un sistema de Machine Learning.

Codificación

La codificación a veces se conoce como representación de palabras y se refiere al proceso de convertir datos de texto en una forma que un modelo de Machine Learning pueda comprender. Las redes neuronales no pueden trabajar con datos de texto sin procesar, los caracteres / palabras deben transformarse en una serie de números que la red pueda interpretar.

El proceso real de convertir palabras en vectores numéricos se conoce como “tokenización”, porque obtienes tokens que representan las palabras reales. Hay varias formas de codificar palabras como valores numéricos. Los métodos principales de codificación son la codificación one-hot y la creación de vectores densamente integrados .

Analizaremos la diferencia entre estos métodos en la sección de teoría a continuación.

Red neuronal recurrente

Una red neuronal básica une una serie de neuronas o nodes, cada uno de los cuales toma algunos datos de entrada y los transforma con alguna función matemática elegida. En una red neuronal básica, los datos deben tener un tamaño fijo, y en cualquier capa dada en la red neuronal, los datos que se pasan son simplemente las salidas de la capa anterior en la red, que luego se transforman por los pesos para esa capa.

Por el contrario, una red neuronal recurrente se diferencia de una red neuronal “básica” gracias a su capacidad para recordar entradas anteriores de capas anteriores en la red neuronal.

Para decirlo de otra manera, las salidas de las capas en una red neuronal recurrente no están influenciadas solo por los pesos y la salida de la capa anterior como en una red neuronal regular, sino que también están influenciadas por el “contexto” hasta ahora, que se deriva de entradas y salidas anteriores.

Las redes neuronales recurrentes son útiles para el procesamiento de texto debido a su capacidad para recordar las diferentes partes de una serie de entradas, lo que significa que pueden tener en cuenta las partes anteriores de una oración para interpretar el contexto.

Memoria a corto plazo

Las redes de memoria a largo y corto plazo (LSTM) son un tipo específico de redes neuronales recurrentes. Los LSTM tienen ventajas sobre otras redes neuronales recurrentes. Si bien las redes neuronales recurrentes generalmente pueden recordar palabras anteriores en una oración, su capacidad para preservar el contexto de entradas anteriores se degrada con el tiempo.

Cuanto más larga es la serie de entrada, más “olvida” la red. Los datos irrelevantes se acumulan a lo largo del tiempo y bloquean los datos relevantes necesarios para que la red haga predicciones precisas sobre el patrón del texto. Esto se conoce como el problema del gradiente de desaparición .

No es necesario que comprenda los algoritmos que se ocupan del problema del gradiente de desaparición (aunque puede leer más al respecto aquí ), pero sepa que un LSTM puede lidiar con este problema “olvidando” selectivamente la información que se considera no esencial para la tarea en cuestión. . Al suprimir la información no esencial, el LSTM puede enfocarse solo en la información que realmente importa, resolviendo el problema del gradiente que se desvanece. Esto hace que los LSTM sean más robustos al manejar largas cadenas de texto.

Teoría / Enfoque de Generación de Texto

Codificación revisada

Codificación One-Hot

Como se mencionó anteriormente, hay dos formas principales de codificar datos de texto. Un método se denomina codificación one-hot, mientras que el otro método se denomina incrustación de palabras.

El proceso de codificación one-hot se refiere a un método de representar texto como una serie de unos y ceros. Se crea un vector que contiene todas las palabras posibles que le interesan, a menudo todas las palabras del corpus, y una sola palabra se representa con un valor “uno” en su posición respectiva. Mientras tanto, todas las demás posiciones (todas las demás palabras posibles) reciben un valor cero. Se crea un vector como este para cada palabra del conjunto de características, y cuando los vectores se unen, el resultado es una matriz que contiene representaciones binarias de todas las palabras características.

Aquí hay otra forma de pensar sobre esto: cualquier palabra dada está representada por un vector de unos y ceros, con un valor de uno en una posición única. El vector se ocupa esencialmente de responder a la pregunta: “¿Es esta la palabra objetivo?” Si la palabra en la lista de palabras características es el objetivo, se ingresa un valor positivo (uno) allí y, en todos los demás casos, la palabra no es el objetivo, por lo que se ingresa un cero. Por lo tanto, tiene un vector que representa solo la palabra objetivo. Esto se hace para cada palabra en la lista de características.

Las codificaciones one-hot son útiles cuando necesita crear una bolsa de palabras o una representación de palabras que tenga en cuenta su frecuencia de aparición. Los modelos de bolsa de palabras son útiles porque, aunque son modelos simples, aún conservan mucha información importante y son lo suficientemente versátiles como para usarse en muchas tareas diferentes relacionadas con la PNL.

Un inconveniente de usar codificaciones one-hot es que no pueden representar el significado de una palabra, ni pueden detectar fácilmente similitudes entre palabras. Si le preocupan el significado y la similitud, a menudo se utilizan incrustaciones de palabras.

Incrustaciones de palabras

La incrustación de palabras se refiere a representar palabras o frases como un vector de números reales, al igual que lo hace la codificación one-hot. Sin embargo, una palabra incrustada puede usar más números que simplemente unos y ceros y, por lo tanto, puede formar representaciones más complejas. Por ejemplo, el vector que representa una palabra ahora puede estar compuesto por valores decimales como 0.5. Estas representaciones pueden almacenar información importante sobre palabras, como la relación con otras palabras, su morfología, su contexto, etc.

Las incrustaciones de palabras tienen menos dimensiones que los vectores codificados en caliente, lo que obliga al modelo a representar palabras similares con vectores similares. Cada vector de palabra en una incrustación de palabras es una representación en una dimensión diferente de la matriz, y la distancia entre los vectores se puede usar para representar su relación. Las incrustaciones de palabras pueden generalizarse porque las palabras semánticamente similares tienen vectores similares. Los vectores de palabras ocupan una región similar de la matriz, lo que ayuda a capturar el contexto y la semántica.

En general, los vectores one-hot son de alta dimensión pero escasos y simples, mientras que las incrustaciones de palabras son de baja dimensión pero densas y complejas.

Generación a nivel de palabra vs generación a nivel de personaje

Hay dos formas de abordar una tarea de procesamiento del lenguaje natural como la generación de texto. Puede analizar los datos y hacer predicciones sobre ellos al nivel de las palabras en el corpus o al nivel de los caracteres individuales. Tanto la generación a nivel de personaje como la generación a nivel de palabra tienen sus ventajas y desventajas.

En general, los modelos de lenguaje a nivel de palabra tienden a mostrar una mayor precisión que los modelos de lenguaje a nivel de carácter. Esto se debe a que pueden formar representaciones más cortas de oraciones y preservar el contexto entre palabras más fácilmente que los modelos de lenguaje a nivel de carácter. Sin embargo, se necesitan grandes corporaciones para entrenar suficientemente los modelos de lenguaje a nivel de palabra, y la codificación one-hot no es muy factible para los modelos a nivel de palabra.

Por el contrario, los modelos de lenguaje a nivel de carácter suelen ser más rápidos de entrenar, requieren menos memoria y tienen una inferencia más rápida que los modelos basados ​​en palabras. Esto se debe a que es probable que el “vocabulario” (el número de funciones de entrenamiento) del modelo sea mucho menor en general, limitado a algunos cientos de caracteres en lugar de cientos de miles de palabras.

Los modelos basados ​​en caracteres también funcionan bien al traducir palabras entre idiomas porque capturan los caracteres que componen las palabras, en lugar de intentar capturar las cualidades semánticas de las palabras. Usaremos un modelo a nivel de personaje aquí, en parte debido a su simplicidad y rápida inferencia.

Usando un RNN / LSTM

Cuando se trata de implementar un LSTM en Keras, el proceso es similar a implementar otras redes neuronales creadas con el modelo secuencial. Empiece declarando el tipo de estructura de modelo que va a utilizar y luego agregue capas al modelo de una en una. Las capas LSTM son fácilmente accesibles para nosotros en Keras, solo tenemos que importar las capas y luego agregarlas con model.add.

Entre las capas primarias del LSTM, usaremos capas de deserción , lo que ayuda a prevenir el problema del sobreajuste. Finalmente, la última capa de la red será una capa densamente conectada que utilizará una función de activación sigmoidea y probabilidades de salida.

Secuencias y características

Es importante comprender cómo manejaremos nuestros datos de entrada para nuestro modelo. Dividiremos las palabras de entrada en partes y las enviaremos a través del modelo de una en una.

Las características de nuestro modelo son simplemente las palabras que nos interesa analizar, representadas con la bolsa de palabras. Los fragmentos en los que dividimos el corpus serán secuencias de palabras, y puede pensar en cada secuencia como una instancia / ejemplo de entrenamiento individual en una tarea tradicional de Machine Learning.

Implementación de un LSTM para la generación de texto

Ahora implementaremos un LSTM y generaremos texto con él. Primero, necesitaremos obtener algunos datos de texto y preprocesar los datos. Después de eso, crearemos el modelo LSTM y lo entrenaremos con los datos. Finalmente, evaluaremos la red.

Para la generación de texto, queremos que nuestro modelo aprenda las probabilidades sobre qué carácter vendrá después, cuando se le da un carácter inicial (aleatorio). Luego, encadenaremos estas probabilidades para crear una salida de muchos caracteres. Primero necesitamos convertir nuestro texto de entrada en números y luego entrenar el modelo en secuencias de estos números.

Comencemos importando todas las bibliotecas que vamos a usar. Necesitamos numpytransformar nuestros datos de entrada en matrices que nuestra red pueda usar, y obviamente usaremos varias funciones de Keras.

También necesitaremos usar algunas funciones del Kit de herramientas de lenguaje natural (NLTK) para preprocesar nuestro texto y prepararlo para entrenar. Finalmente, necesitaremos la sysbiblioteca para manejar la impresión de nuestro texto:

import numpy
import sys
from nltk.tokenize import RegexpTokenizer
from nltk.corpus import stopwords
from keras.models import Sequential
from keras.layers import Dense, Dropout, LSTM
from keras.utils import np_utils
from keras.callbacks import ModelCheckpoint

Para empezar, necesitamos tener datos para entrenar nuestro modelo. Puede usar cualquier archivo de texto que desee para esto, pero usaremos parte del Frankenstein de Mary Shelley, que está disponible para descargar en Project Gutenburg , que aloja textos de dominio público.

Entrenaremos a la red sobre el texto de los primeros 9 capítulos:

file = open("frankenstein-2.txt").read()

Comencemos cargando nuestros datos de texto y haciendo un preprocesamiento de los datos. Necesitaremos aplicar algunas transformaciones al texto para que todo esté estandarizado y nuestro modelo pueda funcionar con él.

Vamos a poner todo en minúsculas y no nos preocuparemos por las mayúsculas en este ejemplo. También usaremos NLTK para hacer tokens a partir de las palabras del archivo de entrada. Creemos una instancia del tokenizador y usémosla en nuestro archivo de entrada.

Finalmente, vamos a filtrar nuestra lista de tokens y solo mantendremos los tokens que no están en una lista de Stop Words, o palabras comunes que brindan poca información sobre la oración en cuestión. Haremos esto usando lambdapara hacer una función rápida de usar y tirar y solo asignaremos las palabras a nuestra variable si no están en una lista de Stop Words proporcionada por NLTK.

Creemos una función para manejar todo eso:

def tokenize_words(input):
    # lowercase everything to standardize it
    input = input.lower()

    # instantiate the tokenizer
    tokenizer = RegexpTokenizer(r'w+')
    tokens = tokenizer.tokenize(input)

    # if the created token isn't in the stop words, make it part of "filtered"
    filtered = filter(lambda token: token not in stopwords.words('english'), tokens)
    return " ".join(filtered)

Ahora llamamos a la función en nuestro archivo:

# preprocess the input data, make tokens
processed_inputs = tokenize_words(file)

Una red neuronal funciona con números, no con caracteres de texto. Así que necesitamos convertir los caracteres en nuestra entrada a números. Ordenaremos la lista del conjunto de todos los caracteres que aparecen en nuestro texto de entrada, luego usaremos la enumeratefunción para obtener números que representan los caracteres. Luego creamos un diccionario que almacena las claves y valores, o los caracteres y números que los representan:

chars = sorted(list(set(processed_inputs)))
char_to_num = dict((c, i) for i, c in enumerate(chars))

Necesitamos la longitud total de nuestras entradas y la longitud total de nuestro conjunto de caracteres para la preparación de datos posterior, por lo que los almacenaremos en una variable. Para tener una idea de si nuestro proceso de conversión de palabras en caracteres ha funcionado hasta ahora, imprimamos la longitud de nuestras variables:

input_len = len(processed_inputs)
vocab_len = len(chars)
print ("Total number of characters:", input_len)
print ("Total vocab:", vocab_len)

Aquí está el resultado:

Total number of characters: 100581
Total vocab: 42

Ahora que hemos transformado los datos en la forma en que deben estar, podemos comenzar a hacer un conjunto de datos con ellos, que alimentaremos a nuestra red. Necesitamos definir cuánto tiempo queremos que sea una secuencia individual (un mapeo completo de caracteres de entrada como números enteros). Estableceremos una longitud de 100 por ahora y declararemos listas vacías para almacenar nuestros datos de entrada y salida:

seq_length = 100
x_data = []
y_data = []

Ahora debemos revisar toda la lista de entradas y convertir los caracteres en números. Haremos esto con un forbucle. Esto creará un montón de secuencias donde cada secuencia comienza con el siguiente carácter en los datos de entrada, comenzando con el primer carácter:

# loop through inputs, start at the beginning and go until we hit
# the final character we can create a sequence out of
for i in range(0, input_len - seq_length, 1):
    # Define input and output sequences
    # Input is the current character plus desired sequence length
    in_seq = processed_inputs[i:i + seq_length]

    # Out sequence is the initial character plus total sequence length
    out_seq = processed_inputs[i + seq_length]

    # We now convert list of characters to integers based on
    # previously and add the values to our lists
    x_data.append([char_to_num[char] for char in in_seq])
    y_data.append(char_to_num[out_seq])

Ahora tenemos nuestras secuencias de entrada de caracteres y nuestra salida, que es el carácter que debe aparecer después de que finalice la secuencia. Ahora tenemos nuestras características y etiquetas de datos de entrenamiento, almacenadas como x_datay y_data.Guardemos nuestro número total de secuencias y verifiquemos cuántas secuencias de entrada totales tenemos:

n_patterns = len(x_data)
print ("Total Patterns:", n_patterns)

Aquí está el resultado:

Total Patterns: 100481

Ahora continuaremos y convertiremos nuestras secuencias de entrada en una matriz numérica procesada que nuestra red puede usar. También necesitaremos convertir los valores de la matriz numérica en flotantes para que la función de activación sigmoidea que usa nuestra red pueda interpretarlos y generar probabilidades de 0 a 1:

X = numpy.reshape(x_data, (n_patterns, seq_length, 1))
X = X/float(vocab_len)

Ahora codificaremos en caliente nuestros datos de etiqueta:

y = np_utils.to_categorical(y_data)

Dado que nuestras características y etiquetas ya están listas para que las utilice la red, sigamos adelante y creemos nuestro modelo LSTM. Especificamos el tipo de modelo que queremos hacer ( sequentialuno) y luego agregamos nuestra primera capa.

Haremos abandonos para evitar el sobreajuste, seguido de otra capa o dos. Luego agregaremos la capa final, una capa densamente conectada que generará una probabilidad sobre cuál será el siguiente carácter de la secuencia:

model = Sequential()
model.add(LSTM(256, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(256, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(128))
model.add(Dropout(0.2))
model.add(Dense(y.shape[1], activation='softmax'))

Compilamos el modelo ahora y está listo para entrenar:

model.compile(loss="categorical_crossentropy", optimizer="adam")

El modelo tarda bastante en entrenarse, por eso guardaremos los pesos y los recargaremos cuando termine el entrenamiento. Configuraremos un checkpointpara guardar los pesos y luego los convertiremos en devoluciones de llamada para nuestro modelo futuro.

filepath = "model_weights_saved.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor="loss", verbose=1, save_best_only=True, mode="min")
desired_callbacks = [checkpoint]

Ahora ajustaremos el modelo y lo dejaremos entrenar.

model.fit(X, y, epochs=4, batch_size=256, callbacks=desired_callbacks)

Una vez que haya terminado el entrenamiento, especificaremos el nombre del archivo y cargaremos los pesos. Luego recompile nuestro modelo con los pesos guardados:

filename = "model_weights_saved.hdf5"
model.load_weights(filename)
model.compile(loss="categorical_crossentropy", optimizer="adam")

Como convertimos los caracteres a números anteriormente, necesitamos definir una variable de diccionario que convertirá la salida del modelo nuevamente en números:

num_to_char = dict((i, c) for i, c in enumerate(chars))

Para generar personajes, necesitamos proporcionar a nuestro modelo entrenado un carácter semilla aleatorio que pueda generar una secuencia de caracteres a partir de:

start = numpy.random.randint(0, len(x_data) - 1)
pattern = x_data[start]
print("Random Seed:")
print(""", ''.join([num_to_char[value] for value in pattern]), """)

Aquí hay un ejemplo de una semilla aleatoria:

" ed destruction pause peace grave succeeded sad torments thus spoke prophetic soul torn remorse horro "

Ahora, para finalmente generar texto, vamos a iterar a través de nuestro número elegido de caracteres y convertir nuestra entrada (la semilla aleatoria) en floatvalores.

Le pediremos al modelo que prediga lo que sigue a partir de la semilla aleatoria, convierta los números de salida en caracteres y luego lo anexe al patrón, que es nuestra lista de caracteres generados más la semilla inicial:

for i in range(1000):
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(vocab_len)
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = num_to_char[index]
    seq_in = [num_to_char[value] for value in pattern]

    sys.stdout.write(result)

    pattern.append(index)
    pattern = pattern[1:len(pattern)]

Veamos qué generó.

"er ed thu so sa fare ver ser ser er serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer...."

¿Parece esto algo decepcionante? Sí, el texto que se generó no tiene ningún sentido y parece que simplemente comienza a repetir patrones después de un rato. Sin embargo, cuanto más entrenes a la red, mejor será el texto que se genere.

Por ejemplo, cuando el número de épocas de entrenamiento se incrementó a 20, el resultado se parecía más a esto:

"ligther my paling the same been the this manner to the forter the shempented and the had an ardand the verasion the the dears conterration of the astore"

El modelo ahora genera palabras reales, incluso si la mayoría todavía no tiene sentido. Aún así, por solo alrededor de 100 líneas de código, no está mal.

Ahora puede jugar con el modelo usted mismo e intentar ajustar los parámetros para obtener mejores resultados.

Conclusión

Querrá aumentar la cantidad de períodos de entrenamiento para mejorar el rendimiento de la red. Sin embargo, es posible que también desee utilizar una red neuronal más profunda (agregar más capas a la red) o una red más amplia (aumentar la cantidad de neuronas / unidades de memoria) en las capas.

También puede intentar ajustar el tamaño del lote, codificar en caliente las entradas, rellenar las secuencias de entrada o combinar cualquier número de estas ideas.

Si desea obtener más información sobre cómo funcionan los LSTM, puede leer más sobre el tema aquí . Aprender cómo los parámetros del modelo influyen en el rendimiento del modelo le ayudará a elegir qué parámetros o hiperparámetros ajustar. También puede leer sobre técnicas y herramientas de procesamiento de texto como las proporcionadas por NLTK.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad