Ssul's Blog

[추천시스템#7] 딥러닝 추천 본문

AI & ML

[추천시스템#7] 딥러닝 추천

Ssul 2024. 1. 15. 22:51

0. 딥러닝을 어떻게 추천시스템에?

- 딥러닝을 통해서, 이미지를 classification하고,

- 문장을 입력해서 감성을 분석하는 등

- 이미 딥러닝을 통해서 다양한 것이 된다는 것을 확인했다.

- 그렇다면 추천시스템도 가능하지 않을까? 당연히 가능하다. 아이디어는 넣을수 있는 것은 모두(?) 신경망에 넣고, 아웃풋으로 rating을 주는 모델을 학습.

- 기존의 머신러닝이 feature extraction(engineering)과 classification으로 구분되었다면, 딥러닝은 신경망에 넣으면 알아서 feature extraction+classification 진행된다. 그 사례를 이번 딥러닝 추천에서도 잘 보여줄 것이다.

 

1. 딥러닝 추천시스템 구조

- 잘 알려져 있는 무비렌즈 데이터를 사용합니다.

- user_id와 movie_id를 적절한 차원으로 임베딩 합니다.

- 이외에 추천에 영향을 줄것이라고 판단되는 데이터들을 임베딩 합니다.

- 임베딩된 데이터들을 Concat으로 일렬로 합쳐줍니다.

- 해당 벡터를 신경망의 input으로 입력합니다.

- 신경망을 구성하고, overfitting을 막기 위해서 적절하게 Dropout을 추가합니다.

- 최종값으로 rating을 예측하게 하고, rating정보를 가지고 학습하여, 신경망 파라미터를 업데이트 합니다.

 

 

2. user_id, movie_id만 사용한 딥러닝 추천시스템

import pandas as pd
import numpy as np

# csv 파일에서 불러오기
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('./u.data', names=r_cols,  sep='\t',encoding='latin-1')
ratings = ratings[['user_id', 'movie_id', 'rating']].astype(int)            # timestamp 제거

# train test 분리
from sklearn.utils import shuffle
TRAIN_SIZE = 0.75
ratings = shuffle(ratings, random_state=12)
cutoff = int(TRAIN_SIZE * len(ratings))
ratings_train = ratings.iloc[:cutoff]
ratings_test = ratings.iloc[cutoff:]

- ratings는 user_id, movie_id, rating 구조

- 해당 데이터 전체를 랜덤하게 섞고,

- 75%는 train data로, 25%는 test data로 분류

 

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Embedding, Flatten
from tensorflow.keras.layers import Dense, Concatenate, Activation
from tensorflow.keras.regularizers import l2
from tensorflow.keras.optimizers import SGD, Adamax
from tensorflow.keras.callbacks import ModelCheckpoint

# Variable 초기화 
K = 350                             # Latent factor 수
reg = 0.0001                        # Regularization penalty
mu = ratings_train.rating.mean()    # 전체 평균 
N = len(set(ratings.user_id)) + 1   # Number of users
M = len(set(ratings.movie_id)) + 1  # Number of movies

- K는 임베딩 차원으로 350차원 > user_id, movie_id는 350차원의 벡터로 임베딩 됩니다.

- N은 user수, M은 movie수, 임베딩시 독립된 데이터의 갯수가 필요함

 

 

# Keras model
user = Input(shape=(1, ))                                               # User input
item = Input(shape=(1, ))                                               # Item input
P_embedding = Embedding(N, K, embeddings_regularizer=l2(reg))(user)     # (N, 1, K)
Q_embedding = Embedding(M, K, embeddings_regularizer=l2(reg))(item)     # (M, 1, K)
user_bias = Embedding(N, 1, embeddings_regularizer=l2(reg))(user)       # User bias term (N, 1, )
item_bias = Embedding(M, 1, embeddings_regularizer=l2(reg))(item)       # Item bias term (M, 1, )

# Concatenate layers
P_embedding = Flatten()(P_embedding)                                    # (K, )
Q_embedding = Flatten()(Q_embedding)                                    # (K, )
user_bias = Flatten()(user_bias)                                        # (1, )
item_bias = Flatten()(item_bias)                                        # (1, )
R = Concatenate()([P_embedding, Q_embedding, user_bias, item_bias])     # (2K + 2, )

- 신경망에 넣을 최종 Input 벡터를 구성코드

- P_embedding = Embedding(N, K, embeddings_regularizer=l2(reg))(user): user_id를 K차원으로 임베딩 합니다.

- user_bias = Embedding(N, 1, embeddings_regularizer=l2(reg))(user): user 임베딩 바이어스 1개를 추가

- movie_id도 동일하게 진행합니다.

- concat을 위해 flatten을 진행합니다.

- R = Concatenate()([P_embedding, Q_embedding, user_bias, item_bias]): 신경망에 입력할 벡터가 완성. 350+350+1+1 해서 702 사이즈

 

# Neural network
R = Dense(2048)(R)
R = Activation('gelu')(R)
R = Dense(1024)(R)
R = Activation('LeakyReLU')(R)
R = Dense(512)(R)
R = Activation('linear')(R)
R = Dense(1)(R)

model = Model(inputs=[user, item], outputs=R)
model.compile(
  loss=RMSE,
  #optimizer=SGD(lr=0.003, momentum=0.9),
  optimizer=Adamax(lr=0.0002),
  metrics=[RMSE],
)
model.summary()

- 신경망을 구성하는 코드입니다.

- 2048노드-gelu-1024-LeakyReLU-512-linear-1 구성: 702(user_id, movie_id)입력 > 2048-1024-512 > 1(rating)

- learning rate=0.0002설정, Adamx설정

 

checkpoint_path = 'CheckPoint'
checkpoint = ModelCheckpoint(checkpoint_path, 
                             save_best_only=True, 
                             save_weights_only=True, 
                             monitor='val_RMSE', 
                             verbose=1)

# Model fitting
result = model.fit(
  x=[ratings_train.user_id.values, ratings_train.movie_id.values],
  y=ratings_train.rating.values - mu,
  callbacks=[checkpoint],
  epochs=15,
  batch_size=128,
  validation_data=(
    [ratings_test.user_id.values, ratings_test.movie_id.values],
    ratings_test.rating.values - mu
  )
)

- 모델 저장 체크포인트 설정합니다.

- 학습을 시작합니다.

- 입력으로 user_id, movie_id 값이 입력됩니다.

- y는 rating의 평균값으로 정규화 하여 사용합니다.

- epoch은 15, 배치사이즈는 128로 설정

val_RMSE가 0.91661

 

학습을 마친 신경망에 임의의 user_id, movie_id를 입력하면, rating을 예측할 수 있습니다.

 

3. user_id, movie_id 그리고, 다른 요소(user의 직업정보)도 함께 입력하기

Etc에 user의 직업정보가 임베딩 되어 신경망에 입력됩니다.

import pandas as pd
import numpy as np

# csv 파일에서 불러오기
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('./u.data', names=r_cols,  sep='\t',encoding='latin-1')
ratings = ratings[['user_id', 'movie_id', 'rating']].astype(int)            # timestamp 제거
u_cols = ['user_id', 'age', 'sex', 'occupation', 'zip_code']
users = pd.read_csv('./u.user', sep='|', names=u_cols, encoding='latin-1')
users = users[['user_id', 'occupation']]

# train test 분리
from sklearn.utils import shuffle
TRAIN_SIZE = 0.75
ratings = shuffle(ratings, random_state=12)
cutoff = int(TRAIN_SIZE * len(ratings))
ratings_train = ratings.iloc[:cutoff]
ratings_test = ratings.iloc[cutoff:]

# Convert occupation(string to integer)
# { 직업명: 번호 매기기 }
occupation = {}
def convert_occ(x):
    # 이미 dict에 기록했다면, 해당 번호 리턴
    if x in occupation:
        return occupation[x]
    # dict에 없던 번호이면, 신규번호 생성후, 부여
    else:
        occupation[x] = len(occupation)
        return occupation[x]
users['occupation']

users['occupation'] = users['occupation'].apply(convert_occ)
# 21개 직업
L = len(occupation)

- 직업정보를 가져오기 위하여, u.user파일을 가져옵니다

- 나머지는 기존 코드와 동일

- convert_occ함수를 통해서, 직업별로 공유한 번호를 부여합니다

- 직업정보를 임베딩하기 위해서, 직업의 독립적인 갯수 확인: 데이터에는 총 21개의 직업이 있음

- 기존과 동일하게 75%, 25%로 train/test데이터 분류 합니다

 

 

# user_id기준으로 병합 > 그중 occupation만 뽑기
train_occ = pd.merge(ratings_train, users, on='user_id')['occupation']
test_occ = pd.merge(ratings_test, users, on='user_id')['occupation']

- user_id기준으로 직업정보를 합치고, 직업정보를 가져옵니다.

 

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Embedding, Flatten
from tensorflow.keras.layers import Dense, Concatenate, Activation
from tensorflow.keras.regularizers import l2
from tensorflow.keras.optimizers import SGD, Adam, Adamax
from tensorflow.keras.callbacks import ModelCheckpoint

# Variable 초기화 
K = 350                             # Latent factor 수 
reg = 0.002                         # Regularization penalty
mu = ratings_train.rating.mean()    # 전체 평균 
N = len(set(ratings.user_id)) + 1   # Number of users
M = len(set(ratings.movie_id)) + 1  # Number of movies

# Defining RMSE measure
def RMSE(y_true, y_pred):
    return tf.sqrt(tf.reduce_mean(tf.square(y_true - y_pred)))

# Keras model
user = Input(shape=(1, ))
item = Input(shape=(1, ))
P_embedding = Embedding(N, K, embeddings_regularizer=l2(reg))(user)     # (N, 1, K)
Q_embedding = Embedding(M, K, embeddings_regularizer=l2(reg))(item)     # (M, 1, K)
user_bias = Embedding(N, 1, embeddings_regularizer=l2(reg))(user)       # User bias term (N, 1, )
item_bias = Embedding(M, 1, embeddings_regularizer=l2(reg))(item)       # Item bias term (M, 1, )

# Concatenate layers
P_embedding = Flatten()(P_embedding)
Q_embedding = Flatten()(Q_embedding)
user_bias = Flatten()(user_bias)
item_bias = Flatten()(item_bias)

 - 기존 코드와 동일하게, user_id, movie_id를 임베딩

 

occ = Input(shape=(1, ))
# 직업 latent space 3으로 지정
occ_embedding = Embedding(L, 3, embeddings_regularizer=l2())(occ)
occ_layer = Flatten()(occ_embedding)
R = Concatenate()([P_embedding, Q_embedding, user_bias, item_bias, occ_layer])

- 직업정보는 3차원으로 입베딩합니다.

- concat을 통해서, user임베딩(350), user bias(1), movie임베딩(350), movie bias(1), 직업임베딩(3) 합쳐서 최종 input벡터를 구성(350+1+350+1+3=705)

 

 

# Neural network
R = Dense(4096)(R)
R = Activation('gelu')(R)
R = Dropout(0.2)(R)
R = Dense(2048)(R)
R = Activation('LeakyReLU')(R)
R = Dropout(0.2)(R)
R = Dense(512)(R)
R = Activation('linear')(R)
R = Dense(1)(R)

model = Model(inputs=[user, item, occ], outputs=R)
model.compile(
  loss=RMSE,
  #optimizer=SGD(lr=0.003, momentum=0.9),
  optimizer=Adamax(lr=0.0002),
  metrics=[RMSE]
)
model.summary()

- 신경망을 구성합니다.

- 기존과 다르게 Dropout을 추가

 

 

checkpoint_path = 'CheckPoint'
checkpoint = ModelCheckpoint(checkpoint_path, 
                             save_best_only=True, 
                             save_weights_only=True, 
                             monitor='val_RMSE', 
                             verbose=1)

# Model fitting
result = model.fit(
  x=[ratings_train.user_id.values, ratings_train.movie_id.values, train_occ.values],
  y=ratings_train.rating.values - mu,
  callbacks=[checkpoint],
  epochs=15,
  batch_size=128,
  validation_data=(
    [ratings_test.user_id.values, ratings_test.movie_id.values, test_occ.values],
    ratings_test.rating.values - mu
  )
)

- 입력에서, 직업정보를 입력해줍니다.

- 나머지는 동일합니다.

val_RMSE가 0.91133으로 성능 개선 확인할 수 있음

 

4. 딥러닝 추천 정리

- 개념은 간단했다. input에 들어갈 데이터를 적절한 차원으로 임베딩하여, 신경망에 입력할 수 있게 만든다.

- 신경망은 원하는 형태로 구성하고, rating을 예측하는 것으로 학습한다.

- 적절한 하이퍼파라미터를 설정한다(예: 임베딩차원, Dropout비율, 러닝레이트 등등)

- 추천에 영향을 준다고 판단되는 데이터는 적절하게 뽑아서, 적절한 차원으로 임베딩하여 input데이터로 구성한다