사용 데이터
MovieLens 영화 데이터 -> ml-latest-small.zip -> movies.csv, ratings.csv를 가공 -> ratings_updated.p, genres.p 생성
https://grouplens.org/datasets/movielens/
<초기 세팅>
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
ratings = pd.read_pickle('[파일 경로]/ratings_updated.p')
genres = pd.read_pickle('[파일 경로]/genres.p')
ratings_updated.p: 기존 ratings.csv 형식과 똑같다. 단순히 개인적으로 데이터 몇 가지를 추가한 것이라 ratings.csv를 그대로 사용해도 무방하다. 어떤 유저가(userId) 어떤 영화에 대해(movieId) 어떤 평점을(rating) 주었는지에 대한 데이터이다.
genres.p: movies.csv의 각 영화별(movieId) 어떤 장르가 있는지에 대해 추출한 데이터이다. 다음 코드를 사용했다.
movies = pd.read_csv('[파일 경로]/movies.csv', index_col='movieId')
movies['genres'].str.get_dummies(sep='|')
Content-Based Recommendation
실습의 목표는 어떤 유저가(userId) 어떤 영화에 대해(movieId) 어떤 평점을(rating) 줄 것인지를 해당 영화의 장르 정보(genre)에 따라 예측하는 모델을 만드는 것이다.
따라서 필요한 정보는 두 가지이다.
- 영화에 대한 장르 정보 - ex. 영화 기생충은 코미디, 스릴러 장르를 가지고 있다. (item_profile)
- 장르에 대한 유저의 평점 정보 - ex. 유저1은 코미디 장르에 대해 평점 3.5를 줄 것이다. (user_profile)
1번 영화에 대한 장르 정보(item_profile)는 이미 genres(genres.p)에 담겨져 있어서 패스
2번 장르에 대한 유저의 평점 정보(user_profile)는 ratings(ratings_updated.p)가 특정 영화에 대한 평점만 보여주기 때문에, ratings와 genres를 적절히 활용해 예측해야 한다.
item_profile, user_profile 데이터가 완성되었다면, 이제 원하는 예측을 할 수 있다.
item_profile을 기반으로 특정 영화가 장르1, 장르2, ...에 속하는 것을 확인하고 해당 유저가 장르1, 장르2, ...에 대해서 줄 평점을 user_profile에서 찾아 이를 적절히 섞어 계산하면 된다.
이제 중요한건 user_profile을 추출하는 방법이다.
유저 1이 코미디 장르에 대해 준 평점의 평균을 계산해서 유저1 - 코미디 장르의 데이터로서 사용하는 방법 등 여러가지 방법이 있지만 여기서는 Linear Model을 활용하겠다.
Linear Model
컨텐츠 베이스 추천을 위해 필요한 정보 중 user_profile 데이터를 확보하기 위해 Linear Model을 사용하기로 했다.
그렇다면 Linear Model이란 무엇일까? 아주 간단히 짚고 넘어가자.
다음 예시를 보자.
왼쪽 테이블은 각 영화에 대한 유저의 평점 정보(회색)와 해당 영화의 장르 정보(노란색)이다.
실습용 데이터로 치면 회색은 ratings이고, 노란색은 genres인 셈이다. 이를 적절히 활용해 오른쪽 표인 user_profile을 추출해야 한다.
그런데 오른쪽 표가 이상하게 보일 것이다. user_profile은 유저가 특정 장르에 대해 줄 평점에 대한 데이터가 있어야 하는데 웬 알파, 베타..? 아래 표처럼 나와야 하는 것이 아닐까?
해당 표는 특정 장르에 대해 특정 유저가 매긴 평점의 평균을 사용해서 계산해낸 표이다. 하지만 지금은 Linear Model을 사용할 것이기 때문에 표의 형태가 다르다.
그럼 이 표는 어떻게 해석해야 할까? 어려워 보이지만 바로 위의 표와 딱히 다를 것이 없다. b1, b2, b3가 각 장르를 나타내는 계수이고, a는 절편(상수)라고 생각하면 된다.
각 장르에 대한 독립 변수를 x1, x2, x3라고 했을 때, 최종적으로 유저가 특정 영화에 대해 줄 예상 평점 y를 선형 회귀 방정식으로 계산하는 것이다.
즉, b1, b2, b3는 각 장르에 대한 가중치라고 보면 된다. 만약 x1 장르에 대해 유저가 평점을 낮게 부여하는 경향이 있다면 b1은 음수쪽으로 가중치를 둘 수 있는 것이다.
그럼 또 의문점이 생긴다. a, b1, b2, b3의 값은 어떻게 찾을 수 있을까?
유저 '조'의 MSE가 가장 적게 나올 수 있는 a, b1, b2, b3를 찾는 것이다.
MSE는 예측한 값과 실제 환경에서 관찰되는 값의 차이를 다룰 때 흔히 사용하는 측도이다. 자세한 내용은 아래 링크를 참고하자.
https://gksdudrb922.tistory.com/169
즉, 조의 예상 평점을 방정식 계산을 통해 계속 대입하면서 실제 평점과의 차이가 가장 낮게 나오는 a, b1, b2, b3를 계산해서 user_profile에 등록한다.
지금은 세 장르에 대해서 고려했기 때문에 독립 변수가 3개이지만 실제 genres데이터로 계산했을 때의 user_profile은 다음과 같이 계산된다.
MSE를 통해 절편과 계수를 구하는 과정을 간단히 설명하자면 아래와 같이 x값에 대한 y값을 나타내는 데이터가 있을 때, 이 데이터들의 특징을 가장 잘 나타내는 선형 회귀 직선을 찾아내는 과정인 것이다.
+) 참고
실제로 Linear Model을 사용하기 위해서는 몇 가지 가정이 필요하다.
- 독립 변수와 종속 변수간에 선형적인 구조를 가진다.
- 독립 변수는 서로 완벽히 독립적이어야 한다. (서로 영향을 주지 않는다)
그러나 현재 데이터에서 영화 장르 간에 서로 완벽히 영향을 주지 않다고 보기 힘들고, 우리가 실생활에서 사용하는 여러 데이터들 역시 위 조건을 완벽히 만족하는 경우는 드물다.
그러나 Linear Model은 어떤 가정을 사용하고 있고, 현실적으로는 어떤 부분을 감수하고 사용하는지 정도로만 알아두자.
Scikit learn을 통한 Linear Model 구현
이제 이론을 학습했으니 실제 컨텐츠 베이스 추천을 위한 코딩을 해보자.
PreProcessing
먼저 각 영화에 대한 평점과 장르 정보를 합치기 위해 merge() 함수를 사용해서 ratings와 genres를 합친다.
ratings = ratings.merge(genres, left_on='movieId', right_index=True)
ratings.sample()
<결과>
Train Test Split
실습의 목적은 평가 데이터(Test)의 사용자(user)들이 영화(movie)에 대해 어떤 평점(rating)을 부여할 것인지 예측하는 것이다.
- 예측 데이터(predict)는 이미 존재하는 학습 데이터(train)를 기반으로 만들고,
- 평가 데이터(test)와 예측 데이터(predict) 간의 MSE를 통해 얼마나 잘 예측했는지 확인해볼 것이다.
Sckikit learn 라이브러리를 통해 전체 데이터를 학습 데이터(train)와 평가 데이터(test)로 분리한다.
from sklearn.model_selection import train_test_split
train, test = train_test_split(ratings, test_size=0.1, random_state=42)
print(train.shape)
print(test.shape)
//Result
(90767, 4)
(10086, 4)
전체 데이터(ratings)를 적당한 비율(test_size)로 학습 데이터(train)와 평가 데이터(test)로 분리한다.
+) random_state를 부여하는 이유는 매번 같은 규칙으로 섞도록 하기 위함이다. 즉, 매번 split할 때마다 학습, 평가 데이터가 달라지는 것이 아닌 고정될 수 있도록 한다. state 값은 어떤 것을 사용해도 무방하다.
결과를 보면 학습 데이터(train)는 90%, 평가 데이터(test)는 10% 비율로 데이터가 split된 것을 볼 수 있다.
user_profile 계산
이제 user_profile을 만들어보자. Scikit learn 라이브러리는 복잡한 Linear Model 계산을 쉽게 하는 함수를 제공하고 있다.
1. 전체 유저에 대해 반복문을 돌리면서,
2. X_train은 Linear Model에서 독립 변수에 해당하고, 장르 정보를 담고 있다.
y_train은 Linear Model에서 종속 변수에 해당하고, 평점 정보를 담고 있다.
3. LinearRegeression라이브러리의 fit 함수는 독립 변수와 종속 변수를 인자로 받아 간편하게 a, b1, b2, b3, ... 값을 계산해준다.
코드에서 reg.intercept_가 바로 절편(a)이고, reg.coef_가 바로 계수(b1, b2, b3, ...)이다.
4. 절편과 계수를 user_profile_list에 차례대로 저장한다.
*reg.coef_ : reg.coef 자체가 리스트 형태이기 때문에 intercept와 병합하면서 flat한 형태의 리스트를 만들기 위함.
from sklearn.linear_model import LinearRegression
user_profile_list = []
for userId in train['userId'].unique():
user = train[train['userId'] == userId]
X_train = user[genres.columns] # feature, X
y_train = user['rating'] # label, y
reg = LinearRegression()
reg.fit(X_train, y_train)
user_profile_list.append([reg.intercept_, *reg.coef_])
리스트 형태인 user_profile_list를 Pandas의 DateFrame 형태로 변경한다.
user_profile = pd.DataFrame(user_profile_list,
index=train['userId'].unique(),
columns=['intercept', *genres.columns])
# 실수 값이 이상하게 보이는 경우 아래 코드 입력
pd.set_option('float_format', '{:f}'.format)
<결과 - user_profile>
이제 원하는 데이터를 얻게 되었다. 각 유저에 대해서 영화에 대한 평점 y를 계산하기 위한 절편과 계수 정보인 user_profile을 얻었다.
평점 예측하기
앞서 train 데이터를 통해 구한 user_profile을 기반으로 test 데이터의 평점을 예측하고 실제 평점과의 MSE를 계산해서 잘 예측이 되었는지 확인해보자.
1. test.iterrows()는 test 데이터의 인덱스와 row 한 줄의 전체 정보를 불러온다.
즉, row['userId'], row['rating']처럼 각 컬럼에 대한 정보에 접근할 수 있다.
2. tqdm_notebook라이브러리는 반복문의 진행 상황에 대해 볼 수 있는 라이브러리이다. 코드 실행시간이 오래 걸리는 경우에 유용하다.
3. intercpet(절편)와 각 계수의 합(genre_score)을 계산하고 이 둘을 더해 y값(expected_score)을 계산한다.
b1x1 + b2x2 + b3x3 + ... 에서 독립 변수 값이 실습에서는 1또는 0이기 때문에 단순히 계수의 합을 통해 계산한 것이다.
4. 예측 평점을 test 컬럼에 추가한다.
from tqdm import tqdm_notebook
predict = []
for idx, row in tqdm_notebook(test.iterrows()):
user = row['userId'] # test row에 user
intercept = user_profile.loc[user, 'intercept'] # 해당 user의 user_profile에서 intercept
genre_score = sum(user_profile.loc[user, genres.columns] * row[genres.columns]) # 해당 movie의 장르에서 비롯되는 예상 점수
expected_score = intercept + genre_score
predict.append(expected_score)
test['predict'] = predict
<결과 - test>
장르가 많아서 잘 안보이지만 오른쪽 끝에 predict 컬럼이 추가되었다. 실제 평점인 rating 컬럼과 비교해보자.
MSE 계산
rmse = np.sqrt(mean_squared_error(test['rating'], test['predict']))
// 결과
0.9744844537311346
실제 평점과 예측 평점은 평균적으로 0.97정도의 차이가 있다. 어느 정도의 예측은 성공했지만 그리 좋은 예측이라고는 하기 힘들다.
문제점?
이론까지 공부해가며 정교하게 예측한 이 Linear Model의 문제점은 무엇일까?
바로 데이터의 과적합(Overfitting)이다.
만약 어떤 유저는 2000개가 넘는 영화에 평점을 주고, 어떤 유저는 5개도 안되는 영화에 평점을 주었다면, 두 유저간에 같은 예측 모델을 사용해도 되는 것일까?
데이터 개수가 적은 user에 대해서는 계수 계산이 정확하지 않을 것이다. 또한, train 데이터를 통해 계산한 Linear Model은 train 데이터에만 적합한 모델이다. 전혀 다른 데이터인 test 데이터를 설명할 때는 적합하지 않다.(과적합)
해결책은?
- 빈도수가 낮은 장르 없애기
- correlation이 높은 장르들을 합치기
- 데이터가 적은 user에 대해서 전체 평균, user 별 평균, 등 다른 방법들을 섞어서 풀기 (앙상블)
- 정규화가 가능한 linear model 사용하기 (Ridge, Lasso 등)
다양한 해결책이 있지만 여기서는 정규화, 그 중에서도 Lasso 모델을 사용하겠다.
정규화
먼저 정규화에 대해 알아보자. 다만, 실제로 정규화를 적용했을 때, MSE가 줄어드는지를 확인하는 것이 실습의 목적이기 때문에 자세한 이론은 다루지 않겠다.
쉽게 말하면 빨간 그래프를 초록 그래프로 변경하는 것이다.
빨간 그래프는 train 데이터에 굉장히 적합한 데이터이다. 그러나 데이터가 없는 구간은 제대로 표현하지 못하고 있다. 단순히 점과 점 사이의 구간의 데이터가 이럴 것이다 라고 예측하는 것일 뿐이다.
이를 방지하기 위해 x와 y의 값의 가장 일반적인 형태로 나타내기 위해 초록 그래프로 변경해 가는 것을 정규화라고 한다. 실제로 초록 그래프가 빨간 그래프에 비해 점의 흐름을 조금 더 잘 표현하고 있다.
정규화를 위해서는 선형 방정식의 절편과 계수를 계산할 때, Regularization Term이라는 항을 추가로 더해준다.
정규화 방식은 제 1정규화, 제 2정규화 방식이 있는데, 바로 Regularization Term을 어떤 것을 가져가느냐에 따라 달라진다.
특히, 제 1 정규화 방식의 Regularization Term은각 계수들의 절댓값의 합에 람다값을 곱해준 형태를 취하는데, 이 람다값을 조정해 가면서 MSE가 최소로 나오는 절편과 계수를 도출해 내는 것이다.
이렇게 제 1 정규화 방식을 사용하는 모델을 Lasso 모델이라고 하며, 실습에서는 Lasso 모델을 사용해 정규화를 진행해볼 것이다.
Scikit learn을 통한 Lasso 모델 구현
앞선 과정과 별 다를 것 없다. 단순히 계산 모델을 Lasso 라이브러리로 교체해주면 된다.
이 때, 인자로 alpha 값을 부여해야 하는데 이는 Regularization Term의 람다값에 해당한다. 여기서는 0.03을 주었다.
from sklearn.linear_model import Lasso
# 유저 프로필 만들기
user_profile_list = []
for userId in train['userId'].unique():
user = train[train['userId'] == userId]
X_train = user[genres.columns] # feature, X
y_train = user['rating'] # label, y
reg = Lasso(alpha=0.03) # 이 부분만 변경
reg.fit(X_train, y_train)
user_profile_list.append([reg.intercept_, *reg.coef_])
user_profile_lasso = pd.DataFrame(user_profile_list,
index=train['userId'].unique(),
columns=['intercept', *genres.columns])
<결과 - user_profile_lasso>
이전 Linear Model로 계산한 user_profile에 비해 0 값이 더 많아진 것을 볼 수 있다. 이는 데이터 양이 적어 예측이 힘들 것 같은 장르에 대해서는 Lasso가 알아서 데이터를 탈락시키기 때문이다.
이제 예측 평점을 구하고, MSE를 계산해보자.
predict = []
for idx, row in tqdm_notebook(test.iterrows()):
user = row['userId'] # test row에 user
intercept = user_profile_lasso.loc[user, 'intercept'] # 해당 user의 user_profile에서 intercept
genre_score = sum(user_profile_lasso.loc[user, genres.columns] * row[genres.columns]) # 해당 movie의 장르에서 비롯되는 예상 점수
expected_score = intercept + genre_score
predict.append(expected_score)
test['predict_lasso'] = predict
rmse = np.sqrt(mean_squared_error(test['rating'], test['predict_lasso']))
//결과
0.9194325167613792
일반적인 Linear Model에 비해 MSE가 확실이 줄어들었다.
지금은 람다값을 전체 0.03으로 고정을 했는데, 각 유저별로 적합한 람다값을 다르게 해서 더욱 보완할 수도 있다.
'python > data analysis' 카테고리의 다른 글
[Google Colab] Github에 push 하기 (2) | 2022.05.19 |
---|---|
[Scikit learn] 학습데이터, 평가 데이터 평균제곱근 편차(RMSE) 계산 (0) | 2021.08.26 |
[Pandas] concat - 데이터 이어 붙이기 (0) | 2021.08.26 |
[Pandas] 영화 평점 데이터 분석 (0) | 2021.08.26 |
[Pandas] 멱함수 분포 (0) | 2021.08.26 |