Preprocesamiento de Texto

S3: Tokenización, Lematización y Stemming

Prof. Francisco Suárez

Universidad Católica Boliviana

2026-03-17

Agenda de Hoy

Primera Parte

  1. 🔤 ¿Qué es la tokenización?
  2. 🛠️ Herramientas: NLTK y spaCy
  3. 📝 Normalización de texto

Segunda Parte

  1. 🌱 Stemming
  2. 📖 Lematización
  3. 🔧 Pipeline completo de preprocesamiento

¿Por qué preprocesar texto?

El Texto Crudo es Ruidoso

Texto real del mundo:

¡¡¡INCREÍBLE oferta!!!  
Visita https://tienda.com 😍😍
Envía un msg al +591-71234567
#Descuento @tienda_bo
    precio: $99.99 USD

Problemas para una máquina:

  • Mayúsculas / minúsculas
  • Puntuación repetida
  • URLs, emails, emojis
  • Hashtags, menciones
  • Espacios irregulares
  • Números en distintos formatos

Regla de Oro del NLP

Basura entra, basura sale (Garbage in, garbage out). La calidad de cualquier modelo depende directamente de la calidad del preprocesamiento.

El Pipeline de Preprocesamiento

Code
flowchart LR
    A[📄 Texto<br>crudo] --> B[🔤 Tokenización]
    B --> C[🔡 Normalización]
    C --> D[🚫 Stopwords]
    D --> E[🌱 Stemming /<br>📖 Lematización]
    E --> F[✅ Texto<br>limpio]

    style A fill:#fff3cd,color:#000
    style B fill:#cfe2ff,color:#000
    style C fill:#d1e7dd,color:#000
    style D fill:#f8d7da,color:#000
    style E fill:#e2d9f3,color:#000
    style F fill:#d4edda,color:#000

flowchart LR
    A[📄 Texto<br>crudo] --> B[🔤 Tokenización]
    B --> C[🔡 Normalización]
    C --> D[🚫 Stopwords]
    D --> E[🌱 Stemming /<br>📖 Lematización]
    E --> F[✅ Texto<br>limpio]

    style A fill:#fff3cd,color:#000
    style B fill:#cfe2ff,color:#000
    style C fill:#d1e7dd,color:#000
    style D fill:#f8d7da,color:#000
    style E fill:#e2d9f3,color:#000
    style F fill:#d4edda,color:#000

Cada paso transforma el texto para que sea más útil para nuestros modelos.

Tokenización

¿Qué es Tokenizar?

Dividir un texto en unidades mínimas significativas llamadas tokens.

Tipos de tokenización:

Nivel Ejemplo
Oración “Hola. ¿Qué tal?” → 2 oraciones
Palabra “el gato negro” → 3 tokens
Subpalabra “jugando” → “jug” + “##ando”
Carácter “gato” → g, a, t, o

¿Por qué importa?

  • Define la unidad básica de análisis
  • Afecta el vocabulario del modelo
  • Diferente en cada idioma
  • No es trivial: “New York” → ¿1 o 2 tokens?

Método 1: split() (Naïve)

texto = "El gato, que estaba en el jardín, cazó un ratón."

# split() divide por espacios
tokens = texto.split()
print(f"Tokens: {tokens}")
print(f"Cantidad: {len(tokens)}")
Tokens: ['El', 'gato,', 'que', 'estaba', 'en', 'el', 'jardín,', 'cazó', 'un', 'ratón.']
Cantidad: 10

Problemas de split()

  • “gato,” se considera un solo token (incluye la coma)
  • No separa puntuación
  • No maneja contracciones (“del” = “de” + “el”)
  • No detecta límites de oración

Método 2: Regex (Mejor)

import re

texto = "El gato, que estaba en el jardín, cazó un ratón."

