RNN 코드구현

SIHUN·2024년 1월 21일
0

NLP

목록 보기
3/7

이전 포스트에서 RNN,LSTM,GRU 모델에 대한 개념을 학습했습니다. 이번 포스트는 RNN,LSTM,GRU의 모델을 파이토치 코드로 구현해보겠습니다.


import torch
import torch.nn as nn

class MyRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()

        self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.h2o = nn.Linear(hidden_size, output_size)

    def forward(self, input, hidden):
        combined = torch.cat((input,hidden), 1)
        hidden = torch.tanh(self.i2h(combined))
        output = self.h2o(hidden)
        return output, hidden
    
    def get_hidden(self):
        return torch.zeros(1, self.hidden_size)

파이토치로 RNN을 구현하면 위의 코드와 같습니다. 이 코드를 분석해 보겠습니다.

class MyRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()

        self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.h2o = nn.Linear(hidden_size, output_size)

먼저 torch.nn의 nn.Module을 상속 받습니다. hidden_layer의 사이즈를 선언해 주고, input_layer와 hidden_layer를 fully connected로 이어주고, 다시 hidden_layer와 output_layer를 fully connected로 이어줍니다.

def forward(self, input, hidden):
        combined = torch.cat((input,hidden), 1)
        self.hidde = torch.tanh(self.i2h(combined))
        output = self.h2o(hidden)
        return output, hidden

이제 forward 함수를 정의해 줍니다. 이 부분이 학습을 진행하는 부분입니다. forward 함수는 input tensor아 hidden tensor를 받아 하나의 차원으로 합쳐준 뒤, input_layer에서 hidden_layer로 이어주는 fully connected layer를 i2h를 적용합니다. 그 다음 그 결과에 tanh활성화 함수를 통과하게 만들어 줍니다. 그럼 그 값이 hidden state가 됩니다.

그리고 그 hidden state를 h2o를 적용하면 그 값이 output이 됩니다.

    def get_hidden(self):
    	self.hidden = torch.zeros(1, self.hidden_size)
        return self.hidden

일반적인 초기 hidden state는 내부가 0입니다. 이를 표현하기 위해 get_hidden함수를 만들어 초기 hidden_size의 값을 모두 0으로 선언해 줍니다. 그리고 이런 state return해 레이어 밖에서 다룰 수 있게 해줍니다.

이제 앞에서 선언한 MyRNN을 사용해 보겠습니다.

rnn = MyRNN(input_size=4, hidden_size=4, output_size=2)
hidden = rnn.get_hidden()

rnn이라는 객체를 input_size는 4, hidden_size는 4, output_size는 2로 선언해줍니다. 그 다음 초기 hidden_state를 get_hidden함수로 선언해 줍니다.

만약 우리가 분석하고자 하는 task가 문장의 감성분석이고, 주어진 문장이 This pizza is good이라면

_, hidden = rnn(input_tensor0, hidden) #This
_, hidden = rnn(input_tensor1, hidden) #pizza
_, hidden = rnn(input_tensor2, hidden) #is
output_tensor3, _ = rnn(input_tensor3, hidden) #good

이런 식으로 구성됩니다.
This에 해당하는 단어 벡터가 input으로 들어오고 첫번째 output과 hidden_state를 갱신하고 두번째 단어 벡터와 첫번째 hidden_state를 받아 두번째 output과 hidden을 갱신합니다. 이런 방식으로 학습이 진행되며 good에 해당하는 단어 벡터가 주어지고 output이 나오면 학습이 종료됩니다.

그렇다면 이제 이 코드를 가지고 이름을 가지고 성별을 예측하는 task를 수행해 보겠습니다.

import pandas as pd

df = pd.read_csv('./deepLearning/rnn/name_gender_filtered.csv')
unique_chars = set()

for name in df['Name']:
    unique_chars.update(name)
unique_chars = sorted(list(unique_chars))
unique_chars = ''.join(unique_chars)
print(unique_chars)
     

먼저 이름데이터와 성별데이터가 존재하는 데이터 파일을 불러와 존재하는 알파벳을 찾습니다.


n_letters = len(unique_chars)
def nameToTensor(name):
    tensor = torch.zeros(len(name), n_letters)
    for char_idx, char in enumerate(name):
        letter_idx = unique_chars.find(char)
        assert letter_idx != -1, f"char is {name}, {char}"
        tensor[char_idx][letter_idx] = 1
    return tensor

gen2num = {'F':0, 'M':1}
num2gen = {0:'F', 1:'M'}

이제 알파벳에 해당하는 정수를 원-핫 인코딩 해주고, 성별에 해당하는 정수를 인코딩해줍니다.


import torch
from torch.optim import Adam, SGD

loss_fn = nn.CrossEntropyLoss()
optimizer = Adam(rnn.parameters(), lr=0.0001)

n_hidden = 32
rnn_model = MyRNN(n_letters, n_hidden, 2)

rnn_model.train()
for epoch_idx in range(200):
    shuffled_df = df.sample(frac=1).reset_index(drop=True)

    total_loss = 0.
    correct_predictions = 0
    total_predictions = 0

    for index, row in shuffled_df.iterrows():
        input_tensor = nameToTensor(row['Name'])
        target_tensor = torch.tensor([gen2num[row['Gender']]], dtype=torch.long)

        hidden = rnn.get_hidden()

        rnn.zero_grad()

        for char_idx in range(input_tensor.size()[0]):
            char_tensor = input_tensor[char_idx]
            output, hidden = rnn_model(char_tensor[None,:], hidden)

        loss = loss_fn(output, target_tensor)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        predicted_index = torch.argmax(output, 1)
        correct_predictions += (predicted_index == target_tensor).sum().item()
        total_predictions += 1

    average_loss = total_loss / total_predictions
    accuracy = 100 * correct_predictions / total_predictions
    print(f'Epoch: {epoch_idx}, Loss: {average_loss:.4f}, Accuracy: {accuracy:.2f}%')

이제 input_size로 알파벳의 개수를 넣어주고, hidden_size는 32로 구성해 학습을 진행합니다.


test_name = 'elsa'
test_name_tensor = nameToTensor(test_name)

rnn_model.eval()
hiddne = rnn_model.get_hidden()
for char_idx in range(len(test_name)):
    char_tensor = test_name_tensor[char_idx]
    output, hidden = rnn(char_tensor[None,:],hidden)
predicted_index = torch.argmax(output, 1).item()
print(num2gen[predicted_index])

[output] F

이제 학습된 모델을 평가모드로 바꿔주고 분류하고자 하는 이름 데이터를 넣어줍니다.

self.hidden_size = hidden_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.h2o = nn.Linear(hidden_size, output_size)
    def forward(self, input, hidden):
        combined = torch.cat((input, hidden), 1)
        hidden = torch.tanh(self.i2h(combined))
        output = self.h2o(hidden)
        return output, hidden

이 부분은 RNN의 모델을 파이토치로 직접 구현한 것으로 nn.RNN, nn.LSTM을 사용하면 더욱 쉽게 파이토치 모델을 구성할 수 있습니다.

refer https://github.com/NoCodeProgram/deepLearning/blob/main/rnn/genderClassification.ipynb

0개의 댓글