Aprender a convertir palabras en características numéricas que una red neuronal pueda procesar, desde la codificación más simple hasta los embeddings. Al terminar, el lector sabrá:
nn.Embedding y nn.EmbeddingBag de PyTorch, incluido el parámetro offsets para procesar varios documentos a la vez.Una aplicación de procesamiento del lenguaje natural (PLN) debe clasificar los correos de una empresa: trabajo, personal o publicidad no deseada. Para decidir puede fijarse en tres cosas: la presencia de ciertas palabras («factura», «oferta»), su frecuencia y el significado en contexto. Pero una red neuronal no entiende letras: solo opera con números. El primer paso de cualquier modelo de texto, desde un clasificador sencillo hasta un modelo de lenguaje grande (LLM), es convertir las palabras en vectores de características.
Una característica (feature) es un número que describe algún aspecto de la entrada; un vector de características es la lista de esos números. En una imagen, las características son los valores de los píxeles; en un texto hay que inventarlas, y la forma de hacerlo decide qué podrá aprender el modelo. Este módulo recorre tres respuestas, de la más simple a la que usan los modelos actuales: one-hot, bolsa de palabras y embeddings.
Para verlo con un ejemplo mínimo se usan tres frases, y la tarea es identificar de qué animal habla cada una:
Un conjunto de frases, documentos o secuencias se llama corpus (o conjunto de datos). Tras tokenizar, cada token distinto recibe un índice; la lista de tokens con sus índices es el vocabulario. Aquí tiene 9 tokens, porque el tokenizador separa «I'm» en «I» y «'m». El siguiente código construye el vocabulario como lo haría torchtext: primero los tokens más frecuentes y, a igualdad, en orden alfabético.
Qué hace el código
Cada parte tiene una función concreta:
tokenizar usa una expresión regular: '\w+ captura fragmentos como «'m» y \w+ captura palabras. Así «I'm» se divide en «I» y «'m».Counter cuenta cuántas veces aparece cada token en todo el corpus.sorted(..., key=lambda t: (-frecuencias[t], t)) ordena primero por frecuencia descendente (de ahí el signo menos) y, en caso de empate, alfabéticamente.enumerate asigna a cada token su posición en esa lista: ese número es su índice, y el diccionario vocab guarda la correspondencia.En PyTorch los índices empiezan en 0. «I» aparece tres veces y recibe el índice 0; el resto aparece una vez y se ordena alfabéticamente (el apóstrofo va antes que las letras). Por eso «I like cats» se convierte en [0, 7, 2].
La codificación one-hot convierte datos categóricos, como los tokens, en vectores que una red puede procesar. Cada vector tiene tantas posiciones como palabras hay en el vocabulario; todas valen 0 excepto la que corresponde al índice del token, que vale 1.
Así, cada token de un documento se representa con su propio vector, el que la red recibe como característica x de esa palabra. Una forma de verlo: el vector one-hot es una fila de interruptores, uno por palabra del vocabulario, en la que solo se enciende el de la palabra actual. No dice nada sobre la palabra salvo cuál es.
1. Con un vocabulario de 9 tokens, ¿cuántas posiciones tiene el vector one-hot de «like»?
La dimensión del vector es el tamaño del vocabulario.
2. Si «like» tiene el índice 7, ¿dónde está el 1 de su vector one-hot?
El índice del token indica qué elemento del vector vale 1.
3. ¿Por qué los vectores one-hot no capturan que «cats» y «dogs» se parecen?
Todas las palabras quedan a la misma distancia entre sí, sin relación de significado.
Con el vocabulario del ejemplo (I 0, 'm 1, cats 2, dogs 3, hate 4, hippos 5, impartial 6, like 7, to 8), escribe los vectores one-hot de «I hate dogs».
I → (1, 0, 0, 0, 0, 0, 0, 0, 0); hate → (0, 0, 0, 0, 1, 0, 0, 0, 0); dogs → (0, 0, 0, 1, 0, 0, 0, 0, 0). Cada vector tiene un único 1, en la posición del índice del token.
Un corpus de correos tiene un vocabulario de 20 000 palabras. ¿Cuántos valores ocupa en one-hot un correo de 100 palabras y cuántos de ellos son unos?
Casi toda la memoria se gasta en ceros. Por eso en la práctica no se construyen estos vectores, sino que se trabaja directamente con los índices.
Con un vector por token, ¿cómo se representa un documento entero? La bolsa de palabras (bag of words) lo representa como la suma (o la media) de los vectores one-hot de sus palabras. Para «I like cats» se suman los vectores de «I», «like» y «cats»: el resultado tiene un 1 en las posiciones 0, 7 y 2.
Con la suma, cada posición cuenta cuántas veces aparece esa palabra; con la media, el vector no depende de la longitud del documento. En ambos casos se pierde el orden: «el perro muerde al hombre» y «el hombre muerde al perro» producen la misma bolsa.
Qué hace el código
F.one_hot(indices, num_classes) convierte cada índice en un vector one-hot de num_classes posiciones; con tres índices devuelve una matriz de 3 × 9, una fila por palabra. sum(dim=0) suma esas filas (recorre la dimensión 0, la de las palabras) y deja un único vector de 9 posiciones: la bolsa de palabras. Para la media bastaría con uno.float().mean(dim=0).
El vector de la bolsa de palabras puede ser la entrada de una red neuronal: la primera capa lineal lo transforma, las siguientes calculan una puntuación por clase y se elige la de mayor valor. Para «I like cats», la salida más alta corresponde a «gatos».
Fíjate en la primera capa: recibe 9 números, casi todos ceros. Los ceros no aportan nada a la multiplicación, así que todo el trabajo lo hacen las pocas posiciones encendidas. Esta observación es la que lleva a los embeddings.
1. ¿Cómo se obtiene la bolsa de palabras de un documento?
El resultado es un único vector del tamaño del vocabulario.
2. ¿Qué información pierde la bolsa de palabras?
«cats like I» y «I like cats» dan el mismo vector.
3. ¿Qué ventaja tiene usar la media en lugar de la suma?
Un correo largo y uno corto quedan en la misma escala.
Con el mismo vocabulario, calcula la bolsa de palabras (suma) de «I like I like cats». ¿Qué frase distinta tendría exactamente la misma bolsa?
(2, 0, 1, 0, 0, 0, 0, 2, 0): «I» y «like» aparecen dos veces y «cats» una. Cualquier reordenación, por ejemplo «cats like I like I», da el mismo vector, porque la bolsa solo cuenta.
Calcula la bolsa de palabras de «I like cats» usando la media en lugar de la suma.
Se divide la suma entre 3 palabras: (1/3, 0, 1/3, 0, 0, 0, 0, 1/3, 0) ≈ (0,33; 0; 0,33; 0; 0; 0; 0; 0,33; 0). Las posiciones activas son las mismas; solo cambia la escala.
Al introducir en la primera capa lineal el vector one-hot de «cats», que solo tiene un 1 en la posición 2, todos los pesos se multiplican por 0 excepto los de esa posición. La salida es, simplemente, la fila de pesos de «cats». Construir el vector one-hot y multiplicar es un rodeo: basta con el índice para leer esa fila.
La capa que recibe el índice y devuelve esa fila se llama capa de embedding, y su salida es el vector de embedding (o simplemente embedding) de la palabra. En la práctica funciona como una tabla de consulta: el índice es el número de fila y la capa devuelve lo que hay escrito en ella. La diferencia con un diccionario normal es que el contenido de la tabla no se escribe a mano, sino que se aprende: durante el entrenamiento, la retropropagación ajusta las filas de las palabras que aparecen en cada lote.
Los pesos de la capa forman la matriz de embeddings: tiene una fila por palabra del vocabulario y tantas columnas como la dimensión del embedding, d. Sus valores se aprenden durante el entrenamiento, como cualquier otro peso de la red.
nn.Embedding(n_embedding, embedding_dim) crea la capa a partir del número de tokens del vocabulario y la dimensión deseada. Recibe un tensor de índices y devuelve un vector por índice: para «I like cats», una matriz de 3 filas.
Qué hace el código
Línea a línea:
torch.manual_seed(0) fija la semilla aleatoria para que los resultados se puedan repetir.nn.Embedding(9, 3) crea una matriz de pesos de 9 × 3 con valores aleatorios, accesible en embeds.weight.embeds(index[0]) recibe los índices [0, 7, 2] y devuelve las filas 0, 7 y 2: una matriz de 3 × 3. Con el último documento, de 5 tokens, devuelve 5 × 3.@ es el producto matricial) coincide con la fila 2 (allclose compara con tolerancia numérica).Los valores concretos de los embeddings son aleatorios hasta que se entrena la red: por eso no se muestran aquí. Lo importante son las formas y la equivalencia.
1. ¿Qué devuelve una capa de embedding cuando recibe el índice 7?
Equivale a multiplicar el one-hot por la matriz, pero sin construirlo.
2. ¿Qué forma tiene la matriz de nn.Embedding(9, 3)?
Filas = palabras del vocabulario; columnas = dimensión del embedding.
3. ¿Qué ventaja tienen los embeddings frente a los vectores one-hot?
Sus valores se aprenden; el orden lo tiene que modelar otra parte de la red.
Un modelo usa un vocabulario de 50 000 palabras y embeddings de dimensión 300. ¿Cuántos parámetros tiene la capa de embedding? ¿Cuántos valores ocuparía la matriz que forman los 50 000 vectores one-hot?
La capa de embedding necesita 15 millones de parámetros; los vectores one-hot de todo el vocabulario formarían una matriz de 2500 millones de valores casi todos nulos.
Con la matriz de la ilustración (I = (0,1; 0,2; 0,3), hate = (0,4; 0,5; 0,6), dogs = (0,5; 0,6; 0,7)), ¿qué devuelve la capa de embedding para «I hate dogs»?
Una matriz de 3 × 3 con una fila por palabra: [[0,1; 0,2; 0,3], [0,4; 0,5; 0,6], [0,5; 0,6; 0,7]]. La capa no combina las filas: devuelve un vector por token.
Si a la primera capa lineal se le da la bolsa de palabras en lugar de un solo one-hot, la salida es la suma de los embeddings de las palabras del documento. Una vez más, no hace falta construir la bolsa: basta con los índices.
Esto es lo que hace una bolsa de embeddings (embedding bag): recibe los índices de los tokens de un documento y devuelve un único vector que combina sus embeddings. Como no guarda los vectores intermedios, es más rápida y ahorra memoria que aplicar nn.Embedding y sumar después.
Se crea igual que nn.Embedding, y el parámetro mode decide cómo se combinan los vectores: "mean" (media, el valor por defecto), "sum" (suma) o "max" (máximo por componente). Además del tensor de índices recibe offsets, la posición donde empieza cada documento; con un solo documento siempre es [0].
Qué hace el código
Línea a línea:
mode="sum" hace que la bolsa sume los embeddings en lugar de promediarlos.bolsa_emb.weight = embeds.weight hace que ambas capas compartan la misma matriz; así se pueden comparar sus resultados. En un modelo real cada capa tiene sus propios pesos.offsets=torch.tensor([0]) indica que hay un único documento que empieza en la posición 0. La salida tiene forma (1, 3): un vector para todo el documento.nn.Embedding y también la bolsa de palabras multiplicada por la matriz.Error común
Suponer que nn.EmbeddingBag suma los embeddings. Por defecto calcula la media; para obtener la suma, que es lo que equivale a multiplicar la bolsa de palabras por la matriz, hay que indicar mode="sum". Las dos opciones son válidas, pero no dan los mismos números.
1. Al pasar a una capa lineal el vector de la bolsa de palabras, ¿qué se obtiene?
La bolsa por la matriz suma las filas de las palabras presentes.
2. ¿Qué modo usa nn.EmbeddingBag si no se indica nada?
La media es el valor por defecto; la suma se pide con mode="sum".
3. ¿Qué forma tiene la salida de EmbeddingBag para un documento de 5 tokens con dimensión 3?
Un vector de 5 × 3 sería la salida de nn.Embedding.
Con la matriz de la ilustración, calcula lo que devuelve una bolsa de embeddings para «I hate dogs» con mode="sum" y con mode="mean".
La media es la suma dividida entre el número de tokens del documento.
Para cada caso, ¿conviene nn.Embedding o nn.EmbeddingBag? (a) Clasificar reseñas por sentimiento con un modelo sencillo. (b) Una red recurrente que procesa la frase palabra a palabra.
(a) EmbeddingBag: el clasificador solo necesita un vector por reseña y la bolsa es más eficiente. (b) Embedding: la red recurrente necesita un vector por palabra y en orden, justo lo que la bolsa descarta.
En PLN es habitual juntar todos los documentos de un lote en un único tensor de una dimensión, sin relleno. Para que la bolsa sepa dónde termina cada documento, se le pasan los offsets: la posición inicial de cada uno.
torch.cat: se obtiene index_flat.torch.cumsum, y quitar el último valor: [0, 3, 6].Qué hace el código
Línea a línea:
torch.cat(index) pega los tres tensores de índices en uno solo de 11 posiciones.[0] + longitudes antepone un 0 a la lista [3, 3, 5]; torch.cumsum calcula la suma acumulada [0, 3, 6, 11]; [:-1] quita el último valor, que no es el inicio de ningún documento.nn.EmbeddingBag sin mode usa la media. La salida tiene forma (3, 3): una fila por documento.Comparado con rellenar las frases hasta igualarlas, los offsets no gastan memoria en tokens de relleno. Es como guardar varias canciones seguidas en una cinta y apuntar en qué segundo empieza cada una.
Error común
Construir los offsets con la suma acumulada sin quitar el último valor, o sin empezar en 0. Si el primer offset no es 0, PyTorch da un error; si falta un offset, no hay error, pero dos documentos se mezclan en la misma bolsa y el modelo aprende de datos equivocados.
1. ¿Qué indica cada valor de offsets?
Las longitudes se usan para calcularlos, pero los offsets son posiciones de inicio.
2. Si los documentos tienen 3, 3 y 5 tokens, ¿cuáles son los offsets?
Suma acumulada [0, 3, 6, 11] sin el último valor.
3. ¿Qué hace torch.cat(index)?
El resultado es index_flat; los offsets indican dónde cortar.
Un lote tiene cuatro documentos de 2, 5, 1 y 4 tokens. Calcula los offsets y la longitud de index_flat.
Suma acumulada empezando en 0: [0, 2, 7, 8, 12]; sin el último valor, los offsets son [0, 2, 7, 8]. index_flat tiene 12 posiciones.
Con los tres documentos del ejemplo, alguien pasa offsets=torch.tensor([0, 3]). El código no da error y devuelve dos vectores. ¿Qué ha pasado?
Falta el offset 6. La primera bolsa toma las posiciones 0–2 («I like cats») y la segunda, de la 3 a la 10, mezcla «I hate dogs» con «I'm impartial to hippos». La salida tiene forma (2, 3) en lugar de (3, 3). Comprobar que haya un offset por documento evita el problema.
Este apartado reúne todo lo aprendido en un programa completo que se puede ejecutar de principio a fin: un clasificador que asigna cada correo a trabajo, personal o spam (publicidad no deseada). Usa un corpus pequeño escrito para el ejemplo; con datos reales el código es el mismo y solo cambia la lista de correos.
El programa sigue los pasos del módulo en orden: tokenizar, construir el vocabulario, convertir en índices, agrupar en lotes con offsets, combinar los embeddings con una bolsa y clasificar con una capa lineal. Solo necesita PyTorch (pip install torch).
CLASES. Hay 24 correos de entrenamiento y 6 de prueba que el modelo no ve al entrenar.<unk> ocupa el índice 0, y a_indices envía a él cualquier palabra desconocida (stoi.get(t, 0)), en lugar de dar un error.collate_fn recibe 4 correos, convierte cada uno en índices, los concatena en un único tensor y calcula los offsets con la suma acumulada, exactamente como en el apartado de offsets. Devuelve etiquetas, índices y offsets.EmbeddingBag (media, dimensión 16) produce un vector por correo y nn.Linear lo convierte en 3 puntuaciones, una por clase.zero_grad), se calcula la pérdida de entropía cruzada entre puntuaciones y etiquetas, se retropropaga (backward) y el optimizador Adam actualiza los pesos, incluidos los embeddings. Una época es una pasada completa por los datos.modelo.eval() y torch.no_grad() desactivan el cálculo de gradientes; argmax elige la clase con mayor puntuación y se cuentan los aciertos.clasificar procesa un correo nuevo con un único offset, [0], y softmax convierte las puntuaciones en probabilidades.parecidas compara el embedding de una palabra con todos los demás mediante la similitud del coseno y devuelve los más cercanos.Esta es la salida del programa:
<unk>, así que la decisión depende solo de «informe», «cliente» y las palabras comunes.Con un corpus tan pequeño, acertar 6 de 6 no garantiza que el modelo funcione con correos reales: sirve para comprobar que todas las piezas encajan. Para un uso real se necesitan miles de ejemplos y una evaluación más amplia.
nn.Embedding y nn.EmbeddingBag consiguen lo mismo sin gastar memoria en ceros.EmbeddingBag para clasificar documentos cuando el orden importa poco; Embedding cuando el modelo necesita un vector por palabra y en orden (redes recurrentes, transformers).<unk> y construye el vocabulario solo con los datos de entrenamiento. Si el relleno es inevitable, usa padding_idx para que su embedding no se entrene.nn.Embedding.from_pretrained suele mejorar mucho los resultados.mode="mean" y mode="sum".torch.manual_seed hace los experimentos repetibles, y medir siempre en datos que el modelo no ha visto evita conclusiones engañosas.| Término | Significado |
|---|---|
| Corpus | Conjunto de frases, documentos o secuencias con el que se trabaja. |
| Vocabulario | Lista de tokens distintos, cada uno con un índice entero. |
| Codificación one-hot | Vector del tamaño del vocabulario con un 1 en la posición del token y 0 en el resto. |
| Bolsa de palabras | Representación de un documento como suma o media de los one-hot de sus palabras; pierde el orden. |
| Embedding | Vector denso y de baja dimensión que representa una palabra; se aprende al entrenar. |
| Matriz de embeddings | Pesos de la capa de embedding: una fila por palabra y una columna por dimensión. |
| Bolsa de embeddings | Capa que combina (media, suma o máximo) los embeddings de un documento en un solo vector. |
| Offsets | Posiciones de inicio de cada documento dentro del tensor plano de índices. |
| Época | Una pasada completa por todos los datos de entrenamiento. |
| Similitud del coseno | Medida de cuánto apuntan dos vectores en la misma dirección; se usa para comparar embeddings. |
| Suma acumulada | Secuencia de sumas parciales (torch.cumsum); con las longitudes, da los offsets. |
nn.Embedding la obtiene directamente a partir del índice.nn.EmbeddingBag la calcula (media por defecto) y, con offsets, procesa varios documentos en un solo tensor.<unk>, lotes con offsets, EmbeddingBag y una capa lineal que se entrenan juntas.1. ¿Por qué hay que convertir las palabras en vectores antes de usar una red neuronal?
Toda entrada de una red es numérica: las palabras se traducen a vectores de características.
2. Con un vocabulario de 30 000 palabras, ¿cuántos unos tiene el vector one-hot de una palabra?
Un único 1, en la posición de su índice; las otras 29 999 son ceros.
3. «El gato persigue al ratón» y «el ratón persigue al gato», ¿tienen la misma bolsa de palabras?
Esa es la principal limitación de la bolsa de palabras.
4. ¿Qué operación sustituye la capa de embedding?
El resultado es el mismo, la fila de la palabra, sin construir el one-hot.
5. ¿Cuántos parámetros tiene nn.Embedding(10000, 64)?
10 000 filas × 64 columnas.
6. ¿De dónde salen los valores de un embedding?
Son pesos de la red y se ajustan con el resto del modelo.
7. Para «I like cats», ¿qué devuelven nn.Embedding y nn.EmbeddingBag con dimensión 3?
Embedding da un vector por token; EmbeddingBag, uno por documento.
8. ¿Cómo se obtiene la suma de embeddings con nn.EmbeddingBag?
Por defecto calcula la media.
9. Documentos de 4, 2 y 6 tokens: ¿cuáles son los offsets?
Suma acumulada [0, 4, 6, 12] sin el último valor.
10. ¿Qué ventaja tienen los offsets frente a rellenar los documentos hasta la misma longitud?
Los documentos se guardan seguidos y los offsets marcan dónde empieza cada uno.