241212 TIL #561 AI Tech #94 Neighborhood CF 모델 구현

김춘복·2024년 12월 12일
0

TIL : Today I Learned

목록 보기
563/575

Today I Learned

movie recommendation 프로젝트를 연장해서 모델서빙까지 진행해보려 한다.
하지만 기본 모델로는 신규 유저에 대한 추천이 불가능해 이를 보완하기 위해
새로운 cold start 용 모델을 만들려 한다.


Neighborhood CF 모델

  • 우선 coldstart 용으로 다른 팀원이 content-based 모델을 간단히 만들고, 내가 CF 모델을 간단하게 만들어서 hybrid로 테스트 해보려 한다.

  • evaluation에서는 데이터 셋의 크기도 작고 간단한 모델이라 test score가 잘 나오진 않았다.

  • 성능과 속도 사이의 trade-off가 있기 때문에 이 부분을 고려해서 진행해보려 한다.

  • 코드 구현

from sklearn.metrics.pairwise import cosine_similarity

class NeighborhoodCF:
    def __init__(self):
        self.user_similarity_matrix = None
        self.rating_matrix = None
        self.movies = None
    
    def fit(self, ratings_df, movies_df):
        
        """오프라인 단계: 유사도 행렬 계산 및 저장"""
        # 유저-아이템 행렬 생성
        self.rating_matrix = ratings_df.pivot(
            index='userId',
            columns='movieId',
            values='rating'
        ).fillna(0)
        
        # 유저 간 유사도 계산
        self.user_similarity_matrix = cosine_similarity(self.rating_matrix)
        self.movies = movies_df
        
    def predict_for_new_user(self, new_user_ratings, n_recommendations=10, n_neighbors=5):
        """온라인 단계: 새로운 유저에 대한 추천"""
        # 새 유저의 평점 벡터 생성
        new_user = pd.Series(0, index=self.rating_matrix.columns, dtype=np.float64)
        for movieId, rating in new_user_ratings:
            if movieId in new_user.index:
                new_user[movieId] = rating
        
        # 새 유저와 기존 유저들 간의 유사도 계산
        new_user_similarity = cosine_similarity(
            new_user.values.reshape(1, -1),
            self.rating_matrix
        )[0]
        
        # 가장 유사한 이웃 선택
        similar_user_indices = np.argsort(new_user_similarity)[::-1][:n_neighbors]
        similar_users = [self.rating_matrix.index[i] for i in similar_user_indices]
        
        # 추천 영화 찾기
        recommendations = []
        rated_movies = [movie_id for movie_id, _ in new_user_ratings]
        
        for movie_idx in range(len(self.rating_matrix.columns)):
            movie_id = self.rating_matrix.columns[movie_idx]
            if movie_id not in rated_movies:
                pred_rating = 0
                sim_sum = 0
                
                for similar_user in similar_users:
                    user_idx = self.rating_matrix.index.get_loc(similar_user)
                    if self.rating_matrix.iloc[user_idx, movie_idx] > 0:
                        similarity = new_user_similarity[user_idx]
                        rating = self.rating_matrix.iloc[user_idx, movie_idx]
                        pred_rating += similarity * rating
                        sim_sum += similarity
                
                if sim_sum > 0:
                    pred_rating /= sim_sum
                    recommendations.append((movie_id, pred_rating))
        
        # 상위 N개 추천
        recommendations.sort(key=lambda x: x[1], reverse=True)
        return recommendations[:n_recommendations]

def evaluate_model(model, train_data, test_data, k=20):
    recalls = []
    precisions = []
    f1_scores = []
    hit_rates = []
    ndcg_scores = []
    
    for user_id in test_data['userId'].unique():
        # 훈련 데이터에서의 사용자 평가와 테스트 데이터 분리
        train_ratings = train_data[train_data['userId'] == user_id]
        test_ratings = test_data[test_data['userId'] == user_id]
        
        # 훈련 데이터로 추천 생성
        user_ratings_tuple = list(zip(
            train_ratings['movieId'],
            train_ratings['rating']
        ))
        
        if len(user_ratings_tuple) > 0:  # 훈련 데이터가 있는 경우만 처리
            recommendations = model.predict_for_new_user(
                user_ratings_tuple,
                n_recommendations=k
            )
            
            # 테스트 데이터와 비교
            actual_movies = set(test_ratings['movieId'])
            pred_movies = set([movie_id for movie_id, _ in recommendations])
            
            # Recall, Precision, F1 계산
            n_relevant = len(actual_movies & pred_movies)
            recall = n_relevant / len(actual_movies) if len(actual_movies) > 0 else 0
            precision = n_relevant / len(pred_movies) if len(pred_movies) > 0 else 0
            f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
            
            recalls.append(recall)
            precisions.append(precision)
            f1_scores.append(f1)
            
            # Hit Rate 계산
            hit_rates.append(1 if n_relevant > 0 else 0)
            
            # NDCG 계산
            dcg = 0
            for i, (movie_id, pred_rating) in enumerate(recommendations):
                if movie_id in test_ratings['movieId'].values:
                    actual_rating = test_ratings[test_ratings['movieId'] == movie_id]['rating'].iloc[0]
                    dcg += (2 ** actual_rating - 1) / np.log2(i + 2)
            
            # IDCG 계산
            ideal_ratings = sorted(test_ratings['rating'].values, reverse=True)[:k]
            idcg = sum((2 ** rating - 1) / np.log2(i + 2) 
                       for i, rating in enumerate(ideal_ratings))
            
            # NDCG 계산
            ndcg = dcg / idcg if idcg > 0 else 0
            ndcg_scores.append(ndcg)
    
    # 평균 계산
    avg_recall = np.mean(recalls) if recalls else 0
    avg_precision = np.mean(precisions) if precisions else 0
    avg_f1 = np.mean(f1_scores) if f1_scores else 0
    avg_hit_rate = np.mean(hit_rates) if hit_rates else 0
    avg_ndcg = np.mean(ndcg_scores) if ndcg_scores else 0
    
    return avg_recall, avg_precision, avg_f1, avg_hit_rate, avg_ndcg
ratings = pd.read_csv('data/ml-latest-small/ratings.csv')
movies = pd.read_csv('data/ml-latest-small/movies.csv')
model = NeighborhoodCF()
model.fit(ratings, movies)

# 새로운 유저 평점 데이터
new_user_ratings = [# movieId, rating
        (1, 1),  # 토이스토리
        (71252, 1), # 파이널데스티네이션
        (189333, 1), # 미션임파서블 fallout
        (177765, 1), # 코코
        (70286, 1), # 디스트릭트9
        (59315, 1), # 아이언맨
        (58559, 1), # 다크나이트
        (57274, 1), # REC
        (2959, 1), # 파이트클럽
        (109673, 1), # 300
        (109487, 1), # 인터스텔라
        (112552, 1) # 위플래시
]


# 추천 받기 (온라인)
recommendations = model.predict_for_new_user(new_user_ratings, n_recommendations=10)
profile
Backend Dev / Data Engineer

0개의 댓글