Mi primer Kaggle, un séptimo puesto que sabe a gloria

Iván de Prado
Iván’s blog
Published in
9 min readJun 4, 2018

Llevo aproximadamente un año y medio en esto del Deep Learning, y los últimos meses me lo estoy tomando más en serio (hice el curso de Stanford y me pienso hacer el de fast.ai). Y había llegado el momento de ponerme a prueba, así que me apunté al concurso iMaterialist Challenge (Fashion) at FGVC5 de Kaggle. Este consiste en clasificar imágenes de moda en diferentes categorías (228 en total). Por ejemplo, esta imagen:

se clasifica en las categorías 62, 66, 153, 171, 111, 137, 70, 204, 184. Seguramente cada número tenga cierto significado: vestido, flores, primavera, o qué sé yo. Realmente esa información no fue revelada, así que en este sentido se iba a “ciegas”. Más aún, no he mirado prácticamente a las imágenes durante mi camino, porque hacerlo no aportaba mucho.

En definitiva, se trataba de meter un chorro de números en una trituradora y generar otro chorro de números. El objetivo: fabricar la mejor trituradora posible.

Había 1 millón de imágenes etiquetadas para entrenar los modelos y unas 10.000 imágenes para evaluar su eficiencia (conjunto de validación). Finalmente, había que clasificar unas 40.000 imágenes (conjunto de test) que Kaggle usaba para evaluarnos a todos los concursantes.

Y para mi sorpresa conseguí quedar séptimo (ya lo hubiera firmado antes de empezar). Voy a contar un poco mi estrategia.

Framework: PyTorch

La verdad es que ha sido un placer utilizar PyTorch. Hace sencilla y rápida la experimentación. Y hay mucho código ahí fuera para reutilizar.

Arquitectura de la red neuronal

Cuando se trata de procesar imágenes con Deep Learning siempre se parte de ciertas arquitecturas ya estudiadas que se sabe que dan buenos rendimientos. Las hay más complejas, con mayor capacidad, número de parámetros y capas que suelen dar mayor exactitud. Pero son también más lentas de entrenar y requieren muchos más recursos de GPU. Hay otras más ligeras, pero claro, pierdes capacidad. El siguiente gráfico muestra una comparación entre varias de ellas: VGG, Resnet 101, Inception, etc.

Extraído de Speed/accuracy trade-offs for modern convolutional object detectors

En mi caso he usado varias de ellas: Resnet101, Xception, Se_resnext50_32x4d, Se_resnext101_32x4d y una modificación propia de Xception a la que añadí módulos de Squeeze & Excitation que he llamado “Sexception”. Estas redes están disponibles en el repositorio de Torchvision y en el de Cadene.

Por dar una idea, este es un ejemplo esquemático de 3 posibles arquitecturas:

Fuente

Como se puede ver constan de muchas capas consecutivas, de ahí el nombre de aprendizaje profundo.

Transferencia de aprendizaje

Algo que me fascina de las redes neuronales para visión es lo que se llama transfer learning. El planteamiento es el siguiente: una red que ya haya sido entrenada viendo millones de imágenes tiene una gran capacidad para adaptarse a otra tarea de visión diferente. No sólo eso, sino que en general son superiores: se tiene mayor rendimiento partiendo de una red pre-entrenada (aunque sea en otro problema) que partiendo de una red “vacía”.

Para quien quiera saber más de esto está muy bien el estudio que Facebook ha hecho con modelos pre-entrenados con 1000 millones de imágenes de Instagram.

En general se suele partir de redes pre-entrenadas con ImageNet (1 millón de imágenes). Este ha sido mi caso. Todos los modelos de los repositorios que indiqué más arriba incluyen modelos pre-entrenados.

Multiples etiquetas

Las redes de los repositorios que he mencionado están adaptadas para clasificar cada imagen en una única categoría concreta dentro de las posibles. Pero para este problema, se necesita poder determinar varias etiquetas por imagen. Además hace falta adaptar la última capa para el número de clases de este problema (228). Para hacer esta adaptación, retiré la última capa de las redes (la capa llamada “fc 1000” en los ejemplos de arquitecturas anteriores) y puse otra cuyo tamaño de salida era 228. A estas 228 terminaciones conecté una función sigmoide para que el resultado estuviera entre 0 (no pertenece a esa clase) y 1 (si pertenece a la clase).

