티스토리 뷰

DL/Pytorch

[Ch6] 순환신경망(RNN)

jeong_reneer 2022. 1. 20. 16:12

6.1 RNN의 발달 과정

RNN : 순서가 있는 데이터에서 의미(패턴, 상관관계, 인과관계 등)를 찾아내기 위해 고안된 모델 

시퀀스(sequence) 데이터 : 순서가 존재하는 데이터 (ex. 언어)

시계열(time series) 데이터 : 순서가 존재하며 시간에 따른 의미도 존재하는 데이터 (ex. 주가)

 

Application

 

 

6.2 RNN의 작동 원리

1) RNN 도식화

 

은닉층의 node들은 어떤 초기값을 가지고 계산이 시작되며,

t=0시점에서 입력값 + 각 node 초기값의 조합으로 t=0일 때 은닉층의 값들이 계산되어 결과값(출력값)이 도출됨

t=1시점에서 입력값 + t=0시점에서 계산된 은닉층 값들의 조합으로 t=1일 때 은닉층의 값과 결과값이 다시 계산됨

이러한 과정이 지정한 시간만큼 반복됨

 

2) 시간에 따른 역전파 Backpropagation through time (BPTT)

(1) RNN은 계산에 사용된 '시점의 수'에 영향을 받음

   ex. t=0 ~ t=2 까지 계산에 사용됐다면 그 시간 전체에 대한 backprop 해야 함

 

(2) t=2시점의 loss를 backprop 하려면?

- loss를 입력과 은닉층들 사이의 가중치로 미분해 loss에 대한 각각의 비중을 구해 update   

   그 과정에서 은닉층의 이전 시점의 값들(=가중치 + 입력값 + 이전 시점의 값들)이 연산에 포함됨

- RNN은 각 위치별로 같은 가중치를 공유하므로 t=0시점의 node값들에도 영향을 줘야 함

   시간을 역으로 거슬러 올라가는 방식으로 각 가중치를 update = BPTT

 

 

 

 

t=2시점에서 발생한 loss는 t=2, 1, 0시점에 전부 영향을 줌

t=1시점에서 발생한 loss는 t=1, 0 시점에 영향을 줌

t=0시점의 loss는 t=0의 가중치에 영향을 줌

실제 update할 때는 가중치에 대해 시점별 기울기를 다 더하여 한 번에 update 진행

 

 

6.3 모델 구현, 학습 및 결과 확인

1) Hyperparameter Setting 및 필요한 함수 구현

# Hyperparameter Setting
n_hidden = 35 
lr = 0.01
epochs = 1000

string = "hello pytorch. how long can a rnn cell remember? show me your limit!"
chars =  "abcdefghijklmnopqrstuvwxyz ?!.,:;01"
char_list = [i for i in chars]
n_letters = len(char_list)

# 문자열을 one-hot 벡터의 스택으로 만드는 함수
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:
        idx = char_list.index(i)
        zero = np.zeros(shape=n_letters ,dtype=int)
        zero[idx]=1
        start = np.vstack([start,zero])
    output = np.vstack([start,end])
    return output
    
# One-hot 벡터를 문자로 바꿔주는 함수 
def onehot_to_word(onehot_1):
    onehot = torch.Tensor.numpy(onehot_1)
    return char_list[onehot.argmax()]

 

2)  RNN with 1 hidden layer

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.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)
        self.act_fn = nn.Tanh()
    
    def forward(self, input, hidden):
        combined = torch.cat((input, hidden), 1) # input과 hidden state를 cat
        hidden = self.act_fn(self.i2h(combined)) # hidden state는 update
        output = self.i2o(combined)              # output은 계산
        return output, hidden
  
    def init_hidden(self):           # 아직 입력이 없을 때(t=0)의 hidden state 초기화
        return torch.zeros(1, self.hidden_size)
    
rnn = RNN(n_letters, n_hidden, n_letters)
loss_func = nn.MSELoss()
optimizer = torch.optim.Adam(rnn.parameters(), lr=lr)

 

3) Train

(1) one_hot : start_token + 문장 + end_token 으로 구성된 matrix 생성

(2) for i in range ~ : 문장 전체를 학습하는 과정은 epochs 만큼 반복하며 문장 전체에 대한 loss 계산 (매번 loss 초기화 진행)

(3) for j in range ~ :  start_token이 들어오면 결과값으로 p가 나옴 → p가 들어오면 y가 나옴 → y가 들어오면 t가 나옴 → ....

