3장. Attention 과 Transformer, GPT 직접 구현
Transformer 블록
아파트를 지을 때는 같은 설계의 층을 여러 번 쌓아 올립니다. Transformer 도 "어텐션으로 서로 참고하고, 글자마다 따로 생각하는" 블록 하나를 설계해 두고, 그것을 여러 번 쌓습니다.
그림은 아래에서 위로 읽습니다. 1번 입력에서 글자 번호를 임베딩으로 바꾸고, 마젠타 점선 상자 안의 블록을 지납니다. 블록 안에서는 2번 마스크 셀프 어텐션으로 앞의 글자를 참고하고, 3번 피드포워드로 글자마다 따로 계산합니다. 오른쪽의 마젠타 선은 잔차 연결로, 블록에 들어온 값을 결과에 그대로 더합니다. 4번에서 글자마다 다음 글자의 점수를 냅니다. 점선 상자 안의 블록을 실습의 미니 GPT 는 2번, GPT-1 은 12번 쌓습니다.
블록의 부품
| 부품 | 하는 일 | 2장에서 견주면 |
|---|---|---|
| LayerNorm | 벡터의 숫자 크기를 고르게 맞춘다 | 실습 3의 StandardScaler 를 층마다 하는 것 |
| 마스크 셀프 어텐션 | 앞의 글자를 참고해 벡터를 다시 쓴다 | 새로 배운 부분 |
| 피드포워드(FFN) | 글자마다 따로 Linear → ReLU → Linear | 2장의 신경망 그대로 |
| 잔차 연결 | 들어온 값 + 층의 결과 | 고칠 몫만 배우게 해 깊이 쌓아도 학습이 된다 |
세 가지 짜임
블록을 어떻게 쓰느냐에 따라 Transformer 는 세 갈래로 나뉩니다.
왼쪽 인코더만 쓰는 모델은 문장 전체를 앞뒤로 읽어 분류나 검색에 쓰고, 가운데 디코더만 쓰는 모델은 앞만 보며 다음 글을 씁니다. 오른쪽 인코더-디코더는 한 문장을 읽고 다른 문장을 새로 쓰는 번역에 씁니다.
| 짜임 | 마스크 | 대표 모델 | 이 장에서 |
|---|---|---|---|
| 인코더만 | 없음 | BERT | 7장 임베딩 모델에서 다시 만난다 |
| 디코더만 | 인과적 마스크 | GPT, 실습 6의 챗봇 모델 | 실습 3, 4의 미니 GPT |
| 인코더-디코더 | 디코더 쪽만 | 처음의 Transformer, 번역 모델 | 실습 5 |
import torch
from torch import nn
# PyTorch 에 들어 있는 멀티헤드 어텐션 층: 좌표 64개, 헤드 4개
attn = nn.MultiheadAttention(embed_dim=64, num_heads=4, batch_first=True)
x = torch.randn(1, 10, 64) # (배치, 글자 수, 좌표 수)
mask = torch.triu(torch.ones(10, 10, dtype=torch.bool), diagonal=1) # True 인 칸(뒤쪽)을 가린다
out, weights = attn(x, x, x, attn_mask=mask) # 셀프 어텐션: Q, K, V 를 모두 x 에서
print(out.shape, weights.shape)결과는 입력과 같은 (1, 10, 64) 이고, 비율 표는 (1, 10, 10) 입니다. nn.MultiheadAttention 은 True 인 칸을 가리므로, 위 삼각형(triu)이 True 인 마스크를 줍니다. 앞 절의 tril 과 뜻은 같고 표시만 반대입니다. 실습 3의 미니 GPT 는 이 층을 씁니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
nn.MultiheadAttention(embed_dim=64, num_heads=4, ...) | 멀티헤드 어텐션 층이다. 좌표 64개를 헤드 4개로 나눠 계산하고 이어 붙인다. Q, K, V 를 만드는 가중치도 안에 들어 있다 |
batch_first=True | 입력 모양을 (배치, 글자 수, 좌표 수) 순서로 받는다. 이 값을 주지 않으면 (글자 수, 배치, 좌표 수) 순서로 본다 |
torch.ones(10, 10, dtype=torch.bool) | 모든 칸이 True 인 (10, 10) 표를 만든다. dtype 은 텐서에 담을 값의 종류다 |
torch.triu(..., diagonal=1) | "upper triangle"의 줄임말이다. 대각선보다 한 칸 위부터 위쪽만 남긴다. 남은 칸, 곧 뒤쪽 글자가 True 다 |
attn(x, x, x, attn_mask=mask) | 차례로 Q, K, V 를 만들 입력을 받는다. 셋을 모두 x 로 주면 셀프 어텐션이다. attn_mask 에서 True 인 칸을 가린다 |
out, weights | 층은 결과 벡터와 비율표 두 개를 돌려준다. 비율표는 헤드 4개의 평균이라 헤드 축이 없는 (1, 10, 10) 이다 |
diagonal=1 이 빠지면 대각선까지 True 가 되어 글자가 자기 자신도 보지 못합니다. 대각선은 남겨 두고 그 위만 가려야 합니다.
이 코드가 뜻하는 것
이 코드는 블록 안의 핵심 부품인 "마스크 셀프 어텐션" 한 층을 PyTorch 가 만들어 둔 부품으로 불러 써 보는 코드입니다. 앞의 "셀프 어텐션 계산"과 "마스크: 앞만 보기"에서 손으로 한 계산(질문과 제목표 견주기, 뒤쪽 가리기, 비율대로 섞기)을 nn.MultiheadAttention 한 줄이 모두 해 줍니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
attn = nn.MultiheadAttention(...) | 좌표 64개를 헤드 4개로 나눠 보는 어텐션 층을 만든다 | 리뷰를 네 사람이 나눠 읽는 모임을 꾸린다 |
x = torch.randn(1, 10, 64) | 문장 1개, 글자 10개, 글자마다 숫자 64개인 입력을 만든다 | 읽을 문장 한 줄(설명용 난수) |
mask = torch.triu(...) | 뒤에 오는 글자 칸만 True 인 가림표를 만든다 | 받아쓰기 답안지의 뒷부분을 가리는 종이 |
attn(x, x, x, attn_mask=mask) | 글자마다 앞의 글자를 참고해 벡터를 다시 쓴다 | 문장 속 앞 낱말을 보고 지금 낱말의 뜻을 다시 적는다 |
out.shape, weights.shape | 결과는 입력과 같은 모양, 비율표는 글자 수 × 글자 수 | 들어간 모양 그대로 나와서 위에 같은 층을 또 쌓을 수 있다 |
맨 아래 줄이 이 절의 아파트 비유와 이어집니다. 어텐션 층은 (1, 10, 64) 를 받아 (1, 10, 64) 를 돌려주므로, 같은 설계의 층을 위로 몇 번이든 쌓을 수 있습니다.
작게 줄여서 눈으로 보기
글자 10개, 좌표 64개는 출력이 너무 커서 눈으로 보기 어렵습니다. 낱말 4개("이불 배송 언제 와요"), 좌표 8개, 헤드 2개로 줄이고 가림표와 비율표에 낱말 이름을 붙여 찍어 봅니다.
import torch
from torch import nn
import pandas as pd
words = ["이불", "배송", "언제", "와요"]
torch.manual_seed(0)
# 낱말 4개, 좌표 8개, 헤드 2개로 크기만 줄였다
attn = nn.MultiheadAttention(embed_dim=8, num_heads=2, batch_first=True)
x = torch.randn(1, 4, 8) # 낱말 4개의 벡터(설명용 난수)
# True 인 칸을 가린다. 대각선 위쪽(뒤에 오는 낱말)만 True
mask = torch.triu(torch.ones(4, 4, dtype=torch.bool), diagonal=1)
print("1) 가림표 (가림 = 볼 수 없는 칸)")
print(pd.DataFrame(mask.numpy(), index=words, columns=words).replace({True: "가림", False: "봄"}))
out, weights = attn(x, x, x, attn_mask=mask)
print("\n2) 비율표 (줄: 지금 낱말, 칸: 참고한 낱말)")
print(pd.DataFrame(weights[0].detach().numpy(), index=words, columns=words).round(2))
print("\n3) 모양: 입력", tuple(x.shape), "→ 결과", tuple(out.shape))1) 가림표 (가림 = 볼 수 없는 칸)
이불 배송 언제 와요
이불 봄 가림 가림 가림
배송 봄 봄 가림 가림
언제 봄 봄 봄 가림
와요 봄 봄 봄 봄
2) 비율표 (줄: 지금 낱말, 칸: 참고한 낱말)
이불 배송 언제 와요
이불 1.00 0.00 0.00 0.00
배송 0.48 0.52 0.00 0.00
언제 0.27 0.39 0.34 0.00
와요 0.18 0.26 0.31 0.26
3) 모양: 입력 (1, 4, 8) → 결과 (1, 4, 8)| 출력 | 읽는 법 |
|---|---|
| 1) 가림표 | "이불"은 자기만 보고, "와요"는 네 낱말을 모두 봅니다. 뒤에 오는 낱말은 "가림"이라 볼 수 없습니다 |
| 2) 비율표 | 가림표에서 "가림"인 칸은 비율이 모두 0.00 입니다. 볼 수 있는 칸끼리만 비율을 나눠 줄마다 합이 1 입니다. 예를 들어 "언제"는 이불 0.27, 배송 0.39, 자기 0.34 를 참고합니다 |
| 3) 모양 | 낱말 4개, 좌표 8개가 들어가서 똑같이 낱말 4개, 좌표 8개로 나옵니다. 그래서 이 층 위에 같은 층을 또 얹을 수 있습니다 |
이 비율은 학습하지 않은 처음 가중치로 계산한 것이라 "언제가 배송을 많이 본다" 같은 뜻은 아직 없습니다. 이 코드로 확인할 것은 "뒤는 0 으로 가려지고, 모양은 그대로 나온다"는 두 가지입니다. 실습 3의 미니 GPT 는 이 층에 LayerNorm, 피드포워드, 잔차 연결을 붙여 블록 하나를 만들고, 그 블록을 두 번 쌓습니다.
주의할 점
- 블록을 많이 쌓을수록 표현력은 늘지만 파라미터와 계산이 함께 늡니다. GPT-1 은 블록 12개에 파라미터가 약 1억 1,700만 개입니다.
- 마스크의 표시 방법(1 을 볼 칸으로 쓰는지, True 를 가릴 칸으로 쓰는지)은 함수마다 다릅니다. 처음 쓰는 함수는 문서에서 확인합니다.