Vicente Rodríguez

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:

linearRegression1

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:

linearRegression2

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:

logistic regression

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

neuronal network 1

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:

model1

model2

Con una red neuronal podemos combinar ambos modelos para que formen uno solo que quede de la siguiente manera:

model3

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:

neuronal network 2

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:

Como mencione podemos tener una red neuronal con más capas y más nodos como en el siguiente ejemplo

neuronal network 3

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:

data set

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

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.