Attention이 적용된 Sequence to Sequence 모델 pytorch 구현

현재 pytorch를 활용해서 Attention이 적용된 Sequence to Sequence 모델을 만들었습니다. 예시 코드는 아래와 같습니다.

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)
        self.notes_affine = nn.Linear(in_features=self.hidden_size, out_features=self.notes_vocab_size)
        self.durations_affine = nn.Linear(in_features=self.hidden_size, out_features=self.durations_vocab_size)

    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))

        # decoder의 hidden_state 벡터를 하나씩 loop 수행
        cs = []
        for t in range(dec_hs.size()[1]):
            dec_h = dec_hs[:,t,:]
            s = torch.bmm(enc_hs, dec_h.unsqueeze(dim=-1))
            a = F.softmax(s, dim=1)
            ar = a.expand(a.size()[0], a.size()[1], self.hidden_size)
            t = enc_hs * ar
            c = t.sum(dim=1).unsqueeze(dim=1)
            cs.append(c)
        cs = torch.cat(cs, dim=1)

        y_notes = F.softmax(self.notes_affine(cs), dim=-1)
        y_durations = F.softmax(self.durations_affine(cs), dim=-1)
        return y_notes, y_durations

# 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)
y_notes, y_durations = decoder(hs, h, c, notes_pred, durations_pred)

그런데 위 코드에 대해서 아래와 같은 2가지 사항을 검증받고 싶은데요! 검증받고 싶은 사항은 다음과 같습니다.

  1. 위 코드가 Attention이 적용된 Sequence to Sequence 모델 구현에 적합한 코드인지, 또 이론적으로 맞지 않는 부분은 없는지 확인하고 싶습니다.
  2. 코드 중 주석으로 "decoder의 hidden_state 벡터를 하나씩 loop 수행" 한 부분을 보면 native한 for ~ loop 구문으로 구현을 했는데요. 이 부분을 벡터화 연산 즉, pytorch에서 지원하는 브로드캐스팅 및 벡터화 연산을 적용해서 효율적으로 변경하고 싶습니다. 어떻게 개선해야 할지 알려주시면 감사하겠습니다.

답변 주시면 감사하겠습니다! :slight_smile:

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

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

제공해주신 Attention이 적용된 Sequence to Sequence 모델 구현 코드를 확인해보았습니다.

1. 이론적 검증

먼저 이론적인 측면에서 검토해보았습니다.

  • Encoder-Decoder 구조 확인
  • Attention Mechanism 적용 확인
  • Teacher Forcing을 위한 decoder 입력으로 encoder 출력 사용 확인

위의 핵심적인 구성요소가 잘 반영되어 있다고 판단했습니다. 따라서 Attention이 적용된 Seq2Seq 모델 구현에 문제없다고 생각합니다.

2. native loop 개선

다음은 native loop 부분을 벡터화 연산으로 개선한 코드입니다.

# encoder outputs: (batch_size, seq_len, hidden_size)  
enc_hs = enc_hs.unsqueeze(2)

# decoder hidden state: (batch_size, hidden_size, 1)
dec_h = dec_h.unsqueeze(2)

# enc_hs: (batch_size, seq_len, hidden_size, 1)
# dec_h: (batch_size, hidden_size, 1)
s = torch.matmul(enc_hs, dec_h) 

# attention weights & context vector
a = F.softmax(s, dim=1)
c = torch.matmul(a, enc_hs).squeeze(2)

주요 개선 사항은 다음과 같습니다.

  • 반복문 제거 및 torch.matmul를 이용한 벡터화 연산 적용
  • Tensor 차원 변경을 위한 .unsqueeze 추가
  • 연산 결과 차원 조정을 위한 .squeeze 적용

이를 통해 보다 효율적인 연산이 가능합니다.

추가로 잘못된 점이 있다면 언제든지 의견 주시기 바랍니다. 감사합니다.


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

