Construir una Red neuronal en Python desde cero

En este tutorial vamos a contruir y entrenar una red neuronal en Python paso a paso y desde cero.

Regresión logística y redes neuronales

Las redes neuronales tienen mucho que ver con la regresión logística así que te recomiendo que si no lo has hecho todavía le eches un vistazo a este tutorial.

De hecho una regresión logística no es más que una red neuronal super sencilla con una única capa y una función de activación sigmoide.

El entrenamiento es el mismo.

  1. Definir las funciones de activación
  2. Definir la función de coste. Al ser un problema de clasificación usaremos la misma que se utiliza en regresión logística
  3. Definir una función que calcule el gradiente de la función de coste (en redes neuronales se denomina backpropagation)
  4. Construir el algoritmo de descenso de gradiente
  5. Juntar todos los pasos 🙂

Arquitectura de una red neuronal

Como digo una red neuronal es como resolver el problema de regresión logística a lo grande puesto que la regresión logística es básicamente una neurona individual con una operación lineal seguida de una función de activación sigmoide.

En una red neuronal, múltiples de estas "neuronas" se combinan y se organizan en capas para crear un modelo más complejo y poderoso.

Cada neurona en estas capas realiza dos operaciones principales:

  1. Operación Lineal: Aquí, cada neurona recibe varias entradas, multiplica cada entrada por un coeficiente (que es un parámetro de la red ajustable), y suma estos productos. Esto es similar a la regresión lineal, donde se hace una suma ponderada de las entradas.
  2. Función de Activación: Después de la operación lineal, se aplica una función de activación. Esta función introduce no linealidades en el modelo, lo que permite a la red neuronal aprender patrones más complejos. A diferencia de la regresión logística no tiene por qué ser siempre la función sigmoide.
arquitectura red neuronal

Programar una red neuronal con numpy

Para construir nuestra red neuronal desde cero y que no se nos haga bola vamos a hacerlo por partes.

  • 1️⃣ Inicializamos los coeficientes de nuestro modelo
  • 2️⃣ Calculamos la salida de nuestra red neuronal, el llamado forward pass. Para ello tendremos que tener en cuenta que:
    • Cada capa hace una combinación lineal de sus entradas.
    • En cada capa se aplica la función de activación que corresponda sobre el resultado de la combinación lineal anterior.

Como funciones de activación tendremos una sigmoide y la función ReLU, un clásico de las redes neuronales.

Seguimos...

La salida de nuestra red con los coeficientes que hemos inicializado en el primer paso será un churro.

Nuestro objetivo es actualizar los coeficientes de manera iterativa hasta que el error entre la predicción de nuestra red y el resultado real sea lo menor posible.

Esto suena a optimización con el algoritmo de descenso de gradiente.

  • 3️⃣ Calculamos la función de coste. Este coste nos indica cuál es el error entre lo que devuelve nuestra red y la realidad.
  • 4️⃣ Calculamos los gradientes de cada capa (si en esta parte te acuerdas de la regla de cadena para resolver derivadas parciales, te vendrá de lujo)
  • 5️⃣ Actualizamos los coeficientes de la red.

Cada paso de este proceso de entrenamiento será una función.

Primero escribiremos el código de todas estas funciones en Numpy y después podremos entrenar la red para que sea capaz de clasificar gatitos en una imagen (como no 😽)

Lo primero es importar las librerías que vamos a utilizar...

import numpy as np
import copy
import matplotlib.pyplot as plt

¡Y al lío!

Membresía requerida

Este contenido está disponible únicamente para suscriptores.

Puedes apuntarte a la plataforma en este enlace

¿Ya eres un ninja? logeate aquí

Entrenamiento de nuestra red neuronal

Ya tenemos todos los pasos para poder construir y entrenar una red neuronal desde cero:

  1. Vamos a cargar los datos de entrenamiento
  2. Vamos a definir la arquitectura de la red y los hiperparámetros
  3. Entrenaremos la red
  4. Comprobaremos los resultados sobre una imagen que la red no ha visto nunca y mediremos su precisión.

Ejemplo de entrenamiento de red neuronal (con gatitos 😻)

En este ejemplo vamos a clasificar imágenes de gatitos 🐱

Vamos a utilizar un dataset con imágenes de baja resolución y un Notebook de Google Colab. No es necesario que te instales nada, solo te hace falta una cuenta en Google 🙂

Así que te aconsejo que vayas siguiendo el tutorial en tu propio Notebook de Colab, será bastante más divertido que sólo leer 😉

Para poder utilizar el dataset directamente en Google Colab sólo tienes que seguir este tutorial y sustituir el dataset que se descarga en ese tutorial por el que realmente queremos utilizar aquí.

!kaggle datasets download -d martaarroyo/pixelpurr-small-size-catnot-cat-image-dataset