Además hace falta definir una función de pérdida (loss) que es la clave pues es la que mide cuán equivocado está el modelo. A partir de la medida de esta función se re-adaptan los parámetros de la red y se consigue así que la red vaya aprendiendo. En este caso usé binary cross entropy que es lo habitual para el caso. De modo simplificado, esta medida compara la predicción del modelo para las 228 posibles etiquetas con las etiquetas verdaderas (etiquetadas por un humano). Si difieren mucho, hay que cambiar mucho los parámetros.

Aumento artificial de los datos

Uno de los peligros cuando se entrenan modelos de machine learning es el overfitting. Este se produce porque el modelo se aprende de memoria tu dataset en lugar de aprender los conceptos básicos subyacentes del problema. Cuanto tu modelo hace overfitting es muy eficiente en tu conjunto de entrenamiento (casi no da error) pero es muy malo cuando le muestras imágenes nuevas. Vamos, en resumen, que si tu modelo hace overfitting tu modelo no sabrá generalizar y será menos eficiente.

Hay métodos para evitar el overfitting. Una técnica básica es modificar las imágenes del conjunto de entrenamiento estirándolas, cambiando su tamaño, rotándolas, etc, de forma que sea más difícil que la red las memorice.

En este caso usé varias de estás técnicas, particularmente: el escalado, modificación de ratio de aspecto, la rotación y la oclusión de parte de la imagen. En cualquier caso no creo que fuera determinante para este caso el usar tanto aumento.

Hay otro tipo de data augmentation también muy típica, que se llama test time augmentation (TTA). En este caso, se hace lo mismo, pero cuando se va a aplicar el modelo sobre una imagen concreta del conjunto de test, en lugar de utilizar la imagen tal cual para la inferencia, se usa la misma imagen transformada de varias maneras.

En este caso de cada imagen obtenía 5 cortes a dos escalas diferentes, más la imagen original escalada. Es decir, para sacar una conclusión sobre una imagen, generaba 11 imágenes que pasaba al modelo y luego me quedaba con la media de todos los resultados como predicción.

Esto mejora los resultados, pero es realmente problemático porque requiere de muchísima capacidad de cómputo.

Ensembling

Una técnica básica en machine learning cuando se quieren obtener mejores resultados es la mezcla de los resultados de varios modelos (ensembling). Esta demostrado que esto mejora las predicciones y en un concurso donde se evalúa únicamente la calidad de las predicciones es imprescindible.

En mi caso usé un ensemble de 5 modelos diferentes de los cuales para cada etiqueta se seleccionaban los 4 mejores para esa etiqueta y la predicción era la media de esos cuatro.

Las claves

Todo lo descrito anteriormente es muy típico y no es nada nuevo ni muy original. Simplemente la aplicación de técnicas que siempre se suelen usar en estos concursos. ¿Cuáles son las claves para haber podido conseguir esta posición? En este punto voy a contar lo que yo considero importante.

1. Tener en cuenta como se evalúa

En este caso se evaluaban los resultados usando Mean F1 Score. Veamos un poco qué implicaciones tiene esto.

Un modelo es muy preciso (precision) si siempre que se anima a decir algo sobre una imagen acierta. Y tiene mucha cobertura (recall) si acierta muchas de las que tenía que haber acertado. Son dos medidas que se enfrentan: si quieres aumentar la cobertura, normalmente es a costa de perder precisión.

Un ejemplo, por fijar mejor las ideas: un modelo superpreciso es aquel que nunca dice nada (así no se la juega), pero su cobertura en este caso será patética. Y el ejemplo contrario, un modelo que siempre predice la pertenencia a cierta etiqueta independientemente de la imagen dada tendrá una cobertura máxima pero su precisión será mala.

