Go together

If you want to go fast, go alone. If you want to go far, go together.

파이썬 머신 러닝 완벽 가이드

행렬 분해를 이용한 잠재 요인 협업 필터링 실습

NowChan 2021. 12. 15. 21:17
Latent2

행렬 분해를 이용한 잠재 요인 협업 필터링 실습

일반적으로 행렬 분해에는 SVD가 자주 사용되지만, 사용자-아이템 평점 행렬에는 사용자가 평점을 매기지 않은 null 데이터가 많기 때문에 SGD나 ALS 기반의 행렬 분해를 사용합니다. 여기서는 SGD를 이용하겠습니다.

앞의 잠재 요인 협업 필터링 절의 경사 하강법을 이용한 행렬 분해에서 사용한 함수 get_rmse()를 다시 활용하고, 행렬 분해 로직을 matrix_factorization()함수로 정리합니다. 파라미터 R은 사용자-아이템 평점 행렬, K는 잠재 요인의 차원 수, steps는 SGD의 반복 횟수, learning_rate는 학습률, r_lambda는 L2 규제 계수입니다.

In [9]:
import numpy as np
from sklearn.metrics import mean_squared_error

def get_rmse(R, P, Q, non_zeros):
  error = 0
  # 두 개의 분해된 행렬 P와 Q.T의 내적으로 예측 R 행렬 생성
  full_pred_matrix = np.dot(P, Q.T)

  # 실제 R 행렬에서 null이 아닌 값의 위치 인덱스를 추출해 실제 R 행렬과 예측 행렬의 RMSE 추출.
  x_non_zero_ind = [non_zero[0] for non_zero in non_zeros]
  y_non_zero_ind = [non_zero[1] for non_zero in non_zeros]
  R_non_zeros = R[x_non_zero_ind, y_non_zero_ind]
  full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind]
  mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros)
  rmse = np.sqrt(mse)

  return rmse


def matrix_factorization(R, K, steps=200, learning_rate=0.01, r_lambda=0.01):
  num_users, num_items = R.shape
  # P와 Q 매트릭스의 크기를 지정하고 정규 분포를 가진 랜덤한 값으로 입력합니다.
  np.random.seed(1)
  P = np.random.normal(scale=1./K, size=(num_users, K))
  Q = np.random.normal(scale=1./K, size=(num_items, K))

  prev_rmse = 10000
  break_count = 0

  # R > 0 인 행 위치, 열 위치, 값을 non_zeros 리스트 객체에 저장.
  non_zeros = [ (i, j, R[i, j]) for i in range(num_users) for j in range(num_items) if R[i, j] > 0]

  # SGD 기법으로 P와 Q 매트릭스를 계속 업데이트.
  for step in range(steps):
    for i, j, r in non_zeros:
      # 실제 값과 예측 값의 차이인 오류 값 구함
      eij = r - np.dot(P[i, :], Q[j, :].T)
      # Regularization을 반영한 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, :])

    rmse = get_rmse(R, P, Q, non_zeros)
    if (step % 10) == 0:
      print("### iteration step : ", step, " rmse : ", rmse)

  return P, Q

영화 평점 행렬 데이터를 다시 사용자-아이템 평점 행렬로 만들겠습니다.

In [5]:
import pandas as pd

movies = pd.read_csv('/content/drive/MyDrive/military/grouplens/movies.csv')
ratings = pd.read_csv('/content/drive/MyDrive/military/grouplens/ratings.csv')
ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')

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

사용자-아이템 행렬을 matrix_factorization()함수를 이용해 행렬 분해하겠습니다.

In [10]:
P, Q = matrix_factorization(ratings_matrix.values, K=50, steps=200, learning_rate=0.01,\
                            r_lambda = 0.01)
pred_matrix = np.dot(P, Q.T)
### iteration step :  0  rmse :  2.9023619751336867
### iteration step :  10  rmse :  0.7335768591017927
### iteration step :  20  rmse :  0.5115539026853442
### iteration step :  30  rmse :  0.37261628282537446
### iteration step :  40  rmse :  0.2960818299181014
### iteration step :  50  rmse :  0.2520353192341642
### iteration step :  60  rmse :  0.22487503275269854
### iteration step :  70  rmse :  0.20685455302331543
### iteration step :  80  rmse :  0.19413418783028685
### iteration step :  90  rmse :  0.18470082002720403
### iteration step :  100  rmse :  0.17742927527209104
### iteration step :  110  rmse :  0.1716522696470749
### iteration step :  120  rmse :  0.1669518194687172
### iteration step :  130  rmse :  0.1630529219199754
### iteration step :  140  rmse :  0.15976691929679646
### iteration step :  150  rmse :  0.1569598699945732
### iteration step :  160  rmse :  0.1545339818671543
### iteration step :  170  rmse :  0.15241618551077643
### iteration step :  180  rmse :  0.1505508073962831
### iteration step :  190  rmse :  0.1488947091323209

더 직관적으로 아이템 칼럼을 표현하고자 칼럼명을 영화 타이틀로 바꾸겠습니다.

In [11]:
ratings_pred_matrix = pd.DataFrame(data=pred_matrix, index=ratings_matrix.index,\
                                   columns=ratings_matrix.columns)
ratings_pred_matrix.head(3)
Out[11]:
title '71 (2014) 'Hellboy': The Seeds of Creation (2004) 'Round Midnight (1986) 'Salem's Lot (2004) 'Til There Was You (1997) 'Tis the Season for Love (2015) 'burbs, The (1989) 'night Mother (1986) (500) Days of Summer (2009) *batteries not included (1987) ...All the Marbles (1981) ...And Justice for All (1979) 00 Schneider - Jagd auf Nihil Baxter (1994) 1-900 (06) (1994) 10 (1979) 10 Cent Pistol (2015) 10 Cloverfield Lane (2016) 10 Items or Less (2006) 10 Things I Hate About You (1999) 10 Years (2011) 10,000 BC (2008) 100 Girls (2000) 100 Streets (2016) 101 Dalmatians (1996) 101 Dalmatians (One Hundred and One Dalmatians) (1961) 101 Dalmatians II: Patch's London Adventure (2003) 101 Reykjavik (101 Reykjavík) (2000) 102 Dalmatians (2000) 10th & Wolf (2006) 10th Kingdom, The (2000) 10th Victim, The (La decima vittima) (1965) 11'09"01 - September 11 (2002) 11:14 (2003) 11th Hour, The (2007) 12 Angry Men (1957) 12 Angry Men (1997) 12 Chairs (1971) 12 Chairs (1976) 12 Rounds (2009) 12 Years a Slave (2013) ... Zathura (2005) Zatoichi and the Chest of Gold (Zatôichi senryô-kubi) (Zatôichi 6) (1964) Zazie dans le métro (1960) Zebraman (2004) Zed & Two Noughts, A (1985) Zeitgeist: Addendum (2008) Zeitgeist: Moving Forward (2011) Zeitgeist: The Movie (2007) Zelary (2003) Zelig (1983) Zero Dark Thirty (2012) Zero Effect (1998) Zero Theorem, The (2013) Zero de conduite (Zero for Conduct) (Zéro de conduite: Jeunes diables au collège) (1933) Zeus and Roxanne (1997) Zipper (2015) Zodiac (2007) Zombeavers (2014) Zombie (a.k.a. Zombie 2: The Dead Are Among Us) (Zombi 2) (1979) Zombie Strippers! (2008) Zombieland (2009) Zone 39 (1997) Zone, The (La Zona) (2007) Zookeeper (2011) Zoolander (2001) Zoolander 2 (2016) Zoom (2006) Zoom (2015) Zootopia (2016) Zulu (1964) Zulu (2013) [REC] (2007) [REC]² (2009) [REC]³ 3 Génesis (2012) anohana: The Flower We Saw That Day - The Movie (2013) eXistenZ (1999) xXx (2002) xXx: State of the Union (2005) ¡Three Amigos! (1986) À nous la liberté (Freedom for Us) (1931)
userId
1 3.055084 4.092018 3.564130 4.502167 3.981215 1.271694 3.603274 2.333266 5.091749 3.972454 1.623927 3.910138 4.775403 3.837260 3.875488 1.550801 2.929129 2.680321 3.225626 3.251925 2.778350 3.331543 2.391855 3.199047 4.148949 1.852731 3.269642 3.448719 4.458060 3.719499 3.231820 3.521511 3.866924 3.961768 4.957933 4.075665 3.509040 3.923190 3.210152 4.374122 ... 3.546313 3.207635 2.082641 3.302390 1.821505 3.814172 4.227119 3.699006 3.009256 4.605246 4.712096 4.284418 3.095067 3.214574 0.990303 1.805794 4.588016 2.295002 4.173353 0.327724 4.817989 1.902907 3.557027 2.881273 3.766529 2.703354 2.395317 2.373198 4.749076 4.281203 1.402608 4.208382 3.705957 2.720514 2.787331 3.475076 3.253458 2.161087 4.010495 0.859474
2 3.170119 3.657992 3.308707 4.166521 4.311890 1.275469 4.237972 1.900366 3.392859 3.647421 1.489588 3.617857 3.785199 3.168660 3.537318 0.995625 3.969397 2.173005 3.464055 2.475622 3.413724 2.665215 1.828840 3.322109 2.654698 1.469953 3.035060 3.163879 4.244324 2.727754 2.879571 3.124665 3.773794 3.774747 3.175855 3.458016 2.923885 3.303497 2.806202 3.504966 ... 3.289954 2.677164 2.087793 3.388524 1.783418 3.267824 3.661620 3.131275 2.475330 3.916692 4.197842 3.987094 3.134310 2.827407 0.829738 1.380996 3.974255 2.685338 3.902178 0.293003 3.064224 1.566051 3.095034 2.769578 3.956414 2.493763 2.236924 1.775576 3.909241 3.799859 0.973811 3.528264 3.361532 2.672535 2.404456 4.232789 2.911602 1.634576 4.135735 0.725684
3 2.307073 1.658853 1.443538 2.208859 2.229486 0.780760 1.997043 0.924908 2.970700 2.551446 0.881095 1.813452 2.687841 1.908641 2.228256 0.695248 1.146590 1.536595 0.809632 1.561342 1.820714 1.097596 1.216409 1.347617 1.760926 0.622817 1.786144 1.934932 2.332054 2.291151 1.983643 1.785523 2.265654 2.055809 2.459728 2.092599 2.512530 2.928443 1.777471 1.808872 ... 1.779506 2.222377 1.448616 2.340729 1.658322 2.231055 2.634708 2.235721 1.340105 2.322287 2.483354 2.199769 2.313019 1.807883 0.617402 0.906815 3.362981 2.024704 2.460702 0.128483 3.936125 1.135435 1.912071 2.419887 3.416503 1.601437 1.177825 1.159584 2.617399 2.675379 0.520354 1.709494 2.281596 1.782833 1.635173 1.323276 2.887580 1.042618 2.293890 0.396941

