Breaking

Post Top Ad

Your Ad Spot

martes, 9 de julio de 2019

Creando 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".
Si no tiene experiencia previa con redes neuronales, le sugiero que lea primero la Parte 1 y la Parte 2 de la serie (enlace arriba). Una vez que se sienta cómodo con los conceptos explicados en esos artículos, puede regresar y continuar este artículo.

Introducción

En el artículo anterior , vimos cómo podemos crear una red neuronal desde cero, que es capaz de resolver problemas de clasificación binaria, 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 del 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 en el que 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 de múltiples clases.

Conjunto de datos

Primero echemos un vistazo a nuestro conjunto de datos. Nuestro conjunto de datos tendrá dos características de entrada y una de las tres salidas posibles. Crearemos manualmente un conjunto de datos para este artículo.
Para ello, ejecuta el siguiente script:
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. Puedes 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 cat_imagesmatriz en un plano bidimensional, se centrarán alrededor de x = 0 e y = -3. De manera similar, los elementos de la mouse_imagesmatriz estarán centrados alrededor de x = 3 e y = 3, y finalmente, los elementos de la matriz dog_imagesestarán centrados alrededor de x = -3 e y = 3. Verás esto una vez que dibujemos 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:
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:
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 abreviada 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 predice una salida, el valor del nodo correspondiente debería ser 1, mientras que los nodos restantes deberían tener un valor de 0. Para eso, necesitamos tres valores para la etiqueta de salida para cada registro. Esta es la razón por la que convertimos nuestro vector de salida en un vector codificado en caliente.
Ejecute la siguiente secuencia de comandos para crear la matriz de vectores codificados para nuestro conjunto de datos:
one_hot_labels = np.zeros((2100, 3))

for i in range(2100):  
    one_hot_labels[i, labels[i]] = 1
En la secuencia de comandos anterior creamos la one_hot_labelsmatriz de tamaño 2100 x 3 donde cada fila contiene un vector codificado en caliente para el registro correspondiente en el conjunto de características. Luego insertamos 1 en la columna correspondiente.
Si ejecuta el script anterior, verá que la one_hot_labelsmatriz tendrá 1 en el índice 0 para los primeros 700 registros, 1 en el índice 1 para los próximos 700 registros, mientras que 1 en el índice 2 para los últimos 700 registros.
Ahora vamos a trazar el conjunto de datos que acabamos de crear. Ejecuta el siguiente script:
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 generados
Se 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 mencionadas.

Red neuronal con múltiples clases de salida

La red neuronal que vamos a diseñar tiene la siguiente arquitectura:
Estructura de la red neuronal
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 de múltiples clases 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ódigos, revisemosbrevemente las funciones de softmax y entropía cruzada, que son respectivamente las funciones de activación y pérdida más utilizadas para crear una red neuronal para la clasificación de múltiples clases.
Función softmax
Desde 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 sigmoide como hicimos en los artículos anteriores.
Sin embargo, hay 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 que la 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 se suman a 1. Matemáticamente, la función softmax se puede representar como:
yyo(zyo)=mizyok=1kmizk
La función softmax simplemente divide el exponente de cada elemento de entrada por la suma de exponentes de todos los elementos de entrada. Echemos un vistazo a un ejemplo simple de esto:
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 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 comprimidos entre 0 y 1, donde la suma de los números será igual a 1. La salida tiene el siguiente aspecto:
[0.09003057 0.24472847 0.66524096]
La función de activación de Softmax tiene dos ventajas principales sobre las otras funciones de activación, en particular para los problemas de clasificación de múltiples clases: la primera ventaja es que la función de 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 de forma instantánea, lo que significa que nuestra salida tendrá valores entre 0 y 1. Sin embargo, la salida del proceso de avance puede ser mayor que 1, por lo que 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
Con la función de activación de softmax en la capa de salida, la función de costo de error cuadrático medio 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 tiene este aspecto:
H(y,y^)=-yoyyoIniciar sesiónyyo^
La entropía cruzada es simplemente la suma de los productos de todas las probabilidades reales con el registro negativo de las probabilidades predichas. Para los 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 el conocimiento suficiente para crear una red neuronal que resuelva problemas de clasificación de clases múltiples. Veamos cómo funcionará nuestra red neuronal.
Como siempre, una red neuronal se ejecuta en dos pasos: Feed-forward y back-propagation.

