LLM 엔지니어링3장 · Attention 과 Transformer, GPT 직접 구현4 / 23← 이전목차다음 →MetaContents

3장. Attention 과 Transformer, GPT 직접 구현

임베딩

지도에서 가까운 동네끼리는 분위기도 비슷한 경우가 많습니다. 낱말에도 이런 지도를 만들 수 있다면, "배송"과 "택배"는 가까이, "배송"과 "청바지"는 멀리 놓일 것입니다. 토큰 번호를 이런 지도의 좌표로 바꾼 것이 임베딩입니다.

배송, 환불, 옷과 관련된 낱말이 무리 지어 놓인 2차원 임베딩 지도

청록 점은 배송 무리, 마젠타 점은 환불 무리, 다홍 점은 옷 무리입니다. 뜻이 가까운 낱말끼리 모여 있습니다. 이 그림의 좌표는 설명을 위해 손으로 정한 것입니다. 실제 임베딩은 이런 자리를 학습으로 스스로 찾고, 좌표도 두 개가 아니라 수십에서 수천 개입니다. 여기서 동네가 낱말, 지도 좌표가 임베딩입니다.

번호에서 좌표로

단계모양(예)뜻
토큰 번호정수 하나, 예: 2"배"는 2번
임베딩 표(어휘 크기, 좌표 수), 예: (7, 4)어휘의 글자마다 좌표 한 줄
임베딩좌표 4개, 예: [0.3, -1.2, 0.8, 0.1]표의 2번 줄을 꺼낸 것

임베딩 표는 2장의 가중치처럼 처음엔 난수이고, 학습하면서 쓸모 있는 좌표로 바뀌어 갑니다. PyTorch 에서는 nn.Embedding 이 이 표입니다.

코드로 표를 열어 보기

앞 절 "토큰과 토크나이저"에서 만든 일곱 글자 어휘를 그대로 씁니다. 표를 만들고, 표 안을 글자 이름을 붙여 찍어 본 뒤, "배송이" 세 글자의 좌표를 꺼냅니다.

Python
import torch
from torch import nn

# 앞 절의 글자 토크나이저: 어휘 7글자에 번호를 붙인다
text = "배송이 늦어요"
chars = sorted(set(text))                      # [' ', '늦', '배', '송', '어', '요', '이']
stoi = {c: i for i, c in enumerate(chars)}     # 글자 → 번호

# 줄 7개(글자마다 한 줄), 칸 4개(좌표 4개)짜리 임베딩 표. 처음엔 난수다
torch.manual_seed(0)
emb = nn.Embedding(len(chars), 4)

# 1) 표를 줄마다 "번호, 글자, 좌표 4개"로 찍어 본다
for i, c in enumerate(chars):
    print(i, repr(c), [round(v, 2) for v in emb.weight[i].tolist()])

# 2) "배송이" 를 번호로 바꾸고, 번호마다 표의 한 줄을 꺼낸다
ids = torch.tensor([stoi[c] for c in "배송이"])
vecs = emb(ids)
print(ids)
print(vecs.detach().round(decimals=2))

# 3) 꺼낸 첫 줄이 표의 2번 줄("배")과 똑같은지 확인한다
print(torch.equal(vecs[0], emb.weight[2]))
print(emb.weight.shape, vecs.shape)
출력
0 ' ' [-1.13, -1.15, -0.25, -0.43]
1 '늦' [0.85, 0.69, -0.32, -2.12]
2 '배' [0.32, -1.26, 0.35, 0.31]
3 '송' [0.17, 0.87, -0.14, -0.11]
4 '어' [-0.61, 0.03, -0.49, 0.25]
5 '요' [0.62, -0.41, -0.84, -2.32]
6 '이' [-0.1, 0.79, -0.29, 0.05]
tensor([2, 3, 6])
tensor([[ 0.3200, -1.2600,  0.3500,  0.3100],
        [ 0.1700,  0.8700, -0.1400, -0.1100],
        [-0.1000,  0.7900, -0.2900,  0.0500]])
True
torch.Size([7, 4]) torch.Size([3, 4])

결과를 차례로 읽기

출력읽는 법
위의 일곱 줄임베딩 표 자체다. 글자 하나에 줄 하나, 줄마다 좌표 4개가 있다. 예를 들어 "배"의 좌표는 0.32, -1.26, 0.35, 0.31 이다
tensor([2, 3, 6])"배송이"를 번호로 바꾼 것이다. 배는 2번, 송은 3번, 이는 6번 줄이다
3줄짜리 텐서emb(ids) 가 꺼낸 좌표다. 첫 줄은 표의 2번 줄("배"), 둘째 줄은 3번 줄("송"), 셋째 줄은 6번 줄("이")과 숫자가 똑같다
True꺼낸 첫 줄과 표의 2번 줄이 완전히 같다는 확인이다. 곧 emb(ids) 는 계산이 아니라 표에서 줄을 찾아 꺼내는 일이다
torch.Size([7, 4]), torch.Size([3, 4])표는 7줄 4칸, 꺼낸 것은 글자 3개 × 좌표 4개다

한마디로, 임베딩은 "번호표를 보고 표에서 그 줄을 꺼내 오는 일"입니다. 도서관에서 청구번호 2번을 들고 가면 2번 칸의 책을 꺼내 주는 것과 같습니다. 여기서 청구번호가 토큰 번호, 서가의 칸이 표의 줄, 꺼낸 책이 좌표(벡터)입니다. 비유와 다른 점은, 이 표의 숫자는 학습하면서 계속 고쳐진다는 것입니다. 지금 숫자는 난수라서 "배"와 "송"이 가까운지 먼지에는 아직 아무 뜻이 없습니다. 학습을 거치면 함께 자주 쓰이는 글자의 좌표가 위 지도처럼 서로 가까워집니다.

코드에 나오는 함수

코드쉬운 뜻
torch.manual_seed(0)난수의 씨앗을 고정한다. 임베딩 표의 처음 난수가 실행할 때마다 같아진다
nn.Embedding(len(chars), 4)줄 7개, 칸 4개짜리 표를 만든다. 첫 숫자가 어휘 크기(글자 수), 둘째 숫자가 좌표 수다
emb.weight임베딩 표 자체다. emb.weight[2] 는 표의 2번 줄이다. 2장에서 본 가중치처럼 requires_grad 가 켜져 있어 학습으로 바뀐다
.tolist(), round(v, 2)텐서 한 줄을 파이썬 목록으로 바꾸고 소수 둘째 자리까지 줄여 보기 좋게 찍는다
torch.tensor([stoi[c] for c in "배송이"])글자를 번호로 바꾼 목록을 정수 텐서로 만든다. 임베딩 층에는 정수 텐서를 넣어야 한다
emb(ids)번호마다 표에서 그 번호의 줄을 꺼내 쌓는다. 곱셈 없는 표 찾기다
.detach()학습 기록을 떼어 낸 값만 꺼낸다. 찍기만 할 때 붙이면 출력에 학습 기록 표시가 섞이지 않는다
torch.equal(a, b)두 텐서의 모양과 숫자가 모두 같으면 True 를 돌려준다

번호의 크기에 뜻이 없어도 괜찮은 까닭이 여기에 있습니다. 번호는 몇 번째 줄인지 가리킬 뿐이고, 뜻은 그 줄에 담긴 좌표가 학습으로 갖게 됩니다.

주의할 점

  • 2장에서 특성을 표준화해 넣었던 것과 달리, 토큰 번호는 그대로 신경망에 넣지 않습니다. 번호의 크기에 뜻이 없기 때문입니다. 반드시 임베딩을 거칩니다.
  • 1장의 get_dummies(0/1 열로 펼치기)도 범주를 숫자로 바꾸는 방법이지만, 어휘가 수만 개면 열이 수만 개가 됩니다. 임베딩은 이것을 짧은 좌표로 줄입니다.
← 이전 절3장 · 토큰과 토크나이저다음 절 →3장 · 순서를 읽는 모델과 한계