# Tokenización con regex
tokens = re.findall(r"\w+|[^\w\s]", texto)
print(f"Tokens: {tokens}")
print(f"Cantidad: {len(tokens)}")
Tokens: ['El', 'gato', ',', 'que', 'estaba', 'en', 'el', 'jardín', ',', 'cazó', 'un', 'ratón', '.']
Cantidad: 13

Mejor, pero aún tiene limitaciones con:

  • Abreviaturas: “EE.UU.” → ¿“EE”, “.”, “UU”, “.”?
  • Números decimales: “3.14” → ¿“3”, “.”, “14”?
  • Contracciones del español: “al” = “a” + “el”

Método 3: NLTK

import nltk
nltk.download('punkt_tab', quiet=True)

from nltk.tokenize import word_tokenize, sent_tokenize

texto = "El Dr. García vive en La Paz. Trabaja en la U.C.B. desde 2019."

# Tokenización por oraciones
oraciones = sent_tokenize(texto, language='spanish')
print("Oraciones:")
for i, sent in enumerate(oraciones):
    print(f"  {i+1}. {sent}")

# Tokenización por palabras
tokens = word_tokenize(texto, language='spanish')
print(f"\nTokens: {tokens}")
Oraciones:
  1. El Dr. García vive en La Paz.
  2. Trabaja en la U.C.B.
  3. desde 2019.

Tokens: ['El', 'Dr.', 'García', 'vive', 'en', 'La', 'Paz', '.', 'Trabaja', 'en', 'la', 'U.C.B', '.', 'desde', '2019', '.']

Método 4: spaCy

import spacy

nlp = spacy.load("es_core_news_sm")

texto = "El Dr. García vive en La Paz. Trabaja en la U.C.B. desde 2019."
doc = nlp(texto)

# Tokenización por oraciones
print("Oraciones:")
for i, sent in enumerate(doc.sents):
    print(f"  {i+1}. {sent.text}")

# Tokenización por palabras con información adicional
print("\nTokens con POS tags:")
for token in doc:
    print(f"  '{token.text:12s}' → POS: {token.pos_:6s} | Lema: {token.lemma_}")
Oraciones:
  1. El Dr. García vive en La Paz.
  2. Trabaja en la U.C.B. desde 2019.

Tokens con POS tags:
  'El          ' → POS: DET    | Lema: el
  'Dr.         ' → POS: PROPN  | Lema: Dr.
  'García      ' → POS: PROPN  | Lema: García
  'vive        ' → POS: VERB   | Lema: vivir
  'en          ' → POS: ADP    | Lema: en
  'La          ' → POS: DET    | Lema: el
  'Paz         ' → POS: PROPN  | Lema: Paz
  '.           ' → POS: PUNCT  | Lema: .
  'Trabaja     ' → POS: VERB   | Lema: trabajar
  'en          ' → POS: ADP    | Lema: en
  'la          ' → POS: DET    | Lema: el
  'U.C.B.      ' → POS: PROPN  | Lema: U.C.B.
  'desde       ' → POS: ADP    | Lema: desde
  '2019        ' → POS: NOUN   | Lema: 2019
  '.           ' → POS: PUNCT  | Lema: .

Comparación de Tokenizadores

import re
import nltk
import spacy
from nltk.tokenize import word_tokenize

nlp = spacy.load("es_core_news_sm")

texto = "No puedo creer que cueste $99.99 en EE.UU."

# Comparar los 4 métodos
metodos = {
    "split()": texto.split(),
    "regex": re.findall(r"\w+|[^\w\s]", texto),
    "NLTK": word_tokenize(texto, language='spanish'),
    "spaCy": [t.text for t in nlp(texto)]
}

for nombre, tokens in metodos.items():
    print(f"{nombre:8s} ({len(tokens):2d} tokens): {tokens}")
