← All projects

Multimodal Fashion Recommender

A two-tower recommender that beats a popularity baseline 3.06× on Recall@10, reached by diagnosing a full representation collapse down to a warmup-schedule fix, and by writing the ADR that draws an honest line on how far one shared process can scale.

Last updated 2026-10-13 · 4 min read

Try it on Hugging Face ↗Source on GitHub ↗

  • Verified source

    Recall@10 lift vs. popularity baseline

    commit 7b5d84b · measured 2026-07-12

    Recall@10 vs. popularity baseline
  • Verified source

    Recall@10, popularity baseline

    commit 7b5d84b · measured 2026-07-12

    Popularity baseline, Recall@10
  • Cited in provenance.md

    `README.md:113-124`

    verified against source 2026-10-10

    NDCG@10 / MRR

The problem

Fashion e-commerce sites need a good "you might also like", but the catalogue is huge, customers almost never leave ratings, and what makes two items similar depends on both how they look and what they actually are. A plain-text search misses the visual half; a plain-image search misses the descriptive half.

This project targets small and mid-size Indian D2C fashion brands specifically: businesses that want real recommendations without hiring a machine-learning team or standing up their own infrastructure to get them.

How it works

The model is trained on the H&M Kaggle dataset, 1.37 million customers, 105,000 articles, 31 million past purchases, and it's a "two-tower" design: one tower turns an item into a single vector of numbers, the other turns a shopper into one, and training pulls a shopper's vector close to the items they actually bought.

