En el anterior articulo vimos como crear la red MLP, ¿Pero como la usamos para solucionar nuestro problema? Todo se basa en saber elegir correctamente las capas de activación y la loss.
Primero de todo repasemos ambos términos:
- Regresión: Son problemas en los que buscamos predecir un valor continuo.
- Clasificación: Son de problemas en los que buscamos predecir a que clase/clases pertenecen.
Las redes neuronales debido a su capacidad de adaptarse al medio, permiten obtener muy buenos resultados en ambos campos.
Regresión:
En el articulo anterior vimos algunas capas de activación ¿Pero cuales son las más aconsejables para regresión? Cuando hablamos de regresión realmente podemos usar cualquier tipo de capa de activación exceptuando la ultima capa donde normalmente haremos que la salida no tenga capa de activación. ¿Porque? Porque si añadimos en la ultima capa una activación por ejemplo sigmoid estaremos forzando a obtener una salida en un rango de [0, 1], y probablemente busquemos una salida que tenga un rango de [-inf, inf].
Actualmente la tendencia es a usar ReLU y Pre-ReLU exceptuando en la ultima capa.
Clasificación:
Usar las redes para clasificación implica hacer una pequeña trampa y es que el problema sea de clasificación no quiere decir que la red lo vea como tal. Las redes internamente tratan el problema como si de un problema de regresión se tratase, lo único que se usan unas capas en concreto que permiten dicha adaptación.
Podemos tener un problema de clasificación de dos formas distintas:
- Única clase: Tenemos una entrada de datos que puede ser una clase de entre N clases.
- Múltiples clases: Tenemos una entrada de datos que puede tener activadas o no cada clase de las N clases.
Única clase:
Cuando hablamos de una problema de clasificación en el que debemos escoger una clase respecto N clases estamos hablando indirectamente de tener que usar One Hot Encoding, Softmax y Cross-entropy.
Primero de todo en la base de datos que usaremos para aprender la red deberemos usar el One Hot Enconding para codificar la clase (y) en un vector de N elementos con un uno y el resto zeros. Para entenderlo veamos el siguiente ejemplo:
X | Clase (Y) #3 | One hot vector |
{1,2,1,12,1} | 1 | 0,1,0 |
{-3,2,13,2,-2} | 0 | 1,0,0 |
{1,1,1,2,3} | 2 | 0,0,1 |
Como veis hemos convertido el numero de la clase a la que pertenece a un vector. Este vector será tan largo como clases haya.
Luego realizaremos el diseño de la red que más nos apetezca pero añadiremos como ultima capa tantas neuronas como clases tengamos (en el ejemplo anterior 3) y la activación Softmax.
¿Que es la activación softmax? Es por excelencia la activación que se usa en problemas de clasificación. Que se define de la siguiente forma:
{softmax}(x)_i = \frac{e^{x_i}}{\sum_j^N e^{x_j}}
softmax(\vec{x}) = \frac{\vec{x}}{\sum_j^N e^{x_j}}
La Softmax usa las N entradas para normalizar la salida, devolviendo un valor de entre [0, 1]. Ademas normaliza la salida y la suma de todas las salidas equivale a 1. Esta condición la hace una función apta para representar la distribución de probabilidades de las N clases.
softmax(\vec{x})_i = P(y=clase_i | w)
La derivada a diferencia de las anteriores capas de activación no es tan sencilla. Eso se debe a que la softmax no es una función element-wise, sino que en su calculo participan las demás entradas.
Cuando consideramos que i=j:
\frac{\partial s_i}{\partial x_i} = \frac{e^{x_i} \cdot \sum e^{x_j} - e^{x_i} \cdot e^{x_i}}{\left ( \sum e^x_j\right )^2}
\frac{\partial s_i}{\partial x_i} = \frac{e^{x_i} \cdot \sum_j e^{x_j} - e^{x_i} \cdot e^{x_i}}{\left ( \sum_j e^{x_j} \right )^2} = \frac{e^{x_i}}{\sum_j e^{x_j}} \cdot \frac{\sum_j e^{x_j} - e^{x_i}}{\sum_j e^{x_j}} = s_i \cdot (1- s_i)
Cuando consideramos que i!=j:
\frac{\partial s_i}{\partial x_j} = \frac{0 \cdot \sum e^{x_j} - e^{x_j} \cdot e^{x_i}}{\left ( \sum e^x_j \right )^2} = -s_i \cdot s_j
Por ese motivo cuando derivamos estamos haciendo uso de la jacobiana (que no es más que una matriz de todas las derivadas parciales, es decir indica la función a derivar y con respecto a que variable se deriva).
J_s(\vec{x}) = \frac{\partial s_1 ... s_n}{\partial x_0 ... x_n} Donde s es la función softmax.
\begin{bmatrix}
\frac{\partial s_0}{\partial x_0} & ... & \frac{\partial s_0}{\partial x_n} \
\vdots & \ddots & \vdots \
\frac{\partial s_n}{\partial x_0} & ... & \frac{\partial s_n}{\partial x_n}
\end{bmatrix}
\frac{\partial out}{\partial \vec{x}} = J_s(\vec{x}) \cdot \frac{\partial out}{\partial s}
Lo veremos mucho más claro si nos centramos en lo que le llega a x_1
Como veis x_1 son la suma de todas las contribuciones en las que participa, tanto su correspondiente salida s_1 como aquellas salidas en las que solamente se encuentra en el denominador s_2, s_3 y s_4.
x_i = \sum_j \frac{\partial out_j}{\partial s_j}\frac{\partial s_j}{\partial x_i}
Basicamente lo que hace la softmax es coger un valor [$-\infty$, $\infty$] (llamado logit) y convertirlo en una probabilidad.
Múltiples clases
¿Que sucede cuando no queremos seleccionar solo una clase? Sino que una entrada puede pertener a distintas clases a la vez. Por ejemplo que la entrada de la red sea una prenda de ropa y la salida los atributos (tiene cuello/no, es tejano/no, color azul/no, etc…).
Obviamente en este caso la softmax no nos será útil puesto que las salidas deben ser independientes entre sí.
En este caso simplemente deberemos hacer uso de la activación sigmoid (explicada en el articulo anterior). Básicamente nos normalizará una entrada cualquiera a un valor de entre 0 y 1 (también podemos usar la tanh).
¿Y que pasa con la loss?
Vale si, hasta ahora sabemos que activaciones usar. ¿Pero que función de perdida es adecuada para tratar es un problema de clasificación?
No debemos olvidar que estamos tratando internamente con probabilidades, así que podemos usar cualquier métrica que permita comparar probabilidades. Al final compararemos la probabilidad obtenida de la salida de la red con el label que tenemos asignado a esa salida.
Por excelencia se acostumbra a usar la cross-entropy (existen otras como KL-Divergence):
loss(true, pred) = - \frac{1}{N} \sum_i true_i \cdot ln(pred_i)
Su derivada es extremadamente simple \frac{\partial loss}{\partial \sigma} = -\sum_i \frac{true_i}{pred_i} \cdot \frac{\partial pred_i}{\partial \sigma}
Donde true es el vector de probabilidades de los labels y pred es el vector de probabilidades de las predicciones (capa sigmoid o softmax de la red).
Las razones son diversas:
- Combinada con la softmax existe una equivalencia que es muy robusta en términos de estabilidad numérica. En muchos frameworks encontrareis una loss como «cross_entropy_softmax». Ya que usar la cross-entropy junto softmax solo tendrá valor la salida en el que el label es 1.
- Fácil de computar.
- En el caso de usar una clasificación mediante sigmoide (binaria), se usa la versión simetrica para contemplar ambos casos: loss(true, pred) = - \frac{1}{N} \sum_i \left ( true_i \cdot ln(pred_i) +\sum_i (1 - true_i) \cdot ln(1 - pred_i) \right ).
Así que menos blablabla y vayamos al código.
¡Al código!
El one hot encode es bastante sencillo, basicamente convierte una valor entero en un vector de 1 y 0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from .layer import Layer import numpy as np class OneHotDecode(Layer): def __init__(self, node, params=None): if params is None: params = {} params['compute_backward'] = False params['number_of_inputs'] = 1 super(OneHotDecode, self).__init__(node, params=params) def computeSize(self): super(OneHotDecode, self).computeSize() return tuple([1]) def forward(self, inputs): super(OneHotDecode, self).forward(inputs) return np.argmax(inputs[0], axis=-1).reshape(-1, 1) |
La softmax tiene una parte en python y otra en cython. Cython es código mezcla entre python y c++ que se transforma en código c++ para ser compilado como una librería dinámica, se usa para realizar el calculo de forma más eficiente. La parte de python solo hace de «wrapper» y la parte cython hace el resto. Aparte para mejorar la estabilidad numérica de la softmax se hace un pequeño truco y es restar el valor máximo.
s_i(x)= \frac{e^{\left ( x_i - max(x) \right )}}{\sum_j e^{\left (x_j - max(x) \right )}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from .layer import Layer import numpy as np from .cython import softmax class Softmax(Layer): def __init__(self, node, params=None): if params is None: params = {} params['number_of_inputs'] = 1 super(Softmax, self).__init__(node, params=params) def computeSize(self): super(Softmax, self).computeSize() return tuple(self.in_size[0]) def forward(self, inputs): super(Softmax, self).forward(inputs) self.values.out = softmax.nb_forward(inputs[0]) return self.values.out def derivatives(self, doutput): dx = softmax.nb_derivatives(doutput, self.values.out) return dx |
Cython:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import cython cimport cython import numpy as np cimport numpy as np from libc.math cimport exp from numpy.math cimport INFINITY from types cimport FLOAT64 # forward @cython.wraparound(False) @cython.boundscheck(False) def nb_forward(np.ndarray[FLOAT64, ndim=2] inputv): cdef unsigned int out_size = inputv.shape[1], \ batch_size = inputv.shape[0] cdef np.ndarray[FLOAT64, ndim=2] out = np.empty(shape=[batch_size, out_size], dtype=np.float64) cdef unsigned int b, i cdef double maxv = - INFINITY, den for b in range(batch_size): maxv = - INFINITY for i in range(out_size): if maxv < inputv[b, i]: maxv = inputv[b, i] den = 0. for i in range(out_size): out[b, i] = exp(inputv[b, i] - maxv) den += out[b, i] for i in range(out_size): out[b, i] /= den return out # derivatives @cython.wraparound(False) @cython.boundscheck(False) def nb_derivatives(np.ndarray[FLOAT64, ndim=2] doutput, np.ndarray[FLOAT64, ndim=2] ovalue): cdef unsigned int out_size = ovalue.shape[1], \ batch_size = ovalue.shape[0] cdef np.ndarray[FLOAT64, ndim=2] dx = np.empty(shape=[batch_size, out_size], dtype=np.float64) cdef unsigned int b, i, j for b in range(batch_size): for i in range(out_size): dx[b, i] = 0. for j in range(out_size): if i == j: dx[b, i] += ovalue[b, i]*(1 - ovalue[b, i])*doutput[b, j] else: dx[b, i] += - ovalue[b, i]*ovalue[b, j]*doutput[b, j] return dxLo habitual es usar la simétrica para sigmoid y asimetrica para softmax. |
La cross-entropy es muy sencilla, solamente se añade una epsilon en la predicción para evitar un log(0):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from .loss import Loss import numpy as np class CrossEntropy(Loss): def __init__(self, node, params=None): if params is None: params = {} super(CrossEntropy, self).__init__(node, params=params) def forward(self, inputs): super(CrossEntropy, self).forward(inputs) pred, true = inputs self.values.pred = pred + 1e-100 self.values.true = true out = - self.values.true*np.log(self.values.pred) return np.mean(np.sum(out, axis=-1), axis=0) def derivatives(self, doutput=None): dx = - (self.values.true/self.values.pred) return dx |
La cross-entropy junto a la softmax, si os fijáis en la derivada lo único que hacemos es pasar la probabilidad del label (que es 1) y queda como $p_i – y_i$ (donde $y_i$ es 1).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from .loss import Loss import numpy as np class SoftmaxCrossEntropy(Loss): def __init__(self, node, params=None): if params is None: params = {} super(SoftmaxCrossEntropy, self).__init__(node, params=params) def computeSize(self): super(SoftmaxCrossEntropy, self).computeSize() return tuple([1]) def forward(self, inputs): super(SoftmaxCrossEntropy, self).forward(inputs) pred, true = inputs probs = np.exp(pred - np.max(pred, axis=-1, keepdims=True)) probs /= np.sum(probs, axis=-1, keepdims=True) self.values.probs = probs self.values.true = true.flatten() #np.argmax(true, axis=-1) return -np.sum(np.log(probs[np.arange(self.values.true.shape[0]), self.values.true] + 1e-100)) / self.values.true.shape[0] def derivatives(self, doutput): dx = self.values.probs.copy() dx[np.arange(self.values.true.shape[0]), self.values.true] -= 1 return dx |
En fin, esto es todo. Hasta ahora ya podemos decir que tenemos un framework de Deep Learning, muy sencillito pero lo tenemos.
Nos vemos en próximos posts 😉