4.4 nn.embedding 코드 리뷰

yunju·6일 전

코드 리뷰

목록 보기
4/5

노트북: 12-06. pytorch_nn_embedding.ipynb (셀 9개, 코드 셀 7개)
예상 시간: 약 10분 (노트북 5분 + 추가 셀 5분) · 짝꿍 글: 4.4_nn.Embedding.md


개념 1분 — 이것만 알고 코드 보기

  • RNN에는 숫자 벡터를 넣어야 한다. 4.1~4.2에서 torch.randn(1, 10, 5)로 만들던 "단어 벡터" 자리다.
  • 단어를 벡터로 바꾸는 과정은 두 단계다.
    1. 단어 → 정수: 단어 집합(vocab)을 만들고 단어마다 번호를 매긴다. '영화' → 2
    2. 정수 → 벡터: 임베딩 테이블에서 그 번호의 행을 꺼낸다. 2 → [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.Embedding6~8같은 테이블을 층으로. 값은 무작위로 시작2분
3. 추가 A~C—(배치, 시점) → (배치, 시점, 차원). 원-핫 × 행렬과 같다4분

읽는 법: 🗣 = 그대로 말해도 되는 문장, ❓ = 나올 만한 질문과 답


Part 1. 손으로 만든 룩업 테이블 (셀 1~4)

셀 2 — 단어 집합 만들기

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개가 된다.
  • 번호를 2부터 매기는 이유: 0과 1은 특수 토큰 자리로 비워 둔다.
    • <unk> (unknown): 단어 집합에 없는 단어가 들어오면 이걸로 바꾼다.
    • <pad> (padding): 문장 길이를 맞추려고 채우는 빈칸이다(4.5에서 0으로 채운다).
  • 단어 집합 크기는 6 + 2 = 8이다.

⚠️ 4.5와 번호가 반대다. 여기서는 <unk>=0, <pad>=1이고, 4.5에서는 <PAD>=0, <UNK>=1이다. 번호는 정하기 나름이지만 섞어 쓰면 헷갈린다.

셀 3 — 임베딩 테이블 (손으로 씀)

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번
  • shape (8, 3) = (단어 집합 크기, 임베딩 차원). 단어 하나가 3차원 벡터 하나다.
  • 행 번호 = 단어 번호다. 2번 단어의 벡터는 2번 행이다.
  • 값은 설명용으로 아무렇게나 적은 것이다. 실제로는 학습으로 정해진다.

셀 4 — 문장을 벡터로 바꾸기

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)

한 줄씩:

  1. vocab[word] → 단어를 정수로. run은 단어 집합에 없어서 KeyError가 난다. 그래서 <unk>(0)으로 바꾼다.
  2. torch.LongTensor(idxes) → 정수 텐서. 인덱스로 쓰려면 정수형(Long) 이어야 한다.
  3. embedding_table[idxes, :] → 팬시 인덱싱. 인덱스 4개에 해당하는 행 4개를 한 번에 꺼낸다. 결과는 (4, 3). 이 한 줄이 임베딩의 전부다.
  4. 마지막 행이 [0, 0, 0]인 이유: run → <unk>(0번) → 0번 행이 0이기 때문이다.

🗣 "임베딩은 결국 표에서 행을 꺼내는 거예요. 단어를 번호로 바꾸고, 그 번호의 행을 가져오면 그게 단어 벡터입니다. 모르는 단어는 0번 unk 행을 가져와요."

❓ (저장된 출력과) 제 숫자가 달라요. 발표 전 체크에 쓴 대로 set의 순서 때문이다. you가 몇 번이 될지가 실행마다 바뀌어서 꺼내는 행이 달라진다. run→0번 행(0 벡터)인 마지막 줄만 늘 같다.


Part 2. 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에서 확인). 빈칸에는 의미가 없으니까.
  • 0번(<unk>) 행은 0이 아니다. 모르는 단어도 "모르는 단어"라는 의미로 학습한다.

🗣 "셀 3에서 손으로 쓴 표를 파이토치가 무작위로 만들어 준 거예요. 다른 점은 이 값들이 학습된다는 것 하나입니다."


Part 3. 추가 셀 — 실제로 넣어 보기

A. 정수 배치 → 벡터 배치

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])
  • 입력 (배치, 시점) 의 정수 → 출력 (배치, 시점, 차원) 의 벡터.
  • 이 출력 모양이 4.1~4.2에서 RNN/LSTM에 넣던 (배치, 시점, 입력 차원) 그대로다. 임베딩 층이 torch.randn을 대신한다.
  • <pad> 자리(문장 2의 뒤 두 칸)는 0 벡터로 나온다.

B. 원-핫 × 행렬과 같다

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
  • 단어를 원-핫 벡터(8칸 중 한 칸만 1)로 만들어 테이블을 곱하면, 1인 칸의 행만 남는다. 행 꺼내기와 결과가 같다.
  • 그래서 임베딩 층은 "원-핫 입력 + 편향 없는 선형층"과 같은 것이다. 다만 곱셈 대신 바로 행을 꺼내서 훨씬 빠르다.

C. padding_idx 행은 학습되지 않는다

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만 개 정도다.


마무리 (1분)

🗣 "단어를 RNN에 넣으려면 단어 → 정수 → 벡터 두 단계를 거쳐요. 정수는 단어 집합에서 번호를 찾는 거고, 벡터는 임베딩 테이블에서 그 번호의 행을 꺼내는 거예요. nn.Embedding은 그 테이블을 학습 가능한 가중치로 들고 있는 층이고, (배치, 길이) 정수를 (배치, 길이, 차원) 벡터로 바꿔 줍니다."

치트시트

항목값 / 모양
nn.Embedding(num_embeddings, embedding_dim)테이블 (단어 수, 차원)
입력정수(Long) (배치, 시점)
출력실수 (배치, 시점, 차원) → RNN/LSTM의 입력
padding_idx=kk번 행 = 0 벡터, 기울기 0
파라미터 수단어 수 × 차원 (편향 없음)
이 노트북<unk>=0, <pad>=1, 8 × 3
4.5<PAD>=0, <UNK>=1, 19193 × 100

0개의 댓글