티스토리 뷰

▶ RNN을 위한 PyTorch 구현

 

RNN 학습 과정

 

t-1 시점의 hidden state와 t 시점의 input vector를 받음 → 학습 → 나온 결과물로 output vector를 도출
RNN 학습 ← U, W, V 이 3가지의 weight를 학습을 하는 것

 

Text generation

 

  • 이번 글에서 해볼 학습은 Text generation 입니다.
  • 예를 들어 'hello'라는 단어가 있다고 했을 때, 모델의 input으로 'h', 'e', 'l', 'l', 'o' 하나하나씩 들어오게 됩니다.
  • 이때 input이 'h'일 경우 output은 그 다음 글자 'e'가 됩니다. 
  • 'e'가 input으로 들어오게 되면 output은 'l'이 되야겠죠?
  • 이러한 input과 output을 알고 있다고 했을 때 이와 관련된 파라미터들을 학습하게 되는 구조입니다.

text generation

  • 이때 각각의 input 들을 원-핫 인코딩을 통해서 임베딩을 하게 되고,
  • hidden state들이 업데이트 되게 되는데
  • 각각의 hidden layer에서는 이전 단계의 hidden state와 임베딩이 된 input 값을 바탕으로 해서 학습을 하고
  • softmax를 통해 최종적으로 결과물을 얻어내는 형태의 과정을 거칩니다.

 

필요한 모듈 import
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

 

1. RNN의 간단한 예제

관련된 하이퍼파라미터 정의

 

  • HIDDEN_DIM: hidden state의 차원 크기
  • LEARING_RATE: 학습률
  • EPOCHS: 에포크
HIDDEN_DIM = 35 # hidden state의 size
LEARNING_RATE = 0.01
EPOCHS = 100

 

디바이스 설정
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

 

RNN의 input으로 사용할 string 정의

 

  • 'h', 'e', 'l', 'l', ... 형태로 input으로 들어가게 되고
  • 'h'가 input으로 들어갔을 때는 output으로 'e'가 나오는 식으로 학습을 쭉 진행
string = "hello pytorch and data analytics."

 

input 데이터를 원핫인코딩

 

  • 원핫인코딩을 진행하기 위해 관련된 character들을 정의해줄 필요가 있음
  • chars="abcdefghijklmnopqrstuvwxyz .01" : a~z, 공백( ), 마침표(.), 0(start 구분을 위해), 1(end 구분을 위해)
chars = "abcdefghijklmnopqrstuvwxyz .01" # 알파벳, 공백, 콤마, 01(start와 end를 구분하기 위해서)
char_list = [i for i in chars]
n_letters = len(char_list)
n_letters

따라서 위를 통해 원핫인코딩을 통해 임베딩을 하게 되면 그 임베딩의 차원이 30차원이 되겠다 생각할 수 있겠습니다.

이제 임베딩을 위한 함수를 정의해 보겠습니다.

def string_to_onehot(string):
  start = np.zeros(shape=n_letters, dtype=int)
  end = np.zeros(shape=n_letters, dtype=int)
  
  start[-2] = 1
  end[-1] = 1
  
  for i in string: # 미리 정의한 string을 하나씩 돌게 됨
    idx = char_list.index(i) # 알파벳을 미리 정의한 characters에서 해당하는 인덱스를 저장
    zero = np.zeros(shape=n_letters, dtype=int)
    zero[idx] = 1
    start = np.vstack([start, zero])
  output = np.vstack([start, end])
  return output

예를 들어 "data"라는 단어를 방금 정의한 함수를 통해 임베딩 해 보겠습니다.

string_to_onehot("data")

처음 성분과 마지막 성분은 start와 end를 의미하므로 첫번째는 설정한 대로 -2번째 인덱스에 1이 들어가 있는 것을 확인할 수 있고, 마지막 성분에는 -1번째 인덱스에 1이 들어가 있는 것을 확인할 수 있습니다.

 

이제 단어들의 임베딩을 자세히 보면

먼저 'd'이므로 4번째 알파벳이므로 (정의한 chars에 의해서) 두 번째 줄('d'를 의미하는 성분)을 보면 4번째 성분에 1로 매핑되어 있음을 확인할 수 있습니다.

마찬가지로 'a', 'd', 'a'도 같은 방식으로 임베딩되었습니다.

 

디코딩해주는 함수도 같이 정의해 두겠습니다.

def onehot_to_string(onehot):
  onehot_value = torch.Tensor.numpy(onehot)
  return char_list[onehot_value.argmax()]

 

