Representaciones densas: la idea central

Representaciones densas: la idea central

23 min read

Un embedding es, por ahora, un nombre. La lección anterior, sobre bolsa de palabras y TF-IDF (term frequency–inverse document frequency), terminó de agotar lo que se puede hacer con un eje reservado para cada entrada del vocabulario —contar, pesar, comparar documentos— y planteó la salida: en lugar de un eje para cada entrada, tomar unas pocas coordenadas, muchas menos que V\lvert V \rvert, en las que todas las entradas tienen un valor. Lo que aquella lección no dijo es qué objeto es ese, cómo se guarda, qué relación tiene con el one-hot al que sustituye y de dónde salen sus números.

El vocabulario de subpalabras de la lección sobre tokenización tiene V=50000\lvert V \rvert = 50\,000 entradas. La otra cifra es cuántas coordenadas darle a cada una: 300, un valor habitual que no fija ninguna teoría —se elige probando, y en uso van desde unas decenas hasta unos miles—, así que nada de lo que sigue depende de que sean 300 exactamente. Con un eje por entrada, lo que un modelo aprenda de estupenda no le sirve de nada al encontrarse con magnífica: son dos ejes distintos y ahí se acaba la historia. En cuanto las dos comparten coordenadas eso deja de ser cierto, y deja de serlo para las 50 000 entradas a la vez. El precio son los números de la tabla, 50000300=1550\,000 \cdot 300 = 15 millones, y quién los decide es la pregunta con la que termina la lección.

Antes de la definición, un mapa. Cada punto es una palabra del español colocada por sus dos coordenadas; elige coche, manzana o gato en el desplegable y mira qué se enciende alrededor.

Los vecinos de la palabra elegida se unen a ella con una línea. Prueba con palabras de grupos distintos y fíjate en que esas listas no las ha escrito nadie: salen de las coordenadas.

Dos advertencias, porque el mapa promete más de lo que puede dar. Sus coordenadas las he puesto yo a mano para el curso: no salen de ningún modelo entrenado. Y un embedding de verdad tiene cientos de dimensiones; lo que sobrevive al aplastarlo hasta dos es la vecindad, que es lo que hay que mirar aquí. El botón de la analogía enseña algo más fuerte —que también las direcciones significan— y esta lección no lo cobra: eso ocupa el resto del bloque.

Un vector corto para cada entrada del vocabulario

Fija un número dmodeld_{\text{model}} mucho menor que V\lvert V \rvert y asigna a cada entrada del vocabulario un vector de esa dimensión, con coordenadas reales cualesquiera. Ese vector es el embedding de la entrada ww, y lo escribimos ew\mathbf{e}_w:

r(w)=ewRdmodel,dmodelV.r(w) = \mathbf{e}_w \in \mathbb{R}^{d_{\text{model}}}, \qquad d_{\text{model}} \ll \lvert V \rvert.

El vector one-hot era disperso: V\lvert V \rvert coordenadas de las cuales una sola es distinta de cero. Este es denso: pocas coordenadas y todas ocupadas. El cambio de nombre marca un cambio de idea. En one-hot, la coordenada jj respondía a una pregunta con sentido —«¿es esta la entrada que ocupa la posición jj del vocabulario?»— y por eso una sola coordenada bastaba para identificar la entrada. Aquí ninguna coordenada responde por separado a nada que sepamos formular: quien sitúa a la entrada es ew\mathbf{e}_w completo, sus dmodeld_{\text{model}} coordenadas a la vez.

Guardar rr vuelve a ser guardar una tabla, y esta vez la tabla cabe en memoria. Con el vocabulario ordenado como en la lección del one-hot, la matriz de embeddings es

ERV×dmodel,\mathbf{E} \in \mathbb{R}^{\lvert V \rvert \times d_{\text{model}}},

una fila por entrada y dmodeld_{\text{model}} columnas, con la fila ii igual a ewi\mathbf{e}_{w_i}^{\top}, usando traspuesta porque los vectores son columnas en este curso.

Buscar el vector de una entrada parece entonces un acceso a memoria y no una operación matemática. Las dos cosas coinciden, y verlo cuesta una línea. Multiplica por la matriz el vector one-hot de la lección del one-hot y mira la coordenada jj del resultado:

