Azure app completed and deployed as serverless function
This commit is contained in:
parent
1a9656b441
commit
25639153aa
12 changed files with 2244 additions and 1 deletions
|
|
@ -10,7 +10,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|||
from utils.data_loader import load_embeddings, load_user_data
|
||||
from utils.content_based import get_recommendations
|
||||
|
||||
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
||||
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
|
||||
|
||||
@app.route(route="recommend")
|
||||
def recommend(req: func.HttpRequest) -> func.HttpResponse:
|
||||
|
|
|
|||
BIN
azure-app_deployment.zip
Normal file
BIN
azure-app_deployment.zip
Normal file
Binary file not shown.
1884
azure-app_deployment/data/clicks_sample.csv
Normal file
1884
azure-app_deployment/data/clicks_sample.csv
Normal file
File diff suppressed because it is too large
Load diff
BIN
azure-app_deployment/data/embeddings_azure_50d.pkl
Normal file
BIN
azure-app_deployment/data/embeddings_azure_50d.pkl
Normal file
Binary file not shown.
73
azure-app_deployment/function_app.py
Normal file
73
azure-app_deployment/function_app.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import azure.functions as func
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ajouter le dossier utils au path pour les imports
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from utils.data_loader import load_embeddings, load_user_data
|
||||
from utils.content_based import get_recommendations
|
||||
|
||||
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
||||
|
||||
@app.route(route="recommend")
|
||||
def recommend(req: func.HttpRequest) -> func.HttpResponse:
|
||||
logging.info('Recommendation request received.')
|
||||
|
||||
# Récupérer le user_id depuis les paramètres de requête
|
||||
user_id = req.params.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
try:
|
||||
req_body = req.get_json()
|
||||
user_id = req_body.get('user_id') if req_body else None
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not user_id:
|
||||
return func.HttpResponse(
|
||||
json.dumps({"error": "user_id parameter required"}),
|
||||
status_code=400,
|
||||
mimetype="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
# Convertir en entier
|
||||
user_id = int(user_id)
|
||||
|
||||
# Générer les recommandations
|
||||
recommendations = get_recommendations(user_id)
|
||||
|
||||
if recommendations:
|
||||
return func.HttpResponse(
|
||||
json.dumps({
|
||||
"user_id": user_id,
|
||||
"recommendations": recommendations
|
||||
}),
|
||||
status_code=200,
|
||||
mimetype="application/json"
|
||||
)
|
||||
else:
|
||||
return func.HttpResponse(
|
||||
json.dumps({
|
||||
"error": f"No recommendations found for user {user_id}"
|
||||
}),
|
||||
status_code=404,
|
||||
mimetype="application/json"
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
return func.HttpResponse(
|
||||
json.dumps({"error": "user_id must be a valid integer"}),
|
||||
status_code=400,
|
||||
mimetype="application/json"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating recommendations: {str(e)}")
|
||||
return func.HttpResponse(
|
||||
json.dumps({"error": "Internal server error"}),
|
||||
status_code=500,
|
||||
mimetype="application/json"
|
||||
)
|
||||
15
azure-app_deployment/host.json
Normal file
15
azure-app_deployment/host.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
}
|
||||
}
|
||||
4
azure-app_deployment/requirements.txt
Normal file
4
azure-app_deployment/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
azure-functions
|
||||
numpy
|
||||
scikit-learn
|
||||
pandas
|
||||
92
azure-app_deployment/test_local.py
Normal file
92
azure-app_deployment/test_local.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Ajouter le dossier utils au path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from utils.content_based import get_recommendations
|
||||
from utils.data_loader import get_available_users, check_data_files, load_embeddings
|
||||
|
||||
def test_data_loading():
|
||||
"""Test du chargement des données."""
|
||||
print("🔄 Test du chargement des données...")
|
||||
|
||||
if not check_data_files():
|
||||
print("❌ Fichiers de données manquants")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Test chargement embeddings
|
||||
embeddings = load_embeddings()
|
||||
print(f"✅ Embeddings chargés: {embeddings.shape}")
|
||||
|
||||
# Test chargement utilisateurs
|
||||
users = get_available_users()
|
||||
print(f"✅ Utilisateurs chargés: {len(users)} utilisateurs")
|
||||
print(f" Premiers utilisateurs: {users[:5]}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors du chargement: {e}")
|
||||
return False
|
||||
|
||||
def test_recommendations():
|
||||
"""Test des recommandations."""
|
||||
print("\n🔄 Test des recommandations...")
|
||||
|
||||
users = get_available_users()
|
||||
test_users = users[:3] if len(users) >= 3 else users
|
||||
|
||||
for user_id in test_users:
|
||||
print(f"\n👤 Test pour utilisateur {user_id}:")
|
||||
|
||||
try:
|
||||
recommendations = get_recommendations(user_id, n_recommendations=5)
|
||||
|
||||
if recommendations:
|
||||
print(f"✅ {len(recommendations)} recommandations générées")
|
||||
print(f" Articles recommandés: {recommendations}")
|
||||
else:
|
||||
print("⚠️ Aucune recommandation générée")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur pour utilisateur {user_id}: {e}")
|
||||
|
||||
def test_edge_cases():
|
||||
"""Test des cas limites."""
|
||||
print("\n🔄 Test des cas limites...")
|
||||
|
||||
# Test utilisateur inexistant
|
||||
print("Test utilisateur inexistant (ID: 99999):")
|
||||
result = get_recommendations(99999)
|
||||
if result is None:
|
||||
print("✅ Utilisateur inexistant géré correctement")
|
||||
else:
|
||||
print(f"⚠️ Résultat inattendu: {result}")
|
||||
|
||||
def main():
|
||||
"""Fonction principale de test."""
|
||||
print("🚀 Démarrage des tests locaux\n")
|
||||
|
||||
# Test 1: Chargement des données
|
||||
if not test_data_loading():
|
||||
print("\n❌ Tests interrompus - problème de chargement")
|
||||
return
|
||||
|
||||
# Test 2: Recommandations
|
||||
test_recommendations()
|
||||
|
||||
# Test 3: Cas limites
|
||||
test_edge_cases()
|
||||
|
||||
print("\n✨ Tests terminés")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
azure-app_deployment/utils/__init__.py
Normal file
0
azure-app_deployment/utils/__init__.py
Normal file
79
azure-app_deployment/utils/content_based.py
Normal file
79
azure-app_deployment/utils/content_based.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import numpy as np
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from typing import List, Optional, Tuple
|
||||
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(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[int]:
|
||||
"""Trie les candidats et retourne le top N."""
|
||||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
return [article_id for article_id, _ in candidates[:n_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é.
|
||||
|
||||
Args:
|
||||
user_id: ID de l'utilisateur
|
||||
n_recommendations: Nombre de recommandations à retourner
|
||||
|
||||
Returns:
|
||||
Liste des article_ids recommandés 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(candidates, n_recommendations)
|
||||
|
||||
logging.info(f"Generated {len(recommendations)} recommendations for user {user_id}")
|
||||
return recommendations
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating recommendations for user {user_id}: {str(e)}")
|
||||
return None
|
||||
87
azure-app_deployment/utils/data_loader.py
Normal file
87
azure-app_deployment/utils/data_loader.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import pickle
|
||||
import pandas as pd
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
import numpy as np
|
||||
|
||||
# Chemins des fichiers
|
||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
|
||||
EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl')
|
||||
CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv')
|
||||
|
||||
# Cache global
|
||||
_embeddings_cache: Optional[np.ndarray] = None
|
||||
_user_data_cache: Optional[Dict[int, List[int]]] = None
|
||||
|
||||
def _load_pickle_file(file_path: str) -> np.ndarray:
|
||||
"""Charge un fichier pickle."""
|
||||
with open(file_path, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
|
||||
def _load_csv_file(file_path: str) -> pd.DataFrame:
|
||||
"""Charge un fichier CSV."""
|
||||
return pd.read_csv(file_path)
|
||||
|
||||
def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]:
|
||||
"""Convertit le DataFrame en dictionnaire user->articles."""
|
||||
user_data = {}
|
||||
for _, row in clicks_df.iterrows():
|
||||
user_id = int(row['user_id'])
|
||||
article_id = int(row['click_article_id'])
|
||||
|
||||
if user_id not in user_data:
|
||||
user_data[user_id] = []
|
||||
user_data[user_id].append(article_id)
|
||||
|
||||
return user_data
|
||||
|
||||
def load_embeddings() -> np.ndarray:
|
||||
"""Charge les embeddings avec cache."""
|
||||
global _embeddings_cache
|
||||
|
||||
if _embeddings_cache is None:
|
||||
try:
|
||||
_embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH)
|
||||
logging.info(f"Embeddings loaded: {_embeddings_cache.shape}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading embeddings: {str(e)}")
|
||||
raise
|
||||
|
||||
return _embeddings_cache
|
||||
|
||||
def load_user_data() -> Dict[int, List[int]]:
|
||||
"""Charge les données utilisateur avec cache."""
|
||||
global _user_data_cache
|
||||
|
||||
if _user_data_cache is None:
|
||||
try:
|
||||
clicks_df = _load_csv_file(CLICKS_PATH)
|
||||
_user_data_cache = _process_clicks_data(clicks_df)
|
||||
logging.info(f"User data loaded: {len(_user_data_cache)} users")
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading user data: {str(e)}")
|
||||
raise
|
||||
|
||||
return _user_data_cache
|
||||
|
||||
def get_user_articles(user_id: int) -> Optional[List[int]]:
|
||||
"""Récupère les articles d'un utilisateur."""
|
||||
user_data = load_user_data()
|
||||
return user_data.get(user_id)
|
||||
|
||||
def check_data_files() -> bool:
|
||||
"""Vérifie que tous les fichiers de données existent."""
|
||||
files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH]
|
||||
|
||||
for file_path in files_to_check:
|
||||
if not os.path.exists(file_path):
|
||||
logging.error(f"Missing data file: {file_path}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_available_users() -> List[int]:
|
||||
"""Retourne la liste de tous les user_ids disponibles."""
|
||||
user_data = load_user_data()
|
||||
return list(user_data.keys())
|
||||
9
streamlit_requirements.txt
Normal file
9
streamlit_requirements.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Azure Functions
|
||||
azure-functions
|
||||
numpy
|
||||
pandas
|
||||
scikit-learn
|
||||
|
||||
# Interface Streamlit
|
||||
streamlit>=1.28.0
|
||||
requests>=2.25.0
|
||||
Loading…
Add table
Reference in a new issue