//
밑바닥부터 만들면서 배우는 LLM · 세바스찬 라시카
4장에서 만든 GPT 모델은 가중치가 랜덤한 상태였다. 이 모델이 의미 있는 텍스트를 만들 수 있게 하려면 레이블 없는 대규모 텍스트로 학습시켜야 한다. 이 단계가 **사전 훈련(Pre-training)**이다.
사전 훈련의 학습 목표는 한 줄로 정리된다. 시퀀스의 다음 토큰을 맞추기. 입력의 각 위치에서 모델은 (학습되지 않은) 어휘사전 분포에 대한 로짓을 출력하고, 이 분포가 실제 다음 토큰의 원-핫 분포와 가까워지도록 가중치를 조정한다.
분류 문제처럼 다음 토큰 예측에도 **교차 엔트로피 손실(Cross-Entropy Loss)**이 자연스럽게 들어맞는다. 모델이 예측한 분포와 실제 분포 사이의 차이를 측정한다.
PyTorch에서는 F.cross_entropy(logits, targets)로 한 줄에 끝낸다. 내부적으로 logits에 소프트맥스 + 로그를 적용하고 정답 위치의 값을 가져와 평균을 낸다.
훈련 루프에서는 두 가지 손실을 측정한다.
두 값의 추세를 함께 보면서 과적합 여부를 모니터링한다.
손실 값을 직접 해석하기는 어렵다. 더 직관적인 지표로 **펄플렉서티(Perplexity)**를 함께 본다.
펄플렉서티는 "모델이 다음 토큰을 예측할 때 평균적으로 몇 개의 후보 사이에서 헷갈리고 있는가"를 의미한다. 어휘사전의 유효 크기라고도 표현한다.
| 펄플렉서티 | 해석 |
|---|---|
| 1 | 완벽한 예측 (이론적 하한) |
| 50,257 | 어휘사전 전체에 균등 분포 (학습 전 상태) |
| 50 | 평균적으로 50개 후보 안에서 망설이는 수준 |
| 10 이하 | 매우 좋은 모델 |
학습이 진행되면서 펄플렉서티가 50,000대에서 시작해 점점 떨어지는 그래프를 그리는 것이 정상적인 학습이다.
훈련 함수의 골격은 PyTorch 표준 패턴 그대로다.
def train_model_simple(model, train_loader, val_loader,
optimizer, num_epochs, eval_freq):
for epoch in range(num_epochs):
model.train()
for batch in train_loader:
optimizer.zero_grad()
loss = calc_loss_batch(batch, model)
loss.backward()
optimizer.step()
if global_step % eval_freq == 0:
# 훈련/검증 손실 측정 + 샘플 텍스트 생성
evaluate_and_log()LLM 사전 훈련에서 사실상 표준은 AdamW다. Adam에 weight decay를 분리해 적용한 변형으로, 큰 가중치에 페널티를 줘 과적합을 억제한다.
| 하이퍼파라미터 | 권장값 |
|---|---|
| 학습률 | 0.0004 |
| Weight decay | 0.1 |
| β1, β2 | 0.9, 0.95 |
학습률 스케줄링(워밍업 + 코사인 감쇠)을 적용하면 더 안정적이지만, 책의 예제에서는 단순 고정 학습률로도 충분히 학습 곡선을 보여준다.
학습이 끝난 모델, 혹은 중간 체크포인트는 디스크에 저장해야 한다.
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
}, "checkpoint.pth")옵티마이저 상태(Adam의 모멘텀, 분산 추정치)까지 함께 저장하는 것이 중요하다. 모델 가중치만 저장하고 옵티마이저를 새로 만들면, 학습률 적응 정보가 사라져 재시작 시 학습이 불안정해진다.
또한 OpenAI가 공개한 GPT-2 가중치를 직접 받아 이 책에서 만든 모델 구조에 매핑해 로드하는 방법도 다룬다. 이 덕에 처음부터 학습할 컴퓨팅이 없어도, 훈련된 GPT-2의 동작을 직접 만든 모델로 재현해 볼 수 있다.
4장에서 본 그리디 디코딩(argmax)은 결정론적이라 출력이 단조롭다. 같은 입력이면 늘 같은 출력이다. 자연스러운 생성을 위해 확률적 샘플링 기법을 도입한다.
소프트맥스에 들어가기 전 로짓을 온도 T로 나눈다.
| 온도 T | 효과 |
|---|---|
| T = 1 | 원본 분포 (변형 없음) |
| T < 1 | 분포가 뾰족해짐. 보수적·결정적 |
| T > 1 | 분포가 평평해짐. 다양·창의적 |
| T → 0 | argmax (그리디 디코딩과 동일) |
코드 생성처럼 정확도가 중요한 작업은 T를 낮게(0.2~0.5), 창작 글쓰기는 높게(0.8~1.2) 쓴다.
확률이 높은 상위 k개 토큰만 후보로 남기고, 나머지는 -∞로 마스킹해 분포에서 제거한 뒤 그 안에서 샘플링한다.
k가 작으면 출력이 보수적이고, 크면 다양성이 늘어난다. 온도 스케일링과 보통 함께 쓴다(예: T = 0.7, k = 50).
torch.multinomial()을 사용해 확률 분포에서 토큰 하나를 뽑고, EOS 토큰이 등장하면 생성을 종료한다.
책에는 등장하지 않지만 실전에서 자주 쓰는 또 하나의 변형이 Top-p(nucleus) 샘플링이다. 누적 확률이 p(예: 0.9)에 도달할 때까지의 토큰들만 후보로 남긴다. 분포가 뾰족할 때는 후보가 적게, 평평할 때는 많이 잡혀서 Top-k의 단점(고정 k)을 보완한다.
5장은 텍스트 생성 모델을 "동작하는 모델"로 만드는 단계다. 손실 함수와 펄플렉서티로 모델을 평가하고, AdamW + 교차 엔트로피로 훈련 루프를 돌리며, 온도와 Top-k로 생성 결과의 성격을 제어한다.
이 단계까지 마치면, 적어도 영어 같은 표층 통사·의미 구조를 흉내 내는 베이스 모델이 만들어진다. 다만 베이스 모델은 "다음 토큰을 잘 맞추는" 것뿐이라, 사용자의 의도를 따라 답하지는 못한다. 그래서 다음 두 장에서 분류 미세 튜닝과 지시 미세 튜닝으로 모델을 한 번 더 다듬는다.
이 글이 도움이 되셨나요?