이전 포스트에서 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