Creación de una red neuronal desde cero en Python: Añadiendo capas ocultas

C

Si es absolutamente principiante en las redes neuronales, primero debe leer la Parte 1 de esta serie. Una vez que se sienta cómodo con los conceptos explicados en ese artículo, puede regresar y continuar con este artículo.

Introducción

En el artículo anterior, comenzamos nuestra discusión sobre redes neuronales artificiales; vimos cómo crear una red neuronal simple con una entrada y una capa de salida, desde cero en Python. Esta red neuronal se llama perceptrón. Sin embargo, las redes neuronales del mundo real, capaces de realizar tareas complejas como la clasificación de imágenes y el análisis del mercado de valores, contienen múltiples capas ocultas además de la capa de entrada y salida.

En el artículo anterior, concluimos que un perceptrón es capaz de encontrar un límite de decisión lineal. Usamos perceptrón para predecir si una persona es diabética o no usando un conjunto de datos de juguetes. Sin embargo, un perceptrón no es capaz de encontrar límites de decisión no lineales.

En este artículo, nos basaremos en los conceptos que estudiamos en la Parte 1 de esta serie y desarrollaremos una red neuronal con una capa de entrada, una capa oculta y una capa de salida. Veremos que la red neuronal que desarrollaremos será capaz de encontrar fronteras no lineales.

Conjunto de datos

Para este artículo, necesitamos datos separables de forma no lineal. En otras palabras, necesitamos un conjunto de datos que no se pueda clasificar mediante una línea recta.

Afortunadamente, la biblioteca Scikit Learn de Python viene con una variedad de herramientas que se pueden usar para generar automáticamente diferentes tipos de conjuntos de datos.

Ejecutamos el siguiente script para generar el conjunto de datos que vamos a utilizar, con el fin de entrenar y probar nuestra red neuronal.

from sklearn import datasets

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

En el script anterior, importamos la datasetsclase de la sklearnbiblioteca. Para crear un conjunto de datos no lineal de 100 puntos de datos, usamos el make_moonsmétodo y lo pasamos 100 como primer parámetro. El método devuelve un conjunto de datos, que cuando se traza contiene dos semicírculos entrelazados, como se muestra en la siguiente figura:

Puede ver claramente que estos datos no se pueden separar por una sola línea recta, por lo que el perceptrón no se puede utilizar para clasificar correctamente estos datos.

Verifiquemos este concepto. Para hacerlo, usaremos un perceptrón simple con una capa de entrada y una capa de salida (la que creamos en el último artículo) e intentaremos clasificar nuestro conjunto de datos de “lunas”. Ejecute el siguiente script:

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

labels = labels.reshape(100, 1)

def sigmoid(x):
    return 1/(1+np.exp(-x))

def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))

np.random.seed(42)
weights = np.random.rand(2, 1) 
lr = 0.5
bias = np.random.rand(1)

for epoch in range(200000):
    inputs = feature_set

    # feedforward step 1
    XW = np.dot(feature_set,weights) + bias

    # feedforward step 2
    z = sigmoid(XW)

    # backpropagation step 1
    error_out = ((1 / 2) * (np.power((z - labels), 2)))
    print(error_out.sum())

    error = z - labels

    # backpropagation step 2
    dcost_dpred = error
    dpred_dz = sigmoid_der(z) 

    z_delta = dcost_dpred * dpred_dz

    inputs = feature_set.T
    weights -= lr * np.dot(inputs, z_delta)

    for num in z_delta:
        bias -= lr * num

Verá que el valor del error cuadrático medio no convergerá más allá del 4,17 por ciento, sin importar lo que haga. Esto nos indica que no podemos clasificar correctamente todos los puntos del conjunto de datos utilizando este perceptrón, sin importar lo que hagamos.

Redes neuronales con una capa oculta

En esta sección, crearemos una red neuronal con una capa de entrada, una capa oculta y una capa de salida. La arquitectura de nuestra red neuronal se verá así:

En la figura anterior, tenemos una red neuronal con 2 entradas, una capa oculta y una capa de salida. La capa oculta tiene 4 nodes. La capa de salida tiene 1 node ya que estamos resolviendo un problema de clasificación binaria, donde solo puede haber dos salidas posibles. Esta arquitectura de red neuronal es capaz de encontrar límites no lineales.

No importa cuántos nodes y capas ocultas haya en la red neuronal, el principio de funcionamiento básico sigue siendo el mismo. Comienza con la fase de avance en la que las entradas de la capa anterior se multiplican con los pesos correspondientes y se pasan a través de la función de activación para obtener el valor final para el node correspondiente en la siguiente capa. Este proceso se repite para todas las capas ocultas hasta que se calcula la salida. En la fase de retropropagación, la salida prevista se compara con la salida real y se calcula el costo del error. El propósito es minimizar la función de costos.

Esto es bastante sencillo si no hay una capa oculta involucrada como vimos en el artículo anterior.

Sin embargo, si una o más capas ocultas están involucradas, el proceso se vuelve un poco más complejo porque el error tiene que propagarse a más de una capa, ya que los pesos en todas las capas contribuyen al resultado final.

En este artículo, veremos cómo realizar pasos de retropropagación y retroalimentación para la red neuronal que tiene una o más capas ocultas.

Feed Forward

Para cada registro, tenemos dos características “x1” y “x2”. Para calcular los valores de cada node en la capa oculta, tenemos que multiplicar la entrada con los pesos correspondientes del node para el que estamos calculando el valor. Luego pasamos el producto escalar a través de una función de activación para obtener el valor final.

Por ejemplo, para calcular el valor final para el primer node de la capa oculta, que se indica con “ah1”, debe realizar el siguiente cálculo:

$$
zh1 = x1w1 + x2w2
$$

$$
ah1 = frac {mathrm {1}} {mathrm {1} + e ^ {- zh1}}
$$

Este es el valor resultante para el node superior de la capa oculta. De la misma manera, puede calcular los valores para los nodes 2, 3 y 4 de la capa oculta.

De manera similar, para calcular el valor de la capa de salida, los valores en los nodes de la capa oculta se tratan como entradas. Por lo tanto, para calcular la salida, multiplique los valores de los nodes de la capa oculta con sus pesos correspondientes y pase el resultado a través de una función de activación.

Esta operación se puede expresar matemáticamente mediante la siguiente ecuación:

$$
entonces = ah1w9 ah2w10 + + + ah2w11 ah4w12
$$

$$
a0 = frac {mathrm {1}} {mathrm {1} + e ^ {- z0}}
$$

Aquí “a0” es la salida final de nuestra red neuronal. ReString que la función de activación que estamos usando es la función sigmoidea, como hicimos en el artículo anterior.

Nota: En aras de la simplicidad, no agregamos un término de sesgo a cada ponderación. Verá que la red neuronal con capa oculta funcionará mejor que el perceptrón, incluso sin el término de sesgo.

Propagación hacia atrás

El paso de avance es relativamente sencillo. Sin embargo, la propagación hacia atrás no es tan sencilla como lo fue en la Parte 1 de esta serie.

En la fase de retropropagación, primero definiremos nuestra función de pérdida. Usaremos la función de costo de error cuadrático medio . Se puede representar matemáticamente como:

$$
MSE =
frac {mathrm {1}} {mathrm {n}}
sumnolimits_ {i = 1} ^ {n}
(predicho – observado) ^ {2}
$$

Aquí nestá el número de observaciones.

Fase 1

En la primera fase de retropropagación, necesitamos actualizar los pesos de la capa de salida, es decir, w9, w10, w11 y w12. Entonces, por el momento, solo considere que nuestra red neuronal tiene la siguiente parte:

Esto se parece al perceptrón que desarrollamos en el artículo anterior. El propósito de la primera fase de retropropagación es actualizar los pesos w9, w10, w11 y w12 de tal manera que se minimice el error final. Este es un problema de optimización donde tenemos que encontrar los mínimos de función para nuestra función de costo.

Para encontrar los mínimos de una función, podemos usar el algoritmo de degradado decente . El algoritmo de degradado decente se puede representar matemáticamente de la siguiente manera:

$$
repetir hasta la convergencia: comenzar {Bmatrix} w_j: = w_j – alpha frac {parcial} {parcial w_j} J (w_0, w_1 ……. w_n) end {Bmatrix} …….. ….. (1)
$$

Los detalles sobre cómo la función decente del gradiente minimiza el costo ya se discutieron en el artículo anterior. Aquí veremos las operaciones matemáticas que necesitamos realizar.

Nuestra función de costos es:

$$
MSE = frac {mathrm {1}} {mathrm {n}} sumnolimits_ {i = 1} ^ {n} (predicho – observado) ^ {2}
$$

En nuestra red neuronal, la salida predicha está representada por “ao”. Lo que significa que básicamente tenemos que minimizar esta función:

$$
cost = frac {mathrm {1}} {mathrm {n}} sumnolimits_ {i = 1} ^ {n} (ao – observado) ^ {2}
$$

Del artículo anterior, sabemos que para minimizar la función de costo, tenemos que actualizar los valores de peso de manera que el costo disminuya. Para hacerlo, necesitamos obtener la derivada de la función de costo con respecto a cada peso. Dado que en esta fase, estamos tratando con pesos de la capa de salida, necesitamos diferenciar la función de costo con respecto a w9, w10, w11 y w2.

