3장. Attention 과 Transformer, GPT 직접 구현
멀티헤드와 위치 정보
상품 리뷰를 여러 사람이 나눠 읽는다고 해 봅시다. 한 사람은 맞춤법을, 한 사람은 상품 이름을, 한 사람은 감정 표현을 봅니다. 따로 읽고 메모를 합치면 혼자 읽을 때보다 많은 것을 잡아냅니다. Attention 을 여러 갈래로 나눠 계산하는 것이 멀티헤드 어텐션입니다.
헤드 1, 2, 3 이 각자 Q, K, V 를 따로 만들어 Attention 을 계산하고, 마지막 카드에서 결과를 이어 붙입니다. 카드에 적은 역할(바로 앞 글자, 상품 이름, 문장 시작)은 예시입니다. 실제로 어떤 헤드가 무엇을 볼지는 학습이 정합니다. 여기서 나눠 읽는 사람이 헤드입니다.
멀티헤드의 모양
좌표 64개를 헤드 4개로 나누면 헤드마다 좌표 16개씩 계산하고, 결과를 이어 붙여 다시 64개가 됩니다. 계산량은 헤드 하나로 64개를 할 때와 비슷합니다.
| 설정 | 실습 3의 미니 GPT | GPT-1 논문 |
|---|---|---|
| 임베딩 좌표 수 | 64 | 768 |
| 헤드 수 | 4 | 12 |
| 헤드 하나의 좌표 수 | 16 | 64 |
위치 임베딩
Attention 은 글자를 한꺼번에 보기 때문에 그대로는 순서를 모릅니다. "이불 배송"과 "배송 이불"을 구별하지 못하는 셈입니다. 그래서 "몇 번째 자리"마다 위치 임베딩을 하나씩 두고, 토큰 임베딩에 더해서 넣습니다. 공연장 표에 좌석 번호가 적혀 있는 것과 같습니다.
import torch
from torch import nn
torch.manual_seed(0)
tok_emb = nn.Embedding(124, 64) # 글자 124종의 임베딩 표
pos_emb = nn.Embedding(64, 64) # 자리 64개(0~63번째)의 임베딩 표
ids = torch.tensor([[5, 9, 2]]) # 글자 번호 3개(배치 1개)
pos = torch.arange(ids.shape[1]) # 자리 번호 [0, 1, 2]
x = tok_emb(ids) + pos_emb(pos) # 글자의 뜻 + 자리 정보
print(x.shape) # (배치, 글자 수, 좌표 수)결과는 (1, 3, 64) 입니다. 같은 글자라도 첫째 자리에 있을 때와 셋째 자리에 있을 때 다른 벡터가 됩니다. 위치 임베딩 표의 줄 수(64)가 모델이 한 번에 볼 수 있는 최대 글자 수입니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
nn.Embedding(124, 64) | 글자 124종마다 좌표 64개를 가진 토큰 임베딩 표다 |
nn.Embedding(64, 64) | 자리 64개마다 좌표 64개를 가진 위치 임베딩 표다. 줄 번호가 글자 번호가 아니라 자리 번호다 |
torch.tensor([[5, 9, 2]]) | 대괄호가 두 겹이라 모양이 (1, 3) 이다. 문장 1개(배치 1개)에 글자 3개라는 뜻이다 |
ids.shape[1] | 모양의 둘째 값, 곧 글자 수 3이다 |
torch.arange(3) | 0부터 2까지의 정수 텐서 [0, 1, 2] 를 만든다. 자리 번호로 쓴다 |
tok_emb(ids) + pos_emb(pos) | (1, 3, 64) 와 (3, 64) 를 더한다. 모양이 모자란 앞쪽 축은 늘려 맞춰 주므로 결과는 (1, 3, 64) 다 |
배치는 문장 여러 개를 한꺼번에 넣는 묶음입니다. 2장의 DataLoader 가 만든 묶음과 같은 뜻입니다. 그래서 모양의 맨 앞 축이 배치, 그다음이 글자 수, 마지막이 좌표 수가 됩니다.
이 코드가 뜻하는 것
공연장 표로 읽으면 쉽습니다. 표에는 "어떤 공연인지"와 "몇 번 좌석인지"가 함께 적혀 있습니다. 이 코드는 글자마다 "무슨 글자인지"(토큰 임베딩)와 "몇 번째 자리인지"(위치 임베딩)를 숫자로 찾아 더해, 한 장의 표로 만듭니다.
| 줄 | 하는 일 | 공연장 표로 치면 |
|---|---|---|
tok_emb = nn.Embedding(124, 64) | 글자 124종마다 숫자 64개를 적은 표를 만든다 | 공연마다 적힌 공연 정보 |
pos_emb = nn.Embedding(64, 64) | 0번째부터 63번째 자리까지, 자리마다 숫자 64개를 적은 표를 만든다 | 좌석 번호마다 적힌 좌석 정보 |
ids = torch.tensor([[5, 9, 2]]) | 글자 세 개의 번호다 | 표 세 장의 공연 이름 |
pos = torch.arange(3) | 자리 번호 0, 1, 2 를 만든다 | 1번, 2번, 3번 좌석 |
tok_emb(ids) + pos_emb(pos) | 글자 표에서 찾은 숫자와 자리 표에서 찾은 숫자를 칸끼리 더한다 | 공연 정보와 좌석 번호를 한 장의 표에 함께 적는다 |
더하고 나면 같은 글자라도 놓인 자리마다 숫자가 달라집니다. 그래서 Attention 이 글자를 한꺼번에 보더라도 "이 글자는 앞에 있었다", "이 글자는 뒤에 있었다"를 구별할 수 있습니다.
숫자 두 개로 따라가 보기
숫자 64개는 눈으로 보기 어렵습니다. 낱말 둘, 숫자 두 개짜리 표를 사람이 직접 정해 "이불 배송"과 "배송 이불"을 견줘 봅니다. nn.Embedding.from_pretrained 는 난수 대신 우리가 준 숫자로 임베딩 표를 만듭니다.
import torch
from torch import nn
stoi = {"이불": 0, "배송": 1}
# 글자 표: 숫자를 사람이 정해 넣는다. 이불은 (10, 0), 배송은 (0, 10)
tok_emb = nn.Embedding.from_pretrained(torch.tensor([[10.0, 0.0],
[0.0, 10.0]]))
# 자리 표: 첫째 자리는 (1, 1), 둘째 자리는 (2, 2)
pos_emb = nn.Embedding.from_pretrained(torch.tensor([[1.0, 1.0],
[2.0, 2.0]]))
for sentence in [["이불", "배송"], ["배송", "이불"]]:
ids = torch.tensor([[stoi[w] for w in sentence]]) # 낱말 → 번호, 모양 (1, 2)
pos = torch.arange(ids.shape[1]) # 자리 번호 [0, 1]
only_tok = tok_emb(ids) # 글자 표만 찾은 것
x = only_tok + pos_emb(pos) # 글자 + 자리
print(" ".join(sentence))
for w, a, b in zip(sentence, only_tok[0].tolist(), x[0].tolist()):
print(f" {w}: 글자만 {a} → 자리를 더하면 {b}")이불 배송
이불: 글자만 [10.0, 0.0] → 자리를 더하면 [11.0, 1.0]
배송: 글자만 [0.0, 10.0] → 자리를 더하면 [2.0, 12.0]
배송 이불
배송: 글자만 [0.0, 10.0] → 자리를 더하면 [1.0, 11.0]
이불: 글자만 [10.0, 0.0] → 자리를 더하면 [12.0, 2.0]| 볼 곳 | 읽는 법 |
|---|---|
| "글자만" 숫자 | 두 문장 모두 이불은 (10, 0), 배송은 (0, 10) 이다. 놓인 순서와 상관없이 같으므로, 이것만으로는 두 문장을 구별할 수 없다 |
| "자리를 더하면" 숫자 | 이불이 첫째 자리면 10+1, 0+1 로 (11, 1), 둘째 자리면 10+2, 0+2 로 (12, 2) 가 된다. 같은 이불인데 자리에 따라 숫자가 다르다 |
| 두 문장 견주기 | "이불 배송"은 (11, 1), (2, 12) 이고, "배송 이불"은 (1, 11), (12, 2) 다. 숫자가 달라져서 모델이 두 문장을 구별할 수 있다 |
글자 표의 숫자가 크고 자리 표의 숫자가 작아서, 더한 뒤에도 "어떤 글자인지"는 그대로 알아볼 수 있습니다. 이 예의 숫자는 설명하려고 정한 것이고, 실제 모델에서는 두 표의 숫자 모두 학습으로 정해집니다.
| 코드 | 쉬운 뜻 |
|---|---|
nn.Embedding.from_pretrained(텐서) | 준 텐서를 그대로 임베딩 표로 쓴다. "미리 학습된(pretrained) 숫자로 표를 만든다"는 뜻의 이름이다 |
only_tok[0] | 모양 (1, 2, 2) 에서 첫째 문장(배치의 0번)만 꺼내 (2, 2) 로 만든다 |
.tolist() | 텐서를 파이썬 목록으로 바꿔 보기 좋게 찍는다 |
주의할 점
- 위치 임베딩 표보다 긴 글은 그대로 넣을 수 없습니다. 실습 4에서 생성할 때 마지막 64글자만 잘라 넣는 까닭입니다.
- 위치를 나타내는 방법은 여러 가지입니다. 이 장의 미니 GPT 와 GPT-1 은 학습하는 위치 임베딩을 쓰고, 처음의 Transformer 논문은 사인 곡선으로 정한 값을 썼습니다.