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:
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:
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:
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:
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:
El conjunto de candidatas puede representarse así:
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:
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:
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:
Las demás palabras aparecen una sola vez:
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:
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:
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:
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:
En términos probabilísticos:
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:
Con esta convención, transformar cold en cord cuesta \(2\):
- Sustituir
lporr: coste \(2\). - Mantener
c,oyd: coste \(0\).
Por tanto:
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:
Transformar un prefijo de longitud \(i\) en una cadena vacía requiere eliminar \(i\) letras:
Transformar una cadena vacía en un prefijo de longitud \(j\) requiere insertar \(j\) letras:
Para las demás celdas se consideran tres posibilidades:
donde:
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:
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:
se está utilizando una variante conocida como distancia de Damerau-Levenshtein.
La diferencia puede resumirse así:
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:
podría reconstruirse como una única transposición:
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.