Bienvenido
Bienvenido a una lección práctica de PNL.
Vas a construir un stemmer de inglés que funcione desde cero: un algoritmo que reduce palabras a su forma raíz.
Al final, tendrás un algoritmo real y probado que transforma palabras como running → run, happiness → happi & organizational → organ.
También escribirás pruebas unitarias, pruebas de integración y pruebas funcionales para tu stemmer: porque un algoritmo sin pruebas es solo una conjetura.
¿Qué es el stemming?
El problema
Los motores de búsqueda enfrentan un problema fundamental: un usuario busca "running" pero el documento contiene "run" o "runs" o "runner". Son todos el mismo concepto: pero son strings diferentes.
Stemming reduce palabras flexionadas a una forma base común (el raíz). No necesita ser una palabra real: solo necesita ser consistente.
| Palabra | Raíz |
|---|---|
| running | run |
| runs | run |
| runner | runner |
| happiness | happi |
| happily | happi |
| happy | happi |
Nota que happi no es una palabra inglesa real. Sin problema. Stemming se trata de agrupar, no de significado. Mientras happiness, happily & happy colapsen a la misma raíz, la búsqueda & recuperación mejoran.
Zellig Harris y Análisis Distribucional
El origen del stemming computacional
En 1955, el lingüista Zellig Harris publicó From Phoneme to Morpheme (Del Fonema al Morfema), describiendo un método para encontrar los límites entre unidades significativas (morfemas) en palabras.
Su insight fue distribucional: si miras un gran corpus de palabras inglesas, el límite entre una raíz & un sufijo se muestra como una señal estadística.
El método de variedad de sucesores
Para cualquier prefijo de una palabra, cuenta cuántos caracteres distintos lo siguen en el corpus. Harris llamó a esto la variedad de sucesores.
Considera el prefijo "work" en un corpus que contiene: worked, worker, working, works, workshop.
| Prefijo | Qué sigue | Variedad de sucesores |
|---|---|---|
| w | o | 1 |
| wo | r | 1 |
| wor | k | 1 |
| work | e, i, s, sh | 4 |
| worke | d, r | 2 |
Después de "work", cuatro caracteres diferentes pueden seguir: un pico en variedad. Ese pico marca un límite de morfema. La raíz es work y todo lo que viene después es un sufijo.
Esto fue radical en 1955. Sin reglas lingüísticas, sin diccionario: solo contar. Harris demostró que la estructura del lenguaje se revela a sí misma a través de la distribución.
Entender la Variedad de Sucesores
El método de Harris funciona en cualquier idioma. No necesitas conocer la gramática: las estadísticas revelan los límites de morfemas.
En la práctica, la variedad de sucesores pura requiere un corpus grande y detección cuidadosa de picos. Investigadores posteriores: Lovins (1968), Porter (1980): simplificaron el enfoque en eliminación de sufijos basada en reglas: en lugar de calcular variedad de sucesores de un corpus, codificaron las reglas de sufijo directamente.
Hoy construirás un eliminador de sufijos basado en reglas inspirado en el insight de Harris. Definirás los sufijos explícitamente, luego los eliminarás de las palabras. Así es como funcionan la mayoría de los stemmers de producción.
Tu Primer Eliminador de Sufijos
Vamos a codificar
Comienza simple. Escribe una función llamada stem que toma una palabra & elimina estos sufijos (en este orden):
1. -ing (running → runn)
2. -ed (walked → walk)
3. -ly (quickly → quick)
4. -s (cats → cat)
Reglas:
- Convierte la palabra a minúsculas primero
- Solo elimina un sufijo (la primera coincidencia en el orden anterior)
- Solo elimina si la raíz restante tiene al menos 3 caracteres de largo
- Devuelve la raíz
Ejemplo:
def stem(word):
word = word.lower()
# tu lógica de eliminación de sufijos aquí
return word
Manejo de Casos Edge
Haciendo el stemmer más inteligente
Tu eliminador básico tiene un problema: running → runn & hoping → hop. Necesitamos dos refinamientos:
1. Limpieza de consonante doble: si eliminar -ing o -ed deja una consonante doble al final (como runn), elimina la última letra → run
2. Restauración de e silenciosa: si eliminar -ing deja una raíz terminando en una consonante (no una vocal), & el original podría haber tenido una e silenciosa (como hop de hoping), añade e de vuelta → hope
Para la regla de e silenciosa, mantenlo simple: si después de eliminar -ing, la raíz es 3+ caracteres, termina con una consonante, & el penúltimo carácter es una vocal (un patrón como hop, mak, tak), añade e de vuelta.
También añade estos nuevos sufijos (revísalos antes de -ing, -ed, -ly, -s):
5. -tion (organization → organiza)
6. -ness (happiness → happi)
7. -ment (movement → move)
8. -able (readable → read)
9. -ible (sensible → sens)
Prioridad de sufijo actualizada: -tion, -ness, -ment, -able, -ible, -ing, -ed, -ly, -s
Mantén la regla de longitud mínima de raíz: solo elimina si la raíz restante es 3+ caracteres.
Reglas -ies & -ier
Más morfología
El inglés tiene otro patrón común: palabras terminando en -y cambian a -ies, -ied, o -ier cuando se flexionan.
| Palabra | Debería ser raíz |
|---|---|
| babies | babi |
| carried | carri |
| earlier | earli |
| flies | fli |
| studied | studi |
Añade estas reglas antes de las revisiones -s & -ed:
- -ies → elimina & añade i (babies → babi)
- -ied → elimina & añade i (carried → carri)
- -ier → elimina & añade i (earlier → earli)
La misma regla de longitud mínima de raíz: solo transforma si el resultado es 3+ caracteres.
¿Por qué probar?
Probar no es opcional
Tienes un stemmer que funciona. ¿Cómo sabes que realmente funciona? Ahora estás ejecutando algunos ejemplos a mano. Eso no escala.
El software profesional usa tres niveles de prueba:
Pruebas unitarias: prueba una función en aislamiento con entradas conocidas y salidas esperadas. Rápidas, numerosas, específicas.
Pruebas de integración: prueba que múltiples componentes funcionen juntos. Para un stemmer, esto significa probar contra un lote de palabras y verificar que los resultados sean consistentes.
Pruebas funcionales: prueba el sistema desde afuera, como lo haría un usuario. Para un stemmer, esto significa alimentarlo con texto real y verificar que la salida tenga sentido para un caso de uso real como búsqueda.
Escribirás las tres.
Escribe Pruebas Unitarias
Pruebas unitarias
Escribe una función llamada run_unit_tests que pruebe tu función stem con al menos 15 casos de prueba cubriendo:
1. Eliminación básica de sufijos: palabras terminando en -ing, -ed, -ly, -s
2. Sufijos complejos: -tion, -ness, -ment, -able, -ible
3. Flexión-Y: -ies, -ied, -ier
4. Casos edge: palabras cortas que no deberían eliminarse, palabras sin sufijo, palabras ya radizadas
5. Limpieza de consonante doble: running → run, sitting → sit
6. Restauración de e silenciosa: hoping → hope
7. Insensibilidad a mayúsculas: entrada en mayúsculas debería convertirse a minúsculas
Estructura tus pruebas así:
def run_unit_tests():
tests = [
('running', 'run'),
('cats', 'cat'),
# ... al menos 15 casos de prueba
]
passed = 0
failed = 0
for word, expected in tests:
result = stem(word)
if result == expected:
passed += 1
else:
failed += 1
print(f'FAIL: stem({word}) = {result}, expected {expected}')
print(f'{passed}/{passed + failed} unit tests passed')
return failed == 0
Escribe Pruebas de Integración
Pruebas de integración
Las pruebas unitarias verifican entradas individuales. Las pruebas de integración verifican que los componentes funcionen juntos correctamente.
Para un stemmer, una propiedad de integración clave es consistencia: si radizas la misma palabra dos veces, obtienes el mismo resultado. Y palabras que deberían agruparse deberían producir la misma raíz.
Escribe una función llamada run_integration_tests que pruebe:
1. Idempotencia: radizar una palabra ya radizada debería devolver la misma raíz. stem(stem(word)) == stem(word) para todas las palabras.
2. Agrupamiento: palabras que deberían compartir raíz realmente lo hacen. Prueba al menos 3 familias de palabras (ej., run/runs/running/runner deberían compartir raíz).
3. Procesamiento en lote: procesa una lista de 20+ palabras y verifica que no haya crashes, strings vacíos, valores None.
def run_integration_tests():
# Prueba 1: idempotencia
# Prueba 2: agrupamiento de familia de palabras
# Prueba 3: estabilidad en lote
...
Escribe Pruebas Funcionales
Pruebas funcionales
Las pruebas funcionales verifican que el sistema funcione para su caso de uso previsto. Tu stemmer existe para mejorar la búsqueda: así que prueba eso.
Escribe una función llamada run_functional_tests que:
1. Simulación de búsqueda: dada una lista de strings de documentos & una palabra de consulta, radiza tanto los documentos como la consulta, luego verifica si los términos de consulta radicalizados aparecen en documentos radicalizados. Prueba que buscar 'running' encuentra un documento conteniendo 'run' & 'runner'.
2. Verificación de precisión: verifica que radicalizar NO agrupe incorrectamente palabras no relacionadas. 'university' & 'universe' podrían compartir raíz: verifica si tu stemmer maneja esto (está OK si las agrupa; documenta el comportamiento).
3. Procesamiento de texto real: radicaliza cada palabra en un párrafo de texto inglés real. Verifica que la salida sea razonable: sin strings vacíos, sin crashes, salida tiene el mismo número de palabras que entrada.
def run_functional_tests():
# Prueba 1: búsqueda encuentra documentos relacionados
# Prueba 2: precisión: verificar sobre-radicalizacion
# Prueba 3: procesamiento de párrafo real
...
Lo Que Construiste
Lo que construiste
Implementaste un stemmer de inglés que funciona con:
- 12 reglas de sufijo (-tion, -ness, -ment, -able, -ible, -ies, -ied, -ier, -ing, -ed, -ly, -s)
- Limpieza de consonante doble
- Restauración de e silenciosa
- Pruebas unitarias, pruebas de integración, & pruebas funcionales
La línea de descendencia
Tu stemmer desciende de una línea de trabajo que comenzó con Zellig Harris en 1955:
- Harris (1955): Descubrió que los límites de morfema aparecen como señales estadísticas (variedad de sucesores)
- Lovins (1968): Primer algoritmo de stemming publicado, 294 reglas de sufijo
- Porter (1980): Simplificado a ~60 reglas en 5 pasos, se convirtió en el estándar durante décadas
- Snowball (2001): Marco de Porter generalizado a múltiples idiomas
- Tu stemmer (hoy): 12 reglas, el mismo principio fundamental
Lo que podrías hacer después
- Implementar el algoritmo completo de Porter (está bien documentado & es un gran ejercicio)
- Portar tu stemmer a C para una mejora de velocidad de 100x
- Construir un motor de búsqueda simple que use tu stemmer para indexar & consultar archivos de texto
- Comparar la salida de tu stemmer contra PorterStemmer de NLTK para medir precisión
El código que escribiste hoy es la misma operación fundamental que se ejecuta dentro de cada motor de búsqueda del planeta. No está mal para un día de trabajo.