21 분 소요

본 게시물은 추천시스템에 대한 구현으로 해당 포스트를 읽고 정리하는 글이다.

1. Introduction

추천 시스템의 목적은 사용자의 선호도를 바탕으로 관련된(relevant) 아이템을 추천해주는 것이다. 이때의 선호도와 관련도는 주관적이며, 일반적으로 사용자가 이전에 소비했던 아이템들을 바탕으로 추론한다.

추천 시스템의 주요 기법들은 다음과 같다.

  1. Collaborative Filtering
    여러 사용자들의 선호도 또는 취향을 수집하여(collaborating) 사용자의 관심사에 대한 예측(filtering)을 수행한다.
    협업 필터링 접근법의 기본적인 가정은 어떤 아이템 집합에 대해 A가 B와 같은 의견을 가지고 있다면, 임의로 주어진 아이템에 대해 A는 다른 사용자보다 B와 유사한 의견을 가진다는 것이다.

  2. Content-Based Filtering
    사용자가 이전에 소비한 아이템의 설명과 속성에 대한 정보만을 사용하여 사용자 선호도를 모델링한다.
    즉, 이 알고리즘은 이전 사용자가 과거 선호했던 아이템을 추천한다.
    특히 사용자에 의해 평가된 이전 아이템들과 다양한 후보 아이템들을 비교하고, 가장 잘 맞는 아이템을 추천한다.

  3. Hybrid Methods
    collaborative filtering과 content-based filtering을 결합한 hybrid approach은 개별 모델보다 효과적이다.
    추천 시스템의 cold start 및 희소성 문제 등을 해결하는데 사용 될 수 있다.

현재의 연구 동향은 딥러닝을 활용한 기법들이며 성능 또한 기존의 방법들에 비해 향상됨을 증명하였다. 이에 대한건 다른 포스트에서 다룰 예정이며 이곳에서는 캐글 데이터셋을 통해 위 기법들에 대한 실습을 진행한다.

2. Data

import numpy as np
import scipy
import pandas as pd
import math
import random
import sklearn
from nltk.corpus import stopwords
from scipy.sparse import csr_matrix
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from scipy.sparse.linalg import svds
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
  1. shared_articles.csv

    이 데이터는 플랫폼에 기재된 기사들이며, 각 기사에는 날짜(timestamp), 원본 URL, 제목, 일반 텍스트의 내용, 기사의 언어(포르투갈어: pt 또는 영어: en) 및 기사를 등록한 사용자(작성자)에 대한 정보가 있다. timestamp는 아래 두 가지 이벤트 유형을 가지고 있다. 정확한 시나리오는 주어진 시간에 사용할 수 있는 기사만 추천을 하는 것이지만, 이번 실습에서는 단순성을 위해 “CONTENT SHARED” 이벤트 유형만 고려한다.

    • CONTENT SHARED: 플랫폼에 공유됨
    • CONTENT REMOVED: 플랫폼에서 제거됨
articles_df = pd.read_csv('./data/shared_articles.csv').loc[lambda d: d['eventType'] == 'CONTENT SHARED']
articles_df.head()
timestamp eventType contentId authorPersonId authorSessionId authorUserAgent authorRegion authorCountry contentType url title text lang
1 1459193988 CONTENT SHARED -4110354420726924665 4340306774493623681 8940341205206233829 NaN NaN NaN HTML http://www.nytimes.com/2016/03/28/business/dea... Ethereum, a Virtual Currency, Enables Transact... All of this work is still very early. The firs... en
2 1459194146 CONTENT SHARED -7292285110016212249 4340306774493623681 8940341205206233829 NaN NaN NaN HTML http://cointelegraph.com/news/bitcoin-future-w... Bitcoin Future: When GBPcoin of Branson Wins O... The alarm clock wakes me at 8:00 with stream o... en
3 1459194474 CONTENT SHARED -6151852268067518688 3891637997717104548 -1457532940883382585 NaN NaN NaN HTML https://cloudplatform.googleblog.com/2016/03/G... Google Data Center 360° Tour We're excited to share the Google Data Center ... en
4 1459194497 CONTENT SHARED 2448026894306402386 4340306774493623681 8940341205206233829 NaN NaN NaN HTML https://bitcoinmagazine.com/articles/ibm-wants... IBM Wants to "Evolve the Internet" With Blockc... The Aite Group projects the blockchain market ... en
5 1459194522 CONTENT SHARED -2826566343807132236 4340306774493623681 8940341205206233829 NaN NaN NaN HTML http://www.coindesk.com/ieee-blockchain-oxford... IEEE to Talk Blockchain at Cloud Computing Oxf... One of the largest and oldest organizations fo... en
  1. users_interactions.csv

    기사에 대한 사용자 상호작용 로그이며, contentId 컬럼을 통해 articles_shared 데이터와 조인할 수 있다. 상호 작용에 대한 유형(eventType)은 아래와 같다.

    • VIEW: 사용자가 문서를 열었습니다.
    • LIKE: 사용자는 그 기사를 좋아했습니다.
    • COMMENT CREATED: 사용자가 기사에 주석을 작성했습니다.
    • FOLLOW: 사용자는 기사의 새로운 댓글에 대해 알림을 받기로 선택했습니다.
    • BOOKMARK: 나중에 쉽게 반환할 수 있도록 사용자가 문서에 책갈피를 추가했습니다.
interactions_df = pd.read_csv('./data/users_interactions.csv')
interactions_df.head()
timestamp eventType contentId personId sessionId userAgent userRegion userCountry
0 1465413032 VIEW -3499919498720038879 -8845298781299428018 1264196770339959068 NaN NaN NaN
1 1465412560 VIEW 8890720798209849691 -1032019229384696495 3621737643587579081 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2... NY US
2 1465416190 VIEW 310515487419366995 -1130272294246983140 2631864456530402479 NaN NaN NaN
3 1465413895 FOLLOW 310515487419366995 344280948527967603 -3167637573980064150 NaN NaN NaN
4 1465412290 VIEW -7820640624231356730 -445337111692715325 5611481178424124714 NaN NaN NaN

3. Preprocessing

3.1 Weight

데이터에서 확인하였듯 상호 작용 유형이 다르기 때문에 가중치를 통해 유형별 관심의 정도를 설정한다.

event_type_strength = {
   'VIEW': 1.0,
   'LIKE': 2.0, 
   'BOOKMARK': 2.5, 
   'FOLLOW': 3.0,
   'COMMENT CREATED': 4.0,  
}

interactions_df['eventStrength'] = interactions_df['eventType'].apply(lambda x: event_type_strength[x])

3.2 Cold-start

추천 시스템은 사용자 cold-start 문제가 있다. 이는 소비한 아이템의 수가 없거나 매우 적은 경우 사용자의 선호도 모델링을 위해 필요한 정보들이 부족하여 개인화된 추천을 제공하기 어려운 경우이다. 따라서 이번 실습에서는 기사와 5개 이상의 상호 작용이 있는 사용자만 추천을 제공하고자 한다.

users_interactions_count_df = interactions_df.groupby(['personId', 'contentId']).size().groupby('personId').size()
print('# 사용자 수: %d' % len(users_interactions_count_df))
users_with_enough_interactions_df = users_interactions_count_df[users_interactions_count_df >= 5].reset_index()[['personId']]
print('# 최소 5번의 상호작용을 한 사용자: %d' % len(users_with_enough_interactions_df))
# 사용자 수: 1895
# 최소 5번의 상호작용을 한 사용자: 1140
print('# 상호작용 수: %d' % len(interactions_df))
interactions_from_selected_users_df = interactions_df.merge(users_with_enough_interactions_df, 
               how = 'right',
               left_on = 'personId',
               right_on = 'personId')
print('# 최소 5번의 상호작용을 한 사용자들이 수행한 상호작용 수: %d' % len(interactions_from_selected_users_df))
# 상호작용 수: 72312
# 최소 5번의 상호작용을 한 사용자들이 수행한 상호작용 수: 69868

3.3 Aggregation

사용자는 기사를 여러번 볼 수 있고, 좋아요나 댓글을 다는 상호작용 또한 동시에 수행할 수 있다. 따라서 사용자가 기사에서 행한 모든 상호작용의 가중치를 모두 합하는 집계 작업을 수행한다. 이후 집계된 데이터의 분포를 부드럽게 하기 위해 로그 변환을 적용한다.

# 로그변환 함수
def smooth_user_preference(x):
    return math.log(1+x, 2)
    
interactions_full_df = interactions_from_selected_users_df \
                    .groupby(['personId', 'contentId'])['eventStrength'].sum() \
                    .apply(smooth_user_preference).reset_index()
print('# of unique user/item interactions: %d' % len(interactions_full_df))
interactions_full_df.head(10)
# of unique user/item interactions: 39106
personId contentId eventStrength
0 -9223121837663643404 -8949113594875411859 1.000000
1 -9223121837663643404 -8377626164558006982 1.000000
2 -9223121837663643404 -8208801367848627943 1.000000
3 -9223121837663643404 -8187220755213888616 1.000000
4 -9223121837663643404 -7423191370472335463 3.169925
5 -9223121837663643404 -7331393944609614247 1.000000
6 -9223121837663643404 -6872546942144599345 1.000000
7 -9223121837663643404 -6728844082024523434 1.000000
8 -9223121837663643404 -6590819806697898649 1.000000
9 -9223121837663643404 -6558712014192834002 1.584963

4. Evaluation

4.1 Data split

평가의 키 포인트는 cross-validation을 통해 학습된 모델을 학습되지 않은 데이터에 대해 일반화 시키는 것이다. 이번 실습에서는 데이터의 20%를 test에 사용한 holdout 방식의 cross-validation을 수행한다. 더 정확한 평가를 위해 날짜 기준으로 구분을 해야 하나, 단순성을 위해 랜덤하게 추출하는 방식을 사용한다.

interactions_train_df, interactions_test_df = train_test_split(interactions_full_df,
                                   stratify=interactions_full_df['personId'], 
                                   test_size=0.20,
                                   random_state=42)

print('# Train data의 상호작용 수: %d' % len(interactions_train_df))
print('# Test data의 상호작용 수: %d' % len(interactions_test_df))
# Train data의 상호작용 수: 31284
# Test data의 상호작용 수: 7822

4.2 Metric

추천 시스템을 위한 metric은 해당 포스트를 참고하며, 이번 실습에서는 Top-N에 대한 정확도를 평가하는 Recall@N을 사용한다.

4.3 Evaluation

평가는 다음과 같이 수행된다.

  • 각 사용자에 대해
    • test data에서 사용자가 상호작용한 각 아이템에 대해
      • 사용자가 상호작용한 적 없는 100개의 다른 아이템들을 샘플링함
      • 위에서 선택한 1개의 상호작용한 아이템과 샘플링한 100개의 상호작용한 적 없는(non-relevant) 아이템을 사용해 추천 순위 리스트를 생성함
      • 추천 순위 리스트에서 Top-N에 대한 metric을 수행함
  • 모든 metric에 대한 집계 수행

Recall@N은 101개 추천 순위 리스트에서 상호작용한 1개의 아이템이 Top-N에 속하는지 여부를 평가한다.

# 평가 속도를 빠르게 하기 위한 personId 인덱싱 작업
# 각 DataFrame의 인덱스가 personId가 되게 함
interactions_full_indexed_df = interactions_full_df.set_index('personId')
interactions_train_indexed_df = interactions_train_df.set_index('personId')
interactions_test_indexed_df = interactions_test_df.set_index('personId')
def get_items_interacted(person_id, interactions_df):
    # 사용가 상호작용한 아이템들을 구함
    # set을 쓰는 이유는 향후 집합 연산을 수행하기 때문
    interacted_items = interactions_df.loc[person_id]['contentId']
    return set(interacted_items if type(interacted_items) == pd.Series else [interacted_items])
# 샘플링할 상호작용 하지 않은 아이템 수
EVAL_RANDOM_SAMPLE_NON_INTERACTED_ITEMS = 100

class ModelEvaluator:


    def get_not_interacted_items_sample(self, person_id, sample_size, seed=42):
        interacted_items = get_items_interacted(person_id, interactions_full_indexed_df)
        all_items = set(articles_df['contentId'])
        # 모든 아이템 집합과 상호작용한 아이템 집합의 차집합
        non_interacted_items = all_items - interacted_items

        random.seed(seed)
        # 상호작용하지 않은 아이템에서 sample_size만큼 샘플링
        non_interacted_items_sample = random.sample(non_interacted_items, sample_size)
        return set(non_interacted_items_sample)

    def _verify_hit_top_n(self, item_id, recommended_items, topn):        
            try:
                # item_id가 recommended_items의 몇번째에 위치하고 있는지(index) 구함
                index = next(i for i, c in enumerate(recommended_items) if c == item_id)
            except:
                index = -1
            # index가 topn의 범위 안에 속하면 hit(=1) 아니면 not hit(=0)
            hit = int(index in range(0, topn))
            return hit, index

    def evaluate_model_for_user(self, model, person_id):
        # test data에서 아이템을 추출
        interacted_values_testset = interactions_test_indexed_df.loc[person_id]
        if type(interacted_values_testset['contentId']) == pd.Series:
            person_interacted_items_testset = set(interacted_values_testset['contentId'])
        else:
            person_interacted_items_testset = set([int(interacted_values_testset['contentId'])])
        # TODO
        # person_interacted_items_testset = get_items_interacted(person_id, interactions_test_indexed_df)
        interacted_items_count_testset = len(person_interacted_items_testset) 

        # 모델로 추천 순위 리스트를 생성함
        person_recs_df = model.recommend_items(person_id, 
                                               items_to_ignore=get_items_interacted(person_id, 
                                                                                    interactions_train_indexed_df), 
                                               topn=10000000000)

        hits_at_5_count = 0
        hits_at_10_count = 0
        
        # test data에서 사용자가 상호작용한 각 아이템에 대해
        for item_id in person_interacted_items_testset:
            # 사용자가 상호작용한 적 없는 100개의 다른 아이템들을 샘플링함
            non_interacted_items_sample = self.get_not_interacted_items_sample(person_id, 
                                                                          sample_size=EVAL_RANDOM_SAMPLE_NON_INTERACTED_ITEMS, 
                                                                          seed=item_id%(2**32))

            # 위에서 선택한 1개의 상호작용한 아이템과 샘플링한 100개의 상호작용한 적 없는(non-relevant) 아이템을 사용해 추천 순위 리스트를 생성함
            items_to_filter_recs = non_interacted_items_sample.union(set([item_id]))

            # 모델로 생성한 추천 순위 리스트에서 샘플링을 통한 추천 리스트들을 필터링
            valid_recs_df = person_recs_df[person_recs_df['contentId'].isin(items_to_filter_recs)]                    
            valid_recs = valid_recs_df['contentId'].values

            # 현재 아이템이 추천 순위 리스트의 상위 N개에 포함되는지 확인
            hit_at_5, index_at_5 = self._verify_hit_top_n(item_id, valid_recs, 5)
            hits_at_5_count += hit_at_5
            hit_at_10, index_at_10 = self._verify_hit_top_n(item_id, valid_recs, 10)
            hits_at_10_count += hit_at_10

        # 추천 순위 리스트에서 Top-N에 대한 metric을 수행함
        recall_at_5 = hits_at_5_count / float(interacted_items_count_testset)
        recall_at_10 = hits_at_10_count / float(interacted_items_count_testset)

        person_metrics = {'hits@5_count':hits_at_5_count, 
                          'hits@10_count':hits_at_10_count, 
                          'interacted_count': interacted_items_count_testset,
                          'recall@5': recall_at_5,
                          'recall@10': recall_at_10}
        return person_metrics

    def evaluate_model(self, model):
        people_metrics = []
        for idx, person_id in enumerate(list(interactions_test_indexed_df.index.unique().values)):
            person_metrics = self.evaluate_model_for_user(model, person_id)  
            person_metrics['_person_id'] = person_id
            people_metrics.append(person_metrics)
        print('%d 명의 사용자 평가 수행' % idx)

        detailed_results_df = pd.DataFrame(people_metrics) \
                            .sort_values('interacted_count', ascending=False)
        
        # 모든 metric에 대한 집계 수행
        global_recall_at_5 = detailed_results_df['hits@5_count'].sum() / float(detailed_results_df['interacted_count'].sum())
        global_recall_at_10 = detailed_results_df['hits@10_count'].sum() / float(detailed_results_df['interacted_count'].sum())
        
        global_metrics = {'modelName': model.get_model_name(),
                          'recall@5': global_recall_at_5,
                          'recall@10': global_recall_at_10}    
        return global_metrics, detailed_results_df
    
model_evaluator = ModelEvaluator() 

5. Modeling

5.1 Popularity model

Popularity model은 개인화된 추천이 아니라 사용자가 이전에 소비하지 않은 것 중 가장 인기 있는 아이템을 사용자에게 추천할 뿐이다. 이러한 방식은 군중 심리를 포함하고 있기 때문에 때로는 좋은 추천을 제공하기도 한다.

# 사용자 상호작용의 가중합이 높은 순서로 정렬
item_popularity_df = interactions_full_df.groupby('contentId')['eventStrength'].sum().sort_values(ascending=False).reset_index()
item_popularity_df.head(10)
contentId eventStrength
0 -4029704725707465084 307.733799
1 -6783772548752091658 233.762157
2 -133139342397538859 228.024567
3 -8208801367848627943 197.107608
4 -6843047699859121724 193.825208
5 8224860111193157980 189.044680
6 -2358756719610361882 183.110951
7 2581138407738454418 180.282876
8 7507067965574797372 179.094002
9 1469580151036142903 170.548969
class PopularityRecommender:
    
    MODEL_NAME = 'Popularity'
    
    def __init__(self, popularity_df, items_df=None):
        self.popularity_df = popularity_df
        self.items_df = items_df
        
    def get_model_name(self):
        return self.MODEL_NAME
        
    def recommend_items(self, user_id, items_to_ignore=[], topn=10, verbose=False):
        # 사용자가 보지 않은 아이템 중 인기도(가중합) 순으로 높은 것을 추천
        recommendations_df = self.popularity_df[~self.popularity_df['contentId'].isin(items_to_ignore)] \
                               .sort_values('eventStrength', ascending = False) \
                               .head(topn)

        if verbose:
            if self.items_df is None:
                raise Exception('"items_df" is required in verbose mode')

            recommendations_df = recommendations_df.merge(self.items_df, how = 'left', 
                                                          left_on = 'contentId', 
                                                          right_on = 'contentId')[['eventStrength', 'contentId', 'title', 'url', 'lang']]


        return recommendations_df
    
popularity_model = PopularityRecommender(item_popularity_df, articles_df)
print('Evaluating Popularity recommendation model...')
pop_global_metrics, pop_detailed_results_df = model_evaluator.evaluate_model(popularity_model)
print('\nGlobal metrics:\n%s' % pop_global_metrics)
pop_detailed_results_df.head(10)
Evaluating Popularity recommendation model...
1139 명의 사용자 평가 수행

Global metrics:
{'modelName': 'Popularity', 'recall@5': 0.2418818716440808, 'recall@10': 0.3725389925850166}
hits@5_count hits@10_count interacted_count recall@5 recall@10 _person_id
76 28 50 192 0.145833 0.260417 3609194402293569455
17 12 25 134 0.089552 0.186567 -2626634673110551643
16 13 23 130 0.100000 0.176923 -1032019229384696495
10 5 9 117 0.042735 0.076923 -1443636648652872475
82 26 40 88 0.295455 0.454545 -2979881261169775358
161 12 18 80 0.150000 0.225000 -3596626804281480007
65 20 34 73 0.273973 0.465753 1116121227607581999
81 17 23 69 0.246377 0.333333 692689608292948411
106 14 18 69 0.202899 0.260870 -9016528795238256703
52 21 28 68 0.308824 0.411765 3636910968448833585

5.2 Content-Based Filtering model

Content-based filtering 방식은 사용자가 상호 작용한 아이템의 profile을 활용하여 유사한 아이템을 추천한다. (단, 이는 사용자의 이전 선택에만 의존하므로 cold-start 문제 방지를 위한 기법들이 필요하다.) 기사, 뉴스 및 책과 같은 텍스트 항목의 경우 자연어 처리 기법을 사용하여 아이템 및 사용자의 profile을 정의할 수 있다. 이번 실습에서는 자연어 처리의 기법 중 하나인 TF-IDF를 사용한다. 이는 간단히 말해 보유한 데이터를 모든 문서 대 모든 단어의 matrix를 만들어 각 단어가 문서에서 얼마나 관련이 있는지를 나타내는 것이다.

# 영어, 포르투칼어의 불용어를 제거함
stopwords_list = stopwords.words('english') + stopwords.words('portuguese')

# TfidfVectorizer 모델 생성
vectorizer = TfidfVectorizer(analyzer='word',
                     ngram_range=(1, 2), # unigram or bigram
                     min_df=0.003,
                     max_df=0.5,
                     max_features=5000,
                     stop_words=stopwords_list)

# 기사 리스트 생성
item_ids = articles_df['contentId'].tolist()
# 현재 기사들에 대해 TfidfVectorizer 모델 학습
tfidf_matrix = vectorizer.fit_transform(articles_df['title'] + "" + articles_df['text'])
# unigram or bigram의 토큰 리스트 생성
tfidf_feature_names = vectorizer.get_feature_names()
tfidf_matrix
<3047x5000 sparse matrix of type '<class 'numpy.float64'>'
	with 638118 stored elements in Compressed Sparse Row format>

사용자 profile은 사용자가 상호 작용한 모든 아이템들의 profile들의 평균을 통해 구한다. 이 때, 각 상호작용의 강도에 따라 가중치를 부여하여 평균을 구한다.

def get_item_profile(item_id):
    idx = item_ids.index(item_id)
    item_profile = tfidf_matrix[idx:idx+1]
    return item_profile

def get_item_profiles(ids):
    item_profiles_list = [get_item_profile(x) for x in ids]
    item_profiles = scipy.sparse.vstack(item_profiles_list)
    return item_profiles

def build_users_profile(person_id, interactions_indexed_df):
    interactions_person_df = interactions_indexed_df.loc[person_id]
    user_item_profiles = get_item_profiles(interactions_person_df['contentId'])
    
    user_item_strengths = np.array(interactions_person_df['eventStrength']).reshape(-1,1)
    # 가중치를 적용한 상호작용 강도의 평균을 구한 뒤 normalize를 진행한다 (단위 벡터로 만들어주기 위함)
    user_item_strengths_weighted_avg = np.sum(user_item_profiles.multiply(user_item_strengths), axis=0) / np.sum(user_item_strengths)
    user_profile_norm = sklearn.preprocessing.normalize(user_item_strengths_weighted_avg)
    return user_profile_norm

def build_users_profiles(): 
    interactions_indexed_df = interactions_train_df[interactions_train_df['contentId'] \
                                                   .isin(articles_df['contentId'])].set_index('personId')
    user_profiles = {}
    for person_id in interactions_indexed_df.index.unique():
        user_profiles[person_id] = build_users_profile(person_id, interactions_indexed_df)
    return user_profiles

user_profiles = build_users_profiles()
len(user_profiles)
1140

profile은 길이가 5000인 단위 벡터이며, 각 위치의 값은 토큰(unigram 혹은 bigram)이 나에게 얼마나 관련있는지를 나타낸다. 아래 예시를 보면 사용자가 관심있는 내용이 유사한 형태로 나타남을 볼 수 있다.

myprofile = user_profiles[-1479311724257856983]
print(myprofile.shape)
pd.DataFrame(sorted(zip(tfidf_feature_names, 
                        user_profiles[-1479311724257856983].flatten().tolist()), key=lambda x: -x[1])[:20],
             columns=['token', 'relevance'])
(1, 5000)
token relevance
0 learning 0.298835
1 machine learning 0.246087
2 machine 0.237933
3 google 0.203117
4 data 0.169849
5 ai 0.156280
6 algorithms 0.115706
7 like 0.097790
8 language 0.087673
9 people 0.082061
10 deep 0.081565
11 deep learning 0.081005
12 research 0.076052
13 algorithm 0.074934
14 apple 0.074144
15 intelligence 0.072694
16 use 0.072631
17 human 0.072517
18 models 0.072435
19 artificial 0.072179
class ContentBasedRecommender:
    
    MODEL_NAME = 'Content-Based'
    
    def __init__(self, items_df=None):
        self.item_ids = item_ids
        self.items_df = items_df
        
    def get_model_name(self):
        return self.MODEL_NAME
        
    def _get_similar_items_to_user_profile(self, person_id, topn=1000):
        # 모든 아이템 profile들과 사용자 profile의 cosine 유사도를 계산함
        cosine_similarities = cosine_similarity(user_profiles[person_id], tfidf_matrix)
        # 가장 유사한 topn의 아이템 선정
        similar_indices = cosine_similarities.argsort().flatten()[-topn:]
        similar_items = sorted([(item_ids[i], cosine_similarities[0,i]) for i in similar_indices], key=lambda x: -x[1])
        return similar_items
        
    def recommend_items(self, user_id, items_to_ignore=[], topn=10, verbose=False):
        similar_items = self._get_similar_items_to_user_profile(user_id)
        similar_items_filtered = list(filter(lambda x: x[0] not in items_to_ignore, similar_items))
        
        recommendations_df = pd.DataFrame(similar_items_filtered, columns=['contentId', 'recStrength']) \
                                    .head(topn)

        if verbose:
            if self.items_df is None:
                raise Exception('"items_df" is required in verbose mode')

            recommendations_df = recommendations_df.merge(self.items_df, how = 'left', 
                                                          left_on = 'contentId', 
                                                          right_on = 'contentId')[['recStrength', 'contentId', 'title', 'url', 'lang']]


        return recommendations_df
    
content_based_recommender_model = ContentBasedRecommender(articles_df)
print('Evaluating Content-Based Filtering model...')
cb_global_metrics, cb_detailed_results_df = model_evaluator.evaluate_model(content_based_recommender_model)
print('\nGlobal metrics:\n%s' % cb_global_metrics)
cb_detailed_results_df.head(10)
Evaluating Content-Based Filtering model...
1139 명의 사용자 평가 수행

Global metrics:
{'modelName': 'Content-Based', 'recall@5': 0.16338532344668882, 'recall@10': 0.26118639734083354}
hits@5_count hits@10_count interacted_count recall@5 recall@10 _person_id
76 15 24 192 0.078125 0.125000 3609194402293569455
17 18 29 134 0.134328 0.216418 -2626634673110551643
16 20 33 130 0.153846 0.253846 -1032019229384696495
10 32 46 117 0.273504 0.393162 -1443636648652872475
82 6 15 88 0.068182 0.170455 -2979881261169775358
161 11 23 80 0.137500 0.287500 -3596626804281480007
65 8 13 73 0.109589 0.178082 1116121227607581999
81 8 19 69 0.115942 0.275362 692689608292948411
106 3 9 69 0.043478 0.130435 -9016528795238256703
52 3 8 68 0.044118 0.117647 3636910968448833585

5.3 Collaborative Filtering model

Collaborative Filtering (CF)은 아래와 같은 두 가지 구현 방식이 있다.

  1. Memory-based
    이 방법은 사용자가 상호 작용한 아이템을 기반으로 사용자 유사성을 계산하거나 사용자와 상호 작용한 사용자를 기반으로 아이템 유사성을 계산한다. 대표적인 예로는 User Neighbourhood-based CF로, 사용자에 대한 top-N의 유사한 사용자(일반적으로 Pearson 상관관계를 사용하여 계산)를 선택하여 유사한 사용자가 좋아하는 아이템을 추천하는 데 사용된다. 이는 구현하기 간단하지만 일반적으로 많은 사용자가 많아질 경우 확장하기 어렵다는 단점이 있다.

  2. Model-based ★★
    다양한 기계 학습 알고리즘을 사용하여 사용자에게 아이템을 추천하는 모델을 개발한다. neural network, bayesian network, clustering model과 같은 모델 기반 CF 알고리즘과 Singular Value Decomposition(SVD) 및 probabilistic latent semantic analysis와 같은 latent factor model이 있다.

5.4 Matrix Factorization(MF)

Latent factor model을 사용하는 MF는 user-item matrix를 저차원의 표현(latent factor)으로 압축한다. 이러한 방식은 많은 결측값을 포함하는 고차원 matrix가 아닌 저차원의 작은 matrix를 다루기 때문에 memory-based에서 사용하는 matrix 보다 희소성 문제를 잘 처리한다는 장점이 있다. 또한 축소된 matrix를 통해 유사도 비교를 하기 떄문에 확장성이 좋아 대규모의 희소 데이터 세트를 다룸에 있어 강점이 있다. 이번 실습에서는 다양한 Latent factor model 중 SVD를 사용한다. Movie dataset SVD 적용에 대한 예시는 이 글을 참조한다.

SVD를 수행함에 있어 중요한 것은 user-item matrix의 factor(차원)의 수를 결정하는 것이다. factor가 많아질 수록 matrix를 분해한 결과가 원본에 가까워질 것이다. 이러한 경우 모델이 학습하지 않은 데이터들에 대해 일반화시키지 못하는 과적합 현상이 발생할 수 있다. 따라서 요인의 개수를 적절히 줄이는 것이 모델의 일반화에 도움이 될 수 있다.

# pivot을 통해 사용자 대 아이템(user-item) matrix를 만들고 빈값(사용자가 상호작용하지 않은 아이템)은 0으로 채운다
users_items_pivot_matrix_df = interactions_train_df.pivot(index='personId', 
                                                          columns='contentId', 
                                                          values='eventStrength').fillna(0)

users_items_pivot_matrix_df.head(10)
contentId -9222795471790223670 -9216926795620865886 -9194572880052200111 -9192549002213406534 -9190737901804729417 -9189659052158407108 -9176143510534135851 -9172673334835262304 -9171475473795142532 -9166778629773133902 ... 9191014301634017491 9207286802575546269 9208127165664287660 9209629151177723638 9209886322932807692 9213260650272029784 9215261273565326920 9217155070834564627 9220445660318725468 9222265156747237864
personId
-9223121837663643404 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9212075797126931087 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9207251133131336884 0.0 2.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9199575329909162940 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9196668942822132778 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9188188261933657343 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9172914609055320039 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9156344805277471150 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9120685872592674274 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
-9109785559521267180 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

10 rows × 2926 columns

users_items_pivot_matrix = users_items_pivot_matrix_df.values
users_items_pivot_matrix[:2]
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]])
users_ids = list(users_items_pivot_matrix_df.index)
users_ids[:10]
[-9223121837663643404,
 -9212075797126931087,
 -9207251133131336884,
 -9199575329909162940,
 -9196668942822132778,
 -9188188261933657343,
 -9172914609055320039,
 -9156344805277471150,
 -9120685872592674274,
 -9109785559521267180]

만들어진 사용자 대 아이템 matrix는 대부분이 0으로 된 sparse한 matrix이다. 이는 계산시 불필요한 메모리가 많이 소요되기 때문에 CSR 또는 COO 변환이 필요하다. 이러한 기법에 대한 내용은 COOCSR를 통해 확인하며, 이번 실습에서는 CSR 방식을 사용한다.

users_items_pivot_sparse_matrix = csr_matrix(users_items_pivot_matrix)
users_items_pivot_sparse_matrix
<1140x2926 sparse matrix of type '<class 'numpy.float64'>'
	with 31284 stored elements in Compressed Sparse Row format>
# user-item matrix의 factor를 정함
NUMBER_OF_FACTORS_MF = 15
# user-item matrix를 SVD를 사용해 분해
U, sigma, Vt = svds(users_items_pivot_sparse_matrix, k = NUMBER_OF_FACTORS_MF)
U.shape
(1140, 15)
Vt.shape
(15, 2926)
sigma = np.diag(sigma)
sigma.shape
(15, 15)

분해된 matrix의 요소 U, sigma, Vt를 다시 곱하여 원래의 matrix의 크기로 복원한 결과는 더이상 sparse 하지 않게 된다. 이는 사용자가 상호작용 하지 않은 아이템에 대한 예측을 생성한 것과 같으며 이를 추천하는 아이템으로 활용한다.

all_user_predicted_ratings = np.dot(np.dot(U, sigma), Vt) 
all_user_predicted_ratings
array([[ 0.01039915,  0.00081872, -0.01725263, ...,  0.00140708,
         0.0110647 ,  0.00226063],
       [-0.00019285, -0.00031318, -0.00264624, ...,  0.00251658,
         0.00017609, -0.00189488],
       [-0.01254721,  0.0065947 , -0.00590676, ...,  0.00698975,
        -0.01015696,  0.01154572],
       ...,
       [-0.02995379,  0.00805715, -0.01846307, ..., -0.01083078,
        -0.00118591,  0.0096798 ],
       [-0.01845505,  0.00467019,  0.01219602, ...,  0.00409507,
         0.00019482, -0.00752562],
       [-0.01506374,  0.00327732,  0.13391269, ..., -0.01191815,
         0.06422074,  0.01303244]])
print("행:", len(all_user_predicted_ratings))
print("열:", len(all_user_predicted_ratings[0]))
행: 1140
열: 2926
# matrix 정규화
all_user_predicted_ratings_norm = (all_user_predicted_ratings - all_user_predicted_ratings.min()) / \
                                    (all_user_predicted_ratings.max() - all_user_predicted_ratings.min())
# Dataframe(item-user matrix)으로 변환
cf_preds_df = pd.DataFrame(all_user_predicted_ratings_norm, columns = users_items_pivot_matrix_df.columns, index=users_ids).transpose()
cf_preds_df.head(10)
-9223121837663643404 -9212075797126931087 -9207251133131336884 -9199575329909162940 -9196668942822132778 -9188188261933657343 -9172914609055320039 -9156344805277471150 -9120685872592674274 -9109785559521267180 ... 9105269044962898535 9109075639526981934 9135582630122950040 9137372837662939523 9148269800512008413 9165571805999894845 9187866633451383747 9191849144618614467 9199170757466086545 9210530975708218054
contentId
-9222795471790223670 0.139129 0.137930 0.136531 0.143948 0.136815 0.137339 0.137508 0.143534 0.136428 0.135681 ... 0.137351 0.127822 0.137946 0.139653 0.140324 0.136888 0.135787 0.134560 0.135862 0.136246
-9216926795620865886 0.138044 0.137916 0.138698 0.137878 0.137969 0.137990 0.137974 0.138049 0.138217 0.138151 ... 0.137962 0.139527 0.138009 0.138117 0.139634 0.138058 0.138222 0.138864 0.138480 0.138323
-9194572880052200111 0.135998 0.137652 0.137283 0.137536 0.140363 0.137807 0.141246 0.136284 0.135301 0.138512 ... 0.139257 0.143161 0.139139 0.140077 0.154976 0.140109 0.140654 0.135861 0.139332 0.153114
-9192549002213406534 0.141924 0.137996 0.134663 0.137080 0.139946 0.138574 0.139473 0.144469 0.143333 0.138428 ... 0.140233 0.167426 0.138849 0.137037 0.141820 0.139260 0.139513 0.136804 0.140862 0.148793
-9190737901804729417 0.140209 0.137408 0.138708 0.138672 0.137725 0.138218 0.138390 0.138418 0.134883 0.140193 ... 0.138373 0.138459 0.138169 0.137990 0.134041 0.137820 0.138100 0.138286 0.138630 0.136178
-9189659052158407108 0.138932 0.138699 0.138117 0.137621 0.138920 0.137766 0.138568 0.138200 0.140572 0.140365 ... 0.140725 0.148152 0.137645 0.138165 0.149152 0.138912 0.139595 0.139807 0.140419 0.145698
-9176143510534135851 0.143208 0.138673 0.139514 0.139114 0.137664 0.137447 0.139833 0.140564 0.144698 0.144440 ... 0.138367 0.146220 0.136204 0.138087 0.137317 0.137917 0.138546 0.142601 0.141431 0.142154
-9172673334835262304 0.138527 0.138021 0.138274 0.137827 0.137997 0.138037 0.138104 0.138259 0.137633 0.138397 ... 0.138588 0.140146 0.138013 0.137839 0.137033 0.137969 0.138337 0.138361 0.138813 0.137538
-9171475473795142532 0.140720 0.137865 0.138061 0.137633 0.138231 0.138089 0.139009 0.137552 0.137143 0.140581 ... 0.139046 0.139895 0.138000 0.137958 0.136061 0.138183 0.138817 0.138060 0.139205 0.137198
-9166778629773133902 0.138989 0.137725 0.136520 0.137723 0.138559 0.137951 0.138189 0.138496 0.139470 0.137546 ... 0.138233 0.144002 0.138050 0.137533 0.139036 0.138399 0.138330 0.137148 0.138027 0.140283

10 rows × 1140 columns

class CFRecommender:
    
    MODEL_NAME = 'Collaborative Filtering'
    
    def __init__(self, cf_predictions_df, items_df=None):
        self.cf_predictions_df = cf_predictions_df
        self.items_df = items_df
        
    def get_model_name(self):
        return self.MODEL_NAME
        
    def recommend_items(self, user_id, items_to_ignore=[], topn=10, verbose=False):
        sorted_user_predictions = self.cf_predictions_df[user_id].sort_values(ascending=False) \
                                    .reset_index().rename(columns={user_id: 'recStrength'})

        recommendations_df = sorted_user_predictions[~sorted_user_predictions['contentId'].isin(items_to_ignore)] \
                               .sort_values('recStrength', ascending = False) \
                               .head(topn)

        if verbose:
            if self.items_df is None:
                raise Exception('"items_df" is required in verbose mode')

            recommendations_df = recommendations_df.merge(self.items_df, how = 'left', 
                                                          left_on = 'contentId', 
                                                          right_on = 'contentId')[['recStrength', 'contentId', 'title', 'url', 'lang']]


        return recommendations_df
    
cf_recommender_model = CFRecommender(cf_preds_df, articles_df)
print('Evaluating Collaborative Filtering (SVD Matrix Factorization) model...')
cf_global_metrics, cf_detailed_results_df = model_evaluator.evaluate_model(cf_recommender_model)
print('\nGlobal metrics:\n%s' % cf_global_metrics)
cf_detailed_results_df.head(10)
Evaluating Collaborative Filtering (SVD Matrix Factorization) model...
1139 명의 사용자 평가 수행

Global metrics:
{'modelName': 'Collaborative Filtering', 'recall@5': 0.33392994119151115, 'recall@10': 0.46803886474047557}
hits@5_count hits@10_count interacted_count recall@5 recall@10 _person_id
76 21 46 192 0.109375 0.239583 3609194402293569455
17 30 56 134 0.223881 0.417910 -2626634673110551643
16 16 34 130 0.123077 0.261538 -1032019229384696495
10 38 51 117 0.324786 0.435897 -1443636648652872475
82 39 48 88 0.443182 0.545455 -2979881261169775358
161 22 34 80 0.275000 0.425000 -3596626804281480007
65 24 32 73 0.328767 0.438356 1116121227607581999
81 16 21 69 0.231884 0.304348 692689608292948411
106 20 28 69 0.289855 0.405797 -9016528795238256703
52 23 30 68 0.338235 0.441176 3636910968448833585

5.5 Hybrid Recommender

Collaborative Filtering과 Content-Based Filtering을 결합한 Hybrid Recommender는 개별 방식들보다 우수한 성능을 보임을 증명하였고, 다양한 실무 영역에서 사용하고 있다. 이번 실습에서 앙상블에 대한 구현은 각 모델의 추천결과에 각 가중치를 곱해 평균을 내는 단순한 방식을 사용한다. 이때 CF 모델이 CB 모델보다 정확하므로 각각의 가중치는 100과 1로 설정하였다.

class HybridRecommender:
    
    MODEL_NAME = 'Hybrid'
    
    def __init__(self, cb_rec_model, cf_rec_model, items_df, cb_ensemble_weight=1.0, cf_ensemble_weight=1.0):
        self.cb_rec_model = cb_rec_model
        self.cf_rec_model = cf_rec_model
        self.cb_ensemble_weight = cb_ensemble_weight
        self.cf_ensemble_weight = cf_ensemble_weight
        self.items_df = items_df
        
    def get_model_name(self):
        return self.MODEL_NAME
        
    def recommend_items(self, user_id, items_to_ignore=[], topn=10, verbose=False):
        # top-1000 Content-based filtering 추천 리스트
        cb_recs_df = self.cb_rec_model.recommend_items(user_id, items_to_ignore=items_to_ignore, verbose=verbose,
                                                           topn=1000).rename(columns={'recStrength': 'recStrengthCB'})
        
        # top-1000 Collaborative filtering 추천 리스트
        cf_recs_df = self.cf_rec_model.recommend_items(user_id, items_to_ignore=items_to_ignore, verbose=verbose, 
                                                           topn=1000).rename(columns={'recStrength': 'recStrengthCF'})
        
        # 두 추천 리스트 병합
        recs_df = cb_recs_df.merge(cf_recs_df,
                                   how = 'outer', 
                                   left_on = 'contentId', 
                                   right_on = 'contentId').fillna(0.0)
        
        # 앙상블 적용
        recs_df['recStrengthHybrid'] = (recs_df['recStrengthCB'] * self.cb_ensemble_weight) \
                                     + (recs_df['recStrengthCF'] * self.cf_ensemble_weight)
        
        recommendations_df = recs_df.sort_values('recStrengthHybrid', ascending=False).head(topn)

        if verbose:
            if self.items_df is None:
                raise Exception('"items_df" is required in verbose mode')

            recommendations_df = recommendations_df.merge(self.items_df, how = 'left', 
                                                          left_on = 'contentId', 
                                                          right_on = 'contentId')[['recStrengthHybrid', 'contentId', 'title', 'url', 'lang']]


        return recommendations_df
    
hybrid_recommender_model = HybridRecommender(content_based_recommender_model, cf_recommender_model, articles_df,
                                             cb_ensemble_weight=1.0, cf_ensemble_weight=100.0)
print('Evaluating Hybrid model...')
hybrid_global_metrics, hybrid_detailed_results_df = model_evaluator.evaluate_model(hybrid_recommender_model)
print('\nGlobal metrics:\n%s' % hybrid_global_metrics)
hybrid_detailed_results_df.head(10)
Evaluating Hybrid model...
1139 명의 사용자 평가 수행

Global metrics:
{'modelName': 'Hybrid', 'recall@5': 0.3430069036052161, 'recall@10': 0.479033495269752}
hits@5_count hits@10_count interacted_count recall@5 recall@10 _person_id
76 22 45 192 0.114583 0.234375 3609194402293569455
17 31 58 134 0.231343 0.432836 -2626634673110551643
16 21 37 130 0.161538 0.284615 -1032019229384696495
10 40 51 117 0.341880 0.435897 -1443636648652872475
82 38 50 88 0.431818 0.568182 -2979881261169775358
161 23 35 80 0.287500 0.437500 -3596626804281480007
65 23 32 73 0.315068 0.438356 1116121227607581999
81 16 21 69 0.231884 0.304348 692689608292948411
106 20 27 69 0.289855 0.391304 -9016528795238256703
52 22 29 68 0.323529 0.426471 3636910968448833585

6. Compare result

global_metrics_df = pd.DataFrame([cb_global_metrics, pop_global_metrics, cf_global_metrics, hybrid_global_metrics]) \
                        .set_index('modelName')
global_metrics_df
recall@5 recall@10
modelName
Content-Based 0.163385 0.261186
Popularity 0.241882 0.372539
Collaborative Filtering 0.333930 0.468039
Hybrid 0.343007 0.479033
%matplotlib inline
ax = global_metrics_df.transpose().plot(kind='bar', figsize=(15,8))
for p in ax.patches:
    ax.annotate("%.3f" % p.get_height(), (p.get_x() + p.get_width() / 2., p.get_height()), ha='center', va='center', xytext=(0, 10), textcoords='offset points')

7. Test

def inspect_interactions(person_id, test_set=True):
    if test_set:
        interactions_df = interactions_test_indexed_df
    else:
        interactions_df = interactions_train_indexed_df
    return interactions_df.loc[person_id].merge(articles_df, how = 'left', 
                                                      left_on = 'contentId', 
                                                      right_on = 'contentId') \
                          .sort_values('eventStrength', ascending = False)[['eventStrength', 
                                                                          'contentId',
                                                                          'title', 'url', 'lang']]
inspect_interactions(-1479311724257856983, test_set=True)
eventStrength contentId title url lang
5 3.459432 -532999578436827210 IBM Seeks to Simplify Graph with New Titan Ser... https://www.datanami.com/2016/07/27/ibm-seeks-... en
6 3.459432 -5658245291907121574 Machine Learning and the VP Debate https://medium.com/@srobtweets/machine-learnin... en
9 3.000000 -9033211547111606164 Google's Cloud Machine Learning service is now... https://techcrunch.com/2016/09/29/googles-clou... en
17 2.584963 524776334673868069 Graph-powered Machine Learning at Google https://research.googleblog.com/2016/10/graph-... en
23 2.321928 -4127059794203205931 TPOT: A Python tool for automating data science http://www.randalolson.com/2016/05/08/tpot-a-p... en
14 2.000000 -3040610224044779845 Things you probably didn't know you could do w... https://medium.freecodecamp.com/10-tips-to-max... en
11 2.000000 -7277691357631151609 Giant-Man Goes Down in CAPTAIN AMERICA: CIVIL ... http://geektyrant.com/news/giant-man-goes-down... en
19 2.000000 -1452340812018195881 Globo G1 lança Bot para o Telegram que te ajud... http://www.diolinux.com.br/2016/10/globo-g1-la... pt
3 2.000000 -7033990154815318757 The Conversational Economy Part 1: What's Caus... http://venturebeat.com/2016/06/13/the-conversa... en
2 2.000000 -3348652277274905234 Google usa aprendizado de máquina para melhora... https://googlediscovery.com/2016/11/17/google-... pt
7 1.584963 1649085803937938638 Lilly Wachowski Makes First Public Appearance ... http://www.usmagazine.com/celebrity-news/news/... en
4 1.584963 1179017557429431126 Imagine Discovering That Your Teaching Assista... http://www.wsj.com/articles/if-your-teacher-so... en
16 1.584963 -2065362330475275026 Minha passagem no Hackathon da Globo #bbb2017 https://medium.com/@rodrigogd/minha-passagem-n... pt
26 1.584963 -7550906488404754851 Governo busca empresas para aprimorar Internet... http://www.tecmundo.com.br/internet-das-coisas... pt
27 1.000000 6016376495317032228 Uma recepção eficiente http://vocerh.uol.com.br/noticias/acervo/uma-r... pt
20 1.000000 -2118981143119783447 What readers think about computer-generated texts http://www.en.uni-muenchen.de/news/newsarchiv/... en
25 1.000000 -1537223691132357350 A arte de encantar clientes: 5 lições que apre... https://endeavor.org.br/jeito-disney-encantar-... pt
24 1.000000 -8909927294729358771 Android N Developer Preview 2, out today! | An... http://android-developers.blogspot.com.br/2016... en
22 1.000000 -8123434787655959885 Machine learning is a poor fit for most busine... http://www.infoworld.com/article/3053505/cloud... en
21 1.000000 4296604388230897073 No "Altas Horas", garoto filho de casal homoaf... https://catracalivre.com.br/geral/cidadania/in... pt
0 1.000000 7572869304086387835 The White House Has Realized Artificial Intell... http://www.popsci.com/white-house-has-realized... en
18 1.000000 -6009377241272750648 Nespresso https://www.nespresso.com/br/pt/pages/cafeteir... pt
15 1.000000 -7585525446062170144 Google launches Duo video calling app, a dull ... https://techcrunch.com/2016/08/15/google-duo/ en
1 1.000000 3472032465864014134 Pabllo Vittar reina no novo comercial da Skol ... http://www.superpride.com.br/2016/08/pabllo-vi... pt
13 1.000000 -23817016428727099 Being a Developer After 40 https://news.ycombinator.com/item?id=11569726 en
12 1.000000 6412479507689487223 Microsoft's new online certification program k... http://www.zdnet.com/article/microsofts-new-on... en
10 1.000000 -4414039190127558142 BEHOLD Amazon One - the first cargo airplane w... http://www.businessinsider.com/amazon-one-carg... en
8 1.000000 8637400873589298078 Google launches distributed version of its Ten... http://techcrunch.com/2016/04/13/google-launch... en
28 1.000000 5350829677696205474 2016 will be the year of conversational commer... https://medium.com/chris-messina/2016-will-be-... en
hybrid_recommender_model.recommend_items(-1479311724257856983, topn=5, verbose=True)
recStrengthHybrid contentId title url lang
0 25.437103 3269302169678465882 The barbell effect of machine learning. http://techcrunch.com/2016/06/02/the-barbell-e... en
1 25.369932 -8085935119790093311 Graph Capabilities with the Elastic Stack https://www.elastic.co/webinars/sneak-peek-of-... en
2 24.493428 1005751836898964351 Seria Stranger Things uma obra de arte do algo... https://www.linkedin.com/pulse/seria-stranger-... pt
3 24.383245 -8377626164558006982 Bad Writing Is Destroying Your Company's Produ... https://hbr.org/2016/09/bad-writing-is-destroy... en
4 24.362064 -6727357771678896471 This Super Accurate Portrait Selection Tech Us... http://petapixel.com/2016/06/29/super-accurate... en
test_user = inspect_interactions(-1479311724257856983, test_set=True)
test_rec = hybrid_recommender_model.recommend_items(-1479311724257856983, topn=30, verbose=True)

댓글남기기