RecSys · textbook
Trainer Widgets Revision About All chapters ← Classics Two towers →

Part II · Candidate generation · chapter 6 of 19

Matrix factorisation, ALS and fold-in

The same collaborative signal, but the generalisation works in a fundamentally different way: not through direct co-occurrences but through compression into a few coordinates. Because of this, factorisation sees links where neighbours see emptiness — and breaks where neighbours work. We take the mechanism apart with numbers, derive ALS, and separately look at how to give a vector to a user who was not in training.

What to take away
  • Factorisation is a method inside collaborative filtering, not an alternative to it. And «learned» does not mean «factorised»: EASE is trained too, but makes no attempt to decompose anything.
  • Generalisation goes through a narrow bottleneck. Items that co-occurred 12 times out of 20 000 get a cosine of 0.04 from neighbours and 0.98 from a rank-2 factorisation.
  • Rank is a hypothesis that «any taste is a mixture of \(d\) archetypes». At \(d = 6\) on a 30×40 matrix there are more parameters than observations.
  • A new user needs no retraining. Fold-in is the solution of one \(d \times d\) system, that is, the same ridge regression that sits inside ALS.

1. Where factorisation sits among the rest

The terms get confused constantly, so let us place them at once.

Collaborative filtering — recommending from behaviour, with no content features by neighbours (memory-based) similarities computed straight from the matrix item-item kNN, user-user kNN → chapter 5 learned (model-based) factorisation: MF, ALS, iALS, BPR-MF ← this chapter linear item-item: EASE, SLIM — no factorisation neural: autoencoders, two towers
«Learned» and «factorised» are independent properties. EASE is trained but does not decompose its matrix: it is full rank.
The setup

We approximate the interaction matrix \(R\) of size \(|U| \times |I|\) by a product of two thin ones:

$$ R \;\approx\; P Q^{\top}, \qquad P \in \mathbb{R}^{|U| \times d},\ \ Q \in \mathbb{R}^{|I| \times d}, \qquad \hat r_{ui} = p_u^{\top} q_i $$

Every user and every item is assigned a vector of \(d\) numbers, and the prediction is their inner product. We train only on the observed cells:

$$ \min_{P,Q} \sum_{(u,i) \in \Omega} \bigl(r_{ui} - p_u^{\top}q_i\bigr)^2 \;+\; \lambda\bigl(\lVert P\rVert^2 + \lVert Q\rVert^2\bigr) $$

The sum over \(\Omega\) rather than over all pairs is essential: a missing entry is an unknown, not a zero.

«SVD» in recommenders is not SVD

A real singular value decomposition requires a complete matrix. Ours has holes, and filling them with zeros is not allowed, for the reason given in the chapter on data.

What has been called SVD since the Netflix Prize is Simon Funk's gradient descent over the observed cells. The name stuck; the method is a different one. Clarifying this distinction in an interview is a cheap way to show that you understand the setup rather than having memorised an acronym.

2. How this differs from neighbours

The main difference is not in the formula but in where the generalisation comes from.

A link that is not in the co-occurrences

Synthetic data: two items A and C load on the same latent taste, but both are rare, so they hardly ever occur together. B is a frequent item of the same taste, E an item of a different taste.

sim(A, C)sim(A, B)sim(A, E)
co-occurrenceA and C together — 12 times out of 20 000
neighbours, cosine0.03960.1256
factorisation, rank 2+0.980+0.889−0.622

The numbers are reproduced by the script _tools/mf_demo.py in this repository.

The neighbours consider A and C almost unrelated and put B three times closer to A. Factorisation recovers that A and C are practically duplicates in taste, having twelve co-occurrences to work with.

The mechanism: compression into two coordinates leaves the model no room to store every item separately. To explain the data it has to find shared axes — and on those axes A and C end up next to each other, because they are linked to the same third items.

The opposite case, which is more telling

Take substitute items: their audiences do not overlap at all, not a single joint interaction.

neighbourssim(A,C) = 0.000«no data»
factorisationsim(A,C) = −1.000«opposites»

These are different statements, and the second is one neighbours cannot express in principle. A zero from kNN means an absence of information; a minus one from factorisation is a substantive conclusion about the relationship.

Hence the practical division: neighbours are good where there is plenty of signal and explainability is needed, factorisation where you have to reach links that direct co-occurrences do not contain.

3. How it is trained

