Maratón Behind the Code Latinoamérica: Sé parte del Desafío. Inscríbete antes del 7 de Setiembre.

Redes neuronales convolucionales

Es probable que en su vida diaria ya haya visto en acción algún tipo de algoritmo de reconocimiento de objetos, por ejemplo, la detección facial de la cámara del teléfono móvil. Pero ¿cómo funciona esto? En el núcleo de soluciones de visión computacional como éstas, se encuentran las redes neurales convolucionales (CNN). En pocas palabras, son redes neurales capaces de construir funciones completas a partir de otras menos complejas. Un ejemplo clásico es la detección facial, en la que primeras capas eligen líneas horizontales y verticales mientras que las capas posteriores encuentran narices y bocas.

Este artículo explica cómo funcionan las redes convolucionales. También muestra cómo utilizar Python para implementar una red sencilla que clasifica dígitos escritos a mano. Así que ¡vamos directamente al asunto!

Manual sobre redes neurales

Este artículo no entra en detalle en cómo funcionan en general las redes neurales, pero es necesario tener un poco de contexto antes de ocuparnos de las redes convolucionales. Las redes neurales tienen una arquitectura estratificada. Cada capa está formada por un número de nodos, y cada nodo realiza de forma efectiva alguna operación matemática sobre alguna entrada para calcular un resultado. La entrada de cualquier nodo es una suma ponderada de los resultados de la capa anterior (más un término de sesgo que normalmente es igual a uno o a cero). Éstas son las ponderaciones que el algoritmo aprende durante su capacitación. Para aprender estos parámetros, el resultado de una ejecución de una capacitación se compara con el valor verdadero, y el error se retropropaga a lo largo de la red para actualizar las ponderaciones.

Convolución

La convolución es una operación matemática, en la que una función se «aplica» de alguna manera a otra función. El resultado se puede entender como una «mezcla» de las dos funciones. La convolución se representa por un asterisco (), que se puede confundir con el operador que generalmente se utiliza para multiplicar en muchos lenguajes de programación.

Pero ¿cómo ayuda esto a detectar los objetos de una imagen? Bueno, resulta que las convoluciones son realmente buenas para detectar estructuras sencillas de una imagen, y después para juntar esas sencillas funciones para construir funciones aún más complejas. En una red convolucional, este proceso ocurre sobre una serie de muchas capas, en la que cada una de ellas realiza una convolución sobre el resultado de la capa anterior.

Así que ¿qué tipo de convoluciones se utilizan para la visión computerizada? Para entender esto, primero hay que entender exactamente lo que es una imagen. Una imagen es una matriz de bytes, de Rasgo 2 (bidimensional, ancho y alto) o de Rango 3 (tridimensional, ancho, alto y más de un canal). Así que una imagen en blanco y negro es de Rasgo 2, mientras que una imagen RGB es de Rango 3 (con tres canales). Los valores de los bytes se interpretan como valores enteros, que describen la cantidad que se debe utilizar de un canal particular en el pixel correspondiente. Así que, básicamente cuando se trata de visión computerizada, hay que imaginar una matriz de 2D de números (para una imagen RGB o RGBA, con tres o cuatro de dichas matrices sobrepuestas unas sobre otras).

Por lo tanto, mi convolución toma esta matriz (por ahora voy a asumir que la imagen es en blanco y negro) y la convoluciona con otra matriz, llamada. El proceso de convolución procede de la siguiente manera. Primero, el filtro cubre la parte superior izquierda de la matriz de la imagen. A continuación, el producto relativo al elemento del filtro se toma con la subsección de la imagen sobre la que se encuentra actualmente el filtro. Es decir, el elemento superior izquierdo del filtro se multiplica por el elemento superior izquierdo de la imagen, y así sucesivamente. Después se añaden esos resultados para producir un valor. Luego el filtro se mueve a lo largo de la imagen recorriendo una distancia que se llama zancada, y se repite el proceso. El resultado es una nueva matriz, de diferentes dimensiones en la matriz de imagen (normalmente el resultado tiene menor altura y anchura, aunque más canales). Para ilustrar cómo funciona esto, eche un vistazo a un ejemplo. Este es un filtro 3 x 3:

