Oct. 25, 2018
Redes Neuronales desde cero con Python
Redes Neuronales
Para comprender este tutorial es necesario leer los tutoriales sobre arboles de decisión y sobre regresión logistica, ya que hay conceptos importantes, sobre todo en el tutorial de regresión logistica.
Como recordamos el modelo o algoritmo de regresión logistica sirve para clasificar datos, si lo ilustramos queda de la siguiente manera:
El algoritmo usa una linea para dividir los datos en dos clases, con la formula W * X + b implementamos la linea que divide los datos y con la optimización (Gradient descent, Backpropagation) del algoritmo podemos buscar la línea que mejor clasifique y se ajuste a los datos.
El modelo de regresión logistica es considerado un modelo de clasificación lineal, esto quiere decir que divide los datos con una línea recta y funciona con datos que sean divisibles linealmente.
Cuando tenemos datos como los siguientes:
como podemos notar este tipo de datos no son divisibles linealmente y necesitamos un modelo diferente que pueda clasificar estos datos, aquí es donde las redes neuronales entran en juego.
Podemos ver graficamente el algoritmo de regresión logistica de la siguiente manera:
Donde X son los datos, W son los pesos con los cuales los datos se multiplican, b es el parametro bias para mover la linea de su punto de origen, f es la función de probabilidad (en regresión logistica suele usarse la función sigmoid) y por ultimo Z es el resultado de la función de probabilidad que posteriormente se usa para la función de costo. En las redes neuronales todos estos conceptos se reutilizan, la función de probabilidad cambia su nombre a función de activación y suelen usarse funciónes diferentes aparte de sigmoid e incluso podemos representar una red neuronal de una manera parecida a la cual representamos el algoritmo de regresión logistica
Podemos notar que en una red neuronal usamos varios modelos de regresión logistica (Los podemos llamar nodos), en este ejemplo tenemos dos modelos los cuales mandan el resultado que obtuvieron a un tercer modelo, las entradas del tercer modelo son estos resultados, en otras palabras las X1, X2 del tercer modelo son las Z1, Z2 de los dos modelos anteriores. Sabemos que un modelo de regresión logistica puede dividir los datos con una línea recta como en el primer ejemplo ahora si tenemos dos modelos como los siguientes:
Con una red neuronal podemos combinar ambos modelos para que formen uno solo que quede de la siguiente manera:
Esta es una de las ventajas de las redes neuronales, son capaces de crear modelos complejos que se ajustan perfectamente a todo tipo de datos, claro que esto puede provocar overfitting, más adelante veremos como evitar que pase esto.
La representación que acabamos de ver es para entender como el modelo de regresión logistica tiene efecto en una red neuronal, pero si queremos representar una red neuronal tendríamos que dibujarla de la siguiente manera:
Como podemos ver es una representación un poco diferente, lo primero que notamos es que las entradas X1, X2, X3 ya no se repiten por cada nodo, ahora solo estan una vez y se reparten a cada nodo, las lineas dibujadas con negro son las relaciones que tienen los datos con los nodos y los nodos con las salidas, cada una de estas líneas tiene un peso W (no aparecen representados porque hay poco espacio para dibujarlos). Los nodos Z son las representaciones de cada modelo de regresión logistica como vimos en la primera representación, esto quiere decir que cada uno de estos nodos calculara la formula con diferentes pesos W para posteriormente combinar ambos modelos y conseguir un modelo más eficaz.
Las redes neuronales tienen diferentes capas (layers), dibujé cada capa de un color diferente para explicarlas:
-
Capa verde: Esta capa se conoce como capa de entrada (Input layer), es la capa donde estan los datos X cada fila de datos se representa con un nodo, cada uno de estos nodos se relaciona con los nodos de la capa escondida (hidden layer).
-
Capa amarilla: Esta capa se conoce como capa escondida (hidden layer) se le nombro de esta manera porque suele ser dificil comprender lo que pasa dentro de este tipo de capas. Pueden existir varias capas de este tipo entre más capas más complejo es el modelo que la red neuronal crea, cada capa puede tener varios nodos e igualmente entre más nodos más complejo es el modelo final, la elección de cuantas capas y cuantos nodos usar depende de la complejidad del problema que se quiera resolver.
-
Capa roja: Esta capa se conoce como capa de salida (output layer) y es la capa que elige a que clase pertenecen los datos en el caso de un problema de clasificación. Si el problema es de clasificación binaria suele usarse la función sigmoid para regresar un valor de 0 o 1. El numero de nodos de esta capa esta determinado por el numero de clases. Es posible usar un solo nodo cuando tenemos dos clases.
Como mencione podemos tener una red neuronal con más capas y más nodos como en el siguiente ejemplo
Esta red neuronal tiene cuatro nodos y al tener más nodos también se tienen más pesos W y la red es más compleja.
Código
Una vez se entienden los conceptos basicos de redes neuronales y del algoritmo de regresión logistica podemos programar una red neuronal desde cero, cabe aclarar que hay diferencias importantes entre ambos algoritmos y conforme programemos la red neuronal los explicaré, también dejare un link a la libreta donde esta todo el código con ejemplos y con resultados de la ejecución, recomiendo seguir la libreta y el tutorial al mismo tiempo.
Importar librerías
import matplotlib.pyplot as plt
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
Estas son las librerías con las que estaremos trabajando.
Crear el set de datos
X, y = datasets.make_moons(300, noise=0.20)
plt.scatter(X[:,0], X[:,1], s=40, c=y, cmap=plt.cm.Spectral)
Creamos los datos con los cuales la red neuronal aprendera, esto genera un set de datos como el de la siguiente imagen:
Funciones de ayuda
Estas funciones seran parte final del modelo pero las escribiremos por separado para luego unir todo.
def sigmoid(Z):
return 1 / ( 1 + np.exp(-Z))
La función sigmoid (también conocida como la función de probabilidad en el modelo de regresión logistica). En las redes neuronales funciona de la misma manera, sirve para otorgar un puntaje dependiendo de que tan bien el modelo predijo cada clase.
Función de activación ReLU
def relu(Z):
return np.maximum(0, Z)
La función relu devuelve 0 si el valor (Z) que le pasamos es igual o menor a cero y devuelve el valor (Z) si este es mayor a cero.
La función relu es una función de activación, las funciones de activación tienen un comportamiento similar a la función de probabilidad. En una red neuronal cada nodo se encargara de aprender alguna característica de los datos, por ejemplo si quisieramos clasificar imagenes de los dígitos (0 al 9), un nodo o varios pueden encargarse de reconocer si un dígito esta formado por un circulo como el 8, el 9 o el 6. Si tenemos una red neuronal entrenada para reconocer dígitos esta tendra pesos W en algunos nodos que verifiquen cuando un dígito esta formado por un circulo e igualemente puede tener nodos que verifiquen cuando un dígito esta formado por lineas rectas, si a esta red neuronal le pasamos una imagen del dígito 8 el resultado de la formula WX + b en los nodos que se encargan de verificar si el dígito esta formado por circulos sera alto y cuando el resultado pase a la función de activación (en este ejemplo la función es relu) el resultado final del nodo tambien sera alto (indicando que hay muchas probabilidades de que el dígito sea 8, 9, 6 o 0), en cambio, los nodos que se encargan de verificar que un dígito este formado por lineas rectas tendran un resultado muy bajo incluso pueden llegar a cancelarse pero esto aumenta las probabilidades de que el numero sea un 8, 9, 6 o 0, toda esta información llega a los nodos de la capa de salida (Output layer), estos nodos conocen a los nodos de las capas anteriores y saben que si un nodo x que se encarga de verificar que los numeros esten formados por circulos entrega un valor muy alto y un nodo y que se encarga de verificar que los numeros esten formados por lineas rectas tiene un valor muy bajo es muy probable que el digito que esta en la imagen sea el de un 8.
def prime_relu(Z):
return np.heaviside(Z, 0)
La derivada de la función relu la necesitamos cuando se ejecute el backpropagation.
def forward_propagation(X, W1, b1, W2, b2):
forward_params = {}
Z1 = np.dot(W1, X.T) + b1
A1 = relu(Z1)
Z2 = np.dot(W2, A1) + b2
A2 = sigmoid(Z2)
forward_params = {
"Z1": Z1,
"A1": A1,
"Z2": Z2,
"A2": A2,
}
return forward_params
El código para el forward propagation es muy parecido al que teniamos en el algoritmo de regresión logistica, solo que aquí tenemos dos capas (Hidden layer y Output layer) y en la regresión logistica es como si solo tuvieramos una, es por eso que aquí tenemos Z2 y A2.
Regresamos un diccionario con los resultados ya que los necesitaremos para el backpropagation.
def loss_function(A2, y):
data_size = y.shape[1]
cost = (-1 / data_size) * (np.dot(y, np.log(A2).T) + np.dot(1 - y, np.log(1 - A2).T))
return cost
La función de perdida (También conocida como función de costo), el código es parecido a la función de perdida de la regresión logistica y funciona exactamente igual, esta función es conocida como binary cross entropy y se usa para optimizar problemas de clasificación cuando existen dos clases.
Como hemos visto funciona para indicar que tan bueno es nuestro modelo, si el valor que regresa esta función es muy alto, nuestra red neuronal tendra un desempeño pobre a la hora de clasificar datos, lo que queremos es que el valor de la función sea lo mas bajo posible.
def backward_propagation(forward_params, X, Y):
A2 = forward_params["A2"]
Z2 = forward_params["Z2"]
A1 = forward_params["A1"]
Z1 = forward_params["Z1"]
data_size = Y.shape[1]
dZ2 = A2 - Y
dW2 = np.dot(dZ2, A1.T) / data_size
db2 = np.sum(dZ2, axis=1) / data_size
dZ1 = np.dot(dW2.T, dZ2) * prime_relu(Z1)
dW1 = np.dot(dZ1, X) / data_size
db1 = np.sum(dZ1, axis=1) / data_size
db1 = np.reshape(db1, (db1.shape[0], 1))
grads = {
"dZ2": dZ2,
"dW2": dW2,
"db2": db2,
"dZ1": dZ1,
"dW1": dW1,
"db1": db1,
}
return grads
Esta sin duda es una de las partes más complejas de las redes neuronales, tengo un tutorial explicando como funciona la parte matematica de este algoritmo por lo que no lo volvere a explicarlo, pero la idea principal es ver como los parametros W y b afectan a la función de perdida y como cambiando los valores de estos parametros podemos optimizar (minimizar) esta función.
En un principio esta bien si no entiendes la explicación, puedes seguir estudiando y aprendiendo, llegara el momento en el que lo comprendas, Puedes mandarme un mensaje y tratare de explicar lo mejor que pueda y tambien darme consejos si mi tutorial sobre backpropagation es muy confuso o explico mal algun termino
def one_hidden_layer_model(X, y, epochs=1000, learning_rate=0.003):
np.random.seed(0)
input_size = X_train.shape[1]
output_size = 1
hidden_layer_nodes = 5
W1 = np.random.randn(hidden_layer_nodes, input_size) / np.sqrt(input_size)
b1 = np.zeros((hidden_layer_nodes, 1))
W2 = np.random.randn(output_size, hidden_layer_nodes) / np.sqrt(hidden_layer_nodes)
b2 = np.zeros((output_size, 1))
loss_history = []
for i in range(epochs):
forward_params = forward_propagation(X, W1, b1, W2, b2)
A2 = forward_params["A2"]
loss = loss_function(A2, y)
grads = backward_propagation(forward_params, X, y)
W1 -= learning_rate * grads["dW1"]
b1 -= learning_rate * grads["db1"]
W2 -= learning_rate * grads["dW2"]
b2 -= learning_rate * grads["db2"]
if i % 1000 == 0:
loss_history.append(loss)
print ("Costo e iteracion %i: %f" % (i, loss))
return W1, b1, W2, b2
Aquí ya tenemos el código necesario para que la red neuronal funcione, esta función tiene varias partes importantes:
input_size = X_train.shape[1]
output_size = 1
hidden_layer_nodes = 5
-
input_size: Indica cuantos nodos tiene la capa de entrada, en este ejemplo son dos nodos.
-
output_size: Indica cuantos nodos tiene la capa de salida, en este ejemplo solo es un nodo.
-
hidden_layer_nodes: indica cuandos nodos tiene la capa oculta, en este ejemplo usaremos cinco nodos.
Estas variables son importantes porque cuando declaremos los valores de los pesos W y de b estos tienen que tener relación entre si para que se puedan realizar las multiplicaciones de matrices W con X y las sumas b.
W1 = np.random.randn(hidden_layer_nodes, input_size) / np.sqrt(input_size)
b1 = np.zeros((hidden_layer_nodes, 1))
W2 = np.random.randn(output_size, hidden_layer_nodes) / np.sqrt(hidden_layer_nodes)
b2 = np.zeros((output_size, 1))
Declaramos los valores de los pesos y de b, como mencione hace poco es importante que la forma de los pesos W sean compatibles con la forma de los datos X, X, esto lo explico en el tutorial de regresión logistica
for i in range(epochs):
forward_params = forward_propagation(X, W1, b1, W2, b2)
A2 = forward_params["A2"]
loss = loss_function(A2, y)
grads = backward_propagation(forward_params, X, y)
W1 -= learning_rate * grads["dW1"]
b1 -= learning_rate * grads["db1"]
W2 -= learning_rate * grads["dW2"]
b2 -= learning_rate * grads["db2"]
if i % 1000 == 0:
loss_history.append(loss)
print ("Costo e iteracion %i: %f" % (i, loss))
En estas líneas de código se realiza el Gradient descent, el algoritmo se ejecutara dependiendo de cuantas epocas (epochs) le indiquemos.
La red neuronal en ejecución
Prepararemos los datos que usara la red neuronal para aprender
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0, test_size=0.20)
y_train = np.reshape(y_train, (1, y_train.shape[0]))
y_val = np.reshape(y_val, (1, y_val.shape[0]))
Y creamos el modelo para que aprenda
W1, b1, W2, b2 = one_hidden_layer_model(X_train, y_train, epochs=20000, learning_rate=0.003)
El modelo nos regresa los parametros entrenados W y b ya que los usaremos para predecir nuevos datos
def predict(W1, b1, W2, b2, X):
data_size = X.shape[0]
forward_params = forward_propagation(X, W1, b1, W2, b2)
y_prediction = np.zeros((1, data_size))
A2 = forward_params["A2"]
for i in range(A2.shape[1]):
y_prediction[0, i] = 1 if A2[0, i] > 0.5 else 0
return y_prediction
Esta función usa los parametros ya entrenados W, b para predecir las clases de los datos que le entreguemos.
train_predictions = predict(W1, b1, W2, b2, X_train)
validation_predictions = predict(W1, b1, W2, b2, X_val)
print("train accuracy: {} %".format(100 - np.mean(np.abs(train_predictions - y_train)) * 100))
print("test accuracy: {} %".format(100 - np.mean(np.abs(validation_predictions - y_val)) * 100))
Imprimimos que tan buena es la red neuronal con ambos set de datos (training, validation).
train accuracy: 85.41666666666667 %
test accuracy: 83.33333333333334 %
Es un buen resultado pero puede ser mejor, todo depende de que parametros usemos, podemos cambiar el numero de nodos, el numero de epocas o la taza de aprendizaje.
Estos parametros son llamados hiperparametros (en ingles hyperparameters) y son variables que cambian dependiendo del modelo y del problema que se quiera resolver, por ejemplo si tenemos un modelo pequeño no necesitaremos valores tan grandes para estos hiperparametros.
Este es un modelo creado desde cero pero existen muchos frameworks que te ayudan a crear modelos sin errores y mucho más optimizados, si vas a crear un modelo desde cero lo mejor es que lo uses con el proposito de aprender como funcionan pero no que lo uses para problemas de la vida real. Existen diferentes tipos de redes neuronales, algunas son muy buenas reconociendo imagenes, otras reconociendo palabras y lenguajes, más adelante explicare más conceptos sobre redes neuronales y usaremos frameworks como Keras para crear nuevos modelos mucho más eficientes y poderosos.