S3: Modelos Ocultos de Markov (HMM) para Etiquetado POS
Prof. Francisco Suárez
Universidad Católica Boliviana
2026-02-28
Agenda de Hoy
Primera Parte
🏷️ ¿Qué es el etiquetado POS?
🔗 De modelos n-gram a secuencias ocultas
🤖 Modelos Ocultos de Markov (HMM)
Segunda Parte
🧮 Los 3 problemas fundamentales de HMM
🛤️ El algoritmo de Viterbi
🛠️ Implementación en Python
Bloque 1: Etiquetado POS
¿Qué es POS Tagging?
Part-of-Speech (POS) Tagging es la tarea de asignar a cada palabra de una oración su categoría gramatical.
Ejemplo
Palabra
Etiqueta POS
El
DET
gato
NOUN
come
VERB
pescado
NOUN
fresco
ADJ
¿Por qué importa?
Desambiguación: “bajo” → ¿ADJ o VERB o PREP?
Parsing sintáctico: estructura de la oración
Extracción de información: encontrar entidades
Traducción automática: orden de palabras
Lematización informada: forma base correcta
Ambigüedad: El Gran Desafío
Muchas palabras pueden tener múltiples categorías:
Palabra
Contexto 1
POS 1
Contexto 2
POS 2
bajo
“el techo bajo”
ADJ
“bajo la mesa”
PREP
como
“como pizza”
VERB
“blanco como la nieve”
CONJ
sobre
“el sobre amarillo”
NOUN
“sobre la mesa”
PREP
canto
“el canto del ave”
NOUN
“yo canto bien”
VERB
traje
“un traje negro”
NOUN
“te traje flores”
VERB
Dato
En español, aproximadamente el 40% de los tipos de palabras (word types) son ambiguos en cuanto a su POS. Sin embargo, la mayoría de los tokens (word tokens) en texto real son no ambiguos en contexto.
Conjunto de Etiquetas POS
Universal POS Tags (UPOS) — 17 categorías
Tag
Categoría
ADJ
Adjetivo
ADP
Adposición
ADV
Adverbio
AUX
Auxiliar
CCONJ
Conj. coordinante
DET
Determinante
Tag
Categoría
INTJ
Interjección
NOUN
Sustantivo
NUM
Numeral
PART
Partícula
PRON
Pronombre
PROPN
Nombre propio
Tag
Categoría
PUNCT
Puntuación
SCONJ
Conj. subordinante
SYM
Símbolo
VERB
Verbo
X
Otro
. . .
Nota
El Penn Treebank usa 45 etiquetas (NN, NNS, VB, VBD, JJ, …). Nosotros usaremos UPOS por simplicidad.
Enfoques para POS Tagging
Basados en reglas
Diccionarios + reglas lingüísticas
Ejemplo: “si termina en -mente → ADV”
Limitado y frágil
Basados en aprendizaje
HMM ← Hoy 🎯
CRF (Campos Aleatorios Condicionales)
Redes neuronales (BiLSTM-CRF)
Transformers (BERT + fine-tuning)
Baseline sorprendentemente buena
“Most Frequent Tag”: asignar a cada palabra su etiqueta más frecuente en el corpus de entrenamiento.
Precisión: ~90% 🤯
Pero falla precisamente en los casos ambiguos e interesantes
Conexión
Los modelos n-gram que vimos en S1 y S2 modelan secuencias de palabras. Los HMM modelan secuencias de etiquetas, condicionadas en las palabras observadas.
Bloque 2: De N-gramas a Secuencias Ocultas
La Intuición
En los modelos n-gram, estimábamos:
\[P(w_1, w_2, \dots, w_n)\]
Para POS tagging, queremos encontrar la mejor secuencia de etiquetas\(t_1, t_2, \dots, t_n\) para una secuencia de palabras \(w_1, w_2, \dots, w_n\):
Ejemplo:\(P(\text{"gato"} \mid \text{NOUN})\) es alta, \(P(\text{"gato"} \mid \text{VERB})\) es baja.
Estas son las probabilidades de emisión.
Resultado
\[\hat{t}_1^n = \arg\max_{t_1^n} \prod_{i=1}^{n} \underbrace{P(w_i \mid t_i)}_{\text{emisión}} \cdot \underbrace{P(t_i \mid t_{i-1})}_{\text{transición}}\] ¡Esta es exactamente la formulación de un HMM!
Bloque 3: Modelos Ocultos de Markov
Definición Formal
Un Modelo Oculto de Markov (HMM) se define por 5 componentes:
\[\lambda = (S, V, A, B, \pi)\]
Componente
Símbolo
Descripción
Estados ocultos
\(S = \{s_1, \dots, s_N\}\)
Las categorías POS (DET, NOUN, VERB, …)
Vocabulario
\(V = \{v_1, \dots, v_M\}\)
Las palabras observables
Prob. de transición
\(A = \{a_{ij}\}\)
\(P(t_j \mid t_i)\) — de estado \(i\) a estado \(j\)
Prob. de emisión
\(B = \{b_j(k)\}\)
\(P(w_k \mid t_j)\) — palabra \(k\) dado estado \(j\)
Prob. inicial
\(\pi = \{\pi_i\}\)
\(P(t_i \mid \langle s \rangle)\) — probabilidad de empezar en estado \(i\)
¿Por qué “oculto”?
Observable: las palabras \(w_1, w_2, \dots, w_n\) (las vemos)
Oculto: las etiquetas \(t_1, t_2, \dots, t_n\) (las queremos descubrir)
Visualización del HMM
Code
graph LR subgraph Estados Ocultos T1["DET"] --> T2["NOUN"] T2 --> T3["VERB"] T3 --> T4["NOUN"] end T1 -.->|"P(el|DET)=0.4"| W1["el"] T2 -.->|"P(gato|NOUN)=0.1"| W2["gato"] T3 -.->|"P(come|VERB)=0.3"| W3["come"] T4 -.->|"P(pescado|NOUN)=0.05"| W4["pescado"] style T1 fill:#0077b6,color:#fff style T2 fill:#0077b6,color:#fff style T3 fill:#0077b6,color:#fff style T4 fill:#0077b6,color:#fff style W1 fill:#cfe2ff,color:#000 style W2 fill:#cfe2ff,color:#000 style W3 fill:#cfe2ff,color:#000 style W4 fill:#cfe2ff,color:#000
graph LR
subgraph Estados Ocultos
T1["DET"] --> T2["NOUN"]
T2 --> T3["VERB"]
T3 --> T4["NOUN"]
end
T1 -.->|"P(el|DET)=0.4"| W1["el"]
T2 -.->|"P(gato|NOUN)=0.1"| W2["gato"]
T3 -.->|"P(come|VERB)=0.3"| W3["come"]
T4 -.->|"P(pescado|NOUN)=0.05"| W4["pescado"]
style T1 fill:#0077b6,color:#fff
style T2 fill:#0077b6,color:#fff
style T3 fill:#0077b6,color:#fff
style T4 fill:#0077b6,color:#fff
style W1 fill:#cfe2ff,color:#000
style W2 fill:#cfe2ff,color:#000
style W3 fill:#cfe2ff,color:#000
style W4 fill:#cfe2ff,color:#000
Flechas horizontales → Transiciones entre estados ocultos \(P(t_i \mid t_{i-1})\)
(Probabilidades simplificadas; las filas suman 1 en un modelo completo.)
Observación
Las matrices de transición son como los bigramas de etiquetas, y las de emisión son como diccionarios probabilísticos.
Bloque 4: Los 3 Problemas Fundamentales
Los 3 Problemas de un HMM
#
Problema
Pregunta
Algoritmo
1
Evaluación (Likelihood)
¿Cuál es \(P(W \mid \lambda)\)? ¿Qué tan probable es observar esta secuencia de palabras dado el modelo?
Forward Algorithm
2
Decodificación
¿Cuál es la mejor secuencia de estados ocultos \(\hat{t}_1^n\)?
Viterbi 🎯
3
Aprendizaje
¿Cómo ajustar los parámetros \(A\), \(B\), \(\pi\) a partir de datos?
Baum-Welch (EM)
Hoy nos enfocamos en el Problema 2: Decodificación
Para POS tagging, lo que nos interesa es: dada una oración, ¿cuál es la mejor secuencia de etiquetas?
Para el Problema 3 (Aprendizaje), en la práctica con datos etiquetados simplemente contamos (como hacíamos con modelos n-gram). Baum-Welch se usa cuando no tenemos etiquetas.
Entrenamiento Supervisado (Conteo)
Si tenemos un corpus anotado con POS tags, entrenar un HMM es simplemente contar:
Probabilidades de transición
\[P(t_j \mid t_i) = \frac{C(t_i, t_j)}{C(t_i)}\]
¿Cuántas veces la etiqueta \(t_j\) sigue a \(t_i\)?
Probabilidades de emisión
\[P(w_k \mid t_j) = \frac{C(t_j, w_k)}{C(t_j)}\]
¿Cuántas veces la palabra \(w_k\) aparece con la etiqueta \(t_j\)?
Estimación MLE — Igual que en modelos n-gram
Es exactamente la misma idea de Maximum Likelihood Estimation que vimos en la Sesión 1. La diferencia es que ahora contamos bigramas de etiquetas en vez de bigramas de palabras.
# Calcular probabilidades de emisiónprint("=== Probabilidades de Emisión P(w | t) ===")tag_count_emis = Counter()for _, tag in [pair for sent in corpus_anotado for pair in sent]: tag_count_emis[tag] +=1for tag insorted(set(t for t, _ in emis_count)):print(f"\n{tag}:")for (t, w), conteo insorted(emis_count.items()):if t == tag: prob = conteo / tag_count_emis[tag]print(f" P({w:<10} | {tag}) = {conteo}/{tag_count_emis[tag]} = {prob:.3f}")
\(w_1\) = “el”: ¿Con qué etiqueta empieza la oración?
Aplicamos: \(v_1(j) = \pi_j \cdot b_j(w_1)\)
Estado \(j\)
\(\pi_j\) (prob. inicial)
\(b_j(\text{el})\) (emisión)
\(v_1(j) = \pi_j \times b_j\)
Resultado
DET
0.8
0.6
\(0.8 \times 0.6\)
0.48
NOUN
0.1
0.0
\(0.1 \times 0.0\)
0.00
VERB
0.1
0.0
\(0.1 \times 0.0\)
0.00
. . .
Interpretación
“el” solo puede ser emitido por DET (emisión > 0), así que solo DET sobrevive con \(v_1 = 0.48\). Los otros estados quedan en 0.
Ejemplo a Mano: Paso 2 — Recursión
\(w_2\) = “gato”: Para cada estado, ¿quién es el mejor predecesor?
Para cada estado \(j\), hacemos una competencia entre todos los caminos que llegan a él:
Estado NOUN (el único interesante porque \(b_{\text{NOUN}}(\text{gato}) = 0.5 > 0\)):
Corredor desde…
\(v_1(i) \cdot a_{i,\text{NOUN}}\)
Score
DET
\(0.48 \times 0.9 = 0.432\)
🏆 Ganador
NOUN
\(0.00 \times 0.1 = 0.000\)
VERB
\(0.00 \times 0.5 = 0.000\)
Ganador: DET con score 0.432. Backpointer: \(\text{bp}_2(\text{NOUN}) = \text{DET}\)
Multiplicamos por emisión: \(v_2(\text{NOUN}) = 0.432 \times 0.5 = \mathbf{0.216}\)
. . .
Tabla completa del Paso 2:
Estado \(j\)
Mejor predecesor
Score transición
\(\times\) Emisión
\(v_2(j)\)
bp
DET
—
\(\max(0.48 \cdot 0.0, \dots) = 0\)
\(\times 0.0\)
0.000
—
NOUN
DET
\(0.48 \times 0.9 = 0.432\)
\(\times 0.5\)
0.216
DET
VERB
DET
\(0.48 \times 0.1 = 0.048\)
\(\times 0.0\)
0.000
—
Ejemplo a Mano: Paso 3 — Recursión
\(w_3\) = “come”: misma competencia para cada estado
Estado \(j\)
Mejor predecesor
Score transición
\(\times\) Emisión
\(v_3(j)\)
bp
DET
NOUN
\(0.216 \times 0.1 = 0.022\)
\(\times 0.0\)
0.000
—
NOUN
NOUN
\(0.216 \times 0.1 = 0.022\)
\(\times 0.0\)
0.000
—
VERB
NOUN
\(0.216 \times 0.8 = 0.173\)
\(\times 0.7\)
0.121
NOUN
. . .
Observación
Incluso DET y NOUN reciben score de transición > 0 desde NOUN, pero sus emisiones para “come” son 0, así que quedan eliminados. La emisión actúa como un filtro: no importa qué tan bueno sea el camino si la palabra no encaja con la etiqueta.
Ejemplo a Mano: Terminación
¿Cuál es el mejor estado final?
Aplicamos: \(\hat{t}_T = \arg\max_j \; v_T(j)\)
Estado \(j\)
\(v_3(j)\)
DET
0.000
NOUN
0.000
VERB
0.121
⭐ Ganador
\[\hat{t}_3 = \text{VERB}, \quad P^* = 0.121\]
. . .
¿Por qué es un paso separado?
Durante la recursión, mantenemos el mejor score para cada estado. Recién en la terminación decidimos cuál de todos los estados finales tiene el mejor camino global. Si la oración fuera más larga, podría haber varios estados con \(v_T > 0\) compitiendo.
Ejemplo a Mano: Backtracking
Reconstrucción: seguimos las “migas de pan” hacia atrás 🔙
Partimos del ganador en \(t=3\) y seguimos los backpointers:
✅ No hay underflow (sumamos números negativos manejables)
✅ El \(\arg\max\) no cambia (log es monótona creciente)
✅ Misma secuencia óptima, diferente representación
Espacio original
Log-espacio
Operación
\(\max(v \cdot a) \cdot b\)
\(\max(\log v + \log a) + \log b\)
Ejemplo (\(t=20\))
\(3.2 \times 10^{-15}\)
\(-14.5\)
Riesgo de underflow
⚠️ Sí
✅ No
Conexión con Sesión 2
Es exactamente la misma idea que usamos para calcular la perplejidad con log-probabilidades. En NLP, casi siempre trabajamos en log-espacio.
Bloque 6: Implementación en Python
HMM Completo en Python
Code
import numpy as npclass HMM_POS_Tagger:"""Modelo Oculto de Markov para etiquetado POS."""def__init__(self, smoothing=1e-6):self.smoothing = smoothingself.tags =set()self.vocab =set()self.trans_count = Counter()self.emis_count = Counter()self.tag_count = Counter()def entrenar(self, corpus_anotado):"""Entrena el HMM contando transiciones y emisiones."""for oracion in corpus_anotado: tags = ["<s>"] + [tag for _, tag in oracion]for i inrange(len(tags) -1):self.trans_count[(tags[i], tags[i+1])] +=1self.tag_count[tags[i]] +=1for palabra, tag in oracion:self.emis_count[(tag, palabra)] +=1self.tags.add(tag)self.vocab.add(palabra)self.tags =sorted(self.tags)self.tag_to_idx = {t: i for i, t inenumerate(self.tags)}def prob_transicion(self, t_prev, t_curr):"""P(t_curr | t_prev) con suavizado.""" num =self.trans_count[(t_prev, t_curr)] +self.smoothing den =self.tag_count[t_prev] +self.smoothing *len(self.tags)return num / dendef prob_emision(self, tag, palabra):"""P(palabra | tag) con suavizado.""" num =self.emis_count[(tag, palabra)] +self.smoothing den =sum(self.emis_count[(tag, w)] for w inself.vocab) +self.smoothing * (len(self.vocab) +1)return num / dendef viterbi(self, palabras):"""Algoritmo de Viterbi en LOG-ESPACIO para evitar underflow.""" T =len(palabras) N =len(self.tags)# Matrices de Viterbi (en log) y backpointers log_v = np.full((T, N), -np.inf) # -inf en log = 0 en prob bp = np.zeros((T, N), dtype=int)# Inicialización (t=0): log(π_j) + log(b_j(w_1))for j, tag inenumerate(self.tags): log_v[0, j] = (np.log(self.prob_transicion("<s>", tag))+ np.log(self.prob_emision(tag, palabras[0])))# Recursión: log_v[t,j] = max_i(log_v[t-1,i] + log(a_ij)) + log(b_j(w_t))for t inrange(1, T):for j, tag_j inenumerate(self.tags): scores = np.array([ log_v[t-1, i] + np.log(self.prob_transicion(tag_i, tag_j))for i, tag_i inenumerate(self.tags) ]) bp[t, j] = np.argmax(scores) log_v[t, j] = scores[bp[t, j]] + np.log(self.prob_emision(tag_j, palabras[t]))# Terminación: mejor estado final best_last = np.argmax(log_v[T-1])# Backtracking best_path = [best_last]for t inrange(T-1, 0, -1): best_path.append(bp[t, best_path[-1]]) best_path.reverse()return [self.tags[i] for i in best_path]# Entrenar el modelohmm = HMM_POS_Tagger()hmm.entrenar(corpus_anotado)# Etiquetar oraciones de pruebatests = ["el gato come pescado","un perro bebe agua","la gata duerme","el gato bebe leche",]print("=== Resultados de POS Tagging con HMM ===\n")for oracion in tests: palabras = oracion.split() tags = hmm.viterbi(palabras) resultado =" | ".join(f"{w}/{t}"for w, t inzip(palabras, tags))print(f" {oracion}")print(f" → {resultado}\n")
=== Resultados de POS Tagging con HMM ===
el gato come pescado
→ el/DET | gato/NOUN | come/VERB | pescado/NOUN
un perro bebe agua
→ un/DET | perro/NOUN | bebe/VERB | agua/NOUN
la gata duerme
→ la/DET | gata/NOUN | duerme/VERB
el gato bebe leche
→ el/DET | gato/NOUN | bebe/VERB | leche/NOUN
Evaluación del Modelo
# Evaluar en el corpus de entrenamiento (para verificar)correctas =0total =0for oracion in corpus_anotado: palabras = [w for w, _ in oracion] tags_gold = [t for _, t in oracion] tags_pred = hmm.viterbi(palabras)for gold, pred inzip(tags_gold, tags_pred):if gold == pred: correctas +=1 total +=1accuracy = correctas / total *100print(f"Accuracy en corpus de entrenamiento: {correctas}/{total} = {accuracy:.1f}%")
Accuracy en corpus de entrenamiento: 19/19 = 100.0%
Nota
La accuracy en el corpus de entrenamiento debería ser muy alta. Lo importante es cómo generaliza a datos no vistos (palabras desconocidas, combinaciones nuevas).
HMM con spaCy (Mundo Real)
En la práctica, no implementamos HMM desde cero. Herramientas como spaCy usan modelos más avanzados, pero podemos comparar:
Code
import spacytry: nlp = spacy.load("es_core_news_sm")exceptOSError:print("Descargando modelo de spaCy para español...") spacy.cli.download("es_core_news_sm") nlp = spacy.load("es_core_news_sm")oraciones = ["El gato come pescado fresco","Bajo la mesa hay un gato bajo","Yo como pizza y ella come ensalada","El canto del ave es hermoso",]print("=== POS Tagging con spaCy ===\n")for oracion in oraciones: doc = nlp(oracion) resultado =" | ".join(f"{t.text}/{t.pos_}"for t in doc)print(f" {oracion}")print(f" → {resultado}\n")
=== POS Tagging con spaCy ===
El gato come pescado fresco
→ El/DET | gato/NOUN | come/VERB | pescado/ADJ | fresco/ADJ
Bajo la mesa hay un gato bajo
→ Bajo/ADP | la/DET | mesa/NOUN | hay/AUX | un/DET | gato/NOUN | bajo/ADP
Yo como pizza y ella come ensalada
→ Yo/PRON | como/SCONJ | pizza/PROPN | y/CCONJ | ella/PRON | come/VERB | ensalada/ADJ
El canto del ave es hermoso
→ El/DET | canto/NOUN | del/ADP | ave/NOUN | es/AUX | hermoso/ADJ
Comparación: Nuestro HMM vs. spaCy
Code
# Comparar en una oración del vocabulario de entrenamientooracion_comun ="el gato come pescado"palabras = oracion_comun.split()# Nuestro HMMtags_hmm = hmm.viterbi(palabras)# spaCydoc = nlp(oracion_comun)tags_spacy = [t.pos_ for t in doc]print(f"Oración: \"{oracion_comun}\"\n")print(f"{'Palabra':<12}{'HMM':>8}{'spaCy':>8}")print("-"*30)for w, t_hmm, t_spacy inzip(palabras, tags_hmm, tags_spacy): match ="✅"if t_hmm == t_spacy else"❌"print(f"{w:<12}{t_hmm:>8}{t_spacy:>8}{match}")
Oración: "el gato come pescado"
Palabra HMM spaCy
------------------------------
el DET DET ✅
gato NOUN NOUN ✅
come VERB VERB ✅
pescado NOUN ADJ ❌
Modelos modernos
spaCy usa redes neuronales (no HMM), pero la intuición es la misma: aprender patrones de secuencias de etiquetas y sus relaciones con las palabras.
Palabras Desconocidas (OOV)
El problema
¿Qué pasa si aparece una palabra que nunca vimos en entrenamiento?
¡La misma catástrofe del cero que vimos con modelos n-gram!
Soluciones (aplicamos suavizado)
Suavizado: Ya lo hicimos con smoothing=1e-6
Características morfológicas:
Termina en “-ción” → NOUN
Termina en “-mente” → ADV
Empieza con mayúscula → PROPN
Token <UNK>: Reemplazar palabras raras por un token especial durante el entrenamiento
Conexión
Igual que en la Sesión 2 usamos suavizado para modelos n-gram de palabras, aquí lo usamos para las probabilidades de emisión del HMM. ¡La misma idea, diferente aplicación!
Resumen
Lo Que Aprendimos Hoy
Conceptos
POS Tagging asigna categorías gramaticales a palabras
Los HMM modelan secuencias de estados ocultos (tags) con observaciones (palabras)
Un HMM tiene: estados, vocabulario, transiciones \(A\), emisiones \(B\), e iniciales \(\pi\)
El entrenamiento supervisado es simplemente contar (como modelos n-gram)
Algoritmos
Viterbi encuentra la mejor secuencia de tags usando programación dinámica
Complejidad: \(O(T \cdot N^2)\) vs. fuerza bruta \(O(N^T)\)
El suavizado resuelve el problema de palabras/transiciones no vistas
graph LR A["Semana 1<br>Preprocesamiento<br>Regex, Tokenización"] --> B["Semana 2<br>Representación<br>BoW, TF-IDF, PMI"] B --> C["Semana 3<br>Modelo de Lenguaje<br>N-gram, PP, Suavizado"] C --> D["S3: HMM<br>POS Tagging<br>Viterbi ✅"] D --> E["Semana 4<br>Word2Vec<br>GloVe, FastText"] style D fill:#2a9d8f,color:#fff,stroke:#fff,stroke-width:2px style E fill:#e9c46a,color:#000
graph LR
A["Semana 1<br>Preprocesamiento<br>Regex, Tokenización"] --> B["Semana 2<br>Representación<br>BoW, TF-IDF, PMI"]
B --> C["Semana 3<br>Modelo de Lenguaje<br>N-gram, PP, Suavizado"]
C --> D["S3: HMM<br>POS Tagging<br>Viterbi ✅"]
D --> E["Semana 4<br>Word2Vec<br>GloVe, FastText"]
style D fill:#2a9d8f,color:#fff,stroke:#fff,stroke-width:2px
style E fill:#e9c46a,color:#000
Conexión con lo que viene
Semana 4: De representaciones discretas (One-hot, BoW) a representaciones densas (Word2Vec, GloVe)
Los embeddings resolverán muchas limitaciones que vimos (dispersión, falta de semántica)
HMM → CRF → BiLSTM-CRF → Transformers: la evolución del etiquetado de secuencias
Para la Próxima Sesión 📚
Semana 4, Sesión 1: Word2Vec
Skip-gram y CBOW
Aprender representaciones de palabras desde corpus no anotados
¡De vectores dispersos a vectores densos!
Lectura:
Jurafsky & Martin, Cap. 6: Vector Semantics and Embeddings