Feed Forward

La fase de avance será 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 de softmax en la capa de salida en lugar de la función sigmoide.
Recuerde, para la salida de capa oculta seguiremos usando la función sigmoide como lo hicimos anteriormente. La función softmax se usará 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 características "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 capa oculta para los cuales estamos calculando el valor. Aviso, también estamos agregando un término de sesgo aquí. Luego pasamos el producto punto a través de la función de activación sigmoide para obtener el valor final.
Por ejemplo, para calcular el valor final del primer nodo en la capa oculta, que se denota con "ah1", debe realizar el siguiente cálculo:
zh1=X1w1+X2w2+segundo
unah1=11+mi-zh1
Este es el valor resultante para el nodo superior de la capa oculta. De la misma manera, puede calcular los valores de los nodos 2, 3 y 4 de la capa oculta.
Fase 2
Para calcular los valores 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 capa ocultos con sus pesos correspondientes y pase el resultado a través de una función de activación, que en este caso será softmax.
Esta operación puede expresarse matemáticamente por la siguiente ecuación:
zo1=unah1w9+unah2w10+unah3w11+unah4w12
zo2=unah1w13+unah2w14+unah3w15+unah4wdieciséis
zo3=unah1w17+unah2w18+unah3w19+unah4w20
Aquí zo1, zo2 y zo3 formarán el vector que usaremos como entrada para la función sigmoide. Permite nombrar este vector "zo".
zo = [zo1, zo2, zo3]  
Ahora, para encontrar el valor de salida a01, podemos usar la función softmax de la siguiente manera:
unao1(zo)=mizo1k=1kmizok
Aquí "a01" es la salida para el nodo más alto en la capa de salida. De la misma manera, puede utilizar la función softmax para calcular los valores para ao2 y ao3.
Puede ver que el paso de avance para una red neuronal con salida de múltiples clases 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

