딥러닝 (Deep Learning)/[03] - 모델

Embedding과 Hidden State 완전 정복 -벡터로 단어와 기억을 표현하는 방법

AI-BT 2025. 4. 9. 13:59
728x90
반응형

지난 블로그에는 단순히 숫자를 index 해서 LSTM 으로 예측하는 코드를 설명했다.

이번에는 알파벳(문자)를 예측하는 코드를 분석하면서, 궁금한 점에 대해 정리했다.

학습, 예측 코드는 맨 아래 부분에 작성 했다.

 

LSTM 코드 분석 [2-2편]

이전 글에서 LSTM 이론적인 부분을 설명했습니다.  LSTM 이란? [2-1편]기본적인 순환신경망인 Vanilla RNN에 대해서 1편에서 설명 했습니다.하지만 현재는 Vanilla RNN 이 사용되고 있지 않습니다.어떠한

ai-bt.tistory.com


1. Embedding이란?

"Embedding은 구분된(discrete) 데이터를 연속된(continuous) 숫자 벡터로 바꿔주는 기술이다."

쉽게 말하면,
"문자나 단어 같은 '이름'을 숫자 좌표 공간에 알맞게 꽂아 넣는 작업"이라고 이해할 수 있다.

  • 'apple' ➔ [0.12, 0.45, ..., 0.78] (100차원 숫자 벡터)
  • 'king' ➔ [0.88, 0.15, ..., 0.91]

즉, 우리가 흔히 아는 단어나 문자를
수학적으로 계산할 수 있는 숫자 배열로 변환하는 것이 바로 Embedding!


2. 왜 Embedding을 할까?

문자나 단어 자체('a', 'king', 'apple')는 컴퓨터가 직접 이해할 수 없다.
컴퓨터는 오직 숫자만 다룰 수 있기 때문!

그래서

  • 문자 ➔ 숫자로 변환하고
  • 단순 숫자가 아니라 단어 간 관계까지 표현할 수 있는 벡터로 변환 한다.

✅ 예를 들어:

  • 'apple'과 'orange'는 비슷한 방향에,
  • 'apple'과 'car'는 멀리 떨어지게.

이렇게 단어 의미를 벡터 공간에서 표현할 수 있다.


3. 100차원 벡터로 수많은 단어를 표현할 수 있을까?

가능하다!

이유는 간단하다.

  • 0~1 사이 실수(real number)는 무한히 많은 값을 가질 수 있고,
  • 100개의 실수를 조합하면
    거의 무한대에 가까운 다양한 벡터 조합이 나온다

✅ 그래서 100차원 벡터 하나로도
수십만, 수백만 개의 단어를 서로 다르게 표현할 수 있다.

실제로 Word2Vec, GloVe 같은 모델도
100~300차원 정도로 수십만 단어를 잘 표현한다.


4. 100차원이 항상 충분한가?

항상 그런 것은 아니다.

✨ 더 복잡한 의미를 담아야 할 때

  • "apple"과 "green apple", "rotten apple"처럼
  • 아주 미세한 의미 차이까지 구분하려면
  • 더 높은 차원이 필요할 수 있다.

✨ 모델 크기와 연산 속도도 고려해야

  • 100차원은 가볍고 빠르다.
  • 768차원 (예: BERT) 모델은 성능은 좋지만 무겁다.

✅ 그래서 문제 난이도사용 환경에 따라 적절한 차원을 선택해야 한다


🔥 근데 왜 100차원이면 무조건 충분하다고는 할 수 없을까?

1) 너무 복잡한 의미까지 담아야 한다면?

  • 단어 하나하나에 미묘한 뉘앙스(의미 차이)까지 담으려면
  • 차원을 더 높여야 정확하게 구분할 수 있다.

예를 들어:

  • "apple"과 "green apple"과 "rotten apple"은 다 비슷하지만 살짝 다르잖아?

=> 의미를 더 세밀하게 담으려면 더 높은 차원이 필요할 수도 있다!

2) 모델 크기 & 연산 속도도 고려 필요

  • 100차원은 학습이 빠르고 연산이 가볍다.
  • 768차원 (BERT) 같은 모델은 성능은 좋은데 엄청 무겁다.

✅ 그래서 "문제 난이도"와 "자원 상황"에 따라

  • 100차원 쓸지
  • 300차원 쓸지
  • 768차원 쓸지 결정해야 한다

5. LSTM의 Hidden State란?

LSTM은 입력 시퀀스를 읽으면서 정보를 기억한다.
이 기억을 저장하는 공간이 바로 "hidden state"이다.

그리고 그 hidden state의 크기를 우리가 정할 수 있는데, 그게 바로 hidden_size 이다.

