from microtc.params import OPTION_GROUP, OPTION_DELETE,\
OPTION_NONEfrom microtc.textmodel import SKIP_SYMBOLS
from b4msa.textmodel import TextModel
from b4msa.lang_dependency import LangDependency
from nltk.stem.snowball import SnowballStemmer
import unicodedata
import re
2 Manejando Texto
El objetivo de la unidad es
Paquetes usados
Video explicando la unidad
2.1 Introducción
Se podría suponer que el texto se que se analizará está bien escrito y tiene un formato adecuado para su procesamiento. Desafortunadamente, la realidad es que en la mayoría de aplicaciones el texto que se analiza tiene errores de ortográficos, errores de formato y además no es trivial identificar la unidad mínima de procesamiento que podría ser de manera natural, en el español, las palabras. Por este motivo, esta unidad trata técnicas comunes que se utilizan para normalizar el texto, esta normalización es un proceso previo al desarrollo de los algoritmos de PLN.
La Figura 2.1 esquematiza el procedimiento que se presenta en esta unidad, la idea es que se un texto pasa primeramente a un proceso de normalización (Sección 2.2 y Sección 2.3), para después ser segmentado (ver Sección 2.4) y el resultado es lo que se utiliza para modelar el lenguaje.
flowchart LR Entrada([Texto]) --> Norm[Normalización de Texto] Norm --> Seg[Segmentación] Seg --> Terminos(...)
Las normalizaciones y segmentaciones descritas en esta unidad se basan principalmente en las utilizadas en los siguientes artículos científicos.
- An automated text categorization framework based on hyperparameter optimization (Tellez et al. (2018))
- A simple approach to multilingual polarity classification in Twitter (Tellez, Miranda-Jiménez, Graff, Moctezuma, Suárez, et al. (2017))
- A case study of Spanish text transformations for twitter sentiment analysis (Tellez, Miranda-Jiménez, Graff, Moctezuma, Siordia, et al. (2017))
2.2 Normalización de Texto Sintáctica
La descripción de las normalizaciones empieza presentando las que se puede aplicar a nivel de caracteres, sin la necesidad de conocer el significado de las palabras. También se agrupan en este conjunto aquellas transformaciones que se realizan mediante expresiones regulares o su búsqueda en una lista de palabras previamente definidas.
2.2.1 Entidades
La descripción de diferentes técnicas de normalización empieza con el manejo de entidades en el texto. Algunas entidades que se tratarán serán los nombres de usuario, números o URLs mencionados en un texto. Por otro lado están las acciones que se realizarán a las entidades encontradas, estas acciones corresponden a su borrado o remplazo por algún otro toquen.
2.2.1.1 Usuarios
En esta sección se trabajará con los nombres de usuarios que siguen el formato usado por Twitter. En un tuit, los nombres de usuarios son aquellas palabras que inician con el caracter @ y terminan con un espacio o caracter terminal. Las acciones que se realizarán con los nombres de usuario encontrados serán su borrado o reemplazo por una etiqueta en particular.
El procedimiento para encontrar los nombres de usuarios es mediante expresiones regulares, en particular se usa la expresión @\S+
, tal y como se muestra en el siguiente ejemplo.
= 'Hola @xx, @mm te está buscando'
text r"@\S+", "", text) re.sub(
'Hola te está buscando'
La segunda acción es reemplazar cada nombre de usuario por una etiqueta particular, en el siguiente ejemplo se reemplaza por la etiqueta _usr
.
= 'Hola @xx, @mm te está buscando'
text r"@\S+", "_usr", text) re.sub(
'Hola _usr _usr te está buscando'
2.2.1.2 URL
Los ejemplos anteriores se pueden adaptar para manejar URL; solamente es necesario adecuar la expresión regular que identifica una URL. En el siguiente ejemplo se muestra como se pueden borrar las URLs que aparecen en un texto.
= "puedes verificar que http://google.com esté funcionando"
text r"https?://\S+", "", text) re.sub(
'puedes verificar que esté funcionando'
2.2.1.3 Números
The previous code can be modified to deal with numbers and replace the number found with a shared label such as _num
.
= "acabamos de ganar 10 M"
text r"\d\d*\.?\d*|\d*\.\d\d*", "_num", text) re.sub(
'acabamos de ganar _num M'
2.2.2 Ortografía
El siguiente bloque de normalizaciones agrupa aquellas modificaciones que se realizan a algún componente de tal manera que aunque impacta en su ortografía puede ser utilizado para reducir la dimensión y se ve reflejado en la complejidad del algoritmo.
2.2.2.1 Mayúsculas y Minúsculas
La primera de estas transformaciones es convertir todas los caracteres a minúsculas. Como se puede observar esta transformación hace que el vocabulario se reduzca, por ejemplo, las palabras México o MÉXICO son representados por la palabra méxico. Esta operación se puede realizar con la función lower
tal y cómo se muestra a continuación.
= "México"
text text.lower()
'méxico'
2.2.2.2 Signos de Puntuación
Los signos de puntuación son necesarios para tareas como la generación de textos, pero existen otras aplicaciones donde los signos de puntuación tienen un efecto positivo en el rendimiento del algorithm, este es el caso de tareas de categorización de texto. El efecto que tiene el quitar los signos de puntuación es que el vocabulario se reduce. Los símbolos de puntuación se pueden remover teniendo una lista de los mismos, esta lista de signos de puntuación se encuentra en la variable SKIP_SYMBOLS
y el siguiente código muestra un procedimiento para quitarlos.
= "¡Hola! buenos días:"
text = ""
output for x in text:
if x in SKIP_SYMBOLS:
continue
+= x
output output
'Hola buenos días'
2.2.2.3 Símbolos Diacríticos
Continuando con la misma idea de reducir el vocabulario, es común eliminar los símbolos diacríticos en las palabras. Esta transformación también tiene el objetivo de normalizar aquellos textos informales donde los símbolos diacríticos son usado con una menor frecuencia, en particular los acentos en el caso del español. Por ejemplo, es común encontrar la palabra México escrita como Mexico.
El siguiente código muestra un procedimiento para eliminar los símbolos diacríticos.
= 'México'
text = ""
output for x in unicodedata.normalize('NFD', text):
= ord(x)
o if 0x300 <= o and o <= 0x036F:
continue
+= x
output output
'Mexico'
2.3 Normalización Semántica
Las siguientes normalizaciones comparten el objetivo con las normalizaciones presentadas hasta este momento, el cual es la reducción del vocabulario; la diferencia es que las siguientes utilizan el significado o uso de la palabra.
2.3.1 Palabras Comunes
Las palabras comunes (stop words) son palabras utilizadas frecuentemente en el lenguaje, las cuales son necesarias para comunicación, pero no aportan información para discriminar un texto de acuerdo a su significado.
The stop words are the most frequent words used in the language. These words are essential to communicate but are not so much on tasks where the aim is to discriminate texts according to their meaning.
Las palabras vacías se pueden guardar en un diccionario y el proceso de identificación consiste en buscar la existencia de la palabra en el diccionario. Una vez que la palabra analizada se encuentra en el diccionario, se procede a quitarla o cambiarla por un token particular. El proceso de borrado se muestra en el siguiente código.
= LangDependency('spanish')
lang
= '¡Buenos días! El día de hoy tendremos un día cálido.'
text = []
output for word in text.split():
if word.lower() in lang.stopwords[len(word)]:
continue
output.append(word)= " ".join(output)
output output
'¡Buenos días! día hoy día cálido.'
2.3.2 Lematización y Reducción a la Raíz
La idea de lematización y reducción a la raíz (stemming) es transformar una palabra a su raíz mediante un proceso heurístico o morfológico. Por ejemplo, las palabras jugando o jugaron se transforman a la palabra jugar.
El siguiente código muestra el proceso de reducción a la raíz utilizando la clase SnowballStemmer
.
= SnowballStemmer('spanish')
stemmer
= 'Estoy jugando futbol con mis amigos'
text = []
output for word in text.split():
= stemmer.stem(word)
w
output.append(w)= " ".join(output)
output output
'estoy jug futbol con mis amig'
2.4 Segmentación
Una vez que el texto ha sido normalizado es necesario segmentarlo (tokenize) a sus componentes fundamentales, e.g., palabras o gramas de caracteres (q-grams) o de palabras (n-grams). Existen diferentes métodos para segmentar un texto, probablemente una de las más sencillas es asumir que una palabra está limitada entre dos espacios o signos de puntuación. Partiendo de las palabras encontradas se empiezan a generar los gramas de palabras, e.g., bigramas, o los gramas de caracteres si se desea solo generarlos a partir de las palabras.
2.4.1 Gramas de Palabras (n-grams)
El primer método de segmentación revisado es la creación de los gramas de palabras. El primer paso es encontrar las palabras las cuales se pueden encontrar mediante la función split
; una vez que las palabras están definidas estás se pueden unir para generar los gramas de palabras del tamaño deseado, tal y como se muestra en el siguiente código.
= 'Estoy jugando futbol con mis amigos'
text = text.split()
words = 3
n = []
n_grams for a in zip(*[words[i:] for i in range(n)]):
"~".join(a))
n_grams.append( n_grams
['Estoy~jugando~futbol',
'jugando~futbol~con',
'futbol~con~mis',
'con~mis~amigos']
2.4.2 Gramas de Caracteres (q-grams)
La segmentación de gramas de caracteres complementa los gramas de palabras. Los gramas de caracteres están definidos como la subcadena de longitud \(q\). Este tipo de segmentación tiene la característica de que es agnóstica al lenguaje, es decir, se puede aplicar en cualquier idioma; contrastando, los gramas de palabras se pueden aplicar solo a los lenguajes que tienen definido el concepto de palabra, por ejemplo en el idioma chino las palabras no se pueden identificar como se pueden identificar en el español o inglés. La segunda característica importante es que ayuda en el problema de errores ortográficos, siguiendo una perspectiva de similitud aproximada.
El código para realizar los gramas de caracteres es similar a la presentada anteriormente, siendo la diferencia que el ciclo está por los caracteres en lugar de la palabras como se había realizado. El siguiente código muestra una implementación para realizar gramas de caracteres.
= 'Estoy jugando'
text = 4
q = []
q_grams for a in zip(*[text[i:] for i in range(q)]):
"".join(a))
q_grams.append( q_grams
['Esto',
'stoy',
'toy ',
'oy j',
'y ju',
' jug',
'juga',
'ugan',
'gand',
'ando']
2.5 TextModel
Habiendo descrito diferentes tipos de normalización (sintáctica y semántica) y el proceso de segmentación es momento para describir la librería B4MSA (Tellez, Miranda-Jiménez, Graff, Moctezuma, Suárez, et al. (2017)) que implementa estos procedimientos; específicamente, el punto de acceso de estos procedimientos corresponde a la clase TextModel
. El método TextModel.text_transformations
es el que realiza todos los métodos de normalización (Sección 2.2 y Sección 2.3) y el método TextModel.tokenize
es el encargado de realizar la segmentación (Sección 2.4) siguiendo el flujo mostrado en la Figura 2.1.
2.5.1 Normalizaciones
El primer conjunto de parámetros que se describen son los que corresponden a las entidades (Sección 2.2.1). Estos parámetros tiene tres opciones, borrar (OPTION_DELETE
), remplazar (OPTION_GROUP
) o ignorar. Los nombres de los parámetros son:
- usr_option
- url_option
- num_option
que corresponden al procesamiento de usuarios, URL y números respectivamente. Adicionalmente, TextModel
trata los emojis, hashtags y nombres, mediante los siguientes parámetros:
- emo_option
- hashtag_option
- ent_option
Por ejemplo, el siguiente código muestra como se borra el usuario y se reemplaza un hashtag; se puede observar que en la respuesta se cambian todos los espacios por el caracter ~
y se incluye ese mismo al inicio y final del texto.
= TextModel(hashtag_option=OPTION_GROUP,
tm =OPTION_DELETE)
usr_option= 'mira @xyz estoy triste. #UnDiaLluvioso'
texto tm.text_transformations(texto)
'~mira~estoy~triste.~_htag~'
Siguiendo con las transformaciones sintácticas, toca el tiempo a describir aquellas que relacionadas a la ortografía (Sección 2.2.2) las cuales corresponden a la conversión a minúsculas, borrado de signos de puntuación y símbolos diacríticos. Estas normalizaciones se activan con los siguiente parámetros.
- lc
- del_punc
- del_diac
En el siguiente ejemplo se transforman el texto a minúscula y se remueven los signos de puntuación.
= TextModel(lc=True,
tm =True,
del_punc=False)
del_diac= 'Hoy está despejado.'
texto tm.text_transformations(texto)
'~hoy~está~despejado~'
Las normalizaciones semánticas (Sección 2.3) que se tienen implementadas en la librería corresponden al borrado de palabras comunes y reducción a la raíz; estás se pueden activar con los siguientes parámetros.
- stopwords
- stemming
Por ejemplo, las siguientes instrucciones quitan las palabras comunes y realizan una reducción a la raíz.
= TextModel(lang='es',
tm =OPTION_DELETE,
stopwords=True)
stemming= 'el clima es perfecto'
texto tm.text_transformations(texto)
'~clim~perfect~'
2.5.2 Segmentación
El paso final es describir el uso de la segmentación. La librería utiliza el parámetro token_list
para indicar el tipo de segmentación que se desea realizar. El formato es una lista de número, donde el valor indica el tipo de segmentación. El número \(1\) indica que se realizará una segmentación por palabras, los número positivo corresponden a los gramas de caracteres y los números negativos a los gramas de palabras.
Por ejemplo, utilizando las normalizaciones que se tienen por defecto, el siguiente código segmenta utilizando gramas de caracteres de tamañan \(4.\)
= TextModel(token_list=[4])
tm 'buenos días') tm.tokenize(
['q:~bue',
'q:buen',
'q:ueno',
'q:enos',
'q:nos~',
'q:os~d',
'q:s~di',
'q:~dia',
'q:dias',
'q:ias~']
para poder identificar cuando se trata de un segmento que corresponde a una palabra o un grama de caracteres, a los últimos se les agrega el prefijo q:
. Cabe mencionar que por defecto se remueven los símbolos diacríticos.
El ejemplo anterior, se utiliza para generar un grama de palabras de tamaño \(2.\) Como se ha mencionado los gramas de palabras se especifican con números negativos siendo el valor absoluto el tamaño del grama.
= TextModel(token_list=[-2])
tm 'buenos días') tm.tokenize(
['buenos~dias']
Para completar la explicación, se combinan la segmentación de gramas de caracteres y palabras además de incluir las palabras en la segmentación.
= TextModel(token_list=[4, -2, -1])
tm 'buenos días') tm.tokenize(
['buenos~dias',
'buenos',
'dias',
'q:~bue',
'q:buen',
'q:ueno',
'q:enos',
'q:nos~',
'q:os~d',
'q:s~di',
'q:~dia',
'q:dias',
'q:ias~']