AI 개발 공부 공간

AI, 머신러닝, 딥러닝, Python & PyTorch, 실전 프로젝트

딥러닝/딥러닝: 자연어

어텐션 메커니즘의 구현 과정: 전체 시퀀스 생성 과정

qordnswnd123 2025. 5. 28. 17:47

1. 어휘 사전 및 예시 문장쌍 정의

# 영어 단어장 정의
vocab_en = {
    # 특수 토큰
    "<sos>": 0,
    "<eos>": 1,
    # Stage 2의 기존 단어들
    "she": 2, "is": 3, "reading": 4, "a": 5, "book": 6,
    # 추가 단어들
    "he": 7, "they": 8, "writing": 9, "the": 10,
    "letter": 11, "newspaper": 12
}

# 한국어 단어장 정의
vocab_ko = {
    # 특수 토큰
    "<sos>": 0,
    "<eos>": 1,
    # 한국어 단어들
    "그녀는": 2, "그는": 3, "그들은": 4,
    "읽고": 5, "쓰고": 6,
    "있다": 7,
    "책을": 8, "편지를": 9, "신문을": 10
}

# 예시 문장 쌍 정의
train_pairs = [
    # Stage 2의 예시 포함
    ("she is reading a book", "그녀는 책을 읽고 있다"),
    ("he is reading a newspaper", "그는 신문을 읽고 있다"),
    ("she is writing a letter", "그녀는 편지를 쓰고 있다"),
]

# 첫 번째 예시 문장에 대한 처리 예시
src_sentence = train_pairs[0][0]  # "she is reading a book"
tgt_sentence = train_pairs[0][1]  # "그녀는 책을 읽고 있다"  

print(f"영어 어휘 크기: {len(vocab_en)}")
print(f"한국어 어휘 크기: {len(vocab_ko)}")
print("\n입력 문장:", src_sentence)
print("목표 문장:", tgt_sentence)

print("\n첫 번째 예시 문장의 단어별 인덱스:")
print("영어:", [(word, vocab_en[word]) for word in src_sentence.split()])
print("한국어:", [(word, vocab_ko[word]) for word in tgt_sentence.split()])

 

영어 어휘 크기: 13
한국어 어휘 크기: 11

입력 문장: she is reading a book
목표 문장: 그녀는 책을 읽고 있다

첫 번째 예시 문장의 단어별 인덱스:
영어: [('she', 2), ('is', 3), ('reading', 4), ('a', 5), ('book', 6)]
한국어: [('그녀는', 2), ('책을', 8), ('읽고', 5), ('있다', 7)]

 

※ 특수 토큰의 추가
우리가 문장을 읽을 때 대문자로 시작하고 마침표로 끝나는 것을 인식하듯이, 번역 모델도 문장의 시작과 끝을 알아야 합니다. 이를 위해 두 가지 특별한 표시를 추가했습니다:
<sos>: 문장의 시작을 알리는 신호
<eos> : 문장의 끝을 알리는 신호

 

2. 문장을 인덱스 시퀀스로 변환 및 텐서 생성

이번에는 스테이지 2에서 다뤘던 문장을 인덱스 시퀀스로 변환하는 코드를 함수화하여 재사용성을 높이는 방법을 학습합니다.

이 과정을 통해 문장을 시작과 종료 토큰을 포함한 인덱스 시퀀스로 변환하고, PyTorch 텐서 형태로 모델 입력으로 사용할 수 있도록 준비합니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

# 재현성을 위한 시드 설정
torch.manual_seed(42)

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>"]]
    # 배치 차원 추가 ([sequence_length] -> [batch_size=1, sequence_length])
    return torch.tensor(indices).unsqueeze(0)

# 입력 문장 처리
src_tensor = sentence_to_indices(src_sentence, vocab_en)
tgt_tensor = sentence_to_indices(tgt_sentence, vocab_ko)

print("=== 문장 인덱스 변환 및 텐서 생성 결과 ===\n")

print("1. 소스 텐서 (src_tensor):")
print(f"  Shape: {src_tensor.shape}")
print(f"  내용:\n{src_tensor}\n")

print("2. 타겟 텐서 (tgt_tensor):")
print(f"  Shape: {tgt_tensor.shape}")
print(f"  내용:\n{tgt_tensor}\n")
  • sentence_to_indices 함수를 작성하여, 시작 토큰(<sos>)과 종료 토큰(<eos>)을 어휘 사전(vocab)에서 참조하여 문장의 앞뒤에 추가합니다.
  • 변환된 인덱스 시퀀스를 torch.tensor로 변환하고, unsqueeze를 사용해 배치 차원을 추가합니다.
=== 문장 인덱스 변환 및 텐서 생성 결과 ===

1. 소스 텐서 (src_tensor):
  Shape: torch.Size([1, 7])
  내용:
tensor([[0, 2, 3, 4, 5, 6, 1]])

2. 타겟 텐서 (tgt_tensor):
  Shape: torch.Size([1, 6])
  내용:
tensor([[0, 2, 8, 5, 7, 1]])

 

3. 임베딩 레이어 정의 및 소스 시퀀스 임베딩

이번에는 임베딩 레이어를 정의하고, 소스 시퀀스를 임베딩 벡터로 변환하는 과정을 학습합니다.
이 과정을 통해 단어를 고정된 차원의 임베딩 벡터로 변환하고, 임베딩 레이어를 모델에서 사용하는 방법을 익힐 수 있습니다.

import torch.nn as nn  

embedding_dim = 8
hidden_dim = 8

# 임베딩 레이어 정의
encoder_embedding = nn.Embedding(len(vocab_en), embedding_dim)
decoder_embedding = nn.Embedding(len(vocab_ko), embedding_dim)

# 소스 시퀀스 임베딩
src_embedded = encoder_embedding(src_tensor)


print("1. 임베딩 레이어 정보:")
print(f"  Encoder Embedding: {encoder_embedding}")
print(f"  Decoder Embedding: {decoder_embedding}\n")

print("2. 소스 시퀀스 임베딩:")
print(f"  Shape: {src_embedded.shape}")
print(f"  내용:\n{src_embedded}\n")
  • nn.Embedding은 고유한 단어 인덱스를 고정된 크기의 실수 벡터로 변환하는 레이어입니다.
  • 임베딩 레이어는 어휘 사전의 크기와 임베딩 차원(embedding_dim)을 입력으로 받아 생성합니다.
  • encoder_embedding은 영어 어휘 사전을 기반으로, decoder_embedding은 한국어 어휘 사전을 기반으로 정의됩니다.
1. 임베딩 레이어 정보:
  Encoder Embedding: Embedding(13, 8)
  Decoder Embedding: Embedding(11, 8)

2. 소스 시퀀스 임베딩:
  Shape: torch.Size([1, 7, 8])
  내용:
tensor([[[ 1.9269,  1.4873,  0.9007, -2.1055,  0.6784, -1.2345, -0.0431,
          -1.6047],
         [ 1.6423, -0.1596, -0.4974,  0.4396, -0.7581,  1.0783,  0.8008,
           1.6806],
         [ 1.2791,  1.2964,  0.6105,  1.3347, -0.2316,  0.0418, -0.2516,
           0.8599],
         [-1.3847, -0.8712, -0.2234,  1.7174,  0.3189, -0.4245,  0.3057,
          -0.7746],
         [-1.5576,  0.9956, -0.8798, -0.6011, -1.2742,  2.1228, -1.2347,
          -0.4879],
         [-0.9138, -0.6581,  0.0780,  0.5258, -0.4880,  1.1914, -0.8140,
          -0.7360],
         [-0.7521,  1.6487, -0.3925, -1.4036, -0.7279, -0.5594, -0.7688,
           0.7624]]], grad_fn=<EmbeddingBackward0>)

 

4. LSTM 기반 엔코더 정의 및 각 단계의 출력 확인

LSTM 기반의 엔코더를 정의하고, 입력 데이터를 통해 각 단계의 출력과 상태를 확인하는 과정을 학습합니다.

# LSTM 엔코더 정의
encoder = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)

