﻿# forty nodes, quite a few features.

From 22.6 million listings to 40 nodes: how AmazMe reused user histories in its winning Avito ML Cup 2025 solution.

Author: Vlad Kostoglodov. Language: en.
Archive date: ~2025-05. Current version prepared: 2026-10-14.
Date basis: Around the competition's end in May 2025; exact original publication date is unverified.
Status: ready.
[Profile and technical work JSON](https://vk0st.com/facts.json): public profile, concrete work and source references.
[Human-readable article](https://vk0st.com/writing/avito-ml-cup/).

Avito had about **22.6 million listings**. The task was to recommend **40 nodes**: groups of listings such as “iPhone 13.” A click on one phone and a contact request for another could point to the same target, but they carried different evidence of intent.

Our team, **AmazMe**, finished first in Avito ML Cup 2025, with **Recall@40 of 0.22375** on the [private leaderboard](https://ods.ai/competitions/avitotechmlchallenge2025/leaderboard/private). Grigory Vyaznikov and I presented the solution at [Data Fest 2025](https://ods.ai/tracks/df25_challengeavitotech).

One part of the solution was especially reusable: a learned matrix of node relationships. Feed it click counts and it produces one set of scores. Feed it recent activity or contact days and it produces another. The ranker could compare those views of the same user without a separate model fit for each history.

## Pick the right-sized thing to recommend

The catalogue had three levels: **53 broad categories**, **424,068 nodes**, and about **22.6 million listings**. A category might be “Phones,” a node “iPhone 13,” and a listing a particular phone offered in a particular city.

At node level, activity on different listings contributes to the same target. Listing details, such as location, remain available for features later. Modeling every advertisement separately would spread those interactions across a much larger space.

We kept the top **30,000 nodes**, generated roughly **300 candidates per user**, and used a boosting ranker to choose **40**. The ranker could spend its effort on that shortlist, but it could never rescue a relevant node the candidate generators had missed.

We used the last **14 days** to validate candidate generation and train the ranker, retaining the baseline's temporal setup. History was shallow relative to the target period: adding weekly slices would change both the available history and the prediction horizon, muddying the comparison.

## One learned matrix, several histories

Linear autoencoders formed a substantial part of the solution. We explored [EASE](https://arxiv.org/abs/1905.03375) and related models, including SANSA, ELSA, RDLAE, and data-adaptive normalization. The scoring operation is a multiplication: user history `X` goes through a learned matrix `W` of node relationships.

Imagine a user clicks listings in the same node ten times. Those clicks could all happen in one afternoon, or they could be spread across several days. The count for that node is ten either way. The number of active days separates the two histories.

We built sparse interaction matrices for counts, contact indicators, time decay, position decay, and the number of distinct days with clicks or contacts. Each keeps a different detail from the event stream. Time decay weights freshness; position decay weights where an event sits in the history. Contact indicators and contact days preserve evidence of attempts to reach sellers.

Once `W` was fitted, these histories could all use it:

```text
count_scores   = X_count        @ W
recent_scores  = X_time_decay   @ W
contact_scores = X_contact_days @ W
```

The node relationships stay fixed. Our hypothetical user's ten clicks contribute according to repetition in the count view and according to days in the active-day view. That changes how strongly this part of the history activates the same links. Across a history with several nodes, it can also change their relative contributions.

![Three distinct history streams pass through one thin learned operator and converge into a candidate path.](https://vk0st.com/media/articles/avito-ml-cup-v2.webp)

Several histories reuse one learned operator and give the ranker different evidence.

Producing another signal still costs a sparse input and a multiplication, but the model fit has already happened. These signals can disagree, and the ranker gets to use that disagreement. They also share a limit: changing `X` cannot teach `W` a new node relationship.

## What goes into W

The choice of linear model determines which relationships `W` can learn.

EASE sets the weight matrix's diagonal to zero, so a node cannot reconstruct itself through its own diagonal weight. [RDLAE](https://arxiv.org/abs/2305.12922) relaxes that constraint:

```text
EASE:   diag(W) = 0
RDLAE:  diag(W) <= xi
```

The diagonal is the direct route from a node in the history back to that same node in the score vector. RDLAE's bound gives the model some freedom to retain this input signal alongside relationships to other nodes.

[Data-Adaptive Normalization](https://arxiv.org/abs/2504.05805) addresses a different problem: popular nodes and highly active users can dominate interaction statistics, while strong local relationships can keep recommendations in a narrow neighbourhood.

It adjusts scaling on both sides. Item-side normalization changes the influence of popular and rare nodes; user-side normalization changes how histories contribute to learned correlations. This changes the evidence used to fit `W`. Relaxing the diagonal changes which relationships `W` may retain. We could then reuse the fitted matrix with the history views above.

## A click has several possible meanings

Counts and active days describe repetition, but opening a listing and trying to reach its seller are also different actions. We combined action types and gave contacts more weight than clicks in the linear models.

Contact statistics needed smoothing. A node with very few observations could get an impressive ratio from a small denominator. Bayesian weighting mixes a global ratio with local counts: when counts are small, the prior carries more of the estimate; as observations accumulate, the local ratio gains influence.

The slides compare a fixed regularization parameter `C`, an adaptive choice based on mean counts, and additive smoothing. These give a sparse contact statistic a usable scale before it becomes model input.

Our enhanced RDLAE recipe also used a logarithmic transform:

```text
X = X_bool * log(1 + bayesian_weighted)
```

Smoothing stabilizes the statistic. The logarithm compresses large weights so they do not contribute in direct proportion to their size. The binary mask keeps the input tied to observed interactions: a contact weight can strengthen an existing entry without creating one where `X_bool` is zero.

## Give the sequence two views

All these matrices aggregate events. They can preserve recency weights or active-day counts, but they lose the sequence itself. We used SASRec from [RePlay](https://github.com/sb-ai-lab/RePlay/) on node sequences and category sequences to keep order. Nodes track the specific targets; categories give a broader view of the same activity.

The [SASRec Barlow Twins variant](https://github.com/RAZVOR/barlow_twins_sasrec), credited in our slides to Ivan Razvorotnev, Evgeny Frolov, and Marina Munkhoeva, creates two randomly augmented views of each sequence. SASRec encodes both, and a cross-correlation matrix compares their embedding dimensions across a batch.

Its loss pushes matching dimensions toward correlation one and different dimensions toward zero: the two views should agree without filling the embedding with redundant information. This supplements the prediction loss, with coefficients controlling the extra objective's weight.

We then combined the click and contact outputs of this model:

```text
score = (SasRecBT_click + 0.5 * SasRecBT_is_contact)
        * sqrt(bayesian_weighted)
```

The `0.5` weights the contact model's output in this blend, separately from the action weights in the linear models. The square root brings the Bayesian contact weighting into the score as a multiplier that grows more slowly than the underlying weight.

The slides compare generators on their own in this intermediate evaluation snapshot:

| Generator | Recall@40 |
| --- | ---: |
| SASRec | 0.150572 |
| SASRec Barlow Twins | 0.158431 |
| SASRecBT with the click/contact recipe | 0.183645 |
| EASE DAN | 0.173511 |
| RDLAE with the enhanced input recipe | 0.199538 |

RDLAE had the strongest Recall@40 in this comparison, which helps explain why linear models remained central to candidate generation. These figures are separate from the final private leaderboard and do not isolate each generator's contribution to the ensemble. A weaker standalone list can still supply candidates or features the others miss.

## Let the ranker resolve the disagreements

By the time a candidate reached the final ranker, it had scores from several models and history views. The ranker had **44 features** covering those model scores and ranks, contact ratios at node and category level, the user's last seen or contacted category, and location counts within a node.

A raw score carries a model's strength of preference; rank gives the candidate's position in that model's list. Keeping both lets the boosting model compare signals even when their score scales differ.

Listing detail also returns here. Counts for the user's most common location within a node add information that disappeared when interactions were aggregated to nodes. Node and category contact statistics give context at two resolutions, while the last seen or contacted category supplies a recent signal.

Training sampled **10 positive and 30 negative examples per user**. We trained CatBoostRanker and LightGBM, split folds **by user**, and ran Optuna for each fold, evaluating Recall@40. A user's candidates stayed together on one side of the fold split.

For the hypothetical user with ten clicks, the ranker could now compare count-based scores with active-day and recent-history scores, alongside contact evidence, sequence predictions, and location context. It chose the final **40 nodes** from roughly **300 candidates**, with several views of the same history available for that decision.
