PyTorch를 활용한 협업필터링¶
이번 포스팅에서는 행렬분해를 사용한 추천시스템 문제를 풀어보려 한다. 그리고 더 일반적인 산경망 모델을 사용해 해당문제를 풀어보겠다.
MovieLens 데이터셋은 영화 추천에 자주 활용되는 유명한 데이터셋이다. 이 데이터셋에는 영화 추천 시스템 내의 별점 5점 만점의 평점데이터와 자유 텍스트 태깅 정보가 들어있다. 총 9125개의 영화에 대한 100004개의 평점과 1296개의 태그가 기록되어 있다.
영화 데이터는 https://grouplens.org/datasets/movielens/ 에서 내려받을 수 있다.
# wget을 사용해 데이터를 내려받는다
!wget http://files.grouplens.org/datasets/movielens/ml-latest-small.zip
!ls ml-latest-small.zip
# 윈도우에서는 GUI를 사용해 압축을 풀어주면 된다.
!unzip ml-latest-small.zip
!ls ml-latest-small/
MovieLens 데이터셋¶
# 패키지 import
from pathlib import Path
import pandas as pd
import numpy as np
data = pd.read_csv('./ml-latest-small/ratings.csv')
data.head()
데이터 인코딩¶
인코딩은 보통 기다란 벡터에 한개만 표시하는 One-Hot Encoding으로 많이 알려져 있지만, 이런 경우에는 범주형 데이터 (Categorical Data)를 표현하는데 쓰이기도 한다. 우리는 사용자의 id와 영화의 id를 인코딩으로 만들어 줄 것이다.
# 먼저 데이터를 Train과 Validation데이터로 나눈다
np.random.seed(3)
msk = np.random.rand(len(data)) < 0.8
train = data[msk].copy()
val = data[~msk].copy()
print(train.head())
print(val.head())
# 다음은 Pandas의 컬럼을 범주형의 id로 인코드해주는 함수이다
def proc_col(col, train_col=None):
""" Encodes a pandas column with continous ids. """
# Unique한 row를 찾는다 즉 사용자 혹은 영화이다
if train_col is not None:
uniq = train_col.unique()
else:
uniq = col.unique()
# 사용자/영화를 인덱스와 매핑해준다
name2idx = {o:i for i,o in enumerate(uniq)}
# 그리고 그것을 포맷팅해서 리턴한다
return name2idx, np.array([name2idx.get(x, -1) for x in col]), len(uniq)
# 다음은 실제로 데이터를 인코딩으로 만들어주는 함수이다
# 위에서 정의해준 proc_col을 사용한다
def encode_data(df, train=None):
""" Encodes rating data with continous user and movie ids.
If train is provided, encodes df with the same encoding as train.
"""
df = df.copy()
for col_name in ["userId", "movieId"]:
train_col = None
if train is not None:
train_col = train[col_name]
_,col,_ = proc_col(df[col_name], train_col)
df[col_name] = col
df = df[df[col_name] >= 0]
return df
# Test와 Validation인코딩을 만들어준다
df_train = encode_data(train)
df_val = encode_data(val, train)
print(df_train.head())
print(df_val.head())
임베딩 레이어¶
이제 PyTorch의 nn모듈에 있는 Embedding레이어를 이용해 임베딩을 만들어보겠다.
import torch
import torch.nn as nn
import torch.nn.functional as F
# 아래의 임베딩모델은 최대 10명의 사용자나 3개의 아이템에 대한 관계를 표현한다
# 임베딩의 숫자들은 랜덤으로 초기화 된다
embed = nn.Embedding(10, 3)
# 10명까지의 id이니 6개를 넣어준다
a = torch.LongTensor([[1,2,0,4,5,1]])
embed(a)
행렬분해 모델¶
아래는 PyTorch의 딥러닝을 제외한 기능을 사용해 만든 행렬분해 모델이다. 임베딩 두개의 행 즉 벡터들을 각각 내적곱(Dot Product)을 하는 역할을 하는 모델이다.
class MatrixFactorization(nn.Module):
def __init__(self, num_unsers, num_items, emb_size=100):
super().__init__()
self.user_emb = nn.Embedding(num_users, emb_size)
self.item_emb = nn.Embedding(num_items, emb_size)
# 정규분포를 사용해 임베딩을 초기화한다
self.user_emb.weight.data.uniform_(0, 0.05)
self.item_emb.weight.data.uniform_(0, 0.05)
def forward(self, u, v):
u = self.user_emb(u)
v = self.item_emb(v)
return (u*v).sum(1)
위의 모델을 설명하기 위해 모델을 사용하지 않고 모델이 하는 일을 절차적으로 작성해 보겠다. 아래는 학습 즉 Training단계가 없는 절차이다.
# 임의의 테스트 데이터를 만들어본다
# 사용자는 6명 영화는 3개로 이루어져있다
# 사용자와 영화 평점을 각 행렬에 넣고 Pandas DF를 생성
users = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 6]
movies = [0, 1, 1, 2, 0, 1, 0, 3, 0, 3, 3, 1, 3]
ratings = [4, 5, 5, 3, 4, 4, 5, 2, 1, 4, 5, 1, 3]
columns = ['userId', 'movieId', 'rating']
test_df = pd.DataFrame(np.asarray([users, movies, ratings]).T, columns=columns)
test_df
num_users = len(test_df.userId.unique())
num_items = len(test_df.movieId.unique())
emb_size = len(test_df.columns)
print("num_users: {}, num_items: {}, emb_size: {}".format(num_users, num_items, emb_size))
users = torch.LongTensor(test_df.userId.values)
items = torch.LongTensor(test_df.movieId.values)
user_emb = nn.Embedding(num_users, emb_size)
item_emb = nn.Embedding(num_items, emb_size)
print("embeddings: {}, {}".format(user_emb,item_emb))
U = user_emb(users)
V = item_emb(items)
# 임베딩의 각 행을 Dot Product해준다
# 아래의 텐서는 각 행의 결과이니 총 13개가 된다
(U * V).sum(1)
행렬분해 모델 학습¶
이제 실제 모델 클래스를 사용해서 행렬분해모델을 학습시킨다. 아래의 예제에서는 임베딩의 사이즈를 100개로 했는데, 이것은 사용자들과 영화들의 관계 즉 평점정보에서 100개의 특성을 뽑아낸다는 것의 의미이다.
# 이제 실제 학습 데이터의 사용자와 영화의 갯수를 구한다.
num_users = len(df_train.userId.unique())
num_items = len(df_train.movieId.unique())
num_users, num_items
# 행렬분해 모델을 만든다
# 임베딩(특성)의 갯수는 100개로 한다
model = MatrixFactorization(num_users, num_items, emb_size=100)
model
def validation_loss(model, unsqueeze=False):
model.eval()
users = torch.LongTensor(df_val.userId.values)
items = torch.LongTensor(df_val.movieId.values)
ratings = torch.FloatTensor(df_val.rating.values)
if unsqueeze:
ratings = ratings.unsqueeze(1)
y_hat = model(users, items)
loss = F.mse_loss(y_hat, ratings)
print("validation loss {:.3f}".format(loss.item()))
def train_mf(model, epochs=10, lr=0.01, wd=0.0, unsqueeze=False):
optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
model.train()
for i in range(epochs):
users = torch.LongTensor(df_train.userId.values)
items = torch.LongTensor(df_train.movieId.values)
ratings = torch.FloatTensor(df_train.rating.values)
if unsqueeze:
ratings = ratings.unsqueeze(1)
y_hat = model(users, items)
loss = F.mse_loss(y_hat, ratings)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(loss.item())
validation_loss(model, unsqueeze)
train_mf(model, epochs=10, lr=0.1)
train_mf(model, epochs=15, lr=0.01)
train_mf(model, epochs=15, lr=0.01)
참고자료
아래의 원본 소스를 기반으로 번역 및 개인 의견을 덧붙인 코드입니다.
'데이터사이언스 > PyTorch' 카테고리의 다른 글
PyTorch 5 - 딥러닝 기반 협업필터링 (0) | 2019.07.22 |
---|---|
PyTorch 3 - 데이터 로더, 신경망 (0) | 2019.07.13 |
PyTorch 2 - 로지스틱 회귀 (0) | 2019.07.11 |
PyTorch 1 - 텐서 (0) | 2019.07.09 |