논문 읽기/NLP

[논문 읽기] PyTorch 구현 코드로 살펴보는 GRU(2014), Learning Phrase Representation using RNN Encoder-Decoder for Statistical Machine Translation

AI 꿈나무 2021. 6. 16. 21:59
반응형

 안녕하세요, 오늘 읽은 논문은 Learning Phrase Representation using RNN Encoder-Decoder for Stistical Machine Translation 입니다. 해당 논문에서는 GRU를 제안합니다.

 

 이전 포스팅에서 살펴보았던 Seq2Seq와 이 논문의 차이점은 (1) LSTM 대신에 GRU를 사용합니다. (2) decoder의 각 셀에 context vector와 embedding vector를 추가합니다. 즉 hidden states 정보뿐만 아니라 embedding, context 정보까지 활용합니다.

 

GRU(Gated Recurrent Units)

 GRU는 LSTM에서 영감을 받아 탄생한 구조입니다. LSTM보다 단순한 구조를 갖고 있으며, cell을 사용하지 않습니다. LSTM과 GRU의 성능이 비슷하다는 후속 연구결과를 보면, 둘의 구조는 다르지만, 동일한 역할을 한다고 이해해볼 수 있습니다.

 

 GRU의 작동원리에 대해 상세하게 설명하는 블로그 포스팅을 참고했습니다.

 

Gated Recurrent Units (GRU)

Gated Recurrent Units (GRU) GRU는 게이트 메커니즘이 적용된 RNN 프레임워크의 일종으로 LSTM에 영감을 받았고, 더 간략한 구조를 가지고 있습니다. 아주 자랑스럽게도 한국인 조경현 박사님이 제안한 방

yjjo.tistory.com

 

GRU의 개요

 

 GRU는 reset gate와 update gate이 존재합니다. LSTM은 forget, get, update 3개의 gate가 존재했으며 추가적으로 cell도 존재했습니다. 확실히 간단한 구조를 갖고 있다는 것을 알 수 있습니다. 또한 가중치의 수가 적기 때문에 적은 파라미터를 갖습니다.

 

(1) reset gate

 

 reset gate는 입력 임베딩x와 이전 히든스테이츠 h의 정보를 활용하여, 미래에 필요없는 정보를 버립니다. reset gate는 다음과 같이 계산합니다.

 

 

 위 수식을 살펴보면, 가중치 Wr와 Ur가 존재합니다. Wr와 Ur가 입력 임베딩, 이전 히든스테이츠와 matmul한 후에 서로 더해집니다. 이 값이 시그모이드를 통과하여 0~1 범위의 값을 갖습니다.

 

 

 계산된 reset gate는 입력 임베딩 x와 이전 히든스테이츠 정보를 사용하여 새로운 히든 스테이츠를 생성합니다. 이 새로운 히든 스테이츠는 다음 히든 스테이츠를 계산하기 위해 필요합니다. 활성화 함수는 Tanh를 사용합니다.

 

 

 즉, 우리가 reset gate를 계산한 이유는 이전 히든 스테이츠에서 필요없는 정보를 버리기 위함으로 생각할 수 있습니다.

 

(2) update gate

 

 updata gate에서는 가중치 Wz와 Uz가 존재합니다. 입력 임베딩 x와 이전 히든스테이츠 h의 정보를 활용하여 update gate를 계산합니다.

 

 

 

 이전에 reset gate로 계산한 새로운 히든 스테이츠와 update gate를 활용하여 다음 히든스테이츠를 계산합니다.

 

 

 최종적으로 GRU에서 사용하는 수식은 다음과 같습니다.

 

 

RNN Encoder-Decoder

 

 

 GRU 논문에서는 Seq2Seq와 다른 새로운 encoder-decoder 구조를 제안합니다. Context vector와 입력 임베딩을 다음과 같이 GRU와 y에 입력합니다. GRU는 context vector, 입력 임베딩, 이전 히든스테이츠 정보를 활용해서 다음 히든 스테이츠를 계산합니다. GRU의 출력값은 다음 히든스테이츠에 입력 임베딩과 context vector가 concat되어 FC layer를 거친 후에 최종 예측값을 계산합니다.

 

PyTorch 구현 코드

 코드 출처: https://github.com/bentrevett/pytorch-seq2seq

 

Encoder

LSTM과 GRU의 차이점은 GRU에 cell을 입력하지 않습니다. 

# input sequence를 encode 합니다.
class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, dropout):
        super().__init__()

        self.hid_dim = hid_dim # hiden state 차원
        
        self.embedding = nn.Embedding(input_dim, emb_dim) # 입력 토큰을 emb_dim 차원으로 임베딩
        
        self.rnn = nn.GRU(emb_dim, hid_dim) # GRU 사용
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):
        
        #src = [src len, batch size]
        
        embedded = self.dropout(self.embedding(src)) # 입력 토큰을 임베딩
        
        #embedded = [src len, batch size, emb dim]
        
        outputs, hidden = self.rnn(embedded) # 임베딩을 GRU에 전달
        
        #outputs = [src len, batch size, hid dim * n directions]
        #hidden = [n layers * n directions, batch size, hid dim]
        
        return hidden

 

decoder

 encoder가 생성한 context vector를 decode 합니다.

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, dropout):
        super().__init__()

        self.hid_dim = hid_dim # hidden state 차원
        self.output_dim = output_dim # 출력값 차원
        
        # 이전 GRU의 output을 emb_dim 차원으로 임베딩
        self.embedding = nn.Embedding(output_dim, emb_dim)
        
        # GRU 생성, 입력값: 입력 임베딩, context veotor, 이전 히든 스테이츠
        self.rnn = nn.GRU(emb_dim + hid_dim, hid_dim)
        
        # fc layer의 입력값은 입력 임베딩, 히든 스테이츠, context vector
        self.fc_out = nn.Linear(emb_dim + hid_dim * 2, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, input, hidden, context):
        
        #input = [batch size]
        #hidden = [n layers * n directions, batch size, hid dim]
        #context = [n layers * n directions, batch size, hid dim]
        
        #n layers and n directions in the decoder will both always be 1, therefore:
        #hidden = [1, batch size, hid dim]
        #context = [1, batch size, hid dim]
        
        # 이전 GRU의 출력값
        input = input.unsqueeze(0)
        
        #input = [1, batch size]
        
        # 이전 GRU의 출력값을 embedding
        embedded = self.dropout(self.embedding(input))
        
        #embedded = [1, batch size, emb dim]
                
        # Context vector와 embedding을 concat
        emb_con = torch.cat((embedded, context), dim = 2)
            
        #emb_con = [1, batch size, emb dim + hid dim]
            
        # 입력값: 입력 임베딩, context vector, 이전 히든 스테이츠
        output, hidden = self.rnn(emb_con, hidden)
        
        #output = [seq len, batch size, hid dim * n directions]
        #hidden = [n layers * n directions, batch size, hid dim]
        
        #seq len, n layers and n directions will always be 1 in the decoder, therefore:
        #output = [1, batch size, hid dim]
        #hidden = [1, batch size, hid dim]
        
        output = torch.cat((embedded.squeeze(0), hidden.squeeze(0), context.squeeze(0)), 
                           dim = 1)
        
        #output = [batch size, emb dim + hid dim * 2]
        
        prediction = self.fc_out(output)
        
        #prediction = [batch size, output dim]
        
        return prediction, hidden

 


참고자료

[1] https://yjjo.tistory.com/18

[2] https://github.com/bentrevett/pytorch-seq2seq

[3] https://arxiv.org/abs/1406.1078

반응형