Inspiration

Me inspiró este reto por ser estudiante de IA interesado en la visión artificial.

What it does

Dada una foto de "bundle" (outfit, modelo en la calle, campaña de ropa), localizamos cada prenda y buscamos los 15 productos más parecidos del catálogo de Inditex. La métrica es Recall@15.

How we built it

El pipeline tiene tres etapas:

Bundle foto → Detección de prendas → Embeddings → Búsqueda en catálogo → Top-15

Traté el reto como un problema de búsqueda / recuperación de información (Information Retrieval), no como un problema de clasificación, generación, o finetuning / reentrenamiento pesado. La pregunta clave es: ¿cómo encontrar, dentro de un catálogo de miles de productos, los 15 más similares a una prenda detectada en una foto de campaña, **de modo que se pueda iterar rápidamente sobre la solución?

Esto se reduce a obtener queries (crops de prendas) y calcular la similitud de embeddings en un espacio vectorial, pero con varias fuentes de ruido que hay que corregir:

  1. Domain gap: Las fotos de bundle (modelos en campaña) tienen un estilo visual muy distinto a las fotos de producto del catálogo (fondo blanco, iluminación estudio). Un mismo artículo genera embeddings diferentes en cada dominio → el DomainMapper lo corrige sin reentrenar el backbone.
  2. Reparto de presupuesto de predicciones: El sistema evalúa hasta 15 productos por bundle. Si hay 3 prendas en el outfit, lo más intuitivo sería dar 5 predicciones a cada una. El Slot Filling con round-robin distribuye esto equitativamente entre todas las queries detectadas.
  3. Calidad de la query (crop): No todos los recortes de YOLO son igualmente buenos. Hay recortes de fondo, de detalles irrelevantes, etc. El garbage filter y la contextualización global sirven de guardrail.

La filosofía fue: evitar finetunings o reentrenamientos pesados del backbone (que consumen mucho tiempo y GPU) y en cambio concentrar las mejoras en la lógica intermedia: mejor mapeo de dominio, mejor selección de negativos en el entrenamiento del mapper, y mejor reparto e interpretación de las predicciones.

Las mejoras incrementales que más puntuaron vinieron casi todas de progresar el DomainMapper con técnicas de aprendizaje contrastivo más ricas (especialmente en el minado de negativos). También el slot filling fue clave para no desperdiciar el presupuesto de 15 predicciones en una sola prenda que era más similar, y la mejora en detección de prendas con YOLOv8-Clothing fue clave para tener buenas "queries" de entrada al sistema.

Challenges we ran into

Muchos callejones sin salida, ideas de optimizacion que no llevaban a ningun sitio y requerían repensar como mejorar la puntuación.

Accomplishments that we're proud of

Quedar en el podio!

What we learned

Mucho sobre fine tuning, uso de modelos preentrenados, busqueda por similaridad y algo de contrastive learning.

What's next for Zara / Inditex challenge: street-to-shop

Hay que refactorizar... Mucho...

Técnicas implementadas

🔍 Detección de prendas

  • YOLOv8-Clothing (kesimeg/yolov8n-clothing-detection): detector principal de crops. Cada crop actúa como una query de búsqueda independiente. Se usa sin NMS adicional para no perder detecciones válidas en casos de solapamiento. Se podría estudiar como incluir NMS y filtros de confianza para mejorar la calidad de los crops, pero mis pruebas no dieron mejoras significativas.
  • Global image fallback: si YOLO detecta menos de 3 prendas (p.ej. imagen borrosa o atípica), se añade el bundle completo como query adicional para garantizar cobertura.
  • Grounding DINO (precomputado offline en test_dino_macro.json): detecta regiones macro del cuerpo con un prompt de texto. No se usa directamente en inferencia (demasiado lento); se precomputa una sola vez.

🧠 Embeddings y Domain Mapping

  • GR-Lite (backbone DINOv3 de Meta, fine-tuneado para moda): extractor de features visual. Genera embeddings de 1024 dimensiones. Mucho mejor que CLIP para ropa fina.
  • SuperDomainMapper (train_mapper.py): red neuronal residual de dos capas con temperatura aprendible (estilo CLIP/SigLIP). Proyecta embeddings de bundle al espacio de embeddings de catálogo. Arquitectura: Dropout(0.1) → Linear(1024, 2048) → LayerNorm → GELU → Dropout(0.2) → Linear(2048, 1024) + skip connection + L2-normalize. Al principio era más simple y luego la refiné.
    • Temperatura aprendible: equivale a aprender automáticamente el factor de escala de los logits, eliminando un hiperparámetro crítico.
    • Online Hard Negative Mining (OHNM): en la función de pérdida InfoNCE, en lugar de usar todos los negativos del batch se selecciona solo el top-15% más difícil. Fuerza al modelo a aprender distinciones finas entre productos similares.
    • Cross-Batch Memory (XBM): banco FIFO de 8192 embeddings de producto de batches anteriores. Expande el campo de negativos efectivos de ~511 (batch) a ~8703 sin coste extra de VRAM. A más detalle, lo que hace es guardar los embeddings de los productos de batches anteriores y usarlos como negativos en la función de pérdida. Cómo sabe que son negativos? Pues porque no son el producto que se está buscando xd. Y si son muy similares a este pues mejor, porque así el modelo aprende a diferenciarlos. Esta fue la técnica que más impacto tuvo en la puntuación final, fue el salto definitivo al 71%.
    • MixUp sobre embeddings: mezclamos pares de embeddings (α=0.2, 30% de probabilidad) para regularizar el espacio y hacerlo más "transitable", evita zonas vacías y mejora la generalización.
    • Label Smoothing (en la pérdida XBM): sustituye los labels duros [0,1] por distribuciones suaves, evitando que el modelo colapse ante pseudo-etiquetas ruidosas. No entiendo de todo esta función pero mejora el entrenamiento.
    • OneCycleLR: scheduler con warmup automático (10% inicial) y cosine annealing. Estabiliza el entrenamiento y permite LRs más altos.
    • Stochastic Weight Averaging (SWA): promedia los pesos del último 20% de epochs, suavizando el landscape de la pérdida y mejorando la generalización.

🔎 Búsqueda y scoring

  • Similarity sharpening (sim^3): el cubo de las similitudes amplifica la separación entre candidatos confiados y mediocres. Seguro porque los embeddings normalizados tienen similitudes positivas en la práctica y no hay riesgo de que se vuelva negativo con el cubo. Esto no tuvo mucho impacto, lo mantuve porque fue una idea de ultima hora.
  • Garbage filter: si el máximo de similitud de un crop con todo el catálogo es < 0.20, se descarta ese crop. Filtra recortes de fondo, elementos no-ropa (farolas, suelo) o imágenes demasiado ruidosas.
  • Temporal Proximity Weighting: las URLs del CDN de Inditex contienen un timestamp (ts=) que indica la colección. Se aplica un decaimiento gaussiano (σ ≈ 1 mes) sobre la diferencia temporal entre el bundle y cada producto. Las prendas sincrónicamente cercanas reciben un bonus, las de otras colecciones una penalización. Sorprendentemente importante para la puntuación final, se siente un poco como hacer trampa pero bueno.
  • Alpha Query Expansion (AQE / α-QE): refina el embedding de la query promediándolo con sus K vecinos más cercanos del catálogo antes de hacer la búsqueda final. Arrastra la query hacia el centro del cluster correcto. Implementado pero no mejoró consistentemente en la práctica, no se usa en la submission final.

🗺️ Filtrado semántico

  • Semantic Filtering (semantic_filtering.py): dos tipos de filtros sobre las similitudes raw:
    • Filtro suave (× 0.7): si el producto y el bundle pertenecen a secciones distintas (e.g., producto de "niño" en un bundle de "mujer"), penaliza su similitud.
    • Filtro duro (× 0.0): si la zona corporal del crop (UPPER, LOWER, FEET…) contradice directamente la zona corporal del producto (e.g., un crop de zapatos buscando camisetas), zeroing completo.

🔬 LoRA fine-tuning (experimento)

  • train_lora.py: fine-tunea el backbone GR-Lite directamente con LoRA + Gradient Checkpointing + Gradient Accumulation. No mejoró de manera concluyente sobre el DomainMapper con XBM: el time-to-train era mucho mayor y los embeddings pre-existentes del catálogo dejan de ser válidos (hay que recomputarlos). En hackathon, no compensa.

Built With

Share this project:

Updates