Esto es lo que encontrarás:

  • CSV de Imágenes de Entrenamiento: Un archivo con 209 imágenes RGB que han sido astutamente aplanadas. Cada imagen originalmente tiene unas dimensiones de 64x64 píxeles. Este pequeño tamaño es genial para que los algoritmos se ejecuten rápidamente.
  • CSV de Etiquetas de Entrenamiento: Acompañando a las imágenes, este archivo proporciona etiquetas que indican si cada imagen contiene un lindo gatito o no.
    • Si la imagen contiene un gato la etiqueta será 1 (😻)
    • Si la imagen no contiene un gato la etiqueta será 0 (😿)
  • CSV de Imágenes de Test: Una vez que hayas entrenado tu modelo, pon a prueba sus habilidades con este conjunto de 50 imágenes, también aplanadas y siguiendo el mismo formato de 64x64 píxeles.
  • CSV de Etiquetas de Test: Comprueba qué la precisión del modelo con estas etiquetas.

Exploración de los datos

Vamos a ver qué nos encontramos...

train_data_flattened = np.loadtxt('train_images.csv', delimiter=',')
train_labels = np.loadtxt('train_labels.csv', delimiter=',')

test_data_flattened = np.loadtxt('test_images.csv', delimiter=',')
test_labels = np.loadtxt('test_labels.csv', delimiter=',')

Lo primero que haremos es escalar la intensidad de los píxeles para poder visualizar alguna imagen.

train_data_flattened = train_data_flattened/255
test_data_flattened = test_data_flattened/255

Después tenemos que darle forma de imagen porque ahora mismo cada imagen es un vector con todos los píxeles seguidos unos detrás de otro.

Es por ello que la matriz de Numpy con las imágenes de entrenamiento tiene unas dimensiones de (209, 12288):

  • 209 imágenes de entrenamiento
  • 12288 píxeles de cada imagen (64x64x3)

Para visualizar la imagen tenemos que devolverla a su forma original:

train_data = train_data_flattened.reshape(209, 64, 64, 3)
test_data = test_data_flattened.reshape(50, 64, 64, 3)

print(f"Las dimensiones del dataset de entrenamiento son: {train_data.shape}")
print(f"Las dimensiones de las etiquetas de entrenamiento son: {train_labels.shape}")
print(f"Las dimensiones del dataset de test son: {test_data.shape}")
print(f"Las dimensiones de las etiquetas de test son: {test_labels.shape}")
Las dimensiones del dataset de entrenamiento son: (209, 64, 64, 3)
Las dimensiones de las etiquetas de entrenamiento son: (209,)
Las dimensiones del dataset de test son: (50, 64, 64, 3)
Las dimensiones de las etiquetas de test son: (50,)

¡Vamos a visualizar una! 🔍

index = 25
plt.imshow(train_data[index])
print(f"La imagen {index} tiene la etiqueta = {train_labels[index]}")

Además, para no tener problemas con las dimensiones de los vectores numpy de etiquetas vamos a redimensionarlos:

train_labels = train_labels.reshape(1, -1)
test_labels = test_labels.reshape(1, -1)

Definición de los hiperparámetros de la red

La arquitectura de la red que vamos a usar va a tener 4 capas:

  • La primera de 20 nodos con una función de activación ReLU
  • La segunda de 7 nodos con una función de activación ReLU
  • La tercera de 5 nodos con una función de activación ReLU
  • La última tendrá un único nodo con función. deactivación sigmoide para poder realizar una tarea de clasificación binaria

Usaremos una tasa de aprendizaje de 0.0075 y ejecutaremos la optimización del descenso de gradiente 2500 veces.

layers_dims = [12288, 20, 7, 5, 1]
alpha = 0.0075
num_iters = 2500

Puedes probar a cambiar la arquitectura de la red y los hiperparámetros y ver si mejoras los resultados 🙂

Fase de entrenamiento 🏋️‍♂️

Para la fase de entrenamiento únicamente tenemos que unir todos los bloques que hemos hecho hasta ahora en una única función que ejecutará la optimización con el algoritmo del descenso de gradiente.

Recuerda que la idea es actualizar los coeficientes del modelo de manera iterativa según el gradiente de la función de coste.

