노트북:
12-06. pytorch_nn_embedding.ipynb(셀 9개, 코드 셀 7개)
예상 시간: 약 10분 (노트북 5분 + 추가 셀 5분) · 짝꿍 글:4.4_nn.Embedding.md
torch.randn(1, 10, 5)로 만들던 "단어 벡터" 자리다.'영화' → 22 → [0.2, 0.9, 0.3]nn.Embedding은 이 테이블과 "번호로 행 꺼내기"를 묶어 둔 층이다. 이 노트북의 제목 그대로, 임베딩 층은 룩업 테이블(lookup table) 이다.⚠️ 셀 2의 단어 번호는 실행할 때마다 바뀐다. set()으로 중복을 없앤 뒤 enumerate로 번호를 매기는데, 파이썬의 문자열 집합은 실행할 때마다 순서가 달라진다. 실행 세 번에 매핑이 세 번 다 달랐다(직접 확인).
{'to': 2, 'know': 3, 'how': 4, 'need': 5, 'you': 6, 'code': 7}
{'need': 2, 'to': 3, 'code': 4, 'know': 5, 'how': 6, 'you': 7}
{'how': 2, 'need': 3, 'you': 4, 'code': 5, 'know': 6, 'to': 7}
그래서 셀 3에서 손으로 쓴 테이블의 각 행이 어느 단어인지도 실행마다 바뀌고, 셀 4의 출력 숫자도 노트북과 다르게 나올 수 있다. 개념에는 문제가 없지만, "왜 제 결과는 다르죠?"라는 질문이 나올 수 있다. 고정하려면 sorted(set(...))를 쓰면 된다.
노트북은 nn.Embedding을 만들고 가중치를 출력하기만 한다. 실제로 정수를 넣어 보는 셀이 없어서 추가 셀 A~C를 붙였다.
셀 8의 가중치 값은 무작위라 실행마다 다르다. 1번 행이 0인 것만 같다.
| 파트 | 셀 | 한 줄 메시지 | 시간 |
|---|---|---|---|
| 1. 손으로 만든 룩업 테이블 | 1~4 | 단어 → 정수 → 테이블의 행 | 4분 |
2. nn.Embedding | 6~8 | 같은 테이블을 층으로. 값은 무작위로 시작 | 2분 |
| 3. 추가 A~C | — | (배치, 시점) → (배치, 시점, 차원). 원-핫 × 행렬과 같다 | 4분 |
읽는 법: 🗣 = 그대로 말해도 되는 문장, ❓ = 나올 만한 질문과 답
train_data = 'you need to know how to code'
word_set = set(train_data.split()) # 중복 제거 → 6개 단어
vocab = {word: i+2 for i, word in enumerate(word_set)} # 2번부터 번호
vocab['<unk>'] = 0
vocab['<pad>'] = 1
train_data.split() → 띄어쓰기로 자른다(4.3의 가장 단순한 토큰화). to가 두 번 나와서 set을 거치면 6개가 된다.<unk> (unknown): 단어 집합에 없는 단어가 들어오면 이걸로 바꾼다.<pad> (padding): 문장 길이를 맞추려고 채우는 빈칸이다(4.5에서 0으로 채운다).⚠️ 4.5와 번호가 반대다. 여기서는 <unk>=0, <pad>=1이고, 4.5에서는 <PAD>=0, <UNK>=1이다. 번호는 정하기 나름이지만 섞어 쓰면 헷갈린다.
embedding_table = torch.FloatTensor([
[0.0, 0.0, 0.0], # 0번 <unk>
[0.0, 0.0, 0.0], # 1번 <pad>
[0.2, 0.9, 0.3], # 2번
[0.1, 0.5, 0.7], # 3번
[0.2, 0.1, 0.8], # 4번
[0.4, 0.1, 0.1], # 5번
[0.1, 0.8, 0.9], # 6번
[0.6, 0.1, 0.1]]) # 7번
(8, 3) = (단어 집합 크기, 임베딩 차원). 단어 하나가 3차원 벡터 하나다.sample = 'you need to run'.split()
idxes = []
for word in sample:
try:
idxes.append(vocab[word]) # 단어 → 정수
except KeyError:
idxes.append(vocab['<unk>']) # 모르는 단어 → 0
idxes = torch.LongTensor(idxes)
lookup_result = embedding_table[idxes, :] # 정수 → 테이블의 행
print(lookup_result) # (4, 3)
한 줄씩:
vocab[word] → 단어를 정수로. run은 단어 집합에 없어서 KeyError가 난다. 그래서 <unk>(0)으로 바꾼다.torch.LongTensor(idxes) → 정수 텐서. 인덱스로 쓰려면 정수형(Long) 이어야 한다.embedding_table[idxes, :] → 팬시 인덱싱. 인덱스 4개에 해당하는 행 4개를 한 번에 꺼낸다. 결과는 (4, 3). 이 한 줄이 임베딩의 전부다.[0, 0, 0]인 이유: run → <unk>(0번) → 0번 행이 0이기 때문이다.🗣 "임베딩은 결국 표에서 행을 꺼내는 거예요. 단어를 번호로 바꾸고, 그 번호의 행을 가져오면 그게 단어 벡터입니다. 모르는 단어는 0번 unk 행을 가져와요."
❓ (저장된 출력과) 제 숫자가 달라요. 발표 전 체크에 쓴 대로 set의 순서 때문이다. you가 몇 번이 될지가 실행마다 바뀌어서 꺼내는 행이 달라진다. run→0번 행(0 벡터)인 마지막 줄만 늘 같다.
nn.Embedding (셀 6~8)embedding_layer = nn.Embedding(num_embeddings=len(vocab), # 8 = 테이블의 행 수
embedding_dim=3, # 3 = 테이블의 열 수
padding_idx=1) # 1번(<pad>) 행은 0으로 고정
print(embedding_layer.weight)
Parameter containing:
tensor([[ 1.1554, -0.6474, 0.0574],
[ 0.0000, 0.0000, 0.0000], ← padding_idx=1
[ 1.0383, -1.8434, -0.5678],
... (8행)
], requires_grad=True)
num_embeddings → 단어 집합 크기. 테이블의 행 수다.embedding_dim → 벡터 차원. 테이블의 열 수다. 4.1~4.2의 input_size가 바로 이 값이다..weight → 셀 3에서 손으로 쓴 그 테이블이다. 처음에는 정규분포 무작위 값이다.requires_grad=True → 학습되는 가중치라는 뜻이다. 2.1에서 본 그 표시다.padding_idx=1 → 1번 행(<pad>)을 0으로 두고 학습도 안 시킨다(추가 셀 C에서 확인). 빈칸에는 의미가 없으니까.<unk>) 행은 0이 아니다. 모르는 단어도 "모르는 단어"라는 의미로 학습한다.🗣 "셀 3에서 손으로 쓴 표를 파이토치가 무작위로 만들어 준 거예요. 다른 점은 이 값들이 학습된다는 것 하나입니다."
idx = torch.LongTensor([[7, 5, 2, 0], # 문장 1: 단어 4개
[3, 4, 1, 1]]) # 문장 2: 단어 2개 + <pad> 2개
out = embedding_layer(idx)
print(idx.shape, '→', out.shape) # torch.Size([2, 4]) → torch.Size([2, 4, 3])
(배치, 시점, 입력 차원) 그대로다. 임베딩 층이 torch.randn을 대신한다.<pad> 자리(문장 2의 뒤 두 칸)는 0 벡터로 나온다.one_hot = torch.nn.functional.one_hot(idx, num_classes=8).float() # (2, 4, 8)
print(torch.allclose(one_hot @ embedding_layer.weight, out)) # True
out.sum().backward()
print(embedding_layer.weight.grad[0]) # tensor([1., 1., 1.]) ← <unk>는 기울기가 생김
print(embedding_layer.weight.grad[1]) # tensor([0., 0., 0.]) ← <pad>는 항상 0
padding_idx 행의 기울기는 0이다. 그래서 학습 내내 0 벡터로 남는다.🗣 "임베딩 층에 (배치, 문장 길이)짜리 정수를 넣으면 (배치, 문장 길이, 차원)이 나와요. 지난 시간까지 RNN에 넣던 바로 그 모양입니다. 4.5 모델의 첫 줄이 이거예요."
❓ 4.5 모델은 padding_idx를 써? 안 쓴다. nn.Embedding(vocab_size, embedding_dim)만 있다. 그래서 <PAD>(0번) 벡터도 학습된다. 4.5 노트의 토론거리로 이어진다.
❓ 4.5의 임베딩은 얼마나 커? 19193 × 100 = 1,919,300개. 4.5 모델 전체 파라미터 2,037,318개의 94% 가 임베딩 테이블이다. LSTM은 11만 개 정도다.
🗣 "단어를 RNN에 넣으려면 단어 → 정수 → 벡터 두 단계를 거쳐요. 정수는 단어 집합에서 번호를 찾는 거고, 벡터는 임베딩 테이블에서 그 번호의 행을 꺼내는 거예요. nn.Embedding은 그 테이블을 학습 가능한 가중치로 들고 있는 층이고, (배치, 길이) 정수를 (배치, 길이, 차원) 벡터로 바꿔 줍니다."
치트시트
| 항목 | 값 / 모양 |
|---|---|
nn.Embedding(num_embeddings, embedding_dim) | 테이블 (단어 수, 차원) |
| 입력 | 정수(Long) (배치, 시점) |
| 출력 | 실수 (배치, 시점, 차원) → RNN/LSTM의 입력 |
padding_idx=k | k번 행 = 0 벡터, 기울기 0 |
| 파라미터 수 | 단어 수 × 차원 (편향 없음) |
| 이 노트북 | <unk>=0, <pad>=1, 8 × 3 |
| 4.5 | <PAD>=0, <UNK>=1, 19193 × 100 |