day 27 songs and movie recommendations

16 minute read

artist recommendation

1) 협업 필터링(Collaborative Filtering) 방식: 다수의 사용자의 아이템 구매 이력 정보만으로 사용자간 유사성 및 아이템 간 유사성을 파악. 협업 필터링에서는 아이템과 사용자 간의 행동 또는 관계에만 주목할 뿐 아이템 자체의 고유한 속성에 주목하지 않습니다. 하지만 몇가지 제약상황이있어서 바로사용은 못한다.

- 시스템이 충분한 정보를 모으지 못한 사용자나 아이템에 대한 추론을 할 수 없는 상태인 콜드 스타트(Cold Start) 상황

- 계산량이 너무 많아 추천의 효율이 떨어지는 상황

- 롱테일의 꼬리 부분, 즉 사용자의 관심이 저조한 항목의 정보가 부족하여 추천에서 배제되는 상황

2) 콘텐츠 기반 필터링(Contents-based Filtering)은 아이템의 고유의 정보를 바탕으로 아이템 간 유사성을 파악합니다. 아이템 자체의 속성에만 주목하고 사용자와 아이템 간의 관련성 에는 주목하지 않습니다.

data exploration and preprocessing

import pandas as pd
import os

fname = os.getenv('HOME') + '/aiffel/recommendata_iu/data/lastfm-dataset-360K/usersha1-artmbid-artname-plays.tsv'
col_names = ['user_id', 'artist_MBID', 'artist', 'play']   # 임의로 지정한 컬럼명
data = pd.read_csv(fname, sep='\t', names= col_names)      # sep='\t'로 주어야 tsv를 열 수 있습니다.  
data.head(10)
user_id artist_MBID artist play
0 00000c289a1829a808ac09c00daf10bc3c4e223b 3bd73256-3905-4f3a-97e2-8b341527f805 betty blowtorch 2137
1 00000c289a1829a808ac09c00daf10bc3c4e223b f2fb0ff0-5679-42ec-a55c-15109ce6e320 die Ärzte 1099
2 00000c289a1829a808ac09c00daf10bc3c4e223b b3ae82c2-e60b-4551-a76d-6620f1b456aa melissa etheridge 897
3 00000c289a1829a808ac09c00daf10bc3c4e223b 3d6bbeb7-f90e-4d10-b440-e153c0d10b53 elvenking 717
4 00000c289a1829a808ac09c00daf10bc3c4e223b bbd2ffd7-17f4-4506-8572-c1ea58c3f9a8 juliette & the licks 706
5 00000c289a1829a808ac09c00daf10bc3c4e223b 8bfac288-ccc5-448d-9573-c33ea2aa5c30 red hot chili peppers 691
6 00000c289a1829a808ac09c00daf10bc3c4e223b 6531c8b1-76ea-4141-b270-eb1ac5b41375 magica 545
7 00000c289a1829a808ac09c00daf10bc3c4e223b 21f3573f-10cf-44b3-aeaa-26cccd8448b5 the black dahlia murder 507
8 00000c289a1829a808ac09c00daf10bc3c4e223b c5db90c4-580d-4f33-b364-fbaa5a3a58b5 the murmurs 424
9 00000c289a1829a808ac09c00daf10bc3c4e223b 0639533a-0402-40ba-b6e0-18b067198b73 lunachicks 403
# 사용하는 컬럼만 남겨줍니다.
using_cols = ['user_id', 'artist', 'play']
data = data[using_cols]
data.head(10)
user_id artist play
0 00000c289a1829a808ac09c00daf10bc3c4e223b betty blowtorch 2137
1 00000c289a1829a808ac09c00daf10bc3c4e223b die Ärzte 1099
2 00000c289a1829a808ac09c00daf10bc3c4e223b melissa etheridge 897
3 00000c289a1829a808ac09c00daf10bc3c4e223b elvenking 717
4 00000c289a1829a808ac09c00daf10bc3c4e223b juliette & the licks 706
5 00000c289a1829a808ac09c00daf10bc3c4e223b red hot chili peppers 691
6 00000c289a1829a808ac09c00daf10bc3c4e223b magica 545
7 00000c289a1829a808ac09c00daf10bc3c4e223b the black dahlia murder 507
8 00000c289a1829a808ac09c00daf10bc3c4e223b the murmurs 424
9 00000c289a1829a808ac09c00daf10bc3c4e223b lunachicks 403
data['artist'] = data['artist'].str.lower() # 검색을 쉽게하기 위해 아티스트 문자열을 소문자로 바꿔줍시다.
data.head(10)
user_id artist play
0 00000c289a1829a808ac09c00daf10bc3c4e223b betty blowtorch 2137
1 00000c289a1829a808ac09c00daf10bc3c4e223b die ärzte 1099
2 00000c289a1829a808ac09c00daf10bc3c4e223b melissa etheridge 897
3 00000c289a1829a808ac09c00daf10bc3c4e223b elvenking 717
4 00000c289a1829a808ac09c00daf10bc3c4e223b juliette & the licks 706
5 00000c289a1829a808ac09c00daf10bc3c4e223b red hot chili peppers 691
6 00000c289a1829a808ac09c00daf10bc3c4e223b magica 545
7 00000c289a1829a808ac09c00daf10bc3c4e223b the black dahlia murder 507
8 00000c289a1829a808ac09c00daf10bc3c4e223b the murmurs 424
9 00000c289a1829a808ac09c00daf10bc3c4e223b lunachicks 403
condition = (data['user_id']== data.loc[0, 'user_id'])
data.loc[condition]
user_id artist play
0 00000c289a1829a808ac09c00daf10bc3c4e223b betty blowtorch 2137
1 00000c289a1829a808ac09c00daf10bc3c4e223b die ärzte 1099
2 00000c289a1829a808ac09c00daf10bc3c4e223b melissa etheridge 897
3 00000c289a1829a808ac09c00daf10bc3c4e223b elvenking 717
4 00000c289a1829a808ac09c00daf10bc3c4e223b juliette & the licks 706
5 00000c289a1829a808ac09c00daf10bc3c4e223b red hot chili peppers 691
6 00000c289a1829a808ac09c00daf10bc3c4e223b magica 545
7 00000c289a1829a808ac09c00daf10bc3c4e223b the black dahlia murder 507
8 00000c289a1829a808ac09c00daf10bc3c4e223b the murmurs 424
9 00000c289a1829a808ac09c00daf10bc3c4e223b lunachicks 403
10 00000c289a1829a808ac09c00daf10bc3c4e223b walls of jericho 393
11 00000c289a1829a808ac09c00daf10bc3c4e223b letzte instanz 387
12 00000c289a1829a808ac09c00daf10bc3c4e223b goldfrapp 361
13 00000c289a1829a808ac09c00daf10bc3c4e223b horrorpops 358
14 00000c289a1829a808ac09c00daf10bc3c4e223b the butchies 329
15 00000c289a1829a808ac09c00daf10bc3c4e223b jack off jill 316
16 00000c289a1829a808ac09c00daf10bc3c4e223b babes in toyland 310
17 00000c289a1829a808ac09c00daf10bc3c4e223b dropkick murphys 302
18 00000c289a1829a808ac09c00daf10bc3c4e223b all:my:faults 288
19 00000c289a1829a808ac09c00daf10bc3c4e223b le tigre 281
20 00000c289a1829a808ac09c00daf10bc3c4e223b schandmaul 244
21 00000c289a1829a808ac09c00daf10bc3c4e223b edguy 232
22 00000c289a1829a808ac09c00daf10bc3c4e223b maximum the hormone 231
23 00000c289a1829a808ac09c00daf10bc3c4e223b all ends 229
24 00000c289a1829a808ac09c00daf10bc3c4e223b jack johnson 227
25 00000c289a1829a808ac09c00daf10bc3c4e223b eluveitie 222
26 00000c289a1829a808ac09c00daf10bc3c4e223b rasputina 220
27 00000c289a1829a808ac09c00daf10bc3c4e223b london after midnight 210
28 00000c289a1829a808ac09c00daf10bc3c4e223b the killers 208
29 00000c289a1829a808ac09c00daf10bc3c4e223b mutyumu 205
30 00000c289a1829a808ac09c00daf10bc3c4e223b judas priest 198
31 00000c289a1829a808ac09c00daf10bc3c4e223b rob zombie 198
32 00000c289a1829a808ac09c00daf10bc3c4e223b the bosshoss 189
33 00000c289a1829a808ac09c00daf10bc3c4e223b blue öyster cult 185
34 00000c289a1829a808ac09c00daf10bc3c4e223b sandra nasic 183
35 00000c289a1829a808ac09c00daf10bc3c4e223b john mayer 182
36 00000c289a1829a808ac09c00daf10bc3c4e223b sleater-kinney 175
37 00000c289a1829a808ac09c00daf10bc3c4e223b the who 168
38 00000c289a1829a808ac09c00daf10bc3c4e223b disciple 154
39 00000c289a1829a808ac09c00daf10bc3c4e223b tanzwut 153
40 00000c289a1829a808ac09c00daf10bc3c4e223b guano apes 151
41 00000c289a1829a808ac09c00daf10bc3c4e223b the rolling stones 150
42 00000c289a1829a808ac09c00daf10bc3c4e223b little big town 145
43 00000c289a1829a808ac09c00daf10bc3c4e223b team dresch 137
44 00000c289a1829a808ac09c00daf10bc3c4e223b betty 135
45 00000c289a1829a808ac09c00daf10bc3c4e223b l7 135
46 00000c289a1829a808ac09c00daf10bc3c4e223b bif naked 134
47 00000c289a1829a808ac09c00daf10bc3c4e223b girlschool 134
48 00000c289a1829a808ac09c00daf10bc3c4e223b the wallflowers 131

