
- Publicado el
Redes neuronales representadas como DAGs de programas computacionales parametrizados
Las redes neuronales pueden verse como algoritmos con parámetros ajustables, cuya optimización se realiza a través del aprendizaje automático. Este enfoque abre la puerta a un análisis más amplio, tanto desde la perspectiva matemática como computacional. En este artículo, exploraremos cómo estos algoritmos se representan mediante grafos dirigidos acíclicos (DAG), siguiendo las ideas presentadas por Blondel et al. (2024). Elements of Differentiable Programming.
La representación de redes neuronales como DAGs tiene varias ventajas:
- Visualización: Los DAGs permiten una visualización más intuitiva de la estructura de las redes.
- Optimización: Facilitan el uso de técnicas de optimización basadas en grafos.
- Diferenciación automática: Facilitan la implementación de algoritmos como la retropropagación.
- Paralelización: Los DAGs permiten ejecutar programas en paralelo en hardware especializado como GPUs y TPUs.
Además, profundizaremos en cómo los DAGs mejoran tanto el diseño como la comprensión de redes neuronales complejas, y su relación con técnicas avanzadas de aprendizaje profundo, como las redes residuales y recurrentes..
Programas computacionales parametrizados
Cadenas de computación
Para comenzar, consideremos los programas computacionales simples definidos por una secuencia de funciones que se aplican de forma secuencial a una entrada Denominaremos a estos programas cadenas de computación y los representaremos como
Un ejemplo práctico de una cadena de computación es el procesamiento de imágenes, donde una imagen puede someterse a una serie de transformaciones como recorte, rotación y normalización. En el contexto de las redes neuronales, estas transformaciones suelen estar parametrizadas y sus parámetros se ajustan mediante un proceso de optimización durante el entrenamiento.
Figura 1. Representación de una cadena de computación como una secuencia de composiciones de funciones. Cada nodo intermedio simboliza una función, La flecha inicial indica la entrada, y la final, la salida. Las flechas internas ilustran las dependencias entre las funciones, conectando las salidas previas o la entrada inicial con las funciones.
Formalmente, una cadena de computación puede ser escrita como:
Donde es la entrada, para son los estados intermedios del programa y es la salida. Es fundamental que el dominio (espacios de entrada) de sea compatible con la imagen (espacio de salida) de para que la composición tenga sentido. Es decir, que para De forma equivalente a la ecuación (1), la cadena de computación completa se puede escribir en una única expresión:
Como veremos a continuación, una cadena de computación puede ser representada como un grafo dirigido acíclico (DAG), donde los nodos del grafo representan las funciones individuales y las aristas representan las dependencias y el flujo de información entre las funciones.
Grafos acíclicos dirigidos
En un programa computacional genérico, las funciones intermedias pueden depender de las salidas de otras funciones. Estas dependencias se pueden representar mediante un Grafo Acíclico Dirigido (DAG). Un grafo dirigido se compone de un conjunto de nodos y un conjunto de aristas Una arista es un par ordenado de nodos y se puede representar como para indicar que depende de Para representar las entradas y las salidas, es conveniente utilizar semi-aristas entrantes y semi-aristas salientes
En un grafo , los padres de un nodo son aquellos nodos tales que denotados como:
De manera similar, los hijos de un nodo son los nodos tales que representados por:
Los nodos sin padres son llamados raíces y los nodos sin hijos se conocen como hojas.
Un camino de a en un grafo es una secuencia de nodos tal que Un camino es simple si no contiene nodos repetidos. Un grafo es acíclico si no contiene ciclos, es decir, no existen caminos simples de un nodo a sí mismo. Un grafo acíclico dirigido (DAG) es un grafo dirigido que carece de ciclos.
La aristas de un DAG inducen un orden parcial entre los nodos, que se denota como si existe un camino de a Este orden parcial es una relación reflexiva, transitiva y antisimétrica. Se denomina «parcial» porque no todos los nodos están relacionados entre sí. Sin embargo, es posible definir un orden total llamado orden topológico: cualquier orden tal que si y solo si no hay un camino de a
Programas computacionales como DAGs
Un programa computacional se puede pensar como una función en términos matemáticos, lo que significa que el programa debería devolver los mismos valores para las mismas entradas y no debería tener efectos secundarios. Además, asumimos que el programa se detiene después de un número finito de pasos. Dado esto, un programa puede descomponerse en un conjunto finito de funciones y variables intermedias, donde las dependencias entre estas pueden representarse mediante un grafo dirigido acíclico (DAG).
Bajo estas condicione, podemos hacer las siguientes suposiciones sobre un programa computacional:
- Existe un nodo entrada y un nodo salida
- Cada función intermedia produce un único valor
Con un número finito de nodos como donde el nodo es la raíz, correspondiente a la entrada del programa, y el nodo es una hoja, correspondiente a la salida del programa. Según estas suposiciones, excepto por , cada variable está en biyección con una función Por lo tanto, el nodo representa la entrada , mientras que los nodos representan las funciones y salidas Es decir, cada nodo representa tanto la función y la salida
Las aristas de un DAG representan las dependencias entre los nodos. Los padres de un nodo denotados como donde indican las variables que la función necesita para calcular su salida . En otras palabras, los padres indican funciones que son necesarias para computar
Encontrar los nodos padres
Calcular
Ejecución de un programa
Para ejecutar un programa, nosotros necesitamos asegurar que las funciones intermedias son evaluadas en un orden en el orden correcto. Por lo tanto, nosotros asumimos que los nodos están ordenados en un orden topológico, (si este no es el caso, entonces el programa no puede ser ejecutado). Nosotros podemos ejecutar un programa para evaluar la salida
Obsérvese que podemos ver como una función de una sola entrada de el cual es una -tupla de elementos, o como una función de múltiples entradas Los dos puntos de vistas son esencialmente equivalentes. El proceso de ejecución de un programa es descrito en el Algoritmo 1.
Tratamiento de múltiples entradas o salidas del programa
Cuando un programa tiene múltiples entradas, los agrupamos siempre en una sola entrada como con ya que las funciones posteriores siempre pueden filtrar los elementos de que necesitan. Del mismo modo, si una función intermedia tiene varias salidas, siempre las agrupamos en una sola salida como con ya que las funciones posteriores siempre pueden filtrar los elementos de .
Figura 2. Dos posibles representaciones de un programa. Izquierda: La funciones son representadas como nodos y las dependencias como aristas. Derecha: Las funciones son representadas como nodos y las dependencias como un conjunto de nodos disjuntos. En ambos casos, la entrada es representada por un nodo raíz y la salida por un nodo hoja.
Representación alternativa de programas: grafos bipartitos
En nuestro formalismo, dado que una función siempre tiene una única salida se puede considerar que un nodo representa tanto la función como la salida Sin embargo, también es posible considerar dos conjuntos de nodos disjuntos: uno para las funciones y otro para las salidas, es decir, un grafo bipartito. Este formalimso es similar a los grafos factoriales (Frey et al., 1997; Loeliger, 2004) utilizados en la modelización probabilísticas, pero con aristas dirigidas. Una ventaja de este formalismo es que permite que las funciones tengan múltiples salidas. Por simplicidad, nos centraremos en la representación de un solo conjunto de nodos.
Circuitos aritméticos
Los circuitos aritméticos son uno de los ejemplos más sencillos de grafos computacionales, originarios de la teoría de la complejidad computacional. Formalmente, un circuito aritmético sobre un campo , como por ejemplo los número reales o los números complejos es un grafo dirigido acíclico (DAG) donde los nodos raíces son elementos del campo y las funciones intermedias son operaciones aritmética como la suma o la multiplicación. Estas últimas suelen denominarse compuertas aritméticas. Contrariamente al caso general de grafos computacionales, cada función sea una suma o un producto puede tener múltiples nodos raíces. Los nodos raíces pueden ser variables o constantes, y deben pertenecer al campo Los circuitos aritméticos puede utilizarse para calcular polinomios. Puede existir múltiples circuitos aritméticos para representar un polinomio dado. Para comparar dos circuitos aritméticos que representan el mismo polinomio, una noción intuitiva de la complejidad es el tamaño del circuito, como se define a continuación.
El tamaño de un circuito aritmético es el número de aristas en el grafo dirigido acíclico (DAG) que representa al circuito. El tamaño del polinomio de un polinomio es el tamaño del circuito aritmético más pequeño que representa a
Para más información sobre circuitos aritméticos, se puede consultar el libro de Chen et al. 2011.
Redes de propagación hacia adelante
Una red de propagación hacia adelante (feedforward network) es un tipo de cadena de computación con funciones parametrizadas que se aplican de forma secuencial a una entrada En este caso, las funciones son usualmente funciones parametrizadas que dependen de un vector de parámetros donde es el espacio de parámetros de la función Por lo tanto,
para una entrada y para los parámetros entrenables Cada función es una capa de la red y cada es una representación intermedia de la entrada la dimension de se conoce como la dimensión de la capa (o número de unidades ocultas) de la capa Una red de propagación hacia adelante puede ser definida como una función que mapea una entrada y parámetros a una salida
Dado un programa parametrizado de este tipo, los parametros los podemos aprender ajustando de acuerdo a un conjunto de datos de entrenamiento. Por ejemplo, dado un conjunto de datos de pares podemos minimizar la función de pérdida La minimización de la función de pérdida se puede hacer utilizando un algoritmo de optimización como el descenso de gradiente.
Perceptrones multicapa
Combinación de capas afines y funciones de activación
En la sección anterior, no especificamos cual es la forma de parametrizar las redes de propagación hacia adelante. Una parametrización típica, es el perceptron multicapa (multilayer perceptron o MLP), que utiliza capas totalmente conectadas (también conocidas densas) de la forma
donde son los parámetros de la capa es un matriz de pesos y es un vector de sesgos. Ademas la capa las podemos descomponer en dos funciones. La función es una capa afín y la función no lineal sin parámetros, llamada función de activación.
Generalmente, podemos remplazar el producto matrix-vector por cualquier función lineal de Por ejemplo, las capas convolucionales utilizan de convolución de la entrada con algunos filtros
A veces es necesario tratar con redes de múltiples entradas. Por ejemplo, supongamos que queremos diseñar una función , donde y son dos entradas. Una forma simple de hacerlo es usar la concatenación como entrada a una red Alternativamente, en lugar de concatenar las entradas, se pueden concatenar después de haber pasado por una o más capas ocultas.
Relación con los modelos lineales generalizados
Cuando la profundidad de la red es (es decir, una sola capa), la salida un MLP es una función lineal de la entrada es
Esto es llamado un modelo lineal generalizado (generalized linear model o GLM). En este caso, la función de activación es la función identidad. Los modelos lineales generalizados son un caso especial de los MLPs. Es particular, cuando es la función , se tiene un modelo de regresión logística. Por lo general para la profundidad la salida de un MLP es
Esto puede ser visto como un GLM sobre la representación intermedia de la entrada Este es el principal atractivo de los MLPs: la capacidad de aprender representaciones intermedias que son útiles para la tarea de interés. Veremos que los MLPs también pueden utilizarse como subcomponentes en otras arquitecturas.
Funciones de activación
Como se mencionó anteriormente, la redes de propagación hacia adelante utilizan funciones de activación para cada capa. En esta sección, vamos a describir algunas de las funciones de activación más comunes de escalar a escalar o de vector a escalar. También presentaremos algunas funciones de probabilidad que pueden ser utilizadas como funciones de activación.
Funciones de activación de escalares a escalares nolineales
La función ReLU (Rectified Linear Unit) es una función de activación no lineal que se define como la parte no negativa de su argumento. Es decir, la función ReLU es la función identidad en los valores no negativos y cero en los valores negativos:
Es una función lineal por partes e incluye un punto de inflexión en Una perceptron multicapa con activaciones ReLU se denomina red neuronal rectificadora. Las capas toman las forma
donde la ReLU es aplicada elemento a elemento. La función ReLU puede ser remplazada con una versión suavizada, conocida como softplus:
A diferencia de la ReLU, la softplus es una función suave, diferenciable y estrictamente positiva.
Funciones de activación de vectores a escalares nolineales
A menudo es útil reducir vectores a escalares. Estos valores escalares pueden ser vistos como puntajes o probabilidades, que representan la importancia de un vector de entrada. Una función común para hacer esto es usando el valor máximo, también conocido como max-pooling. Dado un vector la función de max-pooling es
Como una aproximación suave de la función de max-pooling, se puede usar la función log-sum-exp, la cual se comporta como una versión suavizada de la función max-pooling:
La función log-sum-exp puede ser vista como una generalización de la función softplus a vectores, en efecto para todo
Una implementación númericamente estable de la función log-sum-exp es la siguiente:
donde es un vector de unos y
Mapeos de probabilidad de escalar a escalar
En algunas ocasiones queremos mapear algún número real en un número entre 0 y 1, que puede representar la probabilidad de un evento. Para este propósito se usa con frecuencia las sigmoides. Una sigmoide es una función cuya curva se caracteriza por tener forma de «S». Estas funciones se utilizan para comprimir valores reales en un intervalo. Un ejemplo es la función paso binaria, también conocida como la función de paso de Heaviside,
Es un mapeo de a Desafortunadamente, la función de paso binaria es discontinua en Además, debido a que la función es constante en todos los demás puntos, tiene derivada cero en estos puntos, lo que dificulta su uso como parte de una red neuronal con retropropagación.
Una mejor sigmoide es la función logística, la cual mapea de a y es definida como:
Ella mapea a y a y satisface que Por lo tanto puede ser vista como una función de probabilidad. La función logística puede ser vista como una versión suavizada de la función de paso Además la función logística se puede obtener como la derivada de la función es decir, para todo
Dos propiedades importantes de la función logística es que para todo
y
Mapeos de probabilidad de vectores a vectores
En algunas ocasiones queremos mapear un vector en un vector de probabilidades. Este es una mapeo de a un simplex de probabilidad, definido por:
Dos ejemplos de funciones de mapeo de probabilidad de vector a vector son la función argmax y la función softargmax. La función mapea un vector a un vector de probabilidad que es cero en todas las entradas excepto en la entrada con el valor máximo que lo hace uno. En efecto,
donde denota el one-hot encoding de un entero es decir, si y cero en otro caso, esto es
Este mapeo coloca toda la masa de probabilidad en la entrada con el valor máximo, en caso de empate, se selecciona la primera entrada con el valor máximo. Desafortunadamente, la función no es diferenciable, lo que dificulta su uso en la retropropagación.
Una versión suavizada de la función es la función . La función está definida como
Este operador es comúnmente conocido en la literatura como la función softmax, pero esta denominación es errónea: este operador realmente define una versión suavizada de la función La salida de pertenece al interior relativo del simplex de probabilidad lo que significa que nunca puede alcanzar los bordes del simplex. Si denotamos entonces es decir para todo La función es el gradiente de la función , es decir, para todo
La función se puede ver como una generalización de la función a vectores, en efecto
La función satisface la propiedad que para todo y para todo
Esto quiere decir que la función tiene grados de libertad y que no es invertible. Sin embargo, debido a la anterior propiedad, sin perdida de generalidad, nosotros podemos imponer que (si no es el caso, podemos simplemente hacer , en donde es la media de ). Con esta restricción y junto con el hecho de
obtenemos
Por lo tanto, la función es invertible en el sentido de que podemos recuperar a partir de
Redes neuronales residuales
Como se mencionó anteriormente, las redes de propagación hacia adelante son un tipo de cadena de computación. En este caso, las funciones intermedias son funciones parametrizadas que se aplican de forma secuencial a una entrada. En efecto, consideremos una red de propagación hacia adelante con capas . Seguramente, siempre que sea la función identidad, el conjunto de funciones que esta red puede representar es el mismo que el de una red de capas Por lo tanto, podemos considerar una red de capas como una red de . En otras palabras, la profundidad, en teoría, no debería perjudicar el poder expresivo de las redes de propagación hacia adelante. Desafortunadamente, la suposición de que cada es una función identidad no es realista. Esto significa que la redes más profundas a veces pueden ser más difíciles de entrenar que las más superficiales, haciendo que la precisión se sature o degrade en función de la profundidad.
La idea clave de las redes neuronales residuales (residual neural networks o ResNets) (He et al., 2016) es introducir bloques residuales entre las capas, de tal forma que la salida de una capa se convierte en la entrada de una capa posterior. Formalmente, un bloque residual es una función de la forma
La función es la función residual, dado que modela la diferencia entre la entrada y la salida, es decir, La función puede ser vista como una corrección que se suma a la entrada para obtener la salida La suma con se conoce como conexión de salto (skip connection o shortcut). Siempre que sea posible ajustar para que entonces la función se convierte en la función identidad. Por ejemplo, si nosotros usamos:
donde son los parámetros de la capa entonces basta con ajustar y para que Los bloques residuales son conocidos para remediar el problema de gradiente de fuga.
En muchos artículos y paquetes de software se incluye una activación adicional y en su lugar se define el bloque residual como:
donde es usualmente la función ReLU. Si se debe incluir esta activación adicional o no, es esencialmente una decisión de modelado. En la práctica, los bloques residuales también pueden incluir operaciones adicionales como normalización por lotes (batch norm) y capa convolucionales.
Redes neuronales recurrentes
La redes neuronales recurrentes (recurrent neural networks o RNNs) (Rumelhart et al., 1986) son un tipo de red neuronal que operan con secuencias de vectores, ya sea como entrada, salida o ambas. Su parametrización real depende de la configuración, pero la idea central es mantener un vector de estado que se actualiza paso a paso mediante una función recursiva que utiliza parámetros compartidos a través de los pasos. Distinguimos entre las siguientes configuraciones:
- Vector a secuencia (uno a muchos):
- Secuencia a vector (muchos a uno):
- Secuencia a secuencia (muchos a muchos, alineados):
- Secuencia a secuencia (muchos a muchos, no alineados):
donde es la longitud de la secuencia de entrada y es la longitud de la secuencia de salida. Observe que se usa el mismo número de parámetros para cada una de las configuraciones, pero esto por supuesto no es necesario. A lo largo de este post, usaremos la notación para denotar una secuencia de vectores.
Vector a secuencia
En esta configuración, una función decodificadora mapea un vector y un vector de parámetros a una secuencia de vectores
Figura 3. Uno a muchos (decodificación). Un vector es mapeado a una secuencia de vectores
Esto es útil para la generación de subtítulos de imágenes, donde se genera una oración (una secuencia de palabras) a partir de una imagen (un vector de píxeles). Formalmente, podemos definir a través de la recursión:
donde son los parámetros de la red, es el estado inicial. El objetivo de es actualizar el estado en función de la entrada y el estado anterior mientras que el objetivo de es mapear el estado a un vector dado el estado Es importante destacar que los parámetros de y se cruzan a los largo de los pasos de tiempo. Típicamente, y se parametrizan utilizando MLPs de una capa oculta. Nótese que tiene múltiples entradas.
Secuencia a vector
En esta configuración, se define una función codificadora que mapea una secuencia de vectores y un vector de parámetros a un vector
Figura 4. Muchos a uno (codificación). Una secuencia de vectores es mapeada a un vector
Esto resulta útil para la clasificación de secuencias, donde se clasifica la secuencia completa en una sola categoría, por ejemplo en el análisis de sentimientos. Formalmente, podemos definir a través de la recursión:
donde son los parámetros de la red, es el estado inicial, y ahora actualiza los estados del codificador y no toma predicciones anteriores como entrada. La función de agrupamiento () típicamente no tiene parámetros y se utiliza para reducir la secuencia de estados a un solo vector. Ejemplos comunes de funciones de agrupamiento son la media, la suma y la función de max-pooling.
Secuencia a secuencia (alineada)
En esta configuración, se define una función de alineación que mapea una secuencia de vectores y un vector de parámetros a una secuencia de vectores La longitud de la secuencia de entrada y la secuencia de salida son iguales.
Figura 4. Muchos a muchos (alineados). Una secuencia de vectores es mapeada a una secuencia de vectores
Un ejemplo de aplicación es POS-tagging, donde se asigna una etiqueta a cada palabra en una oración. Formalmente, esto se puede definir como a través de la recursión:
donde son los parámetros de la red, es el estado inicial y y son funciones similares a las definidas en la configuración uno a muchos. La función actualiza el estado en función de la entrada y el estado anterior mientras que la función mapea el estado a un vector dado el estado
Secuencia a secuencia (no alineada)
En esta configuración, se define una función de alineación que mapea una secuencia de vectores y un vector de parámetros a una secuencia de vectores
Figura 5. Muchos a muchos (no alineados). Una secuencia de vectores es mapeada a una secuencia de vectores
La longitud de la secuencia de entrada y la secuencia de salida no necesariamente son iguales. Un ejemplo de aplicación es la traducción automática, donde una oración en un idioma se traduce a una oración en otro idioma. Formalmente, esto se puede definir como a través de la recursión:
donde son los parámetros de la red, es una función codificadora que mapea la secuencia de entrada a un vector de contexto y es una función decodificadora que mapea el vector de contexto a la secuencia de salida. La función de codificación y la función de decodificación pueden ser implementadas como RNNs. Los pasos en detalle, serían:
Esta arquitectura se denomina acertadamente arquitectura codificador-decodificador (encoder-decoder). Tenga en cuenta que denotamos la longitud de la secuencia objetivo como en lugar de para evitar confusiones. Sin embargo, en la práctica, la longitud puede depender de la entrada y a menudo no se conoce de antemano. Para abordar este problema, el vocabulario (de tamaño en nuestra notación) se aumenta típicamente con un token de «final de secuencia» (EOS, por sus siglas en inglés), para que, en el momento de la inferencia, sepamos cuándo dajar de generar la secuencia de salida. Una desventaja de esta arquitectura, es que toda la información sobre la secuencia de entrada esta contenida en el vector de contexto que por lo tanto puede convertirse en un cuello de botella.
Conclusiones
Las redes neuronales, concebidas como programas parametrizados, proporcionan un marco poderoso y flexible para abordar una amplia gama de problemas en aprendizaje automático y procesamiento de datos. A lo largo de este artículo, hemos explorado diversas arquitecturas y configuraciones, desde redes de propagación hacia adelante hasta redes neuronales recurrentes y redes residuales. La representación de estas arquitecturas como grafos dirigidos acíclicos (DAGs) no solo facilita su análisis y optimización, sino que también permite la aplicación eficiente de técnicas como la diferenciación automática. Las diferentes configuraciones discutidas demuestran la versatilidad de estos modelos para tareas que van desde la generación de subtítulos de imágenes hasta la traducción automática.
Este enfoque de ver las redes neuronales como programas parametrizados no solo permite diseñar modelos más eficientes y efectivos, sino que también abre caminos para mejorar la interpretabilidad y explicabilidad de estos modelos, un área de creciente importancia en la inteligencia artificial. Aunque hemos cubierto varios tipos de redes neuronales, es importante destacar que el campo está en constante evolución, con nuevas arquitecturas y técnicas desarrollándose continuamente. En resumen, esta perspectiva proporciona una base sólida para el desarrollo futuro de modelos de aprendizaje automático más avanzados, interpretables y adaptables a una amplia gama de aplicaciones en el mundo real.
Finalmente,si hay errores, omisiones o inexactitudes en este artículo, por favor no dude en contactarnos a través de siguiente canal de Discord: Math & Code.