Creación de una red neuronal desde cero en Python: clasificación multiclase

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

Si no tiene experiencia previa con redes neuronales, le sugiero que primero lea la Parte 1 y la Parte 2 de la serie (enlace anterior). Una vez que se sienta cómodo con los conceptos explicados en esos artículos, puede regresar y continuar con este artículo.

Introducción

En el Artículo anterior, vimos cómo podemos crear una red neuronal desde cero, que sea capaz de resolver binarios problemas de clasificación, en Python. Un problema de clasificación binaria tiene solo dos salidas. Sin embargo, los problemas del mundo real son mucho más complejos.

Considere el ejemplo del problema de reconocimiento de dígitos donde usamos la imagen de un dígito como entrada y el clasificador predice el número de dígito correspondiente. Un dígito puede ser cualquier número entre 0 y 9. Este es un ejemplo clásico de un problema de clasificación de clases múltiples donde la entrada puede pertenecer a cualquiera de las 10 salidas posibles.

En este artículo, veremos cómo podemos crear una red neuronal simple desde cero en Python, que es capaz de resolver problemas de clasificación multiclase.

Conjunto de datos

Primero echemos un vistazo breve a nuestro conjunto de datos. Nuestro conjunto de datos tendrá dos características de entrada y una de las tres posibles salidas. Crearemos manualmente un conjunto de datos para este artículo.

Para hacerlo, ejecute el siguiente script:

1
2
3
4
5
6
7
8
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

cat_images = np.random.randn(700, 2) + np.array([0, -3])
mouse_images = np.random.randn(700, 2) + np.array([3, 3])
dog_images = np.random.randn(700, 2) + np.array([-3, 3])

En el script anterior, comenzamos importando nuestras bibliotecas y luego creamos tres matrices bidimensionales de tamaño 700 x 2. Puede pensar en cada elemento de un conjunto de la matriz como una imagen de un animal en particular. Cada elemento de la matriz corresponde a una de las tres clases de salida.

Un punto importante a tener en cuenta aquí es que, si trazamos los elementos de la matriz cat_images en un plano bidimensional, estarán centrados alrededor de x=0 y y=-3. De manera similar, los elementos de la matriz mouse_images se centrarán alrededor de x=3 y y=3, y finalmente, los elementos de la matriz dog_images se centrarán alrededor de x=-3 y y=3. Verá esto una vez que tracemos nuestro conjunto de datos.

A continuación, debemos unir verticalmente estas matrices para crear nuestro conjunto de datos final. Ejecute el siguiente script para hacerlo:

1
feature_set = np.vstack([cat_images, mouse_images, dog_images])

Creamos nuestro conjunto de características y ahora necesitamos definir las etiquetas correspondientes para cada registro en nuestro conjunto de características. El siguiente script hace eso:

1
labels = np.array([0]*700 + [1]*700 + [2]*700)

El script anterior crea una matriz unidimensional de 2100 elementos. Los primeros 700 elementos se han etiquetado como 0, los siguientes 700 elementos se han etiquetado como 1, mientras que los últimos 700 elementos se han etiquetado como 2. Esta es solo nuestra forma rápida de crear rápidamente las etiquetas para nuestros datos correspondientes.

Para los problemas de clasificación de clases múltiples, debemos definir la etiqueta de salida como un vector codificado en caliente, ya que nuestra capa de salida tendrá tres nodos y cada nodo corresponderá a una clase de salida. Queremos que cuando se prediga una salida, el valor del nodo correspondiente sea 1 mientras que los nodos restantes tengan un valor de 0. Para eso, necesitamos tres valores para la etiqueta de salida para cada registro. Es por eso que convertimos nuestro vector de salida en un vector codificado one-hot.

Ejecute el siguiente script para crear la matriz de vectores codificada en caliente para nuestro conjunto de datos:

1
2
3
4
one_hot_labels = np.zeros((2100, 3))

for i in range(2100):
    one_hot_labels[i, labels[i]] = 1