def entrenamiento(X, Y, layers_dims, learning_rate = 0.0075, num_iters = 3000, imprime_coste=False):
    """
    Implementa una red neuronal de L capas: [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID.

    Argumentos:
    X -- datos de entrada. Array de numpy con la imagen vectorizada y dimensiones (num_px * num_px * 3, número de ejemplos)
    Y -- vector de "etiquetas" verdaderas (contiene 1 si es gato, 0 si no es gato). Dimensiones: (1, número de ejemplos)
    layers_dims   -- lista que contiene el tamaño de entrada y el tamaño de cada capa, de longitud (número de capas + 1).
    learning_rate -- tasa de aprendizaje del descenso del gradiente
    num_iters     -- número de iteraciones del bucle de optimización
    imprime_coste -- si es True, imprime el coste cada 100 pasos

    Devuelve:
    parameteros -- parámetros aprendidos por el modelo. 
    """

    np.random.seed(1) # Para conseguir que nuestros resultados sean reproducibles
    costes = [] # Guardamos un histórico del coste

    # Inicializacion de coeficientes del modelo
    parametros = inicializa_coef_deep(layers_dims)

    # Descenso de gradiente
    for i in range(0, num_iters):

        # Propagación (forward pass): [LINEAR -> RELU]*(L-1) -> LINEAR -> SIGMOID.
        AL, caches = modelo_forward(X, parametros)

        # Cálculo del coste
        coste = funcion_coste(AL, Y)

        # Retropropagación (backpropagation
        grads = modelo_backward(AL, Y, caches)

        # Actualización de parámetros
        parametros = actualiza_coefs(parametros, grads, learning_rate)


        # Imprimimos el coste cada 100 iteraciones
        if imprime_coste and i % 100 == 0 or i == num_iters - 1:
            print(f"Coste después de la iteración {i}: {np.squeeze(coste)}")
        if i % 100 == 0 or i == num_iters:
            costes.append(coste)

    return parametros, costes

Seleccionamos los datos de entrenamiento (imágenes y etiquetas), la artquitectura de la red (layers_dims), la tasa de aprendizaje y el número de iteraciones.

¡Y a entrenar!

parametros, costes = entrenamiento(X_train, train_labels, layers_dims, learning_rate = 0.0075, num_iters = 2500, imprime_coste=True)

Después de un número suficiente de iteraciones (num_iters) la función de coste debería haber disminuido mucho.

Coste después de la iteración 0: 0.7717493284237686
Coste después de la iteración 100: 0.6720534400822913
Coste después de la iteración 200: 0.6482632048575212
Coste después de la iteración 300: 0.6115068816101354
Coste después de la iteración 400: 0.567047326836611
Coste después de la iteración 500: 0.5401376634547801
Coste después de la iteración 600: 0.5279299569455267
Coste después de la iteración 700: 0.4654773771766852
Coste después de la iteración 800: 0.369125852495928
Coste después de la iteración 900: 0.3917469743480535
Coste después de la iteración 1000: 0.31518698886006163
Coste después de la iteración 1100: 0.27269984417893856
Coste después de la iteración 1200: 0.23741853400268134
Coste después de la iteración 1300: 0.19960120532208644
Coste después de la iteración 1400: 0.18926300388463305
Coste después de la iteración 1500: 0.1611885466582775
Coste después de la iteración 1600: 0.14821389662363316
Coste después de la iteración 1700: 0.13777487812972944
Coste después de la iteración 1800: 0.1297401754919012
Coste después de la iteración 1900: 0.12122535068005211
Coste después de la iteración 2000: 0.11382060668633714
Coste después de la iteración 2100: 0.10783928526254133
Coste después de la iteración 2200: 0.10285466069352679
Coste después de la iteración 2300: 0.10089745445261787
Coste después de la iteración 2400: 0.09287821526472397
Coste después de la iteración 2499: 0.088439943441702

Resultado de nuestra red entrenada

Una vez que hemos entrenado la red neuronal y ya tenemos los parámetros de la red podemos clasificar nuevas imágenes.

  1. Los valores de entrada se propagan por la red (forward pass)
  2. Obtenemos la salida que es un vector con las probabilidades de que en la imagen que hemos introducido haya un gato.
  3. Si tenemos una probabilidad mayor de 0.5 podemos suponer que hay un gato en la imagen.
AL, _ = modelo_forward(X_test, parametros)
pred = AL > 0.5 
index = 10
plt.imshow(test_data[index])
print(f"La imagen {index} tiene la etiqueta = {test_labels[0,index]}")

¡Voilà! Un gatito 😽

Precisión de la red

Para calcular la precisión de la red sobre el conjunto de imágenes de test sólo tenemos que comprobar cuantas veces nos hemos equivocado de etiqueta.

print(f"La precisión de la red neuronal es: {np.sum(pred == test_labels)/X_test.shape[1]}")

En este caso hemos obtenido una precisión de 0.8 🙂

Accede a todo el contenido premium

Ya no necesitas pagar cientos de euros por un Bootcamp para convertirte en ninja de los datos. Por solo 17€/mes (o menos 🤯), obtén acceso al podcast premium, a todos los tutoriales y a los resúmenes de los libros más top sobre Machine Learning y Ciencia de datos y aprende a tu ritmo.
¡Empieza ahora!
Copyright © 2024  · Datos 🥷 · Todos los derechos reservados