GloVe y los límites de los embeddings estáticos
25 min read
La lección anterior, sobre Word2Vec, dejó completa y, al final, una tabla de coocurrencias descrita y sin estrenar. De ella parte GloVe (global vectors), publicado en 2014. Lo que la tabla no trae decidido es qué hay que pedirle, y ahí empieza el objetivo. Un conteo bruto informa de poco: banco sale muchísimas veces junto a el y de ahí no se sigue nada.
Lo que distingue a una entrada de otra no es con quién aparece, sino en qué proporción. Esa proporción decide la forma entera del objetivo, y de paso explica algo que el bloque lleva dos lecciones enseñando sin cobrar: lo que ocurre al pulsar el botón del mapa de la lección sobre representaciones densas.
Restar hombre a rey y sumar el resultado a mujer cae sobre reina. Lo que afirma el dibujo no es que rey y reina estén cerca —esa vecindad ya la daba aquella lección—, sino algo más fuerte: que la diferencia entre dos vectores significa por sí misma y reaparece intacta en otro par. La advertencia es la de entonces: las coordenadas las he puesto yo a mano, así que el mapa enseña la forma de la afirmación, no su prueba.
Que las diferencias signifiquen no puede ser un accidente: si restar vectores da algo con sentido, el objetivo tuvo que pedirles que reprodujeran algo que ya funciona por resta. Restar cancela lo compartido y deja lo que separa; en los conteos, la operación que hace eso es dividir. Toma hielo y vapor y, en lugar de mirar con qué aparece cada una, divide una frecuencia por la otra, entrada por entrada. Estas son las cifras que publicaron los autores de GloVe sobre un corpus de 6 000 millones de tokens:
| razón | |||
|---|---|---|---|
| sólido | |||
| gas | |||
| agua | |||
| moda |
Las probabilidades sueltas confunden frecuente con característico: agua sale con hielo más que sólido. La razón, no: mide el contraste con vapor, no la frecuencia. Vale mucho más que cuando es del mundo de la primera, mucho menos cuando es del de la segunda, y se pega a cuando no distingue: moda, que no sale con ninguna, y agua, que sale con las dos por igual. La frecuencia base se cancela al dividir.
Qué les piden a los vectores las razones de coocurrencia
Llamemos a la casilla de la tabla de coocurrencias: el conteo de la entrada dentro de la ventana de la entrada , con la misma de la lección anterior. Es simétrica y tiene casillas. Escribamos también y por y , los vectores de centro y de contexto de aquella lección.
Sumando una fila entera sale el total de vecinos de , y con él la probabilidad de que, mirando una ventana centrada en , aparezca :
La tabla de hielo y vapor señalaba la razón entre dos de estas probabilidades. Pedírsela a los vectores se escribe con una todavía sin decidir:
Dos decisiones la dejan casi fijada. Lo que separa a de a la derecha es un cociente, y a la izquierda lo que la función recibe de los dos es su diferencia , la del mapa de arriba. Y para convertirla junto con en un solo número está el producto escalar de la lección sobre la bolsa de palabras. Con las dos, es una función de una variable real:
Queda una que convierta una resta en su argumento en una división en su valor, y esa propiedad la determina: la exponencial.
Por qué la exponencial es la única salida
Se busca una función continua con para cualesquiera y . Con sale ; con y , la condición se reordena en
la ecuación funcional de Cauchy en su forma multiplicativa, cuyas únicas soluciones continuas y no idénticamente nulas son . La constante se absorbe en la escala de los vectores —si sirve para unos , sirve para —, así que tomar no pierde nada.
Con la igualdad se cumple en cuanto cada probabilidad se empareja con su exponencial,
porque el cociente de la derecha pasa a ser el de dos exponenciales. Tomando logaritmos, , y restando esa igualdad para y para aparece lo que la intuición prometía:
La diferencia de dos vectores, proyectada sobre un tercero, es el logaritmo de la razón de la tabla. Ahí empiezan a significar las direcciones: la analogía del mapa es esta línea leída al revés.
Falta dejarla en una forma que se pueda ajustar. Sustituyendo ,
y ese tiene arreglo barato: no depende de , así que es una constante por cada entrada . Se le da nombre, , y se pasa al otro lado. Con eso la igualdad deja de ser simétrica —, pero el lado izquierdo ya no—, así que un segundo escalar para la entrada que hace de contexto la devuelve:
Son dos números reales más por entrada, uno en cada papel, y lo ganado no es cosmético: la condición de partida hablaba de ternas y esta, de un par. Una ecuación por casilla de la tabla.
El peso que impide que los pares más frecuentes decidan por todos
Ese sistema no tiene solución exacta: son ecuaciones —2 500 millones con el vocabulario de 50 000 entradas del bloque— contra unos 30 millones de incógnitas. Se pide entonces lo posible, en el sentido de los mínimos cuadrados, y antes hay que arreglar dos cosas.
La primera es que la mayor parte de la tabla son ceros —dos entradas cualesquiera no coinciden jamás en una ventana— y no existe. Las vacías se dejan fuera, que es además lo que abarata el ajuste: se paga por los pares vistos y no por .
La segunda es que las celdas no valen lo mismo. Una con es casi ruido, y una con —dos palabras vacías de la lección de la bolsa de palabras— no debería decidir sola dónde van los vectores. El arreglo es un peso que dependa del propio conteo:
Crece mientras el conteo es pequeño, así que un par visto una vez pesa poco, y se aplana a partir de , así que los frecuentísimos no arrastran el ajuste. El artículo toma y , el mismo exponente del muestreo negativo de la lección anterior y encontrado igual: probando. La pérdida de GloVe es entonces
con parámetros y nada más. Minimizarla es descenso de gradiente, y cada paso necesita el gradiente de .
El gradiente de la pérdida usado en cada paso
El peso y el objetivo salen de la tabla de coocurrencias: en la derivada son constantes. Llamando al error de la celda,
la regla de la cadena sobre el cuadrado deja multiplicando a cuatro derivadas inmediatas:
Las cuatro comparten coeficiente, y cada vector recibe al otro. Una celda ya explicada, con , no mueve nada.
La diferencia con la lección anterior no está donde parece: las dos ajustan una función de a una estadística de coocurrencia, y lo que cambia es cuándo se cuenta. Word2Vec cuenta mientras aprende; GloVe cuenta primero y luego ajusta sin volver a abrir el corpus.
Un vector, uno solo, para siempre
El bloque tiene ya dos formas de llenar , y comparten un rasgo que ninguna menciona. Mira el sujeto de esa suma: recorre las entradas del vocabulario. Hay una fila de por entrada, y una sola.
banco es una entrada del vocabulario. En un corpus del español aparece en saqué el dinero del banco y en me senté en el banco del parque, y la tabla de coocurrencias no separa las dos: acumula en la misma fila los vecinos de una y de otra. Nada en recuerda de qué ocurrencia venía cada conteo, porque los conteos se hacen sobre el tipo, y el tipo es la misma cadena. Al ajuste le queda un compromiso, y la pérdida dice cuál: la fila que la minimiza explica la suma de los dos perfiles, que no se parece a ninguno.
No es un defecto de GloVe. Word2Vec lo tiene por la misma razón, y el one-hot de la lección de la maldición de la dimensionalidad y el TF-IDF (term frequency–inverse document frequency) de la lección de la bolsa de palabras lo tenían antes. Y no viene solo: la lección de las representaciones densas anotó otro límite de la misma familia, y es que frío y caliente aparecen en contextos casi idénticos, así que la hipótesis distribucional los coloca cerca y la representación no tiene con qué separarlos. El tercero es el de siempre: una entrada que no estaba en el corpus no tiene fila, y hay que devolverle la de <UNK>, como en la lección sobre palabras fuera de vocabulario (out-of-vocabulary, OOV).
Los tres son el mismo hecho escrito de tres maneras, y se lee en el arranque del bloque: la lección sobre el problema de representar el lenguaje pidió una función
y discutió despacio qué debía cumplir. Lo que ata aquí no es ninguna de aquellas condiciones: es el dominio. Si el argumento de es una entrada del vocabulario, su valor no puede depender de la frase en la que aparece, porque la frase no entra en la función. Estático quiere decir eso.
Contar los pares y ajustar GloVe en NumPy
La celda cuenta y ajusta. El corpus son 680 tokens de plantillas, con banco plantado en dos mundos; he dejado adrede: con más coordenadas la tabla se ajusta casi exacta y la pérdida no se ve trabajar. La tasa de aprendizaje decae hasta casi cero, como en la lección anterior. Ejecútala y mira tres cosas: la columna de razones, los vecinos del ajuste y las tres filas del final.
# --- Un corpus de juguete donde <banco> sale en sus dos sentidos.
finanzas = ["dinero", "hipoteca", "nómina", "ahorro", "préstamo"]
parque = ["árbol", "seto", "sendero", "estanque", "columpio"]
def escribe(banco_f, banco_p):
frases = []
for f in finanzas:
for v in ["ingresó", "retiró", "consultó"]:
frases.append(f"el cliente {v} su {f} en el {banco_f}")
for f2 in finanzas:
if f2 != f:
frases.append(f"el {banco_f} anotó su {f} y su {f2}")
frases.append(f"la niña ingresó su {f} en el {banco_f}")
for p in parque:
for v in ["descansó", "esperó", "leyó"]:
frases.append(f"la niña {v} junto al {p} en el {banco_p}")
for p2 in parque:
if p2 != p:
frases.append(f"el {banco_p} quedó entre el {p} y el {p2}")
frases.append(f"el cliente descansó junto al {p} en el {banco_p}")
return [f.split() for f in frases]
# X_ik: cuántas veces cae k en una ventana de m tokens alrededor de i. UNA pasada.
def coocurrencias(docs, m=4):
V = sorted({w for d in docs for w in d})
pos = {w: i for i, w in enumerate(V)}
X = np.zeros((len(V), len(V)))
for d in docs:
ids = [pos[w] for w in d]
for t, i in enumerate(ids):
for j in range(max(0, t - m), min(len(ids), t + m + 1)):
if j != t:
X[i, ids[j]] += 1.0
return V, pos, X
docs = escribe("banco", "banco")
V, pos, X = coocurrencias(docs)
T = sum(len(d) for d in docs)
print("T =", T, "tokens |", len(V), "entradas |", int((X > 0).sum()), "celdas no nulas de", X.size)
print()
# La razón del desarrollo, medida aquí: P(k|i) = X_ik / X_i.
X_i = X.sum(axis=1)
print("k P(k|hipoteca) P(k|estanque) razón")
for k in ["cliente", "niña", "banco", "en"]:
a = X[pos["hipoteca"], pos[k]] / X_i[pos["hipoteca"]]
b = X[pos["estanque"], pos[k]] / X_i[pos["estanque"]]
print(" ", k.ljust(9), ("%.4f" % a).rjust(9), ("%.4f" % b).rjust(14), ("%.2f" % (a / b)).rjust(9))
print()
d_model, x_max, alpha, eta0, epocas = 3, 10.0, 0.75, 0.1, 30
def ajusta(X, semilla=3):
"""Minimiza sum g(X_ik) (u_k . e_i + b_i + b_k - log X_ik)^2 sobre las celdas NO NULAS."""
r = np.random.default_rng(semilla)
n = len(X)
fila, col = np.nonzero(X)
g = np.minimum((X[fila, col] / x_max) ** alpha, 1.0)
objetivo = np.log(X[fila, col])
E = 0.1 * r.standard_normal((n, d_model))
U = 0.1 * r.standard_normal((n, d_model))
bE, bU = np.zeros(n), np.zeros(n)
n_pasos = epocas * len(fila)
paso = 0
for epoca in range(1, epocas + 1):
perdida = 0.0
for idx in r.permutation(len(fila)):
i, k = fila[idx], col[idx]
eta = eta0 * max(1e-4, 1.0 - paso / n_pasos) # decae hasta casi cero
paso += 1
e, u = E[i].copy(), U[k]
error = e @ u + bE[i] + bU[k] - objetivo[idx]
perdida += g[idx] * error ** 2
# El gradiente del desplegable: 2 g(X_ik) por el error, y cada vector recibe al otro.
coef = 2.0 * g[idx] * error
E[i] -= eta * coef * u
U[k] -= eta * coef * e
bE[i] -= eta * coef
bU[k] -= eta * coef
if epoca in (1, 10, 20, epocas):
print(" época", str(epoca).rjust(2), " pérdida media por celda",
round(float(perdida / len(fila)), 4))
return E + U # las implementaciones se quedan con la suma de las dos tablas
W = ajusta(X)
N = W / np.linalg.norm(W, axis=1, keepdims=True)
S = N @ N.T
# Vecinos entre palabras con contenido: en 680 tokens las vacías caen en todas las ventanas.
contenido = finanzas + parque + ["cliente", "niña", "banco"]
def vecinos(w, k=3):
i = pos[w]
orden = sorted((pos[c] for c in contenido if c != w), key=lambda j: -S[i, j])
return [(V[j], round(float(S[i, j]), 2)) for j in orden[:k]]
print()
for w in ["hipoteca", "estanque"]:
print("vecinos de", w.ljust(10), vecinos(w))
print()
# --- La fila que GloVe tiene que explicar, contra las dos que la componen.
V2, pos2, X2 = coocurrencias(escribe("banco_dinero", "banco_parque"))
muestra = ["dinero", "hipoteca", "árbol", "seto"]
print("fila de X".ljust(18) + "".join(w.rjust(10) for w in muestra))
print("banco".ljust(18) + "".join(str(int(X[pos["banco"], pos[w]])).rjust(10) for w in muestra))
for w in ["banco_dinero", "banco_parque"]:
print((" " + w).ljust(18) + "".join(str(int(X2[pos2[w], pos2[c]])).rjust(10) for c in muestra))
print()
def coseno(u, v):
return float(u @ v / (np.linalg.norm(u) * np.linalg.norm(v)))
def fila(X, pos, w, columnas):
return np.array([X[pos[w], pos[c]] for c in columnas])
cols = finanzas + parque
b = fila(X, pos, "banco", cols)
u = fila(X2, pos2, "banco_dinero", cols)
v = fila(X2, pos2, "banco_parque", cols)
print("la fila de banco es la suma de las dos:", np.array_equal(b, u + v))
print("coseno entre los dos sentidos ", round(coseno(u, v), 2))
print("coseno de la fila compartida con cada uno", round(coseno(b, u), 2), round(coseno(b, v), 2))
todas = [w for w in V if w in pos2]
print("lo mismo contando también las vacías ",
round(coseno(fila(X2, pos2, "banco_dinero", todas), fila(X2, pos2, "banco_parque", todas)), 2))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
La tabla ocupa 432 celdas, frente a los 3 840 pares que la ventana genera sobre el mismo texto. Y sus razones reproducen el patrón de hielo y vapor: cliente vale a favor de hipoteca, niña vale , y banco y en dan exacto porque salen igual con las dos.
El ajuste baja de a por celda y los vecinos son los que tienen que ser: préstamo, dinero y ahorro para hipoteca; columpio, seto y sendero para estanque. Esos redondos son el precio de las plantillas: dentro de cada grupo las entradas ocupan huecos idénticos, y sus filas también.
Las tres últimas filas son el asunto de la lección: la de banco vale en dinero, en hipoteca, en árbol y en seto, y, separados los sentidos, es casilla a casilla la suma de dos filas sin nada que ver: entre ellas el coseno vale sobre las palabras con contenido, y contando las vacías, lo único que comparten. La compartida se queda a de cada una, a mitad de camino, y es esa la que GloVe explica con un vector.
Sube d_model a y ejecuta otra vez: la pérdida por celda cae a la sexta parte y los vecinos son
los mismos. Más coordenadas ajustan mejor la misma tabla; no le sacan lo que no tiene.
Comprueba tu intuición
Cuatro preguntas: cuánto vale una razón, qué se ajusta, qué hacen el peso y la condición del sumatorio, y qué hace falta para que banco tenga dos vectores.
En un corpus, la entrada hielo tiene coocurrencias en total, de las cuales son con sólido. La entrada vapor tiene en total y con sólido. ¿Cuánto vale la razón ?
A margin of ±0.01 is accepted.
¿Qué cantidad ajusta la pérdida de GloVe, celda a celda?
Sobre la suma , marca todo lo que sea cierto.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
El vector de banco mezcla el sentido del dinero y el del parque. ¿Cuál de estos cambios lo arregla?
El bloque cierra así su encargo: hay una que convierte texto en vectores, dos procedimientos para sus números y una geometría donde el parecido significa algo. Sin resolver queda la palabra del título. Cada entrada sale de aquí con un vector y con uno solo, así que el banco cerró a las dos y el banco estaba mojado reciben la misma fila de , y ninguna de las dos frases tiene forma de pedir otra. Que la representación dependa del contexto es el hilo que atraviesa lo que viene y se cobra al final, en el bloque 5, sobre el Transformer.
Antes hay que construir lo que va a consumir esos vectores, y todavía no existe. La lección sobre el problema de representar el lenguaje supuso una función que recibe un vector y devuelve una respuesta, y dejó dicho que cómo es por dentro era asunto del bloque siguiente. El bloque 2, sobre el perceptrón multicapa, empieza por ahí: su lección sobre la neurona artificial define la pieza más pequeña que hace ese trabajo —recibe un vector, lo pondera y devuelve un número— y la suposición se acaba.
Further reading2 sources · 1 paper, 1 interactive
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- GloVe: Global Vectors for Word Representation
El artículo de GloVe. De aquí salen la tabla de «hielo» y «vapor» y los valores del peso —x_máx = 100 y exponente 3/4— que usa la lección. Ese 3/4 es el mismo del muestreo negativo y, como allí, se eligió probando.
- Word2Vec Explorer
La analogía que aquí desarrollas —rey - hombre + mujer ≈ reina— sobre vectores reales, no el mapa dibujado a mano de la lección. Son de word2vec, no de GloVe, pero de la misma familia, y con su mismo límite: una palabra, un vector.