ecuación de una matriz

La siguiente imagen es donde aplicaré este filtro.

mujer caminando en la calle

Después de aplicar un pase del filtro en esta imagen, obtengo el siguiente resultado.

Imagen más oscura de la mujer caminando por la calle

Espero que pueda ver que el filtro está dispuesto para los valores que van verticalmente. Esto significa que elige las funciones verticales de la imagen, como puede ver en el resultado. Cuando se ejecuta un CNN se aprenden los valores del filtro.

Vale la pena señalar que el tamaño de la zancada y el filtro son hiperparámetros, lo que significa que no son aprendidos por el modelo. Así que, tiene que utilizar su mente científica para concretar qué valores de esas cantidades funcionarán mejor para su aplicación.

Un concepto final que hay que entender acerca de la convolución es la idea de normalización. Si la imagen no encaja con el filtro en un número de veces entero (teniendo en cuenta la zancada), hay que normalizar la imagen. Hay dos formas de hacer esto: la normalización VÁLIDA (VALID) y la normalización IGUAL (SAME). La primera básicamente elimina los valores faltantes del borde de la imagen. Es decir, si el filtro es 2 x 2 con una zancada de 2, y la imagen tiene una anchura de 3, entonces la normalización VÁLIDA ignora la tercera columna de valores de la imagen. Entre tanto, la normalización IGUAL añade valores (normalmente ceros) a los bordes de las imágenes para incrementar sus dimensiones hasta que el filtro pueda encajar un número de veces entero. Esta normalización normalmente se realiza de forma simétrica (es decir, intenta añadir el mismo número de columnas/filas a cada lado de la imagen).

También es interesante apuntar que las convoluciones de la imagen tienen aplicaciones que no se limitan a la visión computerizada. Muchas técnicas de filtrado de imágenes se pueden implementar a través de la convolución, por ejemplo, el desenfoque y el enfoque.

El siguiente código básico de Python muestra cómo funciona la operación de convolución (puede utilizar numpy, por ejemplo, para hacer que esto sea más ordenado):


def basic_conv(image, out, in_width, in_height, out_width, out_height, filter,
filter_dim, stride):
    result_element = 0

    for res_y in range(out_height):
        for res_x in range(out_width):
            for filter_y in range(filter_dim):
                for filter_x in range(filter_dim):
                    image_y = res_y + filter_y
                    image_x = res_x + filter_x
                    result_element += (filter[filter_y][filter_x] ∗
image[image_y][image_x])

           out[res_y][res_x] = result_element
           result_element = 0
           res_x += (stride ‑ 1)

        res_y += (stride ‑ 1)

    return out

Observe que si quiere escribir el resultado en un archivo de imagen (para visualizarlo como yo hice antes), debe restringir los valores de la salida para que no sean más de 255.

Agrupación y capas completamente conectadas

Las redes convolucionales reales raramente se construyen sobre solo capas convolucionales. Normalmente también tienen otros tipos de capas. La más sencilla es la capa completamente conectada. Esta es tan sólo una capa de red neural normal en la que todos los resultados de la capa anterior están conectados a todos los nodos de la capa siguiente. Normalmente, esas capas van hacia el final de la red.

El otro tipo de capa que se ve en las redes convolucionales es la capa de agrupación. Viene en diferentes formas, pero la que más habitualmente se utiliza es la agrupación máxima, en la que en la matriz de entrada se divide en segmentos del mismo tamaño, y se toma el valor máximo de cada segmento para rellenar el elemento correspondiente de la matriz de salida.

matriz de salida

En el listado de código anterior, la entrada se dividió en dos cuadrantes 2 x 2 y se aplicó a la agrupación máxima. Por lo tanto, puedo describir esta operación en particular como que tiene un filtro de 2 dimensiones y una zancada de 2. Lo que este proceso hizo es tomar los sectores más amplios en los que reside una función. Imagine que esta red está buscando caras. En este caso, se puede interpretar el resultado de esta agrupación como una muestra de que hay una gran posibilidad de que haya una cara en la parte inferior derecha, alguna posibilidad de que haya una cara en la parte superior izquierda y que probablemente no haya ninguna cara en la parte superior derecha o inferior izquierda.


