Factorization machine 모델(이하 FM)에 대하여 알아보고, Pytorch로 구현해보도록 하겠습니다.
논문 내용 및 수식정리
추천서비스를 만들때, 주로 user-item interaction(rating) matrix를 사용합니다. 행이 유저가 되고 열이 컨텐츠가 되어 특정 인덱스의 값은 컨텐츠에 대한 유저의 선호도 또는 평점을 나타내는 데이터가 됩니다.
기존 MF모델에서는 이러한 데이터형태를 많이 사용하였습니다. 하지만 이에 대한 한계가 존재하는데, 유저와 컨텐츠 수가 많아질 수록 인수분해시 연산량이 기하급수적으로 커집니다. 그리고 유저와 컨텐츠의 메타데이터(성별, 상품의 카테고리, 지역 등등의 추가정보)를 사용할 수 없습니다.
FM 모델은 이러한 단점을 해결할 수 있는데, 결론부터 말하자면 FM의 특정 형태가 MF와 동일합니다.
FM 모델은 유저와 컨텐츠 데이터를 원핫인코딩을 이용하여 표현하여 학습시키는데 위의 데이터에서 한 행렬값이, FM모델에서는 하나의 Row로서 표현이 됩니다. 따라서 데이터가 이전 데이터보다 훨씬 더 Sparse한 matrix가 됩니다.
FM 모델의 수식은 아래와 같습니다
풀어써보면 아래와 같이 긴 수식이 됩니다.
여기서 w값을 추정하기 위한 Vi와 Vj벡터의 내적으로 표현할 수 있습니다. MF 모델에서 user, item latent벡터로 쪼개는 것과 유사합니다.
세번째 interaction term을 정리하면 결론적으로 아래처럼 정리가 됩니다.
위의 수식을 활용하여 pytorch로 FM 모델을 구현해보도록 하겠습니다
FM 모델 구현
데이터 다운로드 및 데이터셋정의
!wget https://files.grouplens.org/datasets/movielens/ml-100k.zip
!tar -xvf ml-100k.zip
import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score
import tqdm
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
class MovieLensDataset(Dataset):
def __init__(self, path):
data = pd.read_csv(path, sep="\t").values
self.items = data[:, :2].astype(np.int32) - 1 # -1 because ID begins from 1
self.targets = self.__preprocess_target(data[:, 2]).astype(np.float32)
self.field_dims = np.max(self.items, axis=0) + 1
self.user_field_idx = np.array((0, ), dtype=np.int32)
self.item_field_idx = np.array((1,), dtype=np.int32)
def __len__(self):
return self.items.shape[0]
def __getitem__(self, index):
return self.items[index], self.targets[index]
def __preprocess_target(self, target):
# return target
target[target <= 3] = 0
target[target > 3] = 1
return target
모델 구현 및 학습 로직
class FM(torch.nn.Module):
def __init__(self, field_dims, embed_dim):
super().__init__()
self.embedding = torch.nn.Embedding(sum(field_dims), embed_dim)
self.fc = torch.nn.Embedding(sum(field_dims), 1)
self.bias = torch.nn.Parameter(torch.zeros((1,)))
def forward(self, x):
square_of_sum = torch.sum(self.embedding(x), dim=1) ** 2
sum_of_square = torch.sum(self.embedding(x) ** 2, dim=1)
ix = 0.5 * (square_of_sum - sum_of_square)
ix = torch.sum(ix, dim=1, keepdim=True)
linear_part = torch.sum(self.fc(x).squeeze(2), dim=1, keepdim=True)
x = self.bias + linear_part + ix
return torch.sigmoid(x.squeeze(1))
def train(model, optimizer, data_loader, criterion, device, log_interval=100):
model.train()
total_loss = 0
tk0 = tqdm.tqdm(data_loader, smoothing=0, mininterval=1.0)
for i, (fields, target) in enumerate(tk0):
fields, target = fields.to(device), target.to(device)
y = model(fields)
loss = criterion(y, target)
model.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
if (i + 1) % log_interval == 0:
tk0.set_postfix(loss=total_loss / log_interval)
total_loss = 0
def test(model, data_loader, device):
model.eval()
targets, predicts = [], []
with torch.no_grad():
for i, (x, y) in enumerate(data_loader):
x, y = x.to(device), y.to(device)
y_hat = model(x)
targets.extend(y.tolist())
predicts.extend(y_hat.tolist())
return roc_auc_score(targets, predicts)
데이터 로딩 및 데이터 로더
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
dataset = MovieLensDataset("./ml-100k/u.data")
train_length = int(len(dataset) * 0.8)
valid_length = int(len(dataset) * 0.1)
test_length = len(dataset) - train_length - valid_length
train_dataset, valid_dataset, test_dataset = torch.utils.data.random_split(
dataset, (train_length, valid_length, test_length)
)
train_data_loader = DataLoader(train_dataset, batch_size=32)
valid_data_loader = DataLoader(valid_dataset, batch_size=32)
test_data_loader = DataLoader(test_dataset, batch_size=32)
field_dims = dataset.field_dims
학습
model = FM(field_dims, 16)
print(model.__class__.__name__)
criterion = torch.nn.BCELoss()
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01, weight_decay=1e-5)
for epoch_i in range(10):
train(model, optimizer, train_data_loader, criterion, device)
auc = test(model, valid_data_loader, device)
test_auc = test(model, test_data_loader, device)
print("test auc:", test_auc)
참고문서
- 논문: https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf
- 모델 : https://github.com/rixwew/pytorch-fm/blob/master/torchfm/model/fm.py
- 논문설명: https://hongl.tistory.com/242
읽어주셔서 감사합니다 :)