day 27 songs and movie recommendations
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
-
explicit: 좋아요나 별점처럼 선호도
-
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)
아래 그림에서 첫 번째 벡터(1, 0.1)은 바로 빨간 모자를 쓴 첫 번째 사용자의 특성(Feature) 벡터가 됩니다.
벡터를 잘 만드는 기준은 유저i의 벡터와 아이템j의 벡터를 내적했을 때 유저i가 아이템j에 대해 평가한 수치와 비슷한지 입니다.
CSR(Compressed Sparse Row) Matrix
컴퓨터의 메모리는 많아야 16GB일 테니 97GB나 되는 거대한 행렬을 메모리에 올려놓고 작업한다는 것은 불가능할 것입니다. 이런 경우의 좋은 대안이 되는 것이 CSR(Compressed Sparse Row) Matrix입니다.
CSR Matrix는 Sparse한 matrix에서 0이 아닌 유효한 데이터로 채워지는 데이터의 값과 좌표 정보만으로 구성하여 메모리 사용량을 최소화하면서도 Sparse한 matrix와 동일한 행렬을 표현할 수 있도록 하는 데이터 구조입니다.
we will be converting our data into a CSR by using the fourth line of code below!
# 실습 위에 설명보고 이해해서 만들어보기
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
분석해 봅시다.
-
ratings에 있는 유니크한 영화 개수
-
rating에 있는 유니크한 사용자 수
-
가장 인기 있는 영화 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