TF-IDF로 장르, 영화 tag이용한 추천 알고리즘 실습
0. 지난 내용
1) 컨텐츠기반 추천시스템
2) 컨텐츠를 활용할 수 있는지
3) 어떻게 컨텐츠들이 서로 연관이 있는지 평가하는 방법
4) 근접이웃기반 컨텐츠기반 추천시스템 -> k-nearest neighbor
5) naive bayes classifer -> 베이즈 추천시스템
6) TF-IDF 개념설명 그리고 실습
📌 추천 시스템 성능 평가 → RMSE
1. Dataset 불러오기
. 필요한 데이터들을 불러온다.
# 각자 작업 환경에 맞는 경로를 지정해주세요. Google Colab과 Jupyter환경에서 경로가 다를 수 있습니다.
path = '/content/drive/MyDrive/Colab Notebooks'
ratings_df = pd.read_csv(os.path.join(path, 'ratings.csv'), encoding='utf-8')
movies_df = pd.read_csv(os.path.join(path, 'movies.csv'), index_col='movieId', encoding='utf-8')
tags_df = pd.read_csv(os.path.join(path, 'tags.csv'), encoding='utf-8')
. 불러온 데이터들 조회
# user 가 moive에 대해 어떤 rating을 줬는지 조회
ratings_df.head()
# 장르 정보는 여기서 가져와야함
movies_df.head()
# user가 movie에 대해 어떤 tag를 줬는지
tags_df.head()
2. Genres를 이용한 movie representation
. movie가 몇개인지, 장르는 몇개인지..
→ 장르는 총 20개이고, 영화 수는 9742개임을 볼 수 있다.
total_count = len(movies_df.index)
total_genres = list(set([genre for sublist in list(map(lambda x: x.split('|'), movies_df['genres'])) for genre in sublist]))
print(f"전체 영화 수: {total_count}")
print(f"장르: {total_genres}")
print(len(total_genres))
. genre_count
→ 전체 영화 중 각 장르가 몇번 등장하는지를 보고자 함 (중복 포함)
genre_count = dict.fromkeys(total_genres)
for each_genre_list in movies_df['genres']:
for genre in each_genre_list.split('|'):
if genre_count[genre] == None:
genre_count[genre] = 1
else:
genre_count[genre] = genre_count[genre]+1
genre_count
. np.log10
→ IDF에 해당되는 부분, 이를 통해 장르별 가중치를 계산해보겠단 의미
ex) 위쪽에서 등장 비중이 높았던 Comedy(3756번)는 가중치가 0.41 정도로 낮고,
등장 비중이 낮았던 IMAX(158번)는 가중치가 1.79 정도로 높다.
for each_genre in genre_count:
genre_count[each_genre] = np.log10(total_count/genre_count[each_genre])
genre_count
. genre_representation을 생성한다
→ 이전에는 0과 1로만 표기를 했었는데, 이번에는 위에서 산출한 IDF로 표현을 한다.
→ 따라서 movie별 표기된 장르에서 중요도가 높은 장르와 중요도가 낮은 장르를 구분하게 된다.
# create genre representations
# 데이터 프레임을 우선 생성
genre_representation = pd.DataFrame(columns=sorted(total_genres), index=movies_df.index)
# MOVIE별로 반복문을 수행하게 된다
# tqdm을 걸어놔서 한줄씩 실행된다.
for index, each_row in tqdm(movies_df.iterrows()):
# 해당되는 장르의 가중치를 가져온다
dict_temp = {i: genre_count[i] for i in each_row['genres'].split('|')}
# 가중치를 가지고 데이터 프레임을 새로 생성
row_to_add = pd.DataFrame(dict_temp, index=[index])
# 위 결과를 모든 장르가 들어가있는 genre_representation에 업데이트 한다
genre_representation.update(row_to_add)
#결과를 보면, movie별로 해당되는 장르에 가중치가 들어간걸 볼 수 있다.
genre_representation
3. Tag를 이용한 Movie Representation
. user가 movie에 어떤 tag를 줬는지 조회
tags_df.head(5)
. movieId=89774가 어떤 영화인지 조회
movies_df.loc[89774]
. TF-IDF를 구하기 위해, 먼저 태그 컬럼 확인
→ 1589개의 unique한 태그를 가지고 영화들을 설명했다는걸 알 수 있다.
# get unique tag
tag_column = list(map(lambda x: x.split(','), tags_df['tag']))
unique_tags = list(set(list(map(lambda x: x.strip(), list([tag for sublist in tag_column for tag in sublist])))))
print(unique_tags)
print(len(tag_column))
print(len(unique_tags))
. tag의 IDF를 구한다.
→ tag_idf[each_tag] = np.log10(total_movie_count / tag_count_dict[each_tag])
→ 마찬가지로 많이 등장한 태그에 가중치를 덜 주기 위해 inverse를 취하게 된다.
. 위에서 봤듯이 unique한 tag에 대해 가중치를 부여했기 때문에 tag_idf도 1589개가 된다.
# Compute IDF for tag
total_movie_count = len(set(tags_df['movieId']))
# key: tag, value: number of movies with such tag
tag_count_dict = dict.fromkeys(unique_tags)
for each_movie_tag_list in tags_df['tag']:
for tag in each_movie_tag_list.split(","):
if tag_count_dict[tag.strip()] == None:
tag_count_dict[tag.strip()] = 1
else:
tag_count_dict[tag.strip()] += 1
tag_idf = dict()
for each_tag in tag_count_dict:
tag_idf[each_tag] = np.log10(total_movie_count / tag_count_dict[each_tag])
tag_idf
len(tag_idf.keys()) #1589
. movie id 별로 각각 해당되는 tag에 대한 가중치를 부여한다.
→ 많은 태그들 중 movie별로 적은 수의 tag가 달려있기 때문에 조회시 NaN이 많이 보인다. (잘못된것 x)
** tag_representation.sort_index() → 예제에는 파라미터로 0이 들어가있으나 제외해야한다.
# Create movie representations
tag_representation = pd.DataFrame(columns=sorted(unique_tags), index=list(set(tags_df['movieId'])))
for name, group in tqdm(tags_df.groupby(by='movieId')):
temp_list = list(map(lambda x: x.split(','), list(group['tag'])))
temp_tag_list = list(set(list(map(lambda x: x.strip(), list([tag for sublist in temp_list for tag in sublist])))))
dict_temp = {i: tag_idf[i.strip()] for i in temp_tag_list}
row_to_add = pd.DataFrame(dict_temp, index=[group['movieId'].values[0]])
tag_representation.update(row_to_add)
tag_representation = tag_representation.sort_index()
tag_representation
. 위에서 만든 2개의 representation 결과 조회
print(genre_representation.shape) #(9742, 20) (영화 수, 장르 수)
print(tag_representation.shape) #(1572, 1589) (태그를 단 영화 수, 태그 수)
4. Final Movie Represenation
. genre와 tag로 만들어진 representation을 합쳐서 각 movie의 vector로 만든다
# 장르와 태그 두개의 representation을 합치고, NaN은 필요가 없으니 0으로 치환한다.
movie_representation = pd.concat([genre_representation, tag_representation], axis=1).fillna(0)
# 장르 20개, 태그 1589
# 따라서 영화 하나를 표현하기 위해 1609개의 feature가 생겨남
print(movie_representation.shape)
print(movie_representation.describe())
5. Contents 유사도 평가
. Cosine similarity를 사용
→ sklearn(싸이킬런)에서 함수로 구현해놓음
from sklearn.metrics.pairwise import cosine_similarity
def cos_sim_matrix(a, b):
cos_sim = cosine_similarity(a, b)
result_df = pd.DataFrame(data=cos_sim, index=[a.index])
return result_df
print(movie_representation.head())
. 각 movie와 해당 movie를 제외한 다른 모든 movie와의 Cosine 유사도를 구한다.
cs_df = cos_sim_matrix(movie_representation, movie_representation)
cs_df.head()
. 예시) 1번 영화에 대해서 Cosine 유사도로 줄을 세운 결과
→ 가장 유사한 것이 46972번의 영화이며, 0.32정도의 Cosine 유사도를 가지고 있다.
→ 1번 영화를 본 유저에게 46972번 영화를 추천할 수 있다.
print(cs_df.shape)
print(cs_df[1].sort_values(ascending=False))
→ 위 결과에서 관련도가 높은 영화 목록 조회해보
print(movies_df.loc[1])
print(movies_df.loc[46972])
print(movies_df.loc[126142])
print(movies_df.loc[2043])
print(movies_df.loc[2399])
6. 추천시스템의 성능 평가
. 학습셋과 테스트셋을 나눈다.
. 테스트셋에서 예측한 평점과 실제 평점의 RMSE를 구한다.
. ratings_df를 활용해서 train/test 데이터를 나눈다.
train_df, test_df = train_test_split(ratings_df, test_size=0.2, random_state=1234)
print(train_df.shape)
print(test_df.shape)
. 테스트에 사용할 user id
test_userids = list(set(test_df.userId.values))
test_userids
. rating정보와 consine 유사도를 가지고 최종 예측 진행
result_df = pd.DataFrame()
for user_id in tqdm(test_userids):
# Train 데이터에서 user id가 해당 되는 부분을 가져온다.
user_record_df = train_df.loc[train_df.userId == int(user_id), :]
# (n, 9742); n은 userId가 평점을 매긴 영화 수
# 해당 user를 위한 consine 유사도 데이터를 만든다.
user_sim_df = cs_df.loc[user_record_df['movieId']]
# user_record_df에서 user_rating을 가져온다.
# 유저가 평점을 매긴 영화 전체의 rating을 가져오는 것.
user_rating_df = user_record_df[['rating']] # (n, 1)
sim_sum = np.sum(user_sim_df.T.to_numpy(), -1) # (9742, 1)
# user rating 정보와 consine 유사도를 가지고 matrix multiplication을 통해 최종 예측한다.
prediction = np.matmul(user_sim_df.T.to_numpy(), user_rating_df.to_numpy()).flatten() / (sim_sum+1) # (9742, 1)
prediction_df = pd.DataFrame(prediction, index=cs_df.index).reset_index()
prediction_df.columns = ['movieId', 'pred_rating']
prediction_df = prediction_df[['movieId', 'pred_rating']][prediction_df.movieId.isin(test_df[test_df.userId == user_id]['movieId'].values)]
temp_df = prediction_df.merge(test_df[test_df.userId == user_id], on='movieId')
result_df = pd.concat([result_df, temp_df], axis=0)
. 예측과 실제 비교
result_df.head(10)
. RMSE 결과
→ 성능이 드라마틱하게 좋아지지는 않았다.
→ 이유? 어떤 feature를 만드는지가 중요!
→ flow는 동일하되, feature를 어떻게 만들지를 수정해야 한다.
mse = mean_squared_error(y_true=result_df['rating'].values, y_pred=result_df['pred_rating'].values)
rmse = np.sqrt(mse)
print(mse, rmse)