+ 임베딩(Embedding)

 

  • 임베딩이란 사람이 쓰는 자연어를 기계가 이해할 수 있는 숫자의 나열인 벡터로 바꾼 결과 혹은 그 과정 전체를 의미
  • 임베딩의 가장 간단한 형태는 단어의 빈도를 그대로 벡터로 사용하는 것
    • 문서 A, B, C에 단어 i, j, k가 나타나는 빈도를 표로 나타내었을 때
      • A의 임베딩은 [0, 1, 1] (i가 0번, j가 1번, k가 1번)
      • B의 임베딩은 [3, 0, 0]
      • C의 임베딩은 [0, 3, 5]
    • 문서 B와 C의 단어 빈도를 살펴봤을 때 매우 유사한 것을 보면
      • 문서 B와 문서 C는 비슷한 내용의 문서라고 추정해 볼 수 있음!
      • 또한 단어 j와 k는 같이 등장한 것을 보면 단어 j와 k간의 의미 차이가 단어 j와 단어 i 간의 의미 차이보다 작을 것이라고 추정도 가능!
  • 이처럼 임베딩이란 단어나 문장, 문서를 벡터로 변환시킨 값이나 그 과정을 말하며
  • 이렇게 변환시킨 벡터 값에 의미와 정보를 손실없이 잘 담아낼 수록 좋은 임베딩이라 할 수 이쏙,
  • 임베딩의 방법은 여러가지가 존재
  • 대표적인 임베딩 방식: Word2Vec

 

모델 class 정의

 

  • 모델을 객체를 만들 때 input_size, hidden_size, output_size를 요구하기 위해 __init__ 메소드의 매개변수에 추가
  • 필요한 layer #1~3
    • RNN 구조를 생각했을 떄 학습을 해야 할 weigh가 어느 부분에 있는지 생각해 보면 총 3가지의 weight 정의 필요
      • input → hidden : nn.Linear(input_size, hidden_size) : input을 받아서 hidden으로 학습을 시켜주는 layer
      • hidden → hidden
      • hidden → output
  • 필요한 layer #4
    • activation : nn.Tanh()
  • forward
    • 입력으로 받은 (현 시점의) input, (이전 시점의) hidden에 대해서
      • 정의한 layer를 각각 통과시켜서 더한 값에 activation function을 적용하면 현 시점의 hidden state 완성
  • init_hidden
    • hidden vector를 초기화 시켜주는 함수
class RNN(nn.Module):
  def __init__(self, input_size, hidden_size, output_size):
    super(RNN, self).__init__()
    self.input_size = input_size
    self.hidden_size = hidden_size
    self.output_size = output_size
    self.input2hidden = nn.Linear(input_size, hidden_size)
    self.hidden2hidden = nn.Linear(hidden_size, hidden_size)
    self.hidden2output = nn.Linear(hidden_size, output_size)
    self.act_fn = nn.Tanh()
    
  def forward(self, input, hidden):
    hidden = self.act_fn(self.input2hidden(input) + self.hidden2hidden(hidden))
    output = self.hidden2output(hidden)
    return hidden, output
    
    def init_hidden(self):
      return torch.zeros(1, self.hidden_size)

만든 RNN 클래스를 인스턴스화 시켜주겠습니다.

rnn = RNN(n_letters, HIDDEN_DIM, n_letters).to(device)

rnn이 지금 어떻게 구성되어 있는지 간단히 출력해 보겠습니다.

print(rnn)

 

loss function, optimizer 정의
loss_func = nn.MSELoss().to(device)
optimizer_rnn = torch.optim.Adam(rnn.parameters(), lr=LEARNING_RATE)

 

학습 진행

 

  • 위에서 정의한 원핫인코딩을 이용한 임베딩 함수를 통해 string을 임베딩
  • input_ : 특정한 하나의 character가 들어가서
  • output_ :  그 다음의 character가 나오도록
  • forward 메소드를 통해 나오는 결과값을 각각 output과 hidden에 저장
  • loss: output(학습을 통해 나온 예측값)과 target(정답)이 얼마나 차이가 나는지를 loss로 계산
one_hot = torch.from_numpy(string_to_onehot(string)).type_as(torch.FloatTensor())

for i in range(EPOCHS):
  optimizer_rnn.zero_grad()
  hidden = rnn.init_hidden()
  total_loss = 0
  
  for j in range(one_hot.size()[0]-1):
    input_ = one_hot[j:j+1, :].to(device)
    target = one_hot[j+1].to(device)
    output, hidden = rnn.forward(input_, hidden)
    loss = loss_func(output.view(-1), target.view(-1))
    total_loss += loss
    
  total_loss.backward()
  optimizer_rnn.step()
  
  if i % 50 == 0:
    print(total_loss)

 

start token만 넣어서 확인해 보기

 

  • 위에서 딱 한문장만 학습을 시켜놓았으므로
  • 시작 토큰 한 줄을 넣게 되면 학습시켜놓았던 그 문장이 잘 나오는지를 확인
  • 학습이 잘 되었다면 거의 비슷하게 문장이 나오게 됨
  • start_tkn = torch.zeros(1, n_letters), start_tkn[:, -2] = 1
    • 위에서 임베딩할 때 start를 구분하기 위해서 -2번째에 1을 넣었던 것처럼
    • 시작 토큰을 생성
    • 이걸 우리가 학습시킨 RNN 모델의 input에 넣을 예정
start_tkn = torch.zeros(1, n_letters)
start_tkn[:, -2] = 1

with torch.no_grad():
  hidden = rnn.init_hidden()
  input_ = start_tkn.to(device)
  output_string = ""
  
  for i in range(len(string)):
    output, hidden = rnn.forward(input_, hidden)
    output_string += onehot_to_string(output.data)
    input_ = output
    
print(output_string)

100% 똑같지는 않지만 한 80% 정도 유사한 문장이 도출되었다고 볼 수 있었습니다.

 

 

728x90

2. RNN with Embedding

 

학습 과정

LSTM

 

Text generation

 

  • 이번에는 tinyshakespeare dataset을 바탕으로 학습 진행할 예정
  • 인코딩 방식
    • Word2Vec 방식 사용
      • 유사한 character끼리는 유사한 임베딩 스코어를 가지도록 하는 효과 적용 가능
      • 원핫인코딩은 그 효과를 주지 못함

 

Embedding ( Word2Vec - CBoW )

 

  • input에 대한 word를 원핫인코딩 형태로 받게 됨
  • Word2Vec에는 2가지 학습 방법 존재, 그 중 하나가 CBoW
    • CBoW
      • cat, sat, on 형태 (cat과 on 사이에 sat이 있는 것)
        • 중심 단어를 예측하는 형태로 학습 진행
      • 임베딩 하면서 dimension 축소됨 (의미없는 차원은 축소)

 

tinyshakespeare dataset 다운로드
!wget https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/tinyshakespeare/input.txt -P ./data

이제 이 텍스트를 좀 쉽게 읽기 위한 패키지를 install 해줍니다.

!pip install unidecode

 

필요한 모듈 import
import re
import unidecode
import random
import string
import time, math

import torch
import torch.nn as nn
import torch.nn.functional as F

 

사용할 파라미터 미리 정의

 

  • CHUNK_LEN: 불러온 데이터셋에서 전체 characters 중 일부만 랜덤하게 추출해서 학습을 진행하기 위해 지정
  • NUM_LAYERS: 1개의 layer ( 한층의 hidden layer )
  • EMBEDDING: 임베딩 했을 때의 차원을 지정
EPOCHS = 1000
HIDDEN_DIM = 100
BATCH_SIZE = 1
CHUNK_LEN = 250
NUM_LAYERS = 1
EMBEDDING = 70
LEARNING_RATE = 0.004

 

string 모듈의 printable 메소드를 사용하여 임베딩에 활용할 characters도 정의해 주겠습니다.

characters = string.printable
n_characters = len(characters)
characters

 

앞서 import한 dataset 모듈을 활용하여 text_file도 정의해 주겠습니다.

text_file = unidecode.unidecode(open('./data/input.txt').read())
len_text_file = len(text_file)
len_text_file

text_file에 100만 character가 넘는 내용들이 포함되어 있는 것을 알 수 있습니다.

 

일부 character만 랜덤하게 추출 작업

 

  • 100만 개가 넘는 character를 모두 활용할 수 없기 때문에 (개인적인 하드웨어적인 문제..)
  • 앞서 지정한 CHUNK_LEN에 의해 랜덤하게 250개의 characters가 추출
def random_chunk():
  start_index = random.randint(0, len_text_file - CHUNK_LEN)
  end_index = start_index + CHUNK_LEN + 1
  return text_file[start_index : end_index]

print(random_chunk())

 

chacater를 tensor로 바꾸는 함수 정의

 

  • 입력한 string의 character를 하나씩 돌면서 그 char가 정의한 characters(모든 characters를 모아둔 문자열)에서의
  • 인덱스로 tensor를 만들어 주게 됨
