추천시스템 (1) 추천시스템 기본 개념 / 머신러닝 기법 개념 및 실습

이영락·2024년 10월 14일
0

인공지능 공부

목록 보기
24/33

목차

  1. 추천 시스템의 개요와 배경
  2. 콘텐츠 기반 필터링 추천 시스템
  3. 최근접 이웃 협업 필터링
  4. 잠재 요인 협업 필터링
  5. 콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트
  6. 아이템 기반 최근접 이웃 협업 필터링 실습

🏖️ 01 | 추천 시스템의 개요와 배경

🍭 Why Recommendation System?

  • 사용자 자신도 좋아하는지 몰랐던 취향을 시스템이 발견하고 그에 맞는 콘텐츠를 추천해주는 것이다.
  • 해당 사이트를 더 강하게 신뢰하게 되어 더 많은 추천 콘텐츠를 선택하게 된다.
  • 더 많은 데이터가 추천 시스템에 축적되면서 추천이 더욱 정확해지고 다양한 결과를 얻을 수 있는 좋은 선순환 시스템을 구축할 수 있게 된다.

추천 시스템의 유형

  1. 콘텐츠 기반 필터링
  2. 협업 필터링
    1. 최근접 이웃 협업 필터링
    2. 잠재 요인 협업 필터링

넷플릭스행렬 분해(Matrix Factorization) 기법을 이용해 잠재 요인 협업 필터링 방식으로 우승하였다.
아마존아이템 기반 최근접 이웃 협업 필터링 방식을 사용하였다.
→ 최신 경향: 개인화 중시콘텐츠 기반협업 기반을 적절히 결합한 하이브리드 형식이다.


🏖️ 02 | 콘텐츠 기반 필터링 추천 시스템

🚨 사용자가 특정한 아이템을 매우 선호하는 경우, 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다.

예시) 사용자가 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 출연 배우, 감독, 영화 키워드 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다.


🏖️ 03 | 최근접 이웃 협업 필터링(Collaborative Filtering)

🚨 사용자가 아이템에 매긴 평점 정보나 상품 구매 이력과 같은 사용자 행동 양식만을 기반으로 추천을 수행하는 방식이다.

예시) 사용자가 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 출연 배우, 감독, 영화 키워드 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다.

목적

: 사용자-아이템 평점 메트릭스와 같은 축적된 행동 데이터를 기반으로, 사용자가 아직 평가하지 않은 아이템을 예측 평가(Predicted Rating) 하는 것이다.

특징

  1. 사용자-아이템 평점 행렬 데이터에만 의지해 추천을 수행하는 것이 특징이다.
    • 행(row) : 개별 사용자
    • 열(column) : 개별 아이템
    • 해당 값 : 평점을 나타내는 형태이다.
  2. 희소 행렬(Sparse Matrix) : 사용자가 아이템에 대한 평점을 매기는 경우가 많지 않다.

최근접 이웃 협업 필터링

: 메모리 협업 필터링이라고도 함.

🚥 종류

  1. 사용자 기반 : 당신과 비슷한 고객들이 다음 상품도 구매했습니다.

    : 특정 사용자와 유사한 다른 사용자를 TOP-N으로 선정해 이 TOP-N 사용자가 좋아하는 아이템을 추천

    → 특정 사용자와 타 사용자 간의 유사도를 측정한 뒤 가장 유사도가 높은 TOP-N 사용자를 추출해 그들이 선호하는 아이템을 추천

  2. 아이템 기반 : 이 상품을 선택한 다른 고객들은 다음 상품도 구매했습니다.

    : 사용자들이 그 아이템을 좋아하는지/싫어하는지의 평가 척도가 유사한 아이템을 추천하는 기준이 되는 알고리즘(”아이템간의 속성”이 얼마나 유사한가 X)

  • 사용자 기반 보다는 아이템 기반 협업 필터링이 정확도가 높음! why?) 비슷한 영화를 좋아한다고 해서 사람들의 취향이 비슷하다는 아니기 때문
  • 코사인 유사도는 추천 시스템의 유사도 측정에 가장 많이 적용됨(다차원 희소 행렬이라는 특징 때문!)

🏖️ 04 | 잠재 요인 협업 필터링

잠재 요인 협업 필터링의 이해

🚨 잠재 요인 협업 필터링 : 사용자-아이템 평점 매트릭스 속에 숨어 있는 잠재 요인을 추출해 추천 예측을 할 수 있게 하는 기법이다.

🤔 행렬분해란?
: 대규모 다차원 행렬을 SVD(Singular Value Decomposition)와 같은 차원 감소 기법으로 분해하는 과정에서 잠재 요인을 추출한다.

  • 잠재 요인이 어떤 것인지 명확히 정의할 수 없다.
  • 다차원 희소 행렬인 사용자-아이템 행렬 데이터를 분해한다.
    • 저차원 밀집 행렬의 사용자-잠재요인 행렬
    • 아이템-잠재요인 행렬의 전치 행렬

→ 분해된 두 행렬의 내적을 통해 새로운 예측 사용자-아이템 평점 행렬 데이터를 만들어서 평점을 부여하지 않은 아이템에 대한 예측 평점을 생성할 수 있다.


행렬 분해의 이해

🚨 행렬 분해
: 다차원의 매트릭스를 저차원 매트릭스로 분해하는 기법이다.

  • SVD(Singular Vector Decomposition)
  • NMF(Non-Negative Matrix Factorization)

MM개의 사용자(UserUser) 행과 NN개의 아이템(ItemItem) → M X N 차원

  • 사용자 - K 차원 잠재 요인 행렬 P(M X K)
  • K 차원 잠재 요인 - 아이템 행렬 Q.T(K X N)

📖 R=PQ.TR = P * Q.T
M : 총 사용자 수
N : 총 아이템 수
K : 잠재 요인의 차원 수
R : M X N 차원의 사용자-아이템 평점 행렬
Q : 아이템과 잠재 요인과의 관계 값을 가지는 N X K 차원의 아이템-잠재 요인 행렬
P : 사용자와 잠재 요인과의 관계 값을 가지는 M X K 차원의 사용자-잠재 요인 행렬
Q.T : Q 매트릭스와 행과 열 값을 교환한 전치 행렬이다.

✅ 평가되지 않은 모든 평점에 대해 PQ 행렬로부터 예측 평점을 계산한다.
r(u,i)=puqiTr_{(u,i)} = p_u \cdot q_i^T
: 사용자 uu 의 잠재 요인 벡터 pup_u 와 아이템 ii 의 잠재 요인 벡터 qiTq_i^T 의 내적을 통해 예측된 평점 r(u,i)r_{(u,i)} 를 계산한다.

RR^=P×QTR \approx \hat{R} = P \times Q^T
→ 미지의 평점 데이터를 예측할 수 있다.


확률적 경사 하강법을 이용한 행렬 분해

: 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)

min(r(u,i)puqiT)2+λ(qi2+pu2)\min \sum (r_{(u,i)} - p_u \cdot q_i^T)^2 + \lambda \left( ||q_i||^2 + ||p_u||^2 \right)

  • 첫 번째 항은 예측 평점과 실제 평점 간의 오차 제곱을 나타낸다.
  • 두 번째 항은 행렬 P와 Q의 규제항으로, 과적합을 방지하기 위해 추가된 항이다.
    λ\lambda는 규제의 강도를 결정하는 하이퍼파라미터이다.

P와 Q 행렬의 업데이트 식

pu=pu+η(eu,iqiλpu)p_u' = p_u + \eta \left( e_{u,i} \cdot q_i - \lambda \cdot p_u \right)
qi=qi+η(eu,ipuλqi)q_i' = q_i + \eta \left( e_{u,i} \cdot p_u - \lambda \cdot q_i \right)

  • pup_u' : 사용자 uu에 대한 잠재 요인 벡터의 업데이트된 값이다.
  • qiq_i' : 아이템 ii에 대한 잠재 요인 벡터의 업데이트된 값이다.
  • η\eta : 모델이 학습하는 속도를 조절하는 학습률이다.
  • eu,ie_{u,i} : 오차 값으로, u행 i열에 위치한 실제 행렬 값과 예측 행렬 값의 차이, 실제 평점과 예측 평점의 차이를 의미한다.
  • λ\lambda : 모델의 복잡도를 제어하는 역할을 하는 규제 계수이다.

SGD를 이용한 행렬 분해 파이썬 구현

1. 원본 행렬 생성

먼저, 결측값이 포함된 원본 행렬 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)의 수

2. P와 Q 행렬 초기화

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))

3. 예측 행렬의 오차 계산

다음으로, 예측 행렬과 실제 행렬 사이의 오차를 계산하는 함수를 작성합니다.

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))

SGD 알고리즘을 통한 행렬 분해

1. 초기 변수 설정

# 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) 계수

2. SGD 알고리즘으로 P와 Q 행렬 업데이트

# 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}")

🏖️ 05 | 콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트

장르 속성을 이용한 영화 콘텐츠 기반 필터링

콘텐츠 기반 필터링 : 사용자가 특정 영화를 감상하고 그 영화를 좋아했다면 그 영화와 비슷한 특성/속성, 구성 요소 등을 가진 다른 영화를 추천하는 것이다.

데이터 로딩 및 가공

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])

→ 비교 대상이 되는 행의 유사도 값이 높은 순으로 정렬된 행렬의 위치 인덱스 값을 추출

장르 콘텐츠 필터링을 이용한 영화 추천


🏖️ 06 | 아이템 기반 최근접 이웃 협업 필터링 실습

데이터 가공 및 변환

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
profile
AI Engineer / 의료인공지능

0개의 댓글

관련 채용 정보