Creo que los hackathones son una oportunidad única para aprender e innovar.

Este año 2021 he participado en el reto de Procesamiento del Lenguaje Natural (NLP) del Hackathon de Spain AI. Quiero dar las gracias a Spain AI, a Zara Tech, al resto de patrocinadores y a todos los participantes por hacer posible este tipo de eventos. Espero poder participar en otro pronto 🙂

Escribo a continuación los experimentos que me han funcionado bien durante el hackathon, los que claramente no funcionaron y los no concluyentes.

No soy experto en análisis de datos ni NLP. Si alguien con más idea ve algo sin sentido (ie: los hiperparámetros usados) o si a alguien le sirve el contenido, por favor enviad feedback 🙂

 

El problema a resolver

«A partir de las descripciones de unos productos de Zara, genera sus nombres exactos».

Además, para cada producto podías enviar 10 nombres candidatos cada vez, pero ordenados por probabilidad de acierto, ya que: acertar en el primer nombre candidato suma 1 punto, acertar en el 2º suma 0,63, acertar en el 3º suma 0,5, acertar en el 4º suma 0,43… acertar en el 10º suma 0,29.

En total se podía participar 100 veces durante los meses del hackathon.

El enunciado completo está aquí.

 

Cosas que he aprendido en el hackathon

🤖 Estado del arte de las redes para generación de textos y generación de resúmenes. Antes del hackathon había jugado solo con redes RNN para generación de textos de juguete. Durante el hackathon he experimentado principalmente con transformers Bart, T5, Bert…

📦 Librerías. He jugado con pytorch por primera vez, he experimentado con las librerías Transformers, SimpleTransformers, y SentenceTransformers

☁️ Cloud: He usado por primera vez en AWS las GPU (instancias EC2 spot p3.2xlarge), pero he entrenado los modelos principalmente en Google Colab (contraté la versión Pro).

🔨 Otras herramientas: Weights & Biases (para comparar experimentos).

👚 Saber más sobre el dominio del problema. He aprendido sobre el catálogo de productos de Zara.

👀 Otras técnicas de análisis de datos y NLP. He aprendido técnicas de clusterización, limpieza de datos, Data Augmentation (sin buenos resultados), análisis de n-gramas (en el dataset de train, y en los productos de validación fallados y acertados).

📉 Custom loss functions, con algún intento de implementación con pytorch.

🧑‍🤝‍🧑 (próximamente) Buenas ideas, nuevas herramientas, nuevas técnicas, feedback, etc… gracias a las futuras charlas de los ganadores en Spain AI.

 

 

Resultados obtenidos

Conseguí una puntuación de 28, séptima posición. Muy lejos del podio 😀 pero muy contento por todo lo aprendido y por haber ido mejorando. Los ganadores harán próximamente un meetup en Spain AI hablando de su solución 🔝

 

Solución implementada que obtuvo mejor score

  • Uso del modelo Bart Large como transformer, hacer fine tuning del mismo con estos parámetros:
    • max_seq_length (input): 128 tokens
    • max_length (output): 17 tokens
    • train_batch_size: 8
    • gradient_accumulation_steps: 64
    • fp16: True
    • train_size: 0.95
    • test_size: 0.05
    • num_beams: 15
    • do_sample: False
    • num_return_sequences: 10
    • n_epoch_max: 50 (en el epoch 27 se obtuvo 34% de matches en validación, es el experimento que dio score 28).
    • warmup_steps: 100
    • learning_rate: 5e-5
    • Uso de la loss function implementada por defecto en la librería Transformers.
    • Implementación de las funciones de evaluación matches_on_the_first_shot() y matches_on_10_shots()
  • Limpiado de datos:
    • Del análisis de las descripciones anormalmente largas:
      • Eliminación de subcadenas más repetidas en dichos productos que dan lugar a esas descripciones largas (‘instructions for use…’, ‘the item has passed all…’, ‘in 1962, hideo matsushita…’, ‘2-year warranty…’, ‘instructions: …’, ‘this new format allows you to enjoy…’, ‘in order to return this item…’, ‘these are some benefits of…’, ‘heat power: …’, ‘acceptable as carry-on luggage …’, ‘height of model …’).
      • Eliminación de los «books» (50 items aprox).
    • Eliminación de stopwords (nltk.corpus).
    • Comprobación de que los caracteres del dataset se tokenizan correctamente con el tokenizer preentrenado de Bart.
    • No eliminar los «<br>» pero normalizarlos (<br/> –> <br>, </br> –> <br> etc…).
    • Del análisis del ranking de caracteres más y menos usados, aplicar normalización: g/m2 –> g/m²; gr/m2 –> g/m²; &amp; –> &; &#39; –> ’ ; ‘\x0b’ –> ‘\n’, etc…
    • Del análisis del ranking de palabras más y menos usadas:
      • Aplicar normalización:  grammage –> gsm.
      • Eliminar productos de la colección srpls (tras confirmar % mínimo de aciertos y que contiene «srpls» solo el 0,2% del dataset). Son productos que utilizan palabras borrando vocales, es decir palabras nunca antes vistas por el modelo BART. Ejemplo de producto:
        • LTHR CWBY NKL BT SRPLS
    • Del análisis de los n-gramas más probables en las descripciones de productos que no logran match durante la validación, se comprueba que:
      • «elastic waistband» aparece en +2.400 descripciones y solo en 17 nombres. Idea: borrar del nombre para enseñar al modelo a crear nombres sin «elastic waistband».
      • «sleeve», aparece en +13.500 descripciones, pero solo el 1,3% de las veces aparece también en el nombre del producto. Idea: borrar del nombre para enseñar al modelo a crear nombres sin «sleeve».
      • sufijo » TRF» en algunos nombres productos (2.800 productos del dataset). «TRF» es una marca de Zara dirigida a gente joven, con productos de muchas categorías. «TRF» es una palabra que no aparece en las descripciones, solo aparece al final de algunos nombres. Se decide probar a eliminar dicho sufijo de los nombres, para que el modelo aprenda a genenar nombres sin «TRF».
      • ‘ – limited edition’, ‘limited edition’, ‘ – special edition’ y ‘special edition’. Se eliminan esos n-gramas de los nombres de productos cuando no aparecen en sus descripciones (291 productos afectados). Hipótesis de que en esos casos no se extrae solo de la descripción del producto la info para decir si es una edición limitada.
      • Ídem con «Christmas». Se elimina de los nombres si dicha palabra no aparece en la descripción. 47 productos afectados.
      • Quitar las frases que contienen las palabras «airfit» o «starfit» (es info sobre la plantilla del calzado).
  • División random del dataset (previamente barajado) en grupo de train y grupo de validación. Probado con diferentes % hasta obtener el mejor resultado con 95×5%.

 

Experimentos no concluyentes

(Otros experimentos implementados después de obtener el score de 28, que no han conseguido mejorar el score, pero me gustaría investigar más).

  • 📏 Borrar pulgadas. En los productos que las medidas están en cm y pulgadas, he borado las pulgadas y dejado solo los cm.
    Ejemplo: …Heel height of 7.2 cm. / 2.8″ –> …Heel height of 7.2 cm.
  • ⚖️ Dividir mejor el dataset en grupo de entrenamiento y grupo de validación. Del análisis de palabras más probables en el dataset de Train con respecto al dataset de Test, se observa por ejemplo que en Test la palabra «heel» es un 25% más probable que en Train; mientras que palabras como «front» y «sleeves» son un 32% y 26% más probables en Test que en Train. Es decir una división aleatoria del dataset no parece adecuada para validar si nuestro modelo funcionará bien con los datos de Test. Para intentar solucionarlo, aplico clusterización sobre las descripciones de los productos del dataset.

Para hacer el clustering, he utilizado la librería «sentence-transformers» + KMeans de scikit-learn. Sentence-transformers para obtener un embedding de cada descripción de producto, con el modelo preentrenado ‘paraphrase-distilroberta-base-v1’. Con el método del codo he establecido el nº de clusters de KMean en 50 (también he probado con 25).

Después, para cada cluster he generado un identificador que resumiera el contenido del cluster. He usado primero BERTopic (resultado no representativo) y después finalmente la palabra más frecuente del cluster.

Gracias estos clusters, he separado de nuevo el dataset en Train y Eval, siguiendo varios criterios:

    • Que todos los clusters con productos de Test, tengan representación en Eval.
    • Que todos los clusters con productos de Test, tengan una representación en Eval proporcional a su nº de productos de Test.
    • Que todos los clusters con productos de Test, tengan una representación en Eval proporcional a su nº de productos de Test, con un nº mínimo siempre.

Con estos cambios, los resultados en eval han subido al 37%, pero no he podido mejorar el score en Test (25-27).

  • 🗑️ Eliminar datos anómalos del dataset de train. He usado también la clusterización anterior para eliminar del entrenamiento aquellos productos de clústeres en los que no se clasificaba ningún producto del Test (o que el % de productos de Test no alcanzaba el 3%). En algunos experimentos el borrado de productos ha llegado a 6.500, esto ha acelerado el entrenamiento posterior pero sin obtener mejores resultados (score 27 aprox). Los clusters borrados correspondían a productos de tipo Bermuda, Dress, Skirt, Shirt, Jacket…
  • 👠 Tacones y suelas, redondear altura y traducir. He traducido las alturas de los tacones que aparecen en las descripciones (nº con decimales) por una palabra:
    re.findall(‘heel height (.*?) cm’, input_string )

      • Entre 0 y 3 cm –> heel height flat
      • Entre 3 y 8 cm –> mid heel height
      • Más de 8 cm –> heel height high

