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

    • Creación de una red neuronal desde cero en Python
    • Crear una red neuronal desde cero en Python: agregar capas ocultas
    • Creación de una red neuronal desde cero en Python: clasificación de clases múltiples

    Si no tiene experiencia previa con redes neuronales, le sugiero que lea primero la Parte 1 y la Parte 2 de la serie (enlazadas 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 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 brevemente 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:

    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 en 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 graficamos los elementos de la cat_imagesmatriz en un plano bidimensional, estarán centrados alrededor de x = 0 e y = -3. De manera similar, los elementos de la mouse_imagesmatriz se centrarán alrededor de x = 3 e y = 3, y finalmente, los elementos de la matriz dog_imagesse centrarán alrededor de x = -3 e y = 3. Verá esto una vez que tracemos nuestro conjunto de datos.

    A continuación, necesitamos 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 de acceso directo para crear rápidamente las etiquetas para nuestros datos correspondientes.

    Para problemas de clasificación de clases múltiples, necesitamos definir la etiqueta de salida como un vector codificado en caliente ya que nuestra capa de salida tendrá tres nodes y cada node corresponderá a una clase de salida. Queremos que cuando se predice una salida, el valor del node correspondiente debe ser 1 mientras que los nodes restantes deben tener 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 en caliente.

    Ejecute la siguiente secuencia de comandos para crear la matriz de vectores codificada en caliente 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 el script 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 y 1 en el índice 2 para los últimos 700 registros.

    Ahora tracemos el conjunto de datos que acabamos de crear. Ejecute 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:

    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:

    Puede 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 nodes. Sin embargo, en la capa de salida, podemos ver que tenemos tres nodes. Esto significa que nuestra red neuronal es capaz de resolver el problema de clasificación de clases múltiples donde el número de salidas posibles es 3.

    Funciones Softmax y Cross-Entropy

    Antes de pasar a la sección de código, repasemos brevemente las funciones softmax y de entropía cruzada , que son, respectivamente, las funciones de activación y pérdida más comúnmente 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 nodes en la capa de salida. Tenemos varias opciones para la función de activación en la capa de salida. Una opción es utilizar la función sigmoidea 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 nodes, podemos considerar la salida de cada node 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}} {sumnolimits_ {k = 1} ^ {k} {e ^ {z_k}}}
    $$

    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 se ve así:

    [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 de alimentación anticipada 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 softmax en la capa de salida, la función de costo de error cuadrático medio se puede utilizar para optimizar el costo como 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 decente de gradiente.

    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: retroalimentación y retropropagación.

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

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

    Fase 1

    Dado que estamos usando 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 node en la capa oculta, tenemos que multiplicar la entrada con los pesos correspondientes del node de la capa oculta para el que estamos calculando el valor. Observe 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 para el primer node de 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 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.

    Fase 2

    Para calcular los valores de la capa de salida, los valores de los nodes de la capa oculta se tratan como entradas. Por tanto, para calcular la salida, multiplique los valores de los nodes de la capa oculta con sus correspondientes pesos y pase 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 + ah2w11 + ah4w12
    $$

    $$
    zo2 = ah1w13 + ah2w14 + ah2w15 + ah4w16
    $$

    $$
    zo3 = ah1w17 + ah2w18 + ah2w19 + ah4w20
    $$

    Aquí zo1, zo2 y zo3 formarán el vector que usaremos como entrada para la función sigmoidea. Vamos a 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:

    $$
    ao1 (zo) = frac {e ^ {zo1}} {sumnolimits_ {k = 1} ^ {k} {e ^ {zok}}}
    $$

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

    Puede ver que el paso de avance para una red neuronal con salida de clases múltiples 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 sigmoidea.

    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 los pesos de manera que el costo se minimice. Sin embargo, a diferencia de los artículos anteriores donde usamos el error cuadrático medio 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 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 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:

    $$
    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 node 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 nodes de salida y “y” es el vector de las salidas reales de los nodes 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 nodes 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 de manera que el costo disminuya. Para hacerlo, necesitamos tomar la derivada de la función de costo con respecto a cada peso. Matemáticamente podemos representarlo como:

    $$
    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 primera parte de la ecuación se puede representar como:

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

    La derivación detallada de la función de pérdida de entropía cruzada con la función de activación 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 predicha 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 que provienen 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 con 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 {dcost}{dbo} = frac {dcost}{dao} * frac {dao}{dzo} * frac {dzo}{dbo} ….. (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:

    $$
    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 se pueden multiplicar simplemente con la tasa de aprendizaje y restar del valor de sesgo actual.

    Fase 2

    En esta sección, propagaremos nuestro error a la capa anterior y encontraremos los nuevos valores de peso para los pesos de las capas ocultas, 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 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} …… (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 diferenciación de la cadena de la siguiente manera:

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

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

    $$
    frac {dcost}{dao} * frac {dao}{dzo} =frac {dcost}{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} = 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:

    $$
    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 peso para los pesos de capa oculta “wh”, los valores devueltos por la Ecuación 6 pueden simplemente multiplicarse con la tasa 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 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 se pueden simplemente multiplicar por la tasa de aprendizaje y restar de los valores de sesgo de la capa oculta actual y eso es todo para la retropropagación.

    Puede ver que el proceso de retropropagación y retroalimentación 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 clases múltiples

    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.

    Eche un vistazo al siguiente guión:

    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 utilizando la softmaxfunción.

    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 softmaxfunción en lugar de la sigmoidfunción.

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

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

    Del mismo modo, si ejecuta el mismo script con la función sigmoidea en la capa de salida, el costo de error mínimo que obtendrá 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: “Neural Network from Scratch in 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.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *