Python para PNL: generación de texto de aprendizaje profundo con Keras

La generación de texto es una de las aplicaciones más avanzadas de la PNL. En este artículo, verá cómo generar texto a través de técnicas de aprendizaje profundo en Python utilizando la biblioteca Keras.

Este es el artículo 21 de mi serie de artículos sobre Python para PNL. En el artículo anterior expliqué cómo usar la biblioteca FastText de Facebook para encontrar similitud semántica y realizar la clasificación de textos. En este artículo, verá cómo generar texto mediante la técnica de aprendizaje profundo en Python utilizando la [Biblioteca de Keras] (https://keras.io/).

La generación de texto es una de las aplicaciones más avanzadas de la PNL. Las técnicas de aprendizaje profundo se utilizan para una variedad de tareas de generación de texto, como escribir poesía, generar guiones para películas e incluso para componer música. Sin embargo, en este artículo veremos un ejemplo muy simple de generación de texto en el que dada una cadena de palabras de entrada, predeciremos la siguiente palabra. Usaremos el texto sin procesar de la famosa novela de Shakespeare "Macbeth" y lo usaremos para predecir la siguiente palabra dada una secuencia de palabras de entrada.

Después de completar este artículo, podrá realizar la generación de texto utilizando el conjunto de datos de su elección. Entonces, comencemos sin más preámbulos.

Importación de bibliotecas y conjuntos de datos

El primer paso es importar las bibliotecas necesarias para ejecutar los scripts de este artículo, junto con el conjunto de datos. El siguiente código importa las bibliotecas requeridas:

1
2
3
4
5
6
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense, Embedding, LSTM, Dropout
from keras.utils import to_categorical
from random import randint
import re

El siguiente paso es descargar el conjunto de datos. Usaremos la biblioteca NLTK de Python para descargar el conjunto de datos. Usaremos el Conjunto de datos de Gutenberg, que contiene 3036 libros en inglés escritos por 142 autores, incluido el "Macbeth" de Shakespeare.

El siguiente script descarga el conjunto de datos de Gutenberg e imprime los nombres de todos los archivos en el conjunto de datos.

1
2
3
4
5
import nltk
nltk.download('gutenberg')
from nltk.corpus import gutenberg as gut

print(gut.fileids())

Debería ver el siguiente resultado:

1
['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']

El archivo shakespeare-macbeth.txt contiene texto sin procesar de la novela "Macbeth". Para leer el texto de este archivo, se puede usar el método raw de la clase gutenberg:

1
macbeth_text = nltk.corpus.gutenberg.raw('shakespeare-macbeth.txt')

Imprimamos los primeros 500 caracteres de nuestro conjunto de datos:

1
print(macbeth_text[:500])

Aquí está la salida:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[The Tragedie of Macbeth by William Shakespeare 1603]


Actus Primus. Scoena Prima.

Thunder and Lightning. Enter three Witches.

  1. When shall we three meet againe?
In Thunder, Lightning, or in Raine?
  2. When the Hurley-burley's done,
When the Battaile's lost, and wonne

   3. That will be ere the set of Sunne

   1. Where the place?
  2. Vpon the Heath

   3. There to meet with Macbeth

   1. I come, Gray-Malkin

   All. Padock calls anon: faire is foule, and foule is faire,
Houer through

Puede ver que el texto contiene muchos caracteres especiales y números. El siguiente paso es limpiar el conjunto de datos.

Preprocesamiento de datos {#preprocesamiento de datos}

Para eliminar los signos de puntuación y los caracteres especiales, definiremos una función llamada preprocess_text():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def preprocess_text(sen):
    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sen)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence.lower()

La función preprocess_text acepta una cadena de texto como parámetro y devuelve una cadena de texto limpia en minúsculas.

Limpiemos ahora nuestro texto y de nuevo imprimamos los primeros 500 caracteres:

1
2
macbeth_text = preprocess_text(macbeth_text)
macbeth_text[:500]

Aquí está la salida:

1
the tragedie of macbeth by william shakespeare actus primus scoena prima thunder and lightning enter three witches when shall we three meet againe in thunder lightning or in raine when the hurley burley done when the battaile lost and wonne that will be ere the set of sunne where the place vpon the heath there to meet with macbeth come gray malkin all padock calls anon faire is foule and foule is faire houer through the fogge and filthie ayre exeunt scena secunda alarum within enter king malcom

Convertir palabras en números

Los modelos de aprendizaje profundo se basan en algoritmos estadísticos. Por lo tanto, para trabajar con modelos de aprendizaje profundo, necesitamos convertir palabras en números.

En este artículo, usaremos un enfoque muy simple en el que las palabras se convertirán en números enteros individuales. Antes de que podamos convertir palabras en números enteros, necesitamos tokenizar nuestro texto en palabras individuales. Para hacerlo, se puede usar el método word_tokenize() del módulo nltk.tokenize.

El siguiente script tokeniza el texto en nuestro conjunto de datos y luego imprime el número total de palabras en el conjunto de datos, así como el número total de palabras únicas en el conjunto de datos:

1
2
3
4
5
6
7
8
from nltk.tokenize import word_tokenize

macbeth_text_words = (word_tokenize(macbeth_text))
n_words = len(macbeth_text_words)
unique_words = len(set(macbeth_text_words))

print('Total Words: %d' % n_words)
print('Unique Words: %d' % unique_words)

La salida se ve así:

1
2
Total Words: 17250
Unique Words: 3436

Nuestro texto tiene 17250 palabras en total, de las cuales 3436 palabras son únicas. Para convertir palabras tokenizadas en números, se puede usar la clase Tokenizer del módulo keras.preprocessing.text. Debe llamar al método fit_on_texts y pasarle la lista de palabras. Se creará un diccionario donde las claves representarán palabras, mientras que los números enteros representarán los valores correspondientes del diccionario.

Mira el siguiente guión:

1
2
3
from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(num_words=3437)
tokenizer.fit_on_texts(macbeth_text_words)

Para acceder al diccionario que contiene palabras y sus índices correspondientes, se puede utilizar el atributo word_index del objeto tokenizador:

1
2
vocab_size = len(tokenizer.word_index) + 1
word_2_index = tokenizer.word_index

Si verifica la longitud del diccionario, contendrá 3436 palabras, que es el número total de palabras únicas en nuestro conjunto de datos.

Ahora imprimamos la palabra única número 500 junto con su valor entero del diccionario word_2_index.

1
2
print(macbeth_text_words[500])
print(word_2_index[macbeth_text_words[500]])

Aquí está la salida:

1
2
comparisons
1456

Aquí a la palabra "comparaciones" se le asigna el valor entero de 1456.

Modificación de la forma de los datos {#modificación de la forma de los datos}

La generación de texto cae en la categoría de problemas de sucesiones muchos a uno ya que la entrada es una secuencia de palabras y la salida es una sola palabra. Usaremos la Red de memoria a largo plazo (LSTM), que es un tipo de red neuronal recurrente para crear nuestro modelo de generación de texto. LSTM acepta datos en un formato tridimensional (cantidad de muestras, cantidad de pasos de tiempo, características por paso de tiempo). Dado que la salida será una sola palabra, la forma de la salida será bidimensional (número de muestras, número de palabras únicas en el corpus).

El siguiente script modifica la forma de las secuencias de entrada y las salidas correspondientes.

1
2
3
4
5
6
7
8
9
input_sequence = []
output_words = []
input_seq_length = 100

for i in range(0, n_words - input_seq_length , 1):
    in_seq = macbeth_text_words[i:i + input_seq_length]
    out_seq = macbeth_text_words[i + input_seq_length]
    input_sequence.append([word_2_index[word] for word in in_seq])
    output_words.append(word_2_index[out_seq])

En el script anterior, declaramos dos listas vacías input_sequence y output_words. input_seq_length se establece en 100, lo que significa que nuestra secuencia de entrada constará de 100 palabras. A continuación, ejecutamos un bucle en el que, en la primera iteración, los valores enteros de las primeras 100 palabras del texto se añaden a la lista input_sequence. La palabra 101 se agrega a la lista output_words. Durante la segunda iteración, una secuencia de palabras que comienza en la segunda palabra del texto y termina en la palabra 101 se almacena en la lista input_sequence, y la palabra 102 se almacena en la matriz output_words, y así sucesivamente. Se generará un total de 17150 secuencias de entrada ya que hay 17250 palabras en total en el conjunto de datos (100 menos que el total de palabras).

Ahora imprimamos el valor de la primera secuencia en la lista input_sequence:

1
print(input_sequence[0])

Producción:

1
[1, 869, 4, 40, 60, 1358, 1359, 408, 1360, 1361, 409, 265, 2, 870, 31, 190, 291, 76, 36, 30, 190, 327, 128, 8, 265, 870, 83, 8, 1362, 76, 1, 1363, 1364, 86, 76, 1, 1365, 354, 2, 871, 5, 34, 14, 168, 1, 292, 4, 649, 77, 1, 220, 41, 1, 872, 53, 3, 327, 12, 40, 52, 1366, 1367, 25, 1368, 873, 328, 355, 9, 410, 2, 410, 9, 355, 1369, 356, 1, 1370, 2, 874, 169, 103, 127, 411, 357, 149, 31, 51, 1371, 329, 107, 12, 358, 412, 875, 1372, 51, 20, 170, 92, 9]

Normalicemos nuestras secuencias de entrada dividiendo los enteros en las secuencias por el valor entero más grande. El siguiente script también convierte la salida en formato bidimensional.

1
2
3
4
X = np.reshape(input_sequence, (len(input_sequence), input_seq_length, 1))
X = X / float(vocab_size)

y = to_categorical(output_words)

El siguiente script imprime la forma de las entradas y las salidas correspondientes.

1
2
print("X shape:", X.shape)
print("y shape:", y.shape)

Producción:

1
2
X shape: (17150, 100, 1)
y shape: (17150, 3437)

Entrenando al modelo {#entrenando al modelo}

El siguiente paso es entrenar nuestro modelo. No existe una regla estricta sobre la cantidad de capas y neuronas que se deben usar para entrenar el modelo. Seleccionaremos aleatoriamente los tamaños de capa y neurona. Puede jugar con los hiperparámetros para ver si puede obtener mejores resultados.

Crearemos tres capas LSTM con 800 neuronas cada una. Se agregará una capa densa final con 1 neurona para predecir el índice de la siguiente palabra, como se muestra a continuación:

1
2
3
4
5
6
7
8
9
model = Sequential()
model.add(LSTM(800, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(LSTM(800, return_sequences=True))
model.add(LSTM(800))
model.add(Dense(y.shape[1], activation='softmax'))

model.summary()

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

Dado que la palabra de salida puede ser una de 3436 palabras únicas, nuestro problema es un problema de clasificación de clases múltiples, por lo que se utiliza la función de pérdida categorical_crossentropy. En caso de clasificación binaria, se utiliza la función binary_crossentropy. Una vez que ejecute el script anterior, debería ver el resumen del modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
lstm_1 (LSTM)                (None, 100, 800)          2566400
_________________________________________________________________
lstm_2 (LSTM)                (None, 100, 800)          5123200
_________________________________________________________________
lstm_3 (LSTM)                (None, 800)               5123200
_________________________________________________________________
dense_1 (Dense)              (None, 3437)              2753037
=================================================================
Total params: 15,565,837
Trainable params: 15,565,837
Non-trainable params: 0

Para entrenar el modelo, simplemente podemos usar el método fit().

1
model.fit(X, y, batch_size=64, epochs=10, verbose=1)

Aquí nuevamente, puedes jugar con diferentes valores para batch_size y epochs. El modelo puede tomar algún tiempo para entrenar.

Hacer predicciones

Para hacer predicciones, seleccionaremos aleatoriamente una secuencia de la lista input_sequence, la convertiremos en una forma tridimensional y luego la pasaremos al método predict() del modelo entrenado. El modelo devolverá una matriz codificada one-hot donde el índice que contiene 1 será el valor de índice de la siguiente palabra. El valor del índice luego se pasa al diccionario index_2_word, donde la palabra índice se usa como clave. El diccionario index_2_word devolverá la palabra que pertenece al índice que se pasa como clave al diccionario.

El siguiente script selecciona aleatoriamente una secuencia de números enteros y luego imprime la secuencia de palabras correspondiente:

1
2
3
4
5
6
7
8
random_seq_index = np.random.randint(0, len(input_sequence)-1)
random_seq = input_sequence[random_seq_index]

index_2_word = dict(map(reversed, word_2_index.items()))

word_sequence = [index_2_word[value] for value in random_seq]

print(' '.join(word_sequence))

Para el guión de este artículo, se seleccionó aleatoriamente la siguiente secuencia. Lo más probable es que la secuencia generada para usted sea diferente a esta:

1
amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane

En el script anterior, el diccionario index_2_word se crea simplemente invirtiendo el diccionario word_2_index. En este caso, invertir un diccionario se refiere al proceso de intercambiar claves con valores.

A continuación, imprimiremos las siguientes 100 palabras que siguen la secuencia de palabras anterior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for i in range(100):
    int_sample = np.reshape(random_seq, (1, len(random_seq), 1))
    int_sample = int_sample / float(vocab_size)

    predicted_word_index = model.predict(int_sample, verbose=0)

    predicted_word_id = np.argmax(predicted_word_index)
    seq_in = [index_2_word[index] for index in random_seq]

    word_sequence.append(index_2_word[ predicted_word_id])

    random_seq.append(predicted_word_id)
    random_seq = random_seq[1:len(random_seq)]

La variable word_sequence ahora contiene nuestra secuencia de palabras de entrada, junto con las siguientes 100 palabras predichas. La variable word_sequence contiene una secuencia de palabras en forma de lista. Simplemente podemos unir las palabras en la lista para obtener la secuencia de salida final, como se muestra a continuación:

1
2
3
4
5
final_output = ""
for word in word_sequence:
    final_output = final_output + " " + word

print(final_output)

Aquí está el resultado final:

1
amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and

El resultado aún no se ve muy bien y parece que nuestro modelo solo está aprendiendo de la última palabra, es decir, y. Sin embargo, tienes la idea de cómo crear un modelo de generación de texto con Keras. Para mejorar los resultados, tengo las siguientes recomendaciones para ti:

  • Cambie los hiperparámetros, incluido el tamaño y la cantidad de capas LSTM y la cantidad de épocas para ver si obtiene mejores resultados.
  • Intente eliminar las palabras vacías como es, soy, son del conjunto de entrenamiento para generar palabras que no sean palabras vacías en el conjunto de prueba (aunque esto dependerá del tipo de aplicación).
  • Cree un modelo de generación de texto a nivel de carácter que prediga los siguientes caracteres N.

Para practicar más, le recomendaría que intente desarrollar un modelo de generación de texto con los otros conjuntos de datos del corpus de Gutenberg.

Yendo más lejos: proyecto de extremo a extremo portátil

¿Tu naturaleza inquisitiva te hace querer ir más allá? Recomendamos consultar nuestro Proyecto guiado: ["Subtítulos de imágenes con CNN y Transformers con Keras"](https://wikihtp.com/courses/image-captioning-with-cnns-and -transformadores-con-keras/#cta){target="_blank"}.

En este proyecto guiado, aprenderá a crear un modelo de subtítulos de imágenes, que acepta una imagen como entrada y produce un subtítulo de texto como salida.

Aprenderás a:

  • Preprocesar texto
  • Vectorizar la entrada de texto fácilmente
  • Trabaje con la API tf.data y cree conjuntos de datos de alto rendimiento
  • Cree Transformers desde cero con TensorFlow/Keras y KerasNLP: la adición horizontal oficial a Keras para crear modelos NLP de última generación
  • Cree arquitecturas híbridas donde la salida de una red se codifica para otra

¿Cómo enmarcamos los subtítulos de las imágenes? La mayoría lo considera un ejemplo de aprendizaje profundo generativo, porque estamos enseñando a una red a generar descripciones. Sin embargo, me gusta verlo como una instancia de traducción automática neuronal: estamos traduciendo las características visuales de una imagen en palabras. A través de la traducción, estamos generando una nueva representación de esa imagen, en lugar de simplemente generar un nuevo significado. Verlo como traducción, y solo por generación de extensión, enfoca la tarea bajo una luz diferente y la hace un poco más intuitiva.

Enmarcar el problema como uno de traducción hace que sea más fácil averiguar qué arquitectura querremos usar. Los transformadores solo de codificador son excelentes para comprender el texto (análisis de opinión, clasificación, etc.) porque los codificadores codifican representaciones significativas. Los modelos de solo decodificador son excelentes para la generación (como GPT-3), ya que los decodificadores pueden inferir representaciones significativas en otra secuencia con el mismo significado. La traducción generalmente se realiza mediante una arquitectura de codificador-decodificador, donde los codificadores codifican una representación significativa de una oración (o imagen, en nuestro caso) y los decodificadores aprenden a convertir esta secuencia en otra representación significativa que es más interpretable para nosotros (como una oración).

Conclusión

En este artículo, vimos cómo crear un modelo de generación de texto utilizando el aprendizaje profundo con la biblioteca Keras de Python. Aunque el modelo desarrollado en este artículo no es perfecto, el artículo transmite la idea de cómo generar texto con aprendizaje profundo.