01. 시퀀스 데이터란?
시퀀스 데이터는 순서가 중요하고, 앞뒤 문맥이 데이터의 의미를 결정하는 데이터를 말합니다.
- 특징: 동일한 단어라도 앞뒤 문맥에 따라 의미가 달라지므로, 순서를 반영할 수 있는 모델이 필요합니다.
- 예시:
- 텍스트: 문장이나 문서 (단어의 순서가 의미 형성)
- 시계열: 날마다 변동하는 주가, 매출 데이터
- 음성: 시간 축에 따라 흐르는 음성 신호
02. RNN (Recurrent Neural Network, 순환 신경망)
RNN은 시퀀스 데이터를 처리하기 위한 가장 기본적인 딥러닝 모델입니다.
- 핵심 원리: 현재 시점의 입력 x_t와 직전 시점의 은닉 상태 h_{t-1}를 함께 사용하여 현재의 은닉 상태 h_t를 계산합니다. 이를 통해 이전 정보를 기억하며 다음 시점으로 넘어갑니다.
- 수식:

- 한계 (기울기 소실/폭발): 문장이 길어질수록 역전파 과정에서 기울기가 0에 가까워지거나(소실) 너무 커지는(폭발) 문제가 발생합니다.
- 결과: 문장 앞부분의 정보가 뒷부분까지 전달되지 못해 장기 의존성(Long-term dependency) 학습이 어렵습니다. (예: 문장 끝에서 의미가 반전되는 경우 파악 힘듦)
03. LSTM (Long Short-Term Memory)
RNN의 장기 기억 문제를 해결하기 위해 고안된 모델로, 기억을 보존하는 별도의 경로를 만든 것이 특징입니다.
- 두 가지 상태:
- Cell state (C_t): 정보가 거의 그대로 흘러가는 장기 기억의 고속도로 역할.
- Hidden state (h_t): 각 시점에서 출력되는 요약 정보.
- 3개의 게이트 (정보의 흐름 조절):
- Forget gate: 이전 기억 중 불필요한 것을 얼마나 지울지 결정.
- Input gate: 이번에 들어온 새로운 정보를 얼마나 저장할지 결정.
- Output gate: 현재 상태를 바탕으로 최종 출력을 얼마나 내보낼지 결정.
04. GRU (Gated Recurrent Unit)
LSTM은 성능이 좋지만 구조가 복잡하여 연산량이 많습니다. GRU는 LSTM을 더 단순화하여 효율성을 높인 모델입니다.
- 특징: Cell state 없이 Hidden state 하나만 사용합니다.
- 2개의 게이트:
- Update gate: 이전 정보와 새 정보를 얼마나 섞을지 결정 (LSTM의 Forget+Input 역할).
- Reset gate: 이전 정보를 얼마나 초기화할지 결정.
- 장점: LSTM보다 파라미터 수가 적어 계산이 빠르고 가볍지만, 성능은 비슷합니다. 실무에서 LSTM과 함께 자주 사용됩니다.

nn.LSTM 모델 파라미터
lstm = nn.LSTM(
input_size=embed_dim,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True, # (batch, seq, feature) 형태 사용
bidirectional=False, # 한 방향으로만(왼→오른쪽) 시퀀스를 읽는 구조
)
'''
batch_first=True: (batch_size, sequence_length, feature_size) 형태로 입력/출력 텐서가 구성됩니다. 이는 일반적인 딥러닝 프레임워크에서 많이 사용하는 형태로, PyTorch의 기본값은 False입니다.
batch_first=False (기본값): (sequence_length, batch_size, feature_size) 형태로 텐서가 구성됩니다.'''
# LSTM
def forward(self, input_ids):
emb = self.embedding(input_ids)
output, (h_n, c_n) = self.lstm(emb)
last_hidden = h_n[0]
logits = self.fc(last_hidden)
return logits
# GRU
def forward(self, input_ids):
emb = self.embedding(input_ids)
output, (h_n, c_n) = self.gru(emb)
last_hidden = h_n[-1]
logits = self.fc(last_hidden).squeeze(1)
return logits
# forward 리턴 : LSTM → (output, (h_n, c_n)), GRU → (output, h_n)

