Normalizar y representar
Páginas de este taller
3. Normalizar: cuándo son iguales dos palabras
Normalizar es decidir qué diferencias se ignoran: mayúsculas, tildes, flexión, palabras gramaticales. Cada decisión borra información, y lo que sobra en una tarea es justo lo que hace falta en otra.
Raíces y lemas
Un stemmer recorta terminaciones con reglas. Un lematizador busca la forma de diccionario con un modelo entrenado. Los comparamos con un proverbio de Machado:
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer("spanish")
for t in nlp(proverbio):
print(t.text, stemmer.stem(t.text), t.lemma_)
Salida real
Raíz frente a lema en un proverbio de Machado
«Caminante, son tus huellas / el camino, y nada más; / caminante, no hay camino, / se hace camino al andar.»
| Forma | Raíz (Snowball) | Lema (spaCy) |
|---|---|---|
| Caminante | camin | Caminante (nombre propio) |
| son | son | ser |
| huellas | huell | huella |
| camino | camin | camino |
| nada | nad | nada |
| caminante | camin | caminantar (verbo) |
| hay | hay | haber |
| hace | hac | hacer |
| andar | andar | andar |
La idea: El stemmer junta «caminante» y «camino» en «camin». El lematizador distingue mejor, pero también se equivoca cuando el texto se aleja de la prensa con la que se entrenó.
El lematizador acierta son → ser y hay → haber, que el stemmer deja igual. Pero el etiquetador de spaCy toma el primer Caminante por nombre propio, guiado por la mayúscula, y el segundo por verbo; el lematizador, que aplica reglas según la categoría, le quita entonces una terminación verbal e inventa caminantar. El etiquetador se entrenó con prensa (el corpus AnCora), y la poesía queda lejos. Es el mismo problema que vimos en Tu primer análisis lingüístico con spaCy.
Palabras vacías
Las palabras vacías (stopwords) son las gramaticales que se suelen quitar para quedarse con el contenido. La lista de NLTK para el español tiene 313.
Para clasificar noticias por tema, quitar no puede dar igual. Para analizar sentimiento o estilo es un error grave, y en la tercera parte veremos que estas palabras son justo las que delatan a un autor. La propia documentación de scikit-learn lo advierte: no hay una lista válida para todo, y las palabras vacías pueden ser útiles para clasificar por estilo.
La ortografía de la edición
Hay un detalle que se ve contando:
| Forma | Machado (ed. 1917) | Bécquer (Obras escogidas) |
|---|---|---|
| á (preposición con tilde) | 0 | 105 |
| ó (conjunción con tilde) | 0 | 8 |
| Mayúscula a principio de verso | No | Sí |
La edición de Bécquer sigue normas ortográficas antiguas y pone mayúscula al principio de cada verso. Para un lector es un rasgo de la edición; para un modelo, es una pista como cualquier otra. El cuaderno define una función normalizar que pasa todo a minúsculas, moderniza esas formas y quita los guiones bajos con los que Gutenberg marca la cursiva:
def normalizar(texto):
texto = texto.lower().replace("_", "")
for antigua, actual in {"á": "a", "ó": "o", "é": "e", "fué": "fue"}.items():
texto = re.sub(rf"\b{antigua}\b", actual, texto) # solo palabras sueltas
return texto
4. Representar: del texto a los números
Bolsa de palabras
En la codificación one-hot, cada palabra del vocabulario es una columna: la palabra tiene un 1 en la suya y 0 en todas las demás. Con 3766 palabras distintas en nuestros fragmentos, son vectores de 3766 posiciones con un solo 1. Crecen en proporción al vocabulario, y todas las palabras quedan a la misma distancia: rosa está tan lejos de violeta como de tierra.
La bolsa de palabras (bag of words, BoW) suma los vectores de un texto: cuenta cuántas veces aparece cada palabra y olvida el orden.
from sklearn.feature_extraction.text import CountVectorizer
frases = ["Una rosa es roja, una violeta es azul",
"Mi amor es rojo, como una rosa roja"]
CountVectorizer().fit_transform(frases)
| amor | azul | como | es | mi | roja | rojo | rosa | una | violeta | |
|---|---|---|---|---|---|---|---|---|---|---|
| frase 1 | 0 | 1 | 0 | 2 | 0 | 1 | 0 | 1 | 2 | 1 |
| frase 2 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
roja y rojo son columnas distintas: sin lematizar, la bolsa no sabe que son la misma palabra.
TF-IDF
TF-IDF da a cada palabra un peso: sube si aparece mucho en el fragmento (TF, term frequency) y baja si aparece en muchos fragmentos (IDF, inverse document frequency). La fórmula clásica del IDF es el logaritmo (aquí, natural) del número de documentos dividido entre los documentos que contienen la palabra:
| Palabra | Aparece en | IDF clásico | IDF de scikit-learn |
|---|---|---|---|
| que | 322 de 678 fragmentos | 0,74 | 1,74 |
| tierra | 34 | 2,99 | 3,97 |
| golondrinas | 3 | 5,42 | 6,13 |
El orden es el mismo, pero las cifras no coinciden: scikit-learn usa por defecto una versión suavizada, ln((1 + N) / (1 + df)) + 1, y además normaliza cada vector. Si haces el cálculo a mano para comprobarlo, tenlo en cuenta.
Embeddings
Un embedding es un vector denso, de 300 dimensiones en el modelo mediano de spaCy, aprendido a partir de qué palabras aparecen cerca de cuáles (cómo se construyen). Las palabras que se usan en contextos parecidos quedan cerca:
nlp("rosa").similarity(nlp("violeta")) # 0.70
nlp("rosa").similarity(nlp("tierra")) # 0.16
nlp("noche").similarity(nlp("tarde")) # 0.66
nlp("golondrina").similarity(nlp("pájaro")) # 1.00
El último resultado es una trampa útil. golondrina y pájaro tienen exactamente el mismo vector: para ahorrar espacio, el modelo mediano guarda 20 000 vectores distintos para 500 000 palabras, y a las que se quedan sin vector propio les asigna el de la más parecida que lo conserva. Conviene comprobar estas cosas antes de fiarse de una cifra.
Dos cautelas más. Las dimensiones de un embedding no significan nada por separado: lo que tiene sentido son las distancias. Y el famoso rey − hombre + mujer = reina solo sale si se excluyen del resultado las palabras de partida; sin ese filtro, la palabra más cercana suele ser la propia rey. Estos vectores, además, son estáticos: banco tiene el mismo en «me senté en el banco» y en «abrí una cuenta en el banco».