Deep Learning - Clases
Deep Learning - Clases
Deep Learning - Clases
Clasificación de Imágenes
Se predice cuál es la probabilidad de que en una determinada imagen haya un gato, por ejemplo.
Para Python, las imágenes son matrices de números (intensidades). En general, son valores que van
entre 0 y 255 o pueden ir entre 0 y 1. Si se trabaja con una imágen a color, se tienen 3 RGB, 3
“rebanadas de pan” una atrás de la otra. Es decir, 3 matrices del mismo tamaño donde cada una
representa un canal de color diferente.
Antes se extraían características/features de las imágenes manualmente por las rutinas de código y
una vez que se tenían las características relevantes para el problema que se quería resolver, se podía
decir, por medio de un clasificador, si lo de la imágen es un perro salchicha o no.
EJEMPLO: tenemos la imagen de una bicicleta, se generan otras representaciones por medio de
rutinas que sirven para hacer una extracción de características (ej. color promedio de la imágen,
desvió estándar en los colores de la imagen, dividir la imagen en cuadrantes y ver el color promedio
de cada cuadrante). Se seleccionan las características más relevantes para el problema, se meten
estás características en un clasificador y se hace la predicción.
El problema estaba en que las características eran extraídas de forma manual, por lo que era difícil
resolver un problema en computer vision.
El Deep Learning es una técnica para implementar aprendizaje maquinal. Los modelos basados en
deep learning son capaces de aprender representaciones de los datos de entrenamiento en múltiples
niveles de abstracción (capas), componiendo módulos simples que sucesivamente transforman
dichas representaciones en otras con mayor nivel de abstracción. Se combinan módulos que son muy
simples pero que combinados, uno atrás del otro en distintas capas (niveles de abstracción),
empiezan a especializarse y se logra aprender características que son útiles para resolver el problema
en cuestión. Las primeras capas se especializan en features de bajo nivel (ej. en imágenes pueden ser
los bordes, las esquinas, etc) y las sucesivas van a empezar a combinar esas capas para construir
conceptos más complejos y finalmente vamos a tener conceptos muy complejos que se basan en los
más simples. Este es un modelo entrenado “end to end” donde todo esto se entrena junto. Ya no se
tienen etapas desacopladas sino que se aprende junto. El componente fundamental son las neuronas
artificiales.
Gradiente Descendiente
Con el gradiente descendiente se grafica el error de pérdida. Es decir, el cómputo de la función de
costo en los datos de entrenamiento. Se computa la función de pérdida para los datos de
entrenamiento y para un determinado valor de w1 y w2 (si tenemos más features, serán también
más pesos), tenemos un error de pérdida.
Para movernos del punto A al punto B usamos la derivada (función matemática que nos dice en qué
dirección caminar para que la función cambie lo más que pueda). Cuando extendemos la derivada a
funciones de múltiples variables tenemos un gradiente, que es el vector de derivadas parciales. En
este caso, son derivadas de la función de pérdida respecto a cada uno de los parámetros que
representan a mi neurona (los ws). El vector gradiente apunta en la dirección en la que la función
crece rápido, pero como buscamos la dirección en la que la función decrece rápido, tomamos el
negativo del gradiente descendiente.
Cómo calcular el gradiente
● Derivación análitica: derivar a mano y escribir el código. Se escribe una expresión que dice la
forma que tiene la derivada a partir de la función. Es fácil de hacer para una neurona, pero
no para más de una.
● Derivación numérica: diferencias finitas. Se computa el valor de la función en dos puntos y se
hace la recta. Estos métodos tienen muchos problemas de estabilidad y son caros.
● Derivación simbólica: se realiza utilizando las reglas estudiadas en Análisis matemático pero
automatizadas (ej. Maple, Mathematica). Devuelven una expresión análitica que puede ser
muy fea.
● Derivación automática: se definen las derivadas para operaciones “primitivas” como por
ejemplo, una suma o un producto (matemáticas y de control). Se construye un grafo de
operaciones y se deriva siguiendo la regla de la cadena. Existen frameworks que
implementan estos algoritmos de diferenciación automática (TensorFlow, Pytorch).
¿Qué es una neurona? → La operación más básica que podemos representar en una neurona es el
producto interno entre la entrada y los pesos.
Funciones de Activación
● Función signo: pasamos z (lo que sale de la sumatoria) por la función signo. Si z<0, la función
vale -1 y si z>0, la función vale 1.
● Función Sigmoidea: manda cualquier cosa del rango de los reales al rango (0,1). Podría servir
para representar, por ejemplo, una probabilidad (ej. para clasificador binario). La regresión
logística, por ejemplo, es un perceptrón simple con función de activación sigmoidea.
1 (verdadero) 0 (falso) 1
0 1 1
1 1 1
El dataset tiene 4 elementos, es decir, 4 posibles valores de verdad que tiene el problema. La idea es
trazar una recta que actúa como frontera de decisión dejando de un lado las combinaciones que dan
1, del otro lado, las combinaciones que dan -1. Solo da negativa (falso) la que vale falso/falso.
Quedan las clases verdaderas y falsas separadas.
A la frontera de decisión le agregamos el término de bias (w0) para desplazar la recta del origen.
Algoritmo: se inicializan los pesos aleatoriamente y dado el dataset, actualizamos w (valor de los
pesos) como el valor que tiene w en ese momento menos el gradiente multiplicado por el tamaño
del paso. Esto lo que va a hacer es escalar el tamaño de ese gradiente para que no se den saltos muy
grandes.
EJEMPLO: cálculo del gradiente en caso lineal de perceptrón
Modelando el XOR
XOR es la operación del OR exclusivo. Es decir, es otra operación lógica donde para que la salida sea
verdadera sólo una de las dos tiene que ser verdadera (cuando son las dos verdaderas, es falso).
Cambia entonces el mapa de puntos cartesianos.
No podemos plantear una frontera de decisión que resuelva el problema del XOR con un perceptrón
simple lineal. Ni siquiera con un perceptrón simple que tenga función de activación no lineal, no se
puede. Se necesita entonces un perceptrón multicapa (multi layer perceptron MLP). Es decir, se
necesita que la salida de la neurona (que va a ser una recta como la del segundo gráfico de arriba)
ahora entre a otra neurona que va a combinar la salida de varias neuronas de la capa anterior. En un
perceptrón multicapa se meten muchas neuronas en paralelo para poder tomar las salidas de esas
neuronas combinadas en una predicción. Se empiezan a combinar rectas por medio de la
concatenación de neuronas. Cuando se dan ciertas características, al MLP se lo conoce como
aproximador universal ya que este perceptrón puede aproximar cualquier función.
Un perceptrón multicapa (MLP) es una estructura que combina varias neuronas en un sólo modelo.
Empezamos a hablar de arquitectura de redes neuronales. La arquitectura está dada por: cuántas
capas tiene el perceptrón, cuántas neuronas tiene cada capa, que función de activación se usa en
cada capa, si la capa tiene bias o no. En un paradigma de aprendizaje supervisado como en el que
estamos, vamos a entrenar al modelo usando etiquetas. Cuando se empieza a entrenar al modelo se
tienen que comparar cada una de las salidas del modelo con la etiqueta que queremos que
corresponda a ese dato. Todo se va a guiar a partir de la función de pérdida.
En el MLP ya no tenemos un sólo vector de peso (antes teníamos un vector de pesos w por cada
neurona) sino que se tiene una matriz donde hay un vector de pesos por cada neurona de la capa y
cada neurona tendrá la misma cantidad de pesos que de features de entrada. Siguiendo el ejemplo
del dibujo siguiente, en la primera capa vamos a tener 5 vectores de pesos que se acomodan en una
matriz donde cada vector tiene 4 pesos (porque hay 4 features de entrada).
En estas redes feedforward donde se alimentan sólo hacia adelante, la salida de la capa anterior es lo
que entra a la siguiente (cada flecha es una x). Entonces, cada neurona de la segunda capa oculta va
a tener 5 pesos porque están entrando 5 features a las neuronas de la siguiente capa. A la última
capa la llamamos capa de salida.
Todas las neuronas de una capa tienen la misma función de activación, pero 2 capas pueden tener
distinta función de activación.
Dato: tener muchos parámetros puede llevar a overfitear.
Interpretación del dibujo: tenemos 3 salidas. Podemos pensar en, por ejemplo, un problema de
clasificación donde entra un dato y lo podemos clasificar en 3 clases diferentes. Decimos que cada
neurona, por ejemplo, escupe la probabilidad de que mi entrada sea de clase 1, 2 o 3. También
podríamos estar regresionando 3 valores como edad, altura y peso de una persona, a partir de un
montón de parámetros de una persona.
Ejemplo: problema con 2 features de entrada
Por ejemplo, podemos estar estimando la lluvia (si va a llover o no) y la temperatura de mañana a
partir de la presión atmosférica y humedad de hoy.
La primera capa es una capa oculta que tiene 3 neuronas, cada neurona va a tener 2 pesos: uno que
multiplica a la primera entrada y otro que multiplica a la segunda. Entonces, la matriz va a tener una
columna por cada neurona (estos son los pesos). Cuando llega el dato vamos a hacer un producto
entre el vector rosa y la matriz Wh. Vamos a multiplicar una primera matriz de 1x2 y una segunda
matriz de 2x3 por lo que la matriz de salida será de 1x3, que representa las salidas de cada neurona.
Tamaño de una matriz de pesos de una capa: cantidad de features de entrada x cantidad de
neuronas de salida. Las matrices son el resultado de hacer XxW.
Batches de tamaño 4: 4 muestras/datos con 2 features de entrada cada una. El producto de la matriz
de entrada y la matriz de pesos es de 4x3 donde tenemos una columna por cada neurona y una fila
por cada observación. El tamaño de las matrices de pesos de mi perceptrón no depende de la
cantidad de elementos del batch, sino que depende de la cantidad de features de entrada y la
cantidad de neuronas. Es decir, podemos entrenar un perceptrón con batch de tamaño 40 y en test
meterle únicamente 1 dato.
Cómo calcular el gradiente: Derivación Automática / Backpropagation
Expresamos la operación matemática como si fuera un grafo de operaciones. Si sabemos cómo
procesar cada una de las operaciones independientemente, después podemos componer el
gradiente por medio de la regla de la cadena y obtener la estimación final del gradiente. El algoritmo
de backpropagation es un algoritmo para obtener gradientes, nos da el gradiente de una función
respecto de ciertos parámetros. El algoritmo de gradiente descendiente es el algoritmo de
optimización, que usa backpropagation cuando haya que calcular el gradiente.
Necesitamos poder derivar para poder hacer el algoritmo de gradiente descendiente, en algunos
casos como en la función máximo hay puntos donde la derivada no está definida entonces tomamos
un sub-gradiente. La función máximo no es diferenciable, es decir que no podemos derivarla en todo
su espectro pero se suele usar una generalización del gradiente para funciones no diferenciables que
se llama sub-gradiente.
La derivada de f respecto a X es 1 cuando X es mayor o igual a Y, y 0 en otro caso. Cuando nos
movemos en X, si X es mayor a Y la función se va a incrementar, pero si Y es mayor a X la función no
cambia.
Vamos a usar la idea de la regla de la cadena para hacer derivadas de forma fácil en redes
neuronales porque podemos pensar a cada operación como si fuera un grafo. La regla de la cadena
optimiza el proceso de cálculo del gradiente porque reutiliza operaciones ya calculadas (ej. la
derivada de f con respecto a q:: primero la usamos para la derivada de f con respecto a x, y después
para la derivada de f con respecto a y). Al plantear las operaciones como un grafo, una vez que
tenemos derivada cada operación vamos a poder ir reutilizandolas y tener un proceso mucho más
optimizado de cálculo del gradiente. Backpropagation es un flujo de gradientes encadenados en un
circuito de operaciones aritméticas.
Flujo de gradientes:
EJEMPLO 1
Para aplicar el algoritmo de backpropagation tenemos que saber en qué punto vamos a instanciar a
esa función derivada. Necesitamos saber en qué punto queremos calcular la derivada. Todo el
algoritmo sirve para calcular la derivada de la función en un punto especial, si queremos la derivada
en otro punto tenemos que volver a correr el algoritmo. Entonces definimos un punto, le damos
valores a los parámetros. En este caso: 2, -1, -3, -2 y -3 (colores en rojo). Estos son los valores en los
que vamos a instanciar la derivada. Hacemos entonces la pasada forward. 0,73 es el valor de haber
evaluado la función en la entrada que le dimos. Hacemos ahora la pasada backward (que le da
nombre a este algoritmo). Se computan los gradientes de cada una de las operaciones
independientemente. Empezamos de atrás para adelante y multiplicamos los gradientes locales (los
calculados en cada operación del grafo) con el gradiente que venimos retro-propagando. Vamos a ir
aplicando parcialmente la regla de la cadena (en verde).
1. Hacemos la derivada de la función en función a la variable
2. Evaluamos el resultado en lo que viene entrando (si el resultado es una constante, no
tenemos nada para evaluar)
3. Multiplicamos por el gradiente que venimos retropropagando
4. Siempre que tenemos una operación de suma copiamos el gradiente en las dos salidas
porque las dos derivadas van a ser lo mismo (esto es así porque la derivada de (x+y) con
respecto a x es igual a la derivada de (x+y) con respecto a y)
Finalmente obtenemos el valor del vector gradiente de la función respecto a cada una de las
variables. Es un vector gradiente con todos números, no tenemos expresiones matemáticas porque
ya está todo instanciado. El algoritmo de backpropagation nos va a dar el gradiente de la función
instanciada en un punto. Es un algoritmo de derivación automática que permite calcular el gradiente.
El algoritmo de backpropation sirve para calcular un gradiente de una forma eficiente.
Es fundamental que para poder pedirle gradiente a cualquier operación de cosas con PyTorch la
hayamos construido como operaciones entre tensores de PyTorch. Es decir, que no se corte el grafo
de PyTorch. Todas las operaciones definidas tienen que ser entre tensores de PyTorch. Si rompemos
el grafo porque usamos una función que no es PyTorch, se corta el gradiente y no hay
retropropagación y no vamos a poder calcular los gradientes.
Componentes de PyTorch
Está estructurado en clases. PyTorch nos ofrece un montón de componentes distintos que siempre se
necesitan cuando se implementa un algoritmo de Deep Learning. Las clases son:
● Datos: para levantar datos PyTorch nos da objetos de tipo datasets y tensores. Al dataset le
vamos a decir que levante los datos y una vez levantados los mete dentro de tensores.
● Data Loaders: una vez que metemos los datos dentro de tensores, creamos un data loader al
que le damos un dataset. Iteramos el data loader y nos devuelve de un batch a la vez.
● Redes: PyTorch también nos permite crear redes neuronales (no tenemos que codear de
cero el producto entre x y w). Le pedimos que nos de capas densas (densas porque cada
neurona está densamente conectada con todas las de la capa anterior, son las que
estudiamos hasta el momento). A PyTorch entonces le pedimos que directamente nos de
una capa lineal/densa, nosotros lo que le pasamos son los features de entrada, features de
salida y la función de activación.
● Funciones de entrenamientos: se pueden construir bucles de entrenamiento donde se hace
una pasada forward con los datos del dataset, se computa la función de costo, se hace la
pasada backward, se actualizan los pesos de la red y se itera.
● TorchScript: se usa para pre-compilar los modelos armados para que se ejecuten de forma
mucho más optimizada. Cuando se hace un modelo y se lo quiere llevar deployment, en
general, se compila al modelo (se le hacen optimizaciones) para que ande más rápido.
Tensores en PyTorch
En PyTorch podemos:
● Computar operaciones en forma paralela en GPU
En una imagen de 1.024x1.024, por ejemplo, cada neurona de la primera capa tendrá ~1 millón de
conexiones/pesos. Y si tenemos, por ejemplo, 1.000 neuronas vamos a tener mil millones de
parámetros únicamente en la primera capa, lo cual no es escalable.
Suponiendo que estamos viendo un carácter, cada una de las neuronas de la primera capa van a
mirar a una zona muy chica de la entrada. Es decir, el valor que la neurona larga va a estar afectado
solamente por una parte de la entrada. Es decir, estas neuronas tienen un campo receptivo acotado.
El campo receptivo es el área de la entrada que afecta el valor de salida de la neurona.
Una neurona de la última capa combina la información que proviene de las capas anteriores (en este
caso, de las 3 capas anteriores) y cada neurona anterior miró una parte específica del dato por lo que
el campo receptivo de cualquier neurona es siempre mayor al de una neurona anterior. Es decir, las
capas superiores tienen un campo receptivo cada vez más amplio y reaccionan a patrones cada vez
más complejos.
Los modelos de deep learning hacen el análisis en múltiples niveles de abstracción y además integran
como parte del modelo al clasificador. El clasificador es perceptrón multicapa. El resto son neuronas
convolucionales que extraen características. Todo esto forma un grafo de operaciones y se lo puede
derivar como si fuese un perceptrón chico y muy simple. Resumiendo: entra un dato, se le extraen
características, se lo pasa por clasificador, se hace una predicción, se computa la función de pérdida y
se retropropaga el gradiente de la función de pérdida respecto a los parámetros del modelo. Se
pueden entrenar juntos al clasificador y al extractor de características, por eso lo llamamos
entrenamiento “end to end”.
Redes Neuronales Convolucionales
Una red neuronal convolucional es cualquier red que usa en algún lugar una capa convolucional.
Convolución 1D
Una convolución es una operación matemática que se realiza entre funciones y representa la integral
de un producto de una función x con una versión trasladada y reflejada de otra w. A x la llamamos
señal de entrada, a w la llamamos filtro y al resultado lo llamamos señal convolucionada. Tenemos
dos funciones, y el resultado es otra función. En el continuo, desplazamos al filtro y vamos sacando
como valor de la salida en nuestra señal convolucionada el área contenida entre las 2 funciones (por
eso, llega al máximo cuando el filtro ocupa toda el área de la señal de entrada).
La señal no suele ser una función continua, en general es una función muestreada ya que tenemos
en un instante de tiempo un número. Ahora la señal x es un vector de números donde cada una de
las posiciones es un valor de la señal. W también es un vector. En la señal discreta vamos a sumar el
valor del producto entre el vector w y la parte de señal de entrada en la que estamos parados en el
momento (ej. la cuenta del primer cuadrado es: (1x1)+(4x2)+(-1x0)+(0x-1) = 9).
Convolución 2D
Si trabajamos con imágenes, las señales son bi-dimensionales, por lo que en lugar de trabajar con un
único índice, trabajamos con dos índices. i es la imágen, k es el filtro convolucional. Vamos a ir
moviéndonos a lo largo de la imagen con el filtro y haciendo el producto interno entre el filtro y lo
que queda abajo de la imágen.
El filtro convolucional también se llama Kernel. En una imágen, desplazamos el kernel por la imágen.
El kernel también es una imágen. Dependiendo de lo que hayamos dibujado en el kernel es lo que
vamos a obtener como señal convolucionada. Lo que sale de haber ejecutado una operación de
convolución, en el contexto de redes neuronales, se llama Feature Map. Valores muy blancos
representan valores altos, mientras que valores muy negros representan valores bajos. Al hacer la
operación de convolución en la imagen, los valores más altos de salida van a estar donde haya algo
en la imagen que se parezca mucho a lo dibujado en el kernel. Esto sucede porque estamos
calculando un producto escalar, y el producto escalar es máximo cuando los dos vectores están
alineados (son iguales). Es una forma de hacer template matching, es decir, buscar cosas en
imágenes. En un feature map, vamos a haber transformado a la imágen en algo que representa una
característica específica de la imagen (ej. un mapa de bordes). Cuando hacemos una convolución
podemos pensar que cada uno de los píxeles de salida es el resultado de lo que “escupe” una
neurona. Una neurona que fue parametrizada por los parámetros del kernel. Es decir, ahora los pesos
de nuestras neuronas van a ser los kernels. El filtro convolucionado es justamente lo que vamos a
aprender, se lo inicializa con valores aleatorios pero como son los parámetros de la neurona los
vamos a aprender. Si queremos aprender a extraer características, dejamos que los parámetros
inicializados con un valor aleatorio sean libres y pueden ser aprendidos a través del proceso de
entrenamiento (exactamente igual que lo visto hasta ahora).
Este proceso forma parte de la pasada forward de tu análisis de una imágen.
Este es el resultado de convolucionar la imágen del fondo con un kernel. En este caso tenemos un
filtro de resaltado de borde, es decir, se resaltan bordes que estén orientados como está orientado el
filtro (kernel). Lo que tiene valores más altos son los bordes.
VGG: propusieron hacer las redes más profundas. Para lograr esto sin incrementar la cantidad de
parámetros se usan kernels muy chicos (de 3x3). El hecho de que las redes sean más profundas hace
que los filtros de capa (kernels) no se entiendan mucho visualmente (son muy chicos).
GoogLeNet: al final se hace una operación que llamamos global average pooling donde por cada
feature map se saca el promedio. Es decir, se tiene un valor por cada feature map. El GAP es lo que se
mete en el MLP. Este modelo tiene solamente 4 millones de parámetros.
Entrenando una red neuronal para clasificación
Hasta ahora siempre entrenamos redes para regresión, pero si queremos entrenar una red para
clasificación nos faltan: cómo asegurarnos de que lo que sale de la red es una distribución de
probabilidad y una función de pérdida que podamos usar para este tipo de problemas. Para entrenar
redes y asegurarnos que su salida sea algo que se parezca a una función de probabilidad vamos a
usar una función de activación especial llamada softmax.
Función de pérdida
La función de pérdida que solemos usar se llama Entropía Cruzada que nos permite encontrar
diferencias entre dos distribuidores de probabilidad p y q. Es decir, tenemos una distribución “ground
truth” a la que nos queremos acercar y una distribución estimada que va a ser la salida del
perceptrón. Vamos a sumar por cada uno de los valores posibles que puede tomar esa variable
aleatoria y comparar el valor de la distribución Ground Truth (p) con el logaritmo de lo que el modelo
le atribuyó a esa etiqueta. La idea de entropía cruzada es como una especie de distancia entre
distribuciones de probabilidad. Vamos a tener un valor bajo si las probabilidades son similares, y un
valor alto si las probabilidades son muy distintas.
Entropía Cruzada vs Accuracy
Cada columna es un dato y las filas representan la probabilidad de que sea de una clase o de la otra.
La predicción del modelo arriba y la etiqueta, o ground truth, abajo.
Primer clasificador: en el primer caso, el modelo no le acertó a la etiqueta real (porque la clase más
alta de la predicción fue la 3 y la clase correcta era la clase 1). Tanto en el caso 2 como en el 3, si
acertó. Si computamos la accuracy, nos da ⅔, y si computamos la entropía cruzada nos da 4.14.
Segundo clasificador: mismo accuracy pero entropía cruzada muchísimo menor.
Si tuviésemos que definir cuál clasificador es mejor, diríamos el segundo porque le asigna más
probabilidad a la clase correcta. Entonces, si usamos la entropía cruzada como función de pérdida
para guiar el aprendizaje de la red, en un caso como el ejemplo, ésta va a llevar a que un modelo con
el segundo clasificador sea mejor. Mientras que si usaramos accuracy ambos modelos serían igual de
buenos ya que es una medida discreta.
● Clasificación Binaria: en el caso binario se tiene una única salida del perceptrón, la cual
representa la probabilidad de clase verdadera. La probabilidad de clase falsa sería 1 - este
valor. En este caso, la entropía se llama entropía cruzada binaria.
● Clasificación Multi-Clase: cuando tenemos múltiples etiquetas estamos en un caso de
entropía cruzada categórica. Se tienen varias categorías y la sumatoria se ejecuta sobre todas
las posibles categorías.
● Clasificación Multi-Etiqueta: se pueden tener varias etiquetas a la vez (ej. en una radiografía
de tórax, una persona puede tener más de una patología a la vez). En este caso, podríamos
usar a la sigmoide como función de activación porque cada etiqueta es independiente de la
otra.
Funciones de activación
Cuando una capa, ya sea convolucional o totalmente conectada, procesa un dato; salen muchos
valores, uno por cada neurona, y a eso se le aplica una función de activación. Básicamente cada
neurona tiene una función de activación.
En un caso de una capa fully connected, se aplica la función sigma que puede ser cualquier cosa. En
el caso de una capa convolucional es lo mismo, a diferencia que el producto interno se hace
únicamente en el cuadradito del kernel.
La función de activación sigmoide, tiene ciertos problemas. Si la usamos en un perceptrón multicapa
muy profundo, el gradiente tiende a 0 cuando X tiende a ∞ o -∞. Vamos a tener señal nula de
gradiente, por lo que los pesos de la red no cambian. Es decir, la red no aprende. Cuando
computamos los gradientes usamos la regla de la cadena y retropropagamos gradientes del final
hasta el principio de la red neuronal. Si tenemos funciones del tipo sigmoide y empiezan a pasar por
esta sigmoide valores muy positivos o muy negativos, los valores que recuperamos se hacen cada vez
más chicos. Se producen entonces gradientes que son muy chicos y el producto entre gradientes es
cada vez más chico también. La multiplicación de pequeños gradientes por la regla de la cadena hace
que el gradiente se desvanezca. Se conoce como el problema del gradiente que se desvanece.
Se propuso entonces usar la función de activación ReLu (rectified linear units), que es el máximo
entre 0 y lo que está entrando a la función (x). Es decir, todo lo que es negativo se transforma en 0 y
todo lo positivo se deja como está. Esta es una función no lineal que tiene puntos donde no es
derivable. En todos los puntos positivos la función si es derivable y tiene gradiente, por lo que, si lo
que fluye por dentro de la red es positivo, vamos a tener señal de gradiente. Hay menos probabilidad
de que el gradiente se desvanezca que en el caso de la función de activación sigmoidea. De todas
formas, el gradiente es 0 para valores negativos, y puede desencadenar un proceso de Dying Relu. Es
por esto que se propuso la Leaky ReLu que evita el problema de la Dying Relu ya que también genera
gradientes no nulos para valores de x negativos. Es decir, se usa una función con pequeña pendiente
para los números negativos.
Las funciones de activación tienen que ser siempre no lineales porque sino no rige el teorema de
aproximación universal. No se tiene poder de modelado.
Sobreajuste y regularización
Las redes neuronales tienen muchos parámetros por lo que son super propensas a overfittear. De
hecho, solemos overfittear cuando atacamos un problema nuevo. Si no logramos que la red
overfitee, el modelo no está aprendiendo. No es una mala práctica empezar entrenando a la red y
ver que overfitea.
El error de predicción es el valor de la función de pérdida, las iteraciones van a estar divididas en
épocas. Se computa una función de pérdida tanto para los datos de entrenamiento como para los
datos de validación (no aprendemos con respecto a estos datos; los miramos durante el
entrenamiento pero no se usan para entrenar). Llega un momento que la función de pérdida en los
datos de entrenamiento sigue bajando y la función de pérdida en los datos de validación empieza a
subir. Este es el momento en el que decimos que el modelo empieza a overfittear ya que se está
especializando en los datos de entrenamiento y empieza a ser malo en los datos que no vio.
Lo primero que podemos hacer es early stopping, es decir, vamos monitoreando la función de
pérdida en validación y en el momento en el que vemos que durante varias iteraciones seguidas la
función de pérdida en validación empieza a subir, frenamos el entrenamiento.
Otra técnica que podemos usar es la regularización por norma L2 (weight decay). Cuando
entrenamos una red neuronal exploramos un espacio de parámetros; el espacio de posibles ws.
Como en principio los ws pueden tomar cualquier valor, para un único problema hay infinitas
posibles redes neuronales. Esto hace que el modelo tenga una complejidad alta. Reducir la cantidad
de parámetros de un modelo, en general, es una forma de regularizar. Le damos menor libertad al
modelo. Pero esta es una forma muy agresiva de reducir la complejidad del modelo. La regularización
por norma L2 podemos verla como una forma “suave” de bajar la complejidad del modelo ya que se
le agrega un término de regularización a la función de pérdida que sea la norma euclídea cuadrada
de los pesos. Es decir, agarramos todos los ws que tenga la red, los metemos dentro de un vector
largo, tomamos la norma y esto es lo que sumamos a la función de pérdida. De esta forma, los ws
van a tender al origen. Le estamos diciendo al modelo que preferimos los valores de w que estén
más cerca del origen. Acotamos el espacio de parámetros que el modelo puede tomar a aquellos
que están cerca del origen. Esto lo hacemos porque si el valor de w es muy grande el modelo le está
dando mucha importancia a esa feature. Por medio de esta regularización estamos evitando que un
feature se lleve toda la responsabilidad; distribuímos la responsabilidad. No lo hacemos de manera
explícita y dura, únicamente le decimos que tienen que estar cerca del origen (sin decirle que tan
cerca). Es una forma soft de acotar el espacio de parámetros. Se disminuye indirectamente la
complejidad del modelo.
Existen muchos w que pueden minimizar una función de pérdida para un dataset de entrenamiento
dado. Podemos reducir la complejidad del modelo acotando la cantidad de posibles modelos a
construir. Una forma de hacerlo es evitando que existan wi muy grandes, es decir, agregando una
restricción sobre el valor que pueden tomar los pesos wi.
Influencia del parámetro λ: λ nos dice cuánta atención le préstamos al término de regularización. Un
λ muy grande, underfitea. El modelo entonces no aprende ya que se le deja de prestar atención al
término de dato. Y si es muy chico, hay overfitting.
La aumentación de datos funciona muy bien sobre todo en imágenes. En tiempo de entrenamiento,
se aumenta artificialmente el dataset utilizando transformaciones en los datos y conservando las
etiquetas. Ej. rotar, agrandar, mover, cambiar el color, agregar ruido gaussiano a las imágenes (el gato
sigue siendo gato pero para la red es un dato nuevo). Estas redes necesitan muchos datos para
generalizar medianamente bien, por lo que usar aumento de datos en tiempo de entrenamiento es
una de las técnicas que más se usan en la práctica. Siempre es importante usar una aumentación de
datos si estamos trabajando con imágenes. En tiempo de prueba, es posible generar N versiones de
la imagen de test, estimar las predicciones y promediarlas.
No se usa únicamente con imágenes, también se pueden usar sobre series, por ejemplo, donde
podemos aplicar transformaciones sobre la serie que no destruyan el dato por completo.
Otra técnica de regularización que se usa mucho es dropout donde se propone apagar
aleatoriamente a algunas neuronas durante el entrenamiento para que el modelo no asigne
demasiada responsabilidad a una neurona o a alguna feature en particular. Cuando decimos apagar
nos referimos a ignorar la salida de las neuronas. Aleatoriamente, con una probabilidad (1-p), las
neuronas son ignoradas en la pasada forward durante entrenamiento. Apagamos neuronas con una
probabilidad determinada en cada iteración del algoritmo de gradiente descendiente. En una
iteración tiene una estructura, en la siguiente iteración una nueva estructura y así. Esto permite que
el modelo no termine confiando demasiado en ninguna neurona en particular. Es una forma indirecta
de entrenar muchos modelos a la vez porque estamos cambiando la arquitectura; y entrenar muchos
modelos y hacerlos predecir todos juntos en general tiene mejor rendimiento que no hacerlo. El
dropout se hace por capas.
Sólo apagamos neuronas en tiempo de entrenamiento, en tiempo de test (cuando ya vamos a hacer
predicciones) todas las neuronas son utilizadas y los pesos de salida de la neurona son multiplicados
por p. No se apagan neuronas aleatoriamente.
Otra cosa que es importante hacer es normalizar las entradas del modelo. Sirve, por lo general,
cuando tenemos datos tabulares. La media y los desvíos son siempre calculados usando sólo los
datos de training (si metemos los de test hacemos leaking de datos), pero todos los datos (test y
validación también) son normalizados.
En el contexto de redes neuronales, está bueno normalizar los datos para tener una “linda”
superficie de error donde es fácil llegar al mínimo. Si los datos no están normalizados, se mueven
todos en diferentes escalas por lo que se tiene una superficie de error donde en algunas direcciones
tenemos que dar pasos muy grandes y en otras pasos más chicos. Hacer esto es difícil cuando
estamos entrenando. Que sea difícil llegar al mínimo implica que el rendimiento del modelo va a ser
peor.
En el caso de las imágenes, el rango de las features (píxeles) es el mismo (0-255), por lo tanto, en
general no es necesario dividir por el desvío. Lo usual es considerar el valor de la media y desvío
calculado sobre todos los pixeles de todas las imágenes de entrenamiento, en lugar de considerarlo
por feature (pixel). La estadística se calcula en cada imagen, distinto del caso con datos tabulares
donde calculamos la estadística con todo el dataset completo. De alguna manera, hacemos
normalización por imágenes. En imágenes multi-canal, cada canal suele ser normalizado
independientemente (una media y un desvío por cada canal).
Algo que se suele hacer es anilear/schedulear el learning rate, por ejemplo arrancando con un
learning rate muy alto para explorar la superficie de error a los saltos y a medida que vamos
avanzando en las iteraciones, empezamos a reducir el learning rate a lo largo del entrenamiento.
Empezamos con un delta grande y lo vamos disminuyendo a medida que avanzamos en el proceso de
entrenamiento en las iteraciones de gradiente descendiente.
● Step decay: reducir el learning rate (ej. dividir por 2) cada cantidad fija de épocas, o cuando
el error en validación no mejora.
● Exponential Decay: decaer el paso con una exp negativa.
También se suele tener un learning rate adaptativo por cada parámetro del vector W. Los métodos
anteriores aplican el mismo LR global a todos los parámetros W. Los métodos adaptativos escalan el
LR por cada parámetro.
- RMSProp
- Adadelta
- Adagrad
- Adam (Momentum + RMSProp)
● Crecimiento de regiones → se pone una semilla en el tumor, la cual empieza a crecer hasta
que se choca con los bordes. Donde se chocan los bordes no se crece más.
● Modelos deformables explícitos → contornos activos que se empiezan a mover hasta
chocarse con bordes.
Una primera aproximación al problema, la más ineficiente y básica de todas, es la sliding window en
la que se corta a la imagen en muchos parches, se entrenan redes de clasificación y se va pasando a
cada parche por la red. Se mira al problema como si fuese un problema de clasificación. Podemos
pensar que la segmentación es un problema de clasificación a nivel pixel. Se clasifica a cada píxel
independientemente. Se toman parches de la imágen y la etiqueta en la que se va a clasificar al
parche es la etiqueta que le corresponde al píxel central. Siguiendo con el ejemplo de la imagen
siguiente el primer y segundo pixel son de clase vaca y el tercer pixel es de clase pasto. Es muy
ineficiente porque no hace uso de las features compartidas entre parches.
Se creó en 2015 un nuevo tipo de arquitectura que sirve para resolver este problema de forma más
eficiente. Fully Convolutional Networks son redes totalmente convolucionales, es decir, son redes
sin capas densas (totalmente conectadas). En estas redes solo hay convoluciones y pooling. Se
procesa a la imagen tal cual vimos hasta ahora y cuando llegamos a algún nivel de baja resolución
sacamos tantos features maps como clases posibles tengamos en el problema de segmentación (ej. si
tenemos 21 clases posibles, sacamos 21 feature maps del mismo tamaño que la imagen de entrada)
y decimos que cada feature map corresponde a la probabilidad de que un píxel sea de la clase que
nos interesa. Armamos como si fuese un mapa de probabilidades por cada píxel (cada píxel ahora
tiene vectores de probabilidad) y la etiqueta que le asignamos es la de la clase con mayor
probabilidad. Es decir, por medio de convoluciones, terminamos transformando a la imagen en una
matriz de probabilidades donde cada uno de los píxeles de la matriz de probabilidades es en realidad
la probabilidad de que ese píxel tenga esa clase en particular. Las probabilidades para un único píxel
siempre suman 1. El último feature map tiene tantos canales como posibles clases. Computamos la
función de pérdida para cada pixel y promediamos; se mira cada pixel como si fuera un problema de
clasificación independiente, se calcula la entropía cruzada y después se calcula el promedio de todo
eso.
UNet es una arquitectura encoder-decoder para segmentación de imágenes. Se toma la imagen de
entrada y se hace convolución-convolución-pooling (en cada convolución no se altera el tamaño pero
en cada pooling el tamaño se baja a la mitad) hasta llegar a una especie de “cuello de botella” donde
desde ahí se empieza a subirle devuelta la resolución a la imagen con convolución-convolución-up
convolution hasta llegar a la resolución original. Se hace un camino en U donde se disminuye la
resolución y después se la empieza a aumentar con operaciones inversas a lo que se hizo
anteriormente. Primero se codifica (encoder) y después se decodifica (decoder).
Las flechas grises se llaman skip connections y sirven para saltar pedazos de la red. Sigue siendo
feed-forward pero nos salteamos conexiones. Esto sirve para no perdernos detalles y además para
que el gradiente salte capas; es decir, las skip connections ayudan a que las primeras capas reciban
gradiente con señal y no desvanecido.
Al pasar la imagen por la UNet, obtenemos mapas de probabilidades. En el ejemplo siguiente, donde
se tiene que clasificar a la imagen en 3 clases se obtiene un mapa de probabilidades con 3 “rodajas
de pan lactal” donde un valor blanco implica alta probabilidad y un valor negro, baja probabilidad.
Cada pixel suma 1. Al mapa de probabilidades se la hace el ArgMax (nos quedamos con la etiqueta
que corresponde a la clase de mayor probabilidad) y con eso reconstruimos la segmentación. Se
calcula la función de pérdida por pixel (ej. Entropía Cruzada) de los mapas de probabilidad de salida
comparándolos con la versión One-hot del ground-truth, y luego se promedia por todos los píxeles
de la imagen.
A diferencia de las arquitecturas que ya vimos (AlexNet, LeNet, etc), a las redes totalmente
convolucionales podemos entrenarlas con una imagen de un tamaño y poder segmentar imágenes
de otro tamaño porque todo se adapta al tamaño de la entrada. Entonces algo que solemos hacer
mucho es el entrenamiento por parches donde se parchea la imagen (especialmente cuando son
imágenes muy grandes): se cortan pedazos, se entrena con los pedazos y cuando se hace la
predicción se mete el volumen completo. Los mini-batches se componen de parches, no de imágenes
completas. En test time, si la red es totalmente convolucional, basta con insertar la nueva imagen y la
predicción será del tamaño acorde. Si la red no es del tamaño acorde, se puede hacer el tiling fuera
de la red, y luego reconstruir la segmentación.
Si la red no cuenta con ninguna capa totalmente conectada (densa), entonces se dice totalmente
convolucional. Las capas densas pueden ser implementadas como capas convolucionales. La gran
ventaja de una red totalmente convolucional es que puede procesar imágenes de cualquier
tamaño, y puede ser entrenada y evaluada en imágenes de diferentes tamaños.
En el caso de las predicciones en series de tiempo podemos trabajar con un perceptrón y mirar
nuestros datos como si fueran simplemente tiras de una tabla cada uno. Podemos tener, por
ejemplo, 4 features (ej. datos de los 4 días anteriores) y tratamos de predecir el 5to feature (día 5).
Se parte en pedazos la serie de tiempo y se arma una tabla donde el ground truth es la etiqueta de
mañana y se usan todos los días anteriores como features de entrada.
Lo interesante de usar convoluciones para predecir series de tiempo es que no es necesario que
recortemos el dato a la hora de testear, porque en las redes totalmente convolucionales se adapta el
tamaño de la salida al tamaño de la entrada. Podríamos entrenar a una red totalmente convolucional
para que dado 4 datos saque 1 (predicción del día de mañana) y poder meter el dataset entero en
tiempos de test. Al ser totalmente convolucional, la red va a sacar un valor por cada una de las
entradas que procesó. De todas maneras, las redes convolucionales no son muy usadas para
procesar series de tiempo. Si estamos implementando una red convolucional para cruzar series de
tiempo y hacer predicciones futuras, las convoluciones tienen que ser causales (los datos que van a
entrar a la red tienen que ser previos a lo que estamos prediciendo).
Si tenemos que hacer predicciones a partir de una serie de tiempo, podemos meter como entrada el
dato recién predecido, descartar el primer día usado hasta el momento, reemplazarlo por el valor
predecido y volver a predecir un nuevo día. De todas formas, haciendo esto se reduce mucho la
accuracy a lo largo de cada uno de los pasos que damos en el tiempo. Las predicciones van a ser
menos seguras porque vamos a estar metiendo como entradas datos que nosotros mismos
predecimos. Ahora, en un modelo que genera música (le damos un modelo de canción y empieza a
generar lo que sigue) este no sería un problema.
Las redes neuronales recurrentes si están pensadas específicamente para procesar series de tiempo
(datos secuenciales). Estas redes tienen bucles, es decir, van a ir consumiendo cada instante de
tiempo y actualizando la memoria. Podemos entonces procesar datos de cualquier longitud porque
vamos a estar consumiendo datos, actualizando la memoria y en algún momento
haciendo la predicción. Así como las redes totalmente convolucionales pueden
escalar a cualquier tamaño de imagen, las redes recurrentes pueden en principio
procesar cualquier longitud de secuencia. La clave de las redes recurrentes son los
pesos compartidos (weight sharing) a través del tiempo. Así como una red
convolucional tenía pesos que se compartían a lo largo del espacio, estas redes
tienen esa memoria que son como si fueran pesos que compartimos para procesar a
lo largo del tiempo. Usamos siempre los mismos pesos para procesar a lo largo del
tiempo, pero vamos actualizando la memoria.
La propiedad distinta que tienen estas redes con respecto a las que venimos viendo es que los pesos
se comparten a lo largo del tiempo. Entra el estado h en su estado inicial, entra el primer dato y
aplicamos la función f. La función f devuelve un estado actualizado (vector de valores actualizado).
Este vector entra de nuevo a la función con el dato 2, pero los pesos que aplicamos de la función son
los mismos que aplicamos antes. Las matrices W son las mismas en cada instante de tiempo mientras
estamos en un paso de gradiente descendiente.
Una vez que tenemos producimos las salidas, las usamos para computar la función de pérdida.
Computamos la función de pérdida para cada uno de los términos y la retropropagamos de la misma
forma vista hasta ahora. Esto lo llamamos retropropagación a través del tiempo porque si bien el
algoritmo es el mismo, el grafo de cómputo se construye a lo largo del tiempo. Cuando tomemos la
derivada, vamos a tomar la derivada de L (función de pérdida) respecto a W.
Problemas abiertos en DL y ML
¿Cómo generalizar a múltiples dominios de datos?
Muchas veces cuando hay un cambio de dominio entre los datos que usamos para entrenar y los
datos que usamos en la vida real los modelos no funcionan tan bien. En estos casos existen ciertas
técnicas que se pueden aplicar. Tiene que ver con la robustez de los modelos que entrenamos y es
clave entre un modelo exitoso en producción y uno no exitoso.
Cuando entrenamos un modelo para resolver una tarea (ej. clasificación de imágenes en distintas
categorías) puede pasar que entrenemos imágenes con un dominio (ej. imágenes sacadas de
Amazon) y después testemos en imágenes que provienen de un dominio distinto (ej. tienen fondo de
color). Un cambio de dominio es un cambio en la distribución de los datos. Cuando estamos en un
problema en el que nos enfrentamos a cambios de dominio, la tarea que queremos resolver es la
misma (ej. queremos predecir las mismas clases), lo que cambia es la distribución de los datos.
Entonces, existen técnicas de adaptación de dominio que permiten adaptar los modelos de un
dominio al otro.
● Problemas multi-sitio: un modelo funciona bien en un hospital pero no en otro porque se usa
un aparato diferente para tomar imágenes médicas (cambia un poco la distribución de los
pixeles).
Atributos protegidos: son variables contra las cuales queremos proteger el sesgo algorítmico como
género, etnia, país de procedencia, edad, pesos, color de piel, etc. No queremos que el modelo esté
sesgado con respecto a estos atributos.
Ej. en un algoritmo que recomienda si deberíamos o no otorgarle un crédito a una persona, el género
podría ser una variable protegida. No queríamos darle un crédito a alguien en base al género.