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

    Este es el artículo número 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 obtener similitudes semánticas y clasificar el texto. En este artículo, verá cómo generar texto usando una técnica de aprendizaje profundo en Python usando el Biblioteca de Keras.

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

    Después de completar este artículo, podrá generar texto utilizando el conjunto de datos de su elección. Así que comencemos sin más preámbulos.

    Importar bibliotecas y conjuntos de datos

    El primer paso son las bibliotecas necesarias para ejecutar los scripts de este artículo, así como el conjunto de datos. El siguiente código importa las bibliotecas necesarias:

    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 Python NLTK 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 “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.

    import nltk
    nltk.download('gutenberg')
    from nltk.corpus import gutenberg as gut
    
    print(gut.fileids())
    

    Debería ver el siguiente resultado:

    ['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 hay un texto en bruto para la novela “Macbeth”. Para leer el texto de este archivo, el raw método de gutenberg se puede usar una clase:

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

    Imprimamos los primeros 500 caracteres del conjunto de datos:

    print(macbeth_text[:500])
    

    Aquí está el resultado:

    [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
    

    Verá que hay muchos caracteres especiales y números en el texto. El siguiente paso es borrar el conjunto de datos.

    Preprocesamiento de datos

    Para eliminar la puntuación y los caracteres especiales, definiremos una función con nombre preprocess_text():

    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()
    

    El es preprocess_text la función toma una cadena de texto como parámetro y devuelve una cadena de texto limpia en minúsculas.

    Limpiemos nuestro texto ahora e imprimamos los primeros 500 caracteres nuevamente:

    macbeth_text = preprocess_text(macbeth_text)
    macbeth_text[:500]
    

    Aquí está el resultado:

    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 en profundidad 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, utilizaremos un enfoque muy simple en el que las palabras se convierten en números enteros individuales. Antes de que podamos convertir palabras en números enteros, necesitamos significar nuestro texto en palabras individuales. Para hacer eso, el word_tokenize() método de nltk.tokenize se puede utilizar un módulo.

    El siguiente script señala 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:

    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í:

    Total Words: 17250
    Unique Words: 3436
    

    Nuestro texto contiene un total de 17250 palabras, de las cuales hay 3436 palabras únicas. Para convertir palabras de signos en números, el Tokenizer clase de keras.preprocessing.text se puede utilizar un módulo. Necesitas llamar al fit_on_texts método y pasarle una lista de palabras. Se creará un diccionario en el que se mostrarán las claves de palabras, pero los números enteros mostrarán los valores correspondientes del diccionario.

    Vea el siguiente script:

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

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

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

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

    Imprimamos ahora la palabra número 500 y su valor entero de word_2_index diccionario.

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

    Aquí está el resultado:

    comparisons
    1456
    

    A la palabra “comparaciones” se le asigna aquí un valor entero de 1456.

    Modificar la forma de los datos

    La generación de texto entra en la categoría de muchos problemas de secuencia, ya que la entrada es una secuencia de palabras y la salida es una sola palabra. Usaremos el Red de memoria corta a largo plazo (LSTM), un tipo de red neuronal recurrente para crear nuestro modelo de generación de texto. LSTM toma datos en formato tridimensional (número de muestras, número 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 cambia la forma de las capas de entrada y salida correspondientes.

    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. El es input_seq_length establecido en 100, lo que significa 100 palabras en nuestra secuencia de entrada. Luego, ejecutamos un ciclo donde los valores enteros de las primeras 100 palabras del texto se adjuntan a la primera versión. input_sequence lista. La 101a palabra se adjunta al output_words lista. Durante la segunda versión, se almacena en el texto una secuencia de palabras que comienza en la segunda palabra del texto y termina en la 101ª palabra del texto. input_sequence lista, y la palabra 102 se almacena en el output_words editar, y así sucesivamente. Se generará un total de 17150 secuencias de entrada ya que el conjunto de datos contiene un total de 17250 palabras (100 menos que las palabras completas).

    Imprimamos ahora el valor de la primera secuencia en el input_sequence lista:

    print(input_sequence[0])
    

    Salida:

    [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 números enteros de las secuencias por el valor entero más grande. La siguiente secuencia de comandos convierte la salida a formato bidimensional.

    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 salidas correspondientes.

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

    Salida:

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

    Entrenando el modelo

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

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

    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 categorical_crossentropy función de pérdida utilizada. En el caso de la clasificación binaria, el binary_crossentropy función utilizada. Cuando ejecute el script anterior, debería ver un resumen del modelo:

    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, solo podemos fit() método.

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

    Aquí de nuevo, puede jugar con diferentes valores para batch_size y el epochs. El modelo tardará algún tiempo en entrenarse.

    Hacer una predicción

    Para hacer una predicción, seleccionaremos aleatoriamente una secuencia de la input_sequence lista, conviértala a una forma tridimensional y luego reenvíela a la predict() método 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 se envía al index_2_word diccionario, donde la palabra índice se utiliza como clave. El es index_2_word dictionary devuelve la palabra asociada con la ejecución del índice como clave del diccionario.

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

    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ó al azar la siguiente secuencia. Es probable que la secuencia generada para usted sea diferente a esta:

    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 guión de arriba, el index_2_word solo se crea un diccionario word_2_index diccionario. En este caso, invertir un diccionario se refiere al proceso de intercambio de claves con valores.

    Luego imprimiremos las siguientes 100 palabras que siguen la secuencia de palabras anterior:

    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)]
    

    El es word_sequence Nuestra secuencia de entrada de palabras ahora está en la variable, junto con las siguientes 100 palabras predichas. El es word_sequence la variable contiene una secuencia de palabras en forma de lista. Solo podemos combinar las palabras de la lista para obtener la secuencia de salida final, como se muestra a continuación:

    final_output = ""
    for word in word_sequence:
        final_output = final_output + " " + word
    
    print(final_output)
    

    Aquí está el resultado final:

    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 no se ve muy bien todavía y nuestro modelo parece estar aprendiendo solo de la última palabra, es decir. and. 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 sugerencias para ti:

    • Cambie los hiperparámetros, incluido el tamaño y la cantidad de filas LSTM y la cantidad de claves para ver si obtiene mejores resultados.
    • Prueba las palabras para como is, am, are desde el conjunto de entrenamiento hasta la generación de palabras en lugar de 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 personaje para predecir el próximo N personaje.

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

    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. Si bien el modelo desarrollado en este artículo no es perfecto, el artículo ofrece la idea de cómo generar texto con aprendizaje profundo.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *