{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "

\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# **NOTEBOOK 16**\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# **Modelos del lenguaje basados en redes neuronales artificiales**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **Modelos seq2seq 3: Atención**\n", "\n", "El mecanismo de **atención** es una técnica que permite al modelo enfocarse en diferentes partes de la entrada para cada paso de la salida. Esto es particularmente útil en tareas como la traducción automática, donde el modelo necesita alinear segmentos de la entrada con la salida correspondiente. \n", "\n", "En un modelo seq2seq tradicional sin atención, se utiliza un codificador para transformar toda la entrada en un vector de características fijo, que luego se pasa a un decodificador para generar la salida. Este enfoque tiene una limitación importante: el vector fijo debe contener toda la información necesaria de la secuencia de entrada, lo cual es difícil cuando las secuencias son largas, ya que puede llevar a la pérdida de información y hacer que el aprendizaje y la traducción sean ineficientes.\n", "\n", "El mecanismo de atención aborda este problema permitiendo que el decodificador tenga acceso a toda la secuencia de entrada en cada paso de tiempo. En lugar de depender de un único vector de contexto para toda la secuencia de salida, el modelo puede aprender a atender (es decir, dar más peso o importancia) a diferentes partes de la entrada para cada palabra o elemento de la secuencia de salida que está generando.\n", "\n", "El mecanismo de atención ha sido fundamental para mejorar el rendimiento de los modelos seq2seq en muchas tareas de procesamiento de lenguaje natural (NLP), y la idea se ha extendido y refinado en modelos más avanzados como Transformer, que utiliza lo que se llama \"auto-atención\" (self-attention) para procesar toda la entrada de una vez, en lugar de secuencialmente. Esto permite que los modelos sean aún más eficientes y efectivos en capturar relaciones complejas en los datos.\n", "\n", "Se introdujo en 2016 en el artículo: [Neural Machine Translation by Jointly Learning to Align and Translate](https://arxiv.org/pdf/1409.0473.pdf)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Esquema de funcionamiento**\n", "\n", "Un mecanismo de atención es una parte de una red neuronal. En cada paso del decodificador, decide qué partes de la fuente son más importantes. En este contexto, el codificador no tiene que comprimir toda la fuente en un solo vector\n", "\n", "

\n", "\n", "\n", "\n", "
\n", "\n", "En cada paso del decodificador, la atención:\n", "\n", "1. **Recibe una entrada de atención:** un estado del decodificador y todos los estados del codificador $( s_1, s_2, ..., s_n )$.\n", "2. **Calcula las puntuaciones de atención:**\n", " Para cada estado del codificador $s_i$, la atención calcula su \"relevancia\" para este estado del decodificador. Formalmente, se aplica una función de atención que recibe un estado del decodificador y un estado del codificador y devuelve un valor escalar $score_i$.\n", "3. **Calcula los pesos de atención:** una distribución de probabilidad - softmax aplicada a las puntuaciones de atención.\n", "4. **Calcula la salida de la atención:** la suma ponderada de los estados del codificador con los pesos de atención.\n", "\n", "\n", "El esquema general del cálculo sería:\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n", "### **Cálculo del score de atención**\n", "\n", "En el proceso general descrito arriba, no hemos especificado cómo se calculan exactamente los *scores* de atención. Puedes aplicar cualquier función que desees, incluso una muy complicada. Sin embargo, usualmente no es necesario hacerlo; hay varias variantes populares y sencillas que funcionan bastante bien.\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n", "Las formas más populares de calcular los *scores* de atención son:\n", "\n", "- **Producto escalar** - el método más simple.\n", "- **Función bilineal** (también conocida como \"atención de Luong\") - utilizada en el artículo \"Effective Approaches to Attention-based Neural Machine Translation.\n", "- **Perceptrón multicapa** (también conocido como \"atención de Bahdanau\") - el método propuesto en el artículo original.\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n", "### **Variantes de modelo: Bahdanau y Luong**\n", "\n", "Cuando se habla de los primeros modelos de atención, es muy probable que veas estas variantes:\n", "\n", "- **Atención de Bahdanau** - del artículo \"Neural Machine Translation by Jointly Learning to Align and Translate\" de Dzmitry Bahdanau, KyungHyun Cho y Yoshua Bengio (este es el artículo que introdujo por primera vez el mecanismo de atención).\n", "\n", "- **Atención de Luong** - del artículo \"Effective Approaches to Attention-based Neural Machine Translation\" de Minh-Thang Luong, Hieu Pham y Christopher D. Manning.\n", "\n", "Estos pueden referirse tanto a las funciones de *score* como a los modelos completos utilizados en estos artículos. En esta parte, examinaremos más de cerca estas dos variantes de modelo.\n", "\n", "#### **Modelo Bahdanau**\n", "\n", "- Codificador bidireccional: Para codificar mejor cada palabra fuente, el codificador tiene dos RNN, hacia adelante y hacia atrás, que leen la entrada en direcciones opuestas. Para cada token, se concatenan los estados de los dos RNN.\n", "\n", "- *Score* de atención: perceptrón multicapa. Para obtener un *score* de atención, se aplica un perceptrón multicapa (MLP) a un estado del codificador y un estado del decodificador.\n", "\n", "- Atención aplicada entre pasos del decodificador: La atención se utiliza entre pasos del decodificador: el estado $ s_t $ se utiliza para calcular la atención y su salida $ a_t $, y ambos $ s_t $ y $ a_t $ se pasan al decodificador en el paso $ t $.\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n", "#### **Modelo Luong**\n", "\n", "Mientras que el artículo considera varias variantes del modelo, la que usualmente se llama \"atención de Luong\" es la siguiente:\n", "\n", "- Codificador: unidireccional (simple)\n", "\n", "- *Score* de atención: función bilineal\n", "\n", "- Atención aplicada: entre el estado RNN del decodificador $ s_t $ y la predicción para este paso\n", "\n", "La atención se utiliza después del paso del RNN decodificador $ s_t $ antes de hacer una predicción. El estado $ s_t $ se utiliza para calcular la atención y su salida $ a_t $. Luego $ a_t $ se combina con $ s_t $ para obtener una representación actualizada $ h_t $, que se utiliza para hacer una predicción.\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n", "#### La atención aprende (casi) la alineación\n", "\n", "¿Recuerdas la motivación de la atención? En diferentes pasos, el decodificador puede necesitar enfocarse en diferentes tokens fuente, aquellos que son más relevantes en este paso. Veamos los pesos de atención: ¿qué palabras fuente utiliza el decodificador?\n", "\n", "Los ejemplos son del artículo \"Traducción Automática Neuronal mediante el Aprendizaje Conjunto de Alineación y Traducción\".\n", "\n", "De los ejemplos, vemos que la atención aprendió una alineación (suave) entre las palabras fuente y objetivo: el decodificador mira aquellos tokens fuente en los que está traduciendo en el paso actual.\n", "\n", "
\n", "\n", "\n", "\n", "
\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## **Práctica 2: Implementación de un mecanismo de atención en un modelo Seq2Seq con LSTMs**\n", "\n", "Partiendo del código del modelo seq2seq con *feedback* para tareas de Traducción Automática Neuronal (NMT) del notebook anterior, se debe implementar el modelo de atención de Bahdanau o Luong.\n", "\n", "\n", "#### **Objetivos de la práctica**\n", "\n", "- Entender el funcionamiento de los modelos Seq2Seq con LSTMs.\n", "- Comprender e implementar mecanismos de atención.\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Tutorial de operaciones con tensores**\n", "\n", "Aquí tienes algunas operaciones con tensores que te pueden ser útiles para la práctica.\n", "\n", "#### **Producto escalar**\n", "\n", "El producto escalar de dos vectores es la suma de los productos de sus componentes. Por ejemplo, el producto escalar de los vectores $a$ y $b$ es:\n", "\n", "\n", "$$a \\cdot b = a_1 b_1 + a_2 b_2 + a_3 b_3$$\n", "\n", "\n", "Si tenemos dos vectores en PyTorch, podemos calcular su producto escalar usando la función torch.dot()." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Producto escalar: 32\n" ] } ], "source": [ "import torch\n", "\n", "a = torch.tensor([1, 2, 3])\n", "b = torch.tensor([4, 5, 6])\n", "c = torch.dot(a, b)\n", "\n", "# 1*4 + 2*5 + 3*6 = 32\n", "print(\"Producto escalar:\", c.item())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Si tenemos un grupo de vectores almacenados en una matriz, podemos calcular el producto escalar de todos los vectores en la matriz con un vector dado." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Producto matricial:\n", " tensor([ 50, 122])\n" ] } ], "source": [ "A = torch.tensor([[1, 2, 3], [4, 5, 6]])\n", "b = torch.tensor([7, 8, 9])\n", "C = torch.matmul(A, b)\n", "\n", "# 1*7 + 2*8 + 3*9 = 50\n", "# 4*7 + 5*8 + 6*9 = 122\n", "\n", "print(\"Producto matricial:\\n\", C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Si ahora tenemos un tensor correspondiente a un batch de matrices de $2 \\times 3 \\times 2$ (batch, secuencia, embedding) y un batch de vectores de $2 \\times 2$, podemos calcular el producto escalar de cada matriz con su vector correspondiente en el batch. La figura siguiente muestras los tres primeros casos. La matriz amarilla de $2 \\times 3$ correspondería al resultado." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tensor([[[ 1, 2],\n", " [ 3, 4],\n", " [ 5, 6]],\n", "\n", " [[ 7, 8],\n", " [ 9, 10],\n", " [11, 12]]])\n", "tensor([[1, 2],\n", " [3, 4]])\n" ] } ], "source": [ "import torch\n", "\n", "A = torch.arange(1, 13) # vector de 12 elementos\n", "A = A.view(2, 3, 2) # vector reconvertido en una matriz de 2x3x2\n", "\n", "B = torch.arange(1, 5)\n", "B = B.view(2, 2)\n", "\n", "print(A)\n", "print(B)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Para hacer el producto escalar de un batch de matrices con un batch de vectores, podemos usar la función torch.bmm() (batch matrix multiplication)." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tensor([[ 5, 11, 17],\n", " [53, 67, 81]])\n" ] } ], "source": [ "C = torch.bmm(A, B.unsqueeze(1).transpose(1, 2))\n", "print(C.squeeze())" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.7" } }, "nbformat": 4, "nbformat_minor": 2 }