본문 바로가기

Learning/Recommendation System

02-2. CBF 기반 평점 예측을 이용한 영화 추천

CBF 기반 예측

 

아이템 유사도 기반 평점 예측

가정 : 유사한 영화에는 유사한 평점을 부여할 것이다.

 

-> 아이템간 유사도를 기준으로 평점에 가중치를 부여하여 평점을 예측

>> (4*0.7 + 3*0.2 + 3*0.3 + 5*0.5) / (0.7 + 0.2 + 0.3 + 0.5) = 4

 

 

컨텐츠 기반 유사도 측정

- 두 아이템간의 유사도를 어떻게 정량화 할 것이냐가 관건

- CBF에서는 유사도를 정량화 하기 위해서 컨텐츠 자체가 가진 특성을 활용

ex.

뉴스 : 제목, 기사 내용 등

영화 : 장르, 감독, 출연자 등

상품 : 카테고리, 가격, 이미지 등

 

영화의 장르를 이용해 유사도 측정 - 장르의 집합으로 간주

 

- 대부분 자카드 유사도를 사용한다.

집합 유사도 예시


Distance & Similarity

 

 

Bag-of-Word Representation
TF-IDF(Term Frequency - Inverse Document Frequency)

tf(t,d) : 어떤 term이 어떠한 document에서 얼마나 중요한 것인가 계산

idf(t, D) : 전체 Document에서 term이 얼마나 중요한 의미를 갖는가 계산

 

TF (Term Frequency)

 

IDF (Inverse Document Frequency)

- 사용된 문서 집합에 존재하지 않는 단어에 대해서 IDF를 고려하게 되면 분모가 0이 되기 때문에 이를 방지하기 위해서 다음과 같이 분자, 분모에 1을 더해서 사용

 

TF-IDF 예제


Cosine 유사도 구하기 실습

print("Items : ", matrix.shape[0])
print("Genres: ", matrix.shape[1])

- 각 item들에 대해 weight 구하기

totalItems = matrix.shape[0]
totalGenres = matrix.shape[1]

weights = np.zeros(matrix.shape)

for i in range(0, totalGenres):
    col = matrix[:,i]
    df = col.sum() # document frequency
    idf = math.log10(totalItems/df)
    for j in range(0, totalItems):
        weights[j, i] = matrix[j, i] * idf

weights

- l2 norm 구하기

def norm2(arr):
    sum = 0.0
    for i in range(0, len(arr)):
        sum += arr[i] * arr[i]
    return math.sqrt(sum)

print(weights[0], "=>", norm2(weights[0]))
print(weights[1], "=>", norm2(weights[1]))

- 내적(Inner Product)

def dot(arr1, arr2):
    sum = 0.0
    for i in range(0, len(arr1)):
        sum += arr1[i] * arr2[i]
    return sum

print(dot(weights[0], weights[1]))
print(dot(weights[0], weights[2]))

 

- Cosine 유사도 구하기

def cosine(arr1, arr2):
    return dot(arr1, arr2)/(norm2(arr1)*norm2(arr2))

print(cosine(weights[0], weights[1]))
print(cosine(weights[0], weights[2]))

- Numpy를 활용하여 Cosine 유사도 구하기

# Numpy Version
from numpy import linalg as LA #선형대수
norms = LA.norm(weights, ord=2, axis=1)
dots = np.matmul(weights, weights.T)
sims = np.divide(np.divide(dots, norms).T, norms)

 

활용 함수 정의
movies_w_imgurl.csv 활용 영화 포스터 display 함수 정의
#%run liblecture.py # 이후 liblecture 파일 실행 후 함수 사용

from IPython.display import display, HTML

def displayMovies(movies, movieIds, ratings=[]):

    html = ""

    for i, movieId in enumerate(movieIds):
        movie = movies[movies['movieId'] == movieId].iloc[0]

        html += f"""
            <div style="display:inline-block;min-width:150px;max-width:150px; vertical-align:top">
                <img src='{movie.imgurl}' width=120> <br/>
                <span>{movie.title}</span> <br/>
                {f"<span>{ratings[i]}</span> <br/>" if len(ratings) > 0 else ""}
                <ul>{"".join([f"<li>{genre}</li>" for genre in movie.genres.split('|')])}</ul>
            </div>
        """

    display(HTML(html))


displayMovies(movies, [1, 2, 3], [4, 3, 3])

 

MAE, RMSE 구하는 함수 정의
import numpy as np
import math

def getMAE(real, pred):
    errors = real - pred
    return errors.abs().mean()

def getRMSE(real, pred):
    errors = real - pred
    return math.sqrt(errors.pow(2).mean())
mat = np.array([
    [1, 2, 3],          # movieId
    [3.0, 4.0, 5.0],    # rating
    [3.2, 3.8, 4.3]     # prediction
])

ratings = pd.DataFrame(data=mat.T, columns=['movieId', 'rating', 'pred'])


mae = getMAE(ratings['rating'], ratings['pred'])
rmse = getRMSE(ratings['rating'], ratings['pred'])

print(f"MAE : {mae:.4f}")
print(f"RMSE: {rmse:.4f}")


Content-based Movie Rating Estimation

movies_w_imgurl.csv

- genres 컬럼 split 후 한 컬럼으로 stack 하기

movieGenres = pd.DataFrame(data=movies['genres'].str.split('|').apply(pd.Series, 1).stack(), columns=['genre'])
movieGenres.index = movieGenres.index.droplevel(1)

 

- 각 genre별 count 후 idf 구하기

genres = pd.DataFrame(data=movieGenres.groupby('genre')['genre'].count())
genres.columns = ['movieCount']

totalItems = movies.shape[0]

#idf 값 구하기
genres['idf'] = genres['movieCount'].apply(lambda x: math.log10(totalItems/x))

genres.head()

- 영화 장르와 idf 값을 join해서 DataFrame 생성

movieGenreWeights = movieGenres.join(genres['idf'], on='genre')

- MovieId별 장르의 idf DataFrame 생성

movieWeights = movies[['movieId']]

for genre in genres.index:
    movieGenreIdf = movieGenreWeights[movieGenreWeights['genre'] == genre][['idf']]
    movieGenreIdf = movieGenreIdf.rename(columns={'idf':genre})
    movieWeights = movieWeights.join(movieGenreIdf)

movieWeights.fillna(0, inplace=True)
movieWeights

 

- 영화의 l2 norm 구하기

movieNorms = pd.DataFrame(data = LA.norm(movieWeights.iloc[:,1:].values, ord=2, axis=1), index=movieWeights.index, columns=['norm2'])

- Normalize movie vector

normalizedMovieWeights = movieWeights.iloc[:, 1:].divide(movieNorms['norm2'], axis=0)
normalizedMovieWeights

 

- 각 MovieId별 유사도 DataFrame 생성

sims = pd.DataFrame(data=np.matmul(normalizedMovieWeights, normalizedMovieWeights.T))

sims.index = movieWeights['movieId']
sims.columns = movieWeights['movieId']

sims

 

예측된 평점을 통해 영화 추천 - userid 한개 test
ratings = pd.read_csv('ratings-9_1.csv')

train = ratings[ratings['type'] == 'train'][['userId', 'movieId', 'rating']]
test = ratings[ratings['type'] == 'test'][['userId', 'movieId', 'rating']]
ratings

- 좋아하는 영화 Top 20 display

userId = 33
userRatings = train[train['userId'] == userId][['movieId', 'rating']] 

#어떤 영화를 좋아했는지 나열해보기
topRatings = userRatings.sort_values(by='rating', ascending=False).head(20) 
displayMovies(movies, topRatings['movieId'].values, topRatings['rating'].values)

 

recSimSums = sims.loc[userRatings['movieId'].values, :].sum().values

#유사도가 0인 애들로 인해 나오는 오류를 방지
recSimSums = recSimSums + 1
recWeightedRatingSums = np.matmul(sims.loc[userRatings['movieId'].values, :].T.values, userRatings['rating'].values)
recItemRatings = pd.DataFrame(data = np.divide(recWeightedRatingSums, recSimSums), index=sims.index)
recItemRatings.columns = ['pred']
recItemRatings

- 좋아할 만한 영화 Top30 display

top30Movies = recItemRatings.sort_values(by='pred', ascending=False).head(30)

displayMovies(movies, top30Movies.index, top30Movies['pred'].values)

- test 사용자에 대한 MAE, RMSE 계산

userTestRatings = pd.DataFrame(data=test[test['userId'] == userId])

temp = userTestRatings.join(recItemRatings.loc[userTestRatings['movieId']], on='movieId')

mae = getMAE(temp['rating'], temp['pred'])
rmse = getRMSE(temp['rating'], temp['pred'])

print(f"MAE : {mae:.4f}")
print(f"RMSE: {rmse:.4f}")

 


출처 : The RED : 현실 데이터를 활용한 추천시스템 구현 A to Z by 번개장터 CTO 이동주

링크 : https://fastcampus.app/course-detail/205535