Clase 3: Feed Forward Networks
Se dice que una Red Neuronal puede aproximar cualquier función en una región cerrada.
Es decir,
\[\underset{x \in \mathbb{D}}{max}|f(x) - \hat{f(x)}| \le \epsilon\]
con \(D \subset \mathbb{R}\) y \(\epsilon >0\).
Es un tipo de Arquitectura caracterizada por Nodos en un nivel que se conectan con todos los nodos del siguiente nivel. Este es probablemente el tipo de Arquitectura de Red Neuronal más común.
Este tipo de Redes tiene distintos nombres que son usados de manera intercambiable:
\[ Z_1 = X\] \[Z_{i +1} = \sigma_i(Z_i W_i + b_i)\] \[h_\theta(X) = Z_{L + 1}\]
con \(\theta = \{W_{1:L}, b_{1:L}\}\)
para \(i=1,...,L\)
\(Z_{i}\) corresponde a la salida de la capa \(i\), \(W_i\) corresponde al conjunto de parámetros de la capa \(i\), y \(b_i\) corresponde al bias de la capa \(i\).
\[Z_{i + 1} = \sigma_i(Z_i W_i + b_i^T)\]
Si chequeamos las dimensiones:
Tenemos un problema y es que esto hace que las dimensiones no calcen. Esto sería una operación no válida en términos matriciales. Sin embargo es posible realizarla aplicando Broadcasting.
Broadcasting
Corresponde a una replica de una dimensión de manera de permitir alguna operación que requiera que ciertas dimensiones calcen.
Broadcasting Rules
Matemáticamente el Broadcasting en este caso corresponde a:
\[ b_i^T = 1 b_i^T\]
donde 1, es un vector de unos de \(m \times 1\) y \(b_i\) es de dimensión \(n_{i+1} \times 1\), al cuál se está aplicando el producto externo.
El Broadcasting permitirá que \(b_i\) tenga ahora dimensiones \(m \times n_{i+1}\), lo cuál permitirá que la operación de suma se pueda realizar.
El Broadcasting evita que se tenga que almacenar información repetida, lo cual permite que las implementaciones sean más eficientes en términos de memoria. Siempre que se pueda se debe utilizar Broadcasting para simplificar un cálculo.
Más info ver: Numpy Docs
Valores necesarios para el cómputo de una red neuronal que deben ser determinados por el modelador. Estos valores NO pueden ser aprendidos de manera autónoma por la red neuronal.
En el aprendizaje supervisado contamos principalmente con la resolución de dos tipos de Problemas: Clasificación y Regresión. Dependiendo del tipo de Problema armaremos nuestra hipótesis.
\[h_\theta(X) = Z_{L+1} \in \mathbb{R}^{m \times k}\]
Adicionalmente se pueden utilizar funciones como la sigmoide o ReLU para forzar salidas entre 0 y 1 o entre 0 e \(\infty\) respectivamente.
Normalmente las funciones necesarias en la capa de salida van embebidas en la Loss Function. Normalmente estas funciones sí deben aplicarse al momento de la Predicción del modelo.
Como convención, las funciones de activación sólo se aplicarán a las Hidden Layers. Es decir \(\sigma_{L+1}(x) = x\).
Otras convenciones utilizan funciones de activación para la capa de salida. Esto bajo el abánico de Pytorch no es correcto ya que la Activación de la última capa esta embebida en la Loss Function (Recordar como Softmax es parte del Cross Entropy).
Ahora, sí es posible utilizar funciones de Activación a la salida de una predicción, pero dichas funciones tienen otro propósito y no son del todo estrictamente necesarias.
¿Puedo aplicar distintas Funciones de Activación a cada Neurona?
Para ver más Activation Functions y detalles de su funcionamiento, ir directamente a la Documentación de Pytorch.
Al igual que el caso de la Hipótesis, la Loss Function dependerá del tipo de problema a resolver. Existen muchas Loss Functions, pero los más comunes para problemas generales son las siguientes:
\[BCE_i = - \left[y_i \cdot log(h(x_i)) + (1-y_i) log(1-h(x_i))\right]\]
donde \(h(x)\) corresponde a un valor de probabilidad de la clase positiva (debe ir entre 0 y 1).
En Pytorch se suele utilizar BCEWithLogitsLoss
ya que aplica una función Sigmoide a la capa de salida además de ser una clase numericamente más estable. Esto garantiza que la salida de la Red tiene valores entre 0 y 1 como se necesita.
\[CE_i = -log \left(\frac{exp(h_{(i=y)}(x_i))}{\sum_{j=1}^k exp(h_j(x_i))}\right)\]
En Pytorch se suele utilizar CrossEntropyLoss
ya que combina aplica una función Softmax a la capa de salida además de ser una clase numericamente más estable.
Para este tipo de problema se debería aplicar un Negative LogLoss combinado con la salidas de una red que van entre 0 y 1 (es decir, que se aplica una Sigmoide)
En Pytorch se suele utilizar BCEWithLogitsLoss
ya que combina aplica una función Softmax a la capa de salida y permite resultados de más de una dimensión.
\[L1_i = |y_i - h(x_i)|\]
\[L2_i = (y_i - h(x_i))^2\]
Es importante recordar que en general se debe calcular un valor agregado de la Loss Function. En Pytorch a esto se le llama reduction
. Donde el más utilizado es reduction="mean"
. Es decir,
\[l = \frac{1}{m}\sum_{i=1}^m L_i\]
Gradient Descent corresponde al algoritmo de Optimización más popular, pero no necesariamente el más eficiente. Distintas variantes han ido apareciendo para ir mejorando eventuales deficiencias de la proposición inicial.
\[\theta := \theta - \frac{\alpha}{m}\nabla_\theta l(h_\theta(X), y), \text{donde $X \in \mathbb{R}^{m \times n}$ e $y \in \mathbb{R}^{m \times 1}$}\]
La dirección del Gradiente utilizando menos puntos debería ser más o menos similar. Sin duda, más ruidoso, pero a la larga debería dirigir en casi la misma dirección. Por lo que podríamos hacer actualizaciones de parámetros utilizando B datos con B << m.
Esto entrega como beneficio, menos requerimientos de memoria, ya que operarían matrices más pequeñas, por lo tanto, requiere de menos RAM tanto en CPU como en GPU.
\[\theta := \theta - \frac{\alpha}{B}\nabla_\theta l(h_\theta(X), y), \text{donde $X \in \mathbb{R}^{B \times n}$ e $y \in \mathbb{R}^{B \times 1}$}\]
Se van tomando \(B\) muestras de manera incremental hasta utilizar la totalidad de datos de entrenamiento
\[u_{t + 1} = \beta u_t + (1-\beta) \nabla_\theta f(\theta_t)\] \[\theta_{t+1} = \theta_t - \alpha u_{t + 1}\]
donde \(0<\beta<1\), pero normalmente \(\beta=0.9\).
Este cálculo se denomina un Exponential Moving Average de los Gradientes.
\[\begin{align} u_{t+1}&=(1-\beta)\nabla_\theta f(\theta_{t}) + \beta u_t \\ u_{t+1}&=(1-\beta)\nabla_\theta f(\theta_{t}) + \beta \left[(1-\beta) \nabla_\theta f(\theta_{t-1}) + \beta u_{t-1}\right] \\ u_{t+1}&=(1-\beta)\nabla_\theta f(\theta_{t}) + \beta (1-\beta) \nabla_\theta f(\theta_{t-1}) + \beta^2 (1-\beta) \nabla_\theta f(\theta_{t-2})... \\ \end{align}\]
La componente de momento, está tomando en consideración todos los otros Gradientes en pasos anteriores para escoger correctamente la dirección del Gradiente actual.
\[u_{t + 1} = \beta u_t + (1-\beta) \nabla_\theta f(\theta_t - \alpha u_t)\] \[\theta_{t+1} = \theta_t - \alpha u_{t + 1}\]
Notar que la lógica es casi la misma, sólo que el Gradiente se evalúa en un punto futuro. Es decir, \(\theta_t-\alpha u_t\) corresponde al punto siguiente utilizando SGD con Momentum.
¿Qué tal, si la tasa de aprendizaje se va adaptando en el tiempo y deja de ser estática?
Idea
\[r_{t+1} = r_t + \nabla_\theta f(\theta_t)^2\] \[\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{r_{t+1}}}\nabla_\theta f(\theta_t)\]
Idea
\[s_{t+1} = \beta r_t + (1-\beta) \nabla_\theta f(\theta_t)^2\] \[\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{s_{t+1}}}\nabla_\theta f(\theta_t)\]
Idea
\[v_{t+1} = \beta_1 v_t + (1-\beta_1) \nabla_\theta f(\theta_t)\] \[s_{t+1} = \beta_2 s_t + (1-\beta_2) \nabla_\theta f(\theta_t)^2\] \[\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{s'_{t+1}}} v'_{t+1}\]
\[v'_{t+1} = \frac{v_{t+1}}{1-\beta_1^{t+1}}\] \[s'_{t+1} = \frac{v_{t+1}}{1-\beta_2^{t+1}}\]
Pytorch utiliza 0.9 y 0.999 como valores de \(\beta_1\) y \(\beta_2\) respectivamente.