split()  ( 8 tokens): ['No', 'puedo', 'creer', 'que', 'cueste', '$99.99', 'en', 'EE.UU.']
regex    (14 tokens): ['No', 'puedo', 'creer', 'que', 'cueste', '$', '99', '.', '99', 'en', 'EE', '.', 'UU', '.']
NLTK     (10 tokens): ['No', 'puedo', 'creer', 'que', 'cueste', '$', '99.99', 'en', 'EE.UU', '.']
spaCy    ( 9 tokens): ['No', 'puedo', 'creer', 'que', 'cueste', '$', '99.99', 'en', 'EE.UU.']

Recomendación

Para proyectos reales, usa spaCy o NLTK. Reserva split() y regex para prototipos rápidos.

Normalización

Técnicas de Normalización

Técnicas Básicas

  1. Lowercase - Convertir a minúsculas
  2. Eliminar puntuación
  3. Eliminar números (o normalizarlos)
  4. Eliminar espacios extra

Técnicas Avanzadas

  1. Eliminar stopwords
  2. Eliminar acentos (depende del caso)
  3. Expandir contracciones
  4. Normalizar Unicode

Ejemplo: Normalización Paso a Paso

import re
import unicodedata

texto = "  ¡¡HOLA Mundo!!  ¿Cómo ESTÁS?   😊  "

# Paso 1: Lowercase
paso1 = texto.lower()
print(f"1. Lowercase:    '{paso1}'")

# Paso 2: Normalizar espacios
paso2 = re.sub(r"\s+", " ", paso1).strip()
print(f"2. Espacios:     '{paso2}'")

# Paso 3: Eliminar puntuación repetida
paso3 = re.sub(r"([¿?¡!.])\1+", r"\1", paso2)
print(f"3. Puntuación:   '{paso3}'")

# Paso 4: Eliminar emojis
paso4 = re.sub(r"[^\w\s¿?¡!.,áéíóúñü]", "", paso3)
print(f"4. Sin emojis:   '{paso4}'")
1. Lowercase:    '  ¡¡hola mundo!!  ¿cómo estás?   😊  '
2. Espacios:     '¡¡hola mundo!! ¿cómo estás? 😊'
3. Puntuación:   '¡hola mundo! ¿cómo estás? 😊'
4. Sin emojis:   '¡hola mundo! ¿cómo estás? '

Stopwords: Palabras Vacías

Stopwords = palabras muy frecuentes que aportan poco significado.

import nltk
nltk.download('stopwords', quiet=True)
from nltk.corpus import stopwords

# Stopwords en español
stops_es = set(stopwords.words('spanish'))
print(f"Total stopwords en español: {len(stops_es)}")
print(f"Ejemplos: {sorted(list(stops_es))[:20]}")
Total stopwords en español: 313
Ejemplos: ['a', 'al', 'algo', 'algunas', 'algunos', 'ante', 'antes', 'como', 'con', 'contra', 'cual', 'cuando', 'de', 'del', 'desde', 'donde', 'durante', 'e', 'el', 'ella']

Eliminación de Stopwords

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

stops = set(stopwords.words('spanish'))

texto = "El procesamiento de lenguaje natural es una rama de la inteligencia artificial"
tokens = word_tokenize(texto.lower(), language='spanish')

# Filtrar stopwords
tokens_limpios = [t for t in tokens if t not in stops and t.isalpha()]

print(f"Original ({len(tokens)} tokens):")
print(f"  {tokens}")
print(f"\nSin stopwords ({len(tokens_limpios)} tokens):")
print(f"  {tokens_limpios}")
Original (12 tokens):
  ['el', 'procesamiento', 'de', 'lenguaje', 'natural', 'es', 'una', 'rama', 'de', 'la', 'inteligencia', 'artificial']

Sin stopwords (6 tokens):
  ['procesamiento', 'lenguaje', 'natural', 'rama', 'inteligencia', 'artificial']

⚠️ Cuidado con las stopwords

No siempre hay que eliminarlas. Para análisis de sentimiento, “no” es crucial. Para modelos de lenguaje, las necesitamos todas.

Stemming

¿Qué es Stemming?

