본문 바로가기
파이토치

[딥러닝 파이토치 교과서] RNN, LSTM, GRU

by 나연하야 2023. 4. 22.

시계열 분석

- 시간에 따라 변하는 데이터를 사용하여 추이를 분석하는 것

- 시계열 데이터는 규칙적 시계열과 불규치적 시계열로 나눌 수 있음

- 규칙적 시계열은 트렌드와 분산이 불변하는 데이터이며, 불규칙적 시계열은 트렌드 혹은 분산이 변화하는 시계열 데이터임

- 불규칙적 시계열 데이터에 규칙성을 부여하는 방법으로는 AR, MA, ARMA, ARIMA 모델을 적용하는 것이 가장 널리 알려져 있음

- 최근에는 딥러닝을 이용하여 시계열 데이터의 연속성을 기계 스스로 찾아내도록 하는 방법이 더 좋은 성능을 내고 있음

 

1) AR모델

- AR(AutoRegressive) (자기 회귀) 모델은 이전 관측 값이 이후 관측 값에 영향을 준다는 아이디어에 대한 모형으로 자기 회귀 모델이라고도 함

 

2) MA모델

- MA(Moving Average)(이동 평균) 모델은 트렌드가 변화하는 상황에 적합한 회귀 모델

- 이동 평균 모델에서는 윈도우라는 개념을 사용하는데, 시계열을 따라 윈도우 크기만큼 슬라이딩된다고 하여 이동 평균 모델이라고 함

- AR모델처럼 이던 데이터의 상태에서 현재 데이터의 상태를 추론하는 것이 아닌 이전 데이터의 오차에서 현재 데이터의 상태를 추론하겠다는 의미임

 

3) ARMA모델

- ARMA(AutoRegressive Moving Average)(자기 회귀 이동 평균) 모델은 AR과 MA를 섞은 모델로 연구 기관에서 주로 사용함

 

4) ARIMA 모델

- ARIMA(AutoRegressive Integrated Moving Average)(자기 회귀 누적 이동 평균) 모델은 ARMA 모델과 달리 과거 데이터의 선형 관계 뿐만 아니라 추세까지 고려한 모델임

 

순환 신경망(RNN, Recurrent Neural Network)

- Recurrent(반복되는)는 이전 은닉층이 현재 은닉층의 입력이 되면서 '반복되는 순환 구조'를 갖음

- 기존 네트워크와 다른 점은 '기억(memory)'을 갖으며, 이 기억은 현재까지 입력한 데이터를 요약한 정보임

- 새로운 입력이 네트워크로 들어올 때마다 기억은 조금씩 수정되며, 최종적으로 남겨진 기억은 모든 입력 데이터를 요약한 정보임

- RNN은 입력과 출력에 따라 일대일, 일대다, 다대일, 다대다 등의 형태를 가짐(예를 들어, 일대다는 입력이 다수이고 출력이 하나인 구조임)

- 일대다는 이미지를 입력해서 이미지에 대한 설명을 문장으로 출력하는 이미지 캡션이 대표적인 사례이며, 다대일은 문장을 이력해서 긍정/부정을 출력하는 감성 분석기에서 사용되며, 다대다는 언어를 번역하는 자동 번역기 등에 사용됨

출처: https://ratsgo.github.io/natural%20language%20processing/2017/03/09/rnnlstm/

 

RNN 구조

- RNN에서는 입력층, 은닉층, 출력층 외에 가중치를 세 개(W_xh, W_hh, W_hy) 가짐

- 가중치는 모든 시점에서 동일함

 

RNN 계산

1. (은닉층 계산) 은닉층은 일반적으로 하이퍼볼릭 탄젠트 활성화 함수를 사용함

2. (출력층) 출력층에서는 소프트맥스 함수를 활용함

3. (오차 계산) RNN의 오차는 심층 신경망에서 전방향 학습과 달리 각 단계마다 오차를 측정하며, 각 단계마다 실제값과 예측값으로 오차를 이용하여 측정함

4. (역전파) 역전파는 BPTT(BackPropagation Through Time)를 이용하여 모든 단계마다 처음부터 끝까지 역전파

 

참고) 생략된-BPTT

- 계산량을 줄이기 위해 현재 단계에서 일정 시점까지만(보통 5단계 이전까지만) 오류를 역전파

출처: https://ratsgo.github.io/natural%20language%20processing/2017/03/09/rnnlstm/

 

참고) 텐서 조작

rnn 코드를 작성하면서 dim의 개념을 정확하게 이해할 필요가 있을 것 같아 텐서에 대해서 찾아봄

출처: https://wikidocs.net/52460

2차원 텐서

- 2차원 텐서는 (batch size, dim)으로 표현됨

- 훈련 데이터 하나의 크기를 256라고 가정([3, 1, 2, 5, ...]와 같은 숫자들의 나열이 256의 길이)하면 벡터의 차원은 256임

- 전체 훈련 데이터셋의 개수가 3000개, batch_size를 64개로 설정하면 컴퓨터는 한번에 64개의 데이터를 꺼내서 훈련하는 것임

 

3차원 텐서

- 자연어 처리에서는 3차원 텐서를 다룸

- 3차원 텐서는 (batch_size, length, dim)이며, (batch_size, 문장 길이, 단어 벡터의 차원)임

 

참고) 파이토치 튜토리얼

- 파이토치를 이용하여 RNN계열의 코드를 이해하는데 파이토치 한국어 튜토리얼을 공부한게 많은 도움이 되었음

https://tutorials.pytorch.kr/

RNN 셀 구현

- 책에 있는 코드를 그대로 돌리면 module 'torchtext' has no attribute 'legacy'와 같은 에러 발생

- 검색해보니까 최신 trochtext에는 legacy가 없는 것 같았음

- trochtext 낮은 버전을 설치하고, legacy도 따로 설치하는 방법으로 해결!

!pip install torchtext==0.10.1
import torch
import torchtext
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import time
!pip install torchtext.legacy

 

데이터 전처리

# 데이터 전처리를 위해 torchtext.legacy.data.Field 사용
start=time.time()
TEXT = torchtext.legacy.data.Field(lower=True, fix_length=200, batch_first=False) # 소문자로 변경, 데이터의 길이를 200으로 고정, 입력되는 데이터는 [시퀀스 길이, 배치크기, 은닉층의 뉴런 개수]
LABEL = torchtext.legacy.data.Field(sequential=False) # 데이터에 순서가 있는지(True가 기본값)
from torchtext.legacy import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
# 전처리
import string

for example in train_data.examples:
    text = [x.lower() for x in vars(example)['text']] # 소문자로 변경
    text = [x.replace("<br","") for x in text] # <br을 공백으로 변경
    text = [''.join(c for c in s if c not in string.punctuation) for s in text] # 구두점 제거
    text = [s for s in text if s] # 공백 제거
    vars(example)['text'] = text
    
for example in test_data.examples:
    text = [x.lower() for x in vars(example)['text']]
    text = [x.replace("<br","") for x in text]
    text = [''.join(c for c in s if c not in string.punctuation) for s in text]
    text = [s for s in text if s]
    vars(example)['text'] = text

 

데이터 train, valid data 분류

import random
train_data, valid_data = train_data.split(random_state = random.seed(0), split_ratio=0.8)

 

데이터 집합 만들기

- 단어 집합을 만들 때 max_size를 10000으로 지정하였는데 10002개인 이유는 단어 집합에 <unk> - 사전에 없는 문자열, <pad> - 정한 길이보다 짧은 샘플는 무조건 들어가야 하기 때문

# 단어 집합 만들기 위해 build_vocab
TEXT.build_vocab(train_data, max_size=10000, min_freq=10, vectors=None)
# max_size: 단어 집합에 포함되는 어휘 수
# min_freq: 훈련 데이터셋에서 특정 단어의 최소 등장 횟수
# vectors에서 임베딩 벡터를 지정할 수 있음(사전 학습된 임베딩으로는 워드투벡터, 글로브 등이 있음)
LABEL.build_vocab(train_data)

print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}")
print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}")
Unique tokens in TEXT vocabulary: 10002
Unique tokens in LABEL vocabulary: 3

 

데이터셋 메모리로 가져오기

- hidden_size는 은닉층의 유닛 개수를 지정하는 것이며, 일반적으로 계층의 유닛 개수를 늘리는 것보다 계층 자체에 대한 개수를 늘리는 것이 선능을 위해서 더 좋음(은닉층 층수는 인공신경망이 비선형 문제를 좀 더 잘 학습할 수 있도록 하는 반면에 층 안에 포함된 뉴런은 가중치와 바이어스를 계산하는 용도로 사용하기 때문)