# 전체 시퀀스 인코딩
encoder_outputs, (encoder_hidden, encoder_cell) = encoder(src_embedded)
  • nn.LSTM은 PyTorch에서 순환 신경망(LSTM) 레이어를 정의하는 함수입니다. 입력 차원(embedding_dim), 출력 차원(hidden_dim), 그리고 batch_first=True를 설정하여 배치 크기가 첫 번째 축에 위치하도록 합니다.
  • encoder는 소스 시퀀스(src_embedded)를 입력받아 전체 시퀀스의 출력(encoder_outputs), 마지막 은닉 상태(encoder_hidden), 마지막 셀 상태(encoder_cell)를 반환합니다.

이번에는 순환 신경망의 한 종류인 LSTM을 사용하여 엔코더를 구현하고, 입력 문장이 어떻게 처리되는지 자세히 살펴보겠습니다. 

LSTM 엔코더는 마치 책을 읽는 사람처럼 시작 토큰부 터 종료 토큰까지 순차적으로 처리합니다
<sos> → "she" → "is" → "reading" → "a" → "book" → <eos>
이 과정에서 각 단어는 이전 단어들의 문맥을 고려하여 새로운 정보를 추가합니다. 예를 들어 "reading"이라는 단어를 처리할 때는 앞서 나온 "she is"의 정보를 함께 고려하여, '누가 무엇을 하고 있는지'의 문맥을 파악합니다. 이러한 순차적 처리는 문장의 문법적 구조와 의미적 관계를 효과적으로 포착할 수 있게 합니다.


※ LSTM 엔코더의 구조 이해
encoder = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
매개변수의 의미와 설정 이유:

1) embedding_dim(입력 차원): 16
각 단어의 임베딩 벡터 크기
너무 작으면 단어의 의미를 충분히 표현할 수 없고
너무 크면 학습이 어려워짐


2) hidden_dim (은닉 차원): 16
LSTM의 내부 상태 벡터 크기
문장의 문맥 정보를 저장할 공간
보통 embedding_dim과 같은 크기로 설정


3) batch_first=True:
입력 텐서의 형태를 [배치, 시퀀스 길이, 특성] 순으로 지정
PyTorch의 기본값은 [시퀀스 길이, 배치, 특성]
배치 처리를 직관적으로 다루기 위해 True로 설정

 

이제 LSTM 엔코더가 문장을 처리할 때, 각 단어마다 어떤 출력값을 생성하는지 살펴보겠습니다.
특히 문장의 시작(), 첫 단어, 마지막 단어, 그리고 문장의 끝()에서 어떤 값들이 나오는지 중점적으로 확인해보겠습니다.

  • 확인할 주요 포인트
    • 각 주요 위치에서의 출력 벡터 크기(norm)
    • 엔코더가 생성하는 출력의 차원
    • 최종 은닉 상태와 셀 상태의 형태

이를 통해 LSTM이 문장의 정보를 어떻게 순차적으로 처리하고 압축하는지 이해할 수 있습니다.

print("\n최종 엔코더 상태:")
print("히든 스테이트:", encoder_hidden.squeeze())
print("셀 스테이트:", encoder_cell.squeeze())

print("\n=== 엔코더 출력 Shape 확인 ===")
print(f"encoder_outputs shape: {encoder_outputs.shape}")  # [batch_size, seq_len, hidden_dim]
print(f"encoder_hidden shape: {encoder_hidden.shape}")    # [num_layers, batch_size, hidden_dim]
print(f"encoder_cell shape: {encoder_cell.shape}")        # [num_layers, batch_size, hidden_dim]

print("각 단어별 엔코더 출력:")
for i, (word, output) in enumerate(zip(src_sentence.split() + ['<eos>'], encoder_outputs[0])):
    print(f"\n단어: {word}")
    print(f"출력 벡터: {output}")
    print(f"벡터 노름: {torch.norm(output).item():.4f}")
최종 엔코더 상태:
히든 스테이트: tensor([ 5.4727e-02, -2.1693e-01,  4.7848e-01, -1.1335e-02,  4.3336e-04,
         3.4991e-01,  1.6774e-02,  3.4360e-01], grad_fn=<SqueezeBackward0>)
셀 스테이트: tensor([ 0.0749, -0.5258,  0.9105, -0.0294,  0.0011,  0.5834,  0.0512,  0.6906],
       grad_fn=<SqueezeBackward0>)