def max_pool(input, out, in_width, in_height, out_width, out_height, kernel_dim,
stride):
    max = 0

    for res_y in range(out_height):
        for res_x in range(out_width):
            for kernel_y in range(kernel_dim):
                for kernel_x in range(kernel_dim):
                    in_y = (res_y ∗ stride) + kernel_y
                    in_x = (res_x ∗ stride) + kernel_x

                    if input[in_y][in_x] > max:
                       max = input[in_y]in_x
           out[res_y][res_x] = max
           max = 0

return out

Ejemplo del fondo

Ahora, vamos a resolver un sencillo problema de visión de computación creando una red que identifica dígitos escritos a mano en una imagen. Este es uno de los ejemplos básicos que más habitualmente se utilizan para mostrar el poder de las redes neurales. El ejemplo está escrito en Python con la biblioteca de TensorFlow, así que usted no se tiene que enfocarse demasiado en detalles específicos de la implementación y puede centrarse más en la arquitectura general. TensorFlow tiene otro beneficio. Proporciona el conjunto de datos MNIST que tiene incorporado, aunque cabe destacar que otras infraestructuras de aprendizaje automático (como SciKit-Learn) también lo hacen.

Para la capacitación y las pruebas, utilizo este conjunto de datos de MNIST. Utilizo una arquitectura de red convolucional relativamente simple que se basa en LeNet-5. Esto logró un porcentaje de error de 0,9 % en el conjunto de MNIST, aunque yo no llegaré a ese nivel de precisión porque prescindiré de muchas de las manipulaciones que realizaron LeCun y otros para que la red tuviese mejor rendimiento, y yo simplificaré determinados aspectos de la arquitectura.

Arquitectura

La arquitectura que utilizaré será la siguiente:

  1. Una capa convolucional, que reduce la imagen MNIST 32x32x1 a un resultado 28x28x6
  2. Una capa de agrupación máxima, que reduce a la mitad el ancho y la altura de los rasgos
  3. Una capa convolucional, que cambia las direcciones a 10x10x16
  4. Una capa de agrupación máxima, que vuelve a reducir a la mitad el ancho y la altura
  5. Una capa completamente conectada, que reduce el número de los rasgos de 400 a 120
  6. Otra capa completamente conectada
  7. Una capa completamente conectada final, que produce un vector con 10 de tamaño

Cada capa intermedia utiliza una no linearidad ReLU y cada capa convolucional utiliza un filtro 5×5 con 1 de zancada y normalización VÁLIDA. Mientras tanto, el filtro de agrupación máxima tiene 2 de dimensión.

Métodos helper

El código tiene varios métodos helper que extraen algunos de los detalles que se repiten en la arquitectura, como la creación de filtros (todos los filtros son 5×5, pero tienen diferentes profundidades) y las capas convolucionales. Observe que utilicé una distribución Gaussiana truncada en la inicialización de ponderaciones porque no importa donde empiezan las ponderaciones mientras no sean todas idénticas. Esto se utiliza para romper la simetría. El siguiente código muestra un ejemplo de uno de los métodos helper.


def make_conv_layer(self, input, in_channels, out_channels):
        layer_weights = self.init_conv_weights(in_channels, out_channels)
        layer_bias = self.make_bias_term(out_channels)
        layer_activations = tf.nn.conv2d(input, layer_weights, strides =
self.conv_strides, padding = self.conv_padding) + layer_bias

        return self.relu(layer_activations)
                

Construir la red

Una vez extraídos los detalles específicos de crear las capas, la construcción de la red es relativamente simple.


