Saltar a contenido

Preprocesamiento de texto

1 Por qué preprocesar el texto

El texto crudo es ruidoso: URLs, menciones de usuario, signos de puntuación, mayúsculas y minúsculas inconsistentes, palabras vacías que no aportan significado. Sin un paso de limpieza, cualquier representación numérica (BoW, TF-IDF) will contain noise que degrada el rendimiento del modelo.

Nota

Analogía con datos tabulares. El preprocesamiento de texto es equivalente a la codificación de categóricas, el escalado y el tratamiento de faltantes en datos numéricos: es el paso que convierte datos “crudos” en datos “limpios” listos para el modelo.

2 Limpieza básica

Antes de cualquier técnica sofisticada, el texto necesita una limpieza mecánica:

2.1 Eliminación de URLs y menciones

En tweets y otros textos de redes sociales, las URLs y menciones (@usuario) rara vez aportan información para la tarea:

import re

def limpiar_urls_menciones(texto):
    texto = re.sub(r'http\S+|www\.\S+', '', texto)   # URLs
    texto = re.sub(r'@\w+', '', texto)                 # menciones
    return texto.strip()

tweet = "Mira este artículo https://t.co/abc123 @usuario genial"
print(limpiar_urls_menciones(tweet))
# → "Mira este artículo  genial"

2.2 Conversión a minúsculas

Garantiza que “Feliz”, “feliz” y “FELIZ” se traten como la misma palabra:

texto = "Me ENCANTA este producto"
texto.lower()
# → "me encanta este producto"

2.3 Eliminación de puntuación y caracteres especiales

Dependiendo de la tarea, la puntuación puede ser ruido o información. En análisis de sentimientos, por ejemplo, los signos de exclamación (!) pueden ser útiles, pero los caracteres especiales como #, $, % suelen eliminarse:

def limpiar_puntuacion(texto):
    texto = re.sub(r'[^\w\s]', '', texto)  # quita todo lo que no sea letra/número/espacio
    texto = re.sub(r'\d+', '', texto)       # quita números
    return texto.strip()

print(limpiar_puntuacion("¡Hola! #ML es genial... ¿3 veces?"))
# → "Hola  ML es genial  veces"

Tip

¿Cuándo conservar la puntuación? En detección de spam o análisis de sentimientos intensos, los signos de exclamación y mayúsculas repitadas (“¡¡¡EXCELENTE!!!”) son señales informativas. Evalúa siempre el impacto en tu tarea específica.

3 Stop words (palabras vacías)

Las stop words son palabras extremadamente comunes que aparecen en casi todos los documentos y por sí solas no discriminan entre clases: “el”, “la”, “de”, “y”, “en”, “a”, “que”…

import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords

stop_es = set(stopwords.words('spanish'))
print(stop_es)
# {'de', 'la', 'el', 'en', 'y', 'a', 'que', 'los', 'del', 'las', ...}

def quitar_stopwords(texto):
    tokens = texto.lower().split()
    return [t for t in tokens if t not in stop_es]

frase = "el gato está sentado en la alfombra"
print(quitar_stopwords(frase))
# → ['gato', 'sentado', 'alfombra']

Advertencia

Cuidado con la negación. En análisis de sentimientos, eliminar “no” como stop word puede cambiar el sentido de la frase: “no me gusta” → “me gusta”. En estos casos, se suele conservar la negación o tratarla como un token especial.

4 Stemming (derivación regresiva)

El stemming reduce las palabras a su raíz eliminando sufijos. Es un proceso puramente heurístico: no garantiza que la raíz sea una palabra real.

from nltk.stem import PorterStemmer, SnowballStemmer

# Porter (agresivo, rápido)
porter = PorterStemmer()
print(porter.stem("corriendo"))   # → "corriendo" (no cambia en español)
print(porter.stem("corredor"))    # → "corredor"
print(porter.stem("connection"))  # → "connect" (inglés)

# Snowball (Porter2, más preciso, soporta español)
snowball = SnowballStemmer("spanish")
print(snowball.stem("corriendo"))  # → "corriendo"
print(snowball.stem("corredor"))   # → "corredor"
print(snowball.stem("correr"))     # → "correr"

Los algoritmos más comunes:

Algoritmo Idiomas Características
Porter Inglés Rápido, agresivo, puede sobre-stemming
Snowball (Porter2) Multiidioma Más preciso, mejor equilibrio
Lancaster Inglés Muy agresivo, reduce mucho
# Ejemplo: cómo stemming agrupa palabras relacionadas
palabras = ["running", "runner", "runs", "ran", "run"]
print([porter.stem(p) for p in palabras])
# → ['run', 'runner', 'run', 'ran', 'run']