✅ 쉽게 말하면:

  • hidden_size = 기억 용량
  • hidden_size = 기억할 정보의 차원 수

예를 들어 hidden_size = 64이면,

  • h_t는 64개의 숫자로 이루어진 벡터
  • c_t (cell state)도 64개의 숫자

✅ hidden_size가 크면

  • 더 많은 정보를 기억할 수 있지만
  • 모델이 무겁고 학습이 느려질 수 있다

✅ hidden_size가 작으면

  • 가볍고 빠르지만
  • 기억할 수 있는 정보량이 줄어든다.

6. 알파벳 학습 및 예측 코드

import torch
import torch.nn as nn
import torch.optim as optim

# 알파벳이 입력값으로 주어지면, 다음 문자를 예측하는 문제

# 1. 문자 사전 정의
# 예를 들어 a~z + 공백 포함 (총 27개 문자)
# 랜덤한 index 시점을 찾아서, 거기로 부터 seq_length 길이까지 일렬로 생성
# index 값들이 실제로 의미하는 것은 a,b,c,d,e ... z, " " 까지 문자열을 의미한다.
char_list = [chr(i) for i in range(ord('a'), ord('z')+1)] + [' ']
char2idx = {char: idx for idx, char in enumerate(char_list)}
idx2char = {idx: char for idx, char in enumerate(char_list)}

vocab_size = len(char_list)  # 총 26개 + 공백 1개 = 27개
print(f"문자 개수: {vocab_size}")  # 27


# 2. 데이터 생성
def generate_char_data(seq_length, num_samples):
    X = []
    Y = []
    for _ in range(num_samples):
        start_idx = torch.randint(0, len(char_list) - seq_length, (1,)).item()
        seq = [char2idx[char_list[start_idx + i]] for i in range(seq_length)]
        X.append(seq[:-1])  # 입력 시퀀스 (n-1개)
        Y.append(seq[1:])   # 정답 시퀀스 (n-1개)
    return torch.tensor(X), torch.tensor(Y)


# 3. LSTM 모델 (vocab_size를 input/output size로 설정)
class CharLSTM(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super(CharLSTM, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = nn.Embedding(vocab_size, vocab_size)  # 간단하게 embedding (one-hot처럼)
        self.lstm_cell = nn.LSTMCell(vocab_size, hidden_size)
        self.fc = nn.Linear(hidden_size, vocab_size)  # hidden -> 문자 분류

    def forward(self, inputs):
        batch_size, seq_len = inputs.size()
        h_t = torch.zeros(batch_size, self.hidden_size)
        c_t = torch.zeros(batch_size, self.hidden_size)

        outputs = []
        embedded = self.embedding(inputs)  # (batch, seq_len, vocab_size)

        for t in range(seq_len):
            x_t = embedded[:, t, :]
            h_t, c_t = self.lstm_cell(x_t, (h_t, c_t))
            output = self.fc(h_t)
            outputs.append(output.unsqueeze(1))  # (batch, 1, vocab_size)

        outputs = torch.cat(outputs, dim=1)  # (batch, seq_len, vocab_size)
        return outputs


# 4. 학습 준비
hidden_size = 64 # LSTM 셀 안에서 정보를 저장하고 처리하는 기억 공간의 크기다.
model = CharLSTM(vocab_size, hidden_size)
criterion = nn.CrossEntropyLoss() # 손실함수
optimizer = optim.Adam(model.parameters(), lr=0.01)

X_train, Y_train = generate_char_data(seq_length=5, num_samples=2000)


# 5. 학습
epochs = 500
losses = []

for epoch in range(epochs):
    model.train()
    optimizer.zero_grad()
    outputs = model(X_train)  # (batch, seq_len, vocab_size)

    # CrossEntropyLoss는 (batch*seq_len, vocab_size) 형태로 기대함
    outputs = outputs.view(-1, vocab_size)  
    Y_train_flat = Y_train.view(-1)

    loss = criterion(outputs, Y_train_flat)
    loss.backward()
    optimizer.step()

    losses.append(loss.item())
    if (epoch+1) % 20 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")


# 6. 테스트
model.eval()
with torch.no_grad():
    test_input = torch.tensor([[char2idx['h'], char2idx['e'], char2idx['l'], char2idx['l'], char2idx['a'], char2idx['b'], char2idx['c']]])
    prediction = model(test_input)

    prediction = prediction.argmax(dim=2)  # 가장 확률 높은 문자 선택
    predicted_chars = [idx2char[idx.item()] for idx in prediction.squeeze(0)]
    input_chars = [idx2char[idx.item()] for idx in test_input.squeeze(0)]

    print("Input Chars: ", input_chars)
    print("Predicted Next Chars: ", predicted_chars)

 

결과

 

 

끝.

감사합니다.

728x90
반응형