꺼내먹는지식 준

MLP (neural)CF, AutoRec 코드 구현 간단 정리 본문

AI/추천시스템

MLP (neural)CF, AutoRec 코드 구현 간단 정리

알 수 없는 사용자 2022. 7. 26. 00:13

MLP NCF

neural collaborative filtering 에 맞는 형태로 데이터를 변환 

 

ratings_df = pd.read_csv(data_path + 'u.data', sep='\t', encoding='latin-1', header=None)
ratings_df.columns = ['user_id', 'movie_id', 'rating', 'timestamp']

user_ids = ratings_df['user_id'].unique()
movie_ids = ratings_df['movie_id'].unique()

ratings_matrix = ratings_df.pivot(index='user_id', columns='movie_id', values='rating')
#pivot 함수, index(각 행의 label), columns(각 열의 label), values

굉장히 유용한 df.pivot 함수  

한번에 column, index, 그리고 value 까지 원하는 값으로 정리 가능 

 

#implicit_feedback 
implicit_df = dict()
implicit_df['user_id'] = list()
implicit_df['movie_id'] = list()
implicit_df['implicit_feedback'] = list()


user_dict = dict()
movie_dict = dict()


for u, user_id in tqdm(enumerate(user_ids)):
    user_dict[u] = user_id 
    #연속적인 순서를 key, value 는 user_id 
    for i, movie_id in enumerate(movie_ids):
        if i not in movie_dict:
            movie_dict[i] = movie_id
        #user_dict, movie_dict 는 lookup table 느낌 
        implicit_df['user_id'].append(u)
        implicit_df['movie_id'].append(i)
        #implicit_df 의 user_id 와 movie_id 는 lookup table 의 key 값 
        if pd.isna(ratings_matrix.loc[user_id, movie_id]):
            #iloc 은 index, loc 은 이름 
            #여기서는 user_id, movie_id 이름으로 뽑아야 하니까 loc 
            implicit_df['implicit_feedback'].append(0)
            #Nan 인 경우에는 0
        else:
            implicit_df['implicit_feedback'].append(1)
            #본적 있는거면 점수에 상관없이 1

implicit_df 

user_id, movie_id 가 지저분해서 look up table 로 정리하기 위하여 enumerate 사용 

pd.isna 함수로 NaN을 0으로 치환 0, 1~5의 rating은 1 

train_X, test_X, train_y, test_y = train_test_split(
    implicit_df.loc[:, implicit_df.columns != 'implicit_feedback'], implicit_df['implicit_feedback'], test_size=0.2, random_state=seed,
    stratify=implicit_df['implicit_feedback']
)
#train_test_split(X,y, test_size, random_state, stratify)
 
implicit_df.columns
Index(['user_id', 'movie_id', 'implicit_feedback'], dtype='object')
implicit_df.columns != 'implicit_feedback'
array([ True,  True, False])

implicit_df.loc 에 어떻게 적용되는 건지에 대한 확인이 필요 

 

※pandas tip

- loc 은 index, column value 의 명칭 으로 indexing 

- iloc 은 index, column 의 index 로 indexing 

implicit_df.loc[:, ('user_id', 'movie_id')]

implicit_df.loc[:,[True, True, False]]

정리하자면, loc은 boolean 으로 indexing 이 가능하다. 

 

MLP에 넘겨주기 위하여 user, item embedding 을 concatenate 한다. 

 

MLP layer 를 선언하는 부분이 흥미롭다. 

self.mlp_layers = MLPLayers([2 * self.emb_dim] + self.layers, self.dropout) #layer: [256, 64]

다음과 같이 선언하면 추후, layer 의 개수에 변화를 주고 싶을 때 얼마든지 자유롭게 변화를 줄 수 있다. 

여기서는 [256, 64] 로 hidden의 크기가 줄어든다. 

(1024, 256) (256, 64)  (64, 1) 총 2개의 hidden 과 1개의 최종 layer 

 

parameter 초기화 

def _init_weights(self, module):
    if isinstance(module, nn.Embedding):
        normal_(module.weight.data, mean=0.0, std=0.01)
    elif isinstance(module, nn.Linear):
        normal_(module.weight.data, 0, 0.01)
        if module.bias is not None:
            module.bias.data.fill_(0.0)
self.apply(self._init_weights)

apply 는 sequential 로 연결된 모든 layer 를 한방에 recursive하게 초기화해준다. 

 

해당 방법이 익숙하지 않아서 왜인가 했더니 

파이토치에서는 기본적인 모듈 클래스(Linear, ConvNd 등) 를 초기화 할 때, 자동으로 파라미터를 적절히 초기화 해 주고 있고

또한 각 모듈 내의 reset_parameters() 메소드만 호출해 주면 어렵지 않게 모듈 파라미터를 초기화 할 수 있기 때문이다.

 

※ 참고

import torch
 
layer = torch.nn.Conv2d(1, 1, 2)
 
# Normal distribution
torch.nn.init.normal_(layer.weight)
 
# Xavier initialization
torch.nn.init.xavier_uniform_(layer.weight)
 
# Kaiming initialization
torch.nn.init.kaiming_uniform_(layer.weight)

 

아래와 같이 xavier 을 많이 사용한다. 