j번째 인덱스에 해당하는 값이 input으로 들어오면 j+1번째 인덱스에 해당하는 값이 target이 됨

# Train
one_hot = torch.from_numpy(string_to_onehot(string)).type_as(torch.FloatTensor())

for i in range(epochs):
    optimizer.zero_grad()
    hidden = rnn.init_hidden()
    total_loss = 0
    
    for j in range(one_hot.size()[0]-1):
        input_ = one_hot[j:j+1,:]          # 입력값은 앞에 글자 (p y t o r c)
        target = one_hot[j+1]              # 목표값은 뒤에 글자 (y t o r c h)
        output, hidden = rnn.forward(input_, hidden)
        loss = loss_func(output.view(-1),target.view(-1))
        total_loss += loss
    total_loss.backward()
    optimizer.step()

    if i % 10 == 0:
        print(total_loss)

 

4) Test

문장의 첫 글자만 입력으로 전달하고, 모델에서 나온 결과값이 새로운 입력으로 전달되는 방식으로 전체 문장 생성

# Test
start = torch.zeros(1,n_letters)
start[:,-2] = 1

with torch.no_grad():
    hidden = rnn.init_hidden()
    input_ = start
    output_string = ""
    
    for i in range(len(string)):
        output, hidden = rnn.forward(input_, hidden)
        output_string += onehot_to_word(output.data)
        input_ = output

print(output_string)

 

6.4 RNN의 한계 및 개선 방안

1) 한계

: 타임 시퀀스가 늘어날수록 (긴 문장일수록) backprop 시 tanh의 미분값(0~1 사이의 값)이 여러 번 곱해지며

Vanishing Gradient 현상이 발생해 학습 제대로 되지 X

 

2) 개선 방안

(1) LSTM (Long Short-Term Memory)

: 기존의 RNN 모델에 장기기억을 담당하는 Cell State 부분을 추가한 모델

남길 건 남기고, 잊을 건 잊고, 새로 추가할 건 추가해서 Cell State에는 중요한 정보만 계속 흘러가도록! by using Gates

각 time step의 output이라고 할 수 있는 Hidden State는 Cell State를 적당히 가공해서 내보냄

 

Gateg_t = sigmoid(W_g * V_input)  : 0~1 사이 값을 갖는 elements로 구성된 vector

C_(t-1) * G : element-wise coefficient multiplication → 각 dimension의 정보를 pass할지 block할지 조절!

 

 

 

Cell State (셀 상태)

: 장기기억을 담당하는 부분

- 곱하기(X) 부분 : 기존 정보를 얼마나 남길 것인지에 따라 비중을 곱하는 부분

- 더하기(+) 부분 : 현재 들어온 데이터와 기존의 Hidden State를 통해 정보를 추가하는 부분

 

Forget Gate (망각 게이트)

: 기존의 정보들로 구성되어 있는 Cell State의 값을 얼마나 잊어버릴 것인지 비중을 정하는 부분

= 현재 시점에서의 입력값 x_t + 직전 시점의 Hidden State 값 h_(t-1)을 입력으로 받아서,

각 입력에 가중치 w를 곱해주고 편차 b를 더한 값을 sigmoid 함수에 넣어 0~1 사이의 값을 출력

1️⃣ f_t 를 통해 C_(t-1)에서 불필요한 정보를 지움

 

Input Gate (입력 게이트)

: 어떤 정보를 얼만큼 Cell State에 새로 저장할 것인지 정하는 부분

새로운 입력값 x_t과 직전 시점의 Hidden State 값 h_(t-1)을 입력으로 받아서,

- sigmoid 함수 통과 : 0~1 사이의 값 출력 → 새롭게 추가할 정보를 얼만큼의 비중으로 Cell State에 더해줄지 정함

- tanh 통과 :  -1~1 사이의 값 출력 → 새롭게 Cell State에 정보 추가 (임시 Cell State 값)

2️⃣ C_(t-1)에 새로운 입력값 x_t와 h_(t-1)을 보고 중요한 정보를 입력함 & 임시 ~C_t를 계산

 

 

Cell State UPDATE

현재 시점의 새로운 입력값 + 직전 시점의 Hidden State 값의 조합으로

기존 Cell State 정보 C_(t-1)를 얼만큼 forget하고 전달할지

& 어떤 정보 ~C_t를 얼만큼의 비중으로 더할지 정함

