Creación de una red neuronal desde cero en Python

Este es el primer artículo de una 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 primer artículo de la serie de artículos sobre "Creación de una red neuronal desde cero en Python".

Introducción

¿Alguna vez te has preguntado cómo los chatbots como Siri, Alexa y Cortona pueden responder a las consultas de los usuarios? ¿O cómo los coches autónomos son capaces de conducirse sin ayuda humana? Todos estos productos de lujo tienen una cosa en común: la inteligencia artificial (IA). Es la IA la que les permite realizar tales tareas sin ser supervisados ​​o controlados por un humano. Pero la pregunta sigue siendo: "¿Qué es la IA?". Una respuesta simple a esta pregunta es: "La IA es una combinación de algoritmos complejos de varios dominios matemáticos, como Álgebra, Cálculo y Probabilidad y Estadística".

En este artículo, estudiaremos una red neuronal artificial simple, que es uno de los principales componentes básicos de la inteligencia artificial. Existen diferentes variantes de una Red Neuronal Artificial, dedicadas a resolver un problema en particular. Por ejemplo, Redes neuronales convolucionales se usan comúnmente para problemas de reconocimiento de imágenes, mientras que Redes neuronales recurrentes se utilizan para resolver problemas de secuencias.

Hay muchas bibliotecas de aprendizaje profundo que se pueden usar para crear una red neuronal en una sola línea de código. Sin embargo, si realmente desea comprender el funcionamiento en profundidad de una red neuronal, le sugiero que aprenda a codificarla desde cero en cualquier lenguaje de programación. Realizar este ejercicio realmente te aclarará muchos de los conceptos. Y esto es exactamente lo que haremos en este artículo.

El problema

Dado que este es un artículo introductorio, el problema que vamos a resolver es bastante simple. Supongamos que tenemos información sobre la obesidad, los hábitos de fumar y los hábitos de ejercicio de cinco personas. También sabemos si estas personas son diabéticas o no. Nuestro conjunto de datos se ve así:

Persona Fumar Obesidad Ejercicio Diabético


Persona 1 0 1 0 1 Persona 2 0 0 1 0 Persona 3 1 0 0 0 Persona 4 1 1 0 1 Persona 5 1 1 1 1

En la tabla anterior, tenemos cinco columnas: Persona, Tabaquismo, Obesidad, Ejercicio y Diabético. Aquí 1 se refiere a verdadero y 0 se refiere a falso. Por ejemplo, la primera persona tiene valores de 0, 1, 0, lo que significa que la persona no fuma, es obesa y no hace ejercicio. La persona también es diabética.

Es claramente evidente a partir del conjunto de datos que la obesidad de una persona es indicativa de que es diabético. Nuestra tarea es crear una red neuronal que sea capaz de predecir si una persona desconocida es diabética o no a partir de datos sobre sus hábitos de ejercicio, obesidad y tabaquismo. Este es un tipo de problema de aprendizaje supervisado en el que se nos dan entradas y salidas correctas correspondientes y nuestra tarea es encontrar el mapeo entre las entradas y las salidas.

Nota: Este es solo un conjunto de datos ficticio, en la vida real, las personas obesas no siempre son necesariamente diabéticas.

La Solución

Crearemos una red neuronal muy simple con una capa de entrada y una capa de salida. Antes de escribir cualquier código real, primero veamos cómo se ejecutará nuestra red neuronal, en teoría.