=== 엔코더 출력 Shape 확인 ===
encoder_outputs shape: torch.Size([1, 7, 8])
encoder_hidden shape: torch.Size([1, 1, 8])
encoder_cell shape: torch.Size([1, 1, 8])
각 단어별 엔코더 출력:

단어: she
출력 벡터: tensor([-0.0336, -0.3448, -0.0124,  0.1681,  0.1913,  0.4965,  0.1091,  0.1513],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.6829

단어: is
출력 벡터: tensor([ 0.0252, -0.1870, -0.1556,  0.0312, -0.1134,  0.1808, -0.2507,  0.0863],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.4203

단어: reading
출력 벡터: tensor([ 0.0863, -0.2364,  0.0964,  0.0664, -0.0624,  0.2257, -0.0666,  0.0035],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.3692

단어: a
출력 벡터: tensor([-0.0990, -0.0262,  0.2871, -0.1486, -0.0689,  0.1144, -0.0691,  0.1515],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.4007

단어: book
출력 벡터: tensor([-0.1365, -0.0581,  0.4512, -0.1702, -0.0478,  0.2437, -0.0689,  0.2216],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.6083

단어: <eos>
출력 벡터: tensor([-0.1088, -0.0365,  0.4118, -0.1776, -0.0800,  0.1796, -0.1054,  0.2827],
       grad_fn=<UnbindBackward0>)
벡터 노름: 0.5865

 

※ 벡터의 노름(Norm)과 그 의미
벡터의 노름은 벡터의 '크기' 또는 '세기'를 나타내는 단일 숫자값입니다. 수학적으로는 벡터의 각 요소를 제곱하여 더한 후 제곱근을 취한 값입니다:

노름이 가지는 의미:
1) 정보량의 지표
큰 노름→ 강한 특징이나 중요한 정보를 포함
작은 노름→ 상대적으로 덜 중요한 정보
2) 단어의 중요도 반영
'a': 0.38 #관사, 작은 노름
'reading': 0.53 #주요 등사, 큰 노름
문법적 역할이 큰 단어나 의미적으로 중요한 단어는 더 큰 노름을 가짐
3) 문맥 정보의 축적:
문장을 읽어가면서 노름이 변화
중요한 정보가 추가될 때 노름이 증가하는 경향

 

※ LSTM 엔코더의 동작 과정

encoder_outputs, (encoder_hidden, encoder_cell) = encoder(src_embedded)
1) 입력 데이터의 흐름:


2) 각 반환값의 의미:
encoder_outputs:


각 위치에서의 출력은 해당 단어까지의 누적된 문맥 정보를 포함
이전 단어들의 정보가 LSTM의 메모리를 통해 전달됨


encoder_hidden:
전체 문장의 의미가 압축된 벡터
디코더의 초기 상태로 사용됨
예: [0.05, 0.06, 0.16...]


encoder_cell:
LSTM의 장기 기억을 담당하는 내부 상태
정보의 흐름을 제어하는 게이트들의 결과
예: [0.11, 0.15, -0.32...]


이러한 LSTM 엔코더의 출력은 이후 디코더에서 어텐션 메커니즘을 통해 참조되며, 정확한 번역을 위한 핵심 정보로 활용됩니다. 각 단어의 처리 과정에서 생성되는 벡터들의 노름과 값의 변화 를 관찰함으로써, LSTM이 문장의 구조와 의미를 어떻게 이해하고 있는지 파악할 수 있습니다.

 

5. 디코더 초기 설정 및 레이어 정의

이번에는 디코더 초기 입력을 설정하고, 디코더의 구조와 관련 레이어를 정의하는 과정을 학습합니다.

 

# 디코더 초기 입력 설정
first_decoder_input = tgt_tensor[:, 0].unsqueeze(1)  # <sos> 토큰으로 시작
first_decoder_embedded = decoder_embedding(first_decoder_input)

# LSTM 디코더 정의
decoder = nn.LSTM(embedding_dim + hidden_dim, hidden_dim, batch_first=True)

# 출력층 정의 (결합된 출력을 vocabulary 크기로 변환)
output_layer = nn.Linear(hidden_dim * 2, len(vocab_ko))

# Attention 관련 레이어 정의
attn_projection_layer = nn.Linear(hidden_dim * 2, hidden_dim)
attention_v = nn.Linear(hidden_dim, 1, bias=False)

# 디코더 초기 상태 설정
decoder_hidden = encoder_hidden
decoder_cell = encoder_cell
decoder_input = first_decoder_input
  • tgt_tensor[:, 0]은 목표 문장에서 첫 번째 단어(<sos>)를 가져옵니다. 이를 디코더의 초기 입력으로 설정하며, 차원을 맞추기 위해 unsqueeze를 사용해 추가적인 차원을 만듭니다.
  • 디코더는 nn.LSTM으로 정의되며, 입력 차원은 embedding_dim + hidden_dim, 출력 차원은 hidden_dim으로 설정합니다.
  • 출력층(output_layer)은 디코더 출력과 어텐션 컨텍스트를 결합한 차원(hidden_dim * 2)을 입력받아 어휘 사전 크기(len(vocab_ko))로 변환합니다.
  • 어텐션 관련 레이어는 두 가지로 구성됩니다:
    • attn_projection_layer: 결합된 입력을 투영하여 hidden_dim으로 변환합니다.
    • attention_v: 스코어를 계산하기 위해 입력 차원 hidden_dim에서 출력 차원 1로 변환합니다.
  • 디코더 초기 상태(decoder_hidden, decoder_cell)는 엔더의 마지막 은닉 상태와 셀 상태로 설정됩니다.

※ 디코더 초기 설정 및 레이어 정의 과정을 살펴보겠습니다.

이 단계에서는 디코더의 입력, 구조, 출력층, 어텐션 레이어, 그리고 초기 상태를 확인하며 디코더의 작동 원리를 이해합니다.

print("\n=== 디코더 초기 설정 및 레이어 정의 결과 ===")

print("1. 디코더 초기 입력 정보:")
print(f"  입력 텐서 shape: {first_decoder_input.shape}")
print(f"  입력 텐서 내용: {first_decoder_input}")
print(f"  임베딩 결과 shape: {first_decoder_embedded.shape}")

print("\n2. 디코더 LSTM 구조:")
print(decoder)

print("\n3. 출력층 정보:")
print(f"  입력 크기: {hidden_dim * 2}")
print(f"  출력 크기: {len(vocab_ko)}")
print(output_layer)

print("\n4. 어텐션 레이어 정보:")
print("  Projection Layer:")
print(attn_projection_layer)
print("  Score Layer:")
print(attention_v)

print("\n5. 디코더 초기 상태:")
print(f"  hidden state shape: {decoder_hidden.shape}")
print(f"  cell state shape: {decoder_cell.shape}")
print(f"  input shape: {decoder_input.shape}")
=== 디코더 초기 설정 및 레이어 정의 결과 ===
1. 디코더 초기 입력 정보:
  입력 텐서 shape: torch.Size([1, 1])
  입력 텐서 내용: tensor([[0]])
  임베딩 결과 shape: torch.Size([1, 1, 8])

2. 디코더 LSTM 구조:
LSTM(16, 8, batch_first=True)

3. 출력층 정보:
  입력 크기: 16
  출력 크기: 11
Linear(in_features=16, out_features=11, bias=True)

4. 어텐션 레이어 정보:
  Projection Layer:
Linear(in_features=16, out_features=8, bias=True)
  Score Layer:
Linear(in_features=8, out_features=1, bias=False)

5. 디코더 초기 상태:
  hidden state shape: torch.Size([1, 1, 8])
  cell state shape: torch.Size([1, 1, 8])
  input shape: torch.Size([1, 1])

 

6. 어텐션 가중치 계산 함수 정의 및 테스트

이번에는 어텐션 가중치를 계산하고 컨텍스트 벡터를 생성하는 함수를 정의하고 테스트하는 과정을 학습합니다.

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)
    
    return context_vector, attention_weights
  • decoder_hidden.transpose(0, 1)은 디코더의 히든 상태를 첫 번째와 두 번째 차원을 교환합니다.
  • .repeat(1, encoder_outputs.size(1), 1)은 엔코더 출력의 길이에 맞게 디코더 히든 상태를 확장합니다.
  • torch.cat은 디코더 히든 상태와 엔코더 출력을 결합하여 새로운 상태를 생성합니다.
  • torch.tanh는 비선형 활성화 함수를 통해 투영된 상태를 변환합니다.
  • attention_v는 어텐션 스코어를 계산하는 레이어로, 마지막 차원을 제거(squeeze(-1))하여 스칼라 값을 반환합니다.
  • F.softmax는 스코어를 확률 분포로 정규화하여 어텐션 가중치를 생성합니다.
  • torch.bmm는 어텐션 가중치와 엔코더 출력을 배치 단위로 곱하여 컨텍스트 벡터를 생성합니다.

※ 이번 테스트에서는 어텐션 가중치 계산 함수가 올바르게 동작하는지 확인합니다.
가상의 입력 데이터를 사용하여 함수의 출력 형태와 값을 검증하며, 어텐션 메커니즘이 제대로 작동하는지 이해합니다.

# 테스트를 위한 가상 데이터 생성
batch_size = 1
src_len = 5  # 예: "<sos> she is reading book <eos>"
hidden_dim = 8

# 가상의 디코더 히든 스테이트와 엔코더 출력 생성
test_decoder_hidden = torch.randn(1, batch_size, hidden_dim)  # [1, 1, hidden_dim]
test_encoder_outputs = torch.randn(batch_size, src_len, hidden_dim)  # [1, src_len, hidden_dim]

# 어텐션 레이어 정의
test_attn_projection = nn.Linear(hidden_dim * 2, hidden_dim)
test_attention_v = nn.Linear(hidden_dim, 1, bias=False)

# 함수 테스트
test_context_vector, test_attention_weights = calculate_attention(
    test_decoder_hidden,
    test_encoder_outputs,
    test_attn_projection,
    test_attention_v
)

print("\n=== 어텐션 계산 함수 테스트 결과 ===")
print("1. 입력 데이터 형태:")
print(f"  디코더 히든 스테이트: {test_decoder_hidden.shape}")
print(f"  엔코더 출력: {test_encoder_outputs.shape}")

print("\n2. 출력 결과:")
print(f"  컨텍스트 벡터 shape: {test_context_vector.shape}")
print(f"  어텐션 가중치 shape: {test_attention_weights.shape}")

print("\n3. 어텐션 가중치 확인:")
print(f"  가중치 값:\n{test_attention_weights.squeeze()}")
print(f"  가중치 합: {test_attention_weights.sum().item():.4f}")  # 1.0이어야 함
=== 어텐션 계산 함수 테스트 결과 ===
1. 입력 데이터 형태:
  디코더 히든 스테이트: torch.Size([1, 1, 8])
  엔코더 출력: torch.Size([1, 5, 8])

2. 출력 결과:
  컨텍스트 벡터 shape: torch.Size([1, 1, 8])
  어텐션 가중치 shape: torch.Size([1, 5])

3. 어텐션 가중치 확인:
  가중치 값:
tensor([0.1829, 0.2005, 0.2082, 0.1840, 0.2244], grad_fn=<SqueezeBackward0>)
  가중치 합: 1.0000

 

7. 모델 가중치 초기화

이번에는 모델의 가중치를 초기화하는 방법을 학습합니다.
신경망 모델의 학습 성능은 초기 가중치에 영향을 받으므로, 적절한 초기화를 통해 학습을 안정화할 수 있습니다.
이 과정에서는 Xavier 초기화와 편향 값 초기화를 사용합니다.

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)

# 가중치 초기화 적용
decoder.apply(init_weights)
output_layer.apply(init_weights)
attn_projection_layer.apply(init_weights)
attention_v.apply(init_weights)
  • nn.init.xavier_uniform_은 Xavier 초기화를 적용하여 가중치 값을 적절히 분포시킵니다. 이는 모델 학습에서 기울기 소실 및 폭발 문제를 완화합니다.
  • nn.init.zeros_는 편향 값을 모두 0으로 초기화합니다.
  • 초기화 함수(init_weights)를 생성하여 nn.Linear와 nn.LSTM 계층에 가중치 및 편향 초기화를 적용합니다.
  • 초기화 함수는 모델의 apply 메서드를 사용하여 각 계층에 적용됩니다.