3️⃣ 위 두 과정을 통해 C_t 만듦 (update) : f_t로 C_(t-1)의 불필요한 정보를 날리고, 임시 C_t 정보를 추가

 

Hidden State UPDATE

Sigmoid 비중을 정하는 부분은 앞단과 동일

새로운 Hidden State는 UPDATE된 Cell State 값을 tanh에 통과시킨 -1~1 사이의 비중을 곱한 값으로 생성됨

4️⃣ C_t를 적절히 가공해 해당 t에서의 h_t를 만듦 (update) : C_t를 가공할 output gate o_t로 h_t 계산

 

5️⃣ C_t와 h_t를 next step인 t+1로 전달

 

Q) LSTM은 어떻게 Vanishing Gradient 문제 해결 ?

: t시점에서 Cell State에 기록된 정보가 지워지지 않고 t+n에서 활용되었을 때,

중간에 Nonlinear act function 거치지 않고 흐른 것이기에 Vanishing Gradient 문제 일부 해소 가능!

 

 

(2) GRU (Gated Recurrent Unit)

- LSTM보다 간단한 구조이지만 성능 면에서 밀리지 않는 모델

- LSTM과 달리 Cell State와 Hidden State를 분리하지 않고, Hidden State 하나로 합침

① Update gate (z_t)

: 현 시점의 새 입력값 x_t와 직전 시점의 Hidden State 값 h_(t-1)에 가중치 곱하고 sigmoid 통과시켜 update 비중 정함

② Reset gate (r_t)

: Update gate와 동일한 입력, sigmoid 통과시켜 ~h_t 구할 때 기존 Hidden State를 얼만큼 반영할지에 대한 비중 정함

~h_t  : 기존 Hidden State h_(t-1)에 가중치가 곱해진 값 + 새로운 입력값 x_t에 가중치 곱한 값을 tanh 통과시켜 구함

새로운 Hidden State (h_t)

: 앞서 구한 가중치로 기존 Hidden State h_(t-1)과 ~h_t의 가중 합을 곱해 새로운 Hidden State 값 h_t 구함

 

 

3) RNN Application - MNIST Classification (many to one)

(1) RNN

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

# Device configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# Hyper-parameters 
hidden_size = 128
num_classes = 10
num_epochs = 2
batch_size = 100
learning_rate = 0.001

input_size = 28
sequence_length = 28
num_layers = 2

# MNIST dataset 
train_dataset = torchvision.datasets.MNIST(root='./data', 
                                           train=True, 
                                           transform=transforms.ToTensor(),  
                                           download=True)

test_dataset = torchvision.datasets.MNIST(root='./data', 
                                          train=False, 
                                          transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 
                                          batch_size=batch_size, 
                                          shuffle=False)

examples = iter(test_loader)
example_data, example_targets = examples.next()

for i in range(6):
    plt.subplot(2,3,i+1)
    plt.imshow(example_data[i][0], cmap='gray')
plt.show()

# many to one RNN
class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super(RNN, self).__init__()
        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first = True)
        # x -> (batch_size, seq, input_size)
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
        out, _ = self.rnn(x, h0)
        # out -> (batch_size, seq, hidden_size)
        # out -> (N, 28, 128)
        out = out[:, -1, :]
        # out -> (N, 128)
        out = self.fc(out)
        return out


model = RNN(input_size, hidden_size, num_layers, num_classes).to(device)

# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  

# Train the model
n_total_steps = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # origin shape: [100, 1, 28, 28]
        # resized: [100, 28, 28]
        images = images.reshape(-1, sequence_length, input_size).to(device)
        labels = labels.to(device)
        
        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if (i+1) % 100 == 0:
            print (f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{n_total_steps}], Loss: {loss.item():.4f}')

# Test the model
# In test phase, we don't need to compute gradients (for memory efficiency)
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    for images, labels in test_loader:
        images = images.reshape(-1, sequence_length, input_size).to(device)
        labels = labels.to(device)
        outputs = model(images)
        # max returns (value ,index)
        _, predicted = torch.max(outputs.data, 1)
        n_samples += labels.size(0)
        n_correct += (predicted == labels).sum().item()

    acc = 100.0 * n_correct / n_samples
    print(f'Accuracy of the network on the 10000 test images: {acc} %')

 

(2) GRU

class GRU(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super(GRU, self).__init__()
        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first = True)
        # x -> (batch_size, seq, input_size)
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
        out, _ = self.gru(x, h0)
        # out -> (batch_size, seq, hidden_size)
        # out -> (N, 28, 128)
        out = out[:, -1, :]
        # out -> (N, 128)
        out = self.fc(out)
        return out


