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