En el script anterior, creamos la matriz one_hot_labels de tamaño 2100 x 3, donde cada fila contiene un vector codificado one-hot para el registro correspondiente en el conjunto de funciones. Luego insertamos 1 en la columna correspondiente.

Si ejecuta el script anterior, verá que la matriz one_hot_labels tendrá 1 en el índice 0 para los primeros 700 registros, 1 en el índice 1 para los siguientes 700 registros y 1 en el índice 2 para los últimos 700 registros.

Ahora vamos a trazar el conjunto de datos que acabamos de crear. Ejecute el siguiente script:

1
2
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5)
plt.show()

Una vez que ejecute el script anterior, debería ver la siguiente figura:

Conjunto de datos generado{.img-responsive}

Puede ver claramente que tenemos elementos que pertenecen a tres clases diferentes. Nuestra tarea será desarrollar una red neuronal capaz de clasificar los datos en las clases antes mencionadas.

Red neuronal con varias clases de salida

La red neuronal que vamos a diseñar tiene la siguiente arquitectura:

Estructura de red neuronal{.img-responsive}

Puedes ver que nuestra red neuronal es bastante similar a la que desarrollamos en la Parte 2 de la serie. Tiene una capa de entrada con 2 entidades de entrada y una capa oculta con 4 nodos. Sin embargo, en la capa de salida, podemos ver que tenemos tres nodos. Esto significa que nuestra red neuronal es capaz de resolver el problema de clasificación multiclase donde el número de salidas posibles es 3.

Softmax y funciones de entropía cruzada

