Designing a Three-Tier Cold-Start Strategy with Fold-In Approximation and Online Blending
How I built an on-device book recommendation engine that gracefully handles the transition from “I know nothing about you” to “I know…

Designing a Three-Tier Cold-Start Strategy with Fold-In Approximation and Online Blending
How I built an on-device book recommendation engine that gracefully handles the transition from “I know nothing about you” to “I know exactly what you’d read next” — using SVD, Apple Accelerate, and a blending parameter that acts as a bias-variance dial.
The hardest part of building a real recommendation system isn’t training, it’s what happens the moment a brand new user opens the app. They have no ratings, no history, no latent vector. And they’re expecting a good recommendation. This is the cold start problem.
I’ll walk through how I designed a three-tier adaptive strategy for an on-device book recommender. One that transitions from blind popularity to genuine personalization as evidence accumulates, entirely offline, using Singular Value Decomposition (SVD) and Apple’s Accelerate framework.
On-Device SVD for Book Recommendation
The project is a book recommendation system built as an iOS app. The core engine uses Collaborative Filtering via SVD, which decomposing a user-item matrix into low-dimensional latent spaces where both users and books are represented as vectors, and proximity in that space encodes taste similarity. SVD weights are pre-trained offline using Python (Surprise library), exported, and loaded into Swift using Apple’s Accelerate vDSP framework.
The constraint of on-device model is, I can’t retrain the model every time someone new shows up. In a server-side system, the model can periodically retrain with new users folded into the matrix. On-device, the model is static, the latent factors for items (V matrix), the singular values (Σ), and the user factors (U) for users who existed at training time. A new user has no row in U, they’re invisible to the model.
Case 1: Popular-Based — When You Know Nothing
When a new user arrives with no ratings at all, the system falls back to a pre-ranked popularity list derived from the training data. These aren’t random books, they’re the books most frequently rated, filtered to exclude anything with fewer than 10 interactions (to avoid fluky one-hit items), and sorted by a combination of rating count and average score.
This is a high-bias, zero-variance strategy. Every new user sees the same list. It’s not personalized, and it’s not trying to be. But it has one critical property: it’s never wrong in a dangerous way. A popular book might not be the perfect recommendation, but it’s unlikely to be a terrible one.
The trigger for staying in this case is simple: n == 0 ratings. The moment the user rates their first book, the system transitions.
Case 2: Cold-Start — Fold-In Approximation
This is where it gets interesting. A user has rated 1–2 books. Not enough to retrain the model, but not nothing. Can we do better than popularity?
Yes! There’s a technique called fold-in approximation
What Fold-In actually does?
During training, SVD decomposes the rating matrix R into three matrices:
R ≈ U × Σ × Vᵀ
Where U contains user latent vectors, Σ contains singular values, and V contains item latent vectors. For a trained user, their row in U encodes their taste profile — their position in latent space relative to all items.
For a new user, we don’t have their row in U. But we do have V and Σ (they’re static, exported with the model), and we have the user’s ratings (even if it’s just one). Fold-in approximates what the user’s latent vector would have been if they’d been included in the original training:
u_new = r_new × V × Σ⁻¹
Where r_new is the new user’s (sparse) rating vector. We take their known ratings, project them through the item latent space, and scale by the inverse singular values to get an approximate user vector.
But there’s still a problem, not all ratings contribute equally to the fold-in. A user who rates a book 10 out of 10 is expressing a much stronger signal than a 6. And in the Book-Crossing dataset I used, the rating distribution is heavily skewed , 81.9% of ratings are 7 or above. A rating of 6 barely distinguishes preference from noise.
So instead of using raw ratings in r_new, I weight each rated book by how far the rating deviates from neutral:
weight = (rating - μ) / scaling_factor
A book rated 10 on a scale where the average is around 7 gets a weight of +3. A book rated 5 gets a minimal contribution. This means the fold-in vector is pulled toward the latent representation of books the user strongly liked, rather than being diluted by lukewarm ratings.
The problem doesn't stop there. When I first implemented fold-in, every new user got the exact same recommendations, identical to the popularity baseline. The fold-in code ran without errors, produced a valid user vector, and generated ranked recommendations. But the rankings were effectively random.
The problem: the fold-in vector was ~10x smaller in magnitude than trained user vectors.
When you compute recommendations via dot product (score = u · v_item), the magnitude of the user vector directly scales all scores. Trained user vectors have magnitudes calibrated by the full optimization process. A fold-in vector, computed from just 1–2 ratings projected through Σ⁻¹, produces a much smaller vector. The dot products become negligible, and the final ranking is dominated by the global bias terms (which are… popularity). The model was “personalizing” with scores so small they were rounding errors on top of the popularity signal.
The fix: normalize the fold-in vector to match the average magnitude of trained user vectors.
let avgUserNorm = precomputedAverageNorm // from training data
let currentNorm = vDSP.rootMeanSquare(foldInVector)
let scale = avgUserNorm / currentNorm
vDSP.multiply(scale, foldInVector, result: &foldInVector)
After normalization, the fold-in vector produces dot products in the same range as trained users, and the recommendations immediately became personalized , even from a single rating.
Case 3: Personalized — Online Blending
Once a user has rated 3+ books in a session, we enter the fully personalized regime. But we don’t just use the fold-in vector alone, we blend it with the user’s pre-existing vector (if they have one from previous sessions).
The Blending Formula: effectiveVec = base + α × foldIn
Where base is the user’s stored vector from prior sessions (or zeros for a genuinely new user), foldIn is the approximated vector from current session ratings, and α is a blending parameter that controls how much the new session shifts the recommendation.
At α = 0, the system ignores the current session entirely and uses only historical preferences, high bias toward past behavior, zero variance from new input. At α = 0.80 (the cap), the current session strongly influences recommendations , lower bias, higher variance, responsive to the user’s evolving taste.
I scale α based on the number of new ratings in the session:
| New Ratings | α Value | Interpretation |
|-------------|---------|-----------------------------------------------|
| 0 | 0.00 | Pure historical (or popularity if no history) |
| 1 | 0.15 | Small taste shift — one rating could be noise |
| 3 | 0.45 | Moderate shift — a pattern is emerging |
| 5+ | 0.80 | Strong session signal — trust it |
The cap prevents a single session from completely overwriting a user’s long-term profile. Without it, one session of rating crime novels could completely erase a lifelong science-fiction reader’s profile. The pre-trained history never completely disappears because of the cap — the model’s knowledge of who this user is, built from thousands of post ratings, always has a 20% floor. The blend vector is then recomputed on every rating interaction, producing a whole-list re-rank rather than simply removing the rated book.
Why This Works Without Retraining?
With fold-in + blending achieves something close to the same effect of retrain model.
- The three decomposed matrices A from training are pre-computed and stored
- On each new rating, the fold-in vector is recomputed using vDSP
- The effective user vector is blended and used for prediction generation
- It’s not necessary to recompute the low-dimensional model from scratch
The result is an incremental system, online blending that updates recommendations in real time as the user rates books, with computational cost proportional to the embedding dimension (a single matrix-vector multiply).
The Transition Map:
[New User, 0 ratings] → Case 1: Popularity-based ranking → User rates a book → refresh trigger
[User with n >= 1 ratings, no pre-trained history]
→ Case 2: Full Refresh — fold-in approximation
→ Exclude entire seen history, generate fresh list
→ User accumulates more ratings → transition to Case 3
[Returning user OR user with 3+ session ratings] → Case 3: Online Blending → effectiveVec = base + α × foldIn → Append next 10 from blended ranking (limited for idle use) → Each new rating triggers re-rank
Every transition is invisible to the user. They just see a list of books that gets better as they interact.
The cold start strategy is ultimately about epistemic humility encoded in software: when you know nothing, admit it and show popular books. When you know a little, use fold-in but don’t overweight it. When you know more, blend confidently but never erase history entirely. The blending parameter α is a mathematical expression of “how much should I trust this new evidence?”, and getting that calibration right matters more than the model architecture.
메타데이터
- post_id
- 0275cdcc9d4c
- slug
- designing-a-three-tier-cold-start-strategy-with-fold-in-approximation-and-online-blending-0275cdcc9d4c
- url
- https://medium.com/@fitriyanivira03/designing-a-three-tier-cold-start-strategy-with-fold-in-approximation-and-online-blending-0275cdcc9d4c
- canonical_url
- https://medium.com/@fitriyanivira03/designing-a-three-tier-cold-start-strategy-with-fold-in-approximation-and-online-blending-0275cdcc9d4c
- author_url
- https://medium.com/@fitriyanivira03
- status
- ok
- fetched_at
- 2026-08-06 10:19:58