556 KiB
Executable File
556 KiB
Executable File
In [34]:
# Instalación de dependencias
# gensim >= 4.3.3 es necesario por compatibilidad con las versiones recientes de scipy/numpy
!pip install -q "gensim>=4.3.3" 2>/dev/null
print("Instalación terminada.")Instalación terminada.
In [35]:
import os, re, time, random, itertools, warnings
warnings.filterwarnings("ignore")
# Reproducibilidad (ver nota al final del laboratorio sobre determinismo real)
SEMILLA = 42
os.environ["PYTHONHASHSEED"] = str(SEMILLA)
random.seed(SEMILLA)
import numpy as np
np.random.seed(SEMILLA)
import matplotlib.pyplot as plt
import pandas as pd
try:
import gensim
from gensim.models import Word2Vec
from gensim.models.callbacks import CallbackAny2Vec
import gensim.downloader as api
except Exception as e:
raise SystemExit(
"No se pudo importar gensim. Reinicia el entorno de ejecución "
"(Entorno de ejecución -> Reiniciar sesión) y ejecuta de nuevo esta celda.\n"
f"Error original: {e}"
)
print("gensim:", gensim.__version__)
print("numpy :", np.__version__)
# ------------------------------------------------------------------
# Configuración global del laboratorio (puedes tocar estos valores)
# ------------------------------------------------------------------
CONFIG = {
"workers": 2, # hilos de CPU (Colab suele dar 2)
"epocas": 5, # épocas para el modelo principal
"n_frases_completo": None, # None = usar todo text8 (~17M palabras)
"n_frases_rapido": 400, # subconjunto para los experimentos (~4M palabras)
}
# ------------------------------------------------------------------
# Mini framework de tests para autoevaluarte
# ------------------------------------------------------------------
def test(nombre, fn):
"""Ejecuta fn() e informa del resultado sin detener el notebook."""
try:
fn()
print(f"[OK] {nombre}")
except NotImplementedError:
print(f"[PENDIENTE] {nombre}: sin implementar")
except AssertionError as e:
print(f"[FALLO] {nombre}: {e}")
except Exception as e:
print(f"[ERROR] {nombre}: {type(e).__name__}: {e}")
print("\\nEntorno preparado.")gensim: 4.4.0 numpy : 2.1.0 \nEntorno preparado.
In [36]:
t0 = time.time()
# Descargamos el fichero y lo leemos con Text8Corpus.
# (Nota: usamos return_path=True a propósito. El "loader" que gensim-data trae para text8
# contiene un import obsoleto de smart_open y falla con las versiones actuales de la librería;
# pedir la ruta y leer el fichero nosotros mismos es equivalente y no se rompe.)
from gensim.models.word2vec import Text8Corpus
ruta = api.load("text8", return_path=True) # ~31 MB la primera vez
frases = list(Text8Corpus(ruta)) # cada elemento = lista de hasta 10 000 tokens
print(f"Descarga + carga: {time.time()-t0:.1f} s")
n_tokens = sum(len(f) for f in frases)
print(f"Número de 'frases' (bloques de 10 000 tokens): {len(frases):,}")
print(f"Número total de tokens: {n_tokens:,}")
print(f"Vocabulario bruto (tipos distintos): {len(set(itertools.chain.from_iterable(frases))):,}")
print("\\nPrimeros 40 tokens del corpus:")
print(frases[0][:40])Descarga + carga: 1.1 s Número de 'frases' (bloques de 10 000 tokens): 1,701 Número total de tokens: 17,005,207 Vocabulario bruto (tipos distintos): 253,854 \nPrimeros 40 tokens del corpus: ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against', 'early', 'working', 'class', 'radicals', 'including', 'the', 'diggers', 'of', 'the', 'english', 'revolution', 'and', 'the', 'sans', 'culottes', 'of', 'the', 'french', 'revolution', 'whilst', 'the', 'term', 'is', 'still', 'used', 'in', 'a', 'pejorative', 'way', 'to']
In [37]:
from collections import Counter
frec = Counter(itertools.chain.from_iterable(frases))
print("10 palabras más frecuentes:", frec.most_common(10))
print("Palabras que aparecen 1 sola vez:", sum(1 for w, c in frec.items() if c == 1), "tipos")
rangos = np.arange(1, 10001)
cuentas = np.array([c for _, c in frec.most_common(10000)])
plt.figure(figsize=(7, 4))
plt.loglog(rangos, cuentas)
plt.xlabel("Rango de la palabra (log)")
plt.ylabel("Frecuencia (log)")
plt.title("Ley de Zipf en text8")
plt.grid(True, which="both", alpha=0.3)
plt.show()10 palabras más frecuentes: [('the', 1061396), ('of', 593677), ('and', 416629), ('one', 411764), ('in', 372201), ('a', 325873), ('to', 316376), ('zero', 264975), ('nine', 250430), ('two', 192644)]
Palabras que aparecen 1 sola vez: 118519 tipos
In [38]:
STOPWORDS = frozenset("""
el la los las un una unos unas de del al a ante bajo con contra desde durante en entre hacia
hasta mediante para por segun sin sobre tras y o u ni que se su sus lo le les es son era eran
ser fue han he ha muy mas pero como cuando donde quien cual esta este esto estos estas
the a an and or but of to in on at for with from by as is are was were be been being this
that these those it its his her their our your not no so if then than there here what which
who whom will would can could should do does did have has had
""".split())
print(len(STOPWORDS), "stopwords cargadas")118 stopwords cargadas
In [39]:
def preprocesar(texto, quitar_stopwords=True, min_len=2):
"""Convierte una cadena de texto en una lista de tokens limpios.
Args:
texto (str): texto crudo.
quitar_stopwords (bool): si True, elimina las palabras de STOPWORDS.
min_len (int): longitud mínima de token que se conserva.
Returns:
list[str]: lista de tokens.
"""
texto = texto.lower()
# Conserva letras (incluye acentos y ñ) y espacios; el resto se convierte en espacio.
texto = re.sub(r"[^a-zà-öø-ÿñ\s]", " ", texto)
tokens = texto.split()
tokens = [t for t in tokens if len(t) >= min_len]
if quitar_stopwords:
tokens = [t for t in tokens if t not in STOPWORDS]
return tokens
In [40]:
# --- Verificación automática del Ejercicio 1 ---
def _t1():
out = preprocesar("El GATO, el Perro y 3 gatos!!!")
assert isinstance(out, list) and all(isinstance(t, str) for t in out), "debe devolver list[str]"
assert out == ["gato", "perro", "gatos"], f"esperaba ['gato','perro','gatos'], obtuve {out}"
def _t2():
out = preprocesar("La NIÑA come MANZANAS en Málaga", quitar_stopwords=False)
assert "niña" in out, "los acentos y la ñ deben conservarse"
assert "málaga" in out, "las palabras con acento deben conservarse enteras"
def _t3():
out = preprocesar("a b cd efg", quitar_stopwords=False, min_len=3)
assert out == ["efg"], f"min_len no se aplica bien: {out}"
def _t4():
assert preprocesar("") == [], "un texto vacío debe dar lista vacía"
for n, f in [("Ej1.a limpieza básica", _t1), ("Ej1.b acentos y ñ", _t2),
("Ej1.c min_len", _t3), ("Ej1.d caso vacío", _t4)]:
test(n, f)[OK] Ej1.a limpieza básica [OK] Ej1.b acentos y ñ [OK] Ej1.c min_len [OK] Ej1.d caso vacío
In [41]:
def generar_pares_skipgram(tokens, ventana=2):
"""Genera los pares (centro, contexto) de Skip-gram.
Recorre la lista de tokens y, para cada posición i, empareja tokens[i]
con cada token dentro de la ventana [i-ventana, i+ventana], excluyendo i.
La ventana se recorta en los bordes de la secuencia.
Args:
tokens (list[str]): secuencia de tokens.
ventana (int): radio de la ventana de contexto.
Returns:
list[tuple[str, str]]: pares (centro, contexto) en orden de aparición.
"""
pares = []
n = len(tokens)
for i in range(n):
inicio = max(0, i - ventana)
fin = min(n, i + ventana + 1)
for j in range(inicio, fin):
if j == i:
continue
pares.append((tokens[i], tokens[j]))
return pares
def generar_pares_cbow(tokens, ventana=2):
"""Genera los ejemplos de CBOW: (lista_de_contexto, centro).
Returns:
list[tuple[list[str], str]]
"""
ejemplos = []
n = len(tokens)
for i in range(n):
inicio = max(0, i - ventana)
fin = min(n, i + ventana + 1)
contexto = [tokens[j] for j in range(inicio, fin) if j != i]
ejemplos.append((contexto, tokens[i]))
return ejemplos
In [42]:
# --- Verificación automática del Ejercicio 2 ---
DEMO = ["el", "gato", "salta", "sobre", "la", "mesa"]
def _t1():
pares = generar_pares_skipgram(["a", "b", "c", "d"], ventana=1)
esperado = [("a","b"),("b","a"),("b","c"),("c","b"),("c","d"),("d","c")]
assert pares == esperado, f"esperaba {esperado}, obtuve {pares}"
def _t2():
pares = generar_pares_skipgram(DEMO, ventana=2)
# 6 tokens, ventana 2 -> 2+3+4+4+3+2 = 18 pares
assert len(pares) == 18, f"esperaba 18 pares, obtuve {len(pares)}"
assert ("salta", "el") in pares and ("salta", "la") in pares
assert ("salta", "salta") not in pares, "un token no puede ser su propio contexto"
def _t3():
ej = generar_pares_cbow(["a","b","c","d"], ventana=1)
esperado = [(["b"],"a"), (["a","c"],"b"), (["b","d"],"c"), (["c"],"d")]
assert ej == esperado, f"esperaba {esperado}, obtuve {ej}"
def _t4():
assert generar_pares_skipgram(["solo"], ventana=3) == [], "un único token no genera pares"
for n, f in [("Ej2.a skip-gram simple", _t1), ("Ej2.b skip-gram ventana 2", _t2),
("Ej2.c cbow", _t3), ("Ej2.d caso borde", _t4)]:
test(n, f)[OK] Ej2.a skip-gram simple [OK] Ej2.b skip-gram ventana 2 [OK] Ej2.c cbow [OK] Ej2.d caso borde
In [43]:
# Inspecciona el dataset que acabas de construir
try:
pares = generar_pares_skipgram(DEMO, ventana=2)
print(f"{len(pares)} pares a partir de {len(DEMO)} tokens\\n")
for centro, ctx in pares[:8]:
print(f" centro={centro:<7} contexto={ctx}")
# Escalado: ¿cuántos pares generaría el corpus completo?
pares_por_token = len(pares) / len(DEMO)
print(f"\\nPares por token (ventana=2): ~{pares_por_token:.1f}")
print(f"Pares estimados en text8 completo: ~{pares_por_token * n_tokens/1e6:.0f} millones")
print("Este volumen explica por qué se usa negative sampling y no un softmax")
print("sobre todo el vocabulario.")
except NotImplementedError:
print("Completa el Ejercicio 2 para ver esta celda.")18 pares a partir de 6 tokens\n centro=el contexto=gato centro=el contexto=salta centro=gato contexto=el centro=gato contexto=salta centro=gato contexto=sobre centro=salta contexto=el centro=salta contexto=gato centro=salta contexto=sobre \nPares por token (ventana=2): ~3.0 Pares estimados en text8 completo: ~51 millones Este volumen explica por qué se usa negative sampling y no un softmax sobre todo el vocabulario.
In [44]:
class Progreso(CallbackAny2Vec):
"""Callback que informa del tiempo y la pérdida al final de cada época."""
def __init__(self):
self.epoca = 0
self.perdida_acumulada = 0.0
self.historial = []
self.t0 = None
def on_epoch_begin(self, model):
self.t0 = time.time()
def on_epoch_end(self, model):
self.epoca += 1
total = model.get_latest_training_loss() # es acumulada
delta = total - self.perdida_acumulada # -> pérdida de esta época
self.perdida_acumulada = total
dt = time.time() - self.t0
self.historial.append({"epoca": self.epoca, "perdida": delta, "segundos": dt})
print(f" época {self.epoca}: pérdida={delta:,.0f} ({dt:.1f} s)")
progreso = Progreso()
print("Callback listo.")Callback listo.
In [45]:
t0 = time.time()
modelo = Word2Vec(
sentences=frases,
vector_size=100,
window=5,
min_count=5,
sg=1,
negative=5,
sample=1e-3,
epochs=CONFIG["epocas"],
workers=CONFIG["workers"],
seed=SEMILLA,
compute_loss=True,
callbacks=[progreso],
)
print(f"\nEntrenamiento total: {(time.time()-t0)/60:.1f} min")
print(f"Vocabulario final: {len(modelo.wv):,} palabras")
print(f"Forma de la matriz de embeddings: {modelo.wv.vectors.shape}")
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
época 1: pérdida=48,672,852 (26.5 s)
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
época 2: pérdida=19,068,380 (28.1 s)
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
época 3: pérdida=1,495,560 (23.1 s)
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
época 4: pérdida=1,352,584 (23.3 s)
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
época 5: pérdida=1,128,728 (28.2 s) Entrenamiento total: 2.2 min Vocabulario final: 71,290 palabras Forma de la matriz de embeddings: (71290, 100)
In [46]:
# --- Verificación automática del Ejercicio 3 ---
def _t1():
assert "modelo" in globals(), "no existe la variable 'modelo'"
assert modelo.wv.vector_size == 100, f"vector_size debe ser 100, es {modelo.wv.vector_size}"
def _t2():
assert modelo.sg == 1, "sg debe ser 1 (Skip-gram)"
assert modelo.window == 5, "window debe ser 5"
assert modelo.negative == 5, "negative debe ser 5"
def _t3():
assert 40_000 < len(modelo.wv) < 120_000, (
f"vocabulario sospechoso ({len(modelo.wv)}): revisa min_count")
def _t4():
v = modelo.wv["king"]
assert v.shape == (100,), f"el vector de 'king' debería tener forma (100,), tiene {v.shape}"
assert np.linalg.norm(v) > 0, "el vector no puede ser nulo"
for n, f in [("Ej3.a modelo entrenado", _t1), ("Ej3.b hiperparámetros", _t2),
("Ej3.c tamaño de vocabulario", _t3), ("Ej3.d vectores válidos", _t4)]:
test(n, f)[OK] Ej3.a modelo entrenado [OK] Ej3.b hiperparámetros [OK] Ej3.c tamaño de vocabulario [OK] Ej3.d vectores válidos
In [47]:
# Curva de pérdida
if progreso.historial:
h = pd.DataFrame(progreso.historial)
display(h)
plt.figure(figsize=(6, 4))
plt.plot(h["epoca"], h["perdida"], marker="o")
plt.xlabel("Época"); plt.ylabel("Pérdida de la época")
plt.title("Curva de entrenamiento (SGNS)")
plt.grid(alpha=0.3); plt.show()
else:
print("No hay historial: ¿pasaste compute_loss=True y callbacks=[progreso]?")| epoca | perdida | segundos | |
|---|---|---|---|
| 0 | 1 | 48672852.0 | 26.538724 |
| 1 | 2 | 19068380.0 | 28.138281 |
| 2 | 3 | 1495560.0 | 23.067414 |
| 3 | 4 | 1352584.0 | 23.335629 |
| 4 | 5 | 1128728.0 | 28.173413 |
In [48]:
def vecinos(palabra, topn=8):
if palabra not in modelo.wv:
print(f"'{palabra}' no está en el vocabulario"); return
print(f"\\nVecinos de '{palabra}':")
for w, s in modelo.wv.most_similar(palabra, topn=topn):
print(f" {w:<18} {s:.3f}")
for p in ["king", "paris", "computer", "music", "three", "physics"]:
vecinos(p)\nVecinos de 'king':
prince 0.762
pretender 0.735
queen 0.732
valdemar 0.728
kings 0.726
haakon 0.725
canute 0.722
stadtholder 0.709
\nVecinos de 'paris':
rodin 0.742
conservatoire 0.728
brussels 0.728
montparnasse 0.723
universelle 0.722
bologna 0.720
france 0.715
cimeti 0.715
\nVecinos de 'computer':
computers 0.807
hardware 0.773
computing 0.769
networked 0.762
bootstrap 0.746
pdas 0.744
hypermedia 0.741
minicomputer 0.730
\nVecinos de 'music':
musical 0.802
jazz 0.791
folk 0.790
reggae 0.776
electronica 0.769
dance 0.765
pop 0.751
improvisation 0.748
\nVecinos de 'three':
four 0.980
five 0.971
two 0.963
six 0.959
seven 0.939
one 0.936
eight 0.929
zero 0.897
\nVecinos de 'physics':
electrodynamics 0.795
mechanics 0.777
chemistry 0.776
electromagnetism 0.771
quantum 0.754
astrophysics 0.743
feynman 0.729
electrochemistry 0.722
In [49]:
# Similitud entre pares: ¿coincide con tu intuición?
pares_test = [("king","queen"), ("king","man"), ("king","banana"),
("paris","france"), ("paris","london"), ("dog","cat"), ("dog","car")]
df_sim = pd.DataFrame(
[(a, b, modelo.wv.similarity(a, b)) for a, b in pares_test],
columns=["palabra_1", "palabra_2", "similitud_coseno"]
).sort_values("similitud_coseno", ascending=False)
display(df_sim)
# El intruso
for grupo in [["breakfast","lunch","dinner","paris"],
["red","blue","green","dog"],
["monday","tuesday","january","wednesday"]]:
print(f"{grupo} → intruso: {modelo.wv.doesnt_match(grupo)}")| palabra_1 | palabra_2 | similitud_coseno | |
|---|---|---|---|
| 0 | king | queen | 0.731937 |
| 3 | paris | france | 0.715271 |
| 5 | dog | cat | 0.686618 |
| 4 | paris | london | 0.565996 |
| 6 | dog | car | 0.389345 |
| 1 | king | man | 0.374814 |
| 2 | king | banana | 0.168914 |
['breakfast', 'lunch', 'dinner', 'paris'] → intruso: paris ['red', 'blue', 'green', 'dog'] → intruso: dog ['monday', 'tuesday', 'january', 'wednesday'] → intruso: january
In [50]:
def analogia(a, b, c, kv=None, topn=3):
"""Resuelve la analogía a : b :: c : ?
Es decir, busca las palabras más parecidas a (b - a + c).
Args:
a, b, c (str): palabras de la analogía.
kv: los KeyedVectors a usar (por defecto, modelo.wv).
topn (int): número de candidatos a devolver.
Returns:
list[tuple[str, float]]: candidatos (palabra, similitud).
Debe devolver [] si alguna de las tres palabras no está en el vocabulario.
"""
kv = modelo.wv if kv is None else kv
if a not in kv or b not in kv or c not in kv:
return []
return kv.most_similar(positive=[b, c], negative=[a], topn=topn)
In [51]:
# --- Verificación automática del Ejercicio 4a ---
def _t1():
r = analogia("man", "king", "woman", topn=3)
assert isinstance(r, list) and len(r) == 3, f"debe devolver 3 tuplas, devolvió {r}"
assert isinstance(r[0], tuple) and isinstance(r[0][0], str)
def _t2():
r = analogia("man", "king", "woman", topn=5)
palabras = [w for w, _ in r]
assert "queen" in palabras, f"'queen' debería estar en el top-5; obtuve {palabras}"
def _t3():
assert analogia("man", "king", "xyzzyqwe") == [], "palabra fuera de vocabulario -> []"
def _t4():
r = analogia("man", "king", "woman", topn=3)
assert "king" not in [w for w, _ in r], "most_similar debe excluir las palabras de entrada"
for n, f in [("Ej4.a formato", _t1), ("Ej4.b king-man+woman=queen", _t2),
("Ej4.c fuera de vocabulario", _t3), ("Ej4.d exclusión de entradas", _t4)]:
test(n, f)[OK] Ej4.a formato [OK] Ej4.b king-man+woman=queen [OK] Ej4.c fuera de vocabulario [OK] Ej4.d exclusión de entradas
In [52]:
# Batería de analogías: semánticas y sintácticas
ANALOGIAS = [
# (a, b, c, respuesta_esperada)
("man", "king", "woman", "queen"),
("france", "paris", "italy", "rome"),
("france", "paris", "japan", "tokyo"),
("spain", "madrid", "germany", "berlin"),
("boy", "brother", "girl", "sister"),
("man", "he", "woman", "she"),
("good", "better", "bad", "worse"),
("walk", "walking", "swim", "swimming"),
("big", "biggest", "small", "smallest"),
("mouse", "mice", "dog", "dogs"),
("copper", "cu", "silver", "ag"),
("einstein", "physics", "picasso", "painting"),
]
def evaluar_analogias(kv, analogias=ANALOGIAS, topn=5, verbose=True):
"""Devuelve (exactitud@1, exactitud@topn, tabla de resultados)."""
filas, top1, topk, evaluadas = [], 0, 0, 0
for a, b, c, esperada in analogias:
cand = analogia(a, b, c, kv=kv, topn=topn)
if not cand:
filas.append({"analogia": f"{a}:{b}::{c}:?", "esperada": esperada,
"prediccion": "—(OOV)", "acierto@1": None, f"acierto@{topn}": None})
continue
evaluadas += 1
palabras = [w for w, _ in cand]
ok1 = palabras[0] == esperada
okk = esperada in palabras
top1 += ok1; topk += okk
filas.append({"analogia": f"{a}:{b}::{c}:?", "esperada": esperada,
"prediccion": palabras[0], "acierto@1": ok1, f"acierto@{topn}": okk})
tabla = pd.DataFrame(filas)
acc1 = top1 / evaluadas if evaluadas else 0.0
acck = topk / evaluadas if evaluadas else 0.0
if verbose:
display(tabla)
print(f"Exactitud@1 = {acc1:.1%} ({top1}/{evaluadas})")
print(f"Exactitud@{topn} = {acck:.1%} ({topk}/{evaluadas})")
return acc1, acck, tabla
try:
acc1_base, acc5_base, _ = evaluar_analogias(modelo.wv)
except NotImplementedError:
print("Completa el Ejercicio 4a primero.")| analogia | esperada | prediccion | acierto@1 | acierto@5 | |
|---|---|---|---|---|---|
| 0 | man:king::woman:? | queen | queen | True | True |
| 1 | france:paris::italy:? | rome | venice | False | False |
| 2 | france:paris::japan:? | tokyo | tokyo | True | True |
| 3 | spain:madrid::germany:? | berlin | berlin | True | True |
| 4 | boy:brother::girl:? | sister | wife | False | False |
| 5 | man:he::woman:? | she | she | True | True |
| 6 | good:better::bad:? | worse | worse | True | True |
| 7 | walk:walking::swim:? | swimming | walkers | False | False |
| 8 | big:biggest::small:? | smallest | largest | False | False |
| 9 | mouse:mice::dog:? | dogs | dogs | True | True |
| 10 | copper:cu::silver:? | ag | mj | False | False |
| 11 | einstein:physics::picasso:? | painting | nouveau | False | False |
Exactitud@1 = 50.0% (6/12) Exactitud@5 = 50.0% (6/12)
In [53]:
frases_rapido = frases[:CONFIG["n_frases_rapido"]]
print(f"Subcorpus: {len(frases_rapido)} bloques ≈ {sum(len(f) for f in frases_rapido):,} tokens")
def entrenar(frases_in, **kwargs):
"""Entrena un Word2Vec con valores por defecto sensatos + los kwargs dados.
Devuelve (modelo, segundos)."""
base = dict(vector_size=100, window=5, min_count=5, negative=5, sample=1e-3,
epochs=CONFIG["epocas"], workers=CONFIG["workers"], seed=SEMILLA)
base.update(kwargs)
t = time.time()
m = Word2Vec(sentences=frases_in, **base)
return m, time.time() - t
print("Helper 'entrenar' listo.")Subcorpus: 400 bloques ≈ 4,000,000 tokens Helper 'entrenar' listo.
In [54]:
resultados_arq = []
for nombre, sg in [("CBOW", 0), ("Skip-gram", 1)]:
print(f"Entrenando {nombre}...")
m, segundos = entrenar(frases_rapido, sg=sg)
acc1, acc5, _ = evaluar_analogias(m.wv, verbose=False)
resultados_arq.append({
"arquitectura": nombre,
"segundos": segundos,
"vocabulario": len(m.wv),
"acc@1": acc1,
"acc@5": acc5,
})
df_arq = pd.DataFrame(resultados_arq)
display(df_arq)
Entrenando CBOW...
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
Entrenando Skip-gram...
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
| arquitectura | segundos | vocabulario | acc@1 | acc@5 | |
|---|---|---|---|---|---|
| 0 | CBOW | 6.882106 | 32080 | 0.250000 | 0.416667 |
| 1 | Skip-gram | 25.492793 | 32080 | 0.166667 | 0.250000 |
In [55]:
# --- Verificación automática del Ejercicio 5 ---
def _t1():
assert len(resultados_arq) == 2, "debe haber 2 filas (CBOW y Skip-gram)"
def _t2():
claves = {"arquitectura", "segundos", "vocabulario", "acc@1", "acc@5"}
assert claves.issubset(resultados_arq[0].keys()), f"faltan claves: {claves - set(resultados_arq[0])}"
def _t3():
assert all(r["segundos"] > 0 for r in resultados_arq), "los tiempos deben ser positivos"
for n, f in [("Ej5.a dos configuraciones", _t1), ("Ej5.b claves correctas", _t2),
("Ej5.c tiempos medidos", _t3)]:
test(n, f)[OK] Ej5.a dos configuraciones [OK] Ej5.b claves correctas [OK] Ej5.c tiempos medidos
In [56]:
try:
fig, ax = plt.subplots(1, 2, figsize=(11, 4))
ax[0].bar(df_arq["arquitectura"], df_arq["segundos"])
ax[0].set_ylabel("segundos"); ax[0].set_title("Tiempo de entrenamiento")
x = np.arange(len(df_arq))
ax[1].bar(x - 0.2, df_arq["acc@1"], width=0.4, label="acc@1")
ax[1].bar(x + 0.2, df_arq["acc@5"], width=0.4, label="acc@5")
ax[1].set_xticks(x); ax[1].set_xticklabels(df_arq["arquitectura"])
ax[1].set_ylabel("exactitud"); ax[1].set_title("Analogías"); ax[1].legend()
plt.tight_layout(); plt.show()
except NameError:
print("Completa el Ejercicio 5 primero.")In [57]:
resultados_ventana = []
modelos_ventana = {}
for w in [2, 5, 10]:
print(f"Entrenando window={w}...")
m, segundos = entrenar(frases_rapido, sg=1, window=w)
acc1, acc5, _ = evaluar_analogias(m.wv, verbose=False)
resultados_ventana.append({
"window": w,
"segundos": segundos,
"vocabulario": len(m.wv),
"acc@1": acc1,
"acc@5": acc5,
})
modelos_ventana[w] = m
df_ventana = pd.DataFrame(resultados_ventana)
display(df_ventana)
plt.figure(figsize=(6, 4))
plt.plot(df_ventana["window"], df_ventana["acc@1"], marker="o", label="acc@1")
plt.plot(df_ventana["window"], df_ventana["acc@5"], marker="s", label="acc@5")
plt.xlabel("window"); plt.ylabel("exactitud en analogías")
plt.title("Efecto del tamaño de ventana"); plt.legend(); plt.grid(alpha=0.3); plt.show()
Entrenando window=2...
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
Entrenando window=5...
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
Entrenando window=10...
Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float' Exception ignored in: 'gensim.models.word2vec_inner.our_dot_float'
| window | segundos | vocabulario | acc@1 | acc@5 | |
|---|---|---|---|---|---|
| 0 | 2 | 13.969611 | 32080 | 0.250000 | 0.416667 |
| 1 | 5 | 25.849061 | 32080 | 0.083333 | 0.250000 |
| 2 | 10 | 46.226222 | 32080 | 0.416667 | 0.583333 |
In [58]:
# Ejercicio 6b: compara cualitativamente los vecinos con ventana pequeña y grande.
for w in [2, 10]:
print(f"\n=== window={w} ===")
for palabra in ["paris", "physics"]:
print(f"\nVecinos de '{palabra}' (window={w}):")
for vecino, sim in modelos_ventana[w].wv.most_similar(palabra, topn=8):
print(f" {vecino:<18} {sim:.3f}")
=== window=2 ===
Vecinos de 'paris' (window=2):
zurich 0.757
bologna 0.756
venice 0.751
vienna 0.747
lyon 0.743
perth 0.741
dublin 0.733
francisco 0.730
Vecinos de 'physics' (window=2):
mathematics 0.835
chemistry 0.816
mechanics 0.788
mathematical 0.774
logic 0.762
sciences 0.759
geometry 0.751
relativity 0.735
=== window=10 ===
Vecinos de 'paris' (window=10):
sur 0.734
des 0.726
mie 0.701
france 0.699
rodin 0.672
recherche 0.671
vevey 0.669
nuremberg 0.668
Vecinos de 'physics' (window=10):
mechanics 0.806
quantum 0.752
bose 0.744
condensates 0.743
electromagnetism 0.737
chemistry 0.721
theoretical 0.718
gravitation 0.717
In [59]:
# --- Verificación automática del Ejercicio 6 ---
def _t1():
assert len(resultados_ventana) == 3, "debe haber 3 configuraciones de ventana"
def _t2():
assert sorted(r["window"] for r in resultados_ventana) == [2, 5, 10]
def _t3():
assert set(modelos_ventana) >= {2, 10}, "guarda los modelos de window=2 y window=10"
for n, f in [("Ej6.a barrido completo", _t1), ("Ej6.b valores de window", _t2),
("Ej6.c modelos guardados", _t3)]:
test(n, f)[OK] Ej6.a barrido completo [OK] Ej6.b valores de window [OK] Ej6.c modelos guardados
In [60]:
GRUPOS = {
"países": ["france", "germany", "italy", "spain", "japan", "china", "russia"],
"capitales": ["paris", "berlin", "rome", "madrid", "tokyo", "beijing", "moscow"],
"números": ["one", "two", "three", "four", "five", "six", "seven"],
"animales": ["dog", "cat", "horse", "cow", "bird", "fish", "mouse"],
"ciencias": ["physics", "chemistry", "biology", "mathematics", "geology"],
"verbos": ["walk", "run", "swim", "jump", "eat", "drink"],
}
palabras, etiquetas = [], []
for g, ws in GRUPOS.items():
for w in ws:
if w in modelo.wv:
palabras.append(w); etiquetas.append(g)
print(f"{len(palabras)} palabras en {len(GRUPOS)} grupos")39 palabras en 6 grupos
In [61]:
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
X = np.array([modelo.wv[w] for w in palabras])
XY_pca = PCA(n_components=2, random_state=SEMILLA).fit_transform(X)
XY_tsne = TSNE(n_components=2, random_state=SEMILLA, perplexity=8, init="pca").fit_transform(X)
In [62]:
def dibujar(XY, titulo):
plt.figure(figsize=(9, 7))
for g in GRUPOS:
idx = [i for i, e in enumerate(etiquetas) if e == g]
plt.scatter(XY[idx, 0], XY[idx, 1], s=60, label=g)
for i, w in enumerate(palabras):
plt.annotate(w, (XY[i, 0], XY[i, 1]), fontsize=8,
xytext=(4, 4), textcoords="offset points")
plt.title(titulo); plt.legend(fontsize=8); plt.grid(alpha=0.2)
plt.tight_layout(); plt.show()
dibujar(XY_pca, "Embeddings proyectados con PCA")
dibujar(XY_tsne, "Embeddings proyectados con t-SNE")In [63]:
# La geometría de las analogías: país -> capital debería ser un vector casi constante
PAIS_CAPITAL = [("france","paris"), ("germany","berlin"), ("italy","rome"),
("spain","madrid"), ("japan","tokyo"), ("russia","moscow")]
sub = [w for par in PAIS_CAPITAL for w in par if w in modelo.wv]
Xs = np.array([modelo.wv[w] for w in sub])
XYs = PCA(n_components=2, random_state=SEMILLA).fit_transform(Xs)
pos = {w: XYs[i] for i, w in enumerate(sub)}
plt.figure(figsize=(8, 6))
for pais, cap in PAIS_CAPITAL:
if pais in pos and cap in pos:
plt.scatter(*pos[pais], color="tab:blue"); plt.scatter(*pos[cap], color="tab:orange")
plt.annotate(pais, pos[pais], fontsize=9, xytext=(4,4), textcoords="offset points")
plt.annotate(cap, pos[cap], fontsize=9, xytext=(4,4), textcoords="offset points")
plt.arrow(*pos[pais], *(pos[cap]-pos[pais]), length_includes_head=True,
head_width=0.06, alpha=0.5, color="gray")
plt.title("¿Son paralelos los vectores país → capital?")
plt.grid(alpha=0.2); plt.tight_layout(); plt.show()
# Medida cuantitativa: coseno entre los vectores de desplazamiento
difs = np.array([modelo.wv[c] - modelo.wv[p] for p, c in PAIS_CAPITAL
if p in modelo.wv and c in modelo.wv])
difs_n = difs / np.linalg.norm(difs, axis=1, keepdims=True)
cos = difs_n @ difs_n.T
print(f"Coseno medio entre desplazamientos país→capital: "
f"{(cos.sum()-len(cos))/(len(cos)**2-len(cos)):.3f}")
print("(1.0 = perfectamente paralelos, 0.0 = ortogonales)")Coseno medio entre desplazamientos país→capital: 0.510 (1.0 = perfectamente paralelos, 0.0 = ortogonales)
In [64]:
# Analogías reveladoras: man : X :: woman : ?
for x in ["doctor", "engineer", "programmer", "professor", "boss", "nurse", "genius"]:
if x in modelo.wv:
r = analogia("man", x, "woman", topn=3)
print(f"man : {x:<11} :: woman : {[w for w, _ in r]}")man : doctor :: woman : ['nurse', 'child', 'midwives'] man : engineer :: woman : ['physician', 'architect', 'educator'] man : programmer :: woman : ['user', 'programmers', 'handler'] man : professor :: woman : ['lecturer', 'researcher', 'emeritus'] man : boss :: woman : ['prostitute', 'boyfriend', 'selina'] man : nurse :: woman : ['midwives', 'pregnant', 'midwife'] man : genius :: woman : ['bodybuilder', 'adolescent', 'fatale']
In [65]:
def sesgo_direccional(palabras, par=("he", "she")):
"""Proyecta cada palabra sobre la dirección (par[0] - par[1]).
Valor > 0 -> más asociada a par[0]; < 0 -> más asociada a par[1].
Devuelve un DataFrame ordenado.
"""
a, b = par
eje = modelo.wv[a] - modelo.wv[b]
eje = eje / np.linalg.norm(eje)
filas = []
for w in palabras:
v = modelo.wv[w]
v = v / np.linalg.norm(v)
proyeccion = float(np.dot(v, eje))
filas.append({"palabra": w, "proyeccion": proyeccion})
df = pd.DataFrame(filas).sort_values("proyeccion", ascending=False).reset_index(drop=True)
return df
OCUPACIONES = ["nurse", "doctor", "teacher", "engineer", "secretary", "programmer",
"librarian", "mechanic", "dancer", "scientist", "receptionist",
"carpenter", "designer", "surgeon", "housekeeper", "pilot"]
df_sesgo = sesgo_direccional([w for w in OCUPACIONES if w in modelo.wv])
display(df_sesgo)
| palabra | proyeccion | |
|---|---|---|
| 0 | engineer | 0.060787 |
| 1 | teacher | 0.017524 |
| 2 | surgeon | 0.012540 |
| 3 | scientist | 0.005343 |
| 4 | mechanic | -0.008503 |
| 5 | carpenter | -0.019465 |
| 6 | doctor | -0.048736 |
| 7 | secretary | -0.064075 |
| 8 | librarian | -0.078206 |
| 9 | designer | -0.118456 |
| 10 | programmer | -0.126157 |
| 11 | pilot | -0.132322 |
| 12 | housekeeper | -0.223932 |
| 13 | nurse | -0.285991 |
| 14 | dancer | -0.324133 |
In [66]:
try:
d = df_sesgo.sort_values("proyeccion")
plt.figure(figsize=(7, 6))
colores = ["tab:red" if v > 0 else "tab:blue" for v in d["proyeccion"]]
plt.barh(d["palabra"], d["proyeccion"], color=colores)
plt.axvline(0, color="black", lw=1)
plt.xlabel("← más cercano a 'she' proyección más cercano a 'he' →")
plt.title("Proyección de ocupaciones sobre el eje he–she")
plt.tight_layout(); plt.show()
except NameError:
print("Completa el Ejercicio 8a primero.")In [67]:
# --- Verificación automática del Ejercicio 8 ---
def _t1():
d = sesgo_direccional(["doctor", "nurse"])
assert list(d.columns) == ["palabra", "proyeccion"], f"columnas incorrectas: {list(d.columns)}"
def _t2():
d = sesgo_direccional(["doctor", "nurse", "teacher"])
assert d["proyeccion"].is_monotonic_decreasing, "el DataFrame debe estar ordenado descendente"
def _t3():
d = sesgo_direccional(["he", "she"])
assert d.iloc[0]["palabra"] == "he", "'he' debe proyectar más alto que 'she' en su propio eje"
assert d["proyeccion"].abs().max() <= 1.01, "una proyección de vectores unitarios está en [-1, 1]"
for n, f in [("Ej8.a formato", _t1), ("Ej8.b orden", _t2), ("Ej8.c sanidad del eje", _t3)]:
test(n, f)[OK] Ej8.a formato [OK] Ej8.b orden [OK] Ej8.c sanidad del eje
In [68]:
# Celda de apoyo para la pregunta 7: explora una palabra polisémica
for p in ["bank", "apple", "right", "bat", "spring"]:
if p in modelo.wv:
print(f"{p:<8} -> {[w for w, _ in modelo.wv.most_similar(p, topn=10)]}")bank -> ['banks', 'monetary', 'fund', 'hsbc', 'loans', 'banking', 'bundesbank', 'cemac', 'commerzbank', 'funds'] apple -> ['macintosh', 'iigs', 'iic', 'amiga', 'hypercard', 'microsoft', 'ibm', 'intel', 'iie', 'wordperfect'] right -> ['left', 'arctan', 'prod', 'parenthesis', 'wingers', 'leaning', 'proviso', 'inalienable', 'fraca', 'subtree'] bat -> ['scooby', 'snail', 'saber', 'ox', 'darts', 'cock', 'leaping', 'snapping', 'flies', 'watermelon'] spring -> ['autumn', 'summer', 'winter', 'litha', 'lughnasadh', 'freeze', 'wintertime', 'autumnal', 'thunderstorm', 'torrential']
In [69]:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(SEMILLA)
dispositivo = "cuda" if torch.cuda.is_available() else "cpu"
print("PyTorch", torch.__version__, "· dispositivo:", dispositivo)
# ---- Datos: subcorpus pequeño y vocabulario ----
tokens_extra = list(itertools.chain.from_iterable(frases[:60])) # ~600k tokens
frec_e = Counter(tokens_extra)
MIN_COUNT, VENTANA, DIM, K_NEG = 20, 3, 64, 5
vocab = [w for w, c in frec_e.most_common() if c >= MIN_COUNT]
w2i = {w: i for i, w in enumerate(vocab)}
i2w = {i: w for w, i in w2i.items()}
V = len(vocab)
print(f"Tokens: {len(tokens_extra):,} · Vocabulario: {V:,}")
# ---- Subsampling de Mikolov: P(descartar w) = 1 - sqrt(t/f(w)) ----
total = sum(frec_e[w] for w in vocab)
t_sub = 1e-3
p_keep = {w: min(1.0, np.sqrt(t_sub * total / frec_e[w])) for w in vocab}
rng = np.random.default_rng(SEMILLA)
ids = [w2i[w] for w in tokens_extra if w in w2i and rng.random() < p_keep[w]]
print(f"Tras subsampling: {len(ids):,} tokens ({100*len(ids)/len(tokens_extra):.0f}%)")
# ---- Distribución de ruido f(w)^0.75 ----
frecs = np.array([frec_e[i2w[i]] for i in range(V)], dtype=np.float64)
p_ruido = frecs ** 0.75
p_ruido = torch.tensor(p_ruido / p_ruido.sum(), dtype=torch.float, device=dispositivo)
print("Distribución de ruido lista.")PyTorch 2.10.0a0+b4e4ee81d3.nv25.12 · dispositivo: cuda Tokens: 600,000 · Vocabulario: 3,363 Tras subsampling: 310,408 tokens (52%) Distribución de ruido lista.
In [70]:
# ---- Pares de entrenamiento (reutilizando la función del Ejercicio 2) ----
pares_e = generar_pares_skipgram(ids, ventana=VENTANA)
pares_e = np.array(pares_e, dtype=np.int64)
print(f"{len(pares_e):,} pares (centro, contexto)")
centros_t = torch.tensor(pares_e[:, 0], device=dispositivo)
contextos_t = torch.tensor(pares_e[:, 1], device=dispositivo)1,862,436 pares (centro, contexto)
In [71]:
class SGNS(nn.Module):
"""Skip-gram con Negative Sampling: dos matrices de embeddings."""
def __init__(self, V, D):
super().__init__()
self.centro = nn.Embedding(V, D)
self.contexto = nn.Embedding(V, D)
nn.init.uniform_(self.centro.weight, -0.5 / D, 0.5 / D)
nn.init.zeros_(self.contexto.weight)
def forward(self, centro, contexto, negativos):
"""
centro: (B,) índices de la palabra central
contexto: (B,) índices del contexto positivo
negativos: (B, K) índices de las muestras negativas
Devuelve la pérdida escalar promediada sobre el batch.
"""
v_c = self.centro(centro) # (B, D)
u_o = self.contexto(contexto) # (B, D)
u_neg = self.contexto(negativos) # (B, K, D)
score_pos = torch.sum(v_c * u_o, dim=1) # (B,)
perdida_pos = F.logsigmoid(score_pos) # (B,)
score_neg = torch.bmm(u_neg, v_c.unsqueeze(2)).squeeze(2) # (B, K)
perdida_neg = F.logsigmoid(-score_neg).sum(dim=1) # (B,)
perdida = -(perdida_pos + perdida_neg)
return perdida.mean()
In [72]:
modelo_pt = SGNS(V, DIM).to(dispositivo)
optim = torch.optim.Adam(modelo_pt.parameters(), lr=2e-3)
BATCH, EPOCAS_PT = 4096, 3
n = len(centros_t)
historial_pt = []
for ep in range(1, EPOCAS_PT + 1):
perm = torch.randperm(n, device=dispositivo)
total_loss, nb, t0 = 0.0, 0, time.time()
for s in range(0, n, BATCH):
idx = perm[s:s + BATCH]
c, o = centros_t[idx], contextos_t[idx]
# muestreo de negativos según p_ruido
neg = torch.multinomial(p_ruido, len(idx) * K_NEG, replacement=True).view(len(idx), K_NEG)
loss = modelo_pt(c, o, neg)
optim.zero_grad(); loss.backward(); optim.step()
total_loss += loss.item(); nb += 1
historial_pt.append(total_loss / nb)
print(f"época {ep}: pérdida media = {total_loss/nb:.4f} ({time.time()-t0:.1f} s)")
plt.figure(figsize=(6, 4))
plt.plot(range(1, EPOCAS_PT + 1), historial_pt, marker="o")
plt.xlabel("Época"); plt.ylabel("Pérdida media"); plt.title("SGNS en PyTorch")
plt.grid(alpha=0.3); plt.show()época 1: pérdida media = 2.8387 (1.4 s) época 2: pérdida media = 2.6126 (0.9 s) época 3: pérdida media = 2.5235 (0.9 s)
In [73]:
# Vecinos más cercanos calculados con los vectores obtenidos
E = modelo_pt.centro.weight.detach().cpu().numpy()
E_n = E / np.linalg.norm(E, axis=1, keepdims=True)
def vecinos_pt(palabra, topn=8):
if palabra not in w2i:
return f"'{palabra}' no está en el vocabulario de esta parte"
sims = E_n @ E_n[w2i[palabra]]
orden = np.argsort(-sims)[1:topn + 1]
return [(i2w[i], round(float(sims[i]), 3)) for i in orden]
for p in ["king", "city", "war", "water", "music"]:
print(f"{p:<7} -> {vecinos_pt(p)}")
print("\\nCompara estos vecinos con los de gensim sobre el corpus completo.")
print("¿Qué explica la diferencia: el algoritmo, el volumen de datos, las épocas")
print("o los detalles de implementación?")king -> [('honour', 0.92), ('commander', 0.917), ('colonel', 0.912), ('philip', 0.911), ('paris', 0.911), ('architect', 0.909), ('henry', 0.906), ('honor', 0.902)]
city -> [('town', 0.903), ('mississippi', 0.891), ('county', 0.888), ('denmark', 0.885), ('lake', 0.884), ('wales', 0.882), ('aalborg', 0.882), ('ankara', 0.88)]
war -> [('battles', 0.906), ('civil', 0.899), ('soldiers', 0.871), ('leaders', 0.871), ('confederacy', 0.869), ('nazi', 0.852), ('fought', 0.847), ('declared', 0.845)]
water -> [('polar', 0.929), ('fresh', 0.914), ('temperatures', 0.908), ('temperature', 0.903), ('low', 0.9), ('snow', 0.885), ('alkanes', 0.868), ('chain', 0.866)]
music -> [('cinema', 0.886), ('pop', 0.86), ('themes', 0.859), ('directors', 0.833), ('entertainment', 0.833), ('chart', 0.833), ('literature', 0.83), ('database', 0.829)]
\nCompara estos vecinos con los de gensim sobre el corpus completo.
¿Qué explica la diferencia: el algoritmo, el volumen de datos, las épocas
o los detalles de implementación?
In [ ]:
# Guardar el modelo completo (se puede seguir entrenando) y solo los vectores (más ligero)
modelo.save("word2vec_text8.model")
modelo.wv.save_word2vec_format("vectores_text8.txt", binary=False)
import os
for f in ["word2vec_text8.model", "vectores_text8.txt"]:
print(f, "->", f"{os.path.getsize(f)/1e6:.1f} MB")
# Descomenta para descargar los vectores a tu ordenador:
# from google.colab import files
# files.download("vectores_text8.txt")
# Cómo recargarlos después:
# from gensim.models import KeyedVectors
# kv = KeyedVectors.load_word2vec_format("vectores_text8.txt", binary=False)word2vec_text8.model -> 59.5 MB vectores_text8.txt -> 83.3 MB