Redes neuronales representadas como DAGs de programas computacionales parametrizados
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:

  1. Visualización: Los DAGs permiten una visualización más intuitiva de la estructura de las redes.
  2. Optimización: Facilitan el uso de técnicas de optimización basadas en grafos.
  3. Diferenciación automática: Facilitan la implementación de algoritmos como la retropropagación.
  4. 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 f1,,fnf_1, \ldots, f_n que se aplican de forma secuencial a una entrada s0S0.s_0 \in S_{0}. Denominaremos a estos programas cadenas de computación y los representaremos como f=fnf1.f = f_n \circ \cdots \circ f_1.

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.

Computation Chain

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:

s0S0,s1=f1(s0)S1,s2=f2(s1)S2,sn=fn(sn1)=f(s0)Sn.\begin{equation} \begin{aligned} s_0 &\in S_{0}, \\ s_{1} &= f_{1}(s_{0}) \in S_{1}, \\ s_{2} &= f_{2}(s_{1}) \in S_{2}, \\ &\vdots \\ s_{n} &= f_{n}(s_{n-1}) = f(s_0) \in S_{n}.\\ \end{aligned} \end{equation}

Donde s0s_0 es la entrada, skSks_k\in S_k para k=1,,n1,k = 1, \ldots, n-1, son los estados intermedios del programa y snSns_n\in S_n es la salida. Es fundamental que el dominio (espacios de entrada) de fkf_k sea compatible con la imagen (espacio de salida) de fk1f_{k-1} para que la composición tenga sentido. Es decir, que fk:Sk1Skf_k: S_{k-1} \to S_k para k=1,,n.k = 1, \ldots, n. De forma equivalente a la ecuación (1), la cadena de computación completa se puede escribir en una única expresión:

sn=f(s0)=fnf1(s0).\begin{equation} s_n = f(s_0) = f_n \circ \cdots \circ f_1(s_0). \end{equation}

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 G=(V,E)G = (V, E) se compone de un conjunto de nodos VV y un conjunto de aristas EV×V.E\subseteq V \times V. Una arista (u,v)E(u, v) \in E es un par ordenado de nodos u,vV,u, v\in V, y se puede representar como uv.u \to v. para indicar que vv depende de u.u. Para representar las entradas y las salidas, es conveniente utilizar semi-aristas entrantes j\to j y semi-aristas salientes i.i \to.

En un grafo G=(V,E)G = (V, E), los padres de un nodo vVv\in V son aquellos nodos uVu\in V tales que (u,v)E,(u, v)\in E, denotados como:

P(v)={uV:(u,v)E}.\begin{equation} \mathcal{P}(v) = \{u\in V: (u, v)\in E\}. \end{equation}

De manera similar, los hijos de un nodo vVv\in V son los nodos wVw\in V tales que (v,w)E,(v, w)\in E, representados por:

C(v)={wV:(v,w)E}.\begin{equation} \mathcal{C}(v) = \{w\in V: (v, w)\in E\}. \end{equation}

Los nodos sin padres son llamados raíces y los nodos sin hijos se conocen como hojas.

Un camino de ii a jj en un grafo G=(V,E)G = (V, E) es una secuencia de nodos v1,,vkv_1, \ldots, v_k tal que iv1vkj.i \to v_1 \to \cdots \to v_k \to j. 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 iji \preceq j si existe un camino de ii a j.j. 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 iji \preceq j si y solo si no hay un camino de jj a i.i.

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:

  1. Existe un nodo entrada s0s_0 y un nodo salida sn.s_n.
  2. Cada función intermedia fkf_k produce un único valor skSk.s_k \in S_k.

Con un número finito de nodos como V={0,1,,n},V = \{0, 1, \ldots, n\}, donde el nodo 00 es la raíz, correspondiente a la entrada del programa, y el nodo nn es una hoja, correspondiente a la salida del programa. Según estas suposiciones, excepto por s0s_0, cada variable sks_k está en biyección con una función fk.f_k. Por lo tanto, el nodo 00 representa la entrada s0s_0, mientras que los nodos 1,,n1, \ldots, n representan las funciones f1,,fnf_1, \ldots, f_n y salidas s1,,sn.s_1, \ldots, s_n. Es decir, cada nodo kk representa tanto la función fkf_k y la salida sk.s_k.

Las aristas de un DAG representan las dependencias entre los nodos. Los padres de un nodo k,k, denotados como {i1,,ipk}:=P(k),\{i_1, \ldots, i_{p_k}\} := \mathcal{P}(k), donde pk:=P(k),p_k:=|\mathcal{P}(k)|, indican las variables sP(k):={si1,,sipk}s_{\mathcal{P}(k)} := \{s_{i_1}, \ldots, s_{i_{p_k}}\} que la función fkf_k necesita para calcular su salida sks_k. En otras palabras, los padres {i1,,ipk}\{i_1, \ldots, i_{p_k}\} indican funciones fi1,,fipkf_{i_1}, \ldots, f_{i_{p_k}} que son necesarias para computar fk.f_k.

Algoritmo 1. Ejecución de un programa

Funciones:\text{\textbf{Funciones:}} f1,,fn en orden topoloˊgicof_1, \ldots, f_n \text{ en orden topológico}

Entrada:\text{\textbf{Entrada:}} s0S0s_0\in S_0

1.

Para k=1,,n\text{\textbf{Para} } k = 1, \ldots, n hacer:\text{\textbf{hacer:}}

2.

Encontrar los nodos padres {i1,,ipk}:=P(k)\{i_1, \ldots, i_{p_k}\} := \mathcal{P}(k)

3.

Calcular sk:=fk(sP(k)):=fk(si1,,sipk)s_k :=f_k\big(s_{\mathcal{P}(k)}\big):= f_k(s_{i_1}, \ldots, s_{i_{p_k}})

Fin: f(s0)=sn\text{\textbf{Fin:} } f(s_0) = s_n

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 V={0,1,,n}V=\{0, 1, \ldots, n\} 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 sk[n]s_k \in [n]

sk:=fk(sP(k))Sk.\begin{equation} s_k := f_k\big(s_{\mathcal{P}(k)}\big)\in S_k. \end{equation}

Obsérvese que podemos ver fkf_k como una función de una sola entrada de sP(k),s_{\mathcal{P}(k)}, el cual es una nn-tupla de elementos, o como una función de múltiples entradas si1,,sipk.s_{i_1}, \ldots, s_{i_{p_k}}. 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 s0S0.s_0 \in S_0. como s0=(s0,1,,s0,m0)s_0 = (s_{0, 1}, \ldots, s_{0, m_0}) con S0=S0,1××S0,m0,S_0 = S_{0, 1} \times \cdots \times S_{0, m_0}, ya que las funciones posteriores siempre pueden filtrar los elementos de s0s_0 que necesitan. Del mismo modo, si una función intermedia fkf_k tiene varias salidas, siempre las agrupamos en una sola salida skSks_k \in S_k como sk=(sk,1,,sk,mk)s_k = (s_{k, 1}, \ldots, s_{k, m_k}) con Sk=Sk,1××Sk,mk,S_k = S_{k, 1} \times \cdots \times S_{k, m_k}, ya que las funciones posteriores siempre pueden filtrar los elementos de sks_k.

Computation Chain

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 fkf_k siempre tiene una única salida sk,s_k, se puede considerar que un nodo kk representa tanto la función fkf_k como la salida sk.s_k. 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 F\mathbb{F}, como por ejemplo los número reales R\mathbb{R} o los números complejos C,\mathbb{C}, es un grafo dirigido acíclico (DAG) donde los nodos raíces son elementos del campo F\mathbb{F} 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 F.\mathbb{F}. 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.

Definición 1. Circuito y tamaño polinomial