Antes de pasar a la sección de código, repasemos brevemente softmax y [entropía cruzada](https://en.wikipedia.org/wiki/ Cross_entropy), que son, respectivamente, las funciones de activación y pérdida más utilizadas para crear una red neuronal para la clasificación de clases múltiples.

Función Softmax

De la arquitectura de nuestra red neuronal, podemos ver que tenemos tres nodos en la capa de salida. Tenemos varias opciones para la función de activación en la capa de salida. Una opción es usar la función sigmoidea como hicimos en los artículos anteriores.

Sin embargo, existe una función de activación más conveniente en forma de softmax que toma un vector como entrada y produce otro vector de la misma longitud como salida. Dado que nuestra salida contiene tres nodos, podemos considerar la salida de cada nodo como un elemento del vector de entrada. La salida será una longitud del mismo vector donde los valores de todos los elementos suman 1. Matemáticamente, la función softmax se puede representar como:

$$ y_i(z_i) = \frac{e^{z_i}}{ \sum\nolimits_{k=1}^{k}{e^{z_k}} } $ ps

La función softmax simplemente divide el exponente de cada elemento de entrada por la suma de los exponentes de todos los elementos de entrada. Echemos un vistazo a un ejemplo simple de esto:

1
2
3
4
5
6
def softmax(A):
    expA = np.exp(A)
    return expA / expA.sum()

nums = np.array([4, 5, 6])
print(softmax(nums))

En el script anterior creamos una función softmax que toma un solo vector como entrada, toma exponentes de todos los elementos en el vector y luego divide los números resultantes individualmente por la suma de los exponentes de todos los números en el vector de entrada.

Puede ver que el vector de entrada contiene los elementos 4, 5 y 6. En la salida, verá tres números aplastados entre 0 y 1 donde la suma de los números será igual a 1. La salida se ve así:

1
[0.09003057 0.24472847 0.66524096]

La función de activación Softmax tiene dos ventajas principales sobre las otras funciones de activación, en particular para problemas de clasificación de clases múltiples: la primera ventaja es que la función softmax toma un vector como entrada y la segunda ventaja es que produce una salida entre 0 y 1. Recuerde, en nuestro conjunto de datos, tenemos etiquetas de salida codificadas en caliente, lo que significa que nuestra salida tendrá valores entre 0 y 1. Sin embargo, la salida del proceso feedforward puede ser mayor que 1, por lo tanto, la función softmax es la opción ideal en la capa de salida ya que aplasta la salida entre 0 y 1.

Función de entropía cruzada {#función de entropía cruzada}

Con la función de activación softmax en la capa de salida, la función de costo error medio cuadrado se puede usar para optimizar el costo como lo hicimos en los artículos anteriores. Sin embargo, para la función softmax, existe una función de costo más conveniente que se llama entropía cruzada.

Matemáticamente, la función de entropía cruzada se ve así:

$$ H(y,\hat{y}) = -\sum_i y_i \log \hat{y_i} $$

La entropía cruzada es simplemente la suma de los productos de todas las probabilidades reales con el logaritmo negativo de las probabilidades predichas. Para problemas de clasificación de clases múltiples, se sabe que la función de entropía cruzada supera a la función de gradiente decente.

Ahora tenemos suficiente conocimiento para crear una red neuronal que resuelva problemas de clasificación multiclase. Veamos cómo funcionará nuestra red neuronal.

Como siempre, una red neuronal se ejecuta en dos pasos: avance y propagación hacia atrás.

Feed Forward

La fase de feedforward seguirá siendo más o menos similar a lo que vimos en el artículo anterior. La única diferencia es que ahora usaremos la función de activación softmax en la capa de salida en lugar de la función sigmoide.

Recuerde, para la salida de la capa oculta seguiremos usando la función sigmoide como lo hicimos anteriormente. La función softmax se utilizará solo para las activaciones de la capa de salida.

Fase 1

Dado que estamos utilizando dos funciones de activación diferentes para la capa oculta y la capa de salida, he dividido la fase de avance en dos subfases.

En la primera fase, veremos cómo calcular la salida de la capa oculta. Para cada registro de entrada, tenemos dos funciones "x1" y "x2". Para calcular los valores de salida para cada nodo en la capa oculta, debemos multiplicar la entrada con los pesos correspondientes del nodo de la capa oculta para el que estamos calculando el valor. Tenga en cuenta que también estamos agregando un término de sesgo aquí. Luego pasamos el producto escalar a través de la función de activación sigmoidea 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 + b
$$

$$
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.

Fase 2

Para calcular los valores de la capa de salida, los valores de los nodos de la capa oculta se tratan como entradas. Por tanto, para calcular la salida, multiplica los valores de los nodos de la capa oculta por sus correspondientes pesos y pasa el resultado por una función de activación, que en este caso será softmax.

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

$$
zo1 = ah1w9 + ah2w10 + ah3w11 + ah4w12
$$

$$
zo2 = ah1w13 + ah2w14 + ah3w15 + ah4w16 $$

$$
zo3 = ah1w17 + ah2w18 + ah3w19 + ah4w20 $$

Aquí, zo1, zo2 y zo3 formarán el vector que usaremos como entrada para la función sigmoidea. Llamemos a este vector "zo".

1
zo = [zo1, zo2, zo3]

Ahora, para encontrar el valor de salida a01, podemos usar la función softmax de la siguiente manera:

$$
ao1(zo) = \frac{e^{zo1}}{ \sum\nolimits_{k=1}^{k}{e^{zok}} }
$$

Aquí "a01" es la salida para el nodo superior en la capa de salida. De la misma manera, puede utilizar la función softmax para calcular los valores de ao2 y ao3.

Puede ver que el paso de avance de una red neuronal con salida multiclase es bastante similar al paso de avance de la red neuronal para problemas de clasificación binaria. La única diferencia es que aquí estamos usando la función softmax en la capa de salida en lugar de la función sigmoide.

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

La idea básica detrás de la retropropagación sigue siendo la misma. Tenemos que definir una función de costo y luego optimizar esa función de costo actualizando los pesos de modo que el costo se minimice. Sin embargo, a diferencia de artículos anteriores en los que usamos error medio cuadrado como función de costo, en este artículo usaremos la función de entropía cruzada.

La retropropagación es un problema de optimización (https://en.wikipedia.org/wiki/optimization_problem) donde tenemos que encontrar la función mínima para nuestro costo función.

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:

$$ H(y,\hat{y}) = -\sum_i y_i \log \hat{y_i} $$

En nuestra red neuronal, tenemos un vector de salida donde cada elemento del vector corresponde a la salida de un nodo en la capa de salida. El vector de salida se calcula utilizando la función softmax. Si "ao" es el vector de las salidas predichas de todos los nodos de salida y "y" es el vector de las salidas reales de los nodos correspondientes en el vector de salida, básicamente tenemos que minimizar esta función:

$$ costo(y, {ao}) = -\sum_i y_i \log {ao_i} $$

Fase 1

En la primera fase, necesitamos actualizar los pesos w9 hasta w20. Estos son los pesos de los nodos de la capa de salida.

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 tomar la derivada de la función de costo con respecto a cada peso. Matemáticamente lo podemos representar como:

$$
\frac {dcost}{trabajo} = \frac {dcost}{muerte} *, \frac {muerte}{profundidad} * \frac {defensa}{defensa} .…. (1 )
$$

Aquí "wo" se refiere a los pesos en la capa de salida.

La primera parte de la ecuación se puede representar como:

$$
\frac {dcost}{dio} *\ \frac {dio}{dzo} ....… (2)
$$

La derivación detallada de la función de pérdida de entropía cruzada con la función de activación de softmax se puede encontrar en este enlace.

La derivada de la ecuación (2) es:

$$
\frac {dcost}{dao} *\ \frac {dao}{dzo} = ao - y ....… (3)
$$

Donde "ao" es la salida prevista mientras que "y" es la salida real.

Finalmente, necesitamos encontrar "dzo" con respecto a "dwo" de la Ecuación 1. La derivada son simplemente las salidas provenientes de la capa oculta como se muestra a continuación:

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

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.

También necesitamos actualizar el sesgo "bo" para la capa de salida. Necesitamos diferenciar nuestra función de costo con respecto al sesgo para obtener un nuevo valor de sesgo como se muestra a continuación:

$$
\frac {dcoste}{dbo} = \frac {dcoste}{dao} *\ \frac {dao}{dzo} * \frac {dzo}{dbo} .…. ( 4)
$$

La primera parte de la Ecuación 4 ya ha sido calculada en la Ecuación 3. Aquí solo necesitamos actualizar "dzo" con respecto a "bo", que es simplemente 1. Entonces:

$$
\frac {dcost}{dbo} = ao - y .......…. (5)
$$

Para encontrar nuevos valores de sesgo para la capa de salida, los valores devueltos por la Ecuación 5 pueden simplemente multiplicarse por la tasa de aprendizaje y restarse del valor de sesgo actual.

Fase 2

En esta sección, propagaremos hacia atrás nuestro error a la capa anterior y encontraremos los nuevos valores de peso para los pesos de capa oculta, es decir, los 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} ...... (6)
$$

Aquí nuevamente, dividiremos la Ecuación 6 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} ...... (7)
$$

Dividamos nuevamente la Ecuación 7 en términos individuales. De la Ecuación 3, sabemos que:

$$
\frac {dcoste}{dao} *\ \frac {dao}{dzo} =\frac {dcoste}{dzo} = = ao - y ....…. (8)
$$

Ahora necesitamos encontrar dzo/dah de la Ecuación 7, que es igual a los pesos de la capa de salida como se muestra a continuación:

$$
\frac {dzo}{dah} = donde ...... (9)
$$

Ahora podemos encontrar el valor de dcost/dah reemplazando los valores de las Ecuaciones 8 y 9 en la Ecuación 7.

Volviendo a la Ecuación 6, 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)) ....…. (10)
$$

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

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

