데이터사이언스/추천시스템

맥주 추천시스템 구현 - 5. CF 기반 추천시스템 구현

ghtis1798 2021. 2. 27. 01:26

CF 기반 추천시스템

🔗아이템 기반 협업필터링(CF)

추천시스템 구현에 사용할 알고리즘은 아이템기반 CF입니다.

협업필터링(CF)은 최근접 이웃 협업필터링과 잠재 요인 협업필터링으로 나뉩니다.

그리고 최근접 이웃 협업필터링은 다시 유저 기반과 아이템 기반으로 나뉩니다.

그 중에서도 아이템 기반을 선택한 이유는 다음과 같습니다.

  1. 유저 개인의 취향이 너무 다양하다.

  2. 그에 반해 리뷰를 남기는 맥주 수는 한정적이다.

따라서 아이템 기반으로 추천하는 것이 적절하다고 판단했습니다.

🎇데이터 정제하기

수집한 맥주 데이터에는 리뷰가 1개인 맥주들이 있습니다.

리뷰를 남긴 유저들도 마찬가지입니다.

이는 추천시스템의 성능을 저하시킬 수 있으므로

최소 10개 이상의 리뷰를 가진 맥주와 유저들만으로 이루어진 데이터셋을 구성합니다.

import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import mean_squared_error

import warnings

warnings.filterwarnings('ignore')

# n개 이상의 데이터 리뷰를 남긴 유저, 맥주를 걸러내는 함수
def preprocessing(data, n):
    min_id = data['아이디'].value_counts() >= n
    min_id = min_id[min_id].index.to_list()
    data = data[data['아이디'].isin(min_id)]

    min_beer = data['맥주'].value_counts() >= n
    min_beer = min_beer[min_beer].index.to_list()
    data = data[data['맥주'].isin(min_beer)]

    return data

temp=tmp.copy()

# 10번 반복합니다.
for i in range(1,10):
    temp = preprocessing(temp, 10)
    print(temp.shape)

temp.to_csv('정제된데이터.csv', encoding='utf-8')

기존의 데이터는 7만6천개 정도였는데 4만5천개로 줄었습니다.

🍺맥주간 유사도 기반 추천

맥주간 유사도를 통해 비슷한 맥주를 추천하려고 합니다.

유사도 측정의 기준은 코사인 유사도와 피어슨 유사도를 고민했습니다.

피어슨 유사도는 너무 크거나 작은 값이 있을 경우 유사도에 크게 영향을 주는 경우 사용합니다.

EDA과정에서 이상치는 따로 존재하지 않았고

평점도 정규분포를 따르는 것을 확인했습니다.

따라서 코사인 유사도를 사용하려고 합니다.

정제된데이터 중 추천시스템 구현에 필요한 3개의 컬럼으로 매트릭스를 구성했습니다.

3개의 요소는 아이디, 맥주, 평점입니다.

data = pd.read_csv('정제된데이터.csv', encoding='utf-8', index_col=0)

ratings = data.copy()

# 피벗 테이블을 이용해 유저-아이디 매트릭스 구성
ratings_matrix = ratings.pivot_table('평점', index='아이디', columns='맥주')
ratings_matrix.head(3)
# fillna함수를 이용해 Nan처리
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix

정제된 데이터

3개의 요소로 구성한 매트릭스

Shape은 2496 x 65입니다.

2496은 유저의 수, 65는 맥주의 수입니다.

맥주의 종류가 줄어든 이유는 10개 이상의 리뷰를 가진 맥주만 뽑아냈기 때문입니다.

그런데 현재 매트릭스는 유저 x 맥주에 대한 매트릭스입니다.

추천에 사용할 알고리즘은 아이템 기반 CF이므로 맥주 x 유저 매트릭스로 변환합니다.

# 유사도 계산을 위해 트랜스포즈
ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T

변환된 매트릭스

# 아이템-유저 매트릭스로부터 코사인 유사도 구하기
item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)

# cosine_similarity()로 반환된 넘파이 행렬에 영화명을 매핑해 DataFrame으로 변환
item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns,
                          columns=ratings_matrix.columns)

print(item_sim_df.shape)
item_sim_df.head(3)

계산된 코사인 유사도

65개의 맥주의 종류입니다.

ratings_matrix.columns

코젤 맥주, 호가든 맥주와 유사도가 높은 맥주 5개만 추천해보겠습니다.

# 코젤 맥주와 유사도가 높은 맥주 5개만 추출하기
item_sim_df['Kozel Černý (Dark) 10°'].sort_values(ascending=False)[:5]

# 호가든 맥주와 유사도가 높은 맥주 5개만 추출하기
item_sim_df['Hoegaarden'].sort_values(ascending=False)[:5]

👩🏼‍🤝‍🧑🏻개인화된 맥주 추천

위의 추천방식은 단순히 맥주간 유사도만으로 추천을 진행했습니다.

이번엔 개인의 평점이 반영된 추천시스템을 구현해보겠습니다.

# ratings_arr.dot(item_sim_arr)는 평점 * 맥주 유사도
# ratings_arr는 사용자 u의 아이템 i와 가장 유사도가 높은 Top_N개 아이템에 대한 실제 평점 벡터
# item_sim_arr는 아이템 i와 가장 유사도가 높은 Top_N개 아이템의 유사도 벡터
def predict_rating(ratings_arr, item_sim_arr):
    ratings_pred = ratings_arr.dot(item_sim_arr) / np.array([np.abs(item_sim_arr).sum(axis=1)])
    return ratings_pred

# 개인화된 예측 평점 구하기
# 평점 value와 유사도 value만 뽑아서 대입
ratings_pred = predict_rating(ratings_matrix.values, item_sim_df.values)
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index,
                                  columns = ratings_matrix.columns)

# 개인별로 계산된 예측 평점
ratings_pred_matrix

# 우리가 예측한 평점과 실제 평점간의 차이를 MSE로 계산
def get_mse(pred, actual):
    # 평점이 있는 실제 영화만 추출
    pred = pred[actual.nonzero()].flatten()
    actual = actual[actual.nonzero()].flatten()
    return mean_squared_error(pred, actual)

print('아이템 기반 모든 최근접 이웃 MSE: ', 
      get_mse(ratings_pred, ratings_matrix.values))

🥈Top-N에 기반한 추천

MSE값을 더 낮추려면 특정 맥주와 가장 비슷한 N개의 맥주들만 유사도 계산에 사용해야합니다.

위의 방식은 모든 맥주들의 유사도 벡터를 사용했기 때문에 MSE값이 다소 낮습니다.

# 3개의 col까지만. 3개의 맥주에 대해서 유사도가 큰 5개 선택
top_n_items = [np.argsort(item_sim_df.values[:,3])[:-5:-1]]
top_n_items

# 따라서 가장 비슷한 유사도를 가지는 맥주만 유사도 벡터로 사용
# 특정 맥주와 비슷한 유사도를 가지는 맥주 Top_N에 대해서만 적용 -> 시간오래걸림

def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
    # 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
    pred = np.zeros(ratings_arr.shape)

    # 사용자-아이템 평점 행렬의 맥주 개수만큼 루프
    for col in range(ratings_arr.shape[1]):
        # 유사도 행렬에서 유사도가 큰 순으로 n개의 데이터 행렬의 인덱스 반환
        top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
        # 개인화된 예측 평점 계산 : 각 col 맥주별(1개), 2496 사용자들의 예측평점
        for row in range(ratings_arr.shape[0]):
            pred[row, col] = item_sim_arr[col,:][top_n_items].dot(
            ratings_arr[row, :][top_n_items].T)
            pred[row, col] /= np.sum(item_sim_arr[col,:][top_n_items])

    return pred

ratings_pred = predict_rating_topsim(ratings_matrix.values, item_sim_df.values, n=10)
print('아이템 기반 최근접 TOP-N 이웃 MSE: ', 
      get_mse(ratings_pred, ratings_matrix.values))

# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index,
                                  columns=ratings_matrix.columns)

ratings_pred_matrix

MSE 값이 감소했고 예측 행렬 값도 변했습니다.

🙎‍♂️실제 사용자에게 추천하기

사용자 아이디와 평점이 주어진 상황에서 맥주 3개를 추천해주겠습니다.

예시로는 이미 행렬에 존재하는 user 정보를 사용했습니다.

# username='snoworsummer(8,581)'
username='007lund(92)'

# 특정 유저 대상으로 맥주추천
user_rating_id = ratings_matrix.loc[username, :]
# 유저가 먹었던 맥주들 출력
user_rating_id[user_rating_id > 0].sort_values(ascending=False)[:10]

사용자가 먹어본 맥주는 제외하겠습니다.

# 사용자가 안 먹어본 맥주를 추천하자.
def get_not_tried_beer(ratings_matrix, userId):
    # userId로 입력받은 사용자의 모든 맥주 정보를 추출해 Series로 반환
    # 반환된 user_rating은 영화명(title)을 인덱스로 가지는 Series 객체
    user_rating = ratings_matrix.loc[userId, :]

    # user_rating이 0보다 크면 기존에 관란함 영화.
    # 대상 인덱스를 추출해 list 객체로 만듦
    tried = user_rating[user_rating>0].index.tolist()

    # 모든 맥주명을 list 객체로 만듦
    beer_list = ratings_matrix.columns.tolist()

    # list comprehension으로 tried에 해당하는 영화는 beer_list에서 제외
    not_tried = [beer for beer in beer_list if beer not in tried]

    return not_tried

# 예측 평점 DataFrame에서 사용자 id 인덱스와 not_tried로 들어온 맥주명 추출 후
# 가장 예측 평점이 높은 순으로 정렬
def recomm_beer_by_userid(pred_df, userId, not_tried, top_n):
    recomm_beer = pred_df.loc[userId, not_tried].sort_values(ascending=False)[:top_n]
    return recomm_beer

# 유저가 먹지 않은 맥주이름 추출
not_tried = get_not_tried_beer(ratings_matrix, username)
not_tried

최종적으로 아이템 기반의 최근접 이웃 CF로 맥주 추천을 진행합니다.

# top_n과 비슷한 맥주만 추천에 사용
ratings_pred = predict_rating_topsim(ratings_matrix.values, item_sim_df.values, n=5)

# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index,
                                   columns=ratings_matrix.columns)

# 유저가 먹지 않은 맥주이름 추출
not_tried = get_not_tried_beer(ratings_matrix, username)

# 아이템 기반의 최근접 이웃 CF로 맥주 추천
recomm_beer = recomm_beer_by_userid(ratings_pred_matrix, username, not_tried, top_n=3)
recomm_beer = pd.DataFrame(data=recomm_beer.values, index=recomm_beer.index,
                           columns=['예측평점'])
recomm_beer

개인의 평점이 반영된 맥주는 오차값이 큰 편입니다.

성능을 최적화하거나 단순 맥주 기반의 유사도 추천으로 가야할 것 같습니다. 🤦‍♂️