El tamaño S(C)S(C) de un circuito aritmético CC es el número de aristas en el grafo dirigido acíclico (DAG) que representa al circuito. El tamaño del polinomio S(f)S(f) de un polinomio ff es el tamaño del circuito aritmético CC más pequeño que representa a f.f.

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 f1,,fnf_1, \ldots, f_n que se aplican de forma secuencial a una entrada s0S0.s_0 \in S_0. En este caso, las funciones fkf_k son usualmente funciones parametrizadas que dependen de un vector de parámetros wkWk,w_k \in \mathcal{W}_k, donde Wk\mathcal{W}_k es el espacio de parámetros de la función fk.f_k. Por lo tanto,

x=s0S0,s1=f1(s0;w1)S1,s2=f2(s1;w2)S2,sn=fn(sn1;wn)=f(s0;w)Sn.\begin{equation} \begin{aligned} x &= s_0 \in S_{0}, \\ s_{1} &= f_{1}(s_{0}; w_1) \in S_{1}, \\ s_{2} &= f_{2}(s_{1}; w_2) \in S_{2}, \\ &\vdots \\ s_{n} &= f_{n}(s_{n-1}; w_n) = f(s_0; w) \in S_{n}.\\ \end{aligned} \end{equation}

para una entrada s0S0s_0\in S_0 y para los parámetros entrenables w=(w1,,wn)W=W1××Wn.w = (w_1, \ldots, w_n) \in \mathcal{W} = \mathcal{W}_1 \times \cdots \times \mathcal{W}_n. Cada función fkf_k es una capa de la red y cada skSks_k \in S_k es una representación intermedia de la entrada s0.s_0. la dimension de SkS_k se conoce como la dimensión de la capa (o número de unidades ocultas) de la capa k.k. Una red de propagación hacia adelante puede ser definida como una función f:S0×WSnf:S_0 \times \mathcal{W} \to S_n que mapea una entrada s0s_0 y parámetros ww a una salida sn.s_n.

Dado un programa parametrizado de este tipo, los parametros ww los podemos aprender ajustando ww de acuerdo a un conjunto de datos de entrenamiento. Por ejemplo, dado un conjunto de datos de pares (xi,yi),(x_i, y_i), podemos minimizar la función de pérdida L(w)=i=1m(f(xi;w),yi).L(w) = \sum_{i=1}^{m} \ell(f(x_i; w), y_i). 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

sk=fk(sk1;wk)=ak(Wksk1+bk),\begin{equation} s_k = f_k(s_{k-1}; w_k) = a_k(W_k s_{k-1} + b_k), \end{equation}

donde wk=(Wk,bk)w_k = (W_k, b_k) son los parámetros de la capa k,k, WkW_k es un matriz de pesos y bkb_k es un vector de sesgos. Ademas la capa las podemos descomponer en dos funciones. La función sWks+bks \mapsto W_k s + b_k es una capa afín y la función sak(s)s \mapsto a_k(s) no lineal sin parámetros, llamada función de activación.

Generalmente, podemos remplazar el producto matrix-vector Wksk1W_k s_{k-1} por cualquier función lineal de sk1.s_{k-1}. Por ejemplo, las capas convolucionales utilizan de convolución de la entrada sk1s_{k-1} con algunos filtros Wk.W_k.

Remark 1. Tratamiento de entradas múltiples

A veces es necesario tratar con redes de múltiples entradas. Por ejemplo, supongamos que queremos diseñar una función g(x1,x2,wg)g(x_1, x_2, w_g), donde x1X1x_1 \in \mathcal{X}_1 y x2X2x_2 \in \mathcal{X}_2 son dos entradas. Una forma simple de hacerlo es usar la concatenación x=x1x2X1X2x = x_1 \oplus x_2 \in \mathcal{X}_1 \oplus \mathcal{X}_2 como entrada a una red f(x,Wf).f(x, W_f). 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 n=1n=1 (es decir, una sola capa), la salida un MLP es una función lineal de la entrada xx es

