함께하는 데이터 분석

[Find-A][Pytorch] 임베딩 함수를 이용한 LSTM, GRU 모델 구현 본문

학회 세션/파인드 알파

[Find-A][Pytorch] 임베딩 함수를 이용한 LSTM, GRU 모델 구현

JEONGHEON 2022. 9. 21. 21:17

RNN 모델 구현

import torch
import torch.nn as nn

import string
import random
import re
import time, math

 

num_epochs = 2000
print_every = 100
plot_every = 10

chunk_len = 200

hidden_size = 100
batch_size = 1
num_layers = 1
embedding_size = 70
lr = 0.002

필요한 하이퍼 파라미터를 지정

 

# import 했던 string에서 출력가능한 문자들을 다 불러옴
all_characters = string.printable

# 출력가능한 문자들의 개수를 저장
n_characters = len(all_characters)
print(all_characters)
print('num_chars = ', n_characters)

>>> 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

    num_chars =  100

현재 예시는 문자에 대한 순환 신경망이므로 사용 가능한 모든 문자를 저장해놓고 전체 가지 수도 지정

 

file = open('경로/tiny-shakespeare.txt').read()
file_len = len(file)
print('file_len =', file_len)

>>> file_len = 1115394

이제 학습에 필요한 셰익스피어의 작품이 담긴 데이터셋(tinyshakespeare)을 사용

 

https://github.com/jcjohnson/torch-rnn/blob/master/data/tiny-shakespeare.txt

 

GitHub - jcjohnson/torch-rnn: Efficient, reusable RNNs and LSTMs for torch

Efficient, reusable RNNs and LSTMs for torch. Contribute to jcjohnson/torch-rnn development by creating an account on GitHub.

github.com

데이터는 여기 깃허브에 존재

 

# 이 함수는 텍스트 파일의 일부분을 랜덤하게 불러오는 코드
def random_chunk():
    # (시작지점 < 텍스트파일 전체길이 - 불러오는 텍스트의 길이)가 되도록 시작점과 끝점을 정함
    start_index = random.randint(0, file_len - chunk_len)
    end_index = start_index + chunk_len + 1
    return file[start_index:end_index]

print(random_chunk())

>>> le husband. Come, Camillo,
    And take her by the hand, whose worth and honesty
    Is richly noted and here justified
    By us, a pair of kings. Let's from this place.
    What! look upon my brother: both your pard

모든 파일을 한 번에 학습할 수 없기 때문에 일정한 크기로 잘라야 함

 

랜덤 한 위치에서 시작해 일정 크기만큼의 문자열을 읽어오는 random_chunk라는 함수를 만들어 줌

 

# 문자열을 받았을때 이를 인덱스의 배열로 바꿔주는 함수
def char_tensor(string):
    tensor = torch.zeros(len(string)).long()
    for c in range(len(string)):
        tensor[c] = all_characters.index(string[c])
    return tensor

print(char_tensor('ABCdef'))

>>> tensor([36, 37, 38, 13, 14, 15])

이렇게 일정 크기로 문자열을 읽어온 다음 이를 앞에서 저장해둔 출력 가능한 문자열 리스트를 통해 인덱스로 바꿔주는 char_tensor 함수를 만듦

 

코드에 예시로 든 것처럼 문자열 ABCdef를 인덱스화하면 결과는 tensor([36, 37, 38, 13, 14, 15])로 나옴

 

# 랜덤한 텍스트 chunk를 불러와서 이를 입력과 목표값을 바꿔주는 함수
# 예를 들어 pytorch라는 문자열이 들어오면 입력은 pytorc / 목표값은 ytorch 가 됨
def random_training_set():    
    chunk = random_chunk()
    inp = char_tensor(chunk[:-1])
    target = char_tensor(chunk[1:])
    return inp, target

ramdom_training_set 함수는 랜덤 한 문자열을 불러와 입력값과 목푯값으로 나눠 리턴함

 

그다음부터는 임베딩과 RNN을 통해 학습

 

이 과정을 그림으로 그려보면 위와 같음(각 수치는 편의를 위해 임의로 지정한 것)

 

이 그림에는 디코딩 부분이 들어 있는데 이는 임베딩과 순환 신경망을 통과해 나온 결과를 임베딩하기 전 데이터 형태로 돌려놓는 역할

 

만약 알파벳 26개를 길이 50짜리 벡터에 임베딩 했다면 다시 알파벳을 나타내는 길이 26짜리 벡터로 돌려놔야 해당 출력을 보고 해석할 수 있기 때문

 

이 예시에서는 이를 단순하게 한 층의 신경망으로 구현

 

class RNN(nn.Module):
    def __init__(self, input_size, embedding_size, hidden_size, output_size, num_layers=1):
        super(RNN, 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_size, self.output_size)
        
    
    def forward(self, input, hidden):
        out = self.encoder(input.view(1,-1))
        out,hidden = self.rnn(out,hidden)
        out = self.decoder(out.view(batch_size,-1))
        return out,hidden

    def init_hidden(self):
        hidden = torch.zeros(self.num_layers, batch_size, self.hidden_size)
        return hidden
    
model = RNN(n_characters, embedding_size, hidden_size, n_characters, num_layers)

 위의 클래스를 인스턴스화하는 코드

 

model = RNN(input_size=n_characters, 
            embedding_size=embedding_size,
            hidden_size=hidden_size, 
            output_size=n_characters, 
            num_layers=2)

해당 인스턴스를 테스트 해보는 코드를 작성해보겠음

 

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

>>> tensor([36])
    torch.Size([2, 1, 100])
    torch.Size([1, 100])

A라는 문자열을 입력으로 주고 char_tensor 함수를 사용해 이를 텐서로 바꿔줌

 

그다음 은닉 상태도 초기화해주고 이렇게 만든 입력과 은닉 상태를 RNN 인스턴스에 인수로 전달하면 forward 함수에 정의된 대로 연산이 진행됨

 

optimizer = torch.optim.Adam(model.parameters(), lr=lr)
loss_func = nn.CrossEntropyLoss()

 

def test():
    start_str = "b"
    inp = char_tensor(start_str)
    hidden = model.init_hidden()
    x = inp

    print(start_str,end="")
    for i in range(200):
        output,hidden = model(x,hidden)

        # 여기서 max값을 사용하지 않고 multinomial을 사용하는 이유는 만약 max 값만 쓰는 경우에
        # 생성되는 텍스트가 다 the the the the the 이런식으로 나오기 때문
        # multinomial 함수를 통해 높은 값을 가지는 문자들중에 램덤하게 다음 글자를 뽑아내는 방식으로 자연스러운 텍스트를 생성
        output_dist = output.data.view(-1).div(0.8).exp()
        top_i = torch.multinomial(output_dist, 1)[0]
        predicted_char = all_characters[top_i]

        print(predicted_char,end="")

        x = char_tensor(predicted_char)

 

for i in range(num_epochs):
    # 랜덤한 텍스트 덩어리를 샘플링하고 이를 인덱스 텐서로 변환 
    inp,label = random_training_set()
    hidden = model.init_hidden()

    loss = torch.tensor([0]).type(torch.FloatTensor)
    optimizer.zero_grad()
    for j in range(chunk_len-1):
        x  = inp[j]
        y_ = label[j].unsqueeze(0).type(torch.LongTensor)
        y,hidden = model(x,hidden)
        loss += loss_func(y,y_)

    loss.backward()
    optimizer.step()
    
    if i % 100 == 0:
        print("\n",loss/chunk_len,"\n")
        test()
        print("\n","="*100)

모델 부분을 보면 입력값이 들어왔을 때 먼저 이를 임베딩 해주고 RNN 노드에 임베딩 값과 은닉 상태 값을 전달해 줌

 

이때 임베딩 차원이 문자열 크기와 다르기 때문에 RNN 노드의 결괏값을 디코더를 통해 맞춰줘야 다시 문자로 바꿀 수 있음

 

초기에는 위와 같은 수준으로 나옴

 

한참 학습을 진행하고 나면 위와 같은 수준까지 결과가 나오게 됨

 

 

 

GRU 모델 구현

self.rnn = nn.RNN(self.embedding_size,self.hidden_size,self.num_layers)

GRU는 기본 RNN 모델을 클래스 정의 내부에서 위의 라인을

 

self.rnn = nn.GRU(self.embedding_size,self.hidden_size,self.num_layers)

이렇게 바꾸면 끝

 

이것이 가능한 이유는 GRU가 RNN과 같이 은닉 상태만 가지고 있기 때문

 

 

 

LSTM

class RNN(nn.Module):
    def __init__(self, input_size, embedding_size, hidden_size, output_size, num_layers=1):
        super(RNN, 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.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):
        out = self.encoder(input.view(1,-1))
        out,(hidden,cell) = self.rnn(out,(hidden,cell))
        out = self.decoder(out.view(batch_size,-1))
        return out,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
    

model = RNN(n_characters, embedding_size, hidden_size, n_characters, num_layers)

구현적 차이는 셀 상태의 유무 정도로 볼 수 있음


https://www.hanbit.co.kr/store/books/look.php?p_code=B7818450418 

 

파이토치 첫걸음

딥러닝 구현 복잡도가 증가함에 따라 ‘파이써닉’하고 사용이 편리한 파이토치가 주목받고 있다. 파이토치 코리아 운영진인 저자는 다년간 딥러닝을 공부하고 강의한 경험을 살려 딥러닝의

www.hanbit.co.kr

 

'학회 세션 > 파인드 알파' 카테고리의 다른 글

[Find-A][Pytorch] 학습률  (1) 2022.09.25
[Find-A][Pytorch] 초기화  (0) 2022.09.25
[Find-A] LSTM, GRU, 임베딩  (1) 2022.09.21
[Find-A] 인공 신경망  (0) 2022.09.12
[Find-A][Scikit Learn] 앙상블 학습  (0) 2022.08.28