지난 블로그에는 단순히 숫자를 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)
결과
끝.
감사합니다.
'딥러닝 (Deep Learning) > [03] - 모델' 카테고리의 다른 글
Sklearn-onnx 모델을 onnx 변환 방법 (with 성능 비교) (0) | 2025.04.03 |
---|---|
Scikit-Learn 모델 분류 및 개념 정리 (0) | 2025.04.03 |
Maskformer (0) | 2024.11.27 |
U-Net3++ (0) | 2024.11.22 |
DeepLab v1 아키텍쳐 분석 (1) | 2024.11.20 |