Auto-completado de textos con la ayuda de N-Gramas, Python y Ricardo Arjona.

José Christian Topete
MCD-UNISON
Published in
9 min readMay 27, 2021

--

Proyecto desarrollado en el marco de la materia de Procesamiento de Lenguaje Natural de la Maestría en Ciencias de Datos de la Universidad de Sonora.

En éste artículo estaré describiendo mi acercamiento a los modelos de N-gramas para crear un generador de textos simple. Un uso práctico de ésta herramienta es en la ayuda a plataformas de búsqueda o predictores de escritura donde el usuario inicia con un caracter, palabra u oración, prediciendo recomendaciones que pueden popular el próximo espacio en el texto.

El modelo de N-Gramas

Wikipedia define al N-Grama como “una subsecuencia de N elementos de una secuencia dada”. N puede representar una cantidad de caracteres, valores numéricos, palabras u oraciones. Entonces, cuando N = 2, se obtiene una secuencia llamada bi-grama. Cuando N = 3 es un tri-grama, y así sucesivamente...

Para entender el funcionamiento de los modelos de N-Gramas, es importante conocer los fundamentos de las Cadenas de Markov.

Cadenas de Márkov

Una Cadena de Márkov es una cadena de valores o estados en el que, por ejemplo, una moneda de dos lados cuenta con dos estados, cara o cruz. En una cadena de Márkov podemos mantenernos en un estado o movernos a otro.

La probabilidad de moverse de un estado X (cara) a un estado Y (cruz) es 50% y la probabilidad de mantenerse en estado X es el 50%. De la misma forma, moverse de estado Y a X o mantenerse en estado Y comparten la probabilidad del 50%.

Es decir, una cadena de Márkov puede generar un patrón como: Cara, Cara, Cruz, Cara, Cruz, Cruz, etc…

En el modelo de N-Gramas, una serie de texto puede ser tratado como una serie de estados en una cadena de Márkov:

“Pican pican los mosquitos…”

Establecemos entonces que los bi-gramas son secuencias en la ocurrencia entre cada dos caracteres. En éste caso, los bigramas que componen la oración serían: [ PI, IC, CA, AN, NP, PI, IC, CA, AN, NL, LO, OS, SM, MO, OS, SQ, QU, UI, IT, TO, OS ]

De forma similar, si tratamos la frase como un tri-grama obtenemos la secuencia: [ PIC, ICA, CAN, ANP, NPI, PIC, ICA, CAN, ANL, NLO, LOS, OSM, … ] Y así sucesivamente.

De la misma forma que tratamos letras como ocurrencias distintas, podemos obtener series de N-Gramas con cadenas de palabras enteras.

Si consideramos la frase del ejemplo anterior como serie de bi-gramas por palabra, obtendríamos: [ PICAN PICAN, PICAN LOS, LOS MOSQUITOS ] y un tri-grama sería: [PICAN PICAN LOS, PICAN LOS MOSQUITOS]

En éste caso, tratamos cada iteración de nuestros N-Gramas como un estado distinto al cual se puede avanzar o retroceder acorde a la relación y distancia de ese estado con los demás.

Descripción del ejercicio

Para la generación de nuestro modelo de N-Gramas necesitamos alimentarle de un corpus de texto del cual pueda generar una cadena de relación de valores. Para ello estaré recurriendo a la ayuda del controversialmente famoso cantautor Guatemalteco, Ricardo Arjona.

“- ¿Por qué Arjona?”, dirán.

Ricardo Arjona se caracteriza (y eleva a estatus de meme) por sus canciones sobresaturadas de analogías, complementas por un léxico que se percibe simple y extrañamente repetitivo. Esto convierte su repertorio lírico en candidato adecuado para experimentar con diversos ajustes y crear un modelo que repita patrones que semánticamente pasen desapercibidos como escritos por el mismo Arjona.

Ejercicio

1. Librerías y configuración de libreta de Jupyter

Para éste proyecto implementé las librerías [ Beautifulsoup4 ], [ ntlk ](Natural Language ToolKit) para la generación del corpus y la tokenización del texto respectivamente, además de[ re ] para el manejo de expresiones regulares.

Se utilizó el módulo de tokenización [ punkt ] para nltk.

2. Conformación del corpus

Como se mencionó anteriormente, necesitaremos conformar un corpus compuesto por la lírica de Arjona. Primero se debe seleccionar una fuente de la cual extraer la información de forma automatizada y condensarla en una sola variable.

Para ello se usó el sitio www.letras.com en donde se ubica la página que contiene el listado de todas las canciones del artista elegido.

Dentro del sitio del artista, se realiza un parseo para ubicar las direcciones que corresponden a cada canción, se almacenan en una variable de lista llamada links y se aleatoriza su orden lo cual puede ayudar a obtener resultados variados cada vez que se corre el script desde cero.

Se identifican los accesos dentro del HTML del sitio entre etiquetas de lista que comparten la misma clase.
Se identifican 267 canciones dentro del índice del artista.

Se prosigue con la extracción de la letra dentro de cada dirección obtenida; para ello se crea la función corpusificador() que recibe dos variables: una variable link y una variable nueva en formato “string” llamada corpus.

Se implementa BeautifulSoup de nuevo para parsear el contenido dentro de cada subdirectorio del artista y se ejecuta la función dentro de un “for loop” para almacenar el texto extraído dentro de la variable correspondiente al corpus.

Ejecutamos y obtenemos de resultado un corpus de 371,488 caracteres

Para la preparación es importante mencionar la limpieza del corpus, bajando todo el texto a minúsculas y limpiando caracteres y símbolos que puedan causar ruido al correr el modelo.

Hemos pre-procesado nuestro corpus y ahora está listo para crear un modelo de N-Gramas.

3. N-Gramas por caracter

El primer modelo de N-Gramas a explorar es por caracteres, hagamos una prueba dónde N = 3, es decir, un modelo de trigramas:

En el script anterior creamos un diccionario de N-Gramas. Dentro de cada llave de éste diccionario se encuentra el trigrama de caracteres de nuestro corpus.

Como se está construyendo un N-Grama de tres caracteres, declaramos la variable y le asignamos un 3 como valor. Después, iteramos cada caracter dentro del corpus, iniciando desde el cuarto caracter del texto.

Dentro de nuestro “loop” extraemos el trigrama al filtrar los siguientes 3 caracteres. El trigrama es almacenado en una la variable secuencia. Entonces revisamos si el trigrama existe en el diccionario; si no existe dentro del diccionario de valores de N-Gramas, se agrega y almacenamos una lista vacía como valor de ese trigrama.

Para finalizar, el caracter que existe después del trigrama, se almacena como un valor a tal lista.

Si revisamos los el diccionario dentro de la variable ngramas podemos ver una sopa de caracteres almacenada en diferentes llaves en la lista.

Dentro de la lista de valores, se encuentran los trigramas como llaves y sus caracteres correspondientes, que ocurren después de cada trigrama dentro del texto.

Para generar un texto se usan los primeros 3 caracteres del corpus como inicio. Los primeros 3 caracteres dentro de nuestro corpus son: “te ”, con espacio en blanco al final. Debemos recordar que los espacios en blanco se consideran como un caracter en el N-Grama por caracter.

Dentro del código de la siguiente imagen se almacena el primer trigrama que corresponde a “te ” dentro de la variable frase. Para generar el autocompletado elegí 200 caracteres como prueba que al inicializar el “for loop” que itera 200 veces.

Durante cada iteración se revisa que la variable frase, que contiene el primer trigrama, esté en nuestro diccionario de N-Gramas. Si el trigrama no se encuentra dentro del diccionario ngramas, detenemos la iteración.

