A.I
Explolation 8 영화 추천 시스템 만들기 본문
플레이리스트 곡 추천 시스템 만들기¶
1. 목표¶
- 추천시스템의 개념과 목적을 이해한다.
- Implicit 라이브러리를 활용하여 Matrix Factorization(이하 MF) 기반의 추천 모델을 만들어 본다.
- 음악 감상 기록을 활용하여 비슷한 아티스트를 찾고 아티스트를 추천해 본다.
- 추천 시스템에서 자주 사용되는 데이터 구조인 CSR Matrix을 익힌다
- 유저의 행위 데이터 중 Explicit data와 Implicit data의 차이점을 익힌다.
- 새로운 데이터셋으로 직접 추천 모델을 만들어 본다.
2. 추천시스템의 종류¶
- 협업 필터링 - 다수의 사용자가 아이템을 구매한 이력 정보만으로 사용자간 유사성 및 아이템 간 유사성을 파악하는 방식
- 콘텐츠 기반 필터링 - 아이템의 고유의 정보를 바탕으로 아이템 간 유사성을 파악
- 모델 기반 필터링
3. 데이터셋 다운로드¶
1) 작업디렉토리 생성
$ mkdir -p ~/aiffel/recommendata_iu/data
2) wget으로 데이터 다운로드 (주의) 오래걸립니다.
$ wget http://mtg.upf.edu/static/datasets/last.fm/lastfm-dataset-360K.tar.gz
3) 다운받은 데이터를 작업디렉토리로 옮기고, 작업디렉토리로 이동합니다.
$ mv lastfm-dataset-360K.tar.gz ~/aiffel/recommendata_iu/data & cd ~/aiffel/recommendata_iu/data
4) gzip으로 압축된 압축을 해제하면 tar 파일이 하나 나옵니다.
$ gunzip lastfm-dataset-360K.tar.gz
5) tar 압축을 다시 해제하면 우리가 사용할 최종 데이터 파일이 나옵니다.
$ tar -xvf lastfm-dataset-360K.tar
6) 필요 라이브러리 설치
$ pip install implicit
# 4,5를 합쳐서 $ tar -xvzf lastfm-dataset-360K.tar.gz 도 된다.
4. 데이터 탐색, 전처리¶
In [119]:
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)
Out[119]:
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 |
In [120]:
# 사용하는 컬럼만 남겨줍니다.
using_cols = ['user_id', 'artist', 'play']
data = data[using_cols]
data.head(10)
Out[120]:
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 |
In [121]:
data['artist'] = data['artist'].str.lower() # 검색을 쉽게하기 위해 아티스트 문자열을 소문자로 바꿔줍시다.
data.head(10)
Out[121]:
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 |
In [122]:
# 첫번째 유저의 플레이리스트
condition = (data['user_id']== data.loc[0, 'user_id'])
data.loc[condition]
Out[122]:
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 |
In [123]:
# 특정 컬럼에 포함된 유니크한 데이터의 개수를 알아보는데 유용한 함수 = pandas.nunique()
# 유저 수
data['user_id'].nunique()
Out[123]:
358868
In [124]:
# 아티스트 수
data['artist'].nunique()
Out[124]:
291346
In [125]:
# 인기 많은 아티스트
artist_count = data.groupby('artist')['user_id'].count()
artist_count.sort_values(ascending=False).head(30)
Out[125]:
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
In [126]:
# 유저별 몇 명의 아티스트를 듣고 있는지에 대한 통계
user_count = data.groupby('user_id')['artist'].count()
user_count.describe()
Out[126]:
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
In [127]:
# 유저별 play횟수 중앙값에 대한 통계
user_median = data.groupby('user_id')['play'].median()
user_median.describe()
Out[127]:
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
모델 검증을 위한 사용자 초기정보 세팅¶
In [128]:
# 본인이 좋아하시는 아티스트 데이터로 바꿔서 추가하셔도 됩니다! 단, 이름은 꼭 데이터셋에 있는 것과 동일하게 맞춰주세요.
my_favorite = ['black eyed peas' , 'maroon5' ,'jason mraz' ,'coldplay' ,'beyoncé']
# 'zimin'이라는 user_id가 위 아티스트의 노래를 30회씩 들었다고 가정하겠습니다.
# 원본 데이터프레임이 5열이라 맞추기 위해 *5를 함
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) # 잘 추가되었는지 확인해 봅시다.
Out[128]:
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 |
In [129]:
# 고유한 유저, 아티스트를 찾아내는 코드, indexing하기에 좋은 함수 unique()
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)}
In [130]:
# 인덱싱이 잘 되었는지 확인해 봅니다.
print(user_to_idx['zimin']) # 358869명의 유저 중 마지막으로 추가된 유저이니 358868이 나와야 합니다.
print(artist_to_idx['black eyed peas'])
358868 376
In [131]:
# 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!!
Out[131]:
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
암묵적 데이터해석¶
In [18]:
# 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%
5. Matrix Factorization(MF)¶
- m명의 사용자들이 n명의 아티스트에 대해 평가한 데이터를 포함한 (m,n) 사이즈의 평가행렬을 (m,k) 사이즈의 행렬 P와 (k,n) 사이즈의 행렬 Q로 분해
- 유저 벡터(1,0.1)과 영화 벡터(0.9,-0.2)를 내적하여 나온 0.88이 선호도 수치가 되고 그것과 유저가 평가한 수치가 비슷한지를 판별하는 것
6. CSR(Compressed Sparse Row) Matrix¶
- Sparse한 matrix에서 0이 아닌 유효한 데이터로 채워지는 데이터의 값과 좌표 정보만으로 구성하여 메모리 사용량을 최소화하면서도 Sparse한 matrix와 동일한 행렬을 표현할 수 있도록 하는 데이터 구조
In [19]:
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
Out[19]:
<358869x291347 sparse matrix of type '<class 'numpy.int64'>' with 17535578 stored elements in Compressed Sparse Row format>
7. MF 모델 학습시키기¶
- implicit 패키지는 암묵적(implicit) dataset을 사용하는 다양한 모델을 굉장히 빠르게 학습할 수 있는 패키지
- implicit의 als(AlternatingLeastSquares) 모델을 사용함
- Matrix Factorization에서 쪼개진 두 Feature Matrix를 한꺼번에 훈련하는 것은 잘 수렴하지 않기 때문에, 한쪽을 고정시키고 다른 쪽을 학습하는 방식을 번갈아 수행하는 AlternatingLeastSquares 방식이 효과적인 것으로 알려져 있음.
In [20]:
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'
In [21]:
# Implicit AlternatingLeastSquares 모델의 선언
# 1. factors : 유저와 아이템의 벡터를 몇 차원으로 할 것인지
# 2. regularization : 과적합을 방지하기 위해 정규화 값을 얼마나 사용할 것인지
# 3. use_gpu : GPU를 사용할 것인지
# 4. iterations : epochs와 같은 의미입니다.
als_model = AlternatingLeastSquares(factors=100, regularization=0.01, use_gpu=False, iterations=15, dtype=np.float32)
In [22]:
# als 모델은 input으로 (item X user 꼴의 matrix를 받기 때문에 Transpose해줍니다.)
csr_data_transpose = csr_data.T
csr_data_transpose
Out[22]:
<291347x358869 sparse matrix of type '<class 'numpy.int64'>' with 17535578 stored elements in Compressed Sparse Column format>
In [23]:
# 모델 훈련
als_model.fit(csr_data_transpose)
In [24]:
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
In [25]:
zimin_vector
Out[25]:
array([-4.9510500e-01, -1.4235295e+00, 2.8452680e-01, 2.9100642e-01, -3.5865343e-01, -3.6839953e-01, 5.7589209e-01, 9.1401860e-02, -1.3590585e-01, -2.2450075e-01, 3.2290813e-01, 2.3693653e-02, 9.8419386e-01, 2.3814332e-01, -6.0928607e-01, 2.7275950e-01, 1.0332026e+00, 4.9265262e-01, 6.6964018e-01, 4.6849036e-01, 1.5774548e-01, 4.7170010e-01, -3.2188049e-01, 5.9436923e-01, -3.1184900e-01, -4.2732749e-03, 3.9436452e-02, 4.7626647e-01, -9.7209367e-04, 3.2539329e-01, 1.6153884e-01, 4.8377526e-01, 1.9756103e-02, 4.3224324e-02, -1.8622506e-01, 1.1093806e-01, 3.3785525e-01, -1.6846085e-01, -6.4407140e-01, 2.2095317e-01, -2.5668651e-01, -4.6768698e-01, 1.3053647e+00, 1.4554694e-01, 9.0846598e-01, -2.7613872e-01, -9.6209466e-01, -1.2002536e-01, 1.3545744e-01, -3.7141562e-01, 7.1928930e-01, 9.3279207e-01, 2.5161257e-02, -7.6657462e-01, -7.6227212e-01, -5.5231971e-01, 1.6480577e-01, 4.0873423e-01, 1.1192833e+00, -1.9666952e-01, -8.9973971e-02, -3.7464428e-01, -7.9177350e-02, 9.6489936e-01, 6.1208165e-01, 1.1389677e-01, -1.7456481e-02, 3.8792670e-01, -9.7241265e-01, 5.0142622e-01, 4.4885240e-03, -4.1235429e-01, 3.6287650e-01, 1.2843001e+00, -3.0194789e-01, -9.7812814e-01, -7.0023859e-01, -5.6704968e-02, 3.4591660e-01, -2.2981296e+00, 5.8736372e-01, -1.8699604e+00, 4.8906776e-01, 6.0248190e-01, 7.9236782e-01, -8.6925620e-01, -2.8362355e-01, -6.7099459e-02, -1.1030551e+00, -7.4215317e-01, 5.5129308e-01, -5.2089435e-01, -6.0184485e-01, 9.9326438e-01, 3.8994676e-01, 5.3475958e-01, 2.2050383e-02, 4.6379486e-01, -3.8469163e-01, 1.2226229e+00], dtype=float32)
In [27]:
black_eyed_peas_vector
Out[27]:
array([ 0.0100072 , -0.01515865, 0.00945084, -0.0018355 , -0.00531362, -0.00701354, 0.00633627, 0.01428174, 0.02187927, 0.00029228, 0.02812726, -0.01406871, 0.01879251, 0.01248477, -0.00338888, 0.01809198, 0.01639173, 0.01223026, 0.01479124, 0.01383292, 0.00810628, 0.0076852 , -0.00516676, 0.00713026, 0.00958604, -0.00411979, 0.01324273, 0.00348666, 0.01663821, 0.01697581, 0.01169868, 0.00587595, -0.00028922, 0.00125099, 0.00067179, 0.01893274, 0.01969319, 0.00860626, 0.00713631, 0.01413464, 0.01519715, 0.00184746, 0.02925006, 0.01753703, 0.00825574, -0.00550648, 0.00408476, 0.00685188, -0.0032142 , -0.00540822, 0.0298967 , -0.00129863, -0.00326962, -0.00349745, -0.00261757, -0.00731679, 0.00963585, 0.02646964, 0.02172663, 0.00087632, 0.00759343, -0.00232931, 0.01153524, 0.00736095, 0.01586488, 0.00476488, 0.01100142, 0.01444802, -0.0040286 , 0.0071806 , -0.01008575, -0.01632634, 0.00995844, 0.02766299, 0.0014728 , -0.00370758, -0.00052825, 0.01800286, 0.00558399, -0.0257668 , -0.00063903, -0.00855784, 0.0110085 , 0.00474735, 0.01344537, -0.0028446 , 0.01354675, -0.01755911, -0.01328867, 0.00159285, 0.00843361, -0.01021624, 0.00814112, 0.02012722, 0.01899944, 0.00305775, 0.00960304, 0.00731487, 0.00198583, 0.0231179 ], dtype=float32)
In [28]:
# zimin과 black_eyed_peas를 내적하는 코드
np.dot(zimin_vector, black_eyed_peas_vector)
Out[28]:
0.48714095
In [29]:
queen = artist_to_idx['queen']
queen_vector = als_model.item_factors[queen]
np.dot(zimin_vector, queen_vector)
Out[29]:
0.30303988
0.48이 나왔지만 1에 가깝지 않다고 해서 제대로 된 학습 모델이 아니라곤 할 수 없다. 객관적인 지표가 다 다르기때문¶
8. 비슷한 아티스트 찾기 + 유저에게 추천하기¶
In [30]:
# (id, 유사도 )
favorite_artist = 'coldplay'
artist_id = artist_to_idx[favorite_artist]
similar_artist = als_model.similar_items(artist_id, N=15)
similar_artist
Out[30]:
[(62, 0.9999999), (277, 0.9875708), (5, 0.9788243), (28, 0.97783685), (473, 0.96755993), (217, 0.9643141), (418, 0.96198434), (490, 0.9616533), (247, 0.95851594), (910, 0.95625687), (694, 0.95437866), (75, 0.9394704), (1018, 0.9373235), (268, 0.93311405), (782, 0.931866)]
In [31]:
#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]
Out[31]:
['coldplay', 'muse', 'red hot chili peppers', 'the killers', 'placebo', 'radiohead', 'u2', 'oasis', 'the beatles', 'nirvana', 'foo fighters', 'queen', 'the smashing pumpkins', 'pink floyd', 'the white stripes']
In [32]:
# 비슷한 아티스트 이름 검색 함수
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
In [33]:
get_similar_artist('2pac')
# 이 경우 힙합 마니아가 추천되고 있다
Out[33]:
['2pac', '50 cent', 'nas', 'jay-z', 'snoop dogg', 'the game', 'notorious b.i.g.', 'dr. dre', 't.i.', 'busta rhymes']
In [34]:
get_similar_artist('lady gaga')
# 레이디가가의 경우 여자 아티스트들이 추천되고 있다
Out[34]:
['lady gaga', 'britney spears', 'rihanna', 'katy perry', 'beyoncé', 'the pussycat dolls', 'christina aguilera', 'kelly clarkson', 'justin timberlake', 'leona lewis']
추천 받기¶
- AlternatingLeastSquares 클래스의 recommend 메서드를 통하여 제가 좋아할 만한 아티스트를 추천받게 됨.
- filter_already_liked_items 는 유저가 이미 평가한 아이템은 제외하는 Argument.
In [35]:
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
Out[35]:
[(350, 0.44104922), (274, 0.42349035), (550, 0.4187476), (369, 0.41069847), (1800, 0.41035873), (354, 0.3886162), (355, 0.3853805), (564, 0.38391814), (2249, 0.37572753), (724, 0.37446743), (434, 0.37237418), (627, 0.37141788), (391, 0.36463428), (409, 0.3633657), (24, 0.3512236), (5, 0.3511018), (1777, 0.35104734), (621, 0.3508524), (618, 0.34964776), (5555, 0.34842014)]
In [36]:
[idx_to_artist[i[0]] for i in artist_recommended]
Out[36]:
['rihanna', 'michael jackson', 'britney spears', 'justin timberlake', 'lady gaga', 'nelly furtado', 'madonna', 'kanye west', 'katy perry', 'lily allen', 'eminem', 'maroon 5', 'christina aguilera', 'amy winehouse', 'jack johnson', 'red hot chili peppers', 'mariah carey', 'alicia keys', 'the pussycat dolls', 'timbaland']
In [38]:
# 기록을 남긴 데이터 중 추천에 기여한 정도에 대해 알수 있는 explain
rihanna = artist_to_idx['rihanna']
explain = als_model.explain(user, csr_data, itemid=rihanna)
In [40]:
[(idx_to_artist[i[0]], i[1]) for i in explain[1]]
# 1, 2번째에서 흑인 아티스트에 성별 정도가 기여했을거라고 추측가능함
Out[40]:
[('beyoncé', 0.21628514204624225), ('black eyed peas', 0.13526949867117205), ('jason mraz', 0.05058101091633696), ('coldplay', 0.039832360803826856), ('maroon5', 0.0004446361542168777)]
9. Movielens 영화 추천 실습¶
- 유저가 영화에 대해 평점을 매긴 데이터가 데이터 크기 별로 있습니다. MovieLens 1M Dataset 사용을 권장합니다.
- 별점 데이터는 대표적인 explicit 데이터입니다. 하지만 implicit 데이터로 간주하고 테스트해볼 수 있습니다.
- 별점을 시청횟수로 해석해서 생각하겠습니다.
- 또한 유저가 3점 미만으로 준 데이터는 선호하지 않는다고 가정하고 제외하겠습니다.
1) wget으로 데이터 다운로드 $ wget http://files.grouplens.org/datasets/movielens/ml-1m.zip
2) 다운받은 데이터를 작업디렉토리로 옮김 $ mv ml-1m.zip ~/aiffel/recommendata_iu/data
3) 작업디렉토리로 이동 $ cd ~/aiffel/recommendata_iu/data
4) 압축 해제 $ unzip ml-1m.zip
9-1. 데이터 전처리¶
In [426]:
import pandas as pd
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')
orginal_data_size = len(ratings)
ratings.head()
Out[426]:
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 |
In [427]:
# 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%
In [428]:
# rating 컬럼의 이름을 count로 바꿉니다.
ratings.rename(columns={'rating':'count'}, inplace=True)
In [429]:
ratings['count']
Out[429]:
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
In [430]:
# 영화 제목을 보기 위해 메타 데이터를 읽어옵니다.
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')
movies.head()
Out[430]:
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 |
In [431]:
# 영화를 title로 입력할 것이기때문에 title이란 컬럼을 불러와야합니다.
ratings = pd.merge(ratings, movies[['title', 'movie_id']], on='movie_id', how='left')
ratings
Out[431]:
user_id | movie_id | count | timestamp | title | |
---|---|---|---|---|---|
0 | 1 | 1193 | 5 | 978300760 | One Flew Over the Cuckoo's Nest (1975) |
1 | 1 | 661 | 3 | 978302109 | James and the Giant Peach (1996) |
2 | 1 | 914 | 3 | 978301968 | My Fair Lady (1964) |
3 | 1 | 3408 | 4 | 978300275 | Erin Brockovich (2000) |
4 | 1 | 2355 | 5 | 978824291 | Bug's Life, A (1998) |
... | ... | ... | ... | ... | ... |
836473 | 6040 | 1090 | 3 | 956715518 | Platoon (1986) |
836474 | 6040 | 1094 | 5 | 956704887 | Crying Game, The (1992) |
836475 | 6040 | 562 | 5 | 956704746 | Welcome to the Dollhouse (1995) |
836476 | 6040 | 1096 | 4 | 956715648 | Sophie's Choice (1982) |
836477 | 6040 | 1097 | 4 | 956715569 | E.T. the Extra-Terrestrial (1982) |
836478 rows × 5 columns
In [400]:
# ratings에 있는 유니크한 영화 개수
ratings['movie_id'].nunique()
Out[400]:
3628
In [401]:
# rating에 있는 유니크한 사용자 수
ratings['user_id'].nunique()
Out[401]:
6039
In [402]:
#가장 인기 있는 영화 30개(인기순) 영화 id별 별점(시청횟수)많은 순
movie_count = ratings.groupby('movie_id')['count'].count()
movie_count.sort_values(ascending=False).head(30)
Out[402]:
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
In [403]:
# 내가 선호하는 영화를 5가지 골라서 rating에 추가
my_favorite = ['minority report', 'mission impossible', 'get out' , 'inception', 'gravity']
my_list = pd.DataFrame({'user_id': ['meissamatar']*5, 'title': my_favorite, 'count':[5]*5})
if not ratings.isin({'user_id':['meissamatar']})['user_id'].any():
ratings = ratings.append(my_list)
ratings.tail(10)
Out[403]:
user_id | movie_id | count | timestamp | title | |
---|---|---|---|---|---|
836473 | 6040 | 1090.0 | 3 | 956715518.0 | Platoon (1986) |
836474 | 6040 | 1094.0 | 5 | 956704887.0 | Crying Game, The (1992) |
836475 | 6040 | 562.0 | 5 | 956704746.0 | Welcome to the Dollhouse (1995) |
836476 | 6040 | 1096.0 | 4 | 956715648.0 | Sophie's Choice (1982) |
836477 | 6040 | 1097.0 | 4 | 956715569.0 | E.T. the Extra-Terrestrial (1982) |
0 | meissamatar | NaN | 5 | NaN | minority report |
1 | meissamatar | NaN | 5 | NaN | mission impossible |
2 | meissamatar | NaN | 5 | NaN | get out |
3 | meissamatar | NaN | 5 | NaN | inception |
4 | meissamatar | NaN | 5 | NaN | gravity |
In [424]:
# 인덱싱
userid_unique = ratings['user_id'].unique()
#movie_unique = ratings['movie_id'].unique()
movie_unique = ratings['title'].unique()
# user, movie indexing 하는 코드 idx는 index의 약자입니다.
user_to_idx = {v:k for k,v in enumerate(userid_unique)}
movie_to_idx = {v:k for k,v in enumerate(movie_unique)}
In [425]:
# 인덱싱이 잘 되었는지 확인
print(user_to_idx['meissamatar'])
print(movie_to_idx['inception'])
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-425-6678ef2a841a> in <module> 1 # 인덱싱이 잘 되었는지 확인 ----> 2 print(user_to_idx['meissamatar']) 3 print(movie_to_idx['inception']) KeyError: 'meissamatar'
In [406]:
# indexing을 통해 데이터 컬럼 내 값을 바꾸는 코드
# dictionary 자료형의 get 함수는 https://wikidocs.net/16 을 참고하세요.
# user_to_idx.get을 통해 user_id 컬럼의 모든 값을 인덱싱한 Series를 구해 봅시다.
# 혹시 정상적으로 인덱싱되지 않은 row가 있다면 인덱스가 NaN이 될 테니 dropna()로 제거합니다.
temp_user_data = ratings['user_id'].map(user_to_idx.get).dropna()
if len(temp_user_data) == len(ratings): # 모든 row가 정상적으로 인덱싱되었다면
print('user_id column indexing OK!!')
ratings['user_id'] = temp_user_data # data['user_id']을 인덱싱된 Series로 교체해 줍니다.
else:
print('user_id column indexing Fail!!')
# artist_to_idx을 통해 artist 컬럼도 동일한 방식으로 인덱싱해 줍니다.
temp_movie_data = ratings['title'].map(movie_to_idx.get).dropna()
if len(temp_movie_data) == len(ratings):
print('movie_id column indexing OK!!')
ratings['movie_id'] = temp_movie_data
else:
print('movie_id column indexing Fail!!')
ratings
user_id column indexing OK!! movie_id column indexing OK!!
Out[406]:
user_id | movie_id | count | timestamp | title | |
---|---|---|---|---|---|
0 | 0 | 0 | 5 | 978300760.0 | One Flew Over the Cuckoo's Nest (1975) |
1 | 0 | 1 | 3 | 978302109.0 | James and the Giant Peach (1996) |
2 | 0 | 2 | 3 | 978301968.0 | My Fair Lady (1964) |
3 | 0 | 3 | 4 | 978300275.0 | Erin Brockovich (2000) |
4 | 0 | 4 | 5 | 978824291.0 | Bug's Life, A (1998) |
... | ... | ... | ... | ... | ... |
0 | 6039 | 3628 | 5 | NaN | minority report |
1 | 6039 | 3629 | 5 | NaN | mission impossible |
2 | 6039 | 3630 | 5 | NaN | get out |
3 | 6039 | 3631 | 5 | NaN | inception |
4 | 6039 | 3632 | 5 | NaN | gravity |
836483 rows × 5 columns
In [407]:
# csr_matrix
from scipy.sparse import csr_matrix
num_user = ratings['user_id'].nunique()
num_movie = ratings['movie_id'].nunique()
csr_data = csr_matrix((ratings['count'], (ratings.user_id, ratings.movie_id)), shape= (num_user, num_movie))
csr_data
Out[407]:
<6040x3633 sparse matrix of type '<class 'numpy.int64'>' with 836483 stored elements in Compressed Sparse Row format>
In [408]:
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 모델의 선언
# 1. factors : 유저와 아이템의 벡터를 몇 차원으로 할 것인지
# 2. regularization : 과적합을 방지하기 위해 정규화 값을 얼마나 사용할 것인지
# 3. use_gpu : GPU를 사용할 것인지
# 4. iterations : epochs와 같은 의미입니다.
als_model = AlternatingLeastSquares(factors=100, regularization=0.01, use_gpu=False, iterations=10, dtype=np.float32)
In [409]:
# als 모델은 input으로 (item X user 꼴의 matrix를 받기 때문에 Transpose해줍니다.)
csr_data_transpose = csr_data.T
csr_data_transpose
Out[409]:
<3633x6040 sparse matrix of type '<class 'numpy.int64'>' with 836483 stored elements in Compressed Sparse Column format>
In [410]:
# 모델 훈련
als_model.fit(csr_data_transpose)
In [411]:
meissamatar, inception = user_to_idx['meissamatar'], movie_to_idx['inception']
meissamatar_vector, inception_vector = als_model.user_factors[meissamatar], als_model.item_factors[inception]
print('슝=3')
슝=3
In [412]:
# 내적하는 코드
np.dot(meissamatar_vector, inception_vector)
Out[412]:
0.002199483
In [417]:
getout = movie_to_idx['get out']
getout_vector = als_model.item_factors[getout]
np.dot(meissamatar_vector, getout_vector)
Out[417]:
0.0018644872
In [418]:
# (id, 유사도 )
favorite_movie = 'inception'
movie_id = movie_to_idx[favorite_movie]
similar_movie = als_model.similar_items(movie_id, N=15)
similar_movie
Out[418]:
[(3631, 1.0000001), (3629, 0.9977376), (3632, 0.9956162), (3628, 0.9944474), (3630, 0.9934629), (3576, 0.9887485), (3579, 0.9886055), (3575, 0.98838395), (3578, 0.9881278), (3574, 0.9878915), (3587, 0.9877299), (3572, 0.9873435), (3577, 0.9872493), (3591, 0.98625475), (3580, 0.98605186)]
In [419]:
#artist_to_idx 를 뒤집어, index로부터 artist 이름을 얻는 dict를 생성합니다.
idx_to_movie = {v:k for k,v in movie_to_idx.items()}
[idx_to_movie[i[0]] for i in similar_movie]
Out[419]:
['inception', 'mission impossible', 'gravity', 'minority report', 'get out', 'War at Home, The (1996)', 'Last of the High Kings, The (a.k.a. Summer Fling) (1996)', "Brother's Kiss, A (1997)", 'Number Seventeen (1932)', 'Century (1993)', 'Shopping (1994)', "I Don't Want to Talk About It (De eso no se habla) (1993)", 'Daens (1992)', "Heaven's Burning (1997)", "Another Man's Poison (1952)"]
In [373]:
# 비슷한 영화 이름 검색 함수
def get_similar_movie(movie_name: str):
movie_id = movie_to_idx[movie_name]
similar_movie = als_model.similar_items(movie_id)
similar_movie = [idx_to_movie[i[0]] for i in similar_movie]
return similar_movie
print("슝=3")
슝=3
In [374]:
get_similar_movie('get out')
Out[374]:
['get out', 'mission impossible', 'gravity', 'inception', 'minority report', 'Chain of Fools (2000)', 'Last of the High Kings, The (a.k.a. Summer Fling) (1996)', 'Neon Bible, The (1995)', 'Number Seventeen (1932)', "Brother's Kiss, A (1997)"]
In [422]:
user = user_to_idx['meissamatar']
# recommend에서는 user*item CSR Matrix를 받습니다.
movie_recommended = als_model.recommend(user, csr_data, N=20, filter_already_liked_items=True)
movie_recommended
Out[422]:
[(399, 0.013890838), (326, 0.012384821), (60, 0.011084938), (832, 0.0109194), (98, 0.010854109), (122, 0.0106233), (2229, 0.010200506), (869, 0.010023468), (38, 0.010013877), (2045, 0.009994587), (2289, 0.00981511), (107, 0.009811414), (2398, 0.009624012), (75, 0.009615339), (476, 0.009600335), (2498, 0.00942111), (313, 0.009312067), (1854, 0.009094686), (2562, 0.009013027), (2878, 0.008928617)]
In [423]:
[idx_to_movie[i[0]] for i in movie_recommended]
Out[423]:
['Anna and the King (1999)', 'Top Gun (1986)', 'Star Wars: Episode I - The Phantom Menace (1999)', 'Jakob the Liar (1999)', 'Mis�rables, Les (1998)', 'Waking Ned Devine (1998)', 'Thieves (Voleurs, Les) (1996)', 'Angel Baby (1995)', 'Sixth Sense, The (1999)', 'For Love of the Game (1999)', 'Welcome To Sarajevo (1997)', 'Jurassic Park (1993)', 'Tigerland (2000)', 'Hunt for Red October, The (1990)', 'Wrong Trousers, The (1993)', 'Two Family House (2000)', 'Nowhere (1997)', 'Before and After (1996)', 'Last Night (1998)', 'Third Miracle, The (1999)']
정리¶
- Implicit 라이브러리를 활용하여 Matrix Factorization(이하 MF) 기반의 추천 모델을 만들어 CSR Matrix를 이용해 추천 모델을 만들었는데 영화 제목 추천을 위해 새로 한 모델링에서 적합 추천도가 너무 낮게 나와 여러 방법으로 학습방법을 바꿔보았으나 알 수 없었다.
- Explicit Datasets란 유저의 호불호를 알 수 있는 명시적 데이터로써 유용하나 데이터수집이 어렵다
- Implicit Datasets은 암묵적으로 선호할 것이다라고 추측할 수 있는 데이터로 검색 기록, 방문 페이지, 구매 내역 등이 있음
- 부정적인 피드백이 없다(No Negative Feedback)
- 애초에 잡음이 많다(Inherently Noisy)
- 수치는 신뢰도를 의미한다.
- CSR Matric에 대한 이해
- CSR Matirc 행렬식 by python
- 추천시스템 MAP
'AIFFEL' 카테고리의 다른 글
Explolation10 인물사진 배경바꿔보기 (0) | 2021.02.04 |
---|---|
Explolation9 캐글 따라해보기 - 주택가격 예측 (0) | 2021.02.02 |
Explolation7 닮은 꼴 연예인 찾기 (0) | 2021.01.26 |
Explolation 6 작사가 만들기 (0) | 2021.01.21 |
Exploration5 오디오 음성 데이터 분류 (0) | 2021.01.19 |