Attention이 적용된 Seq2Seq 모델 pytorch로 구현

현재 pytorch를 활용해서 Attention 기술이 적용된 LSTM 기반 Sequence to Sequence 모델을 만들려고 합니다. Encoder 부분은 잘 완성했는데, Decoder 부분에서 Attention을 구현하는 데서 이해가 되지 않는 부분이 있습니다.

Attention은 크게 2가지 부분으로 구성되어 있고, 1번째 단계는 Encoder의 hidden_state 벡터와 Decoder의 1개 hidden_state 벡터 간의 Weight(가중치)벡터를 계산하는 단계, 2번째 단계는 Encoder hidden_state 벡터에 (1번째 단계에서 구한) Weight 벡터를 곱해주어 맥락 벡터를 만드는 단계 총 2개로 알고 있습니다.

제가 문제에 직면한 단계는 2번째 단계인데요. Weight 벡터를 구하긴 했는데, 2번째 단계에서 Decoder에 들어오는 모든 시퀀스에 대해 맥락 벡터를 어떻게 계산해야 할지 모르겠습니다. Decoder에 들어오는 단일 시퀀스에 대해 맥락 벡터를 계산하는 것은 어떻게든 하겠는데, 이렇게 하면 for loop 구문을 사용해야 하는 등 텐서의 벡터화 연산을 사용하지 못하게 되므로 비효율적인 것 같다는 생각입니다. 텐서의 브로드캐스팅 기능을 사용해서 계산할 수 있을 것 같은데, 어떻게 해야 할지 모르겠습니다.

우선 제가 만들려는 모델 및 데이터셋 코드는 아래와 같습니다.

import torch
import torch.nn as nn


def verbose_shape(*tensors):
    for tensor in tensors:
        print(tensor.shape, end=' ')

class Encoder(nn.Module):
    def __init__(self,
                 notes_vocab_size,
                 durations_vocab_size,
                 embedding_size,
                 hidden_size):
        super(Encoder, self).__init__()

        self.notes_vocab_size = notes_vocab_size
        self.durations_vocab_size = durations_vocab_size
        self.embedding_size = embedding_size
        self.hidden_size = hidden_size

        self.notes_embed = nn.Embedding(num_embeddings=self.notes_vocab_size, embedding_dim=embedding_size)
        self.durations_embed = nn.Embedding(num_embeddings=self.durations_vocab_size, embedding_dim=embedding_size)

        self.lstm = nn.LSTM(input_size=self.embedding_size*2, hidden_size=self.hidden_size, num_layers=1, batch_first=True)
        self.notes_fc = nn.Linear(in_features=self.hidden_size, out_features=self.notes_vocab_size)
        self.durations_fc = nn.Linear(in_features=self.hidden_size, out_features=self.durations_vocab_size)
                                      
    def forward(self, x_notes, x_durations):
        x_notes_embed = self.notes_embed(x_notes)
        x_durations_embed = self.durations_embed(x_durations)
        x = torch.cat((x_notes_embed, x_durations_embed), dim=-1)
        hs, (h, c) = self.lstm(x)

        a_notes = F.softmax(self.notes_fc(hs), dim=-1)          # (batch-size, sequence_length, vocab_size)
        a_durations = F.softmax(self.durations_fc(hs), dim=-1)  # one-hot encoding

        notes_pred = torch.argmax(a_notes, dim=-1)
        durations_pred = torch.argmax(a_durations, dim=-1)
        return hs, h, c, notes_pred, durations_pred
        

