Ssul's Blog

[추천시스템#4]CB(콘텐츠 기반 필터링) 본문

AI & ML

[추천시스템#4]CB(콘텐츠 기반 필터링)

Ssul 2023. 12. 22. 23:32

처음 추천시스템을 공부했을땐, 

CF에서 IBCF/UBCF가 헤깔렸고,

IBCF와 CB가 헤깔렸다. 이번 글에서 명확히 정리해보자.

 

우선 앞에서 본

IBCF는 user와 item(영화)간의 rating을 기반으로, item(영화)간의 유사도를 구해서,

영화(item)간의 유사도를 기반으로 입력된 user-item에 매칭되는 평점을 예상하는 것이다.

UBCF는 user와 item(영화)간의 rating을 기반으로, user간의 유사도를 구해서,

user간의 유사도를 기반으로 입력된 user-item에 매칭되는 평점을 예상하는 것이다.

 

그렇다면 CB는.... 코드를 보며 자세히 설명하겠지만,

간단하게, 콘텐츠(영화)가 가지고 있는 특성을 기반으로, 콘텐츠(영화)간의 유사도를 구해서,

콘텐츠간 유사도를 기반으로 입력된 user-item에 매칭되는 평점을 예상하는 것이다.

여기서 콘텐츠가 가지고 있는 특성은

- 줄거리 데이터

- 영화 출연진, 감독 등

다양한 것을 활용할수 있다.

 

1. 영화들의 줄거리 데이터를 기반으로 콘텐츠간 유사도 측정해서 추천하는 CB

import pandas as pd

# Meta data 읽기
movies = pd.read_csv('./movies.csv', encoding='latin-1', low_memory=False)
movies = movies[['id', 'title', 'overview']]

- 영화의 제목/줄거리 데이터를 가져옴

 

movies = movies.drop_duplicates()
# 데이터프레임에서 모든 결측값이 있는 행을 제거
movies = movies.dropna()
# movies 데이터프레임의 'overview' 열에 있는 모든 결측값을 빈 문자열로 대체
movies['overview'] = movies['overview'].fillna('')

- 데이터 중복/결측값 제거

 

TF-IDF 사용해서 콘텐츠간 유사도를 구하기

*TF-IDF는?

- TF: 해당 문서(줄거리)에서 자주 등장하는 단어인지 표현 > 높으면 해당 문서에서 자주 등장, 낮으면 덜 등장

- DF: 모든 영화의 줄거리에서 해당 단어가 나타난 문서수 > 높으면, 해당 단어는 모든 문서에서 자주 쓰는 단어 > IDF는 DF역수, IDF가 높다는 것은 해당 단어는 모든 줄거리에서 희소하게 등장하는 단어

- TF*IDF값이 크다 = 해당문서에서 자주 등장 & 모든 문서에서 희소하게 등장하는 단어 = 굉장히 중요한 단어(비중있게 다뤄지는, 가중치 높음)

 

# TfIdfVectorizer 가져오기
from sklearn.feature_extraction.text import TfidfVectorizer

# 불용어를 english로 지정하고 tf-idf 계산
# stop_words='english' 옵션은 영어의 불용어(예: the, and, is 등)를 제외
tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(movies['overview'])

- tfidf_matrix는 44,300개 영화가 행, 74686개의 모든 줄거리에서 등장한 단어가 열, 그리고 값이 TF*IDF값

- 근데 이게 spars한 값이기 때문에 아래와 같이 표현됨

1번째 영화에 등장한 단어(28783번)의 TF*IDF값 = 0.133...

44299번째 영화에 등장한 단어(27061번)의 TF*IDF값 = 0.073..

 

그래서 각 영화의 줄거리에 등장하는 모든 단어의 TF*IDF값을 계산

 

# Cosine 유사도 계산, overview기반으로 영화간 유사도 계산
from sklearn.metrics.pairwise import cosine_similarity
# 44300*44300
cosine_sim = cosine_similarity(tfidf_matrix, tfidf_matrix)
cosine_sim = pd.DataFrame(cosine_sim, index=movies.index, columns=movies.index)
cosine_sim.shape
# index-title을 뒤집는다
indices = pd.Series(movies.index, index=movies['title'])

- 각 영화별 줄거리에 등장하는 단어의 TF*IDF값을 기반으로 영화들간의 유사도 구하기

(위에서 IBCF가 user-movie의 rating을 기반으로 했다면, CB는 영화의 줄거릴 단어들의 TF*IDF값으로 유사도 구함)

 

 

# 영화제목을 받아서 추천 영화를 돌려주는 함수
def content_recommender(title, n_of_recomm):
    # title에서 영화 index 받아오기
    idx = indices[title]
    # 주어진 영화(idx) 다른 영화의 similarity를 가져온다 > 1*44300
    sim_scores = cosine_sim[idx]
    # similarity 기준으로 정렬하고 n_of_recomm만큼 가져오기 (자기자신은 빼기)
    sim_scores = sim_scores.sort_values(ascending=False)[1:n_of_recomm+1]
    # 영화 title 반환
    return movies.loc[sim_scores.index]['title']

# 해당 영화와 유사한 영화 추천받기
print(content_recommender('The Lion King', 10))

- 영화제목을 입력받아서,

- 유사한 영화 상위 10개를 받아서 출력해주기

 

 

2. 영화의 감독, 출연진 등의 데이터를 기반으로, 콘텐츠(영화)간 유사도 측정 후 추천하는 CB

import pandas as pd
import numpy as np

# Meta data 읽기
movies = pd.read_csv('./movies_metadata.csv', encoding='latin-1', low_memory=False)
movies = movies[['id', 'title']]
movies = movies.dropna()
# 영화 크래딧 정보
credits = pd.read_csv('./credits.csv', encoding='latin-1', low_memory=False)
# 영화의 키워드
keywords = pd.read_csv('./keywords.csv', encoding='latin-1', low_memory=False)

 

- 데이터 가져오기

movies: 영화제목
credits: 주연, 감독 등 제작자 정보
keywords: 영화관련 주요 키워드

 

# 아이디가 string을 int로
def clean_ids(x):
    try:
        return int(x)
    except:
        return np.nan

# Clean the ids of df
# id값을 int형으로
movies['id'] = movies['id'].apply(clean_ids)
movies['id'].notnull()

# Filter all rows that have a null ID
# id가 결측값이 아닌 무비들만 가져오기
movies = movies[movies['id'].notnull()]

# Convert IDs into integer
# id값 int형으로 변환
movies['id'] = movies['id'].astype('int')
keywords['id'] = keywords['id'].astype('int')
credits['id'] = credits['id'].astype('int')

# Merge keywords and credits into your main metadata dataframe
movies = movies.merge(credits, on='id')
movies = movies.merge(keywords, on='id')
# movies: (46439, 5)
movies.head()

 

- 데이터 preprocessing

 

 

from ast import literal_eval
features = ['cast', 'crew', 'keywords']
for feature in features:
    movies[feature] = movies[feature].apply(literal_eval)
    
# Print the first cast member of the first movie
movies.iloc[0]
movies.iloc[0]['crew']
movies.iloc[0]['crew'][0]
movies.iloc[0]['crew'][1]

 

- movies로 데이터 통합

 

합쳐진 movies데이터

- 한개의 crew안에 여러개의 참여자(dict)가 있는 구조

 

def get_director(x):
    for crew_member in x:
        if crew_member['job'] == 'Director':
            return crew_member['name']
        return np.nan
    
# Define the new director feature
movies['director'] = movies['crew'].apply(get_director)

- crew의 dict중에 job이 'Director'(감독)인 정보를 찾아서, 그 이름을 리턴

- 영화별 감독정보 추가

 

# Return the list top 3 elements
def generate_list(x):
    # x가 list형인지 체크
    if isinstance(x, list):
        # names 리스트 만들고,
        names = [item['name'] for item in x]
        # Check if more than 3 elements exist. If yes, return only first three
        # If not, return entire list
        if len(names) > 3:
            # 3개짜리 리스트로 만들기
            names = names[:3]
        return names
    # Return empty list in case of missing/malformed data
    return []

# Apply the generate_list function to cast and keywords
# cast, keywords 3개씩
movies['cast'] = movies['cast'].apply(generate_list)
movies['keywords'] = movies['keywords'].apply(generate_list)

- cast, keywords의 객체 중, 앞 3개의 이름만 뽑아서 cast, keywords 데이터 생성

 

새롭게 수정된 movies 데이터

 

 

# Removes spaces and converts to lowercase
def sanitize(x):
    if isinstance(x, list):
        # Strip spaces and convert to lowercase
        return [str.lower(i.replace(" ","")) for i in x]
    else:
        # Check if an item exists. If not, return empty string
        if isinstance (x, str):
            return str.lower(x.replace(" ",""))
        else:
            return ''
        
# Apply the generate_list function to cast, keywords, and director
for feature in ['cast', 'director', 'keywords']:
    movies[feature] = movies[feature].apply(sanitize)

- 띄어쓰기 삭제, 소문자 전환

 

def create_soup(x):
    return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director']

# Create the new soup feature
movies['soup'] = movies.apply(create_soup, axis=1)

- soup로 그동안 정제한 metadata 기록 > CB에 활용예정

 


 

# Import CountVectorizer from the scikit-learn library
from sklearn.feature_extraction.text import CountVectorizer

# Define a new CountVectorizer object and create vectors for the soup
count = CountVectorizer(stop_words='english')
# 영화*해당 단어가 있는지 카운트 벡터
count_matrix = count.fit_transform(movies['soup'])

# Cosine 유사도 계산
from sklearn.metrics.pairwise import cosine_similarity
cosine_sim = cosine_similarity(count_matrix, count_matrix)
cosine_sim = pd.DataFrame(cosine_sim, index=movies.index, columns=movies.index)

- soup를 기반으로 영화관 유사도 매트릭스 구성

- CountVectorizer는 soup에 등장하는 모든 단어를 열로, 행은 한개의 영화, 데이터는 그 영화의 soup에 있는 단어는 1로 체크(TfidfVectorizer와 유사하지만, 더 간단한 구조)

count_matrix: (영화id, 단어id), 1(해당단어 있음)

 

cosine_sim: 영화간 유사도

 

 

# index-title을 뒤집는다
indices = pd.Series(movies.index, index=movies['title'])

# 영화제목을 받아서 추천 영화를 돌려주는 함수
def content_recommender(title, n_of_recomm):
    # title에서 영화 index 받아오기
    idx = indices[title]
    # 주어진 영화와 다른 영화의 similarity를 가져온다
    sim_scores = cosine_sim[idx]
    # similarity 기준으로 정렬하고 n_of_recomm만큼 가져오기 (자기자신은 빼기)
    sim_scores = sim_scores.sort_values(ascending=False)[1:n_of_recomm+1]
    # 영화 title 반환
    return movies.loc[sim_scores.index]['title']

# 추천받기
print(content_recommender('The Lion King', 10))

- 영화제목을 받아서, 해당 영화와 유사한 영화 상위 10개 돌려주기

 

 

3. CB정리

- CB는 콘텐츠가 가지고 있는 속성(줄거리, 감독, 출연진 등)을 일정한 구조의 데이터로 구성

- 해당 데이터를 기반으로 TfidfVectorizer, CountVectorizer 등을 사용한 통일된 특성 구성

- 해당 특성을 기반으로 유사도 매트릭스 구성

- 파악한 콘텐츠(영화)간 유사도를 기반으로, 비슷한 상위 콘텐츠 추천하는 구조

 

 

이제 메모리기반의 CF(IBCF, UBCF)와 CB를 봤으니,

모델기반의 CF를 알아보자. MF부터..