def run_network(self, x):
        #Capa 1: convolucional, no linearidad ReLU, 32x32x1 ‑‑> 28x28x6
        c1 = self.make_conv_layer(x, 1, 6)

        #Capa 2: Agrupamiento máximo. 28x28x6 ‑‑> 14x14x6
        p2 = self.make_pool_layer(c1)

        #Layer 3. convolutional, ReLU nonlinearity, 14x14x6 ‑‑> 10x10x16
        c3 = self.make_conv_layer(p2, 6, 16)

        #Layer 4. Max Pooling. 10x10x16 ‑‑> 5x5x16
        p4 = self.make_pool_layer(c3)

        #Aplanamiento de las funciones que alimentarán una capa completamente conectada
        fc5 = self.flatten_input(p4)

        #Capa 5. Completamente conectada. 400 ‑‑> 120
        fc5 = self.make_fc_layer(fc5, 400, 120)

        #Capa 6. Completamente conectada. 120 ‑‑> 84
        fc6 = self.make_fc_layer(fc5, 120, 84)

        #Capa 7. Completamente conectada. 84 ‑‑> 10. Capa de resultado, así que no tiene ReLU.
        fc7 = self.make_fc_layer(fc6, 84, 10, True)

        return fc7

Capacitación

Primero, divido el conjunto de MNIST en un conjunto para capacitación, un conjunto para validación cruzada y un conjunto para pruebas.


x_train, y_train, x_valid, y_valid, x_test, y_test = split()

x_train = pad(x_train)
x_valid = pad(x_valid)
x_test = pad(x_test)

x_train_tensor = tf.placeholder(tf.float32, (None, 32, 32, 1))
y_train_tensor = tf.placeholder(tf.int32, (None))
y_train_one_hot = tf.one_hot(y_train_tensor, 10)
                

Las etiquetas de la capacitación se reforman como un vector one-hot. Un vector one-hot es un vector en el que cada elemento representa una clase, y el elemento es igual a uno si el ejemplo pertenece a esa clase y, en caso contrario, es igual cero. Así que para una imagen que muestre el número 1, la representación one-hot sería:

vector one-hot

Después, configuro las operaciones para definir cómo voy a capacitar el modelo, por ejemplo, definiendo que cantidad quiero minimizar durante la capacitación.


net = lenet.LeNet5()
logits = net.run_network(x_train_tensor)

learn_rate = 0.001
cross_ent = tf.nn.softmax_cross_entropy_with_logits(logits = logits, labels =
y_train_one_hot)
loss = tf.reduce_mean(cross_ent) #We want to minimise the mean cross entropy
optimisation = tf.train.AdamOptimizer(learning_rate = learn_rate)
train_op = optimisation.minimize(loss)

correct = tf.equal(tf.argmax(logits, 1), tf.argmax(y_train_one_hot, 1))
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

Utilizo la entropía cruzada como mi medida de pérdida y utilizo AdamOptimiser para realizar la optimización.

El conjunto de la capacitación se divide en lotes, con 128 de tamaño, y ejecuto la capacitación sobre determinado número de épocas (diez en este caso). Después de cada época, el conjunto de ponderaciones resultante se verifica contra el conjunto de la validación cruzada.


with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    example_count = len(x_train)

    for i in range(num_epochs):
        x_train, y_train = shuffle(x_train, y_train)

        for j in range(0, example_count, batch_size):
            batch_end = j + batch_size
            batch_x, batch_y = x_train[j : batch_end], y_trainj : batch_end            sess.run(train_op, feed_dict = {x_train_tensor: batch_x,
y_train_tensor: batch_y})

        accuracy_valid = eval(x_valid, y_valid)
        print("Precisión: {}".format(accuracy_valid))
        print()

    save.save(sess, "SavedModel/Saved")

Luego se guarda el modelo resultante para usarlo en el conjunto para las pruebas.

Rendimiento

Cuando se ejecutó contra el conjunto para las pruebas, el modelo obtuvo una precisión del 98,5 % (un porcentaje de error del 1,5 %). Este es un rendimiento ligeramente menor que la implementación de LeCun debido a diferencias en la preparación de los datos y a otras cosas. Sin embargo, para una implementación relativamente simple como la mía, ese es un rendimiento bastante alto.

Conclusión

En este artículo, se enseñaron los aspectos básicos de las redes neurales convolucionales, lo que incluye el propio proceso de la convolución, la agrupación máxima y con capas completamente conectadas. Después mostré la implementación de una arquitectura de CNN comparativamente simple. Esperemos que esto le ayude a entender las CNNs cuando las utiliza en su trabajo, o que le ayude a iniciar su camino para aprender acerca de una especialidad fascinante del aprendizaje automático.