3장. Attention 과 Transformer, GPT 직접 구현
ML 디자인 패턴
요리에는 "육수는 찬물부터 끓인다" 같은 정석이 있습니다. 왜 그런지 모르고 따라 해도 대개 맛이 납니다. 머신러닝에도 자주 부딪히는 문제의 검증된 풀이가 있고, 여기에 이름을 붙여 정리한 것을 ML 디자인 패턴이라고 합니다. 같은 이름의 책(Machine Learning Design Patterns, 2020)이 수십 가지를 정리해 두었습니다.
카드 넷은 이 과정에서 실제로 쓰는 패턴입니다. 임베딩은 이 장에서 이미 썼고, 전이 학습은 실습 5, 6에서 남이 학습한 모델을 받아 씁니다. 체크포인트는 실습 4에서 학습한 모델을 파일로 저장하는 일이고, 재현성은 1장부터 씨앗을 고정해 온 일입니다. 여기서 요리의 정석이 디자인 패턴입니다.
이 과정과 이어지는 패턴
| 패턴 | 풀려는 문제 | 풀이 | 이 과정에서 |
|---|---|---|---|
| 임베딩 | 종류가 아주 많은 범주(글자, 낱말, 상품)를 숫자로 넣기 | 짧은 좌표로 바꿔 학습한다 | 3장, 7장 |
| 전이 학습 | 데이터가 적어 처음부터 학습하기 어렵다 | 큰 데이터로 학습된 모델을 받아 다듬는다 | 3장 실습 5·6, 4장, 5장 |
| 체크포인트 | 학습이 길어 중간에 끊기면 처음부터 다시 해야 한다 | 중간중간 가중치를 저장한다 | 3장 실습 4, 4장부터 |
| 재현성 | 돌릴 때마다 결과가 달라 견줄 수 없다 | 씨앗과 데이터 나누기를 고정한다 | 1장부터 내내 |
| 다시 구성(Reframing) | 문제의 꼴이 모델과 맞지 않는다 | 회귀를 분류로 바꾸는 식으로 문제를 바꿔 쓴다 | 1장 "문제의 종류" |
| 워크플로 파이프라인 | 데이터 준비부터 배포까지 손으로 하면 실수가 난다 | 단계를 나눠 자동으로 잇는다 | 19장 |
import torch
from torch import nn
# 체크포인트 패턴: 학습한 가중치를 파일로 저장했다가 다시 불러온다
model = nn.Linear(3, 1)
torch.save(model.state_dict(), "out_checkpoint_demo.pt") # 가중치만 저장
new_model = nn.Linear(3, 1) # 같은 모양의 새 모델
new_model.load_state_dict(torch.load("out_checkpoint_demo.pt")) # 저장한 가중치를 넣는다
print(torch.equal(model.weight, new_model.weight))True 가 나와 두 모델의 가중치가 같아졌음을 확인할 수 있습니다. state_dict() 는 층마다의 가중치를 이름과 함께 담은 사전입니다. 모델의 짜임(클래스)은 저장되지 않으므로, 불러올 때는 같은 짜임의 모델을 먼저 만들어야 합니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
nn.Linear(3, 1) | 2장에서 본 층이다. 입력 3개를 받아 출력 1개를 내고, 가중치는 처음에 난수로 정해진다 |
model.state_dict() | 층마다의 가중치를 이름(예: weight, bias)과 함께 담은 사전을 돌려준다 |
torch.save(사전, 파일 이름) | 텐서가 든 사전을 파일로 저장한다. 확장자는 흔히 .pt 를 쓴다 |
torch.load(파일 이름) | 저장한 파일을 읽어 사전으로 되돌린다 |
new_model.load_state_dict(...) | 사전의 가중치를 새 모델에 채워 넣는다. 이름과 모양이 맞지 않으면 오류가 난다 |
torch.equal(a, b) | 두 텐서의 모양과 값이 모두 같으면 True 를 돌려준다 |
new_model 도 만들 때는 자기만의 난수 가중치를 가집니다. load_state_dict 를 부른 뒤에야 model 과 같은 가중치가 됩니다.
이 코드가 뜻하는 것
게임의 "저장하기"와 "불러오기"로 생각하면 쉽습니다. 이 코드는 모델의 가중치를 파일에 저장한 뒤, 새로 만든 모델에 그 파일을 불러와 둘이 똑같아졌는지 확인합니다. 여기서 저장 파일이 체크포인트, 게임 캐릭터가 모델입니다. 비유와 다른 점은, 게임은 캐릭터까지 통째로 저장하지만 이 코드는 숫자(가중치)만 저장해서 같은 모양의 모델을 먼저 만들어 두어야 한다는 것입니다.
| 줄 | 하는 일 | 게임으로 치면 |
|---|---|---|
model = nn.Linear(3, 1) | 입력 3개, 출력 1개짜리 작은 모델을 만든다 | 캐릭터 하나 |
torch.save(model.state_dict(), ...) | 가중치 숫자만 파일로 저장한다 | 능력치를 저장 파일에 적는다 |
new_model = nn.Linear(3, 1) | 같은 모양의 새 모델을 만든다. 숫자는 다른 난수다 | 새 캐릭터를 만든다 |
new_model.load_state_dict(torch.load(...)) | 저장한 숫자를 새 모델에 채운다 | 저장 파일을 불러와 능력치를 덮어쓴다 |
torch.equal(...) | 두 모델의 숫자가 같은지 본다 | 두 캐릭터의 능력치가 같은지 본다 |
저장 파일에 실제로 무엇이 들어가는지 찍어 보면 이렇습니다.
import torch
from torch import nn
torch.manual_seed(0)
model = nn.Linear(3, 1)
for name, value in model.state_dict().items(): # 저장되는 것: 이름과 숫자
print(name, tuple(value.shape), [round(v, 3) for v in value.flatten().tolist()])weight (1, 3) [-0.004, 0.31, -0.475]
bias (1,) [-0.425]이름 두 개(weight, bias)와 숫자 네 개가 전부입니다. 입력 3개에 곱할 가중치 3개와 끝에 더할 치우침 1개입니다. 실제 LLM 의 체크포인트도 이름과 숫자가 수백만~수십억 개로 늘어날 뿐 짜임은 같습니다. 그래서 학습이 중간에 끊겨도 마지막 체크포인트를 불러와 이어서 학습할 수 있습니다.
주의할 점
- 패턴은 정답이 아니라 출발점입니다. 데이터와 상황이 다르면 맞지 않을 수 있습니다.
- 남이 만든 모델을 받아 쓸 때(전이 학습)는 그 모델의 이용 조건(라이선스)을 먼저 확인합니다. 실습 5, 6의 모델은 상업적으로도 쓸 수 있는 라이선스로 공개되어 있습니다.