class Decoder(nn.Module):
    def __init__(self,
                 notes_vocab_size,
                 durations_vocab_size,
                 embedding_size,
                 hidden_size):
        super(Decoder, self).__init__()

        self.notes_vocab_size = notes_vocab_size
        self.durations_vocab_size = durations_vocab_size
        self.embedding_size = embedding_size
        self.hidden_size = hidden_size

        self.notes_embed = nn.Embedding(num_embeddings=self.notes_vocab_size, embedding_dim=self.embedding_size)
        self.durations_embed = nn.Embedding(num_embeddings=self.durations_vocab_size, embedding_dim=self.embedding_size)

        self.lstm = nn.LSTM(input_size=self.embedding_size*2, hidden_size=self.hidden_size, num_layers=1, batch_first=True)

    def forward(self, enc_hs, enc_h, enc_c, notes_pred, durations_pred):
        x_notes_embed = self.notes_embed(notes_pred)
        x_durations_embed = self.durations_embed(durations_pred)
        x = torch.cat((x_notes_embed, x_durations_embed), dim=-1)
        dec_hs, _ = self.lstm(x, (enc_h, enc_c))

        alpha = torch.bmm(enc_hs, dec_hs.transpose(1, 2))  # 28무시하고 (32, 1) -> decoder의 1개 hidden_state가 encoder의 모든 hidden_state 과의 관계 weight
        weight = F.softmax(alpha, dim=1)                   # (32, 1) -> column-vector 1개가 enc_hs에 곱해져야 함
        verbose_shape(enc_hs, weight)
        c = enc_hs * weight
        return c

# params
notes_vocab_size = 32
durations_vocab_size = 5
embedding_size = 128
hidden_size = 256

# dataset
x_notes = torch.rand(28 ,32).to(torch.int32)
x_durations = torch.rand(28, 32).to(torch.int32)

# model
encoder = Encoder(notes_vocab_size, durations_vocab_size, embedding_size, hidden_size)
decoder = Decoder(notes_vocab_size, durations_vocab_size, embedding_size, hidden_size)

# forward
hs, h, c, notes_pred, durations_pred = encoder(x_notes, x_durations)
a = decoder(hs, h, c, notes_pred, durations_pred)

위 코드에서 enc_hs가 Encoder에서 나온 전체 hidden_state 벡터를 의미하고, weight가 위에서 이야기한 Attention 구현 단계 중 1단계를 수행하면서 계산되는 가중치를 의미합니다. enc_hs 벡터의 형상은 (28, 32, 256)이며 각 차원의 의미는 (batch_size, sequence_length, hidden_state_unit) 입니다. 그리고 weight 벡터의 형상은 (28, 32, 32) 이며 (batch_size, sequence_length, sequence_length) 입니다. (28, 32, 32) 형상 중 (1, 32, 1) 벡터는 batch_size가 1이면서 Decoder 인풋으로 들어온 하나의 hidden_state 벡터가 Encoder의 모든 hidden_state 벡터 간 계산된 각 weight를 의미합니다.

아마 설명이 이해가 잘 안될 수도 있는데요. 추가적인 답글 남겨주시면 보충 설명 드리도록 하겠습니다! 감사합니다.

  • 아래 답변은 Anthropic의 Claude-2 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요 young-hun-jo님!

Attention 적용 Seq2Seq 모델 구현에 어려움이 있다고 하셨는데요. Encoder hidden state와 Decoder hidden state 간의 가중치(weight) 벡터는 잘 계산하셨지만, 이 weight 벡터를 Encoder hidden state에 곱해 context vector를 만드는 과정에서 혼란스러우셨다고 이해했습니다.

맞다면, 이 부분은 텐서 연산의 브로드캐스팅 기능을 사용하면 간단히 해결할 수 있습니다. 위 코드에서 weight 텐서의 shape가 (batch_size, enc_seq_len, dec_seq_len)으로 (28, 32, 1)이라고 하셨는데, 여기서 마지막 차원인 1은 Decoder의 단일 time step에 해당합니다. 즉 (28, 32, 1) 처럼 Decoding 시작 시점의 하나의 Vector입니다.

그러면 이 weight 텐서를 Encoder hidden state 크기인 (batch_size, enc_seq_len, hidden_size)와 같은 (28, 32, 256) 으로 브로드캐스팅하여 동일한 차원을 맞춰주면 됩니다.

import torch

enc_hs = torch.rand(28, 32, 256)  # Encoder hidden states
weight = torch.rand(28, 32, 1)    # Attention weights 