def character_to_tensor(string):
  tensor = torch.zeros(len(string)).long()
  for char in range(len(string)):
    tensor[char] = characters.index(string[char])
  return tensor

테스트로 하나 출력해 봅시다.

print(character_to_tensor('ABCde'))

이런식으로 word를 tensor로 생성

이러한 tensor를 통해 임베딩을 진행할 것입니다.

 

학습 데이터셋 생성(input vector와 output vector로 구성된)
def random_training_set():
  chunk = random_chunk()
  input = character_to_tensor(chunk[:-1])
  target = chracter_to_tensor(chunk[1:])
  return input, target
random_training_set()

출력된 결과를 잘 보게 되면 13을 input으로 넣었을 때 output으로 14가 나오게 되고

14를 input으로 넣었을 때는 outut으로 10이 나오게 되고, ...

앞서 hello에서 input으로 'h'를 넣게 되면 hello에서 그 다음 character인 'e'가 나오게 끔 학습을 시킨 것과

동일한 내용이 되겠습니다.

 

모델 class 정의

 

  • 인코딩과 디코딩이 포함된 RNN 모델
    • nn.Embedding(input_size, embedding_size) : 모듈에 포함되어 있는 임베딩 메소드
      • input을 받아서 몇 차원으로 임베딩 시킬건지 입력
    • nn.RNN(input_size, hidden_size, num_layers)
      • 앞서서는 RNN을 직접 정의했지만, 여기서는 nn모듈의 RNN을 활용
      • input_size = self.embedding_size: 임베딩 vector를 input으로 받게 되므로
    • nn.decoder
      • output을 도출하는 부분
    • forward
      • encoder layer에 넣을 때 input을 flatten해서 넣어야 함!
      • decoder layer에 넣을 때도 output을 flatten해서 넣어야 함!
class EN_RNN_DE(nn.Module):
  def __init__(self, input_size, embedding_size, hidden_size, output_size, num_layers):
    super(EN_RNN_DE, self).__init__()
    
    self.input_size = input_size
    self.embedding_size = embedding_size
    self.hidden_size = hidden_size
    self.output_size = output_size
    self.num_layers = num_layers
    
    # 임베딩 하는 부분
    self.encoder = nn.Embedding(self.input_size, self.embedding_size)
    self.rnn = nn.RNN(self.embedding_size, self.hidden_size, self.num_layers)
    self.decoder = nn.Linear(self.hidden, self.output_size)
    
  def forward(self, input, hidden):
    en_output = self.encoder(input.view(1,-1))
    output, hidden = self.rnn(en_output, hidden)
    de_output = self.decoder(output.view(1, -1))
    return de_output, hidden
    
  def init_hidden(self):
    hidden = torch.zeros(self.num_layers, BATCH_SIZE, self.hidden_size)
    return hidden

모델 class를 다 정의해 주었으니 이제 모델을 인스턴스화 시켜놓겠습니다.

model = EN_RNN_DE(n_characters, EMBEDDING, HIDDEN_DIM, n_chacaters, NUM_LAYERS).to(device)

모델의 흐름이 어떻게 진행되는지를 보기 위해 모델에 character 하나를 넣어보겠습니다.

inp = character_to_tensor("A")
print(inp.size())
hidden = model.init_hidden()
print(hidden.size())
out,hidden = model(inp,hidden)
print(hidden.size())
print(out.size())

 

모델이 어떻게 layers가 쌓여있는지도 확인해 봅시다.

print(model)

 

loss function과 optimizer 정의
loss_func = nn.CrossEntropyLoss()
optimizer_model = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)

 

학습 진행
for i in ranage(EPOCHS):
  input, target, ramdom_training_set()
  input = input.to(device)
  target = target.to(device)
  hidden = model.init_hidden()
  
  loss = torch.tensor([0]).type(torch.FloatTensor)
  optimizer_model.zero_grad()
  
  for j in range(CHUNK_LEN=1):
    x = input[j]
    y_ = target[j].unsqueeze(0).type(torch.LongTensor)
    y, hidden = mode(x, hidden)
    loss += loss_func(y, y_)
    
  loss.backward()
  optimizer_model.step()
  
  if i % 100 == 0:
    print(loss/CHUNK_LEN)

loss가 지속해서 감소하는 것을 확인할 수 있었습니다.

 

결과적으로 text generation이 잘 되었는지 확인!

 

  • 'b'라는 character를 넣었을 때 어떻게 output이 나오게 되는지를 확인
