어텐션 기반 번역 모델 : 학습과 번역 과정
1. 학습용 데이터 준비
import torch
# 어휘 사전 정의
# 영어 어휘 사전
vocab_en = {
"<sos>": 0, "<eos>": 1,
"i": 2, "am": 3, "he": 4, "she": 5, "is": 6,
"you": 7, "are": 8, "we": 9, "they": 10,
"book": 11, "books": 12, "reading": 13, "writing": 14, "eating": 15,
"a": 16, "the": 17, "in": 18, "on": 19, "at": 20,
"home": 21, "school": 22, "library": 23
}
# 한국어 어휘 사전
vocab_ko = {
"<sos>": 0, "<eos>": 1, # <pad> 제거, 인덱스 조정
"나는": 2, "너는": 3, "그는": 4, "그녀는": 5, "우리는": 6, "그들은": 7,
"책을": 8, "읽고": 9, "쓰고": 10, "먹고": 11,
"있다": 12, "있습니다": 13,
"집에서": 14, "학교에서": 15, "도서관에서": 16
}
# 학습용 예제 문장 쌍
train_pairs = [
("she is reading a book", "그녀는 책을 읽고 있다"),
("i am reading a book", "나는 책을 읽고 있다"),
("they are reading books", "그들은 책을 읽고 있다"),
("he is writing at school", "그는 학교에서 쓰고 있다"),
("we are eating at home", "우리는 집에서 먹고 있다")
]
def sentence_to_indices(sentence: str, vocab: dict) -> torch.Tensor:
"""문장을 인덱스 시퀀스로 변환 (시작과 종료 토큰 포함)"""
words = sentence.lower().split()
indices = [vocab["<sos>"]] + [vocab[word] for word in words] + [vocab["<eos>"]]
return torch.tensor(indices).unsqueeze(0)
- sentence.lower()는 문장을 소문자로 변환하여 어휘 사전에서 단어를 정확히 매핑할 수 있도록 합니다.
- sentence.split()은 문장을 공백 기준으로 나눠 단어 리스트를 생성합니다.
- 시작 토큰(<sos>)과 종료 토큰(<eos>)을 어휘 사전(vocab)에서 참조하여 시퀀스의 앞뒤에 추가합니다.
- 변환된 단어 인덱스 리스트를 PyTorch 텐서로 반환합니다.
❗ 학습용 데이터셋의 구성 결과와 입력 문장을 처리한 결과를 확인해 봅니다. 영어와 한국어 문장을 어휘 사전을 기반으로 인덱스 시퀀스로 변환한 결과를 출력합니다.
print("=== 데이터셋 구성 결과 ===")
print(f"영어 어휘 크기: {len(vocab_en)}")
print(f"한국어 어휘 크기: {len(vocab_ko)}")
print("\n=== 입력 문장 처리 결과 ===")
for src_sentence, tgt_sentence in train_pairs[:2]:
src_tensor = sentence_to_indices(src_sentence, vocab_en)
tgt_tensor = sentence_to_indices(tgt_sentence, vocab_ko)
print(f"\n[원문]")
print(f"EN: {src_sentence}")
print(f"KO: {tgt_sentence}")
print(f"\n[텐서 변환 결과]")
print(f"입력 텐서 shape: {src_tensor.shape}")
print(f"출력 텐서 shape: {tgt_tensor.shape}")
print(f"입력 인덱스: {src_tensor.squeeze().tolist()}")
print(f"출력 인덱스: {tgt_tensor.squeeze().tolist()}")
print("-" * 50)
=== 데이터셋 구성 결과 ===
영어 어휘 크기: 24
한국어 어휘 크기: 17
=== 입력 문장 처리 결과 ===
[원문]
EN: she is reading a book
KO: 그녀는 책을 읽고 있다
[텐서 변환 결과]
입력 텐서 shape: torch.Size([1, 7])
출력 텐서 shape: torch.Size([1, 6])
입력 인덱스: [0, 5, 6, 13, 16, 11, 1]
출력 인덱스: [0, 5, 8, 9, 12, 1]
--------------------------------------------------
[원문]
EN: i am reading a book
KO: 나는 책을 읽고 있다
[텐서 변환 결과]
입력 텐서 shape: torch.Size([1, 7])
출력 텐서 shape: torch.Size([1, 6])
입력 인덱스: [0, 2, 3, 13, 16, 11, 1]
출력 인덱스: [0, 2, 8, 9, 12, 1]
--------------------------------------------------
2. 모델 아키텍처 정의
번역 모델의 주요 구성 요소인 임베딩 레이어, 인코더, 디코더, 어텐션 레이어, 그리고 출력 레이어를 정의하는 과정을 학습합니다. 모델 아키텍처의 각 구성 요소가 어떤 역할을 하는지 이해하며, 이를 구현합니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
# 재현성을 위한 시드 설정
torch.manual_seed(42)
# 모델 파라미터 설정
embedding_dim = 8 # 임베딩 차원
hidden_dim = 8 # LSTM 히든 차원
# 임베딩 레이어 정의
encoder_embedding = torch.nn.Embedding(len(vocab_en), embedding_dim)
decoder_embedding = torch.nn.Embedding(len(vocab_ko), embedding_dim)
# 인코더/디코더 정의
encoder = torch.nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
decoder = torch.nn.LSTM(embedding_dim + hidden_dim, hidden_dim, batch_first=True)
# 어텐션 레이어 정의
attn_projection_layer = torch.nn.Linear(hidden_dim * 2, hidden_dim)
attention_v = torch.nn.Linear(hidden_dim, 1, bias=False)
# 출력 레이어 정의
output_layer = torch.nn.Linear(hidden_dim * 2, len(vocab_ko))
- torch.nn.Embedding은 단어를 고정된 크기의 임베딩 벡터로 변환하는 레이어입니다. 입력 크기(어휘 사전 크기)와 출력 크기(임베딩 차원)를 설정합니다.
- torch.nn.LSTM은 순환 신경망 계층으로, 입력 차원과 출력 차원을 설정하며 batch_first=True로 배치 차원이 첫 번째 위치에 오도록 설정합니다.
- torch.nn.Linear는 어텐션 투영 레이어(attn_projection_layer), 어텐션 스코어 계산 레이어(attention_v), 그리고 최종 출력 레이어(output_layer)를 정의합니다.
- 어텐션 투영 레이어는 입력 차원(hidden_dim * 2)을 출력 차원(hidden_dim)으로 변환합니다.
- 어텐션 스코어 계산 레이어는 입력 차원(hidden_dim)을 출력 차원(1)으로 변환하며, 편향은 사용하지 않습니다.
- 출력 레이어는 입력 차원(hidden_dim * 2)을 어휘 사전 크기(len(vocab_ko))로 변환합니다.
❗ 번역 모델 아키텍처의 주요 구성 요소를 확인하고, 전체 학습 가능한 파라미터 수를 계산합니다.
모델의 복잡도를 이해하고, 각 레이어의 입력 크기와 출력 크기를 점검하여 설계가 올바른지 확인합니다.
# 결과 확인을 위한 코드
print("\n=== 모델 아키텍처 구성 ===")
print("\n[임베딩 레이어]")
print(f"인코더 임베딩: 입력 크기={len(vocab_en)}, 출력 크기={embedding_dim}")
print(f"디코더 임베딩: 입력 크기={len(vocab_ko)}, 출력 크기={embedding_dim}")
print("\n[LSTM 레이어]")
print(f"인코더 LSTM: 입력 크기={embedding_dim}, 은닉 크기={hidden_dim}")
print(f"디코더 LSTM: 입력 크기={embedding_dim + hidden_dim}, 은닉 크기={hidden_dim}")
print("\n[어텐션 레이어]")
print(f"투영 레이어: 입력 크기={hidden_dim * 2}, 출력 크기={hidden_dim}")
print(f"어텐션 가중치: 입력 크기={hidden_dim}, 출력 크기=1")
print("\n[출력 레이어]")
print(f"출력 레이어: 입력 크기={hidden_dim * 2}, 출력 크기={len(vocab_ko)}")
# 전체 파라미터 수 계산
def count_parameters(model):
"""모델의 학습 가능한 파라미터 수를 계산하는 함수"""
total = 0 # 전체 파라미터 수를 저장할 변수 초기화
for param in model.parameters(): # 모델의 모든 파라미터를 순회
if param.requires_grad: # 해당 파라미터가 학습 가능한지 확인
total += param.numel() # 파라미터의 원소 수를 전체에 더함
return total # 총 학습 가능한 파라미터 수 반환
total_params = (
count_parameters(encoder_embedding) +
count_parameters(decoder_embedding) +
count_parameters(encoder) +
count_parameters(decoder) +
count_parameters(attn_projection_layer) +
count_parameters(attention_v) +
count_parameters(output_layer)
)
print(f"\n[전체 학습 가능한 파라미터 수: {total_params:,}]")
=== 모델 아키텍처 구성 ===
[임베딩 레이어]
인코더 임베딩: 입력 크기=24, 출력 크기=8
디코더 임베딩: 입력 크기=17, 출력 크기=8
[LSTM 레이어]
인코더 LSTM: 입력 크기=8, 은닉 크기=8
디코더 LSTM: 입력 크기=16, 은닉 크기=8
[어텐션 레이어]
투영 레이어: 입력 크기=16, 출력 크기=8
어텐션 가중치: 입력 크기=8, 출력 크기=1
[출력 레이어]
출력 레이어: 입력 크기=16, 출력 크기=17
[전체 학습 가능한 파라미터 수: 2,169]
※ 모델의 전체 구조 개요
우리의 번역 모델은 다음과 같은 주요 구성 요소로 이루어져 있습니다
- 임베딩 레이어: 단어를 고정된 크기의 벡터로 변환하여 모델이 단어의 의미를 수치적으로 처리할 수 있게 합니다.
- 인코더 LSTM: 입력 문장을 순차적으로 처리하며 각 단어의 문맥 정보를 학습합니다.
- 디코더 LSTM: 이전에 생성된 단어와 어텐션 메커니즘을 통해 얻은 컨텍스트 벡터를 사용하여 다음 단어를 예측합니다.
- 어텐션 메커니즘: 디코더가 입력 문장의 어느 부분에 집중해야 하는지 계산하며 번역 품질을 향상시킵니다.
- 출력 레이어: 디코더의 출력을 실제 단어로 변환하는 역할을 합니다.
임베딩 레이어의 설계
- 인코더 임베딩 레이어
- 입력 크기: 영어 어휘 사전의 크기 (`len(vocab_en)), 여기서는 24입니다. 출력 크기: 임베딩 차원 (embedding_dim), 여기서는 8입니다.
- 역할: 영어 단어 인덱스를 8차원 임베딩 벡터로 변환하여 모델에 입력합니다.
- 디코더 임베딩 레이어
- 입력 크기: 한국어 어휘 사전의 크기 (len(vocab_ko)'), 여기서는 17입니다.
- 출력 크기: 임베딩 차원 (embedding_dim), 8입니다.
- 역할: 한국어 단어 인덱스를 차원 임베딩 벡터로 변환하여 디코더에 입력합니다.
인코더 LSTM의 설계
- 입력 크기: 임베딩 차원 (embedding_dim`), 8입니다.
- 은닉 상태 크기: 히든 차원 (hidden_dim`), 8입니다.
- 역할: 임베딩된 입력 문장을 순차적으로 처리하며 각 단어의 문맥 정보를 학습하고, 마지막 은닉 상태와 셀 상태를 디코더에 전달합니다.
- 출력 텐서의 크기: '[배치 크기, 시퀀스 길이, hidden_dim]`
디코더 LSTM의 설계
- 입력 크기: 임베딩 차원 (embedding_dim`) + 컨텍스트 벡터의 차원 (hidden_dim), 총 16차원입니다.
- 이유: 이전에 생성된 단어의 임베딩 벡터와 어텐션 메커니즘으로부터 얻은 컨텍스트 벡터를 결합하여 디코더의 입력으로 사용합니다.
- 은닉 상태 크기: 히든 차원 (hidden_dim`), 8입니다.
- 역할: 이전 단어와 문맥 정보를 기반으로 다음 단어를 예측하기 위한 은닉 상태를 업데이트합니다.
어텐션 메커니즘의 설계
- 투명 레이어 (attn_projection_layer`)
- 입력 크기: 디코더의 현재 은닉 상태와 인코더의 출력 벡터를 결합한 벡터의 크기 (hidden_dim*2), 16차원입니다.
- 출력 크기: 히든 차원 (hidden_dim), 8차원입니다.
- 역할: 결합된 상태 벡터를 저차원 공간으로 투영하며 어텐션 점수를 계산하기 위한 전처리를 수행합니다.
- 어텐션 가중치 계산 레이어 ('attention_v)
- 입력 크기: 히든 차원 (hidden_dim), 8차원입니다.
- 출력 크기: 스칼라 값 1입니다.
- 역할: 투영된 벡터를 입력으로 받아 각 입력 단어에 대한 어텐션 점수를 계산합니다.
출력 레이어의 설계
- 입력 크기: 디코더의 출력 벡터와 컨텍스트 벡터를 결합한 벡터의 크기 (`hidden_dim*2), 16차원입니다.
- 출력 크기: 한국어 어휘 사전의 크기 (len(vocab_ko)`), 17입니다.
- 역할: 디코더의 현재 출력과 컨텍스트 벡터를 기반으로 각 단어의 확률 분포를 계산하여 다음에 생성할 단어를 예측합니다.
※ 모델 아키텍처 구성 결과 확인
실행 결과를 통해 각 레이어의 입출력 크기가 올바르게 설정되었는지 확인합니다:
임베딩 레이어
- 인코더 임베딩: 입력 크기=24, 출력 크기=8
- 디코더 임베딩: 입력 크기=17, 출력 크기=8
LSTM 레이어
- 인코더 LSTM: 입력 크기=8, 은닉 크기=8
- 디코더 LSTM: 입력 크기=16, 은닉 크기=8
어텐션 레이어
- 투명 레이어: 입력 크기=16, 출력 크기=8
- 어텐션 가중치 레이어: 입력 크기=8, 출력 크기=1
출력 레이어
- 출력 레이어: 입력 크기=16, 출력 크기=17
각 레이어의 입출력 크기가 일관성 있게 설계되어 있어, 모델이 올바르게 작동할 수 있습니다. 특히, 디코더 LSTM과 어텐션 메커니즘에서 입력 차원을 정확히 설정하는 것이 중요합니다.
※ 전체 학습 가능한 파라미터 수
모델의 전체 학습 가능한 파라미터 수는 2,169 개입니다. 이는 비교적 작은 규모로, 다음과 같은 이유로 파라미터 수가 결정되었습니다:
- 임베딩 레이어의 파라미터 수
- 인코더 임베딩: 24 (입력 단어수) × 8 (임베딩 차원)= 192개
- 디코더 임베딩: 17 × 8 = 136개
- LSTM 레이어의 파라미터 수
- 인코더 LSTM과 디코더 LSTM의 파라미터는 입력 및 은닉 차원에 따라 결정됩니다.
- 각 LSTM 레이어는 다수의 게이트를 포함하며, 이에 따른 파라미터 수가 증가합니다.
- 어텐션 및 출력 레이어의 파라미터 수
- 어텐션 레이어와 출력 레이어의 파라미터 수는 입력 및 출력 크기에 따라 계산됩니다.
참고: 파라미터 수는 모델의 복잡도와 학습 시간에 영향을 미칩니다. 적절한 파라미터 수를 유지함으로써 모델의 학습 효율을 높일 수 있습니다.
※ 아키텍처 설계의 핵심 포인트
차원 일관성 유지: 각 레이어의 입출력 차원을 정확히 맞춤으로써 텐서 연산 시 오류를 방지하고, 모델이 원활하게 동작하도록 설계하였습니다.
컨텍스트 벡터의 활용: 어텐션 메커니즘을 통해 얻은 컨텍스트 벡터를 디코더의 입력과 출력 단계에서 적극적으로 활용하여 번역의 정확도를 높입니다.
모델 복잡도 조절: 임베딩 차원과 히든 차원을 8로 설정하여 모델의 복잡도를 낮추고, 학습 시간을 단축하면서도 핵심 개념을 학습할 수 있도록 하였습니다.
3.어텐션 계산 함수 정의
디코더의 히든 상태와 인코더의 출력값을 결합하여 어텐션 스코어와 가중치를 계산하고, 컨텍스트 벡터를 생성합니다.
이 과정을 통해 어텐션 메커니즘의 주요 단계와 역할을 다시 한번 이해할 수 있습니다.
def calculate_attention(decoder_hidden, encoder_outputs, attn_projection_layer, attention_v):
# 히든 스테이트 확장
decoder_hidden_expanded = decoder_hidden.transpose(0, 1).repeat(1, encoder_outputs.size(1), 1)
# 디코더 히든과 인코더 출력 결합
combined_states = torch.cat([decoder_hidden_expanded, encoder_outputs], dim=2)
# 어텐션 스코어 계산
attention_scores_prep = torch.tanh(attn_projection_layer(combined_states))
attention_scores = attention_v(attention_scores_prep).squeeze(-1)
# 어텐션 가중치 정규화
attention_weights = F.softmax(attention_scores, dim=1)
# 컨텍스트 벡터 계산
context_vector = torch.bmm(attention_weights.unsqueeze(1), encoder_outputs).squeeze(1)
return context_vector, attention_weights
- decoder_hidden.transpose(0, 1)은 디코더의 히든 상태에서 첫 번째와 두 번째 차원을 교환합니다.
- .repeat는 인코더 출력 길이에 맞게 디코더 히든 상태를 확장합니다.
- torch.cat은 디코더 히든 상태와 인코더 출력을 결합하여 새로운 상태를 생성합니다.
- torch.tanh는 비선형 활성화 함수를 통해 투영된 상태를 변환합니다.
- attention_v는 어텐션 스코어를 계산하는 레이어로, 마지막 차원을 제거(squeeze(-1))하여 스칼라 값을 반환합니다.
- F.softmax는 스코어를 확률 분포로 정규화하여 어텐션 가중치를 생성합니다.
- torch.bmm는 어텐션 가중치와 인코더 출력을 배치 행렬 곱셈으로 결합해 컨텍스트 벡터를 생성합니다.
4. 학습 파라미터 설정
이번에는 모델 학습을 위한 학습 파라미터와 손실 함수를 설정합니다.
학습 가능한 파라미터를 정의하고, 옵티마이저와 손실 함수를 통해 모델을 학습할 준비를 합니다.
learning_rate = 0.01 # 학습률
# 학습 가능한 파라미터 설정
model_parameters = [
encoder.parameters(), # 인코더 LSTM
decoder.parameters(), # 디코더 LSTM
encoder_embedding.parameters(), # 인코더 임베딩
decoder_embedding.parameters(), # 디코더 임베딩
attn_projection_layer.parameters(), # 어텐션 변환
attention_v.parameters(), # 어텐션 가중치
output_layer.parameters() # 출력 레이어
]
# 모든 파라미터를 하나의 리스트로 결합
parameters = []
for param_group in model_parameters:
parameters.extend(list(param_group))
# 옵티마이저와 손실 함수 설정
optimizer = torch.optim.Adam(parameters, lr=learning_rate)
criterion = torch.nn.CrossEntropyLoss()
print("\n=== 학습 설정 ===")
print(f"학습률: {optimizer.param_groups[0]['lr']}")
print(f"옵티마이저: {optimizer.__class__.__name__}")
print(f"손실 함수: {criterion.__class__.__name__}")
- torch.optim.Adam은 Adam 옵티마이저를 사용하여 학습 가능한 파라미터를 최적화합니다.
- parameters는 학습 가능한 모든 모델 파라미터를 포함한 리스트입니다.
- lr은 학습률(learning rate)로, 옵티마이저의 학습 속도를 조절합니다.
- torch.nn.CrossEntropyLoss는 다중 클래스 분류 문제에서 사용되는 손실 함수로, 예측 확률 분포와 정답 라벨 간의 차이를 최소화합니다.
=== 학습 설정 ===
학습률: 0.01
옵티마이저: Adam
손실 함수: CrossEntropyLoss
5. 단일 문장 학습 과정
이번에는 단일 문장 학습 과정을 학습합니다.
한 스텝의 학습 과정을 통해 모델의 학습 메커니즘(Forward Pass, 손실 계산, 역전파, 파라미터 업데이트)을 이해합니다.
# 데이터 준비
ex_src_tensor = sentence_to_indices(train_pairs[0][0], vocab_en)
ex_tgt_tensor = sentence_to_indices(train_pairs[0][1], vocab_ko)
print("\n[학습 전 파라미터 상태]")
print(f"인코더 첫 번째 레이어 평균: {encoder.weight_ih_l0.data.mean().item():.6f}")
print(f"디코더 첫 번째 레이어 평균: {decoder.weight_ih_l0.data.mean().item():.6f}")
# Forward Pass
optimizer.zero_grad()
# 인코더
ex_src_embedded = encoder_embedding(ex_src_tensor)
ex_enc_outputs, (ex_enc_hidden, ex_enc_cell) = encoder(ex_src_embedded)
# 디코더 (첫 스텝)
ex_dec_input = torch.tensor([[vocab_ko["<sos>"]]])
ex_dec_hidden = ex_enc_hidden
ex_dec_embedded = decoder_embedding(ex_dec_input)
# Attention
ex_context_vec, ex_attn_weights = calculate_attention(
ex_dec_hidden,
ex_enc_outputs,
attn_projection_layer,
attention_v
)
# 디코더 출력
ex_dec_combined = torch.cat([ex_dec_embedded, ex_context_vec.unsqueeze(1)], dim=2)
ex_dec_output, (ex_dec_hidden, ex_dec_cell) = decoder(
ex_dec_combined,
(ex_dec_hidden, ex_enc_cell)
)
ex_output_vec = torch.cat([ex_dec_output.squeeze(1), ex_context_vec], dim=1)
ex_output = output_layer(ex_output_vec)
# 손실 계산 및 역전파
print("\n[손실 계산 과정의 텐서 값]")
print(f"목표 텐서(ex_tgt_tensor[0, 1]) shape: {ex_tgt_tensor[0, 1].shape}") # [1]
ex_target = ex_tgt_tensor[0, 1].unsqueeze(0)
print(f"예측 텐서(ex_output) shape: {ex_output.shape}") # [1, 1, vocab_ko_size]
print(f"목표 텐서(ex_target) shape: {ex_target.shape}") # [1]
loss_ex = criterion(ex_output, ex_target)
loss_ex.backward()
# 옵티마이저 스텝
optimizer.step()
print("\n[파라미터 업데이트 후 상태]")
print(f"인코더 첫 번째 레이어 평균: {encoder.weight_ih_l0.data.mean().item():.6f}")
print(f"디코더 첫 번째 레이어 평균: {decoder.weight_ih_l0.data.mean().item():.6f}")
print("\n[학습 결과]")
print(f"입력 문장: {train_pairs[0][0]}")
print(f"목표 문장: {train_pairs[0][1]}")
print(f"손실: {loss_ex.item():.4f}")
print("한 스텝의 학습 완료")
- optimizer.zero_grad()은 이전 스텝에서 계산된 기울기를 초기화하여 새로운 스텝을 준비합니다.
- criterion(ex_output, ex_target)은 예측값과 목표값 간의 손실을 계산합니다. 여기서는 CrossEntropyLoss를 사용합니다.
- loss_ex.backward()는 손실에 대한 모델 파라미터의 기울기를 계산합니다.
- optimizer.step()은 계산된 기울기를 바탕으로 모델 파라미터를 업데이트합니다.
[학습 전 파라미터 상태]
인코더 첫 번째 레이어 평균: -0.004045
디코더 첫 번째 레이어 평균: -0.002527
[손실 계산 과정의 텐서 값]
목표 텐서(ex_tgt_tensor[0, 1]) shape: torch.Size([])
예측 텐서(ex_output) shape: torch.Size([1, 17])
목표 텐서(ex_target) shape: torch.Size([1])
[파라미터 업데이트 후 상태]
인코더 첫 번째 레이어 평균: -0.003655
디코더 첫 번째 레이어 평균: -0.002449
[학습 결과]
입력 문장: she is reading a book
목표 문장: 그녀는 책을 읽고 있다
손실: 2.8949
한 스텝의 학습 완료
6. 초기화 함수(init_weights) 및 학습 함수 (train_step) 정의
이번에는 모델 초기화 함수와 학습 단계를 정의합니다.
모델의 가중치를 초기화하고, 단일 학습 스텝(teacher forcing 사용)을 구현하여 모델이 데이터를 학습할 수 있도록 설정합니다.
# 모델 초기화
def init_weights(model):
if isinstance(model, (nn.Linear, nn.LSTM)):
for name, param in model.named_parameters():
if 'weight' in name:
nn.init.xavier_uniform_(param)
elif 'bias' in name:
nn.init.zeros_(param)
def train_step(src_tensor, tgt_tensor, optimizer, criterion):
# 기울기 초기화
optimizer.zero_grad()
# 인코더
src_embedded = encoder_embedding(src_tensor)
enc_outputs, (enc_hidden, enc_cell) = encoder(src_embedded)
# 디코더 초기 상태
dec_input = torch.tensor([[vocab_ko["<sos>"]]])
dec_hidden = enc_hidden
# 손실 초기화
batch_loss = 0
# Teacher forcing을 사용한 디코더 학습
for t in range(1, tgt_tensor.size(1)):
# 디코더 입력 임베딩
dec_embedded = decoder_embedding(dec_input)
# Attention 계산
context_vector, attention_weights = calculate_attention(
dec_hidden,
enc_outputs,
attn_projection_layer,
attention_v
)
# 디코더 스텝
dec_input_combined = torch.cat([dec_embedded, context_vector.unsqueeze(1)], dim=2)
dec_output, (dec_hidden, dec_cell) = decoder(
dec_input_combined,
(dec_hidden, enc_cell)
)
# 출력 계산
output_vector = torch.cat([dec_output.squeeze(1), context_vector], dim=1)
output = output_layer(output_vector)
# 손실 계산
loss = criterion(output, tgt_tensor[:, t])
batch_loss += loss
# 다음 스텝 입력 (Teacher forcing)
dec_input = tgt_tensor[:, t].unsqueeze(1)
# 평균 손실 계산
batch_loss = batch_loss / (tgt_tensor.size(1) - 1)
# 역전파 및 옵티마이저 스텝
batch_loss.backward()
torch.nn.utils.clip_grad_norm_(parameters, max_norm=1.0)
optimizer.step()
return batch_loss.item()
1) 초기화 함수 (init_weights)
- nn.Linear와 nn.LSTM 계층에서 가중치를 Xavier 방식으로 초기화합니다.
- 편향 파라미터는 0으로 초기화합니다.
2) 학습 함수 (train_step)
- 기울기를 초기화(optimizer.zero_grad)한 후, 인코더와 디코더를 통해 데이터를 처리합니다.
- 디코더는 teacher forcing을 사용하여 목표 텐서를 학습합니다.
- 각 스텝에서 손실을 계산하고, 역전파를 수행한 뒤, 파라미터를 업데이트합니다.
- 손실 값은 batch_loss로 누적되며, 최종적으로 평균 손실을 반환합니다.
- torch.nn.utils.clip_grad_norm_를 사용해 기울기를 클리핑하여 학습 안정성을 높입니다.
7. 전체 학습 과정 실행
이번에는 전체 학습 과정을 실행합니다.
모델의 가중치를 초기화하고, 에폭 단위로 학습을 수행하며, 각 에폭의 평균 손실을 기록하고 출력합니다.
# 모델 가중치 초기화
init_weights(encoder)
init_weights(decoder)
init_weights(attn_projection_layer)
init_weights(output_layer)
# 전체 학습 시작
n_epochs = 200
print("\n=== 전체 데이터셋 학습 시작 ===")
print(f"전체 에폭: {n_epochs}")
print(f"학습 데이터 크기: {len(train_pairs)}")
loss_history = []
for epoch in range(n_epochs):
epoch_loss = 0
for src_sentence, tgt_sentence in train_pairs:
# 데이터 준비
src_tensor = sentence_to_indices(src_sentence, vocab_en)
tgt_tensor = sentence_to_indices(tgt_sentence, vocab_ko)
# 학습 스텝 수행
step_loss = train_step(src_tensor, tgt_tensor, optimizer, criterion)
epoch_loss += step_loss
# 에폭 평균 손실 계산
avg_epoch_loss = epoch_loss / len(train_pairs)
loss_history.append(avg_epoch_loss)
# 진행 상황 출력 (10 에폭마다)
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}/{n_epochs} : avg loss: {avg_epoch_loss:.4f} ")
print("\n학습 완료!")
print(f"최종 손실: {loss_history[-1]:.4f}")
- src_tensor와 tgt_tensor는 각각 영어와 한국어 문장을 텐서로 변환한 결과입니다. sentence_to_indices 함수를 사용하여 변환합니다.
- train_step 함수는 소스 텐서와 타겟 텐서, 옵티마이저, 손실 함수를 입력으로 받아 한 스텝의 학습을 수행합니다.
- avg_epoch_loss는 에폭 손실(epoch_loss)을 학습 데이터 크기(len(train_pairs))로 나누어 계산한 평균 손실입니다.
=== 전체 데이터셋 학습 시작 ===
전체 에폭: 200
학습 데이터 크기: 5
Epoch 20/200 : avg loss: 0.2706
Epoch 40/200 : avg loss: 0.0430
Epoch 60/200 : avg loss: 0.0188
Epoch 80/200 : avg loss: 0.0110
Epoch 100/200 : avg loss: 0.0074
Epoch 120/200 : avg loss: 0.0054
Epoch 140/200 : avg loss: 0.0042
Epoch 160/200 : avg loss: 0.0033
Epoch 180/200 : avg loss: 0.0027
Epoch 200/200 : avg loss: 0.0022
학습 완료!
최종 손실: 0.0022
❗ 학습 곡선을 시각화하여 학습 손실이 에폭 동안 어떻게 변화했는지 분석합니다.
import matplotlib.pyplot as plt
# 학습 곡선 그리기
plt.figure(figsize=(10, 5))
plt.plot(range(1, n_epochs + 1), loss_history, label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Over Time')
plt.grid(True)
plt.legend()
plt.yscale('log') # y축을 로그 스케일로 표시
plt.show()
print("\n=== Step 6: 학습 곡선 분석 ===")
print(f"초기 손실: {loss_history[0]:.4f}")
print(f"최종 손실: {loss_history[-1]:.4f}")
print(f"손실 감소율: {((loss_history[0] - loss_history[-1]) / loss_history[0] * 100):.2f}%")

=== Step 6: 학습 곡선 분석 ===
초기 손실: 2.8414
최종 손실: 0.0022
손실 감소율: 99.92%
※ 학습 과정에서의 중요 포인트
1) 적절한 에폭 수선택:
에폭 수를 충분히 설정하며 모델이 데이터셋을 완전히 학습할 수 있도록 합니다.
그러나 에폭 수가 너무 많으면 과적합(overfitting)이 발생할 수 있으므로 주의해야 합니다.
2) 손실 모니터링
학습 중 손실 값을 주기적으로 출력하며 모델의 학습 상태를 확인합니다.
손실 값이 더 이상 감소하지 않거나 증가하기 시작하면 학습을 조기에 종료하는 것도 고려할 수 있습니다.
3) 학습률 및 옵티마이저 설정:
이전 단계에서 설정한 학습률(learning_rate = 0.01~)과 옵티마이저(Adam)가 손실 감소에 효과적으로 작용하였음을 알 수 있습니다. 필요에 따라 학습률을 조정하거나 다른 옵티마이저를 시도해 볼 수 있습니다.
8. 번역 함수 구현 및 학습된 모델을 이용한 테스트
이번에는 학습된 번역 모델을 사용하여 입력 문장을 번역하는 함수를 구현합니다.
입력 문장을 인코더로 처리하고, 디코더를 통해 번역을 생성하며, 어텐션 가중치를 함께 계산합니다.
def translate(sentence, max_length=20):
# 모델을 평가 모드로 설정
encoder.eval()
decoder.eval()
with torch.no_grad(): # 추론 시에는 gradient 계산 불필요
# 입력 문장 텐서 변환
src_tensor = sentence_to_indices(sentence, vocab_en)
# 인코더 forward pass
src_embedded = encoder_embedding(src_tensor)
encoder_outputs, (encoder_hidden, encoder_cell) = encoder(src_embedded)
# 디코더 초기 입력 (<sos> 토큰)
decoder_input = torch.tensor([[vocab_ko["<sos>"]]])
decoder_hidden = encoder_hidden
# 번역 결과를 저장할 리스트
translated_tokens = []
attention_weights_history = []
# 디코더를 통한 번역 생성
for t in range(max_length):
# 1. 현재 입력 임베딩
decoder_embedded = decoder_embedding(decoder_input)
# 2. Attention 계산
context_vector, attention_weights = calculate_attention(
decoder_hidden,
encoder_outputs,
attn_projection_layer,
attention_v
)
# attention 가중치 저장 (numpy 배열로 변환)
attention_weights_history.append(attention_weights.cpu().numpy()[0])
# 3. 디코더 LSTM 스텝
decoder_input_combined = torch.cat([decoder_embedded, context_vector.unsqueeze(1)], dim=2)
decoder_output, (decoder_hidden, decoder_cell) = decoder(decoder_input_combined,
(decoder_hidden, encoder_cell))
# 4. 출력 레이어
output_vector = torch.cat([decoder_output.squeeze(1), context_vector], dim=1)
output_hidden = torch.tanh(output_layer(output_vector)) # 비선형성 추가
output_probs = F.softmax(output_hidden, dim=-1)
# 5. 다음 단어 예측
predicted_token = output_probs.argmax(dim=-1).item()
# 예측된 토큰 저장 (이미 같은 시퀀스가 나오면 중단)
if len(translated_tokens) >= 3 and \
translated_tokens[-3:] == translated_tokens[-6:-3]: # 반복 패턴 감지
translated_tokens = translated_tokens[:-3] # 반복된 부분 제거
break
translated_tokens.append(predicted_token)
# <eos> 토큰이 나오면 번역 종료
if predicted_token == vocab_ko["<eos>"]:
break
# 다음 스텝을 위한 입력 업데이트
decoder_input = torch.tensor([[predicted_token]])
# 토큰을 단어로 변환
idx_to_ko = {v: k for k, v in vocab_ko.items()}
translated_words = [idx_to_ko[token] for token in translated_tokens]
# <eos> 토큰이 있으면 제거
if vocab_ko["<eos>"] in translated_tokens:
translated_words = translated_words[:-1]
return translated_words, attention_weights_history
- encoder.eval()과 decoder.eval()을 사용해 모델을 평가 모드로 설정합니다.
- 디코더 출력과 컨텍스트 벡터를 결합하여 output_vector를 생성합니다.
- torch.tanh로 비선형성을 추가한 뒤, F.softmax를 통해 출력 확률 분포를 계산합니다.
- 출력 확률에서 argmax를 사용해 다음 단어를 예측합니다.
❗ 학습 데이터에 포함되지 않은 새로운 문장을 번역하여 모델의 일반화 능력을 평가합니다.
새로운 데이터는 학습 중에 모델이 보지 못한 조합이나 단어를 포함하므로, 번역 결과를 통해 모델의 적응력을 확인할 수 있습니다.
- 입력 문장(new_test_sentences)은 새로운 장소, 단어 조합 등을 포함하여 모델의 일반화 능력을 테스트합니다.
- 출력된 번역이 얼마나 자연스럽고 문맥에 맞는지 평가합니다.
- 이를 통해 모델이 학습 데이터 외의 문장에 대해 얼마나 잘 번역할 수 있는지 알 수 있습니다.
train_test_sentences = [
"she is reading a book",
"i am reading a book",
"he is writing at school"
]
for sentence in train_test_sentences:
translated_words, _ = translate(sentence)
print(f"\n입력: {sentence}")
print(f"번역: {' '.join(translated_words)}")
입력: she is reading a book
번역: 그녀는 책을 읽고 있다
입력: i am reading a book
번역: 나는 책을 읽고 있다
입력: he is writing at school
번역: 그는 학교에서 쓰고 있다
9. 새로운 데이터 테스트: 일반화 능력 평가
학습 데이터에 포함되지 않은 새로운 문장을 번역하여 모델의 일반화 능력을 평가합니다.
새로운 데이터는 학습 중에 모델이 보지 못한 조합이나 단어를 포함하므로, 번역 결과를 통해 모델의 적응력을 확인할 수 있습니다.
new_test_sentences = [
"he is reading at library", # 새로운 장소
"they are writing books", # 새로운 조합
"i am eating at school" # 새로운 조합
]
for sentence in new_test_sentences:
translated_words, _ = translate(sentence)
print(f"\n입력: {sentence}")
print(f"번역: {' '.join(translated_words)}")
입력: he is reading at library
번역: 그는 학교에서 쓰고 있다
입력: they are writing books
번역: 그들은 책을 읽고 있다
입력: i am eating at school
번역: 그들은 책을 읽고 있다
※ 번역의 한계
모델이 새로운 문장들을 번역할 때 학습 데이터에 포함된 문장과 유사한 번역을 생성하지만, 정확한 번역은 아닙니다.
예를 들어, `"he is reading at library"를 "그는 학교에서 쓰고 있다'로 번역하였습니다.
※ 이유 분석
학습 데이터의 부족: 모델이 "library"나"eating""과 같은 단어를 학습하지 않았기 때문에 해당 단어들을 제대로 번역하지 못합니다.
제한된 어휘 사전: 어휘 사전에 포함되지 않은 단어들은 인덱스로 변환할 수 없어 모델이 처리할 수 없습니다.
패턴 인식: 모델은 학습한 패턴에 따라 가장 유사한 번역을 생성하려고 시도하지만, 새로운 조합에 대해서는 일반화 능력이 떨어집니다.
※ 결론
데이터 확대: 더 많은 문장과 다양한 어휘를 포함한 데이터로 모델을 학습시키면 일반화 능력이 향상됩니다.
어휘 사전 확장: 새로운 단어들을 어휘 사전에 추가하여 모델이 다양한 단어를 처리할 수 있도록 합니다.
정교한 모델 구조: 더 깊거나 복잡한 모델을 사용하여 복잡한 패턴을 학습할 수 있도록 합니다.
학습 데이터에 대한 성능: 모델은 학습 데이터에 포함된 문장에 대해서는 정확한 번역을 수행할 수 있습니다.
일반화 능력: 학습되지 않은 새로운 문장에 대해서는 정확한 번역을 생성하지 못하며, 이는 학습 데이터의 부족과 제한된 어휘 사전 때문입니다.
중요성: 이는 딥러닝 모델의 일반적인 특성으로, 충분하고 다양한 데이터를 사용하여 모델을 학습시키는 것이 중요합니다.