oc-p10-content-recommendation/azure-app/utils/content_based.py

95 lines
No EOL
4.1 KiB
Python

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Optional, Tuple, Dict
import logging
from .data_loader import load_embeddings, get_user_articles
def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray:
"""Calcule la moyenne des embeddings pour une liste d'articles."""
valid_ids = [aid for aid in article_ids if aid < len(embeddings)]
if not valid_ids:
raise ValueError("No valid article IDs found")
articles_embeddings = embeddings[valid_ids]
return np.mean(articles_embeddings, axis=0).reshape(1, -1)
def _compute_similarities(reference_embedding: np.ndarray, embeddings: np.ndarray) -> np.ndarray:
"""Calcule la similarité cosinus entre l'embedding de référence et tous les embeddings."""
return cosine_similarity(reference_embedding, embeddings)[0]
def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]:
"""Filtre les candidats en excluant les articles déjà vus."""
candidates = []
for article_id, similarity in enumerate(similarities):
if article_id not in clicked_articles:
candidates.append((article_id, similarity))
return candidates
def _get_top_recommendations_with_scores(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[Dict[str, any]]:
"""Trie les candidats et retourne le top N avec les scores."""
candidates.sort(key=lambda x: x[1], reverse=True)
recommendations = []
for article_id, score in candidates[:n_recommendations]:
recommendations.append({
'article_id': article_id,
'similarity_score': float(score)
})
return recommendations
def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]:
"""Valide et filtre les articles dans les limites des embeddings."""
valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)]
if not valid_articles:
raise ValueError("No valid articles found for user")
return valid_articles
def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]:
"""
Génère des recommandations pour un utilisateur donné (version compatibilité).
Retourne seulement les IDs pour maintenir la compatibilité.
"""
recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations)
if recommendations_with_scores:
return [rec['article_id'] for rec in recommendations_with_scores]
return None
def get_recommendations_with_scores(user_id: int, n_recommendations: int = 5) -> Optional[List[Dict[str, any]]]:
"""
Génère des recommandations avec scores pour un utilisateur donné.
Args:
user_id: ID de l'utilisateur
n_recommendations: Nombre de recommandations à retourner
Returns:
Liste des recommandations avec article_id et similarity_score ou None si erreur
"""
try:
# Charger les données
embeddings = load_embeddings()
clicked_articles = get_user_articles(user_id)
if not clicked_articles:
logging.warning(f"No articles found for user {user_id}")
return None
# Valider les articles
valid_articles = _validate_user_articles(clicked_articles, embeddings)
# Calculer l'embedding de référence (stratégie mean)
reference_embedding = _calculate_mean_embedding(embeddings, valid_articles)
# Calculer similarités
similarities = _compute_similarities(reference_embedding, embeddings)
# Filtrer et trier
candidates = _filter_candidates(similarities, clicked_articles)
recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations)
logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}")
return recommendations
except Exception as e:
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
return None