3 rows × 9719 columns

예측 사용자-아이템 평점 행렬 정보를 이용해 개인화된 영화 추천을 해보겠습니다. 아이템 기반 최근접 이웃 협업 필터링 실습과 마찬가지로 9번 사용자에 대해 추천해보겠습니다.

In [13]:
def get_unseen_movies(rating_matrix, userId):
  # userId로 입력받은 사용자의 모든 영화 정보를 추출해 Series로 반환함.
  # 반환된 user_rating은 영화명(title)을 인덱스로 가지는 Series 객체임.
  user_rating = ratings_matrix.loc[userId, :]

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

  # 모든 영화명을 list 객체로 만듦.
  movies_list = ratings_matrix.columns.tolist()

  # list comprehension으로 already_seen에 해당하는 영화는 movies_list에서 제외함.
  unseen_list = [ movie for movie in movies_list if movie not in already_seen ]

  return unseen_list

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
  # 예측 평점 DataFrame에서 사용자id 인덱스와 unseen_list로 들어온 영화명 칼럼을 추출해
  # 가장 예측 평점이 높은 순으로 정렬함.
  recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
  return recomm_movies
In [14]:
# 사용자가 관람하지 않은 영화명 추출
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
Out[14]:
pred_score
title
Rear Window (1954) 5.704612
South Park: Bigger, Longer and Uncut (1999) 5.451100
Rounders (1998) 5.298393
Blade Runner (1982) 5.244951
Roger & Me (1989) 5.191962
Gattaca (1997) 5.183179
Ben-Hur (1959) 5.130463
Rosencrantz and Guildenstern Are Dead (1990) 5.087375
Big Lebowski, The (1998) 5.038690
Star Wars: Episode V - The Empire Strikes Back (1980) 4.989601

아이템 기반 협업 필터링 결과와는 다릅니다. 모두 훌륭하지만 어둡거나 무거운 영화가 추천됐습니다.

출처: 파이썬 머신러닝 완벽가이드(권철민)
사진 출처: