● 추천 시스템(Recommender Systems)

  • 추천 시스템은 크게 두 가지로 구분 가능
    • 컨텐츠 기반 필터링(content-based filtering): 지금까지 사용자의 이전 행동과 명시적 피드백을 통해 사용자가 좋아하는 것과 유사한 항목을 추천
    • 협업 필터링(collaborative filtering): 사용자와 항목간의 유사성을 동시에 사용해 추천
  • 두 가지를 조합한 hybrid 방식도 가능

 

1. Surprise

  • 추천 시스템 개발을 위한 라이브러리
  • 다양한 모델과 데이터 제공
  • scikit-learn과 유사한 사용방법
# %pip install scikit-surprise
from surprise import SVD
from surprise import Dataset
from surprise.model_selection import cross_validate

data = Dataset.load_builtin('ml-100k', prompt = False)
data.raw_ratings[:10]

# 출력 결과
# (사용자 id, 영화 id, 평점, 시간)
[('196', '242', 3.0, '881250949'),
 ('186', '302', 3.0, '891717742'),
 ('22', '377', 1.0, '878887116'),
 ('244', '51', 2.0, '880606923'),
 ('166', '346', 1.0, '886397596'),
 ('298', '474', 4.0, '884182806'),
 ('115', '265', 2.0, '881171488'),
 ('253', '465', 5.0, '891628467'),
 ('305', '451', 3.0, '886324817'),
 ('6', '86', 3.0, '883603013')]
# SVD 알고리즘을 통해 각각의 measure를 측정
model = SVD()
cross_validate(model, data, measures = ['rmse', 'mae'], cv = 5, verbose = True)

# 출력 결과
Evaluating RMSE, MAE of algorithm SVD on 5 split(s).

                         Fold 1    Fold 2    Fold 3    Fold 4    Fold 5    Mean      Std     
RMSE (testset)    0.9273  0.9320  0.9400  0.9451  0.9362  0.9361  0.0062  
MAE (testset)      0.7333  0.7357  0.7387  0.7444  0.7372  0.7379  0.0037  
Fit time               2.58     3.23      2.98      1.96      2.03      2.56      0.50    
Test time            0.38      0.45      0.31      0.41      0.25      0.36      0.07    
{'test_rmse': array([0.92730437, 0.93195753, 0.93997153, 0.94506308, 0.93619742]),
 'test_mae': array([0.73328536, 0.73566774, 0.73869817, 0.74442193, 0.73723008]),
 'fit_time': (2.5789830684661865,
  3.2331647872924805,
  2.975646495819092,
  1.9638335704803467,
  2.027461051940918),
 'test_time': (0.3779747486114502,
  0.4478921890258789,
  0.3068206310272217,
  0.40511560440063477,
  0.24795103073120117)}

 

 

2. 컨텐츠 기반 필터링(Content-based Filtering)

  • 컨텐츠 기반 필터링은 이전의 행동과 명시적 피드백을 통해 좋아하는 것과 유사한 항목을 추천
    • ex) 내가 지금까지 시청한 영화 목록과 다른 사용자의 시청 목록을 비교해 나와 비슷한 취향의 사용자가 시청한 영화를 추천
  • 유사도를 기반으로 추천
  • 컨텐츠 기반 필터링의 장단점
    • 장점
      • 많은 수의 사용자를 대상으로 쉽게 확장 가능
      • 사용자가 관심을 갖지 않던 상품 추천 가능
    • 단점
      • 입력 틍성을 직접 설계해야 하기 때문에 많은 도메인 지식이 필요
      • 사용자의 기존 관심사항을 기반으로만 추천 가능
import numpy as np
from surprise import Dataset

data = Dataset.load_builtin('ml-100k', prompt = False)
raw_data = np.array(data.raw_ratings, dtype = int)

# raw_data가 0부터 시작하도록 조절
raw_data[:, 0] -= 1
raw_data[:, 1] -= 1

n_users = np.max(raw_data[:, 0])
n_movies = np.max(raw_data[:, 1])
# 행: 총 유저수, 열: 총 영화수
shape = (n_users + 1, n_movies + 1)
shape

# 출력 결과
(943, 1682)
adj_matrix = np.ndarray(shape, dtype = int)
for user_id, movie_id, rating, time in raw_data:
    # 1이 있는 위치가 현재 데이터가 있는 위치
    adj_matrix[user_id][movie_id] = 1.
adj_matrix

# 출력 결과
array([[1, 1, 1, ..., 0, 0, 0],
       [1, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [1, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 1, 0, ..., 0, 0, 0]])
my_id, my_vector = 0, adj_matrix[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(adj_matrix):
    # user_id와 my_id가 다르면 유사도(similarity) 계산
    if my_id != user_id:
        similarity = np.dot(my_vector, user_vector)
        if similarity > best_match:
            best_match = similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 183, Best Match ID: 275
recommend_list = []
for i, log in enumerate(zip(my_vector, best_match_vector)):
    log1, log2 = log
    # 내가 보지 않았지만(log1(my_vector)가 1보가 작음) 유사하게 나온 영화(log2(best_match_vector)가 0보다 큼)를 추천
    if log1 < 1. and log2 > 0.:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[272, 273, 275, ..., 1480, 1481, 1482]

 

  - 유클리드 거리를 사용해 추천

  • 거리가 가까울수록(값이 작을수록) 나와 유사한 사용자

$$ euclidian = \sqrt{\sum_{d=1}^{D}(A_{i}-B_{i})^2} $$

my_id, my_vector = 0, adj_matrix[0]
best_match, best_match_id, best_match_vector = 9999, -1, []
for user_id, user_vector in enumerate(adj_matrix):
    # user_id와 my_id가 다르면 유클리드 거리 계산
    if my_id != user_id:
        # 유클리드 거리 계산식
        euclidian_dist = np.sqrt(np.sum(np.square(my_vector - user_vector)))
        if euclidian_dist < best_match:
            best_match = euclidian_dist
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 14.832396974191326, Best Match ID: 737
recommend_list = []
for i, log in enumerate(zip(my_vector, best_match_vector)):
    log1, log2 = log
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[297, 312, 317, ..., 968, 1015, 1046]

 

 

  - 코사인 유사도를 사용해 추천

  • 두 벡터가 이루고 있는 각을 계산

$$ cos\theta =\frac{A\cdot B}{\left\| A\right\|\times \left\| B\right\|} $$

# 코사인 유사도 계산식
def compute_cos_similarity(v1, v2):
    norm1 = np.sqrt(np.sum(np.square(v1)))
    norm2 = np.sqrt(np.sum(np.square(v2)))
    dot = np.dot(v1, v2)
    return dot / (norm1 * norm2)

my_id, my_vector = 0, adj_matrix[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(adj_matrix):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 0.5278586163659506, Best Match ID: 915
recommend_list = []
for i, log in enumerate(zip(my_vector, best_match_vector)):
    log1, log2 = log
    # 내가 보지 않았지만(log1(my_vector)가 1보가 작음) 유사하게 나온 영화(log2(best_match_vector)가 0보다 큼)를 추천
    if log1 < 1. and log2 > 0.:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[272, 275, 279, ..., 1427, 1596, 1681]

 

 

  - 기존 방법에 명시적 피드백(사용자가 평가한 영화 점수)을 추가해 실험

adj_matrix = np.ndarray(shape, dtype = int)
for user_id, movie_id, rating, time in raw_data:
    # 단순히 데이터가 존재하는지 안하는지에 따라 0, 1이 아니라 존재하는 곳에 rating값을 넣음
    adj_matrix[user_id][movie_id] = rating
adj_matrix

# 출력 결과
array([[5, 3, 4, ..., 0, 0, 0],
       [4, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [5, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 5, 0, ..., 0, 0, 0]])
# 유클리드 거리로 계산
my_id, my_vector = 0, adj_matrix[0]
best_match, best_match_id, best_match_vector = 9999, -1, []
for user_id, user_vector in enumerate(adj_matrix):
    # user_id와 my_id가 다르면 유클리드 거리 계산
    if my_id != user_id:
        # 유클리드 거리 계산식
        euclidian_dist = np.sqrt(np.sum(np.square(my_vector - user_vector)))
        if euclidian_dist < best_match:
            best_match = euclidian_dist
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 55.06359959174482, Best Match ID: 737
# 코사인 거리로 계산
my_id, my_vector = 0, adj_matrix[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(adj_matrix):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 0.569065731527988, Best Match ID: 915

 

 

3. 협업 필터링(Collaborative Filtering)

  • 사용자와 항목의 유사성을 동시에 고려해 추천
  • 기존에 내 관심사가 아닌 항목이라도 추천 가능
  • 자동으로 임베딩 학습 가능
  • 협업 필터링의 장단점
    • 장점
      • 자동으로 임베딩을 학습하기 때문에 도메인 지식이 필요없음
      • 기존의 관심사가 아니더라도 추천 가능
    • 단점
      • 학습 과정에서 나오지 않은 항목은 임베딩을 만들 수 없음
      • 추가 특성을 사용하기 어려움
from surprise import KNNBasic, SVD, SVDpp, NMF
from surprise import Dataset
from surprise.model_selection import cross_validate

data = Dataset.load_builtin('ml-100k', prompt = False)

 

  - KNN을 사용한 협업 필터링

model = KNNBasic()
cross_validate(model, data, measures = ['rmse', 'mae'], cv = 5, n_jobs = 4, verbose = True)

# 출력 결과
Evaluating RMSE, MAE of algorithm KNNBasic on 5 split(s).

                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.9743  0.9842  0.9849  0.9823  0.9689  0.9789  0.0063  
MAE (testset)     0.7689  0.7785  0.7786  0.7752  0.7643  0.7731  0.0056  
Fit time          2.72    2.70    2.85    2.98    1.69    2.59    0.46    
Test time         16.55   16.59   16.43   15.93   7.88    14.68   3.40    
{'test_rmse': array([0.9742902 , 0.98423331, 0.98486835, 0.98230369, 0.96892197]),
 'test_mae': array([0.76887025, 0.77850316, 0.77859337, 0.77524521, 0.76433142]),
 'fit_time': (2.724426746368408,
  2.6965529918670654,
  2.8537588119506836,
  2.977182388305664,
  1.6942033767700195),
 'test_time': (16.55093264579773,
  16.586602687835693,
  16.429330825805664,
  15.92642879486084,
  7.883346796035767)}

 

  - SVD를 사용한 협업 필터링

model = SVD()
cross_validate(model, data, measures = ['rmse', 'mae'], cv = 5, n_jobs = 4, verbose = True)

# 출력 결과
Evaluating RMSE, MAE of algorithm SVD on 5 split(s).

                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.9364  0.9366  0.9345  0.9349  0.9355  0.9356  0.0008  
MAE (testset)     0.7378  0.7368  0.7356  0.7365  0.7381  0.7370  0.0009  
Fit time          3.69    3.54    4.03    4.02    4.02    3.86    0.20    
Test time         1.18    1.36    1.51    1.55    0.45    1.21    0.40    
{'test_rmse': array([0.93641775, 0.93660876, 0.934486  , 0.93493886, 0.93553188]),
 'test_mae': array([0.73780807, 0.73676556, 0.73564193, 0.73649871, 0.73805738]),
 'fit_time': (3.690873146057129,
  3.541585922241211,
  4.030731439590454,
  4.015835523605347,
  4.018853425979614),
 'test_time': (1.1828830242156982,
  1.3635668754577637,
  1.5132882595062256,
  1.5461750030517578,
  0.4530982971191406)}

 

  - NMF를 사용한 협업 필터링

model = NMF()
cross_validate(model, data, measures = ['rmse', 'mae'], cv = 5, n_jobs = 4, verbose = True)

# 출력 결과
Evaluating RMSE, MAE of algorithm NMF on 5 split(s).

                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.9603  0.9552  0.9631  0.9654  0.9632  0.9614  0.0035  
MAE (testset)     0.7569  0.7511  0.7570  0.7578  0.7573  0.7560  0.0025  
Fit time          4.99    5.05    4.98    4.89    2.28    4.44    1.08    
Test time         0.62    0.64    0.53    0.46    0.22    0.49    0.15    
{'test_rmse': array([0.96025745, 0.95521602, 0.96314659, 0.96535182, 0.96319395]),
 'test_mae': array([0.75686291, 0.75106343, 0.75704307, 0.75776259, 0.75726996]),
 'fit_time': (4.98656702041626,
  5.046948671340942,
  4.978093385696411,
  4.887223243713379,
  2.2761926651000977),
 'test_time': (0.6195485591888428,
  0.6439330577850342,
  0.5302431583404541,
  0.4606480598449707,
  0.2169051170349121)}

 

  - SVD++를 사용한 협업 필터링

model = SVDpp()
cross_validate(model, data, measures = ['rmse', 'mae'], cv = 5, n_jobs = 4, verbose = True)

# 출력 결과
Evaluating RMSE, MAE of algorithm SVDpp on 5 split(s).

                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.9180  0.9196  0.9126  0.9336  0.9101  0.9188  0.0082  
MAE (testset)     0.7193  0.7196  0.7155  0.7324  0.7149  0.7204  0.0063  
Fit time          64.45   65.24   65.04   65.49   25.32   57.11   15.90   
Test time         9.41    9.28    8.90    9.10    4.87    8.31    1.73    
{'test_rmse': array([0.91803553, 0.9196422 , 0.91259638, 0.9335687 , 0.91007489]),
 'test_mae': array([0.71934988, 0.719645  , 0.71549358, 0.73242896, 0.71492586]),
 'fit_time': (64.4547209739685,
  65.24023914337158,
  65.03504347801208,
  65.49227690696716,
  25.323737144470215),
 'test_time': (9.414633989334106,
  9.28382134437561,
  8.901108026504517,
  9.10077452659607,
  4.8735032081604)}

 

 

4. 하이브리드(Hybrid)

  • 컨텐츠 기반 필터링과 협업 필터링을 조합한 방식
  • 많은 하이브리드 방식이 존재
  • 실습에서는 협업 필터링으로 임베딩을 학습하고 컨텐츠 기반 필터링으로 유사도 기반 추천을 수행하는 추천 엔진 개발
import numpy as np
from sklearn.decomposition import randomized_svd, non_negative_factorization
from surprise import Dataset

data = Dataset.load_builtin('ml-100k', prompt = False)
raw_data = np.array(data.raw_ratings, dtype = int)
raw_data[:, 0] -= 1
raw_data[:, 1] -= 1

n_users = np.max(raw_data[:, 0])
n_movies = np.max(raw_data[:, 1])
shape = (n_users + 1, n_movies + 1)
shape

# 출력 결과
(943, 1682)


adj_matrix = np.ndarray(shape, dtype = int)
for user_id, movie_id, ratting, time in raw_data:
    adj_matrix[user_id][movie_id] = rating
adj_matrix

# 출력 결과
array([[3, 3, 3, ..., 0, 0, 0],
       [3, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [3, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 3, 0, ..., 0, 0, 0]])
U, S, V = randomized_svd(adj_matrix, n_components = 2)
S = np.diag(S)

print(U.shape)
print(S.shape)
print(V.shape)

# 출력 결과
(943, 2)
(2, 2)
(2, 1682)


# 분해 후 재조합
# (U * S) * V
np.matmul(np.matmul(U, S), V)

# 출력 결과
array([[ 2.75413997e+00,  1.40492906e+00,  7.42675835e-01, ...,
        -8.18516364e-04,  1.56868753e-02,  1.47148277e-02],
       [ 1.37986536e+00,  9.01276577e-02,  4.55027384e-01, ...,
         1.09681871e-02,  4.52128919e-04, -1.05261346e-03],
       [ 9.93032597e-01,  7.39687516e-03,  3.35229530e-01, ...,
         8.95866035e-03, -3.68132502e-04, -1.54632039e-03],
       ...,
       [ 6.28183642e-01,  6.62904209e-02,  2.03737923e-01, ...,
         4.52498630e-03,  5.10681060e-04, -1.32467414e-04],
       [ 9.59658161e-01,  4.03024146e-01,  2.70469585e-01, ...,
         1.31860581e-03,  4.42188957e-03,  3.93973413e-03],
       [ 1.80307089e+00,  1.01280345e+00,  4.73641731e-01, ...,
        -2.26049256e-03,  1.13925590e-02,  1.09104420e-02]])

 

 

  - 사용자 기반 추천

  • 나와 비슷한 취향을 가진 다른 사용자의 행동을 추천
  • 사용자 특징 벡터의 유사도 사용
my_id, my_vector = 0, U[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(U):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
# 무려 99% 유사
Best Match: 0.9999996213542565, Best Match ID: 639
recommend_list = []
for i, log in enumerate(zip(adj_matrix[my_id], adj_matrix[best_match_id])):
    log1, log2 = log
    # 내가 보지 않았지만(log1(my_vector)가 1보가 작음) 유사하게 나온 영화(log2(best_match_vector)가 0보다 큼)를 추천
    if log1 < 1. and log2 > 0.:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[300, 301, 303, ..., 1227, 1243, 1257]

 

  - 항목 기반 추천

  • 내가 본 항목과 비슷한 항목을 추천
  • 항목 특징 벡터의 유사도 사용
my_id, my_vector = 0, V.T[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(V.T):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
# 더 높은 유사도
Best Match: 0.9999999998965627, Best Match ID: 1425
recommend_list = []
for i, user_vector in enumerate(adj_matrix):
    if adj_matrix[i][my_id] > 0.9:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[0, 1, 4, ..., 935, 937, 940]

 

  - 비음수 행렬 분해를 사용한 하이브리드 추천

A, B, iter = non_negative_factorization(adj_matrix, n_components = 2)

np.matmul(A, B)

# 출력 결과
array([[2.65054793e+00, 1.43305723e+00, 7.12899311e-01, ...,
        4.99364929e-03, 1.61183931e-02, 1.52342501e-02],
       [1.46896452e+00, 3.82199135e-02, 4.63060557e-01, ...,
        7.41842083e-03, 0.00000000e+00, 0.00000000e+00],
       [1.05504090e+00, 2.74503376e-02, 3.32579733e-01, ...,
        5.32806429e-03, 0.00000000e+00, 0.00000000e+00],
       ...,
       [6.45969509e-01, 2.42500330e-02, 2.02959350e-01, ...,
        3.21642261e-03, 8.79481547e-05, 8.31239301e-05],
       [9.88950712e-01, 3.86580826e-01, 2.79306078e-01, ...,
        2.77435827e-03, 4.26387068e-03, 4.02998437e-03],
       [1.66732038e+00, 1.06422315e+00, 4.33815308e-01, ...,
        2.13993190e-03, 1.20624618e-02, 1.14007989e-02]])

 

  • 사용자 기반 추천
my_id, my_vector = 0, U[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(U):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 0.9999996213542565, Best Match ID: 639
recommend_list = []
for i, log in enumerate(zip(adj_matrix[my_id], adj_matrix[best_match_id])):
    log1, log2 = log
    # 내가 보지 않았지만(log1(my_vector)가 1보가 작음) 유사하게 나온 영화(log2(best_match_vector)가 0보다 큼)를 추천
    if log1 < 1. and log2 > 0.:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[300, 301, 303, ..., 1227, 1243, 1257]

 

  • 항목 기반 추천
my_id, my_vector = 0, V.T[0]
best_match, best_match_id, best_match_vector = -1, -1, []
for user_id, user_vector in enumerate(U):
    # user_id와 my_id가 다르면 코사인 유사도 계산
    if my_id != user_id:
        cos_similarity = compute_cos_similarity(my_vector, user_vector)
        if cos_similarity > best_match:
            best_match = cos_similarity
            best_match_id = user_id
            best_match_vector = user_vector

print('Best Match: {}, Best Match ID: {}'.format(best_match, best_match_id))

# 출력 결과
Best Match: 0.9999999998965627, Best Match ID: 1425
recommend_list = []
for i, user_vector in enumerate(adj_matrix):
    if adj_matrix[i][my_id] > 0.9:
        recommend_list.append(i)
print(recommend_list)

# 출력 결과
[0, 1, 4, ..., 935, 937, 940]

+ Recent posts