본문 바로가기
TIL - 외/추천시스템

[추천시스템] Content Based Filtering

by chaemj97 2023. 4. 11.
728x90

카카오 AI추천 : 카카오의 콘텐츠 기반 필터링 (Content-based Filtering in Kakao) – tech.kakao.com

 

카카오 AI추천 : 카카오의 콘텐츠 기반 필터링 (Content-based Filtering in Kakao)

카카오 서비스 사용자들의 아이템(콘텐츠 또는 상품) 소비 패턴을 살펴보면, 기존에 소비한 아이템과 유사한 아이템을 소비하는 경우를 쉽게 찾아볼 수 있습니다. 예를 들면, 브런치의 특정 작

tech.kakao.com

Content Based Filtering

사용자가 특정 아이템을 선호하는 경우 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천해주는 방식

사례 : 최근 본 상품과 비슷한 상품 추천, 읽은 자품과 비슷한 작품 추천 등등

 

아이템 특성화

아이템 특성을 벡터 형태로 어떻게 표현할지가 중요한 포인트

예 : One-Hot Encoding, Embedding

  • Text 데이터 임베딩 (책 제목)
    • TF-IDF
      • 다른 문서에는 많지 않고, 해당 문서에서 자주 등장하는 단어에 높은 값을 부여 문서를 대표하는 특정 단어를 알아낼 수 있음
        • tf(d,t) : 특정 문서 d에서의 특정 단어 t의 등장 횟수 
        • df(t) : 특정 단어 t가 등장하는 문서의 수
        • idf(d,t) : log(n/(1+df(t)))
    • Word2Vec
      • 단어의 의미를 다차원 공간에 벡터화하는 방법을 사용
      • 단어 간 의미적 유사성을 벡터화 하는 작업을 통해 임베딩 수행 

 

유사도 측정

Similarity : 특성화된 아이템이 서로 얼마나 비슷한지

콘텐츠 기반 추천의 장점

  • 다른 유저들의 데이터가 필요X -> cold start, sparsity 문제가 없다
  • 독특한 취향을 가진 유저에게도 추천 가능
  • 새로운 아이템이나 유명하지 않은 아이템도 추천 가능

 

콘텐츠 기반 추천의 한계점

  • 아이템에 적합한 피처를 뽑기 어려움
  • 유사한 아이템만 반복적으로 추천되어 다양성이 떨어짐
  • 다른 유저의 데이터나 사용자의 평가를 반영하기 어려움

 


실습

캐글 데이터 사용

https://www.kaggle.com/datasets/ruchi798/bookcrossing-dataset

 

Book-Crossing: User review ratings

A collection of book ratings

www.kaggle.com

 

데이터

import pandas as pd
import numpy as np
from typing import List, Set,Optional

books = pd.read_csv(path+'books.csv') # (232348,6)
# Index(['isbn', 'book_title', 'book_author', 'publisher', 'language', 'category'], dtype='object')
         
users = pd.read_csv(path+'users.csv') # (79516,3)
# Index(['user_id', 'location', 'age'], dtype='object')

ratings = pd.read_csv(path+'ratings.csv') # (56290,3)
# Index(['user_id', 'isbn', 'rating'], dtype='object')

# books, users, ratings 합치기
df = ratings.merge(books, on='isbn')
df = df.merge(users, on='user_id', how='inner') # (56290, 10)

총 916개의 책이 존재, 책 리스트 따로 만들기

df['book_title'].nunique() # 916
book_title_list = df['book_title'].unique()

자카드 유사도(교집합/합집합)를 이용한 Content Based Filtering

# 예시
a = set('I like banana'.split()) # {'I', 'banana', 'like'}
b = set('I like apple'.split()) # {'I', 'apple', 'like'}
c = a.intersection(b) # {'I', 'like'}

jaccard = len(c) / (len(a) + len(b) - len(c)) # 0.5

def content_based_filtering_jaccard(book_title_list: list, title: str, topn: Optional[int]=None) -> pd.DataFrame:
    topn=11 if topn is None else topn+1
    # 단어로 나누기
    target_split_set = set(title.split())
    # 책 제목 df
    sim_df = pd.DataFrame(book_title_list, columns=['book_title'])

    # 전체 책 제목들과 자카드 유사도 구하기
    sim_list = []
    for idx, book in enumerate(book_title_list):
        title_split_set = set(book.split())
        title_intersection = target_split_set.intersection(title_split_set)
        jac_sim = float(len(title_intersection)) / (len(target_split_set) + len(title_split_set) - len(title_intersection))
        sim_list.append(jac_sim)
    sim_df['jaccard_similarity'] = sim_list
    
    # 자카드 유사도가 높은 순으로 topn개
    return sim_df.sort_values('jaccard_similarity', ascending=False).reset_index(drop=True)[1:topn]
    
    # 해리 포터와 아즈카반의 죄수와 유사한 책 5권 -> 해리포터 관련 책
    content_based_filtering_jaccard(book_title_list,'Harry Potter and the Chamber of Secrets (Book 2)', 5)

TF-IDF 를 이용한 텍스트 데이터 Vectorization

from sklearn.feature_extraction.text import TfidfVectorizer

# 예시
title_example = ["The Lion, the Witch, and the Wardrobe",
                 "King Solomon's Mines"]
                 
tfidf = TfidfVectorizer()
print(tfidf.fit_transform(title_example).toarray()) # 각 TF-idf 를 계산
print(tfidf.vocabulary_) # 각 단어의 인덱스가 어떻게 부여되었는지 보여준다

유클리드 유사도(거리)를 이용한 Content Based Filtering

tfidf = TfidfVectorizer()
wordmatrix = tfidf.fit_transform(book_title_list).toarray() #책 제목 리스트를 TF-IDF를 활용해 벡터라이징 합니다.

from sklearn.metrics.pairwise import euclidean_distances # 유클리드 유사도를 바로 계산

# wordmatrix는 책 제목을 vectorization 한 array 입니다.
def content_based_filtering_euclidean(book_title_list: list, wordmatrix: np.array, title: str, topn: Optional[int]=None) -> pd.DataFrame:
    topn=11 if topn is None else topn+1
    # 유클리드 유사도
    sim_matrix = pd.DataFrame(euclidean_distances(wordmatrix), index=book_title_list, columns=book_title_list)
    target_similarity_df = sim_matrix[title].reset_index().copy()
    target_similarity_df.columns=['title', 'euclidean_similarity']
    # 유클리드 유사도 상위 topn개 반환
    return target_similarity_df.sort_values('euclidean_similarity', ascending=True).reset_index(drop=True)[1:topn]
    
# 해리 포터와 아즈카반의 죄수와 유사한 책 5권 -> 해리포터 관련 책
content_based_filtering_euclidean(book_title_list, wordmatrix, 'Harry Potter and the Chamber of Secrets (Book 2)',5)

코사인 유사도를 이용한 Content Based Filtering

from sklearn.metrics.pairwise import cosine_similarity

#코사인 유사도 계산를 content_based_filtering 결과
def content_based_filtering_cosin(book_title_list: list, wordmatrix: np.array, title: str, topn: Optional[int]=None) -> pd.DataFrame:
    topn=11 if topn is None else topn+1
    # 코사인 유사도 계산
    sim_matrix = pd.DataFrame(cosine_similarity(wordmatrix), index=book_title_list, columns=book_title_list)
    target_similarity_df = sim_matrix[title].reset_index().copy()
    target_similarity_df.columns=['title', 'cosine_similarity']
    # 코사인 유사도 상위 topn개 반환
    return target_similarity_df.sort_values('cosine_similarity', ascending=False).reset_index(drop=True)[1:topn]
    
# 해리 포터와 아즈카반의 죄수와 유사한 책 5권 -> 해리포터 관련 책
content_based_filtering_cosin(book_title_list, wordmatrix, 'Harry Potter and the Chamber of Secrets (Book 2)', 5)

피어슨 상관 계수를 이용한 Content Based Filtering

def content_based_filtering_pearson(book_title_list: list, wordmatrix: np.array, title: str, topn: Optional[int]=None) -> pd.DataFrame:
    topn=11 if topn is None else topn+1
    # corr메서드는 각 열 간의 상관 계수를 반환하는 메서드
    # 각 행으로 바꾸고 싶어서 transpose
    sim_matrix = pd.DataFrame(wordmatrix, index=book_title_list).T.corr(method='pearson')
    target_similarity_df = sim_matrix[title].reset_index().copy()
    target_similarity_df.columns=['title', 'pearson_similarity']
    # 피어슨 상관계수 상위 topn개 반환
    return target_similarity_df.sort_values('pearson_similarity', ascending=False).reset_index(drop=True)[1:topn]
    
# 해리 포터와 아즈카반의 죄수와 유사한 책 5권 -> 해리포터 관련 책
content_based_filtering_pearson(book_title_list, wordmatrix, 'Harry Potter and the Chamber of Secrets (Book 2)', 5)

Content Based Filtering - Train / Test Split - T

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer

# 훈련,테스트 데이터 나누기
train_df, test_df = train_test_split(df,test_size=0.1,random_state=42)

# 책 제목 리스트를 TF-IDF를 활용해 벡터라이징
book_title_train_list = train_df['book_title'].unique()
tfidf = TfidfVectorizer()
wordmatrix = tfidf.fit_transform(book_title_train_list).toarray()

# 각 책 벡터간 유사도 계산
sim_matrix = pd.DataFrame(cosine_similarity(wordmatrix), index=book_title_train_list, columns=book_title_train_list)

F-IDF, Cosine Similarity

def content_based_filtering(train_df: pd.DataFrame,
                            test_df: pd.DataFrame) -> list:
    
    # 각 유저가 책마다 매긴 점수 테이블
    user_item_matrix = train_df.pivot_table(index=['user_id'], columns=['book_title'], values='rating').fillna(0)

    # 테스트 데이터 예측 결과
    pred_rating_list = []
    # test_id유저가 test_title책에 매길 평점 예측
    for test_id, test_title in zip(test_df['user_id'], test_df['book_title']):
        similarity_list = []
        rating_list = []
        # test_id유저가 매긴 책
        for read_book in train_df[train_df['user_id']==test_id]['book_title']:
            # test_title책과 다른 책과의 유사도
            similarity_list.append(sim_matrix[read_book][test_title])
            # test_id가 매긴 다른 책들의 평점
            rating_list.append(user_item_matrix[read_book][test_id])
        similarity_list = np.array(similarity_list)
        rating_list = np.array(rating_list)
        # 평점 예측
        pred = (similarity_list * rating_list).sum() / (similarity_list.sum() + 1e-10) #분모가 0이되는 것을 방지하기 위해 작은 수를 더합니다.
        pred_rating_list.append(pred)
    return pred_rating_list
    
pred_rating = content_based_filtering(train_df, test_df)

RMSE

표준편차와 동일하다. 특정 수치에 대한 예측의 정확도를 표현할 때, Accuracy로 판단하기에는 정확도를 올바르게 표기할 수 없어, RMSE 수치로 정확도 판단을 하곤 한다. 일반적으로 해당 수치가 낮을수록 정확도가 높다고 판단한다.

from sklearn.metrics import mean_squared_error

mean_squared_error(test_df['rating'],pred_rating)**0.5 # 3.31792070726792
728x90
반응형

댓글