Si reemplazamos los valores de las Ecuaciones 7, 10 y 11 en la Ecuación 6, 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 6 pueden simplemente multiplicarse por la tasa de aprendizaje y restarse de los valores de ponderación de capas ocultas actuales.

De manera similar, la derivada de la función de costo con respecto al sesgo de capa oculta "bh" se puede calcular simplemente como:

$$
\frac {dcost}{dbh} = \frac {dcost}{dah} *, \frac {dah}{dzh} * \frac {dzh}{dbh} ...... (12)
$$

Que es simplemente igual a:

$$
\frac {dcost}{dbh} = \frac {dcost}{dah} *, \frac {dah}{dzh} ...... (13)
$$

porque,

$$
\frac{dzh}{dbh} = 1
$$

Para encontrar nuevos valores de sesgo para la capa oculta, los valores devueltos por la Ecuación 13 pueden simplemente multiplicarse por la tasa de aprendizaje y restarse de los valores de sesgo de la capa oculta actual y eso es todo para la propagación hacia atrás.

Puede ver que el proceso de propagación hacia adelante y hacia atrás es bastante similar al que vimos en nuestros últimos artículos. Lo único que cambiamos es la función de activación y la función de costo.

Código para redes neuronales para clasificación multiclase

Hemos cubierto la teoría detrás de la red neuronal para la clasificación de clases múltiples, y ahora es el momento de poner esa teoría en práctica.