Teoría de redes neuronales {#teoría de redes neuronales}

Una red neuronal es un algoritmo de aprendizaje supervisado, lo que significa que le proporcionamos los datos de entrada que contienen las variables independientes y los datos de salida que contienen la variable dependiente. Por ejemplo, en nuestro ejemplo, nuestras variables independientes son el tabaquismo, la obesidad y el ejercicio. La variable dependiente es si una persona es diabética o no.

Al principio, la red neuronal hace algunas predicciones aleatorias, estas predicciones se comparan con la salida correcta y se calcula el error o la diferencia entre los valores predichos y los valores reales. La función que encuentra la diferencia entre el valor real y los valores propagados se llama función de costo. El costo aquí se refiere al error. Nuestro objetivo es minimizar la función de coste. Entrenar una red neuronal básicamente se refiere a minimizar la función de costo. Veremos cómo podemos realizar esta tarea.

La red neuronal que vamos a crear tiene la siguiente representación visual.

neural network{.img-responsive}

Una red neuronal se ejecuta en dos pasos: Feed Forward y Back Propagation. Discutiremos estos dos pasos en detalle.

Feed Forward

En la parte de avance de una red neuronal, las predicciones se realizan en función de los valores en los nodos de entrada y los pesos. Si observa la red neuronal en la figura anterior, verá que tenemos tres características en el conjunto de datos: fumar, obesidad y ejercicio, por lo tanto, tenemos tres nodos en la primera capa, también conocida como capa de entrada. Hemos reemplazado nuestros nombres de características con la variable x, por generalidad en la figura anterior.

Los pesos de una red neuronal son básicamente las cuerdas que tenemos que ajustar para poder predecir correctamente nuestra salida. Por ahora, recuerde que para cada característica de entrada, tenemos un peso.

Los siguientes son los pasos que se ejecutan durante la fase de avance de una red neuronal:

Paso 1: (Calcular el producto escalar entre entradas y pesos) {#paso1calcular el producto escalar entre entradas y pesos}

Los nodos en la capa de entrada están conectados con la capa de salida a través de tres parámetros de peso. En la capa de salida, los valores de los nodos de entrada se multiplican por sus pesos correspondientes y se suman. Finalmente, el término de sesgo se agrega a la suma. La b en la figura anterior se refiere al término de sesgo.

El término sesgo es muy importante aquí. Supongamos que tenemos una persona que no fuma, no es obesa y no hace ejercicio, la suma de los productos de los nodos de entrada y los pesos será cero. En ese caso, la salida siempre será cero sin importar cuánto entrenemos los algoritmos. Por lo tanto, para poder hacer predicciones, incluso si no tenemos información distinta de cero sobre la persona, necesitamos un término de sesgo. El término de sesgo es necesario para hacer una red neuronal robusta.

Matemáticamente, en el paso 1, realizamos el siguiente cálculo:

$$
X.W = x1w1 + x2w2 + x3w3 + b
$$

Paso 2: (Pasar el resultado del paso 1 a través de una función de activación) {#paso2pasar el resultado del paso 1 a través de una función de activación}

El resultado del Paso 1 puede ser un conjunto de cualquier valor. Sin embargo, en nuestra salida tenemos los valores en forma de 1 y 0. Queremos que nuestra salida tenga el mismo formato. Para ello necesitamos una función de activación, que aplasta los valores de entrada entre 1 y 0. Una de estas funciones de activación es el [sigmoideo](https:// en.wikipedia.org/wiki/Sigmoid_function).

La función sigmoidea devuelve 0,5 cuando la entrada es 0. Devuelve un valor cercano a 1 si la entrada es un número positivo grande. En caso de entrada negativa, la función sigmoidea genera un valor cercano a cero.

Matemáticamente, la función sigmoidea se puede representar como:

$$
\theta_{X.W} = \frac{\mathrm{1} }{\mathrm{1} + e^{-X.W} }
$$

Tratemos de trazar la función sigmoidea:

1
2
3
4
5
6
7
input = np.linspace(-10, 10, 100)

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

from matplotlib import pyplot as plt
plt.plot(input, sigmoid(input), c="r")

En el script anterior, primero generamos aleatoriamente 100 puntos espaciados linealmente entre -10 y 10. Para hacerlo, usamos el método linspace de la [biblioteca numpy](/tutorial-numpy-una-guia-simple-basada -en-ejemplos/). A continuación, definimos la función sigmoide. Finalmente, usamos la biblioteca matplotlib para trazar los valores de entrada contra los valores devueltos por la función sigmoid. La salida se ve así:

función sigmoide{.img-responsive}

Puede ver que si la entrada es un número negativo, la salida está cerca de cero; de lo contrario, si la entrada es positiva, la salida está cerca de 1. Sin embargo, la salida siempre está entre 0 y 1. Esto es lo que queremos.

Esto resume la parte de avance de nuestra red neuronal. Es bastante sencillo. Primero tenemos que encontrar el producto punto de la matriz de características de entrada con la matriz de peso. A continuación, pase el resultado de la salida a través de una función de activación, que en este caso es la función sigmoidea. El resultado de la función de activación es básicamente la salida prevista para las características de entrada.

Propagación hacia atrás {#propagación hacia atrás}

Al principio, antes de realizar ningún entrenamiento, la red neuronal hace predicciones aleatorias que distan mucho de ser correctas.

El principio detrás del funcionamiento de una red neuronal es simple. Empezamos dejando que la red haga predicciones aleatorias sobre la salida. Luego comparamos la salida predicha de la red neuronal con la salida real. A continuación, ajustamos nuestros pesos y el sesgo de tal manera que nuestra salida prevista se acerque más a la salida real, lo que básicamente se conoce como "entrenamiento de la red neuronal".

En la sección de propagación hacia atrás, entrenamos nuestro algoritmo. Echemos un vistazo a los pasos involucrados en la sección de propagación hacia atrás.

Paso 1: (Calcular el costo)

El primer paso en la sección de propagación hacia atrás es encontrar el "costo" de las predicciones. El costo de la predicción se puede calcular simplemente encontrando la diferencia entre la salida pronosticada y la salida real. Cuanto mayor sea la diferencia, mayor será el costo.

Hay varias otras formas de encontrar el costo, pero usaremos la función de costo error medio cuadrado. Una función de costo es simplemente la función que encuentra el costo de las predicciones dadas.

La función de costo del error cuadrático medio 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.

Paso 2: (Minimizar el costo)

Nuestro objetivo final es afinar las perillas de nuestra red neuronal de tal manera que se minimice el costo. Si observa nuestra red neuronal, notará que solo podemos controlar los pesos y el sesgo. Todo lo demás está fuera de nuestro control. No podemos controlar las entradas, no podemos controlar los productos escalares y no podemos manipular la función sigmoidea.

Para minimizar el costo, necesitamos encontrar los valores de ponderación y sesgo para los cuales la función de costo arroja el menor valor posible. Cuanto menor sea el costo, más correctas serán nuestras predicciones.

Este es un [problema de optimización] (https://en.wikipedia.org/wiki/Optimization_problem) donde tenemos que encontrar la [función mínima] (https://en.wikipedia.org/wiki/Maxima_and_minima).

Para encontrar los mínimos de una función, podemos usar el algoritmo degradado 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) $$

Aquí, en la ecuación anterior, ‘J’ es la función de costo. Básicamente, lo que dice la ecuación anterior es: encuentre la derivada parcial de la función de costo con respecto a cada peso y sesgo y reste el resultado de los valores de peso existentes para obtener los nuevos valores de peso.

La derivada de una función nos da su pendiente en cualquier punto dado. Para encontrar si el costo aumenta o disminuye, dado el valor de peso, podemos encontrar la derivada de la función en ese valor de peso particular. Si el costo aumenta con el aumento de peso, el derivado devolverá un valor positivo que luego se restará del valor existente.

Por otro lado, si el costo está disminuyendo con un aumento de peso, se devolverá un valor negativo, que se sumará al valor de peso existente, ya que negativo en negativo es positivo.

En la Ecuación 1, podemos ver que hay un símbolo alfa, que se multiplica por el gradiente. Esto se llama la tasa de aprendizaje. La tasa de aprendizaje define qué tan rápido aprende nuestro algoritmo. Para obtener más detalles sobre cómo se puede definir la tasa de aprendizaje, consulte Este artículo .

Necesitamos repetir la ejecución de la Ecuación 1 para todos los pesos y sesgos hasta que el costo se minimice al nivel deseable. En otras palabras, debemos seguir ejecutando la Ecuación 1 hasta que obtengamos tales valores de sesgo y pesos, para los cuales la función de costo devuelve un valor cercano a cero.

Y eso es más o menos. Ahora es el momento de implementar lo que hemos estudiado hasta ahora. Crearemos una red neuronal simple con una entrada y una capa de salida en Python.

Implementación de redes neuronales en Python

Primero creemos nuestro conjunto de funciones y las etiquetas correspondientes. Ejecute el siguiente script:

1
2
3
4
import numpy as np
feature_set = np.array([[0,1,0],[0,0,1],[1,0,0],[1,1,0],[1,1,1]])
labels = np.array([[1,0,0,1,1]])
labels = labels.reshape(5,1)

En el script anterior, creamos nuestro conjunto de características. Contiene cinco registros. De manera similar, creamos un conjunto de etiquetas que contiene las etiquetas correspondientes para cada registro en el conjunto de funciones. Las etiquetas son las respuestas que intentamos predecir con la red neuronal.

El siguiente paso es definir hiperparámetros para nuestra red neuronal. Ejecute el siguiente script para hacerlo:

1
2
3
4
np.random.seed(42)
weights = np.random.rand(3,1)
bias = np.random.rand(1)
lr = 0.05

En el script anterior, usamos la función random.seed para que podamos obtener los mismos valores aleatorios cada vez que se ejecuta el script.

En el siguiente paso, inicializamos nuestros pesos con números aleatorios normalmente distribuidos. Como tenemos tres características en la entrada, tenemos un vector de tres pesos. Luego inicializamos el valor de sesgo con otro número aleatorio. Finalmente, fijamos la tasa de aprendizaje en 0,05.

A continuación, necesitamos definir nuestra función de activación y su derivada (explicaré en un momento por qué necesitamos encontrar la derivada de la activación). Nuestra función de activación es la función sigmoidea, que cubrimos anteriormente.

El siguiente script de Python crea esta función:

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

Y el método que calcula la derivada de la función sigmoidea se define de la siguiente manera:

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

La derivada de la función sigmoide es simplemente sigmoid(x) * sigmoid(1-x).

Ahora estamos listos para entrenar nuestra red neuronal que será capaz de predecir si una persona es obesa o no.

Mira el siguiente guión:

 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
for epoch in range(20000):
    inputs = feature_set

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

    #feedforward step2
    z = sigmoid(XW)


    # backpropagation step 1
    error = z - labels

    print(error.sum())

    # 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

No se deje intimidar por este código. Lo explicaré línea por línea.

En el primer paso, definimos el número de épocas. Una época es básicamente la cantidad de veces que queremos entrenar el algoritmo en nuestros datos. Entrenaremos el algoritmo en nuestros datos 20.000 veces. Probé este número y descubrí que el error se minimiza bastante después de 20,000 iteraciones. Puedes probar con otro número. El objetivo final es minimizar el error.

A continuación, almacenamos los valores de feature_set en la variable input. Luego ejecutamos la siguiente línea:

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

Aquí encontramos el producto punto de la entrada y el vector de peso y le agregamos un sesgo. Este es el paso 1 de la sección de avance.

En esta línea:

1
z = sigmoid(XW)

Pasamos el producto punto a través de la función de activación sigmoidea, como se explica en el paso 2 de la sección de avance. Esto completa la parte de avance de nuestro algoritmo.

Ahora es el momento de comenzar la retropropagación. La variable z contiene las salidas predichas. El primer paso de la retropropagación es encontrar el error. Lo hacemos en la siguiente línea:

1
error = z - labels

Luego imprimimos el error en la pantalla.

Ahora es el momento de ejecutar el Paso 2 de retropropagación, que es la esencia de este código.

Sabemos que nuestra función de costo es:

$$
MSE = \frac{\mathrm{1} }{\mathrm{n}} \sum\nolimits_{i=1}^{n} (predicho - observado)^{2}
$$

Necesitamos diferenciar esta función con respecto a cada peso. Usaremos la regla de la cadena de diferenciación para este fin. Supongamos que "d_cost" es la derivada de nuestra función de costo con respecto al peso "w", podemos usar la regla de la cadena para encontrar esta derivada, como se muestra a continuación:

$$ \frac {d\\_coste}{dw} = \frac {d\\_coste}{d\\_pred} \, \frac {d\\ _pred}{dz}, \frac {dz}{dw} $$

Aquí,

$$ \frac {d\_coste}{d\_pred} $$

se puede calcular como:

$$ 2 (predicho - observado) $$

Aquí, 2 es constante y, por lo tanto, puede ignorarse. Este es básicamente el error que ya calculamos. En el código, se puede ver la línea:

1
dcost_dpred = error # ........ (2)

A continuación tenemos que encontrar:

$$ \frac {d\\_pred}{dz} $$

Aquí "d_pred" es simplemente la función sigmoidea y la hemos diferenciado con respecto al producto punto de entrada "z". En el script, esto se define como:

1
dpred_dz = sigmoid_der(z) # ......... (3)

Finalmente, tenemos que encontrar:

$$ \frac{d\_z}{dw} $$

Lo sabemos:

$$ z = x1w1 + x2w2 + x3w3 + b $$

Por lo tanto, la derivada con respecto a cualquier peso es simplemente la entrada correspondiente. Por lo tanto, nuestra derivada final de la función de costo con respecto a cualquier peso es:

1
slope = input x dcost_dpred x dpred_dz

Echa un vistazo a las siguientes tres líneas:

1
2
3
z_delta = dcost_dpred * dpred_dz
inputs = feature_set.T
weights -= lr * np.dot(inputs, z_delta)

Aquí tenemos la variable z_delta, que contiene el producto de dcost_dpred y dpred_dz. En lugar de recorrer cada registro y multiplicar la entrada con el z_delta correspondiente, tomamos la transposición de la matriz de características de entrada y la multiplicamos por z_delta. Finalmente, multiplicamos la variable de tasa de aprendizaje lr con la derivada para aumentar la velocidad de convergencia.

Luego recorrimos cada valor derivado y actualizamos nuestros valores de sesgo, como se muestra en este script:

Una vez que comience el ciclo, verá que el error total comienza a disminuir como se muestra a continuación:

 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
0.001700995120272485
0.001700910187124885
0.0017008252625468727
0.0017007403465365955
0.00170065543909367
0.0017005705402162556
0.0017004856499031988
0.0017004007681529695
0.0017003158949647542
0.0017002310303364868
0.0017001461742678046
0.0017000613267565308
0.0016999764878018585
0.0016998916574025129
0.00169980683555691
0.0016997220222637836
0.0016996372175222992
0.0016995524213307602
0.0016994676336875778
0.0016993828545920908
0.0016992980840424554
0.0016992133220379794
0.0016991285685766487
0.0016990438236577712
0.0016989590872797753
0.0016988743594415108
0.0016987896401412066
0.0016987049293782815

Puede ver que el error es extremadamente pequeño al final del entrenamiento de nuestra red neuronal. En este momento, nuestros pesos y sesgos tendrán valores que se pueden usar para detectar si una persona es diabética o no, en función de sus hábitos de tabaquismo, obesidad y hábitos de ejercicio.

Ahora puede probar y predecir el valor de una sola instancia. Supongamos que tenemos un registro de un paciente que ingresa que fuma, no es obeso y no hace ejercicio. Averigüemos si es probable que sea diabético o no. La característica de entrada se verá así: [1,0,0].

Ejecute el siguiente script:

1
2
3
single_point = np.array([1,0,0])
result = sigmoid(np.dot(single_point, weights) + bias)
print(result)

En la salida verás:

1
[0.00707584]

Puede ver que es probable que la persona no sea diabética ya que el valor está mucho más cerca de 0 que de 1.

Ahora probemos a otra persona que no fuma, es obesa y no hace ejercicio. El vector de características de entrada será [0,1,0]. Ejecute este script:

1
2
3
single_point = np.array([0,1,0])
result = sigmoid(np.dot(single_point, weights) + bias)
print(result)

En la salida verá el siguiente valor:

1
[0.99837029]

Puede ver que el valor es muy cercano a 1, lo que probablemente se deba a la obesidad de la persona.

Recursos

¿Quiere aprender más sobre la creación de redes neuronales para resolver problemas complejos? Si es así, intente consultar otros recursos, como este curso en línea:

Aprendizaje profundo de la A a la Z: redes neuronales artificiales prácticas

Cubre las redes neuronales con mucho más detalle, incluidas las redes neuronales convolucionales, las redes neuronales recurrentes y mucho más.

Conclusión

En este artículo, creamos una red neuronal muy simple con una entrada y una capa de salida desde cero en Python. Tal red neuronal simplemente se llama [perceptrón] (https://en.wikipedia.org/wiki/Perceptron). Un perceptrón es capaz de clasificar datos linealmente separables. Los datos separables linealmente son el tipo de datos que se pueden separar mediante un hiperplano en un espacio n-dimensional.

Las redes neuronales artificiales de palabra real son mucho más complejas, poderosas y consisten en múltiples capas ocultas y múltiples nodos en la capa oculta. Tales redes neuronales pueden identificar límites de decisión reales no lineales. Explicaré cómo crear una red neuronal multicapa desde cero en Python en un próximo artículo.