Posteriormente, el trigrama dentro de la variable frase se almacena como llave en el diccionario ngramas, lo que regresa la lista de posibles caracteres a elegir. De esa lista, un índice se selecciona de forma aleatoria, que pasa sus valores a la variable de lista car_posibles para obtener el siguiente caracter para ese preciso trigrama seleccionado. El siguiente caracter es entonces agregado a la variable de resultado que contiene el resultado final.

Para finalizar, la variable frase se actualiza con el siguiente trigrama del corpus. Al imprimir la variable que contiene los 200 caracteres generados automáticamente resulta algo como ésto

Como podemos observar, los resultados no tienen mucho sentido. Esto debido a la poca distancia que existe entre los caracteres dentro del N-Grama. Para ello se incrementa N a 5 caracteres.

Y al incrementar N a 8 caracteres y generando 300 caracteres obtenemos lo siguiente:

Y ahora N = 10 generando 500 caracteres:

N = 11, 50 caracteres:

Podemos observar en todo caso que el rango Arjona en un N-Grama por caracter debe ser N≥8.

4. N-Gramas por palabra

En un modelo de N-Gramas por palabra cada palabra, en su conjunto, es tratada como un elemento individual. En esencia, el principio es el mismo con el modelo de N-Gramas por caracter. Veamos el código y los resultados donde N = 3:

Primero creamos el diccionario que crea el trigrama de palabras como llaves y la lista de palabras que pueden ocurrir dentro de sus valores.

De forma similar al ejercicio anterior donde usamos cada caracter como parte del trigrama, aquí optamos por elegir tres palabras para componerlo. Para ello debemos tokenizar nuestro corpus y lo almacenamos en la variable tokens.

A continuación, se itera por cada palabra y se seleccionan 3 palabras contiguas para generar el trigrama, se revisa si el trigrama existe en el diccionario ngramas y en caso de que no, se inserta en el diccionario como llave.

Al revisar el diccionario de ngramas se puede observar que cada trigrama está almacenado como llave e incluye las palabras correspondientes como valores dentro de su respectiva llave en el diccionario.

A continuación se presenta el generador de texto, usando el trigrama de palabras que se almacenó.

En el snippet superior, se inicializa la variable resultado con el primer trigrama del corpus que corresponde a “te conocí un”. Se establece un rango de 100 palabras en el generador usando como base el primer trigrama, Para ello, ejecutamos un “for loop” que lo ejecuta 100 veces. Durante cada iteración se revisa si el trigrama de palabras existe en el diccionario ngramas. (En caso de que no, el cíclo se detiene). Al continuar, la lista de palabras que son más probables a seguir el trigrama se obtienen de la llave correspondiente del diccionario ngramas, pasando el trigrama como valor de llave.

Para la lista de palabras posibles, se selecciona una sola palabra de forma aleatoria, se agrega al final de la variable resultado y se obtiene el siguiente trigrama.

El texto generado donde N = 3 resulta así:

Y donde N = 5:

Se puede observar que la generación del texto es más completa pero evita quiebres en el verso. Esto es debido a que no se consideran los quiebres o espacios como elementos, sólo palabras completas y símbolos de puntuación. Además, debido al la poca cantidad de palabras existentes dentro de la lírica que compone el corpus, resulta en oraciones muy cercanas a las frases originales del cantautor.

Conclusión

El modelo de N-Gramas es una herramienta muy poderosa, pues permite capturar contexto entre N-Gramas de palabras en una oración. Ayuda en la automatización en la generación de contenido de forma rápida y competente, especialmente donde la información es amplia y agregando un corpus completo y bien tratado, pueden alimentar un modelo satisfactorio.

El uso de N-Gramas se presta para una amplia gama de nichos de aplicación… Incluso escribir mejores canciones de Arjona que el mismo Arjona.

El problema es convencer a Arjona que lo utilice también…

Octa-grama de caracteres.

Todos los créditos de las líricas van a los autores de las canciones utilizadas.
Artículo con fines académicos.

--

--

José Christian Topete
MCD-UNISON

🇲🇽 Market research professional and Data Science Graduate student.