we want to check the following information before we proceed.

  • 유저수, 아티스트수, 인기 많은 아티스트

  • 유저들이 몇 명의 아티스트를 듣고 있는지에 대한 통계

  • 유저 play 횟수 중앙값에 대한 통계

# 유저 수
data['user_id'].nunique()
358868
# 아티스트 수
data['artist'].nunique()
291346
# 인기 많은 아티스트
artist_count = data.groupby('artist')['user_id'].count()
artist_count.sort_values(ascending=False).head(30)
artist
radiohead                77254
the beatles              76245
coldplay                 66658
red hot chili peppers    48924
muse                     46954
metallica                45233
pink floyd               44443
the killers              41229
linkin park              39773
nirvana                  39479
system of a down         37267
queen                    34174
u2                       33206
daft punk                33001
the cure                 32624
led zeppelin             32295
placebo                  32072
depeche mode             31916
david bowie              31862
bob dylan                31799
death cab for cutie      31482
arctic monkeys           30348
foo fighters             30144
air                      29795
the rolling stones       29754
nine inch nails          28946
sigur rós                28901
green day                28732
massive attack           28691
moby                     28232
Name: user_id, dtype: int64
# 유저별 몇 명의 아티스트를 듣고 있는지에 대한 통계
user_count = data.groupby('user_id')['artist'].count()
user_count.describe()
count    358868.000000
mean         48.863234
std           8.524272
min           1.000000
25%          46.000000
50%          49.000000
75%          51.000000
max         166.000000
Name: artist, dtype: float64
# 유저별 play횟수 중앙값에 대한 통계
user_median = data.groupby('user_id')['play'].median()
user_median.describe()
count    358868.000000
mean        142.187676
std         213.089902
min           1.000000
25%          32.000000
50%          83.000000
75%         180.000000
max       50142.000000
Name: play, dtype: float64