# En español con Snowball
palabras_es = ["corriendo", "corredor", "corremos", "correr"]
print([snowball.stem(p) for p in palabras_es])
# → ['corriendo', 'corredor', 'corremos', 'correr']

5 Lematización

La lematización reduce las palabras a su forma base o lema, pero usando análisis lingüístico (no solo heurística). El resultado siempre es una palabra real del diccionario.

import nltk
nltk.download('wordnet')
from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()

# Inglés
print(lemmatizer.lemmatize("running", pos='v'))  # → "run"
print(lemmatizer.lemmatize("better", pos='a'))    # → "good"
print(lemmatizer.lemmatize("geese"))              # → "goose"

# En español se puede usar spaCy
# import spacy
# nlp = spacy.load("es_core_news_sm")
# doc = nlp("Los gatos están corriendo")
# print([token.lemma_ for token in doc])
# → ['el', 'gato', 'estar', 'correr']

5.1 Stemming vs. lematización

Característica Stemming Lematización
Método Heurístico (recortar sufijos) Análisis lingüístico
Resultado Puede no ser palabra real Siempre es palabra real
Velocidad Más rápido Más lento
Precisión Menor (sobre-stemming) Mayor
Ejemplo “corriendo” → “corriendo” “corriendo” → “correr”

Nota

¿Cuándo usar cada una? Para conjuntos de datos grandes y tareas donde la velocidad importa (clasificación de spam), stemming suele ser suficiente. Para tareas donde la precisión lingüística es crítica (traducción, resumen), lematización es preferible.

6 Corrección ortográfica mediante distancia de edición

Un autocorrector detecta palabras posiblemente mal escritas y propone la corrección más probable. Por ejemplo, en la frase:

I have a beutiful cat

podría identificar beutiful como una palabra sospechosa y sugerir beautiful.

Este enfoque corrige principalmente errores en palabras que no aparecen en el diccionario. No detecta necesariamente errores contextuales en palabras válidas. Por ejemplo, their y there existen en inglés, pero solo una será correcta según el contexto.

6.1 Paso 1: detectar la palabra sospechosa

El método más sencillo consiste en comparar cada palabra con un diccionario, es decir, una lista de palabras conocidas y consideradas correctas.

  • Si la palabra aparece en el diccionario, se considera ortográficamente válida.
  • Si no aparece, se marca como sospechosa y se intenta encontrar una corrección.

Por ejemplo, beutiful probablemente no aparecerá en el diccionario, mientras que beautiful sí.

Este método no determina por sí solo si una palabra tiene sentido dentro de la oración. Una palabra puede estar bien escrita y, aun así, ser incorrecta en ese contexto.

6.2 Paso 2: generar palabras candidatas

Para encontrar posibles correcciones se utiliza la distancia de edición, que mide cuántas operaciones mínimas hacen falta para transformar una cadena en otra.

Las operaciones básicas son:

Operación Descripción Ejemplo
Inserción Añadir una letra plan → plane
Eliminación Quitar una letra plane → plan
Sustitución Cambiar una letra por otra cold → cord
Transposición Intercambiar dos letras adyacentes form → from

En el caso de beutiful y beautiful, las letras u y t aparecen en un orden incorrecto. Si el modelo permite transposiciones, el error puede corregirse intercambiando esas dos letras:

beutiful → beautiful

El autocorrector genera palabras que están a poca distancia de la palabra sospechosa, normalmente a distancia 1 o 2.

Una forma sencilla de generar candidatos es aplicar pequeñas modificaciones a la palabra original:

  • insertar una letra;
  • eliminar una letra;
  • sustituir una letra;
  • intercambiar dos letras adyacentes.

Por ejemplo, a partir de recieve, una de las variantes generadas sería:

recieve → receive

Esta variante se obtiene intercambiando las letras i y e.

En una implementación básica, el sistema genera muchas variantes y después comprueba cuáles aparecen en el diccionario. En una implementación más eficiente, utiliza índices o estructuras de búsqueda para evitar comparar exhaustivamente todas las palabras posibles.

6.3 Paso 3: filtrar las candidatas

La lista generada puede contener muchas cadenas que no son palabras reales, como:

beutifal
beutifull
beutifuk

Por eso, se conservan únicamente las variantes que aparecen en el diccionario y que están suficientemente cerca de la palabra original.

Para una palabra sospechosa como recieve, podrían aparecer candidatas como:

receive
recipe
relieve

El conjunto de candidatas puede representarse así:

\[ C(o)= \left\{ w\in V: d(o,w)\leq k \right\} \]

donde:

  • \(o\) es la palabra observada;
  • \(V\) es el vocabulario o diccionario;
  • \(d(o,w)\) es la distancia de edición entre la palabra observada y una palabra del diccionario;
  • \(k\) es la distancia máxima permitida;
  • \(C(o)\) es el conjunto de candidatas para \(o\).

Por ejemplo, si \(o=\text{recieve}\) y \(k=2\), el sistema conservaría las palabras del diccionario cuya distancia respecto de recieve no superase 2.

La distancia no es el único criterio. Una palabra como computer puede estar en el diccionario, pero no debería ser candidata para corregir recieve porque está demasiado lejos y requeriría demasiadas operaciones.

6.4 Paso 4: elegir la corrección más probable

Una primera aproximación consiste en asignar a cada palabra una probabilidad basada en su frecuencia en un corpus grande:

\[ P(w)=\frac{f(w)}{N} \]

donde:

  • \(w\) es una palabra;
  • \(f(w)\) es el número de veces que aparece \(w\) en el corpus;
  • \(N\) es el número total de palabras del corpus.

Por ejemplo, en el corpus:

We learn from mistakes and we improve

hay \(N=8\) palabras si se distingue entre We y we:

We learn from mistakes and we improve
1  2     3    4        5   6  7  8

La palabra We aparece una vez y we aparece otra vez. Si no se distingue entre mayúsculas y minúsculas, ambas se consideran la misma palabra y we aparece dos veces:

\[ P(\text{we})=\frac{2}{8}=\frac{1}{4} \]

Las demás palabras aparecen una sola vez:

\[ P(\text{learn}) = P(\text{from}) = P(\text{mistakes}) = P(\text{and}) = P(\text{improve}) = \frac{1}{8} \]

En un corpus real, la frecuencia ayuda a decidir entre varias palabras candidatas. Sin embargo, un autocorrector práctico también considera la distancia entre la palabra escrita y cada candidata, además del contexto de la oración.

Sea:

  • \(o\) la palabra observada;
  • \(c\) una palabra candidata.

El objetivo es elegir la candidata con mayor probabilidad:

\[ \hat{c} = \underset{c\in C(o)}{\operatorname{arg\,max}} \;P(c\mid o) \]

Aquí, \(P(c\mid o)\) es la probabilidad de que \(c\) sea la palabra correcta dado que se observó \(o\).

Aplicando el teorema de Bayes:

\[ P(c\mid o) = \frac{P(o\mid c)\,P(c)}{P(o)} \]

donde:

  • \(P(c)\) es la probabilidad de que \(c\) aparezca en el idioma;
  • \(P(o\mid c)\) es la probabilidad de escribir \(o\) cuando la palabra correcta era \(c\);
  • \(P(o)\) es la probabilidad general de observar \(o\).

Como \(P(o)\) es igual para todas las candidatas, puede omitirse al compararlas:

\[ \hat{c} = \underset{c\in C(o)}{\operatorname{arg\,max}} \;P(o\mid c)\,P(c) \]

La probabilidad \(P(c)\) favorece las palabras frecuentes. Por su parte, \(P(o\mid c)\) favorece las palabras que pueden transformarse fácilmente en la palabra observada mediante errores habituales.

Por ejemplo, para recieve, el sistema puede comparar candidatas como receive, recipe y relieve. La elección final depende de varios factores:

\[ \text{corrección más probable} = \text{similitud ortográfica} + \text{frecuencia} + \text{contexto} \]

En términos probabilísticos:

\[ \hat{c} = \underset{c\in C(o)}{\operatorname{arg\,max}} \; P(o\mid c)\,P(c) \]

La distancia de edición puede ayudar a estimar \(P(o\mid c)\): cuanto más cerca esté \(o\) de \(c\), más probable será que \(o\) sea un error de escritura de \(c\). La frecuencia de la palabra se refleja en \(P(c)\), y el contexto puede incorporarse mediante un modelo de lenguaje.

6.5 Distancia mínima de edición

La distancia mínima de edición es el coste mínimo necesario para transformar una cadena en otra. Se utiliza en autocorrectores, búsquedas aproximadas, comparación de documentos, procesamiento del lenguaje natural y análisis de secuencias biológicas.

Para el siguiente ejemplo usaremos estos costes:

  • Inserción: \(1\)
  • Eliminación: \(1\)
  • Sustitución: \(2\)