La diferenciación de la función de costo con respecto a los pesos en la capa de salida se puede representar matemáticamente de la siguiente manera utilizando la regla de diferenciación de la cadena .

$$
frac {dcost}{dwo} = frac {dcost}{dao} *, frac {dao}{dzo} * frac {dzo}{dwo} …… (1)
$$

Aquí “wo” se refiere a los pesos en la capa de salida. La letra “d” al comienzo de cada término se refiere a la derivada.

Encontremos el valor de cada expresión en la Ecuación 1.

Aquí,

$$
frac {dcost} {dao} = frac {2} {n} * (ao – etiquetas)
$$

Aquí 2y nson constantes. Si los ignoramos, tenemos la siguiente ecuación.

$$
frac {dcost} {dao} = (ao – etiquetas) …….. (5)
$$

A continuación, podemos encontrar “dao” con respecto a “dzo” de la siguiente manera:

$$
frac {dao}{dzo} = sigmoid(zo) * (1-sigmoid(zo)) …….. (6)
$$

Finalmente, necesitamos encontrar “dzo” con respecto a “dwo”. La derivada son simplemente las entradas que provienen de la capa oculta como se muestra a continuación:

$$
frac {dzo}{dwo} = ah
$$

Aquí “ah” se refiere a las 4 entradas de las capas ocultas. La ecuación 1 se puede utilizar para encontrar los valores de peso actualizados para los pesos de la capa de salida. Para encontrar nuevos valores de peso, los valores devueltos por la Ecuación 1 pueden simplemente multiplicarse con la tasa de aprendizaje y restarse de los valores de peso actuales. Esto es sencillo y lo hemos hecho anteriormente.

Fase 2

En la sección anterior, vimos cómo podemos encontrar los valores actualizados para los pesos de la capa de salida, es decir, w9, w10, w11 y 12. En esta sección, propagaremos nuestro error a la capa anterior y encontraremos los nuevos valores de peso. para pesos de capa oculta, es decir, pesos w1 a w8.

Denotemos colectivamente los pesos de las capas ocultas como “wh”. Básicamente tenemos que diferenciar la función de coste con respecto a “wh”. Matemáticamente podemos usar la regla de diferenciación de la cadena para representarla como:

$$
frac {dcost} {dwh} = frac {dcost} {dah} *, frac {dah} {dzh} * frac {dzh} {dwh} …… (2)
$$

Aquí nuevamente dividiremos la Ecuación 2 en términos individuales.

El primer término “dcost” se puede diferenciar con respecto a “dah” usando la regla de diferenciación de la cadena de la siguiente manera:

$$
frac {dcost} {dah} = frac {dcost} {dzo} *, frac {dzo} {dah} …… (3)
$$

Dividamos nuevamente la Ecuación 3 en términos individuales. Usando la regla de la cadena nuevamente, podemos diferenciar “dcost” con respecto a “dzo” de la siguiente manera:

$$
frac {dcost}{dzo} = frac {dcost}{dao} *, frac {dao}{dzo} …… (4)
$$

Ya hemos calculado el valor de dcost / dao en la Ecuación 5 y dao / dzo en la Ecuación 6.

Ahora necesitamos encontrar dzo / dah de la Ecuación 3. Si miramos zo, tiene el siguiente valor:

$$
entonces = a01w9 a02w10 + + + a03w11 a04w12
$$

Si lo diferenciamos con respecto a todas las entradas de la capa oculta, denotado por “ao”, entonces nos quedamos con todos los pesos de la capa de salida, denotados por “wo”. Por lo tanto,

$$
frac {dzo} {dah} = wo …… (7)
$$

Ahora podemos encontrar el valor de dcost / dah reemplazando los valores de las ecuaciones 7 y 4 en la ecuación 3.

Volviendo a la Ecuación 2, todavía tenemos que encontrar dah / dzh y dzh / dwh.

El primer término dah / dzh se puede calcular como:

$$
frac {dah} {dzh} = sigmoide (zh) * (1-sigmoide (zh)) …….. (8)
$$

Y finalmente, dzh / dwh son simplemente los valores de entrada:

$$
frac {dzh} {dwh} = características de entrada …….. (9)
$$

Si reemplazamos los valores de las ecuaciones 3, 8 y 9 en la ecuación 3, podemos obtener la matriz actualizada para los pesos de las capas ocultas. Para encontrar nuevos valores de peso para los pesos de capa ocultos “wh”, los valores devueltos por la Ecuación 2 se pueden multiplicar simplemente con la tasa de aprendizaje y restar de los valores de peso actuales. Y eso es todo.

Las ecuaciones pueden parecerle agotadoras ya que se están realizando muchos cálculos. Sin embargo, si los observa de cerca, solo hay dos operaciones que se realizan en una cadena: derivaciones y multiplicaciones.

