Redes Neuronales Con Python
Redes Neuronales Con Python
Python
Joaquín Amat Rodrigo
Mayo, 2021
SPON
Introducción
Los ejemplos de este documento se corresponden con el primer caso de uso, por lo que
se utiliza la librería Scikit-learn para crear los modelos.
Redes neuronales
Estructura de la red
Representación de una red neuronal feed-forward (single-layer perceptron). Fuente: Computer Age
Statistical Inference 2016
La primera capa de la red neuronal (color verde) se conoce como capa de entrada o
input layer y recibe los datos en bruto, es decir, el valor de los predictores. La capa
intermedia (color azul), conocida como capa oculta o hidden layer, recibe los valores la
capa de entrada, ponderados por los pesos (flechas grises). La última capa, llamada
output layer, combina los valores que salen de la capa intermedia para generar la
predicción.
Para facilitar la comprensión de la estructura de las redes, es útil representar una red
equivalente a un modelo de regresión lineal.
y = w1 x 1 +. . . +wd x d + b
Representación de una red neuronal equivalente a un modelo lineal con 4 predictores. Fuente:
COMS W4995 Applied Machine Learning
Cada neurona de la capa de entrada representa el valor de uno de los predictores. Las
flechas representan los coeficientes de regresión, que en términos de redes se llaman
pesos, y la neurona de salida representa el valor predicho. Para que esta representación
equivalga a la ecuación de un modelo lineal, faltan dos cosas:
La capa intermedia de una red tiene un valor de bias, pero suele omitirse en las
representaciones gráficas. En cuanto a las operaciones matemáticas, es el elemento
clave que ocurre dentro de las neuronas y conviene verlo con detalle.
La neurona (unidad)
Si bien el valor que llega a la neurona, multiplicación de los pesos por las entradas,
siempre es una combinación lineal, gracias a la función de activación, se pueden
generar salidas muy diversas. Es en la función de activación donde reside el potencial
de los modelos de redes para aprender relaciones no lineales.
Representación de una neurona. Fuente: Deep Learning A Practitioner’s Approach by Josh
Patterson and Adam Gibson
La anterior ha sido una explicación intuitiva del funcionamiento de una neurona. Véase
ahora una definición más matemática.
El valor neto de entrada a una neurona es la suma de los valores que le llegan,
ponderados por el peso de las conexiones, más el bias.
n
entrada = ∑ x i wi + b
i=1
A este valor se le aplica una función de activación (g) que lo transforma en lo que se
conoce como valor de activación (a), que es lo que finalmente sale de la neurona.
a = g(entrada) = g(XW + b)
La función de activación ReLu aplica una transformación no lineal muy simple, activa la
neurona solo si el input está por encima de cero. Mientras el valor de entrada está por
debajo de cero, el valor de salida es cero, pero cuando es superior de cero, el valor de
salida aumenta de forma lineal con el de entrada.
ReLU(x) = max(x, 0)
Sigmoide
Un caso en el que la función de activación sigmoide sigue siendo la función utilizada por
defecto es en las neuronas de la capa de salida de los modelos de clasificación binaria,
ya que su salida puede interpretarse como probabilidades.
Sin las funciones de activación, las redes neuronales solo pueden aprender relaciones
lineales.
Función de coste (loss function)
La función de coste (l), también llamada función de pérdida, loss function o cost
function, es la encargada de cuantificar la distancia entre el valor real y el valor predicho
por la red, en otras palabras, mide cuánto se equivoca la red al realizar predicciones. En
la mayoría de casos, la función de coste devuelve valores positivos. Cuanto más
próximo a cero es el valor de coste, mejor son las predicciones de la red (menor error),
siendo cero cuando las predicciones se corresponden exactamente con el valor real.
La función de coste puede calcularse para una única observación o para un conjunto de
datos (normalmente promediando el valor de todas las observaciones). El segundo
caso, es el que se utiliza para dirigir el entrenamiento de los modelos.
El error cuadrático medio (mean squared error, MSE) es con diferencia la función de
coste más utilizada en problemas de regresión. Para una determinada observación i , el
error cuadrático se calcula como la diferencia al cuadrado entre el valor predicho y^ y el
valor real y .
2
(i) (i) (i)
l ^
(w, b) = (y − y )
Las funciones de coste suelen escribirse con la notación l(w, b) para hacer referencia a
que su valor depende de los pesos y bias del modelo, ya que son estos los que
determinan el valor de las predicciones y (i) .
2
, esto es
simplemente por conveniencia matemática para simplificar el cálculo de su derivada.
2
(i)
1 (i) (i)
l (w, b) = ^
(y − y )
2
Para cuantificar el error que comete el modelo todo un conjunto de datos, por ejemplo
los de entrenamiento, simplemente se promedia el error de todas las N observaciones.
n n
2
1 (i)
1 (i) (i)
L(w, b) = ∑l (w, b) = ^
∑ (y − y )
n n
i=1 i=1
Cuando un modelo se entrena utilizando el error cuadrático medio como función de
coste, está aprendiendo a predecir la media de la variable respuesta.
El error medio absoluto (mean absolute error, MAE) consiste en promediar el error
absoluto de las predicciones.
n
1 (i) (i)
L(w, b) = ^
∑ |y − y |
n
i=1
El error medio absoluto es más robusto frente a outliers que el error cuadrático medio.
Esto significa que el entrenamiento del modelo se ve menos influenciado por datos
anómalos que pueda haber en el conjunto de entrenamiento. Cuando un modelo se
entrena utilizando el error absoluto medio como función de coste, está aprendiendo a
predecir la mediana de la variable respuesta.
Para problemas de clasificación con más de dos clases, esta fórmula se generaliza a:
N −1 K−1
1
Llog (Y , P ) = − log Pr(Y |P ) = − ∑ ∑ yi,k log pi,k
N
i=0 k=0
En ambos casos, minimizar esta la función equivale a que la probabilidad predicha para
la clase correcta tienda a 1, y a 0 en las demás clases.
Dado que esta función se ha utilizado en campos diversos, se le conoce por nombres
distintos: Log loss, logistic loss o cross-entropy loss, pero todos hacen referencia a lo
mismo. Puede encontrarse una explicación más detallada de esta función de coste aquí
(https://scikit-learn.org/stable/modules/model_evaluation.html#log-loss).
Múltiples capas
El modelo de red neuronal con una única capa (single-layer perceptron), aunque supuso
un gran avance en el campo del machine learning, solo es capaz de aprender patrones
sencillos. Para superar esta limitación, los investigadores descubrieron que, combinando
múltiples capas ocultas, la red puede aprender relaciones mucho más complejas entre
los predictores y la variable respuesta. A esta estructura se le conoce como perceptrón
multicapa o multilayer perceptron (MLP), y puede considerarse como el primer modelo
de deep learning.
Para cada observación de entrenamiento (X, y ), calcular el error que comete la red
al hacer sus predicciones. Promediar los errores de todas las observaciones.
Backpropagation
En el caso de las redes, la derivada parcial del error respecto a un parámetro (peso o
bias) mide cuanta "responsabilidad" ha tenido ese parámetro en el error cometido.
Gracias a esto, se puede identificar qué pesos de la red hay que modificar para
mejorarla. El siguiente paso necesario, es determinar cuánto y cómo modificarlos
(optimización).
Descenso de gradiente
Dado que, calcular el error del modelo para todas las observaciones de entrenamiento,
en cada iteración, puede ser computacionalmente muy costoso, existe una alternativa al
método de descenso de gradiente llamada gradiente estocástico (stochastic gradient
descent, SGD). Este método consiste en dividir el conjunto de entrenamiento en lotes
(minibatch o batch) y actualizar los parámetros de la red con cada uno. De esta forma,
en lugar de esperar a evaluar todas las observaciones para actualizar los parámetros,
se pueden ir actualizando de forma progresiva. Una ronda completa de iteraciones
sobre todos los batch se llama época. El número de épocas con las que se entrena una
red equivale al número de veces que la red ve cada ejemplo de entrenamiento.
Preprocesado
Cuando los predictores son numéricos, la escala en la que se miden, así como la
magnitud de su varianza pueden influir en gran medida en el modelo. Si no se igualan
de alguna forma los predictores, aquellos que se midan en una escala mayor o que
tengan más varianza dominarán el modelo aunque no sean los que más relación tienen
con la variable respuesta. Existen principalmente 2 estrategias para evitarlo:
Centrado: consiste en restarle a cada valor la media del predictor al que pertenece.
Si los datos están almacenados en un dataframe, el centrado se consigue
restándole a cada valor la media de la columna en la que se encuentra. Como
resultado de esta transformación, todos los predictores pasan a tener una media de
cero, es decir, los valores se centran en torno al origen.
La gran "flexibilidad" que tienen las redes neuronales es un arma de doble filo. Por un
lado, son capaces de generar modelos que aprenden relaciones muy complejas, sin
embargo, sufren fácilmente el problema de sobreajuste (overfitting)
(https://es.wikipedia.org/wiki/Sobreajuste#:~:text=En%20aprendizaje%20autom%C3%A1ti
lo que los incapacita al tratar de prdecir nuevas observaciones. La forma de minimizar
este problema y conseguir modelos útiles, pasa por configurar de forma adecuada sus
hiperparámetros. Algunos de los más importantes son:
La capa de entrada y salida son sencillas de establecer. La capa de entrada tiene tantas
neuronas como predictores y la capa de salida tiene una neurona en problemas de
regresión y tantas como clases en problemas de clasificación. En la mayoría de
implementaciones, estos valores se establecen automáticamente en función del
conjunto de entrenamiento. El usuario suele especificar únicamente el número de capas
intermedias (ocultas) y el tamaño de las mismas.
Cuantas más neuronas y capas, mayor la complejidad de las relaciones que puede
aprender el modelo. Sin embargo, dado que en cada neurona está conectada por pesos
al resto de neuronas de las capas adyacentes, el número de parámetros a aprender
aumenta y con ello el tiempo de entrenamiento.
Learning rate
El learning rate o ratio de aprendizaje establece cómo de rápido pueden cambiar los
parámetros de un modelo a medida que se optimiza (aprende). Este hiperparámetro es
uno de los más complicados de establecer, ya que depende mucho de los datos e
interacciona con el resto de hiperparámetros. Si el learning rate es muy grande, el
proceso de optimización puede ir saltando de una región a otra sin que el modelo sea
capaz de aprender. Si por el contrario, el learning rate es muy pequeño, el proceso de
entrenamiento puede tardar demasiado y no llegar a completarse.Algunas de las
recomendaciones heurísticas basadas en prueba y error son:
Algoritmo de optimización
Regularización
Penalización L1 y L2
El objetivo de la penalización L1 y L2, esta última también conocida como weight decay,
es evitar que los pesos tomen valores excesivamente elevados. De esta forma se evita
que unas pocas neuronas dominen el comportamiento de la red y se fuerza a que las
características poco informativas (ruido) tengan pesos próximos o iguales a cero.
Dropout
Para crear modelos basado en redes neuronales con scikit-learn, se utilizan las clases
sklearn.neural_network.MLPRegressor (https://scikit-
learn.org/stable/modules/generated/sklearn.neural_network.MLPRegressor.html#sklearn.n
para regresión y sklearn.neural_network.MLPClassifier (https://scikit-
learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html) para
clasificación.
Son muchos los argumentos que controlan el comportamiento de este tipo de modelos.
Afortunadamente, los responsables de su implementación han establecido valores por
defecto que suelen funcionar adecuadamente en muchos escenarios. A continuación, se
muestran los más influyentes:
power_t : exponent
eutilizado para reducir el learning rate cuando
learning_rate='invscaling' . Por defecto se emplea el valor 0.5. Solo se utiliza
este argumento cuando solver='sgd' .
random_state : semilla utilizada para todos los pasos del entrenaiento que
requieren de valores aleatorios (inicialización depesos, splits, bias).
Ejemplo clasificación
Librerías
# Gráficos
# ====================================================================
==========
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
plt.style.use('fivethirtyeight')
# Modelado
# ====================================================================
==========
from sklearn.datasets import make_blobs
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import RandomizedSearchCV
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import KFold
import multiprocessing
# Configuración warnings
# ====================================================================
==========
import warnings
warnings.filterwarnings('ignore')
Datos
ax.set_title('Datos simulados')
ax.legend();
Arquitectura de la red
Se procede a crear 4 modelos en orden creciente de complejidad (número de neuronas
y capas), para comprobar cómo la arquitectura de la red afecta a su capacidad de
aprendizaje.
In [16]: # Modelos
# ====================================================================
==========
modelo_1 = MLPClassifier(
hidden_layer_sizes=(5),
learning_rate_init=0.01,
solver = 'lbfgs',
max_iter = 1000,
random_state = 123
)
modelo_2 = MLPClassifier(
hidden_layer_sizes=(10),
learning_rate_init=0.01,
solver = 'lbfgs',
max_iter = 1000,
random_state = 123
)
modelo_3 = MLPClassifier(
hidden_layer_sizes=(20, 20),
learning_rate_init=0.01,
solver = 'lbfgs',
max_iter = 5000,
random_state = 123
)
modelo_4 = MLPClassifier(
hidden_layer_sizes=(50, 50, 50),
learning_rate_init=0.01,
solver = 'lbfgs',
max_iter = 5000,
random_state = 123
)
modelo_1.fit(X=X, y=y)
modelo_2.fit(X=X, y=y)
modelo_3.fit(X=X, y=y)
modelo_4.fit(X=X, y=y)
predicciones = modelo.predict(X_grid)
for j in np.unique(predicciones):
axs[i].scatter(
x = X_grid[predicciones == j, 0],
y = X_grid[predicciones == j, 1],
c = plt.rcParams['axes.prop_cycle'].by_key()['color'][j],
#marker = 'o',
alpha = 0.3,
label= f"Grupo {j}"
)
for j in np.unique(y):
axs[i].scatter(
x = X[y == j, 0],
y = X[y == j, 1],
c = plt.rcParams['axes.prop_cycle'].by_key()['color'][j],
marker = 'o',
edgecolor = 'black'
)
Optimización de hiperparámetros
grid = GridSearchCV(
estimator = MLPClassifier(
learning_rate_init=0.01,
solver = 'lbfgs',
alpha = 0,
max_iter = 5000,
random_state = 123
),
param_grid = param_grid,
scoring = 'accuracy',
cv = 5,
refit = True,
return_train_score = True
)
_ = grid.fit(X, y)
In [19]: fig, ax = plt.subplots(figsize=(6, 3.84))
scores = pd.DataFrame(grid.cv_results_)
scores.plot(x='param_hidden_layer_sizes', y='mean_train_score', yerr
='std_train_score', ax=ax)
scores.plot(x='param_hidden_layer_sizes', y='mean_test_score', yerr='s
td_test_score', ax=ax)
ax.set_ylabel('accuracy')
ax.set_xlabel('número de neuronas')
ax.set_title('Error de validacion cruzada');
grid = GridSearchCV(
estimator = MLPClassifier(
hidden_layer_sizes=(10),
solver = 'adam',
alpha = 0,
max_iter = 5000,
random_state = 123
),
param_grid = param_grid,
scoring = 'accuracy',
cv = 5,
refit = True,
return_train_score = True
)
_ = grid.fit(X, y)
In [21]: fig, ax = plt.subplots(figsize=(6, 3.84))
scores = pd.DataFrame(grid.cv_results_)
scores.plot(x='param_learning_rate_init', y='mean_train_score', yerr
='std_train_score', ax=ax)
scores.plot(x='param_learning_rate_init', y='mean_test_score', yerr='s
td_test_score', ax=ax)
ax.set_xscale('log')
ax.set_xlabel('log(learning rate)')
ax.set_ylabel('accuracy')
ax.set_title('Error de validacion cruzada');
Si bien los dos ejemplos anteriores sirven para tener una idea intuitiva de cómo afecta
cada hiperparámetro, no es posible optimizarlos de forma individual, ya que el impacto
final que tiene cada uno depende de qué valor tomen los demás. La búsqueda de
hiperparámetros debe hacerse en conjunto.
grid.fit(X = X, y = y)
Out[22]:
param_learning_rate_init param_hidden_layer_sizes param_alpha mean_test_score std_te
Una vez entrenado el modelo, al tratarse de solo dos predictores, puede mostrarse
gráficamente las regiones de clasificación aprendidas.
for i in np.unique(predicciones):
ax.scatter(
x = X_grid[predicciones == i, 0],
y = X_grid[predicciones == i, 1],
c = plt.rcParams['axes.prop_cycle'].by_key()['color'][i],
#marker = 'o',
alpha = 0.3,
label= f"Grupo {i}"
)
for i in np.unique(y):
ax.scatter(
x = X[y == i, 0],
y = X[y == i, 1],
c = plt.rcParams['axes.prop_cycle'].by_key()['color'][i],
marker = 'o',
edgecolor = 'black'
)
ax.set_title('Regiones de clasificación')
ax.legend();