El resultado de los modelos es una probabilidad sobre la creencia de si esa imagen debe ser etiquetada o no. Pero realmente se evalúa en función de un resultado discreto, no de una probabilidad. Así que hay que decidir, a partir de qué probabilidad (threshold) decimos que una imagen pertenece a una etiqueta.

Así pues, subiendo ese threshold podemos tener un modelo más preciso pero con menos cobertura y bajándolo lo contrario.

Para este caso, ¿qué queremos, más precisión o más cobertura? La respuesta es que un balance. Eso es lo que calcula la función F1-Score. Es una especie de media entre la precisión y la cobertura de forma que se valoran mejor aquellos modelos que estén más o menos bien balanceados.

A efectos prácticos para este problema, la conclusión es que fue muy importante ajustar los thresholds para cada clase de forma que el F1 fuera máximo.

También probé a balancear entre precisión y cobertura usando positive weighting en la función de pérdida y funcionaba, pero era más efectivo ajustar los thresholds a posteriori.

2. El conjunto de imágenes de evaluación era muy diferente al de entrenamiento

Esto resultó clave. Resulta que ajustando los thresholds usando las imágenes de entrenamiento tenía mucho peores resultados en la evaluación que si los ajustaba con las imágenes de validación. Esto ya me dio una pista… así que analicé la distribución de las etiquetas en los diferentes conjuntos de imágenes:

En el eje Y la frecuencia y el X las etiquetas ordenadas por frecuencia en el conjunto de entrenamiento

Lo primero que vemos es que hay muy pocas etiquetas que son muy frecuentes y luego el resto que son muy poco frecuentes (la típica distribución de Pareto).

Se puede ver como la distribución es más parecida entre test y validación que entre estos dos y el de entrenamiento. Pero es que además se ve que esto es así para ciertas etiquetas que son muy poco frecuentes en entrenamiento pero mucho en el conjunto de test.

En la evaluación la penalización por fallo en cada etiqueta concreta se interpreta con igual peso. Es decir, lo importante es fallar lo mínimo, independientemente de la etiqueta en que falles. Es por ésto que merece la pena enfocarse en hacerlo bien en las etiquetas más frecuentes. Además en este caso, había que focalizarse en las etiquetas más frecuentes del conjunto de test.

Por todo lo anterior, tome las siguientes iniciativas

  • Forzar a que se entrenen los modelos usando imágenes con las etiquetas más frecuentes en el conjunto de test. Para esto muestreaba las imágenes de entrenamiento con igual probabilidad entre aquellas con las etiquetas más frecuentes, ignorando el resto de etiquetas. Hice diferentes modelos con muestreo sobre 15/25/50 etiquetas.
  • Utilizar el conjunto de validación también para entrenar. De hecho, la estrategia del ganador fue usar casi únicamente el conjunto de validación para entrenar los modelos.

Otras cuestiones

No cuento con una GPU, así que tiré de Google Cloud: n1-highmem-8 (8 vCPUs, 52 GB de memoria) con 1 x NVIDIA Tesla K80. 280€ me costó la broma (suerte que tenía un bono por 250€ :-D). Creo que va tocando invertir en un buen bicho para Deep Learning.

Hice uso de TensorBoard como panel para controlar los entrenamientos. La verdad es que es tremendamente útil. A pesar de ser un proyecto de Tensorflow, no hubo ningún problema para usarlo con PyTorch a través de la librería tensorboardX.

Para el ajuste de hiperparámetros (como las tasa de aprendizaje y demás), tiré de los valores recomendados en los papers y luego jugué con ellos. Use cyclical learning rates para no tener que fijar con concreción hitos de bajada de la tasa de aprendizaje.

Conclusión

Creo que la clave de tan buen resultado fue la mezcla entre la aplicación de buenas prácticas generales (nada muy original) con el haberme dado cuenta de la gran diferencia entre el conjunto de entrenamiento y el de validación/test.

En el camino he aprendido mucho. Nada como “meter las manos en la masa” junto con la motivación de saber que te mides contra otros para fijar los conceptos e ideas que ya has ido aprendiendo en los diferentes cursos o papers. Te llegas a centrar tanto que es incluso algo adictivo y obsesionante. Veredicto: recomendado.

--

--