Portfolio 2026년 2월 22일
ML 기반 추천 시스템 - 협업 필터링 및 딥러닝 아키텍처

ML 기반 추천 시스템 - 협업 필터링 및 딥러닝 아키텍처

협업 필터링과 딥러닝을 결합한 실시간 추천 시스템

#Python #TensorFlow #Recommendation System #Collaborative Filtering #Deep Learning

image: “/images/projects/ml-recommendation-system.png”

프로젝트 개요

이 프로젝트는 협업 필터링(Collaborative Filtering)과 딥러닝 모델을 결합한 추천 시스템입니다. 사용자 행동 데이터를 분석하여 개인화된 콘텐츠를 추천하고, A/B 테스트를 통해 추천 품질을 지속적으로 개선했습니다. 온라인 학습(Online Learning)을 통해 실시간으로 사용자 피드백을 반영합니다.

주요 목표:

  • CTR(클릭률) 30% 향상
  • 실시간 추천 응답 시간 100ms 미만
  • 새로운 아이템 Cold Start 문제 해결
  • 다양한 추천 알고리즘 비교 및 최적화

기술 스택

카테고리기술
ML 프레임워크TensorFlow 2.14, PyTorch 2.1
데이터 처리Pandas, NumPy, Apache Spark
추천 알고리즘Surprise, LightFM, TensorFlow Recommenders
벡터 데이터베이스FAISS, Milvus
API 서버FastAPI, Redis
MLOpsMLflow, Kubeflow, Prometheus
데이터 저장소PostgreSQL, MongoDB, Redis
스트리밍Apache Kafka
모니터링Grafana, TensorBoard

주요 기능

1. 협업 필터링 모델 (Matrix Factorization)

행렬 분해를 사용한 사용자-아이템 상호작용 예측입니다.

# models/collaborative_filtering.py
import numpy as np
from typing import List, Tuple
from dataclasses import dataclass

@dataclass
class CollaborativeFilteringConfig:
    n_factors: int = 50
    learning_rate: float = 0.01
    regularization: float = 0.1
    n_epochs: int = 100
    batch_size: int = 1000

class MatrixFactorization:
    def __init__(self, config: CollaborativeFilteringConfig):
        self.config = config
        self.n_users = 0
        self.n_items = 0
        self.user_factors = None
        self.item_factors = None
        self.user_bias = None
        self.item_bias = None
        self.global_bias = 0.0

    def fit(self, ratings: np.ndarray) -> None:
        """
        SGD를 사용한 행렬 분해 학습

        Args:
            ratings: (n_ratings, 3) 형태의 배열 [user_id, item_id, rating]
        """
        self.n_users = int(ratings[:, 0].max()) + 1
        self.n_items = int(ratings[:, 1].max()) + 1

        # 잠재 요소 초기화 (Xavier 초기화)
        self.user_factors = np.random.randn(self.n_users, self.config.n_factors) * 0.01
        self.item_factors = np.random.randn(self.n_items, self.config.n_factors) * 0.01
        self.user_bias = np.zeros(self.n_users)
        self.item_bias = np.zeros(self.n_items)
        self.global_bias = ratings[:, 2].mean()

        n_samples = len(ratings)
        indices = np.arange(n_samples)

        for epoch in range(self.config.n_epochs):
            np.random.shuffle(indices)

            for start in range(0, n_samples, self.config.batch_size):
                end = min(start + self.config.batch_size, n_samples)
                batch_indices = indices[start:end]
                batch = ratings[batch_indices]

                self._update_batch(batch)

            # 로그 기록
            mse = self._compute_mse(ratings)
            print(f"Epoch {epoch + 1}/{self.config.n_epochs}, MSE: {mse:.4f}")

    def _update_batch(self, batch: np.ndarray) -> None:
        """배치별 SGD 업데이트"""
        user_ids = batch[:, 0].astype(int)
        item_ids = batch[:, 1].astype(int)
        actual_ratings = batch[:, 2]

        # 예측 계산
        user_f = self.user_factors[user_ids]
        item_f = self.item_factors[item_ids]
        predicted = (self.global_bias +
                     self.user_bias[user_ids] +
                     self.item_bias[item_ids] +
                     np.sum(user_f * item_f, axis=1))

        # 오차 계산
        errors = actual_ratings - predicted

        # 그라디언트 계산 및 업데이트
        lr = self.config.learning_rate
        reg = self.config.regularization

        # 사용자 요소 업데이트
        user_f_grad = lr * (errors[:, np.newaxis] * item_f - reg * user_f)
        self.user_factors[user_ids] += user_f_grad

        # 아이템 요소 업데이트
        item_f_grad = lr * (errors[:, np.newaxis] * user_f - reg * item_f)
        self.item_factors[item_ids] += item_f_grad

        # 바이어스 업데이트
        self.user_bias[user_ids] += lr * (errors - reg * self.user_bias[user_ids])
        self.item_bias[item_ids] += lr * (errors - reg * self.item_bias[item_ids])

    def predict(self, user_id: int, item_id: int) -> float:
        """단일 사용자-아이템 쌍에 대한 예측"""
        if user_id >= self.n_users or item_id >= self.n_items:
            return self.global_bias

        prediction = (self.global_bias +
                      self.user_bias[user_id] +
                      self.item_bias[item_id] +
                      np.dot(self.user_factors[user_id], self.item_factors[item_id]))

        return min(5.0, max(1.0, prediction))

    def recommend(self, user_id: int, top_k: int = 10, exclude_seen: List[int] = None) -> List[Tuple[int, float]]:
        """
        사용자에게 추천할 상위 K개 아이템 반환

        Args:
            user_id: 사용자 ID
            top_k: 추천 아이템 수
            exclude_seen: 이미 평가한 아이템 제외

        Returns:
            [(item_id, predicted_rating), ...]
        """
        if exclude_seen is None:
            exclude_seen = []

        # 모든 아이템에 대한 예측 계산
        predictions = []
        for item_id in range(self.n_items):
            if item_id in exclude_seen:
                continue

            pred = self.predict(user_id, item_id)
            predictions.append((item_id, pred))

        # 상위 K개 추천
        predictions.sort(key=lambda x: x[1], reverse=True)
        return predictions[:top_k]

    def _compute_mse(self, ratings: np.ndarray) -> float:
        """MSE 계산"""
        user_ids = ratings[:, 0].astype(int)
        item_ids = ratings[:, 1].astype(int)
        actual = ratings[:, 2]

        predicted = np.array([self.predict(uid, iid) for uid, iid in zip(user_ids, item_ids)])
        mse = np.mean((actual - predicted) ** 2)
        return mse

2. 딥러닝 기반 추천 모델 (NCF)

Neural Collaborative Filtering을 사용한 딥러닝 모델입니다.

# models/neural_collaborative_filtering.py
import tensorflow as tf
from tensorflow.keras import layers, Model
from typing import Tuple

class NCFModel(Model):
    """
    Neural Collaborative Filtering
    GMF (Generalized Matrix Factorization) + MLP (Multi-Layer Perceptron) 아키텍처
    """

    def __init__(self, n_users: int, n_items: int, embedding_dim: int = 32):
        super(NCFModel, self).__init__()
        self.n_users = n_users
        self.n_items = n_items
        self.embedding_dim = embedding_dim

        # GMF 파트
        self.user_embedding_gmf = layers.Embedding(n_users, embedding_dim)
        self.item_embedding_gmf = layers.Embedding(n_items, embedding_dim)

        # MLP 파트
        self.user_embedding_mlp = layers.Embedding(n_users, embedding_dim * 2)
        self.item_embedding_mlp = layers.Embedding(n_items, embedding_dim * 2)

        # MLP 레이어
        self.mlp_layers = tf.keras.Sequential([
            layers.Dense(128, activation='relu'),
            layers.Dropout(0.2),
            layers.Dense(64, activation='relu'),
            layers.Dropout(0.2),
            layers.Dense(32, activation='relu'),
        ])

        # 결합 레이어
        self.concat_layer = layers.Concatenate()
        self.final_layers = tf.keras.Sequential([
            layers.Dense(16, activation='relu'),
            layers.Dense(8, activation='relu'),
            layers.Dense(1, activation='sigmoid'),
        ])

    def call(self, inputs: Tuple[tf.Tensor, tf.Tensor]) -> tf.Tensor:
        """
        Args:
            inputs: (user_ids, item_ids)
        Returns:
            predicted ratings (0-1)
        """
        user_ids, item_ids = inputs

        # GMF
        user_emb_gmf = self.user_embedding_gmf(user_ids)
        item_emb_gmf = self.item_embedding_gmf(item_ids)
        gmf_vector = tf.multiply(user_emb_gmf, item_emb_gmf)

        # MLP
        user_emb_mlp = self.user_embedding_mlp(user_ids)
        item_emb_mlp = self.item_embedding_mlp(item_ids)
        mlp_vector = self.mlp_layers(tf.concat([user_emb_mlp, item_emb_mlp], axis=-1))

        # 결합
        concat_vector = self.concat_layer([gmf_vector, mlp_vector])
        output = self.final_layers(concat_vector)

        return output

    def recommend(self, user_id: int, top_k: int = 10) -> Tuple[np.ndarray, np.ndarray]:
        """
        사용자에게 추천할 상위 K개 아이템

        Returns:
            (item_ids, scores)
        """
        # 모든 아이템에 대한 예측 계산
        user_ids = tf.constant([user_id] * self.n_items)
        item_ids = tf.constant(list(range(self.n_items)))
        predictions = self.call((user_ids, item_ids))

        # 상위 K개 선택
        scores, indices = tf.nn.top_k(predictions[:, 0], k=top_k)
        return indices.numpy(), scores.numpy()

# 모델 학습 예시
def train_ncf_model(train_data: tf.data.Dataset, n_users: int, n_items: int):
    model = NCFModel(n_users, n_items, embedding_dim=32)

    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
        loss=tf.keras.losses.BinaryCrossentropy(),
        metrics=[tf.keras.metrics.AUC(name='auc')]
    )

    # 학습
    history = model.fit(
        train_data,
        epochs=20,
        validation_split=0.2,
        callbacks=[
            tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True),
            tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=3)
        ]
    )

    return model, history

3. 콘텐츠 기반 필터링 (Content-Based Filtering)

아이템 특성을 활용한 콘텐츠 기반 추천입니다.

# models/content_based.py
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Dict
import numpy as np

class ContentBasedRecommender:
    def __init__(self):
        self.vectorizer = TfidfVectorizer(max_features=5000, stop_words='english')
        self.item_features = None
        self.item_ids = None
        self.similarity_matrix = None

    def fit(self, items: List[Dict]):
        """
        아이템 특성을 TF-IDF로 벡터화하고 유사도 행렬 계산

        Args:
            items: [{'id': int, 'title': str, 'description': str, 'tags': List[str]}, ...]
        """
        # 텍스트 결합 (제목 + 설명 + 태그)
        texts = []
        self.item_ids = []

        for item in items:
            combined_text = f"{item['title']} {item['description']} {' '.join(item.get('tags', []))}"
            texts.append(combined_text)
            self.item_ids.append(item['id'])

        # TF-IDF 벡터화
        self.item_features = self.vectorizer.fit_transform(texts)

        # 코사인 유사도 행렬 계산
        self.similarity_matrix = cosine_similarity(self.item_features)

    def get_similar_items(self, item_id: int, top_k: int = 10) -> List[Tuple[int, float]]:
        """
        특정 아이템과 유사한 아이템 추천

        Returns:
            [(item_id, similarity_score), ...]
        """
        try:
            idx = self.item_ids.index(item_id)
        except ValueError:
            return []

        # 유사도 점수 가져오기
        similarities = list(enumerate(self.similarity_matrix[idx]))

        # 자기 자신 제외하고 정렬
        similarities = [(self.item_ids[i], score) for i, score in similarities if i != idx]
        similarities.sort(key=lambda x: x[1], reverse=True)

        return similarities[:top_k]

    def recommend_by_profile(self, user_profile: Dict[str, float], top_k: int = 10) -> List[Tuple[int, float]]:
        """
        사용자 프로필(카테고리 선호도 등) 기반 추천

        Args:
            user_profile: {'category1': score, 'category2': score, ...}
        Returns:
            [(item_id, relevance_score), ...]
        """
        # 사용자 프로필을 쿼리 벡터로 변환
        profile_text = ' '.join([cat * weight for cat, weight in user_profile.items()])
        query_vector = self.vectorizer.transform([profile_text])

        # 모든 아이템과의 유사도 계산
        similarities = cosine_similarity(query_vector, self.item_features)[0]

        # 상위 K개 추천
        top_indices = similarities.argsort()[-top_k:][::-1]
        results = [(self.item_ids[i], similarities[i]) for i in top_indices]

        return results

4. 앙상블 추천 시스템

여러 모델의 결과를 결합한 앙상블 추천입니다.

# models/ensemble.py
from typing import List, Tuple, Dict
import numpy as np

class EnsembleRecommender:
    def __init__(self, recommenders: List, weights: List[float] = None):
        """
        Args:
            recommenders: 추천 모델 리스트
            weights: 각 모델의 가중치
        """
        self.recommenders = recommenders
        self.weights = weights if weights else [1.0 / len(recommenders)] * len(recommenders)

    def recommend(self, user_id: int, item_ids: List[int] = None, top_k: int = 10) -> List[Tuple[int, float]]:
        """
        여러 모델의 추천 결과를 결합

        Args:
            user_id: 사용자 ID
            item_ids: 추천 후보 아이템 ID들 (None이면 전체)
            top_k: 최종 추천 개수

        Returns:
            [(item_id, final_score), ...]
        """
        if item_ids is None:
            # 각 모델에서 추천 받아 전체 후보 아이템 수집
            all_recommendations = {}
            for recommender in self.recommenders:
                recs = recommender.recommend(user_id, top_k=top_k * 2)
                for item_id, score in recs:
                    if item_id not in all_recommendations:
                        all_recommendations[item_id] = []
                    all_recommendations[item_id].append(score)

            # 앙상블 스코어 계산
            final_scores = []
            for item_id, scores in all_recommendations.items():
                # 가중 평균
                ensemble_score = sum(s * w for s, w in zip(scores, self.weights)) / len(scores)
                final_scores.append((item_id, ensemble_score))
        else:
            # 후보 아이템 목록이 있는 경우
            final_scores = []
            for item_id in item_ids:
                scores = []
                for recommender in self.recommenders:
                    score = recommender.predict(user_id, item_id)
                    scores.append(score)
                ensemble_score = sum(s * w for s, w in zip(scores, self.weights)) / len(scores)
                final_scores.append((item_id, ensemble_score))

        # 정렬하여 상위 K개 반환
        final_scores.sort(key=lambda x: x[1], reverse=True)
        return final_scores[:top_k]

5. 추천 API 서버

FastAPI를 사용한 추천 서버입니다.

# api/recommendation_server.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import redis
import json

from models.collaborative_filtering import MatrixFactorization, CollaborativeFilteringConfig
from models.content_based import ContentBasedRecommender
from models.ensemble import EnsembleRecommender

app = FastAPI(title="Recommendation API", version="1.0.0")

# CORS 설정
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Redis 캐싱
redis_client = redis.Redis(host='localhost', port=6379, db=0)

# 모델 로드
cf_model = MatrixFactorization(CollaborativeFilteringConfig(n_factors=50))
cb_model = ContentBasedRecommender()
ensemble_model = EnsembleRecommender([cf_model, cb_model], weights=[0.7, 0.3])

# 요청 모델
class RecommendRequest(BaseModel):
    user_id: int
    item_ids: Optional[List[int]] = None
    top_k: int = 10
    use_cache: bool = True

class RecommendResponse(BaseModel):
    user_id: int
    recommendations: List[dict]

def get_cached_recommendations(user_id: int, top_k: int) -> Optional[List[dict]]:
    """캐시된 추천 조회"""
    cache_key = f"rec:{user_id}:{top_k}"
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)
    return None

def cache_recommendations(user_id: int, top_k: int, recommendations: List[dict], ttl: int = 3600):
    """추천 결과 캐싱"""
    cache_key = f"rec:{user_id}:{top_k}"
    redis_client.setex(cache_key, ttl, json.dumps(recommendations))

@app.post("/recommend", response_model=RecommendResponse)
async def recommend(request: RecommendRequest):
    """
    사용자에게 개인화된 추천 제공

    - **user_id**: 사용자 ID
    - **item_ids**: 추천 후보 아이템 ID들 (선택사항)
    - **top_k**: 추천 개수
    - **use_cache**: 캐시 사용 여부
    """
    # 캐시 확인
    if request.use_cache:
        cached = get_cached_recommendations(request.user_id, request.top_k)
        if cached:
            return RecommendResponse(
                user_id=request.user_id,
                recommendations=cached
            )

    # 추천 생성
    try:
        recommendations = ensemble_model.recommend(
            user_id=request.user_id,
            item_ids=request.item_ids,
            top_k=request.top_k
        )

        # 응답 형식 변환
        formatted_recs = [
            {"item_id": item_id, "score": float(score)}
            for item_id, score in recommendations
        ]

        # 캐싱
        if request.use_cache:
            cache_recommendations(request.user_id, request.top_k, formatted_recs)

        return RecommendResponse(
            user_id=request.user_id,
            recommendations=formatted_recs
        )

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/feedback")
async def record_feedback(user_id: int, item_id: int, feedback: float):
    """
    사용자 피드백 기록 (온라인 학습용)

    - **user_id**: 사용자 ID
    - **item_id**: 아이템 ID
    - **feedback**: 피드백 점수 (0-5)
    """
    # 실시간 업데이트 로직
    # ...

    return {"status": "success"}

@app.get("/health")
async def health_check():
    """헬스 체크"""
    return {"status": "healthy"}

개발 과정

1. 데이터 수집 및 전처리 (2주)

  • 로그 데이터 수집 (클릭, 구매, 평가)
  • 데이터 정제 및 특징 추출
  • 학습/검증/테스트 데이터셋 분리

2. 베이스라인 모델 개발 (2주)

  • 협업 필터링 모델 구현
  • 콘텐츠 기반 필터링 구현
  • 성능 메트릭 정의 (Precision@K, Recall@K, NDCG)

3. 딥러닝 모델 개발 (3주)

  • NCF 모델 구현
  • 튜닝 및 최적화
  • 앙상블 구조 설계

4. 서비스 배포 및 A/B 테스트 (3주)

  • 추천 API 서버 구축
  • 캐싱 레이어 추가
  • A/B 테스트 설정 및 결과 분석

5. 모니터링 및 지속적 개선 (지속)

  • 추천 품질 모니터링
  • 모델 재학습 파이프라인
  • 이상 탐지 및 롤백 시스템

문제 해결 과정

문제 1: Cold Start 문제

문제: 새로운 사용자나 아이템에 대한 추천이 불가능

해결책:

class ColdStartHandler:
    def __init__(self, content_model, cf_model):
        self.content_model = content_model
        self.cf_model = cf_model
        self.min_interactions = 5  # CF 모델 사용 최소 상호작용 수

    def recommend(self, user_id: int, user_history: List[int], top_k: int):
        # 사용자 상호작용 수 확인
        if len(user_history) >= self.min_interactions:
            # CF 모델 사용
            return self.cf_model.recommend(user_id, top_k)
        else:
            # 콘텐츠 기반 추천 (인기 아이템 + 콘텐츠 유사도)
            popular_items = self.get_popular_items(top_k // 2)
            content_recs = self.content_based_on_history(user_history, top_k // 2)

            # 결합
            recommendations = popular_items + content_recs
            return recommendations[:top_k]

    def get_popular_items(self, top_k: int):
        """인기 아이템 추출"""
        # 전체 사용자에게 가장 많이 추천된 아이템
        pass

    def content_based_on_history(self, user_history: List[int], top_k: int):
        """사용자 기록 기반 콘텐츠 추천"""
        if not user_history:
            # 완전히 새로운 사용자 - 인기 아이템 반환
            return self.get_popular_items(top_k)

        # 가장 최근 아이템과 유사한 아이템 추천
        last_item = user_history[-1]
        return self.content_model.get_similar_items(last_item, top_k)

문제 2: 추천 다양성 부족

문제: 항상 비슷한 카테고리의 아이템만 추천됨

해결책:

def diversify_recommendations(recommendations: List[Tuple[int, float]],
                              item_categories: Dict[int, str],
                              top_k: int) -> List[Tuple[int, float]]:
    """
    추천 다양성을 보장하기 위해 카테고리 분산 최대화

    Args:
        recommendations: [(item_id, score), ...]
        item_categories: {item_id: category, ...}
        top_k: 최종 추천 개수
    """
    if len(recommendations) <= top_k:
        return recommendations

    # MM (Maximal Marginal Relevance) 알고리즘 적용
    selected = []
    remaining = recommendations.copy()

    # 점수가 가장 높은 아이템 먼저 선택
    selected.append(remaining.pop(0))

    while len(selected) < top_k and remaining:
        best_item = None
        best_score = float('-inf')

        for candidate in remaining:
            # 점수 + 다양성 페널티
            diversity_penalty = max(
                [category_similarity(item_categories[candidate[0]], item_categories[s[0]])
                 for s in selected],
                default=0
            )
            mmr_score = candidate[1] - 0.3 * diversity_penalty  # 0.3은 다양성 가중치

            if mmr_score > best_score:
                best_score = mmr_score
                best_item = candidate

        if best_item:
            selected.append(best_item)
            remaining.remove(best_item)

    return selected

def category_similarity(cat1: str, cat2: str) -> float:
    """카테고리 유사도 계산 (같으면 1, 다르면 0)"""
    return 1.0 if cat1 == cat2 else 0.0

성과와 배운 점

성과

  • CTR 개선: 30% 향상 (2.5% → 3.25%)
  • 평균 세션 시간: 25% 증가
  • 추천 응답 시간: 80ms (P95: 150ms)
  • 추천 다양성 점수: 0.4 → 0.7

배운 점

  1. 추천 시스템의 트레이드오프: 정확도 vs 다양성 vs 신선도
  2. 실시간 학습: 스트리밍 데이터 처리와 모델 업데이트의 어려움
  3. A/B 테스트의 중요성: 오프라인 메트릭과 온라인 성능의 차이
  4. MLOps의 필요성: 모델 배포, 모니터링, 롤백의 자동화

코드 스니펫

MLflow를 사용한 실험 추적

# tracking/experiments.py
import mlflow
import mlflow.sklearn
from sklearn.metrics import precision_at_k, recall_at_k, ndcg_at_k

def log_experiment(model, config, metrics, params):
    """MLflow에 실험 로그 기록"""
    with mlflow.start_run():
        # 하이퍼파라미터 기록
        mlflow.log_params({
            'n_factors': config.n_factors,
            'learning_rate': config.learning_rate,
            'regularization': config.regularization,
            'n_epochs': config.n_epochs,
        })

        # 메트릭 기록
        mlflow.log_metrics({
            'precision_at_10': metrics['precision@10'],
            'recall_at_10': metrics['recall@10'],
            'ndcg_at_10': metrics['ndcg@10'],
            'auc': metrics['auc'],
        })

        # 모델 기록
        mlflow.sklearn.log_model(model, "model")

# 예측 메트릭 계산
def calculate_metrics(model, test_data, k=10):
    """추천 시스템 메트릭 계산"""
    user_items = {}

    # 정답 데이터 준비
    for user_id, item_id, rating in test_data:
        if rating >= 4:  # 긍정 평가만 고려
            if user_id not in user_items:
                user_items[user_id] = []
            user_items[user_id].append(item_id)

    # 메트릭 계산
    precision_scores = []
    recall_scores = []
    ndcg_scores = []

    for user_id, true_items in user_items.items():
        recommendations = model.recommend(user_id, top_k=k)
        rec_items = [item_id for item_id, _ in recommendations]

        # Precision@K
        hits = len(set(rec_items) & set(true_items))
        precision_scores.append(hits / k)

        # Recall@K
        recall_scores.append(hits / len(true_items))

        # NDCG@K
        ndcg_scores.append(calculate_ndcg(rec_items, true_items, k))

    return {
        f'precision@{k}': np.mean(precision_scores),
        f'recall@{k}': np.mean(recall_scores),
        f'ndcg@{k}': np.mean(ndcg_scores),
    }

def calculate_ndcg(recommended, relevant, k):
    """NDCG 계산"""
    dcg = 0.0
    for i, item in enumerate(recommended[:k]):
        if item in relevant:
            dcg += 1.0 / np.log2(i + 2)

    # IDCG (이상적인 DCG)
    ideal_dcg = sum(1.0 / np.log2(i + 2) for i in range(min(k, len(relevant))))

    return dcg / ideal_dcg if ideal_dcg > 0 else 0.0

향후 개선 계획

  • 그래프 기반 추천 (Graph Neural Networks)
  • 강화 학습 기반 대화형 추천
  • 멀티모달 추천 (텍스트, 이미지, 비디오)
  • 페더레이티드 러닝 (프라이버시 보호)
  • 시계열 기반 상황 인지 추천

GitHub: https://github.com/username/recommendation-system Paper: https://arxiv.org/abs/xxxx.xxxxx Demo: https://rec-system-demo.vercel.app