Aller au contenu principal

Recommandations personnalisées

Objectifs

  • Construire un système de recommandation basé sur les embeddings
  • Implémenter la recommandation par contenu et par utilisateur
  • Gérer le cold start et la diversité

Le principe : similarité = recommandation

Si un utilisateur a aimé un article, recommandez-lui les articles dont les embeddings sont les plus proches. C’est la recommandation content-based : elle se base sur le contenu, pas sur les comportements d’autres utilisateurs.

Recommandation par contenu

from openai import OpenAI
import numpy as np

client = OpenAI()

# Catalogue d'articles
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..."},
]

# Indexer le catalogue
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]:
    """Recommande les k articles les plus similaires."""
    idx = next(i for i, a in enumerate(articles) if a["id"] == article_id)
    article_emb = catalogue_embs[idx]

    # Similarité avec tous les autres articles
    scores = catalogue_embs @ article_emb
    scores[idx] = -1  # Exclure l'article lui-même

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

# Un utilisateur lit "Introduction au ML"
recos = recommander_similaires("a1", k=3)
print("Vous avez lu : Introduction au machine learning")
print("Recommandations :")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Profil utilisateur par historique

Combinez les embeddings des articles consultés pour créer un profil :

def creer_profil_utilisateur(
    articles_consultes: list[str],
    poids_recency: bool = True
) -> np.ndarray:
    """Crée un vecteur profil à partir de l'historique."""
    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:
        # Les articles récents comptent plus
        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)

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

def recommander_pour_utilisateur(
    articles_consultes: list[str],
    k: int = 3
) -> list[dict]:
    """Recommande des articles basés sur le profil utilisateur."""
    profil = creer_profil_utilisateur(articles_consultes)

    scores = catalogue_embs @ profil
    # Exclure les articles déjà consultés
    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
    ]

# Utilisateur qui a lu du ML et du deep learning
recos = recommander_pour_utilisateur(["a1", "a2"], k=3)
print("Historique : ML + PyTorch")
print("Recommandations :")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Recommandation avec requête texte

Permettez à l’utilisateur de décrire ce qu’il cherche :

def recommander_par_interet(
    description: str,
    k: int = 3
) -> list[dict]:
    """Recommande des articles selon une description d'intérêt."""
    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
    ]

# "Je veux apprendre à créer des APIs"
recos = recommander_par_interet("créer des APIs web en Python")
for r in recos:
    print(f"  [{r['score']:.3f}] {r['article']['titre']}")

Diversifier les recommandations

Évitez de recommander des articles trop similaires entre eux avec le MMR (Maximal Marginal Relevance) :

def recommander_divers(
    query_emb: np.ndarray,
    k: int = 5,
    lambda_param: float = 0.7
) -> list[int]:
    """MMR : équilibre pertinence et diversité."""
    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:
            # Pertinence
            pertinence = scores[idx]

            # Redondance max avec les déjà sélectionnés
            if selectionnes:
                sims_selectionnes = [
                    float(np.dot(catalogue_embs[idx], catalogue_embs[s]))
                    for s in selectionnes
                ]
                redondance = max(sims_selectionnes)
            else:
                redondance = 0

            # Score MMR
            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

Résumé

  • Les embeddings permettent de recommander par similarité de contenu
  • Le profil utilisateur se construit en moyennant les embeddings consultés
  • La pondération par récence donne plus de poids aux consultations récentes
  • Le MMR diversifie les recommandations en pénalisant la redondance