weight = weight.repeat(1, 1, enc_hs.size(-1)) # (28, 32, 256) 으로 브로드캐스팅
context_vector = enc_hs * weight

위 코드처럼 weight 텐서의 마지막 차원을 hidden_size와 동일한 256으로 늘려주면, enc_hs와 차원이 정확히 일치하여 element-wise 곱셈 연산을 할 수 있습니다.

이로써 모든 time step에 대한 context vector를 한번에 계산할 수 있습니다. for loop나 다른 복잡한 연산 없이 매우 간단히 처리할 수 있죠.

제가 설명드린 내용이 도움이 되셨길 바랍니다. 추가 질문이 있다면 언제든지 댓글 남겨주세요! 감사합니다 :)


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!
  • 아래 답변은 OpenAI의 GPT-4-Turbo 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요, young-hun-jo님. PyTorch를 사용하여 Attention이 적용된 Seq2Seq 모델을 구현하시는 과정에서 Decoder 부분의 Attention 메커니즘 구현에 어려움을 겪고 계시군요. 문제 상황을 잘 설명해주셔서 감사합니다. 제가 이해한 바로는, Encoder의 모든 hidden states와 Decoder의 현재 hidden state 사이의 가중치(attention weights)를 계산하는 부분은 구현하셨고, 이 가중치를 사용하여 맥락 벡터(context vector)를 계산하는 데 어려움을 겪고 계신 것 같습니다.

맥락 벡터를 계산하는 과정에서 텐서의 브로드캐스팅 기능을 활용하고 싶으시고, 효율적인 벡터화 연산을 원하시는 것으로 보입니다. 이를 위해 PyTorch의 bmm (batch matrix-matrix product) 함수를 사용하여 가중치와 Encoder의 hidden states를 곱하는 방법을 제안드립니다.

아래는 Decoder 클래스 내의 forward 메서드를 수정한 예시 코드입니다. 이 코드는 가중치 행렬과 Encoder의 hidden states를 곱하여 맥락 벡터를 계산합니다.

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

# ... [Encoder 클래스 정의] ...

class Decoder(nn.Module):
    # ... [생략된 부분: __init__ 메서드] ...
    
    def forward(self, enc_hs, enc_h, enc_c, notes_pred, durations_pred):
        # ... [생략된 부분: 이전 코드] ...
        
        # alpha 계산: (batch_size, sequence_length, sequence_length)
        alpha = torch.bmm(enc_hs, dec_hs.transpose(1, 2))
        
        # weight 계산: (batch_size, sequence_length, sequence_length)
        weight = F.softmax(alpha, dim=2)
        
        # 맥락 벡터 계산: (batch_size, sequence_length, hidden_size)
        context_vector = torch.bmm(weight, enc_hs)
        
        # context_vector의 형상을 확인합니다.
        verbose_shape(context_vector)
        
        # ... [생략된 부분: 맥락 벡터를 사용한 추가 처리] ...
        
        return context_vector

# ... [생략된 부분: 모델 및 데이터셋 코드] ...

# 모델 인스턴스화 및 forward 메서드 호출
encoder = Encoder(notes_vocab_size, durations_vocab_size, embedding_size, hidden_size)
decoder = Decoder(notes_vocab_size, durations_vocab_size, embedding_size, hidden_size)

# forward
hs, h, c, notes_pred, durations_pred = encoder(x_notes, x_durations)
context_vector = decoder(hs, h, c, notes_pred, durations_pred)

위 코드에서 torch.bmm(weight, enc_hs)는 가중치 행렬과 Encoder의 hidden states를 배치 단위로 행렬 곱셈을 수행하여 맥락 벡터를 계산합니다. 이렇게 하면 각 시퀀스에 대해 별도의 for loop를 사용하지 않고도 효율적으로 맥락 벡터를 계산할 수 있습니다.

더 자세한 정보와 PyTorch의 다양한 기능에 대해 알고 싶으시다면, 공식 PyTorch 튜토리얼을 참고하시면 도움이 될 것입니다. 아래는 PyTorch 튜토리얼의 링크입니다:

