Attention 기반 Sequence to Sequenct 모델 튜토리얼

현재 pytorch 공식 튜토리얼에 소개되고 있는 Attention 기반 Sequence to Sequence 튜토리얼을 따라하고 있는데요.(링크: 기초부터 시작하는 NLP: Sequence to Sequence 네트워크와 Attention을 이용한 번역 — 파이토치 한국어 튜토리얼 (PyTorch tutorials in Korean))

그런데, 모델은 잘 구축한 것 같은데, 역전파를 수행할 때, 텐서에 Auto Gradient function 이없다는 아래와 같은 메세지가 발생했습니다.

  • 에러 메세지 : "RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn"

대체 코드의 어떤 부분이 문제인지 잘 모르겠습니다. 답변 부탁드립니다.

코드 전문은 아래와 같습니다.

from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random

import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
from typing import List

SOS_TOKEN = 0
EOS_TOKEN = 1
MAX_LENGTH = 10

eng_prefixes = (
    "i am ", "i m ",
    "he is", "he s ",
    "she is", "she s ",
    "you are", "you re ",
    "we are", "we re ",
    "they are", "they re "
)


class Lang:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}  # label <-> word mapping
        self.n_words = 2  # word에 대한 labeling 하기 위함

    def addSentence(self, sentence):
        for word in sentence.split(" "):
            self.addWord(word)

    def addWord(self, word):
        # word가 처음 등장한다면
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.word2count[word] = 1  # 단어 등장 횟수 기록
            self.index2word[self.n_words] = word
            self.n_words += 1
        else:
            self.word2count[word] += 1

# unicode -> ascii 변환
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )

# 소문자, 다듬기, 그리고 문자가 아닌 문자 제거
def normalizeString(s):
    s = unicodeToAscii(s.lower().strip())
    s = re.sub(r"([.!?])", r" \1", s)
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s


def readLangs(lang1, lang2, reverse=False):
    print("Reading lines...")

    # 파일을 읽고 줄로 분리
    lines = open('dataset/data-pytorch-tutorial-seq2seq-attention/%s-%s.txt' % (lang1, lang2), encoding='utf-8').\
        read().strip().split('\n')

    # 모든 줄을 쌍으로 분리하고 정규화
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]

    # 쌍을 뒤집고, Lang 인스턴스 생성
    if reverse:
        pairs = [list(reversed(p)) for p in pairs]
        input_lang = Lang(lang2)
        output_lang = Lang(lang1)
    else:
        input_lang = Lang(lang1)
        output_lang = Lang(lang2)

    return input_lang, output_lang, pairs


def filterPair(p):
    return len(p[0].split(' ')) < MAX_LENGTH and \
        len(p[1].split(' ')) < MAX_LENGTH and \
        p[1].startswith(eng_prefixes)


def filterPairs(pairs):
    return [pair for pair in pairs if filterPair(pair)]

def prepareData(lang1, lang2, reverse=False):
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
    print("Read %s sentence pairs" % len(pairs))
    pairs = filterPairs(pairs)
    print("Trimmed to %s sentence pairs" % len(pairs))
    print("Counting words...")
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
    print("Counted words:")
    print(input_lang.name, input_lang.n_words)
    print(output_lang.name, output_lang.n_words)
    return input_lang, output_lang, pairs

input_lang, output_lang, pairs = prepareData('eng', 'fra', True)

def indexesFromSentence(lang, sentence):
    # 특정 단어의 index 번호를 리스트로 반환
    return [lang.word2index[word] for word in sentence.split(' ')]

def tensorFromSentence(lang, sentence, is_input=False):
    indexes: List[int] = indexesFromSentence(lang, sentence)
    indexes.append(EOS_TOKEN) # 문장의 끝이라고 표시하기 위한 토큰 끝에 추가
    tensor = torch.Tensor(indexes).to(torch.long)
    if is_input:
        return F.pad(tensor, pad=(0, MAX_LENGTH-tensor.size()[0])).view(1, -1)
    else:
        return tensor.view(1, -1, 1)

def tensorsFromPair(pair):
    global input_lang, output_lang
    input_tensor = tensorFromSentence(input_lang, pair[0], is_input=True) # french
    target_tensor = tensorFromSentence(output_lang, pair[1], is_input=True) # english
    return input_tensor, target_tensor

tensors = (tensorsFromPair(p) for p in pairs)

fren, engs = [], []
for t in tensors:
    fren.append(t[0])
    engs.append(t[1])

x_french = torch.cat(fren, dim=0)
y_english = torch.cat(engs, dim=0)

x_french.shape, y_english.shape

from torch.utils.data import Dataset, DataLoader

class FrenchEnglishDataset(Dataset):
    def __init__(self, x_french: torch.Tensor, y_english: List[torch.Tensor]):
        self.x_fren = x_french
        self.y_eng = y_english

    def __len__(self):
        return len(self.x_fren)

    def __getitem__(self, idx):
        x_fren: torch.Tensor = self.x_fren[idx]
        y_eng: torch.Tensor = self.y_eng[idx]
        return x_fren, y_eng

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

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

        self.vocab_size = vocab_size
        self.embedding_size = embedding_size
        self.hidden_size = hidden_size

        self.embed_layer = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_size)
        self.lstm = nn.LSTM(input_size=self.embedding_size, hidden_size=self.hidden_size, num_layers=1, batch_first=True)

    def forward(self, x):
        x = self.embed_layer(x)
        hs, (h, c) = self.lstm(x)
        return hs, h, c


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

        self.vocab_size = vocab_size
        self.embedding_size = embedding_size
        self.hidden_size = hidden_size

        self.embed_layer = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_size)
        self.lstm = nn.LSTM(input_size=self.embedding_size, hidden_size=self.hidden_size, num_layers=1, batch_first=True)
        self.affine = nn.Linear(in_features=self.hidden_size*2, out_features=self.vocab_size)

    def forward(self, enc_hs, enc_h, enc_c, y):
        """ 여기서는 1개의 input sequence가 들어왔다고 가정하고 작성되어야 함
        """
        x = self.embed_layer(y)
        dec_hs, (dec_h, dec_c) = self.lstm(x, (enc_h, enc_c))

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

        ch = torch.cat((c, dec_hs), dim=-1)
        z = self.affine(ch).squeeze()
        a = torch.argmax(z, dim=1, keepdim=True)
        return a

from torch.optim import Adam

# dataset
dataset = FrenchEnglishDataset(x_french, y_english)
dataloader = DataLoader(dataset, batch_size=128)

# params
french_vocab_size = len(input_lang.word2index)
english_vocab_size = len(output_lang.word2index)
embedding_size = 128
hidden_size = 256
learning_rate = 0.001

# model
encoder = Encoder(french_vocab_size, embedding_size, hidden_size)
decoder = Decoder(english_vocab_size, embedding_size, hidden_size)

# Loss
criterion = nn.CrossEntropyLoss()

# optimizer
encoder_optimizer = Adam(encoder.parameters(), lr=learning_rate)
decoder_optimizer = Adam(decoder.parameters(), lr=learning_rate)

# train
n_batch = 1
for x_fren, y_eng in dataloader:
    # init gradients
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()
    
    # Encoder-forward
    enc_hs, enc_h, enc_c = encoder(x_fren)

    # Decoder-forward (1-step씩)
    B, T = y_eng.size()
    decoder_input = torch.zeros(B, 1, dtype=torch.long)
    decoder_outputs = []
    for t in range(T):
        decoder_output = decoder(enc_hs, enc_h, enc_c, decoder_input)
        decoder_outputs.append(decoder_output)
        decoder_input = y_eng[:,t].view(-1,1) # Teacher-forcing
        
    decoder_outputs = torch.cat(decoder_outputs, dim=1).to(torch.float32)

    # loss
    decoder_labels = y_eng.to(torch.float32)
    loss = criterion(decoder_outputs, decoder_labels)
    print(loss.item())
    loss.backward()

    # update params
    encoder_optimizer.step()
    decoder_optimizer.step()

    print(f"Epoch | N({n_batch})-bacth Loss :{loss.item()}")
    n_batch += 1
    break

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

먼저 문제 해결을 위해 질문해 주셔서 감사합니다.

에러 메시지를 보면 "tensors does not require grad"라고 나오는데, 이는 역전파 계산을 위해서는 텐서에 대한 gradient 정보가 필요하다는 의미입니다.

즉, 모델 파라미터들의 텐서에 대해서 .requires_grad = True 설정을 추가해 주면 문제가 해결될 것 같습니다.

encoder = Encoder(french_vocab_size, embedding_size, hidden_size)
encoder.apply(lambda p: p.requires_grad_(True)) 

decoder = Decoder(english_vocab_size, embedding_size, hidden_size) 
decoder.apply(lambda p: p.requires_grad_(True))

위 코드에서 보듯이 Encoder, Decoder 모델 객체 생성 후, 각 모델의 파라미터들에 대해 requires_grad_ 메서드를 True로 설정해주면 gradient 연산 가능하게 만들어줍니다.

문제 해결이 도움이 되었길 바랍니다. 혹시 추가로 궁금한 점이 있다면 질문 부탁드립니다.


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

안녕하세요 young-hun-jo님, Attention 기반 Sequence to Sequence 모델을 PyTorch로 구현하시면서 겪고 계신 문제에 대해 도움을 드리고자 합니다. 제공해주신 정보를 바탕으로 문제를 정리해보겠습니다.

현재 PyTorch 공식 튜토리얼의 "기초부터 시작하는 NLP: Sequence to Sequence 네트워크와 Attention을 이용한 번역"을 따라하고 계시는데, 모델의 역전파 단계에서 "RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn"라는 오류 메시지가 발생했다고 합니다. 이는 텐서가 자동 미분을 위한 정보를 가지고 있지 않다는 것을 의미합니다.

문제를 해결하기 위해 제공해주신 코드를 살펴보았습니다. 오류 메시지가 나타나는 원인은 대개 모델의 파라미터나 입력 데이터가 올바르게 설정되지 않았을 때 발생합니다. 특히, 모델의 학습 가능한 파라미터에 대해 .requires_grad 속성이 True로 설정되어 있어야 하며, 손실 함수에 전달되는 입력과 타겟 텐서가 적절한 형태와 데이터 타입을 가져야 합니다.

코드를 살펴본 결과, 몇 가지 문제점을 발견했습니다. 먼저, tensorFromSentence 함수에서 텐서를 생성할 때 torch.Tensor 대신 torch.tensor를 사용해야 합니다. torch.Tensor는 기본적으로 requires_grad=False로 설정되어 있어 자동 미분이 활성화되지 않습니다. 또한, 손실 함수에 전달되는 타겟 텐서(decoder_labels)는 정수형(LongTensor)이어야 하며, to(torch.float32)로 변환하는 것은 적절하지 않습니다.

아래는 수정된 코드의 일부입니다. 주석을 통해 변경된 부분을 설명하겠습니다.

# ...

def tensorFromSentence(lang, sentence, is_input=False):
    indexes: List[int] = indexesFromSentence(lang, sentence)
    indexes.append(EOS_TOKEN) # 문장의 끝을 나타내는 토큰 추가
    tensor = torch.tensor(indexes, dtype=torch.long) # torch.tensor를 사용하고 dtype을 명시적으로 지정
    if is_input:
        return F.pad(tensor, pad=(0, MAX_LENGTH-tensor.size()[0])).view(1, -1)
    else:
        return tensor.view(1, -1, 1)

# ...

# train
n_batch = 1
for x_fren, y_eng in dataloader:
    # ...
    
    # loss
    decoder_labels = y_eng # 타겟 텐서는 LongTensor여야 함
    loss = criterion(decoder_outputs, decoder_labels) # 손실 함수에 전달
    print(loss.item())
    loss.backward()

    # ...

이 외에도 decoder_outputs 텐서와 decoder_labels 텐서의 차원이 일치하는지 확인하고, 필요한 경우 viewsqueeze 메서드를 사용하여 차원을 조정해야 할 수 있습니다.

위의 수정사항을 반영하여 코드를 다시 실행해보시기 바랍니다. 만약 여전히 문제가 해결되지 않는다면, 추가적인 정보를 제공해주시면 더 구체적인 도움을 드릴 수 있을 것입니다.

참고하실 수 있는 PyTorch 공식 문서 링크를 아래에 첨부합니다:

도움이 필요하시면 언제든지 질문해주세요. 오류 해결에 조금이나마 도움이 되길 바랍니다.


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

해결하였습니다! torch.argmax 함수를 사용하면 텐서에 grad_fn 이 사라지네요 ㅜ