Rekomendacje produktowe to jeden z niewielu systemów ML, które sklep internetowy może czysto przetestować: połowie odwiedzających pokazuje się jedną wersję, drugiej połowie drugą, i porównuje przychód. Dlatego są dobrym pierwszym projektem ML, ale też łatwo je przebudować ponad potrzebę. Ta notatka opisuje kolejność, w jakiej je budujemy: baseline na współoglądaniach, który powstaje w kilka tygodni, potem candidate retrieval i ranking, jeśli liczby to uzasadniają, z kodem dla tych części, które mieszczą się na jednej stronie.
Dla czytelnika biznesowego
Ile są warte rekomendacje. Salesforce podał, że podczas Cyber Week 2025 AI i agenci wpłynęli na około 20% globalnych zamówień, czyli około 67 mld USD sprzedaży, poprzez spersonalizowane rekomendacje produktów i konwersacyjną obsługę klienta. Tę liczbę należy czytać ostrożnie: „wpłynęli” to atrybucja na własnej platformie commerce Salesforce, a nie zmierzony przyrost, i miesza ona rekomendacje z czatem. Mówi, że rekomendacje są dziś standardem w dużych sklepach. Nie mówi, co dodałyby w Państwa sklepie. To pokaże tylko test na Państwa ruchu.
Gdzie się pojawiają. Każde miejsce odpowiada na inne pytanie i powinno być mierzone osobno.
| Miejsce | Na jakie pytanie odpowiada | Typowe źródło |
|---|---|---|
| Strona produktu | Co jeszcze mogłoby pasować, jeśli nie ten produkt? | Produkty podobne i współoglądane |
| Koszyk | Co pasuje do tego, co już wybrano? | Produkty kupowane razem, akcesoria |
| Strona główna i strony kategorii | Czego ten klient prawdopodobnie chce dzisiaj? | Spersonalizowany ranking |
| Wyniki wyszukiwania | Które trafne produkty ten klient prawdopodobnie kupi? | Ranking na wierzchu trafności wyszukiwania |
| E-mail i push | Co skłoni tego klienta do powrotu? | Te same modele, uruchamiane wsadowo |
Co mierzyć. Zanim powstanie jakikolwiek kod, należy uzgodnić jedną główną metrykę: przychód na sesję albo zamówienia na sesję w całym serwisie, a nie kliknięcia w widżet. Kliknięcia w rekomendacje mogą rosnąć, podczas gdy przychód stoi w miejscu, bo klienci klikają rekomendowany produkt, który i tak by znaleźli. Click-through widżetu to przydatna diagnostyka, a nie kryterium sukcesu.
Kiedy to się nie opłaca. W dwóch przypadkach. Mały katalog, kilkaset produktów, które klienci mogą przejrzeć w całości, rzadko potrzebuje modelu; wystarczy wyselekcjonowana lista i sortowanie według popularności. Drugi przypadek to mały ruch, a arytmetyka jest tu bezlitosna. Żeby wykryć względny wzrost o 5% przy konwersji 2% (z 2,0% do 2,1%) przy typowych ustawieniach (istotność 5%, moc 80%), potrzeba około 315 000 sesji w każdym wariancie. Sklep z 100 000 sesji miesięcznie czekałby na jedną odpowiedź ponad sześć miesięcy. Wzrost o 10% wymaga około 81 000 sesji na wariant. Jeśli ruch nie pozwala odpowiedzieć na to pytanie, nie da się udowodnić, że model pomaga, i płaci się za przekonanie.
Koszt i harmonogram. Nasz przedział dla systemu rekomendacji to 14k–42k € w ciągu 2–4 miesięcy, zależnie od tego, jak daleko poza baseline sięga projekt. Poglądowy podział jest na końcu tej notatki.
Etap 1: współoglądania z rozwiązaniem awaryjnym opartym na popularności
Baseline działa w trybie item-to-item: produkty, które pojawiają się w tych samych sesjach, są prawdopodobnie powiązane. To stary pomysł, a praca inżynierów Amazona z 2003 roku w IEEE Internet Computing opisała podejście item-to-item na dużą skalę: podobne produkty liczy się offline, więc koszt odczytu online nie rośnie wraz z liczbą klientów czy produktów. To wciąż właściwy pierwszy krok, bo wymaga wyłącznie logu zdarzeń, łatwo go wyjaśnić merchandiserowi i daje każdemu późniejszemu modelowi liczbę do pobicia.
Dane wejściowe: jeden wiersz na wyświetlenie produktu (albo dodanie do koszyka, albo zakup) z session_id, item_id i ts.
import numpy as np
import pandas as pd
def co_view_scores(events, max_items_per_session=50, min_pairs=3):
# one row per session and item; long sessions are capped to keep the join small
s = (events.sort_values("ts")
.drop_duplicates(["session_id", "item_id"], keep="last")
.groupby("session_id").tail(max_items_per_session)[["session_id", "item_id"]])
pairs = s.merge(s, on="session_id", suffixes=("_a", "_b"))
pairs = pairs[pairs.item_id_a != pairs.item_id_b]
co = pairs.groupby(["item_id_a", "item_id_b"]).size().rename("co").reset_index()
co = co[co.co >= min_pairs] # drop pairs seen in too few sessions
n_item = s.groupby("item_id").size() # sessions that viewed each item
co["score"] = co.co / np.sqrt(co.item_id_a.map(n_item) * co.item_id_b.map(n_item))
return co
def top_k(co, k=20):
return (co.sort_values(["item_id_a", "score"], ascending=[True, False])
.groupby("item_id_a").head(k))
Trzy decyzje w tym kodzie mają większe znaczenie, niż kiedykolwiek będzie miał model.
- Normalizacja. Surowe liczby współwystąpień rekomendują bestsellery obok wszystkiego. Dzielenie przez pierwiastek z liczby sesji obu produktów (wynik cosinusowy) nagradza pary, które pojawiają się razem częściej, niż przewidywałaby ich popularność. Wynik lift,
co * n_sessions / (n_a * n_b), przesuwa wyniki jeszcze bardziej w stronę niszowych par i wymaga surowszego minimalnego progu. - Minimalne wsparcie. Bez
min_pairsdwa produkty obejrzane razem raz przez jedną osobę dostają idealny wynik. Próg należy dostroić na odłożonych danych, a nie na oko. - Okno i sygnał. Warto używać okna kroczącego (60–90 dni to typowy początek), żeby produkty sezonowe wygasały. Współoglądania znajdują alternatywy dla strony produktu; współzakupy znajdują produkty uzupełniające dla koszyka. Oba warianty buduje się tą samą funkcją z różnymi typami zdarzeń.
Dla produktów ze zbyt małą liczbą sąsiadów wolne miejsca wypełnia się popularnymi produktami z tej samej kategorii:
def recommend(item_id, recs, catalog, popular_by_cat, k=10):
own = recs.loc[recs.item_id_a == item_id, "item_id_b"].tolist()[:k]
if len(own) < k: # fallback: popular in the same category
cat = catalog.set_index("item_id").at[item_id, "category"]
own += [i for i in popular_by_cat[cat] if i != item_id and i not in own][: k - len(own)]
return own
Na produkcji staje się to nocnym zadaniem wsadowym, które zapisuje 20 najlepszych sąsiadów każdego produktu do magazynu klucz-wartość, oraz API, które je odczytuje. To wystarczy, by przeprowadzić pierwszy test A/B przeciwko temu, co sklep pokazuje dziś.
Etap 2: pobrać kandydatów, potem ich uszeregować
Item-to-item ignoruje to, kim jest klient. Personalizacja wymaga modelu, który ocenia produkty dla konkretnej osoby, a ocenianie całego katalogu przy każdym wyświetleniu strony jest zbyt wolne. Standardową odpowiedzią są dwa etapy, opisane w pracy o rekomendacjach YouTube autorstwa Covingtona, Adamsa i Sargina (RecSys 2016): etap generowania kandydatów zawęża korpus milionów produktów do setek, a osobny model rankingowy porządkuje te setki, korzystając ze znacznie bogatszego zestawu cech.
Generowanie kandydatów modelem two-tower. Jedna sieć tworzy embedding klienta (ostatnie wyświetlenia, koszyk, kontekst taki jak urządzenie i strona wejścia), druga embedding produktu (kategoria, marka, cena, tekst). Trening przybliża embedding klienta do produktów, z którymi wchodził w interakcję. W czasie serwowania embeddingi produktów są policzone z góry i zaindeksowane do przybliżonego wyszukiwania najbliższych sąsiadów, więc retrieval to jedno obliczenie embeddingu i jedno zapytanie do indeksu.
Typowy trik treningowy to in-batch negatives: dla każdego klienta w batchu produkty pozostałych klientów służą jako przykłady negatywne. Ten trik ma obciążenie. Popularne produkty pojawiają się w wielu batchach, więc są nadużywane jako negatywy i model uczy się zaniżać ich pozycję. Yi i in. (RecSys 2019) pokazali, jak to skorygować, odejmując od wyniku logarytm szacowanej częstości próbkowania każdego produktu, i wdrożyli skorygowany model w YouTube na korpusie dziesiątek milionów filmów. W PyTorch rdzeń jest krótki:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Tower(nn.Module):
def __init__(self, n_ids, dim=64):
super().__init__()
self.emb = nn.Embedding(n_ids, dim)
self.mlp = nn.Sequential(nn.Linear(dim, dim), nn.ReLU(), nn.Linear(dim, dim))
def forward(self, ids):
return F.normalize(self.mlp(self.emb(ids)), dim=-1)
user_tower, item_tower = Tower(n_users), Tower(n_items)
def in_batch_loss(user_ids, item_ids, log_q, temperature=0.05):
u = user_tower(user_ids) # [B, d]
v = item_tower(item_ids) # [B, d]
logits = u @ v.T / temperature # other items in the batch act as negatives
logits = logits - log_q[item_ids] # logQ correction for popular items
labels = torch.arange(len(user_ids)) # the diagonal holds the true pairs
return F.cross_entropy(logits, labels)
Tutaj log_q to logarytm udziału każdego produktu w interakcjach treningowych. Wieże przyjmują ID, żeby przykład był krótki; w prawdziwym sklepie wieża użytkownika przyjmuje ostatnie N oglądanych produktów i kontekst zamiast ID użytkownika, co oznacza też, że odwiedzający po raz pierwszy dostaje embedding już po jednym kliknięciu.
Listy współoglądań z etapu 1 warto zachować jako drugie źródło kandydatów. Two-tower retrieval i współoglądania zawodzą na różne sposoby, a ranker może korzystać z obu.
Ranking. Ranker widzi kilkaset kandydatów na zapytanie i może sobie pozwolić na wiele cech: wyniki z każdego źródła kandydatów, cenę względem typowego przedziału cenowego klienta, to, czy produkt dzieli kategorię z ostatnio oglądanymi, trend wyświetleń produktu z ostatnich dni, liczbę dni od dodania produktu. Gradient-boosted model learning-to-rank to typowy pierwszy ranker, a ważność jego cech łatwo sprawdzić:
import lightgbm as lgb
# one row per (request, candidate); label 1 if the candidate was clicked or bought
features = ["covis_score", "two_tower_score", "price_vs_user_median",
"same_category_as_viewed", "item_views_7d_trend", "days_since_added"]
train = train.sort_values("request_id")
ranker = lgb.LGBMRanker(objective="lambdarank", n_estimators=300, learning_rate=0.05)
ranker.fit(train[features], train["label"],
group=train.groupby("request_id", sort=False).size().to_numpy())
Reguły biznesowe po modelu. Stany magazynowe, marża i zasady należą do etapu po rankingu, w zwykłym kodzie, który biznes może przeczytać i zmienić: usunąć produkty, których nie da się wysłać, ograniczyć liczbę produktów jednej marki w bloku, wykluczyć kategorie, których nie wolno sprzedawać krzyżowo, a jeśli liczy się marża, zastosować ograniczone podbicie zamiast trenować model na marży. Trzymanie reguł poza modelem oznacza, że merchandiser może je zmienić we wtorek bez wydania ML.
Ewaluacja offline i dlaczego nie jest odpowiedzią
Dane należy dzielić według czasu, nie losowo: trenować na wszystkim sprzed daty granicznej, ewaluować na tygodniu po niej. Losowy podział przecieka przyszłością do treningu i zawyża każdą metrykę.
Każdy etap mierzy się własną metryką. Dla generowania kandydatów recall@k: ile z produktów, z którymi klient faktycznie wszedł w interakcję w następnej kolejności, było na liście kandydatów. Dla rankingu NDCG@k: czy te produkty znalazły się blisko góry.
def recall_at_k(recommended, relevant, k=20):
return len(set(recommended[:k]) & relevant) / len(relevant)
def ndcg_at_k(recommended, relevant, k=10):
dcg = sum(1 / np.log2(i + 2) for i, item in enumerate(recommended[:k]) if item in relevant)
ideal = sum(1 / np.log2(i + 2) for i in range(min(len(relevant), k)))
return dcg / ideal
Liczby offline decydują, który model trafia do testu. Nie decydują, czy zostanie wdrożony. Logi wytworzył stary system, więc nowy model jest oceniany na tym, co robili klienci, gdy pokazywano im coś innego, a pozycja na stronie kształtuje kliknięcia niezależnie od trafności. Covington i in. napisali, że wyniki testów A/B na żywo nie zawsze korelują z eksperymentami offline, a dwa powyższe powody wystarczą, by się tego spodziewać.
Test A/B
- Jedna główna metryka, ta uzgodniona na początku, na przykład przychód na sesję. Przed startem należy ustalić minimalny wzrost wart wykrycia i policzyć wielkość próby, jak w arytmetyce powyżej.
- Randomizacja według odwiedzającego, a nie według wyświetlenia strony, żeby jeden klient nie widział obu wersji.
- Pełne tygodnie, co najmniej dwa, bo klienci w dni robocze i w weekendy zachowują się inaczej. Nie należy kończyć wcześniej, bo krzywa dobrze wygląda czwartego dnia.
- Metryki ochronne, które nie mogą się pogorszyć: czas odpowiedzi strony, udział rekomendowanych produktów niedostępnych w momencie kliknięcia, odsetek zwrotów zamówień zawierających rekomendowane produkty oraz przychód w kategoriach, które nowy model pokazuje rzadziej.
- Stary system jako grupa kontrolna, dopóki główna metryka nie pokaże czegoś innego, z zachowaniem przełącznika do rollbacku.
Cold start
Nowe produkty nie mają interakcji, więc nie znają ich ani współoglądania, ani embedding ID w wieży produktu. Umieszcza się je na podstawie atrybutów (kategoria, marka, przedział cenowy, embeddingi tekstu i zdjęć) i rezerwuje niewielką liczbę miejsc w każdym bloku dla niedawno dodanych produktów, żeby zebrały potrzebne interakcje. Nowi odwiedzający dostają kandydatów opartych na kontekście (strona wejścia, kategoria, urządzenie) i popularne produkty, a po kilku pierwszych wyświetleniach w sesji przechodzą na spersonalizowany retrieval.
Ile to kosztuje, etap po etapie
Nasz przedział dla systemu rekomendacji to 14k–42k € w ciągu 2–4 miesięcy. Poniższa tabela to poglądowy podział dla średniej wielkości projektu, który przechodzi przez oba etapy, wyceniony według naszego opublikowanego cennika. To nie jest oferta ani zapis przeszłego projektu.
| Etap | Role i godziny | Koszt |
|---|---|---|
| Audyt danych, metryka, projekt testu A/B | Data 30 h po 45 €, Senior ML 20 h po 70 € | 2 750 € |
| Etap 1: współoglądania, rozwiązanie awaryjne, API | ML 60 h po 50 €, Backend 40 h po 40 € | 4 600 € |
| Etap 2: two-tower retrieval i ranker | Senior ML 80 h po 70 €, ML 80 h po 50 € | 9 600 € |
| Przeprowadzenie testu A/B i omówienie wyników | Senior ML 15 h po 70 €, Data 20 h po 45 € | 1 950 € |
| Potoki, monitoring, rollback | MLOps 80 h po 55 € | 4 400 € |
| We wszystkich etapach | QA 30 h po 30 €, PM 50 h po 40 € | 2 900 € |
| Razem | 505 h | 26 200 € |
Projekt, który kończy się po etapie 1, bo pierwszy test odpowiedział na pytanie biznesowe, mieści się w dolnej części przedziału. Katalogi z wieloma źródłami danych, kilka miejsc testowanych osobno albo ścisłe budżety czasu odpowiedzi przesuwają go w stronę górnej granicy. Strona o systemach rekomendacji opisuje, jak przebiega taki projekt, a kalkulator na /pricing wycenia własny zakres według tego samego cennika.
Źródła
- Salesforce, Salesforce Data: AI and Agents Propel Cyber Week to Record $336.6B in Global Spend, grudzień 2025 — https://www.salesforce.com/news/press-releases/2025/12/05/cyber-week-ai-agents-sales/
- IEEE Internet Computing, Amazon.com Recommendations: Item-to-Item Collaborative Filtering (Linden, Smith, York), styczeń 2003 — https://ieeexplore.ieee.org/document/1167344
- ACM RecSys, Deep Neural Networks for YouTube Recommendations (Covington, Adams, Sargin), wrzesień 2016 — https://research.google/pubs/pub45530/
- ACM RecSys, Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations (Yi et al.), wrzesień 2019 — https://research.google/pubs/sampling-bias-corrected-neural-modeling-for-large-corpus-item-recommendations/