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