marzo 5, 2020 10:00 am

Jesús

>>> Descarga el código de este post aquí <<<

A pesar de la importancia del chasis y la transmisión en un automóvil, es inútil sin la presencia de un motor para aprovechar tal infraestructura. 

Lo mismo sucede con un detector de objetos. Aunque dispongamos de un mecanismo para escanear la imagen en búsqueda de elementos de interés en diferentes escalas, sin un clasificador poderoso, eficaz y veloz, no hemos logrado nada.

Así, pues, hoy iniciaremos el estudio e implementación de este crucial componente. Puesto que la creación de un clasificador involucra múltiples etapas, el foco de hoy estará puesto en la extracción de los features que serán ingeridos por un algoritmo de machine learning durante el entrenamiento.

Este artículo es denso, por lo que ve, prepárate un delicioso café, realiza unos estiramientos y alístate para este viaje.

¿Listo? ¡Empecemos!

Características de un Clasificador de Objetos

Un clasificador de objetos basado en machine learning es un algoritmo que, esencialmente, extrae patrones de una representación matemática de un banco de imágenes para determinar la mejor manera de diferenciar entre las categorías de un problema.

Salvo algunas redes neuronales especializadas, la inmensidad de los modelos de aprendizaje supervisado trabajan con data unidimensional. En otras palabras, sólo lidian con vectores.

El problema que esto nos presenta tiene varias aristas:

  • Los imágenes son bidimensionales (grises) y, en su mayoría, tridimensionales.
  • Pasar de un espacio multidimensional a uno unidimensional conlleva una inevitable pérdida de información.
  • Los seres humanos no estamos equipados para entender información vectorizada de la misma forma que la información visual.

Afortunadamente, más allá de estos indudables contratiempos, existen muchas formas de aprovechar el poder predictivo de los modelos de machine learning, minimizando la pérdida de información, a la vez que maximizamos nuestros resultados (es decir, precisión).

Curiosamente, la eficacia de un clasificador radica primordialmente en la calidad de los datos, no en la escogencia particular de un algoritmo. Después de todo, por algo una de las máximas en machine learning es: “Basura entra, basura sale.”

Cuando hablamos de machine learning aplicado a computer vision, este adagio aplica tanto a las imágenes como a la calidad de los vectores (features) derivados de estas.

Uno de las herramientas a nuestro alcance se conoce como Histograma de Gradientes Orientados (HOG, por sus siglas en inglés). Como veremos en breve, es uno de los extractores de features más poderosos en el mercado.

Histograma de Gradientes Orientados (HOG)

HOG es un descriptor (nombre alternativo para “extractor de features”) típicamente usado en el contexto de computer vision y machine learning, aunque sus aplicaciones se extienden a la cuantificación y representación de otras características interesantes, como forma y textura.

Las cinco etapas del algoritmo son:

  1. Normalizar la imagen antes de pasarla al descriptor.
  2. Computar los gradientes en la dirección de los ejes X e Y.
  3. Obtener una votación ponderada por celdas.
  4. Normalización de contraste en celdas superpuestas.
  5. Recopilar todos los histogramas para formar el vector final.

El resultado del proceso descrito arriba será un vector de números reales. La dimensionalidad de dicho vector dependerá fuertemente de los parámetros orientations, pixels_per_cell y cells_per_block.

La premisa de HOG es que la apariencia de un objeto puede modelarse mediante la distribución de los gradientes en regiones rectangulares de una imagen.

Estas regiones conectadas se conocen como celdas y para cada una de ella calcularemos el histograma del gradiente de los pixeles. Posteriormente, acumularemos los histogramas de cada celda para producir el vector final. 

Recordemos que el gradiente de una imagen nos dice la dirección de cambio de la intensidad de los pixeles. Así mismo, tal gradiente es útil para detectar bordes, y los bordes, a su vez, definen los límites entre los objetos de una fotografía.

Las regiones están conectadas entre sí porque la manera en la que las seleccionamos es con el deslizamiento de ventanas. Por tanto, dependiendo de cómo configuremos el descriptor, éstas se superpondrán en mayor o menor medida. 

Podemos aprovechar este hecho para hacer nuestro descriptor más robusto, mediante la normalización de bloques

Pero, ¿qué es un bloque?

Buena pregunta. Un bloque es a las celdas, lo éstas a los pixeles. Es decir, una celda es un grupo de píxeles, y un bloque es un grupo de celdas.

