Gilbert.dice(): desarrollo

/ enero 30, 2021/ Ciencia de información y datos, Gilbert dice, Novedades, Porfolio, Proyectos personales

Introducción

Como ya he explicado, Gilbert no parte de un planteamiento inicial vacío, ya tenía desde finales de 2018 como base el Generador de Ideas del Excel y un conjunto de palabras. Aún así, uno de los problemas que me planteaba a la hora de pasar del Generador de Ideas a Gilbert era que, además, crease una frase coherente, no solo lanzase las palabras con la que construir la frase, tal como hace el Generador.

Lo que podía traducirse como: La luna excéntrica come.

En el Generador, el texto gira en torno a la idea que arroja la frase; luego, puedes complementarla con más palabras o el apartado del reto. Cuando hice el generador y lo usamos por primera vez, el hecho de tener que construir la frase y que no viniese ya construida, generó al principio un poco de confusión.

Quería que Gilbert sorteara, desde el principio, este escollo, por lo que debía tener en cuenta para generar estas frases el género del sujeto y del adjetivo, para hacerlos concordar. Esto hacía obligatorio tocar el conjunto original con el que trabaja el Generador, que entre todas las columnas solo tenía 2190 palabras. Además, debía tener en cuenta que el generador necesitaba de alguna condición para saber cuándo el sujeto era femenino o masculino y elegir el adjetivo con el género correcto. Requería replantear el conjunto de palabras.

El conjunto original tenía una columna de adjetivos, otra de sujetos y otra de verbos; para poder usarlo con Gilbert, tuve que ampliarla; una nueva con los artículos la y el, otra de sujetos, otra para verbos (acciones) y dos para los adjetivos (una para masculinos y otra para femeninos). Incluir la columna de artículos solucionaba el problema de la selección del género del adjetivo.

Planteamiento

Una vez finalizado el trabajo con el conjunto, lo guardé convenientemente en un archivo csv; comencé a plantear y desarrollar el problema y la estructura para solucionarlo. Todavía sin demasiado orden.

Carga de bibliotecas Python necesarias y datos externos

El conjunto final con el que luego trabaja a Gilbert es un conjunto que está compuesto por 715 sujetos, 715 adjetivos, 715 verbos y 50 retos; lo que supone que la columna de Retos aparezca con valores vacíos en el csv que podrían dar problemas a la hora de trabajar con ellos; aunque para guardar los datos, tenerlo todo en el mismo sitio es muy cómodo.

import pandas as pd
import numpy as np
diccionario = pd.read_csv('diccionario_reducido.csv', sep=',')
diccionario.sample(5)

Una de las primeras cosa que tenía que hacer era separar la columna de Retos del resto del conjunto. Una vez separado, necesitaba generar un dado que seleccionara un único registro de forma aleatoria.

# Columna de retos separada del resto del diccionario
retos = diccionario['Retos'].dropna()

# Generador aleatorio
aleatorio = int(np.random.randint(len(retos), size=1))

retos[aleatorio]

Los retos no era lo único que iba a necesitar de un dado, también tenía que hacer otro para crear las frases, en este caso de 3 valores (aunque para formar la frase necesitase 5 columnas), uno para el artículo y el sujeto, otro para el adjetivo y un tercero para el verbo. Además, los números aleatorios tendrían un rango de datos mayor: igual al total de las filas del conjunto.

Para mantener el conjunto de diccionario tal como estaba, seleccioné las columnas que iba a necesitar y las guardé en una nueva variable: frase, con la que trabajé el planteamiento.

frase = diccionario[['artículo', 'sujeto', 'adjetivo masculino', 'adjetivo femenino', 'acciones']]

aleatorios = np.random.randint(len(frase['artículo']), size=3)

Teniendo los elementos listos para generar la frase, lo único que me quedaba era crear la lógica condicional para que se formara la frase con los elementos correctos:

if frase['artículo'][aleatorios[0]] == 'El':
    print(frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo masculino'][aleatorios[1]], frase['acciones'][aleatorios[2]])
else:
    print(frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo femenino'][aleatorios[1]], frase['acciones'][aleatorios[2]])

Según la lógica planteada, cuando el artículo en la posición aleatoria que haya sacado en primer lugar sea El, el programa devolverá una frase construida con ese artículo y el sujeto que esté en esa misma posición; luego, utilizando el segundo número aleatorio generado, añadirá el adjetivo en masculino y la acción que resolverá la situación utilizando el tercer aleatorio que haya generado aleatorios.

Cuando no se cumpla la condición, solo podrá ser porque el adjetivo será La, en ese caso, hará la misma construcción de la frase, variando únicamente la columna de donde cogerá el adjetivo (la de femeninos).

Al principio pensé que no sería capaz o que me encontraría con problemas más complicados con los que me atascaría a la hora de plantear la lógica que utilizaría, pero a la hora de sentarme el desarrollo resultó ser muy natural. Supongo que porque cuando estuve trabajando el conjunto ya había detectado los problemas que podía ocasionarme a la hora de crear una frase gramaticalmente correcta y fui subsanándolo sobre la marcha. Obviamente, el programa como tal tampoco es demasiado complejo y no pretende ser una solución basada en NLG, solo es una forma sencilla de generar un creador de frases que no tienen por qué basarse en ningún contexto ni coherencia interna.

En este caso, el programa potencia enormemente el desarrollo de la creatividad y la imaginación porque, al no tener restricciones semánticas ni de uso del lenguaje más allá de seleccionar correctamente el género del adjetivo, puede hacer cualquier combinación. Esta carencia de entendimiento fomenta la incoherencia, lo que puede complicar el reto de trabajar con una idea determinada. En este mismo proceso, un humano lo tiene más difícil al tener interiorizada las convenciones y la percepción de la realidad y la lógica. La máquina no tiene esta clase de sesgos ni sabe lo que está uniendo. Solo devuelve los resultados que están almacenados en celdas; su única limitación entonces es la cantidad de ítems a los que puede acceder para generar una respuesta.

Siguiendo con el planteamiento de Gilbert, lo único que faltaba por crear era el listado de palabras; tal como lo planteé en el Generador este apartado daba como máximo 5 sujetos, 5 adjetivos y 5 verbos, como mínimo ninguno y todos los que daba eran independientes. Para Gilbert, al principio decidí hacerlo solo con adjetivos; después de todo, eran los que daban más juego a la hora de desarrollar el texto. No descarto modificarlo en algún futuro para que Gilbert proporcione sujetos, adjetivos y verbos tal como hace el Generador.

palabras = []
for _ in range(int(np.random.randint(11, size=1))):
    palabras.append(frase['adjetivo masculino'][int(np.random.randint(len(frase['artículo']), size=1))])
    
print('El texto debe contener:', palabras)

En el caso de las palabras que el texto creativo debía contener, era fácil plantearlo con un bucle for. La traducción de la lógica que emplea el for para crear esa lista de palabras podría ser algo así: para cada valor en un rango aleatorio de, como máximo, 11 elementos, agrega a la lista palabras el adjetivo recuperado del conjunto que tenga un número aleatorio entre 0 y el número total de elementos que encuentres en frase['artículo'] (elegí esta columna como podía haber elegido otra.

Una vez terminado esto, podría haber ordenado el proceso y haberlo dejado ahí: ya tenía algo que funcionaba tal como yo quería para generar las ideas. Pero usarlo requería de abrir el cuaderno de Jupyter, activar todas las celdas, etc. Además, no estaba organizado ni era cómodo; lo que requería seguir trabajando un poquito más.

Lo siguiente que hice, por tanto, fue ordenar el código para que siguiera una estructura lógica y que recordaba a la hora de devolver los resultados un poquito al Generador.

# Carga de bibliotecas
import pandas as pd
import numpy as np

# Carga de diccionario
diccionario = pd.read_csv('diccionario_reducido.csv', sep=',')

# Idea general del texto
frase = diccionario[['artículo', 'sujeto', 'adjetivo masculino', 'adjetivo femenino', 'acciones']]
aleatorios = np.random.randint(len(frase['artículo']), size=3)
if frase['artículo'][aleatorios[0]] == 'El':
    print('La idea del relato es:', frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo masculino'][aleatorios[1]], frase['acciones'][aleatorios[2]])
else:
    print('La idea del relato es:', frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo femenino'][aleatorios[1]], frase['acciones'][aleatorios[2]])

# Las palabras que debe contener el texto
palabras = []
for _ in range(int(np.random.randint(11, size=1))):
    palabras.append(frase['adjetivo masculino'][int(np.random.randint(len(frase['artículo']), size=1))])
    
print('El texto debe contener:', palabras)

# El reto
retos = diccionario['Retos'].dropna()
aleatorio = int(np.random.randint(len(retos), size=1))
print('El reto esta vez es:', retos[aleatorio])
diccionario = {'artículo': ['La', 'El', 'La', 'La', 'La', 'La', 'El', 'La', 'El', 'La', 'El', 'La', 'El', 'El', 'El', 'El', 'El', 'El', 'La', 'La'], 'sujeto': ['llave', 'polizón', 'hormiga', 'manta', 'casa solariega', 'gabardina', 'impermeable', 'promesa', 'judo', 'brújula', 'whisky', 'inyección', 'sueño', 'utensilio', 'marinero', 'fruto seco', 'bocadillo', 'tulipán', 'piedra', 'vaca'], 'adjetivo masculino': ['naciente', 'sensato', 'juvenil', 'notorio', 'criminal', 'glacial', 'liberal', 'silencioso', 'malicioso', 'coherente', 'pechugón', 'lluvioso', 'envenenado', 'amarillo', 'numeroso', 'geológico', 'caprichoso', 'añejo', 'roto', 'rojo'], 'adjetivo femenino': ['naciente', 'sensata', 'juvenil', 'notoria', 'criminal', 'glacial', 'liberal', 'silenciosa', 'maliciosa', 'coherente', 'pechugona', 'lluviosa', 'envenenada', 'amarilla', 'numerosa', 'geológica', 'caprichosa', 'añeja', 'rota', 'roja'], 'acciones': ['nace', 'se molesta', 'injuria a alguien', 'obtiene', 'contagia', 'gotea', 'jadea', 'se queja', 'llega', 'cercena', 'estudia', 'late', 've algo insólito', 'busca un final feliz', 'oficia', 'golpea', 'bucea', 'se despierta en otra época', 'se hiere', 'se hace derogar']}
retos = {'Retos': ['Este reto es un alivio, te permite la elipsis de 1 palabra que te haya salido como obligatoria para tu texto. Elige sabiamente', 'Qué paradójico sería que tu texto no tuviese una paradoja', 'Era como… la descripción que has hecho. Ex-ac-ta-men-te', 'No es dislexia, es un sinécdoque, ¡que no te enteras!', 'Don Quijote estaría orgulloso de tu aporte al noble arte de las historias de caballería', 'Seguro que, gracias a tu emotiva oda, el protagonista de tu historia será recordado eternamente', 'Este aire suena como una sinestesia, ¿no os parece?"', 'Tiene que parecer un ensayo, no serlo, porque de esto sé que no tienes ni idea', '¿Cuántas líneas tiene ese papel? Bueno, pues como mucho, puedes llenar 30 líneas', 'Alíviate o no te alivies, altérate o no te alteres, pero haz que tu texto sea aliterado']}

Pasando el planteamiento a funciones

Obviamente, no podía dejar las cosas así y planteé las funciones que iba a requerir para desarrollar el programa. Desde el primer momento que planteé las funciones lo quise hacer con tres diferentes para luego implementar una clase que las contuviera, la clase Gilbert.

Creé la función .idea().palabras() y .reto() utilizando los códigos que ya había probado durante el planteamiento. Además, como había creado los diccionarios internos, los aproveché para dejar de necesitar el archivo original.

frase = pd.DataFrame(diccionario)
aleatorios = np.random.randint(len(frase['artículo']), size=3)

def idea():
    if frase['artículo'][aleatorios[0]] == 'El':
        return ' '.join([frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo masculino'][aleatorios[1]], frase['acciones'][aleatorios[2]]])
    else:
        return ' '.join([frase['artículo'][aleatorios[0]], frase['sujeto'][aleatorios[0]], frase['adjetivo femenino'][aleatorios[1]], frase['acciones'][aleatorios[2]]])
    

def palabras():
    palabra = []
    for n in range(int(np.random.randint(11, size=1))):
        palabra.append(frase['adjetivo masculino'][int(np.random.randint(len(frase['artículo']), size=1))])
    return palabra

def reto():
    return retos['Retos'][int(np.random.randint(len(retos['Retos']), size=1))]
              
print('La idea del relato es:', idea())
print('El texto debe contener:', palabras())
print('El reto esta vez es:', reto())

De la función a la clase

Para finalizar, tocaba crear la clase desde las funciones que había establecido como punto intermedio y hacer que la clase fuese completamente independiente; para ello definí en el init los elementos que tenía que recuperar y estructurar el programa. En este caso, directamente introduje en frase la creación de la tabla a partir del diccionario que había creado y dejé el elemento de retos como un diccionario con el que trabajar.

La clase Gilbert contendría 4 métodos: .idea().palabras().reto() y .dice(); los tres primeros actuarían de forma independiente y darían como resultado solo lo que trabajaban, el cuarto llamaría al resto para devolver una respuesta completa.

import pandas as pd
import numpy as np

class Gilbert:
    
    frase = pd.DataFrame({'artículo': ['La', 'El', 'La', 'La', 'La', 'La', 'El', 'La', 'El', 'La', 'El', 'La', 'El', 'El', 'El', 'El', 'El', 'El', 'La', 'La'], 'sujeto': ['llave', 'polizón', 'hormiga', 'manta', 'casa solariega', 'gabardina', 'impermeable', 'promesa', 'judo', 'brújula', 'whisky', 'inyección', 'sueño', 'utensilio', 'marinero', 'fruto seco', 'bocadillo', 'tulipán', 'piedra', 'vaca'], 'adjetivo masculino': ['naciente', 'sensato', 'juvenil', 'notorio', 'criminal', 'glacial', 'liberal', 'silencioso', 'malicioso', 'coherente', 'pechugón', 'lluvioso', 'envenenado', 'amarillo', 'numeroso', 'geológico', 'caprichoso', 'añejo', 'roto', 'rojo'], 'adjetivo femenino': ['naciente', 'sensata', 'juvenil', 'notoria', 'criminal', 'glacial', 'liberal', 'silenciosa', 'maliciosa', 'coherente', 'pechugona', 'lluviosa', 'envenenada', 'amarilla', 'numerosa', 'geológica', 'caprichosa', 'añeja', 'rota', 'roja'], 'acciones': ['nace', 'se molesta', 'injuria a alguien', 'obtiene', 'contagia', 'gotea', 'jadea', 'se queja', 'llega', 'cercena', 'estudia', 'late', 've algo insólito', 'busca un final feliz', 'oficia', 'golpea', 'bucea', 'se despierta en otra época', 'se hiere', 'se hace derogar']})
    aleatorios = np.random.randint(len(frase['artículo']), size=3)
    retos = {'Retos': ['Este reto es un alivio, te permite la elipsis de 1 palabra que te haya salido como obligatoria para tu texto. Elige sabiamente', 'Qué paradójico sería que tu texto no tuviese una paradoja', 'Era como… la descripción que has hecho. Ex-ac-ta-men-te', 'No es dislexia, es un sinécdoque, ¡que no te enteras!', 'Don Quijote estaría orgulloso de tu aporte al noble arte de las historias de caballería', 'Seguro que, gracias a tu emotiva oda, el protagonista de tu historia será recordado eternamente', 'Este aire suena como una sinestesia, ¿no os parece?"', 'Tiene que parecer un ensayo, no serlo, porque de esto sé que no tienes ni idea', '¿Cuántas líneas tiene ese papel? Bueno, pues como mucho, puedes llenar 30 líneas', 'Alíviate o no te alivies, altérate o no te alteres, pero haz que tu texto sea aliterado']}    
    def __init__(self, frase = frase, aleatorios = aleatorios, retos = retos):
        self.frase = frase
        self.aleatorios = aleatorios
        self.retos = retos

    def idea(self):
        '''Genera una frase aleatoria que podrás utilizar como la idea principal del relato.
        El programa no utiliza ninguna lógica ni coherencia para la selección de las columnas,
        por lo que puedes enfrentarte a ideas bastante incoherentes; lo que puede resultar en
        un ejercicio bastante estimulante para la imaginación'''
        if self.frase['artículo'][self.aleatorios[0]] == 'El':
            return ' '.join([self.frase['artículo'][self.aleatorios[0]], self.frase['sujeto'][self.aleatorios[0]], self.frase['adjetivo masculino'][self.aleatorios[1]], self.frase['acciones'][self.aleatorios[2]]])
        else:
            return ' '.join([self.frase['artículo'][self.aleatorios[0]], self.frase['sujeto'][self.aleatorios[0]], self.frase['adjetivo femenino'][self.aleatorios[1]], self.frase['acciones'][self.aleatorios[2]]])

    def palabras(self):
        '''Genera un listado de palabras aleatorio en base a adjetivos que debes utilizar en el
        desarrollo del texto; estas palabras pueden aparecer en todas sus variantes de género y número.'''
        palabras = []
        for n in range(int(np.random.randint(11, size=1))):
            palabras.append(self.frase['adjetivo masculino'][int(np.random.randint(len(self.frase['artículo']), size=1))])
        return palabras

    def reto(self):
        '''Lanza un reto aleatorio de los que existen dentro de la lista, para hacer más complicado
        (o facilitar a veces) la ejecución del relato.'''
        return self.retos['Retos'][int(np.random.randint(len(self.retos['Retos']), size=1))]

    def dice():
        '''¡Devuelve la respuesta que ha generado Gilbert!'''
        print('La idea del relato es:', Gilbert().idea())
        print('El texto debe contener:', Gilbert().palabras())
        print('El reto esta vez es:', Gilbert().reto())

Gilbert.dice()

Reconozco que tuve bastantes problemas a la hora de entender y trabajar con la creación de la clase, porque no terminaba de encontrar la manera de que funcionase. Le dediqué un montón de horas hasta que me di cuenta de que el problema era… los paréntesis. El hecho de darle tantas vueltas a ese punto y encontrar qué era lo que no estaba funcionando hizo que comprendiera mucho mejor la estructura y funcionamiento de las clases en Python. Siendo sincera, aunque funcione, no estoy demasiado segura de tal como lo he hecho es correcto.

Podría decir que el planteamiento de Gilbert.dice() está terminado. Aunque, igual cuando tenga más avanzados otros proyectos vuelva a él y cree una interfaz pequeñita y simple para utilizarlo y empaquetarlo en un programa. Eso lo sabré el día que me siente a hacerlo.

Nota. Aunque la versión original consta del número de filas que he mencionado, he estado unos días dándole vueltas a si debía guardarme el conjunto completo para mí y solo proporcionar un conjunto reducido que haga funcionar el programa. Dado que me ha llevado bastante tiempo la parte de crear el conjunto y revisarlo, creo que es la opción más justa. Por eso, si estás probando este código, lo harás con un conjunto de 20 filas de palabras y 10 retos.

Aquí puedes descargarte el archivo csv: diccionario_reducido

Si quieres descargarte el cuaderno de Jupyter, visita el repositorio en GitHub.

Compartir esta entrada

Dejar un Comentar