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

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

멀티헤드와 위치 정보

상품 리뷰를 여러 사람이 나눠 읽는다고 해 봅시다. 한 사람은 맞춤법을, 한 사람은 상품 이름을, 한 사람은 감정 표현을 봅니다. 따로 읽고 메모를 합치면 혼자 읽을 때보다 많은 것을 잡아냅니다. Attention 을 여러 갈래로 나눠 계산하는 것이 멀티헤드 어텐션입니다.

헤드 셋이 각자 다른 관계를 보고 결과를 합치는 멀티헤드 어텐션을 네 장의 카드로 놓은 그림

헤드 1, 2, 3 이 각자 Q, K, V 를 따로 만들어 Attention 을 계산하고, 마지막 카드에서 결과를 이어 붙입니다. 카드에 적은 역할(바로 앞 글자, 상품 이름, 문장 시작)은 예시입니다. 실제로 어떤 헤드가 무엇을 볼지는 학습이 정합니다. 여기서 나눠 읽는 사람이 헤드입니다.

멀티헤드의 모양

좌표 64개를 헤드 4개로 나누면 헤드마다 좌표 16개씩 계산하고, 결과를 이어 붙여 다시 64개가 됩니다. 계산량은 헤드 하나로 64개를 할 때와 비슷합니다.

설정실습 3의 미니 GPTGPT-1 논문
임베딩 좌표 수64768
헤드 수412
헤드 하나의 좌표 수1664

위치 임베딩

Attention 은 글자를 한꺼번에 보기 때문에 그대로는 순서를 모릅니다. "이불 배송"과 "배송 이불"을 구별하지 못하는 셈입니다. 그래서 "몇 번째 자리"마다 위치 임베딩을 하나씩 두고, 토큰 임베딩에 더해서 넣습니다. 공연장 표에 좌석 번호가 적혀 있는 것과 같습니다.

Python
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 는 난수 대신 우리가 준 숫자로 임베딩 표를 만듭니다.

Python
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 논문은 사인 곡선으로 정한 값을 썼습니다.
← 이전 절3장 · 마스크: 앞만 보기다음 절 →3장 · Transformer 블록