start_string = "b"

input = character_to_tensor(start_string)
hidden = model.init_hidden()

print(start_string, end="")

for i in range(300):
  output, hidden = model(input, hidden)

  output_dist = output.data.view(-1).div(0.8).exp()
  top_i = torch.multinomial(output_dist, 1)[0]
  predicted_char = characters[top_i]

  print(predicted_char, end="")

  input = character_to_tensor(predicted_char)

위와 같은 text가 generation 된 것을 확인할 수 있었습니다.

 

3. LSTM with Embedding

모델 class 정의
class EN_LSTM_DE(nn.Module):
  def __init__(self, input_size, embedding_size, hidden_size, output_size, num_layers):
    super(EN_LSTM_DE, self).__init__()
    
    self.input_size = input_size
    self.embedding_size = embedding_size
    self.hidden_size = hidden_size
    self.output_size = output_size
    self.num_layers = num_layers
    
    # 임베딩 하는 부분
    self.encoder = nn.Embedding(self.input_size, self.embedding_size)
    self.lstm = nn.LSTM(self.embedding_size, self.hidden_size, self.num_layers)
    self.decoder = nn.Linear(self.hidden_size, self.output_size)
    
  def forward(self, input, hidden, cell):
    en_output = self.encoder(input.view(1, -1))
    output, {hidden, cell} = self.lstm(en_output, {hidden, cell})
    de_output = self.decoder(output.view(1, -1))
    return de_output, hidden, cell
    
  def init_hidden(self):
    hidden = torch.zeros(self.num_layers, BATCH_SIZE, self.hidden_size)
    cell = torch.zeros(self.num_layers, BATCH_SIZE, self.hidden_size)
    return hidden, cell

모델 class를 정의했으니 모델을 인스턴스화 시켜줍니다.

model_LSTM = EN_LSTM_DE(n_characters, EMBEDDING, HIDDEN_DIM, n_characters, NUM_LAYERS).to(device)

모델이 어떤 식으로 layer를 구성하고 있는지도 출력해 봅시다.

print(model_LSTM)

 

loss function과 optimizer 정의
loss_func = nn.CrossEntropyLoss()
optimizer_lstm = torch.optim.Adam(model_LSTM.parameters(), lr=LEARNING_RATE)

 

학습 진행

 

기존 RNN
인코딩된 input , hidden state → output, hidden state
LSTM,
인코딩된 input, { hidden state, cell state } → output, { hidden state, cell state }

≫ 초기화 할 때도 hidden과 cell을 같이 초기화 해주어야 함!
for i in range(EPOCHS):
  input, target = random_training_set()
  input = input.to(device)
  target = target.to(device)
  hidden, cell = model_LSTM.init_hidden()
  
  loss = torch.tensor([0]).type(torch.FloatTensor)
  optimizer_lstm.zero_grad()
  
  for j in range(CHUNK_LEN-1):
    x = input[j]
    y_ = target[j].unsqueeze(0).type(torch.LongTensor)
    y, hidden, cell = model_LSTM(x, hidden, cell)
    loss += loss_func(y, y_)
  
  loss.backward()
  optimizer_lstm,step()
  
  if i % 100 == 0:
    print(loss/CHUNK_LEN)

 

결과적으로 text generation이 잘 되었는지 확인!
start_string = "b"

input = character_to_tensor(start_string)
hidden, cell = model_LSTM.init_hidden()

print(start_string, end="")

for i in range(300):
  output, hidden, cell = model_LSTM(input, hidden, cell)

  output_dist = output.data.view(-1).div(0.8).exp()
  top_i = torch.multinomial(output_dist, 1)[0]
  predicted_char = characters[top_i]

  print(predicted_char, end="")

  input = character_to_tensor(predicted_char)

위와 같은 형태의 text로 generation된 것을 확인할 수 있었습니다.

 

+ Gated Recurrent Unit

  • LSTM과 다르게, cell state를 활용 X
    • 기존 RNN 처럼 hidden state와 input만을 바탕으로 해서 구성
  • Gated Recurrent Unit이 각광을 받은 이유는?
    • LSTM과는 다르게 cell state를 사용하지는 않지만
    • LSTM과 거의 비슷한 성능을 보임
  • RNN 모델 class를 구성하는 것과 모두 동일한데
    • nn.RNN → nn.GRU 로 바꿔주면 바로 구현 가능

Gated Recurrnet Unit

 

728x90
LIST
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함