Echa 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

cat_images = np.random.randn(700, 2) + np.array([0, -3])
mouse_images = np.random.randn(700, 2) + np.array([3, 3])
dog_images = np.random.randn(700, 2) + np.array([-3, 3])

feature_set = np.vstack([cat_images, mouse_images, dog_images])

labels = np.array([0]*700 + [1]*700 + [2]*700)

one_hot_labels = np.zeros((2100, 3))

for i in range(2100):
    one_hot_labels[i, labels[i]] = 1

plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap='plasma', s=100, alpha=0.5)
plt.show()

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

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

def softmax(A):
    expA = np.exp(A)
    return expA / expA.sum(axis=1, keepdims=True)

instances = feature_set.shape[0]
attributes = feature_set.shape[1]
hidden_nodes = 4
output_labels = 3

wh = np.random.rand(attributes,hidden_nodes)
bh = np.random.randn(hidden_nodes)

wo = np.random.rand(hidden_nodes,output_labels)
bo = np.random.randn(output_labels)
lr = 10e-4

error_cost = []

for epoch in range(50000):
############# feedforward

    # Phase 1
    zh = np.dot(feature_set, wh) + bh
    ah = sigmoid(zh)

    # Phase 2
    zo = np.dot(ah, wo) + bo
    ao = softmax(zo)

########## Back Propagation

########## Phase 1

    dcost_dzo = ao - one_hot_labels
    dzo_dwo = ah

    dcost_wo = np.dot(dzo_dwo.T, dcost_dzo)

    dcost_bo = dcost_dzo

########## Phases 2

    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)

    dcost_bh = dcost_dah * dah_dzh

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

    wh -= lr * dcost_wh
    bh -= lr * dcost_bh.sum(axis=0)

    wo -= lr * dcost_wo
    bo -= lr * dcost_bo.sum(axis=0)

    if epoch % 200 == 0:
        loss = np.sum(-one_hot_labels * np.log(ao))
        print('Loss function value: ', loss)
        error_cost.append(loss)

El código es bastante similar al que creamos en el artículo anterior. En la sección de feed-forward, la única diferencia es que "ao", que es el resultado final, se calcula usando la función softmax.

De manera similar, en la sección de retropropagación, para encontrar los nuevos pesos para la capa de salida, la función de costo se deriva con respecto a la función ‘softmax’ en lugar de la función ‘sigmoide’.

Si ejecuta el script anterior, verá que el costo final del error será 0.5. La siguiente figura muestra cómo el costo disminuye con el número de épocas.

Coste vs épocas{.img-responsive}

Como puede ver, no se necesitan muchas épocas para alcanzar nuestro costo de error final.

De manera similar, si ejecuta el mismo script con la función sigmoid en la capa de salida, el costo de error mínimo que logrará después de 50000 épocas será de alrededor de 1,5, que es mayor que 0,5, logrado con softmax.

Conclusión

Las redes neuronales del mundo real son capaces de resolver problemas de clasificación de clases múltiples. En este artículo, vimos cómo podemos crear una red neuronal muy simple para la clasificación de clases múltiples, desde cero en Python. Este es el artículo final de la serie: "Red neuronal desde cero en Python". En los próximos artículos, explicaré cómo podemos crear redes neuronales más especializadas, como redes neuronales recurrentes y redes neuronales convolucionales desde cero en Python.