Abrir en Google Colab
|
Descargar notebook
|
Normalización de texto
Introducción
Cuando hablamos de entrenar un modelo de aprendizaje automático, en general ocupamos una porción de nuestro tiempo en preprocesar los datos para generar representaciones útiles y deshacernos de problemas especificos que podría exhibir nuestro conjunto de datos. En particular, para el procesamiento del lenguaje natural, sabemos que debemos representar nuestras palabras de forma vectorial utilizando un vocabulario. También sabemos que el tamaño del vocabulario es algo que deseamos manejar.
Tipicamente, las siguientes técnicas se aplican para procesar el texto:
Para ejecutar este notebook
Para ejecutar este notebook, instale las siguientes librerias:
[ ]:
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/docs/nlp/preprocessing/Normalization.txt --quiet --no-clobber
!pip install -r Normalization.txt
!pip install unidecode spaCy
Normalización (canonización)
La normalización (canonización) de texto hace referencia al proceso por el cual transformamos el texto en una única forma canónica común. Normalizar el texto antes de almacenarlo o procesarlo permite liberarnos de preocupaciones posteriores, ya que se garantiza que la entrada sea consistente antes de que se realicen operaciones sobre el mismo. La normalización del texto, sin embargo, requiere saber qué tipo de texto se está normalizando y cómo se procesará posteriormente. Por lo tanto, no existe un procedimiento de normalización universal.
A pesar de no existir un proceso univeral, algunas técnicas si son comunes, como por ejemplo eliminar caracteres no alfanuméricos o marcas diacríticas (acentos, dieresis) y sustitución de mayusculas por minúsculas. Otras tareas podrían ser más específicas como ser el tratamiento de direcciones URL o incluso algunas combinaciones de caracteres como ser los emojis, los hashtags, etc.
Implementación
Para aquellas tareas sencillas, podemos utilizar algunas funciones pre-existentes. Para tareas más especificas, la utilización de expresiones regulares pueden ser de gran utilidad. Las expresiones regulares nos permiten buscar patrones específicos dentro de los textos. Veamos algunas trasnformaciones de texto.
Los siguientes ejemplos utilizan tweets reales extraidos del conjunto de datos Spanish Corpus of Tweets for Marketing
[2]:
sample = "Ecologistas en Acción valora positivamente la decisión de Carrefour España de dejar de vender panga… https://t.co/16RuHAeNhY"
print(sample)
Ecologistas en Acción valora positivamente la decisión de Carrefour España de dejar de vender panga… https://t.co/16RuHAeNhY
Convertir el texto en minusculas
[3]:
sample = sample.lower()
print(sample)
ecologistas en acción valora positivamente la decisión de carrefour españa de dejar de vender panga… https://t.co/16ruhaenhy
Marcas diacríticas
[4]:
import unidecode
sample = unidecode.unidecode(sample)
print(sample)
ecologistas en accion valora positivamente la decision de carrefour espana de dejar de vender panga... https://t.co/16ruhaenhy
Eliminación de caracteres especiales
[5]:
import re
charsToKepp = r'[^a-zA-Z0-9\s]'
sample = re.sub(charsToKepp, '', sample)
print(sample)
ecologistas en accion valora positivamente la decision de carrefour espana de dejar de vender panga httpstco16ruhaenhy
Es interesante revisar el ejemplo anterior, dado que el efecto que obtuvo eliminar los caracteres especiales no fué el más indicado. En este caso, quisieramos eliminar las URLs por completo en lugar de solamente los caracteres especiales que están dentro de ellas.
En general, deberiamos invertir el orden de la celda anterior con la celda siguiente (primero eliminar las URLs y luego los caracteres especiales.
[34]:
import re
urls_regex = re.compile(r'http\S+')
sample = [token for token in sample.split(' ') if not re.match(urls_regex, token)]
print(' '.join(sample))
Vaya estafa de Mercadona. Voy y compro salsa de soja, curry, comino y sal sería buena idea. #Kiev
Como se puede ver, el procesamiento del texto a realizar dependerá mucho del contexto.
Stemming y Lemmatization
Existen palabras cuyo significado no cambia ya que estan atados a una palabra raiz que les da el significado:
Organizan, organiza, organizando, organizaron
Stemming y Lemmatization son dos técnicas que generan la palabra raiz dada una palabra. La diferencia que hay entre estas técnicas es que Lemmatization utiliza reglas del lenguaje para extraer las palabras raiz y por lo tanto, el resultado son palabras que existen en el vocabulario. Por el contrario, Stemming utiliza heuristicas que truncan la palabra hasta su raiz invariable. El resultado son «psudopalabras» o mejor conocidos como tokens que no forman una palabra del lenguaje propiamente dicho. Esta técnica, como se puede intuir, es más rápida computacionalmente.
Stemming
Stemming (o en español derivación) es el proceso en el que estandarizamos las formas de las palabras a su raíz base independientemente de las inflexiones o cojugación en la que se encuentre.
Para demostrar esta técnica utilizaremos la popular libreria de NLP nltk:
[7]:
from nltk import stem
stemmer = stem.SnowballStemmer(language='spanish')
[8]:
words = ['amigos', 'amigo', 'amiga', 'amistad' ]
[9]:
[stemmer.stem(word) for word in words]
[9]:
['amig', 'amig', 'amig', 'amist']
Lemmatization
El proceso de lemmatization es similar al de stemming salvo que al no utilizar reglas del lenguaje para extraer las palabras raiz. Como consecuencia, el resultado es el vocablo raiz propiamente dicho.
Para aplicar esta técnica utilizaremos la librería spaCy.
Sobre la libreria spaCy: Spacy es una libreria para NLP muy polupar actualmente ya que, al contrario de nltk, ofrece formas muy eficientes de hacer solo algunos tipos de operaciones. NLTK es una herramienta más general. Para instalar spaCy en español necesitaran ejecutar:
conda install -c spacy spacy
python -m spacy download es_core_news_sm
Si bien
ntlkofrece la opción de hacer Lemmatization, su soporte mayoritariamente es para ingles. La versión en español no es demasiado buena. Si les interesa probarla puede hacerlo a traves del metodo.
nltk.wordnet.lemas("palabra", lang='spa')
Cargamos el modelo en español e instanciamos el parser:
[10]:
!python -m spacy download es_core_news_sm
Collecting es-core-news-sm==3.8.0
Downloading https://github.com/explosion/spacy-models/releases/download/es_core_news_sm-3.8.0/es_core_news_sm-3.8.0-py3-none-any.whl (12.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.9/12.9 MB 97.6 MB/s eta 0:00:00
Installing collected packages: es-core-news-sm
Successfully installed es-core-news-sm-3.8.0
✔ Download and installation successful
You can now load the package via spacy.load('es_core_news_sm')
⚠ Restart to reload dependencies
If you are in a Jupyter or Colab notebook, you may need to restart Python in
order to load all the package's dependencies. You can do this by selecting the
'Restart kernel' or 'Restart runtime' option.
[11]:
import es_core_news_sm as spa
parser = spa.load()
Creamos una funcion que nos ayuden a simplificar el uso de este método:
[12]:
lemmatizer = lambda word : " ".join([token.lemma_ for token in parser(word)])
[13]:
words = ['amigos', 'amigo', 'amiga', 'amistad' ]
[14]:
[lemmatizer(word) for word in words]
[14]:
['amigo', 'amigo', 'amiga', 'amistad']
Nota: La precisión de Lemmatization depende de la implementación. La de español no es demasiado buena. Notar también lo que sucede con la palabra «amigo»: ¿Es el verbo amigar o el sustantivo amigo?
Adicionalmente, spaCy procesa el texto tokenizándolo en tokens y enriqueciendolos con anotaciones.
[15]:
words_tagged = parser(' '.join(words))
[16]:
for t in words_tagged:
print(t.text+'/'+t.lemma_ + '/'+ t.pos_)
amigos/amigo/NOUN
amigo/amigo/ADJ
amiga/amigo/ADJ
amistad/amistad/NOUN
Eliminación de stopwords
Algunas palabras que son extremadamente frecuentes, «a-priori» (revisaremos este concepto luego) no son de mucha utilidad para resolver una tarea de clasificación de texto específica. Estas palabras se las conoce como Stop words y, dado que son de poca utilidad, son eliminadas del texto.
Spoiler Alert: Mencionamos “a priori”, porque la tendencia general en los ultimos tiempos ha sido ir desde grandes listas de stop words en el order de 200-300 a listas muy pequeñas (10-15 - si es que las hay). Los buscadores, por ejemplo, hoy en día no eliminan estas palabras. Cuando veamos modelos de lenguaje, en realidad las vamos a necesitar.
Una de las formas más sencillas de eliminar estas palabras es utilizando la libreria nltk de la siguiente forma:
[17]:
import nltk
from nltk.corpus import stopwords
[18]:
nltk.download('stopwords', quiet=True)
[18]:
True
[19]:
spa_stopwords = stopwords.words('spanish')
Revisemos como lucen estas palabras:
[20]:
spa_stopwords[:10]
[20]:
['de', 'la', 'que', 'el', 'en', 'y', 'a', 'los', 'del', 'se']
Implementación
Podemos implementar facilmente una rútina que elimine estas palabras de un texto de la siguiente forma:
[21]:
sample = "ecologistas en accion valora positivamente la decision de carrefour espana de dejar de vender panga"
print('Antes:', sample)
sample = ' '.join([token for token in sample.split(' ') if token not in spa_stopwords])
print('Despues:', sample)
Antes: ecologistas en accion valora positivamente la decision de carrefour espana de dejar de vender panga
Despues: ecologistas accion valora positivamente decision carrefour espana dejar vender panga
Tokenización
Se refiere al proceso de generación de tokens basado en un texto. A alto nivel, se podría ver como la tarea de dividir oraciones en palabras. Un token se diferencia de una palabra en el hecho de que una palabra es una instancia de un token. Existen varias técnicas para separar una oración o texto en general en tokens:
Lectura recomendada: Diferentes tokenizers disponibles en nltk
Tomemos un tweet de ejemplo:
[22]:
sample = ". @PoliciadeBurgos @PCivilBurgos @Aytoburgos Mismo peligro c/ Rio Viejo junto Mercadona Villimar"
print(sample)
. @PoliciadeBurgos @PCivilBurgos @Aytoburgos Mismo peligro c/ Rio Viejo junto Mercadona Villimar
Instanciaremos un tokenizer del tipo TreebankWordTokenizer, uno de los más genéricos:
[23]:
from nltk.tokenize.treebank import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
[24]:
tokenizer.tokenize(sample)
[24]:
['.',
'@',
'PoliciadeBurgos',
'@',
'PCivilBurgos',
'@',
'Aytoburgos',
'Mismo',
'peligro',
'c/',
'Rio',
'Viejo',
'junto',
'Mercadona',
'Villimar']
Intentemos ahora con un tokenizer un poco más específico para procesar tweets:
[25]:
from nltk.tokenize.casual import TweetTokenizer
tokenizer = TweetTokenizer()
[26]:
tokenizer.tokenize(sample)
[26]:
['.',
'@PoliciadeBurgos',
'@PCivilBurgos',
'@Aytoburgos',
'Mismo',
'peligro',
'c',
'/',
'Rio',
'Viejo',
'junto',
'Mercadona',
'Villimar']
Notar como el tratamiento del arroba resulta distinto dependiendo del
tokenizerque estamos utilizando.
Otra estrategia
El problema de reducir las palabras a sus formatos raiz radica en que en general cada palabra (separada por espacios, puntos, etc) conforma un elemento en nuestro vocabulario y no queremos diferentes elementos de nuestro vocabulario que mapeen al mismo elemento o concepto. Si por el contrario utilizaramos otra estrategia para determinar nuestro vocabulario (o mejor dicho, cada elemento de nuestro vocabulario) entonces este problema quizás no existiría (o se volvería peor).
Este tipo de técnicas por lo general intentan representar el vocabulario con «sub-palabras» o partes de las palabras como unidad. Un ejemplo de esto es SentencePiec or Byte pair encoding (BPE).
El siguiente ejemplo muetra el caso de BPE, el cual es utilizado por modelos como GPT. Veremos estos encodings más adelante.
[27]:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
[28]:
[enc.decode([t]) for t in enc.encode(". @PoliciadeBurgos @PCivilBurgos @Aytoburgos Mismo peligro c/ Rio Viejo junto Mercadona Villimar")]
[28]:
['.',
' @',
'Pol',
'ici',
'ade',
'B',
'urg',
'os',
' @',
'PC',
'ivil',
'B',
'urg',
'os',
' @',
'A',
'yt',
'ob',
'urg',
'os',
' M',
'ismo',
' pel',
'ig',
'ro',
' c',
'/',
' Rio',
' Vie',
'jo',
' junto',
' Merc',
'ad',
'ona',
' Vill',
'imar']
Creando una rutina de preparación del texto
Idealmente, podemos empaquetar todos los pasos relevantes del preprocesamiento de texto en una rutina coherente y consolidada. Esto es importante no solo por cuestiones de practicidad, sino que también es relevante dado que en todos estos pasos el orden en el que se ejecutan importa. Una ejecución en un orden distinto al que se pensó o diseño podría lugar a perdida de información. Por ejemplo, ¿que pasaría si quisieramos procesar los hashtags de tweets de alguna manera si eliminaramos los caracteres especiales al principio?
Una rutina podría ser la siguiente:
[31]:
import unidecode
import spacy
import es_core_news_sm as spa
import re
from nltk import stem
from nltk.corpus import stopwords
from nltk.tokenize.casual import TweetTokenizer
nltk.download('stopwords', quiet=True)
parser = spa.load() # Cargamos el parser en español
tokenizer = TweetTokenizer(strip_handles=True, reduce_len=True) # Creamos un tokenizer
stemmer = stem.SnowballStemmer(language='spanish') # Creamos un steammer
lemmatizer = lambda word : " ".join([token.lemma_ for token in parser(word)]) # Creamos un lemmatizer
stopwords = set(stopwords.words('spanish')) # Instanciamos las stopwords en español
urls_regex = re.compile(r'http\S+') # Usamos una expresion regular para encontrar las URLs
def normalize(text):
tokens = tokenizer.tokenize(text.lower()) # Tokenizamos el texto
tokens = [token for token in tokens if not re.match(urls_regex, token)] # Eliminamos URLs
tokens = [token for token in tokens if len(token) > 4] # Eliminamos palabras con menos de 4 letras
tokens = [token for token in tokens if token not in stopwords] # Eliminamos stopwords
tokens = [unidecode.unidecode(token) for token in tokens] # Quitamos acentos
tokens = [lemmatizer(token) for token in tokens] # Aplicamos lematization
return tokens
Luego podemos aplicar esta rutina facilmente a nuevo texto:
[32]:
sample = "Vaya estafa de Mercadona. Voy y compro salsa de soja, curry, comino y sal sería buena idea. #Kiev https://t.co/Wej37UxCAs"
[33]:
normalize(sample)
[33]:
['estafa',
'mercadonar',
'compro',
'salsar',
'curry',
'comino',
'buen',
'# kiev']
Nota: En el futuro - en este curso - utilizaremos esta rutina de preprocesamiento de texto empaquedata en una transformación de Scikit-Learn. Los conceptos que utilizaremos serán los mismos que se utilizon aquí pero nos ahorraremos tener que escribir este código de preprocesamiento todo el tiempo. En la práctica, las rutinas de preprocesamiento de texto suelen ser tediosas y por lo tanto se las trata de escribir una sola vez y luego empaquetarlas de alguna forma para reutilizarlas una y otra vez.
Abrir en Google Colab
Descargar notebook