def weight_init_xavier_uniform(submodule):
    if isinstance(submodule, torch.nn.Conv2d):
        torch.nn.init.xavier_uniform_(submodule.weight)
        submodule.bias.data.fill_(0.01)
    elif isinstance(submodule, torch.nn.BatchNorm2d):
        submodule.weight.data.fill_(1.0)
        submodule.bias.data.zero_()

정리해보면, 

input_feature = torch.cat((user_e, item_e), -1)
mlp_output = self.mlp_layers(input_feature)
output = self.predict_layer(mlp_output)
output = self.sigmoid(output)

다음과 같이 user embedding, item embedding 을 concat 하고 mlp 통과시킨 후 sigmoid 로 얻은 output을 RMSE loss 를 사용한다. mlp 는 각 hidden layer 전에 dropout 을 넣기도 한다. 

 for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        # Compute prediction and loss
        pred = model(X)
        loss = loss_fn(pred, y)
        train_loss += loss.item()

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

 모델을 학습시킨다. 

사진 출처: https://glanceyes.tistory.com/entry/PyTorch-AutoGrad-Optimizer

optimizer.zero_grad() 와 loss.backward(), optimizer.step() 을 한번만 더 정리해본다. 

 

※ zero_grad() 

: 보통 딥러닝에서는 미니배치+루프 조합을 사용해서 parameter들을 업데이트한다. 
이 때 한 루프에서 업데이트를 위해 loss.backward()를 호출하면 각 파라미터들의 .grad 값에 변화도가 저장이 된다.

이후 다음 루프에서 zero_grad()를 하지않고 역전파를 시키면 이전 루프에서 .grad에 저장된 값이 다음 루프의 업데이트에도 간섭을 해서 원하는 방향으로 학습이 안된다. 

따라서 루프가 한번 돌고나서 역전파를 하기전에 반드시 zero_grad()로 .grad 값들을 0으로 초기화시킨 후 학습을 진행해야 한다.

 

그럼 이 기능은 왜 존재할까? 

gradients을 더해주는 방식은 RNN을 학습시킬때 매우 편리한 방식이라고 한다. 

 

loss.backward() , optimizer.step()

# parameters에 대한 gradients 구하기
# 직접 미분 수식을 입력해주는 것과 같은 효과 (Autograd)
loss.backward() # loss를 w(가중치)로 편미분한다.

# update parameters
# 원래의 weight/bias에서 lr * gradients를 뺀 값으로 업데이트
optimizer.step() # w = w - lr * grad

 

AutoRec 

대부분 상단에서 정리되어 추가적으로 언급할 내용이 적다. 

하지만 데이터 전처리에서 차이를 보이는데, MLP 의 경우 rating 을 1, 0 binary 처리하여 학습하였다. 

binary 로 처리한건 그냥 구현상 편리 같고, 0, 1, 2, 3, 4, 5 로 multi class classification 으로 풀어도 되지 않았나 싶다. (multi-class 는 cross-entropy)

 

AutoRec 은 말그대로 input을 embedding 하고 다시 복원하는 기법이다. 

user 가 평가하지 않은 영화의 NaN 은 0으로 치환 

다음과 같은 데이터를

이와 같은 형태로, 즉 rating 점수로 하여 train set, test set을 만든다. 

 

self.encoder =  nn.Linear(self.input_dim, self.emb_dim) # FILL HERE : USE nn.Linear() #
self.hidden_activation_function = activation_layer(hidden_activation)
self.decoder =  nn.Linear(self.emb_dim, self.input_dim)  # FILL HERE : USE nn.Linear() #
self.out_activation_function = activation_layer(out_activation)


def forward(self, input_feature):
    h = self.encoder(input_feature) # FILL HERE : USE self.encoder() # 128(batch), 1682  X 1682 512 
    h = self.hidden_activation_function(h)

    output = self.decoder(h) # FILL HERE : USE self.decoder() #
    output = self.out_activation_function(output)

    return output

들어온 input (batch_size, 1682) 를 512 크기로 embedding 한다. 

activation 은 relu 로 선택 

그 후 decoder 는 복원 한다. 

 

y_for_compute = y.clone().to('cpu')
index = np.where(y != 0) # FILL HERE : USE np.where & y_for_compute. WARNING: y를 사용 시, y의 device가 gpu일 경우 오류 발생 #
loss = self.loss_fn(pred[index], y[index])

복원한 값으로 loss 를 구한다. 

np.where 에서 조건을 만족할 때의 결과를 따로 정해주지 않으면 각 차원의 index 를 return 해주고 이를 편리하게 index 로 바로 사용할 수 있다. 

$$\underset{\theta}{min} \sum^{n}_{i=1} ||r^{(i)} - h(r^{(i)}; \theta) ||^2_{O}$$

 

여기까지 했을 때 이해가 잘 안가는 부분이 있다. 비단 Rating 값들을 embedding 하고 복원하는 과정이 어떤 의미가 있을까? 

정리해보자면, 각 유저가 평가한 영화에 대한 서로 다른 rating 이 input으로 들어가고, 각 유저의 rating 을 복원하는 과정에서 반복 학습이 되면 우선적으로 representation vector 가 잘 만들어질 것으로 보인다. 

그리고 약간 오염된 user data 가 복원이 잘 되도록 할 때도 사용할 수 있을 것 같다. (노이즈 낀 이미지 처럼)

Comments