Skip to content
← All posts

Project write-up ·

HM Recommendations: Find! Then, Rank!

How I built a two-stage recommender from 31.8 million purchases, tested it on a later week, and produced recommendations for 1.37 million customers.

The H&M dataset contains 31.8 million purchase records and more than 100,000 articles. The task sounds simple: choose 12 items that each customer might buy next.

What made the project interesting was connecting the pieces. Shopping history, product similarity and recent demand all tell us something different. I needed a way to bring them together, train on examples that reflected how the system would be used, and run the final model for 1.37 million customers.

The finished system has two stages: find a manageable set of candidates, then rank them. This post walks through the choices behind it. The code and experiment records are on GitHub.

Two jobs, with different budgets

Retrieval asks which items are worth considering. Ranking asks which of those items should appear first.

I kept these jobs separate. Retrieval narrows the catalogue to at most 500 items per customer. A LightGBM model then scores those candidates and returns the top 12. That lets the first stage cover several kinds of shopping intent while the second stage spends more effort comparing a smaller set of choices.

9

Retrieval channels

Shopping history, demand, learned models, text and images.

500

Candidates at most

Combine the suggestions and keep track of their sources.

12

Recommendations

LightGBM ranks the candidates into a final short list.

The finished H&M pipeline: broad retrieval first, then a closer comparison of a much smaller set of items.

Give different kinds of evidence room

I used nine retrieval channels. Two look at popularity over the previous seven and fourteen days. Others look for repeat purchases, items often bought by the same shoppers, and products that tend to be bought close together in time.

The learned channels use a two-tower MMoE model and LightGCN. Text and image similarity add another view: an article can be relevant because of what it is or how it looks, even when purchase history is sparse.

After combining the lists, I remove duplicate items but keep a record of where each candidate came from, its score and its position in each source list. If several channels suggest the same item, the ranker can see that agreement.

I also reserve space for different channels before filling the remaining places. The point is to give complementary suggestions a chance to reach the ranker, rather than let one large list occupy the entire candidate budget.

Train on the choices the model will actually see

An important part of this project was how I built the training lists.

For each historical customer-week, I reconstruct the same 500-item candidate pool that the system would have produced at that time. I keep the candidates that were purchased in the following week and sample the other candidates uniformly from that pool.

This connects training to the actual ranking task: compare plausible recommendations for the same customer. It also keeps the mix of candidate sources close to the mix the model will encounter when making predictions.

The ranker uses customer, article and customer–article features, together with the retrieval records. I added six direct affinity features to describe how well a particular article fits a customer’s past behaviour. I then used LambdaRank, which trains the model to order items within each customer-week list.

Keep time in the right order

Each training example uses only information available before its prediction cutoff. Purchases in the following week supply the labels, not the input features.

I trained on four rolling weeks, with 25,000 customer-week groups per week. That gives 100,000 training groups; one customer can appear in more than one week.

Once the features, training size and model settings were fixed, I evaluated them once on a later week: 9–15 September 2020. I did not use that week to choose features or tune the model. Its MAP@12 was 0.034479.

Run it one batch at a time

The final prediction run covered 1,371,980 customers. I split them into 138 batches and ran retrieval, feature construction and ranking for each batch separately.

Only the final top-12 lists needed to stay on disk. The larger candidate tables could be cleared after each batch. This kept the execution bounded without changing the selected ranking model.

Customers with an eligible shopping history received personalised ranked lists. For the 9,699 customers without one, the system used recent popularity. The final output passed checks for valid article IDs, duplicate items and exactly 12 recommendations per customer.

Results

MAP@12 measures how well the recommended list matches the items a customer later purchases, giving more credit to relevant items near the top. These are the final recorded scores:

EvaluationMAP@12
Development period0.035894
Held-out later week0.034479
Kaggle public leaderboard0.03117
Kaggle private leaderboard0.03116

The offline weeks and the Kaggle leaderboard cover different customer groups and periods, so I report their scores separately. The saved metrics and experiment summary give the exact settings and results.

Selected development checkpoints · MAP@12

  1. Candidate-pool sampling0.033679
  2. Customer–item affinity0.034375
  3. LambdaRank0.035475
  4. Final 100k-group model0.035894
Selected steps in the build, all evaluated on the complete development set. The axis starts at zero. The values come from the recorded experiments.

What I took from the project

I spent as much attention on the connections between stages as on the models themselves: which items reach the ranker, how the training lists are sampled, and which information exists at the prediction cutoff. Those choices make the final scores much easier to interpret.

I am now exploring another way to recommend items in my Amazon Beauty project: a small Transformer generates product codes from a customer’s history. It is a different approach to the same practical question—how to turn a large catalogue into a useful short list.