Quedó pendiente aplicar esta operación de redondeo y traducción a otras productos que tienen sus dimensiones en la descripción (mochilas, bolsos, monederos, frascos de gel/perfume, etc…).

  • 📉 Funciones de pérdida personalizadas. En los 2 últimos días del hackathon implementé estas 2 loss functions para sustituir a la loss function por defecto que usa la librería Transformers en BART. Con esta propuesta se da más peso a los aciertos que a las soluciones «cercanas», ya que eso es lo que dice el enunciado del problema (1 acierto suma 1 pero un fallo suma siempre 0 aunque esté muy cerca de acertar).
    • Loss_v1 = num_productos_no_acertados + 0,001 * default_loss
    • Loss_v2 = num_productos_no_acertados + porcentaje_tokens_no_acertados + 0,001 * default_loss
      Dejo aquí el código, no estoy seguro que sea correcto. Derivable creo que sí que es. Loss_v1 la usé en el último submission del hackathon (score 27,22), sin modificar ningún otro parámetro.

Experimentos que no me han funcionado

  • Poner un prefijo en las descripciones de los productos, seguido del char «:», identificando el cluster al que pertenece el producto. Probé diferentes técnicas para escoger dicho prefijo (topic del cluster, palabra más frecuente, etc). Ejemplo:
    • Descripción original:
      • «lace-up sneakers six-eyelet facing. contrast colours materials upper. mesh metallic details. includes pull tab tongue. chunky maxi air cushion sole»
    • Descripción con prefijo:
      • «sneaker: lace-up sneakers six-eyelet facing. contrast colours materials upper. mesh metallic details. includes pull tab tongue. chunky maxi air cushion sole»
  • Data Augmentation, probé a generar nuevos productos artificialmente:
      • Traduciendo los productos a otro idioma y después volviendo a traducirlos al idioma original. Usé para ello otro transformer: Marian (modelos Helsinki-NLP/opus-mt-en-es y Helsinki-NLP/opus-mt-es-en).
      • Cambiando el orden de las frases de las descripciones de los productos. Esto no funcionó, pero me quedó pendiente probarlo de nuevo manteniendo fijas las primeras frases de cada descripción.
      • Borrando ciertas palabras, como el tipo de cuello (round neck, v-neck, round neck, etc…). Esto lo probé porque eran n-gramas que aparecían entre los más frecuentes durante la evaluación, en los productos cuya predicción de nombre fallaba.
      • Ídem borrando los tipos de mangas (sleeves falling below the elbow, etc…), por mismo motivo.
  • Otros modelos de Transformers: DistilBART, la versión destilada de Bart no generaba buenas predicciones o no supe encontrar los parámetros adecuados. El modelo T5 y el modelo Pegasus de Google tampoco.

 

 

Siguientes pasos

Si tuviera más tiempo para este proyecto, lo dedicaría a:

      • 📉 Probar mejor las Loss Functions personalizadas comentadas arriba, depurarlas, entender mejor el funcionamiento de pytorch y cómo implementar loss functions. Ajustar los hiperparámetros para dichas Loss Functions. Repetir el experimento con dichas Loss Functions sin hacer tanta limpieza en el dataset (sin borrar books, ni «TRF», «Christmas», etc…).
      • 🥠 Hacer una prueba utilizando un vocabulario desde cero para BART, en lugar de utilizar el vocabulario que viene con el modelo preentrenado de BART.
      • 🚀 Usar la funcionalidad Sweeps (Hyperparameter Tuning) de Weights & Biases
      • 🗃️ Probar K-Fold Cross Validation, en lugar de dividir en dataset en 2.
      • 🔀 Orden de los 10 nombres candidatos para cada producto. Desde el principio he trabajado usando el orden devuelto por BART, que está configurado con num_beams=15, do_sample=False y num_return_sequences=10. Quizá adicionalmente se puede diseñar un discriminador que rechace alguna de esas secuencias. También se puede probar a incrementar num_beams.

Lo siguiente que haré es ver los próximos meetups de Spain AI 📺, donde los ganadores contarán sus soluciones 🍿😃

 

Espero que esta info te sea de utilidad, al menos para animarte a participar en el próximo hackathon 😃 . Si te ha servido o si ves algo que cambiarías, por favor envía feedback. ¡Gracias!