La idea básica detrás de la propagación hacia atrás sigue siendo la misma. Tenemos que definir una función de costo y luego optimizar esa función de costo actualizando las ponderaciones de tal manera que se minimice el costo. Sin embargo, a diferencia de los artículos anteriores en los que utilizamos el error cuadrático medio como función de costo, en este artículo usaremos la función de entropía cruzada.
La propagación hacia atrás es un problema de optimización en el que 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 gradiente decente . El algoritmo de gradiente decente se puede representar matemáticamente de la siguiente manera:
rmipagmiunat tunortetyol doonortevmirsolminortedomi:{wj: =wj-αwjJ(w0,w1.......wnorte)}.............(1)
Los detalles sobre cómo la función de degradado decente minimiza el costo ya se han analizado en el artículo anterior. Aquí solo veremos las operaciones matemáticas que necesitamos realizar.
Nuestra función de costo es:
H(y,y^)=-yoyyoIniciar sesiónyyo^
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 pronosticadas 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 debemos minimizar esta función:
doost(y,unao)=-yoyyoIniciar sesiónunaoyo
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.
Por el 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, debemos tomar el derivado de la función de costo con respecto a cada peso. Matemáticamente podemos representarlo como:
redoostrewo=redoostreunao,reunaorezorezorewo.....(1)
Aquí "wo" se refiere a los pesos en la capa de salida.
La primera parte de la ecuación se puede representar como:
redoostreunao reunaorezo.......(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:
redoostreunao reunaorezo=unao-y.......(3)
Donde "ao" es el resultado predicho mientras que "y" es el resultado real.
Finalmente, necesitamos encontrar "dzo" con respecto a "dwo" de la Ecuación 1 . La derivada es simplemente las salidas que provienen de la capa oculta como se muestra a continuación:
rezorewo=unah
Para encontrar nuevos valores de peso, los valores devueltos por la Ecuación 1 pueden multiplicarse simplemente con la velocidad 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:
redoostresegundoo=redoostreunao reunaorezorezoresegundoo.....(4)
La primera parte de la Ecuación 4 ya se ha calculado en la Ecuación 3 . Aquí solo necesitamos actualizar "dzo" con respecto a "bo", que es simplemente 1. Entonces:
redoostresegundoo=unao-y...........(5)
Para encontrar nuevos valores de sesgo para la capa de salida, los valores devueltos por la Ecuación 5 pueden multiplicarse simplemente con la tasa de aprendizaje y restarse del valor de sesgo actual.
Fase 2
En esta sección, propagaremos de nuevo nuestro error a la capa anterior y encontraremos los nuevos valores de peso para las capas ocultas, es decir, las ponderaciones w1 a w8.
Denotemos colectivamente los pesos de capa ocultos 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 representarla como:
redoostrewh=redoostreunah,reunahrezhrezhrewh......(6)
Aquí nuevamente, dividiremos la Ecuación 6 en términos individuales.
El primer término "dcost" se puede diferenciar con respecto a "dah" utilizando la regla de diferenciación en cadena de la siguiente manera:
redoostreunah=redoostrezo rezoreunah......(7)
Volvamos a romper la ecuación 7 en términos individuales. De la ecuación 3 , sabemos que:
redoostreunao reunaorezo=redoostrezo==unao-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:
rezoreunah=wo......(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:
reunahrezh=syosolmetrooyore(zh)(1-syosolmetrooyore(zh))........(10)
Y finalmente, dzh / dwh son simplemente los valores de entrada:
rezhrewh=yonortepagtutFmiunatturmis........(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 capa ocultos. Para encontrar nuevos valores de peso para los pesos de capa ocultos "wh", los valores devueltos por la Ecuación 6 pueden multiplicarse simplemente por la velocidad de aprendizaje y restarse de los valores actuales de peso de capa oculta.
De manera similar, la derivada de la función de costo con respecto al sesgo de la capa oculta "bh" se puede calcular simplemente como:
redoostresegundoh=redoostreunah,reunahrezhrezhresegundoh......(12)
Que es simplemente igual a:
redoostresegundoh=redoostreunah,reunahrezh......(13)
porque,
rezhresegundoh=1
Para encontrar nuevos valores de sesgo para la capa oculta, los valores devueltos por la Ecuación 13 pueden multiplicarse simplemente con la tasa de aprendizaje y restarse de los valores actuales de sesgo de capa oculta y eso es todo para la propagación hacia atrás.
Puedes ver que el proceso de feed-forward y back-propagation 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 de múltiples clases

Hemos cubierto la teoría detrás de la red neuronal para la clasificación de múltiples clases, y ahora es el momento de poner esa teoría en práctica.
Echa un vistazo a la siguiente secuencia de comandos:
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 avance, la única diferencia es que "ao", que es la salida final, se calcula utilizando la softmaxfunción.
De manera similar, en la sección de propagación hacia atrás, para encontrar los nuevos pesos para la capa de salida, la función de costo se deriva con respecto a la softmaxfunción en lugar de la sigmoidfunción.
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.
Costo vs épocas
Como puede ver, no se necesitan muchas épocas para alcanzar nuestro costo final de error.
De manera similar, si ejecuta la misma secuencia de comandos con la función sigmoide en la capa de salida, el costo mínimo de error que logrará después de 50000 épocas será de alrededor de 1.5, que es mayor que 0.5, alcanzado con softmax.

Conclusión

Las redes neuronales del mundo real son capaces de resolver problemas de clasificación de múltiples clases. En este artículo, vimos cómo podemos crear una red neuronal muy simple para la clasificación de múltiples clases, 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.

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Post Top Ad

Your Ad Spot

Páginas