사람들의 스팀 게임 플레이시간을 바탕으로 그 사람에게 알맞는 스팀 게임을 추천할 수 있는 시스템을 만들어 보기로 했다. 이를 위해서 kaggle 의 데이터셋을 사용하였다.
코드 내용들은 깃허브에 있다.
https://github.com/GKooK/steam_game_recommend
GitHub - GKooK/steam_game_recommend
Contribute to GKooK/steam_game_recommend development by creating an account on GitHub.
github.com
과정
https://www.kaggle.com/tamber/steam-video-games 의 데이터에서 필요없는 값들을 제거한
https://www.kaggle.com/jwyang91/steam-games-2017-cleaned
Steam Games 2017 cleaned
www.kaggle.com
의 데이터셋을 사용하였다.
데이터셋은 위와 같이 유저가 어떤 게임을 몇시간 플래이했는지/구매했는지에 대한 정보가 나와있다.
처음에는 유저-유저 사이의 유사성을 구하기 위해서 행이User 이고 Column이 게임인 12388x5064 크기의 matrix를 만든 뒤 구성하는 값들의 코사인 유사도로 한 유저와 비슷한 성질을 가진 다른 유저를 추천하려고 하였으나, 게임을 보유하지 않은 유저의 데이터에 대해서는 0 의 값이 있고, 실질적으로 전체 게임 5064개에 달하는 게임들이 존재하는데, 한 유저의 입장에서 보면 100개의 게임을 보유하는것도 상당히 많은 게임을 보유했다고 볼 수 있다. 다만. 그로 인해서 각각의 User 행에 빈 공간이 많이 생겨 오히려 역으로 게임을 적게 보유한 유저들 간의 코사인 유사도가 높게 측정되는 문제점이 발생하였다. 이 당시에는 생각을 하지 못했는데 저렇게 12388x5064의 행렬에서 코사인 유사도로 값을 추측하는 것보다 12388x1 인 User x Play_Games 행렬을 만들고 각각의 유저에 대해서 (게임 플레이시간) * (게임이름) 의 string을 붙여서 tf-idf을 적용한 후 각 값들 끼리의 cosine 유사도를 구하는 방법도 있을 것 같다는 생각이 들었다.
그 생각을 하지 못해서 방법을 찾아보다가 행렬의 연산을 통하여 최적의 feature 들을 추출하고 그렇게 새로 만들게 된 오차가 가장 적은 matrix 에 들어 있는 값을 추천하는 방식을 보게 되었다.
이런 방식으로 각각의 Loss 값이 나올 때마다 정한 학습률만큼 각각의 행렬의 features를 업데이트하여 손실을 줄이는 방법이다. 위 사진에서 처음에는 U, G 행렬이 각각 임의의 값을 가지게 되지만 학습을 거듭하여 점점 그 값을 줄여 나가게 된다. 또한 그 과정에서 추출하는 features의 갯수들이 많을 수록 더 정확한 결과를 기대할 수 있지만 그만큼 시간이 오래 걸린다는 단점이 존재한다.
위 과정을 거치게 되면 우리는 각각 User-User 에 기반한 값들 중 유저와 관련된 내용과, 게임과 관련된 내용들의 feature 값들을 알아낼 수 있다.(단점으로는 각각 그 feature들이 무엇을 의미하는지는 알 수 없다.)
또한 여기서 그치지 않고 user-feature matrix에는 공백값이 거의 없기 떄문에 각각의 user 사이의 관계를
github에 존재하는 makeit_to_class 파일 안에 user-baised 시스템이 들어가 있다.
item-based system의 경우 게임과 관련된 정보 데이터들이 들어가 있다.
위와 같은 형식의 데이터들이 들어가 있다. 여기에 있는 데이터들 중 유의미하게 사용할 수 있는 데이터들을 살펴 보면,
url 은 한 게임의 고유한 값이 될 수 있다. 또한 steam 웹 사이트에서 store.steampowered.com/[상품type]/[게임번호] 만 입력을 해도 해당 사이트로 리다이렉트 되기 때문에 데이터의 크기를 늘리는 불필요한 부분을 삭제해야 된다고 생각을 하게 되었다. 또한 type에는 app, bundle, 또하나있었는데 기억이 안난다.. 같은 값들을 링크에 넣어 주어야 해서 남겨 두었고, 해당 타입들은 평가 방식이 조금씩 다르기 때문에 가져왔다. name항목도 게임을 비교할 수 있는 좋은 column이다. desc_snippet은 게임에 대한 설명인데 데이터셋에는 필요가 없다고 생각하여 가져오지 않았다. 게임의 평점과 직접적으로 관련된 recent_reviews, all_reviews가 있는데, 한정적인 정보를 제공하는 recent_reviews보다는 전체적인 정보를 제공하는 all_reviews들이 훨씬 나아 보여서 해당 항목을 채택하였다. 게임을 고를 때 게임의 개발자, 퍼블리싱 회사에 대한 선호도도 유저별로 상이하기 때문에 이것도 지표로 사용할 수 있었다. 그리고 게임 아이템에 있어서 서로 유사한지 안한지, 어디에 속해있는지을 알려 줄 수 있는 중요한 데이터인 popular_tags들이 있었다.
위 데이터를 보게 되면 704 번째 줄과 35169 번째 줄에 이상한 데이터값이 들어가 있으므로 이를 배제하고 진행하였다.
df = pd.read_csv('steam_games.csv', usecols=['url','types','name', 'all_reviews','developer', 'publisher', 'popular_tags'])
def change_url(link):
if('/bundle/' in link):
return str(link).split('/bundle/')[1].split('/')[0]
elif('/app/'in link):
return str(link).split('/app/')[1].split('/')[0]
elif('/sub/'in link):
return str(link).split('/sub/')[1].split('/')[0]
else:#curruent outlier is 704, 35169
print('stranger : '+link)
#해서 디버깅해본결과....types에 Other(240) 종류의 데이터들이 있었다.... 공백데이터를 삭제를 안한 것이다. 그래서 삭제해 주었다.
df = df.drop([704, 35169])
test =df['url'].apply(change_url)
df['url'] = test.astype('int')
또한 각각의 columns들에 대하여 nan값들에 대한 전처리도 필요하였다.
전처리를 마치게 되면 위와 같은 형태의 데이터 프레임을 볼 수 있으며, 사용하게 될 형식이다.
우선 all_reviews항목에 긍적적인 %를 가져오긴 했지만 신뢰할 수 있는 정보는 아니다. 아주 극 소수의 사용자들이 높은 평점을 준 경우도 있고 그 반대의 경우도 있기 때문에 100% 신뢰할 수 있는 정보는 아니다.
remove_things_list = test_sort[test_sort['num_reviews'] < 100].index
test_sort = test_sort.drop(remove_things_list)
그래서 위와 같은 코드를 통하여 reviews 수들이 너무 작아 신뢰도가 너무 떨어지는 데이터들에 대해서는 drop 처리를 진행해 주었다.
그래도 데이터에 존재하는 들쑥날쑥한 평가한 평점, 플레이한 유저 평점이 있어서 IMDB 에서 사용하는 rating 방식을 한번 게임으로 가져와 보았다.
https://www.quora.com/How-does-IMDbs-rating-system-work
How does IMDb's rating system work?
Answer (1 of 21): All the registered members of IMDb can cast their votes/ratings for any movie. IMDb takes all the individual votes cast by the registered users and uses them to calculate a single rating. But, they don’t use the arithmetic mean or media
www.quora.com
위의 링크 안에 있는 방식으로 영화를 평가하지만 우리의 경우 게임이 영화를 대체하기 때문에
mean_of_review_score = test_sort['all_reviews'].mean()
#m = 100 minimum votes required to be listed in the chart(평가가 최소 100이상인거사용)
#r = average score to the game
#c = mean of all scores
def imdb_score(val):
v = val['num_reviews']
r = val['all_reviews']
return (v/(v+100)*r + 100/(100+v)*mean_of_review_score)
#데이터프레임이 imdb스코어 영역 만들기(코드에서 오타났음)
test_sort['imbd_score'] = test_sort.apply(imdb_score, axis=1)
test_sort = test_sort.sort_values('imbd_score', ascending=False)#imdb 점수 순 정렬
위 python 코드와 같은 방법으로 데이터들을 산출해 낼 수 있다.
우선 각각의 평가 요소가 되는 것들에 대한 항목들을 합친 후 벡터화 진행을 한 뒤, cosine 유사도를 계산해야 한다. 합치는 과정에서 특정 키워드들이 더 많이 나오게 적절한 조정을 진행하여도 된다. 그런 방법이 있지만 우선 태그 만으로 평가를 진행하였다.
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.metrics.pairwise import linear_kernel, cosine_similarity
tfidf_vectorizer = TfidfVectorizer()
make_vector['popular_tags'] = make_vector['popular_tags'].apply(lambda x:str(x).replace(' ', '').lower())
data_to_vector = tfidf_vectorizer.fit_transform(make_vector['popular_tags'])
cosine_similarity = linear_kernel(data_to_vector, data_to_vector)
여러 단어들 간의 특징을 추출하기 위해서 tf-idf 방법을 통하여 벡터를 추출하였고, 그 추출한 벡터간의 코사인 유사도를 측정하였다.
make_vector.reset_index()
game_name = make_vector['name']
index_game_name = pd.Series(make_vector.index, index=make_vector['name'])
def output_by_game_name(name):
global cosine_similarity
sim_scores = list(enumerate(cosine_similarity[index_game_name[name]]))
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
sim_scores = sim_scores[1:]
games = [i[0] for i in sim_scores]
return game_name.iloc[games]
위 와 같은 함수를 만들어서 게임 이름으로 코사인 유사도가 비슷한 게임을 추천해주는 함수를 만들었다.
최종 결과물로는 맨 위에서 설명한 github에 나와 있는 Usage Example에서는 두 가지의 추천 시스템을 활용하여 최종적으로 user-based 에다가 item based를 추가한 종합 추천 시스템의 사용 예시를 확인할 수 있다.
'recommend system' 카테고리의 다른 글
item2vec 요약 (0) | 2022.07.06 |
---|---|
GNNExplainer 요약 (0) | 2022.06.27 |