SGD: the derivation in two lines

For a single observation the error is \(e_{ui} = r_{ui} - p_u^{\top}q_i\) and the loss is \(L = e_{ui}^2 + \lambda(\lVert p_u\rVert^2 + \lVert q_i\rVert^2)\). Differentiating:

$$ \frac{\partial L}{\partial p_u} = -2e_{ui}q_i + 2\lambda p_u, \qquad \frac{\partial L}{\partial q_i} = -2e_{ui}p_u + 2\lambda q_i $$

The descent step (with the two absorbed into \(\eta\)):

$$ p_u \leftarrow p_u + \eta\,(e_{ui}\,q_i - \lambda\,p_u), \qquad q_i \leftarrow q_i + \eta\,(e_{ui}\,p_u - \lambda\,q_i) $$

It reads in plain words: move the user vector towards the item vector in proportion to the error, and symmetrically.

ALS: why production likes it

The derivation

The problem is not convex in \((P, Q)\) jointly, but it is convex in each matrix separately. Fix \(Q\) — then for a user \(u\) what remains is an ordinary ridge regression. Setting the derivative to zero:

$$ \bigl(Q_u^{\top}Q_u + \lambda I\bigr)\,p_u = Q_u^{\top} r_u \quad\Longrightarrow\quad p_u = \bigl(Q_u^{\top}Q_u + \lambda I\bigr)^{-1} Q_u^{\top} r_u $$

where \(Q_u\) are the rows of the items \(u\) interacted with. Then symmetrically fix \(P\) and solve for the items. Repeat.

Three reasons this is convenient in production:

  • every \(p_u\) is computed independently of the others — it parallelises perfectly over users;
  • there is no learning rate to tune;
  • it converges in a dozen iterations rather than dozens of epochs.

Note the size of the system: \(d \times d\), where \(d\) is the rank, usually tens. The matrix being inverted is the size of the rank, not of the catalogue.

Rank is a hypothesis, not a hyperparameter

The word «rank» here is not a metaphor. The product of two matrices of width \(d\) has rank at most \(d\) — a fact of linear algebra. By choosing \(d\) we are literally asserting: any taste is a mixture of \(d\) archetypes.

At \(d = 2\) the hypothesis is strict, at \(d = 50\) it commits to almost nothing. And it has a measurable price.

Parameters against observations

The number of trainable parameters is \((|U| + |I|)\cdot d\). For a 30×40 matrix filled to 45%, with a third held out for validation — about 378 training observations:

Rank \(d\)ParametersPer observation
1700.19
21400.37
42800.74
64201.11 — more than there is data

The numbers are reproduced by the script _tools/mf_demo.py.

At \(d = 6\) the model can fit the training data almost exactly while learning nothing. That is overfitting — and it is visible only on the held-out set, because the training error falls all the while.

The two roles of regularisation

In the loss \(\lambda\) is a penalty on the length of the vectors; in the ALS solution it turns into \(\lambda I\) on the diagonal. Usually only the first role gets named.

Shrinkage towards zero

For a user with a single rating it pays to produce an enormous vector that hits that rating perfectly. The penalty on length makes that unprofitable, the vector stays short — and a short vector gives a prediction close to the average.

That is, \(\lambda\) makes the model say «I do not know» where there is little data, instead of inventing confidently.

Conditioning

The second role is purely computational and gets forgotten. The matrix \(Q_u^{\top}Q_u\) for a user with two or three interactions is singular: its rank is less than \(d\), and no inverse exists.

Adding \(\lambda I\) makes it invertible always. So in ALS regularisation is not an option but a condition of the thing working at all: at \(\lambda = 0\) the solution for a rare user simply does not compute.

What to look for here
  1. Move the rank and watch both errors. The training error falls monotonically, the held-out one has a minimum — and it sits near the true rank of the data.
  2. Increase the rank past the minimum: the training error keeps falling, the held-out one grows. That is exactly the «more parameters than observations» arithmetic from the table above.
  3. Raise \(\lambda\): the minimum over rank shifts to the right. Regularisation lets you keep a larger rank without overfitting — at the cost of using each coordinate more cautiously.

What to say in an interview: «Rank is a hypothesis about the number of latent factors. It is chosen on a held-out set rather than on the training one: the training error falls monotonically and tells you nothing».

4. iALS: what to do when there are no negatives

Everything above assumed \(r_{ui}\) is a rating. In implicit data there is no rating: we see «listened», we do not see «did not like it». And if we learn only from the observed pairs, then all of them are positive, and the trivial solution is to predict one everywhere.

Confidence instead of a rating

The solution of Hu, Koren and Volinsky: learn from all pairs, but with different weights.

$$ \min_{P,Q}\; \sum_{u,i} c_{ui}\bigl(\mathbb{1}[r_{ui} > 0] - p_u^{\top}q_i\bigr)^2 \;+\; \lambda\bigl(\lVert P\rVert^2 + \lVert Q\rVert^2\bigr), \qquad c_{ui} = 1 + \alpha\, r_{ui} $$

Two things that are fused together in explicit feedback are separated here:

  • the target — a binary «there is a preference or there is not»;
  • the weight \(c_{ui}\) — how confident we are of that: listened a hundred times or once.

Unobserved pairs enter with a weight of 1 — these are soft negatives: the model assumes an absence of interest, but weakly, and a single observation overturns that assumption.

The parameter \(\alpha\) is a hyperparameter, and it is not one: in the original work sensible values are of the order of tens. It sets how many times more an observation weighs than a missing entry.

Why the sum over all pairs does not explode

Naively a sum over \(|U|\times|I|\) is unaffordable. But in the ALS solution it decomposes:

$$ Q^{\top}C_u Q \;=\; Q^{\top}Q \;+\; Q^{\top}(C_u - I)Q $$

The first term does not depend on the user and is computed once per iteration. The second is a sum only over the items where \(c_{ui} \ne 1\), that is, over the observed ones, of which there are few.

It is precisely this trick that made iALS practical. Without it, «learn from all pairs» would have remained a theoretical proposal.

5. A new user: fold-in

A user arrives, watches a few things, and the model was trained yesterday. Retraining the whole of ALS for them is not an option. And it is not necessary.

It is the same ridge regression

The item vectors \(Q\) are already trained and change slowly. So for a new user it is enough to solve exactly the ALS step responsible for users:

$$ p_{\text{new}} = \bigl(Q_s^{\top}Q_s + \lambda I\bigr)^{-1} Q_s^{\top} r_s $$

where \(Q_s\) are the vectors of the items the person has already interacted with. This is a \(d \times d\) system: at rank 8, eight by eight, microseconds.

How well it works

A check on synthetic data: a catalogue of 300, rank 8, a new user with 25 interactions whose vectors were not in training at all.

The top-25 prediction matches their real interests in 17 cases out of 25 — and that is with the model seeing them for the first time and solving one 8×8 system.

The numbers are reproduced by the script _tools/mf_demo.py.

Hence the practical conclusion: cold start for users is solved cheaply if there is even a little history. Cold start for items is not: a new item has no vector \(q_i\), and without one it takes part in no score at all.

6. Where factorisation hits its limits

LimitationWhat it consists of
Cold start for itemsa new item is not in \(Q\); the model does not accept content features by construction
No contextno time of day, no device, nothing about what the person is doing right now
The order of the history is ignored\(p_u\) is learned from a set of interactions; «yesterday» and «two years ago» are indistinguishable
Only one form of interactionan inner product. Anything not expressible by it the model cannot express
Popularity bias in the geometrythe norm \(\lVert q_i\rVert\) grows with popularity and MIPS does not cancel it — see the chapter on biases

The first three limitations are removed by one and the same move: replace the learned item vector with one computed from features. That is the next big line of the textbook — from matrix factorisation to two-tower models.

Interview questions

How does matrix factorisation differ from collaborative filtering?

They are not alternatives: factorisation is a method inside collaborative filtering. CF splits into neighbour-based approaches (kNN, item-item — similarities computed straight from the matrix) and learned ones, which include factorisation as well as EASE and SLIM. And «learned» and «factorised» are different properties: EASE is trained but does not decompose its matrix.

The substantive difference is in the mechanism of generalisation. Neighbours generalise through direct co-occurrences, factorisation through compression into \(d\) coordinates. So it finds links where there are hardly any co-occurrences: in the example, items that co-occurred 12 times out of 20 000 get a cosine of 0.04 from neighbours and 0.98 from a rank-2 factorisation.

And the converse: for mutually exclusive items neighbours give 0 («no data»), factorisation −1 («opposites»). The second statement is unavailable to neighbours.

Why is «SVD» in recommenders not SVD?

A real singular value decomposition requires a complete matrix. Ours is sparse, and filling the gaps with zeros is not allowed: a gap means «it was not shown», not «it was not liked».

What came to be called SVD after the Netflix Prize is Simon Funk's gradient descent over the observed cells, that is, minimising the error only on known pairs with regularisation. The name stuck; the method is a different one.

Why is ALS preferred to SGD in production?

The problem is not convex in \((P,Q)\) jointly but is convex in each matrix separately. Fixing \(Q\), for each user we get an ordinary ridge regression with the solution \(p_u = (Q_u^\top Q_u + \lambda I)^{-1} Q_u^\top r_u\).

Hence three advantages: every \(p_u\) is computed independently and the task parallelises perfectly; there is no learning rate to tune; convergence takes a dozen iterations. And the system is of size \(d \times d\) — the size of the rank, not of the catalogue.

SGD wins when the matrix is very sparse and online updating is needed, and also when the loss is not quadratic — for BPR or a logistic loss you cannot derive ALS.

What is rank and how do you choose it?

Rank is the number of coordinates in each vector, and it is a substantive hypothesis: «any taste is a mixture of \(d\) archetypes». A product of matrices of width \(d\) has rank at most \(d\), so the choice of \(d\) limits what the model is able to express at all.

The price is measurable: the number of parameters is \((|U| + |I|)\cdot d\). On a 30×40 matrix with 378 training observations, rank 6 gives 420 parameters — more than there is data, and the model will fit the training set while learning nothing.

It is chosen on a held-out set. The training error falls monotonically with rank and suggests nothing; only the held-out error has a minimum.

Why is regularisation needed in ALS — name both roles.

The first, the usual one: shrinkage towards zero. For a user with a single rating it pays to produce an enormous vector that hits it perfectly; the penalty on length makes that unprofitable, and the model says «I do not know» instead of inventing confidently.

The second, the one people forget: conditioning. The matrix \(Q_u^\top Q_u\) for a user with two or three interactions is singular — its rank is less than \(d\) and no inverse exists. Adding \(\lambda I\) makes it invertible always, so at \(\lambda = 0\) the solution for a rare user simply will not compute.

How does iALS work with implicit feedback?

It separates the target from the confidence. The target is binary — «there is a preference» — while the weight \(c_{ui} = 1 + \alpha r_{ui}\) says how confident we are of that: listened a hundred times or once. We learn from all pairs rather than only the observed ones, and the unobserved enter with a weight of 1 — soft negatives that a single observation overturns.

The sum over all pairs does not explode because of the decomposition \(Q^\top C_u Q = Q^\top Q + Q^\top(C_u - I)Q\): the first term does not depend on the user and is computed once per iteration, the second is a sum only over the observed items, of which there are few.

And \(\alpha\) is a genuine hyperparameter, not one: sensible values are of the order of tens.

How do you serve recommendations to a user who was not in training?

Fold-in. The item vectors are already trained and change slowly, so it is enough to solve exactly the ALS step responsible for users: \(p_{\text{new}} = (Q_s^\top Q_s + \lambda I)^{-1} Q_s^\top r_s\), where \(Q_s\) are the vectors of what the person interacted with.

This is a \(d \times d\) system — at rank 8, microseconds, and no retraining. On synthetic data with a catalogue of 300 and 25 interactions the top 25 matches the real interests in 17 cases out of 25.

What matters is that the converse does not work: cold start for items is not treated this way. A new item has no vector and there is nowhere to take one from — that needs content features, that is, a different architecture.

One-screen cheat sheet

The place

MF is a method inside CF. «Learned» ≠ «factorised»: EASE learns but does not decompose.

Generalisation

Through a narrow bottleneck, not through co-occurrences. 12 co-occurrences out of 20 000 → cosine 0.04 from neighbours, 0.98 from MF.

ALS

\(p_u = (Q_u^\top Q_u + \lambda I)^{-1} Q_u^\top r_u\). Parallelises, no learning rate, a dozen iterations.

Rank

A hypothesis about the number of archetypes. Parameters \((|U|+|I|)d\); chosen on a held-out set.

iALS

Binary target, weight \(c = 1 + \alpha r\). Missing entries are soft negatives. \(\alpha\) of the order of tens.

Fold-in

A \(d\times d\) system, no retraining needed. Works for users, does not work for items.

Primary sources