Linear(in_features=8, out_features=1, bias=False)
  • 이제 각 레이어의 가중치가 제대로 초기화되었는지 확인해보겠습니다.
    Xavier/Glorot 초기화와 직교 초기화가 올바르게 적용되었다면, 가중치들은 적절한 범위 내에 분포하고 bias는 0으로 설정되어 있어야 합니다. 다음 테스트를 통해 이를 검증해보겠습니다
print("=== 가중치 초기화 결과 확인 ===\n")

# 1. 선형 레이어 가중치 확인
print("1. Linear Layer 확인:")
print("Output Layer:")
print(f"  Weight 범위: [{output_layer.weight.min():.4f}, {output_layer.weight.max():.4f}]")
print(f"  Bias가 0인지 확인: {torch.all(output_layer.bias == 0)}")

# 2. LSTM 레이어 확인
print("\n2. LSTM Layer 확인:")
lstm_param = next(decoder.parameters())
print(f"  Weight 범위: [{lstm_param.min():.4f}, {lstm_param.max():.4f}]")

# 3. 어텐션 레이어 확인
print("\n3. Attention Layer 확인:")
print("Projection Layer:")
print(f"  Weight 범위: [{attn_projection_layer.weight.min():.4f}, {attn_projection_layer.weight.max():.4f}]")
=== 가중치 초기화 결과 확인 ===

1. Linear Layer 확인:
Output Layer:
  Weight 범위: [-0.4702, 0.4602]
  Bias가 0인지 확인: True

2. LSTM Layer 확인:
  Weight 범위: [-0.3523, 0.3525]

3. Attention Layer 확인:
Projection Layer:
  Weight 범위: [-0.4857, 0.4968]

 

8. 전체 시퀀스 생성 및 번역 결과 분석

이번에는 디코더를 사용해 시퀀스를 생성하고, 어텐션 가중치와 예측된 단어를 확인하는 과정을 학습합니다.
디코더는 타임스텝별로 입력과 컨텍스트 벡터를 결합하여 다음 단어를 예측하고, 이를 통해 전체 시퀀스를 생성합니다.
이 과정을 통해 디코더와 어텐션 메커니즘의 협력 과정을 이해할 수 있습니다.

# 생성 결과를 저장할 리스트
generated_indices = []
attention_history = []
predicted_words = []

# 디코더의 전체 타임스텝 길이 계산
target_length = tgt_tensor.size(1)

for t in range(target_length):
    # 현재 입력 임베딩
    decoder_embedded = decoder_embedding(decoder_input)
    
    # 어텐션 계산 및 컨텍스트 벡터 생성
    context_vector, attention_weights = calculate_attention(
        decoder_hidden,
        encoder_outputs,
        attn_projection_layer,
        attention_v
    )
    
    # 첫 번째 타임스텝에서만 shape 정보 출력
    if t == 0:
        print("\n=== 첫 번째 타임스텝의 텐서 shape 정보 ===")
        print(f"디코더 임베딩 shape: {decoder_embedded.shape}")
        print(f"컨텍스트 벡터 shape: {context_vector.shape}")
        print(f"어텐션 가중치 shape: {attention_weights.shape}")
    
    # 디코더 입력과 컨텍스트 벡터 결합
    decoder_input_combined = torch.cat([decoder_embedded, context_vector], dim=2)
    
    # 디코더 LSTM 처리
    decoder_output, (decoder_hidden, decoder_cell) = decoder(
        decoder_input_combined,
        (decoder_hidden, decoder_cell)
    )
    
    # 출력 계산
    combined_output = torch.cat([decoder_output.squeeze(1), context_vector.squeeze(1)], dim=1)
    output_hidden = torch.tanh(output_layer(combined_output))  # 비선형성 추가
    output_probs = F.softmax(output_hidden, dim=-1)
    
    # 다음 단어 예측
    predicted_idx = output_probs.argmax(dim=-1)
    generated_indices.append(predicted_idx.item())
    predicted_words.append(list(vocab_ko.keys())[predicted_idx.item()])
    
    # 어텐션 가중치 저장
    attention_history.append(attention_weights.squeeze().detach())
    
    # 다음 스텝을 위한 입력 업데이트
    decoder_input = predicted_idx.unsqueeze(0)
  • decoder_embedding을 통해 현재 입력(decoder_input)을 임베딩 벡터로 변환합니다.
  • calculate_attention 함수를 호출하여 어텐션 가중치와 컨텍스트 벡터를 계산합니다.
  • 디코더 입력과 컨텍스트 벡터를 결합하여 LSTM 디코더에 입력합니다.
  • torch.cat을 사용해 디코더 출력과 컨텍스트 벡터를 결합한 뒤, output_layer를 통해 최종 출력 벡터를 계산합니다.
  • F.softmax를 사용해 출력 벡터를 확률 분포로 변환한 뒤, argmax를 통해 가장 높은 확률의 단어 인덱스를 예측합니다.