- BucketIterator는 데이터로더와 비슷한 용도로 사용됨(배치 크기 단위로 값을 차례대로 꺼내어 메모리로 가져오고 싶을 때 사용함)

# 데이터셋을 메모리로 가져오기 위해 BucketIterator
BATCH_SIZE = 64
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

embeding_dim = 100 # 각 단어를 100차원으로 조정
hidden_size = 300 # 은닉층의 유닛 -> 은닉층을 유닛 단위로 설정해주는지 처음 알게됨!

train_iterator, valid_iterator, test_iterator = torchtext.legacy.data.BucketIterator.splits(
    (train_data, valid_data, test_data), 
    batch_size = BATCH_SIZE,
    device = device)

 

워드 임베딩 및 RNN 셀 정의

class RNNCell_Encoder(nn.Module):
    def __init__(self, input_dim, hidden_size):
        super(RNNCell_Encoder, self).__init__()
        self.rnn = nn.RNNCell(input_dim, hidden_size) # RNN 셀 구현 -> input_dim은 (배치, 입력 데이터 칼럼 개수), hidden_size는 은닉층의 (배치, 은닉층의 뉴런 개수) 형태를 가짐

    def forward(self, inputs): # inputs는 입력 시퀀스로 (시퀀스 길이, 배치, 임베딩)의 형태를 가짐
        bz = inputs.shape[1] # 배치를 가져옴
        ht = torch.zeros((bz, hidden_size)).to(device) # 배치와 은닉층 뉴런의 크기를 0으로 초기화

        for word in inputs:
            ht = self.rnn(word, ht) # 재귀적으로 발생하는 상태값을 처리하기 위한 구문(word는 현재 입력 벡터, ht는 이전 상태의 은닉층)
        return ht

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.em = nn.Embedding(len(TEXT.vocab.stoi), embeding_dim) # 임베딩 할 단어 수, 임베딩할 벡터의 차원
        self.rnn = RNNCell_Encoder(embeding_dim, hidden_size)
        self.fc1 = nn.Linear(hidden_size, 256)
        self.fc2 = nn.Linear(256, 3)

    def forward(self, x):
        x = self.em(x)
        x = self.rnn(x)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

 

옵티마이저와 손실 함수 정의

model = Net() # model이라는 이름으로 모델을 객체화
model.to(device)

loss_fn = nn.CrossEntropyLoss() # torch.nn.CrossEntropyloss는 다중 분류에 사용되며, nn.LogSoftmax와 nn.NLLLoss 연산의 조합으로 구성
# 신경망에서 로그 확률 값을 얻으려면 마지막에 LogSoftmax를 추가해야 함
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

 

모델 학습을 위한 함수 정의

- 코드에서 len(validloader.dataset)과 valid_total은 valid data의 갯수로 같은 값인데 따로 지정해주어서 다른 값인지 확인해보기 위해 print해봄 -> 생각한 값이 맞았음