Initializing what my choice of music genre are

# 본인이 좋아하시는 아티스트 데이터로 바꿔서 추가하셔도 됩니다! 단, 이름은 꼭 데이터셋에 있는 것과 동일하게 맞춰주세요. 
my_favorite = ['black eyed peas' , 'maroon5' ,'jason mraz' ,'coldplay' ,'beyoncé']

# 'zimin'이라는 user_id가 위 아티스트의 노래를 30회씩 들었다고 가정하겠습니다.
my_playlist = pd.DataFrame({'user_id': ['zimin']*5, 'artist': my_favorite, 'play':[30]*5})

if not data.isin({'user_id':['zimin']})['user_id'].any():  # user_id에 'zimin'이라는 데이터가 없다면
    data = data.append(my_playlist)                           # 위에 임의로 만든 my_favorite 데이터를 추가해 줍니다. 

data.tail(10)       # 잘 추가되었는지 확인해 봅시다.
user_id artist play
17535650 sep 20, 2008 turbostaat 12
17535651 sep 20, 2008 cuba missouri 11
17535652 sep 20, 2008 little man tate 11
17535653 sep 20, 2008 sigur rós 10
17535654 sep 20, 2008 the smiths 10
0 zimin black eyed peas 30
1 zimin maroon5 30
2 zimin jason mraz 30
3 zimin coldplay 30
4 zimin beyoncé 30

preprocessing the data

# 고유한 유저, 아티스트를 찾아내는 코드
user_unique = data['user_id'].unique()
artist_unique = data['artist'].unique()

# 유저, 아티스트 indexing 하는 코드 idx는 index의 약자입니다.
user_to_idx = {v:k for k,v in enumerate(user_unique)}
artist_to_idx = {v:k for k,v in enumerate(artist_unique)}
# 인덱싱이 잘 되었는지 확인해 봅니다. 
print(user_to_idx['zimin'])    # 358869명의 유저 중 마지막으로 추가된 유저이니 358868이 나와야 합니다. 
print(artist_to_idx['black eyed peas'])
358868
376
# indexing을 통해 데이터 컬럼 내 값을 바꾸는 코드
# dictionary 자료형의 get 함수는 https://wikidocs.net/16 을 참고하세요.

# user_to_idx.get을 통해 user_id 컬럼의 모든 값을 인덱싱한 Series를 구해 봅시다. 
# 혹시 정상적으로 인덱싱되지 않은 row가 있다면 인덱스가 NaN이 될 테니 dropna()로 제거합니다. 
temp_user_data = data['user_id'].map(user_to_idx.get).dropna()
if len(temp_user_data) == len(data):   # 모든 row가 정상적으로 인덱싱되었다면
    print('user_id column indexing OK!!')
    data['user_id'] = temp_user_data   # data['user_id']을 인덱싱된 Series로 교체해 줍니다. 
else:
    print('user_id column indexing Fail!!')

# artist_to_idx을 통해 artist 컬럼도 동일한 방식으로 인덱싱해 줍니다. 
temp_artist_data = data['artist'].map(artist_to_idx.get).dropna()
if len(temp_artist_data) == len(data):
    print('artist column indexing OK!!')
    data['artist'] = temp_artist_data
else:
    print('artist column indexing Fail!!')

data
user_id column indexing OK!!
artist column indexing OK!!
user_id artist play
0 0 0 2137
1 0 1 1099
2 0 2 897
3 0 3 717
4 0 4 706
... ... ... ...
0 358868 376 30
1 358868 270115 30
2 358868 3746 30
3 358868 62 30
4 358868 396 30

17535660 rows × 3 columns

explicit and implicit Feedback Datasets

  1. explicit: 좋아요나 별점처럼 선호도

  2. implicit: 플레이 횟수, 플레이 시간 이외에도 클릭 수, 구매 여부, 플레이 스킵 여부, 검색 기록, 방문 페이지 이력, 구매 내역, 심지어 마우스 움직임 기록

# 1회만 play한 데이터의 비율을 보는 코드
only_one = data[data['play']<2]
one, all_data = len(only_one), len(data)
print(f'{one},{all_data}')
print(f'Ratio of only_one over all data is {one/all_data:.2%}')  # f-format에 대한 설명은 https://bit.ly/2DTLqYU
147740,17535660
Ratio of only_one over all data is 0.84%

we can apply this rule for the implicit data.

한 번이라도 들었으면 선호한다고 판단한다. 많이 재생한 아티스트에 대해 가중치를 주어서 더 확실히 좋아한다고 판단한다.

Matrix Factorization(MF)

image

아래 그림에서 첫 번째 벡터(1, 0.1)은 바로 빨간 모자를 쓴 첫 번째 사용자의 특성(Feature) 벡터가 됩니다.

image

벡터를 잘 만드는 기준은 유저i의 벡터와 아이템j의 벡터를 내적했을 때 유저i가 아이템j에 대해 평가한 수치와 비슷한지 입니다.

CSR(Compressed Sparse Row) Matrix

컴퓨터의 메모리는 많아야 16GB일 테니 97GB나 되는 거대한 행렬을 메모리에 올려놓고 작업한다는 것은 불가능할 것입니다. 이런 경우의 좋은 대안이 되는 것이 CSR(Compressed Sparse Row) Matrix입니다.

CSR Matrix는 Sparse한 matrix에서 0이 아닌 유효한 데이터로 채워지는 데이터의 값과 좌표 정보만으로 구성하여 메모리 사용량을 최소화하면서도 Sparse한 matrix와 동일한 행렬을 표현할 수 있도록 하는 데이터 구조입니다.

image

we will be converting our data into a CSR by using the fourth line of code below!

image

# 실습 위에 설명보고 이해해서 만들어보기
from scipy.sparse import csr_matrix

num_user = data['user_id'].nunique()
num_artist = data['artist'].nunique()

csr_data = csr_matrix((data.play, (data.user_id, data.artist)), shape= (num_user, num_artist))
csr_data
<358869x291347 sparse matrix of type '<class 'numpy.longlong'>'
	with 17535578 stored elements in Compressed Sparse Row format>

model training

from implicit.als import AlternatingLeastSquares
import os
import numpy as np

# implicit 라이브러리에서 권장하고 있는 부분입니다. 학습 내용과는 무관합니다.
os.environ['OPENBLAS_NUM_THREADS']='1'
os.environ['KMP_DUPLICATE_LIB_OK']='True'
os.environ['MKL_NUM_THREADS']='1'
# Implicit AlternatingLeastSquares 모델의 선언
als_model = AlternatingLeastSquares(factors=100, regularization=0.01, use_gpu=False, iterations=15, dtype=np.float32)
# als 모델은 input으로 (item X user 꼴의 matrix를 받기 때문에 Transpose해줍니다.)
csr_data_transpose = csr_data.T
csr_data_transpose
<291347x358869 sparse matrix of type '<class 'numpy.longlong'>'
	with 17535578 stored elements in Compressed Sparse Column format>
# 모델 훈련
als_model.fit(csr_data_transpose)
  0%|          | 0/15 [00:00<?, ?it/s]
zimin, black_eyed_peas = user_to_idx['zimin'], artist_to_idx['black eyed peas']
zimin_vector, black_eyed_peas_vector = als_model.user_factors[zimin], als_model.item_factors[black_eyed_peas]

print('슝=3')
슝=3
zimin_vector
array([-0.7592462 , -0.06493481, -0.507626  ,  0.96602815, -0.23503311,
       -0.05180614,  0.63202494, -0.34085822,  0.63247466, -0.7291423 ,
       -1.0455726 ,  0.2067991 ,  0.5978946 ,  0.4936649 ,  0.0948993 ,
       -0.4576465 , -0.36899385,  0.36306298,  0.18059005,  0.14147176,
       -0.79331464,  0.54938596,  1.7006662 , -1.02685   , -1.455536  ,
       -0.9249583 ,  0.5028596 ,  0.62338763,  0.52531445,  0.6298627 ,
       -0.09430686, -0.26444176, -1.0283402 , -0.44847754, -0.3544845 ,
       -0.45059034, -0.7516798 , -0.44101644, -0.22910073, -0.20027487,
       -0.29566193,  1.0920273 ,  1.0365067 ,  0.90407234, -0.12368082,
        0.08903237,  0.43249854,  0.00501352,  0.37376893, -0.0112507 ,
       -1.0098957 , -0.6799084 ,  1.271641  ,  0.9156588 ,  0.74463266,
       -0.6343055 , -0.1391036 , -0.10668989, -1.8061879 , -0.07961043,
        0.43269402,  0.81586665,  0.29758987,  0.11674579,  0.55735   ,
       -0.58191884,  0.91736406, -0.06631707, -0.5778925 , -0.45255226,
        0.16005418,  0.51755655,  0.6397744 ,  0.69170374, -0.61512697,
        0.17513062, -0.55918634, -0.7539491 , -0.51165926,  0.10475736,
       -0.71279156,  0.10860785, -0.31442195,  1.0399585 ,  0.07249789,
        0.6748526 ,  0.56756145, -0.04082086,  0.1807014 ,  0.35132658,
        0.31796828, -0.8967092 ,  1.8156484 ,  0.5019691 ,  0.2647283 ,
       -0.55166066,  0.22360323,  0.83356273, -0.45412877,  0.23655899],
      dtype=float32)
black_eyed_peas_vector
array([ 0.00092155,  0.0037288 , -0.00591179,  0.01229655, -0.00022075,
        0.01623239,  0.02380466,  0.00271064,  0.02181727,  0.00268115,
        0.01081787,  0.01389915,  0.01485916,  0.00138209, -0.00918201,
        0.00513296,  0.00140428,  0.02887422, -0.00255375,  0.00589194,
        0.0129447 ,  0.01982983,  0.0341149 ,  0.01034041, -0.02419258,
        0.0025793 ,  0.00443419,  0.01260059,  0.00931566,  0.01145281,
        0.00615998, -0.01471031,  0.00313114,  0.00076899, -0.00730769,
       -0.02007815, -0.00487638, -0.00980756,  0.01258231, -0.00085783,
       -0.00516806,  0.03189239,  0.02696881,  0.01481936,  0.00401158,
       -0.0053828 ,  0.0041779 , -0.00211049,  0.01258902,  0.01062932,
        0.01339797, -0.00208662,  0.02383751,  0.03083708,  0.01757795,
       -0.00358369,  0.00649732,  0.008264  , -0.01099218,  0.01492189,
        0.00632308,  0.01573377,  0.00395725,  0.01466446,  0.01079403,
       -0.01202705,  0.01345134,  0.00812779, -0.00727663, -0.00091335,
        0.00375464,  0.01123345,  0.02673124,  0.01178155, -0.00515146,
       -0.00118297, -0.00093771,  0.01222679, -0.00529956,  0.0041867 ,
        0.00414685,  0.01096029, -0.00664166,  0.01638241,  0.00452505,
        0.00847527,  0.01331143,  0.00811779,  0.01545997,  0.0018015 ,
        0.01908703,  0.01254102,  0.02516586,  0.01617545, -0.00367989,
       -0.00846714, -0.00124175,  0.01470638, -0.00821195,  0.00629747],
      dtype=float32)
# zimin과 black_eyed_peas를 내적하는 코드
np.dot(zimin_vector, black_eyed_peas_vector)
0.50943065

1이 나와야 될 것 같은데 한참 낮은 수치인 0.49정도가 나왔습니다. factors를 늘리거나 iterations를 늘려야 할 것 같습니다.

queen = artist_to_idx['queen']
queen_vector = als_model.item_factors[queen]
np.dot(zimin_vector, queen_vector)
0.2931291

비슷한 아티스트 찾기 + 유저에게 추천하기

AlternatingLeastSquares 클래스에 구현되어 있는 similar_items 메서드를 통하여 비슷한 아티스트를 찾습니다. 처음으로는 제가 좋아하는 coldplay로 찾아보겠습니다.

favorite_artist = 'coldplay'
artist_id = artist_to_idx[favorite_artist]
similar_artist = als_model.similar_items(artist_id, N=15)
similar_artist
[(62, 0.9999999),
 (277, 0.988214),
 (28, 0.9780598),
 (5, 0.97733897),
 (217, 0.97256804),
 (490, 0.97201747),
 (473, 0.96647596),
 (247, 0.96248823),
 (418, 0.9578663),
 (910, 0.9492244),
 (694, 0.94809544),
 (782, 0.94644564),
 (268, 0.94387203),
 (55, 0.943281),
 (1018, 0.93693)]
#artist_to_idx 를 뒤집어, index로부터 artist 이름을 얻는 dict를 생성합니다. 
idx_to_artist = {v:k for k,v in artist_to_idx.items()}
[idx_to_artist[i[0]] for i in similar_artist]
['coldplay',
 'muse',
 'the killers',
 'red hot chili peppers',
 'radiohead',
 'oasis',
 'placebo',
 'the beatles',
 'u2',
 'nirvana',
 'foo fighters',
 'the white stripes',
 'pink floyd',
 'arctic monkeys',
 'the smashing pumpkins']
def get_similar_artist(artist_name: str):
    artist_id = artist_to_idx[artist_name]
    similar_artist = als_model.similar_items(artist_id)
    similar_artist = [idx_to_artist[i[0]] for i in similar_artist]
    return similar_artist

print("슝=3")
슝=3
get_similar_artist('2pac')
['2pac',
 'notorious b.i.g.',
 'nas',
 'dr. dre',
 'the game',
 '50 cent',
 'jay-z',
 'busta rhymes',
 't.i.',
 'dmx']
