- 추천 시스템의 개요와 배경
- 콘텐츠 기반 필터링 추천 시스템
- 최근접 이웃 협업 필터링
- 잠재 요인 협업 필터링
- 콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트
- 아이템 기반 최근접 이웃 협업 필터링 실습
🍭 Why Recommendation System?
- 사용자 자신도 좋아하는지 몰랐던 취향을 시스템이 발견하고 그에 맞는 콘텐츠를 추천해주는 것이다.
- 해당 사이트를 더 강하게 신뢰하게 되어 더 많은 추천 콘텐츠를 선택하게 된다.
- 더 많은 데이터가 추천 시스템에 축적되면서 추천이 더욱 정확해지고 다양한 결과를 얻을 수 있는 좋은 선순환 시스템을 구축할 수 있게 된다.
→ 넷플릭스는 행렬 분해(Matrix Factorization)
기법을 이용해 잠재 요인 협업 필터링 방식으로 우승하였다.
→ 아마존은 아이템 기반 최근접 이웃 협업 필터링 방식을 사용하였다.
→ 최신 경향: 개인화 중시 → 콘텐츠 기반과 협업 기반을 적절히 결합한 하이브리드 형식이다.
🚨 사용자가 특정한 아이템을 매우 선호하는 경우, 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다.
예시) 사용자가 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 출연 배우, 감독, 영화 키워드 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다.
🚨 사용자가 아이템에 매긴 평점 정보나 상품 구매 이력과 같은 사용자 행동 양식만을 기반으로 추천을 수행하는 방식이다.
예시) 사용자가 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 출연 배우, 감독, 영화 키워드 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다.
: 사용자-아이템 평점 메트릭스와 같은 축적된 행동 데이터를 기반으로, 사용자가 아직 평가하지 않은 아이템을 예측 평가(Predicted Rating) 하는 것이다.
: 메모리 협업 필터링이라고도 함.
🚥 종류
사용자 기반 : 당신과 비슷한 고객들이 다음 상품도 구매했습니다.
: 특정 사용자와 유사한 다른 사용자를
TOP-N
으로 선정해 이TOP-N
사용자가 좋아하는 아이템을 추천→ 특정 사용자와 타 사용자 간의 유사도를 측정한 뒤 가장 유사도가 높은
TOP-N
사용자를 추출해 그들이 선호하는 아이템을 추천아이템 기반 : 이 상품을 선택한 다른 고객들은 다음 상품도 구매했습니다.
: 사용자들이 그 아이템을 좋아하는지/싫어하는지의 평가 척도가 유사한 아이템을 추천하는 기준이 되는 알고리즘(”아이템간의 속성”이 얼마나 유사한가 X)
🚨 잠재 요인 협업 필터링 : 사용자-아이템 평점 매트릭스 속에 숨어 있는 잠재 요인을 추출해 추천 예측을 할 수 있게 하는 기법이다.
🤔 행렬분해란?
: 대규모 다차원 행렬을 SVD(Singular Value Decomposition)와 같은 차원 감소 기법으로 분해하는 과정에서 잠재 요인을 추출한다.✅
- 잠재 요인이 어떤 것인지 명확히 정의할 수 없다.
- 다차원 희소 행렬인 사용자-아이템 행렬 데이터를 분해한다.
- 저차원 밀집 행렬의 사용자-잠재요인 행렬
- 아이템-잠재요인 행렬의 전치 행렬
→ 분해된 두 행렬의 내적을 통해 새로운 예측 사용자-아이템 평점 행렬 데이터를 만들어서 평점을 부여하지 않은 아이템에 대한 예측 평점을 생성할 수 있다.
🚨 행렬 분해
: 다차원의 매트릭스를 저차원 매트릭스로 분해하는 기법이다.
- SVD(Singular Vector Decomposition)
- NMF(Non-Negative Matrix Factorization)
개의 사용자() 행과 개의 아이템() → M X N 차원
📖
M
: 총 사용자 수
N
: 총 아이템 수
K
: 잠재 요인의 차원 수
R
: M X N 차원의 사용자-아이템 평점 행렬
Q
: 아이템과 잠재 요인과의 관계 값을 가지는 N X K 차원의 아이템-잠재 요인 행렬
P
: 사용자와 잠재 요인과의 관계 값을 가지는 M X K 차원의 사용자-잠재 요인 행렬
Q.T
: Q 매트릭스와 행과 열 값을 교환한 전치 행렬이다.
✅ 평가되지 않은 모든 평점에 대해 P와 Q 행렬로부터 예측 평점을 계산한다.
: 사용자 의 잠재 요인 벡터 와 아이템 의 잠재 요인 벡터 의 내적을 통해 예측된 평점 를 계산한다.
→ 미지의 평점 데이터를 예측할 수 있다.
: SVD(Singular Value Decomposition) 방식이 사용되지만, 결측값(NaN)
이 포함된 행렬에는 SVD 방식을 직접적으로 적용할 수 없다.
→ 결측값이 있는 경우에는 확률적 경사 하강법(SGD) 또는 ALS(Alternating Least Squares) 방식을 이용하여 행렬 분해를 수행할 수 있다.
🍭 확률적 경사 하강법을 이용한 행렬 분해 절차
STEP1. P와 Q를 임의의 값을 가진 행렬로 설정한다.
STEP2. P와 Q.T를 곱해 예측 R 행렬을 계산하고 예측 R 행렬과 실제 R 행렬에 해당하는 오류값을 계산한다.
STEP3. 이 오류 값을 최소화 할 수 있도록 P와 Q 행렬을 적절한 값으로 각각 업데이트한다.
STEP4. 만족할 만한 오류 값을 가질 때까지 2,3번 작업을 반복하면서 P와 Q 값을 업데이트해 근사화한다.비용 함수 (Cost Function) 및 규제(Regularization)
- 첫 번째 항은 예측 평점과 실제 평점 간의 오차 제곱을 나타낸다.
- 두 번째 항은 행렬 P와 Q의 규제항으로, 과적합을 방지하기 위해 추가된 항이다.
는 규제의 강도를 결정하는 하이퍼파라미터이다.P와 Q 행렬의 업데이트 식
- : 사용자 에 대한 잠재 요인 벡터의 업데이트된 값이다.
- : 아이템 에 대한 잠재 요인 벡터의 업데이트된 값이다.
- : 모델이 학습하는 속도를 조절하는 학습률이다.
- : 오차 값으로, u행 i열에 위치한 실제 행렬 값과 예측 행렬 값의 차이, 실제 평점과 예측 평점의 차이를 의미한다.
- : 모델의 복잡도를 제어하는 역할을 하는 규제 계수이다.
먼저, 결측값이 포함된 원본 행렬 R -을 생성합니다.
import numpy as np
# 원본 행렬 R 생성, 결측값(NaN)이 포함됨
R = np.array([[4, np.NaN, 2, np.NaN],
[np.NaN, 5, np.NaN, 3, 1],
[np.NaN, np.NaN, 3, 4, 4],
[5, 2, 1, 2, np.NaN]])
num_users, num_items = R.shape
K = 3 # 잠재 요인(Latent Factor)의 수
P와 Q 행렬의 크기와 값을 설정합니다. 이 값들은 정규 분포를 따르는 임의의 값으로 초기화됩니다.
python
np.random.seed(1)
# P와 Q 행렬의 크기를 지정하고 정규 분포를 가진 임의의 값으로 입력
P = np.random.normal(scale=1./K, size=(num_users, K))
Q = np.random.normal(scale=1./K, size=(num_items, K))
다음으로, 예측 행렬과 실제 행렬 사이의 오차를 계산하는 함수를 작성합니다.
def get_rmse(R, P, Q, non_zero_index):
# P와 Q를 이용해 예측 행렬 R을 계산
R_pred = np.dot(P, Q.T)
# 실제 R 행렬에서 결측값이 아닌 인덱스에 대한 오차 계산
error = 0
for (u, i) in non_zero_index:
error += (R[u, i] - R_pred[u, i]) ** 2
return np.sqrt(error / len(non_zero_index))
# 0이 아닌 인덱스, 실제 값 저장
non_zeros = [(i, j, R[i, j]) for i in range(num_users) for j in range(num_items) if R[i, j] > 0]
steps = 1000 # 반복 횟수
learning_rate = 0.01 # 학습률
r_lambda = 0.01 # 규제(Regularization) 계수
# SGD 기반으로 P와 Q 행렬을 계속 업데이트
for step in range(steps):
for i, j, r in non_zeros:
# 실제 값과 예측 값의 차이인 오차 값 구함
eij = r - np.dot(P[i, :], Q[j, :].T)
# 규제를 반영한 SGD 업데이트 공식 적용
P[i, :] = P[i, :] + learning_rate * (eij * Q[j, :] - r_lambda * P[i, :])
Q[j, :] = Q[j, :] + learning_rate * (eij * P[i, :] - r_lambda * Q[j, :])
# 매 50번 반복할 때마다 RMSE 출력
if (step + 1) % 50 == 0:
rmse = get_rmse(R, P, Q, non_zeros)
print(f"Step: {step + 1}, RMSE: {rmse:.4f}")
콘텐츠 기반 필터링 : 사용자가 특정 영화를 감상하고 그 영화를 좋아했다면 그 영화와 비슷한 특성/속성, 구성 요소 등을 가진 다른 영화를 추천하는 것이다.
데이터 로딩 및 가공
import pandas as pd
import numpy as np
import warnings; warnings.filterwarnings('ignore')
movies = pd.read_csv('./tmdb-5000-movie-dataset/tmdb_5000_movies.csv')
print(movies.shape)
movies.head(5)
→ 주요 칼럼만 추출해 새롭게 DataFrame 만들기
movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count',
'popularity', 'keywords', 'overview']]
pd.set_option('max_colwidth', 100)
movies_df[['genres','keywords']][:1]
from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
장르 콘텐츠 유사도 측정
from sklearn.feature_extraction.text import CountVectorizer
# CountVectorizer를 적용하기 위해 공백문자로 word 단위가 구분되는 문자열로 변환.
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
count_vect = CountVectorizer(min_df=0, ngram_range=(1,2))
genre_mat = count_vect.fit_transform(movies_df['genres_literal'])
print(genre_mat.shape)
from sklearn.metrics.pairwise import cosine_similarity
genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
print(genre_sim[:2])
→ 코사인 유사도 비교
genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]
print(genre_sim_sorted_ind[:1])
→ 비교 대상이 되는 행의 유사도 값이 높은 순으로 정렬된 행렬의 위치 인덱스 값을 추출
장르 콘텐츠 필터링을 이용한 영화 추천
데이터 가공 및 변환
import pandas as pd
import numpy as np
movies = pd.read_csv('./ml-latest-small/movies.csv')
ratings = pd.read_csv('./ml-latest-small/ratings.csv')
print(movies.shape)
print(ratings.shape)
ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')
ratings_matrix.head(3)
# title 컬럼을 얻기 이해 movies 와 조인 수행
rating_movies = pd.merge(ratings, movies, on='movieId')
# columns='title' 로 title 컬럼으로 pivot 수행.
ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')
# NaN 값을 모두 0 으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head(3)
→ 평점이 0.5 이상이므로 NaN 모두 0으로 만듦
→ title 로 colum을 바꿔 영화제목으로 알아볼 수 있게 만들었음
영화 간 유사도 산출
ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head(3)
from sklearn.metrics.pairwise import cosine_similarity
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)
코사인 유사도를 이용해 영화 간의 유사도 측정 → rating_matrix에 cosine_similarity() 적용
item_sim_df["Godfather, The (1972)"].sort_values(ascending=False)[:6]
item_sim_df["Inception (2010)"].sort_values(ascending=False)[1:6]
아이템 기반 최근접 이웃 협업 필터링으로 개인화된 영화 추천
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
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.head(3)
from sklearn.metrics import mean_squared_error
# 사용자가 평점을 부여한 영화에 대해서만 예측 성능 평가 MSE 를 구함.
def get_mse(pred, actual):
# Ignore nonzero terms.
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 ))
def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
# 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
pred = np.zeros(ratings_arr.shape)
# 사용자-아이템 평점 행렬의 열 크기만큼 Loop 수행.
for col in range(ratings_arr.shape[1]):
# 유사도 행렬에서 유사도가 큰 순으로 n개 데이터 행렬의 index 반환
top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
# 개인화된 예측 평점을 계산
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(np.abs(item_sim_arr[col, :][top_n_items]))
return pred
ratings_pred = predict_rating_topsim(ratings_matrix.values , item_sim_df.values, n=20)
print('아이템 기반 인접 TOP-20 이웃 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)
user_rating_id = ratings_matrix.loc[9, :]
user_rating_id[ user_rating_id > 0].sort_values(ascending=False)[:10]
def get_unseen_movies(ratings_matrix, userId):
# userId로 입력받은 사용자의 모든 영화정보 추출하여 Series로 반환함.# 반환된 user_rating 은 영화명(title)을 index로 가지는 Series 객체임.user_rating= ratings_matrix.loc[userId,:]
# user_rating이 0보다 크면 기존에 관람한 영화임. 대상 index를 추출하여 list 객체로 만듬already_seen= user_rating[ user_rating> 0].index.tolist()
# 모든 영화명을 list 객체로 만듬.movies_list= ratings_matrix.columns.tolist()
# list comprehension으로 already_seen에 해당하는 movie는 movies_list에서 제외함.unseen_list= [ moviefor moviein movies_listif movienotin already_seen]
return unseen_list
def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
# 예측 평점 DataFrame에서 사용자id index와 unseen_list로 들어온 영화명 컬럼을 추출하여
# 가장 예측 평점이 높은 순으로 정렬함.
recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
return recomm_movies
# 사용자가 관람하지 않는 영화명 추출
unseen_list = get_unseen_movies(ratings_matrix, 9)
# 아이템 기반의 인접 이웃 협업 필터링으로 영화 추천
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)
# 평점 데이타를 DataFrame으로 생성.
recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score'])
recomm_movies