s1=a1(W1x+b1).\begin{equation} s_1 = a_1(W_1 x + b_1). \end{equation}

Esto es llamado un modelo lineal generalizado (generalized linear model o GLM). En este caso, la función de activación a1a_1 es la función identidad. Los modelos lineales generalizados son un caso especial de los MLPs. Es particular, cuando a1a_1 es la función softargmax\operatorname*{softargmax}, se tiene un modelo de regresión logística. Por lo general para la profundidad n>1,n>1, la salida de un MLP es

sn=an(Wnsn1+bn).\begin{equation} s_n = a_n(W_n s_{n-1} + b_n). \end{equation}

Esto puede ser visto como un GLM sobre la representación intermedia sn1s_{n-1} de la entrada s0.s_0. 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 aka_k 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:

ReLU(x)=max(0,x)={xsi x0,0si x<0.\begin{equation} \text{ReLU}(x) = \max(0, x) = \begin{cases} x & \text{si } x \geq 0, \\ 0 & \text{si } x < 0. \end{cases} \end{equation}

Es una función lineal por partes e incluye un punto de inflexión en x=0.x=0. Una perceptron multicapa con activaciones ReLU se denomina red neuronal rectificadora. Las capas toman las forma

sk=ReLU(Wksk1+bk).\begin{equation} s_k = \text{ReLU}(W_k s_{k-1} + b_k). \end{equation}

donde la ReLU es aplicada elemento a elemento. La función ReLU puede ser remplazada con una versión suavizada, conocida como softplus:

softplus(x)=log(1+exp(x)).\begin{equation} \text{softplus}(x) = \log(1 + \exp(x)). \end{equation}

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 xRd,x\in \mathbb{R}^d, la función de max-pooling es

maxpooling(x)=maxj[d]xj.\begin{equation} \text{maxpooling}(x) = \max_{j \in [\,d\,]} x_j. \end{equation}

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:

logsumexp(x):=softmax(x):=log(j=1dexp(xj)).\begin{equation} \text{logsumexp}(x) := \text{softmax}(x) := \log\left(\sum_{j=1}^{d} \exp(x_j)\right). \end{equation}

La función log-sum-exp puede ser vista como una generalización de la función softplus a vectores, en efecto para todo xRx \in \mathbb{R}

logsumexp((x,0))=softplus(x).\begin{equation} \operatorname*{logsumexp}((x, 0)) = \operatorname*{softplus}(x). \end{equation}

Una implementación númericamente estable de la función log-sum-exp es la siguiente:

logsumexp(x)=logsumexp(xc1)+c,\begin{equation} \text{logsumexp}(x) = \text{logsumexp}(x - c\mathbf{1}) + c, \end{equation}

donde 1\mathbf{1} es un vector de unos y c=maxj[d]xj.c = \max_{j\in [\,d\,]} x_j.

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,

step(x)={1si x0,0si x<0.\begin{equation} \text{step}(x) = \begin{cases} 1 & \text{si } x \geq 0, \\ 0 & \text{si } x < 0. \end{cases} \end{equation}

Es un mapeo de R\mathbb{R} a {0,1}.\{0, 1\}. Desafortunadamente, la función de paso binaria es discontinua en x=0.x=0. 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 R\mathbb{R} a (0,1)(0, 1) y es definida como:

logistic(x)=11+exp(x)=exp(x)1+exp(x)=12+12tanh(x2).\begin{equation} \begin{aligned} \text{logistic}(x) &= \frac{1}{1 + \exp(-x)} \\ &= \frac{\exp(x)}{1 + \exp(x)} \\ &= \frac{1}{2} + \frac{1}{2}\tanh\left(\frac{x}{2}\right). \end{aligned} \end{equation}

Ella mapea (,0)(-\infty, 0) a (0,0.5)(0, 0.5) y [0,)\lbrack 0, \infty \rparen a [0.5,1).\lbrack 0.5, 1\rparen. y satisface que logistic(0)=0.5.\text{logistic}(0) = 0.5. 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 step(x).\text{step}(x). Además la función logística se puede obtener como la derivada de la función softplus(x),\text{softplus}(x), es decir, para todo xRx \in \mathbb{R}

logistic(x)=ddxsoftplus(x).\begin{equation} \text{logistic}(x) = \frac{d}{dx}\text{softplus}(x). \end{equation}

Dos propiedades importantes de la función logística es que para todo xRx \in \mathbb{R}

logistic(x)=1logistic(x),\begin{equation} \text{logistic}(-x) = 1 - \text{logistic}(x), \end{equation}

y

ddxlogistic(x)=logistic(x)(1logistic(x))=logistic(x)logistic(x).\begin{equation} \begin{aligned} \frac{d}{dx}\text{logistic}(x) &= \text{logistic}(x)(1 - \text{logistic}(x)) \\ &= \text{logistic}(x)\text{logistic}(-x). \end{aligned} \end{equation}

Mapeos de probabilidad de vectores a vectores

En algunas ocasiones queremos mapear un vector en un vector de probabilidades. Este es una mapeo de Rd\mathbb{R}^d a un simplex de probabilidad, definido por:

Δd={pRd:j[d],  pj0   y   j=1dpj=1}.\begin{equation} \Delta^{d} = \left\{p\in \mathbb{R}^d: \forall j \in [\,d\,],\; p_j \geq 0\; \text{ y }\; \sum_{j=1}^{d} p_j = 1\right\}. \end{equation}

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 argmax\text{argmax} 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,

argmax(x)=ϕ(argmaxj[d]xj),\begin{equation} \text{argmax}(x) = \phi(\arg\max_{j\in [\,d\,]} x_j), \end{equation}

donde ϕ(j)\phi(j) denota el one-hot encoding de un entero j[d],j\in [\,d\,], es decir, ϕ(j)i=1\phi(j)_i = 1 si i=ji = j y cero en otro caso, esto es

ϕ(j)=(0,,0,1,0,,0)=ej{0,1}d.\begin{equation} \phi(j) = (0, \ldots, 0, 1, 0, \ldots, 0) = e_j \in \{0, 1\}^d. \end{equation}

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 argmax\text{argmax} no es diferenciable, lo que dificulta su uso en la retropropagación.

Una versión suavizada de la función argmax\text{argmax} es la función softargmax\text{softargmax}. La función softargmax\text{softargmax} está definida como

softargmax(x)=exp(x)j=1dexp(xj).\begin{equation} \text{softargmax}(x) = \frac{\exp(x)}{\sum_{j=1}^{d} \exp(x_j)}. \end{equation}

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 argmax.\text{argmax}. La salida de softargmax\text{softargmax} pertenece al interior relativo del simplex de probabilidad Δd,\Delta^{d}, lo que significa que nunca puede alcanzar los bordes del simplex. Si denotamos p=softargmax(x),p = \text{softargmax}(x), entonces pj(0,1),p_j \in (0, 1), es decir 0<pj<10 < p_j < 1 para todo j[d].j \in [\,d\,]. La función softargmax\text{softargmax} es el gradiente de la función log-sum-exp\text{log-sum-exp}, es decir, para todo xRdx \in \mathbb{R}^d

softargmax(x)=log-sum-exp(x).\begin{equation} \text{softargmax}(x) = \nabla \text{log-sum-exp}(x). \end{equation}

La función softargmax\text{softargmax} se puede ver como una generalización de la función logistic\text{logistic} a vectores, en efecto

softargmax(x)=[logistic(x1)logistic(xd)].\begin{equation} \text{softargmax}(x) = \begin{bmatrix} \text{logistic}(x_1) \\ \vdots \\ \text{logistic}(x_d) \end{bmatrix}. \end{equation}
Remark 2. Grados de libertad e inversa de la función softargmax

La función softargmax\text{softargmax} satisface la propiedad que para todo xRdx \in \mathbb{R}^d y para todo cR,c \in \mathbb{R},

p=softargmax(x+c1)=softargmax(x).\begin{equation} p = \text{softargmax}(x + c\mathbf{1}) = \text{softargmax}(x). \end{equation}

Esto quiere decir que la función softargmax\text{softargmax} tiene d1d-1 grados de libertad y que no es invertible. Sin embargo, debido a la anterior propiedad, sin perdida de generalidad, nosotros podemos imponer que x1=j=1dxj=0\pmb{x}^\top \pmb{1} = \sum_{j=1}^{d} x_j = 0 (si no es el caso, podemos simplemente hacer xixixˉx_i \leftarrow x_i - \bar{x}, en donde xˉ=1dj=1dxj\bar{x} = \frac{1}{d}\sum_{j=1}^{d} x_j es la media de xx). Con esta restricción y junto con el hecho de

log(pi)=xilogj=1dexp(xj),\begin{equation} \log(p_i) = x_i - \log\sum_{j=1}^{d} \exp(x_j), \end{equation}

obtenemos

j=1dlog(pj)=dlogj=1dexp(xj).\begin{equation} \sum_{j=1}^{d} \log(p_j) = - d \log \sum_{j=1}^{d} \exp(x_j). \end{equation}

Por lo tanto, la función softargmax\text{softargmax} es invertible en el sentido de que podemos recuperar xx a partir de

xi=[softargmax1(p)]i=log(pi)1dj=1dlog(pj).\begin{equation} x_i = [\text{softargmax}^{-1}(p)]_i = \log(p_i) - \frac{1}{d}\sum_{j=1}^{d} \log(p_j). \end{equation}

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 n+1n+1 capas f1,,fn+1f_1, \ldots, f_{n+1}. Seguramente, siempre que fn+1f_{n+1} sea la función identidad, el conjunto de funciones que esta red puede representar es el mismo que el de una red de nn capas f1,,fn.f_1, \ldots, f_n. Por lo tanto, podemos considerar una red de n+1n+1 capas como una red de nn. 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 fkf_k 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

sk=fk(sk1;wk):=sk1+hk(sk1;wk).\begin{equation} s_{k} = f_k(s_{k-1}; w_k):= s_{k-1} + h_k(s_{k-1}; w_k). \end{equation}

La función hkh_k es la función residual, dado que modela la diferencia entre la entrada y la salida, es decir, hk(sk1;wk)=sksk1.h_k(s_{k-1}; w_k) = s_{k} - s_{k-1}. La función hkh_k puede ser vista como una corrección que se suma a la entrada sk1s_{k-1} para obtener la salida sk.s_k. La suma con sk1s_{k-1} se conoce como conexión de salto (skip connection o shortcut). Siempre que sea posible ajustar wkw_k para que hk(sk1;wk)=0,h_k(s_{k-1}; w_k) = 0, entonces la función fkf_k se convierte en la función identidad. Por ejemplo, si nosotros usamos:

hk(sk1;wk)=Ckak(Wksk1+bk)+dk,\begin{equation} h_k(s_{k-1}; w_k) = C_ka_k(W_ks_{k-1} + b_k) + d_k, \end{equation}

donde wk=(Wk,bk,Ck,dk)w_k = (W_k, b_k, C_k, d_k) son los parámetros de la capa k,k, entonces basta con ajustar Ck=0C_k = 0 y dk=0d_k = 0 para que hk(sk1;wk)=0.h_k(s_{k-1}; w_k) = 0. 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:

sk=fk(sk1;wk):=ak(sk1+hk(sk1;wk)),\begin{equation} s_{k} = f_k(s_{k-1}; w_k):= a_k(s_{k-1} + h_k(s_{k-1}; w_k)), \end{equation}

donde aka_k 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:

  1. Vector a secuencia (uno a muchos): fd:Rd×RpRl×m.\begin{equation} f^d:\mathbb{R}^d \times \mathbb{R}^{p} \to \mathbb{R}^{l\times m }. \end{equation}
  2. Secuencia a vector (muchos a uno): fe:Rl×d×RpRm.\begin{equation} f^e:\mathbb{R}^{l\times d} \times \mathbb{R}^{p} \to \mathbb{R}^{m}. \end{equation}
  3. Secuencia a secuencia (muchos a muchos, alineados): fa:Rl×d×RpRl×m.\begin{equation} f^a:\mathbb{R}^{l\times d} \times \mathbb{R}^{p} \to \mathbb{R}^{l\times m}. \end{equation}
  4. Secuencia a secuencia (muchos a muchos, no alineados): fu:Rl×d×RpRl×m.\begin{equation} f^u:\mathbb{R}^{l\times d} \times \mathbb{R}^{p} \to \mathbb{R}^{l'\times m}. \end{equation}

donde ll es la longitud de la secuencia de entrada y ll' es la longitud de la secuencia de salida. Observe que se usa el mismo número de parámetros pp para cada una de las configuraciones, pero esto por supuesto no es necesario. A lo largo de este post, usaremos la notación p1:l:=(p1,,pl)p_{1:l} := (p_1, \ldots, p_l) para denotar una secuencia de ll vectores.

Vector a secuencia

En esta configuración, una función decodificadora p1:l=fd(x;w)p_{1:l} = f^d(x; w) mapea un vector xRdx \in \mathbb{R}^d y un vector de parámetros wRpw \in \mathbb{R}^p a una secuencia de vectores p1:lRl×m.p_{1:l} \in \mathbb{R}^{l\times m}.

Computation Chain

Figura 3. Uno a muchos (decodificación). Un vector xRdx \in \mathbb{R}^d es mapeado a una secuencia de vectores p1:lRl×m.p_{1:l} \in \mathbb{R}^{l\times m}.

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 p1:l=fd(x;w)p_{1:l} = f^d(x; w) a través de la recursión:

si:=g(x,si1;wg),i[l],pi:=h(si;wh),i[l].\begin{equation} \begin{aligned} s_i &:= g(x, s_{i-1}; w_g), \quad i \in [\,l\,], \\ p_i &:= h(s_i; w_h), \quad i \in [\,l\,]. \end{aligned} \end{equation}

donde w;=(wg,wh,s0)w ;= (w_g, w_h, s_0) son los parámetros de la red, s0s_0 es el estado inicial. El objetivo de gg es actualizar el estado sis_i en función de la entrada xx y el estado anterior si1,s_{i-1}, mientras que el objetivo de hh es mapear el estado sis_i a un vector pip_i dado el estado si.s_i. Es importante destacar que los parámetros de gg y hh se cruzan a los largo de los pasos de tiempo. Típicamente, gg y hh se parametrizan utilizando MLPs de una capa oculta. Nótese que gg tiene múltiples entradas.

Secuencia a vector

En esta configuración, se define una función codificadora p=fe(x1:l;w)p = f^e(x_{1:l}; w) que mapea una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} y un vector de parámetros wRpw \in \mathbb{R}^p a un vector pRm.p \in \mathbb{R}^m.

Computation Chain

Figura 4. Muchos a uno (codificación). Una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} es mapeada a un vector pRm.p \in \mathbb{R}^m.

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 p=fe(x1:l;w)p = f^e(x_{1:l}; w) a través de la recursión:

si:=g(xi,si1;wg),i[l],p:=pooling(s1:l),\begin{equation} \begin{aligned} s_i &:= g(x_i, s_{i-1}; w_g), \quad i \in [\,l\,], \\ p &:= \text{pooling}(s_{1:l}), \end{aligned} \end{equation}

donde w:=(wg,s0)w := (w_g, s_0) son los parámetros de la red, s0s_0 es el estado inicial, y gg ahora actualiza los estados del codificador y no toma predicciones anteriores como entrada. La función de agrupamiento (pooling\text{pooling}) 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 p1:l=fa(x1:l;w)p_{1:l} = f^a(x_{1:l}; w) que mapea una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} y un vector de parámetros wRpw \in \mathbb{R}^p a una secuencia de vectores p1:lRl×m.p_{1:l} \in \mathbb{R}^{l\times m}. La longitud de la secuencia de entrada y la secuencia de salida son iguales.

