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

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

Transformer 블록

아파트를 지을 때는 같은 설계의 층을 여러 번 쌓아 올립니다. Transformer 도 "어텐션으로 서로 참고하고, 글자마다 따로 생각하는" 블록 하나를 설계해 두고, 그것을 여러 번 쌓습니다.

입력 임베딩 위에 LayerNorm, 마스크 셀프 어텐션, LayerNorm, 피드포워드를 쌓고 잔차 연결로 잇는 GPT 블록 그림

그림은 아래에서 위로 읽습니다. 1번 입력에서 글자 번호를 임베딩으로 바꾸고, 마젠타 점선 상자 안의 블록을 지납니다. 블록 안에서는 2번 마스크 셀프 어텐션으로 앞의 글자를 참고하고, 3번 피드포워드로 글자마다 따로 계산합니다. 오른쪽의 마젠타 선은 잔차 연결로, 블록에 들어온 값을 결과에 그대로 더합니다. 4번에서 글자마다 다음 글자의 점수를 냅니다. 점선 상자 안의 블록을 실습의 미니 GPT 는 2번, GPT-1 은 12번 쌓습니다.

블록의 부품

부품하는 일2장에서 견주면
LayerNorm벡터의 숫자 크기를 고르게 맞춘다실습 3의 StandardScaler 를 층마다 하는 것
마스크 셀프 어텐션앞의 글자를 참고해 벡터를 다시 쓴다새로 배운 부분
피드포워드(FFN)글자마다 따로 Linear → ReLU → Linear2장의 신경망 그대로
잔차 연결들어온 값 + 층의 결과고칠 몫만 배우게 해 깊이 쌓아도 학습이 된다

세 가지 짜임

블록을 어떻게 쓰느냐에 따라 Transformer 는 세 갈래로 나뉩니다.

인코더만, 디코더만, 인코더-디코더의 세 가지 Transformer 짜임을 나란히 놓은 비교 그림

왼쪽 인코더만 쓰는 모델은 문장 전체를 앞뒤로 읽어 분류나 검색에 쓰고, 가운데 디코더만 쓰는 모델은 앞만 보며 다음 글을 씁니다. 오른쪽 인코더-디코더는 한 문장을 읽고 다른 문장을 새로 쓰는 번역에 씁니다.

짜임마스크대표 모델이 장에서
인코더만없음BERT7장 임베딩 모델에서 다시 만난다
디코더만인과적 마스크GPT, 실습 6의 챗봇 모델실습 3, 4의 미니 GPT
인코더-디코더디코더 쪽만처음의 Transformer, 번역 모델실습 5
Python
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개로 줄이고 가림표와 비율표에 낱말 이름을 붙여 찍어 봅니다.

Python
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 를 가릴 칸으로 쓰는지)은 함수마다 다릅니다. 처음 쓰는 함수는 문서에서 확인합니다.
← 이전 절3장 · 멀티헤드와 위치 정보다음 절 →3장 · 다음 글자 고르기: 생성과 온도