def training(epoch, model, trainloader, validloader):
    correct = 0
    total = 0
    running_loss = 0

    model.train()
    for b in trainloader:
        x, y = b.text, b.label 
        x, y = x.to(device), y.to(device) # 반드시 모델과 같은 장치를 사용하도록 지정해야 함
        y_pred = model(x)
        loss = loss_fn(y_pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        with torch.no_grad():
            y_pred = torch.argmax(y_pred, dim=1)
            correct += (y_pred == y).sum().item()
            total += y.size(0)
            running_loss += loss.item()
    epoch_loss = running_loss / len(trainloader.dataset) # 누적된 오차를 전체 데이터셋으로 나누어서 에포크 단계마다 오차를 구함
    epoch_acc = correct / total

    valid_correct = 0
    valid_total = 0
    valid_running_loss = 0

    model.eval()
    with torch.no_grad():
        for b in validloader:
            x, y = b.text, b.label
            x, y = x.to(device), y.to(device)
            y_pred = model(x)
            loss = loss_fn(y_pred, y)
            y_pred = torch.argmax(y_pred, dim=1)
            valid_correct += (y_pred == y).sum().item()
            valid_total += y.size(0)
            valid_running_loss += loss.item()

    epoch_valid_loss = valid_running_loss / len(validloader.dataset)
    epoch_valid_acc = valid_correct / valid_total

    print('epoch: ', epoch,
          'loss: ', round(epoch_loss, 3),
          'accuracy:', round(epoch_acc, 3),
          'valid_loss: ', round(epoch_valid_loss, 3),
          'valid_accuracy:', round(epoch_valid_acc, 3),
          'len_validloader.dataset: ', len(validloader.dataset),
          'valid_total: ', (valid_total)
          )
    return epoch_loss, epoch_acc, epoch_valid_loss, epoch_valid_acc

 

실제 모델 학습 진행

- 모델이 학습된 결과를 보면 학습과 검증 데이터셋에 대한 오차가 유사하므로 과적합은 발생하지 않았다고 해석할 수 있음

epochs = 5
train_loss = []
train_acc = []
valid_loss = []
valid_acc = []

for epoch in range(epochs):
    epoch_loss, epoch_acc, epoch_valid_loss, epoch_valid_acc = training(epoch,
                                                                 model,                                                                 train_iterator,                                                                 valid_iterator)
    train_loss.append(epoch_loss)
    train_acc.append(epoch_acc)
    valid_loss.append(epoch_valid_loss)
    valid_acc.append(epoch_valid_acc)

end = time.time()
print(end-start)

--------------------------------------------------------------------------------

epoch:  0 loss:  0.011 accuracy: 0.528 valid_loss:  0.011 valid_accuracy: 0.496 len_validloader.dataset:  5000 valid_total:  5000
epoch:  1 loss:  0.011 accuracy: 0.54 valid_loss:  0.011 valid_accuracy: 0.511 len_validloader.dataset:  5000 valid_total:  5000
epoch:  2 loss:  0.011 accuracy: 0.549 valid_loss:  0.011 valid_accuracy: 0.493 len_validloader.dataset:  5000 valid_total:  5000
epoch:  3 loss:  0.011 accuracy: 0.552 valid_loss:  0.011 valid_accuracy: 0.508 len_validloader.dataset:  5000 valid_total:  5000
epoch:  4 loss:  0.01 accuracy: 0.559 valid_loss:  0.011 valid_accuracy: 0.508 len_validloader.dataset:  5000 valid_total:  5000
1866.6983551979065

 

테스트 데이터에 대한 예측

def evaluate(epoch, model, testloader):    
    test_correct = 0
    test_total = 0
    test_running_loss = 0
    
    model.eval()
    with torch.no_grad():
        for b in testloader:
            x, y = b.text, b.label
            x, y = x.to(device), y.to(device)
            y_pred = model(x)
            loss = loss_fn(y_pred, y)
            y_pred = torch.argmax(y_pred, dim=1)
            test_correct += (y_pred == y).sum().item()
            test_total += y.size(0)
            test_running_loss += loss.item()

    epoch_test_loss = test_running_loss / len(testloader.dataset)
    epoch_test_acc = test_correct / test_total

    print('epoch: ', epoch,
          'test_loss: ', round(epoch_test_loss, 3),
          'test_accuracy:', round(epoch_test_acc, 3)
          )
    return epoch_test_loss, epoch_test_acc
epochs = 5
test_loss = []
test_acc = []

for epoch in range(epochs):
    epoch_test_loss, epoch_test_acc = evaluate(epoch,
                                               model,
                                               test_iterator)
    test_loss.append(epoch_test_loss)
    test_acc.append(epoch_test_acc)

end = time.time()
print(end-start)

----------------------------------------------------

epoch:  0 test_loss:  0.011 test_accuracy: 0.503
epoch:  1 test_loss:  0.011 test_accuracy: 0.503
epoch:  2 test_loss:  0.011 test_accuracy: 0.503
epoch:  3 test_loss:  0.011 test_accuracy: 0.503
epoch:  4 test_loss:  0.011 test_accuracy: 0.503
2396.1323733329773

 

RNN 계층 구현

- RNN 모델의 코드는 아래와 같으며, 다른 부분은 RNN 셀 코드와 유사하므로 생략

# RNN 계층 네트워크
class BasicRNN(nn.Module):
    def __init__(self, n_layers, hidden_dim, n_vocab, embed_dim, n_classes, dropout_p = 0.2):
        super(BasicRNN, self).__init__()
        self.n_layers = n_layers # RNN 계층에 대한 개수(시점 수?)
        self.embed = nn.Embedding(n_vocab, embed_dim) # 워드 임베딩 적용
        self.hidden_dim = hidden_dim # 은닉층 차원
        self.dropout = nn.Dropout(dropout_p) # 드롭아웃 적용
        self.rnn = nn.RNN(embed_dim, self.hidden_dim, num_layers = self.n_layers, batch_first = True) # (훈련 데이터셋의 칼럼 개수, 은닉 계층의 뉴런(유닛) 개수, RNN 계층의 개수)
        # batch_first의 경우 기본값은 false이며, 입력 데이터의 형태는 (시퀀스의 길이, 배치 크기, 특성 개수) // true인 경우 (배치 크기, 시퀀스의 길이, 특성 개수)
        self.out = nn.Linear(self.hidden_dim, n_classes) 

    def forward(self, x):
        x = self.embed(x) 
        h_0 = self._init_state(batch_size = x.size(0)) # 최초 은닉 상태의 값을 0으로 초기화
        x, _ = self.rnn(x, h_0) # RNN 계층을 의미하며, 파라미터로 입력과 이전 은닉 상태의 값을 받음
        h_t = x[:, -1, :] # 모든 네트워크를 거쳐서 가장 마지막에 나온 단어의 임베딩 값(마지막 은닉 상태의 값)
        self.dropout(h_t)
        logit = torch.sigmoid(self.out(h_t))
        return logit

    def _init_state(self, batch_size = 1):
        weight = next(self.parameters()).data # 모델의 파라미터 값을 가져와서 weight 변수에 저장
        return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_() # 크기가 (계층의 수, 배치 크기, 은닉층의 뉴런/유닛 개수)인 은닉 상태를 생성하여 0으로 초기화한 후 반환

 

LSTM

- RNN의 경우 가중치가 업데이트되는 과정에서 기울기가 1보다 작은 값이 계속 곱해지기 때문에 기울기 소실 문제 발생

- 이를 해결하기 위해 LSTM이나 GRU와 같은 모델이 사용됨

 

  • LSTM 순전파

- LSTM은 기울기 소멸 문제를 해결하기 위해 망각 게이트, 입력 게이트, 출력 게이트라는 새로운 요소를 은닉층의 각 뉴런에 추가함

 

  • 망각 게이트

- 망각 게이트는 과거 정보를 어느 정도 기억할지 결정

- 과거 정보와 현재 데이터를 입력받아 시그모이드를 취한 후 그 값을 과거 정보에 곱해줌(시그모이드의 출력이 0이면 과거 정보를 버리고, 1이면 과거 정보 보존)

 

  • 입력 게이트

- 입력 게이트는 현재 정보를 기억하기 위해 만들어짐

- 과거 정보와 현재 데이터를 입력받아 시그모이드와 하이퍼볼릭 탄젠트 함수를 기반으로 현재 정보에 대한 보존량을 결정

 

- 망각 게이트와 입력 게이트의 셀 정보를 계산하여 현재 단계의 셀 상태를 업데이트

 

  • 출력 게이트

- 과거 정보와 현재 데이터를 사용하여 뉴런의 출력을 결정

 

  • LSTM 역전파

- LSTM은 셀을 통해서 역전파를 수행하기 때문에 '중단 없는 기울기'라고도 함

class LSTMCell(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True):
        super(LSTMCell, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.bias = bias
        self.x2h = nn.Linear(input_size, 4 * hidden_size, bias=bias) # gates가 망각, 입력, 셀, 출력으로 네 개로 쪼개지는 상황이기 때문에 4가 곱해진 것
        # 일반적으로 바이어스도 4를 곱해줌
        self.h2h = nn.Linear(hidden_size, 4 * hidden_size, bias=bias)
        self.reset_parameters()

    def reset_parameters(self):
        std = 1.0 / math.sqrt(self.hidden_size)
        for w in self.parameters():
            w.data.uniform_(-std, std)
    
    def forward(self, x, hidden):        
        hx, cx = hidden        
        x = x.view(-1, x.size(1))
        
        gates = self.x2h(x) + self.h2h(hx)    
        gates = gates.squeeze()        
        ingate, forgetgate, cellgate, outgate = gates.chunk(4, 1)
        
        ingate = F.sigmoid(ingate)
        forgetgate = F.sigmoid(forgetgate)
        cellgate = F.tanh(cellgate)
        outgate = F.sigmoid(outgate)
        
        cy = torch.mul(cx, forgetgate) +  torch.mul(ingate, cellgate)        
        hy = torch.mul(outgate, F.tanh(cy))        
        return (hy, cy)

 

GRU

- GRU는 LSTM에서 사용한 망각 게이트와 입력 게이트를 하나로 합친 것임

- 하나의 게이트 컨트롤러가 망각 게이트와 입력 게이트를 모두 제어(게이트 컨트롤러가 1을 출력하면 망각 게이트만 열리고, 반대로 0을 출력하면 입력 게이트만 열림)

 

  • 망각 게이트

- 과거 정보를 적당히 초기화시키려는 목적으로 시그모이드 함수를 출력으로 이용하여 (0, 1) 값을 이전 은닉층에 곱함

 

  • 업데이트 게이트

- 과거와 현재 정보의 최신화 비율을 결정하는 역할을 함

- 시그모이드로 출력된 결과는 현시점의 정보량을 결정하고 1에서 뺸 값을 직전 시점의 은닉층 정보와 곱함

 

  • 후보군

- 과거 은닉층의 정보를 그대로 이용하지 않고 망각 게이트의 결과를 이용하여 후보군 계산

 

  • 은닉층 계산

- 마지막으로 업데이트 게이트 결과와 후보군 결과를 결합하여 현시점의 은닉층을 계산함

class GRUCell(nn.Module):
    def __init__(self, input_size, hidden_size, bias=True):
        super(GRUCell, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.bias = bias
        self.x2h = nn.Linear(input_size, 3 * hidden_size, bias=bias) # LSRM 셀에서는 4를 곱했지만 GRU 셀에서는 세 개의 게이트가 사용되느로 3을 곱함
        self.h2h = nn.Linear(hidden_size, 3 * hidden_size, bias=bias)
        self.reset_parameters()

    def reset_parameters(self):
        std = 1.0 / math.sqrt(self.hidden_size)
        for w in self.parameters():
            w.data.uniform_(-std, std)
    
    def forward(self, x, hidden):        
        x = x.view(-1, x.size(1))
        
        gate_x = self.x2h(x) 
        gate_h = self.h2h(hidden)
        
        gate_x = gate_x.squeeze()
        gate_h = gate_h.squeeze()
        
        i_r, i_i, i_n = gate_x.chunk(3, 1)
        h_r, h_i, h_n = gate_h.chunk(3, 1)
                
        resetgate = F.sigmoid(i_r + h_r)
        inputgate = F.sigmoid(i_i + h_i)
        newgate = F.tanh(i_n + (resetgate * h_n))
        
        hy = newgate + inputgate * (hidden - newgate)              
        return hy

 

양방향 RNN

- 이전 시점의 데이터뿐만 아니라 이후 시점의 데이터도 함께 활용하여 출력 값을 예측하고자 하는 것이 양방향 RNN임

- 하나의 출력 값을 예측하는 데 메모리 셀 두 개를 사용함(첫 번째 메모리 셀은 이전 시점의 은닉 상태를 전달받아 현재의 은닉 상태를 계산하며, 두 번째 메모리 셀은 다음 시점의 은닉 상태를 전달받아 현재의 은닉 상태를 계산함)

- 이 두 개의 값을 출력 값을 예측하는 데 사용함

class biLSTM(nn.Module):
    def __init__(self, num_classes, input_size, hidden_size, num_layers, seq_length):
        super(biLSTM, self).__init__()
        self.num_classes = num_classes 
        self.num_layers = num_layers 
        self.input_size = input_size 
        self.hidden_size = hidden_size 
        self.seq_length = seq_length 

        self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size,
                          num_layers=num_layers, bidirectional=True, batch_first=True) # bidirectional=True 옵션을 사용하면 양방향 LSTM을 사용하겠다는 의미
        self.fc =  nn.Linear(hidden_size*2, num_classes) # 은닉 상태에 2를 곱해야 함
        self.relu = nn.ReLU()
    
    def forward(self,x):
        h_0 = Variable(torch.zeros(self.num_layers*2, x.size(0), self.hidden_size)) # 출력층에도 2를 곱해야 함
        c_0 = Variable(torch.zeros(self.num_layers*2, x.size(0), self.hidden_size))                 
        out, _ = self.lstm(x, (h_0, c_0)) 
        out = self.fc(out[:, -1, :])               
        out = self.relu(out)
        return out

 

 

출처: 서지영(2022). 딥러닝 파이토치 교과서. p360-p440.

https://ratsgo.github.io/natural%20language%20processing/2017/03/09/rnnlstm/

https://tutorials.pytorch.kr/

https://wikidocs.net/52460

Sooftware NLP - LSTM & GRU