Computation Chain

Figura 4. Muchos a muchos (alineados). Una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} es mapeada a una secuencia de vectores p1:lRl×m.p_{1:l} \in \mathbb{R}^{l\times m}.

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 p1:l=fa(x1:l;w)p_{1:l} = f^a(x_{1:l}; w) a través de la recursión:

si:=g(xi,si1;wg),i[l],pi:=h(si;wh),i[l].\begin{equation} \begin{aligned} s_i &:= g(x_i, s_{i-1}; w_g), \quad i \in [\,l\,], \\ p_i &:= h(s_i; w_h), \quad i \in [\,l\,]. \end{aligned} \end{equation}

donde w:=(wg,wh,s0)w := (w_g, w_h, s_0) son los parámetros de la red, s0s_0 es el estado inicial y gg y hh son funciones similares a las definidas en la configuración uno a muchos. La función gg actualiza el estado sis_i en función de la entrada xix_i y el estado anterior si1,s_{i-1}, mientras que la función hh mapea el estado sis_i a un vector pip_i dado el estado si.s_i.

Secuencia a secuencia (no alineada)

En esta configuración, se define una función de alineación p1:l=fu(x1:l;w)p_{1:l'} = f^u(x_{1:l}; w) que mapea una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} y un vector de parámetros wRpw \in \mathbb{R}^p a una secuencia de vectores p1:lRl×m.p_{1:l'} \in \mathbb{R}^{l'\times m}.

Computation Chain

Figura 5. Muchos a muchos (no alineados). Una secuencia de vectores x1:lRl×dx_{1:l} \in \mathbb{R}^{l\times d} es mapeada a una secuencia de vectores p1:lRl×m.p_{1:l'} \in \mathbb{R}^{l'\times m}.

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 p1:l=fu(x1:l;w)p_{1:l'} = f^u(x_{1:l}; w) a través de la recursión:

c:=fe(x1:l;we),p1:l:=fd(c,wd),\begin{equation} \begin{aligned} c &:= f^e(x_{1:l}; w_e), \\ p_{1:l} &:= f^d(c, w_d), \end{aligned} \end{equation}

donde w:=(we,wd)w := (w_e, w_d) son los parámetros de la red, fef^e es una función codificadora que mapea la secuencia de entrada a un vector de contexto c,c, y fdf^d es una función decodificadora que mapea el vector de contexto a la secuencia de salida. La función de codificación fef^e y la función de decodificación fdf^d pueden ser implementadas como RNNs. Los pasos en detalle, serían:

si:=g(xi,si1;wg),i[l],c:=pooling(s1:l),zi:=g(c,pi1,zi1;wg),i[l],pi:=h(zi;wh),i[l].\begin{equation} \begin{aligned} s_i &:= g(x_i, s_{i-1}; w_g), \quad i \in [\,l\,], \\ c &:= \text{pooling}(s_{1:l}), \\ z_i &:= g(c, p_{i-1}, z_{i-1}; w_g), \quad i \in [\,l'\,], \\ p_i &:= h(z_i; w_h), \quad i \in [\,l'\,]. \end{aligned} \end{equation}

Esta arquitectura se denomina acertadamente arquitectura codificador-decodificador (encoder-decoder). Tenga en cuenta que denotamos la longitud de la secuencia objetivo como ll' en lugar de ll 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 dd 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 c,c, 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.

Referencias