model = GRU(input_size, hidden_size, num_layers, num_classes).to(device)

 

(3) LSTM

class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super(LSTM, self).__init__()
        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first = True)
        # x -> (batch_size, seq, input_size)
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(device)
        out, _ = self.lstm(x, (h0,c0)) #Tuple 형태
        # out -> (batch_size, seq, hidden_size)
        # out -> (N, 28, 128)
        out = out[:, -1, :]
        # out -> (N, 128)
        out = self.fc(out)
        return out


model = LSTM(input_size, hidden_size, num_layers, num_classes).to(device)

 

 

 

4) Embedding layer

(1) 등장 배경

One-hot vector의 한계점 : ① 의미적 연산 + ② 확장성 측면에서 한계점을 가짐 

① 내적 = 0 → 연산을 통해 두 알파벳 or 단어 or 문장 간의 의미적 차이나 유사도(ex. cosine similarity) 구하는 것 불가능

하나의 요소가 추가될 때마다 vector 길이가 늘어남 → 기존 모델이 무의미해짐 

 

(2) Embedding

: 사람이 쓰는 자연어를 기계가 이해할 수 있는 숫자형태인 vector로 바꾼 결과 혹은 그 일련의 과정 전체

= 알파벳이나 단어 같은 기본 단위 요소들을 일정한 길이를 가지는 vector 공간에 투영하는 것

ex. Word Embedding : 일정한 크기의 vector에 단어들을 투영하는 방법 (의미적 공통점 가지는 단어끼리 유사한 값)

(3) 역할(기능)

- 단어/문장 간 관련도 계산(Word2Vex) : 단어를 전체 단어들 간의 관계에 맞춰 해당 단어의 특성을 갖는 vector로 변경

- 의미적/문법적 정보 함축 → 단어 vector 간 사칙 연산 가능 

- 전이 학습(Transfer learning) : 모델의 성능과 수렴속도가 빠른 임베딩을 다른 딥러닝 모델의 입력값으로 사용 가능

- 특정 도메인에서 임베딩 해두면 비슷한 의미를 가지는 단어들끼리 가까운 곳에 위치하게 되므로 유사 단어 찾기 편리

 

(4) 대표적인 기법

① CBOW (Continuous Bag-Of-Words)

: '주변 단어들'로부터 '가운데 들어갈 단어 하나'가 나오도록 모델을 학습해 임베딩 벡터를 얻는 방식

- 문장 또는 문서의 모든 문장을 이 방식으로 학습하면 문서에서 사용된 단어들이 의미적(semantically)으로 임베딩됨

  (ex. 'The quick brown dog jumps over the laxy fox' 에서 'dog'와 'fox'는 vector 공간상 가까운 위치에 임베딩됨)

 

② skip-gram

: '중심 단어'로부터 '주변 단어들'이 나오도록 모델을 학습해 임베딩 벡터를 얻는 방식

  (ex. 'fox'가 들어오면 'quick', 'brown', 'jumps' 등의 단어들이 나오도록 학습됨)

- 학습 과정을 끝낸 후 가중치 행렬의 각 행을 단어 vector로 사용함

- CBOW로 만든 단어 vector 보다 단어 간의 유사도를 잘 측정하며 복잡한 특징도 잘 잡아냄 (일반적으로 성능 더 좋음) 

 

 

(5) Embedding 함수를 이용한 LSTM, GRU 모델 구현

torch.nn.Embedding class 이용해서 Embedding instance 생성

문자가 입력으로 들어오면 먼저 문자 사전(ASCII)에 의해 Index로 변환됨

→ 이 Index를 Embedding instance에 전달하면 vector가 결과로 나옴

→ 이 vector를 가지고 RNN, LSTM, GRU를 통해 모델을 학습시킴 !

 

 

 

 

◾ 공통 코드

◾ LSTM (Long Short-Term Memory)

◾ GRU (Gated Recurrent Unit)

 

'DL > Pytorch' 카테고리의 다른 글

[Ch8] Neural Style Transfer  (0) 2022.01.24
[Ch7] 학습 시 생길 수 있는 문제점 및 해결방안  (0) 2022.01.22
[Ch5] 합성곱 신경망(CNN)  (0) 2022.01.15
[Ch4] 인공 신경망(ANN)  (0) 2022.01.15
[Ch3] 선형회귀분석  (0) 2022.01.15
댓글
공지사항