[핸즈온 추천시스템] Neural Collaborative Filtering (NCF)란?

chrisjune
10 min readJun 19, 2023

머신러닝 알고리즘인 Neural Collaborative Filtering(NCF)에 대해 알아보고 Pytorch를 활용하여 구현해봅니다.

이 모델은 스트리밍 플랫폼이나 다양한 이커머스에서 적용되었습니다. 이 모델을 제안한 논문은 무려 5천번이 넘게 인용되었습니다. (https://arxiv.org/abs/1708.05031)

Neural Collaborative Filtering(NCF) 알고리즘이란?

전통적인 협업 필터링 기법은 주로 행렬분해에 기반을 두고 있습니다. 이러한 기법들은 사용자-아이템 상호작용(interaction)을 행렬 형태로 표현한 후 이 행렬을 두개이상의 행렬로 “분해”하여 평점이나 선호도를 예측하는 방식으로 동작합니다.

2017년에 제안된 Neural Collaborative Filterinv(NCF) 알고리즘은 이러한 접근 방식에 신경망을 결합하여 성능을 높혔습니다. 행렬분해 모델에 비선형 함수를 통합하여 NCF는 사용자-아이템 상호작용간 복잡한 구조를 표현함으로써, 행렬분해 기법의 선형결합 한계를 극복할 수 있게 되었습니다.

NCF 모델의 주요 구성 요소

NCF모델은 두 가지 구성 요소로 이루어져 있습니다.

  1. 일반화 행렬 인수분해(Generalized Matrix Factorization, GMF): 전통적인 행렬 인수분해 방법을 사용합니다. 사용자-아이템 상호작용의 선형구조를 표현합니다.
  2. 다층 퍼셉트론 (Multi-Layer Perceptron, MLP): 여러개의 Layer로 더 복잡한 구조인 비선형성 구조를 학습할 수 있습니다.

두 요소의 출력은 하나로 연결하여 출력층에 적용됩니다.

모델 프레임워크

NCF Framework

Generalized Matrix Factorization(GMF) Layer

r^ = a ( w ( q (*) p))

  • a: 활성화 함수
  • w: Weights
  • q, p: 각각 사용자와 상품의 임베딩 벡터

이러한 식으로 정의할 수 있는데, 여기서 활성화 함수(a)를 Identity matrix와 가중치(w)를 값이 1인 벡터로 가정하면 MF모델과 완전히 동일합니다.

Multi-Layer Perceptron(MLP) Layer

r^ = a(wx + b)

  • a: 활성화 함수
  • w: Weights
  • x: 사용자와 상품이 합쳐진 임베딩
  • b: biases

사용자와 상품 두개의 임베딩 레이어를 가로로 붙여 하나의 임베딩 레이어로 만듭니다.

perceptron은 기본적으로 y = a(wx + b)의 모델이기에 동일하게 임베딩레이어의 weight matrix, bias vector, activation function이 layer별로 각각 필요합니다. 활성화 함수로는 tanh, relu, sidmoid 등등 선택이 가능합니다.

Fusion of GMF and MLP Layer

두 모델을 결합하기 위하여 두 레이어를 Concatenate하고 출력 레이어는 0 또는 1로 예측하기 위하여 Sigmoid 레이어를 사용합니다.

Loss function, Optimizer
- Binary cross entropy loss와 Adam optimizer를 사용합니다.

Pytorch를 활용한 NCF 모델 구현

Pytorch 라이브러리를 사용해 NCF모델을 구현하겠습니다. 구현된 모델은 아래와 같습니다.

class NCF(torch.nn.Module):
def __init__(self, config):
super().__init__()
self.config = config

self.num_users = config["num_users"]
self.num_items = config["num_items"]
self.latent_dim_mf = config["latent_dim_mf"]
self.latent_dim_mlp = config["latent_dim_mlp"]

# Input
self.embedding_user_mlp = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mlp)
self.embedding_item_mlp = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mlp)
self.embedding_user_mf = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mf)
self.embedding_item_mf = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mf)

# Layer
self.fc_layers = torch.nn.ModuleList()
for idx, (in_size, out_size) in enumerate(zip(config["layers"][:-1], config["layers"][1:])):
self.fc_layers.append(torch.nn.Linear(in_size, out_size))

# Output
self.last_layer = torch.nn.Linear(in_features=(config["layers"][-1] + self.latent_dim_mf), out_features=1)
self.output_layer = torch.nn.Sigmoid()

def forward(self, user_indices, item_indices):
user_embedding_mlp = self.embedding_user_mlp(user_indices)
item_embedding_mlp = self.embedding_item_mlp(item_indices)
user_embedding_mf = self.embedding_user_mf(user_indices)
item_embedding_mf = self.embedding_item_mf(item_indices)

# GMF
gmf_layer = torch.mul(user_embedding_mf, item_embedding_mf)

# MLP
mlp_concat_layer = torch.cat([user_embedding_mlp, item_embedding_mlp], dim=-1)
for idx in range(len(self.fc_layers)):
mlp_concat_layer = self.fc_layers[idx](mlp_concat_layer)
mlp_concat_layer = torch.nn.ReLU()(mlp_concat_layer)

# Concatenate
neu_mf_layer = torch.cat([gmf_layer, mlp_concat_layer], dim=-1)

# Output
return self.output_layer(self.last_layer(neu_mf_layer)).view(-1)

해당 Implicit data의 negative sampling과 같은 전처리는 건너뛰고 userId, itemId, rating(0 또는 1) 컬럼이 포함된 dataset이 준비되어있다고 가정합니다.

class TrainDataset(Dataset):
def __init__(self, df):
self.users = df["userId"].values
self.items = df["itemId"].values
self.ratings = df["rating"].values.astype(np.float32) # Convert to float

def __len__(self):
return len(self.users)

def __getitem__(self, idx):
return self.users[idx], self.items[idx], self.ratings[idx]

def train_model(model, config, data):
# Loss / Optimizer
loss_fn = torch.nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

for epoch in range(num_epochs):
model.train()

train_data, test_data = train_test_split(data)
train_loader = DataLoader(TrainDataset(train_data, batch_size=config['batch_size'])
test_loader = DataLoader(TrainDataset(test_data, batch_size=config['batch_size'])
for user, item, rating in train_loader:
output = model(user, item)
loss = loss_fn(output, rating)
loss.backward()

optimizer.step()
optimizer.zero_grad()
print(f"LOSS:{loss.item()}")

# Train model
df = pd.read_csv("your_data_has.csv")

config = {
"num_users": df.userId.max() + 1,
"num_items": df.itemId.max() + 1,
"latent_dim_mf": 8,
"latent_dim_mlp": 8,
"layers": [16, 32, 16, 8],
"num_epochs": 10,
"learning_rate": 1e-3,
"batch_size": 1024,
}

model = NCF(config)
model = model
train_model(model, config, df)

위 스크립트는 NCF모델의 가장 중요한 부분만 구현한 모델이고, loss function에 정규화를 추가하거나, MLP Layer에 Dropout을 추가하거나 하이퍼파라미터를 튜닝하여 모델의 성능을 더 높일 수 있습니다.

NCF 모델은 많은 추천서비스의 Baseline으로 사용하기 좋은 MF모델이 선형결합만 가능한 한계를 딥러닝의 비선형모델과 앙상블하여 성능을 끌어 올렸습니다.

오늘 포스팅이 도움이 되셨다면 👏🏻 부탁드립니다.

--

--