Asignar coste \(2\) a la sustitución equivale a considerar que sustituir una letra consiste en eliminar la letra original e insertar la nueva:

\[ \operatorname{coste}(\text{sustitución}) = \operatorname{coste}(\text{eliminación}) + \operatorname{coste}(\text{inserción}) = 1+1=2 \]

Con esta convención, transformar cold en cord cuesta \(2\):

  1. Sustituir l por r: coste \(2\).
  2. Mantener c, o y d: coste \(0\).

Por tanto:

\[ 2+0+0+0=2 \]

6.6 Programación dinámica

Probar todas las secuencias posibles de operaciones mediante fuerza bruta sería muy costoso. La solución habitual es utilizar programación dinámica.

Se construye una tabla \(D\), donde \(D[i,j]\) representa la distancia mínima entre:

  • el prefijo de longitud \(i\) de la cadena de origen;
  • el prefijo de longitud \(j\) de la cadena objetivo.

Para transformar cold en cord, las filas corresponden a los prefijos de cold y las columnas a los prefijos de cord.

La tabla comienza con estas condiciones:

\[ D[0,0]=0 \]

Transformar un prefijo de longitud \(i\) en una cadena vacía requiere eliminar \(i\) letras:

\[ D[i,0]=i \]

Transformar una cadena vacía en un prefijo de longitud \(j\) requiere insertar \(j\) letras:

\[ D[0,j]=j \]

Para las demás celdas se consideran tres posibilidades:

\[ D[i,j] = \min \begin{cases} D[i-1,j]+1 & \text{eliminación}\\ D[i,j-1]+1 & \text{inserción}\\ D[i-1,j-1]+c & \text{sustitución o coincidencia} \end{cases} \]

donde:

\[ c= \begin{cases} 0, & \text{si los caracteres coinciden}\\ 2, & \text{si los caracteres son distintos} \end{cases} \]

6.6.1 Ejemplo: cold → cord

\(D\) “” c o r d
“” 0 1 2 3 4
c 1 0 1 2 3
o 2 1 0 1 2
l 3 2 1 2 3
d 4 3 2 3 2

La esquina inferior derecha contiene el resultado:

\[ D[4,4]=2 \]

La distancia mínima es \(2\), porque basta con sustituir l por r.

La tabla reutiliza resultados calculados anteriormente. Esta es la idea esencial de la programación dinámica: resolver primero problemas pequeños y aprovechar sus resultados para resolver problemas mayores.

Por ejemplo, para calcular la distancia entre col y cor, el algoritmo no empieza desde cero. Utiliza los valores que ya calculó para los prefijos más pequeños, como co y c.

6.7 Distancia de Levenshtein y transposiciones

La distancia de Levenshtein estándar permite inserciones, eliminaciones y sustituciones. Normalmente, cada una de estas operaciones tiene coste \(1\).

Si también se permite intercambiar dos caracteres adyacentes, como en:

form → from

se está utilizando una variante conocida como distancia de Damerau-Levenshtein.

La diferencia puede resumirse así:

\[ \text{Levenshtein} = \{\text{inserción},\text{eliminación},\text{sustitución}\} \]
\[ \text{Damerau-Levenshtein} = \{\text{inserción},\text{eliminación},\text{sustitución},\text{transposición}\} \]

Además de calcular la distancia, a veces interesa conocer la secuencia concreta de operaciones utilizada. Para ello, se guarda en cada celda un puntero hacia la celda anterior que produjo el mínimo. Después, se recorre ese camino desde la esquina inferior derecha hasta la esquina superior izquierda.

Este procedimiento, llamado backtrace, permite reconstruir las ediciones realizadas. Por ejemplo:

form → from

podría reconstruirse como una única transposición:

o ↔ r

7 Tokenización

La tokenización es el proceso de dividir el texto en unidades más pequeñas (tokens). Es el primer paso antes de cualquier otra técnica:

from nltk.tokenize import word_tokenize

texto = "¡Me encanta machine learning! ¿Y tú?"
tokens = word_tokenize(texto, language='spanish')
print(tokens)
# → ['¡', 'Me', 'encanta', 'machine', 'learning', '!', '¿', 'Y', 'tú', '?']

# Sin signos de puntuación
tokens_limpios = [t.lower() for t in tokens if t.isalpha()]
print(tokens_limpios)
# → ['me', 'encanta', 'machine', 'learning', 'y', 'tú']

Tip