(owiE)j=k=1V(owi)kEkj=Eij,\left(\mathbf{o}_{w_i}^{\top}\mathbf{E}\right)_j = \sum_{k=1}^{\lvert V \rvert} (\mathbf{o}_{w_i})_k \, \mathbf{E}_{kj} = \mathbf{E}_{ij},

porque el único sumando que no se anula es el de k=ik = i, donde el one-hot vale 11. Coordenada a coordenada, lo que sale es la fila ii:

ewi=owiE.\mathbf{e}_{w_i}^{\top} = \mathbf{o}_{w_i}^{\top}\mathbf{E}.

El one-hot no desaparece, entonces: se convierte en el índice con el que se lee una fila. En la práctica nadie construye las 50 000 coordenadas para multiplicarlas —se escribe E[i]—, y aun así la operación que se está haciendo es un producto de matrices. Un texto entero hereda la cuenta: aquella lección convertía TT tokens en XRT×V\mathbf{X} \in \mathbb{R}^{T \times \lvert V \rvert}, una fila one-hot por posición, y multiplicar por E\mathbf{E} da

XERT×dmodel,\mathbf{X}\mathbf{E} \in \mathbb{R}^{T \times d_{\text{model}}},

la misma frase escrita con 300 columnas en lugar de 50 000.

Las tres exigencias de la lección del problema de representar el lenguaje siguen cumplidas. La dimensión es fija, dmodeld_{\text{model}} para toda entrada. El determinismo lo da que E\mathbf{E} se congele al terminar el entrenamiento. Y la totalidad la sigue dando <UNK>, que tiene su fila como cualquier otra entrada, igual que en la lección sobre vocabulario y palabras fuera de vocabulario.

Lo que sí cambia es el precio de la tabla. Son

Vdmodelnuˊmeros,\lvert V \rvert \cdot d_{\text{model}} \quad \text{números},

frente a los V2\lvert V \rvert^{2} del one-hot: 15 millones frente a 2 500 millones, unos 60 MB en lugar de 10 GB. La comparación es real y no es el argumento: la tabla del one-hot es la identidad y no se guarda nunca. Lo que se gana es que el vector que el sistema recibe tenga 300 coordenadas y no 50 000, todas con información.

Lo que compra que dos entradas puedan estar cerca

Con vectores densos vuelve a haber geometría, y esta vez no la hereda de un orden alfabético que no sabe nada de significados. La medida es la de la lección anterior, la similitud coseno, que allí comparaba documentos y aquí compara entradas:

cos(eu,ev)=eueveuev.\cos(\mathbf{e}_u, \mathbf{e}_v) = \frac{\mathbf{e}_u^{\top}\mathbf{e}_v}{\lVert \mathbf{e}_u \rVert \, \lVert \mathbf{e}_v \rVert}.

Una cosa cambia al cambiar de objeto. Los pesos TF-IDF nunca eran negativos, así que el coseno vivía en [0,1][0, 1]; las coordenadas de un vector denso son reales cualesquiera, el producto escalar puede salir negativo y el coseno recorre [1,1][-1, 1] entero. El cero deja de ser el mínimo y pasa a significar «sin relación», con los negativos apuntando en direcciones opuestas.

El contraste con el one-hot cierra el argumento. Allí, dos entradas distintas daban producto escalar 00 y distancia 2\sqrt{2} siempre, un único valor sin excepción. Aquí el coseno puede valer cualquier cosa, y qué valga depende de los números de E\mathbf{E}: la equidistancia se ha roto, y romperla era la condición para todo lo demás.

Con eso, el argumento que aquella lección no podía usar queda disponible: las representaciones de dimensión alta se salvan porque tienen vecindad —dos fotografías que difieren en un píxel son parecidas—, y el one-hot no podía acogerse a eso porque en él no hay vecinos. Con vectores densos los hay: si enin˜o\mathbf{e}_{\textit{niño}} y enin˜a\mathbf{e}_{\textit{niña}} apuntan casi en la misma dirección, la ventana la niña juega en la plaza deja de ser territorio desconocido para un modelo que solo vio el niño juega en la plaza. Por qué una entrada parecida produce una salida parecida es cosa del bloque 2, sobre el perceptrón multicapa; lo que hace falta aquí es que la representación permita el parecido.

Y aquí la concesión, que es grande. Nada de lo anterior dice que las coordenadas de E\mathbf{E} signifiquen algo: rellena la matriz con números al azar y tendrás vectores densos de 300 dimensiones, con vecinos equivocados. La densidad hace posible que estupenda y magnífica queden cerca; no las acerca. Quien las acerca son los números, y no los ha puesto nadie todavía.

La hipótesis de la que salen los números

Son 15 millones y no hay forma humana de escribirlos: fijar a mano las 300 coordenadas de una sola entrada ya es dudoso —¿cuánto vale la tercera coordenada de harina?—, y hay 50 000 entradas. Los tiene que poner un procedimiento automático a partir de texto, y para que exista uno hace falta antes una hipótesis sobre dónde está, en un corpus, la información del significado.

La hipótesis distribucional es esa hipótesis: dos entradas que aparecen rodeadas de las mismas entradas tienden a significar cosas parecidas. Se cita casi siempre en la frase de Firth —you shall know a word by the company it keeps— y se comprueba con un par de huecos en español. En «sirvió una copa de ___», «una botella de ___ en la mesa» y «el ___ estaba frío» encajan vino y zumo, y no encajan martillo ni lunes. Nadie ha dicho qué significan vino y zumo; lo que se ha mirado es con qué se juntan.

Lo que compra la hipótesis es un cambio de pregunta. «¿Qué significa esta entrada?» no se responde contando nada. «¿Con qué otras entradas aparece?» se responde recorriendo el corpus, sin que nadie tenga que anotarlo antes: el texto crudo ya trae la respuesta dentro. El significado deja de ser algo que introducir a mano y pasa a ser algo que se mide.

Conviene ver también lo que la hipótesis no dice, porque de ahí saldrán los límites de este bloque. No dice que dos entradas con el mismo contexto sean sinónimas: frío y caliente aparecen en contextos casi idénticos y significan lo contrario, y una representación construida así los coloca cerca. Y no es un teorema, sino una apuesta empírica que vale lo que valgan sus resultados. Resulta que valen mucho.

Vectores densos en NumPy: la búsqueda, el coseno y los vecinos

La celda monta una matriz E\mathbf{E} de ocho entradas en español con dmodel=4d_{\text{model}} = 4 y comprueba sobre ella lo que acabas de desarrollar. Sus números los he puesto yo a mano —con 50 000 entradas sería imposible, y ese es el problema abierto—, y a esta escala sirven para ver la mecánica. Ejecútala y mira tres cosas: que el producto por el one-hot devuelve la fila, qué aspecto tiene la matriz de cosenos frente a la del one-hot, y cuántos números ocupa cada tabla.

import numpy as np

V = ["<UNK>", "coche", "automóvil", "camión", "harina", "azúcar", "gato", "perro"]
d_model = 4

# Los números de E los he puesto a mano. Aquí no hay nada entrenado todavía.
E = np.array([
[ 0.12, 0.09, 0.11, 0.95], # <UNK>
[ 0.91, -0.28, 0.19, 0.06], # coche
[ 0.86, -0.21, 0.27, 0.10], # automóvil
[ 0.79, -0.16, 0.12, 0.24], # camión
[-0.22, 0.88, 0.07, 0.13], # harina
[-0.31, 0.86, 0.10, 0.05], # azúcar
[ 0.06, 0.13, 0.93, -0.11], # gato
[ 0.03, 0.19, 0.87, -0.05], # perro
])
print("E tiene forma", E.shape, " -> una fila por entrada,", d_model, "columnas")
print()

# La búsqueda ES un producto por el one-hot, y por eso nadie construye el one-hot.
i = V.index("coche")
o = np.zeros(len(V))
o[i] = 1.0
print("o_coche @ E =", o @ E)
print("E[i] =", E[i])
print("iguales:", np.array_equal(o @ E, E[i]))
print()

# Coseno entre todas las entradas: normaliza las filas y multiplica por la traspuesta.
U = E / np.linalg.norm(E, axis=1, keepdims=True)
S = U @ U.T

print("similitud coseno")
print(" " * 10 + "".join(w[:6].rjust(7) for w in V))
for j, w in enumerate(V):
print(w[:9].rjust(10) + "".join(("%+.2f" % S[j, k]).rjust(7) for k in range(len(V))))
print()

def vecinos(w, k=2):
j = V.index(w)
orden = np.argsort(-S[j])
return [(V[m], round(float(S[j, m]), 2)) for m in orden if m != j][:k]

for w in ["coche", "harina", "gato"]:
print("vecinos de", w.ljust(9), vecinos(w))
print()

