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
Verified source
Recall@10 lift vs. popularity baseline
commit 7b5d84b · measured 2026-07-12
Recall@10 vs. popularity baselineVerified source
Recall@10, popularity baseline
commit 7b5d84b · measured 2026-07-12
Popularity baseline, Recall@10Cited 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.
H&M dataset + brand catalogues
1.37M customers, 105K articles, 31M transactions; Snitch/Fashor/Powerlook/Virgio via CSV ingestion
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
InfoNCE contrastive training
in-batch negatives, temperature τ=0.1
FAISS IndexFlatIP per brand
one BrandRegistry, one Cloud Run process serves every brand
FastAPI endpoints
/v1/{brand}/recommend and /v1/{brand}/item/{id}/similar
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
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.
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.
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.
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)
- 1Oversized Square Black Sunglasses ( Sunglasses)0.908Sunglasses
- 2Bold Oversized Black Sunglasses ( Sunglasses)0.889Sunglasses
- 3Glossy Rectangular Black Sunglasses ( Sunglasses)0.886Sunglasses
- 4Stylish Round Golden Sunglasses ( Sunglasses)0.883Sunglasses
- 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)
- 1Blue Abstract Slim Fit Shirt ( Shirts)0.915Shirts
- 2Multi Textured Abstract Shirt ( Shirts)0.913Shirts
- 3Blue Geometric Regular Fit Overshirt ( Overshirt)0.896Overshirt
- 4Geometric Relaxed Fit Overshirt ( Overshirt)0.894Overshirt
- 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)
- 1Core Lab 100% Cotton Regular Fit T-Shirt ( T-Shirts)0.896T-Shirts
- 2LEGACY Open Collar Polo T-Shirt ( T-Shirts)0.884T-Shirts
- 3Regular Fit Stretch Ribbed Polo T-Shirt ( T-Shirts)0.872T-Shirts
- 4100% Mercerized Cotton Luxe T-Shirt ( T-Shirts)0.870T-Shirts
- 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)
- 1Relaxed Fit Herringbone Trousers ( Trousers)0.802Trousers
- 2Linen Blend Baggy Trousers ( Trousers)0.738Trousers
- 3Black Regular Fit Luxe Korean Trousers ( Trousers)0.727Trousers
- 4Black Textured Slim Fit Trousers ( Trousers)0.714Trousers
- 5Black Relaxed Fit Jeans ( Jeans)0.708Jeans
4 of 5 neighbors share this item's own catalogue category.
Overshirt Double Pocket Plaid Overshirt ( Overshirt)
- 1Black Checks Relaxed Fit Overshirt ( Overshirt)0.878Overshirt
- 2Double Pocket Plaid Overshirt ( Overshirt)0.877Overshirt
- 3White Abstract Overshirt ( Overshirt)0.864Overshirt
- 4Navy Checks Relaxed Fit Overshirt ( Overshirt)0.863Overshirt
- 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)
- 1Black Slim Fit Cargo ( Cargo Pants)0.920Cargo Pants
- 2Regular Fit Stretch Cargo Pants ( Cargo Pants)0.914Cargo Pants
- 35 Pocket Relaxed Fit Cargo Pants ( Cargo Pants)0.901Cargo Pants
- 45 Pocket Stretch Cargo Joggers ( Cargo Pants)0.894Cargo Pants
- 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.
Building something like this? I'm looking for Lead or Senior Applied AI roles, and I take on select AI/ML consulting engagements.
Related projects
- Style Maitri
An AI shopping assistant for Indian weddings that searches eight store catalogues at once and will not invent a price or a size to make the sale.
- Reclaim
Cleans up disk space on Windows and shows you exactly what it will remove before it removes anything, so a safe cleanup stays safe.
- TriageIQ
Reads a new GitHub issue and tells you which component it belongs to, which already-solved issues look like it, and roughly how long it will take to close.