Reducir una palabra a su raíz (stem) eliminando sufijos morfológicos mediante reglas heurísticas.

Ejemplo:

Palabra Stem
jugando jug
jugador jug
juegos jueg
jugar jug
jugable jug

Características:

  • ✅ Rápido y simple
  • ✅ No necesita diccionario
  • ❌ La raíz puede no ser una palabra real
  • ❌ Errores frecuentes
  • ❌ Basado en reglas fijas

Snowball Stemmer para Español

from nltk.stem import SnowballStemmer

stemmer = SnowballStemmer("spanish")

palabras = [
    "corriendo", "correr", "corrió", "corredor", "corrida",
    "computadora", "computadoras", "computación", "computar",
    "universidad", "universitario", "universitaria", "universidades"
]

print(f"{'Palabra':<18} {'Stem':<12}")
print("-" * 30)
for palabra in palabras:
    stem = stemmer.stem(palabra)
    print(f"{palabra:<18} {stem:<12}")
Palabra            Stem        
------------------------------
corriendo          corr        
correr             corr        
corrió             corr        
corredor           corredor    
corrida            corr        
computadora        comput      
computadoras       comput      
computación        comput      
computar           comput      
universidad        univers     
universitario      universitari
universitaria      universitari
universidades      univers     

Errores Comunes del Stemming

from nltk.stem import SnowballStemmer

stemmer = SnowballStemmer("spanish")

# Over-stemming: palabras diferentes → mismo stem
print("Over-stemming (falsos positivos):")
pares_over = [("universo", "universidad"), ("general", "generoso")]
for p1, p2 in pares_over:
    s1, s2 = stemmer.stem(p1), stemmer.stem(p2)
    print(f"  '{p1}' → '{s1}' | '{p2}' → '{s2}' | ¿Iguales? {s1 == s2}")

# Under-stemming: palabras relacionadas → stems diferentes
print("\nUnder-stemming (falsos negativos):")
pares_under = [("absorber", "absorción"), ("producir", "producto")]
for p1, p2 in pares_under:
    s1, s2 = stemmer.stem(p1), stemmer.stem(p2)
    print(f"  '{p1}' → '{s1}' | '{p2}' → '{s2}' | ¿Iguales? {s1 == s2}")
Over-stemming (falsos positivos):
  'universo' → 'univers' | 'universidad' → 'univers' | ¿Iguales? True
  'general' → 'general' | 'generoso' → 'gener' | ¿Iguales? False

Under-stemming (falsos negativos):
  'absorber' → 'absorb' | 'absorción' → 'absorcion' | ¿Iguales? False
  'producir' → 'produc' | 'producto' → 'product' | ¿Iguales? False

Lematización

¿Qué es la Lematización?

Reducir una palabra a su lema: la forma canónica que aparecería en un diccionario, considerando el contexto gramatical.

Ejemplo:

Palabra Lema
jugando jugar
jugador jugador
juegos juego
mejores mejor
fue ir / ser

Características:

  • ✅ El resultado es una palabra real
  • ✅ Considera el contexto (POS)
  • ✅ Más preciso que stemming
  • ❌ Más lento
  • ❌ Necesita diccionario / modelo

Lematización con spaCy

import spacy

nlp = spacy.load("es_core_news_sm")

texto = "Los estudiantes corrieron rápidamente hacia las mejores universidades del país"
doc = nlp(texto)

print(f"{'Token':<18} {'Lema':<16} {'POS':<8} {'¿Cambió?'}")
print("-" * 52)
for token in doc:
    cambio = "✅" if token.text.lower() != token.lemma_ else ""
    print(f"{token.text:<18} {token.lemma_:<16} {token.pos_:<8} {cambio}")
Token              Lema             POS      ¿Cambió?
----------------------------------------------------
Los                el               DET      ✅
estudiantes        estudiante       NOUN     ✅
corrieron          correr           VERB     ✅
rápidamente        rápidamente      ADV      
hacia              hacia            ADP      
las                el               DET      ✅
mejores            mejor            ADJ      ✅
universidades      universidad      NOUN     ✅
del                del              ADP      
país               país             NOUN     