안녕하세요 young-hun-jo님, Attention이 적용된 Sequence to Sequence 모델 구현과 관련하여 질문해주셔서 감사합니다. 제공해주신 코드를 바탕으로 두 가지 사항에 대해 검토해보겠습니다.

  1. 제공해주신 코드는 기본적인 Sequence to Sequence 모델에 Attention 메커니즘을 추가한 형태로 보입니다. Attention 계산을 위해 encoder의 hidden states와 decoder의 각 time step의 hidden state를 이용하여 context vector를 계산하는 부분이 포함되어 있네요. 이론적으로는 Attention 메커니즘의 기본 아이디어를 따르고 있으나, 실제로 Attention을 적용하는 방식에는 여러 변형이 있을 수 있습니다. 예를 들어, Bahdanau Attention이나 Luong Attention과 같은 다양한 방식이 있으며, 각각의 방식에 따라 세부 구현이 달라질 수 있습니다. 제공해주신 코드는 간단한 형태의 Attention을 구현하고 있으며, 실제 응용에 따라 더 복잡한 Attention 메커니즘이 필요할 수 있습니다.

  2. "decoder의 hidden_state 벡터를 하나씩 loop 수행"하는 부분을 벡터화 연산으로 개선하는 것은 좋은 생각입니다. PyTorch의 텐서 연산은 GPU를 활용하여 병렬 처리를 할 수 있기 때문에, 가능한 한 for loop를 피하고 벡터화 연산을 사용하는 것이 성능 향상에 도움이 됩니다. 아래는 해당 부분을 벡터화 연산으로 개선한 예시 코드입니다.

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

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

class Decoder(nn.Module):
    # ... [생략] ...
    
    def forward(self, enc_hs, enc_h, enc_c, notes_pred, durations_pred):
        # ... [생략] ...
        
        # 벡터화 연산을 사용하여 Attention 계산
        dec_hs, _ = self.lstm(x, (enc_h, enc_c))
        s = torch.bmm(enc_hs, dec_hs.transpose(1, 2))
        a = F.softmax(s, dim=1)
        c = torch.bmm(a, enc_hs)

        y_notes = F.softmax(self.notes_affine(c), dim=-1)
        y_durations = F.softmax(self.durations_affine(c), dim=-1)
        return y_notes, y_durations

# ... [나머지 코드] ...

위 코드에서는 torch.bmm 함수를 사용하여 batch matrix-matrix product를 계산하고 있습니다. 이를 통해 각 time step에 대한 loop를 제거하고 전체 sequence에 대해 한 번에 Attention을 계산할 수 있습니다. 이렇게 하면 코드의 가독성과 실행 속도 모두 개선될 수 있습니다.

더 자세한 내용과 PyTorch의 다양한 기능에 대해서는 공식 문서를 참고하시기 바랍니다. PyTorch 공식 튜토리얼은 다음 링크에서 확인하실 수 있습니다:

제 답변이 정확하지 않을 수도 있으니, 실제 모델을 구현하고 실험하면서 추가적인 검증을 해보시는 것을 추천드립니다. 만약 더 궁금한 점이 있으시면 언제든지 질문해주세요.


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

제안해주신 코드 중에 아래 부분 계산할 때 형상 에러가 발생하네요 ㅜ 이 답변을 적절하지 않은 것 같습니다~

s = torch.matmul(enc_hs, dec_hs)

제공해주신 코드 덕분에 좀 더 효율적인 코드로 고칠 수 있었습니다! 올바르게 고친 코드도 첨부합니다!