Tokenización por palabras vs. por caracteres. La tokenización por palabras es la más común, pero en modelos modernos (BERT, GPT) se usa tokenización subpalabra (WordPiece, BPE) que divide palabras desconocidas en unidades más pequeñas. Esto permite manejar palabras raras o neologismos sin词汇外 (out-of-vocabulary) tokens.

8 Pipeline completo de preprocesamiento

Veamos cómo combinar todo en un solo flujo de trabajo:

import re
import nltk
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
from nltk.tokenize import word_tokenize

nltk.download('punkt')
nltk.download('stopwords')

stop_es = set(stopwords.words('spanish'))
stemmer = SnowballStemmer("spanish")

def preprocesar_texto(texto):
    # 1. Limpiar URLs, menciones, números
    texto = re.sub(r'http\S+|www\.\S+', '', texto)
    texto = re.sub(r'@\w+', '', texto)
    texto = re.sub(r'\d+', '', texto)

    # 2. Minúsculas
    texto = texto.lower()

    # 3. Tokenizar
    tokens = word_tokenize(texto, language='spanish')

    # 4. Quitar puntuación y stop words, aplicar stemming
    tokens = [
        stemmer.stem(t)
        for t in tokens
        if t.isalpha() and t not in stop_es
    ]

    return tokens

# Ejemplo con tweets
tweets = [
    "¡Me encanta este producto! https://t.co/abc #genial @usuario",
    "El servicio al cliente es HORRIBLE, nunca más compraré aquí",
    "El producto está bien, ni bueno ni malo",
]

for tweet in tweets:
    print(f"Original:  {tweet}")
    print(f"Procesado: {preprocesar_texto(tweet)}\n")

Salida:

Original:  ¡Me encanta este producto! https://t.co/abc #genial @usuario
Procesado: ['encanta', 'producto', 'genial']

Original:  El servicio al cliente es HORRIBLE, nunca más compraré aquí
Procesado: ['servicio', 'client', 'horribl', 'nunc', 'comprar']

Original:  El producto está bien, ni bueno ni malo
Procesado: ['producto', 'bien', 'bueno', 'malo']

Nota

Observa el stemming. “HORRIBLE” → “horribl”, “nunca” → “nunc”, “compraré” → “comprar”. El stemming reduce la variabilidad léxica, agrupando formas flexionadas de la misma palabra.

9 Impacto en el rendimiento

¿Cuánto ayuda el preprocesamiento? Veamos un ejemplo rápido con TF-IDF:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
import numpy as np

# Datos de ejemplo (sentimientos)
docs = [
    "este producto es fantástico, lo recomiendo",
    "el servicio es pésimo, nunca más",
    "producto normal, nada especial",
    "compra perfecta, llegada rápida",
    "devolución imposible, pésimo servicio",
    "me encanta, calidad excepcional",
    "no funciona, basura total",
    "regular, esperaba más",
]
y = np.array([1, 0, 0, 1, 0, 1, 0, 0])  # 1=positivo, 0=negativo

# Sin preprocesamiento
vec_raw = TfidfVectorizer()
X_raw = vec_raw.fit_transform(docs)
score_raw = cross_val_score(LogisticRegression(), X_raw, y, cv=3).mean()

# Con preprocesamiento
docs_proc = [" ".join(preprocesar_texto(d)) for d in docs]
vec_proc = TfidfVectorizer()
X_proc = vec_proc.fit_transform(docs_proc)
score_proc = cross_val_score(LogisticRegression(), X_proc, y, cv=3).mean()

print(f"Sin preprocesamiento: {score_raw:.3f}")
print(f"Con preprocesamiento: {score_proc:.3f}")

En general, el preprocesamiento mejora la generalización cuando hay mucho ruido (tweets, reviews), pero puede empeorar si se aplica de forma excesiva (eliminar palabras que sí son informativas).

10 Resumen

Técnica Qué hace Cuándo usarla
Limpieza básica Quita URLs, menciones, números Siempre (redes sociales, web)
Minúsculas Normaliza capitalización Casi siempre
Stop words Elimina palabras vacías Clasificación, análisis de sentimientos
Stemming Reduce a raíz heurística Datos grandes, velocidad
Lematización Reduce a lema lingüístico Precisión lingüística crítica
Tokenización Divide en tokens Primer paso siempre

Tip

Consejo práctico. No apliques todas las técnicas ciegamente. El preprocesamiento ideal depende de la tarea: en análisis de sentimientos, conservar “no” y los signos de exclamación es importante; en clasificación de temas, eliminar stop words es más relevante. Siempre evalúa el impacto con validación cruzada.