Part III · Ranking · chapter 11 of 19
Features
The loss is chosen — what remains is deciding what to feed in. In recommendations that is not a routine matter: here the feature space is arranged so that it dictates the choice of architecture itself. We work through why gradient boosting loses precisely here, how an embedding layer is built from the inside, and what is done to real-valued features before they are shown to a network.
- A tree cannot add up coordinates. To reproduce a single inner product in 16 dimensions, an axis-aligned tree needs 4.29 · 109 leaves; a linear layer needs 16 multiplications.
- An embedding is a one-hot multiplied by a matrix. There is no separate magic to it, and target encoding turns out to be the special case \(d = 1\).
- Encoding can destroy information irreversibly. Two items with the same average CTR and different audiences are indistinguishable after target encoding — minus 50% of the clicks, and no loss will bring them back.
- A row-wise optimiser saves 20.4 GB on a single table, and sparse gradients touch 0.041% of the parameters. The engineering of the embedding layer is half the work.
1. Why boosting loses here
The question comes up at interviews constantly and bluntly: «why do you need a neural network, just take CatBoost». There is an answer, and it consists of three concrete points rather than general words about deep learning.
Reason 1: high cardinality
How does a tree encode a category? There are two ways, and both break.
- One-hot. For a feature with a million values it simply cannot be built.
- Target encoding. It can be built, but it loses the semantics: two different items with the same average CTR become indistinguishable, even though completely different people like them.
Two items and two audience segments of equal size:
| Item | CTR in segment A | CTR in segment B | Overall CTR |
|---|---|---|---|
| X | 0.20 | 0.00 | 0.100 |
| Y | 0.00 | 0.20 | 0.100 |
After target encoding both items are the same number, 0.100. The model physically cannot tell them apart.
Showing each segment the item that suits it gives a CTR of 0.200. Not distinguishing the items gives 0.100. That is a loss of 50% of the clicks, and no loss function will bring it back: the information was destroyed while encoding the feature, before any training.
The numbers are reproduced by the script _tools/features_demo.py in this repository.
In a neural network the same category is encoded naturally — through an embedding, and two dimensions are enough to separate X and Y.
Reason 2: unstructured data
Texts, images, audio, the user's history. Features for boosting can be generated from them, but it works worse. Neural models can be built and features extracted from them — an inner product from a two-tower model, say. And that is where something more interesting comes up.
Reason 3: an embedding at the input of a tree
The coordinates of an embedding mean nothing individually: the semantics is in the vector as a whole. And every split in a tree analyses exactly one coordinate.
To distinguish \(k\) levels along each of \(d\) coordinates, an axis-aligned tree needs a grid of \(k^d\) cells:
| \(d\) | leaves at \(k=2\) | leaves at \(k=4\) | multiplications in a linear layer |
|---|---|---|---|
| 4 | 16 | 256 | 4 |
| 8 | 256 | 6.55e+04 | 8 |
| 16 | 6.55e+04 | 4.29e+09 | 16 |
| 32 | 4.29e+09 | 1.84e+19 | 32 |
At \(d=16\) with four levels per coordinate that is 4.29 · 109 leaves — more than there are samples in any dataset, and a depth of 32 levels. A linear layer does the same thing in 16 multiplications and 15 additions.
The numbers are reproduced by the script _tools/features_demo.py.
And 16 is a modest dimension: real embeddings are 64 to 256 dimensional. This is not «boosting is a bit worse», this is a fundamental incompatibility of the representation with the model.
An honest interview answer has to contain this half too. The main drawback of neural networks is a high barrier to entry: you have to know how to train networks, work with GPUs, run distributed training and put all of it into a runtime.
A checklist for «deep learning is justified»:
- plenty of data;
- high-cardinality features matter;
- unstructured data matters;
- many signals at once — likes, full views, purchases: folding them into one boosting model is painful, and separate models are expensive;
- fast adaptation to drift is needed: boosting has to be rebuilt entirely, a network can be fine-tuned incrementally.
If not a single point holds — CatBoost on aggregates will be both cheaper and better. That is not a polite caveat: a substantial share of recommendation problems is exactly like that.
The general background here is Sutton's bitter lesson: the methods that win are the ones that scale with computation rather than with human ingenuity. NLP and vision have already made that transition; recommendations are the last line where it is still under way.
2. Categorical features
A categorical feature is given by a set of values, one of which is realised on each sample. Cardinality in recommendations ranges from 2 (morning/evening) to billions (the identifier of an item).
A derivation worth being able to show on a whiteboard in twenty seconds. Represent the feature by a one-hot vector \(e_i \in \{0,1\}^n\) and apply a linear layer with no bias, that is, multiply by a matrix \(W \in \mathbb{R}^{n\times d}\):
$$ e_i W = \bigl(W_{ij}\bigr)_{j=1}^{d} $$That is simply the \(i\)-th row of the matrix. Two conclusions follow:
- \(W\) is the matrix of trainable embeddings, where every value of the feature is assigned its own vector;
- \(e_i W\) is the
embedding lookupoperation: instead of multiplying by a sparse vector we fetch the row we need. There is no separate «embedding magic» — there is an optimisation of a multiplication by a one-hot.
And a pleasant consequence people like to ask about: at \(d = 1\) the model can learn the average CTR of a value, that is, target encoding is the special case of an embedding of dimension one. Everything an embedding gives beyond that is the extra dimensions in which «who exactly likes it» fits.
What size of embedding to take
The same size for every feature is suboptimal. It should depend on cardinality — a wide vector is not needed to describe a feature with two values — and on informativeness, that is, on how useful the feature is for the task.
A lower bound, informational. Suppose an embedding consists of \(d\) elements of \(s\) bits. In total \(2^{ds}\) values are encoded, so to distinguish \(n\) values it is enough that
$$ d\,s = \log_2 n $$Google's practical heuristic: \(d = 6\sqrt[4]{n}\).
| Cardinality | \(\log_2 n\), bits | \(6\sqrt[4]{n}\) | Memory in float32 |
|---|---|---|---|
| 1e+03 | 10.0 | 34 | 0.00 GB |
| 1e+05 | 16.6 | 107 | 0.04 GB |
| 1e+07 | 23.3 | 337 | 13.50 GB |
| 1e+09 | 29.9 | 1067 | 4267.87 GB |
The numbers are reproduced by the script _tools/features_demo.py.
The gap is enormous and it is meaningful. At \(n = 10^7\) the informational estimate is 23 bits, less than a single float32 number: to merely distinguish the values, \(d = 1\) would be enough. All the rest of the capacity goes into semantics rather than into identification.
The row for \(n = 10^9\) in the table above is 4.3 terabytes for a single feature. In production the typical dimensions are 32 to 128, not 337 and certainly not 1067.
The heuristic is useful as a guide to the order of magnitude and as a reminder that the size should grow with cardinality. A more honest route is dimension optimisation: choosing embedding sizes during training rather than fixing them as a constant.
3. The engineering of an embedding layer
Here begins what distinguishes the conversation of someone who has done this from a retelling of a paper. Three techniques people ask about.
| Technique | Problem | Solution |
|---|---|---|
| Batched lookup | a separate lookup per feature means many small operations on the GPU, and each one carries the overhead of a kernel launch | combine the embeddings of all features into one matrix and do a single lookup with an offset that puts each feature in its own region |
| Sparse gradients | only a small share of the rows occurs in a batch; for the rest the gradient is exactly zero | a sparse optimiser updates only the rows with a non-zero gradient; under distributed training the zero gradients are not sent at all |
| Row-wise optimiser | Adam keeps two moments per parameter — that is a tripling of memory | keep two statistics per row rather than per parameter: memory falls almost to the size of the table itself |
A feature of cardinality 10 M with an embedding of 256:
| the table itself in float32 | 10.2 GB |
| the table + two Adam moments | 30.7 GB (3 times more) |
| the table + row-wise statistics | 10.32 GB — 0.8% more than the table |
| saving | 20.4 GB, or a factor of 3.0 |
And about sparsity: a batch of 4096 samples touches no more than 4096 rows out of 10 M — that is 0.0410% of the table. Exactly that share of the parameters has to be sent and updated; everything else is zeros.
The numbers are reproduced by the script _tools/features_demo.py.
Parameters with a sparse gradient are updated far less often than dense ones: the row of a rare value will see a gradient a few times per epoch, while the weights of the MLP see one on every batch.
So for embeddings the learning rate is usually increased relative to the rest of the network. This is not tuning for tuning's sake but compensation for a different update frequency — and a frequent reason why «the embeddings somehow will not train».
A large separate topic is what to do when the tables do not fit on a GPU: sharding (by coordinate or by feature value), offloading to the CPU with asynchronous updates, and hashing. The hashing trick and multi-hash are taken apart in chapter 8 together with the arithmetic of collisions, and are not repeated here.
4. Real-valued features
Most features in recommendations are real-valued: counters, statistics over sets, time statistics such as «time since the last purchase in the category», characteristics of the user and of the item.
Three reasons, and all three are specific to neural networks:
- Scale. Networks are sensitive to scale: features with large values contribute a large gradient and dominate training.
- Outliers are always there, and they either break training or degrade quality badly.
- Missing values a network cannot handle without a separate decision.
Note that trees have none of these problems — they are invariant to monotone transformations and handle missing values natively. All this fuss arises as the price of moving to networks, and it is worth saying so out loud in an interview.
Five transformations
1. Logarithm. \(F(x) = \log(x+1)\) reduces skew. Real data often holds log-normal distributions: the difference between 1 and 2 matters, the one between 100001 and 100002 no longer does. The logarithm expresses exactly that.
2. Sigmoid. \(F(x) = \sigma(\gamma x + \beta)\) squeezes the feature into a bounded range along with the outliers. It is important to normalise the values before the sigmoid, otherwise the derivative at the edges is close to zero and the gradients vanish. Several sigmoids with different trainable \(\gamma, \beta\) can be applied.
3. The cumulative distribution function. \(F(x) = P(X \le x)\) maps a value to its quantile and brings the feature to a uniform distribution on \([0,1]\). It removes both the skew and the outliers in one move. Computing an empirical CDF over all the data is expensive, so a quantile approximation over reference points is used.
4. Periodic functions.
$$ F(x) = \bigl[\sin(2\pi c_1 x),\, \cos(2\pi c_1 x),\, \sin(2\pi c_2 x),\, \cos(2\pi c_2 x), \dots\bigr] $$For time features above all. The coefficients \(c_i\) can be fixed (the frequencies «hour», «day», «week») or trainable. The mechanism is the same as for positional embeddings in a transformer.
5. Quantisation. \(F(x) = \sum_i \mathbb{1}[a_i < x \le b_i]\cdot i\) turns a real-valued feature into a categorical one by quantile bins. Its drawback is fundamental: the order relation is lost — both between bins and inside a bin.
In raw form, 23 and 0 turn out to be the most distant values possible, though they are adjacent hours. A sine and a cosine of one frequency place time on a circle:
| Pair of hours | raw \(|h_1 - h_2|\) | distance on the circle |
|---|---|---|
| 23 and 0 | 23 | 0.2611 |
| 23 and 12 | 11 | 1.9829 |
| 0 and 1 | 1 | 0.2611 |
| 11 and 13 | 2 | 0.5176 |
The distance between 23 and 0 becomes exactly the same as between 0 and 1 — 0.2611, the smallest possible. And the most distant pair turns out, as it should, to be 23 and 12, the opposite points of the day.
The numbers are reproduced by the script _tools/features_demo.py.
Several frequencies give several circles of different scale, so the model sees the hour, the day of the week and the season at once.
Piecewise linear encoding
Quantisation catches non-linearity but produces steps: inside a bin the values are indistinguishable, and at a boundary the prediction jumps. The idea of PLE is to encode not only the number of the bin but also the position inside it.
A value \(x\) is encoded by a vector of length \(T\), one component per bin: to the left of the current bin ones, to the right zeros, and exactly one fractional component — the share of the bin travelled.
$$ \mathrm{PLE}(x)_t = \begin{cases} 1, & x \ge b_t \\[2pt] \dfrac{x - b_{t-1}}{b_t - b_{t-1}}, & b_{t-1} \le x < b_t \\[2pt] 0, & \text{otherwise} \end{cases} $$Then \(\mathrm{Linear}(\mathrm{PLE}(x)) = v_0 + \sum_t e_t v_t\) gives a continuous piecewise linear function: non-linearity as with a one-hot, but with no discontinuities and no loss of resolution inside a bin.
We approximate \(f(x) = x^2\) on \([0,1]\) with a linear model on top of an encoding:
| Bins | MSE, one-hot by bins | MSE, piecewise linear | How many times better |
|---|---|---|---|
| 2 | 2.649e-02 | 2.081e-03 | 12.7 |
| 4 | 6.893e-03 | 1.301e-04 | 53.0 |
| 8 | 1.741e-03 | 8.130e-06 | 214.1 |
| 16 | 4.358e-04 | 5.081e-07 | 857.6 |
The numbers are reproduced by the script _tools/features_demo.py.
Note not only the gap but how it grows with the number of bins: for piecewise constant encoding the error falls as \(T^{-2}\), for piecewise linear as \(T^{-4}\). Adding bins is four times more profitable for piecewise linear encoding.
- Compare the three curves. A raw feature in a linear model gives only a straight line. A one-hot by bins catches the non-linearity but produces steps.
- Move \(x\) inside one bin: with a one-hot the output stands still, with PLE it moves smoothly. That is the main difference and the main argument.
- Increase the number of bins: the curve approximates an ever more complex dependence — at the price of \(T\) parameters per feature.
What to say in an interview: «PLE is a compromise between a raw feature and binning: non-linearity as with a one-hot, but with no discontinuities and no loss of resolution inside a bin».
A practical technique from the Airbnb work on deep learning in search: after the transformations, look at the smoothness of the feature's distribution.
A smooth distribution close to normal is a sign that the transformation was chosen well. Humps, heavy tails and spikes at particular values are a signal that something is wrong with the feature: most likely there is a placeholder instead of a missing value, a clip from above, or two different entities merged into one column.
This is a cheap check that catches a whole class of data errors before training.
5. The outputs of other models as features
The third kind of input is embeddings that came from outside: content vectors, the outputs of a two-tower model, the results of pre-training. Three things are done with them.
- \(l_2\) normalisation — the same argument as in chapter 7: we remove the influence of the norm, which correlates with popularity.
- PCA for dimensionality reduction.
- Vector quantisation: turn the embedding into a categorical feature by mapping vectors to elements of a codebook — train k-means, say, and take the number of the centroid. The development of this idea is semantic IDs.
The main feature of YouTube's ranking model is the identifier of the video. It used to use the hashing trick; that was replaced with semantic IDs by training an RQ-VAE on top of content embeddings of the videos. The result was a noticeable gain on the cold slice.
The most interesting detail, and exactly the one worth being able to explain: using the content embedding directly works worse than the discretised semantic ID.
Why is that? A dense content vector is a «soft» feature: the model generalises from it but cannot memorise the specifics of a particular video. A discrete code gives both at once: the shared prefix is responsible for generalisation, the full code for memorisation. Discretisation here is not a loss of information but a separation of two regimes that are mixed together in a dense vector.
The flip side of the same argument is the problem with hashing: the identifiers it produces carry no semantics, and for a new item a multi-hash gives a combination the model has never seen. Full reliance on memorisation and zero generalisation.
Interview questions
Why is gradient boosting not enough in recommendations?
Three concrete reasons. High cardinality: a one-hot over a million values cannot be built, and target encoding loses the semantics — two items with the same average CTR and different audiences are merged into one number, and that costs 50% of the clicks in a simple example with two segments.
Unstructured data: texts, pictures, history — features can be generated from them, but it works worse.
And the main one: embeddings at the input of a tree work badly in principle. The coordinates mean nothing individually, and a split looks at one coordinate; to distinguish 4 levels along each of 16 dimensions you need 4.29 billion leaves, while a linear layer gets by with 16 multiplications.
Plus two practical arguments: many signals at once, and drift — boosting has to be rebuilt entirely, a network can be fine-tuned.
And when is boosting better instead?
When not a single point of the checklist holds: plenty of data, important high-cardinality features, unstructured data, many signals, a need for fast fine-tuning. If none of that is there, CatBoost on aggregates will be both cheaper and better.
The main drawback of networks is the barrier to entry: GPUs, distributed training, inference in a runtime. That is a real cost, and pretending it does not exist is not a good look in an interview.
Show that an embedding is a linear layer.
Represent the category by a one-hot vector \(e_i \in \{0,1\}^n\) and multiply by a matrix \(W \in \mathbb{R}^{n \times d}\) with no bias. The result \(e_i W\) is the \(i\)-th row of \(W\). So \(W\) is the embedding table, and a lookup is simply an optimisation of a multiplication by a sparse vector.
A useful consequence: at \(d = 1\) the model learns the average CTR of a value, that is, target encoding is the special case of an embedding of dimension one. Everything an embedding gives beyond that is the dimensions in which «who exactly likes it» fits.
How do you choose the size of an embedding?
It should depend on the cardinality and on the informativeness of the feature; the same size for everything is suboptimal.
The informational estimate: \(d\cdot s = \log_2 n\) bits. Google's heuristic: \(d = 6\sqrt[4]{n}\). The gap between them is enormous — at \(n=10^7\) that is 23 bits against d = 337 — and it is meaningful: almost all the capacity goes into semantics rather than identification.
The heuristic is not taken literally: at \(n=10^9\) it gives 1067 and 4.3 terabytes for one feature. In production the typical dimensions are 32 to 128, and the honest route is to choose the dimensions during training.
What optimisations of an embedding layer do you know?
Batched lookup: combine the tables of all features into one matrix and do a single lookup with an offset — otherwise you get many small operations on the GPU with the overhead of a kernel launch.
Sparse gradients: a batch of 4096 samples touches no more than 4096 rows out of 10 M, that is, 0.041% of the table; the rest of the gradients are zeros and need neither updating nor sending between cards. An important subtlety: for such parameters the learning rate is worth increasing, because they are updated far less often than dense weights.
Row-wise optimiser: keep Adam's statistics per row rather than per parameter. A table of 10 M × 256 takes 10.2 GB, with Adam's moments 30.7 GB, and with row-wise statistics 10.32 GB, that is, 0.8% more than the table.
Plus sharding, offloading to the CPU and the hashing trick when the tables do not fit on a GPU.
Why transform real-valued features, and how?
Because networks are sensitive to scale (large values dominate the gradient), break on outliers and cannot handle missing values. Trees have none of these problems — this is the price of moving to networks.
Five transformations: a logarithm against skew; a sigmoid that squeezes outliers (with normalisation before it, otherwise the gradients vanish); the cumulative distribution function, which brings the feature to a uniform distribution; periodic functions for time; quantisation by quantiles.
Quantisation has a fundamental drawback — the order relation is lost both between bins and inside a bin. Piecewise linear encoding repairs that.
Why can't the hour be fed as a number from 0 to 23?
Because in raw form 23 and 0 are the most distant pair possible, though they are adjacent hours. The model is forced to spend capacity on learning the join at the boundary of the day.
A sine-cosine pair of one frequency places time on a circle: the distance between 23 and 0 becomes 0.2611 — exactly the same as between 0 and 1, and the smallest possible. The most distant pair turns out to be 23 and 12, the opposite points of the day, as it should be.
Several frequencies give several circles of different scale — the hour, the day of the week and the season at once. The mechanism is the same as for positional embeddings in a transformer.
What is PLE and why is it better than binning?
Piecewise linear encoding: a vector of length T where to the left of the current bin there are ones, to the right zeros, and exactly one component is fractional — the share of the bin travelled. A linear layer on top of such a vector gives a continuous piecewise linear function.
The difference from a one-hot by bins: inside a bin the output is not a constant but moves linearly. On approximating \(x^2\) with 16 bins the MSE differs by a factor of 857. And more important than the gap is how it grows: for piecewise constant encoding the error falls as \(T^{-2}\), for piecewise linear as \(T^{-4}\).
Why does a discretised semantic ID work better than a dense content vector?
Because it separates two regimes that are mixed together in a dense vector. A dense content vector is a «soft» feature: the model generalises from it but cannot memorise the specifics of a particular object. A discrete hierarchical code gives both: the shared prefix is responsible for generalising to similar objects, the full code for memorising the particular one.
This is exactly the case where discretisation does not lose information but structures it. The flip side of the same argument is the hashing trick: its identifiers carry no semantics, so for a new object you get a combination the model has never seen — full reliance on memorisation and zero generalisation.
One-screen cheat sheet
Against boosting
Cardinality, unstructured data, embeddings. At d=16 and k=4 a tree needs 4.29e9 leaves.
For boosting
If not a single point of the checklist holds — CatBoost on aggregates is cheaper and better.
Embedding
one-hot × W = a row of W. Target encoding is the same embedding at d = 1.
Dimension
From below \(\log_2 n\) bits, the heuristic \(6\sqrt[4]{n}\). The gap is capacity for semantics, not for the ID.
Engineering the layer
Batched lookup, sparse gradients (0.041% of rows), row-wise Adam: 30.7 → 10.32 GB.
Real-valued
log1p, sigmoid, CDF, sin/cos, quantisation. Scale, outliers, missing values are problems of networks, not trees.
Time
23 and 0 in raw form are the most distant pair. On a circle 0.2611, as for 0 and 1.
PLE
Non-linearity with no steps. The error falls as \(T^{-4}\) instead of \(T^{-2}\).
Primary sources
- P. Covington, J. Adams, E. Sargin. Deep Neural Networks for YouTube Recommendations, RecSys 2016 — where the dimension heuristic and much else came from.
- Y. Gorishniy, I. Rubachev, A. Babenko. On Embeddings for Numerical Features in Tabular Deep Learning, NeurIPS 2022 — piecewise linear encoding.
- B. Coleman, W.-C. Kang et al. Unified Embedding: Battle-Tested Feature Representations for Web-Scale ML Systems, NeurIPS 2023.
- M. Haldar et al. Applying Deep Learning to Airbnb Search, KDD 2019 — the smoothness of distributions as a feature diagnostic.
- A. Singh, T. Vu, N. Mehta et al. Better Generalization with Semantic IDs: A Case Study in Ranking for Recommendations, 2023 — the case in section 5.
- Z. Liu et al. Monolith: Real Time Recommendation System With Collisionless Embedding Table, 2022 — on fine-tuning under drift.
- R. Sutton. The Bitter Lesson, 2019.
- The numbers in this chapter:
_tools/features_demo.pyin this repository.