# La misma cuenta sobre one-hot, la representación anterior.
O = np.eye(len(V))
fuera = O @ O.T - np.eye(len(V))
print("one-hot: el mayor coseno entre dos entradas distintas es", fuera.max())
print()

# Lo que ocupa cada tabla. Calculado, NO reservado.
print(" |V| d_model E: |V|*d_model one-hot: |V|^2")
for tam, dm in [(8, 4), (50000, 300)]:
print(str(tam).rjust(6), str(dm).rjust(8), str(tam * dm).rjust(18), str(tam * tam).rjust(20))
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.

La búsqueda sale exacta: o @ E y E[i] son el mismo vector coordenada a coordenada, que es lo que el desarrollo prometía. La matriz de cosenos es lo que había que ver. coche con automóvil da 0.990.99 y con camión 0.970.97; con harina da 0.48-0.48, un valor negativo que en one-hot no podía existir; gato con <UNK> da 0.020.02, que es «nada que ver» escrito en números. Debajo, la misma cuenta sobre one-hot devuelve un cero en cada casilla fuera de la diagonal: el «un solo valor» de la lección del one-hot, ahora al lado de una matriz con relieve.

Un detalle sin maquillar: el segundo vecino de gato es automóvil, con 0.300.30. Es ruido, y está ahí porque me he inventado los números: con cuatro coordenadas y ocho entradas no hay sitio para más. Con 300 coordenadas y un corpus detrás, ese segundo vecino sería otro animal.

Una prueba de treinta segundos. Copia la fila de coche en la de harina, cambiándole un par de decimales, y ejecuta otra vez: harina se va con los vehículos, sin que nadie haya tocado el vocabulario ni la forma de la tabla. La representación no está en la tabla, está en sus números.

Comprueba tu intuición

Cuatro preguntas sobre lo que acabas de obtener: qué calcula el producto por el one-hot, qué compra la densidad, qué le pasa al coseno al cambiar de objeto y qué afirma exactamente la hipótesis distribucional.

Guardas los vectores en la matriz ERV×dmodel\mathbf{E} \in \mathbb{R}^{\lvert V \rvert \times d_{\text{model}}} y quieres el de la entrada que ocupa la posición ii. ¿Qué calcula owiE\mathbf{o}_{w_i}^{\top}\mathbf{E}?

Cambias one-hot por vectores densos de dmodel=300d_{\text{model}} = 300 sobre un vocabulario de V=50000\lvert V \rvert = 50\,000 entradas. ¿Qué se gana? Marca todo lo que valga.

Select every correct option. This is graded all-or-nothing: there is no partial credit.

Entre dos vectores TF-IDF la similitud coseno vivía en [0,1][0, 1]. Entre dos vectores densos puede salir negativa.

¿Qué afirma exactamente la hipótesis distribucional?


El objeto ya está definido, y con él la mitad de lo que la lección del one-hot dejó pendiente: 300 coordenadas en lugar de 50 000, todas con información, y una geometría donde dos entradas pueden estar cerca. La otra mitad sigue abierta: la matriz E\mathbf{E} son 15 millones de números, y todo lo que has visto vale igual para una E\mathbf{E} llena de ruido. La hipótesis distribucional dice dónde mirar —en las compañías de cada entrada—, pero mirar no es un procedimiento: de contar contextos a fijar 300 coordenadas hay un salto que nadie ha dado todavía.

Darlo es la lección sobre Word2Vec. En lugar de contar contextos, se le pide a un modelo que los prediga, y se usan como embedding los números que le permiten acertar: el skip-gram predice el contexto a partir de la entrada, y CBOW (continuous bag of words) hace lo contrario. Las dos tropiezan con la misma piedra en cuanto se escriben con cuidado, y es una piedra grande: repartir probabilidad entre las entradas posibles obliga a sumar sobre las 50 000, una vez por cada par (entrada, contexto) del corpus. Quitar esa suma de en medio ocupa media lección.

Further reading1 source · 1 book

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • Speech and Language Processing, cap. 5: Embeddings
    bookJurafsky y Martin, 3.ª ed., borrador librestanford.eduEN

    El capítulo que fija la hipótesis distribucional y las representaciones densas: los mismos objetos de la lección, con la cita de Firth y más ejemplos. Apunta ya a los embeddings contextuales, que aquí no llegan hasta el bloque 5.