Stemming vs. Lematización

from nltk.stem import SnowballStemmer
import spacy

stemmer = SnowballStemmer("spanish")
nlp = spacy.load("es_core_news_sm")

palabras = ["corriendo", "mejores", "ciudades", "producción", "dificultades", "haciendo"]

print(f"{'Palabra':<16} {'Stem':<14} {'Lema (spaCy)':<16}")
print("-" * 46)
for palabra in palabras:
    stem = stemmer.stem(palabra)
    doc = nlp(palabra)
    lema = doc[0].lemma_
    print(f"{palabra:<16} {stem:<14} {lema:<16}")
Palabra          Stem           Lema (spaCy)    
----------------------------------------------
corriendo        corr           correr          
mejores          mejor          mejor           
ciudades         ciudad         ciudad          
producción       produccion     producción      
dificultades     dificultad     dificultad      
haciendo         hac            hacer           

¿Cuándo usar cuál?

  • Stemming: Búsqueda de información, indexación, cuando la velocidad importa
  • Lematización: Análisis de sentimiento, chatbots, cuando la precisión importa

Pipeline Completo

Función de Preprocesamiento

import re
import spacy
from nltk.corpus import stopwords

nlp = spacy.load("es_core_news_sm")
stops = set(stopwords.words('spanish'))

def preprocesar(texto, usar_lemas=True, quitar_stops=True):
    """Pipeline completo de preprocesamiento para NLP en español."""
    # 1. Lowercase
    texto = texto.lower()
    # 2. Eliminar URLs y emails
    texto = re.sub(r"https?://\S+|www\.\S+", "", texto)
    texto = re.sub(r"\S+@\S+", "", texto)
    # 3. Eliminar caracteres especiales (mantener letras y espacios)
    texto = re.sub(r"[^\w\sáéíóúñü]", " ", texto)
    # 4. Normalizar espacios
    texto = re.sub(r"\s+", " ", texto).strip()
    # 5. Tokenizar y lematizar con spaCy
    doc = nlp(texto)
    tokens = []
    for token in doc:
        palabra = token.lemma_ if usar_lemas else token.text
        if quitar_stops and palabra in stops:
            continue
        if not palabra.isalpha():
            continue
        tokens.append(palabra)
    return tokens

Demo del Pipeline

texto_crudo = """
¡¡Los MEJORES estudiantes de la Universidad Católica están 
desarrollando proyectos increíbles!! 🎓🚀
Visita https://ucb.edu.bo para más información.
Contacto: info@ucb.edu.bo
"""

print(f"TEXTO ORIGINAL:\n{texto_crudo}")

# Con lemas y sin stopwords
resultado1 = preprocesar(texto_crudo, usar_lemas=True, quitar_stops=True)
print(f"Con lemas, sin stops:\n  {resultado1}\n")

# Sin lemas, sin stopwords
resultado2 = preprocesar(texto_crudo, usar_lemas=False, quitar_stops=True)
print(f"Sin lemas, sin stops:\n  {resultado2}\n")

# Con lemas, con stopwords
resultado3 = preprocesar(texto_crudo, usar_lemas=True, quitar_stops=False)
print(f"Con lemas, con stops:\n  {resultado3}")
TEXTO ORIGINAL:

¡¡Los MEJORES estudiantes de la Universidad Católica están 
desarrollando proyectos increíbles!! 🎓🚀
Visita https://ucb.edu.bo para más información.
Contacto: info@ucb.edu.bo

Con lemas, sin stops:
  ['mejor', 'estudiante', 'universidad', 'católico', 'desarrollar', 'proyecto', 'increíble', 'visitar', 'información', 'contacto']

Sin lemas, sin stops:
  ['mejores', 'estudiantes', 'universidad', 'católica', 'desarrollando', 'proyectos', 'increíbles', 'visita', 'información', 'contacto']

Con lemas, con stops:
  ['el', 'mejor', 'estudiante', 'de', 'el', 'universidad', 'católico', 'estar', 'desarrollar', 'proyecto', 'increíble', 'visitar', 'para', 'más', 'información', 'contacto']

Procesando Múltiples Documentos

documentos = [
    "El procesamiento de lenguaje natural permite analizar textos automáticamente.",
    "Las redes neuronales han revolucionado la inteligencia artificial moderna.",
    "Bolivia tiene una gran diversidad lingüística con más de 30 idiomas nativos.",
]

print("DOCUMENTOS PREPROCESADOS:\n")
for i, doc in enumerate(documentos):
    tokens = preprocesar(doc)
    print(f"Doc {i+1}: {tokens}")
DOCUMENTOS PREPROCESADOS:

Doc 1: ['procesamiento', 'lenguaje', 'natural', 'permitir', 'analizar', 'texto', 'automáticamente']
Doc 2: ['red', 'neuronal', 'haber', 'revolucionar', 'inteligencia', 'artificial', 'moderno']
Doc 3: ['bolivia', 'tener', 'gran', 'diversidad', 'lingüístico', 'idioma', 'nativo']

Siguiente paso

Con el texto preprocesado, estamos listos para representarlo numéricamente (Semana 2: Bag of Words, TF-IDF…).

Ejercicio Práctico

Desafío: Preprocesador de Tweets

Crea una función que preprocese tweets en español:

  1. Eliminar menciones (@usuario)
  2. Extraer y eliminar hashtags
  3. Eliminar URLs
  4. Lematizar el texto restante
Code
def preprocesar_tweet(tweet):
    """Preprocesa un tweet en español."""
    # Extraer hashtags antes de limpiar
    hashtags = re.findall(r"#\w+", tweet)
    # Eliminar menciones, hashtags, URLs
    limpio = re.sub(r"@\w+", "", tweet)
    limpio = re.sub(r"#\w+", "", limpio)
    limpio = re.sub(r"https?://\S+", "", limpio)
    # Preprocesar
    tokens = preprocesar(limpio)
    return {"tokens": tokens, "hashtags": hashtags}

# Prueba
tweet = "Gran clase de @fsuarez sobre #NLP y #MachineLearning 🔥 https://t.co/abc123"
resultado = preprocesar_tweet(tweet)
print(f"Tweet: {tweet}")
print(f"Tokens: {resultado['tokens']}")
print(f"Hashtags: {resultado['hashtags']}")
Tweet: Gran clase de @fsuarez sobre #NLP y #MachineLearning 🔥 https://t.co/abc123
Tokens: ['gran', 'clase']
Hashtags: ['#NLP', '#MachineLearning']

Resumen

Lo que Aprendimos Hoy ✅

Tokenización:

  • split() — simple pero limitado
  • Regex — más control
  • NLTK — soporte multi-idioma
  • spaCy — info lingüística completa

Normalización:

  • Lowercase, puntuación
  • Stopwords
  • Limpieza de URLs, emails, emojis

Stemming:

  • Reducción heurística a raíz
  • Snowball Stemmer para español
  • Rápido pero impreciso

Lematización:

  • Reducción a forma de diccionario
  • spaCy para español
  • Más lento pero más preciso

Para la Próxima Clase 📚

Semana 2, S1: Bag of Words y One-hot Encoding

Aprenderemos a representar texto como vectores numéricos para que los modelos de machine learning puedan trabajar con ellos.

Lectura:

  • Capítulo 2: Speech and Language Processing (Jurafsky & Martin)
  • Documentación de spaCy

¿Preguntas? 🙋

¡Gracias!

📧 fsuarez@ucb.edu.bo

🔗 Materiales: github.com/fjsuarez/ucb-nlp