Crear una red neuronal desde cero en Python: agregar capas ocultas

Este es el segundo artículo de la serie de artículos sobre "Creación de una red neuronal desde cero en Python". Creando una Red Neuronal desde Cero en...

Este es el segundo artículo de la serie de artículos sobre "Creación de una red neuronal desde cero en Python".

Si es un principiante absoluto en las redes neuronales, primero debe leer la Parte 1 de esta serie (vinculada arriba). 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 las redes neuronales artificiales; vimos cómo crear una red neuronal simple con una entrada y una capa de salida, desde cero en Python. Tal 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 Perceptron 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 no linealmente. En otras palabras, necesitamos un conjunto de datos que no se pueda clasificar usando una línea recta.

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

Ejecute el siguiente script para generar el conjunto de datos que vamos a utilizar para entrenar y probar nuestra red neuronal.

1
2
3
4
5
6
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 clase datasets de la biblioteca sklearn. Para crear un conjunto de datos no lineal de 100 puntos de datos, usamos el método make_moons y le pasamos 100 como primer parámetro. El método devuelve un conjunto de datos que, cuando se traza, contiene dos semicírculos intercalados, como se muestra en la siguiente figura:

Moons dataset{.img-responsive}

Puede ver claramente que estos datos no se pueden separar por una sola línea recta, por lo tanto, el perceptrón no se puede usar 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 usando 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í:

Red neuronal con capa oculta{.img-responsive}

En la figura anterior, tenemos una red neuronal con 2 entradas, una capa oculta y una capa de salida. La capa oculta tiene 4 nodos. La capa de salida tiene 1 nodo 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 nodos y capas ocultas haya en la red neuronal, el principio básico de funcionamiento 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 nodo 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 producción prevista se compara con la producción real y se calcula el costo del error. El propósito es minimizar la función de costo.

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 debe 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 propagación hacia adelante y hacia atrás para la red neuronal que tiene una o más capas ocultas.

Feed Forward

Para cada registro, tenemos dos funciones "x1" y "x2". Para calcular los valores de cada nodo en la capa oculta, debemos multiplicar la entrada con los pesos correspondientes del nodo 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 del primer nodo en 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 nodo superior de la capa oculta. De la misma manera, puede calcular los valores para los nodos 2, 3 y 4 de la capa oculta.

De manera similar, para calcular el valor de la capa de salida, los valores en los nodos de la capa oculta se tratan como entradas. Por lo tanto, para calcular la salida, multiplique los valores de los nodos 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 + ah3w11 + ah4w12
$$

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

Aquí "a0" es la salida final de nuestra red neuronal. Recuerda que la función de activación que estamos usando es la función sigmoide, 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 {#propagación hacia atrás}

El paso de alimentación hacia adelante 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 error medio cuadrado. Se puede representar matemáticamente como:

$$
MSE =
\frac{\mathrm{1} }{\mathrm{n}}
\suma\sin límites_{i=1}^{n}
(predicho - observado)^{2}
$$

Aquí n es el número de observaciones.

Fase 1

En la primera fase de la propagación hacia atrás, 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:

Fase de retropropagación 1{.img-responsive}

Esto se parece al perceptrón que desarrollamos en el último artículo. 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 optimizacion donde tenemos que encontrar la función mínima para nuestro funcion de costo

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

$$ repetir \ hasta \ convergencia: \begin{Bmatrix} w_j := w_j - \alpha \frac{\parcial }{\parcial w_j} J(w_0,w_1 ...\ …. w_n) \end{Bmatriz} ..........… (1) $$

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

Nuestra función de costo es:

$$
MSE = \frac{\mathrm{1} }{\mathrm{n}} \sum\nolimits_{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:

$$
costo = \frac{\mathrm{1} }{\mathrm{n}} \sum\nolimits_{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 para que el costo disminuya. Para hacerlo, necesitamos derivar 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 usando la cadena de reglas de diferenciación.

$$
\frac {dcost}{trabajo} = \frac {dcost}{trabajo} *, \frac {trabajo}{trabajo} * \frac {trabajo}{trabajo} ....(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í 2 y n son 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} = sigmoide(zo) * (1-sigmoide(zo)) ....…. (6)
$$

Finalmente, necesitamos encontrar "dzo" con respecto a "dwo". La derivada son simplemente las entradas provenientes 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 ponderación actualizados para las ponderaciones de la capa de salida. Para encontrar nuevos valores de peso, los valores devueltos por la Ecuación 1 pueden simplemente multiplicarse por 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 capas ocultas, 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 costo con respecto a "wh". Matemáticamente podemos usar la regla de la cadena de diferenciación para representarlo 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 la cadena de diferenciación de la siguiente manera:

$$
\frac {dcoste}{dah} = \frac {dcoste}{dzo} *, \frac {dzo}{dah} ...... (3)
$$

Vamos a dividir 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 {dcoste}{dzo} = \frac {dcoste}{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 a zo, tiene el siguiente valor:

$$
Sol = a01w9 + a02w10 + a03w11 + a04w12
$$

Si lo diferenciamos con respecto a todas las entradas de la capa oculta, denotadas por "ao", entonces nos quedan todos los pesos de la capa de salida, denotadas por "wo". Por lo tanto,

$$
\frac {dzo}{dah} = donde ...... (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 ponderación para las ponderaciones de capas ocultas "wh", los valores devueltos por la Ecuación 2 pueden simplemente multiplicarse por la tasa de aprendizaje y restarse de los valores de ponderación actuales. Y eso es más o menos.

Las ecuaciones pueden parecerle agotadoras ya que se están realizando muchos cálculos. Sin embargo, si los miras 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 otros algoritmos de aprendizaje automático 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 nodos, dos entradas y una salida, pero 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 lo 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 script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# -*- 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 de 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 avance. El fragmento de código para la operación de avance es el siguiente:

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
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 gradientes para los pesos de las capas ocultas. El siguiente script se ejecuta en la segunda fase de la propagación hacia atrás.

1
2
3
4
5
6
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:

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

Cuando se ejecuta 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 multiclase.