본문 바로가기
추천시스템

협업필터링 + KNN

by 장찐 2021. 10. 11.

◈ 협업필터링과  KNN을 함께 사용 

 특정 사용자에 대해서 예측값 가중치를 계산할 때, 가장 가까운 K명의 neighbor의 데이터만 사용하는 방식 

 

  가정  

취향이 비슷한 사람들은 모든 아이템에 대한 선호가 일관되게 유사하다 

추천 대상인 사용자와 유사도가 높은 사용자일수록, 취향을 정확하게 반영할 수 있다. 

 

  이웃의 크기 결정 방식 

KNN :

특정 사용자와 최대한 유사한 K 명의 이웃을 사용

 

Thresholding :

유사도의 기준을 정해두고(ex. 0.7 이상) 그에 따라서 이웃을 정하는 방식. 

KNN보다 정확도는 높지만, Coverage가 낮아진다(추천할 수 있는 사용자의 %가 줄어든다) 

→ Thresholding의 경우 해당 조건을 만족하는 이웃이 존재하지 않을 수 있으므로, 주로 KNN 방식을 사용한다

 

 

 최적의 K 값? 

 너무 크면 정확도가 떨어지고, 너무 작으면 과적합 가능성 생김 

optimal neighbor size는 k 를 여러번 바꿔가면서 확인해보면 됨 


 

◈ 실습 

•  이전 기본 CF 게시글과 나머지 부분은 모두 동일하고, cf_simple() 함수와 score() 함수만 변경함 

 

# RMSE 계산을 위한 함수
def RMSE(y_true, y_pred):
    return np.sqrt(np.mean((np.array(y_true) - np.array(y_pred))**2))

def score(model, neighbor_size=0):
    id_pairs = zip(x_test['user_id'], x_test['movie_id'])
    y_pred = np.array([model(user, movie, neighbor_size) for (user, movie) in id_pairs])
    y_true = np.array(x_test['rating'])
    return RMSE(y_true, y_pred)



# CF + KNN
def cf_knn(user_id, movie_id, neighbor_size=20):
    if movie_id in rating_matrix:
        # 현재 사용자와 다른 사용자 간의 similarity 가져오기 (총 943개) 
        sim_scores = user_similarity[user_id]
        
        # 현재 영화에 대한 모든 사용자의 rating값 가져오기
        movie_ratings = rating_matrix[movie_id]
        
        # 현재 영화를 평가하지 않은 사용자의 index 가져오기
        none_rating_idx = movie_ratings[movie_ratings.isnull()].index
        
        # 현재 영화를 평가하지 않은 사용자의 rating (null) 제거
        movie_ratings = movie_ratings.drop(none_rating_idx)
        
        # 현재 영화를 평가하지 않은 사용자의 similarity값 제거
        sim_scores = sim_scores.drop(none_rating_idx)
        
        ### 여기서부터 KNN 추가한 내용  
        if neighbor_size == 0:               # Neighbor size가 지정되지 않은 경우
            # 현재 영화를 평가한 모든 사용자의 가중평균값 구하기
            mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
            
        else:                                # Neighbor size가 지정된 경우
            # 지정된 neighbor size 값과 해당 영화를 평가한 총사용자 수 중 작은 것으로 결정
            neighbor_size = min(neighbor_size, len(sim_scores))
            
            # array로 바꾸기 (argsort를 사용하기 위함)
            sim_scores = np.array(sim_scores)
            movie_ratings = np.array(movie_ratings)
            
            # 유사도를 순서대로 정렬
            user_idx = np.argsort(sim_scores)
            
            # 유사도를 neighbor size만큼 받기
            sim_scores = sim_scores[user_idx][-neighbor_size:]
            
            # 영화 rating을 neighbor size만큼 받기
            movie_ratings = movie_ratings[user_idx][-neighbor_size:]
            
            # 최종 예측값 계산 
            mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
   
   else:
        mean_rating = 3.0
        
    return mean_rating

score(cf_knn, 30)

 

• 최종 결과가 1.0095 정도로, 단순 CF 보다는 성능이 개선됨을 확인 

• neighbor 사이즈가 입력된 경우에도, 해당 영화를 평가한 유저의 수가 K 보다 적을 수 있으므로 한번 더 확인 해야함 

 

 

 

 

<최적의 K값 확인> 

#최적의 K값 확인 
for k in (10,20,30,40,50):
    print('k = ', k,'RMSE = ', score(cf_knn, k))

• 모든 경우에 통용할 수 있는 K 값을 단정지어서 말할 수 없으므로, 여러번 시도하면서 찾는게 최선이다. 

이 경우에는 k=30 ~ 40인 구간에 대해서 다시 세부적으로 탐색해볼 필요가 있다. 

 

 

 

<실제로 추천 하기>

recommender 함수 내에서 cf_knn() 부분에 파라미터가 3개 들어가는 것 제외하고 동일함 

# 전체 데이터 로딩 
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('u.data', names=r_cols,  sep='\t',encoding='latin-1')
ratings = ratings.drop('timestamp', axis=1)
rating_matrix = ratings.pivot(values='rating', index='user_id', columns='movie_id')

# 영화 제목 가져오기
i_cols = ['movie_id', 'title', 'release date', 'video release date', 'IMDB URL', 
          'unknown', 'Action', 'Adventure', 'Animation', 'Children\'s', 'Comedy', 
          'Crime', 'Documentary', 'Drama', 'Fantasy', 'Film-Noir', 'Horror', 
          'Musical', 'Mystery', 'Romance', 'Sci-Fi', 'Thriller', 'War', 'Western']
movies = pd.read_csv('u.item', sep='|', names=i_cols, encoding='latin-1')
movies = movies[['movie_id', 'title']]
movies = movies.set_index('movie_id')

# Cosine similarity 계산
from sklearn.metrics.pairwise import cosine_similarity
matrix_dummy = rating_matrix.copy().fillna(0)
user_similarity = cosine_similarity(matrix_dummy, matrix_dummy)
user_similarity = pd.DataFrame(user_similarity, index=rating_matrix.index, columns=rating_matrix.index)

# 추천하기
def recommender(user, n_items=10, neighbor_size=20):
    # 현재 사용자의 모든 아이템에 대한 예상 평점 계산
    predictions = []
    rated_index = rating_matrix.loc[user][rating_matrix.loc[user] > 0].index    # 이미 평가한 영화 확인
    items = rating_matrix.loc[user].drop(rated_index)
    
    # 여기서 파라미터 값이 3개 들어감 
    # 예상평점 계산
    for item in items.index:
        predictions.append(cf_knn(user, item, neighbor_size))
        
    recommendations = pd.Series(data=predictions, index=items.index, dtype=float)
    recommendations = recommendations.sort_values(ascending=False)[:n_items]    # 예상평점이 가장 높은 영화 선택
    recommended_items = movies.loc[recommendations.index]['title']
    return recommended_items

# 영화 추천 함수 부르기
recommender(2, 10, 20)

 


 

◈ 참고자료 출처 

 "Python을 이용한 개인화 추천시스템", 임일, 청람

 

댓글