DonHurry

[Python] Latent Factor Model 본문

Data Science

[Python] Latent Factor Model

_도녁 2022. 12. 12. 17:57

데이터 준비

우선 오늘 실습에 사용할 데이터입니다. 영화 평점 데이터셋으로, 아래 링크에서 다운 받으실 수 있습니다. 데이터의 크기가 다양하게 존재하는데, 오늘 사용할 데이터셋 크기는 100K로 작은 데이터를 활용하겠습니다.

 

MovieLens

GroupLens Research has collected and made available rating data sets from the MovieLens web site ( The data sets were collected over various periods of time, depending on the size of the set. …

grouplens.org

 

Colab에서 구글 드라이브 연결하기

코랩 환경에서 실습을 진행할 때, 구글 드라이브에서 데이터를 불러올 수 있습니다. 연결하려는 구글 드라이브에 미리 파일을 저장하면 됩니다. 파일이 큰 경우에는 코랩 상에서 !unzip 명령어를 사용하여 압축을 해제하는 것이 더 빠르지만, 파일이 작으니 그냥 압축을 먼저 해제하고 저장하시면 됩니다.

from google.colab import drive
drive.mount('/content/drive')

Mounted at /content/drive

 

코랩(주피터 노트북) 상에서 리눅스 명령어를 사용할 때는 '!'를 앞에 붙혀주시면 됩니다. ls 명령어를 통해 파일을 확인합니다.

!ls "/content/drive/MyDrive/ml-100k"

allbut.pl u1.base u2.test u4.base u5.test ub.base u.genre u.occupation

mku.sh u1.test u3.base u4.test ua.base ub.test u.info u.user

README u2.base u3.test u5.base ua.test u.data u.item

 

모듈 불러오기

먼저 앞으로 필요한 모듈들을 import 해줍니다.

import torch
import pandas as pd
import torch.nn.functional as F
import matplotlib.pyplot as plt

 

파일 불러오기

이제 파일을 불러올 차례입니다. 앞서 ls 명령어를 통해 살펴본 파일 중, ua.base와 ua.test를 활용하도록 하겠습니다.

파일의 형식은 csv로 pandas를 활용하여 불러올 수 있습니다. 이때 파일의 형식을 살펴보면 데이터의 구분자가 tab이고, 각 column명이 없습니다. 따라서 pd.read_csv로 불러올 때, sep과 names 함수 파라미터들을 사용합니다.

# 구분자가 tab임을 알려주어야 함
# 파일에 column 명이 없으므로 names 활용
train = pd.read_csv("/content/drive/MyDrive/ml-100k/ua.base", sep="\t", names=['user', 'movie', 'rating', 'timestamp'])
test = pd.read_csv("/content/drive/MyDrive/ml-100k/ua.test", sep="\t", names=['user', 'movie', 'rating', 'timestamp'])

 

다음으로 불러온 데이터를 pytorch tensor 형식으로 변환하도록 하겠습니다. 앞서 데이터를 train과 test셋으로 분류하였으므로, 여기서도 나누어서 변환해줍니다.

items = torch.tensor(train['movie'], dtype=torch.long)
users = torch.tensor(train['user'], dtype=torch.long)
ratings = torch.tensor(train['rating'], dtype=torch.float)

items_test = torch.tensor(test['movie'], dtype=torch.long)
users_test = torch.tensor(test['user'], dtype=torch.long)
ratings_test = torch.tensor(test['rating'], dtype=torch.float)

 

Latent Factor Model

Matrix Factorization을 진행하기 전에 사용자와 아이템에 관한 matrix 세팅을 먼저 해주어야 합니다. 잠재 벡터 크기를 rank라는 변수로 설정합니다. 예제에서는 10으로 진행하도록 하겠습니다.

rank = 10 # 사용자 vector, 아이템 vector의 차원

numItems = items.max() + 1 # 아이템 수
numUsers = users.max() + 1 # 사용자 수

# 0으로 초기화 하는 것보다 랜덤하게 초기화 하는 것이 학습이 잘 될 확률이 높음
P = torch.randn(numItems, rank, requires_grad=True) # 아이템 매트릭스
Q = torch.randn(numUsers, rank, requires_grad=True) # 사용자 매트릭스

 

생성된 매트릭스의 차원을 살펴보면 다음과 같습니다. 

print(P.shape, Q.shape)

(torch.Size([1683, 10]), torch.Size([944, 10]))

 

이제 본격적으로 학습을 진행합니다. optimizer는 Adam을 사용하며, 파라미터는 앞서 생성한 P와 Q를 이용하겠습니다. 앞서 수행한 실습들과 마찬가지로 가설 h와 비용 cost를 설정해줍니다. 아래 코드에서는 test도 함께 진행했는데, 실제로는 이런 식으로 진행하지는 않습니다. 보통 train과 test를 따로 수행합니다. 학습을 진행할수록 P와 Q의 파라미터가 손실함수를 줄이는 방향으로 조정됩니다.

optim = torch.optim.Adam([P, Q], lr = 0.1)

X = []
Y = []
Y_test = []

for epoch in range(1001):
  h = (P[items] * Q[users]).sum(dim=1)
  cost = F.mse_loss(h, ratings)

  optim.zero_grad()
  cost.backward()
  optim.step()

  with torch.no_grad():
    X.append(epoch)
    Y.append(cost.item())

    # 실제로 이런 식으로 코딩 하진 않음
    h_test = (P[items_test] * Q[users_test]).sum(dim=1)
    cost_test = F.mse_loss(h_test, ratings_test)
    Y_test.append(cost_test.item())

    if epoch % 100 == 0:
      print(f"epoch: {epoch}, cost: {cost.item()}")

epoch: 0, cost: 23.919946670532227

epoch: 100, cost: 0.5669679641723633

epoch: 200, cost: 0.49337249994277954

epoch: 300, cost: 0.4697325825691223

epoch: 400, cost: 0.4582059681415558

epoch: 500, cost: 0.45214635133743286

epoch: 600, cost: 0.4487079083919525

epoch: 700, cost: 0.4463438093662262

epoch: 800, cost: 0.4446922540664673

epoch: 900, cost: 0.443502277135849

epoch: 1000, cost: 0.44253602623939514

 

시각화를 진행해봅니다. train에서는 cost가 매우 작은 수로 수렴하는 것을 확인할 수 있지만, test 데이터에서는 점점 cost가 높아지는 것을 확인할 수 있습니다. 이는 파라미터들이 지나치게 train 데이터셋에 오버피팅되었음을 의미합니다.

plt.figure(dpi=100)
plt.plot(X, Y, label="Train MSE")
plt.plot(X, Y_test, label="Test MSE")
plt.legend()
plt.show()

 

Regularization

오버피팅을 해결하는 방법 중 하나는 정규화를 시켜주는 것입니다. 이번에는 학습과 시각화 모두 한 코드블럭으로 진행해보겠습니다. 기존에는 cost를 backward시켰지만, 이번에는 loss라는 변수를 만들고 cost에 특정 값을 더해줍니다.

 

P와 Q를 제곱해서 mean 혹은 sum을 해서 기존 cost 값에 더해주는데, 이는 튀는 값들을 없애주기 위함입니다. 파라미터들이 특정 데이터에 과적합되어 있어 일부 값이 특출나게 튀어있다면, 새로운 데이터가 들어왔을 때 성능이 좋지 않습니다. 때문에 아래와 같은 과정을 통해 파라미터의 튀는 값들을 없애는 방향으로 학습을 진행시키는 것입니다. 시각화한 결과를 보면 test 데이터셋에서도 성능이 좋게 나오는 것을 확인할 수 있습니다.

P = torch.randn(numItems, rank, requires_grad=True) # 아이템 매트릭스
Q = torch.randn(numUsers, rank, requires_grad=True) # 사용자 매트릭스

lambda1 = 1
lambda2 = 1
optim = torch.optim.Adam([P, Q], lr = 0.1)

X = []
Y = []
Y_test = []

for epoch in range(1001):
  h = (P[items] * Q[users]).sum(dim=1)
  cost = F.mse_loss(h, ratings)
  # sum을 하든 mean을 하든 결과는 같음
  # 튀는 값들을 사라지게 하기 위함이므로
  loss = cost + lambda1 * (P**2).mean() + lambda2 * (Q**2).mean()

  optim.zero_grad()
  loss.backward()
  optim.step()

  with torch.no_grad():
    X.append(epoch)
    Y.append(cost.item())

    h_test = (P[items_test] * Q[users_test]).sum(dim=1)
    cost_test = F.mse_loss(h_test, ratings_test)
    Y_test.append(cost_test.item())

    if epoch % 100 == 0:
      print(f"epoch: {epoch}, cost: {cost.item()}, test_cost: {cost_test.item()}")


print()

plt.figure(dpi=100)
plt.plot(X, Y, label="Train MSE")
plt.plot(X, Y_test, label="Test MSE")
plt.legend()
plt.show()

epoch: 0, cost: 23.172462463378906, test_cost: 21.258317947387695

epoch: 100, cost: 0.6601173281669617, test_cost: 1.007965326309204

epoch: 200, cost: 0.6048451066017151, test_cost: 1.0196946859359741

epoch: 300, cost: 0.5932756662368774, test_cost: 1.0203676223754883

epoch: 400, cost: 0.5900246500968933, test_cost: 1.0199319124221802

epoch: 500, cost: 0.5887171030044556, test_cost: 1.019777536392212

epoch: 600, cost: 0.5881035327911377, test_cost: 1.0199692249298096

epoch: 700, cost: 0.5878186225891113, test_cost: 1.020143747329712

epoch: 800, cost: 0.5876771807670593, test_cost: 1.0202783346176147

epoch: 900, cost: 0.5876007676124573, test_cost: 1.0204029083251953

epoch: 1000, cost: 0.5875604748725891, test_cost: 1.02051842212677

 

 

Bias 추가

앞서 시각화한 결과들을 보면 초반에 급격하게 꺾이는 부분이 매끄럽지 못하다는 것을 알 수 있습니다. 이는 편향값을 추가함으로써 해결할 수 있습니다. 방법은 앞선 정규화 방법과 비슷합니다. 다만, 편향 파라미터 매트릭스를 따로 생성해주어야 합니다. 또한 편향은 가설 h에도 더해주어야합니다.

P = torch.randn(numItems, rank, requires_grad=True) # 아이템 매트릭스
Q = torch.randn(numUsers, rank, requires_grad=True) # 사용자 매트릭스

bias_item = torch.randn(numItems, requires_grad=True)
bias_user = torch.randn(numUsers, requires_grad=True)
mean = ratings.mean()

lambda1 = 1
lambda2 = 1
lambda3 = 1
lambda4 = 1

optim = torch.optim.Adam([P, Q, bias_item, bias_user], lr = 0.1)

X = []
Y = []
Y_test = []

for epoch in range(1001):
  h = (P[items] * Q[users]).sum(dim=1) + mean + bias_item[items] + bias_user[users]
  cost = F.mse_loss(h, ratings)
  loss = cost + lambda1 * (P**2).mean() + lambda2 * (Q**2).mean() + lambda3 * (bias_user**2).mean() + lambda4 * (bias_item**2).mean()

  optim.zero_grad()
  loss.backward()
  optim.step()

  with torch.no_grad():
    X.append(epoch)
    Y.append(cost.item())

    h_test = (P[items_test] * Q[users_test]).sum(dim=1) + mean + bias_item[items_test] + bias_user[users_test]
    cost_test = F.mse_loss(h_test, ratings_test)

    Y_test.append(cost_test.item())

    if epoch % 100 == 0:
      print(f"epoch: {epoch}, cost: {cost.item()}, test_cost: {cost_test.item()}")


print()

plt.figure(dpi=100)
plt.plot(X, Y, label="Train MSE")
plt.plot(X, Y_test, label="Test MSE")
plt.legend()
plt.show()

epoch: 0, cost: 13.782000541687012, test_cost: 10.609686851501465

epoch: 100, cost: 0.608058750629425, test_cost: 0.9282452464103699

epoch: 200, cost: 0.5786076784133911, test_cost: 0.9187856316566467

epoch: 300, cost: 0.5731916427612305, test_cost: 0.9131919741630554

epoch: 400, cost: 0.5717451572418213, test_cost: 0.9107112288475037

epoch: 500, cost: 0.5712419152259827, test_cost: 0.9093411564826965

epoch: 600, cost: 0.571007251739502, test_cost: 0.9086859226226807

epoch: 700, cost: 0.5708647966384888, test_cost: 0.9083752632141113

epoch: 800, cost: 0.5707736611366272, test_cost: 0.9081287980079651

epoch: 900, cost: 0.5707151293754578, test_cost: 0.9077973961830139

epoch: 1000, cost: 0.5706698298454285, test_cost: 0.9073463678359985

 

 

'Data Science' 카테고리의 다른 글

[Python] PageRank  (1) 2022.12.15
[Python] PCA (Principal Component Analysis)  (0) 2022.12.14
[Python] KNN (K-Nearest Neighbors)  (0) 2022.12.13
[Python] Clustering  (0) 2022.12.12
[Python] Linear Regression  (2) 2022.11.07