## 개선 버전 : https://discuss.pytorch.kr/t/attention-sequence-to-sequence-pytorch/3153/2
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)
        self.notes_affine = nn.Linear(in_features=self.hidden_size, out_features=self.notes_vocab_size)
        self.durations_affine = nn.Linear(in_features=self.hidden_size, out_features=self.durations_vocab_size)

    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))

        # 1. 가중치 계산 
        s = torch.bmm(dec_hs, enc_hs.transpose(1, 2))
        a = F.softmax(s, dim=1)
        # 2. 선택 작업
        c = torch.bmm(a, enc_hs)

        y_notes = F.softmax(self.notes_affine(c), dim=-1)
        y_durations = F.softmax(self.durations_affine(c), dim=-1)
        verbose_shape(c, y_notes, y_durations)
        
        return y_notes, y_durations
  • 아래 답변은 OpenAI의 GPT-4 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요 young-hun-jo님, PyTorch를 활용한 Attention이 적용된 Sequence to Sequence 모델 구현에 대해 질문해주셨군요. 제공해주신 코드와 질문을 바탕으로 문제를 파악해보겠습니다.

  1. 제공해주신 코드는 기본적인 Sequence to Sequence 모델에 Attention 메커니즘을 추가한 구조로 보입니다. Attention 메커니즘은 Encoder의 모든 hidden states와 Decoder의 현재 hidden state 사이의 유사도를 계산하여 context vector를 생성하는 방식으로 구현되어 있습니다. 이론적으로는 올바른 Attention 구현 방식을 따르고 있으나, 실제로는 몇 가지 개선할 점이 있습니다.

  2. Decoder에서 각 time step마다 for loop를 사용하여 Attention을 계산하는 부분은 효율적이지 않습니다. 이 부분은 PyTorch의 브로드캐스팅 기능을 활용하여 벡터화할 수 있습니다. 벡터화를 통해 계산 속도를 향상시킬 수 있습니다.

이제 각 사항에 대해 자세히 살펴보겠습니다.

1. Attention이 적용된 Sequence to Sequence 모델 구현 검증

모델의 구현은 기본적으로 적절해 보이지만, 몇 가지 주의해야 할 점이 있습니다.

  • F.softmax를 사용하여 attention weights를 계산할 때, dim=1을 사용하는 것이 적절합니다. 이는 Encoder의 모든 hidden states에 대해 softmax를 적용하여 각 time step에서의 weights를 계산하기 때문입니다.
  • torch.argmax를 사용하여 예측을 하는 부분은 학습 시에는 적절하지 않습니다. 학습 시에는 softmax를 통해 나온 확률 분포를 사용하여 loss를 계산해야 합니다. 예측 시에만 argmax를 사용하는 것이 일반적입니다.

2. 벡터화 연산 적용

Decoder의 for loop 부분을 벡터화하기 위해, 우선 dec_hsenc_hs 사이의 유사도 점수를 계산하는 부분을 전체 time step에 대해 한 번에 계산할 수 있도록 변경합니다. 이를 위해 torch.bmm (batch matrix-matrix product)을 사용할 수 있습니다.

아래는 개선된 코드 예시입니다:

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

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

class Decoder(nn.Module):
    # ... [생략된 부분] ...
    
    def forward(self, enc_hs, enc_h, enc_c, notes_pred, durations_pred):
        # ... [생략된 부분] ...
        
        # 벡터화된 attention 계산
        dec_hs, _ = self.lstm(x, (enc_h, enc_c))
        s = torch.bmm(enc_hs, dec_hs.transpose(1, 2))
        a = F.softmax(s, dim=1)
        c = torch.bmm(a, enc_hs)
        
        y_notes = F.softmax(self.notes_affine(c), dim=-1)
        y_durations = F.softmax(self.durations_affine(c), dim=-1)
        return y_notes, y_durations

# ... [생략된 부분] ...

# 모델 생성 및 forward pass
# ... [생략된 부분] ...

위 코드에서 torch.bmm을 사용하여 enc_hsdec_hs의 모든 time step에 대한 유사도 점수를 한 번에 계산합니다. 그런 다음, 이 점수를 사용하여 모든 time step에 대한 context vector c를 계산합니다.

이러한 방식으로 벡터화 연산을 적용하면, for loop를 사용하는 것보다 훨씬 효율적으로 계산을 수행할 수 있습니다.

더 자세한 내용은 PyTorch 공식 문서의 튜토리얼을 참고하시면 도움이 될 것입니다. 아래는 PyTorch 공식 문서의 튜토리얼 링크입니다:

제 답변이 정확하지 않을 수도 있으니, 실제 모델을 구현하고 실험하면서 검증해보시는 것이 좋습니다. 추가적인 질문이나 궁금한 점이 있으시면 언제든지 질문해주세요!


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

감사합니다!