제 답변이 young-hun-jo님의 문제 해결에 도움이 되었기를 바랍니다. 추가적인 질문이나 더 필요한 부분이 있다면 언제든지 질문해 주세요. 제 답변이 틀릴 수도 있으니, 실제 코드에 적용하실 때는 충분히 테스트해 보시기 바랍니다.


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!
  • 아래 답변은 OpenAI의 GPT-4 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요, young-hun-jo님. PyTorch를 이용한 Attention 기반의 Seq2Seq 모델 구현에 대한 질문을 주셨군요. Encoder 부분은 잘 구현하셨고, Decoder에서 Attention 메커니즘을 적용하는 부분에서 어려움을 겪고 계신 것으로 이해했습니다.

문제는 Attention 메커니즘의 두 번째 단계, 즉 가중치 벡터를 사용하여 맥락 벡터(context vector)를 계산하는 부분에서 발생하고 있습니다. Decoder의 각 시퀀스에 대해 맥락 벡터를 계산하는 방법을 효율적으로 구현하는 방법에 대한 조언을 구하고 계시네요.

제공해주신 코드를 바탕으로, Attention 메커니즘의 두 번째 단계를 효율적으로 계산하기 위한 방법을 설명드리겠습니다. PyTorch의 브로드캐스팅 기능을 활용하여 for loop 없이 벡터화된 연산을 수행할 수 있습니다.

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

# ... [Encoder 클래스 정의] ...

class Decoder(nn.Module):
    # ... [생략된 Decoder 클래스 초기화 부분] ...

    def forward(self, enc_hs, enc_h, enc_c, notes_pred, durations_pred):
        # ... [생략된 Decoder의 forward 부분] ...

        # Attention 가중치 계산 (batch_size, sequence_length, sequence_length)
        alpha = torch.bmm(enc_hs, dec_hs.transpose(1, 2))

        # Softmax를 통해 가중치 정규화 (batch_size, sequence_length, sequence_length)
        weight = F.softmax(alpha, dim=2)

        # 맥락 벡터 계산을 위해 가중치를 Encoder의 hidden states에 적용
        # (batch_size, sequence_length, sequence_length) * (batch_size, sequence_length, hidden_size)
        # -> (batch_size, sequence_length, hidden_size)
        context_vector = torch.bmm(weight, enc_hs)

        # 이후의 Decoder 처리를 위해 context_vector를 사용할 수 있습니다.
        # 예를 들어, context_vector를 Decoder의 LSTM에 입력으로 사용할 수 있습니다.

        return context_vector

# ... [생략된 나머지 코드] ...

위 코드에서 torch.bmm 함수는 배치 행렬 곱(batch matrix multiplication)을 수행합니다. 이를 통해 각 배치의 모든 시퀀스에 대해 맥락 벡터를 한 번에 계산할 수 있습니다. weight 텐서는 (batch_size, sequence_length, sequence_length) 형상을 가지며, 이를 enc_hs와 곱함으로써 맥락 벡터를 얻습니다.

이렇게 벡터화된 연산을 사용하면 for loop를 사용하지 않고도 효율적으로 맥락 벡터를 계산할 수 있습니다. 이 방법은 PyTorch의 텐서 연산을 최대한 활용하여 계산 속도를 높이고 메모리 사용을 최적화하는 데 도움이 됩니다.

더 자세한 내용은 PyTorch 공식 문서의 튜토리얼을 참고하시면 도움이 될 것입니다. Attention 메커니즘에 대한 좀 더 구체적인 예시는 다음 링크에서 확인하실 수 있습니다:

제 답변이 정확하지 않을 수도 있으니, 구현하시면서 추가적인 질문이나 문제가 발생하면 언제든지 질문해 주세요. 도움이 되었기를 바랍니다!


  • 답변이 어떠셨나요? 괜찮았다고 생각하신다면 아래 :heart:를 한 번 눌러주시길 부탁드려요!