Zum Hauptinhalt springen
Lektion 19 von 22 · Embeddings, RAG und semantische Suche

Personalisierte Empfehlungen

Lektion 19 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Das Prinzip: Ähnlichkeit gleich Empfehlung
  3. Inhaltsbasierte Empfehlung
  4. Nutzerprofil aus Historie
  5. Empfehlung mit Textabfrage
  6. Diversifizierung von Empfehlungen
  7. Zusammenfassung

Lernziele

  • Aufbau eines Empfehlungssystems auf Basis von Embeddings
  • Implementierung von inhaltsbasierten und nutzerbasierten Empfehlungen
  • Umgang mit dem Cold-Start-Problem und Diversität

Das Prinzip: Ähnlichkeit gleich Empfehlung

Ein klassisches Empfehlungssystem beobachtet das Verhalten: „Wer diesen Artikel liest, liest auch diesen anderen.” Es benötigt viel Traffic, bevor es nützliche Aussagen treffen kann – dies ist das Cold-Start-Problem, das sowohl neue Kataloge als auch heute Morgen veröffentlichte Artikel trifft.

Der Embeddings-Ansatz überspringt dieses Warten. Wenn ein Nutzer einen Artikel magte, werden die Artikel mit den ähnlichsten Embeddings empfohlen. Dies ist inhaltsbasierte Empfehlung: Sie stützt sich auf den Inhalt, nicht auf das Verhalten anderer Nutzer. Ein Artikel, der vor zehn Minuten veröffentlicht wurde und noch von niemandem geöffnet wurde, besitzt bereits ein Embedding und ist sofort empfehlbar.

Inhaltsbasierte Empfehlung

Der untenstehende Katalog mischt absichtlich zwei Welten – maschinelles Lernen auf der einen Seite und Softwareentwicklung auf der anderen –, sodass die Ergebnisse überprüfbar sind. Jeder Artikel wird durch Konkatenation von Titel und Inhalt codiert: Der Titel erfasst das Thema auf den Punkt, der Inhalt liefert das Vokabular, und die Verwendung nur eines von beiden verschlechtert die Qualität der gefundenen Nachbarn.

Der Trick liegt in der Zeile scores[idx] = -1. Ohne sie wäre der dem Artikel am ähnlichste Artikel immer er selbst, mit einem Score von 1,0 – und man würde einem Leser empfehlen, was er gerade beendet hat.

from openai import OpenAI
import numpy as np

client = OpenAI()

# Artikelkatalog
articles = [
    {"id": "a1", "titre": "Introduction au machine learning",
     "contenu": "Le ML permet aux machines d'apprendre à partir de données..."},
    {"id": "a2", "titre": "Deep learning avec PyTorch",
     "contenu": "PyTorch est un framework de deep learning flexible..."},
    {"id": "a3", "titre": "Déployer une API REST avec FastAPI",
     "contenu": "FastAPI est un framework Python pour créer des APIs..."},
    {"id": "a4", "titre": "Les bases de PostgreSQL",
     "contenu": "PostgreSQL est un SGBD relationnel open source..."},
    {"id": "a5", "titre": "Réseaux de neurones convolutifs",
     "contenu": "Les CNN sont utilisés pour la vision par ordinateur..."},
    {"id": "a6", "titre": "NLP avec les transformers",
     "contenu": "Les transformers ont révolutionné le traitement du langage..."},
]

# Katalog indizieren
textes = [f"{a['titre']}. {a['contenu']}" for a in articles]
response = client.embeddings.create(
    input=textes, model="text-embedding-3-large"
)
catalogue_embs = np.array(
    [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
)

def recommander_similaires(
    article_id: str,
    k: int = 3
) -> list[dict]:
    """Empfiehlt die k ähnlichsten Artikel."""
    idx = next(i for i, a in enumerate(articles) if a["id"] == article_id)
    article_emb = catalogue_embs[idx]

    # Ähnlichkeit mit allen anderen Artikeln
    scores = catalogue_embs @ article_emb
    scores[idx] = -1  # Den Artikel selbst ausschließen

    top_k = np.argsort(scores)[-k:][::-1]
    return [
        {"article": articles[i], "score": float(scores[i])}
        for i in top_k
    ]

# Ein Nutzer liest "Einführung in maschinelles Lernen"
recos = recommander_similaires("a1", k=3)
print("Sie lesen: Introduction au machine learning")
print("Empfehlungen:")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Nutzerprofil aus Historie

Empfehlungen basierend auf einem einzelnen Artikel fangen den Leser schnell ein. Um die gesamte Reise zu berücksichtigen, erstellen Sie einen Profilvektor, indem Sie die Embeddings der von ihm angesehenen Artikel kombinieren: Dieser Durchschnittspunkt repräsentiert seine Interessen im selben Raum wie der Katalog, und Sie suchen dann nach den Artikeln, die diesem Punkt am nächsten liegen.

Lesungen von vor sechs Monaten sollten jedoch nicht so viel wiegen wie die von gestern. Gewichtung nach Aktualität (Recency weighting) bewältigt dies: Der Faktor 0.5 ** (n - 1 - i) halbiert das Gewicht bei jedem Schritt zurück in der Historie. Ein Leser, der lange maschinelles Lernen verfolgt hat, aber kürzlich zu APIs gewechselt ist, wird seine Empfehlungen innerhalb weniger Artikel verschieben sehen, anstatt eingefroren zu bleiben. Die finale Normalisierung bringt das Profil zurück auf die Einheitskugel, eine Bedingung dafür, dass das Skalarprodukt mit dem Katalog eine echte Kosinusähnlichkeit darstellt.

def creer_profil_utilisateur(
    articles_consultes: list[str],
    poids_recency: bool = True
) -> np.ndarray:
    """Erstellt einen Profilvektor aus der Historie."""
    indices = [
        i for i, a in enumerate(articles)
        if a["id"] in articles_consultes
    ]

    if not indices:
        return np.zeros(catalogue_embs.shape[1])

    embs = catalogue_embs[indices]

    if poids_recency:
        # Aktuelle Artikel zählen mehr
        n = len(indices)
        poids = np.array([0.5 ** (n - 1 - i) for i in range(n)])
        poids /= poids.sum()
        profil = np.average(embs, axis=0, weights=poids)
    else:
        profil = np.mean(embs, axis=0)

    # Normalisieren
    profil /= np.linalg.norm(profil)
    return profil

def recommander_pour_utilisateur(
    articles_consultes: list[str],
    k: int = 3
) -> list[dict]:
    """Empfiehlt Artikel basierend auf dem Nutzerprofil."""
    profil = creer_profil_utilisateur(articles_consultes)

    scores = catalogue_embs @ profil
    # Bereits angezeigte Artikel ausschließen
    for i, a in enumerate(articles):
        if a["id"] in articles_consultes:
            scores[i] = -1

    top_k = np.argsort(scores)[-k:][::-1]
    return [
        {"article": articles[i], "score": float(scores[i])}
        for i in top_k
    ]

# Nutzer, der ML und Deep Learning gelesen hat
recos = recommander_pour_utilisateur(["a1", "a2"], k=3)
print("Historie: ML + PyTorch")
print("Empfehlungen:")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Empfehlung mit Textabfrage

Das verhaltensbasierte Profil ignoriert etwas, das der Nutzer besser als Sie kennt: wonach er heute sucht. Da der Katalog bereits codiert ist, müssen Sie lediglich einen freien Textausdruck codieren, um denselben Raum abzufragen. „Ich möchte lernen, wie man APIs erstellt” bringt den FastAPI-Artikel hervor, auch wenn die Historie des Lesers nur maschinelles Lernen enthält. Dies ist auch die einzige Option für einen Besucher, über den Sie nichts wissen.

def recommander_par_interet(
    description: str,
    k: int = 3
) -> list[dict]:
    """Empfiehlt Artikel basierend auf einer Beschreibung des Interesses."""
    query_emb = client.embeddings.create(
        input=description,
        model="text-embedding-3-large"
    ).data[0].embedding

    scores = catalogue_embs @ np.array(query_emb)
    top_k = np.argsort(scores)[-k:][::-1]

    return [
        {"article": articles[i], "score": float(scores[i])}
        for i in top_k
    ]

# "Ich möchte lernen, wie man Web-APIs in Python erstellt"
recos = recommander_par_interet("créer des APIs web en Python")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Diversifizierung von Empfehlungen

Das Sortieren nach reiner Ähnlichkeit erzeugt einen offensichtlichen Fehler in der Produktion: Die fünf Empfehlungen sehen aus wie eine Kopie. Ein Leser des CNN-Artikels erhält fünf Varianten der Computer Vision, während der zweite oder dritte Vorschlag das Tor zu Transformern hätte öffnen können.

MMR (Maximal Marginal Relevance) behebt dies, indem er Artikel einzeln auswählt: In jedem Schritt wählt er denjenigen aus, der die Relevanz zur Abfrage maximiert, abzüglich der maximalen Redundanz mit dem bereits Ausgewählten. Der lambda_param setzt das Verhältnis – bei 1,0 erhält man das klassische Sortieren, bei 0,5 wird die Vielfalt stark bevorzugt, und der Standardwert von 0,7 liefert eine relevante Liste, die „atmet”.

def recommander_divers(
    query_emb: np.ndarray,
    k: int = 5,
    lambda_param: float = 0.7
) -> list[int]:
    """MMR: balanciert Relevanz und Vielfalt."""
    scores = catalogue_embs @ query_emb
    candidats = list(range(len(articles)))
    selectionnes = []

    for _ in range(k):
        if not candidats:
            break

        meilleur_score = -float("inf")
        meilleur_idx = -1

        for idx in candidats:
            # Relevanz
            pertinence = scores[idx]

            # Maximale Redundanz mit bereits Ausgewählten
            if selectionnes:
                sims_selectionnes = [
                    float(np.dot(catalogue_embs[idx], catalogue_embs[s]))
                    for s in selectionnes
                ]
                redondance = max(sims_selectionnes)
            else:
                redondance = 0

            # MMR-Score
            mmr_score = (
                lambda_param * pertinence
                - (1 - lambda_param) * redondance
            )

            if mmr_score > meilleur_score:
                meilleur_score = mmr_score
                meilleur_idx = idx

        selectionnes.append(meilleur_idx)
        candidats.remove(meilleur_idx)

    return selectionnes

Zusammenfassung

  • Embeddings ermöglichen Empfehlungen basierend auf inhaltlicher Ähnlichkeit
  • Das Nutzerprofil wird durch den Durchschnitt der angesehenen Embeddings erstellt
  • Die Gewichtung nach Aktualität gibt aktuellen Ansichten mehr Gewicht
  • MMR diversifiziert Empfehlungen, indem er Redundanz bestraft