사실 이전 LSTM 모델에서 에폭을 5로 줄여서 학습을 시켜보았는데
모든걸 부정적으로 보는 모델이 생성되었다
개선할 수 있는 방법은 없을까???
떠오른 방법과 부트 캠프가 추천하는 방법은 아래와 같다.
부트캠프 추천
자 그러면 떠오른 방법의 예상되는 성능 향상률을 생각해보자
class GRUModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim):
super(GRUModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.gru = nn.GRU(embed_dim, hidden_dim, batch_first=True) # GRU로 변경
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
embedded = self.embedding(text)
output, hidden = self.gru(embedded) # GRU는 hidden 상태만 반환
return self.fc(hidden[-1]) # 마지막 타임스텝의 출력을 사용
2-1. 손실함수 수정, optimzer 수정
단 라벨 스무딩을 이용해줄 생각임
라벨 스무딩이란?
정답이 클래스 2라면 정답 레이블은 [0, 0, 1]처럼 나타납니다. 하지만 라벨 스무딩을 적용하면 이 레이블이 [0.1, 0.1, 0.8]처럼 정답 클래스가 1이 아닌 소프트한 확률 분포로 변환됩니다.
즉, 과적합되는 것을 방지해주는 것이라고 생각하면 된다.
- optimzer 수정:
1. SGD는
배치 데이터의 무작위 샘플을 선택하여 손실 함수를 최소화하는 방향으로 가중치를 업데이트함
매 반복마다 (미니배치 또는 전체 배치) 기울기를 계산하고, 학습률(learning rate)과 기울기를 곱하여 가중치를 조정함
특징 :
학습률이 고정되어 있으며, 기울기의 변동에 민감합니다.
수렴 속도가 느릴 수 있으며, 지역 최적점에 빠지기 쉬운 경향이 있습니다.
2. Adam는
SGD의 확장으로, 각 파라미터에 대해 개별적인 학습률을 사용함
즉, 기울기뿐만 아니라 기울기의 이동 평균(모멘텀)과 제곱 기울기의 이동 평균(스케일)도 고려함
특징 :
각 파라미터에 대한 학습률이 적응적으로 변경되므로,
기울기의 크기가 다를 때 더 안정적인 업데이트가 가능합니다.
학습률의 감소가 자동으로 조정되므로, 초기 학습률이 상대적으로 커도 효과적으로 수렴할 수 있습니다.
일반적으로 빠르고 효율적으로 수렴하며, 지역 최적점에 빠질 위험이 적습니다.
-> Adam으로 바꿔보기 결론
optimizer = optim.Adam(model.parameters(), lr=0.01)
3. 레이블 데이터를 변경 (기준이 좀 더 포괄적인 것으로)
아래의 라이브러리와 변환 함수를 이용한 방법임
# 텍스트 전처리와 자연어 처리를 위한 라이브러리
import nltk
from textblob import TextBlob
# 토픽 모델링을 위한 라이브러리
import gensim
from gensim import corpora
from gensim.utils import simple_preprocess
# 감성 분석을 위한 함수
def get_sentiment(text):
return TextBlob(text).sentiment.polarity
4. 모델 변경 (LSTM -> LSTM + CNN)
-> 시너지를 내겠다는 논문 발견
import torch
import torch.nn as nn
import torch.nn.functional as F
class CNNLSTMModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim, kernel_sizes, num_filters):
super(CNNLSTMModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
# CNN 레이어
self.convs = nn.ModuleList([
nn.Conv2d(1, num_filters, (ks, embed_dim)) for ks in kernel_sizes
])
self.lstm = nn.LSTM(num_filters * len(kernel_sizes), hidden_dim, batch_first=True)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
# 텍스트를 임베딩으로 변환
embedded = self.embedding(text) # shape: (batch_size, seq_len, embed_dim)
embedded = embedded.unsqueeze(1) # shape: (batch_size, 1, seq_len, embed_dim)
# CNN 레이어 통과
conv_outputs = [F.relu(conv(embedded)).squeeze(3) for conv in self.convs] # 각 conv 레이어 통과
pooled_outputs = [F.max_pool1d(output, output.size(2)).squeeze(2) for output in conv_outputs] # max pooling
cnn_output = torch.cat(pooled_outputs, 1) # shape: (batch_size, num_filters * len(kernel_sizes))
# LSTM에 입력으로 사용
cnn_output = cnn_output.unsqueeze(1) # LSTM의 입력 형태 맞추기
lstm_out, (hidden, cell) = self.lstm(cnn_output) # LSTM 통과
return self.fc(hidden[-1]) # 마지막 타임스텝의 출력을 사용
5. Attention 알고리즘 적용 (LSTM -> LSTM + Attention 알고리즘)
Attention 개념 복습
먼저 인코더와 디코더 사이에 층이 하나 생깁니다.
새로 삽입된 층에는 각 셀로부터 계산된 스코어들이 모입니다. 이 스코어를 이용해 소프트맥스 함수를 사용해서 어텐션 가중치를 만듭니다. 이 가중치를 이용해 입력 값 중 어떤 셀을 중점적으로 볼지 결정합니다.
예를 들어 첫 번째 출력 단어인 ‘당신께’ 자리에 가장 적절한 단어는 4번째 셀 ‘you’라는 것을 학습하는 것이지요. 이러한 방식으로 매 출력마다 모든 입력 값을 두루 활용하게 하는 것이 어텐션입니다.
필요한 이유?
RNN은 여러 개의 입력 값이 있을 때 이를 바로 처리하는 것이 아니라 잠시 가지고 있는 것이라고 했습니다. 입력된 값끼리 서로 관련이 있다면 이를 모두 받아 두어야 적절한 출력 값을 만들 수 있겠지요.
-> RNN은 오래된 정보의 경우에는 그 중요도가 희미해지는 문제가 있기 때문에 Attention 알고리즘을 적용해 중요한 정보에 가중치를 주자라는 아이디어임
Attention 클래스
class Attention(nn.Module):
def __init__(self, hidden_dim):
super(Attention, self).__init__()
# 쿼리 벡터
self.Wa = nn.Linear(hidden_dim, hidden_dim)
# 키 벡터
self.Ua = nn.Linear(hidden_dim, hidden_dim)
# 쿼리와 키 벡터로 생성된 Score
self.Va = nn.Linear(hidden_dim, 1)
def forward(self, lstm_output):
# lstm_output: (batch_size, seq_len, hidden_dim)
batch_size, seq_len, _ = lstm_output.size()
hidden = lstm_output[:, -1, :] # 마지막 타임스텝의 hidden state
# Score 계산
score = self.Va(torch.tanh(self.Wa(lstm_output) + self.Ua(hidden).unsqueeze(1)))
attention_weights = torch.softmax(score, dim=1) # 가중치 계산
# Context vector 생성
context_vector = torch.sum(attention_weights * lstm_output, dim=1)
return context_vector
class LSTMModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim):
super(LSTMModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True)
self.attention = Attention(hidden_dim) # Attention 모듈 추가
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
embedded = self.embedding(text)
output, (hidden, cell) = self.lstm(embedded) # unsqueeze(0) 제거
context_vector = self.attention(output) # Attention을 통해 context vector를 생성
return self.fc(context_vector) # 최종 예측을 위한 fully connected layer

텍스트 데이터 변환 과정에 대한 기본적인 이해
(입력값이 list여야 한다, Torch 변환이 필요한 경우) 가 시간이 꽤나 걸렸지만 확실한 이해가 더 우선이기 때문에 이해를 완전히 하도록 계속 복기해야겠다.
유용했던 라이브러리
제대로 데이터 로더를 거치는지가 궁금했음
-> tqdm을 사용해보자