One-hot encoding y la maldición de la dimensionalidad

One-hot encoding y la maldición de la dimensionalidad

22 min de lectura

La lección anterior, sobre vocabulario y frecuencia, dejó el vocabulario completamente definido: una lista cerrada de V\lvert V \rvert entradas, construida a partir de las palabras más frecuentes y completada con <UNK> para todo lo que queda fuera. Con eso, rr ya puede recibir cualquier texto. Solo queda decidir qué vector asignar a cada una de las entradas del vocabulario.

Hay una respuesta inmediata a esa pregunta, y la lección existe porque esa respuesta es correcta: cumple una por una las exigencias que la lección sobre el problema de representar el lenguaje le puso a rr, y aun así no basta. Tiene nombre —one-hot encoding— y lo que queda por hacer con ella es ponerle precio. Una parte del precio se ve venir, porque los vectores salen enormes, y es precisamente la parte que no importa. La otra aparece al preguntar qué puede aprender un modelo que recibe entradas así, y esa no la arregla ninguna máquina más grande.

Mira colocadas de las dos maneras las tres palabras con las que aquella lección descartó d=1d = 1, antes de leer una sola fórmula. Sobre la recta, los índices 11, 22 y 33 dejan a gato a distancia 11 de casa y a perro a distancia 22: hay un medio, y alguien lo ocupa. Con un eje por palabra hay tres ejes perpendiculares y cada palabra cae sobre el suyo, en (1,0,0)(1,0,0), (0,1,0)(0,1,0) y (0,0,1)(0,0,1). Cuenta cuántos lados distintos tiene el triángulo que forman.

Arriba, casa, gato y perro sobre una recta con los índices 1, 2 y 3: gato queda a distancia 1 de casa y perro a distancia 2, de modo que gato cae entre las otras dos. Abajo, las mismas tres palabras en un espacio de tres ejes perpendiculares, una sobre cada eje, en los puntos (1,0,0), (0,1,0) y (0,0,1); los tres puntos forman un triángulo equilátero cuyos lados miden raíz de 2.
Las mismas tres palabras, dos representaciones. Sobre la recta hay una palabra en medio; con un eje por palabra cada una se va a su propio eje y las tres quedan a la misma distancia entre sí.

Un eje para cada entrada del vocabulario

Ordena el vocabulario de una vez por todas y escríbelo como V={w1,w2,,wV}V = \{w_1, w_2, \dots, w_{\lvert V \rvert}\}. El orden da igual —alfabético, por frecuencia, el que salga— mientras no cambie después; es el determinismo que la lección sobre el problema de representar el lenguaje exigía. Lo que ese orden aporta es una correspondencia: la entrada que ocupa la posición ii tendrá reservada la coordenada ii del vector, y ninguna otra entrada la usará. Posiciones del vocabulario y coordenadas del vector son, de aquí en adelante, la misma lista de números de 11 a V\lvert V \rvert.

Queda precisar de qué está hecha esa lista, porque el resto de la lección lo da por sabido. Las entradas de VV son tokens, y qué sean depende de la tokenización que fijó la lección sobre tokenización: palabras enteras con un tokenizador de palabras, trozos de palabra con uno de subpalabras. Nada de lo que sigue cambia por eso, y los ejemplos usan palabras enteras porque se leen mejor.

La representación one-hot asigna a la entrada wiw_i el vector de V\lvert V \rvert coordenadas que vale 11 en la posición ii y 00 en todas las demás. Coordenada a coordenada, escribiendo (ow)j(\mathbf{o}_w)_j para la jj-ésima coordenada del vector de la entrada ww:

owi{0,1}V,(owi)j={1si j=i0si ji.\mathbf{o}_{w_i} \in \{0,1\}^{\lvert V \rvert}, \qquad (\mathbf{o}_{w_i})_j = \begin{cases} 1 & \text{si } j = i \\ 0 & \text{si } j \neq i \end{cases}.

Es una rr como la que pedía la lección sobre el problema de representar el lenguaje, con la dimensión fijada por el propio vocabulario: r(w)=owr(w) = \mathbf{o}_w y d=Vd = \lvert V \rvert. Un texto de TT tokens se convierte entonces en una matriz XRT×V\mathbf{X} \in \mathbb{R}^{T \times \lvert V \rvert}, una fila por posición.

Ahora la propiedad por la que se elige. Toma dos entradas distintas del vocabulario, uu en la posición ii y vv en la posición jj, con iji \neq j. Su producto escalar recorre las coordenadas kk y en cada una encuentra un cero:

ouov=k=1V(ou)k(ov)k=0,\mathbf{o}_u^{\top} \mathbf{o}_v = \sum_{k=1}^{\lvert V \rvert} (\mathbf{o}_u)_k \, (\mathbf{o}_v)_k = 0,

porque el único término que podría no anularse exigiría k=ik = i y k=jk = j a la vez. Los vectores son ortogonales, dos a dos, todos.

Y la distancia entre ellos sale de la misma cuenta. La escribimos con doble barra: x\lVert \mathbf{x} \rVert es la longitud euclídea de x\mathbf{x}, la raíz de la suma de sus coordenadas al cuadrado. La resta ouov\mathbf{o}_u - \mathbf{o}_v vale +1+1 en la posición ii, 1-1 en la jj y 00 en el resto, de modo que

ouov2=k=1V((ou)k(ov)k)2=12+(1)2=2,\lVert \mathbf{o}_u - \mathbf{o}_v \rVert^{2} = \sum_{k=1}^{\lvert V \rvert} \big( (\mathbf{o}_u)_k - (\mathbf{o}_v)_k \big)^{2} = 1^{2} + (-1)^{2} = 2,

y por tanto ouov=2\lVert \mathbf{o}_u - \mathbf{o}_v \rVert = \sqrt{2}, sean cuales sean uu y vv, y sea cual sea el tamaño del vocabulario. Ahí está lo que se buscaba, escrito en una línea: todos los pares de palabras están exactamente a la misma distancia. Ninguna queda entre otras dos, y nada de lo que la representación dice del idioma se puede leer mal, porque no dice nada. La geometría inventada de la lección sobre el problema de representar el lenguaje ha desaparecido.

Por qué las dimensiones son |V| y no |V| − 1

Aquella lección terminaba con una cuenta: nn puntos mutuamente equidistantes necesitan n1n - 1 dimensiones. El mínimo son entonces V1\lvert V \rvert - 1 dimensiones —el triángulo de arriba, con tres palabras, cabe en un plano— y el one-hot gasta una más. No es un despiste, y la diferencia se ve mirando dónde viven estos vectores.

Todos ellos cumplen la misma ecuación, porque cada uno tiene un solo 11:

j=1V(ow)j=1.\sum_{j=1}^{\lvert V \rvert} (\mathbf{o}_w)_j = 1.

Eso es un hiperplano de RV\mathbb{R}^{\lvert V \rvert}, y un hiperplano tiene una dimensión menos que el espacio que lo contiene: los V\lvert V \rvert vértices viven, de hecho, en V1\lvert V \rvert - 1 dimensiones, que es el mínimo. Lo que compras con la dimensión sobrante es que las coordenadas sean legibles. Dentro del hiperplano serían combinaciones sin significado propio; en RV\mathbb{R}^{\lvert V \rvert}, la coordenada jj responde a «¿es esta la palabra wjw_j?», y por eso representar una palabra es mirar una posición en lugar de calcular nada.

Lo que cuesta no afirmar nada

El precio ya estaba escrito en la lección anterior, solo faltaba sustituir. Guardar rr como una tabla, una fila por entrada del vocabulario y dd columnas, cuesta Vd\lvert V \rvert \cdot d números; con d=Vd = \lvert V \rvert eso es

V2nuˊmeros.\lvert V \rvert^{2} \quad \text{números}.

Con el vocabulario de subpalabras de la lección sobre tokenización, entre 30 000 y 50 000 entradas, son entre 900 y 2 500 millones de números: unos 10 GB con cuatro bytes cada uno. Y crece al cuadrado: duplicar el vocabulario cuadruplica la tabla.

Esa cifra pide una corrección inmediata, porque nadie paga esos 10 GB. La tabla del one-hot es la matriz identidad, y una matriz identidad no contiene información: está determinada por su tamaño. Se guarda el índice ii de cada entrada —un entero— y el vector se fabrica cuando hace falta, o ni siquiera se fabrica. El coste de almacenar la representación desaparece.

Lo que no desaparece es la dimensión. El vector que el sistema recibe tiene V\lvert V \rvert coordenadas y solo una es distinta de cero, así que la parte del vector que lleva información, con 50 000 entradas, es

coordenadas no nulascoordenadas del vector=1V=150000=0.00002=0.002 %.\frac{\text{coordenadas no nulas}}{\text{coordenadas del vector}} = \frac{1}{\lvert V \rvert} = \frac{1}{50\,000} = 0.00002 = 0.002\ \%.

Antes de seguir, un apunte a favor. La lección sobre el problema de representar el lenguaje te pidió que guardaras una observación: allí, añadir agua al vocabulario renumeraba palabras que no tenían nada que ver con ella y dejaba inservible cualquier modelo entrenado antes. El one-hot no tiene ese defecto. Añade una entrada al final y cada vector antiguo conserva sus coordenadas; solo le crece un cero. Es la estabilidad que faltaba. Es real, aunque en la práctica VV se congela una sola vez —para eso está <UNK>— y la ocasión casi no llega.

Por qué la ortogonalidad impide aprender

Queda la cuenta que da título a la lección, y no es la de la memoria. Un sistema que lee una ventana de nn tokens seguidos recibe los nn vectores uno detrás de otro, nVn \lvert V \rvert coordenadas en total. ¿Cuántas entradas distintas puede recibir? Cada una de las nn posiciones admite cualquiera de las V\lvert V \rvert entradas del vocabulario, con independencia de las demás, así que

Vn.\lvert V \rvert^{n}.

Pon números de los pequeños: V=30000\lvert V \rvert = 30\,000 y una ventana de n=5n = 5 tokens, un cuarto de la frase de veinte tokens de la lección sobre vocabulario y frecuencia. Son 3000052.4×102230\,000^{5} \approx 2.4 \times 10^{22} entradas posibles. Un corpus enorme tiene del orden de T=1010T = 10^{10} tokens, y por tanto como mucho 101010^{10} ventanas distintas, de modo que el modelo llega a ver

10102.4×10224×1013\frac{10^{10}}{2.4 \times 10^{22}} \approx 4 \times 10^{-13}

del espacio de entradas. Cuatro diezmillonésimas de millonésima. Y no hay corpus que arregle eso: el exponente está en nn, así que mirar un token más de contexto multiplica el espacio por 3000030\,000 mientras el corpus sigue donde estaba. Esta es la maldición de la dimensionalidad: el número de configuraciones distintas crece exponencialmente con la dimensión de la entrada, y cualquier cantidad de datos cubre una fracción que tiende a cero.

Dicho así suena a condena general, y no lo es. Los modelos trabajan con entradas de dimensión enorme y aprenden de todos modos, porque en esas entradas hay vecindad: dos fotografías que difieren en un píxel son parecidas, lo aprendido sobre una vale para la otra, y por eso una fracción minúscula del espacio alcanza. Ese es el argumento que salva a cualquier representación de dimensión alta, y es el que el one-hot no puede usar. La equidistancia desarrollada arriba dice, con otras palabras, que aquí no hay vecinos: la ventana el niño juega en la plaza y la ventana la niña juega en la plaza están a la misma distancia entre sí que de cualquier otra.

De modo que la virtud es el defecto, y es la misma propiedad vista dos veces. Pedimos una representación que no afirmara parecidos falsos y salió una que no afirma parecidos, punto. Cada una de esas 2.4×10222.4 \times 10^{22} entradas hay que aprenderla por separado, porque ninguna se parece a ninguna, y solo se van a ver 101010^{10}.

La matriz one-hot en NumPy, y lo que pesaría

La celda construye el one-hot de un vocabulario diminuto en español y comprueba sobre él las tres afirmaciones de arriba: que los vectores son ortogonales, que todas las distancias valen 2\sqrt{2} y qué fracción de cada vector es distinta de cero. Después hace las cuentas de tamaño sin reservar memoria, que es la única forma de hacerlas: una tabla de 50 000 por 50 000 no cabe en el navegador, y ese es parte del resultado.

import numpy as np

V = ["<UNK>", "de", "la", "niño", "niña", "escuela", "programación", "río"]
n = len(V)

O = np.eye(n, dtype=int) # fila i = vector one-hot de V[i]
print("|V| =", n, " -> O tiene forma", O.shape)
print(O)
print()

# Ortogonalidad: el producto de la matriz por su traspuesta es la identidad,
# es decir, todo producto escalar entre vectores distintos vale 0.
print("O @ O.T == identidad:", np.array_equal(O @ O.T, np.eye(n, dtype=int)))

# Todas las distancias entre pares valen lo mismo, y ese mismo es raíz de 2.
distancias = set()
for i in range(n):
for j in range(i + 1, n):
distancias.add(round(float(np.linalg.norm(O[i] - O[j])), 6))
print("distancias distintas entre pares:", distancias, " | raiz(2) =", round(float(np.sqrt(2)), 6))
print("no nulos por vector:", int((O[3] != 0).sum()), "de", n,
"->", round(100 / n, 3), "%")
print()

# La tabla explícita: |V| x |V| números. Calculada, NO reservada.
print(" |V| números GB (4 bytes) % no nulo")
for tam in [8, 1000, 30000, 50000]:
numeros = tam * tam
gb = numeros * 4 / 1024**3
print(str(tam).rjust(6), str(numeros).rjust(15), ("%.2f" % gb).rjust(13),
("%.5f" % (100 / tam)).rjust(11))
print()

# El espacio de entradas de una ventana de n tokens, contra un corpus grande.
T = 10**10 # tokens de un corpus enorme
tam = 30000
for ventana in [1, 2, 3, 5]:
posibles = tam ** ventana
cubierta = min(T, posibles) / posibles
print("n =", ventana, "->", ("%.1e" % posibles).rjust(8),
"ventanas posibles | fracción que cubre el corpus:", "%.1e" % cubierta)
numpy

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

Las tres comprobaciones salen exactas y una conviene mirarla dos veces: el conjunto de distancias entre pares tiene un solo elemento. No es que se parezcan, es que no hay más de un valor. Con ocho palabras se ve de un vistazo; con 50 000 sigue siendo el problema.

La tabla de tamaños tiene una fila que sorprende y otra que no. La que no: 50 000 entradas piden 9.31 GB, y por eso nadie guarda esta tabla. La que sí: la columna de la derecha, el porcentaje de coordenadas no nulas, que ya con 30 000 entradas es 0.003 %. Y las últimas líneas son la maldición de la dimensionalidad en cuatro renglones: con una o dos palabras de contexto el corpus cubre el espacio entero, con tres cubre ya solo el 0.04 %, y con cinco la fracción se escribe con doce ceros detrás del punto. El salto ocurre entre la segunda palabra de contexto y la tercera, no en el infinito.

Una prueba de treinta segundos. Cambia V por un vocabulario de dos palabras y vuelve a ejecutar: la distancia entre pares sigue valiendo 2\sqrt{2}, exactamente igual que con ocho. Ahí ves que V\lvert V \rvert no entra en esa cuenta.

Comprueba tu intuición

Cuatro preguntas sobre lo que acabas de obtener: cuánto mide la distancia entre dos palabras, qué arregla el one-hot respecto de la lección sobre el problema de representar el lenguaje, qué cuesta la tabla y qué pasa con dos palabras que se parecen.

Tu vocabulario tiene V=20000\lvert V \rvert = 20\,000 entradas y representas cada una con su vector one-hot. ¿Cuánto vale la distancia euclídea ouov\lVert \mathbf{o}_u - \mathbf{o}_v \rVert entre las de dos entradas distintas uu y vv?

Se acepta un margen de ±0.01.

Comparas el one-hot con la representación por índices de la lección sobre el problema de representar el lenguaje. ¿En qué mejora? Marca todo lo que valga.

Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.

Con V=50000\lvert V \rvert = 50\,000, ¿cuántos números tendría la tabla que guarda rr explícitamente, una fila por entrada del vocabulario?

Un modelo entrenado con vectores one-hot ha visto muchas veces el niño juega y ninguna la niña juega. Lo aprendido sobre niño le ayuda algo con niña, porque las dos palabras se parecen.


El one-hot cumple las tres exigencias, es estable, y su geometría no afirma nada falso sobre el idioma. También es un callejón sin salida, y por el mismo motivo por el que cumple: una representación que no relaciona ninguna palabra con ninguna otra obliga a aprenderlo todo por separado, y no hay corpus para eso. Para salir hacen falta dos cosas a la vez, y son las dos que faltan aquí: muchas menos coordenadas que V\lvert V \rvert, y coordenadas que signifiquen algo, de modo que dos palabras parecidas acaben cerca.

La segunda es la difícil y ocupa el resto del bloque. Pero antes hay un cambio de pregunta que sale casi gratis desde aquí, y conviene hacerlo ahora, con los vectores one-hot todavía a la vista. Hasta ahora rr representa una palabra, y lo que un sistema suele necesitar es el vector de un documento entero: una reseña, un correo, una noticia. La forma más directa de obtenerlo es sumar los vectores one-hot de todos sus tokens, t=1Towt\sum_{t=1}^{T} \mathbf{o}_{w_t}, donde tt recorre las TT posiciones del documento y no las entradas del vocabulario. Cada coordenada pasa entonces a contar cuántas veces sale su palabra. Eso ya no es un vector de ceros y unos, y ya no es tan mudo: la coordenada de gol distingue una crónica deportiva de una receta. Sigue teniendo V\lvert V \rvert dimensiones y sigue sin saber en qué orden iban las palabras. Es la bolsa de palabras, y con la corrección que la hace útil —TF-IDF— es la lección sobre la bolsa de palabras y TF-IDF.

Para profundizar1 fuente · 1 paper

De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.

  • A Neural Probabilistic Language Model
    paperBengio, Ducharme, Vincent y Jauvin, 2003JMLR 3EN

    Plantea la maldición de la dimensionalidad para el lenguaje y propone salir de ella con vectores densos: el argumento que acabas de desarrollar. Va más allá y monta un modelo de lenguaje entero, cosa ya del bloque 2.