Una de las razones por las que las redes neuronales son más lentas que los otros algoritmos de Machine Learning es el hecho de que se realizan muchos cálculos en el back-end. Nuestra red neuronal tenía solo una capa oculta con cuatro nodes, dos entradas y una salida, sin embargo, tuvimos que realizar largas operaciones de derivación y multiplicación para actualizar los pesos para una sola iteración. En el mundo real, las redes neuronales pueden tener cientos de capas con cientos de valores de entrada y salida. Por tanto, las redes neuronales se ejecutan lentamente.

Código para redes neuronales con una capa oculta

Ahora implementemos la red neuronal que acabamos de discutir en Python desde cero. Verá claramente la correspondencia entre los fragmentos de código y la teoría que discutimos en la sección anterior. Intentaremos nuevamente clasificar los datos no lineales que creamos en la sección Conjunto de datos del artículo. Eche un vistazo al siguiente guión.

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 25 13:46:08 2018

@author: usman
"""

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

labels = labels.reshape(100, 1)

def sigmoid(x):
    return 1/(1+np.exp(-x))

def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))

wh = np.random.rand(len(feature_set[0]),4) 
wo = np.random.rand(4, 1)
lr = 0.5

for epoch in range(200000):
    # feedforward
    zh = np.dot(feature_set, wh)
    ah = sigmoid(zh)

    zo = np.dot(ah, wo)
    ao = sigmoid(zo)

    # Phase1 =======================

    error_out = ((1 / 2) * (np.power((ao - labels), 2)))
    print(error_out.sum())

    dcost_dao = ao - labels
    dao_dzo = sigmoid_der(zo) 
    dzo_dwo = ah

    dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)

    # Phase 2 =======================

    # dcost_w1 = dcost_dah * dah_dzh * dzh_dw1
    # dcost_dah = dcost_dzo * dzo_dah
    dcost_dzo = dcost_dao * dao_dzo
    dzo_dah = wo
    dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
    dah_dzh = sigmoid_der(zh) 
    dzh_dwh = feature_set
    dcost_wh = np.dot(dzh_dwh.T, dah_dzh * dcost_dah)

    # Update Weights ================

    wh -= lr * dcost_wh
    wo -= lr * dcost_wo

En el script anterior comenzamos importando las bibliotecas deseadas y luego creamos nuestro conjunto de datos. A continuación, definimos la función sigmoidea junto con su derivada. Luego inicializamos la capa oculta y los pesos de la capa de salida con valores aleatorios. La tasa de aprendizaje es 0,5. Probé diferentes tasas de aprendizaje y descubrí que 0.5 es un buen valor.

Luego ejecutamos el algoritmo para 2000 épocas. Dentro de cada época, primero realizamos la operación de feed-forward. El fragmento de código para la operación de avance es el siguiente:

zh = np.dot(feature_set, wh)
ah = sigmoid(zh)

zo = np.dot(ah, wo)
ao = sigmoid(zo)

Como se discutió en la sección de teoría, la propagación hacia atrás consta de dos fases. En la primera fase, se calculan los gradientes para los pesos de la capa de salida. El siguiente script se ejecuta en la primera fase de la propagación hacia atrás.

error_out = ((1 / 2) * (np.power((ao - labels), 2)))
print(error_out.sum())

dcost_dao = ao - labels
dao_dzo = sigmoid_der(zo) 
dzo_dwo = ah

dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)

En la segunda fase, se calculan los degradados para los pesos de las capas ocultas. El siguiente script se ejecuta en la segunda fase de la propagación inversa.

dcost_dzo = dcost_dao * dao_dzo
dzo_dah = wo
dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
dah_dzh = sigmoid_der(zh) 
dzh_dwh = feature_set
dcost_wh = np.dot( dzh_dwh.T, dah_dzh * dcost_dah)

Finalmente, los pesos se actualizan en el siguiente script:

wh -= lr * dcost_wh
wo -= lr * dcost_wo

Cuando se ejecute el script anterior, verá un valor de error cuadrático medio mínimo de 1,50, que es menor que nuestro error cuadrático medio anterior de 4,17, que se obtuvo utilizando el perceptrón. Esto muestra que la red neuronal con capas ocultas funciona mejor en el caso de datos separables no linealmente.

Conclusión

En este artículo, vimos cómo podemos crear una red neuronal con 1 capa oculta, desde cero en Python. Vimos cómo nuestra red neuronal superó a una red neuronal sin capas ocultas para la clasificación binaria de datos no lineales.

Sin embargo, es posible que necesitemos clasificar los datos en más de dos categorías. En nuestro próximo artículo, veremos cómo crear una red neuronal desde cero en Python para problemas de clasificación de varias clases.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad