Zum Inhalt springen
Alle Insights
Tutorial11 Min. Lesezeit

Empfehlungen für einen Onlineshop: zuerst eine Co-View-Baseline ausliefern, Two-Tower-Retrieval erst ergänzen, wenn ein Test zeigt, dass es sich lohnt

Ein zweistufiger Plan für Produktempfehlungen, mit Business Case, funktionierendem pandas-Code für die Baseline, der Architektur, die danach kommt, und dem Nachweis beider Stufen in einem A/B-Test.

Produktempfehlungen gehören zu den wenigen ML-Systemen, die ein Onlineshop sauber testen kann: der Hälfte der Besucher die eine Version zeigen, der anderen Hälfte die andere, und den Umsatz vergleichen. Das macht sie zu einem guten ersten ML-Projekt und zugleich zu einem, das man leicht überdimensioniert. Diese Notiz beschreibt die Reihenfolge, in der wir sie bauen: eine Co-View-Baseline, die in Wochen live geht, dann Kandidatenretrieval und Ranking, wenn die Zahlen es rechtfertigen, mit Code für die Teile, die auf eine Seite passen.

Für Leser aus dem Business

Was Empfehlungen wert sind. Salesforce berichtete, dass KI und Agenten während der Cyber Week 2025 etwa 20% der weltweiten Bestellungen beeinflusst haben, rund 67 Mrd. $ Umsatz, über personalisierte Produktempfehlungen und dialogbasierten Kundenservice. Lesen Sie diese Zahl genau: „beeinflusst“ ist Attribution auf Salesforces eigener Commerce-Plattform, kein gemessener Uplift, und sie vermischt Empfehlungen mit Chat. Sie sagt Ihnen, dass Empfehlungen in großen Shops inzwischen Standard sind. Sie sagt Ihnen nicht, was sie Ihrem Shop bringen würden. Das kann nur ein Test auf Ihrem Traffic.

Wo sie platziert sind. Jede Platzierung beantwortet eine andere Frage und sollte getrennt gemessen werden.

Platzierung Beantwortete Frage Typische Quelle
Produktseite Was könnte sonst passen, wenn nicht dieses Produkt? Ähnliche und gemeinsam angesehene Artikel
Warenkorb Was passt zu dem, was schon ausgewählt ist? Gemeinsam gekaufte Artikel, Zubehör
Start- und Kategorieseiten Was will diese Person heute wahrscheinlich? Personalisiertes Ranking
Suchergebnisse Welche relevanten Artikel kauft diese Person wahrscheinlich? Ranking auf Basis der Suchrelevanz
E-Mail und Push Was bringt diese Person zurück? Dieselben Modelle, im Batch ausgeführt

Was zu messen ist. Vereinbaren Sie eine primäre Kennzahl, bevor Code geschrieben wird: Umsatz pro Session oder Bestellungen pro Session über die gesamte Website, nicht Klicks auf das Widget. Klicks auf Empfehlungen können steigen, während der Umsatz gleich bleibt, weil Kunden einen empfohlenen Artikel anklicken, den sie ohnehin gefunden hätten. Die Klickrate des Widgets ist eine nützliche Diagnose, kein Erfolgskriterium.

Wann es sich nicht lohnt. Zwei Fälle. Ein kleiner Katalog, einige hundert Produkte, die Kunden komplett durchsehen können, braucht selten ein Modell; eine kuratierte Liste und eine Sortierung nach Beliebtheit erfüllen den Zweck. Geringer Traffic ist der zweite Fall, und die Rechnung ist unnachgiebig. Um einen relativen Lift von 5% bei einer Conversion-Rate von 2% (2,0% auf 2,1%) mit den üblichen Einstellungen (5% Signifikanz, 80% Power) zu erkennen, brauchen Sie etwa 315.000 Sessions pro Arm. Ein Shop mit 100.000 Sessions im Monat würde mehr als sechs Monate auf eine Antwort warten. Ein Lift von 10% braucht etwa 81.000 Sessions pro Arm. Kann Ihr Traffic die Frage nicht beantworten, lässt sich nicht nachweisen, dass ein Modell hilft, und Sie bezahlen für eine Überzeugung.

Kosten und Zeitrahmen. Unsere Spanne für ein Empfehlungssystem liegt bei 14k–42k € über 2–4 Monate, je nachdem, wie weit das Projekt über die Baseline hinausgeht. Eine illustrative Aufteilung steht am Ende dieser Notiz.

Stufe 1: Co-Views mit Beliebtheits-Fallback

Die Baseline ist Item-to-Item: Produkte, die in denselben Sessions auftauchen, hängen wahrscheinlich zusammen. Die Idee ist alt, und ein Paper von Amazon-Engineers in IEEE Internet Computing von 2003 beschrieb den Item-to-Item-Ansatz im großen Maßstab: ähnliche Artikel offline berechnen, sodass die Kosten der Online-Abfrage nicht mit der Zahl der Kunden oder Produkte wachsen. Es ist immer noch der richtige erste Schritt, weil er nichts außer dem Event-Log braucht, sich einem Merchandiser leicht erklären lässt und jedem späteren Modell eine Zahl gibt, die es schlagen muss.

Eingabe: eine Zeile pro Produktansicht (oder Warenkorb-Hinzufügung oder Kauf) mit session_id, item_id und 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))

Drei Entscheidungen in diesem Code zählen mehr, als das Modell je zählen wird.

  • Normalisierung. Rohe Co-Occurrence-Zählungen empfehlen Bestseller neben allem. Die Division durch die Wurzel aus den Session-Zahlen beider Artikel (ein Kosinus-Score) belohnt Paare, die häufiger gemeinsam auftauchen, als ihre Beliebtheit erwarten lässt. Ein Lift-Score, co * n_sessions / (n_a * n_b), schiebt weiter in Richtung Nischenpaare und braucht eine strengere Mindestzahl.
  • Mindest-Support. Ohne min_pairs erhalten zwei Artikel, die eine Person einmal zusammen angesehen hat, einen perfekten Score. Stimmen Sie die Schwelle auf zurückgehaltenen Daten ab, nicht nach Augenmaß.
  • Fenster und Signal. Nutzen Sie ein rollierendes Fenster (60–90 Tage sind ein üblicher Start), damit saisonale Artikel auslaufen. Co-Views finden Alternativen für die Produktseite; Co-Purchases finden Ergänzungen für den Warenkorb. Bauen Sie beide mit derselben Funktion und unterschiedlichen Event-Typen.

Für Artikel mit zu wenigen Nachbarn füllen Sie die Plätze mit beliebten Artikeln derselben Kategorie auf:

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

In Produktion wird daraus ein nächtlicher Batch-Job, der die Top 20 Nachbarn pro Artikel in einen Key-Value-Store schreibt, und eine API, die sie liest. Das reicht für den ersten A/B-Test gegen das, was der Shop heute zeigt.

Stufe 2: Kandidaten abrufen, dann ranken

Item-to-Item ignoriert, wer die Person ist. Personalisierung braucht ein Modell, das Artikel für eine Person bewertet, und den ganzen Katalog bei jedem Seitenaufruf zu bewerten ist zu langsam. Die Standardantwort sind zwei Stufen, beschrieben im YouTube-Empfehlungspaper von Covington, Adams und Sargin (RecSys 2016): Eine Stufe zur Kandidatengenerierung verkleinert einen Bestand von Millionen auf Hunderte Artikel, und ein separates Ranking-Modell ordnet diese Hunderte mit einem viel reicheren Satz an Features.

Kandidatengenerierung mit einem Two-Tower-Modell. Ein Netz bettet die Person ein (letzte Ansichten, Warenkorb, Kontext wie Gerät und Einstiegsseite), ein anderes den Artikel (Kategorie, Marke, Preis, Text). Das Training rückt das Embedding einer Person nah an die Artikel, mit denen sie interagiert hat. Zur Laufzeit sind die Artikel-Embeddings vorberechnet und für eine approximative Nächste-Nachbarn-Suche indiziert, sodass das Retrieval aus einer Embedding-Berechnung und einer Index-Abfrage besteht.

Der übliche Trainingstrick sind In-Batch-Negatives: Für jede Person in einem Batch dienen die Artikel der anderen Personen als Negative. Dieser Trick hat einen Bias. Beliebte Artikel tauchen in vielen Batches auf, werden also übermäßig als Negative verwendet, und das Modell lernt, sie zu niedrig zu ranken. Yi et al. (RecSys 2019) zeigten, wie man das korrigiert, indem man vom Score jedes Artikels den Logarithmus seiner geschätzten Sampling-Häufigkeit abzieht, und setzten das korrigierte Modell bei YouTube auf einem Bestand von zig Millionen Videos ein. In PyTorch ist der Kern kurz:

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)

Hier ist log_q der Logarithmus des Anteils jedes Artikels an den Trainingsinteraktionen. Die Towers nehmen IDs, damit das Beispiel klein bleibt; in einem echten Shop nimmt der User-Tower die letzten N angesehenen Artikel und den Kontext statt einer User-ID, was auch bedeutet, dass ein Erstbesucher schon nach einem Klick ein Embedding erhält.

Behalten Sie die Co-View-Listen aus Stufe 1 als zweite Kandidatenquelle. Two-Tower-Retrieval und Co-Views scheitern auf unterschiedliche Weise, und der Ranker kann beide nutzen.

Ranking. Der Ranker sieht einige hundert Kandidaten pro Anfrage und kann sich viele Features leisten: die Scores jeder Kandidatenquelle, den Preis relativ zur üblichen Preisspanne der Person, ob der Artikel eine Kategorie mit den letzten Ansichten teilt, den jüngsten Ansichtstrend des Artikels, die Tage seit Aufnahme des Artikels. Ein Gradient-Boosted-Learning-to-Rank-Modell ist ein verbreiteter erster Ranker, und seine Feature Importances lassen sich leicht prüfen:

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())

Geschäftsregeln nach dem Modell. Bestand, Marge und Richtlinien gehören hinter das Ranking, in einfachen Code, den das Business lesen und ändern kann: Artikel entfernen, die nicht lieferbar sind, die Zahl der Artikel pro Marke in einem Block begrenzen, Kategorien ausschließen, die nicht quer verkauft werden dürfen, und wenn Marge zählt, einen begrenzten Boost anwenden, statt das Modell auf Marge neu zu trainieren. Bleiben die Regeln außerhalb des Modells, kann ein Merchandiser sie an einem Dienstag ändern, ohne ML-Release.

Offline-Evaluation, und warum sie nicht die Antwort ist

Teilen Sie nach Zeit, nicht zufällig: Trainieren Sie auf allem vor einem Stichtag und evaluieren Sie auf der Woche danach. Ein zufälliger Split lässt die Zukunft ins Training durchsickern und bläht jede Kennzahl auf.

Messen Sie jede Stufe mit ihrer eigenen Kennzahl. Für die Kandidatengenerierung recall@k: Wie viele der Artikel, mit denen die Person als Nächstes tatsächlich interagiert hat, standen in der Kandidatenliste. Für das Ranking NDCG@k: Landeten diese Artikel weit oben.

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

Offline-Zahlen entscheiden, welches Modell in den Test geht. Sie entscheiden nicht, ob es live geht. Die Logs wurden vom alten System erzeugt, also wird das neue Modell daran gemessen, was Kunden taten, als ihnen etwas anderes gezeigt wurde, und die Position auf der Seite prägt Klicks unabhängig von der Relevanz. Covington et al. schrieben, dass „live A/B results are not always correlated with offline experiments“, und die beiden Gründe oben reichen, um das zu erwarten.

Der A/B-Test

  • Eine primäre Kennzahl, die zu Beginn vereinbarte, etwa Umsatz pro Session. Legen Sie den kleinsten Lift fest, den zu erkennen sich lohnt, und berechnen Sie die Stichprobengröße vor dem Start, wie in der Rechnung oben.
  • Randomisieren Sie nach Besucher, nicht nach Seitenaufruf, damit eine Person nicht beide Versionen sieht.
  • Laufen Sie ganze Wochen, mindestens zwei, weil sich Kunden unter der Woche und am Wochenende unterschiedlich verhalten. Brechen Sie nicht früh ab, weil die Kurve am vierten Tag gut aussieht.
  • Leitplanken-Kennzahlen, die sich nicht verschlechtern dürfen: Antwortzeit der Seite, der Anteil empfohlener Artikel, die beim Klick nicht vorrätig sind, die Retourenquote bei Bestellungen mit empfohlenen Artikeln und der Umsatz in Kategorien, die das neue Modell seltener zeigt.
  • Behalten Sie das alte System als Kontrolle, bis die primäre Kennzahl etwas anderes sagt, und behalten Sie den Schalter für den Rollback.

Cold Start

Neue Produkte haben keine Interaktionen, also kennen weder Co-Views noch das ID-Embedding des Item-Towers sie. Platzieren Sie sie anhand von Attributen (Kategorie, Marke, Preisspanne, Text- und Bild-Embeddings) und reservieren Sie in jedem Block eine kleine Zahl von Plätzen für neu aufgenommene Artikel, damit sie die nötigen Interaktionen sammeln. Neue Besucher erhalten kontextbasierte Kandidaten (die Einstiegsseite, die Kategorie, das Gerät) und beliebte Artikel und wechseln nach ihren ersten Ansichten in der Session zum personalisierten Retrieval.

Was es kostet, Stufe für Stufe

Unsere Spanne für ein Empfehlungssystem liegt bei 14k–42k € über 2–4 Monate. Die folgende Tabelle ist eine illustrative Aufteilung für ein mittelgroßes Projekt, das beide Stufen durchläuft, bepreist nach unserer veröffentlichten Preisliste. Sie ist kein Angebot und keine Aufzeichnung eines vergangenen Projekts.

Stufe Rollen und Stunden Kosten
Datenaudit, Kennzahl, Design des A/B-Tests Data 30 h à 45 €, Senior ML 20 h à 70 € 2.750 €
Stufe 1: Co-Views, Fallback, API ML 60 h à 50 €, Backend 40 h à 40 € 4.600 €
Stufe 2: Two-Tower-Retrieval und Ranker Senior ML 80 h à 70 €, ML 80 h à 50 € 9.600 €
Durchführung und Auswertung des A/B-Tests Senior ML 15 h à 70 €, Data 20 h à 45 € 1.950 €
Pipelines, Monitoring, Rollback MLOps 80 h à 55 € 4.400 €
Über alle Stufen QA 30 h à 30 €, PM 50 h à 40 € 2.900 €
Summe 505 h 26.200 €

Ein Projekt, das nach Stufe 1 endet, weil der erste Test die geschäftliche Frage beantwortet hat, liegt am unteren Ende der Spanne. Kataloge mit vielen Datenquellen, mehrere getrennt getestete Platzierungen oder strenge Vorgaben für die Antwortzeit schieben es Richtung oberes Ende. Die Seite zu Empfehlungssystemen beschreibt, wie ein solches Projekt abläuft, und der Rechner unter /pricing bepreist Ihren eigenen Umfang nach derselben Preisliste.

Quellen

Haben Sie einen Prozess, der zu viel kostet?

Erzählen Sie uns davon. Das erste Gespräch ist kostenlos, und wenn KI nicht die Antwort ist, sagen wir das.

Aufgabe besprechen