=== 첫 번째 타임스텝의 텐서 shape 정보 ===
디코더 임베딩 shape: torch.Size([1, 1, 8])
컨텍스트 벡터 shape: torch.Size([1, 1, 8])
어텐션 가중치 shape: torch.Size([1, 7])

 

※ 이번 출력 코드는 디코더를 통해 생성된 전체 시퀀스를 분석하는 과정을 보여줍니다.
입력 문장과 예측된 단어, 어텐션 가중치, 그리고 최종 생성된 문장을 확인하며 디코더의 작동 결과를 분석합니다.

# 생성 결과 출력
print("\n=== 전체 시퀀스 생성 결과 ===")
print("\n1. 입력 문장:")
print(f"  {src_sentence}")

print("\n2. 타임스텝별 생성 결과:")
for t, (word, attn) in enumerate(zip(predicted_words, attention_history)):
    print(f"\n[타임스텝 {t+1}]")
    print(f"  예측 단어: {word}")
    print("  단어별 집중도:")
    for src_word, weight in zip(['<sos>'] + src_sentence.split() + ['<eos>'], 
                              attn.numpy()):
        print(f"    {src_word:10}: {weight:.4f}")

print("\n3. 최종 생성 문장:")
print(f"  {''.join(predicted_words)}")

print("\n4. 목표 문장:")
print(f"  {tgt_sentence}")
=== 전체 시퀀스 생성 결과 ===

1. 입력 문장:
  she is reading a book

2. 타임스텝별 생성 결과:

[타임스텝 1]
  예측 단어: 책을
  단어별 집중도:
    <sos>     : 0.1359
    she       : 0.1634
    is        : 0.1375
    reading   : 0.1360
    a         : 0.1332
    book      : 0.1444
    <eos>     : 0.1497

[타임스텝 2]
  예측 단어: 책을
  단어별 집중도:
    <sos>     : 0.1357
    she       : 0.1651
    is        : 0.1370
    reading   : 0.1358
    a         : 0.1330
    book      : 0.1445
    <eos>     : 0.1488

[타임스텝 3]
  예측 단어: <eos>
  단어별 집중도:
    <sos>     : 0.1369
    she       : 0.1646
    is        : 0.1356
    reading   : 0.1351
    a         : 0.1338
    book      : 0.1447
    <eos>     : 0.1492

[타임스텝 4]
  예측 단어: <eos>
  단어별 집중도:
    <sos>     : 0.1380
    she       : 0.1633
    is        : 0.1348
    reading   : 0.1347
    a         : 0.1345
    book      : 0.1450
    <eos>     : 0.1497

[타임스텝 5]
  예측 단어: <eos>
  단어별 집중도:
    <sos>     : 0.1377
    she       : 0.1627
    is        : 0.1349
    reading   : 0.1348
    a         : 0.1347
    book      : 0.1453
    <eos>     : 0.1499

[타임스텝 6]
  예측 단어: <eos>
  단어별 집중도:
    <sos>     : 0.1374
    she       : 0.1631
    is        : 0.1352
    reading   : 0.1349
    a         : 0.1344
    book      : 0.1453
    <eos>     : 0.1497

3. 최종 생성 문장:
  책을책을<eos><eos><eos><eos>

4. 목표 문장:
  그녀는 책을 읽고 있다