Para culminar esta sección, detallemos un poco más lo que ocurre en cada etapa enunciada arriba:

  1. Normalización de la imagen: Aunque es un paso opcional, solemos aplicarlo ya que conduce, en la mayoría de los casos, a un mejor desempeño de HOG. Las formas de normalización más populares son, entre otras:
    1. Normalización gamma: Se computa el logaritmo de cada píxel.
    2. Normalización con raíz cuadrada: Se computa la raíz cuadrada de cada píxel.
    3. Normalización de varianza: Se computa la media y la desviación estándar de los píxeles en la imagen, para luego restarle a cada uno la media, dividiendo el resultado entre la desviación estándar.
  2. Cálculo de gradientes: Primera obtenemos el gradiente en ambas direcciones, posteriormente lo combinamos. Para mayor detalle sobre el cálculo de gradientes, lee este artículo (NOTA: Se calculan sobre toda la imagen).
  3. Voto ponderado por celda: Con la magnitud y orientación de los gradientes de cada pixel, dividimos la imagen en celdas (grupos de píxeles) y bloques (grupos de celdas). En el código que veremos más adelante, el número de píxeles por celda lo especifica el parámetro pixels_per_cell. Análogamente, el número de celdas por bloque es blocks_per_cell.

    Hecha esta división, tenemos que calcular el histograma de cada celda. ¿Cuántas particiones (bins) tendrá? Tantas como indiquemos en orientations. Típicamente usamos entre 9 y 12 orientaciones.

    Cada píxel tiene un peso asociado en el histograma, el cual no es más que la magnitud de su gradiente.
  4. Normalización de contraste por bloques: En teoría, podríamos juntar todos los histogramas del paso 3 para generar el vector final. No obstante, reducir el impacto de la variación de contraste e iluminación en el resultado tiende a producir mejores descriptores. Como en el caso de las celdas, los bloques también se superponen debido a la naturaleza del deslizamiento de ventanas subyacente.

    Para cada celda en un bloque dado, obtendremos su histograma de gradientes correspondiente. Luego, los concatenaremos todos en un único vector, para posteriormente aplicar normalización L1 o L2.

¡Uf! ¡Qué cantidad de teoría! Mejor pasemos a la práctica, ¿no crees?

Un Momento… ¿Tengo Que Implementar Todo Esto Desde Cero?

Sí… Si quieres. Pero no tienes que hacerlo. Tanto OpenCV como scikit-image ofrecen implementaciones de HOG, para nuestra gran fortuna y sanidad mental.

Como de costumbre, la versión de OpenCV, aunque indudablemente útil, es engorrosa y confusa, por lo que la mayoría de las personas, incluyéndome, se decantan por la de scikit-image.

Código

>>> Descarga el código de este post aquí <<<

El proyecto de hoy es extenso, por lo que iremos por partes.

Primeramente, tenemos que crear el virtualenv para instalar los paquetes requeridos. Lo primero lo llevamos a cabo así:

Las librerías instaladas son:

Algunas menciones especiales:

  • h5py: La usaremos para manipular archivos en formato HDF5, extremadamente útil para lidiar rápidamente con conjuntos de datos numéricos multidimensionales.
  • scikit-image: Entre otras cosas, contiene la implementación de HOG que nos interesa.
  • scikit-learn: Aunque no entrenaremos ningún modelo hoy, la función extract_patches_2d, la cual reside en sklearn será de gran utilidad para extraer ejemplos negativos más adelante.

La clave de un descriptor de imágenes exitoso es la consistencia. Esto se traduce a que el mismo conjunto de parámetros debe usarse para generar el vector de cada imagen, ya que de lo contrario, sería imposible compararlos entre sí.

Una fórmula sencilla para garantizar tal consistencia es encapsular la llamada a la función features.hog de skimage en una clase, como a continuación:

La clase HOG implementada arriba (ubicada en descriptors/hog.py) recibe los parámetros sólo durante la inicialización; de resto, los reutiliza en cada invocación a describe

La parte difícil está lista. No obstante, tenemos otras tareas más mecánicas por delante, por lo que aún no acabamos.

Tenemos que guardar los vectores en alguna parte, ¿no? No hay necesidad de extraer vectores más de una vez. 

Usaremos HDF5 para guardar los features entregados por HOG, ya que es excelente para manipular arreglos numéricos bastante volumétricos velozmente.

En el extracto de abajo definimos dos funciones auxiliares para guardar y cargar conjuntos de datos en este formato, respectivamente: 

Estas funciones se encuentran en el archivo utils/dataset.py.

Tal parece que tenemos todas las partes que necesitamos, lo único que falta es juntarlas, ¿cierto?

Sí, pero antes de seguir adelante, agrandemos nuestro archivo de configuración (resources/airplanes.json, del cual hablamos en el artículo anterior) con los parámetros relevantes a HOG y a la extracción de features propiamente:

Repasemos brevemente los nuevos valores:

  • features_path: Ruta donde se guardará el archivo HDF5 con los vectores correspondientes a las imágenes.
  • percent_gt_images: Proporción de las imágenes que se usarán para entrenar el modelo.
  • offset: Número de píxeles que constituirán el margen de las anotaciones de las imágenes usadas para compilar el conjunto de datos. Este margen añade contexto al objeto, lo que hace al clasificador más robusto.
  • use_flip: Interruptor booleano que indica si debemos aumentar nuestro conjunto de datos volteando las imágenes.
  • num_distraction_images: Cantidad de imágenes negativas que usaremos para muestrear instancias de objetos que no son aviones.
  • num_distractions_per_images: Cantidad de muestras aleatorias que extraeremos de cada imagen negativa.
  • orientations: Número de particiones de cada histograma del descriptor HOG.
  • pixels_per_cell: Número de píxeles por celda del descriptor HOG.
  • cells_per_block: Número de celdas por bloque del descriptor HOG.
  • normalize: Interruptor booleano que indica si debemos aplicar normalización por bloques o no.

Algunos de los siguientes parámetros no serán usados en este artículo, puesto que se corresponden al detector de objetos, el cual estudiaremos en el próximo post. Sólo window_dimensions es relevante por el momento:

  • window_step: Número de píxeles a mover la ventana deslizante en ambas direcciones.
  • overlap_threshold: Proporción de superposición entre dos ventanas. Relevante para Non-Maxima Suppression, tópico que cubriremos en un futuro.
  • pyramid_scale: Factor de reducción para la generación de la pirámide de imágenes.
  • window_dimensions: Dimensiones de la ventana deslizante, la cual usaremos también para extraer los ejemplos positivos y negativos. Para una explicación sobre cómo hallamos las dimensiones, lee el artículo anterior y los párrafos venideros.
  • min_probability: Probabilidad mínima que debe arrojar el clasificador para considerar una clasificación como positiva (es decir, un avión).

Ahora sí estamos listos para armar, y vectorizar nuestro conjunto de datos. De ello se encarga el script de abajo:

Muchas cosas cosas están sucediendo en el extracto anterior, por lo que te exhorto a leer detenidamente los comentarios que acompañan al código. Como notarás, sin la ayuda de la serie de funciones y clases auxiliares definidas con anterioridad, este programa sería considerablemente más complejo. 

La modularización siempre es una buena idea.

Imagino que habrá llamado tu atención la función auxiliar crop_caltech101_bounding_box, usada en la línea 53. Se trata de una función utilitaria para extraer la región de interés contenedora de un aeroplano de una imagen del conjunto de datos CALTECH101. Está definida en datasmarts/helpers.py:

Una de las interrogantes que quedaron abiertas en el post anterior es cómo escoger las dimensiones de la ventana deslizante, considerando que esta tiene un impacto importante en el funcionamiento de HOG. En esa ocasión nuestra exploración de los datos nos informó que el ancho promedio de una región de interés es 293.11, la altura promedio es 95.45 y la relación de aspecto es 3.07. 

La cantidad de trabajo llevado a cabo por HOG no es trivial. Por tanto, mientras más ventanas tengamos, más tardaremos en extraer los features. Por otro lado, si nuestras ventanas son muy grandes o insuficientes, corremos el riesgo de no capturar los objetos de interés para su posterior detección. El punto es que escoger las dimensiones de la ventana no es tan fácil y directo como supondríamos. De hecho, tiene mucho de ensayo y error.

Una buena estrategia inicial es tomar las dimensiones de la exploración y dividirlas entre 2, puesto que nos provee de un buen balance entre eficiencia y eficacia. Eso nos daría 146.55 para el ancho y 47.725 para el alto. Redondeando, tendríamos una ventana de dimensiones 147×48. Sin embargo, como las dimensiones de pixels_per_cell son 4×4, lo ideal es que las dimensiones de la ventana sean divisibles entre 4; así es como llegamos al 148×48 final.

Lo último que nos queda por hacer es ejecutar el script:

Al cabo de unos segundos (dependiendo de las prestaciones de tu sistema), tendremos un archivo HDF5 en la ubicación indicada en la configuración, el cual corresponde a los vectores generados por HOG, tanto para las instancias positivas como negativas:

>>> Descarga el código de este post aquí <<<

¡Wow! Ha sido todo un viaje, pero por fin hemos arribado a nuestro destino. No caben dudas sobre la versatilidad y capacidad descriptiva de HOG. A pesar de ello, hoy aprendimos que la selección de los parámetros adecuados juega un rol crucial en la calidad de los vectores resultantes. Siempre que podamos, hemos de apuntar a un balance entre eficacia (vectores suficientemente representativos de la imagen original), y eficiencia (vectores de un tamaño manejable). 

En el próximo artículo tomaremos los features creados el día de hoy para, finalmente, entrenar un clasificador de aviones. 

¡Hasta pronto!

Sobre el Autor

Jesús Martínez es el creador de DataSmarts, un lugar para los apasionados por computer vision y machine learning. Cuando no se encuentra bloggeando, jugando con algún algoritmo o trabajando en un proyecto (muy) cool, disfruta escuchar a The Beatles, leer o viajar por carretera.

Paso 2/2: Celebra tu NUEVO EMPLEO en Machine Learning ?

A %d blogueros les gusta esto: