비둘기 둥지

[ML / 추천 시스템] 2. Contents Based Filtering (CBF) 본문

인공지능 공부/추천시스템

[ML / 추천 시스템] 2. Contents Based Filtering (CBF)

KimDove 2022. 11. 4. 10:58
728x90

1. 컨텐츠 기반 필터링 (Contents Based Filtering / CBF)

1-1. 컨텐츠 기반 필터링 이란

  • 컨텐츠 기반 필터링(이하, CBF)는 사용자가 과거에 좋아했던 컨텐츠를 파악하고 비슷한 아이템을 추천해 주는 방식이다.
    e.g.) 음식을 예로 들면 어떤 유저가 배달 어플을 통해 'KFC'와 '맥도날드'에 좋은 평점을 주었다면,
             같은 햄버거 프랜차이즈인 '롯데리아'를 추천해 줄 수 있다.  
  • 유저가 좋아한 아이템을 뽑아낸 목록을 Item Profile이라 하고,
    Item Profile로부터 공통된 특징을 뽑아낸 결과를 User Profile이라고 한다.
    e.g.) Item Profile | 'KFC', '맥도날드', '버거킹', ...
            User Profile | '햄버거', '프랜차이즈', '패스트푸드', ...

1-2. CBF의 장점

  1. 다른 유저의 데이터를 필요로 하지 않는다.
  2. 개인의 취향을 고려한 추천이 가능하다.
  3. 새로운 아이템이나 대중적이지 않은 아이템도 추천할 수 있다.
  4. 사용자에게 추천하는 이유에 대해 설명할 수 있다.
    → User Profile을 뽑아낼 때 구성한 Feature들로 설명가능하다.

⚠️ 1,2,3번으로 인해 얻을 수 있는 장점은 다음과 같다.

  • 새로운 아이템이 추가되었을 때, 평가한 사람이 없어 추천이 어려운 Cold Start Problem에서 자유로워진다.
  • 개인의 평가에 기반하기 때문에 모든 유저들이 모든 아이템에 대해 평가하지 않는
    Sparsity Problem에서 자유로워 진다.
  • 아무도 평가하지 않은 새로운 아이템과 인기 없는 아이템도 해당 아이템들의 Feature들만 뽑아낼 수 있다면,
    협업 필터링 구현시 발생하는 No first-rater Problem에서 자유로워진다.

1-3. CBF의 단점

  1. Feature를 뽑아내기 어려운 데이터들이 존재한다.
    → 음악 같은 경우 장르가 너무 다양하기 때문에 공통된 Feature를 찾기 어렵다.
  2. 처음 유입된 유저에 대해 추천할 수 없다.
    → 주변 유저 평가는 영향을 받지 않지만, 아예 처음 유입된 유저는 그에 대한 데이터가 존재하지 않아 추천할 수 없다.

2. 실제 데이터 셋을 이용해 구현해보자

  • 평소에 게임을 좋아해서 캐글에 있는 Metacritic 리뷰 데이터 셋을 가져왔다.
import pandas as pd
import numpy as np
import swifter
import ast
  • csv 포맷으로 되어있는 데이터 셋을 불러온다.
  • 데이터 셋의 칼럼은 총 9개로 구성되어 있었으며, 아래처럼 구성되어 있었다.
column 설명
game_name 게임 이름
meta_score 메타 크리틱 점수
user_score 유저 점수
platform 게임 플랫폼
description 게임에 관한 내용
url 메타크리틱 페이지 내 게임 링크
developer 개발사
genre 게임 장르
type 싱글 게임인지 멀티 게임인지
rating 게임 등급
  • 이 중에서 게임 장르를 User profile로 사용하여 게임을 추천하도록 하였다.
    ⚠️ 추천할 게임의 등급은 굳이 필요없을 것 같아 게임 추천 데이터에서 제거하였다.
DATASET_PATH = '~/project/TIL/AI_study/dataset/metacritic'

csv = pd.read_csv(f'{DATASET_PATH}/games_of_all_time.csv')
csv = csv.iloc[:, :-1]
csv.head(3)
[사진 1] 위 코드의 출력 결과
csv.info()

## 출력 결과
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8831 entries, 0 to 8830
Data columns (total 9 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   game_name    8831 non-null   object 
 1   meta_score   8831 non-null   float64
 2   user_score   8831 non-null   float64
 3   platform     8831 non-null   object 
 4   description  8831 non-null   object 
 5   url          8831 non-null   object 
 6   developer    8821 non-null   object 
 7   genre        8827 non-null   object 
 8   type         6727 non-null   object 
dtypes: float64(2), object(7)
memory usage: 621.1+ KB
  • 총 8831개의 row에서 genre, developer, type column에는 더 적은 개수의 row가 있는것으로 보아 결측치를 처리해 주었다.
    • genre column을 이용해서 CBF를 구현해 볼 것이기 때문에, genre column에서 결측치는 제거해 주었다.
    • developer, type column은 개발사와 싱글, 멀티 플레이어인지 나타내는 데이터이기 때문에 그냥 결측치를
      'unknown'으로 채워줬다.
csv = csv[csv['genre'].notna()]
csv['developer'] = csv['developer'].fillna('unknown')
csv['type']      = csv['type'].fillna('unknown')
csv.info()

## 출력 결과
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8827 entries, 0 to 8830
Data columns (total 9 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   game_name    8827 non-null   object 
 1   meta_score   8827 non-null   float64
 2   user_score   8827 non-null   float64
 3   platform     8827 non-null   object 
 4   description  8827 non-null   object 
 5   url          8827 non-null   object 
 6   developer    8827 non-null   object 
 7   genre        8827 non-null   object 
 8   type         8827 non-null   object 
dtypes: float64(2), object(7)
memory usage: 689.6+ KB
  • 이번 주제를 공부하며 아주 신기한 패키지 두 가지를 알게 되었다.
  • swifter는 좀 더 자세히 실험할 내용과 설명이 필요하여 다음에 따로 적어두겠다.
함수 설명
swifter pandas에서의 DataFrame 처리 속도 향상을 위한 병렬 처리 패키지
ast.literal_eval 문자열 안의 리스트, 딕셔너리를 진짜 리스트, 딕셔너리로 바꿔주는 함수
## ast.literal_eval() 함수 예시
expr = '[1, 2, 3, 4, 5, 6, 7]'
print(f'before | {type(expr)}  ,  이거 리스트니? | {type(expr) == list}')

eval_list = ast.literal_eval(expr)
print(f'after  | {type(eval_list)} ,  이거 리스트니? | {type(eval_list) == list}\n')

## 정말 리스트로 변한 녀석이라 리스트에서 하는 모든 것들을 할 수 있다.
print(f'인덱싱을 해보자 | {eval_list[5]}, {eval_list[3]} \n슬라이싱도 가능 | {eval_list[2:5]}')

## 리스트와 동일하게 문자열 안에 딕셔너리 형태로 있으면, 딕셔너리로 변한다.
expr = '{1 : "dove", 2 : "pigeon", 3 : "hippo", 4 : "turtle"}'
print(f'before | {type(expr)}  ,  이거 딕셔너리니? | {type(expr) == dict}')

eval_dict = ast.literal_eval(expr)
print(f'after  | {type(eval_dict)} ,  이거 딕셔너리니? | {type(eval_dict) == dict}\n')

## 마찬가지로 딕셔너리에서 하는 모든 것들을 할 수 있다.
print(f'key로 value를 가져와보자 | {eval_dict[3]}, {eval_dict[2]} \nitems()도 가능         | {eval_dict.items()}')

## 출력 결과
before | <class 'str'>  ,  이거 리스트니? | False
after  | <class 'list'> ,  이거 리스트니? | True

인덱싱을 해보자 | 6, 4 
슬라이싱도 가능 | [3, 4, 5]

before | <class 'str'>  ,  이거 딕셔너리니? | False
after  | <class 'dict'> ,  이거 딕셔너리니? | True

key로 value를 가져와보자 | hippo, pigeon 
items()도 가능         | dict_items([(1, 'dove'), (2, 'pigeon'), (3, 'hippo'), (4, 'turtle')])
  • swifter와 ast.literal_eval을 이용하여 genre와 platform에 있는 칼럼을 전처리 해주었다.
csv['genre'] = csv['genre'].swifter.apply(lambda x: ' '.join(ast.literal_eval(x)))
csv['platform'] = csv['platform'].swifter.apply(lambda x: set(ast.literal_eval(x)))

2-2. 추천 시스템을 만들어보자

  • genre를 이용한 추천 시스템과 description을 이용한 추천 시스템에는 어떤 차이가 있을지 궁금하여
    두 가지 방법으로 만들어 보았다.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
  • genre column을 이용한 데이터는 CountVectorizer를 사용하여 인코딩 하였다.
    ⚠️ CountVectorizer는 아래 세가지 작업을 수행한다.
    1. 문서를 토큰 리스트로 변환한다.
    2. 각 문서에서 토큰의 출현 빈도를 센다.
    3. 각 문서를 BoW(Bag of Words) 인코딩 벡터로 변환한다.
  • description column을 이용한 데이터는 TfidfVectorizer를 사용하여 인코딩 하였다.
    ⚠️ tf-idf | 여러 문서로 이루어진 문서군이 있을 때, 한 문서에서 특정 단어의 가중치를 부여하는 방법
    • tf-idf는 아래 나와있는 tf와 idf를 곱한 값이다.

    함수 설명
    tf(d, t) 특정 문서 d에서의 단어 t의 등장 횟수
    df(t) 특정 단어 t가 등장한 문서의 수
    idf(d, t) df(t)에 반비례하는 수
  • genre column을 이용한 코사인 유사도 데이터는 8,827개의 row, 3013개의 column 데이터가 만들어졌다.
  • description column을 이용한 코사인 유사도 데이터는 8,827개의 row, 32,326개의 column 데이터가 만들어졌다.
vectorizer = CountVectorizer(ngram_range = (1, 3))
genre_vecs = vectorizer.fit_transform(csv['genre'])
genre_sims = cosine_similarity(genre_vecs, genre_vecs).argsort()[:, ::-1]
genre_vecs.shape, csv['genre'].shape

## 출력 결과
((8827, 3013), (8827,))
tf_idf     = TfidfVectorizer(stop_words = 'english')
tf_idf_mat = tf_idf.fit_transform(csv['description'])
des_sim = cosine_similarity(tf_idf_mat, tf_idf_mat).argsort()[:, ::-1]

tf_idf_mat.shape, csv['description'].shape

## 출력 결과
((8827, 32326), (8827,))
  • 필터링, 그러니까 전체 데이터에서 추천 결과만 보여주는 함수
    • 게임 이름을 입력받아 인덱스 값을 추출하고, 코사인 유사도 행렬에서 해당 인덱스에 해당하는 데이터를 가져왔다.
    • 추출된 데이터 중 입력으로 받은 게임의 인덱스와 같은 녀석은 걸러내었다.
    • 마지막으로 meta_score와 user_score를 기준으로 내림차 순 정렬해주었다.
def filtering(name, sims, top = 15):
    
    game_idx = csv[csv['game_name'] == name].index.values
    sim_idx  = sims[game_idx, :top].reshape(-1)
    
    sim_idx  = sim_idx[sim_idx != game_idx]
    result   = csv.iloc[sim_idx].sort_values(['meta_score', 'user_score'], ascending = [False, False])
    
    return result
  • 실제 서비스처럼 쿼리를 입력받아 동작할 수 있도록 함수를 만들어 보았다.
def advanced_filtering(sims, top=15, **query):
    
    ## 입력받은 게임이름과 유사도 행렬을 이용해 한 번 필터링한다.
    df = filtering(query['name'], sims)
    
    
    platform, p_include  = query['platform']
    developer, d_include = query['developer']
    
    ## 인자값으로 딕셔너리에 저장되어 있는 값들을 이용해 데이터를 필터링 해준다.
    df = df[df['type'] == query['type']]
    
    ## 좀 더 깔끔하게 짜고 싶다..
    if (p_include == True) and (d_include == True): 
        df = df[df['platform'].apply(lambda x: platform in x)]
        df = df[df['developer'].apply(lambda x: developer in x)] 
        
    elif (p_include == False) and (d_include == True): 
        df = df[df['platform'].apply(lambda x: platform not in x)]
        df = df[df['developer'].apply(lambda x: developer in x)] 
        
    elif (p_include == True) and (d_include == False):
        df = df[df['platform'].apply(lambda x: platform in x)]
        df = df[df['developer'].apply(lambda x: developer not in x)]
        
    else:
        df = df[df['platform'].apply(lambda x: platform not in x)]
        df = df[df['developer'].apply(lambda x: developer not in x)]
    
    return df

2-3. 비교

  • 닌텐도 스위치의 명작 슈퍼마리오 오디세이를 입력 값으로 넣어 두 개를 비교해 보았다.
sample = csv[csv['game_name'] == 'Super Mario Odyssey']
print(sample['description'].values)

sample

## 출력 결과
["New Evolution of Mario Sandbox-Style Gameplay. 
  Mario embarks on a new journey through unknown worlds, 
  running and jumping through huge 3D worlds in the first sandbox-style 
  Mario game since Super Mario 64 and Super Mario Sunshine. Set sail between 
  expansive worlds aboard an airship, and perform all-new actions, 
  such as throwing Mario's cap."]

[사진 2] 위 코드의 출력결과

  • 게임 이름만을 입력하여 필터링하도록 한 함수에서의 결과
    • genre column 데이터를 이용한 필터링 결과에서는 Action, Platformer 류의 게임이 추천해 주었고,
    • description column의 데이터를 이용한 필터링 결과에서는 장르는 다르지만, 주로 제목에 Mario가
      들어가 있는 게임을 추천해 주었다.
filtering('Super Mario Odyssey', genre_sims).head(3)

## 출력 결과

[사진 3] 위 코드의 출력 결과

filtering('Super Mario Odyssey', des_sim).head(3)

## 출력 결과

[사진 4[ 위 코드의 출력 결과

  • 쿼리를 이용하여 필터링 하도록 한 결과를 비교해 보았다.
    • 게임 이름은 동일하게 Super Mario Odyssey로 했고, singleplayer 게임, 닌텐도에서 개발한 pc 게임이 아닌것을
      쿼리로 넣어주었다.
    • genre column 데이터를 이용한 필터링 결과에서는 아무런 게임도 찾을 수 없었다.
    • description column 데이터를 이용한 필터링 결과에서는 게임 이름만을 이용한 필터링 결과와 비슷하게
      'Mario'가 들어간 제목의 게임이 추천되었지만, 이름만을 이용한 필터링과는 살짝 다르게 나왔다.
query = {
            'name' : 'Super Mario Odyssey',
            'type' : 'singleplayer',
             'platform' : ['pc', False], 
            'developer' : ['Nintendo', True]
        }
        
advanced_filtering(sims = genre_sims, top = 15, **query)

[사진 5] 위 코드의 출력 결과

advanced_filtering(sims = des_sim, top = 15, **query)

## 출력 결과

[사진 6] 위 코드의 출력 결과


99. 자료 출처

99-1. 도서

99-2.논문, 학술지

99-3. 웹사이트

  • 딥러닝을 이용한 자연어 처리 입문 : 04.4) TF-IDF(Term Frequency-Inverse Document Frequency) | [페이지 링크]
  • 꿈 많은 사람의 이야기 : 파이썬과 함께 추천 시스템 이해하기 기본편 - content based filtering | [블로그 링크]
  • Today I Learned. : 추천 시스템 - 컨텐츠 기반 필터링이란? (CBF) | [블로그 링크]
  • 비둘기 둥지 : [ML / 추천 시스템] 1. 추천 시스템에 대해 알아봅시다. | [블로그 링크]
  • DataScience School : Scikit-Learn의 문서 전처리 기능 |[페이지 링크]

99-4. 데이터셋 출처


전체코드

 

GitHub - EvoDmiK/TIL: Today I Learn

Today I Learn. Contribute to EvoDmiK/TIL development by creating an account on GitHub.

github.com


내용 추가 이력


부탁 말씀

개인적으로 공부하는 과정에서 오류가 있을 수 있으니, 오류가 있는 부분은 댓글로 정정 부탁드립니다.


728x90
Comments