seq2seq pytorch 튜토리얼 Loss 수렴하지 못하는 문제

Attention을 사용하는 LSTM 기반의 Sequence to Sequenct 모델을 사용해서 불어를 영어로 번역하는 언어 모델을 만들고 있습니다. 레퍼런스는 pytorch 튜토리얼에서 제공하는 가이드를 따랐는데요.(링크: 기초부터 시작하는 NLP: Sequence to Sequence 네트워크와 Attention을 이용한 번역 — 파이토치 한국어 튜토리얼 (PyTorch tutorials in Korean))

제가 튜토리얼에서 제공하는 코드를 그대로 따라하지 않고 데이터셋와 데이터 전처리 방법만 이용하되 모델링하는 코드는 제가 직접 작성했습니다. 그런데 아래와 같이 제가 만든 모델로 7만 5천번의 Epoch로 학습을 시키고 있습니다. 그런데 처음에 Loss값이 3500정도 하던 것이 Epoch가 5,000번에 접어들면서 Loss값이 2473 까지 수렴하였습니다. 그런데 문제는 그 이후로 Loss 값이 더 줄어들지 않고 있습니다.

학습이 잘되었는지 눈으로 확인하기 위해서 Attention weight matrix를 확인하였고, 불어와 영어의 동일한 의미의 단어 간에 weight 값이 높은 것도 확인하여 학습이 잘 되고 있는 상태임은 확인했습니다. 다만, 더 Loss 값을 줄지는 않는 상태입니다. 어떤 점들을 더 개선시켜야할까요?

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

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

import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torch.optim import Adam

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/%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

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

## => input 시퀀스 길이가 서로 달라서, pytorch-tutorial에서는 encoder도 loop로 예측함
## => output 시퀀스 길이가 서로 달라서, 하나의 tensor로 concatenate 할 수 없어서 for loop로 예측함
## => 하지만 MAX_LENGTH로 padding 해서 하나의 tensor로 통합할 수는 있음. 고로 input은 패딩 하고, output은 어차피 loop로 예측하기 떄문에, concatenate 하지 않기

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

def verbose_grad_fn(*tensors):
    for t in tensors:
        print(t._grad_fn)
        
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
    

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

    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))
        w = F.softmax(s, dim=1)
        # 2.선택 작업
        c = torch.sum(enc_hs * w, dim=1, keepdim=True)

        ch = torch.cat((c, dec_hs), dim=-1)
        z = self.affine(ch).view(-1,1)
        return z, w
    

def save_model(encoder: nn.Module, decoder: nn.Module, epoch: int, loss, attn_weights: torch.Tensor, x_fren: torch.Tensor, y_eng: torch.Tensor):
    ckpt = {"epoch": epoch,
            "loss": loss.data.item(),
            "encoder_state_dict": encoder.state_dict(),
            "decoder_state_dict": decoder.state_dict(),
            "attn_weights": attn_weights,
            "x_fren": x_fren,
            "y_eng": y_eng
           }
    
    torch.save(ckpt, f"seq2seq_ckpt/epoch_{epoch}.pt")
    print(f"ㄴ Epoch {epoch} checkpoint is successfully saved!")
    

# data preprocessing
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 "
)

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

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)

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

# params
french_vocab_size = input_lang.n_words
english_vocab_size = output_lang.n_words
embedding_size = 128
hidden_size = 256
learning_rate = 0.001
epochs = 75000

# 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
start = time.time()
ckpt_start = time.time()
for epoch in range(epochs):
    losses = []
    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 = []
        attn_weights = []
        for t in range(T):
            decoder_output, attn_w = decoder(enc_hs, enc_h, enc_c, decoder_input)
            decoder_outputs.append(decoder_output)
            attn_weights.append(attn_w)
            decoder_input = y_eng[:,t].view(-1,1) # Teacher-forcing
            
        decoder_outputs = torch.cat(decoder_outputs, dim=1).to(torch.float32)
        attn_weights = torch.cat(attn_weights, dim=-1)
    
        # loss
        decoder_labels = y_eng.to(torch.float32)
        loss = criterion(decoder_outputs, decoder_labels)
        loss.backward()
        losses.append(loss.item())
    
        # update params
        encoder_optimizer.step()
        decoder_optimizer.step()
    
        n_batch += 1
    print(f"Epoch({epoch+1})| N({n_batch}) -> Loss: {sum(losses)/len(losses): .3f}")
    if (epoch+1) % 5000 == 0:
        save_model(encoder, decoder, epoch+1, loss, attn_weights, x_fren, y_eng)
        ckpt_end = time.time()
        print("## => Training time during per checkpoint periods:", round(ckpt_end - ckpt_start), "secs")
        ckpt_start = ckpt_end
print()
print("### ==> Total training time:", round(time.time() - start),"secs")
  • 아래 답변은 Anthropic의 Claude-2 모델로 자동 생성한 것으로, 정확하지 않을 수 있습니다.
  • 잘못된 답변을 보셨다면 댓글로 첨삭 지도 부탁드립니다. :sweat_smile:

안녕하세요 young-hun-jo님, seq2seq 모델의 loss 수렴 문제로 고민이신 것 같습니다. 저도 pytorch 튜토리얼을 참고하여 유사한 문제를 겪은 적이 있기에 공감합니다.

제가 보기에 code와 전처리 과정이 잘 구현된 것 같습니다. 다만 loss가 더 이상 감소하지 않는 문제는 overfitting 때문일 가능성이 높다고 생각합니다.

몇 가지 overfitting을 막을 수 있는 tip을 드리겠습니다.

  • 데이터 크기를 늘려보세요. 지금 7500번의 에포크로 훈련시키고 있는데, 데이터가 적어 overfitting이 일어나고 있을 수 있습니다.
  • Dropout layer를 추가하여 regularization 효과를 높입니다.
  • Batch size를 줄여보세요. 128보다 작은 batch size로 해보면 과적합이 적어질 수 있습니다.

이외에도 gaussian noise 주입, weight decay 설정 등의 방법으로 regularization을 해보시길 바랍니다.

만약에 위의 tip들을 모두 시도해보셨음에도 여전히 loss가 감소하지 않는다면, 모델 아키텍처 자체의 문제일 수 있습니다. 이 경우에는 LSTM 대신 GRU나 Transformer 계열로 바꿔보시는 것도 좋습니다.

추가로 궁금한 점이 있으시면 주저마시고 질문 주세요. 함께 고민하겠습니다!


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

안녕하세요! seq2seq 모델을 사용하여 불어를 영어로 번역하는 언어 모델을 만드는 과정에서 Loss가 수렴하지 않는 문제를 겪고 계신 것으로 이해했습니다. PyTorch 튜토리얼을 참고하여 데이터셋과 데이터 전처리는 그대로 사용하고 모델링 코드는 직접 작성하셨다고 하셨네요. 학습 과정에서 Attention weight matrix를 통해 어느 정도 학습이 진행되고 있음을 확인하셨지만, Loss가 더 이상 감소하지 않는 상황이신 것 같습니다.

Loss가 더 이상 줄어들지 않는 문제는 여러 가지 원인이 있을 수 있습니다. 다음은 이러한 문제를 해결하기 위해 시도해볼 수 있는 몇 가지 방법입니다:

  1. 학습률(Learning Rate) 조정: 현재 학습률이 너무 높거나 낮을 수 있습니다. 학습률을 조정하여 실험해보세요. 학습률 스케줄러를 사용하여 학습 과정에서 학습률을 점진적으로 감소시키는 것도 좋은 방법입니다.

  2. 모델의 복잡도 증가: 모델이 데이터의 복잡성을 충분히 학습할 수 있도록 LSTM의 레이어 수를 늘리거나 hidden size를 증가시켜보세요.

  3. 정규화 기법 사용: 과적합을 방지하기 위해 Dropout이나 Weight Decay와 같은 정규화 기법을 사용해보세요.

  4. 데이터셋 확인: 데이터셋에 문제가 없는지 확인하고, 필요하다면 데이터를 더 추가하거나 다양화시켜보세요.

  5. Teacher Forcing 비율 조정: 학습 과정에서 Teacher Forcing을 사용하고 있다면, 이 비율을 조정해보세요. 너무 높은 비율은 모델이 자체 예측에 의존하는 것을 학습하지 못하게 할 수 있습니다.

  6. Gradient Clipping: Gradient Exploding 문제가 발생할 수 있으므로, Gradient Clipping을 적용해보세요.

  7. 다른 최적화 알고리즘 시도: Adam 외에도 SGD, RMSprop 등 다른 최적화 알고리즘을 시도해볼 수 있습니다.

아래는 학습률 스케줄러를 적용하는 예시 코드입니다:

from torch.optim.lr_scheduler import StepLR

# 모델, 손실 함수, 최적화 알고리즘 정의 후

# 학습률 스케줄러 설정
scheduler = StepLR(encoder_optimizer, step_size=1000, gamma=0.95)
scheduler_decoder = StepLR(decoder_optimizer, step_size=1000, gamma=0.95)

# 학습 루프
for epoch in range(epochs):
    # ...
    # 학습 과정
    # ...
    
    # 스케줄러의 학습률 갱신
    scheduler.step()
    scheduler_decoder.step()

또한, Gradient Clipping을 적용하는 예시 코드는 다음과 같습니다:

# 학습 루프 내부
for x_fren, y_eng in dataloader:
    # ...
    # 손실 계산 후 역전파
    loss.backward()
    
    # Gradient Clipping 적용
    torch.nn.utils.clip_grad_norm_(encoder.parameters(), max_norm=1)
    torch.nn.utils.clip_grad_norm_(decoder.parameters(), max_norm=1)
    
    # 매개변수 업데이트
    encoder_optimizer.step()
    decoder_optimizer.step()

이러한 방법들을 시도해보시고, 여전히 문제가 해결되지 않는다면 모델 구조나 데이터 전처리 과정에 문제가 있을 수 있으니, 코드를 다시 한번 검토해보시기 바랍니다.

더 자세한 정보는 PyTorch 공식 문서의 학습률 스케줄러 섹션(Learning Rate Scheduler)과 Gradient Clipping 섹션(Gradient Clipping)을 참고하시기 바랍니다.

제 답변이 정확하지 않을 수도 있으니, 여러 방법을 시도해보시고 커뮤니티에도 질문을 남겨 다른 전문가들의 의견을 들어보시는 것도 좋을 것 같습니다. 문제 해결에 도움이 되길 바랍니다!


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

안녕하세요 young-hun-jo님, seq2seq 모델을 사용하여 불어를 영어로 번역하는 언어 모델을 만드는 과정에서 Loss가 수렴하지 않는 문제에 대해 질문해주셨군요. 문제 상황을 잘 설명해주셔서 이해하는 데 도움이 되었습니다.

문제를 정리해보면, Attention을 사용하는 LSTM 기반의 seq2seq 모델을 학습시키는 과정에서 초기에는 Loss가 감소하다가 특정 지점 이후로 더 이상 감소하지 않는 현상을 겪고 계시는 것으로 보입니다. 모델의 Attention weight matrix를 통해 학습이 어느 정도는 잘 진행되고 있음을 확인하셨지만, Loss 감소가 멈춘 상태라고 하셨습니다.

이러한 문제를 해결하기 위해 몇 가지 점을 살펴볼 수 있습니다:

  1. 학습률(Learning Rate) 조정: 현재 사용하고 계신 학습률이 너무 높거나 낮을 수 있습니다. 학습률을 조정하여 실험해보세요. 학습률 스케줄러를 사용하여 학습 과정에서 학습률을 점진적으로 감소시키는 방법도 고려해볼 수 있습니다.

  2. 과적합(Overfitting) 확인: 모델이 훈련 데이터에 과적합되어 있을 수 있습니다. 검증 데이터셋을 사용하여 모델의 일반화 성능을 확인하고, 필요하다면 정규화 기법(예: 드롭아웃)을 추가하거나 모델의 복잡도를 줄여보세요.

  3. 데이터 전처리 개선: 데이터 전처리 과정에서 더 많은 데이터 정제 작업을 수행하거나 데이터 증강을 통해 모델이 학습할 수 있는 다양성을 높여보세요.

  4. 모델 구조 변경: 현재 모델 구조나 하이퍼파라미터 설정이 최적이 아닐 수 있습니다. 다른 구조의 LSTM 레이어나 Attention 메커니즘을 시도해보세요.

  5. 손실 함수 및 최적화 알고리즘 변경: 사용하고 있는 손실 함수나 최적화 알고리즘이 문제에 적합하지 않을 수 있습니다. 다른 손실 함수나 최적화 알고리즘을 사용해보세요.

  6. 배치 크기 조정: 배치 크기를 조정하여 모델이 더 안정적으로 학습할 수 있도록 해보세요.

  7. 기울기 클리핑(Gradient Clipping): RNN과 같은 순환 신경망에서는 기울기 폭발 문제가 발생할 수 있습니다. 기울기 클리핑을 적용하여 이를 방지해보세요.

아래는 몇 가지 수정 사항을 포함한 코드 예시입니다:

# 학습률 스케줄러 설정
encoder_scheduler = optim.lr_scheduler.StepLR(encoder_optimizer, step_size=1000, gamma=0.95)
decoder_scheduler = optim.lr_scheduler.StepLR(decoder_optimizer, step_size=1000, gamma=0.95)

# 기울기 클리핑을 위한 함수
def clip_gradients(model, max_norm):
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

# 학습 루프 내에서
for epoch in range(epochs):
    # ... [중략] ...
    loss.backward()
    clip_gradients(encoder, 1.0)  # 기울기 클리핑 적용
    clip_gradients(decoder, 1.0)  # 기울기 클리핑 적용
    encoder_optimizer.step()
    decoder_optimizer.step()
    encoder_scheduler.step()  # 학습률 스케줄러 업데이트
    decoder_scheduler.step()  # 학습률 스케줄러 업데이트
    # ... [중략] ...

위의 코드에서 clip_gradients 함수는 모델의 모든 파라미터에 대해 기울기 클리핑을 적용합니다. encoder_schedulerdecoder_scheduler는 학습률을 점진적으로 감소시키는 역할을 합니다.

더 자세한 정보와 다양한 기법들에 대해서는 PyTorch 공식 문서의 튜토리얼을 참고하시기 바랍니다:

제가 제안한 해결 방법이 문제 해결에 도움이 되길 바라며, 추가적인 질문이나 정보가 필요하시면 언제든지 질문해주세요. 제 답변이 정확하지 않을 수도 있으니, 여러 시도를 통해 최적의 해결책을 찾으시길 바랍니다.


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