get_similar_artist('lady gaga')
['lady gaga',
 'katy perry',
 'britney spears',
 'rihanna',
 'beyoncé',
 'the pussycat dolls',
 'christina aguilera',
 'justin timberlake',
 'leona lewis',
 'pink']

AlternatingLeastSquares 클래스에 구현되어 있는 recommend 메서드를 통하여 제가 좋아할 만한 아티스트를 추천받습니다. filter_already_liked_items 는 유저가 이미 평가한 아이템은 제외하는 Argument입니다.

user = user_to_idx['zimin']
# recommend에서는 user*item CSR Matrix를 받습니다.
artist_recommended = als_model.recommend(user, csr_data, N=20, filter_already_liked_items=True)
artist_recommended
[(550, 0.44718507),
 (350, 0.44545946),
 (369, 0.4248806),
 (1800, 0.419877),
 (354, 0.41939032),
 (621, 0.41919178),
 (627, 0.41269463),
 (355, 0.40721822),
 (2249, 0.40551537),
 (274, 0.40278572),
 (391, 0.3868053),
 (24, 0.38224107),
 (724, 0.3809113),
 (382, 0.37476385),
 (2902, 0.36877534),
 (618, 0.36738253),
 (901, 0.3638745),
 (409, 0.35522002),
 (1777, 0.35415196),
 (899, 0.34994948)]
[idx_to_artist[i[0]] for i in artist_recommended]
['britney spears',
 'rihanna',
 'justin timberlake',
 'lady gaga',
 'nelly furtado',
 'alicia keys',
 'maroon 5',
 'madonna',
 'katy perry',
 'michael jackson',
 'christina aguilera',
 'jack johnson',
 'lily allen',
 'mika',
 'james blunt',
 'the pussycat dolls',
 'pink',
 'amy winehouse',
 'mariah carey',
 'kylie minogue']
rihanna = artist_to_idx['rihanna']
explain = als_model.explain(user, csr_data, itemid=rihanna)
[(idx_to_artist[i[0]], i[1]) for i in explain[1]]
[('beyoncé', 0.22667906711933644),
 ('black eyed peas', 0.14912920069744284),
 ('jason mraz', 0.04193791264596783),
 ('coldplay', 0.02954028414112784),
 ('maroon5', -0.00010856273679905773)]

filter_already_liked_items=True로 했는데 앞에 maroon 5는 제가 이미 평가한 아이템이 나오고 있습니다. 이것은 implicit 버전 0.4.2에서 생긴 버그입니다. 코드는 사람이 만들기 때문에 이렇게 실수할 가능성이 있습니다.

우리가 만든 모델은 몇 가지 아쉬운 점이 있습니다. 1. 유저, 아티스트에 대한 Meta 정보를 반영하기 쉽지 않습니다. 연령대별로 음악 취향이 굉장히 다르지 않을까요? 2. 유저가 언제 play 했는지에 반영하기 쉽지 않습니다. 10년 전에 즐겨듣던 아티스트와 지금 즐겨듣는 아티스트를 비교해보세요.

이러한 이유와 딥러닝의 발전으로 MF 이외의 모델 구조도 많이 연구/ 사용되고 있습니다. 하지만 어떤 추천 모델도 핵심은 MF와 비슷합니다. 유저와 아이템에 대한 벡터를 잘 학습하여 취향에 맞게(유저에 맞게) 아이템을 보여주거나(Retrieval) 걸러내는(Filtering) 역할입니다.

Self Project - Movielens 영화 추천 실습

데이터 준비와 전처리

import os
rating_file_path=os.getenv('HOME') + '/aiffel/recommendata_iu/data/ml-1m/ratings.dat'
ratings_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv(rating_file_path, sep='::', names=ratings_cols, engine='python', encoding = "ISO-8859-1")
orginal_data_size = len(ratings)
ratings.head()
user_id movie_id rating timestamp
0 1 1193 5 978300760
1 1 661 3 978302109
2 1 914 3 978301968
3 1 3408 4 978300275
4 1 2355 5 978824291
# 3점 이상만 남깁니다.
ratings = ratings[ratings['rating']>=3]
filtered_data_size = len(ratings)

print(f'orginal_data_size: {orginal_data_size}, filtered_data_size: {filtered_data_size}')
print(f'Ratio of Remaining Data is {filtered_data_size / orginal_data_size:.2%}')
orginal_data_size: 1000209, filtered_data_size: 836478
Ratio of Remaining Data is 83.63%
# rating 컬럼의 이름을 count로 바꿉니다.
ratings.rename(columns={'rating':'count'}, inplace=True)
ratings['count']
0          5
1          3
2          3
3          4
4          5
          ..
1000203    3
1000205    5
1000206    5
1000207    4
1000208    4
Name: count, Length: 836478, dtype: int64
# 영화 제목을 보기 위해 메타 데이터를 읽어옵니다.
movie_file_path=os.getenv('HOME') + '/aiffel/recommendata_iu/data/ml-1m/movies.dat'
cols = ['movie_id', 'title', 'genre'] 
movies = pd.read_csv(movie_file_path, sep='::', names=cols, engine='python', encoding='ISO-8859-1')
movies.head()
movie_id title genre
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy
movies.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3883 entries, 0 to 3882
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype 
---  ------    --------------  ----- 
 0   movie_id  3883 non-null   int64 
 1   title     3883 non-null   object
 2   genre     3883 non-null   object
dtypes: int64(1), object(2)
memory usage: 91.1+ KB

분석해 봅시다.

  1. ratings에 있는 유니크한 영화 개수

  2. rating에 있는 유니크한 사용자 수

  3. 가장 인기 있는 영화 30개(인기순)

# ratings에 있는 유니크한 영화 개수
ratings['movie_id'].nunique()
3628
# rating에 있는 유니크한 사용자 수
ratings['user_id'].nunique()
6039
ratings.head()
user_id movie_id count timestamp
0 1 1193 5 978300760
1 1 661 3 978302109
2 1 914 3 978301968
3 1 3408 4 978300275
4 1 2355 5 978824291
# 가장 인기 있는 영화 30개(인기순)
# 인기 많은 아티스트
movie_count = ratings.groupby('movie_id')['count'].count()
movie_count.sort_values(ascending=False).head(30)
movie_id
2858    3211
260     2910
1196    2885
1210    2716
2028    2561
589     2509
593     2498
1198    2473
1270    2460
2571    2434
480     2413
2762    2385
608     2371
110     2314
1580    2297
527     2257
1197    2252
2396    2213
1617    2210
318     2194
858     2167
1265    2121
1097    2102
2997    2066
2716    2051
296     2030
356     2022
1240    2019
1       2000
457     1941
Name: count, dtype: int64

내가 선호하는 영화를 5가지 골라서 rating에 추가해 줍시다.

ratings.tail()
user_id movie_id count timestamp
1000203 6040 1090 3 956715518
1000205 6040 1094 5 956704887
1000206 6040 562 5 956704746
1000207 6040 1096 4 956715648
1000208 6040 1097 4 956715569
# lets just add movies id 1~5 
row = pd.DataFrame([[9999, 1, 5, 956715518],[9999, 2, 5, 956715518],[9999, 3, 5, 956715518],[9999, 4, 5, 956715518],[9999, 5, 5, 956715518]],columns =['user_id','movie_id','count','timestamp'])
if not ratings.isin({'user_id':['9999']})['user_id'].any():  # user_id에 'zimin'이라는 데이터가 없다면
    ratings = ratings.append(row) 
ratings.tail(10)
user_id movie_id count timestamp
1000203 6040 1090 3 956715518
1000205 6040 1094 5 956704887
1000206 6040 562 5 956704746
1000207 6040 1096 4 956715648
1000208 6040 1097 4 956715569
0 9999 1 5 956715518
1 9999 2 5 956715518
2 9999 3 5 956715518
3 9999 4 5 956715518
4 9999 5 5 956715518

CSR matrix를 직접 만들어 봅시다.

from scipy.sparse import csr_matrix

num_user = ratings['user_id'].nunique()
num_movie = ratings['movie_id'].nunique()
print(num_user)
num_movie
6040





3628
ratings.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 836483 entries, 0 to 4
Data columns (total 4 columns):
 #   Column     Non-Null Count   Dtype
---  ------     --------------   -----
 0   user_id    836483 non-null  int64
 1   movie_id   836483 non-null  int64
 2   count      836483 non-null  int64
 3   timestamp  836483 non-null  int64
dtypes: int64(4)
memory usage: 31.9 MB
movies.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3883 entries, 0 to 3882
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype 
---  ------    --------------  ----- 
 0   movie_id  3883 non-null   int64 
 1   title     3883 non-null   object
 2   genre     3883 non-null   object
dtypes: int64(1), object(2)
memory usage: 91.1+ KB
csr_data = csr_matrix((ratings["count"], (ratings.user_id, ratings.movie_id)))
csr_data
<10000x3953 sparse matrix of type '<class 'numpy.longlong'>'
	with 836483 stored elements in Compressed Sparse Row format>

als_model = AlternatingLeastSquares 모델을 직접 구성하여 훈련시켜 봅시다.

als_model = AlternatingLeastSquares(factors=100, regularization=0.01, use_gpu=False, iterations=15, dtype=np.float32)

csr_data_transpose = csr_data.T
csr_data_transpose
<3953x10000 sparse matrix of type '<class 'numpy.longlong'>'
	with 836483 stored elements in Compressed Sparse Column format>
als_model.fit(csr_data_transpose)
  0%|          | 0/15 [00:00<?, ?it/s]


내가 선호하는 5가지 영화 중 하나와 그 외의 영화 하나를 골라 훈련된 모델이 예측한 나의 선호도를 파악해 보세요.

name_vector, movie_vector = als_model.user_factors[9999], als_model.item_factors[1]
np.dot(name_vector, movie_vector)
0.4537994

내가 좋아하는 영화와 비슷한 영화를 추천받아 봅시다.

movies[movies.movie_id.isin([1])]
movie_id title genre
0 1 Toy Story (1995) Animation|Children's|Comedy
similar_artist = als_model.similar_items(1, N=15)
similar_artist
[(1, 1.0000001),
 (3114, 0.79136944),
 (34, 0.63305384),
 (1265, 0.5710132),
 (2355, 0.5687365),
 (588, 0.55116296),
 (364, 0.43986875),
 (2321, 0.43400142),
 (595, 0.41679445),
 (1923, 0.3979896),
 (1907, 0.37711278),
 (356, 0.360941),
 (2396, 0.3558033),
 (2687, 0.33613083),
 (1566, 0.33067682)]
recommendation_list = []
for i in similar_artist:
    if i[1] < 1 and 0.5 < i[1]:
        recommendation_list.append(i[0])
print(similar_artist)        
movies[movies.movie_id.isin(recommendation_list)]
[(1, 1.0000001), (3114, 0.79136944), (34, 0.63305384), (1265, 0.5710132), (2355, 0.5687365), (588, 0.55116296), (364, 0.43986875), (2321, 0.43400142), (595, 0.41679445), (1923, 0.3979896), (1907, 0.37711278), (356, 0.360941), (2396, 0.3558033), (2687, 0.33613083), (1566, 0.33067682)]
movie_id title genre
33 34 Babe (1995) Children's|Comedy|Drama
584 588 Aladdin (1992) Animation|Children's|Comedy|Musical
1245 1265 Groundhog Day (1993) Comedy|Romance
2286 2355 Bug's Life, A (1998) Animation|Children's|Comedy
3045 3114 Toy Story 2 (1999) Animation|Children's|Comedy

apparently, toy story 2 is the most recommended with 0.79 score!

내가 가장 좋아할 만한 영화들을 추천받아 봅시다.

movies_unique = movies['title'].unique()
# 유저, 아티스트 indexing 하는 코드 idx는 index의 약자입니다.
title_to_idx = {k:v for k,v in enumerate(movies_unique)}
artist_recommended = als_model.recommend(9999, csr_data, N=20, filter_already_liked_items=True)
artist_recommended
[(3114, 0.4786437),
 (317, 0.29126978),
 (34, 0.2789289),
 (2355, 0.27169204),
 (788, 0.24487683),
 (3489, 0.24344388),
 (3450, 0.22649994),
 (367, 0.21434616),
 (588, 0.19017583),
 (60, 0.17881674),
 (653, 0.17460798),
 (1249, 0.1710667),
 (736, 0.16995412),
 (3247, 0.16898002),
 (673, 0.16891783),
 (586, 0.16856252),
 (2082, 0.16357541),
 (2628, 0.16131014),
 (2023, 0.15151238),
 (364, 0.13983035)]
[title_to_idx[i[0]] for i in artist_recommended]
['Third Miracle, The (1999)',
 'Suture (1993)',
 'Carrington (1995)',
 "You've Got Mail (1998)",
 'Daylight (1996)',
 'Law, The (Le Legge) (1958)',
 'Force 10 from Navarone (1978)',
 'Paper, The (1994)',
 'Batman (1989)',
 'Eye for an Eye (1996)',
 'Purple Noon (1960)',
 'Arsenic and Old Lace (1944)',
 'Force of Evil (1948)',
 'Reindeer Games (2000)',
 'Alphaville (1965)',
 'Dances with Wolves (1990)',
 'Gods Must Be Crazy II, The (1989)',
 'My Son the Fanatic (1998)',
 'Return of Jafar, The (1993)',
 'Maverick (1994)']
recommendation_list = []
for i in artist_recommended:
    if i[1] < 1 and 0.2 < i[1]:
        recommendation_list.append(i[0])
print(artist_recommended)        
movies[movies.movie_id.isin(recommendation_list)]
[(3114, 0.4786437), (317, 0.29126978), (34, 0.2789289), (2355, 0.27169204), (788, 0.24487683), (3489, 0.24344388), (3450, 0.22649994), (367, 0.21434616), (588, 0.19017583), (60, 0.17881674), (653, 0.17460798), (1249, 0.1710667), (736, 0.16995412), (3247, 0.16898002), (673, 0.16891783), (586, 0.16856252), (2082, 0.16357541), (2628, 0.16131014), (2023, 0.15151238), (364, 0.13983035)]
movie_id title genre
33 34 Babe (1995) Children's|Comedy|Drama
314 317 Santa Clause, The (1994) Children's|Comedy|Fantasy
363 367 Mask, The (1994) Comedy|Crime|Fantasy
778 788 Nutty Professor, The (1996) Comedy|Fantasy|Romance|Sci-Fi
2286 2355 Bug's Life, A (1998) Animation|Children's|Comedy
3045 3114 Toy Story 2 (1999) Animation|Children's|Comedy
3381 3450 Grumpy Old Men (1993) Comedy
3420 3489 Hook (1991) Adventure|Fantasy

toy story 2 was recommended here as well!

movies[movies.movie_id.isin([1,2,3,4,5])]
movie_id title genre
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy

One of the limitations in this is that I chose the personal movie choices by random. Luckly, The genre of the movies were mostly comedy which helped the model predict similar genre movies.

Leave a comment