The item tower doesn't learn to see or read from scratch. It takes a frozen CLIP model's image embedding (CLIP already knows how to describe what's in a photo) and a frozen SBERT model's text embedding (SBERT already knows what a product description means), concatenates them, and passes the combination through a small neural network down to one 256-number vector. The user tower runs a small Transformer over a shopper's last 20 purchases and pools that into the same 256-number space.

Training uses a contrastive loss (InfoNCE): for each shopper in a batch, their real next purchase should sit closer in that 256-dimensional space than every other item in the batch. At serving time, a FAISS index (a fast nearest-neighbor search library) finds the closest items to a shopper's vector, and a Groq-hosted LLM writes a one-sentence, cached explanation for why each item was picked.

The same pipeline was extended to ingest real Indian brand catalogues: Snitch, Fashor, Powerlook, Virgio, from CSV, so a brand can get "more like this" recommendations from day one, before it has any purchase history of its own on the platform.

Architecture

Two towers trained together in one shared 256-dimensional space, served through a per-brand FAISS index behind one shared process.

  1. H&M dataset + brand catalogues

    1.37M customers, 105K articles, 31M transactions; Snitch/Fashor/Powerlook/Virgio via CSV ingestion

  2. Item Tower and User Tower

    trained jointly, project into the same 256-dim space

    Item Tower

    frozen CLIP (512d) + frozen SBERT (384d) → concat → MLP 896→512→256, L2-normalized

    User Tower

    last 20 purchases → 2-layer/4-head Transformer → masked mean-pool → MLP → 256d

  3. InfoNCE contrastive training

    in-batch negatives, temperature τ=0.1

  4. FAISS IndexFlatIP per brand

    one BrandRegistry, one Cloud Run process serves every brand

  5. FastAPI endpoints

    /v1/{brand}/recommend and /v1/{brand}/item/{id}/similar

  6. Groq LLM explainer + HuggingFace Space UI

    one-sentence "why this" explanation, Redis/LRU-cached

H&M's own 2M-row transaction history is deliberately excluded from live serving via a BRANDS_ENABLED flag, see the FAISS scaling decision below.

Key decisions, and why

  1. Frozen CLIP + SBERT fusion, not end-to-end fine-tuning

    The image and text encoders are frozen and only the fusion MLP on top of them is trained. That means the model transfers to any fashion catalogue, including Indian ethnic wear it never saw during training, without retraining the encoders themselves.

  2. Temperature 0.1, learning rate 3e-4, and a 500-step warmup, after the standard config collapsed

    The textbook contrastive-learning config (temperature 0.07, learning rate 1e-3, no warmup) caused total representation collapse: every item and user vector converged to the same point in space, loss looked deceptively low, and the model learned nothing. Raising the temperature, lowering the learning rate, and, the single most important fix, adding a 500-step warmup period, resolved it.

  3. FAISS, not a managed vector database, for up to 4 brands, a written trade-off, not an oversight

    For a handful of brands, a plain FAISS index that lives in local process memory needs zero extra infrastructure and behaves identically in development and production. An architecture decision record spells out the math on when that stops being true and lays out a 4-phase migration path (today's single shared process, then lazy-loading with an LRU cache, then a real vector database, Qdrant, then per-brand service instances) for when it does.

  4. Evaluated on exactly the 10,556-item pool the production index actually serves

    Recall is measured against the same active item pool FAISS serves at inference time, not a larger offline-only pool, so the reported number is what a real user would actually experience, not an optimistic upper bound.

Results, the honest numbers

Verified source

Recall@10 lift vs. popularity baseline

commit 7b5d84b · measured 2026-07-12

Recall@10 vs. popularity baseline · also 2.12× vs. a co-purchase baseline (0.0155) and ahead of text-only SBERT (0.0248); temporal held-out test, 110,390 users

Verified source

Recall@10, popularity baseline

commit 7b5d84b · measured 2026-07-12

Popularity baseline, Recall@10 · the baseline the 3.06× lift is measured against, same README table, same commit

Cited in provenance.md

`README.md:113-124`

verified against source 2026-10-10

NDCG@10 / MRR

Cited in provenance.md

`COST.md:26-27,64`

verified against source 2026-10-10

Serving cost · with a warm explanation cache

Cited in provenance.md

`README.md:178-190`

verified against source 2026-10-10

Honest caveat: new-brand personalization · for a brand new to the platform, the personalized /recommend endpoint runs on synthetic users because the user tower never saw that brand's real purchase history; only the content-based /similar endpoint is a validated day-one capability

Diagnosed a full representation collapse to a specific warmup-schedule fix, then wrote the scaling ADR that says exactly where this architecture stops working

Training with the paper-standard contrastive-learning recipe, temperature 0.07, learning rate 1e-3, no warmup, produced a suspiciously clean-looking loss curve and a completely useless model: every item and user embedding had converged to the same point in the 256-dimensional space. Cosine similarity between any two items was effectively 1.0. Nothing had been learned; the loss function had just found the cheapest way to look small.

The fix came from empirical iteration rather than a known formula: raising the temperature to 0.1, dropping the learning rate to 3e-4, and, the change that mattered most, adding a 500-step warmup before the learning rate reached its full value. Warmup gives the towers a chance to spread out into distinct regions of the space before the optimizer starts taking large steps, which is exactly what a cold, aggressive learning rate short-circuits.

Once the model worked, a second question came up: how many brands can one shared FAISS process actually hold? An architecture decision record ran the numbers rather than guessing: three real brand catalogues fit comfortably in roughly 250–400MB, but H&M's own 2-million-row transaction history alone would push the current 4GiB Cloud Run instance over budget, scaling to include it is estimated to need 8GiB, and even that is a tight fit, not comfortable headroom. Rather than ship a serving path that would eventually fall over, H&M is deliberately excluded from live serving behind a BRANDS_ENABLED flag, with a 4-phase migration path, lazy-loading with an LRU cache, then a real vector database like Qdrant, then per-brand service instances, written down for when the catalogue count grows past what one process can hold.

Try it

500 real items from snitch's live catalogue, fused by the trained item tower this project actually ships and laid out in three dimensions. Pick one of the six and you see the five items the model's own space actually returned for it, in its order, with whether each shares its real catalogue category.

What the picture is, and what it is not. Every neighbor here was ranked in the model's own 256 dimensions before anything was drawn. What you see is a t-SNE layout, chosen to look tidy, so two dots sitting together are not necessarily each other's nearest neighbours and a highlighted item can land anywhere on screen. That is why the highlight follows product ids rather than distance. This is one brand's catalogue the model never trained on -- the day-one, no-purchase-history case the results above already name -- run through open_clip ViT-B-32 (openai) and sentence-transformers/all-MiniLM-L6-v2, fused by checkpoints/best.pt item_tower.* -- concat(512+384) -> MLP(512) -> 256, L2-normalized.

Sunglasses Bold Rectangular Black Sunglasses ( Sunglasses)

  1. 1Oversized Square Black Sunglasses ( Sunglasses)0.908Sunglasses
  2. 2Bold Oversized Black Sunglasses ( Sunglasses)0.889Sunglasses
  3. 3Glossy Rectangular Black Sunglasses ( Sunglasses)0.886Sunglasses
  4. 4Stylish Round Golden Sunglasses ( Sunglasses)0.883Sunglasses
  5. 5Sleek Rectangle Black Sunglasses ( Sunglasses)0.873Sunglasses

5 of 5 neighbors share this item's own catalogue category.

Shirts Multi Abstract Slim Fit Shirt ( Shirts)

  1. 1Blue Abstract Slim Fit Shirt ( Shirts)0.915Shirts
  2. 2Multi Textured Abstract Shirt ( Shirts)0.913Shirts
  3. 3Blue Geometric Regular Fit Overshirt ( Overshirt)0.896Overshirt
  4. 4Geometric Relaxed Fit Overshirt ( Overshirt)0.894Overshirt
  5. 5Tropiblu Abstract Grey Shirt ( Shirts)0.884Shirts

3 of 5 neighbors share this item's own catalogue category.

T-Shirts Nintendo Oversized T-Shirt ( T-Shirts)

  1. 1Core Lab 100% Cotton Regular Fit T-Shirt ( T-Shirts)0.896T-Shirts
  2. 2LEGACY Open Collar Polo T-Shirt ( T-Shirts)0.884T-Shirts
  3. 3Regular Fit Stretch Ribbed Polo T-Shirt ( T-Shirts)0.872T-Shirts
  4. 4100% Mercerized Cotton Luxe T-Shirt ( T-Shirts)0.870T-Shirts
  5. 5Regular Fit Graphic Print T-Shirt ( T-Shirts)0.867T-Shirts

5 of 5 neighbors share this item's own catalogue category.

Trousers Black Slim Fit Gurkha Trousers ( Trousers)

  1. 1Relaxed Fit Herringbone Trousers ( Trousers)0.802Trousers
  2. 2Linen Blend Baggy Trousers ( Trousers)0.738Trousers
  3. 3Black Regular Fit Luxe Korean Trousers ( Trousers)0.727Trousers
  4. 4Black Textured Slim Fit Trousers ( Trousers)0.714Trousers
  5. 5Black Relaxed Fit Jeans ( Jeans)0.708Jeans

4 of 5 neighbors share this item's own catalogue category.

Overshirt Double Pocket Plaid Overshirt ( Overshirt)

  1. 1Black Checks Relaxed Fit Overshirt ( Overshirt)0.878Overshirt
  2. 2Double Pocket Plaid Overshirt ( Overshirt)0.877Overshirt
  3. 3White Abstract Overshirt ( Overshirt)0.864Overshirt
  4. 4Navy Checks Relaxed Fit Overshirt ( Overshirt)0.863Overshirt
  5. 5Keystone Black Solid Overshirt ( Overshirt)0.863Overshirt

5 of 5 neighbors share this item's own catalogue category.

Cargo Pants Loose Fit Stretch Cargo Pants ( Cargo Pants)

  1. 1Black Slim Fit Cargo ( Cargo Pants)0.920Cargo Pants
  2. 2Regular Fit Stretch Cargo Pants ( Cargo Pants)0.914Cargo Pants
  3. 35 Pocket Relaxed Fit Cargo Pants ( Cargo Pants)0.901Cargo Pants
  4. 45 Pocket Stretch Cargo Joggers ( Cargo Pants)0.894Cargo Pants
  5. 5Baggy Fit Cargo Pants ( Cargo Pants)0.891Cargo Pants

5 of 5 neighbors share this item's own catalogue category.

What this means if you need something similar

If you're building contrastive or two-tower retrieval and your loss curve looks suspiciously clean, this is worth checking first: a collapsed representation can look like convergence. And if you're deciding how far a single-process architecture can scale, write the math down before you find out the hard way in production.