예제1
import torch
import torch.nn as nn
# 1. 하이퍼파라미터 및 데이터 정의
vocab_size = 4 # 전체 어휘의 개수 (단어 4개: 나는, 밥을, 라면을, 먹었다)
embed_dim = 4 # 단어를 표현할 임베딩 벡터의 차원 (input_size와 동일해야 함)
hidden_size = 3 # LSTM의 은닉 상태(Hidden state) 벡터의 크기
num_layers = 1 # 사용할 LSTM 레이어의 개수 (단층 LSTM)
# 단어와 인덱스를 매핑하는 사전(Vocabulary)
vocab = {"나는": 0, "밥을": 1, "라면을": 2, "먹었다": 3}
# 단어 인덱스로 표현된 두 개의 시퀀스(문장)
sent1 = [vocab["나는"], vocab["밥을"], vocab["먹었다"]] # [0, 1, 3]
sent2 = [vocab["나는"], vocab["라면을"], vocab["먹었다"]] # [0, 2, 3]
# 배치(Batch) 생성: (batch_size=2, seq_len=3)
# batch_size=2: 문장 2개
# seq_len=3: 각 문장의 길이 3
batch = torch.tensor([sent1, sent2]) # shape: (2, 3)
print("batch:", batch)
print("batch.shape:", batch.shape)
# ---
# 2. 임베딩 레이어 정의 및 통과
# nn.Embedding: 단어 인덱스를 밀집된 벡터(임베딩 벡터)로 변환
embedding = nn.Embedding(
num_embeddings=vocab_size, # 임베딩할 단어의 개수
embedding_dim=embed_dim, # 임베딩 벡터의 차원
)
# 단어 인덱스 → 임베딩 벡터로 변환
# batch (2, 3) → emb (2, 3, 4)
emb = embedding(batch) # shape: (batch_size, seq_len, embed_dim)
print("\n[임베딩 통과 후]")
print("emb.shape:", emb.shape) # (2, 3, 4)
# ---
# 3. LSTM 레이어 정의 및 통과
# nn.LSTM: 순환 신경망 레이어 정의
lstm = nn.LSTM(
input_size=embed_dim, # 각 타임스텝 입력 벡터 크기 (emb의 마지막 차원: 4)
hidden_size=hidden_size, # 은닉 상태 벡터 크기: 3
num_layers=num_layers, # LSTM 층 개수: 1
batch_first=True, # 입력/출력 텐서 형태를 (batch, seq_len, feature)로 설정
)
# LSTM 통과:
# lstm(입력 텐서) → (출력, (최종 은닉 상태, 최종 셀 상태))
output, (h_n, c_n) = lstm(emb)
# 결과 출력
print("\n[LSTM 결과]")
# output: 모든 시점(단어)에서의 Hidden state (h_t) 출력
print("output.shape:", output.shape) # (batch, seq_len, hidden_size) → (2, 3, 3)
# h_n: 최종 시점(마지막 단어)의 Hidden state (h_T)
print("h_n.shape:", h_n.shape) # (num_layers, batch, hidden_size) → (1, 2, 3)
# c_n: 최종 시점(마지막 단어)의 Cell state (c_T)
print("c_n.shape:", c_n.shape) # (num_layers, batch, hidden_size) → (1, 2, 3)
print("\noutput:", output)
print("\nh_n:", h_n)
print("\nc_n:", c_n)
예제2
import re
from collections import Counter
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader, TensorDataset
import torch.nn.functional as F
# 1. 원본 텍스트 데이터와 레이블
raw_texts = [
"영화가 정말 재미있고 감동적이었어요", # 긍정 (1)
"스토리가 지루하고 시간 낭비였어요", # 부정 (0)
"배우 연기가 훌륭하고 음악도 좋았어요", # 긍정 (1)
"내용이 별로고 전개가 너무 느렸어요", # 부정 (0)
"정말 최고의 영화였어요 또 보고 싶어요", # 긍정 (1)
"연출이 엉성하고 집중이 안 됐어요", # 부정 (0)
]
raw_labels = [1, 0, 1, 0, 1, 0]
# 2. 간단한 토큰화 함수 정의
def simple_tokenize(text: str):
# 특수문자를 공백으로 치환 (숫자, 영문, 한글, 공백 제외)
text = re.sub(r"[^0-9a-zA-Z가-힣\s]", " ", text)
# 2개 이상의 연속된 공백을 하나로 치환하고 양쪽 공백 제거
text = re.sub(r"\s+", " ", text).strip()
# 공백 기준으로 토큰 분리
tokens = text.split()
return tokens
# 3. 토큰화 실행
tokenized_sentences = [simple_tokenize(s) for s in raw_texts]
print("토큰화된 문장들:\n", tokenized_sentences)
# 4. 특수 토큰 정의
PAD_TOKEN = "[PAD]" # 패딩 토큰
UNK_TOKEN = "[UNK]" # 미등록 단어 토큰
# 5. 단어 인덱스 딕셔너리 (word2idx) 초기화
word2idx = {
PAD_TOKEN: 0,
UNK_TOKEN: 1,
}
# Counter 활용해서 단어 빈도수 계산 및 word2idx 완성
counter = Counter()
for tokens in tokenized_sentences:
counter.update(tokens) # 모든 토큰의 빈도수를 계산
# 빈도수가 높은 순서대로 딕셔너리에 추가 (PAD, UNK 제외)
for word, _ in counter.most_common():
if word not in word2idx:
word2idx[word] = len(word2idx) # 현재 딕셔너리 크기를 새 단어의 인덱스로 할당
vocab_size = len(word2idx) # 전체 단어장의 크기
print("\nword2idx (단어 -> 인덱스):\n", word2idx)
# 6. 인덱스 단어 딕셔너리 (idx2word) 생성
idx2word = {idx: word for word, idx in word2idx.items()}
print("\nidx2word (인덱스 -> 단어):\n", idx2word)
# 7. 문장을 인덱스화하고 패딩 처리하는 함수
def encode_tokens(tokens, word2idx, max_len):
# 단어를 인덱스로 변환. 단어장에 없으면 UNK_TOKEN 인덱스 사용
idxs = [word2idx.get(t, word2idx[UNK_TOKEN]) for t in tokens]
idxs_len = len(idxs)
# 문장 길이가 max_len보다 짧으면 PAD_TOKEN으로 채움
if idxs_len < max_len:
idxs += [word2idx[PAD_TOKEN]] * (max_len - idxs_len)
# 문장 길이가 max_len보다 길면 잘라냄
elif idxs_len > max_len:
idxs = idxs[:max_len]
return idxs
# 8. 최대 시퀀스 길이 계산 및 인코딩
MAX_LEN = max(len(tokens) for tokens in tokenized_sentences)
print(f"\n최대 시퀀스 길이 (max_len): {MAX_LEN}")
encode_sentences = [
encode_tokens(tokens, word2idx, MAX_LEN) for tokens in tokenized_sentences
]
# print("\n인코딩 및 패딩된 문장들:\n", encode_sentences) # 확인용
# 9. 데이터셋 생성 (PyTorch 텐서로 변환)
X = torch.tensor(encode_sentences, dtype=torch.long) # 입력 데이터 (문장 인덱스)
y = torch.tensor(raw_labels, dtype=torch.float32).unsqueeze(
1
) # 레이블 (이진 분류를 위해 (N, 1) 형태로 변환)
dataset = TensorDataset(X, y)
# 10. 데이터 로더 생성
BATCH_SIZE = 2
train_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
# 11. LSTM 기반 감성 분류 모델 정의
class LSTMSentimentClassifier(nn.Module):
# 모델 초기화: 필요한 파라미터 정의
def __init__(self, vocab_size, embed_dim, hidden_size, num_layers=1, pad_idx=0):
super().__init__()
# 임베딩 레이어: 단어 인덱스를 벡터로 변환 (패딩 인덱스는 학습 제외)
self.embedding = nn.Embedding(
num_embeddings=vocab_size, # 단어장의 크기
embedding_dim=embed_dim, # 임베딩 벡터의 차원
padding_idx=pad_idx, # 패딩 토큰의 인덱스 (0)
)
# LSTM 레이어: 시퀀스 데이터를 처리하고 문맥 정보를 추출
self.lstm = nn.LSTM(
input_size=embed_dim, # 입력 차원 (임베딩 차원)
hidden_size=hidden_size, # 은닉 상태의 차원
num_layers=num_layers, # LSTM 레이어의 개수
batch_first=True, # 입력 텐서의 첫 번째 차원이 batch 크기임을 명시
bidirectional=False, # 단방향 LSTM 사용
)
# 출력 레이어: LSTM의 최종 은닉 상태를 입력받아 최종 예측값 (로짓)을 출력
# hidden_size 차원의 벡터를 1차원 로짓으로 변환 (이진 분류)
self.fc = nn.Linear(hidden_size, 1)
# 순전파 (Forward propagation) 정의
def forward(self, input_ids): # input_ids: (batch, seq_len)
# 1. 임베딩: 단어 인덱스를 임베딩 벡터로 변환
emb = self.embedding(input_ids) # (batch, seq_len, embed_dim)
# 2. LSTM 통과
# output: 모든 시점의 은닉 상태 (batch, seq_len, hidden_size * num_directions)
# (h_n, c_n): 마지막 시점의 은닉 상태와 셀 상태 (num_layers*num_directions, batch, hidden_size)
output, (h_n, c_n) = self.lstm(emb)
# 3. 최종 은닉 상태 추출 및 사용
# 단방향 LSTM이므로 h_n의 첫 번째 레이어 (0)를 사용
# h_n shape: (1, batch, hidden_size) -> h_n[0] shape: (batch, hidden_size)
last_hidden = h_n[0]
# 4. 선형 레이어 (FC) 통과
# last_hidden을 사용하여 최종 로짓 계산
# fc(last_hidden) shape: (batch, 1)
logits = self.fc(last_hidden)
return logits
# 12. 모델 파라미터 및 모델 객체 생성
embed_dim = 16 # 임베딩 벡터 차원
hidden_size = 32 # LSTM 은닉 상태 차원
num_layers = 1 # LSTM 레이어 개수
pad_idx = word2idx[PAD_TOKEN] # 패딩 토큰 인덱스 (0)
# 모델 인스턴스화
model = LSTMSentimentClassifier(
vocab_size=vocab_size,
embed_dim=embed_dim,
hidden_size=hidden_size,
num_layers=num_layers,
pad_idx=pad_idx,
)
# 13. 손실 함수 (Loss function) 및 옵티마이저 (Optimizer) 정의
# 이진 분류 (0 또는 1)이므로 BCEWithLogitsLoss 사용
criterion = nn.BCEWithLogitsLoss()
# Adam 옵티마이저 사용
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
def train_model(model, loader, criterion, optimizer, num_epochs=20):
model.train()
print("\n모델 학습 시작...")
for epoch in range(1, num_epochs + 1):
total_loss = 0.0
total_correct = 0
total_samples = 0
for batch_X, batch_y in loader:
optimizer.zero_grad()
logits = model(batch_X)
loss = criterion(logits, batch_y)
loss.backward()
optimizer.step()
# 손실 누적 (배치 크기 고려)
total_loss += loss.item() * batch_X.size(0)
# 정확도 계산
probs = torch.sigmoid(logits)
preds = (probs >= 0.5).float()
total_correct += (preds == batch_y).sum().item()
total_samples += batch_X.size(0)
avg_loss = total_loss / total_samples
acc = total_correct / total_samples
# ----------------------------------------------------
print(
f"[Epoch {epoch:02d}/{num_epochs}] Train Loss: {avg_loss:.4f}, Train Acc: {acc:.4f}"
)
print("모델 학습 완료.")
print("\n=== 모델 학습 시작 ===")
train_model(model, train_loader, criterion, optimizer, num_epochs=20)
def predict_sentiment(text):
model.eval()
# 1. 토큰화
tokens = simple_tokenize(text)
# 2. 인덱스화 및 패딩 (MAX_LEN 사용)
# 인덱스 리스트를 텐서로 변환하고 배치 차원 추가 (1, seq_len)
input_idxs = encode_tokens(tokens, word2idx, MAX_LEN)
print("\ninput_idxs\n", input_idxs)
input_tensor = torch.tensor(input_idxs, dtype=torch.long).unsqueeze(0)
# 학습이 아닐땐 미분이 필요없어서 with문 사용
with torch.no_grad():
# 3. 모델 예측 (로짓 획득)
logits = model(input_tensor)
# 4. 확률 및 라벨 변환
prob = torch.sigmoid(logits).item()
label = prob >= 0.5
# 5. 결과 반환: 라벨(0 또는 1), 확률, 토큰
return int(label), prob, tokens
test_setences = ["스토리가 정말 지루하고 재미없었어요", "완전 감동적이고 눈물났어요"]
for s in test_setences:
label, prob, tokens = predict_sentiment(s)
print(f"문장 : {s}")
print(f"토큰: {tokens}")
print(f"예측 라벨: , {label}(1:긍정, 0:부정), 확률: {prob:.4f}")
1. Seq2Seq 모델의 이해
Seq2Seq (Sequence to Sequence) 모델은 이름 그대로 하나의 시퀀스(순서가 있는 데이터)를 다른 시퀀스로 변환해주는 신경망 구조입니다.
- 본질적인 역할: 이 모델의 핵심은 입력 문장 전체의 '의미'를 파악한 다음, 그 의미를 유지하면서 목표 언어나 형태의 문장으로 '재구성'해내는 것입니다.
- 일반 분류 모델과의 차이:
- 일반 분류 모델은 고정된 길이의 입력(예: 사진, 문장)을 받아 '하나의 라벨' (예: 강아지, 스팸 메일)이라는 고정된 형태의 결과물을 냅니다.
- Seq2Seq는 입력의 길이도, 출력의 길이도 얼마든지 가변적일 수 있습니다. 예를 들어, 한국어 문장 5단어를 입력받아 영어로 10단어 문장을 출력할 수도 있고, 긴 문서를 입력받아 몇 줄로 요약할 수도 있습니다.
- 주요 활용 분야: 기계 번역이 가장 대표적이며, 긴 대화의 흐름을 이해하고 응답하는 챗봇, 장문의 기사를 짧게 줄이는 문서 요약, 질문에 대한 답을 찾아내는 질의응답 시스템 등 시퀀스를 다루는 거의 모든 분야에 사용됩니다.
2. 기본 구조: 인코더-디코더 (Encoder-Decoder)
Seq2Seq는 입력 시퀀스를 '이해'하는 부분과 '생성'하는 부분, 두 개의 독립적인 신경망(주로 RNN, LSTM, GRU 계열)으로 나뉩니다.
인코더 (Encoder)
- 역할: 입력 문장을 읽고 그 내용을 요약하여 **'문맥 벡터 (Context Vector)'**라는 압축된 정보 덩어리로 만듭니다. 마치 학생이 긴 강의 내용을 노트 한 장에 핵심만 정리하는 것과 같습니다.
- 작동 방식:
- 입력 단어들 (x_1, x_2, ..., x_T)을 순서대로 받아 처리합니다.
- 단어를 처리할 때마다 내부의 hidden state를 업데이트하여 이전까지의 정보를 누적시킵니다.
- 최종적으로 마지막 hidden state (h_T)가 입력 문장 전체의 의미를 담은 문맥 벡터가 됩니다. 이 벡터는 디코더로 전달되어 '번역의 참고 자료'로 쓰입니다.
디코더 (Decoder)
- 역할: 인코더로부터 받은 문맥 벡터를 바탕으로, 목표 시퀀스를 한 단어씩 차례로 생성해냅니다.
- 작동 방식:
- 인코더의 문맥 벡터를 초기 hidden state로 받아 번역을 시작합니다.
- 첫 단어는 문장의 시작을 알리는 <sos> (start of sentence) 토큰을 입력으로 넣어 시작합니다.
- 매 단계 (step)마다:
- 입력: 직전에 자신이 출력한 단어의 임베딩 정보와 이전 hidden state를 함께 받습니다.
- 출력: 다음 단어로 적절한 모든 단어에 대한 확률 분포를 계산하고 (softmax), 이 중 가장 확률이 높은 단어를 선택해 다음 출력 (y_1, y_2, ...)으로 내보냅니다.
- 이 과정을 문장의 끝을 알리는 <eos> 토큰이 나올 때까지 반복합니다.
3. 학습 안정화 기법: Teacher Forcing
Seq2Seq 모델을 학습시키는 과정에서 발생하는 고질적인 문제를 해결하기 위한 기술이 Teacher Forcing입니다.
디코더의 문제점 (오차 누적)
- 디코더는 이전 단계에서 자신이 예측한 단어를 다음 단계의 입력으로 사용하며 문장을 생성합니다 (자기 회귀적 생성).
- 학습 초기: 모델의 예측 성능이 낮을 때, t=1에서 잘못된 단어를 예측하면, t=2 단계는 이미 틀린 정보를 입력으로 받게 되어 예측이 더 심하게 꼬입니다. 이 오류는 문장 끝까지 연쇄적으로 누적되어 학습이 불안정해지고 모델이 제대로 수렴하지 못하게 됩니다.
Teacher Forcing의 해결책
- 전략: 모델을 학습시키는 단계에 한해서, 모델이 예측한 단어 대신 항상 '실제 정답 (Ground Truth)' 단어를 다음 단계의 입력으로 강제로 넣어주는 방식입니다.
- 효과: 매 단계마다 디코더가 가장 이상적인 입력(정답)을 받기 때문에, 오류 누적을 차단하고 학습을 극도로 안정적이고 빠르게 진행할 수 있습니다. 마치 옆에서 '정답 단어'를 알려주는 선생님(Teacher)의 도움을 받는 것과 같습니다.
Teacher Forcing 비율 (ratio)의 중요성
- 비율 1.0 (100%): 학습 시 항상 정답 단어만 사용합니다. 학습은 가장 빠르고 잘 되지만, '학습-추론 불일치 (Train-Inference Discrepancy)' 문제가 발생할 수 있습니다. 추론 시에는 정답 단어를 사용할 수 없기 때문에, 모델이 자신의 예측 오류를 다루는 법을 학습하지 못하게 됩니다.
- 비율 0.0 (0%): 항상 모델의 예측 단어만 사용합니다. 학습이 매우 어렵습니다.
- 실제 적용: 실제 환경에서는 0.5나 0.7처럼 일정 비율로 정답 단어와 모델 예측 단어를 섞어 사용하거나, 학습이 진행될수록 정답 단어의 비율을 점진적으로 줄여나가면서 (ratio scheduling) 모델이 점차 스스로의 예측에 의존하며 생성하도록 유도합니다. 이는 학습 안정성과 실제 추론 성능 사이의 균형을 맞추기 위함입니다.
import random
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
chars = list("abcdefghijklmnopqrstuvwxyz")
PAD_TOKEN = "<pad>"
SOS_TOKEN = "<sos>"
EOS_TOKEN = "<eos>"
itos = [PAD_TOKEN, SOS_TOKEN, EOS_TOKEN] + chars
stoi = {ch: i for i, ch in enumerate(itos)}
PAD_IDX = stoi[PAD_TOKEN]
SOS_IDX = stoi[SOS_TOKEN]
EOS_IDX = stoi[EOS_TOKEN]
vocab_size = len(stoi)
def random_string(
min_len=3, max_len=7
): # random.randint(), random.choice(), 단어하나 리턴
length = random.randint(min_len, max_len)
s = "".join(random.choice(chars) for _ in range(length))
return s
def encode_sequence(text: str): # "aaeifd"
# 문자열(단어) 인덱스로 바꾸고 앞뒤로 <sos>, <eos>를 붙여서 리스트 리턴 [1, 4,7,11, 2]
seq = [SOS_IDX] + [stoi[s] for s in text] + [EOS_IDX]
return torch.tensor(seq, dtype=torch.long)
def decode_sequence(indices):
# 인덱스 벡터가 입력되면 <sos>, <eos>를 지우고 인덱스에 해당하는 문자를 붙여서 문자열(단어)를 리턴
result = []
for idx in indices:
ch = itos[idx]
if ch in [PAD_TOKEN, SOS_TOKEN, EOS_TOKEN]:
continue
result.append(ch)
return "".join(result)
class ReverseDataset(Dataset):
def __init__(self, num_samples=2000, min_len=3, max_len=5):
super().__init__()
self.data = []
for _ in range(num_samples):
s = random_string(min_len=min_len, max_len=max_len)
input = encode_sequence(s)
target = encode_sequence(s[::-1])
self.data.append((input, target))
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
def collate_fn(batch): #
inp_seqs, tgt_seqs = zip(*batch)
inp_lens = [len(s) for s in inp_seqs]
tgt_lens = [len(s) for s in tgt_seqs]
max_inp = max(inp_lens)
max_tgt = max(tgt_lens)
padded_inp = []
padded_tgt = []
for inp, tgt in zip(inp_seqs, tgt_seqs):
pad_len_inp = max_inp - len(inp)
padded_inp.append(
torch.cat([inp, torch.full((pad_len_inp,), PAD_IDX, dtype=torch.long)])
)
pad_len_tgt = max_tgt - len(tgt)
padded_tgt.append(
torch.cat([tgt, torch.full((pad_len_tgt,), PAD_IDX, dtype=torch.long)])
)
batch_inp = torch.stack(padded_inp, dim=0)
batch_tgt = torch.stack(padded_tgt, dim=0)
return batch_inp, batch_tgt
train_dataset = ReverseDataset(num_samples=2000)
train_loader = DataLoader(
train_dataset, batch_size=32, shuffle=True, collate_fn=collate_fn
)
class Encoder(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(
vocab_size, embedding_dim=embed_dim, padding_idx=PAD_IDX
)
self.gru = nn.GRU(embed_dim, hidden_size=hidden_dim, batch_first=True)
def forward(self, src):
embedded = self.embedding(src)
outputs, hidden = self.gru(embedded)
return outputs, hidden
class Decoder(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(
vocab_size, embedding_dim=embed_dim, padding_idx=PAD_IDX
)
self.gru = nn.GRU(
input_size=embed_dim, hidden_size=hidden_dim, batch_first=True
)
self.fc_out = nn.Linear(hidden_dim, vocab_size)
def forward(self, input_step, hidden):
input_step = input_step.unsqueeze(1) # (32,) =>(32,1)
embedded = self.embedding(
input_step
) # (batch, seq_len) => (batch, seq_len, hidden_size)
output, hidden = self.gru(embedded, hidden)
# output : (batch, seq_len, hidden) : (32, 1, hidden_size)
# hidden : (1, batch, hidden) : (1, 32, hidden_size)
output = output.squeeze(1) # (batch, hidden_size)
logits = self.fc_out(output) # (batch, vocab_size)
return logits, hidden
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder):
super().__init__()
self.encoder = encoder
self.decoder = decoder
def forward(self, src, tgt, teacher_forcing_rate=0.7):
batch_size = src.size(0)
tgt_len = tgt.size(1)
outputs = torch.zeros(batch_size, tgt_len, vocab_size)
_, hidden = self.encoder(src)
input_step = tgt[:, 0]
for t in range(1, tgt_len):
logits, hidden = self.decoder(input_step, hidden)
outputs[:, t, :] = logits # (batch, vocab_size)
teacher_force = random.random() < teacher_forcing_rate
top1 = logits.argmax(dim=1) # (batch, 1)
if teacher_force:
input_step = tgt[:, t]
else:
input_step = top1
return outputs
embedded_dim = 64
hidden_dim = 256
num_epochs = 30
encoder = Encoder(vocab_size=vocab_size, embed_dim=embedded_dim, hidden_dim=hidden_dim)
decoder = Decoder(vocab_size=vocab_size, embed_dim=embedded_dim, hidden_dim=hidden_dim)
model = Seq2Seq(encoder, decoder)
criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(1, num_epochs + 1):
model.train()
total_loss = 0.0
total_tokens = 0
for src_batch, tgt_batch in train_loader:
optimizer.zero_grad()
outputs = model(
src_batch, tgt_batch, teacher_forcing_rate=0.8
) # (batch_size, tgt_len, vocabsize)
# (N, C) , (N,)
outputs_reshape = outputs[:, 1:, :].reshape(-1, vocab_size)
tgt_reshape = tgt_batch[:, 1:].reshape(-1)
loss = criterion(outputs_reshape, tgt_reshape) # (N, C) , (N,)
loss.backward()
optimizer.step()
valid_tokens = (tgt_reshape != PAD_IDX).sum().item()
total_loss += loss.item() * valid_tokens
total_tokens += valid_tokens
avg_loss = total_loss / total_tokens
print(f"Epoch : {epoch} - loss {avg_loss:.4f}")
def predict(model, s, max_len=20):
model.eval()
with torch.no_grad():
src = encode_sequence(s).unsqueeze(0) # (1, src_len)
_, hidden = model.encoder(src)
input_step = torch.tensor([SOS_IDX])
outputs = []
for _ in range(max_len):
logits, hidden = model.decoder(input_step, hidden)
top1 = logits.argmax(dim=1)
if top1.item() == EOS_IDX:
break
outputs.append(top1.item())
input_step = top1
pred_str = decode_sequence(outputs)
return pred_str
test_sample = ["abede", "xyz", "hello", "korea"]
for s in test_sample:
pred = predict(model, s)
print(f"input : ", s)
print("target : ", {s[::-1]})
print("pred : ", pred)'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글
| Transformer (0) | 2025.12.10 |
|---|---|
| Attention (0) | 2025.12.09 |
| 단어 임베딩 (0) | 2025.12.03 |
| 딥러닝 텍스트 전처리 (0) | 2025.12.01 |
| K-means Document Clustering (0) | 2025.11.28 |